中英文切换 初始化

This commit is contained in:
mmc
2026-08-12 11:29:44 +08:00
parent 541e16f742
commit a8af55a856
76 changed files with 5847 additions and 1962 deletions

212
.workbuddy/batch_i18n.py Normal file
View File

@@ -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")

161
.workbuddy/fix_i18n.py Normal file
View File

@@ -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'(?<![{:((])(\b\w+)=t\(\'([^\']+)\'\)',
fix_jsx_attr,
content
)
# Also fix: word=t("key") with double quotes
content = re.sub(
r'(?<![{:((])(\b\w+)=t\("([^"]+)"\)',
lambda m: f'{m.group(1)}={{t(\'{m.group(2)}\')}}',
content
)
# Step 3: Re-add useTranslation import if file uses t() but doesn't have the import
has_t_calls = bool(re.search(r"\bt\(['\"]", content))
has_import = 'useTranslation' in content and 'import' in content
if has_t_calls and not has_import:
# Add import after 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:]
fixes += 1
# Step 4: Re-add the hook in the correct place (main function component)
if has_t_calls and has_import or (has_t_calls and 'useTranslation' in content):
# Check if hook is already present
if 'const { t } = useTranslation();' not in content:
# Find the main function component and add the hook after its opening brace
# Pattern 1: export default function ComponentName() {
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:
# Pattern 2: function ComponentName() {
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:
# Pattern 3: const ComponentName = () => {
# 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")

63
.workbuddy/fix_jsx_gt.py Normal file
View File

@@ -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: <Option value={1}{t('key')}</Option> -> <Option value={1}>{t('key')}</Option>
content = re.sub(
r'\}\{t\(\'',
'}>{t(\'',
content
)
# Fix 2: "{t(' -> ">{t(' (consumed > after JSX string attribute)
# Example: <Option value="X"{t('key')}</Option> -> <Option value="X">{t('key')}</Option>
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")

82
.workbuddy/fix_jsx_gt2.py Normal file
View File

@@ -0,0 +1,82 @@
"""
Fix script 3: Fix self-closing tag consumed '>'.
Pattern: <Icon /{t('key')} -> <Icon />{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</Tag> where > was consumed)
# Pattern: (letter or >){t(' -> (letter or >)>{t('
# But we need to be careful not to break valid JSX expressions like:
# <div>{t('key')}</div> -- 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: <Tag>中文 -> <Tag{t('key')
# Fix: insert > 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:
# <Tag{t('key')}</Tag> -> <Tag>{t('key')}</Tag>
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")

View File

@@ -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")

View File

@@ -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(
'<p className="text-lg font-medium">\u6a21\u5757\u5f00\u53d1\u4e2d</p>',
'<p className="text-lg font-medium">{t("router.moduleDeveloping")}</p>'
)
# Replace placeholder pages
content = content.replace(
'<h2>\u901a\u77e5\u7b56\u7565\u9875\u9762</h2>',
'<h2>{t("router.noticePolicy")}</h2>'
)
content = content.replace(
'<h2>\u6570\u636e\u4e0e\u5b89\u5168\u9875\u9762</h2>',
'<h2>{t("router.dataSafety")}</h2>'
)
# Add useTranslation import and helper components
add_after_imports = """
import { useTranslation } from 'react-i18next';
function ModuleDeveloping() {
\tconst { t } = useTranslation();
\treturn (
\t\t<div className="flex flex-col items-center justify-center h-full gap-4 text-gray-400 select-none">
\t\t\t<span className="text-6xl opacity-20">\ud83c\udde7</span>
\t\t\t<p className="text-lg font-medium">{t('router.moduleDeveloping')}</p>
\t\t</div>
\t);
}
function NoticePolicyPage() {
\tconst { t } = useTranslation();
\treturn <div style={{ padding: 20 }}><h2>{t('router.noticePolicy')}</h2></div>;
}
function DataSafetyPage() {
\tconst { t } = useTranslation();
\treturn <div style={{ padding: 20 }}><h2>{t('router.dataSafety')}</h2></div>;
}
"""
# 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 = '<div className="flex flex-col items-center justify-center h-full gap-4 text-gray-400 select-none">\n\t\t\t\t\t\t\t<span className="text-6xl opacity-20">\ud83c\udde7</span>\n\t\t\t\t\t\t\t<p className="text-lg font-medium">{t("router.moduleDeveloping")}</p>\n\t\t\t\t\t\t</div>'
content = content.replace(old_404, '<ModuleDeveloping />')
# Replace the inline notice/dataSafety elements
old_notice = '<div style={{ padding: 20 }}><h2>{t("router.noticePolicy")}</h2></div>'
content = content.replace(old_notice, '<NoticePolicyPage />')
old_datasafety = '<div style={{ padding: 20 }}><h2>{t("router.dataSafety")}</h2></div>'
content = content.replace(old_datasafety, '<DataSafetyPage />')
with open('src/router/index.tsx', 'w', encoding='utf-8') as f:
f.write(content)
print("router/index.tsx updated successfully")

119
.workbuddy/update_utils.py Normal file
View File

@@ -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")

78
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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 (
<AntdApp>
<StaticAntd />
<RouterProvider router={router} />
</AntdApp>
<ConfigProvider
locale={isEn ? enUS : zhCN}
theme={{
algorithm: theme.defaultAlgorithm,
token: {
colorPrimary: '#1890ff',
borderRadius: 2,
padding: 12,
paddingLG: 16,
margin: 12,
marginLG: 16,
},
components: {
Layout: { headerBg: '#fff', siderBg: '#fff' },
Menu: { itemBg: '#fff', itemSelectedBg: '#e6f4ff' },
Card: { paddingLG: 12 },
Table: { padding: 8, paddingXS: 4 },
Form: { itemMarginBottom: 12 },
Space: { gap: 8 },
}
}}
>
{children}
</ConfigProvider>
);
};
export default function App() {
return (
<I18nextProvider i18n={i18n}>
<AntdConfigWrapper>
<AntdApp>
<StaticAntd />
<RouterProvider router={router} />
</AntdApp>
</AntdConfigWrapper>
</I18nextProvider>
);
}

View File

@@ -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);
}

View File

@@ -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) => (
<Space>
@@ -80,19 +82,19 @@ const DictManager = ({
<Popconfirm
title="确定要删除这条数据吗?"
description="删除后将无法恢复"
okText="确定"
cancelText="取消"
okText={t('common.ok')}
cancelText={t('common.cancel')}
onConfirm={async () => {
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 = ({
<Modal
open={visible}
title={editing ? '编辑' : '新增'}
title={editing ? t('roles.edit') : t('common.addNew')}
onCancel={() => setVisible(false)}
onOk={handleSubmit}
confirmLoading={loading}

View File

@@ -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<EmptyDataProps> = ({
description = '暂无数据',
description = t('common.noData'),
height = 220
}) => {
return (

View File

@@ -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: (
<span style={{ fontWeight: currentLng === 'zh' ? 600 : 400 }}>
中文
</span>
),
},
{
key: 'en',
label: (
<span style={{ fontWeight: currentLng === 'en' ? 600 : 400 }}>
English
</span>
),
},
];
const handleChange: MenuProps['onClick'] = ({ key }) => {
i18n.changeLanguage(key);
localStorage.setItem('i18nextLng', key);
};
return (
<Dropdown menu={{ items, onClick: handleChange }} placement="bottomRight">
<Button
type="text"
icon={<GlobalOutlined />}
style={{ color: '#fff', fontSize: 'clamp(12px, 1vw, 14px)' }}
>
{currentLng === 'zh' ? '中文' : 'EN'}
</Button>
</Dropdown>
);
}

View File

@@ -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 (
<ConfigProvider
locale={zhCN}
theme={{
algorithm: theme.defaultAlgorithm,
token: {
colorPrimary: '#1890ff',
borderRadius: 2,
padding: 12,
paddingLG: 16,
margin: 12,
marginLG: 16,
},
components: {
Layout: { headerBg: '#fff', siderBg: '#fff' },
Menu: { itemBg: '#fff', itemSelectedBg: '#e6f4ff' },
Card: {
paddingLG: 12,
},
Table: {
padding: 8,
paddingXS: 4,
},
Form: {
itemMarginBottom: 12,
},
Space: {
gap: 8,
}
}
}}
>
<Layout style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<Header style={{
background: '#0837a8',
color: '#fff',
minHeight: "40px",
height: "auto",
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '2px 12px',
flexWrap: 'wrap',
gap: '4px'
}}>
<Space size="large" style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', height: "100%", justifyContent: "center" }}>
<Layout style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<Header style={{
background: '#0837a8',
color: '#fff',
minHeight: "40px",
height: "auto",
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '2px 12px',
flexWrap: 'wrap',
gap: '4px'
}}>
<Space size="large" style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', height: "100%", justifyContent: "center" }}>
<Button
type="text"
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
onClick={() => setCollapsed(!collapsed)}
style={{
fontSize: '16px',
color: '#fff',
width: 40,
height: 40,
}}
/>
<div style={{ height: "100%", display: "flex", gap: "10px", justifyContent: "center", alignItems: "center" }}>
<img src={logo} alt="Logo" style={{ width: 24, height: 24 }} />
{!collapsed && (
<span style={{ fontWeight: 'bold', fontSize: 'clamp(14px, 1.2vw, 16px)', color: '#fff', whiteSpace: 'nowrap' }}>
{t('layout.systemName')}
</span>
)}
</div>
<Select
value={stationId}
placeholder={t('layout.selectStation')}
onChange={handleStationChange}
options={stationList.map(s => ({ label: s.siteName, value: s.id }))}
style={{
width: 'clamp(160px, 15vw, 220px)',
backgroundColor: '#0837a8',
color: '#fff',
border: 'none',
}}
/>
</Space>
<Button
type="text"
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
onClick={() => setCollapsed(!collapsed)}
style={{
fontSize: '16px',
color: '#fff',
width: 40,
height: 40,
}}
/>
<Space size="middle" style={{ flexWrap: 'wrap' }}>
<Button
type="text"
icon={isFullScreen ? <CompressOutlined /> : <ExpandOutlined />}
onClick={toggleFullScreen}
style={{ color: '#fff', fontSize: 'clamp(12px, 1vw, 14px)' }}
>
{isFullScreen ? t('layout.exitFullScreen') : t('layout.fullScreen')}
</Button>
<LanguageSwitcher />
<span style={{ fontSize: 'clamp(12px, 1vw, 13px)' }}>{time}</span>
<Space
style={{ cursor: 'pointer', padding: '0 4px' }}
onClick={() => navigate('/profile')}
className="hover:opacity-80"
>
{userInfo?.avatar && (
<Avatar
size="small"
src={userInfo.avatar.startsWith('http') ? userInfo.avatar : `${envConfig.baseURL}${userInfo.avatar}`}
/>
)}
<span style={{ fontSize: 'clamp(12px, 1vw, 14px)' }}>{userInfo?.nickName || userInfo?.username}</span>
</Space>
<Button type="text" danger icon={<LogoutOutlined />} onClick={handleLogout} style={{ fontSize: 'clamp(12px, 1vw, 14px)' }}>
{t('layout.logout')}
</Button>
</Space>
</Header>
<div style={{ height: "100%", display: "flex", gap: "10px", justifyContent: "center", alignItems: "center" }}>
<img src={logo} alt="Logo" style={{ width: 24, height: 24 }} />
{!collapsed && (
<span style={{ fontWeight: 'bold', fontSize: 'clamp(14px, 1.2vw, 16px)', color: '#fff', whiteSpace: 'nowrap' }}>
光伏电站智能监控与运维系统
</span>
)}
<Layout style={{ flex: 1, overflow: 'hidden', background: '#fff' }}>
<Sider
collapsible
collapsed={collapsed}
trigger={null}
width={180}
theme="light"
style={{ borderRight: '1px solid #e5e7eb' }}
>
<Layout
style={{
height: '100%',
flexDirection: 'column',
background: '#fff',
overflow: 'hidden'
}}
>
<div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden' }}>
<Menu
mode="inline"
theme="light"
items={menuList}
selectedKeys={[location.pathname]}
onClick={({ key }) => navigate(key)}
style={{ borderRight: 0 }}
/>
</div>
<Select
value={stationId}
placeholder="请选择电站"
onChange={handleStationChange}
options={stationList.map(s => ({ label: s.siteName, value: s.id }))}
style={{
width: 'clamp(160px, 15vw, 220px)',
backgroundColor: '#0837a8',
color: '#fff',
border: 'none',
}}
/>
</Space>
<Space size="middle" style={{ flexWrap: 'wrap' }}>
<Button
type="text"
icon={isFullScreen ? <CompressOutlined /> : <ExpandOutlined />}
onClick={toggleFullScreen}
style={{ color: '#fff', fontSize: 'clamp(12px, 1vw, 14px)' }}
>
{isFullScreen ? '退出全屏' : '全屏'}
</Button>
<span style={{ fontSize: 'clamp(12px, 1vw, 13px)' }}>{time}</span>
<Space
style={{ cursor: 'pointer', padding: '0 4px' }}
onClick={() => navigate('/profile')}
className="hover:opacity-80"
>
{userInfo?.avatar && (
<Avatar
size="small"
src={userInfo.avatar.startsWith('http') ? userInfo.avatar : `${envConfig.baseURL}${userInfo.avatar}`}
/>
)}
<span style={{ fontSize: 'clamp(12px, 1vw, 14px)' }}>{userInfo?.nickName || userInfo?.username}</span>
</Space>
<Button type="text" danger icon={<LogoutOutlined />} onClick={handleLogout} style={{ fontSize: 'clamp(12px, 1vw, 14px)' }}>
退出登录
</Button>
</Space>
</Header>
<Layout style={{ flex: 1, overflow: 'hidden', background: '#fff' }}>
<Sider
collapsible
collapsed={collapsed}
trigger={null}
width={180}
theme="light"
style={{ borderRight: '1px solid #e5e7eb' }}
>
<Layout
style={{
height: '100%',
flexDirection: 'column',
background: '#fff',
overflow: 'hidden'
}}
>
{/* 菜单滚动区域:弹性占满剩余空间,超出滚动 */}
<div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden' }}>
<Menu
mode="inline"
theme="light"
items={menuList}
selectedKeys={[location.pathname]}
onClick={({ key }) => navigate(key)}
style={{ borderRight: 0 }}
/>
</div>
{/* 底部场站卡片,固定在底部,不被菜单挤压 */}
{!collapsed && currentStation && (
<Card
size="small"
style={{
border: '1px solid #e5e7eb',
borderRadius: 8,
margin: 4,
marginBottom: 8
}}
styles={{ body: { padding: '12px 4px' } }}
>
<div style={{ textAlign: 'left' }}>
<div style={{ fontSize: 14, fontWeight: 600, marginBottom: 8 }}>
{currentStation.siteName || '1MW 光伏电站'}
</div>
<img
style={{ width: '100%', borderRadius: "8px", marginBottom: 8 }}
src="https://images.unsplash.com/photo-1509391366360-2e959784a276?ixlib=rb-1.2.1&auto=format&fit=crop&w=1200&q=80"
/>
<div style={{ fontSize: 11, color: '#666', lineHeight: 1.8 }}>
<div>电站地址:{currentStation.address || '河南省林州'}</div>
<div>电站类型:{currentStation.type || '地面电站'}</div>
<div style={{ marginTop: 4, display: 'flex', alignItems: 'center', color: '#41ad0a' }}>
<span>运行状态:</span>
<span style={{ marginLeft: 4 }}>●</span>
<span style={{ marginLeft: 4 }}>正常运行</span>
</div>
{!collapsed && currentStation && (
<Card
size="small"
style={{
border: '1px solid #e5e7eb',
borderRadius: 8,
margin: 4,
marginBottom: 8
}}
styles={{ body: { padding: '12px 4px' } }}
>
<div style={{ textAlign: 'left' }}>
<div style={{ fontSize: 14, fontWeight: 600, marginBottom: 8 }}>
{currentStation.siteName || t('layout.defaultStationName')}
</div>
<img
style={{ width: '100%', borderRadius: "8px", marginBottom: 8 }}
src="https://images.unsplash.com/photo-1509391366360-2e959784a276?ixlib=rb-1.2.1&auto=format&fit=crop&w=1200&q=80"
/>
<div style={{ fontSize: 11, color: '#666', lineHeight: 1.8 }}>
<div>{t('layout.stationAddress')}:{currentStation.address || t('layout.defaultStationAddress')}</div>
<div>{t('layout.stationType')}:{currentStation.type || t('layout.groundStation')}</div>
<div style={{ marginTop: 4, display: 'flex', alignItems: 'center', color: '#41ad0a' }}>
<span>{t('layout.runningStatus')}:</span>
<span style={{ marginLeft: 4 }}>●</span>
<span style={{ marginLeft: 4 }}>{t('layout.normalRunning')}</span>
</div>
</div>
</Card>
)}
</Layout>
</Sider>
</div>
</Card>
)}
</Layout>
</Sider>
<Content style={{ background: '#f4f7f9', overflowY: 'auto', padding: '4px 8px' }}>
<Outlet />
</Content>
</Layout>
<Content style={{ background: '#f4f7f9', overflowY: 'auto', padding: '4px 8px' }}>
<Outlet />
</Content>
</Layout>
</ConfigProvider>
</Layout>
);
}

View File

@@ -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];

View File

@@ -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) => (
<Space>
<Button type="link" onClick={() => handleEdit(record)}>编辑</Button>
<Button type="link" danger onClick={() => handleDelete(record.id)}>删除</Button>
</Space>
)
}
];
// 表格多选
const rowSelection = {
selectedRowKeys,
onChange: (keys: (string | number)[]) => setSelectedRowKeys(keys),
};
return (
<div>
<Card>
<Space style={{ marginBottom: 16 }}>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>新增告警模型</Button>
<Button icon={<ReloadOutlined />} onClick={fetchList}>刷新</Button>
<Button
danger
icon={<DeleteOutlined />}
onClick={handleBatchDelete}
disabled={selectedRowKeys.length === 0}
>
批量删除{selectedRowKeys.length ? `(${selectedRowKeys.length})` : ''}
</Button>
</Space>
<Table
rowSelection={rowSelection}
columns={columns}
dataSource={list}
rowKey="id"
loading={loading}
pagination={false}
/>
</Card>
<Modal
title={editingItem ? '编辑告警模型' : '新增告警模型'}
open={modalVisible}
onCancel={() => setModalVisible(false)}
footer={null}
width={600}
>
<Form form={form} layout="vertical" onFinish={handleSave}>
{/* 错误编码(关联错误管理) */}
<Form.Item
label="错误编码"
name="errorCode"
rules={[{ required: true, message: '请选择错误编码' }]}
>
<Select placeholder="请选择错误编码" options={errorCodeOptions} allowClear />
</Form.Item>
{/* 工单模型ID(关联工单模型) */}
<Form.Item
label="关联工单模型"
name="modelId"
rules={[{ required: true, message: '请选择工单模型' }]}
>
<Select placeholder="请选择工单模型" options={modelOptions} allowClear />
</Form.Item>
<Form.Item
label="错误来源"
name="sourceType"
rules={[{ required: true, message: '请选择错误来源' }]}
>
<Select placeholder="请选择错误来源" options={errorSourceOptions} allowClear />
</Form.Item>
{/* 是否自动生成工单 */}
<Form.Item
label="自动生成工单"
name="autoGenerateOrder"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">保存</Button>
<Button onClick={() => setModalVisible(false)}>取消</Button>
</Space>
</Form.Item>
</Form>
</Modal>
</div>
);
};
const AlarmSettingsManagement: React.FC = () => {
return (
<div style={{ padding: 0 }}>
<AlarmModelManagement />
</div>
);
};
export default AlarmSettingsManagement;
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';
import { useTranslation } from 'react-i18next';
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 { t } = useTranslation();
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: t('common.ok'),
cancelText: t('common.cancel'),
onOk: async () => {
try {
const res = await deleteAlarmOrderConfigApi([id]);
if (res.code === 200) {
message.success(t('common.deleteSuccess'));
fetchList();
}
} catch (e) {
message.error(t('common.deleteFail'));
}
}
});
};
// 批量删除
const handleBatchDelete = () => {
if (!selectedRowKeys.length) {
message.warning('请先勾选要删除的数据');
return;
}
modal.confirm({
title: '批量删除确认',
content: `您确定要删除选中的 ${selectedRowKeys.length} 条告警模型吗?删除后不可恢复!`,
okText: t('common.ok'),
cancelText: t('common.cancel'),
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 ? '更新成功' : t('common.addSuccess'));
setModalVisible(false);
form.resetFields();
fetchList();
}
} catch (e) {
message.error(t('common.saveFail'));
}
};
// 表格列配置
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: t('roles.actions'),
key: 'action',
render: (_: any, record: any) => (
<Space>
<Button type="link" onClick={() => handleEdit(record)}>{t('roles.edit')}</Button>
<Button type="link" danger onClick={() => handleDelete(record.id)}>{t('roles.delete')}</Button>
</Space>
)
}
];
// 表格多选
const rowSelection = {
selectedRowKeys,
onChange: (keys: (string | number)[]) => setSelectedRowKeys(keys),
};
return (
<div>
<Card>
<Space style={{ marginBottom: 16 }}>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>新增告警模型</Button>
<Button icon={<ReloadOutlined />} onClick={fetchList}>{t('common.refresh')}</Button>
<Button
danger
icon={<DeleteOutlined />}
onClick={handleBatchDelete}
disabled={selectedRowKeys.length === 0}
>
批量删除{selectedRowKeys.length ? `(${selectedRowKeys.length})` : ''}
</Button>
</Space>
<Table
rowSelection={rowSelection}
columns={columns}
dataSource={list}
rowKey="id"
loading={loading}
pagination={false}
/>
</Card>
<Modal
title={editingItem ? '编辑告警模型' : '新增告警模型'}
open={modalVisible}
onCancel={() => setModalVisible(false)}
footer={null}
width={600}
>
<Form form={form} layout="vertical" onFinish={handleSave}>
{/* 错误编码(关联错误管理) */}
<Form.Item
label="错误编码"
name="errorCode"
rules={[{ required: true, message: '请选择错误编码' }]}
>
<Select placeholder="请选择错误编码" options={errorCodeOptions} allowClear />
</Form.Item>
{/* 工单模型ID(关联工单模型) */}
<Form.Item
label="关联工单模型"
name="modelId"
rules={[{ required: true, message: '请选择工单模型' }]}
>
<Select placeholder="请选择工单模型" options={modelOptions} allowClear />
</Form.Item>
<Form.Item
label="错误来源"
name="sourceType"
rules={[{ required: true, message: '请选择错误来源' }]}
>
<Select placeholder="请选择错误来源" options={errorSourceOptions} allowClear />
</Form.Item>
{/* 是否自动生成工单 */}
<Form.Item
label="自动生成工单"
name="autoGenerateOrder"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">{t('common.save')}</Button>
<Button onClick={() => setModalVisible(false)}>{t('common.cancel')}</Button>
</Space>
</Form.Item>
</Form>
</Modal>
</div>
);
};
const AlarmSettingsManagement: React.FC = () => {
return (
<div style={{ padding: 0 }}>
<AlarmModelManagement />
</div>
);
};

View File

@@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Table,
Input,
@@ -36,6 +37,7 @@ const allDeviceList = [
];
export default function AllDeviceManagement() {
const { t } = useTranslation();
const { modal } = App.useApp();
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
@@ -43,10 +45,10 @@ export default function AllDeviceManagement() {
const columns = [
{ title: '设备编码', dataIndex: 'code', key: 'code' },
{ title: '设备名称', dataIndex: 'name', key: 'name' },
{ title: '类型', dataIndex: 'type', key: 'type' },
{ title: t('devices.deviceName'), dataIndex: 'name', key: 'name' },
{ title: t('common.type'), dataIndex: 'type', key: 'type' },
{
title: '在线', dataIndex: 'online', key: 'online',
title: t('devices.online'), dataIndex: 'online', key: 'online',
render: (text) => (
<Tag color={text === 'ONLINE' ? 'green' : 'red'}>{text}</Tag>
),
@@ -64,11 +66,11 @@ export default function AllDeviceManagement() {
},
},
{
title: '操作', key: 'action',
title: t('roles.actions'), key: 'action',
render: (_, record: any) => (
<Space>
<Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)}>编辑</Button>
<Button type="link" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record)}>删除</Button>
<Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)}>{t('roles.edit')}</Button>
<Button type="link" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record)}>{t('roles.delete')}</Button>
</Space>
),
},
@@ -122,13 +124,13 @@ export default function AllDeviceManagement() {
<Input placeholder="请输入设备名称" style={{ width: 220 }} />
</Form.Item>
<Form.Item>
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>查询</Button>
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>{t('common.search2')}</Button>
</Form.Item>
<Form.Item>
<Button icon={<ReloadOutlined />} onClick={handleReset}>重置</Button>
<Button icon={<ReloadOutlined />} onClick={handleReset}>{t('common.reset')}</Button>
</Form.Item>
<Form.Item>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>新增</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>{t('common.addNew')}</Button>
</Form.Item>
</Form>
</div>

View File

@@ -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: <BellOutlined style={{ fontSize: 24, color: '#ff4d4f' }} />,
bg: 'linear-gradient(135deg, #fff2f0 0%, #ffccc7 100%)'
},
{
title: '数据备份状态',
value: '正常',
value: t('roles.normal'),
trend: '●',
trendColor: '#52c41a',
icon: <DatabaseOutlined style={{ fontSize: 24, color: '#13c2c2' }} />,
@@ -130,7 +132,7 @@ const BasicSettings = () => {
title: '运维工程师',
users: 5,
perms: 18,
tags: ['设备管理', '工单管理'],
tags: [t('router.devices'), '工单管理'],
color: '#52c41a',
bg: '#f6ffed',
icon: <Wrench size={20} color="#52c41a" />
@@ -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 (
<div className="normal-spacing" style={{ padding: '8px 0px', background: '#f5f7fa', minHeight: '100vh' }}>
<div style={{ padding: '0px 0px 8px' }}>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>基础设置</Typography.Title>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>{t('systemSetting.basicSettings')}</Typography.Title>
</div>
@@ -210,27 +212,27 @@ const BasicSettings = () => {
<Form form={form} layout="vertical">
<Row gutter={16}>
<Col xs={24} sm={12} md={8}>
<Form.Item label="系统名称" name="systemName" style={{ marginBottom: 12 }}>
<Input size="small" defaultValue="光伏电站智能监控与运维系统" />
<Form.Item label={t('systemSetting.systemName')} name="systemName" style={{ marginBottom: 12 }}>
<Input size="small" defaultValue={t('login.systemName')} />
</Form.Item>
</Col>
<Col xs={24} sm={12} md={8}>
<Form.Item label="默认电站" name="defaultSite" style={{ marginBottom: 12 }}>
<Select size="small" defaultValue="1MW 光伏电站">
<Select.Option value="1MW">1MW 光伏电站</Select.Option>
<Select size="small" defaultValue={t('layout.defaultStationName')}>
<Select.Option value="1MW">{t('layout.defaultStationName')}</Select.Option>
</Select>
</Form.Item>
</Col>
<Col xs={24} sm={12} md={8}>
<Form.Item label="默认语言" name="defaultLang" style={{ marginBottom: 12 }}>
<Form.Item label={t('systemSetting.defaultLanguage')} name="defaultLang" style={{ marginBottom: 12 }}>
<Select size="small" defaultValue="zh">
<Select.Option value="zh">中文</Select.Option>
<Select.Option value="zh">{t('users.chinese')}</Select.Option>
<Select.Option value="en">English</Select.Option>
</Select>
</Form.Item>
</Col>
<Col xs={24} sm={12} md={8}>
<Form.Item label="时区" name="timezone" style={{ marginBottom: 12 }}>
<Form.Item label={t('systemSetting.timezone')} name="timezone" style={{ marginBottom: 12 }}>
<Select size="small" defaultValue="UTC+08:00">
<Select.Option value="8">UTC+08:00</Select.Option>
</Select>
@@ -239,7 +241,7 @@ const BasicSettings = () => {
<Col xs={24} sm={12} md={8}>
<Form.Item label="首页默认页面" name="defaultPage" style={{ marginBottom: 12 }}>
<Select size="small" defaultValue="dashboard">
<Select.Option value="dashboard">总览首页</Select.Option>
<Select.Option value="dashboard">{t('router.overview')}</Select.Option>
</Select>
</Form.Item>
</Col>
@@ -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 <Tag bordered={false} style={{ color: s.color, background: s.bg, fontSize: 10, margin: 0 }}>{role}</Tag>;
}
},
{ 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) => (
<Space size={4}>
<div style={{ width: 6, height: 6, borderRadius: '50%', background: status === '在线' ? '#52c41a' : '#bfbfbf' }} />
<div style={{ width: 6, height: 6, borderRadius: '50%', background: status === t('devices.online') ? '#52c41a' : '#bfbfbf' }} />
<span style={{ fontSize: 10 }}>{status}</span>
</Space>
)
},
{
title: '操作', key: 'action', width: 60, render: () => <Button
title: t('roles.actions'), key: 'action', width: 60, render: () => <Button
type="link"
size="small"
icon={<EyeOutlined />}
@@ -383,10 +385,10 @@ const BasicSettings = () => {
},
]}
dataSource={[
{ name: '张运维', role: '超级管理员', phone: '138****0001', email: 'zhangyw@pvsmart.com', status: '在线' },
{ name: '李工程师', role: '运维工程师', phone: '139****0002', email: 'li.gongcheng@pvsmart.com', status: '在线' },
{ name: '王班长', role: '巡检主管', phone: '137****0003', email: 'wang.banzhang@pvsmart.com', status: '在线' },
{ name: '赵分析师', role: '数据分析师', phone: '136****0004', email: 'zhao.fx@pvsmart.com', status: '在线' },
{ name: '张运维', role: '超级管理员', phone: '138****0001', email: 'zhangyw@pvsmart.com', status: t('devices.online') },
{ name: '李工程师', role: '运维工程师', phone: '139****0002', email: 'li.gongcheng@pvsmart.com', status: t('devices.online') },
{ name: '王班长', role: '巡检主管', phone: '137****0003', email: 'wang.banzhang@pvsmart.com', status: t('devices.online') },
{ name: '赵分析师', role: '数据分析师', phone: '136****0004', email: 'zhao.fx@pvsmart.com', status: t('devices.online') },
]}
/>
</AntdCard>
@@ -464,11 +466,11 @@ const BasicSettings = () => {
<rect x="93" y="73" width="14" height="14" rx="2" fill="none" stroke="#fff" strokeWidth="1.2" />
<path d="M96 76h8M96 80h8M96 84h8" stroke="#fff" strokeWidth="0.8" />
{[
{ x: 60, y: 40, icon: <Cpu size={10} />, label: '逆变器' },
{ x: 140, y: 40, icon: <Box size={10} />, label: '汇流箱' },
{ x: 60, y: 40, icon: <Cpu size={10} />, label: t('home.inverter') },
{ x: 140, y: 40, icon: <Box size={10} />, label: t('home.combinerBox') },
{ x: 160, y: 80, icon: <Wind size={10} />, label: '气象站' },
{ x: 140, y: 120, icon: <Camera size={10} />, label: '摄像头' },
{ x: 60, y: 120, icon: <Activity size={10} />, label: '机器人' },
{ x: 140, y: 120, icon: <Camera size={10} />, label: t('devices.camera') },
{ x: 60, y: 120, icon: <Activity size={10} />, label: t('devices.robot') },
{ x: 40, y: 80, icon: <Zap size={10} />, label: '接口' },
].map((node, i) => (
<g key={i}>
@@ -486,7 +488,7 @@ const BasicSettings = () => {
{/* 数据与安全 */}
<AntdCard
title={<Space><Shield size={16} style={{ color: '#1677ff' }} /><b>数据与安全</b></Space>}
title={<Space><Shield size={16} style={{ color: '#1677ff' }} /><b>systemSetting.dataSafety</b></Space>}
bordered={false}
style={{ borderRadius: 10, marginBottom: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}
bodyStyle={{ padding: '12px' }}
@@ -496,7 +498,7 @@ const BasicSettings = () => {
{ label: '数据保留', value: '3 年', icon: <DatabaseOutlined style={{ color: '#1677ff' }} /> },
{ label: '登录审计', value: '已开启', icon: <CheckCircleOutlined style={{ color: '#52c41a' }} />, color: '#52c41a' },
{ label: '自动备份', value: '02:00', icon: <RefreshCw size={12} color="#1677ff" /> },
{ label: 'API 密钥', value: '正常', icon: <Key size={12} color="#52c41a" />, color: '#52c41a' },
{ label: 'API 密钥', value: t('roles.normal'), icon: <Key size={12} color="#52c41a" />, color: '#52c41a' },
{ label: '最近备份', value: '2025-05-24 02:00', icon: <CalendarOutlined style={{ color: '#1677ff' }} /> },
{ label: 'SSL 证书', value: '有效期至 2026-12-31', icon: <SafetyCertificateOutlined style={{ color: '#fa8c16' }} /> },
{ label: '双因素认证', value: '已开启', icon: <Lock size={12} color="#52c41a" />, color: '#52c41a' },
@@ -516,7 +518,7 @@ const BasicSettings = () => {
{/* 系统维护 */}
<AntdCard
title={<Space><Wrench size={16} style={{ color: '#1677ff' }} /><b>系统维护</b></Space>}
title={<Space><Wrench size={16} style={{ color: '#1677ff' }} /><b>systemSetting.systemMaintenance</b></Space>}
extra={<Button type="link" size="small" danger style={{ fontSize: 10, padding: 0 }}>管理权限</Button>}
bordered={false}
style={{ borderRadius: 10, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}

View File

@@ -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) => (
<Space>
<EnvironmentOutlined style={{ color: '#1890ff' }} />
@@ -134,13 +136,13 @@ export default function CameraManagement() {
render: (nv) => nv ? <Tag color="green">支持</Tag> : <Tag color="default">不支持</Tag>
},
{
title: '在线', dataIndex: 'online', key: 'online', width: 100,
title: t('devices.online'), dataIndex: 'online', key: 'online', width: 100,
render: (text) => (
<Tag color={text === 'ONLINE' ? 'green' : 'red'}>{text === 'ONLINE' ? '在线' : '离线'}</Tag>
<Tag color={text === 'ONLINE' ? 'green' : 'red'}>{text === 'ONLINE' ? t('devices.online') : t('devices.offline')}</Tag>
),
},
{
title: '状态', dataIndex: 'status', key: 'status', width: 100,
title: t('roles.status'), dataIndex: 'status', key: 'status', width: 100,
render: (text) => {
const colorMap: Record<string, string> = {
NORMAL: 'green',
@@ -148,20 +150,20 @@ export default function CameraManagement() {
WARNING: 'orange',
};
const textMap: Record<string, string> = {
NORMAL: '正常',
FAULT: '故障',
WARNING: '警告',
NORMAL: t('roles.normal'),
FAULT: t('devices.error'),
WARNING: t('constants.warning'),
};
return <Tag color={colorMap[text] || 'default'}>{textMap[text] || text}</Tag>;
},
},
{
title: '操作', key: 'action', width: 200, fixed: 'right' as const,
title: t('roles.actions'), key: 'action', width: 200, fixed: 'right' as const,
render: (_, record: any) => (
<Space>
<Button type="link" icon={<VideoCameraOutlined />} onClick={() => handleView(record)}>预览</Button>
<Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)}>编辑</Button>
<Button type="link" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record)}>删除</Button>
<Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)}>{t('roles.edit')}</Button>
<Button type="link" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record)}>{t('roles.delete')}</Button>
</Space>
),
},
@@ -282,7 +284,7 @@ export default function CameraManagement() {
<Card size="small">
<Space align="center" style={{ width: '100%', justifyContent: 'space-between' }}>
<div>
<div style={{ color: '#86909C', fontSize: 12 }}>在线</div>
<div style={{ color: '#86909C', fontSize: 12 }}>{t('devices.online')}</div>
<div style={{ fontSize: 28, fontWeight: 700, color: '#52c41a' }}>
{cameraList.filter((c: any) => c.online === 'ONLINE').length}
</div>
@@ -337,16 +339,16 @@ export default function CameraManagement() {
</Form.Item>
<Form.Item name="status">
<Select placeholder="状态筛选" style={{ width: 140 }} allowClear>
<Select.Option value="NORMAL">正常</Select.Option>
<Select.Option value="WARNING">警告</Select.Option>
<Select.Option value="FAULT">故障</Select.Option>
<Select.Option value="NORMAL">{t('roles.normal')}</Select.Option>
<Select.Option value="WARNING">{t('constants.warning')}</Select.Option>
<Select.Option value="FAULT">{t('devices.error')}</Select.Option>
</Select>
</Form.Item>
<Form.Item>
<Button type="primary" onClick={handleSearch}>查询</Button>
<Button type="primary" onClick={handleSearch}>{t('common.search2')}</Button>
</Form.Item>
<Form.Item>
<Button icon={<ReloadOutlined />} onClick={handleReset}>重置</Button>
<Button icon={<ReloadOutlined />} onClick={handleReset}>{t('common.reset')}</Button>
</Form.Item>
<Form.Item>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>新增摄像头</Button>
@@ -387,12 +389,12 @@ export default function CameraManagement() {
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="设备名称" name="name" rules={[{ required: true, message: '请输入设备名称' }]}>
<Form.Item label={t('devices.deviceName')} name="name" rules={[{ required: true, message: '请输入设备名称' }]}>
<Input placeholder="请输入设备名称" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="IP地址" name="ip" rules={[{ required: true, message: '请输入IP地址' }]}>
<Form.Item label={t('systemSetting.ipAddress')} name="ip" rules={[{ required: true, message: '请输入IP地址' }]}>
<Input placeholder="例如: 192.168.1.100" />
</Form.Item>
</Col>
@@ -412,7 +414,7 @@ export default function CameraManagement() {
<Select.Option value="海康威视">海康威视</Select.Option>
<Select.Option value="大华">大华</Select.Option>
<Select.Option value="宇视">宇视</Select.Option>
<Select.Option value="其他">其他</Select.Option>
<Select.Option value={t('constants.others')}>{t('constants.others')}</Select.Option>
</Select>
</Form.Item>
</Col>

View File

@@ -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<number, {
props: any[];
events: any[];
actions: any[];
}> = {
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<number, {
props: any[];
events: any[];
actions: any[];
}> = {
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) => <Tag color={r.onlineStatus ? 'green' : 'red'}>
{r.onlineStatus ? '在线' : '离线'}
{r.onlineStatus ? t('devices.online') : t('devices.offline')}
</Tag>
},
{
title: '操作',
title: t('roles.actions'),
fixed: 'right',
render: (r) => (
<Button type="text" icon={<EditOutlined />} onClick={() => openModal('device', r)}>
@@ -482,11 +485,11 @@ export default function DeviceAccessPage() {
}
},
{
title: '操作', fixed: "right", render: r => (
title: t('roles.actions'), fixed: "right", render: r => (
<Space>
<Button type="text" icon={<EditOutlined />} onClick={() => openModal('model', r)}>编辑</Button>
<Popconfirm title="确定要删除该物模型吗?" onConfirm={() => handleDeleteModel(r.id)} okText="确定" cancelText="取消">
<Button type="text" danger icon={<DeleteOutlined />}>删除</Button>
<Button type="text" icon={<EditOutlined />} onClick={() => openModal('model', r)}>{t('roles.edit')}</Button>
<Popconfirm title="确定要删除该物模型吗?" onConfirm={() => handleDeleteModel(r.id)} okText={t('common.ok')} cancelText={t('common.cancel')}>
<Button type="text" danger icon={<DeleteOutlined />}>{t('roles.delete')}</Button>
</Popconfirm>
</Space>
)
@@ -496,11 +499,11 @@ export default function DeviceAccessPage() {
<div className="p-0 pt-[8px] bg-[#f5f7fa] min-h-screen">
{contextHolder}
<div style={{ padding: '0px 0px 8px' }}>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>设备接入</Typography.Title>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>{t('systemSetting.deviceAccess')}</Typography.Title>
</div>
<Card className="mb-4">
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane tab={<span><VideoCameraOutlined />设备管理</span>} key="device" />
<TabPane tab={<span><VideoCameraOutlined />{t('router.devices')}</span>} key="device" />
<TabPane tab={<span><ProductOutlined />产品管理</span>} key="product" />
<TabPane tab={<span><AppstoreOutlined />物模型管理</span>} key="model" />
</Tabs>
@@ -546,15 +549,15 @@ export default function DeviceAccessPage() {
<Option value="udp">UDP</Option>
</Select>
</Form.Item>
<Form.Item name="status" label="状态" initialValue={1}>
<Select placeholder="状态" allowClear style={{ width: 100 }}>
<Option value={1}>启用</Option>
<Form.Item name="status" label={t('roles.status')} initialValue={1}>
<Select placeholder={t('roles.status')} allowClear style={{ width: 100 }}>
<Option value={1}>{t('common.enable')}</Option>
<Option value={0}>禁用</Option>
</Select>
</Form.Item>
<Form.Item style={{ marginLeft: 8 }}>
<Button type="primary" onClick={handleFilter}>查询</Button>
<Button onClick={handleReset} style={{ marginLeft: 8 }}>重置</Button>
<Button type="primary" onClick={handleFilter}>{t('common.search2')}</Button>
<Button onClick={handleReset} style={{ marginLeft: 8 }}>{t('common.reset')}</Button>
</Form.Item>
</Form>
<Button type="primary" icon={<PlusOutlined />} onClick={() => openModal('product')}>
@@ -574,23 +577,23 @@ export default function DeviceAccessPage() {
{ title: '上报Topic', dataIndex: 'uploadTopic', ellipsis: true },
{ title: '订阅Topic', dataIndex: 'subscribeTopic', ellipsis: true },
{
title: '状态',
title: t('roles.status'),
dataIndex: 'status',
width: 90,
render: (s) => <Tag color={s === 1 ? 'green' : 'red'}>{s === 1 ? '启用' : '禁用'}</Tag>
render: (s) => <Tag color={s === 1 ? 'green' : 'red'}>{s === 1 ? t('common.enable') : '禁用'}</Tag>
},
{
title: '操作', fixed: "right", width: 180, render: (r) => (
title: t('roles.actions'), fixed: "right", width: 180, render: (r) => (
<Space>
<Button type="text" style={{ color: '#1677ff' }} icon={<EditOutlined />} onClick={() => openModal('product', r)}>编辑</Button>
<Button type="text" style={{ color: '#1677ff' }} icon={<EditOutlined />} onClick={() => openModal('product', r)}>{t('roles.edit')}</Button>
<Button type="text" icon={<SwapOutlined />} onClick={() => openMappingModal()}>字段映射</Button>
<Popconfirm
title="确定要删除该产品吗?"
onConfirm={() => handleDeleteProduct(r.id)}
okText="确定"
cancelText="取消"
okText={t('common.ok')}
cancelText={t('common.cancel')}
>
<Button type="text" danger icon={<DeleteOutlined />}>删除</Button>
<Button type="text" danger icon={<DeleteOutlined />}>{t('roles.delete')}</Button>
</Popconfirm>
</Space>
)
@@ -616,7 +619,7 @@ export default function DeviceAccessPage() {
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
<Form form={filterForm} layout="inline">
<Form.Item name="name" label="模型名称">
<Input placeholder="名称" allowClear />
<Input placeholder={t('common.name')} allowClear />
</Form.Item>
<Form.Item name="connectType" label="连接协议">
<Select placeholder="协议" allowClear style={{ width: 160 }}>
@@ -626,8 +629,8 @@ export default function DeviceAccessPage() {
</Select>
</Form.Item>
<Form.Item>
<Button type="primary" onClick={handleFilter}>查询</Button>
<Button onClick={handleReset}>重置</Button>
<Button type="primary" onClick={handleFilter}>{t('common.search2')}</Button>
<Button onClick={handleReset}>{t('common.reset')}</Button>
</Form.Item>
</Form>
<Button type="primary" icon={<PlusOutlined />} onClick={() => openModal('model')}>新增物模型</Button>
@@ -645,7 +648,7 @@ export default function DeviceAccessPage() {
{modalType === 'device' && (
<>
<Form.Item
label="设备名称"
label={t('devices.deviceName')}
name="name"
rules={[{ required: true, message: '请输入设备名称' }]}
>
@@ -799,9 +802,9 @@ export default function DeviceAccessPage() {
</Select>
</Form.Item>
<Form.Item label="状态" name="status">
<Form.Item label={t('roles.status')} name="status">
<Select defaultValue={1}>
<Option value={1}>启用</Option>
<Option value={1}>{t('common.enable')}</Option>
<Option value={0}>禁用</Option>
</Select>
</Form.Item>
@@ -919,18 +922,18 @@ export default function DeviceAccessPage() {
<Input style={{ width: '100%' }} placeholder="默认值" />
</Form.Item>
<Form.Item
label="类型"
label={t('common.type')}
name={['actionConfig', action.actionId, paramKey, 'type']}
style={{ flex: 1 }}
>
<Input disabled style={{ width: '100%' }} placeholder="类型" />
<Input disabled style={{ width: '100%' }} placeholder={t('common.type')} />
</Form.Item>
<Form.Item
label="描述"
label={t('common.description')}
name={['actionConfig', action.actionId, paramKey, 'description']}
style={{ flex: 2 }}
>
<Input disabled style={{ width: '100%' }} placeholder="描述" />
<Input disabled style={{ width: '100%' }} placeholder={t('common.description')} />
</Form.Item>
</div>
))}
@@ -965,19 +968,19 @@ export default function DeviceAccessPage() {
</div>
<Divider>属性</Divider>
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={() => openItemModal('props')} className="mb-2">新增</Button>
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={() => openItemModal('props')} className="mb-2">{t('common.addNew')}</Button>
<Table size="small" dataSource={modelDetail.props} pagination={false} columns={[
{ title: '编码', dataIndex: 'code' },
{ title: '名称', dataIndex: 'name' },
{ title: '类型', dataIndex: 'type' },
{ title: t('common.name'), dataIndex: 'name' },
{ title: t('common.type'), dataIndex: 'type' },
{ title: '单位', dataIndex: 'unit' },
{ title: '默认值', dataIndex: 'defaultValue' },
{ title: '告警', render: r => <Switch checked={r.isAlarm} disabled size="small" /> },
{ title: t('constants.sourceAlert'), render: r => <Switch checked={r.isAlarm} disabled size="small" /> },
{
title: '操作', render: r => (
title: t('roles.actions'), render: r => (
<Space>
<Button type="text" onClick={() => openItemModal('props', r)}>编辑</Button>
<Popconfirm onConfirm={() => deleteItem('props', r.id)}><Button type="text" danger>删除</Button></Popconfirm>
<Button type="text" onClick={() => openItemModal('props', r)}>{t('roles.edit')}</Button>
<Popconfirm onConfirm={() => deleteItem('props', r.id)}><Button type="text" danger>roles.delete</Button></Popconfirm>
</Space>
)
}
@@ -988,13 +991,13 @@ export default function DeviceAccessPage() {
setCurrentAction({ actionId: '', actionName: '', params: {} });
setParamList([{ paramKey: '', type: 'int', desc: '', defaultValue: '', enum: [] }]);
setActionModal(true);
}} className="mb-2">新增</Button>
}} className="mb-2">{t('common.addNew')}</Button>
<Table size="small" dataSource={modelDetail.actions} pagination={false} columns={[
{ title: '指令编码', dataIndex: 'actionId' },
{ title: '指令名称', dataIndex: 'actionName' },
{ title: '参数', render: r => Object.keys(r.params || []).join(',') || '无' },
{
title: '操作', render: r => (
title: t('roles.actions'), render: r => (
<Space>
<Button type="text" onClick={() => {
setCurrentAction(r);
@@ -1014,9 +1017,9 @@ export default function DeviceAccessPage() {
}
setParamList(arr);
setActionModal(true);
}}>编辑</Button>
}}>{t('roles.edit')}</Button>
<Popconfirm onConfirm={() => setModelDetail({ ...modelDetail, actions: modelDetail.actions.filter(x => x.actionId !== r.actionId) })}>
<Button type="text" danger>删除</Button>
<Button type="text" danger>roles.delete</Button>
</Popconfirm>
</Space>
)
@@ -1033,7 +1036,7 @@ export default function DeviceAccessPage() {
<Form.Item name="id" hidden><Input /></Form.Item>
<Form.Item label="属性编码" name="code" rules={[{ required: true }]}><Input placeholder="如 temp" /></Form.Item>
<Form.Item label="属性名称" name="name" rules={[{ required: true }]}><Input placeholder="如 温度" /></Form.Item>
<Form.Item label="类型" name="type">
<Form.Item label={t('common.type')} name="type">
<Select>
{dataTypeList.map(item => (
<Option key={item.code} value={item.code}>{item.name}</Option>
@@ -1074,7 +1077,7 @@ export default function DeviceAccessPage() {
<Option key={item.code} value={item.code}>{item.code}</Option>
))}
</Select>
<Input placeholder="描述" value={paramList[0]?.desc || ''} onChange={e => setParamList([{ ...paramList[0], desc: e.target.value }])} />
<Input placeholder={t('common.description')} value={paramList[0]?.desc || ''} onChange={e => setParamList([{ ...paramList[0], desc: e.target.value }])} />
<Input placeholder="默认值" value={paramList[0]?.defaultValue || ''} onChange={e => setParamList([{ ...paramList[0], defaultValue: e.target.value }])} />
<Input placeholder="枚举(,分隔)" value={(paramList[0]?.enum || []).join(',')} onChange={e => setParamList([{ ...paramList[0], enum: e.target.value.split(',').map(s => s.trim()) }])} />
</div>

View File

@@ -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<any[]>([]);
@@ -79,14 +81,14 @@ export default function DeviceManagementPage() {
return (
<div style={{ padding: "8px 0px", paddingTop: 0 }}>
<div style={{ padding: '0px 0px 4px' }}>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>设备管理</Typography.Title>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>{t('router.devices')}</Typography.Title>
</div>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
style={{ marginBottom: 0 }}
items={[
{ key: 'all', label: '设备管理' },
{ key: 'all', label: t('router.devices') },
{ key: 'drone', label: '无人机管理' },
{ key: 'smart', label: '智能装备' },
{ key: 'camera', label: '摄像头管理' },

View File

@@ -31,6 +31,7 @@ import { getUser } from '@/api/user/index';
import { useSelector } from 'react-redux';
import { RootState } from '@/src/store';
import utils from '@/lib/utils';
import { useTranslation } from 'react-i18next';
interface DroneManagementProps {
allOrgOptions: any[];
@@ -45,6 +46,7 @@ export default function DroneManagement({
allUserOptions,
onAllDataNeedRefresh,
}: DroneManagementProps) {
const { t } = useTranslation();
const { modal } = App.useApp();
const { userInfo } = useSelector((state: RootState) => 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) : <Text type="secondary">未绑定</Text>;
},
},
{
title: '状态', width: 150, dataIndex: 'onlineStatus', key: 'status',
render: s => <Tag color={s === 1 ? 'green' : 'default'}>{s === 1 ? '在线' : '离线'}</Tag>
title: t('roles.status'), width: 150, dataIndex: 'onlineStatus', key: 'status',
render: s => <Tag color={s === 1 ? 'green' : 'default'}>{s === 1 ? t('devices.online') : t('devices.offline')}</Tag>
},
{
title: '操作', key: 'action', fixed: 'right' as const, width: 250,
title: t('roles.actions'), key: 'action', fixed: 'right' as const, width: 250,
render: (_, record: any) => (
<Space>
<Tooltip title="人员解绑">
@@ -326,16 +328,16 @@ export default function DroneManagement({
onChange={(e) => setDroneParams({ ...droneParams, sn: e.target.value })}
/>
<Select
placeholder="状态"
placeholder={t('roles.status')}
style={{ width: 120 }}
value={droneParams.status}
allowClear
onChange={(val) => setDroneParams({ ...droneParams, status: val })}
>
<Select.Option value="1">在线</Select.Option>
<Select.Option value="2">离线</Select.Option>
<Select.Option value="1">{t('devices.online')}</Select.Option>
<Select.Option value="2">{t('devices.offline')}</Select.Option>
</Select>
<Button icon={<ReloadOutlined />} onClick={() => setDroneParams({ pageNum: 1, pageSize: 10, sn: '', status: '' })}>重置</Button>
<Button icon={<ReloadOutlined />} onClick={() => setDroneParams({ pageNum: 1, pageSize: 10, sn: '', status: '' })}>{t('common.reset')}</Button>
<Button type="primary" icon={<LinkOutlined />} onClick={openBindModal}>绑定无人机</Button>
<Button disabled={selectedDroneKeys.length === 0} onClick={() => handleUnbind(selectedDroneKeys, 1)}>批量解绑(用户)</Button>
<Button disabled={selectedDroneKeys.length === 0} onClick={() => handleUnbind(selectedDroneKeys, 2)}>批量解绑(场站)</Button>
@@ -434,7 +436,7 @@ export default function DroneManagement({
))}
</Select>
</Form.Item>
<Form.Item label="负责人" name="userId" rules={[{ required: true, message: '请选择负责人' }]}>
<Form.Item label={t('systemSetting.leader')} name="userId" rules={[{ required: true, message: '请选择负责人' }]}>
<Select
placeholder="请先选择场站后选择负责人"
allowClear

View File

@@ -37,6 +37,7 @@ import utils from '@/lib/utils';
import { errorLevelOptions, errorSourceOptions, CompareEnumOptions } from '@/lib/utils';
import { useSelector } from 'react-redux';
import { RootState } from '@/src/store';
import { useTranslation } from 'react-i18next';
const { TextArea } = Input;
const { Text, Title } = Typography;
@@ -46,6 +47,7 @@ const AntdCard = Card as any;
export default function ErrorManagement() {
const { t } = useTranslation();
const { userInfo } = useSelector((state: RootState) => state.user);
const { stationId, currentStation } = useSelector((state: RootState) => state.station);
const { modal } = App.useApp();
@@ -89,7 +91,7 @@ export default function ErrorManagement() {
setTotal(res.total || res.rows?.length || 0);
}
} catch (err) {
utils.message.error('获取错误列表失败');
utils.message.error(t('systemSetting.getErrorListFailed'));
} finally {
setLoading(false);
}
@@ -159,26 +161,26 @@ export default function ErrorManagement() {
// 真实接口
const res = await addErrorApi(submitData);
if (res.code === 200) {
utils.message.success(modalType == 'edit' ? '编辑成功' : '添加成功');
utils.message.success(modalType == 'edit' ? t('common.editSuccess') : t('common.addSuccess'));
setModalVisible(false);
fetchErrorList();
} else {
utils.message.error(res.msg || '操作失败');
utils.message.error(res.msg || t('systemSetting.operationFailed'));
}
} catch (err) {
utils.message.error('操作失败');
utils.message.error(t('systemSetting.operationFailed'));
}
};
// 删除错误
const handleDelete = (record: any) => {
modal.confirm({
title: '确认删除',
content: `确定要删除错误 "${record.errorName}" 吗?`,
title: t('common.confirmDelete'),
content: t('systemSetting.confirmDeleteError', { name: record.errorName }),
icon: <ExclamationCircleOutlined />,
okText: '确定',
cancelText: '取消',
okText: t('common.ok'),
cancelText: t('common.cancel'),
onOk: async () => {
try {
// 真实接口
@@ -188,15 +190,15 @@ export default function ErrorManagement() {
console.log('删除请求参数', data);
const res = await deleteErrorApi(data.ids);
if (res.code == 200) {
utils.message.success('删除成功');
utils.message.success(t('common.deleteSuccess'));
fetchErrorList();
} else {
utils.message.error(res.msg || '删除失败');
utils.message.error(res.msg || t('common.deleteFail'));
}
} catch (err) {
utils.message.error('删除失败');
utils.message.error(t('common.deleteFail'));
}
},
});
@@ -258,20 +260,20 @@ export default function ErrorManagement() {
// 表格列(完全不变)
const columns = [
{
title: '错误编码',
title: t('systemSetting.errorCode'),
dataIndex: 'errorCode',
key: 'errorCode',
width: 150,
fixed: 'left' as const,
},
{
title: '错误名称',
title: t('systemSetting.errorName'),
dataIndex: 'errorName',
key: 'errorName',
width: 200,
},
{
title: '错误来源',
title: t('systemSetting.errorSourceLabel'),
dataIndex: 'errorSource',
key: 'errorSource',
width: 120,
@@ -281,7 +283,7 @@ export default function ErrorManagement() {
},
},
{
title: '错误等级',
title: t('systemSetting.errorLevelLabel'),
dataIndex: 'errorLevel',
key: 'errorLevel',
width: 100,
@@ -291,13 +293,13 @@ export default function ErrorManagement() {
},
},
{
title: '校验字段',
title: t('systemSetting.checkField'),
dataIndex: 'field',
key: 'field',
width: 130,
},
{
title: '对比规则',
title: t('systemSetting.compareRule'),
dataIndex: 'compareType',
key: 'compareType',
width: 140,
@@ -308,26 +310,26 @@ export default function ErrorManagement() {
},
{
title: '对比数值',
title: t('systemSetting.compareValueCol'),
dataIndex: 'compareValues',
key: 'compareValues',
width: 140,
},
{
title: '描述',
title: t('common.description'),
dataIndex: 'errorDescription',
key: 'errorDescription',
ellipsis: true,
},
{
title: '建议',
title: t('aiAnalysis.suggestion'),
dataIndex: 'suggestion',
key: 'suggestion',
width: 180,
},
{
title: '操作',
title: t('roles.actions'),
key: 'action',
width: 300,
fixed: 'right' as const,
@@ -338,26 +340,20 @@ export default function ErrorManagement() {
size="small"
icon={<EyeOutlined />}
onClick={() => openModal('view', record)}
>
查看
</Button>
>{t('common.view')}</Button>
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => openModal('edit', record)}
>
编辑
</Button>
>{t('common.edit')}</Button>
<Button
type="link"
size="small"
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(record)}
>
删除
</Button>
>{t('common.delete')}</Button>
</Space>
),
},
@@ -368,21 +364,21 @@ export default function ErrorManagement() {
<AntdCard style={{ borderRadius: 8 }}>
{/* 搜索区域完全不变 */}
<Form form={searchForm} layout="inline" style={{ marginBottom: 0 }}>
<Form.Item name="errorCode" label="错误编码">
<Input placeholder="请输入错误编码" style={{ width: 150 }} allowClear />
<Form.Item name="errorCode" label={t("systemSetting.errorCode")}>
<Input placeholder={t("systemSetting.pleaseInputErrorCode")} style={{ width: 150 }} allowClear />
</Form.Item>
<Form.Item name="errorName" label="错误名称">
<Input placeholder="请输入错误名称" style={{ width: 150 }} allowClear />
<Form.Item name="errorName" label={t("systemSetting.errorName")}>
<Input placeholder={t("systemSetting.pleaseInputErrorName")} style={{ width: 150 }} allowClear />
</Form.Item>
<Form.Item name="errorSource" label="错误来源">
<Select placeholder="请选择来源" style={{ width: 120 }} allowClear >
<Form.Item name="errorSource" label={t("systemSetting.errorSourceLabel")}>
<Select placeholder={t("systemSetting.pleaseSelectSource")} style={{ width: 120 }} allowClear >
{errorSourceOptions.map(option => (
<Option key={option.value} value={option.value}>{option.label}</Option>
))}
</Select>
</Form.Item>
<Form.Item name="errorLevel" label="错误等级">
<Select placeholder="请选择等级" style={{ width: 120 }} allowClear >
<Form.Item name="errorLevel" label={t("systemSetting.errorLevelLabel")}>
<Select placeholder={t("systemSetting.pleaseSelectLevel")} style={{ width: 120 }} allowClear >
{errorLevelOptions.map(option => (
<Option key={option.value} value={option.value}>{option.label}</Option>
))}
@@ -390,9 +386,9 @@ export default function ErrorManagement() {
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch} >查询</Button>
<Button icon={<ReloadOutlined />} onClick={handleReset} >重置</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={() => openModal('add')} >新增</Button>
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>{t('common.search2')}</Button>
<Button icon={<ReloadOutlined />} onClick={handleReset}>{t('common.reset')}</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={() => openModal('add')}>{t('common.addNew')}</Button>
</Space>
</Form.Item>
</Form>
@@ -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() {
<Modal
title={
modalType === 'add'
? '新增错误规则'
? t('systemSetting.addErrorRule')
: modalType === 'edit'
? '编辑错误规则'
: '查看错误规则详情'
? t('systemSetting.editErrorRule')
: t('systemSetting.viewErrorRuleDetail')
}
open={modalVisible}
onCancel={() => {
@@ -435,15 +431,15 @@ export default function ErrorManagement() {
footer={
modalType === 'view'
? [
<Button key="close" onClick={() => setModalVisible(false)}>关闭</Button>
<Button key="close" onClick={() => setModalVisible(false)}>{t('common.close')}</Button>
]
: [
<div style={{ margin: '8px 0 24px 0' }}></div>,
<Button key="cancel" onClick={() => {
setModalVisible(false);
form.resetFields();
}}>取消</Button>,
<Button key="submit" type="primary" onClick={() => form.submit()}>保存</Button>
}}>{t('common.cancel')}</Button>,
<Button key="submit" type="primary" onClick={() => form.submit()}>{t('common.save')}</Button>
]
}
>
@@ -457,13 +453,13 @@ export default function ErrorManagement() {
{/* 第一行:基础编码名称 */}
<Row gutter={16}>
<Col span={12}>
<Form.Item label="错误编码" name="errorCode" rules={[{ required: true, message: '请输入错误编码' }]}>
<Input placeholder="例如:ERR-ROBOT-001" />
<Form.Item label={t("systemSetting.errorCode")} name="errorCode" rules={[{ required: true, message: t("systemSetting.pleaseInputErrorCode") }]}>
<Input placeholder={t("systemSetting.errorCodePlaceholder")} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="错误名称" name="errorName" rules={[{ required: true, message: '请输入错误名称' }]}>
<Input placeholder="请输入错误名称" />
<Form.Item label={t("systemSetting.errorName")} name="errorName" rules={[{ required: true, message: t("systemSetting.pleaseInputErrorName") }]}>
<Input placeholder={t("systemSetting.pleaseInputErrorName")} />
</Form.Item>
</Col>
</Row>
@@ -471,8 +467,8 @@ export default function ErrorManagement() {
{/* 第二行:来源、等级、校验字段 */}
<Row gutter={16}>
<Col span={8}>
<Form.Item label="错误来源" name="errorSource" rules={[{ required: true, message: '请选择错误来源' }]}>
<Select placeholder="请选择">
<Form.Item label={t("systemSetting.errorSourceLabel")} name="errorSource" rules={[{ required: true, message: t("systemSetting.pleaseSelectErrorSource") }]}>
<Select placeholder={t('common.pleaseSelect')}>
{errorSourceOptions.map(option => (
<Option key={option.value} value={option.value}>{option.label}</Option>
))}
@@ -480,8 +476,8 @@ export default function ErrorManagement() {
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="错误等级" name="errorLevel" rules={[{ required: true, message: '请选择错误等级' }]}>
<Select placeholder="请选择">
<Form.Item label={t("systemSetting.errorLevelLabel")} name="errorLevel" rules={[{ required: true, message: t("systemSetting.pleaseSelectErrorLevel") }]}>
<Select placeholder={t('common.pleaseSelect')}>
{errorLevelOptions.map(option => (
<Option key={option.value} value={option.value}>{option.label}</Option>
))}
@@ -489,8 +485,8 @@ export default function ErrorManagement() {
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="校验字段" name="field" rules={[{ required: true, message: '请填写校验字段名' }]}>
<Input placeholder="如:battery、speed、temperature" />
<Form.Item label={t("systemSetting.checkField")} name="field" rules={[{ required: true, message: t("systemSetting.pleaseInputCheckField") }]}>
<Input placeholder={t("systemSetting.checkFieldPlaceholder")} />
</Form.Item>
</Col>
</Row>
@@ -498,8 +494,8 @@ export default function ErrorManagement() {
{/* 第三行:对比类型 + 对比值(自动切换单输入/区间双输入) */}
<Row gutter={16}>
<Col span={8}>
<Form.Item label="对比规则" name="compareType" rules={[{ required: true, message: '请选择对比规则' }]}>
<Select placeholder="请选择对比规则">
<Form.Item label={t("systemSetting.compareRule")} name="compareType" rules={[{ required: true, message: t("systemSetting.pleaseSelectCompareRule") }]}>
<Select placeholder={t("systemSetting.pleaseSelectCompareRule")}>
{CompareEnumOptions.map(item => (
<Option key={item.value} value={item.value}>{item.label} ({item.symbol})</Option>
))}
@@ -510,33 +506,33 @@ export default function ErrorManagement() {
{CompareEnumOptions.find(o => o.value === compareTypeWatch)?.range ? (
<>
<Col span={8}>
<Form.Item label="区间最小值" name="rangeMin" rules={[{ required: true, message: '请填写最小值' }]}>
<InputNumber style={{ width: '100%' }} placeholder="最小值" />
<Form.Item label={t("systemSetting.rangeMin")} name="rangeMin" rules={[{ required: true, message: t("systemSetting.pleaseInputMinValue") }]}>
<InputNumber style={{ width: '100%' }} placeholder={t("systemSetting.minValuePlaceholder")} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="区间最大值" name="rangeMax" rules={[{ required: true, message: '请填写最大值' }]}>
<InputNumber style={{ width: '100%' }} placeholder="最大值" />
<Form.Item label={t("systemSetting.rangeMax")} name="rangeMax" rules={[{ required: true, message: t("systemSetting.pleaseInputMaxValue") }]}>
<InputNumber style={{ width: '100%' }} placeholder={t("systemSetting.maxValuePlaceholder")} />
</Form.Item>
</Col>
</>
) : (
<Col span={16}>
<Form.Item label="对比值" name="compareValues" rules={[{ required: true, message: '请填写对比值' }]}>
<Input placeholder="数值/文本,多个用逗号分隔" />
<Form.Item label={t("systemSetting.compareValueLabel")} name="compareValues" rules={[{ required: true, message: t("systemSetting.pleaseInputCompareValue") }]}>
<Input placeholder={t("systemSetting.compareValuePlaceholder")} />
</Form.Item>
</Col>
)}
</Row>
{/* 错误描述 */}
<Form.Item label="错误描述 " name="errorDescription">
<TextArea rows={3} placeholder="详细描述触发该错误的场景" showCount maxLength={500} />
<Form.Item label={t("systemSetting.errorDesc")} name="errorDescription">
<TextArea rows={3} placeholder={t("systemSetting.errorDescPlaceholder")} showCount maxLength={500} />
</Form.Item>
{/* 处理建议 */}
<Form.Item label="处理建议" name="suggestion">
<TextArea rows={3} placeholder="出现该错误后的处理方案" showCount maxLength={500} />
<Form.Item label={t('aiAnalysis.suggestion')} name="suggestion">
<TextArea rows={3} placeholder={t("systemSetting.suggestionPlaceholder")} showCount maxLength={500} />
</Form.Item>
</Form>
</Modal>

View File

@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
import flvjs from 'flv.js';
import { Spin, Alert, Button, Space } from 'antd';
import { DownloadOutlined, ReloadOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
interface FlvPlayerProps {
url: string;
@@ -200,7 +201,7 @@ const FlvPlayer: React.FC<FlvPlayerProps> = ({ url, fileName }) => {
<div>
{loading && (
<div style={{ textAlign: 'center', padding: 40 }}>
<Spin tip="视频加载中..." />
<Spin tip={t('video.videoLoading')} />
</div>
)}
{mediaReady && (

View File

@@ -7,6 +7,7 @@ import {
InfoCircleOutlined, WarningOutlined, ExclamationCircleOutlined, CheckCircleOutlined
} from '@ant-design/icons';
import dayjs from 'dayjs';
import { useTranslation } from 'react-i18next';
const { Title, Text } = Typography;
const { RangePicker } = DatePicker;
@@ -18,29 +19,24 @@ const { RangePicker } = DatePicker;
// 状态服务历史 GET /api/status/:deviceId/history,或后端 /system/operlog 操作日志接口。
// ======================
const MOCK_LOGS = [
{ id: 1, time: '2026-08-10 13:02:11', level: 'info', source: '系统', operator: '系统', message: '机器人智慧服务系统 Alpha 三服务(接入/状态/命令)启动完成' },
{ id: 2, time: '2026-08-10 12:58:40', level: 'action', source: '命令服务', operator: '张运维', message: '向 ROBOT-001 下发移动命令(seq=d6b5d545),执行结果:成功' },
{ id: 3, time: '2026-08-10 12:51:22', level: 'action', source: '用户管理', operator: '张运维', message: '新增运维工程师账号「李工」,角色:运维工程师' },
{ id: 4, time: '2026-08-10 12:40:05', level: 'warning', source: '设备接入', operator: '系统', message: 'DRONE-001 心跳超时(>60s),已自动置为离线' },
{ id: 5, time: '2026-08-10 12:33:18', level: 'error', source: '命令服务', operator: '系统', message: 'ROBOT-002 执行「自主巡检」超时,已自动重试 1 次后仍失败' },
{ id: 6, time: '2026-08-10 12:20:09', level: 'action', source: '系统设置', operator: '张运维', message: '修改系统名称与数据刷新频率(5min → 1min)' },
{ id: 7, time: '2026-08-10 11:55:44', level: 'info', source: '状态服务', operator: '系统', message: '设备 ROBOT-001 上报状态:电量 85%,模式 patrol' },
{ id: 8, time: '2026-08-10 11:30:02', level: 'warning', source: '告警中心', operator: '系统', message: '逆变器 INV-03 输出功率较昨日下降 16%,触发预警' },
{ id: 9, time: '2026-08-10 10:48:37', level: 'action', source: '视频管理', operator: '王班长', message: '调整 CAM-04 热成像通道码率参数' },
{ id: 10, time: '2026-08-10 10:12:15', level: 'info', source: '系统', operator: '系统', message: '每日自动备份完成,备份文件:backup-20260810-0200.tar' },
{ id: 11, time: '2026-08-10 09:40:50', level: 'error', source: '设备接入', operator: '系统', message: '未知设备尝试注册(secret 校验失败),已拒绝,来源 IP:10.0.12.45' },
{ id: 12, time: '2026-08-10 09:05:23', level: 'action', source: '角色管理', operator: '张运维', message: '为「数据分析师」角色授权「报表中心-导出」权限' },
];
const LEVEL_CONFIG: Record<string, { color: string; label: string; icon: React.ReactNode }> = {
info: { color: 'blue', label: '信息', icon: <InfoCircleOutlined /> },
action: { color: 'green', label: '操作', icon: <CheckCircleOutlined /> },
warning: { color: 'orange', label: '警告', icon: <WarningOutlined /> },
error: { color: 'red', label: '错误', icon: <ExclamationCircleOutlined /> },
};
const LogManagement = () => {
const { t } = useTranslation();
const MOCK_LOGS = [
{ id: 1, time: '2026-08-10 13:02:11', level: 'info', source: '系统', operator: '系统', message: '机器人智慧服务系统 Alpha 三服务(接入/状态/命令)启动完成' },
{ id: 2, time: '2026-08-10 12:58:40', level: 'action', source: '命令服务', operator: '张运维', message: '向 ROBOT-001 下发移动命令(seq=d6b5d545),执行结果:成功' },
{ id: 3, time: '2026-08-10 12:51:22', level: 'action', source: t('router.users'), operator: '张运维', message: '新增运维工程师账号「李工」,角色:运维工程师' },
{ id: 4, time: '2026-08-10 12:40:05', level: 'warning', source: t('systemSetting.deviceAccess'), operator: '系统', message: 'DRONE-001 心跳超时(>60s),已自动置为离线' },
{ id: 5, time: '2026-08-10 12:33:18', level: 'error', source: '命令服务', operator: '系统', message: 'ROBOT-002 执行「自主巡检」超时,已自动重试 1 次后仍失败' },
{ id: 6, time: '2026-08-10 12:20:09', level: 'action', source: t('router.systemSetting'), operator: '张运维', message: '修改系统名称与数据刷新频率(5min → 1min)' },
{ id: 7, time: '2026-08-10 11:55:44', level: 'info', source: '状态服务', operator: '系统', message: '设备 ROBOT-001 上报状态:电量 85%,模式 patrol' },
{ id: 8, time: '2026-08-10 11:30:02', level: 'warning', source: t('router.alerts'), operator: '系统', message: '逆变器 INV-03 输出功率较昨日下降 16%,触发预警' },
{ id: 9, time: '2026-08-10 10:48:37', level: 'action', source: t('router.videoManage'), operator: '王班长', message: '调整 CAM-04 热成像通道码率参数' },
{ id: 10, time: '2026-08-10 10:12:15', level: 'info', source: '系统', operator: '系统', message: '每日自动备份完成,备份文件:backup-20260810-0200.tar' },
{ id: 11, time: '2026-08-10 09:40:50', level: 'error', source: t('systemSetting.deviceAccess'), operator: '系统', message: '未知设备尝试注册(secret 校验失败),已拒绝,来源 IP:10.0.12.45' },
{ id: 12, time: '2026-08-10 09:05:23', level: 'action', source: t('router.roles'), operator: '张运维', message: '为「数据分析师」角色授权「报表中心-导出」权限' },
];
const [data, setData] = useState(MOCK_LOGS);
const [levelFilter, setLevelFilter] = useState<string>('all');
const [keyword, setKeyword] = useState('');
@@ -58,6 +54,7 @@ const LogManagement = () => {
});
}, [data, levelFilter, keyword, dateRange]);
const handleRefresh = () => {
setData(MOCK_LOGS);
setLevelFilter('all');
@@ -65,6 +62,13 @@ const LogManagement = () => {
setDateRange(null);
message.success('日志已刷新');
};
const LEVEL_CONFIG: Record<string, { color: string; label: string; icon: React.ReactNode }> = {
info: { color: 'blue', label: t('constants.infoLevel'), icon: <InfoCircleOutlined /> },
action: { color: 'green', label: t('roles.actions'), icon: <CheckCircleOutlined /> },
warning: { color: 'orange', label: t('constants.warning'), icon: <WarningOutlined /> },
error: { color: 'red', label: t('constants.error'), icon: <ExclamationCircleOutlined /> },
};
const handleExport = () => {
const header = '时间,级别,来源,操作人,消息\n';
@@ -80,7 +84,7 @@ const LogManagement = () => {
};
const columns = [
{ title: '时间', dataIndex: 'time', key: 'time', width: 170, fixed: 'left' as const },
{ title: t('common.time'), dataIndex: 'time', key: 'time', width: 170, fixed: 'left' as const },
{
title: '级别', dataIndex: 'level', key: 'level', width: 80,
render: (lv: string) => {
@@ -88,15 +92,15 @@ const LogManagement = () => {
return <Tag color={c.color} icon={c.icon} style={{ margin: 0 }}>{c.label}</Tag>;
},
},
{ title: '来源', dataIndex: 'source', key: 'source', width: 120 },
{ title: '操作人', dataIndex: 'operator', key: 'operator', width: 100 },
{ title: t('workOrder.source'), dataIndex: 'source', key: 'source', width: 120 },
{ title: t('systemSetting.operator'), dataIndex: 'operator', key: 'operator', width: 100 },
{ title: '消息', dataIndex: 'message', key: 'message', ellipsis: true },
];
return (
<div style={{ padding: '8px 0px', background: '#f5f7fa', minHeight: '100vh' }}>
<div style={{ marginBottom: 8 }}>
<Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.1vw, 18px)' }}>日志管理</Title>
<Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.1vw, 18px)' }}>{t('router.log')}</Title>
<Text type="secondary" style={{ fontSize: 12 }}>系统运行、设备控制与管理员操作流水,仅超级管理员可查看</Text>
</div>
@@ -109,10 +113,10 @@ const LogManagement = () => {
onChange={setLevelFilter}
options={[
{ value: 'all', label: '全部级别' },
{ value: 'info', label: '信息' },
{ value: 'action', label: '操作' },
{ value: 'warning', label: '警告' },
{ value: 'error', label: '错误' },
{ value: 'info', label: t('constants.infoLevel') },
{ value: 'action', label: t('roles.actions') },
{ value: 'warning', label: t('constants.warning') },
{ value: 'error', label: t('constants.error') },
]}
/>
</Col>
@@ -131,8 +135,8 @@ const LogManagement = () => {
</Col>
<Col xs={24} sm={10} md={3}>
<Space>
<Button size="small" icon={<ReloadOutlined />} onClick={handleRefresh}>刷新</Button>
<Button size="small" type="primary" icon={<DownloadOutlined />} onClick={handleExport}>导出</Button>
<Button size="small" icon={<ReloadOutlined />} onClick={handleRefresh}>{t('common.refresh')}</Button>
<Button size="small" type="primary" icon={<DownloadOutlined />} onClick={handleExport}>{t('common.export')}</Button>
</Space>
</Col>
</Row>

View File

@@ -11,6 +11,7 @@ import {
} from '@ant-design/icons';
import { addMenu, deleteMenu, getMenuList, updateMenu } from '../../api/mune';
import { buildPermTree, buildTreeFromFlat } from '../../lib/utils';
import { useTranslation } from 'react-i18next';
const { Option } = Select;
@@ -23,6 +24,7 @@ const iconList = Object.keys(AllIcons).filter(item => {
const rolePermissionMap = {};
export default function MenuList() {
const { t } = useTranslation();
const [rawMenuList, setRawMenuList] = useState([]);
const [menuTreeData, setMenuTreeData] = useState([]);
const [menuTree, setMenuTree] = useState([]);
@@ -107,26 +109,26 @@ export default function MenuList() {
};
updateMenu(data).then(res => {
if (res.code === 200) {
messageApi.success("编辑成功");
messageApi.success(t('common.editSuccess'));
setMenuModal(false);
getMenuListApi();
} else {
messageApi.error(res.msg);
}
}).catch(() => {
messageApi.error('编辑失败');
messageApi.error(t('common.editFail'));
});
} else {
addMenu(values).then(res => {
if (res.code === 200) {
messageApi.success("新增成功");
messageApi.success(t('common.addSuccess'));
setMenuModal(false);
getMenuListApi();
} else {
messageApi.error(res.msg);
}
}).catch(() => {
messageApi.error('新增失败');
messageApi.error(t('common.addFail'));
});
}
});
@@ -136,13 +138,13 @@ export default function MenuList() {
const handleDelete = (menuId) => {
deleteMenu(menuId).then(res => {
if (res.code == 200) {
messageApi.success('删除成功');
messageApi.success(t('common.deleteSuccess'));
getMenuListApi();
} else {
messageApi.error(res.msg);
}
}).catch(() => {
messageApi.error("删除失败");
messageApi.error(t('common.deleteFail'));
});
};
@@ -152,14 +154,14 @@ export default function MenuList() {
const columns = [
{
title: '菜单名称',
title: t('systemSetting.menuName'),
dataIndex: 'menuName',
key: 'menuName',
},
{ title: '路由地址', dataIndex: 'path', key: 'path' },
{ title: '排序', dataIndex: 'orderNum', key: 'orderNum' },
{ title: t('systemSetting.menuPath'), dataIndex: 'path', key: 'path' },
{ title: t('roles.roleSort'), dataIndex: 'orderNum', key: 'orderNum' },
{
title: '操作',
title: t('roles.actions'),
key: 'action',
width: 320,
render: (_, record) => (
@@ -169,9 +171,9 @@ export default function MenuList() {
新增子菜单
</Button>
)}
<Button type="text" icon={<EditOutlined />} onClick={() => handleEdit(record)}>编辑</Button>
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(record.menuId)}>
<Button type="text" danger>删除</Button>
<Button type="text" icon={<EditOutlined />} onClick={() => handleEdit(record)}>{t('roles.edit')}</Button>
<Popconfirm title={t('common.confirmDelete')} onConfirm={() => handleDelete(record.menuId)}>
<Button type="text" danger>roles.delete</Button>
</Popconfirm>
</Space>
),
@@ -188,7 +190,7 @@ export default function MenuList() {
<div style={{ padding: '8px 0px' }}>
{contextHolder}
<div style={{ padding: '0px 0px 8px' }}>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>菜单管理</Typography.Title>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>{t('router.menuList')}</Typography.Title>
</div>
<Card style={{ borderRadius: 8 }}>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
@@ -220,7 +222,7 @@ export default function MenuList() {
destroyOnClose
>
<Form form={menuForm} layout="vertical">
<Form.Item label="上级菜单" name="parentId">
<Form.Item label={t('systemSetting.parentMenu')} name="parentId">
<Select placeholder="无(一级菜单)">
<Option value={0}>无(一级菜单)</Option>
{topMenus.map(m => (
@@ -229,11 +231,11 @@ export default function MenuList() {
</Select>
</Form.Item>
<Form.Item label="菜单名称" name="menuName" rules={[{ required: true }]}>
<Form.Item label={t('systemSetting.menuName')} name="menuName" rules={[{ required: true }]}>
<Input placeholder="请输入菜单名称" />
</Form.Item>
<Form.Item label="路由地址" name="path">
<Form.Item label={t('systemSetting.menuPath')} name="path">
<Input placeholder="/system/menu" />
</Form.Item>

View File

@@ -4,6 +4,7 @@ import {
Typography
} from 'antd';
import { SearchOutlined, ReloadOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
const { Search } = Input;
const { TabPane } = Tabs;
@@ -35,6 +36,7 @@ const mockOnlineUsers = [
];
const OnlineUserPage = () => {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState('onlineUser'); // 默认选中在线用户
const [loading, setLoading] = useState(false);
const [dataSource, setDataSource] = useState(mockOnlineUsers);
@@ -77,7 +79,7 @@ const OnlineUserPage = () => {
{ title: '浏览器', dataIndex: 'browser' },
{ title: '登录时间', dataIndex: 'loginTime' },
{
title: '操作', width: 100,
title: t('roles.actions'), width: 100,
render: (_, record) => (
<Button type="link" onClick={() => handleForceLogout(record)} style={{ color: '#1890ff' }}>
强退
@@ -97,7 +99,7 @@ const OnlineUserPage = () => {
onChange={setActiveTab}
style={{ marginBottom: 0 }}
>
<TabPane tab="在线用户" key="onlineUser" />
<TabPane tab={t('systemSetting.onlineUsers')} key="onlineUser" />
<TabPane tab="其他监控" key="other" />
</Tabs>
@@ -116,7 +118,7 @@ const OnlineUserPage = () => {
/>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span>用户名称</span>
<span>profile.username</span>
<Input
placeholder="请输入用户名称"
value={searchParams.userName}

View File

@@ -7,12 +7,14 @@ import {
PlusOutlined, EditOutlined, DeleteOutlined
} from '@ant-design/icons';
import { addOrg, deleteOrg, getOrgList } from '../../api/organization';
import { useTranslation } from 'react-i18next';
const { Title } = Typography;
const { Option } = Select;
const AntdCard = Card as any;
export default function Organization() {
const { t } = useTranslation();
const [orgList, setOrgList] = useState([]);
const [modalVisible, setModalVisible] = useState(false);
const [form] = Form.useForm();
@@ -78,13 +80,13 @@ export default function Organization() {
const handleDelete = (id) => {
deleteOrg({ id }).then(res => {
if (res.code == 200) {
messageApi.success('删除成功');
messageApi.success(t('common.deleteSuccess'));
getOrgData();
} else {
messageApi.error(res.msg || '删除失败');
messageApi.error(res.msg || t('common.deleteFail'));
}
}).catch(() => {
messageApi.error('删除失败');
messageApi.error(t('common.deleteFail'));
});
};
@@ -98,21 +100,21 @@ export default function Organization() {
};
addOrg(params).then(res => {
if (res.code === 200) {
messageApi.success('编辑成功');
messageApi.success(t('common.editSuccess'));
getOrgData();
setModalVisible(false);
} else {
messageApi.error(res.msg || '编辑失败');
messageApi.error(res.msg || t('common.editFail'));
}
});
} else {
addOrg(values).then(res => {
if (res.code === 200) {
messageApi.success('新增成功');
messageApi.success(t('common.addSuccess'));
getOrgData();
setModalVisible(false);
} else {
messageApi.error(res.msg || '新增失败');
messageApi.error(res.msg || t('common.addFail'));
}
});
}
@@ -120,27 +122,27 @@ export default function Organization() {
};
const columns = [
{ title: '组织名称', dataIndex: 'orgName' },
{ title: t('systemSetting.orgName'), dataIndex: 'orgName' },
//{ title: '组织编码', dataIndex: 'orgCode' },
{ title: '负责人', dataIndex: 'manager' },
{ title: '联系电话', dataIndex: 'phone' },
{ title: '邮箱', dataIndex: 'email' },
{ title: t('systemSetting.leader'), dataIndex: 'manager' },
{ title: t('systemSetting.contact'), dataIndex: 'phone' },
{ title: t('users.email'), dataIndex: 'email' },
//{
// title: '状态',
// title: t('roles.status'),
// dataIndex: 'status',
// render: (s) => (
// <span className={s == 1 ? 'text-green-600' : 'text-red-600'}>
// {s == 1 ? '正常' : '禁用'}
// {s == 1 ? t('roles.normal') : '禁用'}
// </span>
// )
//},
{
title: '操作',
title: t('roles.actions'),
render: (_, r) => (
<Space>
<Button type="text" icon={<EditOutlined />} onClick={() => handleEdit(r)}>编辑</Button>
<Button type="text" icon={<EditOutlined />} onClick={() => handleEdit(r)}>{t('roles.edit')}</Button>
<Popconfirm title="确定删除该组织?" onConfirm={() => handleDelete(r.id)}>
<Button type="text" danger icon={<DeleteOutlined />}>删除</Button>
<Button type="text" danger icon={<DeleteOutlined />}>{t('roles.delete')}</Button>
</Popconfirm>
</Space>
)
@@ -151,7 +153,7 @@ export default function Organization() {
<div style={{ padding: '8px 0px' }}>
{contextHolder}
<div style={{ padding: '0px 0px 8px' }}>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>组织管理</Typography.Title>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>{t('router.organization')}</Typography.Title>
</div>
<AntdCard style={{ borderRadius: 8 }}>
@@ -188,7 +190,7 @@ export default function Organization() {
destroyOnClose
>
<Form form={form} layout="vertical">
<Form.Item name="orgName" label="组织名称" rules={[{ required: true }]}>
<Form.Item name="orgName" label={t('systemSetting.orgName')} rules={[{ required: true }]}>
<Input placeholder="请输入组织名称" />
</Form.Item>
@@ -196,15 +198,15 @@ export default function Organization() {
<Input placeholder="请输入组织编码" />
</Form.Item>*/}
<Form.Item name="manager" label="负责人" rules={[{ required: true, message: '请输入负责人' }]}>
<Form.Item name="manager" label={t('systemSetting.leader')} rules={[{ required: true, message: '请输入负责人' }]}>
<Input placeholder="请输入负责人" />
</Form.Item>
<Form.Item name="phone" label="联系电话" rules={[{ required: true, message: '请输入联系电话' }]}>
<Form.Item name="phone" label={t('systemSetting.contact')} rules={[{ required: true, message: '请输入联系电话' }]}>
<Input placeholder="请输入联系电话" />
</Form.Item>
<Form.Item name="email" label="邮箱">
<Form.Item name="email" label={t('users.email')}>
<Input placeholder="请输入邮箱" />
</Form.Item>
@@ -212,13 +214,13 @@ export default function Organization() {
<Input placeholder="请输入地址" />
</Form.Item>
<Form.Item name="remark" label="备注">
<Form.Item name="remark" label={t('roles.remark')}>
<Input.TextArea rows={3} placeholder="请输入备注" />
</Form.Item>
<Form.Item name="status" label="状态">
<Form.Item name="status" label={t('roles.status')}>
<Select>
<Option value={1}>正常</Option>
<Option value={1}>{t('roles.normal')}</Option>
<Option value={0}>禁用</Option>
</Select>
</Form.Item>

View File

@@ -7,6 +7,7 @@ import QRCode from 'qrcode';
import DictManager from "../DictManager";
import request from '../../api/request';
import { Country, State } from 'country-state-city';
import { useTranslation } from 'react-i18next';
import {
EyeOutlined,
EditOutlined,
@@ -39,6 +40,7 @@ const { Text } = Typography;
const { Option } = Select;
const SerialNumGenerate = () => {
const { t } = useTranslation();
const [messageApi, contextHolder] = message.useMessage();
const [form] = Form.useForm();
const [searchForm] = Form.useForm();
@@ -135,10 +137,10 @@ const SerialNumGenerate = () => {
const handleDelete = async (id) => {
try {
await delteCodesApi(id);
messageApi.success('删除成功');
messageApi.success(t('common.deleteSuccess'));
loadCodeList();
} catch (e) {
messageApi.error('删除失败');
messageApi.error(t('common.deleteFail'));
}
};
@@ -258,7 +260,7 @@ const SerialNumGenerate = () => {
const res = await saveCodeApi(data);
if (res?.code === 200) {
messageApi.success('保存成功');
messageApi.success(t('common.saveSuccess'));
loadCodeList();
setAddModalVisible(false);
form.resetFields();
@@ -269,7 +271,7 @@ const SerialNumGenerate = () => {
messageApi.error('保存失败:' + res?.msg);
}
} catch (e) {
messageApi.error('保存失败');
messageApi.error(t('common.saveFail'));
} finally {
setLoading(false);
}
@@ -349,7 +351,7 @@ const SerialNumGenerate = () => {
}
},
{
title: '类型',
title: t('common.type'),
key: 'type',
render: (_, r) => {
const qrInfo = parseQrJson(r.qrJson);
@@ -374,15 +376,15 @@ const SerialNumGenerate = () => {
render: (_, r) => r.qrCodeBase64 && <img src={r.qrCodeBase64} style={{ width: 50, height: 50 }} alt="qrcode" />
},
{
title: '操作',
title: t('roles.actions'),
key: 'opt',
width: 180,
fixed: "right",
render: (_, r) => (
<Space>
<Button type="link" icon={<EyeOutlined />} onClick={() => handleView(r)}>查看</Button>
<Button type="link" icon={<EyeOutlined />} onClick={() => handleView(r)}>{t('common.view')}</Button>
<Popconfirm
title="确定删除?"
title={t('common.confirmDelete')}
onConfirm={() => handleDelete(r.id)}
>
<Button type="link" danger icon={<DeleteOutlined />} >
@@ -409,10 +411,10 @@ const SerialNumGenerate = () => {
title="机器人二维码管理"
extra={
<Space wrap>
<Button type="primary" onClick={() => setAddModalVisible(true)}>新增</Button>
<Button type="primary" onClick={() => setAddModalVisible(true)}>{t('common.addNew')}</Button>
<Button onClick={() => setManageVisible(true)}>数据管理</Button>
<Popconfirm title="确定删除选中项?" onConfirm={handleBatchDelete}>
<Button danger disabled={selectedRowKeys.length === 0}>批量删除</Button>
<Button danger disabled={selectedRowKeys.length === 0}>{t('common.batchDelete')}</Button>
</Popconfirm>
</Space>
}
@@ -425,8 +427,8 @@ const SerialNumGenerate = () => {
</Form.Item>
<Form.Item style={{ margin: 0 }}>
<Space wrap>
<Button type="primary" onClick={handleQueryQRCode}>搜索</Button>
<Button onClick={() => { searchForm.resetFields(); loadCodeList(); }}>重置</Button>
<Button type="primary" onClick={handleQueryQRCode}>{t('common.search')}</Button>
<Button onClick={() => { searchForm.resetFields(); loadCodeList(); }}>{t('common.reset')}</Button>
</Space>
</Form.Item>
</Form>
@@ -474,7 +476,7 @@ const SerialNumGenerate = () => {
{configs.map(c => <Option key={c.id} value={c.storeValue}>{c.displayName}</Option>)}
</Select>
</Form.Item>
<Form.Item name="type" label="类型" rules={[{ required: true }]}>
<Form.Item name="type" label={t('common.type')} rules={[{ required: true }]}>
<Select placeholder="选择类型" showSearch>
{types.map(t => <Option key={t.id} value={t.storeValue}>{t.displayName}</Option>)}
</Select>
@@ -508,8 +510,8 @@ const SerialNumGenerate = () => {
<div style={{ color: '#999', padding: '60px 0' }}>请先生成二维码</div>
)}
<Space style={{ marginTop: 24 }}>
<Button onClick={downloadQRCode}>下载</Button>
<Button type="primary" loading={loading} onClick={saveToDatabase}>保存</Button>
<Button onClick={downloadQRCode}>{t('common.download')}</Button>
<Button type="primary" loading={loading} onClick={saveToDatabase}>{t('common.save')}</Button>
</Space>
</div>
</Col>
@@ -527,8 +529,8 @@ const SerialNumGenerate = () => {
</Modal>
{/* 数据管理 */}
<Modal open={manageVisible} title="数据管理" width="90%" style={{ maxWidth: 900 }} onCancel={() => setManageVisible(false)} footer={<Button onClick={() => setManageVisible(false)}>关闭</Button>} destroyOnClose>
<DictManager title="机器人类型" data={types} fields={[{ name: 'displayName', label: '类型名称', required: true }, { name: 'storeValue', label: '存储值', required: true }, { name: 'remark', label: '备注' }]} onAdd={handleAddType} onUpdate={handleUpdateType} onDelete={deleteRobotTypeApi} refresh={loadTypeList} />
<Modal open={manageVisible} title="数据管理" width="90%" style={{ maxWidth: 900 }} onCancel={() => setManageVisible(false)} footer={<Button onClick={() => setManageVisible(false)}>{t('common.close')}</Button>} destroyOnClose>
<DictManager title="机器人类型" data={types} fields={[{ name: 'displayName', label: '类型名称', required: true }, { name: 'storeValue', label: '存储值', required: true }, { name: 'remark', label: t('roles.remark') }]} onAdd={handleAddType} onUpdate={handleUpdateType} onDelete={deleteRobotTypeApi} refresh={loadTypeList} />
<DictManager title="机器人型号" data={models} fields={[{ name: 'displayName', label: '型号名称', required: true }, { name: 'storeValue', label: '存储值', required: true }]} onAdd={handleAddModel} onUpdate={handleUpdateModel} onDelete={deleteRobotModelApi} refresh={loadModelList} />
<DictManager title="配置" data={configs} fields={[{ name: 'displayName', label: '配置名称', required: true }, { name: 'storeValue', label: '存储值', required: true }]} onAdd={handleAddConfig} onUpdate={handleUpdateConfig} onDelete={deleteRobotConfigApi} refresh={loadConfigList} />
</Modal>

View File

@@ -37,6 +37,7 @@ import { useSelector } from 'react-redux';
import { RootState } from '@/src/store';
import utils from '@/lib/utils';
import DeviceParamModal from '../devices/DeviceParamModal';
import { useTranslation } from 'react-i18next';
interface SmartDeviceManagementProps {
allOrgOptions: any[];
@@ -51,6 +52,7 @@ export default function SmartDeviceManagement({
allUserOptions,
onAllDataNeedRefresh,
}: SmartDeviceManagementProps) {
const { t } = useTranslation();
const { modal } = App.useApp();
const { userInfo } = useSelector((state: RootState) => state.user);
const { stationId, currentStation } = useSelector((state: RootState) => state.station);
@@ -291,8 +293,8 @@ export default function SmartDeviceManagement({
modal.confirm({
title: '确认解绑',
content: `确定要为选中的智能装备清除${typeText}绑定吗?`,
okText: '确定',
cancelText: '取消',
okText: t('common.ok'),
cancelText: t('common.cancel'),
onOk: async () => {
try {
const res = await unbindSmartDevice({ deviceIds: validIds, type });
@@ -312,7 +314,7 @@ export default function SmartDeviceManagement({
};
const smartColumns = [
{ title: '设备名称', width: 150, dataIndex: 'deviceAlias', key: 'name' },
{ title: t('devices.deviceName'), width: 150, dataIndex: 'deviceAlias', key: 'name' },
{ title: '设备码', width: 280, dataIndex: 'serialNumber', key: 'sn' },
{
title: '所属组织', width: 150, key: 'orgName',
@@ -329,15 +331,15 @@ export default function SmartDeviceManagement({
},
},
{
title: '负责人', width: 150, key: 'userName',
title: t('systemSetting.leader'), width: 150, key: 'userName',
render: (_, record: any) => {
const user = allUserOptions.find(u => u.userId == record.userId);
return user ? (user.nickName || user.userName) : <Text type="secondary">未绑定</Text>;
},
},
{ title: '在线状态', width: 120, dataIndex: 'onlineStatus', key: 'online', render: s => <Tag color={s === 'ONLINE' || s === 1 ? 'green' : 'red'}>{s === 'ONLINE' || s === 1 ? '在线' : '离线'}</Tag> },
{ title: t('devices.onlineStatus'), width: 120, dataIndex: 'onlineStatus', key: 'online', render: s => <Tag color={s === 'ONLINE' || s === 1 ? 'green' : 'red'}>{s === 'ONLINE' || s === 1 ? t('devices.online') : t('devices.offline')}</Tag> },
{
title: '操作', width: 280, key: 'action', fixed: 'right' as const,
title: t('roles.actions'), width: 280, key: 'action', fixed: 'right' as const,
render: (_, record: any) => (
<Space>
<Tooltip title="清除人员绑定">
@@ -382,7 +384,7 @@ export default function SmartDeviceManagement({
allowClear
onChange={(e) => setSmartParams({ ...smartParams, serialNumber: e.target.value.trim() })}
/>
<Button icon={<ReloadOutlined />} onClick={() => setSmartParams({ pageNum: 1, pageSize: 10, serialNumber: '' })}>重置</Button>
<Button icon={<ReloadOutlined />} onClick={() => setSmartParams({ pageNum: 1, pageSize: 10, serialNumber: '' })}>{t('common.reset')}</Button>
<Button type="primary" icon={<LinkOutlined />} onClick={openBindSmartModal}>绑定装备</Button>
<Button disabled={selectedSmartKeys.length === 0} onClick={() => handleUnbindSmart(selectedSmartKeys, 1)}>批量解绑(用户)</Button>
<Button disabled={selectedSmartKeys.length === 0} onClick={() => handleUnbindSmart(selectedSmartKeys, 2)}>批量解绑(场站)</Button>
@@ -482,7 +484,7 @@ export default function SmartDeviceManagement({
))}
</Select>
</Form.Item>
<Form.Item label="负责人" name="userId" rules={[{ required: true, message: '请选择负责人' }]}>
<Form.Item label={t('systemSetting.leader')} name="userId" rules={[{ required: true, message: '请选择负责人' }]}>
<Select
placeholder="请先选择场站后选择负责人"
allowClear
@@ -512,16 +514,16 @@ export default function SmartDeviceManagement({
width={480}
>
<Form form={editSmartForm} layout="vertical" onFinish={saveEditSmart}>
<Form.Item label="设备名称" name="deviceAlias" rules={[{ required: true, message: '请填写设备名称' }]}>
<Form.Item label={t('devices.deviceName')} name="deviceAlias" rules={[{ required: true, message: '请填写设备名称' }]}>
<Input placeholder="请输入设备名称" />
</Form.Item>
<Form.Item label="设备序列号" name="serialNumber" rules={[{ required: true, message: '请填写设备码' }]}>
<Input placeholder="请输入设备序列号" />
</Form.Item>
<Form.Item label="在线状态" name="onlineStatus" rules={[{ required: true, message: '请选择在线状态' }]}>
<Form.Item label={t('devices.onlineStatus')} name="onlineStatus" rules={[{ required: true, message: '请选择在线状态' }]}>
<Select placeholder="选择在线状态">
<Select.Option value="ONLINE">在线</Select.Option>
<Select.Option value="OFFLINE">离线</Select.Option>
<Select.Option value="ONLINE">{t('devices.online')}</Select.Option>
<Select.Option value="OFFLINE">{t('devices.offline')}</Select.Option>
</Select>
</Form.Item>
</Form>
@@ -586,7 +588,7 @@ export default function SmartDeviceManagement({
</Form.Item>
<Form.Item style={{ marginTop: 20, marginBottom: 8 }}>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10 }}>
<Button onClick={() => setParamModalVisible1(false)} style={{ borderRadius: 8, padding: '4px 20px' }}>取消</Button>
<Button onClick={() => setParamModalVisible1(false)} style={{ borderRadius: 8, padding: '4px 20px' }}>{t('common.cancel')}</Button>
<Button type="primary" htmlType="submit" style={{ borderRadius: 8, padding: '4px 24px' }}>保存授权</Button>
</div>
</Form.Item>

View File

@@ -14,10 +14,12 @@ import {
import { getOrgList } from '../../api/organization';
import { useSelector } from 'react-redux';
import { RootState } from '../../store';
import { useTranslation } from 'react-i18next';
const { Option } = Select;
export default function StationManage() {
const { t } = useTranslation();
const [stationList, setStationList] = useState([]);
const [currentStation, setCurrentStation] = useState(null);
const [regionList, setRegionList] = useState([]);
@@ -171,11 +173,11 @@ export default function StationManage() {
}
saveSite(params).then(res => {
if (res.code === 200) {
messageApi.success(editRecord ? '编辑成功' : '新增成功');
messageApi.success(editRecord ? t('common.editSuccess') : t('common.addSuccess'));
getStationData();
setModalVisible(false);
} else {
messageApi.error(res.msg || '保存失败');
messageApi.error(res.msg || t('common.saveFail'));
}
});
});
@@ -185,10 +187,10 @@ export default function StationManage() {
const handleDelete = (id) => {
deleteSite([id]).then(res => {
if (res.code === 200) {
messageApi.success('删除成功');
messageApi.success(t('common.deleteSuccess'));
getStationData();
} else {
messageApi.error(res.msg || '删除失败');
messageApi.error(res.msg || t('common.deleteFail'));
}
});
};
@@ -215,7 +217,7 @@ export default function StationManage() {
saveRegion(params).then(res => {
if (res.code === 200) {
messageApi.success('保存成功');
messageApi.success(t('common.saveSuccess'));
getStationRegions({ id: currentStation.id }).then(r => {
setRegionList(r.data || []);
});
@@ -229,7 +231,7 @@ export default function StationManage() {
const handleDeleteRegion = (id) => {
deleteRegion([id]).then(res => {
if (res.code === 200) {
messageApi.success('删除成功');
messageApi.success(t('common.deleteSuccess'));
setRegionList(regionList.filter(x => x.id !== id));
}
});
@@ -243,7 +245,7 @@ export default function StationManage() {
// ====================== 表格列 ======================
const columns = [
{ title: '场站名称', dataIndex: 'siteName' },
{ title: t('systemSetting.siteName'), dataIndex: 'siteName' },
{
title: '所属组织',
dataIndex: 'orgId',
@@ -254,15 +256,15 @@ export default function StationManage() {
},
{ title: '地址', dataIndex: 'address' },
{
title: '操作',
title: t('roles.actions'),
width: 400,
render: (_, record) => (
<Space wrap size="small">
<Button icon={<EditOutlined />} onClick={() => handleEdit(record)}>编辑</Button>
<Button icon={<EditOutlined />} onClick={() => handleEdit(record)}>{t('roles.edit')}</Button>
<Button icon={<BgColorsOutlined />} onClick={() => handleOpenArea(record)}>分区设置</Button>
<Button onClick={() => handleOpenMap(record)}>边界绘制</Button>
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(record.id)}>
<Button danger>删除</Button>
<Popconfirm title={t('common.confirmDelete')} onConfirm={() => handleDelete(record.id)}>
<Button danger>roles.delete</Button>
</Popconfirm>
</Space>
)
@@ -273,7 +275,7 @@ export default function StationManage() {
<div style={{ padding: '8px 0px' }}>
{contextHolder}
<div style={{ padding: '0px 0px 8px' }}>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>场站管理</Typography.Title>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>{t('router.station')}</Typography.Title>
</div>
@@ -293,7 +295,7 @@ export default function StationManage() {
</Option>
))}
</Select>
<Button onClick={handleReset}>重置</Button>
<Button onClick={handleReset}>{t('common.reset')}</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
新增场站
</Button>
@@ -329,8 +331,8 @@ export default function StationManage() {
<Form form={form} layout="vertical">
<Row gutter={16}>
<Col span={12}>
<Form.Item name="siteName" label="场站名称" rules={[{ required: true }]}>
<Input placeholder="请输入" />
<Form.Item name="siteName" label={t('systemSetting.siteName')} rules={[{ required: true }]}>
<Input placeholder={t('common.pleaseInput')} />
</Form.Item>
</Col>
<Col span={12}>
@@ -348,13 +350,13 @@ export default function StationManage() {
<Row gutter={16}>
<Col span={12}>
<Form.Item name="longitude" label="经度">
<Input placeholder="经度" />
<Form.Item name="longitude" label={t('systemSetting.longitude')}>
<Input placeholder={t('systemSetting.longitude')} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="latitude" label="纬度">
<Input placeholder="纬度" />
<Form.Item name="latitude" label={t('systemSetting.latitude')}>
<Input placeholder={t('systemSetting.latitude')} />
</Form.Item>
</Col>
</Row>
@@ -368,22 +370,22 @@ export default function StationManage() {
<Row gutter={16}>
<Col span={6}>
<Form.Item name="dingTalkPush" label="钉钉告警推送" valuePropName="checked">
<Switch checkedChildren="开启" unCheckedChildren="关闭" />
<Switch checkedChildren="开启" unCheckedChildren={t('common.close')} />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item name="dingTalkOrderPush" label="钉钉工单推送" valuePropName="checked">
<Switch checkedChildren="开启" unCheckedChildren="关闭" />
<Switch checkedChildren="开启" unCheckedChildren={t('common.close')} />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item name="emailPush" label="邮件推送" valuePropName="checked">
<Switch checkedChildren="开启" unCheckedChildren="关闭" />
<Switch checkedChildren="开启" unCheckedChildren={t('common.close')} />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item name="alertPush" label="告警弹窗推送" valuePropName="checked">
<Switch checkedChildren="开启" unCheckedChildren="关闭" />
<Switch checkedChildren="开启" unCheckedChildren={t('common.close')} />
</Form.Item>
</Col>
</Row>
@@ -416,12 +418,12 @@ export default function StationManage() {
</Row>
) : null}
<Form.Item name="remark" label="备注">
<Form.Item name="remark" label={t('roles.remark')}>
<Input.TextArea rows={3} />
</Form.Item>
<Form.Item name="status" label="场站状态" valuePropName="checked">
<Switch checkedChildren="启用" unCheckedChildren="停用" />
<Form.Item name="status" label={t('systemSetting.siteStatus')} valuePropName="checked">
<Switch checkedChildren={t('common.enable')} unCheckedChildren={t('roles.stopped')} />
</Form.Item>
</Form>
</Modal>
@@ -454,12 +456,12 @@ export default function StationManage() {
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="status" label="状态" valuePropName="checked">
<Switch checkedChildren="启用" unCheckedChildren="停用" />
<Form.Item name="status" label={t('roles.status')} valuePropName="checked">
<Switch checkedChildren={t('common.enable')} unCheckedChildren={t('roles.stopped')} />
</Form.Item>
</Col>
</Row>
<Form.Item name="remark" label="备注"><Input.TextArea rows={2} /></Form.Item>
<Form.Item name="remark" label={t('roles.remark')}><Input.TextArea rows={2} /></Form.Item>
<Button type="primary" onClick={handleSaveRegion}>保存分区</Button>
</Form>
@@ -471,15 +473,15 @@ export default function StationManage() {
columns={[
{ title: '分区名称', dataIndex: 'regionName' },
{ title: '编码', dataIndex: 'regionCode' },
{ title: '类型', dataIndex: 'type' },
{ title: t('common.type'), dataIndex: 'type' },
{
title: '状态', render: r => r.status === 1 ? '启用' : '停用'
title: t('roles.status'), render: r => r.status === 1 ? t('common.enable') : t('roles.stopped')
},
{
title: '操作', render: (_, r) => (
title: t('roles.actions'), render: (_, r) => (
<Space>
<Button onClick={() => { setEditRegion(r); regionForm.setFieldsValue(r); }}>编辑</Button>
<Button danger onClick={() => handleDeleteRegion(r.id)}>删除</Button>
<Button onClick={() => { setEditRegion(r); regionForm.setFieldsValue(r); }}>{t('roles.edit')}</Button>
<Button danger onClick={() => handleDeleteRegion(r.id)}>{t('roles.delete')}</Button>
</Space>
)
}

View File

@@ -5,10 +5,12 @@ import { ExclamationCircleOutlined, AlertOutlined, FormOutlined } from '@ant-des
import ErrorManagement from './ErrorManagement';
import AlarmSettingsManagement from './AlarmSettingsManagement';
import WorkOrderSettingsManagement from './WorkOrderSettingsManagement';
import { useTranslation } from 'react-i18next';
const { TabPane } = Tabs;
export const SystemMaintenancePage = () => {
const { t } = useTranslation();
const [searchParams, setSearchParams] = useSearchParams();
const [activeSubTab, setActiveSubTab] = useState<string>('error');
@@ -29,7 +31,7 @@ export const SystemMaintenancePage = () => {
return (
<div className="p-0 pt-[8px] bg-[#f5f7fa] min-h-screen">
<div style={{ padding: '0px 0px 8px' }}>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>系统维护</Typography.Title>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>{t('systemSetting.systemMaintenance')}</Typography.Title>
</div>
<Card className="mb-4">
<Tabs activeKey={activeSubTab} onChange={handleTabChange}>

View File

@@ -17,12 +17,14 @@ import { useSelector } from 'react-redux';
import { RootState } from '../../store';
import { getOrgList } from '../../api/organization';
import { getSiteByOrgId } from '../../api/stationManage';
import { useTranslation } from 'react-i18next';
const { Text } = Typography;
const { TabPane } = Tabs;
const AntdCard = Card as any;
export default function UserRolePage() {
const { t } = useTranslation();
// ====================== 全局提示 ======================
const [messageApi, contextHolder] = message.useMessage();
const { modal } = App.useApp();
@@ -206,9 +208,9 @@ export default function UserRolePage() {
const handleDeleteRole = (roleId: number) => {
deleteRole([roleId]).then(res => {
if (res.code == 200) {
messageApi.success('删除成功');
messageApi.success(t('common.deleteSuccess'));
getRoleData();
} else messageApi.error(res.msg || '删除失败');
} else messageApi.error(res.msg || t('common.deleteFail'));
});
};
@@ -218,7 +220,7 @@ export default function UserRolePage() {
title: '批量删除角色',
content: `确定要删除已选中的 ${selectedRoleIds.length} 个角色吗?`,
okText: '确定删除',
cancelText: '取消',
cancelText: t('common.cancel'),
okType: 'danger',
onOk: () => {
deleteRole(selectedRoleIds).then(res => {
@@ -226,7 +228,7 @@ export default function UserRolePage() {
messageApi.success('批量删除成功');
setSelectedRoleIds([]);
getRoleData();
} else messageApi.error(res.msg || '删除失败');
} else messageApi.error(res.msg || t('common.deleteFail'));
});
}
});
@@ -262,11 +264,11 @@ export default function UserRolePage() {
const api = editRole ? editRoleApi : addRole;
api(params).then(res => {
if (res.code == 200) {
messageApi.success(editRole ? '编辑成功' : '新增成功');
messageApi.success(editRole ? t('common.editSuccess') : t('common.addSuccess'));
setRoleModalVisible(false);
getRoleData();
getRoleForSelect();
} else messageApi.error(res.msg || '保存失败');
} else messageApi.error(res.msg || t('common.saveFail'));
});
});
};
@@ -324,13 +326,13 @@ export default function UserRolePage() {
// 重置密码
const handleResetPassword = (user: any) => {
modal.confirm({
title: '重置密码',
content: '确定将密码重置为 123456 吗?',
okText: '确认',
cancelText: '取消',
title: t('users.resetPassword'),
content: t('users.resetPasswordConfirm'),
okText: t('common.confirm'),
cancelText: t('common.cancel'),
onOk: () => {
editUser({ userId: user.userId, password: '123456' }).then(res => {
res.code === 200 ? messageApi.success('密码已重置为:123456') : messageApi.error('重置失败');
res.code === 200 ? messageApi.success(t('users.passwordResetTo')) : messageApi.error('重置失败');
});
}
});
@@ -339,9 +341,9 @@ export default function UserRolePage() {
const handleDeleteUser = (userId: number) => {
deleteUser(userId).then(res => {
if (res.code === 200) {
messageApi.success('删除成功');
messageApi.success(t('common.deleteSuccess'));
getUserData();
} else messageApi.error(res.msg || '删除失败');
} else messageApi.error(res.msg || t('common.deleteFail'));
});
};
@@ -351,7 +353,7 @@ export default function UserRolePage() {
title: '批量删除用户',
content: `确定要删除已选中的 ${selectedUserIds.length} 个用户吗?`,
okText: '确定删除',
cancelText: '取消',
cancelText: t('common.cancel'),
okType: 'danger',
onOk: () => {
Promise.all(selectedUserIds.map(id => deleteUser(id))).then(() => {
@@ -376,11 +378,11 @@ export default function UserRolePage() {
const api = currentUser ? editUser : addUser;
api(params).then(res => {
if (res.code === 200) {
messageApi.success(currentUser ? '编辑成功' : '新增成功');
messageApi.success(currentUser ? t('common.editSuccess') : t('common.addSuccess'));
setModalVisible(false);
getUserData();
} else {
messageApi.error(res.msg || '保存失败');
messageApi.error(res.msg || t('common.saveFail'));
}
});
});
@@ -404,14 +406,14 @@ export default function UserRolePage() {
<AntdCard style={{ borderRadius: 12, boxShadow: '0 2px 10px rgba(0,0,0,0.06)' }}>
<Tabs defaultActiveKey="user" type="card" size="large" style={{ marginBottom: 20 }}>
{/* 用户管理 */}
<TabPane tab="用户管理" key="user">
<TabPane tab={t('router.users')} key="user">
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
{selectedUserIds.length > 0 && (
<Button danger onClick={handleBatchDeleteUsers}>
批量删除(已选 {selectedUserIds.length} 项)
</Button>
)}
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddUser}>新增用户</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddUser}>{t('users.addUser')}</Button>
</div>
<Table
rowKey="userId"
@@ -420,10 +422,10 @@ export default function UserRolePage() {
onChange: (selectedRowKeys) => setSelectedUserIds(selectedRowKeys)
}}
columns={[
{ title: '用户账号', dataIndex: 'userName', ellipsis: true },
{ title: '用户昵称', dataIndex: 'nickName', ellipsis: true },
{ title: '手机号', dataIndex: 'phonenumber', ellipsis: true },
{ title: '邮箱', dataIndex: 'email', ellipsis: true },
{ title: t('users.usernameLabel'), dataIndex: 'userName', ellipsis: true },
{ title: t('users.nicknameLabel'), dataIndex: 'nickName', ellipsis: true },
{ title: t('users.phone'), dataIndex: 'phonenumber', ellipsis: true },
{ title: t('users.email'), dataIndex: 'email', ellipsis: true },
{
title: '所属机构',
render: (_, r) => {
@@ -442,17 +444,17 @@ export default function UserRolePage() {
ellipsis: true
},
{
title: '角色',
title: t('users.role'),
render: (_, r) => (r.roles || []).map(i => (
<Tag color="blue" key={i.roleId}>{i?.roleKey}</Tag>
)),
ellipsis: true
},
{
title: '状态', dataIndex: 'status', width: 80,
title: t('roles.status'), dataIndex: 'status', width: 80,
render: (_, s) => s.status === '0'
? <Tag color="green">正常</Tag>
: <Tag color="red">停用</Tag>
? <Tag color="green">{t('roles.normal')}</Tag>
: <Tag color="red">{t('roles.stopped')}</Tag>
},
{
title: '是否有设备权限', dataIndex: 'deviceOperatePerm', width: 140,
@@ -461,16 +463,16 @@ export default function UserRolePage() {
: <Tag color="red">无权限</Tag>
},
{
title: '操作',
title: t('roles.actions'),
width: 300,
fixed: 'right',
render: (_, r) => (
<Space wrap>
<Button type="text" icon={<EditOutlined style={{ color: '#1890ff' }} />} onClick={() => handleEditUser(r)}>编辑</Button>
<Button type="text" icon={<EditOutlined style={{ color: '#1890ff' }} />} onClick={() => handleEditUser(r)}>{t('roles.edit')}</Button>
<Popconfirm onConfirm={() => handleDeleteUser(r.userId)} title="确定删除该用户吗?">
<Button type="text" danger icon={<DeleteOutlined />}>删除</Button>
<Button type="text" danger icon={<DeleteOutlined />}>{t('roles.delete')}</Button>
</Popconfirm>
<Button type="text" icon={<UserOutlined style={{ color: '#faad14' }} />} onClick={() => { setCurrentUser(r); handleResetPassword(r); }}>重置密码</Button>
<Button type="text" icon={<UserOutlined style={{ color: '#faad14' }} />} onClick={() => { setCurrentUser(r); handleResetPassword(r); }}>{t('users.resetPassword')}</Button>
</Space>
)
}
@@ -492,14 +494,14 @@ export default function UserRolePage() {
{/* 角色管理 */}
{userInfo?.userId == 1 && (
<TabPane tab="角色管理" key="role">
<TabPane tab={t('router.roles')} key="role">
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
{selectedRoleIds.length > 0 && (
<Button danger onClick={handleBatchDeleteRoles}>
批量删除(已选 {selectedRoleIds.length} 项)
</Button>
)}
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddRole}>新增角色</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddRole}>{t('roles.addRole')}</Button>
</div>
<Row gutter={[16, 16]}>
{roles.length > 0 ? roles.map(r => (
@@ -508,9 +510,9 @@ export default function UserRolePage() {
hoverable
style={{ borderRadius: 10, position: 'relative' }}
actions={[
<Button type="text" icon={<EditOutlined />} onClick={() => handleEditRole(r)}>编辑</Button>,
<Popconfirm onConfirm={() => handleDeleteRole(r.roleId)} title="确定删除?">
<Button type="text" danger>删除</Button>
<Button type="text" icon={<EditOutlined />} onClick={() => handleEditRole(r)}>{t('roles.edit')}</Button>,
<Popconfirm onConfirm={() => handleDeleteRole(r.roleId)} title={t('common.confirmDelete')}>
<Button type="text" danger>roles.delete</Button>
</Popconfirm>
]}
>
@@ -564,7 +566,7 @@ export default function UserRolePage() {
{/* 角色弹窗 */}
<Modal
title={editRole ? '编辑角色' : '新增角色'}
title={editRole ? '编辑角色' : t('roles.addRole')}
open={roleModalVisible}
onCancel={() => setRoleModalVisible(false)}
onOk={handleSaveRole}
@@ -573,19 +575,19 @@ export default function UserRolePage() {
<Form form={roleForm} layout="vertical">
<Row gutter={16}>
<Col span={12}>
<Form.Item name="roleName" label="角色名称" rules={[{ required: true }]}>
<Form.Item name="roleName" label={t('roles.roleName')} rules={[{ required: true }]}>
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="roleKey" label="权限标识" rules={[{ required: true }]}>
<Form.Item name="roleKey" label={t('systemSetting.permission')} rules={[{ required: true }]}>
<Input />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="roleSort" label="排序">
<Form.Item name="roleSort" label={t('roles.roleSort')}>
<InputNumber style={{ width: '100%' }} min={1} max={100} />
</Form.Item>
</Col>
@@ -598,7 +600,7 @@ export default function UserRolePage() {
{userInfo?.roleId == 23 && (
<>
<Divider>菜单权限</Divider>
<Divider>roles.menuPermissions</Divider>
<Tree
checkable
treeData={menuTree}
@@ -614,7 +616,7 @@ export default function UserRolePage() {
{/* 用户弹窗 */}
<Modal
title={currentUser ? '编辑用户' : '新增用户'}
title={currentUser ? t('systemSetting.editUser') : t('users.addUser')}
open={modalVisible}
onCancel={() => setModalVisible(false)}
onOk={handleSaveUser}
@@ -628,19 +630,19 @@ export default function UserRolePage() {
</Row>
<Row gutter={16}>
<Col span={12}><Form.Item name="phonenumber" label="手机"><Input /></Form.Item></Col>
<Col span={12}><Form.Item name="email" label="邮箱"><Input /></Form.Item></Col>
<Col span={12}><Form.Item name="email" label={t('users.email')}><Input /></Form.Item></Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="password" label="密码">
<Input.Password placeholder={currentUser ? "编辑不显示,重置为123456" : "请输入密码"} />
<Form.Item name="password" label={t('users.passwordLabel')}>
<Input.Password placeholder={currentUser ? "编辑不显示,重置为123456" : t('login.passwordRequired')} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="language" label="语言">
<Form.Item name="language" label={t('users.languageLabel')}>
<Select>
<Select.Option value="zh-CN">中文</Select.Option>
<Select.Option value="en">英语</Select.Option>
<Select.Option value="zh-CN">{t('users.chinese')}</Select.Option>
<Select.Option value="en">{t('users.english')}</Select.Option>
</Select>
</Form.Item>
</Col>
@@ -651,7 +653,7 @@ export default function UserRolePage() {
{!isEditSelf ? (
<>
<Col span={12}>
<Form.Item name="roleIds" label="分配角色" rules={[{ required: true }]}>
<Form.Item name="roleIds" label={t('users.assignRoleLabel')} rules={[{ required: true }]}>
<Select
placeholder="选择角色"
key={rolesOption.length}
@@ -672,10 +674,10 @@ export default function UserRolePage() {
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="status" label="状态" initialValue="0">
<Form.Item name="status" label={t('roles.status')} initialValue="0">
<Select>
<Select.Option value="0">正常</Select.Option>
<Select.Option value="1">停用</Select.Option>
<Select.Option value="0">{t('roles.normal')}</Select.Option>
<Select.Option value="1">{t('roles.stopped')}</Select.Option>
</Select>
</Form.Item>
</Col>
@@ -683,10 +685,10 @@ export default function UserRolePage() {
) : (
// 编辑自己 → 状态占满一行 span=24
<Col span={24}>
<Form.Item name="status" label="状态" initialValue="0">
<Form.Item name="status" label={t('roles.status')} initialValue="0">
<Select>
<Select.Option value="0">正常</Select.Option>
<Select.Option value="1">停用</Select.Option>
<Select.Option value="0">{t('roles.normal')}</Select.Option>
<Select.Option value="1">{t('roles.stopped')}</Select.Option>
</Select>
</Form.Item>
</Col>
@@ -695,11 +697,11 @@ export default function UserRolePage() {
<Row gutter={16}>
<Col span={12}>
<Form.Item name="sex" label="性别" initialValue="2">
<Form.Item name="sex" label={t('users.genderLabel')} initialValue="2">
<Select>
<Select.Option value="0">男</Select.Option>
<Select.Option value="1">女</Select.Option>
<Select.Option value="2">未知</Select.Option>
<Select.Option value="2">{t('constants.locationUnknown')}</Select.Option>
</Select>
</Form.Item>
</Col>

View File

@@ -8,6 +8,7 @@ import {
import axios from 'axios';
import flvjs from 'flv.js';
import FlvPlayer from './FlvPlayer';
import { useTranslation } from 'react-i18next';
const { Title, Text } = Typography;
@@ -41,6 +42,8 @@ const isFlv = (name: string) => name.toLowerCase().endsWith('.flv');
const isMp4 = (name: string) => name.toLowerCase().endsWith('.mp4');
const VideoManagement = () => {
const { t } = useTranslation();
const [channels, setChannels] = useState<string[]>([]);
const [channel, setChannel] = useState<string | undefined>();
const [years, setYears] = useState<string[]>([]);
@@ -151,10 +154,10 @@ const VideoManagement = () => {
};
const columns = [
{ title: '文件名', dataIndex: 'fileName', key: 'fileName', ellipsis: true },
{ title: t('video.fileName'), dataIndex: 'fileName', key: 'fileName', ellipsis: true },
{ title: '大小', dataIndex: 'sizeFormatted', key: 'sizeFormatted', width: 120 },
{
title: '操作', key: 'action', width: 160,
title: t('roles.actions'), key: 'action', width: 160,
render: (_: any, rec: { fileName: string }) => (
<a
@@ -169,7 +172,7 @@ const VideoManagement = () => {
target="_blank"
rel="noreferrer"
>
<Button size="small" type="link" icon={<DownloadOutlined />}>下载</Button>
<Button size="small" type="link" icon={<DownloadOutlined />}>{t('common.download')}</Button>
</a>
),
},
@@ -178,7 +181,7 @@ const VideoManagement = () => {
return (
<div style={{ padding: '8px 0px', background: '#f5f7fa', minHeight: '100vh' }}>
<div style={{ marginBottom: 8 }}>
<Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.1vw, 18px)' }}>视频管理</Title>
<Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.1vw, 18px)' }}>{t('router.videoManage')}</Title>
<Text type="secondary" style={{ fontSize: 12 }}>
录像通道与回放管理 · 共 {channels.length} 个通道
</Text>

View File

@@ -33,12 +33,14 @@ import { getSiteDevice, getRobotRouteList, getWayLine, getSiteUAVList } from '..
import { useSelector } from 'react-redux';
import { RootState } from '@/src/store';
import utils, { workOrderTypeOptions, getWorkOrderTypeText, errorLevelOptions, LEVEL_TAG_COLOR } from '@/lib/utils';
import { useTranslation } from 'react-i18next';
const { TabPane } = Typography;
const { Option } = Select;
// 工单模型管理子组件
const WorkOrderModelManagement: React.FC = () => {
const { t } = useTranslation();
const [form] = Form.useForm();
const [list, setList] = useState([]);
const [loading, setLoading] = useState(false);
@@ -78,8 +80,8 @@ const WorkOrderModelManagement: React.FC = () => {
if (robotRes.code === 200) {
const robots = robotRes.rows || [];
robotOptions.push({
label: '机器人',
title: '机器人',
label: t('devices.robot'),
title: t('devices.robot'),
options: robots.map((item: any) => ({
label: item.deviceAlias || item.serialNumber,
value: item.serialNumber,
@@ -92,8 +94,8 @@ const WorkOrderModelManagement: React.FC = () => {
if (uavRes.code === 200) {
const uavs = uavRes.rows || [];
uavOptions.push({
label: '无人机',
title: '无人机',
label: t('constants.uav'),
title: t('constants.uav'),
options: uavs.map((item: any) => ({
label: item.drone_callsign,
@@ -176,6 +178,7 @@ const WorkOrderModelManagement: React.FC = () => {
}, [stationId]);
const handleAdd = () => {
const { t } = useTranslation();
setEditingItem(null);
form.resetFields();
setSelectedDeviceType(null);
@@ -210,17 +213,17 @@ const WorkOrderModelManagement: React.FC = () => {
modal.confirm({
title: '确认删除',
content: '您确定要删除这条工单模型吗?',
okText: '确定',
cancelText: '取消',
okText: t('common.ok'),
cancelText: t('common.cancel'),
onOk: async () => {
try {
const res = await deleteWorkOrderModelApi([id]);
if (res.code === 200) {
message.success('删除成功');
message.success(t('common.deleteSuccess'));
fetchList();
}
} catch (e) {
message.error('删除失败');
message.error(t('common.deleteFail'));
}
}
});
@@ -235,8 +238,8 @@ const WorkOrderModelManagement: React.FC = () => {
modal.confirm({
title: '批量删除确认',
content: `您确定要删除选中的 ${selectedRowKeys.length} 条工单模型吗?删除后不可恢复!`,
okText: '确定',
cancelText: '取消',
okText: t('common.ok'),
cancelText: t('common.cancel'),
danger: true,
onOk: async () => {
try {
@@ -271,13 +274,13 @@ const WorkOrderModelManagement: React.FC = () => {
res = await addWorkOrderModelApi(payload);
}
if (res.code === 200 && res.data == true) {
message.success(editingItem ? '更新成功' : '新增成功');
message.success(editingItem ? '更新成功' : t('common.addSuccess'));
setModalVisible(false);
form.resetFields();
fetchList();
}
} catch (e) {
message.error('保存失败');
message.error(t('common.saveFail'));
}
};
@@ -297,7 +300,7 @@ const WorkOrderModelManagement: React.FC = () => {
const columns = [
{ title: '工单名称', dataIndex: 'orderName', key: 'orderName' },
{
title: '工单类型',
title: t('workOrder.orderType'),
dataIndex: 'orderType',
key: 'orderType',
render: getWorkOrderTypeText
@@ -308,7 +311,7 @@ const WorkOrderModelManagement: React.FC = () => {
key: 'errorLevel',
render: (val) => {
const color = LEVEL_TAG_COLOR[val] || 'default';
const text = { 1: '轻微', 2: '一般', 3: '严重' }[val] || val;
const text = { 1: '轻微', 2: t('constants.general'), 3: t('constants.critical') }[val] || val;
return <Tag color={color}>{text}</Tag>;
}
},
@@ -342,12 +345,12 @@ const WorkOrderModelManagement: React.FC = () => {
render: (val: boolean) => val ? '是' : '否'
},
{
title: '操作',
title: t('roles.actions'),
key: 'action',
render: (_: any, record: any) => (
<Space>
<Button type="link" onClick={() => handleEdit(record)}>编辑</Button>
<Button type="link" danger onClick={() => handleDelete(record.id)}>删除</Button>
<Button type="link" onClick={() => handleEdit(record)}>{t('roles.edit')}</Button>
<Button type="link" danger onClick={() => handleDelete(record.id)}>{t('roles.delete')}</Button>
</Space>
)
}
@@ -364,7 +367,7 @@ const WorkOrderModelManagement: React.FC = () => {
<Card>
<Space style={{ marginBottom: 16 }}>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>新增工单模型</Button>
<Button icon={<ReloadOutlined />} onClick={fetchList}>刷新</Button>
<Button icon={<ReloadOutlined />} onClick={fetchList}>{t('common.refresh')}</Button>
<Button
danger
icon={<DeleteOutlined />}
@@ -395,8 +398,8 @@ const WorkOrderModelManagement: React.FC = () => {
<Form.Item label="工单名称" name="orderName" rules={[{ required: true, message: '请输入工单名称' }]}>
<Input placeholder="请输入工单名称" />
</Form.Item>
<Form.Item label="工单类型" name="orderType" rules={[{ required: true, message: '请选择工单类型' }]}>
<Select placeholder="请选择工单类型" options={workOrderTypeOptions} />
<Form.Item label={t('workOrder.orderType')} name="orderType" rules={[{ required: true, message: t('workOrder.pleaseSelectType') }]}>
<Select placeholder={t('workOrder.pleaseSelectType')} options={workOrderTypeOptions} />
</Form.Item>
<Form.Item label="错误等级" name="errorLevel" rules={[{ required: true, message: '请选择错误等级' }]}>
<Select placeholder="请选择错误等级" options={errorLevelOptions} />
@@ -429,8 +432,8 @@ const WorkOrderModelManagement: React.FC = () => {
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">保存</Button>
<Button onClick={() => setModalVisible(false)}>取消</Button>
<Button type="primary" htmlType="submit">{t('common.save')}</Button>
<Button onClick={() => setModalVisible(false)}>{t('common.cancel')}</Button>
</Space>
</Form.Item>
</Form>

View File

@@ -1,6 +1,8 @@
import React, { useState, useEffect } from 'react';
import Empty from './Empty';
import VideoCanvas from './VideoCanvas';
import { useTranslation } from 'react-i18next';
import { t } from 'i18next';
interface VideoPlayerProps {
deviceId: string;
@@ -31,7 +33,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
}
if (!ready || !video) {
return <Empty description="视频加载中..." />;
return <Empty description={t('video.videoLoading')} />;
}
return (

View File

@@ -12,23 +12,25 @@ import {
getAlarmDetail
} from '../../api/alarmApi.js';
import { alarmTypeMap } from '@/constants.js';
import { useTranslation } from 'react-i18next';
import { t } from 'i18next';
const { Option } = Select;
const { RangePicker } = DatePicker;
const alarmLevelConfig = {
1: { description: '严重', color: '#FF4D4F' },
2: { description: '重要', color: '#FA8C16' },
3: { description: '一般', color: '#FADB14' },
4: { description: '提示', color: '#1890FF' }
1: { description: t('constants.critical'), color: '#FF4D4F' },
2: { description: t('constants.important'), color: '#FA8C16' },
3: { description: t('constants.general'), color: '#FADB14' },
4: { description: t('constants.info'), color: '#1890FF' }
};
const handleStatusMap = {
1: '待处理',
1: t('constants.pending'),
2: '处理中',
3: '已关闭',
4: '已忽略'
3: t('constants.closed'),
4: t('constants.ignored')
};
// 自动生成颜色/文字映射
@@ -43,6 +45,7 @@ const levelText = Object.keys(alarmLevelConfig).reduce((map, key) => {
}, {});
const AlarmHistory = () => {
const { t } = useTranslation();
const [form] = Form.useForm();
// 抽屉
@@ -128,21 +131,21 @@ const AlarmHistory = () => {
const columns = [
{ title: '告警编号', dataIndex: 'alarmNo', width: 180 },
{
title: '告警时间',
title: t('alerts.alertTime'),
dataIndex: 'alarmTime',
width: 190,
render: (t) => t ? dayjs(t).format('YYYY-MM-DD HH:mm:ss') : '-'
},
{ title: '设备ID', dataIndex: 'deviceId', width: 160 },
{ title: '设备名称', dataIndex: 'deviceName', ellipsis: true },
{ title: t('devices.deviceName'), dataIndex: 'deviceName', ellipsis: true },
{
title: '告警类型',
title: t('alerts.alertType'),
dataIndex: 'alarmType',
width: 130,
render: (t) => alarmTypeMap[t] || '-'
},
{
title: '告警级别',
title: t('alerts.alertLevel'),
width: 100,
render: (r) => (
<Tag color={levelColor[r.alarmLevel]} style={{ border: 'none', color: '#fff' }}>
@@ -153,7 +156,7 @@ const AlarmHistory = () => {
{ title: '告警标题', dataIndex: 'alarmTitle', ellipsis: true, width: 180 },
{ title: '告警内容', dataIndex: 'alarmContent', ellipsis: true },
{
title: '处理状态',
title: t('alerts.handleStatus'),
width: 110,
render: (r) => {
const s = r.handleStatus;
@@ -166,7 +169,7 @@ const AlarmHistory = () => {
}
},
{
title: '操作',
title: t('roles.actions'),
width: 120,
fixed: 'right',
render: (_, r) => (
@@ -191,29 +194,29 @@ const AlarmHistory = () => {
<Input placeholder="请输入设备ID" style={{ width: 160 }} allowClear />
</Form.Item>
<Form.Item name="alarmLevel" label="告警级别">
<Select placeholder="全部" style={{ width: 130 }} allowClear>
<Option value={1}>严重</Option>
<Option value={2}>重要</Option>
<Option value={3}>一般</Option>
<Option value={4}>提示</Option>
<Form.Item name="alarmLevel" label={t('alerts.alertLevel')}>
<Select placeholder={t('common.all')} style={{ width: 130 }} allowClear>
<Option value={1}>{t('constants.critical')}</Option>
<Option value={2}>{t('constants.important')}</Option>
<Option value={3}>{t('constants.general')}</Option>
<Option value={4}>{t('constants.info')}</Option>
</Select>
</Form.Item>
<Form.Item name="alarmType" label="告警类型">
<Select placeholder="全部" style={{ width: 150 }} allowClear>
<Option value="UAV_FAULT">无人机故障</Option>
<Option value="MOWER_FAULT">割草机故障</Option>
<Option value="OTHER_DEVICE_FAULT">其他设备故障</Option>
<Option value="SERVER_FAILURE">服务器故障</Option>
<Option value="SYSTEM_ERROR">系统错误</Option>
<Form.Item name="alarmType" label={t('alerts.alertType')}>
<Select placeholder={t('common.all')} style={{ width: 150 }} allowClear>
<Option value="UAV_FAULT">{t('constants.uavFault')}</Option>
<Option value="MOWER_FAULT">{t('constants.mowerFault')}</Option>
<Option value="OTHER_DEVICE_FAULT">{t('constants.otherDeviceFault')}</Option>
<Option value="SERVER_FAILURE">{t('constants.serverFailure')}</Option>
<Option value="SYSTEM_ERROR">{t('constants.systemError')}</Option>
</Select>
</Form.Item>
<Form.Item name="handleStatus" label="处理状态">
<Select placeholder="全部" style={{ width: 130 }} allowClear>
<Option value={1}>待处理</Option>
<Option value={2}>已关闭</Option>
<Form.Item name="handleStatus" label={t('alerts.handleStatus')}>
<Select placeholder={t('common.all')} style={{ width: 130 }} allowClear>
<Option value={1}>{t('constants.pending')}</Option>
<Option value={2}>{t('constants.closed')}</Option>
</Select>
</Form.Item>
@@ -228,8 +231,8 @@ const AlarmHistory = () => {
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">查询</Button>
<Button htmlType="reset">重置</Button>
<Button type="primary" htmlType="submit">{t('common.search2')}</Button>
<Button htmlType="reset">{t('common.reset')}</Button>
</Space>
</Form.Item>
</Form>
@@ -269,17 +272,17 @@ const AlarmHistory = () => {
<Descriptions.Item label="告警标题">{currentAlarm?.alarmTitle || '-'}</Descriptions.Item>
<Descriptions.Item label="告警内容">{currentAlarm?.alarmContent || '-'}</Descriptions.Item>
<Descriptions.Item label="设备ID">{currentAlarm?.deviceId || '-'}</Descriptions.Item>
<Descriptions.Item label="设备名称">{currentAlarm?.deviceName || '-'}</Descriptions.Item>
<Descriptions.Item label="告警时间">
<Descriptions.Item label={t('devices.deviceName')}>{currentAlarm?.deviceName || '-'}</Descriptions.Item>
<Descriptions.Item label={t('alerts.alertTime')}>
{currentAlarm?.alarmTime ? dayjs(currentAlarm.alarmTime).format('YYYY-MM-DD HH:mm:ss') : '-'}
</Descriptions.Item>
<Descriptions.Item label="告警类型">{alarmTypeMap[currentAlarm?.alarmType] || '-'}</Descriptions.Item>
<Descriptions.Item label="告警级别">
<Descriptions.Item label={t('alerts.alertType')}>{alarmTypeMap[currentAlarm?.alarmType] || '-'}</Descriptions.Item>
<Descriptions.Item label={t('alerts.alertLevel')}>
<Tag color={levelColor[currentAlarm?.alarmLevel]} style={{ border: 'none', color: '#fff' }}>
{levelText[currentAlarm?.alarmLevel]}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="处理状态">
<Descriptions.Item label={t('alerts.handleStatus')}>
{(() => {
const s = currentAlarm?.handleStatus;
let color = 'default';
@@ -290,11 +293,11 @@ const AlarmHistory = () => {
return <Tag color={color}>{handleStatusMap[s] || '-'}</Tag>;
})()}
</Descriptions.Item>
<Descriptions.Item label="处理人">{currentAlarm?.handleUserName || '-'}</Descriptions.Item>
<Descriptions.Item label="处理时间">
<Descriptions.Item label={t('alerts.handler')}>{currentAlarm?.handleUserName || '-'}</Descriptions.Item>
<Descriptions.Item label={t('alerts.handleTime')}>
{currentAlarm?.handleTime ? dayjs(currentAlarm.handleTime).format('YYYY-MM-DD HH:mm:ss') : '-'}
</Descriptions.Item>
<Descriptions.Item label="处理备注">{currentAlarm?.handleRemark || '-'}</Descriptions.Item>
<Descriptions.Item label={t('alerts.handleRemark')}>{currentAlarm?.handleRemark || '-'}</Descriptions.Item>
<Descriptions.Item label="根因分析">{currentAlarm?.rootCause || '-'}</Descriptions.Item>
<Descriptions.Item label="推荐处理措施">
{currentAlarm?.handleSuggestion || '-'}
@@ -308,19 +311,19 @@ const AlarmHistory = () => {
<Space>
<span>短信:</span>
<Tag color={currentAlarm.notifySms === 1 ? 'green' : 'default'}>
{currentAlarm.notifySms === 1 ? '已开启' : '已关闭'}
{currentAlarm.notifySms === 1 ? '已开启' : t('constants.closed')}
</Tag>
</Space>
<Space>
<span>钉钉:</span>
<Tag color={currentAlarm.notifyTelegram === 1 ? 'green' : 'default'}>
{currentAlarm.notifyTelegram === 1 ? '已开启' : '已关闭'}
{currentAlarm.notifyTelegram === 1 ? '已开启' : t('constants.closed')}
</Tag>
</Space>
<Space>
<span>邮件:</span>
<Tag color={currentAlarm.notifyEmail === 1 ? 'green' : 'default'}>
{currentAlarm.notifyEmail === 1 ? '已开启' : '已关闭'}
{currentAlarm.notifyEmail === 1 ? '已开启' : t('constants.closed')}
</Tag>
</Space>
</Space>

View File

@@ -27,6 +27,7 @@ import { addWorkOrder } from '@/api/workOrder.js';
import { alarmLevelConfig, alarmTypeMap, handleStatusMap } from '../../constants.js';
import { useSelector } from 'react-redux';
import { RootState } from '@/src/store/index.js';
import { useTranslation } from 'react-i18next';
const { RangePicker } = DatePicker;
const { Text } = Typography;
@@ -37,8 +38,10 @@ const { Option } = Select;
// 快速根据 value 获取 label
const getLevelLabel = (val) => {
const { t } = useTranslation();
const item = errorLevelOptions.find(opt => opt.value === val);
return item?.label || '未知';
return item?.label || t('constants.locationUnknown');
};
/**
@@ -90,6 +93,7 @@ const AlarmLevelTag = ({ level, size = 'small' }) => {
};
export default function AlarmCenterPage() {
const { t } = useTranslation();
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [detailLoading, setDetailLoading] = useState(false);
@@ -196,7 +200,7 @@ export default function AlarmCenterPage() {
// 表单赋值
form.setFieldsValue({
handleStatus: res.data.handleStatus,
handleRemark: '已处理'
handleRemark: t('constants.handled')
});
setDrawerVisible(true);
}
@@ -214,7 +218,7 @@ export default function AlarmCenterPage() {
alarmId: currentAlarm.id,
handleStatus: values.handleStatus,
handleResult: values.handleResult,
handleRemark: values.handleRemark || '已处理',
handleRemark: values.handleRemark || t('constants.handled'),
rootCause: values.rootCause || '无'
});
messageApi.success('处理成功');
@@ -254,12 +258,12 @@ export default function AlarmCenterPage() {
const handleUpdateAlarm = async (values) => {
try {
await updateAlarm(values);
messageApi.success('修改成功');
messageApi.success(t('profile.modifySuccess'));
setEditModalVisible(false);
fetchList(); // 刷新列表
fetchStat(); // 刷新统计
} catch (err) {
messageApi.error('修改失败');
messageApi.error(t('profile.modifyFailed'));
console.error(err);
}
};
@@ -300,31 +304,31 @@ export default function AlarmCenterPage() {
ellipsis: true
},
{
title: '设备名称',
title: t('devices.deviceName'),
dataIndex: 'deviceName',
width: 280
},
{
title: '告警类型',
title: t('alerts.alertType'),
dataIndex: 'alarmType',
width: 120,
render: (t) => alarmTypeMap[t] || '-'
},
{
title: '告警级别',
title: t('alerts.alertLevel'),
dataIndex: 'alarmLevel',
width: 100,
render: (l) => <AlarmLevelTag level={l} size="small" />
},
{
title: '告警时间',
title: t('alerts.alertTime'),
dataIndex: 'alarmTime',
width: 180,
render: (t) => t ? dayjs(t).format('YYYY-MM-DD HH:mm:ss') : '-'
},
{
title: '处理状态',
title: t('alerts.handleStatus'),
dataIndex: 'handleStatus',
width: 110,
render: (s) => {
@@ -339,7 +343,7 @@ export default function AlarmCenterPage() {
}
},
{
title: '操作',
title: t('roles.actions'),
width: 100,
fixed: 'right',
render: (_, record) => (
@@ -390,17 +394,17 @@ export default function AlarmCenterPage() {
const pieData = [
{
name: '错误',
name: t('constants.error'),
value: statData.levelStats?.["ERROR"] || 0,
color: '#FF4D4F',
},
{
name: '警告',
name: t('constants.warning'),
value: statData.levelStats?.['WARNING'] || 0,
color: '#FA8C16',
},
{
name: '信息',
name: t('constants.infoLevel'),
value: statData.levelStats?.['INFO'] || 0,
color: '#FADB14',
},
@@ -657,7 +661,7 @@ export default function AlarmCenterPage() {
<Card bordered={false} bodyStyle={{ padding: '16px 24px' }} style={{ borderRadius: 8 }}>
<Row gutter={[8, 8]} align="middle">
<Col>
<div style={{ fontSize: 12, color: '#4E5969', marginBottom: 4 }}>告警级别</div>
<div style={{ fontSize: 12, color: '#4E5969', marginBottom: 4 }}>{t('alerts.alertLevel')}</div>
<Select
style={{ width: 120 }}
value={queryParams.alarmLevel}
@@ -666,15 +670,15 @@ export default function AlarmCenterPage() {
/>
</Col>
<Col>
<div style={{ fontSize: 12, color: '#4E5969', marginBottom: 4 }}>处理状态</div>
<div style={{ fontSize: 12, color: '#4E5969', marginBottom: 4 }}>{t('alerts.handleStatus')}</div>
<Select
style={{ width: 120 }}
value={queryParams.handleStatus}
allowClear
onChange={v => setQueryParams({ ...queryParams, handleStatus: v })}
options={[
{ value: 1, label: '待处理' },
{ value: 2, label: '已关闭' },
{ value: 1, label: t('constants.pending') },
{ value: 2, label: t('constants.closed') },
]}
/>
</Col>
@@ -722,7 +726,7 @@ export default function AlarmCenterPage() {
fetchList();
fetchStat();
fetchChartAllAlarm(); // 新增:刷新图表全量告警
}}>重置</Button>
}}>{t('common.reset')}</Button>
<Button type="primary" style={{ borderRadius: 8, background: '#165DFF' }} onClick={() => {
fetchList(); // 刷新列表
fetchStat(); // 刷新统计
@@ -770,7 +774,7 @@ export default function AlarmCenterPage() {
{/* 三个图表 */}
<Row gutter={16}>
<Col span={8}>
<Card bordered={false} title="告警趋势" style={{ borderRadius: 8 }}>
<Card bordered={false} title={t('alerts.alertTrend')} style={{ borderRadius: 8 }}>
<UniversalLineChart
data={getAlarmTrendData()}
xKey="date"
@@ -781,19 +785,19 @@ export default function AlarmCenterPage() {
series={[
{
key: 'ERROR',
name: '错误',
name: t('constants.error'),
color: '#FF4D4F',
strokeWidth: 2,
},
{
key: 'WARNING',
name: '警告',
name: t('constants.warning'),
color: '#FA8C16',
strokeWidth: 2,
},
{
key: 'INFO',
name: '信息',
name: t('constants.infoLevel'),
color: '#FADB14',
strokeWidth: 2,
},
@@ -818,12 +822,12 @@ export default function AlarmCenterPage() {
<UniversalBarChart
data={[
{
name: '待处理',
name: t('constants.pending'),
pending: statData.pending || 0,
},
{
name: '已关闭',
name: t('constants.closed'),
pending: statData.closed || 0,
},
]}
@@ -875,9 +879,9 @@ export default function AlarmCenterPage() {
{/* 告警基础信息 */}
<Descriptions column={1} bordered size="small" style={{ marginBottom: 16 }}>
<Descriptions.Item label="来源设备">{currentAlarm.deviceName || '-'}</Descriptions.Item>
<Descriptions.Item label="告警类型">{alarmTypeMap[currentAlarm.alarmType] || '-'}</Descriptions.Item>
<Descriptions.Item label={t('alerts.alertType')}>{alarmTypeMap[currentAlarm.alarmType] || '-'}</Descriptions.Item>
<Descriptions.Item label="发生时间">{dayjs(currentAlarm.alarmTime).format('YYYY-MM-DD HH:mm:ss')}</Descriptions.Item>
<Descriptions.Item label="处理状态">
<Descriptions.Item label={t('alerts.handleStatus')}>
{(() => {
let color = 'default';
const status = currentAlarm.handleStatus;
@@ -920,14 +924,14 @@ export default function AlarmCenterPage() {
{/* 处理操作 */}
<Card size="small" title="处理操作" style={{ marginBottom: 16 }}>
<Form.Item label="处理状态" name="handleStatus" rules={[{ required: true, message: '请选择处理状态' }]}>
<Form.Item label={t('alerts.handleStatus')} name="handleStatus" rules={[{ required: true, message: '请选择处理状态' }]}>
<Select>
<Option value={1}>待处理</Option>
<Option value={1}>{t('constants.pending')}</Option>
<Option value={2}>已关闭</Option>
<Option value={2}>{t('constants.closed')}</Option>
</Select>
</Form.Item>
<Form.Item label="处理结果" name="handleResult" rules={[{ required: true, message: '请选择确认处理结果' }]}>
<Form.Item label={t('alerts.handleResult')} name="handleResult" rules={[{ required: true, message: '请选择确认处理结果' }]}>
<Select
placeholder="请选择确认处理结果"
options={alarmHandleResultOptions}
@@ -936,7 +940,7 @@ export default function AlarmCenterPage() {
<Form.Item label="根因分析" name="rootCause">
<Input.TextArea rows={2} placeholder="请输入处理备注" />
</Form.Item>
<Form.Item label="处理备注" name="handleRemark">
<Form.Item label={t('alerts.handleRemark')} name="handleRemark">
<Input.TextArea rows={2} placeholder="请输入处理备注" />
</Form.Item>
@@ -980,7 +984,7 @@ export default function AlarmCenterPage() {
</Form.Item>
<Form.Item
label="告警级别"
label={t('alerts.alertLevel')}
name="alarmLevel"
rules={[{ required: true, message: '请选择告警级别' }]}
>
@@ -990,16 +994,16 @@ export default function AlarmCenterPage() {
</Form.Item>
<Form.Item
label="告警类型"
label={t('alerts.alertType')}
name="alarmType"
rules={[{ required: true, message: '请选择告警类型' }]}
>
<Select>
<Option value="UAV_FAULT">无人机故障</Option>
<Option value="MOWER_FAULT">割草机故障</Option>
<Option value="OTHER_DEVICE_FAULT">其他设备故障</Option>
<Option value="SERVER_FAILURE">服务器故障</Option>
<Option value="SYSTEM_ERROR">系统错误</Option>
<Option value="UAV_FAULT">{t('constants.uavFault')}</Option>
<Option value="MOWER_FAULT">{t('constants.mowerFault')}</Option>
<Option value="OTHER_DEVICE_FAULT">{t('constants.otherDeviceFault')}</Option>
<Option value="SERVER_FAILURE">{t('constants.serverFailure')}</Option>
<Option value="SYSTEM_ERROR">{t('constants.systemError')}</Option>
</Select>
</Form.Item>
@@ -1012,7 +1016,7 @@ export default function AlarmCenterPage() {
</Form.Item>
<Form.Item style={{ textAlign: 'right', marginBottom: 0 }}>
<Button onClick={() => setEditModalVisible(false)}>取消</Button>
<Button onClick={() => setEditModalVisible(false)}>{t('common.cancel')}</Button>
<Button type="primary" htmlType="submit" style={{ marginLeft: 8 }}>
确认修改
</Button>
@@ -1038,11 +1042,11 @@ export default function AlarmCenterPage() {
onFinish={submitCreateOrder}
>
<Form.Item
label="工单标题"
label={t('workOrder.orderTitle')}
name="orderTitle"
rules={[{ required: true, message: '请输入工单标题' }]}
rules={[{ required: true, message: t('workOrder.pleaseInputTitle') }]}
>
<Input placeholder="请输入工单标题" />
<Input placeholder={t('workOrder.pleaseInputTitle')} />
</Form.Item>
<Form.Item
@@ -1059,7 +1063,7 @@ export default function AlarmCenterPage() {
<Form.Item style={{ textAlign: 'right', marginBottom: 0 }}>
<Button onClick={() => setCreateOrderModalVisible(false)}>取消</Button>
<Button onClick={() => setCreateOrderModalVisible(false)}>{t('common.cancel')}</Button>
<Button type="primary" htmlType="submit" style={{ marginLeft: 8 }}>
确认生成工单
</Button>

View File

@@ -4,6 +4,7 @@ import {
Modal, Descriptions, Input, Select
} from 'antd';
import { EyeOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
const AntdCard = Card as any;
const { Search } = Input;
@@ -17,6 +18,7 @@ const mockAlarms = [
];
const AlarmRealtime = () => {
const { t } = useTranslation();
// 弹窗控制
const [detailVisible, setDetailVisible] = useState(false);
const [currentAlarm, setCurrentAlarm] = useState<any>(null);
@@ -43,32 +45,32 @@ const AlarmRealtime = () => {
// 表格列
const columns = [
{ title: '时间', dataIndex: 'time', width: 180 },
{ title: t('common.time'), dataIndex: 'time', width: 180 },
{ title: '设备', dataIndex: 'deviceName' },
{
title: '级别',
render: (l: any) => {
const color = l.level === 'warn' ? 'orange' : l.level === 'error' ? 'red' : 'blue';
const text = l.level === 'warn' ? '警告' : l.level === 'error' ? '严重' : '提示';
const text = l.level === 'warn' ? t('constants.warning') : l.level === 'error' ? t('constants.critical') : t('constants.info');
return <Tag color={color}>{text}</Tag>;
}
},
{ title: '内容', dataIndex: 'content' },
{
title: '状态',
title: t('roles.status'),
render: (r: any) => {
const statusMap = {
unprocessed: { status: 'error', text: '未处理' },
unprocessed: { status: 'error', text: t('home.unhandled') },
processing: { status: 'warning', text: '处理中' },
processed: { status: 'success', text: '已处理' },
closed: { status: 'default', text: '已关闭' },
processed: { status: 'success', text: t('constants.handled') },
closed: { status: 'default', text: t('constants.closed') },
};
const s = statusMap[r.status] || statusMap.unprocessed;
return <Badge status={s.status as any} text={s.text} />;
}
},
{
title: '操作',
title: t('roles.actions'),
render: (_: any, record: any) => (
<Button type="link" icon={<EyeOutlined />} onClick={() => showDetail(record)}>
详情
@@ -80,7 +82,7 @@ const AlarmRealtime = () => {
return (
<div style={{ padding: 24 }}>
<AntdCard
title="实时告警"
title={t('alerts.realtimeAlert')}
extra={
<Search
placeholder="搜索设备名称/告警内容"
@@ -108,23 +110,23 @@ const AlarmRealtime = () => {
>
{currentAlarm && (
<Descriptions column={1} bordered>
<Descriptions.Item label="告警时间">{currentAlarm.time}</Descriptions.Item>
<Descriptions.Item label="设备名称">{currentAlarm.deviceName}</Descriptions.Item>
<Descriptions.Item label={t('alerts.alertTime')}>{currentAlarm.time}</Descriptions.Item>
<Descriptions.Item label={t('devices.deviceName')}>{currentAlarm.deviceName}</Descriptions.Item>
<Descriptions.Item label="告警内容">{currentAlarm.content}</Descriptions.Item>
<Descriptions.Item label="告警级别">
<Descriptions.Item label={t('alerts.alertLevel')}>
<Tag color={currentAlarm.level === 'warn' ? 'orange' : currentAlarm.level === 'error' ? 'red' : 'blue'}>
{currentAlarm.level === 'warn' ? '警告' : currentAlarm.level === 'error' ? '严重' : '提示'}
{currentAlarm.level === 'warn' ? t('constants.warning') : currentAlarm.level === 'error' ? t('constants.critical') : t('constants.info')}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="处理状态">
<Descriptions.Item label={t('alerts.handleStatus')}>
<Badge
status={
currentAlarm.status === 'unprocessed' ? 'error' :
currentAlarm.status === 'processing' ? 'warning' : 'success'
}
text={
currentAlarm.status === 'unprocessed' ? '未处理' :
currentAlarm.status === 'processing' ? '处理中' : '已处理'
currentAlarm.status === 'unprocessed' ? t('home.unhandled') :
currentAlarm.status === 'processing' ? '处理中' : t('constants.handled')
}
/>
</Descriptions.Item>

View File

@@ -7,6 +7,7 @@ import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
// 从你的设备接入页面 导入 物模型数据
//import { modelList, modelDetailList } from './DeviceAccessPage';
import { useTranslation } from 'react-i18next';
const AntdCard = Card as any;
const { Search } = Input;
@@ -18,6 +19,7 @@ const mockRules = [
];
const AlarmRules = () => {
const { t } = useTranslation();
const [list, setList] = useState(mockRules);
const [filteredList, setFilteredList] = useState(mockRules);
const [modalVisible, setModalVisible] = useState(false);
@@ -27,7 +29,7 @@ const AlarmRules = () => {
// 下拉选项
const [deviceTypeOpts, setDeviceTypeOpts] = useState([
{ value: 'sensor', label: '传感器' },
{ value: 'camera', label: '摄像头' },
{ value: 'camera', label: t('devices.camera') },
{ value: 'battery', label: '电池设备' },
]);
const [propList, setPropList] = useState<any[]>([]);
@@ -66,12 +68,12 @@ const AlarmRules = () => {
const updated = list.map(item => item.id === editingId ? { ...item, ...data } : item);
setList(updated);
setFilteredList(updated);
message.success('修改成功');
message.success(t('profile.modifySuccess'));
} else {
const newRule = { ...data, id: Date.now() };
setList([...list, newRule]);
setFilteredList([...list, newRule]);
message.success('新增成功');
message.success(t('common.addSuccess'));
}
setModalVisible(false);
});
@@ -82,7 +84,7 @@ const AlarmRules = () => {
const newList = list.filter(x => x.id !== id);
setList(newList);
setFilteredList(newList);
message.success('删除成功');
message.success(t('common.deleteSuccess'));
};
// 加载物模型属性
@@ -103,26 +105,26 @@ const AlarmRules = () => {
const columns = [
{ title: '规则名称', dataIndex: 'name' },
{ title: '设备类型', dataIndex: 'deviceType' },
{ title: t('devices.deviceType'), dataIndex: 'deviceType' },
{
title: '告警级别',
title: t('alerts.alertLevel'),
render: (r: any) => {
const color = r.level === 'error' ? 'red' : r.level === 'warn' ? 'orange' : 'blue';
const text = { error:'严重', warn:'警告', info:'提示' }[r.level];
const text = { error:t('constants.critical'), warn:t('constants.warning'), info:t('constants.info') }[r.level];
return <Tag color={color}>{text}</Tag>;
}
},
{ title: '触发条件', dataIndex: 'condition' },
{
title: '状态',
render: (r: any) => <Badge status={r.status ? 'success' : 'error'} text={r.status ? '启用' : '禁用'} />
title: t('roles.status'),
render: (r: any) => <Badge status={r.status ? 'success' : 'error'} text={r.status ? t('common.enable') : '禁用'} />
},
{
title: '操作',
title: t('roles.actions'),
render: (r: any) => (
<Space>
<Button type="link" icon={<EditOutlined />} onClick={() => openEdit(r)}>编辑</Button>
<Popconfirm onConfirm={() => handleDelete(r.id)}><Button type="link" danger>删除</Button></Popconfirm>
<Button type="link" icon={<EditOutlined />} onClick={() => openEdit(r)}>{t('roles.edit')}</Button>
<Popconfirm onConfirm={() => handleDelete(r.id)}><Button type="link" danger>roles.delete</Button></Popconfirm>
</Space>
)
}
@@ -134,7 +136,7 @@ const AlarmRules = () => {
title="告警规则配置"
extra={
<Space>
<Search placeholder="搜索" style={{ width: 260 }} onSearch={handleSearch} />
<Search placeholder={t('common.search')} style={{ width: 260 }} onSearch={handleSearch} />
<Button icon={<PlusOutlined />} type="primary" onClick={openAdd}>新增规则</Button>
</Space>
}
@@ -154,7 +156,7 @@ const AlarmRules = () => {
<Input placeholder="例如:温度超上限" />
</Form.Item>
<Form.Item label="设备类型" name="deviceType" rules={[{ required: true }]}>
<Form.Item label={t('devices.deviceType')} name="deviceType" rules={[{ required: true }]}>
<Select>
{deviceTypeOpts.map(o => <Select.Option key={o.value} value={o.value}>{o.label}</Select.Option>)}
</Select>
@@ -193,15 +195,15 @@ const AlarmRules = () => {
<Input placeholder="填写数值" />
</Form.Item>
<Form.Item label="告警级别" name="level" rules={[{ required: true }]}>
<Form.Item label={t('alerts.alertLevel')} name="level" rules={[{ required: true }]}>
<Select>
<Select.Option value="error">严重</Select.Option>
<Select.Option value="warn">警告</Select.Option>
<Select.Option value="info">提示</Select.Option>
<Select.Option value="error">{t('constants.critical')}</Select.Option>
<Select.Option value="warn">{t('constants.warning')}</Select.Option>
<Select.Option value="info">{t('constants.info')}</Select.Option>
</Select>
</Form.Item>
<Form.Item label="启用" name="status" valuePropName="checked">
<Form.Item label={t('common.enable')} name="status" valuePropName="checked">
<Switch />
</Form.Item>
</Form>

View File

@@ -10,8 +10,10 @@ import * as echarts from 'echarts';
// 接口引入
import { getAlarmStatistics } from '../../api/alarmApi.js';
import { useTranslation } from 'react-i18next';
const AlarmStatistics = () => {
const { t } = useTranslation();
const barChartRef = useRef(null);
const pieChartRef = useRef(null);
@@ -77,7 +79,7 @@ const AlarmStatistics = () => {
series: [{
type: 'pie',
radius: ['40%', '70%'],
data: pieData.length ? pieData : [{ value: 1, name: '暂无数据' }],
data: pieData.length ? pieData : [{ value: 1, name: t('common.noData') }],
color: ['#F53F3F', '#FF7D00', '#F7BA1E', '#165DFF'],
}]
};
@@ -97,7 +99,7 @@ const AlarmStatistics = () => {
return (
<div style={{ padding: 24, background: '#f5f7fa', minHeight: '100vh' }}>
<Card title="告警概览" bordered={false} style={{ marginBottom: 24, borderRadius: 8 }}>
<Card title={t('alerts.alertOverview')} bordered={false} style={{ marginBottom: 24, borderRadius: 8 }}>
<Row gutter={16} align="middle">
<Col xs={24} sm={12} md={6}>
<Space size={16}>
@@ -105,7 +107,7 @@ const AlarmStatistics = () => {
<BellOutlined style={{ fontSize: 24, color: '#F53F3F' }} />
</div>
<div>
<div style={{ fontSize: 14, color: '#4E5969' }}>告警总数</div>
<div style={{ fontSize: 14, color: '#4E5969' }}>{t('report.totalAlerts')}</div>
<div style={{ fontSize: 24, fontWeight: 700 }}>{statData.total || 0}</div>
</div>
</Space>
@@ -129,7 +131,7 @@ const AlarmStatistics = () => {
<ClockCircleOutlined style={{ fontSize: 24, color: '#FF7D00' }} />
</div>
<div>
<div style={{ fontSize: 14, color: '#4E5969' }}>待处理</div>
<div style={{ fontSize: 14, color: '#4E5969' }}>{t('constants.pending')}</div>
<div style={{ fontSize: 24, fontWeight: 700 }}>{statData.pending || 0}</div>
</div>
</Space>
@@ -141,7 +143,7 @@ const AlarmStatistics = () => {
<CheckCircleOutlined style={{ fontSize: 24, color: '#00B42A' }} />
</div>
<div>
<div style={{ fontSize: 14, color: '#4E5969' }}>已关闭</div>
<div style={{ fontSize: 14, color: '#4E5969' }}>{t('constants.closed')}</div>
<div style={{ fontSize: 24, fontWeight: 700 }}>{statData.closed || 0}</div>
</div>
</Space>

View File

@@ -4,6 +4,7 @@ import {
Modal, Form, Input, Select
} from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
const AntdCard = Card as any;
const { Option } = Select;
@@ -17,6 +18,7 @@ const mockSub = [
];
const AlarmSubscription = () => {
const { t } = useTranslation();
const [list, setList] = useState(mockSub);
const [filteredList, setFilteredList] = useState(mockSub);
const [modalVisible, setModalVisible] = useState(false);
@@ -77,19 +79,19 @@ const AlarmSubscription = () => {
const columns = [
{ title: '订阅名称', dataIndex: 'name' },
{ title: '设备类型', dataIndex: 'deviceType' },
{ title: '告警级别', dataIndex: 'level' },
{ title: t('devices.deviceType'), dataIndex: 'deviceType' },
{ title: t('alerts.alertLevel'), dataIndex: 'level' },
{
title: '通知渠道',
render: (r) => r.channels.map(i => <Tag key={i}>{i}</Tag>)
},
{
title: '操作',
title: t('roles.actions'),
render: (r) => (
<Space>
<Button type="link" icon={<EditOutlined />} onClick={() => openEdit(r)}>编辑</Button>
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(r.id)}>
<Button type="link" danger icon={<DeleteOutlined />}>删除</Button>
<Button type="link" icon={<EditOutlined />} onClick={() => openEdit(r)}>{t('roles.edit')}</Button>
<Popconfirm title={t('common.confirmDelete')} onConfirm={() => handleDelete(r.id)}>
<Button type="link" danger icon={<DeleteOutlined />}>{t('roles.delete')}</Button>
</Popconfirm>
</Space>
)
@@ -99,7 +101,7 @@ const AlarmSubscription = () => {
return (
<div style={{ padding: 24 }}>
<AntdCard
title="告警订阅"
title={t('alerts.alarmSubscription')}
extra={
<Space>
<Search
@@ -134,21 +136,21 @@ const AlarmSubscription = () => {
<Input placeholder="请输入订阅名称" />
</Form.Item>
<Form.Item label="设备类型" name="deviceType" rules={[{ required: true }]}>
<Form.Item label={t('devices.deviceType')} name="deviceType" rules={[{ required: true }]}>
<Select>
<Option value="all">全部设备</Option>
<Option value="sensor">传感器</Option>
<Option value="camera">摄像头</Option>
<Option value="camera">{t('devices.camera')}</Option>
<Option value="battery">电池设备</Option>
</Select>
</Form.Item>
<Form.Item label="告警级别" name="level" rules={[{ required: true }]}>
<Form.Item label={t('alerts.alertLevel')} name="level" rules={[{ required: true }]}>
<Select>
<Option value="all">全部级别</Option>
<Option value="error">严重</Option>
<Option value="warn">警告</Option>
<Option value="info">提示</Option>
<Option value="error">{t('constants.critical')}</Option>
<Option value="warn">{t('constants.warning')}</Option>
<Option value="info">{t('constants.info')}</Option>
</Select>
</Form.Item>

View File

@@ -20,6 +20,7 @@ import VideoPlayer from "./VideoPlayer.tsx";
import { wsManager, WSStatus } from "@/api/websocket/WebSocketManager.ts";
import Cookies from "js-cookie";
import DroneLivePlayer from './VolcRtcPlayer.jsx';
import { useTranslation } from 'react-i18next';
antdMessage.config({
top: 100,
@@ -66,15 +67,16 @@ const INITIAL_WAYPOINTS: Waypoint[] = [];
const StatusTagMap = {
working: { text: '工作中', color: 'bg-[#00e5ff]/10 text-[#00e5ff] border-[#00e5ff]/30' },
online: { text: '在线', color: 'bg-[#00e5ff]/10 text-[#00e5ff] border-[#00e5ff]/30' },
offline: { text: '离线', color: 'bg-[#ff4d4f]/10 text-[#ff4d4f] border-[#ff4d4f]/30' },
maintenance: { text: '维护', color: 'bg-[#ffad0d]/10 text-[#ffad0d] border-[#ffad0d]/30' },
online: { text: t('devices.online'), color: 'bg-[#00e5ff]/10 text-[#00e5ff] border-[#00e5ff]/30' },
offline: { text: t('devices.offline'), color: 'bg-[#ff4d4f]/10 text-[#ff4d4f] border-[#ff4d4f]/30' },
maintenance: { text: t('workOrder.maintainTask'), color: 'bg-[#ffad0d]/10 text-[#ffad0d] border-[#ffad0d]/30' },
configured: { text: '视频已配', color: 'bg-[#00e5ff]/10 text-[#00e5ff] border-[#00e5ff]/30' },
no_video: { text: '无视频', color: 'bg-[#6b7280]/10 text-[#9ca3af] border-[#6b7280]/30' },
failed: { text: '连接失败', color: 'bg-[#ff4d4f]/10 text-[#ff4d4f] border-[#ff4d4f]/30' }
};
const RobotControlCenter = ({ setView }) => {
const { t } = useTranslation();
const FILTERED_MOCKS = MOCK_DEVICES.filter(
item => item.type === 'robot' || item.type === 'mower' || item.type === 'cut'
);
@@ -937,7 +939,7 @@ const RobotControlCenter = ({ setView }) => {
const _list = res.data.data.list.map(item => {
return {
...item,
label: item.name || `航线_${item.id || '未知'}`,
label: item.name || `航线_${item.id || t('constants.locationUnknown')}`,
value: item.id
}
})
@@ -1160,7 +1162,7 @@ const RobotControlCenter = ({ setView }) => {
onClick={() => setMowerView(v as any)}
className={`px-2 py-1 text-[10px] rounded-md ${mowerView === v ? 'bg-blue-500 text-white' : 'text-slate-300'}`}
>
{v === 'front' ? '前' : v === 'back' ? '后' : v === 'left' ? '左' : v === 'right' ? '右' : v === 'top' ? '上' : '全部'}
{v === 'front' ? '前' : v === 'back' ? '后' : v === 'left' ? '左' : v === 'right' ? '右' : v === 'top' ? '上' : t('common.all')}
</button>
))}
</div>
@@ -1173,7 +1175,7 @@ const RobotControlCenter = ({ setView }) => {
onClick={() => setMowerView(v as any)}
className={`px-2 py-1 text-[10px] rounded-md ${mowerView === v ? 'bg-blue-500 text-white' : 'text-slate-300'}`}
>
{v === 'front' ? '前' : v === 'back' ? '后' : v === 'left' ? '左' : v === 'right' ? '右' : '全部'}
{v === 'front' ? '前' : v === 'back' ? '后' : v === 'left' ? '左' : v === 'right' ? '右' : t('common.all')}
</button>
))}
</div>
@@ -1383,7 +1385,7 @@ const RobotControlCenter = ({ setView }) => {
<h3 className="font-medium text-white text-sm flex items-center gap-1.5">
{device?.type === 'plane' && <Box className="w-4 h-4 text-blue-400" />}
{device.name}
<span className="text-[10px] text-slate-400">[{device?.type == 'plane' ? '无人机' : device?.type === 'mower' ? '割草机' : '巡检机'}]</span>
<span className="text-[10px] text-slate-400">[{device?.type == 'plane' ? t('constants.uav') : device?.type === 'mower' ? t('constants.mower') : '巡检机'}]</span>
<span className="text-[8px] text-slate-400">[{device?.type == 'plane' ? '支持GPS规划' : "支持摇控、GPS规划"}]</span>
</h3>
<span className={`text-xs px-2 py-1 rounded-full border ${StatusTagMap[device?.status]?.color}`}>
@@ -1501,7 +1503,7 @@ const RobotControlCenter = ({ setView }) => {
<p className="text-sm font-bold text-white">{selectedDevice?.battery}% / 92°</p>
</div>
<div className="bg-white/5 backdrop-blur-sm rounded-xl p-4 border border-[#243157]/40 shadow-sm">
<p className="text-xs text-[#9ca3af] mb-1.5">速度</p>
<p className="text-xs text-[#9ca3af] mb-1.5">{t('devices.speed')}</p>
<p className="text-sm font-bold text-white">{selectedDevice?.speed} m/s</p>
</div>
<div className="bg-white/5 backdrop-blur-sm rounded-xl p-4 border border-[#243157]/40 shadow-sm flex items-center justify-between">
@@ -1547,7 +1549,7 @@ const RobotControlCenter = ({ setView }) => {
<p className="text-lg font-bold text-white">{selectedDevice.gpsPlan}</p>
</div>
<div className="bg-white/5 backdrop-blur-sm rounded-xl p-4 border border-[#243157]/40 shadow-sm">
<p className="text-xs text-[#9ca3af] mb-1.5">任务状态</p>
<p className="text-xs text-[#9ca3af] mb-1.5">{t('devices.taskStatus')}</p>
<p className="text-lg font-bold text-white">{selectedDevice.taskStatus}</p>
</div>
</div>*/}
@@ -1702,14 +1704,14 @@ hover:overflow-y-auto"> {planList.length > 0 ? planList.map((task, index
const statusMap = {
"waiting": { text: "待开始", color: "bg-yellow-500" },
"starting_failure": { text: "启动失败", color: "bg-red-500" },
"executing": { text: "执行中", color: "bg-blue-500" },
"suspended": { text: "已挂起", color: "bg-orange-500" },
"restored": { text: "执行中", color: "bg-blue-500" },
"executing": { text: t('workOrder.inProgress'), color: "bg-blue-500" },
"suspended": { text: t('workOrder.suspended'), color: "bg-orange-500" },
"restored": { text: t('workOrder.inProgress'), color: "bg-blue-500" },
"terminated": { text: "终止", color: "bg-black" },
"success": { text: "成功", color: "bg-green-500" },
"success": { text: t('common.success'), color: "bg-green-500" },
"timeout": { text: "超时", color: "bg-red-600" }
};
const status = statusMap[task.status] || { text: "未知", color: "bg-gray-400" };
const status = statusMap[task.status] || { text: t('constants.locationUnknown'), color: "bg-gray-400" };
const isSelected = selectedTask?.uuid === task.uuid;
return (
@@ -1724,7 +1726,7 @@ hover:overflow-y-auto"> {planList.length > 0 ? planList.map((task, index
>
<div className="flex justify-between items-center mb-1">
<div className="text-[10px] text-slate-400 truncate w-[70%]">{task.name}</div>
<Tooltip title="查看详情">
<Tooltip title={t('common.viewDetail')}>
<Eye className="w-3 h-3 text-blue-400 cursor-pointer" onClick={(e) => {
e.stopPropagation();
getTaskDetail(task.uuid);
@@ -1762,7 +1764,7 @@ hover:overflow-y-auto"> {planList.length > 0 ? planList.map((task, index
<div className="text-xs truncate text-white">{task.workName}</div>
</div>
)) : (
<div className="col-span-2 text-center text-slate-400 text-xs p-3">暂无数据</div>
<div className="col-span-2 text-center text-slate-400 text-xs p-3">{t('common.noData')}</div>
)}
</div>
</div>
@@ -1841,7 +1843,7 @@ hover:overflow-y-auto"> {planList.length > 0 ? planList.map((task, index
</button>
<button onClick={toggleTaskRunning} className="px-4 py-2 bg-blue-600 hover:bg-blue-500 rounded-lg text-white flex items-center gap-2">
{taskRunning ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
{taskRunning ? "暂停" : "执行"}
{taskRunning ? t('video.pause') : "执行"}
</button>
<button onClick={stopGPSTask} className="px-4 py-2 bg-red-600 hover:bg-red-500 rounded-lg text-white flex items-center gap-2">
<Square className="w-4 h-4" /> 停止
@@ -1888,7 +1890,7 @@ hover:overflow-y-auto"> {planList.length > 0 ? planList.map((task, index
disabled={flightCommandLoading}
>
<Square className="w-5 h-5" />
<span className="text-[10px] font-bold uppercase">暂停</span>
<span className="text-[10px] font-bold uppercase">{t('video.pause')}</span>
</button>
)}
{/* 动态:返航 / 取消返航 */}
@@ -1908,7 +1910,7 @@ hover:overflow-y-auto"> {planList.length > 0 ? planList.map((task, index
disabled={flightCommandLoading}
>
<MapPin className="w-5 h-5" />
<span className="text-[10px] font-bold uppercase">返航</span>
<span className="text-[10px] font-bold uppercase">{t('devices.returnHome')}</span>
</button>
)}
</div>
@@ -1920,7 +1922,7 @@ hover:overflow-y-auto"> {planList.length > 0 ? planList.map((task, index
</button>
<button onClick={toggleTaskRunning} className="px-4 py-2 bg-blue-600 hover:bg-blue-500 rounded-lg text-white flex items-center gap-2">
{taskRunning ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
{taskRunning ? "暂停" : "执行"}
{taskRunning ? t('video.pause') : "执行"}
</button>
<button onClick={stopGPSTask} className="px-4 py-2 bg-red-600 hover:bg-red-500 rounded-lg text-white flex items-center gap-2">
<Square className="w-4 h-4" /> 停止
@@ -2108,7 +2110,7 @@ hover:overflow-y-auto"> {planList.length > 0 ? planList.map((task, index
// 字段中文名映射
const keyMap = {
name: '任务名称',
name: t('devices.taskName'),
uuid: '任务ID',
wayline_uuid: '航线UUID',
sn: '机场SN',
@@ -2120,11 +2122,11 @@ hover:overflow-y-auto"> {planList.length > 0 ? planList.map((task, index
resumable_status: '断点续飞',
task_type: '任务类型',
repeat_type: '重复模式',
begin_at: '开始时间',
end_at: '结束时间',
begin_at: t('devices.startTime'),
end_at: t('devices.endTime'),
min_battery_capacity: '最低电量(%)',
status: '任务状态',
time_zone: '时区',
status: t('devices.taskStatus'),
time_zone: t('systemSetting.timezone'),
current_waypoint_index: '当前航点',
};
@@ -2149,11 +2151,11 @@ hover:overflow-y-auto"> {planList.length > 0 ? planList.map((task, index
status: {
waiting: '待开始',
starting_failure: '启动失败',
executing: '执行中',
suspended: '已挂起',
executing: t('workOrder.inProgress'),
suspended: t('workOrder.suspended'),
restored: '继续执行',
terminated: '已终止',
success: '已完成',
success: t('workOrder.completed'),
timeout: '执行超时',
},
};
@@ -2181,7 +2183,7 @@ hover:overflow-y-auto"> {planList.length > 0 ? planList.map((task, index
<div className="flex flex-col items-center justify-center h-full text-black">
{/* 旋转加载圈 */}
<div className="w-6 h-6 border-2 border-gray-500 border-t-white rounded-full animate-spin mb-2"></div>
<p className="text-sm text-blue">加载中...</p>
<p className="text-sm text-blue">{t('common.loading')}</p>
</div>
)}
</div>
@@ -2378,7 +2380,7 @@ hover:overflow-y-auto"> {planList.length > 0 ? planList.map((task, index
</div>
<div className="flex justify-end gap-2 mt-4">
<Button onClick={resetModals}>取消</Button>
<Button onClick={resetModals}>{t('common.cancel')}</Button>
<Button type="primary" loading={loading} onClick={createFlightTask}>创建航线任务</Button>
</div>
</div>
@@ -2389,8 +2391,8 @@ hover:overflow-y-auto"> {planList.length > 0 ? planList.map((task, index
open={isModalOpen}
onOk={handleOk}
onCancel={handleCancel}
okText="确定"
cancelText="取消"
okText={t('common.ok')}
cancelText={t('common.cancel')}
>
<p>{modalcontent.content}</p>

View File

@@ -35,6 +35,7 @@ import { useSelector } from 'react-redux';
import { RootState } from '@/store';
import envConfig from '../../../env';
import utils from '@/lib/utils.ts';
import { useTranslation } from 'react-i18next';
const { Content } = Layout;
const { Text, Title } = Typography;
@@ -48,16 +49,9 @@ const taskTimeline = [
{ time: '09:58:10', color: 'green', text: 'RBT-02 完成巡检航线 P-01', status: '任务完成' },
];
const logs = [
{ time: '10:24:35', device: 'UAV-C01', type: '命令', desc: '起飞指令下发,开始作业' },
{ time: '10:21:40', device: 'RBT-01', type: '命令', desc: '开始巡检任务 P-02' },
{ time: '10:20:11', device: 'UAV-C02', type: '命令', desc: '转入待命状态' },
{ time: '10:18:05', device: 'GR-02', type: '命令', desc: '除草作业开始 G-01' },
{ time: '10:12:35', device: 'UAV-C01', type: '告警', desc: '返航补水' },
{ time: '10:05:22', device: 'RBT-01', type: '事件', desc: '电量较低,建议回充', level: '低电量' },
];
export default function DeviceOverviewPage() {
const { t } = useTranslation();
const { userInfo } = useSelector((state: RootState) => state.user);
const { stationId, currentStation } = useSelector((state: RootState) => state.station);
@@ -87,6 +81,16 @@ export default function DeviceOverviewPage() {
const navigate = useNavigate();
const [robotPlayActive, setRobotPlayActive] = useState<boolean>(false);
const logs = [
{ time: '10:24:35', device: 'UAV-C01', type: '命令', desc: '起飞指令下发,开始作业' },
{ time: '10:21:40', device: 'RBT-01', type: '命令', desc: '开始巡检任务 P-02' },
{ time: '10:20:11', device: 'UAV-C02', type: '命令', desc: '转入待命状态' },
{ time: '10:18:05', device: 'GR-02', type: '命令', desc: '除草作业开始 G-01' },
{ time: '10:12:35', device: 'UAV-C01', type: t('constants.sourceAlert'), desc: '返航补水' },
{ time: '10:05:22', device: 'RBT-01', type: '事件', desc: '电量较低,建议回充', level: '低电量' },
];
// 获取数据
const fetchAllData = async () => {
@@ -113,7 +117,7 @@ export default function DeviceOverviewPage() {
latitude: item.latitude || 0,
},
deviceAlias: item.drone_callsign || item.device_sn
|| '无人机',
|| t('constants.uav'),
}));
@@ -447,12 +451,12 @@ export default function DeviceOverviewPage() {
const totalDrones = planedevices?.length || 0;
const onlineDrones = planedevices?.filter(d => d.onlineStatus === 1).length || 0;
const totalRobots = 1;
//const totalRobots = devicesAll?.filter(d => d.productName?.includes('巡检')).length || 0;
//const totalRobots = devicesAll?.filter(d => d.productName?.includes(t('constants.inspection'))).length || 0;
//key 0 有任务 1 无任务
const activeRobots = 0;
const totalMowers = 1;
//const totalMowers = devicesAll?.filter(d => d.productName?.includes('割草机')).length || 0;
//const totalMowers = devicesAll?.filter(d => d.productName?.includes(t('constants.mower'))).length || 0;
const activeMowers = devices?.filter(d => d.feStatus == 1).length || 0; // 作业中状态数量
const washDroneStandby = planedevices?.[0]?.modeStatistics?.["0"] || 0;
//key 设备状态:{"0":"空闲中","1":"现场调试","2":"远程调试","3":"固件升级中","4":"作业中"}
@@ -474,7 +478,7 @@ export default function DeviceOverviewPage() {
iconBg: '#E8F3FF'
},
{
title: '巡检机器人',
title: t('home.inspectionRobot'),
icon: <RobotOutlined style={{ color: '#13c2c2', fontSize: 24 }} />,
value: `${totalRobots} 台`,
sub: `执行中 ${activeRobots}`,
@@ -522,9 +526,9 @@ export default function DeviceOverviewPage() {
...d,
id: d.deviceId || index,
name: d.deviceAlias || d.deviceName || d.callsign || '未知设备',
type: d.productName || '无人机',
status: d.onlineStatus === 1 ? '在线' : '离线',
mode: d.feStatus == 1 ? '作业中' : d.feStatus == 2 ? '空闲' : d.mode_code == 0 ? '空闲' : d.mode_code == 1 ? '现场调试' : d.mode_code == 2 ? '远程调试' : d.mode_code == 3 ? '固件升级中' : '作业中',
type: d.productName || t('constants.uav'),
status: d.onlineStatus === 1 ? t('devices.online') : t('devices.offline'),
mode: d.feStatus == 1 ? '作业中' : d.feStatus == 2 ? t('devices.idle') : d.mode_code == 0 ? t('devices.idle') : d.mode_code == 1 ? '现场调试' : d.mode_code == 2 ? '远程调试' : d.mode_code == 3 ? '固件升级中' : '作业中',
capacity_percent: d.capacity_percent || 100,
position: d.position || '站内',
task: d.status === 4 ? '待命' : '作业中',
@@ -644,13 +648,13 @@ export default function DeviceOverviewPage() {
<Table rowKey={(record) => record.id || record.serialNumber || record.device_sn} size="small" pagination={false}
columns={[
{ title: '装备名称', dataIndex: 'name', width: 140 },
{ title: '类型', dataIndex: 'type', width: 90 },
{ title: t('common.type'), dataIndex: 'type', width: 90 },
{
title: '当前状态',
title: t('workOrder.currentStatus'),
dataIndex: 'onlineStatus',
width: 70,
render: (s) => {
const text = s === 1 ? '在线' : '离线';
const text = s === 1 ? t('devices.online') : t('devices.offline');
const color = s === 1 ? 'success' : 'default';
return <Tag color={color} style={{ margin: 0 }}>{text}</Tag>;
}
@@ -658,7 +662,7 @@ export default function DeviceOverviewPage() {
title: '模式', dataIndex: 'mode', width: 70, render: (text) => {
let color = 'default';
if (text === '作业中') color = 'processing';
if (text === '空闲') color = 'success';
if (text === t('devices.idle')) color = 'success';
if (text === '空闲中') color = 'success';
if (text === '现场调试') color = 'warning';
if (text === '远程调试') color = 'warning';
@@ -667,7 +671,7 @@ export default function DeviceOverviewPage() {
}
},
{
title: '电量',
title: t('devices.batteryLevel'),
dataIndex: 'lastRunningStatus',
width: 70,
render: (lastRunningStatus) => {
@@ -678,11 +682,11 @@ export default function DeviceOverviewPage() {
status={v < 40 ? 'exception' : 'active'}
/>;
}
}, { title: '位置', dataIndex: 'position', width: 90 },
}, { title: t('aiAnalysis.location'), dataIndex: 'position', width: 90 },
//{ title: '当前任务', dataIndex: 'task', width: 100 },
{ title: '通讯状态', dataIndex: 'comm', width: 100, render: v => <Badge status={v.includes('-') ? 'warning' : 'success'} text={v} /> },
{
title: '操作',
title: t('roles.actions'),
key: 'action',
width: 150,
//fixed: 'right',
@@ -694,7 +698,7 @@ export default function DeviceOverviewPage() {
type="text"
icon={<EditOutlined />}
onClick={() => openEditRobotModal(record)}
title="编辑"
title={t('roles.edit')}
/>
<Tooltip title="设备参数设置">
<Button
@@ -718,10 +722,10 @@ export default function DeviceOverviewPage() {
<Card title={<><SettingOutlined /> <span >控制日志 / 告警记录</span></>} bordered={false} style={{ borderRadius: 8 }} extra={<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><Button size="small" style={{ borderRadius: 4, border: '1px solid #d9d9d9' }}>全部级别 ▼</Button></div>}>
<Table rowKey="time" size="small" pagination={false} dataSource={logs} scroll={{ x: 'max-content' }}
columns={[
{ title: '时间', dataIndex: 'time', width: 90, render: t => <span style={{ fontSize: 13 }}>{t}</span> },
{ title: t('common.time'), dataIndex: 'time', width: 90, render: t => <span style={{ fontSize: 13 }}>{t}</span> },
{ title: '设备', dataIndex: 'device', width: 100, render: t => <span style={{ fontSize: 13 }}>{t}</span> },
{ title: '类型', dataIndex: 'type', width: 70, render: t => { let c = '#52c41a'; if (t === '告警') c = '#fa8c16'; if (t === 'events') c = '#1890ff'; return <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}><span style={{ width: 10, height: 10, borderRadius: 2, background: c }} /><span style={{ fontSize: 13 }}>{t}</span></div> } },
{ title: '描述', dataIndex: 'desc', render: t => <span style={{ fontSize: 13 }}>{t}</span> },
{ title: t('common.type'), dataIndex: 'type', width: 70, render: r => { let c = '#52c41a'; if (r === t('constants.sourceAlert')) c = '#fa8c16'; if (t === 'events') c = '#1890ff'; return <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}><span style={{ width: 10, height: 10, borderRadius: 2, background: c }} /><span style={{ fontSize: 13 }}>{t}</span></div> } },
{ title: t('common.description'), dataIndex: 'desc', render: t => <span style={{ fontSize: 13 }}>{t}</span> },
{ title: '', width: 70, render: (_, r) => r.level ? <Tag color="red" style={{ fontSize: 11, margin: 0 }}>{r.level}</Tag> : null }
]}
/>
@@ -745,8 +749,8 @@ export default function DeviceOverviewPage() {
String(option?.label || "").toLowerCase().includes(input.toLowerCase())
}
options={[
{ label: <span>机器人</span>, options: devices.map(item => ({ value: item.device_sn || item.serialNumber, label: item.deviceAlias || item.deviceName || '未知设备' })) },
{ label: <span>无人机</span>, options: planedevices.map(item => ({ value: item.device_sn, label: item.drone_callsign || item.callsign || '未知设备' })) },
{ label: <span>devices.robot</span>, options: devices.map(item => ({ value: item.device_sn || item.serialNumber, label: item.deviceAlias || item.deviceName || '未知设备' })) },
{ label: <span>constants.uav</span>, options: planedevices.map(item => ({ value: item.device_sn, label: item.drone_callsign || item.callsign || '未知设备' })) },
]}
/>
@@ -755,14 +759,14 @@ export default function DeviceOverviewPage() {
<Col span={12}>
<div style={{ marginBottom: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 12, flexWrap: "wrap" }}>
{selectedDevice.deviceType === 'drone' || selectedDevice.deviceName?.includes('无人机') || selectedDevice.drone_callsign ? (
{selectedDevice.deviceType === 'drone' || selectedDevice.deviceName?.includes(t('constants.uav')) || selectedDevice.drone_callsign ? (
<span style={{ fontSize: 20, color: '#1890ff' }}><RocketOutlined /></span>
) : (
<span style={{ fontSize: 20, color: '#1890ff' }}><CarOutlined /></span>
)}
<span style={{ fontSize: 14, fontWeight: 600 }}>{selectedDevice.deviceAlias || selectedDevice.drone_callsign || selectedDevice.callsign || selectedDevice.deviceName}</span>
<Tag color={selectedDevice.onlineStatus === 1 ? "success" : "default"} size="small" style={{ margin: 0 }}>
{selectedDevice.onlineStatus == 1 ? "在线" : "离线"}
{selectedDevice.onlineStatus == 1 ? t('devices.online') : t('devices.offline')}
</Tag>
<Button
type="primary" size="small" ghost icon={<InfoCircleOutlined />}
@@ -778,7 +782,7 @@ export default function DeviceOverviewPage() {
</div>
{realtimeDevice?.environment_temperature && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 12 }}>温度</span>
<span style={{ fontSize: 12 }}>{t('home.temperature')}</span>
<span style={{ color: '#faad14' }}>{fix2(realtimeDevice.environment_temperature || 25)}℃</span>
</div>
)}
@@ -809,7 +813,7 @@ export default function DeviceOverviewPage() {
<span style={{ fontSize: 12 }}>v{selectedDevice.firmwareVersion}</span>
</div>
)}
{(selectedDevice.productName?.includes('无人机') || selectedDevice.device_sn) && (
{(selectedDevice.productName?.includes(t('constants.uav')) || selectedDevice.device_sn) && (
<>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#666', fontSize: 12 }}>飞行高度</span>
@@ -835,8 +839,8 @@ export default function DeviceOverviewPage() {
<div style={{ display: 'flex', justifyContent: "space-between", flexWrap: 'wrap', gap: 6 }}>
<Button type="primary" style={{ borderRadius: 8, flex: 1 }} onClick={handleStart}>启动</Button>
<Button style={{ borderRadius: 8, flex: 1 }} onClick={handlePause}>暂停</Button>
<Button type="primary" style={{ borderRadius: 8, flex: 1 }} onClick={handleReturn}>返航</Button>
<Button style={{ borderRadius: 8, flex: 1 }} onClick={handlePause}>{t('video.pause')}</Button>
<Button type="primary" style={{ borderRadius: 8, flex: 1 }} onClick={handleReturn}>{t('devices.returnHome')}</Button>
<Button style={{ flex: 1, background: '#52c41a', borderColor: '#52c41a', color: "#fff", borderRadius: 8, }} onClick={handleRecharge}>回充</Button>
</div>
</div>
@@ -973,12 +977,12 @@ export default function DeviceOverviewPage() {
</Row>
</Card>
{(selectedDevice?.productName?.includes('无人机') || selectedDevice?.device_sn) && (
{(selectedDevice?.productName?.includes(t('constants.uav')) || selectedDevice?.device_sn) && (
<Card title="环境与飞行条件" bordered={false} style={{ borderRadius: 12, marginBottom: 8 }}>
<div style={{ display: "flex", justifyContent: "space-between", gap: 12, alignItems: "center" }}>
<div style={{ display: "flex", border: "1px solid #e8e8e8", borderRadius: 12, overflow: "hidden", flex: 1 }}>
<div style={{ flex: 1, textAlign: 'center', whiteSpace: "nowrap", padding: "12px 4px", borderRight: "1px solid #e8e8e8" }}><div style={{ fontSize: 11, color: '#666', fontWeight: 600 }}>风速</div><div style={{ fontSize: 14, fontWeight: 700, color: '#222', marginTop: 2 }}>{fix2(realtimeDevice?.wind_speed)} m/s</div></div>
<div style={{ flex: 1, textAlign: 'center', whiteSpace: "nowrap", padding: "12px 4px", borderRight: "1px solid #e8e8e8" }}><div style={{ fontSize: 11, color: '#666', fontWeight: 600 }}>温度</div><div style={{ fontSize: 14, fontWeight: 700, color: '#222', marginTop: 2 }}>{fix2(realtimeDevice?.environment_temperature)}℃</div></div>
<div style={{ flex: 1, textAlign: 'center', whiteSpace: "nowrap", padding: "12px 4px", borderRight: "1px solid #e8e8e8" }}><div style={{ fontSize: 11, color: '#666', fontWeight: 600 }}>{t('home.temperature')}</div><div style={{ fontSize: 14, fontWeight: 700, color: '#222', marginTop: 2 }}>{fix2(realtimeDevice?.environment_temperature)}℃</div></div>
<div style={{ flex: 1, textAlign: 'center', whiteSpace: "nowrap", padding: "12px 4px", borderRight: "1px solid #e8e8e8" }}><div style={{ fontSize: 11, color: '#666', fontWeight: 600 }}>降雨</div><div style={{ fontSize: 14, fontWeight: 700, color: '#222', marginTop: 2 }}>{getVal(realtimeDevice?.rainfall)}</div></div>
<div style={{ flex: 1, textAlign: 'center', whiteSpace: "nowrap", padding: "12px 4px" }}><div style={{ fontSize: 11, color: '#666', fontWeight: 600 }}>RTK</div><div style={{ fontSize: 14, fontWeight: 700, color: '#52c41a', marginTop: 2 }}>{realtimeDevice?.position_state?.is_fixed === 'fixing_successful' ? '固定' : '未固定'}</div></div>
</div>
@@ -993,7 +997,7 @@ export default function DeviceOverviewPage() {
</Card>
)}
{(!selectedDevice?.productName?.includes('无人机') && !selectedDevice?.device_sn) && (
{(!selectedDevice?.productName?.includes(t('constants.uav')) && !selectedDevice?.device_sn) && (
<Card title={<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 14, fontWeight: 600 }}><RobotOutlined style={{ color: '#165DFF' }} />机器人作业状态</div>} bordered={false} style={{ borderRadius: 12, marginBottom: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.06)', border: '1px solid #f0f0f0' }}>
<Row gutter={16} style={{ alignItems: 'center' }}>
<Col span={8}>
@@ -1090,7 +1094,7 @@ export default function DeviceOverviewPage() {
</div>
<div style={{ marginBottom: 16 }}>
<Typography.Text strong>设备类型:</Typography.Text>
<Typography.Text>{currentEditRobot.productName || '未知'}</Typography.Text>
<Typography.Text>{currentEditRobot.productName || t('constants.locationUnknown')}</Typography.Text>
</div>
<div style={{ marginBottom: 8 }}>
<Typography.Text strong>设备别名 <span style={{ color: 'red' }}>*</span></Typography.Text>

View File

@@ -14,8 +14,10 @@ import { useSelector } from 'react-redux';
import { RootState } from '@/store';
import { deviceRunParamSelectByDeviceId, deviceRunParamSave } from '@/api/stationManage/index';
import utils from '@/lib/utils';
import { useTranslation } from 'react-i18next';
const GainSliderInput = ({ value, onChange, disabled, min = 0.1, max = 1, step = 0.01 }) => {
const { t } = useTranslation();
const handleSliderChange = (val) => {
onChange?.(val);
};
@@ -69,55 +71,7 @@ const GainSliderInput = ({ value, onChange, disabled, min = 0.1, max = 1, step =
);
};
const paramFieldConfigs = [
{ key: 'leftForwardGain', label: '左轮前进增益', type: 'InputNumber', editable: true, min: 0, max: 1, step: 0.01 },
{ key: 'leftBackwardGain', label: '左轮后退增益', type: 'InputNumber', editable: true, min: 0, max: 1, step: 0.01 },
{ key: 'rightForwardGain', label: '右轮前进增益', type: 'InputNumber', editable: true, min: 0, max: 1, step: 0.01 },
{ key: 'rightBackwardGain', label: '右轮后退增益', type: 'InputNumber', editable: true, min: 0, max: 1, step: 0.01 },
{ key: 'runSpeed', label: '运行速度', type: 'InputNumber', editable: true },
{ key: 'chipUidSign', label: '芯片UID固化标志位', type: 'InputNumber', editable: false },
{ key: 'chipUid', label: '芯片UID', type: 'Input', editable: false },
{ key: 'remoteConfig', label: '遥控器通道配置', type: 'InputNumber', editable: false },
{ key: 'knifeMotorMode', label: '割刀电机模式', type: 'InputNumber', editable: false },
{ key: 'walkMotorMode', label: '行走电机模式', type: 'InputNumber', editable: false },
{ key: 'leftMotorReverse', label: '左轮电机方向极性', type: 'InputNumber', editable: false },
{ key: 'rightMotorReverse', label: '右轮电机方向极性', type: 'InputNumber', editable: false },
{ key: 'swapChannel', label: '左右轮通道互换标志', type: 'InputNumber', editable: false },
{ key: 'use4G', label: '联网标志位', type: 'InputNumber', editable: false },
{ key: 'forwardSpeedLimit', label: '前进速度限制', type: 'InputNumber', editable: false },
{ key: 'turnSpeedLimit', label: '转向速度限制', type: 'InputNumber', editable: false },
{ key: 'knifePolarity', label: '割刀通道极性', type: 'InputNumber', editable: false },
{ key: 'fanPolarity', label: '风门通道极性', type: 'InputNumber', editable: false },
{ key: 'throttlePolarity', label: '油门通道极性', type: 'InputNumber', editable: false },
{ key: 'liftProtectTime', label: '底盘升降电机保护时间', type: 'InputNumber', editable: false },
{ key: 'dualRtk', label: 'RTK配置', type: 'InputNumber', editable: false },
{ key: 'knifeChannel', label: '割刀通道配置', type: 'InputNumber', editable: false },
{ key: 'fanChannel', label: '风门通道配置', type: 'InputNumber', editable: false },
{ key: 'throttleChannel', label: '油门通道配置', type: 'InputNumber', editable: false },
{ key: 'remoteType', label: '遥控器类型', type: 'InputNumber', editable: false },
{ key: 'relayBoard', label: '是否搭载继电器板', type: 'Checkbox', editable: false },
{ key: 'liftChannel', label: '底盘升通道配置', type: 'InputNumber', editable: false },
{ key: 'chassisChannel', label: '底盘降通道配置', type: 'InputNumber', editable: false },
{ key: 'armChannel', label: '抱闸通道配置', type: 'InputNumber', editable: false },
{ key: 'fuelPumpChannel', label: '燃油励磁通道配置', type: 'InputNumber', editable: false },
{ key: 'wifiName', label: 'wifi名称', type: 'Input', editable: false },
{ key: 'wifiPassword', label: 'wifi密码', type: 'Input', editable: false },
{ key: 'batteryType', label: '电池类型', type: 'InputNumber', editable: false },
{ key: 'driveType', label: '行走驱动配置', type: 'InputNumber', editable: false },
{ key: 'gearRatio', label: '转速比', type: 'InputNumber', editable: false, step: 0.01 },
{ key: 'robotLength', label: '机器人长度', type: 'InputNumber', editable: false, step: 0.01 },
{ key: 'robotWidth', label: '机器人宽度', type: 'InputNumber', editable: false, step: 0.01 },
{ key: 'robotHeight', label: '机器人高度', type: 'InputNumber', editable: false, step: 0.01 },
{ key: 'knifeWidth', label: '割刀宽度', type: 'InputNumber', editable: false, step: 0.01 },
{ key: 'tyreSize', label: '轮胎尺寸', type: 'InputNumber', editable: false, step: 0.01 },
{ key: 'firmwareVersion', label: '固件版本', type: 'Input', editable: false },
{ key: 'crc16', label: 'CRC16', type: 'InputNumber', editable: false },
{ key: 'tail1', label: 'tail1', type: 'InputNumber', editable: false },
{ key: 'tail2', label: 'tail2', type: 'InputNumber', editable: false },
{ key: 'remark', label: '备注', type: 'Input', editable: false },
{ key: 'delFlag', label: '删除标志', type: 'Checkbox', editable: false },
];
const editableKeys = ['leftForwardGain', 'leftBackwardGain', 'rightForwardGain', 'rightBackwardGain'];
@@ -141,6 +95,8 @@ const DeviceParamModal: React.FC<DeviceParamModalProps> = ({ open, device, onCan
const [paramForm] = Form.useForm();
const [paramData, setParamData] = useState<any>(null);
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
const deviceId = device?.serialNumber || device?.device_sn || '';
const deviceName = device?.deviceAlias || device?.deviceName || device?.drone_callsign || device?.callsign || deviceId;
@@ -211,6 +167,55 @@ const DeviceParamModal: React.FC<DeviceParamModalProps> = ({ open, device, onCan
paramForm.resetFields();
onCancel?.();
};
const paramFieldConfigs = [
{ key: 'leftForwardGain', label: '左轮前进增益', type: 'InputNumber', editable: true, min: 0, max: 1, step: 0.01 },
{ key: 'leftBackwardGain', label: '左轮后退增益', type: 'InputNumber', editable: true, min: 0, max: 1, step: 0.01 },
{ key: 'rightForwardGain', label: '右轮前进增益', type: 'InputNumber', editable: true, min: 0, max: 1, step: 0.01 },
{ key: 'rightBackwardGain', label: '右轮后退增益', type: 'InputNumber', editable: true, min: 0, max: 1, step: 0.01 },
{ key: 'runSpeed', label: '运行速度', type: 'InputNumber', editable: true },
{ key: 'chipUidSign', label: '芯片UID固化标志位', type: 'InputNumber', editable: false },
{ key: 'chipUid', label: '芯片UID', type: 'Input', editable: false },
{ key: 'remoteConfig', label: '遥控器通道配置', type: 'InputNumber', editable: false },
{ key: 'knifeMotorMode', label: '割刀电机模式', type: 'InputNumber', editable: false },
{ key: 'walkMotorMode', label: '行走电机模式', type: 'InputNumber', editable: false },
{ key: 'leftMotorReverse', label: '左轮电机方向极性', type: 'InputNumber', editable: false },
{ key: 'rightMotorReverse', label: '右轮电机方向极性', type: 'InputNumber', editable: false },
{ key: 'swapChannel', label: '左右轮通道互换标志', type: 'InputNumber', editable: false },
{ key: 'use4G', label: '联网标志位', type: 'InputNumber', editable: false },
{ key: 'forwardSpeedLimit', label: '前进速度限制', type: 'InputNumber', editable: false },
{ key: 'turnSpeedLimit', label: '转向速度限制', type: 'InputNumber', editable: false },
{ key: 'knifePolarity', label: '割刀通道极性', type: 'InputNumber', editable: false },
{ key: 'fanPolarity', label: '风门通道极性', type: 'InputNumber', editable: false },
{ key: 'throttlePolarity', label: '油门通道极性', type: 'InputNumber', editable: false },
{ key: 'liftProtectTime', label: '底盘升降电机保护时间', type: 'InputNumber', editable: false },
{ key: 'dualRtk', label: 'RTK配置', type: 'InputNumber', editable: false },
{ key: 'knifeChannel', label: '割刀通道配置', type: 'InputNumber', editable: false },
{ key: 'fanChannel', label: '风门通道配置', type: 'InputNumber', editable: false },
{ key: 'throttleChannel', label: '油门通道配置', type: 'InputNumber', editable: false },
{ key: 'remoteType', label: '遥控器类型', type: 'InputNumber', editable: false },
{ key: 'relayBoard', label: '是否搭载继电器板', type: 'Checkbox', editable: false },
{ key: 'liftChannel', label: '底盘升通道配置', type: 'InputNumber', editable: false },
{ key: 'chassisChannel', label: '底盘降通道配置', type: 'InputNumber', editable: false },
{ key: 'armChannel', label: '抱闸通道配置', type: 'InputNumber', editable: false },
{ key: 'fuelPumpChannel', label: '燃油励磁通道配置', type: 'InputNumber', editable: false },
{ key: 'wifiName', label: 'wifi名称', type: 'Input', editable: false },
{ key: 'wifiPassword', label: 'wifi密码', type: 'Input', editable: false },
{ key: 'batteryType', label: '电池类型', type: 'InputNumber', editable: false },
{ key: 'driveType', label: '行走驱动配置', type: 'InputNumber', editable: false },
{ key: 'gearRatio', label: '转速比', type: 'InputNumber', editable: false, step: 0.01 },
{ key: 'robotLength', label: '机器人长度', type: 'InputNumber', editable: false, step: 0.01 },
{ key: 'robotWidth', label: '机器人宽度', type: 'InputNumber', editable: false, step: 0.01 },
{ key: 'robotHeight', label: '机器人高度', type: 'InputNumber', editable: false, step: 0.01 },
{ key: 'knifeWidth', label: '割刀宽度', type: 'InputNumber', editable: false, step: 0.01 },
{ key: 'tyreSize', label: '轮胎尺寸', type: 'InputNumber', editable: false, step: 0.01 },
{ key: 'firmwareVersion', label: '固件版本', type: 'Input', editable: false },
{ key: 'crc16', label: 'CRC16', type: 'InputNumber', editable: false },
{ key: 'tail1', label: 'tail1', type: 'InputNumber', editable: false },
{ key: 'tail2', label: 'tail2', type: 'InputNumber', editable: false },
{ key: 'remark', label: t('roles.remark'), type: 'Input', editable: false },
{ key: 'delFlag', label: '删除标志', type: 'Checkbox', editable: false },
];
return (
<Modal
@@ -238,7 +243,7 @@ const DeviceParamModal: React.FC<DeviceParamModalProps> = ({ open, device, onCan
>
{/* 加载时表单区域简单遮罩提示(打开弹窗拉取参数时的loading反馈) */}
<div style={{ maxHeight: '60vh', overflowY: 'auto', paddingRight: 8, position: 'relative' }}>
<Spin spinning={loading} tip="加载中...">
<Spin spinning={loading} tip={t('common.loading')}>
<Form form={paramForm} layout="vertical" onFinish={saveParam}>
<Row gutter={16}>

View File

@@ -61,11 +61,13 @@ import { useMqtt } from '@/hooks/useMqtt';
import envConfig from '../../../env';
import { dataUrlToFile } from '@/lib/utils.ts';
import { PauseOutlined, PlayCircleOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
const { RangePicker } = DatePicker;
const { Text } = Typography;
export default function DeviceStatusPage() {
const { t } = useTranslation();
const { userInfo } = useSelector((state: RootState) => state.user);
const { stationId, currentStation } = useSelector((state: RootState) => state.station);
const [persistentDroneData, setPersistentDroneData] = useState(null); // 持久化无人机数据
@@ -162,7 +164,7 @@ export default function DeviceStatusPage() {
latitude: item.latitude || 0,
},
deviceAlias: item.drone_callsign || item.device_sn
|| '无人机',
|| t('constants.uav'),
}));
@@ -581,7 +583,7 @@ export default function DeviceStatusPage() {
name: device.deviceAlias || device.callsign || device.deviceName || '设备',
type: isUAV
? 'drone'
: device.productName?.includes('割草机')
: device.productName?.includes(t('constants.mower'))
? 'mower'
: 'robot',
};
@@ -799,18 +801,18 @@ export default function DeviceStatusPage() {
const res = await addRoute(formData);
if (res.code === 200) {
messageApi.success('保存成功');
messageApi.success(t('common.saveSuccess'));
setIsSaveModalVisible(false);
// 重置状态
setMarkedPoints([]);
setCompleteFlag(false);
setSavePath(null);
} else {
messageApi.error(res.msg || '保存失败');
messageApi.error(res.msg || t('common.saveFail'));
}
} catch (error) {
console.error('保存失败:', error);
messageApi.error('保存失败');
messageApi.error(t('common.saveFail'));
} finally {
setIsSaving(false);
}
@@ -834,7 +836,7 @@ export default function DeviceStatusPage() {
return (
<div style={{ background: '#f5f7fa', minHeight: '100%', padding: 0 }}>
<div style={{ padding: '4px 0px 8px' }}>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>设备状态</Typography.Title>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>{t('devices.deviceStatus')}</Typography.Title>
</div>
<div style={{ padding: 0 }}>
{contextHolder}
@@ -921,7 +923,7 @@ export default function DeviceStatusPage() {
loading={devicesAll.length === 0}
options={[
{
label: '机器人',
label: t('devices.robot'),
title: 'robot',
options: devicesAll.filter(item => !item.gateway_sn && !item.drone_callsign && item.serialNumber).map(item => ({
value: item.device_sn || item.serialNumber,
@@ -929,7 +931,7 @@ export default function DeviceStatusPage() {
})),
},
{
label: '无人机',
label: t('constants.uav'),
title: 'uav',
options: devicesAll.filter(item => item.gateway_sn || item.drone_callsign).map(item => ({
value: item.device_sn,
@@ -940,11 +942,11 @@ export default function DeviceStatusPage() {
/>
<Tag color={isUAV ? 'cyan' : 'blue'}>
{isUAV ? '无人机' : '机器人'}
{isUAV ? t('constants.uav') : t('devices.robot')}
</Tag>
<Tag color={device?.onlineStatus == 1 ? 'green' : 'default'}>
{device?.onlineStatus === 1 ? '在线' : '离线'}
{device?.onlineStatus === 1 ? t('devices.online') : t('devices.offline')}
</Tag>
</div>
@@ -1079,7 +1081,7 @@ export default function DeviceStatusPage() {
icon={isVideoPlaying ? <PauseOutlined /> : <PlayCircleOutlined />}
onClick={toggleVideoPlay}
>
{isVideoPlaying ? '停止' : '播放'}
{isVideoPlaying ? t('video.stop') : t('video.play')}
</Button>
}
>
@@ -1129,18 +1131,18 @@ export default function DeviceStatusPage() {
<span>{runningStatus.headingStatus !== undefined ? `${runningStatus.headingStatus == 1 ? '初始化' : '未初始化'}` : '-'}</span>
<span>定位:</span>
<span>
{runningStatus.qual == 0 ? '无效' :
{runningStatus.qual == 0 ? t('constants.locationInvalid') :
runningStatus.qual == 1 ? 'GPS单点' :
runningStatus.qual == 2 ? 'DGPS差分' :
runningStatus.qual == 4 ? 'RTK固定' :
runningStatus.qual == 5 ? 'RTK浮点' : '未知'}
runningStatus.qual == 5 ? 'RTK浮点' : t('constants.locationUnknown')}
</span>
<span>卫星:</span>
<span>{runningStatus.satelliteCnt || 0}颗</span>
<span>模式:</span>
<span>
{runningStatus.controlMode == 1 ? '本地' :
runningStatus.controlMode == 3 ? '远程' : '未知'}
runningStatus.controlMode == 3 ? '远程' : t('constants.locationUnknown')}
</span>
</div>
</div>
@@ -1188,7 +1190,7 @@ export default function DeviceStatusPage() {
}}>
<VideoCameraOutlined style={{ fontSize: 56, opacity: 0.6 }} />
<div style={{ fontSize: 15 }}>
{!isVideoPlaying ? '已停止视频' : '视频加载中...'}
{!isVideoPlaying ? '已停止视频' : t('video.videoLoading')}
</div>
<div style={{ fontSize: 12, opacity: 0.7 }}>
{!isVideoPlaying ? '点击右上角「播放」按钮开启视频' : ''}
@@ -1242,18 +1244,18 @@ export default function DeviceStatusPage() {
<span>{runningStatus.headingStatus !== undefined ? `${runningStatus.headingStatus == 1 ? '初始化' : '未初始化'}` : '-'}</span>
<span>定位:</span>
<span>
{runningStatus.qual == 0 ? '无效' :
{runningStatus.qual == 0 ? t('constants.locationInvalid') :
runningStatus.qual == 1 ? 'GPS单点' :
runningStatus.qual == 2 ? 'DGPS差分' :
runningStatus.qual == 4 ? 'RTK固定' :
runningStatus.qual == 5 ? 'RTK浮点' : '未知'}
runningStatus.qual == 5 ? 'RTK浮点' : t('constants.locationUnknown')}
</span>
<span>卫星:</span>
<span>{runningStatus.satelliteCnt || 0}颗</span>
<span>模式:</span>
<span>
{runningStatus.controlMode == 1 ? '本地' :
runningStatus.controlMode == 3 ? '远程' : '未知'}
runningStatus.controlMode == 3 ? '远程' : t('constants.locationUnknown')}
</span>
</div>
</div>
@@ -1345,8 +1347,8 @@ export default function DeviceStatusPage() {
onOk={handleOk}
onCancel={handleCancel}
title={modalContent.title}
okText="确定"
cancelText="取消"
okText={t('common.ok')}
cancelText={t('common.cancel')}
>
<p>{modalContent.content}</p>
</Modal>
@@ -1357,8 +1359,8 @@ export default function DeviceStatusPage() {
title="保存路径"
onOk={handleSave}
onCancel={() => setIsSaveModalVisible(false)}
okText="保存"
cancelText="取消"
okText={t('common.save')}
cancelText={t('common.cancel')}
confirmLoading={isSaving}
>
<div style={{ marginBottom: 16 }}>

View File

@@ -5,6 +5,7 @@ import DroneLivePlayer from '../VolcRtcPlayer.jsx';
import { changeCameragateway, changeCameraPlane } from '../../api/device.ts';
import utils from '../../lib/utils';
import AgoraRtcRoom from '../AgoraRtc.tsx';
import { useTranslation } from 'react-i18next';
const { Text } = Typography;
@@ -38,6 +39,7 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
const [internalVideoSource, setInternalVideoSource] = useState<'station' | 'drone'>(defaultSource);
const videoSource = propsVideoSource !== undefined ? propsVideoSource : internalVideoSource;
const setVideoSource = (source: 'station' | 'drone') => {
const { t } = useTranslation();
if (onSourceChange) onSourceChange(source);
else setInternalVideoSource(source);
};
@@ -237,6 +239,8 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
const isDrone = type === 'drone';
const currentDisplayType = displayType || type;
const data = statusData;
const { t } = useTranslation();
return (
<div style={{
@@ -327,7 +331,7 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
}}>
{loading ? <ReloadOutlined spin style={{ color: '#fff' }} /> : null}
<span style={{ color: '#fff', fontSize: 12 }}>
{loading ? '视频加载中...' : '暂无视频信号'}
{loading ? t('video.videoLoading') : '暂无视频信号'}
</span>
</div>
)}

View File

@@ -48,11 +48,13 @@ import DeviceController from './DeviceController';
import Draggable, { DraggableData, DraggableEvent } from 'react-draggable';
import { MowerAllView } from './mowerAllView.jsx';
import { useWebRTCPlayer } from '../useWebRTCStream.js';
import { useTranslation } from 'react-i18next';
const { Text, Title } = Typography;
const { RangePicker } = DatePicker;
export default function RobotTaskPage() {
const { t } = useTranslation();
const { userInfo } = useSelector((state: RootState) => state.user);
const { stationId, currentStation } = useSelector((state: RootState) => state.station);
const [excuteStatus, setExcuteStatus] = useState("stop"); //"stop" "running" "pause"
@@ -186,7 +188,7 @@ export default function RobotTaskPage() {
content: `确定要删除航线「${route.workName}」吗?删除后数据无法恢复!`,
icon: <ExclamationCircleOutlined style={{ color: '#FF7D00' }} />,
okText: '确定删除',
cancelText: '取消',
cancelText: t('common.cancel'),
okButtonProps: { danger: true, loading: deleteLoading },
onOk: async () => {
setDeleteLoading(true);
@@ -481,12 +483,12 @@ export default function RobotTaskPage() {
deleteDeviceTask({ id }).then(res => {
if (res.code == 200) {
utils.message.success('删除成功');
utils.message.success(t('common.deleteSuccess'));
} else {
utils.message.error(res.msg);
}
}).catch(() => message.error('删除失败')).finally(() => {
}).catch(() => message.error(t('common.deleteFail'))).finally(() => {
fetchTaskList();
fetchTaskPoolList();
fetchTaskHistoryList();
@@ -757,14 +759,14 @@ export default function RobotTaskPage() {
const taskColumns = [
{
title: '任务名称',
title: t('devices.taskName'),
dataIndex: 'planName',
width: 160,
ellipsis: true,
fixed: "left"
},
{
title: '设备',
title: t('devices.device'),
dataIndex: 'deviceAlias',
width: 320,
render: (_, record) => {
@@ -773,20 +775,20 @@ export default function RobotTaskPage() {
},
{
title: '创建者',
title: t('devices.creator'),
dataIndex: 'createBy',
width: 100,
render: (text) => text
},
{
title: '创建时间',
title: t('common.createTime'),
dataIndex: 'createTime',
width: 170,
render: t => t ? dayjs(t).format('YYYY-MM-DD HH:mm:ss') : '-'
},
// 执行计划 + 周期时间
{
title: '执行计划',
title: t('devices.executionPlan'),
width: 280,
render: (_, record) => {
const rule = record.deviceTaskPlanRules || {};
@@ -794,65 +796,65 @@ export default function RobotTaskPage() {
// 周映射
const weekMap = {
SUNDAY: '周日',
MONDAY: '周一',
TUESDAY: '周二',
WEDNESDAY: '周三',
THURSDAY: '周四',
FRIDAY: '周五',
SATURDAY: '周六'
SUNDAY: t('devices.sun'),
MONDAY: t('devices.mon'),
TUESDAY: t('devices.tue'),
WEDNESDAY: t('devices.wed'),
THURSDAY: t('devices.thu'),
FRIDAY: t('devices.fri'),
SATURDAY: t('devices.sat')
};
switch (type) {
case 'DAY':
return `按天 | 开始日期${startDate || ''} ${dayTime || ''}`;
return t('devices.planByDay', { startDate: startDate || '', dayTime: dayTime || '' });
case 'WEEK':
const weekText = (days || []).map(d => weekMap[d]).join('、');
return `按周 | ${weekText} ${dayTime || ''}`;
return t('devices.planByWeek', { weekText, dayTime: dayTime || '' });
case 'MONTH':
const monthText = (dayOfMonth || []).map(d => `${d}日`).join('、');
return `按月 | ${monthText} ${dayTime || ''}`;
const monthText = (dayOfMonth || []).map(d => t('devices.daySuffix', { day: d })).join('、');
return t('devices.planByMonth', { monthText, dayTime: dayTime || '' });
default:
return '未知计划';
return t('devices.unknownPlan');
}
}
},
{
title: '执行周期',
title: t('devices.executionCycle'),
width: 320,
render: (_, record) => {
return `${record.startTime || ''} - ${record.finishTime || ''}`;
}
},
{
title: '状态',
title: t('roles.status'),
dataIndex: 'taskStaus',
width: 100,
render: (status, record) => {
const statusMap = {
'NEW': { color: 'default', text: '新建' },
'EXECUTING': { color: 'processing', text: '执行中' },
'PAUSE': { color: 'warning', text: '暂停中' },
'FINISH': { color: 'success', text: '执行成功' },
'FAILED': { color: 'error', text: '执行失败' }
'NEW': { color: 'default', text: t('devices.statusNew') },
'EXECUTING': { color: 'processing', text: t('workOrder.inProgress') },
'PAUSE': { color: 'warning', text: t('devices.statusPaused') },
'FINISH': { color: 'success', text: t('devices.statusFinished') },
'FAILED': { color: 'error', text: t('devices.statusFailed') }
};
const item = statusMap[status] || { color: 'default', text: record.taskStausTranslate || '未知' };
const item = statusMap[status] || { color: 'default', text: record.taskStausTranslate || t('constants.locationUnknown') };
return <Tag color={item.color}>{item.text}</Tag>;
}
},
{
title: '操作',
title: t('roles.actions'),
width: 300,
fixed: 'right',
render: (_, record) => (
<Space size="small">
<Popconfirm
title="确定要删除该任务吗?"
title={t('devices.confirmDeleteTask')}
onConfirm={() => handleDelete(record.id)}
okText="确定"
cancelText="取消"
okText={t('common.ok')}
cancelText={t('common.cancel')}
>
<Button
type="text"
@@ -860,7 +862,7 @@ export default function RobotTaskPage() {
size="small"
icon={<DeleteOutlined />}
>
删除
{t('common.delete')}
</Button>
</Popconfirm>
@@ -873,33 +875,33 @@ export default function RobotTaskPage() {
const taskPoolColumns = [
{
title: '设备',
title: t('devices.device'),
dataIndex: 'deviceId',
width: 320,
},
{
title: '状态',
title: t('roles.status'),
dataIndex: 'taskStaus',
width: 100,
render: (status, record) => {
const statusMap = {
'NEW': { color: 'default', text: '新建' },
'EXECUTING': { color: 'processing', text: '执行中' },
'PAUSE': { color: 'warning', text: '暂停中' },
'FINISH': { color: 'success', text: '执行成功' },
'FAILED': { color: 'error', text: '执行失败' }
'NEW': { color: 'default', text: t('devices.statusNew') },
'EXECUTING': { color: 'processing', text: t('workOrder.inProgress') },
'PAUSE': { color: 'warning', text: t('devices.statusPaused') },
'FINISH': { color: 'success', text: t('devices.statusFinished') },
'FAILED': { color: 'error', text: t('devices.statusFailed') }
};
const item = statusMap[status] || { color: 'default', text: '未知' };
const item = statusMap[status] || { color: 'default', text: t('constants.locationUnknown') };
return <Tag color={item.color}>{item.text}</Tag>;
}
},
{
title: '创建时间',
title: t('common.createTime'),
dataIndex: 'createTime',
width: 170,
},
{
title: '操作',
title: t('roles.actions'),
width: 100, // 加宽一点
fixed: 'right',
render: (_, record) => (
@@ -907,35 +909,38 @@ export default function RobotTaskPage() {
<>
{/*{record.taskStaus === 'NEW' && (*/}
<Popconfirm
title="确定要取消该任务吗?"
onConfirm={() => handleCancelTask(record)}
okText="确定" cancelText="取消"
>
<Button type="text" size="small" danger icon={<CloseOutlined />} onClick={(e) => e.stopPropagation()}
>取消</Button>
</Popconfirm>
<Popconfirm
title={t('devices.confirmCancelTask')}
onConfirm={() => handleCancelTask(record)}
okText={t('common.ok')} cancelText={t('common.cancel')}
>
<Button type="text" size="small" danger icon={<CloseOutlined />} onClick={(e) => e.stopPropagation()}>
{t('common.cancel')}
</Button>
</Popconfirm>
{/*//)}*/}
{/*{record.taskStaus === 'EXECUTING' && (*/}
<Popconfirm
title="确定要暂停该任务吗?"
onConfirm={() => handlePauseTask(record)}
okText="确定" cancelText="取消"
>
<Button type="text" size="small" style={{ color: '#FF9500' }} onClick={(e) => e.stopPropagation()}
>暂停</Button>
</Popconfirm>
<Popconfirm
title={t('devices.confirmPauseTask')}
onConfirm={() => handlePauseTask(record)}
okText={t('common.ok')} cancelText={t('common.cancel')}
>
<Button type="text" size="small" style={{ color: '#FF9500' }} onClick={(e) => e.stopPropagation()}>
{t('video.pause')}
</Button>
</Popconfirm>
{/*)}/*/}
{record.taskStaus === 'PAUSE' && (
<Popconfirm
title="确定要恢复该任务吗?"
title={t('devices.confirmResumeTask')}
onConfirm={() => handleRecoverTask(record)}
okText="确定" cancelText="取消"
okText={t('common.ok')} cancelText={t('common.cancel')}
>
<Button type="text" size="small" style={{ color: '#00B42A' }} onClick={(e) => e.stopPropagation()}
>恢复</Button>
<Button type="text" size="small" style={{ color: '#00B42A' }} onClick={(e) => e.stopPropagation()}>
{t('devices.recover')}
</Button>
</Popconfirm>
)}
</>
@@ -949,12 +954,12 @@ export default function RobotTaskPage() {
const taskHistoryColumns = [
{
title: '设备',
title: t('devices.device'),
dataIndex: 'deviceId',
width: 320,
},
{
title: '状态',
title: t('roles.status'),
dataIndex: 'taskStausTranslate',
width: 100,
render: (text, record) => {
@@ -972,16 +977,16 @@ export default function RobotTaskPage() {
},
{
title: '开始时间',
title: t('devices.startTime'),
dataIndex: 'startTime',
width: 170,
},
{
title: '结束时间',
title: t('devices.endTime'),
dataIndex: 'finishTime',
width: 170,
}, {
title: '创建时间',
title: t('common.createTime'),
dataIndex: 'createTime',
width: 170,
}
@@ -1015,16 +1020,16 @@ export default function RobotTaskPage() {
const topCards = [
{ title: '今日机器人计划', value: taskList.length, unit: '条', desc: '进行中', icon: <LayoutGrid size={22} />, color: '#165DFF' },
{ title: '作业中机器人', value: taskPoolList?.filter(item => ['EXECUTING', 'PAUSE'].includes(item.taskStaus))?.length, unit: '台', desc: '作业中', icon: <Car size={22} />, color: '#165DFF' },
{ title: '累计作业里程', value: '12.5', unit: 'km', trend: '较昨日 +1.2', trendColor: '#00B42A', icon: <TrendingUp size={22} />, color: '#722ED1' },
{ title: '任务完成率', value: successfulTaskList.length ? Math.round((successfulTaskList.length / taskHistoryList.length) * 100) : 0, unit: '%', trend: '较昨日 +5%', trendColor: '#00B42A', icon: <CheckCircle2 size={22} />, color: '#00B42A' },
{ title: '异常告警', value: '0', unit: '次', trend: '较昨日 -1', trendColor: '#00B42A', icon: <AlertCircle size={22} />, color: '#F53F3F' },
{ title: t('devices.statTodayPlan'), value: taskList.length, unit: t('devices.unitPlan'), desc: t('devices.statInProgress'), icon: <LayoutGrid size={22} />, color: '#165DFF' },
{ title: t('devices.statWorkingRobots'), value: taskPoolList?.filter(item => ['EXECUTING', 'PAUSE'].includes(item.taskStaus))?.length, unit: t('devices.unitRobot'), desc: t('devices.statWorking'), icon: <Car size={22} />, color: '#165DFF' },
{ title: t('devices.statTotalDistance'), value: '12.5', unit: 'km', trend: t('devices.vsYesterday', { value: '+1.2' }), trendColor: '#00B42A', icon: <TrendingUp size={22} />, color: '#722ED1' },
{ title: t('devices.statCompletionRate'), value: successfulTaskList.length ? Math.round((successfulTaskList.length / taskHistoryList.length) * 100) : 0, unit: '%', trend: t('devices.vsYesterday', { value: '+5%' }), trendColor: '#00B42A', icon: <CheckCircle2 size={22} />, color: '#00B42A' },
{ title: t('devices.statAlerts'), value: '0', unit: t('devices.unitTimes'), trend: t('devices.vsYesterday', { value: '-1' }), trendColor: '#00B42A', icon: <AlertCircle size={22} />, color: '#F53F3F' },
{
title: '在线率',
title: t('devices.statOnlineRate'),
value: `${devices?.length ? Math.round((devices.filter(d => d.onlineStatus == 1).length / devices.length) * 100) : 0}`,
unit: '%',
desc: '良好',
desc: t('devices.good'),
icon: <Satellite size={22} />,
color: '#165DFF'
}];
@@ -1032,7 +1037,7 @@ export default function RobotTaskPage() {
return (
<div className="normal-spacing" style={{ background: '#f5f7fa', minHeight: '100vh', padding: '0px', paddingTop: 0 }}>
<div style={{ padding: '4px 0px 8px' }}>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>机器人任务</Typography.Title>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>{t('devices.robotTask')}</Typography.Title>
</div>
<Row gutter={[16, 16]} style={{ marginBottom: 8 }}>
{topCards.map((item, index) => (
@@ -1121,7 +1126,7 @@ export default function RobotTaskPage() {
}}
>
<span style={{ fontSize: fontNormal, fontWeight: 700 }}>
机器人路径规划与监控
{t('devices.robotRouteMonitor')}
</span>
@@ -1130,12 +1135,12 @@ export default function RobotTaskPage() {
size="small"
extra={
<Space size={4} wrap> {/* Space开启自动换行,小屏按钮自动折行 */}
<Button size="small" icon={<DeleteOutlined />} type="primary" style={{ borderRadius: 4 }} onClick={() => setClearNavLine(v => v + 1)}>清空实时轨迹</Button>
<Button size="small" icon={<DeleteOutlined />} danger style={{ borderRadius: 4 }} onClick={() => { setPoints([]); setClearPlanLine(v => v + 1); }}>清空规划航线</Button>
{/*<Button size="small" icon={<PlusOutlined />} style={{ borderRadius: 4 }}>新增</Button>
<Button size="small" icon={<ImportOutlined />} style={{ borderRadius: 4 }}>导入</Button>
<Button size="small" icon={<DeleteOutlined />} type="primary" style={{ borderRadius: 4 }} onClick={() => setClearNavLine(v => v + 1)}>{t('devices.clearRealtimeTrack')}</Button>
<Button size="small" icon={<DeleteOutlined />} danger style={{ borderRadius: 4 }} onClick={() => { setPoints([]); setClearPlanLine(v => v + 1); }}>{t('devices.clearPlanRoute')}</Button>
{/*<Button size="small" icon={<PlusOutlined />} style={{ borderRadius: 4 }}>{t('common.addNew')}</Button>
<Button size="small" icon={<ImportOutlined />} style={{ borderRadius: 4 }}>{t('common.import')}</Button>
<Button size="small" icon={<NodeIndexOutlined />} style={{ borderRadius: 4 }}>优化</Button>
<Button size="small" icon={<SaveOutlined />} style={{ borderRadius: 4 }}>保存</Button>*/}
<Button size="small" icon={<SaveOutlined />} style={{ borderRadius: 4 }}>{t('common.save')}</Button>*/}
{/*<Button size="small" icon={<Maximize2 size={12} />} />*/}
</Space>
}
@@ -1185,25 +1190,25 @@ export default function RobotTaskPage() {
</span>
<span>
控制模式:
{t('devices.controlMode')}
{robotRealtimePosition?.controlMode == '1' ? (
<span style={{ color: '#ff6f6f' }}>本地</span>
<span style={{ color: '#ff6f6f' }}>{t('devices.localMode')}</span>
) : (
<span style={{ color: '#9eff9e' }}>远程</span>
<span style={{ color: '#9eff9e' }}>{t('devices.remoteMode')}</span>
)}
</span>
<span>
航向:
{t('devices.heading')}
{robotRealtimePosition?.headingStatus == '1' ? (
<span style={{ color: '#9eff9e' }}>已初始化</span>
<span style={{ color: '#9eff9e' }}>{t('devices.initialized')}</span>
) : (
<span style={{ color: '#ff6f6f' }}>未初始化</span>
<span style={{ color: '#ff6f6f' }}>{t('devices.uninitialized')}</span>
)}
</span>
<span>
定位:
{t('devices.location')}
{(() => {
const qual = robotRealtimePosition?.qual;
const info = getLocationQuality(qual);
@@ -1213,7 +1218,7 @@ export default function RobotTaskPage() {
{/* 视频开关 */}
<span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
视频:
{t('devices.video')}
<Switch
size="small"
checked={showVideo}
@@ -1244,16 +1249,16 @@ export default function RobotTaskPage() {
items={[
{
key: 'pool',
label: '任务池',
label: t('devices.taskPool'),
},
{
key: 'task',
label: '任务执行计划',
label: t('devices.taskExecutionPlan'),
},
{
key: 'history',
label: '历史任务',
label: t('devices.taskHistory'),
}
]}
tabBarExtraContent={
@@ -1263,7 +1268,7 @@ export default function RobotTaskPage() {
fetchTaskPoolList();
fetchTaskHistoryList();
}} />
<Tooltip title="创建任务">
<Tooltip title={t('devices.createTask')}>
<PlusOutlined
style={{ cursor: 'pointer', color: '#165DFF', fontSize: 14 }}
onClick={() => setIsTaskModalOpen(true)}
@@ -1282,7 +1287,7 @@ export default function RobotTaskPage() {
<RangePicker
size="small"
format="YYYY-MM-DD"
placeholder={['开始日期', '结束日期']}
placeholder={[t('devices.startDate'), t('devices.endDate')]}
value={filterForm.dateRange}
onChange={(dates) => {
setFilterForm(prev => ({ ...prev, dateRange: dates }));
@@ -1291,14 +1296,14 @@ export default function RobotTaskPage() {
<Select
style={{ width: 120 }}
size="small"
placeholder="状态"
placeholder={t('roles.status')}
allowClear
options={[
{ label: '新建', value: 'NEW' },
{ label: '执行中', value: 'EXECUTING' },
{ label: '暂停中', value: 'PAUSE' },
{ label: '执行成功', value: 'FINISH' },
{ label: '执行失败', value: 'FAILED' },
{ label: t('devices.statusNew'), value: 'NEW' },
{ label: t('workOrder.inProgress'), value: 'EXECUTING' },
{ label: t('devices.statusPaused'), value: 'PAUSE' },
{ label: t('devices.statusFinished'), value: 'FINISH' },
{ label: t('devices.statusFailed'), value: 'FAILED' },
]}
value={filterForm.taskStatus}
onChange={(val) => {
@@ -1308,7 +1313,7 @@ export default function RobotTaskPage() {
<Select
style={{ width: 140 }}
size="small"
placeholder="全部机器人"
placeholder={t('devices.allRobots')}
allowClear
options={devices?.map(item => ({
value: item.serialNumber,
@@ -1347,12 +1352,12 @@ export default function RobotTaskPage() {
<Select
style={{ width: 120 }}
size="small"
placeholder="状态"
placeholder={t('roles.status')}
allowClear
options={[
{ label: '新建', value: 'NEW' },
{ label: '执行中', value: 'EXECUTING' },
{ label: '暂停中', value: 'PAUSE' },
{ label: t('devices.statusNew'), value: 'NEW' },
{ label: t('workOrder.inProgress'), value: 'EXECUTING' },
{ label: t('devices.statusPaused'), value: 'PAUSE' },
]}
value={poolFilterForm.taskStatus}
onChange={(val) => {
@@ -1362,7 +1367,7 @@ export default function RobotTaskPage() {
<Select
style={{ width: 140 }}
size="small"
placeholder="全部机器人"
placeholder={t('devices.allRobots')}
allowClear
options={devices?.map(item => ({
value: item.serialNumber,
@@ -1404,7 +1409,7 @@ export default function RobotTaskPage() {
<RangePicker
size="small"
format="YYYY-MM-DD HH:mm:ss"
placeholder={['开始时间', '结束时间']}
placeholder={[t('devices.startTime'), t('devices.endTime')]}
value={historyFilterForm.dateRange}
onChange={(dates) => {
setHistoryFilterForm(prev => ({ ...prev, dateRange: dates }));
@@ -1414,12 +1419,12 @@ export default function RobotTaskPage() {
<Select
style={{ width: 120 }}
size="small"
placeholder="状态"
placeholder={t('roles.status')}
allowClear
options={[
{ label: '执行成功', value: 'FINISH' },
{ label: '执行失败', value: 'FAILED' },
{ label: '已取消', value: 'CANCELED' },
{ label: t('devices.statusFinished'), value: 'FINISH' },
{ label: t('devices.statusFailed'), value: 'FAILED' },
{ label: t('clean.cancelled'), value: 'CANCELED' },
]}
value={historyFilterForm.taskStatus}
onChange={(val) => {
@@ -1429,7 +1434,7 @@ export default function RobotTaskPage() {
<Select
style={{ width: 340 }}
size="small"
placeholder="全部机器人"
placeholder={t('devices.allRobots')}
allowClear
options={devices?.map(item => ({
value: item.serialNumber,
@@ -1462,30 +1467,30 @@ export default function RobotTaskPage() {
<Col xs={24} xl={10} style={{ display: "flex", flexDirection: "column" }} >
<Card
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>作业编排建议</span>}
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>{t('devices.operationSuggestion')}</span>}
bordered={false}
bodyStyle={{ padding: '12px 16px' }}
style={{ ...cardStyle, marginBottom: 8 }}
>
<Timeline mode="left">
<Timeline.Item label="09:30" color="green">
<Text strong>RBT-01 开始除草任务</Text>
<div style={{ fontSize: 12, color: '#86909C' }}>预计耗时 2 小时,当前进度 45%</div>
<Text strong>{t('devices.timelineMow', { robot: 'RBT-01' })}</Text>
<div style={{ fontSize: 12, color: '#86909C' }}>{t('devices.timelineMowDetail')}</div>
</Timeline.Item>
<Timeline.Item label="11:00" color="blue">
<Text strong>RBT-02 预定巡检任务</Text>
<div style={{ fontSize: 12, color: '#86909C' }}>待审批,建议执行区域 C</div>
<Text strong>{t('devices.timelineInspect', { robot: 'RBT-02' })}</Text>
<div style={{ fontSize: 12, color: '#86909C' }}>{t('devices.timelineInspectDetail')}</div>
</Timeline.Item>
<Timeline.Item label="14:00" color="gray">
<Text strong>设备例行维护</Text>
<div style={{ fontSize: 12, color: '#86909C' }}>所有割草机返回充电站</div>
<Text strong>{t('devices.timelineMaintenance')}</Text>
<div style={{ fontSize: 12, color: '#86909C' }}>{t('devices.timelineMaintenanceDetail')}</div>
</Timeline.Item>
</Timeline>
<Button type="primary" block style={{ marginTop: 16, borderRadius: 18 }}>智能生成今日计划</Button>
<Button type="primary" block style={{ marginTop: 16, borderRadius: 18 }}>{t('devices.autoGenerateTodayPlan')}</Button>
</Card>
<Card
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>设备环境与状态</span>}
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>{t('devices.deviceEnvironment')}</span>}
bordered={false}
bodyStyle={{ padding: '16px' }}
style={{ ...cardStyle, flex: 1 }}
@@ -1493,32 +1498,32 @@ export default function RobotTaskPage() {
<Row gutter={[8, 8]}>
<Col span={12}>
<div style={{ background: '#F7F8FA', padding: 12, borderRadius: 10 }}>
<div style={{ fontSize: 12, color: '#86909C' }}>当前温度</div>
<div style={{ fontSize: 12, color: '#86909C' }}>{t('devices.currentTemp')}</div>
<div style={{ fontSize: 18, fontWeight: 700 }}>24.5 ℃</div>
</div>
</Col>
<Col span={12}>
<div style={{ background: '#F7F8FA', padding: 12, borderRadius: 10 }}>
<div style={{ fontSize: 12, color: '#86909C' }}>环境湿度</div>
<div style={{ fontSize: 12, color: '#86909C' }}>{t('devices.humidity')}</div>
<div style={{ fontSize: 18, fontWeight: 700 }}>65 %</div>
</div>
</Col>
<Col span={12}>
<div style={{ background: '#F7F8FA', padding: 12, borderRadius: 10 }}>
<div style={{ fontSize: 12, color: '#86909C' }}>定位精度</div>
<div style={{ fontSize: 18, fontWeight: 700, color: '#00B42A' }}>RTK 固定</div>
<div style={{ fontSize: 12, color: '#86909C' }}>{t('devices.positionAccuracy')}</div>
<div style={{ fontSize: 18, fontWeight: 700, color: '#00B42A' }}>{t('devices.rtkFixed')}</div>
</div>
</Col>
<Col span={12}>
<div style={{ background: '#F7F8FA', padding: 12, borderRadius: 10 }}>
<div style={{ fontSize: 12, color: '#86909C' }}>网络延迟</div>
<div style={{ fontSize: 12, color: '#86909C' }}>{t('devices.networkLatency')}</div>
<div style={{ fontSize: 18, fontWeight: 700 }}>45 ms</div>
</div>
</Col>
</Row>
<div style={{ marginTop: 8, padding: 12, background: 'rgba(255, 125, 0, 0.1)', borderRadius: 8, display: 'flex', alignItems: 'center', gap: 8 }}>
<AlertCircle size={16} style={{ color: '#FF7D00' }} />
<span style={{ fontSize: 13, color: '#FF7D00' }}>注意:区域 B 存在积水风险,建议避开</span>
<span style={{ fontSize: 13, color: '#FF7D00' }}>{t('devices.waterRiskWarning')}</span>
</div>
</Card>
</Col>
@@ -1529,9 +1534,9 @@ export default function RobotTaskPage() {
<Card
title={
<div className="flex justify-between items-center w-full">
<span style={{ fontSize: fontTitle, fontWeight: 700 }}>预设路线</span>
<span style={{ fontSize: fontTitle, fontWeight: 700 }}>{t('devices.presetRoutes')}</span>
<Input
placeholder="搜索..."
placeholder={t('devices.searchPlaceholder')}
size="small"
style={{ width: 200 }}
value={routeSearch}
@@ -1619,14 +1624,14 @@ export default function RobotTaskPage() {
{/* 路线信息 */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 600, fontSize: 14, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{route.workName || route.name || '未命名路线'}
{route.workName || route.name || t('devices.unnamedRoute')}
</div>
<div style={{ fontSize: 12, color: '#86909C', marginTop: 4 }}>
ID: {route.id}
</div>
</div>
{/* 删除按钮 */}
<Tooltip title="删除航线">
<Tooltip title={t('devices.deleteRoute')}>
<Button
type="text"
icon={<DeleteOutlined style={{ color: '#F53F3F', fontSize: 20 }} />}
@@ -1638,7 +1643,7 @@ export default function RobotTaskPage() {
}}
/>
</Tooltip>
<Tooltip title="创建任务">
<Tooltip title={t('devices.createTask')}>
<Button
type="text"
icon={<PlusOutlined style={{ color: '#165DFF', fontSize: 20 }} />}
@@ -1651,7 +1656,7 @@ export default function RobotTaskPage() {
</Tooltip>
{/* 执行按钮 */}
<Tooltip title="执行航线">
<Tooltip title={t('devices.executeRoute')}>
<Button
type="text"
icon={<PlayCircleOutlined style={{ color: '#165DFF', fontSize: 28 }} />}
@@ -1670,7 +1675,7 @@ export default function RobotTaskPage() {
</Col>
<Col xs={24} xl={18}>
<Card
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>任务看板</span>}
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>{t('devices.taskBoard')}</span>}
bordered={false}
bodyStyle={{ padding: '12px' }}
style={{ ...cardStyle, height: '100%' }}
@@ -1678,23 +1683,23 @@ export default function RobotTaskPage() {
<Row gutter={16}>
<Col span={8}>
<div style={{ background: '#F7F8FA', padding: 12, borderRadius: 8, height: 400 }}>
<Title level={5} style={{ fontSize: 14, marginBottom: 12 }}><Badge color="#86909C" text="待执行" /></Title>
<Title level={5} style={{ fontSize: 14, marginBottom: 12 }}><Badge color="#86909C" text={t('devices.pending')} /></Title>
{waitList.map((item, idx) => (
<Card key={idx} size="small" style={{ marginBottom: 8, borderRadius: 6 }}>
<div style={{ fontWeight: 600 }}>{item.planName || item.workName}</div>
<div style={{ fontSize: 12, color: '#86909C' }}>设备: {item.deviceId}</div>
<div style={{ fontSize: 12, color: '#86909C' }}>{t('devices.device')}: {item.deviceId}</div>
</Card>
))}
</div>
</Col>
<Col span={8}>
<div style={{ background: '#E8F3FF', padding: 12, borderRadius: 8, height: 400 }}>
<Title level={5} style={{ fontSize: 14, marginBottom: 12 }}><Badge color="#165DFF" text="进行中" /></Title>
<Title level={5} style={{ fontSize: 14, marginBottom: 12 }}><Badge color="#165DFF" text={t('devices.statInProgress')} /></Title>
{runList.map((item, idx) => (
<Card key={idx} size="small" style={{ marginBottom: 8, borderRadius: 6 }}>
<div style={{ fontWeight: 600 }}>{item.planName || item.workName}</div>
<div style={{ fontSize: 12, color: '#165DFF' }}>
{item.taskStaus === 'EXECUTING' ? '正在执行...' : '已暂停'}
{item.taskStaus === 'EXECUTING' ? t('devices.executingEllipsis') : t('devices.paused')}
</div>
</Card>
))}
@@ -1702,12 +1707,12 @@ export default function RobotTaskPage() {
</Col>
<Col span={8}>
<div style={{ background: '#F6FFED', padding: 12, borderRadius: 8, height: 400 }}>
<Title level={5} style={{ fontSize: 14, marginBottom: 12 }}><Badge color="#52C41A" text="已完成" /></Title>
<Title level={5} style={{ fontSize: 14, marginBottom: 12 }}><Badge color="#52C41A" text={t('workOrder.completed')} /></Title>
{finishList.slice(0, 3).map((item, idx) => (
<Card key={idx} size="small" style={{ marginBottom: 8, borderRadius: 6 }}>
<div style={{ fontWeight: 600 }}>{item.planName || item.workName}</div>
<div style={{ fontSize: 12, color: item.taskStaus === 'FAILED' ? '#F53F3F' : '#52C41A' }}>
{item.taskStaus === 'FAILED' ? '执行失败' : '执行成功'}
{item.taskStaus === 'FAILED' ? t('devices.statusFailed') : t('devices.statusFinished')}
</div>
</Card>
))}
@@ -1733,26 +1738,26 @@ export default function RobotTaskPage() {
{/* 执行任务弹窗 */}
<Modal
title="执行路线任务"
title={t('devices.executeRouteTask')}
open={executeModalVisible}
onCancel={() => setExecuteModalVisible(false)}
onOk={handleExecuteConfirm}
okText="执行"
cancelText="取消"
okText={t('devices.execute')}
cancelText={t('common.cancel')}
>
<div style={{ padding: '10px 0' }}>
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 8, fontWeight: 500 }}>路线信息</div>
<div style={{ marginBottom: 8, fontWeight: 500 }}>{t('devices.routeInfo')}</div>
<div style={{ padding: 12, background: '#f5f7fa', borderRadius: 6 }}>
<div><strong>路线名称:</strong>{selectedRouteForExecute?.workName || selectedRouteForExecute?.name}</div>
<div><strong>{t('devices.routeName')}</strong>{selectedRouteForExecute?.workName || selectedRouteForExecute?.name}</div>
</div>
</div>
<div>
<div style={{ marginBottom: 8, fontWeight: 500 }}>选择执行设备</div>
<div style={{ marginBottom: 8, fontWeight: 500 }}>{t('workOrder.selectDevice')}</div>
<Select
style={{ width: '100%' }}
placeholder="请选择执行设备"
placeholder={t('devices.selectExecuteDevice')}
value={selectedRobotForExecute?.serialNumber}
onChange={(val) => {
const robot = devices.find(d => d.serialNumber === val);
@@ -1772,7 +1777,7 @@ export default function RobotTaskPage() {
{showVideo && executingRobot && execute && (
<DraggableBox
defaultPosition={{ x: 50, y: 50 }}
title={`机器人全景视频 - ${executingRobot.deviceAlias || executingRobot.serialNumber}`}
title={t('devices.robotPanoramaVideo', { robot: executingRobot.deviceAlias || executingRobot.serialNumber })}
onClose={() => setShowVideo(false)}
>
<div style={{ height: '100%', width: '100%' }}>
@@ -1790,6 +1795,7 @@ export default function RobotTaskPage() {
}
const NewTaskModal = ({ isOpen, onClose, robots, selectedPathWork, handlGetTaskList, routeList, stationId, onTaskCreated, userInfo }) => {
const { t } = useTranslation();
const [formData, setFormData] = useState({
path: selectedPathWork || null,
device: null,
@@ -1813,13 +1819,13 @@ const NewTaskModal = ({ isOpen, onClose, robots, selectedPathWork, handlGetTaskL
// ====================== 周/月选择 ======================
const weekDays = [
{ label: '日', value: 'SUNDAY' },
{ label: '一', value: 'MONDAY' },
{ label: '二', value: 'TUESDAY' },
{ label: '三', value: 'WEDNESDAY' },
{ label: '四', value: 'THURSDAY' },
{ label: '五', value: 'FRIDAY' },
{ label: '六', value: 'SATURDAY' },
{ label: t('devices.wdSun'), value: 'SUNDAY' },
{ label: t('devices.wdMon'), value: 'MONDAY' },
{ label: t('devices.wdTue'), value: 'TUESDAY' },
{ label: t('devices.wdWed'), value: 'WEDNESDAY' },
{ label: t('devices.wdThu'), value: 'THURSDAY' },
{ label: t('devices.wdFri'), value: 'FRIDAY' },
{ label: t('devices.wdSat'), value: 'SATURDAY' },
];
const monthDays = Array.from({ length: 31 }, (_, i) => (i + 1).toString());
@@ -1861,14 +1867,14 @@ const NewTaskModal = ({ isOpen, onClose, robots, selectedPathWork, handlGetTaskL
// ====================== 保存 ======================
const handleSave = () => {
const missingFields = [];
if (!formData.device?.serialNumber) missingFields.push('设备编号');
if (!formData.planName) missingFields.push('任务名称');
if (!formData.path?.id) missingFields.push('路线');
//if (!formData.excuteDayStartTime) missingFields.push('执行开始时间');
if (!formData.device?.serialNumber) missingFields.push(t('devices.deviceSerial'));
if (!formData.planName) missingFields.push(t('devices.taskName'));
if (!formData.path?.id) missingFields.push(t('devices.route'));
//if (!formData.excuteDayStartTime) missingFields.push(t('workOrder.execStartTime'));
//if (!formData.excuteDayEndTime) missingFields.push('执行结束时间');
if (missingFields.length > 0) {
utils.message.error({ title: '提示', content: `请填写:${missingFields.join('、')}` });
utils.message.error({ title: t('constants.info'), content: t('devices.fillRequired', { fields: missingFields.join('、') }) });
return;
}
@@ -1906,25 +1912,25 @@ const NewTaskModal = ({ isOpen, onClose, robots, selectedPathWork, handlGetTaskL
};
}
if (!finalData.deviceTaskPlanRules.dayTime) {
utils.message.error('请选择执行时间');
utils.message.error(t('devices.selectExecTime'));
return;
}
saveDeviceTask(finalData).then(res => {
if (res.code === 200) {
utils.message.success('任务创建成功');
utils.message.success(t('devices.taskCreated'));
onClose();
onTaskCreated();
} else {
utils.message.error(res.data.msg || '创建失败');
utils.message.error(res.data.msg || t('devices.createFailed'));
}
}).catch(() => utils.message.error('创建任务异常'));
}).catch(() => utils.message.error(t('devices.createTaskError')));
};
// ====================== 渲染 ======================
return (
<Modal
title="新建任务"
title={t('devices.newTask')}
open={isOpen}
onCancel={onClose}
width={500}
@@ -1935,20 +1941,20 @@ const NewTaskModal = ({ isOpen, onClose, robots, selectedPathWork, handlGetTaskL
{/* 任务名称 */}
<div>
<label className="block mb-2 font-medium">任务名称</label>
<label className="block mb-2 font-medium">{t('devices.taskName')}</label>
<Input
value={formData.planName}
onChange={(e) => setFormData({ ...formData, planName: e.target.value })}
placeholder="请输入任务名称"
placeholder={t('devices.inputTaskName')}
/>
</div>
{/* 执行路线 */}
<div>
<label className="block mb-2 font-medium">执行路线</label>
<label className="block mb-2 font-medium">{t('devices.executeRouteLabel')}</label>
<Select
className="w-full"
placeholder="选择路线"
placeholder={t('devices.selectRoute')}
onSelect={(_, option) => {
const route = routeList.find(r => r.id === option.value);
setFormData(prev => ({ ...prev, path: route }));
@@ -1968,10 +1974,10 @@ const NewTaskModal = ({ isOpen, onClose, robots, selectedPathWork, handlGetTaskL
{/* 选择设备 */}
<div className="relative">
<label className="block mb-2 font-medium">选择设备</label>
<label className="block mb-2 font-medium">{t('video.selectDevice')}</label>
<Select
className="w-full"
placeholder="选择设备"
placeholder={t('video.selectDevice')}
onSelect={(_, option) => {
const robot = robots.find(r => r.serialNumber === option.value);
setFormData(prev => ({ ...prev, device: robot }));
@@ -1991,15 +1997,15 @@ const NewTaskModal = ({ isOpen, onClose, robots, selectedPathWork, handlGetTaskL
{/* 重复计划 */}
<div>
<label className="block mb-2 font-medium">重复计划</label>
<label className="block mb-2 font-medium">{t('devices.repeatPlan')}</label>
<Select
value={formData.repeatPlan}
onChange={(val) => setFormData({ ...formData, repeatPlan: val })}
className="w-full"
>
<Select.Option value="DAY">天</Select.Option>
<Select.Option value="WEEK">周</Select.Option>
<Select.Option value="MONTH">月</Select.Option>
<Select.Option value="DAY">{t('devices.day')}</Select.Option>
<Select.Option value="WEEK">{t('devices.week')}</Select.Option>
<Select.Option value="MONTH">{t('devices.month')}</Select.Option>
</Select>
</div>
@@ -2039,38 +2045,38 @@ const NewTaskModal = ({ isOpen, onClose, robots, selectedPathWork, handlGetTaskL
{/* 按天:选择日期 */}
{formData.repeatPlan === 'DAY' && (
<>
<label className="block mb-2 font-medium">执行日期</label>
<label className="block mb-2 font-medium">{t('devices.execDate')}</label>
<DatePicker
onChange={handleDateChange}
className="w-full"
placeholder="选择执行日期"
placeholder={t('devices.selectExecDate')}
/>
</>
)}
{/* 时间 */}
<label className="block mb-2 font-medium">执行时间</label>
<label className="block mb-2 font-medium">{t('devices.execTime')}</label>
<TimePicker
format="HH:mm:ss"
onChange={handleTimeChange}
className="w-full"
placeholder="选择执行时间"
placeholder={t('devices.selectExecTime')}
/>
{/* 执行周期 */}
<label className="block mb-2 font-medium">执行周期</label>
<label className="block mb-2 font-medium">{t('devices.executionCycle')}</label>
<RangePicker
onChange={handleCycleChange}
className="w-full"
placeholder={['开始日期', '结束日期']}
placeholder={[t('devices.startDate'), t('devices.endDate')]}
showTime
format="YYYY-MM-DD HH:mm:ss"
/>
{/* 底部按钮 */}
<div className="flex justify-end gap-2 mt-6">
<Button onClick={onClose}>取消</Button>
<Button type="primary" onClick={handleSave}>保存任务</Button>
<Button onClick={onClose}>{t('common.cancel')}</Button>
<Button type="primary" onClick={handleSave}>{t('devices.saveTask')}</Button>
</div>
</div>
</Modal>

View File

@@ -30,11 +30,13 @@ import { useMqtt } from '@/hooks/useMqtt';
import envConfig from '../../../env';
import { useSelector } from 'react-redux';
import { RootState } from '@/store';
import { useTranslation } from 'react-i18next';
const { Text, Title } = Typography;
const { RangePicker } = DatePicker;
export default function WayLinePage({ onRefresh }) {
const { t } = useTranslation();
const { userInfo } = useSelector((state: RootState) => state.user);
const { stationId } = useSelector((state: RootState) => state.station);
@@ -74,7 +76,7 @@ export default function WayLinePage({ onRefresh }) {
latitude: item.latitude || 0,
},
deviceAlias: item.drone_callsign || item.device_sn
|| '无人机',
|| t('constants.uav'),
}));
@@ -356,7 +358,7 @@ export default function WayLinePage({ onRefresh }) {
const _list = res?.data?.list.map(item => {
return {
...item,
label: item.name || `航线_${item.id || '未知'}`,
label: item.name || `航线_${item.id || t('constants.locationUnknown')}`,
value: item.id
}
})
@@ -576,12 +578,12 @@ export default function WayLinePage({ onRefresh }) {
const statusMap = {
success: { color: 'success', text: '执行成功' },
preparing: { color: 'blue', text: '准备中' },
running: { color: 'processing', text: '执行中' },
running: { color: 'processing', text: t('workOrder.inProgress') },
failed: { color: 'error', text: '执行失败' },
waiting: { color: 'default', text: '待开始' },
starting_failure: { color: 'error', text: '启动失败' },
executing: { color: 'processing', text: '执行中' },
suspended: { color: 'warning', text: '已挂起' },
executing: { color: 'processing', text: t('workOrder.inProgress') },
suspended: { color: 'warning', text: t('workOrder.suspended') },
restored: { color: 'processing', text: '恢复执行' },
terminated: { color: 'default', text: '已终止' },
timeout: { color: 'error', text: '执行超时' },
@@ -592,15 +594,15 @@ export default function WayLinePage({ onRefresh }) {
// 列表表格列
const taskColumns = [
{ title: '任务名称', dataIndex: 'name', width: 260, ellipsis: true, fixed: "left" },
{ title: t('devices.taskName'), dataIndex: 'name', width: 260, ellipsis: true, fixed: "left" },
{ title: '设备SN', dataIndex: 'sn', width: 140 },
{ title: '状态', dataIndex: 'status', width: 80, render: getStatusTag },
{ title: t('roles.status'), dataIndex: 'status', width: 80, render: getStatusTag },
{
title: '开始时间', dataIndex: 'begin_at', width: 170,
title: t('devices.startTime'), dataIndex: 'begin_at', width: 170,
render: t => t ? dayjs(t).format('YYYY-MM-DD HH:mm:ss') : '-'
},
{
title: '结束时间', dataIndex: 'end_at', width: 170,
title: t('devices.endTime'), dataIndex: 'end_at', width: 170,
render: t => t ? dayjs(t).format('YYYY-MM-DD HH:mm:ss') : '-'
},
];
@@ -824,7 +826,7 @@ export default function WayLinePage({ onRefresh }) {
return (
<div className="normal-spacing" style={{ background: '#f5f7fa', minHeight: '100%', padding: '0px', paddingTop: 0 }}>
<div style={{ padding: '4px 0px 8px' }}>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>航线任务</Typography.Title>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>{t('devices.waylineTask')}</Typography.Title>
</div>
<Row gutter={[16, 16]} style={{ marginBottom: 8 }}>
{topCards.map((item, index) => (
@@ -881,10 +883,10 @@ export default function WayLinePage({ onRefresh }) {
>
清空规划航线
</Button>
<Button size="small" icon={<PlusOutlined />} style={{ borderRadius: 4 }}>新增</Button>
<Button size="small" icon={<ImportOutlined />} style={{ borderRadius: 4 }}>导入</Button>
<Button size="small" icon={<PlusOutlined />} style={{ borderRadius: 4 }}>{t('common.addNew')}</Button>
<Button size="small" icon={<ImportOutlined />} style={{ borderRadius: 4 }}>{t('common.import')}</Button>
<Button size="small" icon={<NodeIndexOutlined />} style={{ borderRadius: 4 }}>优化</Button>
<Button size="small" icon={<SaveOutlined />} style={{ borderRadius: 4 }}>保存</Button>
<Button size="small" icon={<SaveOutlined />} style={{ borderRadius: 4 }}>{t('common.save')}</Button>
<Button size="small" icon={<Maximize2 size={12} />} />
</Space>
}
@@ -921,7 +923,7 @@ export default function WayLinePage({ onRefresh }) {
format="YYYY-MM-DD HH:mm:ss"
value={listDateRange}
onChange={(val) => setListDateRange(val)}
placeholder={['开始时间', '结束时间']}
placeholder={[t('devices.startTime'), t('devices.endTime')]}
/>
<Select
style={{ width: 180 }}
@@ -996,11 +998,11 @@ export default function WayLinePage({ onRefresh }) {
]}
columns={[
{ title: '编号', dataIndex: 'id', width: 70, render: (t) => <Text style={{ color: '#165DFF', fontWeight: 600 }}>{t}</Text> },
{ title: '装备', dataIndex: 'device', width: 80 },
{ title: '类型', dataIndex: 'type', ellipsis: true },
{ title: '优先级', dataIndex: 'level', width: 80, render: (l) => <span style={{ color: l === '高' ? '#F53F3F' : l === '中' ? '#FF7D00' : '#86909C', fontSize: 11, fontWeight: 600 }}>▲ {l}</span> },
{ title: t('constants.sourceEquipment'), dataIndex: 'device', width: 80 },
{ title: t('common.type'), dataIndex: 'type', ellipsis: true },
{ title: t('workOrder.priority'), dataIndex: 'level', width: 80, render: (l) => <span style={{ color: l === '高' ? '#F53F3F' : l === '中' ? '#FF7D00' : '#86909C', fontSize: 11, fontWeight: 600 }}>▲ {l}</span> },
{ title: '建议时间', dataIndex: 'time', width: 110 },
{ title: '状态', dataIndex: 'status', width: 90, render: (s) => <Text style={{ color: s.includes('建议') ? '#165DFF' : '#86909C', fontSize: 11, fontWeight: 500 }}>{s}</Text> },
{ title: t('roles.status'), dataIndex: 'status', width: 90, render: (s) => <Text style={{ color: s.includes('建议') ? '#165DFF' : '#86909C', fontSize: 11, fontWeight: 500 }}>{s}</Text> },
]}
/>
<div style={{ background: '#F8F9FB', padding: '2px 14px', borderRadius: 8, marginTop: 8, border: '1px solid #F1F2F5' }}>
@@ -1016,7 +1018,7 @@ export default function WayLinePage({ onRefresh }) {
<Row gutter={12} style={{ marginTop: 8 }}>
<Col span={8}><Button type="primary" block style={{ background: '#165DFF', height: 28, borderRadius: 18, fontWeight: 600 }}>一键排程</Button></Col>
<Col span={8}><Button type="primary" block style={{ background: '#165DFF', height: 28, borderRadius: 18, fontWeight: 600 }}>智能分配</Button></Col>
<Col span={8}><Button type="primary" block style={{ background: '#165DFF', height: 28, borderRadius: 18, fontWeight: 600 }}>生成工单</Button></Col>
<Col span={8}><Button type="primary" block style={{ background: '#165DFF', height: 28, borderRadius: 18, fontWeight: 600 }}>{t('clean.generateOrder')}</Button></Col>
</Row>
</Card>
@@ -1051,7 +1053,7 @@ export default function WayLinePage({ onRefresh }) {
taskDetail?.repeat_type === 'relative_monthly' ? '按月(星期)' : '-',
unit: ''
},
{ label: '失控动作', value: taskDetail?.out_of_control_action_in_flight === 'return_home' ? '返航' : '-', unit: '' },
{ label: '失控动作', value: taskDetail?.out_of_control_action_in_flight === 'return_home' ? t('devices.returnHome') : '-', unit: '' },
].map((item, idx) => (
<div
key={idx}
@@ -1111,7 +1113,7 @@ export default function WayLinePage({ onRefresh }) {
style={{ background: '#165DFF', height: 28, borderRadius: 18, fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 4 }}
>
恢复任务
{/*{selectedTask?.status === 'suspended' ? '继续任务' : '执行任务'}*/}
{/*{selectedTask?.status === 'suspended' ? '继续任务' : t('devices.executeTask')}*/}
</Button>
)}
<Button
@@ -1122,7 +1124,7 @@ export default function WayLinePage({ onRefresh }) {
loading={flightCommandLoading}
style={{ background: '#165DFF', height: 28, borderRadius: 18, fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 4 }}
>
{isReturningHome ? '取消返航' : '返航'}
{isReturningHome ? '取消返航' : t('devices.returnHome')}
</Button>
<Button
@@ -1170,7 +1172,7 @@ export default function WayLinePage({ onRefresh }) {
<Thermometer size={20} style={{ color: '#FF7D00' }} />
</div>
<div>
<div style={{ fontSize: 12, color: '#86909C' }}>温度</div>
<div style={{ fontSize: 12, color: '#86909C' }}>{t('home.temperature')}</div>
<div style={{ fontSize: 14, fontWeight: 700, color: '#1D2129' }}>{fix2((mqttDerivedData || listDroneRealtime || listDrone)?.environment_temperature)}℃</div>
<div style={{ fontSize: 12, color: '#00B42A' }}>适宜</div>
</div>
@@ -1184,7 +1186,7 @@ export default function WayLinePage({ onRefresh }) {
<div style={{ fontSize: 12, color: '#86909C' }}>降水状态</div>
<div style={{ fontSize: 14, fontWeight: 700, color: '#1D2129' }}>{getVal((mqttDerivedData || listDroneRealtime || listDrone)?.rainfall)}</div>
<div style={{ fontSize: 12, color: (mqttDerivedData || listDroneRealtime || listDrone)?.rainfall === 'no_rain' ? '#00B42A' : '#F53F3F' }}>
{(mqttDerivedData || listDroneRealtime || listDrone)?.rainfall === 'no_rain' ? '低风险' : '有雨'}
{(mqttDerivedData || listDroneRealtime || listDrone)?.rainfall === 'no_rain' ? t('aiAnalysis.lowRisk') : '有雨'}
</div>
</div>
</div>
@@ -1407,7 +1409,7 @@ export default function WayLinePage({ onRefresh }) {
}))
},
{
title: '执行中', count: runList.length, color: '#165DFF', bg: '#E8F3FF',
title: t('workOrder.inProgress'), count: runList.length, color: '#165DFF', bg: '#E8F3FF',
list: runList.map(item => ({
id: item.sn?.slice(-5) || '',
name: item.name,
@@ -1416,7 +1418,7 @@ export default function WayLinePage({ onRefresh }) {
}))
},
{
title: '已完成', count: finishList.length, color: '#00B42A', bg: '#E8FFEA',
title: t('workOrder.completed'), count: finishList.length, color: '#00B42A', bg: '#E8FFEA',
list: finishList.map(item => ({
id: item.sn?.slice(-5) || '',
name: item.name,
@@ -1482,7 +1484,7 @@ export default function WayLinePage({ onRefresh }) {
<Input
value={taskForm.name}
onChange={(e) => setTaskForm({ ...taskForm, name: e.target.value })}
placeholder="请输入"
placeholder={t('common.pleaseInput')}
/>
</div>
<div>
@@ -1581,7 +1583,7 @@ export default function WayLinePage({ onRefresh }) {
{(taskForm.task_type === 'timed' || taskForm.task_type === 'recurring' || taskForm.task_type === 'continuous') && (
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm block mb-1">开始时间</label>
<label className="text-sm block mb-1">{t('devices.startTime')}</label>
<DatePicker
showTime
value={taskForm.begin_at}
@@ -1591,7 +1593,7 @@ export default function WayLinePage({ onRefresh }) {
</div>
{(taskForm.task_type === 'recurring' || taskForm.task_type === 'continuous') && (
<div>
<label className="text-sm block mb-1">结束时间</label>
<label className="text-sm block mb-1">{t('devices.endTime')}</label>
<DatePicker
showTime
value={taskForm.end_at}
@@ -1735,7 +1737,7 @@ export default function WayLinePage({ onRefresh }) {
{/* 按钮 */}
<div className="flex justify-end gap-2 mt-4">
<Button onClick={resetModals}>取消</Button>
<Button onClick={resetModals}>{t('common.cancel')}</Button>
<Button type="primary" loading={loading} onClick={createFlightTask}>确认创建</Button>
</div>
</div>
@@ -1749,7 +1751,7 @@ export default function WayLinePage({ onRefresh }) {
onCancel={() => setRthModalVisible(false)}
confirmLoading={loading}
okText="立即执行"
cancelText="取消"
cancelText={t('common.cancel')}
centered
width={400}
>

View File

@@ -6,6 +6,7 @@ import {
} from '../../api/device.ts';
import envConfig from '../../../env';
import { useTranslation } from 'react-i18next';
export default class DeviceController {
constructor(userInfo, messageApi) {
@@ -321,7 +322,7 @@ export default class DeviceController {
if (isSecure) return;
const type = obstacle?.type;
const distance = (obstacle?.distance || 0).toFixed(2);
const types = { 0: '未知', 1: '人', 2: '车辆', 3: '围栏', 4: '坑洞', 5: '草丛' };
const types = { 0: t('constants.locationUnknown'), 1: '人', 2: '车辆', 3: '围栏', 4: '坑洞', 5: '草丛' };
this.messageApi.warning({
key: 'obstacle-warning',
content: `检测到 ${types[type] ?? '未知物'},距离 ${distance}m`,

View File

@@ -1,39 +1,39 @@
import i18n from './i18n';
export const orderTypeConfig = {
1: '巡检',
2: '检修',
3: '清洗',
4: '维修',
5: '运维'
get 1() { return i18n.t('constants.inspection') },
get 2() { return i18n.t('constants.maintenance') },
get 3() { return i18n.t('constants.cleaning') },
get 4() { return i18n.t('constants.repair') },
get 5() { return i18n.t('constants.operation') },
};
export const sourceTypeMap = {
1: 'AI',
2: '无人机',
3: '清洗',
4: '视频',
5: '装备',
6: '人工',
7: '告警'
get 1() { return i18n.t('constants.sourceAI') },
get 2() { return i18n.t('constants.sourceUAV') },
get 3() { return i18n.t('constants.sourceCleaning') },
get 4() { return i18n.t('constants.sourceVideo') },
get 5() { return i18n.t('constants.sourceEquipment') },
get 6() { return i18n.t('constants.sourceManual') },
get 7() { return i18n.t('constants.sourceAlert') },
};
export const alarmTypeMap = {
'UAV_FAULT': '无人机故障',
'MOWER_FAULT': '割草机故障',
'OTHER_DEVICE_FAULT': '其他设备故障',
'SERVER_FAILURE': '服务器故障',
'SYSTEM_ERROR': '系统错误'
get 'UAV_FAULT'() { return i18n.t('constants.uavFault') },
get 'MOWER_FAULT'() { return i18n.t('constants.mowerFault') },
get 'OTHER_DEVICE_FAULT'() { return i18n.t('constants.otherDeviceFault') },
get 'SERVER_FAILURE'() { return i18n.t('constants.serverFailure') },
get 'SYSTEM_ERROR'() { return i18n.t('constants.systemError') },
};
export const alarmLevelConfig = {
1: { description: '严重', color: '#FF4D4F' },
2: { description: '重要', color: '#FA8C16' },
3: { description: '一般', color: '#FADB14' },
4: { description: '提示', color: '#1890FF' }
1: { get description() { return i18n.t('constants.critical') }, color: '#FF4D4F' },
2: { get description() { return i18n.t('constants.important') }, color: '#FA8C16' },
3: { get description() { return i18n.t('constants.general') }, color: '#FADB14' },
4: { get description() { return i18n.t('constants.info') }, color: '#1890FF' },
};
export const handleStatusMap = {
1: '待处理',
2: '已关闭',
get 1() { return i18n.t('constants.pending') },
get 2() { return i18n.t('constants.closed') },
};

27
src/i18n/index.ts Normal file
View File

@@ -0,0 +1,27 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import zhTranslation from '../locales/zh';
import enTranslation from '../locales/en';
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: {
zh: { translation: zhTranslation },
en: { translation: enTranslation },
},
fallbackLng: 'zh',
lng: localStorage.getItem('i18nextLng') || 'zh',
interpolation: {
escapeValue: false,
},
detection: {
order: ['localStorage', 'navigator'],
lookupLocalStorage: 'i18nextLng',
caches: ['localStorage'],
},
});
export default i18n;

View File

@@ -4,6 +4,7 @@ import Cookies from 'js-cookie'
import type { MessageInstance } from 'antd/es/message/interface';
import type { ModalStaticFunctions } from 'antd/es/modal/confirm';
import type { NotificationInstance } from 'antd/es/notification/interface';
import i18n from '../i18n';
let message: MessageInstance;
let notification: NotificationInstance;
@@ -41,13 +42,13 @@ export function cn(...inputs: ClassValue[]) {
export const getLocationQuality = (value) => {
const map = {
0: { text: '无效', color: '#ff4d4f' },
1: { text: 'GPS 单点定位', color: '#999' },
2: { text: 'DGPS 伪距差分/SBAS', color: '#1890ff' },
4: { text: 'RTK 固定解', color: '#52c41a' }, // 最优
5: { text: 'RTK 浮点解', color: '#faad14' },
0: { get text() { return i18n.t('constants.locationInvalid') }, color: '#ff4d4f' },
1: { get text() { return i18n.t('constants.locationGPS') }, color: '#999' },
2: { get text() { return i18n.t('constants.locationDGPS') }, color: '#1890ff' },
4: { get text() { return i18n.t('constants.locationRTKFixed') }, color: '#52c41a' }, // 最优
5: { get text() { return i18n.t('constants.locationRTKFloat') }, color: '#faad14' },
};
return map[value] || { text: '未知', color: '#999' };
return map[value] || { get text() { return i18n.t('constants.locationUnknown') }, color: '#999' };
};
@@ -175,14 +176,14 @@ export const fix2 = (val) => {
return Number(val).toFixed(2);
};
export const getVal = (val) => val == "no_rain" ? "无" : (val || '-');
export const getVal = (val) => val == "no_rain" ? i18n.t("constants.none") : (val || '-');
// ==============================================
// 通用导出表格为 CSV / Excel(通用版,不写死任何字段)
// ==============================================
export const exportTable = (columns, dataSource, fileName = "导出数据") => {
export const exportTable = (columns, dataSource, fileName = i18n.t("constants.exportData")) => {
// 1. 从 columns 自动提取表头(过滤掉不显示的列)
const headers = columns
.filter(col => col.title && col.dataIndex) // 只取有标题+有字段的列
@@ -249,9 +250,9 @@ export const errorSourceOptions: Array<{
value: ErrorSourceType;
label: string;
}> = [
{ label: '无人机', value: ERROR_SOURCE.UAV },
{ label: '割草机', value: ERROR_SOURCE.MOWER },
{ label: '其他', value: ERROR_SOURCE.OTHERS },
{ value: ERROR_SOURCE.UAV, get label() { return i18n.t('constants.uav') } },
{ value: ERROR_SOURCE.MOWER, get label() { return i18n.t('constants.mower') } },
{ value: ERROR_SOURCE.OTHERS, get label() { return i18n.t('constants.others') } },
];
export const getErrorSourceText = (source?: ErrorSourceType | string): string => {
@@ -262,9 +263,9 @@ export const getErrorSourceText = (source?: ErrorSourceType | string): string =>
// 错误等级
export const errorLevelOptions = [
{ label: '信息', value: 'INFO' },
{ label: '警告', value: 'WARNING' },
{ label: '错误', value: 'ERROR' },
{ value: 'INFO', get label() { return i18n.t('constants.infoLevel') } },
{ value: 'WARNING', get label() { return i18n.t('constants.warning') } },
{ value: 'ERROR', get label() { return i18n.t('constants.error') } },
];
// 文本 → 数字(提交接口用)
export const levelText2Num = {
@@ -287,16 +288,16 @@ export const LEVEL_TAG_COLOR = {
export const CompareEnumOptions = [
{ value: 'EQ', label: '等于', symbol: '=' },
{ value: 'NEQ', label: '不等于', symbol: '!=' },
{ value: 'GT', label: '大于', symbol: '>' },
{ value: 'LT', label: '小于', symbol: '<' },
{ value: 'GTE', label: '大于等于', symbol: '>=' },
{ value: 'LTE', label: '小于等于', symbol: '<=' },
{ value: 'BETWEEN', label: '在之间', symbol: 'between', range: true },
{ value: 'NOT_BETWEEN', label: '不在之间', symbol: 'notBetween', range: true },
{ value: 'CONTAIN', label: '包含', symbol: 'contain' },
{ value: 'NOT_CONTAIN', label: '不包含', symbol: 'notContain' },
{ value: 'EQ', get label() { return i18n.t('constants.eq') }, symbol: '=' },
{ value: 'NEQ', get label() { return i18n.t('constants.neq') }, symbol: '!=' },
{ value: 'GT', get label() { return i18n.t('constants.gt') }, symbol: '>' },
{ value: 'LT', get label() { return i18n.t('constants.lt') }, symbol: '<' },
{ value: 'GTE', get label() { return i18n.t('constants.gte') }, symbol: '>=' },
{ value: 'LTE', get label() { return i18n.t('constants.lte') }, symbol: '<=' },
{ value: 'BETWEEN', get label() { return i18n.t('constants.between') }, symbol: 'between', range: true },
{ value: 'NOT_BETWEEN', get label() { return i18n.t('constants.notBetween') }, symbol: 'notBetween', range: true },
{ value: 'CONTAIN', get label() { return i18n.t('constants.contain') }, symbol: 'contain' },
{ value: 'NOT_CONTAIN', get label() { return i18n.t('constants.notContain') }, symbol: 'notContain' },
];
// 工单类型常量
@@ -320,14 +321,14 @@ export const workOrderTypeOptions: Array<{
value: WorkOrderType;
label: string;
}> = [
{ value: WORK_ORDER_TYPE.MOWER_ERROR, label: "割草机故障" },
{ value: WORK_ORDER_TYPE.UAV_ERROR, label: "无人机故障" },
{ value: WORK_ORDER_TYPE.INSPECTION_TASK, label: "巡检" },
{ value: WORK_ORDER_TYPE.COMPONENT_DEFECT, label: "光伏组件故障" },
{ value: WORK_ORDER_TYPE.CLEAN_TASK, label: "清洗" },
{ value: WORK_ORDER_TYPE.MAINTAIN_TASK, label: "维护" },
{ value: WORK_ORDER_TYPE.REPAIR_TASK, label: "维修" },
{ value: WORK_ORDER_TYPE.OTHER, label: "其他" },
{ value: WORK_ORDER_TYPE.MOWER_ERROR, get label() { return i18n.t('workOrder.mowerError') } },
{ value: WORK_ORDER_TYPE.UAV_ERROR, get label() { return i18n.t('workOrder.uavError') } },
{ value: WORK_ORDER_TYPE.INSPECTION_TASK, get label() { return i18n.t('workOrder.typeInspection') } },
{ value: WORK_ORDER_TYPE.COMPONENT_DEFECT, get label() { return i18n.t('workOrder.componentDefect') } },
{ value: WORK_ORDER_TYPE.CLEAN_TASK, get label() { return i18n.t('workOrder.typeCleaning') } },
{ value: WORK_ORDER_TYPE.MAINTAIN_TASK, get label() { return i18n.t('workOrder.maintainTask') } },
{ value: WORK_ORDER_TYPE.REPAIR_TASK, get label() { return i18n.t('workOrder.typeRepair') } },
{ value: WORK_ORDER_TYPE.OTHER, get label() { return i18n.t('workOrder.other') } },
];
/**
@@ -351,10 +352,10 @@ export const levelColorMap = {
// 告警处理结果枚举(和后端AlarmHandleResult保持一致)
export const alarmHandleResultOptions = [
{ value: "Handled", label: "已处理" },
{ value: "Restored", label: "已恢复" },
{ value: "Ignored", label: "已忽略" },
{ value: "UnableToHandle", label: "无法处理" },
{ value: "ManufacturerHandling", label: "厂家处理中" },
{ value: "FalseAlarm", label: "误报" },
{ value: "Handled", get label() { return i18n.t("constants.handled") } },
{ value: "Restored", get label() { return i18n.t("constants.restored") } },
{ value: "Ignored", get label() { return i18n.t("constants.ignored") } },
{ value: "UnableToHandle", get label() { return i18n.t("constants.unableToHandle") } },
{ value: "ManufacturerHandling", get label() { return i18n.t("constants.manufacturerHandling") } },
{ value: "FalseAlarm", get label() { return i18n.t("constants.falseAlarm") } },
];

870
src/locales/en/index.ts Normal file
View File

@@ -0,0 +1,870 @@
const en = {
common: {
save: 'Save',
saveChanges: 'Save Changes',
cancel: 'Cancel',
confirm: 'Confirm',
ok: 'OK',
delete: 'Delete',
edit: 'Edit',
add: 'Add',
addNew: 'Add New',
search: 'Search',
search2: 'Search',
reset: 'Reset',
submit: 'Submit',
close: 'Close',
back: 'Back',
export: 'Export',
import: 'Import',
download: 'Download',
upload: 'Upload',
refresh: 'Refresh',
view: 'View',
viewAll: 'View All',
viewDetail: 'View Details',
operation: 'Action',
actions: 'Actions',
status: 'Status',
type: 'Type',
name: 'Name',
description: 'Description',
remark: 'Remark',
time: 'Time',
date: 'Date',
createTime: 'Created At',
updateTime: 'Updated At',
enable: 'Enable',
disable: 'Disable',
normal: 'Normal',
stopped: 'Disabled',
loading: 'Loading...',
noData: 'No Data',
success: 'Success',
failure: 'Failed',
deleteSuccess: 'Deleted successfully',
deleteFail: 'Delete failed',
editSuccess: 'Updated successfully',
editFail: 'Update failed',
addSuccess: 'Added successfully',
addFail: 'Add failed',
saveSuccess: 'Saved successfully',
saveFail: 'Save failed',
confirmDelete: 'Are you sure to delete?',
pleaseSelect: 'Please select',
pleaseInput: 'Please enter',
all: 'All',
yes: 'Yes',
no: 'No',
male: 'Male',
female: 'Female',
unknown: 'Unknown',
notSet: 'Not Set',
total: 'Total',
items: 'items',
selectAll: 'Select All',
batchDelete: 'Batch Delete',
selectedItems: '{{count}} items selected',
more: 'More',
expand: 'Expand',
collapse: 'Collapse',
monday: 'Monday',
tuesday: 'Tuesday',
wednesday: 'Wednesday',
thursday: 'Thursday',
friday: 'Friday',
saturday: 'Saturday',
sunday: 'Sunday',
},
layout: {
systemName: 'PV Station Intelligent Monitoring & O&M System',
selectStation: 'Select Station',
fullScreen: 'Fullscreen',
exitFullScreen: 'Exit Fullscreen',
logout: 'Logout',
stationAddress: 'Station Address',
stationType: 'Station Type',
runningStatus: 'Status',
normalRunning: 'Running Normal',
groundStation: 'Ground-mounted',
defaultStationName: '1MW PV Station',
defaultStationAddress: 'Linzhou, Henan',
moduleDeveloping: 'Module Under Development',
},
login: {
systemName: 'PV Station Intelligent Monitoring & O&M System',
accountPlaceholder: 'Enter account',
passwordPlaceholder: 'Enter password',
accountRequired: 'Please enter account',
passwordRequired: 'Please enter password',
agreeRequired: 'Please read and agree to the user agreement',
agreeText: 'I have read and agree to the',
userAgreement: 'User Agreement',
privacyPolicy: 'Privacy Policy',
loginButton: 'Login',
loginSuccess: 'Login successful',
loginFailed: 'Login failed',
welcome: 'Welcome',
},
home: {
title: 'Overview',
todayGeneration: 'Today\'s Generation',
currentPower: 'Current Power',
systemAvailability: 'System Availability',
alertCount: 'Alert Count',
costSaving: 'O&M Cost Savings',
dailyTarget: 'Daily Target',
installedCapacity: 'Installed Capacity',
yesterday: 'Yesterday',
unhandled: 'Unhandled',
closed: 'Closed',
monthlyAccumulated: 'Monthly Accumulated',
generationTrend: 'Generation Trend',
irradianceTempTrend: 'Irradiance & Temperature Trend',
inverterEfficiency: 'Inverter Efficiency Comparison',
maintenanceProgress: 'Maintenance Task Progress',
stationOverview: 'Station Overview',
recentInspection: 'Recent Inspection Records',
aiAnalysisSummary: 'AI Analysis Summary',
realtimeAlerts: 'Real-time Alerts',
deviceOnlineStatus: 'Device Online Status',
weatherInfo: 'Weather Information',
cleaningSuggestion: 'Cleaning Suggestion',
actual: 'Actual',
predict: 'Predicted',
irradiance: 'Irradiance',
temperature: 'Temperature',
combinerBox: 'Combiner Box',
inverter: 'Inverter',
inspectionRobot: 'Inspection Robot',
abnormal: 'Abnormal',
normal2: 'Normal',
generateCleanOrder: 'Generate Cleaning Order',
pleaseSelectStation: 'Please select a station first',
cleanOrderGenerated: 'Cleaning order generated successfully',
kWh: 'kWh',
kWp: 'kWp',
},
devices: {
deviceManagement: 'Device Management',
deviceOverview: 'Device Overview',
deviceStatus: 'Device Status',
waylineTask: 'Wayline Tasks',
robotTask: 'Robot Tasks',
deviceName: 'Device Name',
deviceType: 'Device Type',
deviceModel: 'Model',
onlineStatus: 'Online Status',
lastHeartbeat: 'Last Heartbeat',
batteryLevel: 'Battery',
signalStrength: 'Signal Strength',
position: 'Position',
control: 'Control',
uploadWayline: 'Upload Wayline',
downloadWayline: 'Download Wayline',
executeTask: 'Execute Task',
taskName: 'Task Name',
taskStatus: 'Task Status',
flightTime: 'Flight Time',
altitude: 'Altitude',
speed: 'Speed',
distance: 'Distance',
startTime: 'Start Time',
endTime: 'End Time',
drone: 'Drone',
mower: 'Mower',
robot: 'Robot',
camera: 'Camera',
online: 'Online',
offline: 'Offline',
standby: 'Standby',
running: 'Running',
charging: 'Charging',
idle: 'Idle',
busy: 'Busy',
error: 'Error',
deviceControl: 'Device Control',
realtimeVideo: 'Live Video',
takeoff: 'Take Off',
land: 'Land',
returnHome: 'Return Home',
pause: 'Pause',
resume: 'Resume',
emergencyStop: 'Emergency Stop',
forward: 'Forward',
backward: 'Backward',
left: 'Turn Left',
right: 'Turn Right',
up: 'Ascend',
down: 'Descend',
rotateLeft: 'Rotate Left',
rotateRight: 'Rotate Right',
gimbalPitch: 'Gimbal Pitch',
gimbalYaw: 'Gimbal Yaw',
zoomIn: 'Zoom In',
zoomOut: 'Zoom Out',
// Robot task page
device: 'Device',
creator: 'Creator',
executionPlan: 'Execution Plan',
sun: 'Sun',
mon: 'Mon',
tue: 'Tue',
wed: 'Wed',
thu: 'Thu',
fri: 'Fri',
sat: 'Sat',
planByDay: 'Daily | Start {{startDate}} {{dayTime}}',
planByWeek: 'Weekly | {{weekText}} {{dayTime}}',
planByMonth: 'Monthly | {{monthText}} {{dayTime}}',
daySuffix: '{{day}}',
unknownPlan: 'Unknown Plan',
executionCycle: 'Execution Cycle',
statusNew: 'New',
statusPaused: 'Paused',
statusFinished: 'Succeeded',
statusFailed: 'Failed',
confirmDeleteTask: 'Are you sure you want to delete this task?',
confirmCancelTask: 'Are you sure you want to cancel this task?',
confirmPauseTask: 'Are you sure you want to pause this task?',
confirmResumeTask: 'Are you sure you want to resume this task?',
recover: 'Resume',
statTodayPlan: "Today's Robot Plans",
unitPlan: 'items',
statInProgress: 'In Progress',
statWorkingRobots: 'Working Robots',
unitRobot: 'units',
statWorking: 'Working',
statTotalDistance: 'Total Distance',
vsYesterday: 'vs yesterday {{value}}',
statCompletionRate: 'Task Completion Rate',
statAlerts: 'Alerts',
unitTimes: 'times',
statOnlineRate: 'Online Rate',
good: 'Good',
robotRouteMonitor: 'Robot Path Planning & Monitoring',
clearRealtimeTrack: 'Clear Track',
clearPlanRoute: 'Clear Planned Route',
controlMode: 'Control Mode:',
localMode: 'Local',
remoteMode: 'Remote',
heading: 'Heading:',
initialized: 'Initialized',
uninitialized: 'Not Initialized',
location: 'Position:',
video: 'Video:',
taskPool: 'Task Pool',
taskExecutionPlan: 'Task Schedule',
taskHistory: 'Task History',
createTask: 'Create Task',
startDate: 'Start Date',
endDate: 'End Date',
allRobots: 'All Robots',
operationSuggestion: 'Operation Suggestions',
timelineMow: '{{robot}} starts mowing task',
timelineMowDetail: 'Estimated 2 hours, 45% completed',
timelineInspect: '{{robot}} scheduled inspection',
timelineInspectDetail: 'Pending approval, suggested area C',
timelineMaintenance: 'Routine Equipment Maintenance',
timelineMaintenanceDetail: 'All mowers returning to charging station',
autoGenerateTodayPlan: "Auto-generate Today's Plan",
deviceEnvironment: 'Device Environment & Status',
currentTemp: 'Temperature',
humidity: 'Humidity',
positionAccuracy: 'Position Accuracy',
rtkFixed: 'RTK Fixed',
networkLatency: 'Network Latency',
waterRiskWarning: 'Warning: Water accumulation risk in Area B, avoid it',
presetRoutes: 'Preset Routes',
searchPlaceholder: 'Search...',
unnamedRoute: 'Unnamed Route',
deleteRoute: 'Delete Route',
executeRoute: 'Execute Route',
taskBoard: 'Task Board',
pending: 'Pending',
executingEllipsis: 'Executing...',
paused: 'Paused',
executeRouteTask: 'Execute Route Task',
execute: 'Execute',
routeInfo: 'Route Info',
routeName: 'Route Name:',
selectExecuteDevice: 'Please select a device',
robotPanoramaVideo: 'Robot Panorama Video - {{robot}}',
wdSun: 'Su',
wdMon: 'Mo',
wdTue: 'Tu',
wdWed: 'We',
wdThu: 'Th',
wdFri: 'Fr',
wdSat: 'Sa',
deviceSerial: 'Device Serial',
route: 'Route',
fillRequired: 'Please fill in: {{fields}}',
selectExecTime: 'Please select execution time',
taskCreated: 'Task created successfully',
createFailed: 'Failed to create',
createTaskError: 'Task creation error',
newTask: 'New Task',
inputTaskName: 'Please enter task name',
executeRouteLabel: 'Execution Route',
selectRoute: 'Select Route',
repeatPlan: 'Repeat Plan',
day: 'Day',
week: 'Week',
month: 'Month',
execDate: 'Execution Date',
selectExecDate: 'Select date',
execTime: 'Execution Time',
selectExecTime: 'Select time',
saveTask: 'Save Task',
},
video: {
onlineVideo: 'Online Video',
videoMonitor: 'Video Monitor',
realtimeMonitor: 'Real-time Monitor',
videoWall: 'Video Wall',
videoDownload: 'Video Download',
fullScreen: 'Fullscreen',
snapshot: 'Snapshot',
record: 'Record',
play: 'Play',
pause: 'Pause',
stop: 'Stop',
mute: 'Mute',
unmute: 'Unmute',
live: 'Live',
replay: 'Replay',
selectDevice: 'Select Device',
selectCamera: 'Select Camera',
noVideo: 'No Video',
videoLoading: 'Loading video...',
streamQuality: 'Quality',
fluent: 'Fluent',
standard: 'SD',
high: 'HD',
super: 'UHD',
downloadTime: 'Download Time',
fileSize: 'File Size',
fileName: 'File Name',
downloadStatus: 'Download Status',
downloading: 'Downloading',
downloaded: 'Downloaded',
downloadFailed: 'Download Failed',
monitorMode: 'Monitor Mode',
singleView: '1-View',
fourView: '4-View',
nineView: '9-View',
sixteenView: '16-View',
},
alerts: {
title: 'Alert Center',
alertOverview: 'Alert Overview',
realtimeAlert: 'Real-time Alerts',
alarmHistory: 'Alert History',
alarmRules: 'Alert Rules',
alarmStatistics: 'Alert Statistics',
alarmSubscription: 'Alert Subscription',
alertLevel: 'Alert Level',
alertType: 'Alert Type',
alertSource: 'Alert Source',
alertTime: 'Alert Time',
handleStatus: 'Handle Status',
handleResult: 'Handle Result',
handleTime: 'Handle Time',
handler: 'Handler',
handleRemark: 'Handle Remark',
handle: 'Handle',
closeAlert: 'Close Alert',
confirmAlert: 'Confirm Alert',
exportAlert: 'Export Alerts',
critical: 'Critical',
important: 'Important',
general: 'General',
info: 'Info',
pending: 'Pending',
closed: 'Closed',
handled: 'Handled',
restored: 'Restored',
ignored: 'Ignored',
unableToHandle: 'Unable to Handle',
manufacturerHandling: 'Manufacturer Handling',
falseAlarm: 'False Alarm',
totalAlerts: 'Total Alerts',
todayAlerts: 'Today\'s Alerts',
unhandledAlerts: 'Unhandled Alerts',
handledRate: 'Handled Rate',
uavFault: 'UAV Fault',
mowerFault: 'Mower Fault',
otherDeviceFault: 'Other Device Fault',
serverFailure: 'Server Failure',
systemError: 'System Error',
alertTrend: 'Alert Trend',
alertDistribution: 'Alert Distribution',
},
aiAnalysis: {
title: 'AI Diagnosis',
thermalImaging: 'Thermal Imaging Analysis',
defectDetection: 'Defect Detection',
temperatureAnalysis: 'Temperature Analysis',
hotspotDetection: 'Hotspot Detection',
componentAnalysis: 'Component Analysis',
stringAnalysis: 'String Analysis',
inverterAnalysis: 'Inverter Analysis',
uploadImage: 'Upload Image',
startAnalysis: 'Start Analysis',
analysisResult: 'Analysis Result',
defectType: 'Defect Type',
defectLevel: 'Defect Level',
confidence: 'Confidence',
location: 'Location',
suggestion: 'Suggestion',
abnormalTemperature: 'Abnormal Temperature',
maxTemperature: 'Max Temperature',
avgTemperature: 'Avg Temperature',
minTemperature: 'Min Temperature',
normalTemp: 'Normal Temperature',
hotspotArea: 'Hotspot Area',
riskLevel: 'Risk Level',
highRisk: 'High Risk',
mediumRisk: 'Medium Risk',
lowRisk: 'Low Risk',
noRisk: 'No Risk',
detectedDefects: 'Defects Detected',
noDefects: 'No Defects Detected',
analysisHistory: 'Analysis History',
reanalyze: 'Re-analyze',
},
clean: {
title: 'Cleaning Optimization',
cleaningTask: 'Cleaning Tasks',
cleaningSuggestion: 'Cleaning Suggestion',
cleaningPlan: 'Cleaning Plan',
cleaningRecord: 'Cleaning Records',
cleaningEffect: 'Cleaning Effect',
efficiencyBefore: 'Efficiency Before',
efficiencyAfter: 'Efficiency After',
efficiencyImprovement: 'Efficiency Improvement',
estimatedRevenue: 'Estimated Revenue',
cleaningCost: 'Cleaning Cost',
netRevenue: 'Net Revenue',
priority: 'Priority',
suggestedDate: 'Suggested Date',
area: 'Cleaning Area',
method: 'Cleaning Method',
manualCleaning: 'Manual',
robotCleaning: 'Robot',
waterCleaning: 'Water',
dryCleaning: 'Dry',
status: 'Status',
pending: 'Pending',
inProgress: 'In Progress',
completed: 'Completed',
cancelled: 'Cancelled',
generateOrder: 'Generate Order',
soilingRate: 'Soiling Rate',
powerLoss: 'Power Loss',
lastCleanDate: 'Last Clean Date',
nextCleanDate: 'Suggested Clean Date',
},
workOrder: {
title: 'Work Orders',
orderTitle: 'Order Title',
orderType: 'Order Type',
orderStatus: 'Order Status',
priority: 'Priority',
source: 'Source',
execStartTime: 'Execution Start Time',
relatedDevice: 'Related Device',
relatedArea: 'Related Area',
currentStatus: 'Current Status',
taskDescription: 'Task Description',
attachments: 'Attachments',
execute: 'Execute',
modify: 'Modify',
suspend: 'Suspend',
complete: 'Complete',
dispatch: 'Dispatch',
modifyOrder: 'Modify Order',
dispatchOrder: 'Dispatch Order',
executeOrder: 'Execute Order',
completeOrder: 'Complete Order',
relatedAlerts7d: 'Related Alerts (Last 7 Days)',
pleaseInputTitle: 'Enter order title',
pleaseSelectType: 'Select order type',
pleaseSelectPriority: 'Select priority',
confirmModify: 'Confirm Modify',
confirmDispatch: 'Confirm Dispatch',
confirmExecute: 'Confirm Execute',
confirmComplete: 'Confirm Complete',
selectReceiver: 'Select Receiver',
pleaseSelectDispatcher: 'Select dispatch person',
selectDevice: 'Select Device',
confirmSuspend: 'Confirm suspend?',
created: 'Created',
inProgress: 'In Progress',
suspended: 'Suspended',
completed: 'Completed',
pending: 'Pending',
high: 'High',
medium: 'Medium',
low: 'Low',
aiSuggestion: 'AI Suggestions & Related Info',
suggestedEquipment: 'Suggested Equipment',
thermalCamera: 'Thermal Camera',
priorityInspect: 'Priority Inspect Components',
estimatedImpact: 'Estimated Generation Impact',
relatedAlertTrend: 'Related Alert Trend',
relatedAlertList: 'Related Alert List',
noAlerts7d: 'No alerts in the last 7 days',
near7Days: '(Last 7 Days)',
highAlarm: 'High',
midAlarm: 'Medium',
lowAlarm: 'Low',
typeInspection: 'Inspection',
typeMaintenance: 'Maintenance',
typeCleaning: 'Cleaning',
typeRepair: 'Repair',
typeOperation: 'O&M',
sourceAI: 'AI',
sourceUAV: 'UAV',
sourceCleaning: 'Cleaning',
sourceVideo: 'Video',
sourceEquipment: 'Equipment',
sourceManual: 'Manual',
sourceAlert: 'Alert',
mowerError: 'Mower Fault',
uavError: 'UAV Fault',
componentDefect: 'PV Component Defect',
cleanTask: 'Cleaning',
maintainTask: 'Maintenance',
repairTask: 'Repair',
other: 'Other',
},
report: {
title: 'Report Center',
generationReport: 'Generation Report',
deviceReport: 'Device Report',
alertReport: 'Alert Report',
workOrderReport: 'Work Order Report',
inspectionReport: 'Inspection Report',
cleaningReport: 'Cleaning Report',
efficiencyReport: 'Efficiency Report',
customReport: 'Custom Report',
daily: 'Daily',
weekly: 'Weekly',
monthly: 'Monthly',
quarterly: 'Quarterly',
yearly: 'Yearly',
dateRange: 'Date Range',
generateReport: 'Generate Report',
exportReport: 'Export Report',
totalGeneration: 'Total Generation',
peakPower: 'Peak Power',
averagePower: 'Average Power',
totalAlerts: 'Total Alerts',
totalWorkOrders: 'Total Work Orders',
completionRate: 'Completion Rate',
averageEfficiency: 'Average Efficiency',
},
systemSetting: {
title: 'System Settings',
basicSettings: 'Basic Settings',
userManagement: 'User Management',
menuManagement: 'Menu Management',
organizationManagement: 'Organization Management',
stationManagement: 'Station Management',
noticePolicy: 'Notification Policy',
deviceAccess: 'Device Access',
dataSafety: 'Data & Security',
systemMaintenance: 'System Maintenance',
onlineUsers: 'Online Users',
deviceManagement: 'Device Management',
logManagement: 'Log Management',
videoManagement: 'Video Management',
systemName: 'System Name',
systemVersion: 'System Version',
systemDescription: 'System Description',
defaultLanguage: 'Default Language',
systemLogo: 'System Logo',
theme: 'Theme',
light: 'Light',
dark: 'Dark',
followSystem: 'Follow System',
username: 'Username',
nickname: 'Nickname',
phone: 'Phone',
email: 'Email',
role: 'Role',
password: 'Password',
passwordPlaceholder: 'Leave empty to keep unchanged',
language: 'Language',
assignRole: 'Assign Role',
gender: 'Gender',
status: 'Status',
addUser: 'Add User',
editUser: 'Edit User',
resetPassword: 'Reset Password',
resetPasswordConfirm: 'Reset password to 123456?',
passwordResetTo: 'Password has been reset to: 123456',
chinese: 'Chinese',
english: 'English',
menuName: 'Menu Name',
menuType: 'Menu Type',
menuPath: 'Route Path',
menuIcon: 'Menu Icon',
menuSort: 'Sort Order',
permission: 'Permission Key',
parentMenu: 'Parent Menu',
directory: 'Directory',
menu: 'Menu',
button: 'Button',
orgName: 'Organization Name',
orgType: 'Organization Type',
parentOrg: 'Parent Organization',
leader: 'Leader',
contact: 'Contact Phone',
siteName: 'Station Name',
siteType: 'Station Type',
siteAddress: 'Station Address',
siteCapacity: 'Installed Capacity',
siteStatus: 'Station Status',
longitude: 'Longitude',
latitude: 'Latitude',
timezone: 'Timezone',
logType: 'Log Type',
loginLog: 'Login Log',
operationLog: 'Operation Log',
errorLog: 'Error Log',
systemLog: 'System Log',
operator: 'Operator',
operationContent: 'Operation Content',
operationTime: 'Operation Time',
ipAddress: 'IP Address',
videoName: 'Video Name',
videoUrl: 'Video URL',
videoType: 'Video Type',
videoStatus: 'Video Status',
backupDatabase: 'Backup Database',
clearCache: 'Clear Cache',
systemInfo: 'System Info',
diskUsage: 'Disk Usage',
memoryUsage: 'Memory Usage',
cpuUsage: 'CPU Usage',
serverStatus: 'Server Status',
uptime: 'Uptime',
accessProtocol: 'Access Protocol',
accessStatus: 'Access Status',
accessTime: 'Access Time',
snCode: 'SN Code',
verifyCode: 'Verify Code',
bindDevice: 'Bind Device',
// Error Management
errorCode: 'Error Code',
errorName: 'Error Name',
errorSourceLabel: 'Error Source',
errorLevelLabel: 'Error Level',
errorDesc: 'Error Description',
checkField: 'Check Field',
compareRule: 'Compare Rule',
compareValueCol: 'Compare Value',
compareValueLabel: 'Compare Value',
rangeMin: 'Range Min',
rangeMax: 'Range Max',
pleaseInputErrorCode: 'Please enter error code',
pleaseInputErrorName: 'Please enter error name',
pleaseSelectSource: 'Please select source',
pleaseSelectLevel: 'Please select level',
pleaseSelectErrorSource: 'Please select error source',
pleaseSelectErrorLevel: 'Please select error level',
pleaseInputCheckField: 'Please enter check field name',
pleaseSelectCompareRule: 'Please select compare rule',
pleaseInputMinValue: 'Please enter min value',
pleaseInputMaxValue: 'Please enter max value',
pleaseInputCompareValue: 'Please enter compare value',
errorCodePlaceholder: 'e.g., ERR-ROBOT-001',
checkFieldPlaceholder: 'e.g., battery, speed, temperature',
minValuePlaceholder: 'Min value',
maxValuePlaceholder: 'Max value',
compareValuePlaceholder: 'Number/text, separated by commas',
errorDescPlaceholder: 'Describe the scenario that triggers this error',
suggestionPlaceholder: 'Solution after this error occurs',
addErrorRule: 'Add Error Rule',
editErrorRule: 'Edit Error Rule',
viewErrorRuleDetail: 'View Error Rule Detail',
confirmDeleteError: 'Are you sure to delete error "{{name}}"?',
getErrorListFailed: 'Failed to get error list',
operationFailed: 'Operation failed',
},
profile: {
title: 'Profile',
getProfileFailed: 'Failed to get profile',
modifySuccess: 'Modified successfully',
modifyFailed: 'Modification failed',
passwordChanged: 'Password changed successfully',
avatarUploadSuccess: 'Avatar uploaded successfully',
avatarUploadFailed: 'Upload failed',
passwordMismatch: 'The two passwords do not match',
changeAvatar: 'Change Avatar',
username: 'Username',
nickname: 'Nickname',
gender: 'Gender',
phone: 'Phone',
email: 'Email',
role: 'Role',
remark: 'Remark',
oldPassword: 'Old Password',
newPassword: 'New Password',
confirmPassword: 'Confirm Password',
nicknamePlaceholder: 'Enter nickname',
phonePlaceholder: 'Enter phone number',
saveChanges: 'Save Changes',
confirmChange: 'Confirm Change',
male: 'Male',
female: 'Female',
notSet: 'Not Set',
},
users: {
title: 'User Management',
addUser: 'Add User',
username: 'Username',
nickname: 'Nickname',
phone: 'Phone',
email: 'Email',
role: 'Role',
status: 'Status',
actions: 'Actions',
edit: 'Edit',
delete: 'Delete',
usernameLabel: 'Username',
nicknameLabel: 'Nickname',
phoneLabel: 'Phone',
passwordLabel: 'Password',
languageLabel: 'Language',
assignRoleLabel: 'Assign Role',
statusLabel: 'Status',
genderLabel: 'Gender',
passwordPlaceholder: 'Leave empty to keep unchanged',
chinese: 'Chinese',
english: 'English',
normal: 'Normal',
stopped: 'Disabled',
male: 'Male',
female: 'Female',
unknown: 'Unknown',
batchDelete: 'Batch Delete ({{count}} selected)',
resetPassword: 'Reset Password',
passwordResetTo: 'Password has been reset to: 123456',
resetPasswordConfirm: 'Reset password to 123456?',
},
roles: {
title: 'Role Management',
addRole: 'Add Role',
roleName: 'Role Name',
roleKey: 'Role Key',
roleSort: 'Sort Order',
status: 'Status',
remark: 'Remark',
actions: 'Actions',
edit: 'Edit',
delete: 'Delete',
assignPermissions: 'Assign Permissions',
dataScope: 'Data Scope',
menuPermissions: 'Menu Permissions',
roleDescription: 'Role Description',
normal: 'Normal',
stopped: 'Disabled',
},
constants: {
inspection: 'Inspection',
maintenance: 'Maintenance',
cleaning: 'Cleaning',
repair: 'Repair',
operation: 'O&M',
sourceAI: 'AI',
sourceUAV: 'UAV',
sourceCleaning: 'Cleaning',
sourceVideo: 'Video',
sourceEquipment: 'Equipment',
sourceManual: 'Manual',
sourceAlert: 'Alert',
uavFault: 'UAV Fault',
mowerFault: 'Mower Fault',
otherDeviceFault: 'Other Device Fault',
serverFailure: 'Server Failure',
systemError: 'System Error',
critical: 'Critical',
important: 'Important',
general: 'General',
info: 'Info',
pending: 'Pending',
closed: 'Closed',
uav: 'UAV',
mower: 'Mower',
others: 'Others',
infoLevel: 'Info',
warning: 'Warning',
error: 'Error',
handled: 'Handled',
restored: 'Restored',
ignored: 'Ignored',
unableToHandle: 'Unable to Handle',
manufacturerHandling: 'Manufacturer Handling',
falseAlarm: 'False Alarm',
eq: 'Equals',
neq: 'Not Equals',
gt: 'Greater Than',
lt: 'Less Than',
gte: 'Greater or Equal',
lte: 'Less or Equal',
between: 'Between',
notBetween: 'Not Between',
contain: 'Contains',
notContain: 'Does Not Contain',
locationInvalid: 'Invalid',
locationGPS: 'GPS Single Point',
locationDGPS: 'DGPS / SBAS',
locationRTKFixed: 'RTK Fixed',
locationRTKFloat: 'RTK Float',
locationUnknown: 'Unknown',
exportData: 'Exported Data',
none: 'None',
},
router: {
overview: 'Overview',
devices: 'Device Management',
video: 'Online Video',
monitor: 'Real-time Monitor',
alerts: 'Alert Center',
aiAnalysis: 'AI Diagnosis',
clean: 'Cleaning Optimization',
task: 'Work Orders',
table: 'Report Center',
systemSetting: 'System Settings',
log: 'Log Management',
videoManage: 'Video Management',
downloads: 'Video Download',
users: 'User Management',
roles: 'Role Management',
menuList: 'Menu Management',
organization: 'Organization Management',
station: 'Station Management',
realtimeMonitor: 'Real-time Monitor',
profile: 'Profile',
noticePolicy: 'Notification Policy',
dataSafety: 'Data & Security',
moduleDeveloping: 'Module Under Development',
pleaseSelectMenu: 'Please select a menu',
},
api: {
loginExpired: 'Login expired, please log in again',
networkError: 'Network error, please try again later',
},
weather: {
sunny: 'Sunny',
cloudy: 'Cloudy',
overcast: 'Overcast',
rainy: 'Rainy',
},
};
export default en;

895
src/locales/zh/index.ts Normal file
View File

@@ -0,0 +1,895 @@
const zh = {
common: {
// 按钮
save: '保存',
saveChanges: '保存修改',
cancel: '取消',
confirm: '确认',
ok: '确定',
delete: '删除',
edit: '编辑',
add: '新增',
addNew: '新增',
search: '搜索',
search2: '查询',
reset: '重置',
submit: '提交',
close: '关闭',
back: '返回',
export: '导出',
import: '导入',
download: '下载',
upload: '上传',
refresh: '刷新',
view: '查看',
viewAll: '查看全部',
viewDetail: '查看详情',
operation: '操作',
actions: '操作',
status: '状态',
type: '类型',
name: '名称',
description: '描述',
remark: '备注',
time: '时间',
date: '日期',
createTime: '创建时间',
updateTime: '更新时间',
enable: '启用',
disable: '停用',
normal: '正常',
stopped: '停用',
loading: '加载中...',
noData: '暂无数据',
success: '成功',
failure: '失败',
deleteSuccess: '删除成功',
deleteFail: '删除失败',
editSuccess: '编辑成功',
editFail: '编辑失败',
addSuccess: '新增成功',
addFail: '新增失败',
saveSuccess: '保存成功',
saveFail: '保存失败',
confirmDelete: '确定删除?',
pleaseSelect: '请选择',
pleaseInput: '请输入',
all: '全部',
yes: '是',
no: '否',
male: '男',
female: '女',
unknown: '未知',
notSet: '未设置',
total: '共',
items: '条',
selectAll: '全选',
batchDelete: '批量删除',
selectedItems: '已选 {{count}} 项',
more: '更多',
expand: '展开',
collapse: '收起',
// 星期
monday: '星期一',
tuesday: '星期二',
wednesday: '星期三',
thursday: '星期四',
friday: '星期五',
saturday: '星期六',
sunday: '星期日',
},
layout: {
systemName: '光伏电站智能监控与运维系统',
selectStation: '请选择电站',
fullScreen: '全屏',
exitFullScreen: '退出全屏',
logout: '退出登录',
stationAddress: '电站地址',
stationType: '电站类型',
runningStatus: '运行状态',
normalRunning: '正常运行',
groundStation: '地面电站',
defaultStationName: '1MW 光伏电站',
defaultStationAddress: '河南省林州',
moduleDeveloping: '模块开发中',
},
login: {
systemName: '光伏电站智能监控与运维系统',
accountPlaceholder: '请输入账号',
passwordPlaceholder: '请输入密码',
accountRequired: '请输入账号',
passwordRequired: '请输入密码',
agreeRequired: '请阅读并同意用户协议',
agreeText: '我已阅读并同意',
userAgreement: '用户协议',
privacyPolicy: '隐私政策',
loginButton: '登录',
loginSuccess: '登录成功',
loginFailed: '登录失败',
welcome: '欢迎',
},
home: {
title: '总览首页',
todayGeneration: '今日发电量',
currentPower: '当前功率',
systemAvailability: '系统可用率',
alertCount: '告警数量',
costSaving: '节省运维成本',
dailyTarget: '日目标',
installedCapacity: '装机容量',
yesterday: '昨日',
unhandled: '未处理',
closed: '已关闭',
monthlyAccumulated: '本月累计节省',
generationTrend: '发电趋势',
irradianceTempTrend: '辐照度与温度趋势',
inverterEfficiency: '逆变器效率对比',
maintenanceProgress: '运维任务进度',
stationOverview: '电站概览',
recentInspection: '近期巡检记录',
aiAnalysisSummary: 'AI 分析摘要',
realtimeAlerts: '实时告警',
deviceOnlineStatus: '设备在线状态',
weatherInfo: '天气信息',
cleaningSuggestion: '清洗建议',
actual: '实际',
predict: '预测',
irradiance: '辐照度',
temperature: '温度',
combinerBox: '汇流箱',
inverter: '逆变器',
inspectionRobot: '巡检机器人',
abnormal: '异常',
normal2: '正常',
generateCleanOrder: '生成清洗工单',
pleaseSelectStation: '请先选择电站',
cleanOrderGenerated: '清洗工单生成成功',
kWh: 'kWh',
kWp: 'kWp',
},
devices: {
deviceManagement: '设备管理',
deviceOverview: '设备总览',
deviceStatus: '设备状态',
waylineTask: '航线任务',
robotTask: '机器人任务',
deviceName: '设备名称',
deviceType: '设备类型',
deviceModel: '设备型号',
onlineStatus: '在线状态',
lastHeartbeat: '最后心跳',
batteryLevel: '电量',
signalStrength: '信号强度',
position: '位置',
control: '控制',
uploadWayline: '上传航线',
downloadWayline: '下载航线',
executeTask: '执行任务',
taskName: '任务名称',
taskStatus: '任务状态',
flightTime: '飞行时间',
altitude: '高度',
speed: '速度',
distance: '距离',
startTime: '开始时间',
endTime: '结束时间',
drone: '无人机',
mower: '割草机',
robot: '机器人',
camera: '摄像头',
online: '在线',
offline: '离线',
standby: '待机',
running: '运行中',
charging: '充电中',
idle: '空闲',
busy: '忙碌',
error: '故障',
deviceControl: '设备控制',
realtimeVideo: '实时视频',
takeoff: '起飞',
land: '降落',
returnHome: '返航',
pause: '暂停',
resume: '继续',
emergencyStop: '紧急停止',
forward: '前进',
backward: '后退',
left: '左转',
right: '右转',
up: '上升',
down: '下降',
rotateLeft: '左旋',
rotateRight: '右旋',
gimbalPitch: '云台俯仰',
gimbalYaw: '云台偏航',
zoomIn: '放大',
zoomOut: '缩小',
// 机器人任务页
device: '设备',
creator: '创建者',
executionPlan: '执行计划',
sun: '周日',
mon: '周一',
tue: '周二',
wed: '周三',
thu: '周四',
fri: '周五',
sat: '周六',
planByDay: '按天 | 开始日期{{startDate}} {{dayTime}}',
planByWeek: '按周 | {{weekText}} {{dayTime}}',
planByMonth: '按月 | {{monthText}} {{dayTime}}',
daySuffix: '{{day}}日',
unknownPlan: '未知计划',
executionCycle: '执行周期',
statusNew: '新建',
statusPaused: '暂停中',
statusFinished: '执行成功',
statusFailed: '执行失败',
confirmDeleteTask: '确定要删除该任务吗?',
confirmCancelTask: '确定要取消该任务吗?',
confirmPauseTask: '确定要暂停该任务吗?',
confirmResumeTask: '确定要恢复该任务吗?',
recover: '恢复',
statTodayPlan: '今日机器人计划',
unitPlan: '条',
statInProgress: '进行中',
statWorkingRobots: '作业中机器人',
unitRobot: '台',
statWorking: '作业中',
statTotalDistance: '累计作业里程',
vsYesterday: '较昨日 {{value}}',
statCompletionRate: '任务完成率',
statAlerts: '异常告警',
unitTimes: '次',
statOnlineRate: '在线率',
good: '良好',
robotRouteMonitor: '机器人路径规划与监控',
clearRealtimeTrack: '清空实时轨迹',
clearPlanRoute: '清空规划航线',
controlMode: '控制模式:',
localMode: '本地',
remoteMode: '远程',
heading: '航向:',
initialized: '已初始化',
uninitialized: '未初始化',
location: '定位:',
video: '视频:',
taskPool: '任务池',
taskExecutionPlan: '任务执行计划',
taskHistory: '历史任务',
createTask: '创建任务',
startDate: '开始日期',
endDate: '结束日期',
allRobots: '全部机器人',
operationSuggestion: '作业编排建议',
timelineMow: '{{robot}} 开始除草任务',
timelineMowDetail: '预计耗时 2 小时,当前进度 45%',
timelineInspect: '{{robot}} 预定巡检任务',
timelineInspectDetail: '待审批,建议执行区域 C',
timelineMaintenance: '设备例行维护',
timelineMaintenanceDetail: '所有割草机返回充电站',
autoGenerateTodayPlan: '智能生成今日计划',
deviceEnvironment: '设备环境与状态',
currentTemp: '当前温度',
humidity: '环境湿度',
positionAccuracy: '定位精度',
rtkFixed: 'RTK 固定',
networkLatency: '网络延迟',
waterRiskWarning: '注意:区域 B 存在积水风险,建议避开',
presetRoutes: '预设路线',
searchPlaceholder: '搜索...',
unnamedRoute: '未命名路线',
deleteRoute: '删除航线',
executeRoute: '执行航线',
taskBoard: '任务看板',
pending: '待执行',
executingEllipsis: '正在执行...',
paused: '已暂停',
executeRouteTask: '执行路线任务',
execute: '执行',
routeInfo: '路线信息',
routeName: '路线名称:',
selectExecuteDevice: '请选择执行设备',
robotPanoramaVideo: '机器人全景视频 - {{robot}}',
wdSun: '日',
wdMon: '一',
wdTue: '二',
wdWed: '三',
wdThu: '四',
wdFri: '五',
wdSat: '六',
deviceSerial: '设备编号',
route: '路线',
fillRequired: '请填写:{{fields}}',
selectExecTime: '请选择执行时间',
taskCreated: '任务创建成功',
createFailed: '创建失败',
createTaskError: '创建任务异常',
newTask: '新建任务',
inputTaskName: '请输入任务名称',
executeRouteLabel: '执行路线',
selectRoute: '选择路线',
repeatPlan: '重复计划',
day: '天',
week: '周',
month: '月',
execDate: '执行日期',
selectExecDate: '选择执行日期',
execTime: '执行时间',
selectExecTime: '选择执行时间',
saveTask: '保存任务',
},
video: {
onlineVideo: '在线视频',
videoMonitor: '视频监控',
realtimeMonitor: '实时监控',
videoWall: '视频墙',
videoDownload: '视频下载',
fullScreen: '全屏',
snapshot: '截图',
record: '录制',
play: '播放',
pause: '暂停',
stop: '停止',
mute: '静音',
unmute: '取消静音',
live: '直播',
replay: '回放',
selectDevice: '选择设备',
selectCamera: '选择摄像头',
noVideo: '暂无视频',
videoLoading: '视频加载中...',
streamQuality: '画质',
fluent: '流畅',
standard: '标清',
high: '高清',
super: '超清',
downloadTime: '下载时间',
fileSize: '文件大小',
fileName: '文件名',
downloadStatus: '下载状态',
downloading: '下载中',
downloaded: '已下载',
downloadFailed: '下载失败',
monitorMode: '监控模式',
singleView: '单画面',
fourView: '四画面',
nineView: '九画面',
sixteenView: '十六画面',
},
alerts: {
title: '告警中心',
alertOverview: '告警概览',
realtimeAlert: '实时告警',
alarmHistory: '历史告警',
alarmRules: '告警规则',
alarmStatistics: '告警统计',
alarmSubscription: '告警订阅',
alertLevel: '告警级别',
alertType: '告警类型',
alertSource: '告警来源',
alertTime: '告警时间',
handleStatus: '处理状态',
handleResult: '处理结果',
handleTime: '处理时间',
handler: '处理人',
handleRemark: '处理备注',
handle: '处理',
closeAlert: '关闭告警',
confirmAlert: '确认告警',
exportAlert: '导出告警',
critical: '严重',
important: '重要',
general: '一般',
info: '提示',
pending: '待处理',
closed: '已关闭',
handled: '已处理',
restored: '已恢复',
ignored: '已忽略',
unableToHandle: '无法处理',
manufacturerHandling: '厂家处理中',
falseAlarm: '误报',
totalAlerts: '告警总数',
todayAlerts: '今日告警',
unhandledAlerts: '未处理告警',
handledRate: '处理率',
uavFault: '无人机故障',
mowerFault: '割草机故障',
otherDeviceFault: '其他设备故障',
serverFailure: '服务器故障',
systemError: '系统错误',
alertTrend: '告警趋势',
alertDistribution: '告警分布',
},
aiAnalysis: {
title: 'AI诊断分析',
thermalImaging: '热成像分析',
defectDetection: '缺陷检测',
temperatureAnalysis: '温度分析',
hotspotDetection: '热斑检测',
componentAnalysis: '组件分析',
stringAnalysis: '组串分析',
inverterAnalysis: '逆变器分析',
uploadImage: '上传图片',
startAnalysis: '开始分析',
analysisResult: '分析结果',
defectType: '缺陷类型',
defectLevel: '缺陷等级',
confidence: '置信度',
location: '位置',
suggestion: '处理建议',
abnormalTemperature: '异常温度',
maxTemperature: '最高温度',
avgTemperature: '平均温度',
minTemperature: '最低温度',
normalTemp: '正常温度',
hotspotArea: '热斑区域',
riskLevel: '风险等级',
highRisk: '高风险',
mediumRisk: '中风险',
lowRisk: '低风险',
noRisk: '无风险',
detectedDefects: '检测到缺陷',
noDefects: '未检测到缺陷',
analysisHistory: '分析历史',
reanalyze: '重新分析',
},
clean: {
title: '清洗优化',
cleaningTask: '清洗任务',
cleaningSuggestion: '清洗建议',
cleaningPlan: '清洗计划',
cleaningRecord: '清洗记录',
cleaningEffect: '清洗效果',
efficiencyBefore: '清洗前效率',
efficiencyAfter: '清洗后效率',
efficiencyImprovement: '效率提升',
estimatedRevenue: '预计收益',
cleaningCost: '清洗成本',
netRevenue: '净收益',
priority: '优先级',
suggestedDate: '建议日期',
area: '清洗区域',
method: '清洗方式',
manualCleaning: '人工清洗',
robotCleaning: '机器人清洗',
waterCleaning: '水清洗',
dryCleaning: '干清洗',
status: '状态',
pending: '待清洗',
inProgress: '清洗中',
completed: '已清洗',
cancelled: '已取消',
generateOrder: '生成工单',
soilingRate: '污损率',
powerLoss: '功率损失',
lastCleanDate: '上次清洗日期',
nextCleanDate: '建议清洗日期',
},
workOrder: {
title: '工单任务',
orderTitle: '工单标题',
orderType: '工单类型',
orderStatus: '工单状态',
priority: '优先级',
source: '来源',
execStartTime: '执行开始时间',
relatedDevice: '关联设备',
relatedArea: '关联区域',
currentStatus: '当前状态',
taskDescription: '任务描述',
attachments: '附件素材',
execute: '执行工单',
modify: '修改',
suspend: '挂起',
complete: '完成',
dispatch: '派单',
modifyOrder: '修改工单',
dispatchOrder: '工单派单',
executeOrder: '执行工单',
completeOrder: '完成工单',
relatedAlerts7d: '近7天全部关联告警',
pleaseInputTitle: '请输入工单标题',
pleaseSelectType: '请选择工单类型',
pleaseSelectPriority: '请选择优先级',
confirmModify: '确认修改',
confirmDispatch: '确认派单',
confirmExecute: '确认执行',
confirmComplete: '确认完成',
selectReceiver: '选择接收人员',
pleaseSelectDispatcher: '请选择派单人员',
selectDevice: '选择执行设备',
confirmSuspend: '确定挂起?',
created: '已创建',
inProgress: '执行中',
suspended: '已挂起',
completed: '已完成',
pending: '待处理',
high: '高',
medium: '中',
low: '低',
aiSuggestion: 'AI 建议与关联信息',
suggestedEquipment: '建议携带',
thermalCamera: '红外热像设备',
priorityInspect: '优先复检组件',
estimatedImpact: '预计影响发电',
relatedAlertTrend: '关联告警趋势',
relatedAlertList: '关联告警列表',
noAlerts7d: '近7天无告警记录',
near7Days: '(近 7 天)',
highAlarm: '高告警',
midAlarm: '中告警',
lowAlarm: '低告警',
// 工单类型
typeInspection: '巡检',
typeMaintenance: '检修',
typeCleaning: '清洗',
typeRepair: '维修',
typeOperation: '运维',
// 来源
sourceAI: 'AI',
sourceUAV: '无人机',
sourceCleaning: '清洗',
sourceVideo: '视频',
sourceEquipment: '装备',
sourceManual: '人工',
sourceAlert: '告警',
// 工单类型枚举
mowerError: '割草机故障',
uavError: '无人机故障',
componentDefect: '光伏组件故障',
cleanTask: '清洗',
maintainTask: '维护',
repairTask: '维修',
other: '其他',
},
report: {
title: '报表中心',
generationReport: '发电量报表',
deviceReport: '设备报表',
alertReport: '告警报表',
workOrderReport: '工单报表',
inspectionReport: '巡检报表',
cleaningReport: '清洗报表',
efficiencyReport: '效率报表',
customReport: '自定义报表',
daily: '日报',
weekly: '周报',
monthly: '月报',
quarterly: '季报',
yearly: '年报',
dateRange: '日期范围',
generateReport: '生成报表',
exportReport: '导出报表',
totalGeneration: '总发电量',
peakPower: '峰值功率',
averagePower: '平均功率',
totalAlerts: '告警总数',
totalWorkOrders: '工单总数',
completionRate: '完成率',
averageEfficiency: '平均效率',
},
systemSetting: {
title: '系统设置',
basicSettings: '基础设置',
userManagement: '用户管理',
menuManagement: '菜单管理',
organizationManagement: '组织管理',
stationManagement: '场站管理',
noticePolicy: '通知策略',
deviceAccess: '设备接入',
dataSafety: '数据与安全',
systemMaintenance: '系统维护',
onlineUsers: '在线用户',
deviceManagement: '设备管理',
logManagement: '日志管理',
videoManagement: '视频管理',
systemName: '系统名称',
systemVersion: '系统版本',
systemDescription: '系统描述',
defaultLanguage: '默认语言',
systemLogo: '系统Logo',
theme: '主题',
light: '浅色',
dark: '深色',
followSystem: '跟随系统',
// 用户管理
username: '用户账号',
nickname: '用户昵称',
phone: '手机号',
email: '邮箱',
role: '角色',
password: '密码',
passwordPlaceholder: '不填则不修改',
language: '语言',
assignRole: '分配角色',
gender: '性别',
status: '状态',
addUser: '新增用户',
editUser: '编辑用户',
resetPassword: '重置密码',
resetPasswordConfirm: '确定将密码重置为 123456 吗?',
passwordResetTo: '密码已重置为:123456',
chinese: '中文',
english: '英语',
// 菜单管理
menuName: '菜单名称',
menuType: '菜单类型',
menuPath: '路由地址',
menuIcon: '菜单图标',
menuSort: '排序',
permission: '权限标识',
parentMenu: '上级菜单',
directory: '目录',
menu: '菜单',
button: '按钮',
// 组织管理
orgName: '组织名称',
orgType: '组织类型',
parentOrg: '上级组织',
leader: '负责人',
contact: '联系电话',
// 场站管理
siteName: '场站名称',
siteType: '场站类型',
siteAddress: '场站地址',
siteCapacity: '装机容量',
siteStatus: '场站状态',
longitude: '经度',
latitude: '纬度',
timezone: '时区',
// 日志管理
logType: '日志类型',
loginLog: '登录日志',
operationLog: '操作日志',
errorLog: '错误日志',
systemLog: '系统日志',
operator: '操作人',
operationContent: '操作内容',
operationTime: '操作时间',
ipAddress: 'IP地址',
// 视频管理
videoName: '视频名称',
videoUrl: '视频地址',
videoType: '视频类型',
videoStatus: '视频状态',
// 系统维护
backupDatabase: '备份数据库',
clearCache: '清除缓存',
systemInfo: '系统信息',
diskUsage: '磁盘使用',
memoryUsage: '内存使用',
cpuUsage: 'CPU使用率',
serverStatus: '服务器状态',
uptime: '运行时长',
// 设备接入
accessProtocol: '接入协议',
accessStatus: '接入状态',
accessTime: '接入时间',
snCode: 'SN码',
verifyCode: '验证码',
bindDevice: '绑定设备',
// 错误管理
errorCode: '错误编码',
errorName: '错误名称',
errorSourceLabel: '错误来源',
errorLevelLabel: '错误等级',
errorDesc: '错误描述',
checkField: '校验字段',
compareRule: '对比规则',
compareValueCol: '对比数值',
compareValueLabel: '对比值',
rangeMin: '区间最小值',
rangeMax: '区间最大值',
pleaseInputErrorCode: '请输入错误编码',
pleaseInputErrorName: '请输入错误名称',
pleaseSelectSource: '请选择来源',
pleaseSelectLevel: '请选择等级',
pleaseSelectErrorSource: '请选择错误来源',
pleaseSelectErrorLevel: '请选择错误等级',
pleaseInputCheckField: '请填写校验字段名',
pleaseSelectCompareRule: '请选择对比规则',
pleaseInputMinValue: '请填写最小值',
pleaseInputMaxValue: '请填写最大值',
pleaseInputCompareValue: '请填写对比值',
errorCodePlaceholder: '例如:ERR-ROBOT-001',
checkFieldPlaceholder: '如:battery、speed、temperature',
minValuePlaceholder: '最小值',
maxValuePlaceholder: '最大值',
compareValuePlaceholder: '数值/文本,多个用逗号分隔',
errorDescPlaceholder: '详细描述触发该错误的场景',
suggestionPlaceholder: '出现该错误后的处理方案',
addErrorRule: '新增错误规则',
editErrorRule: '编辑错误规则',
viewErrorRuleDetail: '查看错误规则详情',
confirmDeleteError: '确定要删除错误 "{{name}}" 吗?',
getErrorListFailed: '获取错误列表失败',
operationFailed: '操作失败',
},
profile: {
title: '个人中心',
getProfileFailed: '获取个人信息失败',
modifySuccess: '修改成功',
modifyFailed: '修改失败',
passwordChanged: '密码修改成功',
avatarUploadSuccess: '头像上传成功',
avatarUploadFailed: '上传失败',
passwordMismatch: '两次输入的密码不一致',
changeAvatar: '更换头像',
username: '用户名称',
nickname: '用户昵称',
gender: '性别',
phone: '手机号码',
email: '用户邮箱',
role: '所属角色',
remark: '备注',
oldPassword: '旧密码',
newPassword: '新密码',
confirmPassword: '确认密码',
nicknamePlaceholder: '请输入用户昵称',
phonePlaceholder: '请输入手机号码',
saveChanges: '保存修改',
confirmChange: '确认修改',
male: '男',
female: '女',
notSet: '未设置',
},
users: {
title: '用户管理',
addUser: '新增用户',
username: '用户账号',
nickname: '用户昵称',
phone: '手机号',
email: '邮箱',
role: '角色',
status: '状态',
actions: '操作',
edit: '编辑',
delete: '删除',
usernameLabel: '用户账号',
nicknameLabel: '用户昵称',
phoneLabel: '手机号码',
passwordLabel: '密码',
languageLabel: '语言',
assignRoleLabel: '分配角色',
statusLabel: '状态',
genderLabel: '性别',
passwordPlaceholder: '不填则不修改',
chinese: '中文',
english: '英语',
normal: '正常',
stopped: '停用',
male: '男',
female: '女',
unknown: '未知',
batchDelete: '批量删除(已选 {{count}} 项)',
resetPassword: '重置密码',
passwordResetTo: '密码已重置为:123456',
resetPasswordConfirm: '确定将密码重置为 123456 吗?',
},
roles: {
title: '角色管理',
addRole: '新增角色',
roleName: '角色名称',
roleKey: '权限字符',
roleSort: '排序',
status: '状态',
remark: '备注',
actions: '操作',
edit: '编辑',
delete: '删除',
assignPermissions: '分配权限',
dataScope: '数据权限',
menuPermissions: '菜单权限',
roleDescription: '角色描述',
normal: '正常',
stopped: '停用',
},
constants: {
// 工单类型
inspection: '巡检',
maintenance: '检修',
cleaning: '清洗',
repair: '维修',
operation: '运维',
// 来源
sourceAI: 'AI',
sourceUAV: '无人机',
sourceCleaning: '清洗',
sourceVideo: '视频',
sourceEquipment: '装备',
sourceManual: '人工',
sourceAlert: '告警',
// 告警类型
uavFault: '无人机故障',
mowerFault: '割草机故障',
otherDeviceFault: '其他设备故障',
serverFailure: '服务器故障',
systemError: '系统错误',
// 告警级别
critical: '严重',
important: '重要',
general: '一般',
info: '提示',
// 处理状态
pending: '待处理',
closed: '已关闭',
// 错误来源
uav: '无人机',
mower: '割草机',
others: '其他',
// 错误等级
infoLevel: '信息',
warning: '警告',
error: '错误',
// 告警处理结果
handled: '已处理',
restored: '已恢复',
ignored: '已忽略',
unableToHandle: '无法处理',
manufacturerHandling: '厂家处理中',
falseAlarm: '误报',
// 比较操作符
eq: '等于',
neq: '不等于',
gt: '大于',
lt: '小于',
gte: '大于等于',
lte: '小于等于',
between: '在之间',
notBetween: '不在之间',
contain: '包含',
notContain: '不包含',
// 定位质量
locationInvalid: '无效',
locationGPS: 'GPS 单点定位',
locationDGPS: 'DGPS 伪距差分/SBAS',
locationRTKFixed: 'RTK 固定解',
locationRTKFloat: 'RTK 浮点解',
locationUnknown: '未知',
// 导出
exportData: '导出数据',
// 无
none: '无',
},
router: {
overview: '总览首页',
devices: '设备管理',
video: '在线视频',
monitor: '实时监控',
alerts: '告警中心',
aiAnalysis: 'AI诊断分析',
clean: '清洗优化',
task: '工单任务',
table: '报表中心',
systemSetting: '系统设置',
log: '日志管理',
videoManage: '视频管理',
downloads: '视频下载',
users: '用户管理',
roles: '角色管理',
menuList: '菜单管理',
organization: '组织管理',
station: '场站管理',
realtimeMonitor: '实时监控',
profile: '个人中心',
noticePolicy: '通知策略页面',
dataSafety: '数据与安全页面',
moduleDeveloping: '模块开发中',
pleaseSelectMenu: '请选择菜单',
},
api: {
loginExpired: '登录已过期,请重新登录',
networkError: '网络异常,请稍后重试',
},
weather: {
sunny: '晴',
cloudy: '多云',
overcast: '阴',
rainy: '雨',
},
};
export default zh;

View File

@@ -4,6 +4,7 @@ import App from './App.tsx';
import { Provider } from 'react-redux';
import { store } from './store/index.ts';
import { App as Appantd } from 'antd';
import './i18n';
import './index.css';
createRoot(document.getElementById('root')!).render(
@@ -11,9 +12,7 @@ createRoot(document.getElementById('root')!).render(
<StrictMode>
<Appantd>
<App />
</Appantd>
</StrictMode>,
</Provider>
);

View File

@@ -11,18 +11,11 @@ import {
SelectOutlined, FireOutlined, WarningOutlined, ExclamationCircleOutlined
} from '@ant-design/icons';
import { Line, Area } from '@ant-design/charts';
import { useTranslation } from 'react-i18next';
import { UniversalLineChart } from "../components/recharts"
const { Title, Text } = Typography;
// 模拟数据
const aiSummaryData = [
{ key: '1', diagnosis: '热斑检测', status: '异常', confidence: '92%', impact: '高', conclusion: '检测到 36 处热斑疑似点' },
{ key: '2', diagnosis: '组串失配', status: '异常', confidence: '90%', impact: '高', conclusion: '8 串组串存在异常' },
{ key: '3', diagnosis: '遮挡风险', status: '轻微', confidence: '78%', impact: '中', conclusion: '局部遮挡影响 2% 发电量' },
{ key: '4', diagnosis: '组件衰减', status: '异常', confidence: '86%', impact: '中', conclusion: '平均衰减率 -5.32%' },
{ key: '5', diagnosis: '逆变器效率异常', status: '正常', confidence: '93%', impact: '低', conclusion: '所有逆变器运行正常' },
];
const aiRecommendations = [
{ key: '1', measure: '处理热斑组件', priority: '高', expectedGain: '+2.15%', confidence: '92%' },
@@ -137,6 +130,7 @@ const generationPerformanceData = [
export default function AIDiagnosisPage() {
const { t } = useTranslation();
const cardStyle = {
borderRadius: 12,
border: '1px solid #f0f0f0',
@@ -146,9 +140,9 @@ export default function AIDiagnosisPage() {
const renderStatusTag = (status: string) => {
let color = '';
if (status === '异常') color = '#ff4d4f';
if (status === t('home.abnormal')) color = '#ff4d4f';
else if (status === '轻微') color = '#1677ff';
else if (status === '正常') color = '#52c41a';
else if (status === t('roles.normal')) color = '#52c41a';
return <Tag color={color} style={{ margin: 0, fontWeight: 600 }}>{status}</Tag>;
};
@@ -214,7 +208,7 @@ export default function AIDiagnosisPage() {
{ title: '异常组串数量', value: '8', trend: '较昨日 +2 ↑', icon: <AppstoreOutlined />, color: '#1D2129', bg: '#F5F7FA', trendColor: '#F53F3F', direction: 'up' },
{ title: '热斑疑似数量', value: '36', trend: '较昨日 +5 ↑', icon: <BellOutlined />, color: '#F53F3F', bg: '#FFF1F0', trendColor: '#F53F3F', direction: 'up' },
{ title: '电站健康度评分', value: '86.3', unit: '分', trend: '较昨日 -1.7', icon: <CheckCircleOutlined />, color: '#00B42A', bg: '#E8FFEA', trendColor: '#F53F3F', direction: 'down' },
{ title: '预测发电量', value: '4,821', unit: 'kWh', trend: '较昨日 +2.6% ↑', icon: <ThunderboltOutlined />, color: '#165DFF', bg: '#E8F3FF', trendColor: '#00B42A', direction: 'up' },
{ title: '预测发电量', value: '4,821', unit: t('home.kWh'), trend: '较昨日 +2.6% ↑', icon: <ThunderboltOutlined />, color: '#165DFF', bg: '#E8F3FF', trendColor: '#00B42A', direction: 'up' },
].map((item, index) => (
<Col key={index} xs={24} sm={12} md={8} xl={4}>
<Card
@@ -478,8 +472,8 @@ export default function AIDiagnosisPage() {
/> </div>
<div style={{ width: 120, display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: 16, flexShrink: 0, paddingLeft: 16, borderLeft: '1px solid #f0f0f0' }}>
{[
{ label: '当日实际发电', value: '4,752', unit: 'kWh', color: '#165DFF' },
{ label: '当日预测发电', value: '4,960', unit: 'kWh', color: '#00B42A' },
{ label: '当日实际发电', value: '4,752', unit: t('home.kWh'), color: '#165DFF' },
{ label: '当日预测发电', value: '4,960', unit: t('home.kWh'), color: '#00B42A' },
{ label: '发电偏差率', value: '-4.35%', unit: '', color: '#F53F3F' },
{ label: '实时 PR 值', value: '82.6%', unit: '', color: '#1D2129' },
].map(item => (
@@ -508,17 +502,17 @@ export default function AIDiagnosisPage() {
<Table
columns={[
{ title: '诊断项', dataIndex: 'diagnosis', width: 78 },
{ title: '状态', dataIndex: 'status', render: renderStatusTag, width: 52 },
{ title: '置信度', dataIndex: 'confidence', width: 60 },
{ title: t('roles.status'), dataIndex: 'status', render: renderStatusTag, },
{ title: t('aiAnalysis.confidence'), dataIndex: 'confidence', },
{ title: '影响', dataIndex: 'impact', render: renderImpactTag, width: 52 },
{ title: '详情', dataIndex: 'detail', ellipsis: true },
]}
dataSource={[
{ key: '1', diagnosis: '热斑检测', status: '异常', confidence: '92%', impact: '高', detail: '检测到 36 处热斑疑似点' },
{ key: '2', diagnosis: '组串失配', status: '异常', confidence: '90%', impact: '高', detail: '8 串组串存在异常' },
{ key: '1', diagnosis: t('aiAnalysis.hotspotDetection'), status: t('home.abnormal'), confidence: '92%', impact: '高', detail: '检测到 36 处热斑疑似点' },
{ key: '2', diagnosis: '组串失配', status: t('home.abnormal'), confidence: '90%', impact: '高', detail: '8 串组串存在异常' },
{ key: '3', diagnosis: '遮挡风险', status: '轻微', confidence: '78%', impact: '中', detail: '局部遮挡影响 2% 发电量' },
{ key: '4', diagnosis: '组件衰减', status: '异常', confidence: '86%', impact: '中', detail: '平均衰减率 -5.32%' },
{ key: '5', diagnosis: '逆变器异常', status: '正常', confidence: '93%', impact: '低', detail: '所有逆变器运行正常' },
{ key: '4', diagnosis: '组件衰减', status: t('home.abnormal'), confidence: '86%', impact: '中', detail: '平均衰减率 -5.32%' },
{ key: '5', diagnosis: '逆变器异常', status: t('roles.normal'), confidence: '93%', impact: '低', detail: '所有逆变器运行正常' },
]}
pagination={false}
size="small"
@@ -536,11 +530,11 @@ export default function AIDiagnosisPage() {
<Table
columns={[
{ title: '建议措施', dataIndex: 'measure', ellipsis: true },
{ title: '优先级', dataIndex: 'priority', render: renderPriorityTag, width: 58 },
{ title: t('workOrder.priority'), dataIndex: 'priority', render: renderPriorityTag, width: 58 },
{ title: '收益', dataIndex: 'gain', render: g => <span style={{ color: '#00B42A', fontWeight: 600 }}>{g}</span>, width: 68 },
{ title: '置信度', dataIndex: 'confidence', width: 60 },
{ title: t('aiAnalysis.confidence'), dataIndex: 'confidence', width: 60 },
{
title: '操作',
title: t('roles.actions'),
key: 'action',
width: 76,
render: (_, record) => (
@@ -549,7 +543,7 @@ export default function AIDiagnosisPage() {
size="small"
style={{ fontSize: 11, borderRadius: 4, height: 22, padding: '0 6px' }}
>
{record.isView ? '详情' : '派单'}
{record.isView ? '详情' : t('workOrder.dispatch')}
</Button>
)
},
@@ -581,9 +575,9 @@ export default function AIDiagnosisPage() {
<Row gutter={[16, 16]}>
{[
{ title: '无人机载荷热成像', time: '2025-05-20 09:30', icon: <ScanOutlined />, status: '同步中' },
{ title: 'SCADA 实时遥测数据', time: '2025-05-20 09:45:12', icon: <DatabaseOutlined />, status: '正常' },
{ title: '云端气象分析站', time: '2025-05-20 09:45:00', icon: <CloudOutlined />, status: '正常' },
{ title: '电站运维资产库', time: '最近更新: 2025-05-18', icon: <RestOutlined />, status: '正常' },
{ title: 'SCADA 实时遥测数据', time: '2025-05-20 09:45:12', icon: <DatabaseOutlined />, status: t('roles.normal') },
{ title: '云端气象分析站', time: '2025-05-20 09:45:00', icon: <CloudOutlined />, status: t('roles.normal') },
{ title: '电站运维资产库', time: '最近更新: 2025-05-18', icon: <RestOutlined />, status: t('roles.normal') },
].map((source, idx) => (
<Col xs={24} sm={12} md={6} key={idx}>
<div style={{ background: '#f8fafc', padding: '16px', borderRadius: 12, display: 'flex', alignItems: 'center', gap: 16, border: '1px solid #f0f0f0', transition: 'all 0.3s' }} className="thermal-card">
@@ -593,8 +587,8 @@ export default function AIDiagnosisPage() {
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 14, fontWeight: 700, color: '#1D2129' }}>{source.title}</div>
<div style={{ fontSize: 12, color: '#86909C', marginTop: 4, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>数据点: {source.time}</div>
<div style={{ fontSize: 11, color: source.status === '正常' ? '#00B42A' : '#165DFF', marginTop: 6, display: 'flex', alignItems: 'center', gap: 6 }}>
<div style={{ width: 6, height: 6, background: source.status === '正常' ? '#00B42A' : '#165DFF', borderRadius: '50%' }}></div>
<div style={{ fontSize: 11, color: source.status === t('roles.normal') ? '#00B42A' : '#165DFF', marginTop: 6, display: 'flex', alignItems: 'center', gap: 6 }}>
<div style={{ width: 6, height: 6, background: source.status === t('roles.normal') ? '#00B42A' : '#165DFF', borderRadius: '50%' }}></div>
状态: {source.status}
</div>
</div>

View File

@@ -23,36 +23,39 @@ import {
getTargetDemo
} from '../api/aiAnalysis';
import utils from '../lib/utils';
import { useTranslation } from 'react-i18next';
const { Title, Text } = Typography;
const { Dragger } = Upload;
const { Option } = Select;
// 算法列表定义
const algorithmList = [
{
id: 'detect',
name: '目标检测',
alias: 'waterway-trt',
desc: '图片/视频检测,创建任务后轮询结果。',
icon: <AimOutlined />,
status: '运行中'
},
{
id: 'segmentation',
name: '热成像分割',
alias: 'v1.0.0',
desc: '热成像热点、面板分区和温度统计。',
icon: <FireOutlined />,
status: '运行中'
}
];
export default function Agri() {
const { t } = useTranslation();
const [activeAlgo, setActiveAlgo] = useState('detect');
const [models, setModels] = useState([]);
const [loadingModels, setLoadingModels] = useState(false);
const [history, setHistory] = useState([]);
const algorithmList = [
{
id: 'detect',
name: '目标检测',
alias: 'waterway-trt',
desc: '图片/视频检测,创建任务后轮询结果。',
icon: <AimOutlined />,
status: t('devices.running')
},
{
id: 'segmentation',
name: '热成像分割',
alias: 'v1.0.0',
desc: '热成像热点、面板分区和温度统计。',
icon: <FireOutlined />,
status: t('devices.running')
}
];
// 检测任务状态
const [detectForm, setDetectForm] = useState({
@@ -4390,7 +4393,7 @@ export default function Agri() {
} else if (['failed', 'cancelled'].includes(statusRes.status)) {
clearInterval(pollTimer.current);
setPolling(false);
utils.message.error(`任务执行${statusRes.status === 'failed' ? '失败' : '被取消'}`);
utils.message.error(`任务执行${statusRes.status === 'failed' ? t('common.failure') : '被取消'}`);
}
} catch (err) {
console.error('轮询失败', err);
@@ -4575,7 +4578,7 @@ export default function Agri() {
size="small"
>
<Radio.Button value="image">图片</Radio.Button>
<Radio.Button value="video">视频</Radio.Button>
<Radio.Button value="video">{t('constants.sourceVideo')}</Radio.Button>
</Radio.Group>
</Col>
<Col span={12}>
@@ -4586,7 +4589,7 @@ export default function Agri() {
size="small"
>
<Radio.Button value="quality">质量</Radio.Button>
<Radio.Button value="speed">速度</Radio.Button>
<Radio.Button value="speed">{t('devices.speed')}</Radio.Button>
</Radio.Group>
</Col>
</Row>
@@ -4962,7 +4965,7 @@ export default function Agri() {
bodyStyle={{ padding: '12px 8px' }}
>
<Statistic
title={<span style={{ fontSize: 15, color: '#86909C' }}>最高温度</span>}
title={<span style={{ fontSize: 15, color: '#86909C' }}>{t('aiAnalysis.maxTemperature')}</span>}
value={hasResult ? `${segResult.summary?.max_temp_c || 0}℃` : '-'}
titleStyle={{ fontSize: 12, color: '#333', marginBottom: 4 }}
valueStyle={{ fontSize: 18, fontWeight: 600, color: '#FF7D00' }}
@@ -5101,7 +5104,7 @@ export default function Agri() {
</div>
{[
{ label: '温度', value: `${selectedPoint.peak_temp_c}℃` },
{ label: t('home.temperature'), value: `${selectedPoint.peak_temp_c}℃` },
{ label: '块号', value: selectedPoint.panel_id || '-' },
{ label: 'GPS', value: selectedPoint.gps?.text || '-' },
{ label: '原图像素', value: `${selectedPoint.peak_x}, ${selectedPoint.peak_y}` },

View File

@@ -11,6 +11,7 @@ import AlarmHistory from '../components/alerts/AlarmHistory';
import AlarmStatistics from '../components/alerts/AlarmStatistics';
import AlarmRules from '../components/alerts/AlarmRules';
import AlarmSubscription from '../components/alerts/AlarmSubscription';
import { useTranslation } from 'react-i18next';
@@ -18,6 +19,7 @@ const { Header, Content } = Layout;
const { Text } = Typography;
export default function AlarmCenterPage() {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState('overview');
@@ -36,7 +38,7 @@ export default function AlarmCenterPage() {
<Layout style={{ minHeight: '100vh', overflow: 'hidden' }}>
<div style={{ display: 'flex', paddingBottom: "4px" }}>
<div style={{ padding: '8px 12px 0' }}>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>告警中心</Typography.Title>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>{t('router.alerts')}</Typography.Title>
<Text type="secondary" style={{ fontSize: '12px' }}>告警处置闭环、规则管理与事件中心</Text>
</div>

View File

@@ -30,6 +30,7 @@ import {
Leaf
} from 'lucide-react';
import { UniversalBarChart, UniversalLineChart, UniversalPieChart } from '../components/recharts';
import { useTranslation } from 'react-i18next';
const { Title, Text } = Typography;
const { RangePicker } = DatePicker;
@@ -62,12 +63,7 @@ const pollutionLineData = [
{ date: '05-24', pollution: 68.5, threshold: 75 },
];
const pieChartData = [
{ name: '灰尘积累', value: 6120, color: '#165DFF' },
{ name: '鸟粪遮挡', value: 1480, color: '#F53F3F' },
{ name: '沙尘沉积', value: 1120, color: '#F7BA1E' },
{ name: '其他', value: 600, color: '#86909C' },
];
const dualAxesData = [
[
@@ -109,6 +105,7 @@ const compareChartData = [
export default function CleanOptimizationPage() {
const { t } = useTranslation();
const cardStyle = {
borderRadius: 12,
border: '1px solid #f0f0f0',
@@ -116,6 +113,13 @@ export default function CleanOptimizationPage() {
overflow: 'hidden'
};
const pieChartData = [
{ name: '灰尘积累', value: 6120, color: '#165DFF' },
{ name: '鸟粪遮挡', value: 1480, color: '#F53F3F' },
{ name: '沙尘沉积', value: 1120, color: '#F7BA1E' },
{ name: t('constants.others'), value: 600, color: '#86909C' },
];
const renderPriorityTag = (priority: string) => {
const config: Record<string, any> = {
'高': { color: '#ff4d4f', bg: '#fff1f0' },
@@ -185,7 +189,7 @@ export default function CleanOptimizationPage() {
{
title: '预计发电提升',
value: '12,850',
unit: 'kWh',
unit: t('home.kWh'),
trend: '占当前日发电量 14.2%',
icon: <TrendingUp />,
color: '#1890ff',
@@ -336,7 +340,7 @@ export default function CleanOptimizationPage() {
<div style={{ position: 'absolute', left: '30%', top: '50%', transform: 'translate(-50%, -50%)', textAlign: 'center' }}>
<div style={{ fontSize: 10, color: '#86909C' }}>总损失电量</div>
<div style={{ fontSize: 16, fontWeight: 800, color: '#1D2129' }}>9,320</div>
<div style={{ fontSize: 10, color: '#86909C' }}>kWh</div>
<div style={{ fontSize: 10, color: '#86909C' }}>{t('home.kWh')}</div>
</div>
</div>
</Card>
@@ -366,7 +370,7 @@ export default function CleanOptimizationPage() {
<Row gutter={[16, 16]}>
<Col span={12}>
<div style={{ fontSize: 11, color: '#86909C', marginBottom: 4 }}>预计发电提升</div>
<div style={{ fontSize: 15, fontWeight: 800 }}>12,850 <span style={{ fontSize: 11, fontWeight: 400 }}>kWh</span></div>
<div style={{ fontSize: 15, fontWeight: 800 }}>12,850 <span style={{ fontSize: 11, fontWeight: 400 }}>{t('home.kWh')}</span></div>
</Col>
<Col span={12}>
<div style={{ fontSize: 11, color: '#86909C', marginBottom: 4 }}>电价 (含税)</div>
@@ -457,12 +461,12 @@ export default function CleanOptimizationPage() {
<Table
columns={[
{ title: '区块', dataIndex: 'area', key: 'area' },
{ title: '优先级', dataIndex: 'priority', key: 'priority', render: renderPriorityTag },
{ title: t('workOrder.priority'), dataIndex: 'priority', key: 'priority', render: renderPriorityTag },
{ title: '预计工时', dataIndex: 'duration', key: 'duration' },
{ title: '负责人', dataIndex: 'handler', key: 'handler' },
{ title: t('systemSetting.leader'), dataIndex: 'handler', key: 'handler' },
{ title: '计划时间', dataIndex: 'planTime', key: 'planTime' },
{ title: '状态', dataIndex: 'status', key: 'status', render: (s) => <Tag color="blue" variant="outline" style={{ borderRadius: 4 }}>{s}</Tag> },
{ title: '操作', key: 'action', render: () => <Button type="primary" size="small" style={{ borderRadius: 4 }}>创建工单</Button> },
{ title: t('roles.status'), dataIndex: 'status', key: 'status', render: (s) => <Tag color="blue" variant="outline" style={{ borderRadius: 4 }}>{s}</Tag> },
{ title: t('roles.actions'), key: 'action', render: () => <Button type="primary" size="small" style={{ borderRadius: 4 }}>创建工单</Button> },
]}
dataSource={workOrderPreviewData}
pagination={false}
@@ -481,7 +485,7 @@ export default function CleanOptimizationPage() {
<Table
columns={[
{ title: '推荐计划', dataIndex: 'id', key: 'id', width: 85 },
{ title: '优先级', dataIndex: 'priority', key: 'priority', render: (p) => <span style={{ color: p === '高' ? '#F53F3F' : '#F7BA1E', fontWeight: 600 }}>{p}</span>, width: 65 },
{ title: t('workOrder.priority'), dataIndex: 'priority', key: 'priority', render: (p) => <span style={{ color: p === '高' ? '#F53F3F' : '#F7BA1E', fontWeight: 600 }}>{p}</span>, width: 65 },
{ title: '建议区块', dataIndex: 'area', key: 'area' },
{ title: '预计提升', dataIndex: 'boost', key: 'boost', render: (b) => <span style={{ color: '#00B42A', fontWeight: 600 }}>{b}</span> },
{ title: '天气适宜度', dataIndex: 'weather', key: 'weather', render: (w) => <Space size={4}>{w} <SunOutlined style={{ color: '#F7BA1E' }} /></Space> },
@@ -562,7 +566,7 @@ export default function CleanOptimizationPage() {
{ icon: <Droplets size={18} color="#165DFF" />, bg: '#E8F3FF', label: '预计用水量', value: '18 吨', sub: '节水 30% (机器人)', subColor: '#00B42A' },
{ icon: <Calendar size={18} color="#165DFF" />, bg: '#E8F3FF', label: '预计总耗时', value: '12 小时', sub: '并行作业 2 组', subColor: '#86909C' },
{ icon: <Leaf size={18} color="#00B42A" />, bg: '#E8FFEA', label: '碳减排贡献', value: '6.85 吨', sub: 'CO₂ 减排量', subColor: '#86909C' },
{ icon: <ShieldCheck size={18} color="#F7BA1E" />, bg: '#FFF7E8', label: '作业风险评估', value: '低风险', valueColor: '#00B42A', sub: '安全可控', subColor: '#86909C' },
{ icon: <ShieldCheck size={18} color="#F7BA1E" />, bg: '#FFF7E8', label: '作业风险评估', value: t('aiAnalysis.lowRisk'), valueColor: '#00B42A', sub: '安全可控', subColor: '#86909C' },
].map((item, idx) => (
<Col span={12} key={idx}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>

View File

@@ -8,12 +8,14 @@ import { useSelector } from 'react-redux';
import { RootState } from '../store';
import WayLinePage from '../components/devices/WayLinePage';
import RobotTaskPage from '../components/devices/RobotTaskPage';
import { useTranslation } from 'react-i18next';
const { Content } = Layout;
const { Text } = Typography;
export default function DeviceIndexPage() {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState('overview');
const [selectedSn, setSelectedSn] = useState(null);
const { stationId } = useSelector((state: RootState) => state.station);
@@ -24,9 +26,9 @@ export default function DeviceIndexPage() {
const menuItems = [
{ key: 'overview', label: '装备总览', icon: <AppstoreOutlined /> },
{ key: 'status', label: '设备状态', icon: <SettingOutlined /> },
{ key: 'wayLineTask', label: '航线任务', icon: <AimOutlined /> },
{ key: 'robotTask', label: '机器人任务', icon: <BranchesOutlined /> },
{ key: 'status', label: t('devices.deviceStatus'), icon: <SettingOutlined /> },
{ key: 'wayLineTask', label: t('devices.waylineTask'), icon: <AimOutlined /> },
{ key: 'robotTask', label: t('devices.robotTask'), icon: <BranchesOutlined /> },
];

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
Table,
Button,
@@ -21,6 +22,7 @@ const { Text, Title } = Typography;
const { RangePicker } = DatePicker;
export default function Downloads() {
const { t } = useTranslation();
const columns = [
{
title: '文件详情',
@@ -61,7 +63,7 @@ export default function Downloads() {
render: (text: string) => <Text type="secondary" style={{ fontSize: 12 }}>{text}</Text>,
},
{
title: '操作',
title: t('roles.actions'),
key: 'action',
align: 'right' as const,
render: () => (

View File

@@ -27,10 +27,12 @@ import { addWorkOrder } from '../api/workOrder';
import { useSelector } from 'react-redux';
import { getAlarmList } from '../api/alarmApi';
import { RootState } from '../store';
import { useTranslation } from 'react-i18next';
const { Text } = Typography;
export default function StationDashboard() {
const { t } = useTranslation();
// 👇 消息提示(修复缺失)
const [messageApi, contextHolder] = message.useMessage();
@@ -86,10 +88,10 @@ export default function StationDashboard() {
const deviceStatus = [
{ name: "汇流箱", count: "2/2", status: "异常 1" },
{ name: "逆变器", count: "3/3", status: "异常 1" },
{ name: "电表", count: "1/1", status: "正常" },
{ name: "巡检机器人", count: "1/1", status: "正常" }
{ name: t('home.combinerBox'), count: "2/2", status: "异常 1" },
{ name: t('home.inverter'), count: "3/3", status: "异常 1" },
{ name: "电表", count: "1/1", status: t('roles.normal') },
{ name: t('home.inspectionRobot'), count: "1/1", status: t('roles.normal') }
];
const patrolRecords = [
@@ -100,8 +102,8 @@ export default function StationDashboard() {
const aiAnalysis = [
{ type: "AI分析", content: "AI发现逆变器温度与组串电流存在轻微异常,建议优先复核A区设备。", desc: "发现2处组件存在异常热斑" },
{ type: "严重", content: "INV-A02散热检修建议:建议检查INV-A02散热风扇、风道和运行负载,持续高温时转维修工单。", desc: "3个组串存在发电异常" },
{ type: "严重", content: "A05阵列热斑处置建议:建议现场复核PV-A05-018至PV-A05-020组件接线和旁路二极管状态,必要时更换异常组件。", desc: "整体发电偏差-2.8%" }
{ type: t('constants.critical'), content: "INV-A02散热检修建议:建议检查INV-A02散热风扇、风道和运行负载,持续高温时转维修工单。", desc: "3个组串存在发电异常" },
{ type: t('constants.critical'), content: "A05阵列热斑处置建议:建议现场复核PV-A05-018至PV-A05-020组件接线和旁路二极管状态,必要时更换异常组件。", desc: "整体发电偏差-2.8%" }
];
// 图表数据
@@ -141,10 +143,10 @@ export default function StationDashboard() {
];
const taskProgressData = [
{ name: '已完成', value: 40, color: '#1890ff' },
{ name: t('workOrder.completed'), value: 40, color: '#1890ff' },
{ name: '进行中', value: 35, color: '#52c41a' },
{ name: '待开始', value: 20, color: '#faad14' },
{ name: '异常', value: 5, color: '#ff4d4f' },
{ name: t('home.abnormal'), value: 5, color: '#ff4d4f' },
];
const cardStyle = { borderRadius: 12, border: '1px solid #f0f0f0', boxShadow: '0 2px 8px rgba(0,0,0,0.04)' };
@@ -157,7 +159,7 @@ export default function StationDashboard() {
// ==============================================
const handleaddOrder = async () => {
if (!currentStation) {
messageApi.warning('请先选择电站');
messageApi.warning(t('home.pleaseSelectStation'));
return;
}
try {
@@ -239,12 +241,12 @@ export default function StationDashboard() {
{/* 顶部统计卡片 */}
<Row gutter={[8, 8]} style={{ margin: '0 4px 0px 4px' }}>
{[
{ title: '今日发电量', value: '4,752', unit: 'kWh', sub: '日目标 5,000 kWh', color: '#00B42A', bg: '#E8FFEA', icon: <ThunderboltOutlined /> },
{ title: '当前功率', value: '812.6', unit: 'kW', sub: '装机容量 1,000 kWp', color: '#165DFF', bg: '#E8F3FF', icon: <DashboardOutlined /> },
{ title: t('home.todayGeneration'), value: '4,752', unit: t('home.kWh'), sub: '日目标 5,000 kWh', color: '#00B42A', bg: '#E8FFEA', icon: <ThunderboltOutlined /> },
{ title: t('home.currentPower'), value: '812.6', unit: 'kW', sub: '装机容量 1,000 kWp', color: '#165DFF', bg: '#E8F3FF', icon: <DashboardOutlined /> },
{ title: 'Performance Ratio', value: '82.6%', unit: '', sub: '昨日 81.3% ↑1.3%', color: '#00B42A', bg: '#E8FFEA', icon: <BarChartOutlined /> },
{ title: '系统可用率', value: '99.35%', unit: '', sub: '昨日 99.12% ↑0.23%', color: '#165DFF', bg: '#E8F3FF', icon: <RiseOutlined /> },
{ title: '告警数量', value: alarmList.length, unit: '', sub: `未处理 ${alarmList.filter(item => item.handleStatus == 1).length} / 已关闭 ${alarmList.filter(item => item.handleStatus == 2).length}`, color: '#F53F3F', bg: '#FFF1F0', icon: <BellOutlined /> },
{ title: '节省运维成本', value: '¥ 36,540', unit: '', sub: '本月累计节省', color: '#FF7D00', bg: '#FFF7E8', icon: <MoneyCollectOutlined /> },
{ title: t('home.systemAvailability'), value: '99.35%', unit: '', sub: '昨日 99.12% ↑0.23%', color: '#165DFF', bg: '#E8F3FF', icon: <RiseOutlined /> },
{ title: t('home.alertCount'), value: alarmList.length, unit: '', sub: `未处理 ${alarmList.filter(item => item.handleStatus == 1).length} / 已关闭 ${alarmList.filter(item => item.handleStatus == 2).length}`, color: '#F53F3F', bg: '#FFF1F0', icon: <BellOutlined /> },
{ title: t('home.costSaving'), value: '¥ 36,540', unit: '', sub: t('home.monthlyAccumulated'), color: '#FF7D00', bg: '#FFF7E8', icon: <MoneyCollectOutlined /> },
].map((item, idx) => (
<Col key={idx} style={{
flex: '1 1 auto',
@@ -280,7 +282,7 @@ export default function StationDashboard() {
{/* 左侧图表 */}
<Col xs={24} xl={6}>
<Space direction="vertical" size={8} style={{ width: '100%' }}>
<Card title={<span style={{ fontWeight: 800 }}>发电趋势</span>} extra={<Space><SyncOutlined /><FullscreenOutlined /></Space>} bordered={false} style={cardStyle} bodyStyle={{ padding: '8px' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('home.generationTrend')}</span>} extra={<Space><SyncOutlined /><FullscreenOutlined /></Space>} bordered={false} style={cardStyle} bodyStyle={{ padding: '8px' }}>
<div style={{ fontSize: 11, color: '#666', marginBottom: 8 }}>今日发电功率趋势 (kW)</div>
<UniversalLineChart
data={powerTrendData}
@@ -290,12 +292,12 @@ export default function StationDashboard() {
dot
backgroundColor="transparent"
series={[
{ key: 'actual', name: '实际', color: '#165DFF', strokeWidth: 3 },
{ key: 'predict', name: '预测', color: '#00B42A', strokeWidth: 2 },
{ key: 'actual', name: t('home.actual'), color: '#165DFF', strokeWidth: 3 },
{ key: 'predict', name: t('home.predict'), color: '#00B42A', strokeWidth: 2 },
]}
/>
</Card>
<Card title={<span style={{ fontWeight: 800 }}>辐照度与温度趋势</span>} bordered={false} style={cardStyle} bodyStyle={{ padding: '8px' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('home.irradianceTempTrend')}</span>} bordered={false} style={cardStyle} bodyStyle={{ padding: '8px' }}>
<UniversalLineChart
data={irradianceData}
xKey="time"
@@ -304,12 +306,12 @@ export default function StationDashboard() {
dot
backgroundColor="transparent"
series={[
{ key: 'irradiance', name: '辐照度', color: '#165DFF', strokeWidth: 3 },
{ key: 'temperature', name: '温度', color: '#00B42A', strokeWidth: 2 },
{ key: 'irradiance', name: t('home.irradiance'), color: '#165DFF', strokeWidth: 3 },
{ key: 'temperature', name: t('home.temperature'), color: '#00B42A', strokeWidth: 2 },
]}
/>
</Card>
<Card title={<span style={{ fontWeight: 800 }}>逆变器效率对比</span>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('home.inverterEfficiency')}</span>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
<UniversalBarChart
data={invEfficiencyData}
xKey="device"
@@ -320,7 +322,7 @@ export default function StationDashboard() {
series={[{ key: 'efficiency', name: '效率', color: '#165DFF' }]}
/>
</Card>
<Card title={<span style={{ fontWeight: 800 }}>运维任务进度</span>} extra={<Button type="link" size="small">更多 &gt;</Button>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('home.maintenanceProgress')}</span>} extra={<Button type="link" size="small">更多 &gt;</Button>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<UniversalPieChart
data={taskProgressData}
@@ -347,11 +349,11 @@ export default function StationDashboard() {
<Card
title={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontWeight: 800 }}>电站概览</span>
<span style={{ fontWeight: 800 }}>{t('home.stationOverview')}</span>
<div style={{ display: 'flex', gap: 16, fontSize: 12 }}>
<span style={{ display: 'flex', alignItems: 'center' }}><span style={{ width: 8, height: 8, borderRadius: '50%', background: '#52c41a', marginRight: 4 }}></span>正常</span>
<span style={{ display: 'flex', alignItems: 'center' }}><span style={{ width: 8, height: 8, borderRadius: '50%', background: '#ff4d4f', marginRight: 4 }}></span>告警</span>
<span style={{ display: 'flex', alignItems: 'center' }}><span style={{ width: 8, height: 8, borderRadius: '50%', background: '#888', marginRight: 4 }}></span>离线</span>
<span style={{ display: 'flex', alignItems: 'center' }}><span style={{ width: 8, height: 8, borderRadius: '50%', background: '#52c41a', marginRight: 4 }}></span>roles.normal</span>
<span style={{ display: 'flex', alignItems: 'center' }}><span style={{ width: 8, height: 8, borderRadius: '50%', background: '#ff4d4f', marginRight: 4 }}></span>constants.sourceAlert</span>
<span style={{ display: 'flex', alignItems: 'center' }}><span style={{ width: 8, height: 8, borderRadius: '50%', background: '#888', marginRight: 4 }}></span>devices.offline</span>
<span style={{ display: 'flex', alignItems: 'center' }}><span style={{ width: 8, height: 8, borderRadius: '50%', background: '#faad14', marginRight: 4 }}></span>维护中</span>
</div>
</div>
@@ -376,7 +378,7 @@ export default function StationDashboard() {
<Row gutter={16}>
<Col span={12}>
<Card title={<span style={{ fontWeight: 800 }}>近期巡检记录</span>} extra={<Button type="link" size="small">更多 &gt;</Button>} bordered={false} style={{ ...cardStyle, height: "100%" }} bodyStyle={{ padding: '0 12px' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('home.recentInspection')}</span>} extra={<Button type="link" size="small">更多 &gt;</Button>} bordered={false} style={{ ...cardStyle, height: "100%" }} bodyStyle={{ padding: '0 12px' }}>
<Table
dataSource={patrolRecords}
pagination={false}
@@ -385,13 +387,13 @@ export default function StationDashboard() {
{ title: '巡检类型', dataIndex: 'name', key: 'name', ellipsis: true },
{ title: '巡检时间', dataIndex: 'time', key: 'time' },
{ title: '巡检人员', dataIndex: 'person', key: 'person', render: (t) => t.substring(0, 10) },
{ title: '状态', dataIndex: 'status', key: 'status', render: () => <span style={{ color: '#52c41a', fontSize: 12 }}>● 已完成</span> },
{ title: t('roles.status'), dataIndex: 'status', key: 'status', render: () => <span style={{ color: '#52c41a', fontSize: 12 }}>● 已完成</span> },
]}
/>
</Card>
</Col>
<Col span={12}>
<Card title={<span style={{ fontWeight: 800 }}>AI 分析摘要</span>} bordered={false} style={{ ...cardStyle, height: "100%" }} bodyStyle={{ padding: '12px' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('home.aiAnalysisSummary')}</span>} bordered={false} style={{ ...cardStyle, height: "100%" }} bodyStyle={{ padding: '12px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
@@ -403,7 +405,7 @@ export default function StationDashboard() {
<div style={{ fontSize: 14, color: '#4E5969', marginTop: 4 }}>发现 <span style={{ color: '#165DFF', fontWeight: 600 }}>2</span> 处组件存在异常热斑</div>
</div>
</div>
<Button type="link" size="small" style={{ color: '#165DFF' }}>查看详情</Button>
<Button type="link" size="small" style={{ color: '#165DFF' }}>{t('common.viewDetail')}</Button>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
@@ -415,7 +417,7 @@ export default function StationDashboard() {
<div style={{ fontSize: 14, color: '#4E5969', marginTop: 4 }}><span style={{ color: '#165DFF', fontWeight: 600 }}>3</span> 个组串存在发电异常</div>
</div>
</div>
<Button type="link" size="small" style={{ color: '#165DFF' }}>查看详情</Button>
<Button type="link" size="small" style={{ color: '#165DFF' }}>{t('common.viewDetail')}</Button>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
@@ -427,7 +429,7 @@ export default function StationDashboard() {
<div style={{ fontSize: 14, color: '#4E5969', marginTop: 4 }}>整体发电偏差 <span style={{ color: '#165DFF', fontWeight: 600 }}>-2.8%</span></div>
</div>
</div>
<Button type="link" size="small" style={{ color: '#165DFF' }}>查看详情</Button>
<Button type="link" size="small" style={{ color: '#165DFF' }}>{t('common.viewDetail')}</Button>
</div>
</div>
</Card>
@@ -439,7 +441,7 @@ export default function StationDashboard() {
{/* 右侧 */}
<Col xs={24} xl={6}>
<Space direction="vertical" size={8} style={{ width: '100%' }}>
<Card title={<span style={{ fontWeight: 800 }}>实时告警</span>} extra={<Button type="link" size="small" onClick={() => navigate('/alerts')}>更多 &gt;</Button>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('alerts.realtimeAlert')}</span>} extra={<Button type="link" size="small" onClick={() => navigate('/alerts/all')}>更多 &gt;</Button>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 12, padding: '0 4px 8px', borderBottom: '1px solid #f0f0f0', fontSize: 13, color: '#666' }}>
<span style={{ flex: 3 }}>告警内容</span>
<span style={{ flex: 1, textAlign: 'center' }}>级别</span>
@@ -463,17 +465,17 @@ export default function StationDashboard() {
})}
</Card>
<Card title={<span style={{ fontWeight: 800 }}>设备在线状态</span>} extra={<Button type="link" size="small">更多 &gt;</Button>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('home.deviceOnlineStatus')}</span>} extra={<Button type="link" size="small">更多 &gt;</Button>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
<Row gutter={[16, 16]}>
{[
{ name: '组件阵列', count: '320/320', icon: <AppstoreOutlined /> },
{ name: '逆变器', count: '5/5', icon: <ThunderboltOutlined /> },
{ name: '汇流箱', count: '20/20', icon: <BranchesOutlined /> },
{ name: t('home.inverter'), count: '5/5', icon: <ThunderboltOutlined /> },
{ name: t('home.combinerBox'), count: '20/20', icon: <BranchesOutlined /> },
{ name: '气象站', count: '1/1', icon: <CloudOutlined /> },
{ name: '摄像头', count: '12/12', icon: <VideoCameraOutlined /> },
{ name: t('devices.camera'), count: '12/12', icon: <VideoCameraOutlined /> },
{ name: '环境传感器', count: '8/8', icon: <ExperimentOutlined /> },
{ name: '无人机', count: '1/1', icon: <RocketOutlined /> },
{ name: '巡检机器人', count: '1/1', icon: <RobotOutlined /> },
{ name: t('constants.uav'), count: '1/1', icon: <RocketOutlined /> },
{ name: t('home.inspectionRobot'), count: '1/1', icon: <RobotOutlined /> },
].map((item, idx) => (
<Col span={6} key={idx} style={{ display: 'flex', alignItems: 'center' }}>
<div style={{ width: 20, height: 20, borderRadius: '50%', border: '2px solid #52c41a', display: 'flex', alignItems: 'center', justifyContent: 'center', marginRight: 12, flexShrink: 0 }}>
@@ -488,7 +490,7 @@ export default function StationDashboard() {
</Row>
</Card>
<Card title={<span style={{ fontWeight: 800 }}>天气信息</span>} extra={<Button type="link" size="small">详细 &gt;</Button>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('home.weatherInfo')}</span>} extra={<Button type="link" size="small">详细 &gt;</Button>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'start', gap: 20 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<WeatherDisplay type={weatherType} />
@@ -508,13 +510,13 @@ export default function StationDashboard() {
{/* ====================================== */}
{/* 👇 清洗建议 + 生成工单按钮(已完善) */}
{/* ====================================== */}
<Card title={<span style={{ fontWeight: 800 }}>清洗建议</span>} extra={<Button type="link" size="small">更多 &gt;</Button>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
<Card title={<span style={{ fontWeight: 800 }}>{t('clean.cleaningSuggestion')}</span>} extra={<Button type="link" size="small">更多 &gt;</Button>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 16 }}>
<div style={{ width: 80, height: 80, position: 'relative' }}>
<UniversalPieChart
data={[
{ name: '清洁度', value: 85, color: '#00B42A' },
{ name: '待清洗', value: 15, color: '#E8FFEA' },
{ name: t('clean.pending'), value: 15, color: '#E8FFEA' },
]}
width="100%"
height="100%"

View File

@@ -19,11 +19,13 @@ import logincard from '../assets/logincard.png';
import { userLogin } from '../store/userSlice';
import { useAppDispatch } from '../store';
import Cookies from 'js-cookie';
import { useTranslation } from 'react-i18next';
const { Text, Title } = Typography;
const AntdCard = Card as any;
export default function Login() {
const { t } = useTranslation();
const navigate = useNavigate();
const dispatch = useAppDispatch();
const { message } = App.useApp();
@@ -42,7 +44,7 @@ export default function Login() {
// loginResult 就是你 userLogin 里 return 的 res
if (loginResult.code == 200) {
message.success('登录成功');
message.success(t('login.loginSuccess'));
const searchParams = new URLSearchParams(window.location.search);
const goRemote = searchParams.get('goRemote');
if (Number(goRemote) == 1) {
@@ -91,18 +93,18 @@ export default function Login() {
</Title>
<Form name="login" layout="vertical" onFinish={onFinish} autoComplete="off" size="large">
<Form.Item name="username" rules={[{ required: true, message: '请输入账号' }]}>
<Form.Item name="username" rules={[{ required: true, message: t('login.accountRequired') }]}>
<Input
prefix={<UserOutlined style={{ color: '#999' }} />}
placeholder="请输入账号"
placeholder={t('login.accountRequired')}
style={{ borderRadius: 8, height: 52, fontSize: 15, borderColor: '#e5e7eb' }}
/>
</Form.Item>
<Form.Item name="password" rules={[{ required: true, message: '请输入密码' }]}>
<Form.Item name="password" rules={[{ required: true, message: t('login.passwordRequired') }]}>
<Input.Password
prefix={<LockOutlined style={{ color: '#999' }} />}
placeholder="请输入密码"
placeholder={t('login.passwordRequired')}
style={{ borderRadius: 8, height: 52, fontSize: 15, borderColor: '#e5e7eb' }}
/>
</Form.Item>
@@ -111,11 +113,11 @@ export default function Login() {
<Form.Item
name="agreement"
valuePropName="checked"
rules={[{ validator: (_, value) => value ? Promise.resolve() : Promise.reject(new Error('请阅读并同意用户协议')) }]}
rules={[{ validator: (_, value) => value ? Promise.resolve() : Promise.reject(new Error(t('login.agreeRequired'))) }]}
>
<Checkbox>
<p style={{ fontSize: 14, color: '#666' }}>
我已阅读并同意 <span type="link" style={{ padding: 0, color: '#1677ff' }}>用户协议</span> 和 <span type="link" style={{ padding: 0, color: '#1677ff' }}>隐私政策</span>
我已阅读并同意 <span type="link" style={{ padding: 0, color: '#1677ff' }}>{t('login.userAgreement')}</span> 和 <span type="link" style={{ padding: 0, color: '#1677ff' }}>{t('login.privacyPolicy')}</span>
</p>
</Checkbox>
</Form.Item>

View File

@@ -32,6 +32,7 @@ import { setUserInfo } from '../store/userSlice';
import { getUserProfile, updateUserProfile, updateUserPwd, uploadAvatar } from '../api/user';
import envConfig from '../../env';
import utils from '../lib/utils';
import { useTranslation } from 'react-i18next';
const { TabPane } = Tabs;
const { Title } = Typography;
@@ -53,7 +54,7 @@ const Profile: React.FC = () => {
form.setFieldsValue(res.data);
}
} catch (error) {
message.error('获取个人信息失败');
message.error(t('profile.getProfileFailed'));
}
};
@@ -71,12 +72,12 @@ const Profile: React.FC = () => {
};
const res: any = await updateUserProfile(updateData);
if (res.code === 200) {
message.success('修改成功');
message.success(t('profile.modifySuccess'));
const newUserInfo = { ...userInfo, ...values };
dispatch(setUserInfo(newUserInfo));
fetchProfile();
} else {
message.error(res.msg || '修改失败');
message.error(res.msg || t('profile.modifyFailed'));
}
} catch (error) {
message.error('请求失败');
@@ -87,17 +88,17 @@ const Profile: React.FC = () => {
const handleUpdatePwd = async (values: any) => {
if (values.newPassword !== values.confirmPassword) {
message.error('两次输入的密码不一致');
message.error(t('profile.passwordMismatch'));
return;
}
setLoading(true);
try {
const res: any = await updateUserPwd(values.oldPassword, values.newPassword);
if (res.code === 200) {
message.success('密码修改成功');
message.success(t('profile.passwordChanged'));
pwdForm.resetFields();
} else {
message.error(res.msg || '修改失败');
message.error(res.msg || t('profile.modifyFailed'));
}
} catch (error) {
message.error('请求失败');
@@ -114,15 +115,15 @@ const Profile: React.FC = () => {
try {
const res: any = await uploadAvatar(formData);
if (res.code === 200) {
message.success('头像上传成功');
message.success(t('profile.avatarUploadSuccess'));
const newUserInfo = { ...userInfo, avatar: res.imgUrl };
dispatch(setUserInfo(newUserInfo));
fetchProfile();
} else {
message.error(res.msg || '上传失败');
message.error(res.msg || t('profile.avatarUploadFailed'));
}
} catch (error) {
message.error('上传失败');
message.error(t('profile.avatarUploadFailed'));
}
};
@@ -193,20 +194,20 @@ const Profile: React.FC = () => {
<div style={{ display: 'flex', alignItems: 'center', fontSize: 14 }}>
<PhoneOutlined style={{ marginRight: 12, color: '#165DFF', fontSize: 16 }} />
<span style={{ color: '#666' }}>手机号码:</span>
<strong style={{ marginLeft: 4, color: '#333' }}>{profileData?.phonenumber || '未设置'}</strong>
<strong style={{ marginLeft: 4, color: '#333' }}>{profileData?.phonenumber || t('profile.notSet')}</strong>
</div>
<div style={{ display: 'flex', alignItems: 'center', fontSize: 14 }}>
<MailOutlined style={{ marginRight: 12, color: '#165DFF', fontSize: 16 }} />
<span style={{ color: '#666' }}>用户邮箱:</span>
<strong style={{ marginLeft: 4, color: '#333' }}>{profileData?.email || '未设置'}</strong>
<strong style={{ marginLeft: 4, color: '#333' }}>{profileData?.email || t('profile.notSet')}</strong>
</div>
<div style={{ display: 'flex', alignItems: 'center', fontSize: 14 }}>
<SafetyOutlined style={{ marginRight: 12, color: '#165DFF', fontSize: 16 }} />
<span style={{ color: '#666' }}>所属角色:</span>
<strong style={{ marginLeft: 4, color: '#333' }}>
{profileData?.roles?.map((r: any) => r.roleName).join(', ') || '未设置'}
{profileData?.roles?.map((r: any) => r.roleName).join(', ') || t('profile.notSet')}
</strong>
</div>
</Space>
@@ -243,7 +244,7 @@ const Profile: React.FC = () => {
size="large"
>
<Form.Item
label="用户名称"
label={t('profile.username')}
name="userName"
>
<Input
@@ -254,19 +255,19 @@ const Profile: React.FC = () => {
</Form.Item>
<Form.Item
label="用户昵称"
label={t('users.nicknameLabel')}
name="nickName"
rules={[{ required: true, message: '请输入用户昵称' }]}
rules={[{ required: true, message: t('profile.nicknamePlaceholder') }]}
>
<Input
prefix={<UserOutlined />}
placeholder="请输入用户昵称"
placeholder={t('profile.nicknamePlaceholder')}
style={{ borderRadius: 6 }}
/>
</Form.Item>
<Form.Item
label="性别"
label={t('users.genderLabel')}
name="sex"
>
<Radio.Group>
@@ -276,19 +277,19 @@ const Profile: React.FC = () => {
</Form.Item>
<Form.Item
label="手机号码"
label={t('users.phoneLabel')}
name="phonenumber"
rules={[{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码' }]}
>
<Input
prefix={<PhoneOutlined />}
placeholder="请输入手机号码"
placeholder={t('profile.phonePlaceholder')}
style={{ borderRadius: 6 }}
/>
</Form.Item>
<Form.Item
label="邮箱"
label={t('users.email')}
name="email"
rules={[{ type: 'email', message: '请输入正确的邮箱格式' }]}
>
@@ -300,7 +301,7 @@ const Profile: React.FC = () => {
</Form.Item>
<Form.Item
label="备注"
label={t('roles.remark')}
name="remark"
>
<Input.TextArea
@@ -339,7 +340,7 @@ const Profile: React.FC = () => {
size="large"
>
<Form.Item
label="旧密码"
label={t('profile.oldPassword')}
name="oldPassword"
rules={[{ required: true, message: '请输入旧密码' }]}
>
@@ -351,7 +352,7 @@ const Profile: React.FC = () => {
</Form.Item>
<Form.Item
label="新密码"
label={t('profile.newPassword')}
name="newPassword"
rules={[
{ required: true, message: '请输入新密码' },
@@ -366,7 +367,7 @@ const Profile: React.FC = () => {
</Form.Item>
<Form.Item
label="确认密码"
label={t('profile.confirmPassword')}
name="confirmPassword"
rules={[{ required: true, message: '请确认新密码' }]}
>

View File

@@ -16,22 +16,14 @@ import { Line, Bar, Pie } from '@ant-design/charts';
import bgMain from '../assets/images/bgmain.jpg';
import { UniversalBarChart, UniversalLineChart, UniversalPieChart } from '../components/recharts';
import CesiumMap from '../components/Map';
import { useTranslation } from 'react-i18next';
const { Option } = Select;
const { Search } = Input;
const { Text, Title } = Typography;
// 模拟数据扩展
const deviceList = [
{ id: 1, name: '逆变器 INV-01', type: '逆变器', status: '正常', data: '164.2 KW', communication: '通讯正常', updateTime: '2025-05-25 10:24:15' },
{ id: 2, name: '逆变器 INV-02', type: '逆变器', status: '正常', data: '166.8 KW', communication: '通讯正常', updateTime: '2025-05-25 10:24:15' },
{ id: 3, name: '逆变器 INV-03', type: '逆变器', status: '正常', data: '162.1 KW', communication: '通讯正常', updateTime: '2025-05-25 10:24:15' },
{ id: 4, name: '逆变器 INV-04', type: '逆变器', status: '正常', data: '159.7 KW', communication: '通讯正常', updateTime: '2025-05-25 10:24:15' },
{ id: 5, name: '逆变器 INV-05', type: '逆变器', status: '告警', data: '159.8 KW', communication: '通讯正常', updateTime: '2025-05-25 10:24:15' },
{ id: 6, name: '汇流箱 01', type: '汇流箱', status: '正常', data: '32 组 / 32 组', communication: '通讯正常', updateTime: '2025-05-25 10:24:15' },
{ id: 7, name: '汇流箱 02', type: '汇流箱', status: '正常', data: '32 组 / 32 组', communication: '通讯正常', updateTime: '2025-05-25 10:24:15' },
{ id: 8, name: '气象站', type: '气象站', status: '正常', data: '28.0 ℃ / 815W/m²', communication: '通讯正常', updateTime: '2025-05-25 10:24:15' },
];
const powerTrendData = [
{ time: '00:00', value: 0, type: '实时功率' },
@@ -54,6 +46,7 @@ const inverterPowerData = [
const heatmapColors = ['#52c41a', '#1890ff', '#faad14', '#1890ff', '#52c41a'];
const RealTimeMonitorPage = () => {
const { t } = useTranslation();
const [filterStatus, setFilterStatus] = useState('all');
const [filterType, setFilterType] = useState(undefined);
const [searchName, setSearchName] = useState('');
@@ -61,7 +54,7 @@ const RealTimeMonitorPage = () => {
const cardStyle = { borderRadius: 12, border: '1px solid #f0f0f0', boxShadow: '0 2px 8px rgba(0,0,0,0.04)' };
const getStatusTag = (status) => {
const color = status === '正常' ? '#52c41a' : status === '告警' ? '#faad14' : '#999';
const color = status === t('roles.normal') ? '#52c41a' : status === t('constants.sourceAlert') ? '#faad14' : '#999';
return (
<Space size={4}>
<span style={{ width: 8, height: 8, borderRadius: '50%', background: color, display: 'inline-block' }} />
@@ -72,18 +65,18 @@ const RealTimeMonitorPage = () => {
const columns = [
{
title: '设备名称', dataIndex: 'name', key: 'name',
title: t('devices.deviceName'), dataIndex: 'name', key: 'name',
render: (text, record) => (
<Space>
<div style={{ width: 24, height: 24, background: '#f5f5f5', borderRadius: 4, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
{record.type === '逆变器' ? <ThunderboltOutlined style={{ fontSize: 14 }} /> : <BranchesOutlined style={{ fontSize: 14 }} />}
{record.type === t('home.inverter') ? <ThunderboltOutlined style={{ fontSize: 14 }} /> : <BranchesOutlined style={{ fontSize: 14 }} />}
</div>
<span style={{ fontSize: 13 }}>{text}</span>
</Space>
)
},
{ title: '类型', dataIndex: 'type', render: (t) => <span style={{ fontSize: 13, color: '#666' }}>{t}</span> },
{ title: '当前状态', dataIndex: 'status', render: getStatusTag },
{ title: t('common.type'), dataIndex: 'type', render: (t) => <span style={{ fontSize: 13, color: '#666' }}>{t}</span> },
{ title: t('workOrder.currentStatus'), dataIndex: 'status', render: getStatusTag },
{ title: '实时功率/数据', dataIndex: 'data', render: (d) => <span style={{ fontSize: 13, fontWeight: 500 }}>{d}</span> },
{
title: '通讯状态', dataIndex: 'communication',
@@ -94,7 +87,17 @@ const RealTimeMonitorPage = () => {
</Space>
)
},
{ title: '更新时间', dataIndex: 'updateTime', render: (t) => <span style={{ fontSize: 12, color: '#999' }}>{t}</span> }
{ title: t('common.updateTime'), dataIndex: 'updateTime', render: (t) => <span style={{ fontSize: 12, color: '#999' }}>{t}</span> }
];
const deviceList = [
{ id: 1, name: '逆变器 INV-01', type: t('home.inverter'), status: t('roles.normal'), data: '164.2 KW', communication: '通讯正常', updateTime: '2025-05-25 10:24:15' },
{ id: 2, name: '逆变器 INV-02', type: t('home.inverter'), status: t('roles.normal'), data: '166.8 KW', communication: '通讯正常', updateTime: '2025-05-25 10:24:15' },
{ id: 3, name: '逆变器 INV-03', type: t('home.inverter'), status: t('roles.normal'), data: '162.1 KW', communication: '通讯正常', updateTime: '2025-05-25 10:24:15' },
{ id: 4, name: '逆变器 INV-04', type: t('home.inverter'), status: t('roles.normal'), data: '159.7 KW', communication: '通讯正常', updateTime: '2025-05-25 10:24:15' },
{ id: 5, name: '逆变器 INV-05', type: t('home.inverter'), status: t('constants.sourceAlert'), data: '159.8 KW', communication: '通讯正常', updateTime: '2025-05-25 10:24:15' },
{ id: 6, name: '汇流箱 01', type: t('home.combinerBox'), status: t('roles.normal'), data: '32 组 / 32 组', communication: '通讯正常', updateTime: '2025-05-25 10:24:15' },
{ id: 7, name: '汇流箱 02', type: t('home.combinerBox'), status: t('roles.normal'), data: '32 组 / 32 组', communication: '通讯正常', updateTime: '2025-05-25 10:24:15' },
{ id: 8, name: '气象站', type: '气象站', status: t('roles.normal'), data: '28.0 ℃ / 815W/m²', communication: '通讯正常', updateTime: '2025-05-25 10:24:15' },
];
return (
@@ -112,9 +115,9 @@ const RealTimeMonitorPage = () => {
title={<span style={{ fontWeight: 800, fontSize: 13 }}>电站拓扑图 (实时)</span>}
extra={
<div style={{ display: 'flex', gap: 12, fontSize: 11 }}>
<span style={{ display: 'flex', alignItems: 'center' }}><span style={{ width: 6, height: 6, borderRadius: '50%', background: '#52c41a', marginRight: 4 }}></span>正常</span>
<span style={{ display: 'flex', alignItems: 'center' }}><span style={{ width: 6, height: 6, borderRadius: '50%', background: '#faad14', marginRight: 4 }}></span>告警</span>
<span style={{ display: 'flex', alignItems: 'center' }}><span style={{ width: 6, height: 6, borderRadius: '50%', background: '#888', marginRight: 4 }}></span>离线</span>
<span style={{ display: 'flex', alignItems: 'center' }}><span style={{ width: 6, height: 6, borderRadius: '50%', background: '#52c41a', marginRight: 4 }}></span>roles.normal</span>
<span style={{ display: 'flex', alignItems: 'center' }}><span style={{ width: 6, height: 6, borderRadius: '50%', background: '#faad14', marginRight: 4 }}></span>constants.sourceAlert</span>
<span style={{ display: 'flex', alignItems: 'center' }}><span style={{ width: 6, height: 6, borderRadius: '50%', background: '#888', marginRight: 4 }}></span>devices.offline</span>
<span style={{ display: 'flex', alignItems: 'center' }}><span style={{ color: '#1890ff', marginLeft: 4 }}>•• 通讯中</span></span>
</div>
}
@@ -185,7 +188,7 @@ const RealTimeMonitorPage = () => {
{ title: '当前总功率', value: '812.6', unit: 'kW', sub: '额定功率 1,000 kW', color: '#165DFF', icon: <ThunderboltOutlined /> },
{ title: '并网状态', value: '并网正常', unit: '', sub: '频率 50.02 Hz', color: '#00B42A', icon: <CheckCircleOutlined /> },
{ title: '电网电压', value: '380.2', unit: 'V', sub: 'A相 380.1 V', color: '#165DFF', icon: <BarChartOutlined /> },
{ title: '今日累计发电量', value: '4,752', unit: 'kWh', sub: '昨日 5,000 kWh', color: '#00B42A', icon: <RiseOutlined /> },
{ title: '今日累计发电量', value: '4,752', unit: t('home.kWh'), sub: '昨日 5,000 kWh', color: '#00B42A', icon: <RiseOutlined /> },
{ title: '当前辐照度', value: '815', unit: 'W/m²', sub: '较昨日 +5.6%', color: '#faad14', icon: <SunOutlined /> },
{ title: '环境温度', value: '28.0', unit: '℃', sub: '较昨日 +1.2 ℃', color: '#165DFF', icon: <CloudOutlined /> },
].map((item, idx) => (
@@ -268,18 +271,18 @@ const RealTimeMonitorPage = () => {
</div>
<div style={{ maxHeight: 180, overflow: 'auto' }} className="custom-scrollbar">
{[
{ name: '逆变器 INV-01', status: '正常', color: '#52c41a' },
{ name: '逆变器 INV-02', status: '正常', color: '#52c41a' },
{ name: '逆变器 INV-03', status: '正常', color: '#52c41a' },
{ name: '逆变器 INV-04', status: '正常', color: '#52c41a' },
{ name: '逆变器 INV-05', status: '告警', color: '#faad14' },
{ name: '气象站', status: '正常', color: '#52c41a' },
{ name: '无人机机场', status: '正常', color: '#52c41a' },
{ name: '巡检机器人', status: '正常', color: '#52c41a' },
{ name: '逆变器 INV-01', status: t('roles.normal'), color: '#52c41a' },
{ name: '逆变器 INV-02', status: t('roles.normal'), color: '#52c41a' },
{ name: '逆变器 INV-03', status: t('roles.normal'), color: '#52c41a' },
{ name: '逆变器 INV-04', status: t('roles.normal'), color: '#52c41a' },
{ name: '逆变器 INV-05', status: t('constants.sourceAlert'), color: '#faad14' },
{ name: '气象站', status: t('roles.normal'), color: '#52c41a' },
{ name: '无人机机场', status: t('roles.normal'), color: '#52c41a' },
{ name: t('home.inspectionRobot'), status: t('roles.normal'), color: '#52c41a' },
].map((item, idx) => (
<div key={idx} style={{ display: 'flex', justifyContent: 'space-between', padding: '6px 4px', borderBottom: '1px solid #f5f5f5' }}>
<Space size={4}>
{item.name.includes('逆变器') ? <ThunderboltOutlined style={{ fontSize: 10 }} /> : <ExperimentOutlined style={{ fontSize: 10 }} />}
{item.name.includes(t('home.inverter')) ? <ThunderboltOutlined style={{ fontSize: 10 }} /> : <ExperimentOutlined style={{ fontSize: 10 }} />}
<span style={{ fontSize: 11 }}>{item.name}</span>
</Space>
<span style={{ fontSize: 11, color: item.color }}>{item.status}</span>
@@ -300,17 +303,17 @@ const RealTimeMonitorPage = () => {
outerRadius={65}
data={[
{
name: '在线',
name: t('devices.online'),
value: 52,
color: '#52c41a',
},
{
name: '离线',
name: t('devices.offline'),
value: 3,
color: '#d9d9d9',
},
{
name: '告警',
name: t('constants.sourceAlert'),
value: 1,
color: '#faad14',
},

View File

@@ -19,6 +19,7 @@ import { Line, Column, Pie, Area, DualAxes, Bar } from '@ant-design/charts';
import dayjs from 'dayjs';
import { ChevronRight } from 'lucide-react';
import { UniversalBarChart, UniversalLineChart, UniversalPieChart } from '../components/recharts';
import { useTranslation } from 'react-i18next';
const { TabPane } = Tabs;
const { RangePicker } = DatePicker;
@@ -142,15 +143,10 @@ const reportTemplates = [
{ name: '巡检与机器人作业报告', desc: '巡检与机器人作业统计模板', color: '#722ED1', bg: '#F9F0FF', icon: <RobotOutlined /> },
];
const generationRecord = [
{ name: '电站月报', time: '2025-05-31 08:15', operator: '张运维', type: '发电报表', status: '已生成' },
{ name: '告警周报', time: '2025-05-26 09:10', operator: '系统自动', type: '告警报表', status: '已生成' },
{ name: 'AI诊断日报', time: '2025-05-31 07:30', operator: '系统自动', type: 'AI分析报告', status: '生成中' },
{ name: '运维汇总报表', time: '2025-05-31 09:00', operator: '李工程师', type: '运维报表', status: '已生成' },
{ name: '清洗效果报告', time: '2025-05-28 10:20', operator: '王运维', type: '运维报表', status: '已生成' },
];
export default function ReportCenterPage() {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState('overview');
const [reportCycle, setReportCycle] = useState('month');
const [reportType, setReportType] = useState('all');
@@ -162,6 +158,13 @@ export default function ReportCenterPage() {
boxShadow: '0 4px 12px rgba(0,0,0,0.05)',
overflow: 'hidden'
};
const generationRecord = [
{ name: '电站月报', time: '2025-05-31 08:15', operator: '张运维', type: '发电报表', status: '已生成' },
{ name: '告警周报', time: '2025-05-26 09:10', operator: '系统自动', type: t('report.alertReport'), status: '已生成' },
{ name: 'AI诊断日报', time: '2025-05-31 07:30', operator: '系统自动', type: 'AI分析报告', status: '生成中' },
{ name: '运维汇总报表', time: '2025-05-31 09:00', operator: '李工程师', type: '运维报表', status: '已生成' },
{ name: '清洗效果报告', time: '2025-05-28 10:20', operator: '王运维', type: '运维报表', status: '已生成' },
];
return (
<div style={{ padding: '4px 0px', background: '#f5f7fa', minHeight: '100vh', boxSizing: 'border-box' }}>
@@ -198,14 +201,14 @@ export default function ReportCenterPage() {
`}
</style>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 8 }}>
<Typography.Title level={4} style={{ margin: '0 32px 0 0', }}>报表中心</Typography.Title>
<Typography.Title level={4} style={{ margin: '0 32px 0 0', }}>{t('router.table')}</Typography.Title>
{/* 页面头部标签 */}
<Tabs activeKey={activeTab} onChange={setActiveTab} className="report-tabs">
<TabPane tab="报表总览" key="overview" />
<TabPane tab="发电报表" key="power" />
<TabPane tab="运维报表" key="operation" />
<TabPane tab="告警报表" key="alarm" />
<TabPane tab={t('report.alertReport')} key="alarm" />
<TabPane tab="AI分析报告" key="ai" />
</Tabs>
</div>
@@ -269,18 +272,18 @@ export default function ReportCenterPage() {
<div style={{ display: 'flex', alignItems: 'center' }}>
<span className="filter-label">报表周期:</span>
<Select value={reportCycle} onChange={setReportCycle} style={{ width: 100 }} size="small">
<Option value="day">日报</Option>
<Option value="week">周报</Option>
<Option value="month">月报</Option>
<Option value="day">{t('report.daily')}</Option>
<Option value="week">{t('report.weekly')}</Option>
<Option value="month">{t('report.monthly')}</Option>
</Select>
</div>
<div style={{ display: 'flex', alignItems: 'center' }}>
<span className="filter-label">报表类型:</span>
<Select value={reportType} onChange={setReportType} style={{ width: 120 }} size="small">
<Option value="all">全部</Option>
<Option value="all">{t('common.all')}</Option>
<Option value="power">发电报表</Option>
<Option value="operation">运维报表</Option>
<Option value="alarm">告警报表</Option>
<Option value="alarm">{t('report.alertReport')}</Option>
<Option value="ai">AI分析报告</Option>
</Select>
</div>
@@ -295,7 +298,7 @@ export default function ReportCenterPage() {
</div>
<div style={{ marginLeft: 'auto' }}>
<Space size={12}>
<Button type="primary" icon={<FileTextOutlined />} size="small" style={{ borderRadius: 6, height: 32, padding: '0 16px' }}>生成报表</Button>
<Button type="primary" icon={<FileTextOutlined />} size="small" style={{ borderRadius: 6, height: 32, padding: '0 16px' }}>{t('report.generateReport')}</Button>
<Button icon={<FilePdfOutlined />} size="small" style={{ borderRadius: 6, height: 32, color: '#F53F3F', borderColor: '#F53F3F' }}>导出PDF</Button>
<Button icon={<FileExcelOutlined />} size="small" style={{ borderRadius: 6, height: 32, color: '#00B42A', borderColor: '#00B42A' }}>导出Excel</Button>
</Space>
@@ -382,8 +385,8 @@ export default function ReportCenterPage() {
height={200}
series={[
{ key: 'generation', name: '发电量', color: '#1890ff' },
{ key: 'irradiance', name: '辐照度', color: '#52c41a' },
{ key: 'temp', name: '温度', color: '#fa8c16' },
{ key: 'irradiance', name: t('home.irradiance'), color: '#52c41a' },
{ key: 'temp', name: t('home.temperature'), color: '#fa8c16' },
]}
/>
</Card>
@@ -399,7 +402,7 @@ export default function ReportCenterPage() {
<div style={{ fontSize: 10, color: '#86909C', marginBottom: 12 }}>
<Space size={8} wrap>
<Badge color="#165DFF" text="新增工单" style={{ fontSize: 10 }} />
<Badge color="#00B42A" text="完成工单" style={{ fontSize: 10 }} />
<Badge color="#00B42A" text={t('workOrder.completeOrder')} style={{ fontSize: 10 }} />
<Badge color="#722ED1" text="闭环率 (%)" style={{ fontSize: 10 }} />
</Space>
</div>
@@ -409,7 +412,7 @@ export default function ReportCenterPage() {
height={200}
series={[
{ key: 'newOrder', name: '新增工单', color: '#1890ff' },
{ key: 'completed', name: '完成工单', color: '#52c41a' },
{ key: 'completed', name: t('workOrder.completeOrder'), color: '#52c41a' },
{ key: 'alarmCount', name: '告警数', color: '#fa8c16' },
{ key: 'closedRate', name: '闭环率', color: '#722ed1' },
]}
@@ -515,7 +518,7 @@ export default function ReportCenterPage() {
<CalendarOutlined style={{ color: '#165DFF' }} />
<span style={{ fontSize: 13, fontWeight: 700 }}>每周一 09:00 自动发送</span>
</Space>
<Button type="link" size="small" icon={<EditOutlined />} style={{ fontSize: 12, whiteSpace: 'nowrap' }}>编辑</Button>
<Button type="link" size="small" icon={<EditOutlined />} style={{ fontSize: 12, whiteSpace: 'nowrap' }}>{t('roles.edit')}</Button>
</div>
</Col>
</Row>
@@ -534,15 +537,15 @@ export default function ReportCenterPage() {
render: (text) => <Space size={8}><FilePdfOutlined style={{ color: '#F53F3F' }} />{text}</Space>
},
{
title: '状态', dataIndex: 'status', width: 80,
title: t('roles.status'), dataIndex: 'status', width: 80,
render: (v) => <Tag color={v === '已生成' ? 'success' : 'warning'} style={{ borderRadius: 4, fontSize: 10, margin: 0 }}>{v}</Tag>
},
{
title: '操作', width: 120,
title: t('roles.actions'), width: 120,
render: () => (
<Space split={<Divider type="vertical" />}>
<Button type="link" size="small" icon={<EyeOutlined />} style={{ fontSize: 12, padding: 0 }}>查看</Button>
<Button type="link" size="small" icon={<DownloadOutlined />} style={{ fontSize: 12, padding: 0 }}>下载</Button>
<Button type="link" size="small" icon={<EyeOutlined />} style={{ fontSize: 12, padding: 0 }}>{t('common.view')}</Button>
<Button type="link" size="small" icon={<DownloadOutlined />} style={{ fontSize: 12, padding: 0 }}>{t('common.download')}</Button>
</Space>
)
}
@@ -581,13 +584,13 @@ export default function ReportCenterPage() {
{ title: '报表名称', dataIndex: 'name', width: 100, ellipsis: true },
{ title: '生成时间', dataIndex: 'time', width: 130 },
{ title: '生成人', dataIndex: 'operator', width: 80 },
{ title: '类型', dataIndex: 'type', width: 80 },
{ title: t('common.type'), dataIndex: 'type', width: 80 },
{
title: '状态', dataIndex: 'status', width: 70,
title: t('roles.status'), dataIndex: 'status', width: 70,
render: (v) => <Tag color={v === '已生成' ? 'success' : 'warning'} style={{ borderRadius: 4, fontSize: 10, margin: 0 }}>{v}</Tag>
},
{
title: '操作', width: 90,
title: t('roles.actions'), width: 90,
render: () => (
<Space size={8}>
<Button type="text" icon={<EyeOutlined />} size="small" />

View File

@@ -11,11 +11,13 @@ import { getMenuList } from '../api/mune';
import { buildPermTree, buildTreeFromFlat } from '../lib/utils';
import { addRole, deleteRole, editRoleApi, getRoleList } from '../api/role';
import EmptyData from '../components/Empty';
import { useTranslation } from 'react-i18next';
const { Text } = Typography;
const AntdCard = Card as any;
export default function Roles() {
const { t } = useTranslation();
const [roles, setRoles] = useState([]);
const [menuTree, setMenuTree] = useState([]);
const [checkedKeys, setCheckedKeys] = useState<number[]>([]);
@@ -107,13 +109,13 @@ export default function Roles() {
const handleDeleteRole = (roleId) => {
deleteRole([roleId]).then(res => {
if (res.code == 200) {
messageApi.success('删除成功');
messageApi.success(t('common.deleteSuccess'));
getRole();
} else {
messageApi.error(res.msg || '删除失败');
messageApi.error(res.msg || t('common.deleteFail'));
}
}).catch(() => {
messageApi.error('删除失败');
messageApi.error(t('common.deleteFail'));
})
};
@@ -134,10 +136,10 @@ export default function Roles() {
getRole();
setRoleModalVisible(false);
} else {
messageApi.error(res.msg || "编辑失败");
messageApi.error(res.msg || t('common.editFail'));
}
}).catch(() => {
messageApi.error("编辑失败");
messageApi.error(t('common.editFail'));
});
} else {
const params = {
@@ -152,10 +154,10 @@ export default function Roles() {
getRole();
setRoleModalVisible(false);
} else {
messageApi.error(res.msg || "新增失败");
messageApi.error(res.msg || t('common.addFail'));
}
}).catch(() => {
messageApi.error("新增失败");
messageApi.error(t('common.addFail'));
});
}
});
@@ -166,7 +168,7 @@ export default function Roles() {
{contextHolder}
{/* 统一卡片容器(和菜单管理一致) */}
<AntdCard title="角色管理" style={{ borderRadius: 8 }}>
<AntdCard title={t('router.roles')} style={{ borderRadius: 8 }}>
{/* 顶部操作栏(和菜单管理统一) */}
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
<Button
@@ -224,7 +226,7 @@ export default function Roles() {
<Space direction="vertical" size={2} style={{ width: '100%' }}>
<Text type="secondary">权限标识:{role.roleKey}</Text>
<Text type="secondary">
状态:{role.status == 1 ? '正常' : '停用'}
状态:{role.status == 1 ? t('roles.normal') : t('roles.stopped')}
</Text>
<Text type="secondary">
{role.admin && '【超级管理员】'}
@@ -238,7 +240,7 @@ export default function Roles() {
))
) : (
<Col span={24}>
<EmptyData height={260} description="暂无数据" />
<EmptyData height={260} description={t('common.noData')} />
</Col>
)}
</Row>
@@ -257,7 +259,7 @@ export default function Roles() {
{/* 新增/编辑弹窗 */}
<Modal
title={editRole ? '编辑角色' : '新增角色'}
title={editRole ? '编辑角色' : t('roles.addRole')}
open={roleModalVisible}
onCancel={() => setRoleModalVisible(false)}
onOk={handleSaveRole}
@@ -265,7 +267,7 @@ export default function Roles() {
maskClosable={false}
>
<Form form={roleForm} layout="vertical">
<Form.Item name="roleName" label="角色名称" rules={[{ required: true }]}>
<Form.Item name="roleName" label={t('roles.roleName')} rules={[{ required: true }]}>
<Input placeholder="请输入角色名称" />
</Form.Item>
@@ -280,8 +282,8 @@ export default function Roles() {
{/* 状态 Switch */}
<Row gutter={16}>
<Col span={8}>
<Form.Item name="status" label="状态" valuePropName="checked">
<Switch checkedChildren="正常" unCheckedChildren="停用" />
<Form.Item name="status" label={t('roles.status')} valuePropName="checked">
<Switch checkedChildren={t('roles.normal')} unCheckedChildren={t('roles.stopped')} />
</Form.Item>
</Col>
</Row>

View File

@@ -28,6 +28,7 @@ import ErrorManagement from '../components/SystemSetting/ErrorManagement';
import AlarmSettingsManagement from '../components/SystemSetting/AlarmSettingsManagement';
import WorkOrderSettingsManagement from '../components/SystemSetting/WorkOrderSettingsManagement';
import { Outlet } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
const { Header, Content } = Layout;
const { Text } = Typography;
@@ -41,6 +42,7 @@ const AntdCard = Card as any;
// 主页面
// ======================
export default function SystemSettings() {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState('basic');
const [rawMenuList, setRawMenuList] = useState([]);
const [showTabs, setShowTabs] = useState([]);
@@ -53,15 +55,15 @@ export default function SystemSettings() {
const NoticePage = () => <div style={{ padding: 20 }}><h2>通知策略页面</h2></div>;
const DataSafetyPage = () => <div style={{ padding: 20 }}><h2>数据与安全页面</h2></div>;
const DefaultPage = () => <div style={{ padding: 20 }}><h2>请选择菜单</h2></div>;
const NoticePage = () => <div style={{ padding: 20 }}><h2>{t('router.noticePolicy')}</h2></div>;
const DataSafetyPage = () => <div style={{ padding: 20 }}><h2>{t('router.dataSafety')}</h2></div>;
const DefaultPage = () => <div style={{ padding: 20 }}><h2>{t('router.pleaseSelectMenu')}</h2></div>;
return (
<Layout style={{ minHeight: '100vh', overflow: 'hidden' }}>
{/*<div style={{ padding: '4px 0px 0' }}>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>系统设置</Typography.Title>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>{t('router.systemSetting')}</Typography.Title>
<Text type="secondary" style={{ fontSize: '12px' }}>告警规则配置、用户角色、通知管理与系统参数设置</Text>
</div>*/}

View File

@@ -8,11 +8,13 @@ import {
} from '@ant-design/icons';
import { addUser, deleteUser, editUser, getUser } from '../api/user';
import { getRoleList } from '../api/role';
import { useTranslation } from 'react-i18next';
const { Title } = Typography;
const { Option } = Select;
export default function UserList() {
const { t } = useTranslation();
const [userList, setUserList] = useState([]);
const [modalVisible, setModalVisible] = useState(false);
const [form] = Form.useForm();
@@ -91,13 +93,13 @@ export default function UserList() {
const handleDelete = (userId) => {
deleteUser(userId).then(res => {
if (res.code === 200) {
messageApi.success('删除成功');
messageApi.success(t('common.deleteSuccess'));
getUserList();
} else {
messageApi.error(res.msg || '删除失败');
messageApi.error(res.msg || t('common.deleteFail'));
}
}).catch(() => {
messageApi.error('删除失败');
messageApi.error(t('common.deleteFail'));
})
};
@@ -113,26 +115,26 @@ export default function UserList() {
params.userId = currentUser.userId;
editUser(params).then(res => {
if (res.code === 200) {
messageApi.success('编辑成功');
messageApi.success(t('common.editSuccess'));
getUserList();
setModalVisible(false);
} else {
messageApi.error(res.msg || '编辑失败');
messageApi.error(res.msg || t('common.editFail'));
}
}).catch(() => {
messageApi.error('编辑失败');
messageApi.error(t('common.editFail'));
});
} else {
addUser(params).then(res => {
if (res.code === 200) {
messageApi.success('新增成功');
messageApi.success(t('common.addSuccess'));
getUserList();
setModalVisible(false);
} else {
messageApi.error(res.msg || '新增失败');
messageApi.error(res.msg || t('common.addFail'));
}
}).catch(() => {
messageApi.error('新增失败');
messageApi.error(t('common.addFail'));
})
}
});
@@ -140,37 +142,37 @@ export default function UserList() {
// 表格列(支持多角色展示)
const columns = [
{ title: '用户账号', dataIndex: 'userName' },
{ title: '用户昵称', dataIndex: 'nickName' },
{ title: '手机号', dataIndex: 'phonenumber' },
{ title: '邮箱', dataIndex: 'email' },
{ title: t('users.usernameLabel'), dataIndex: 'userName' },
{ title: t('users.nicknameLabel'), dataIndex: 'nickName' },
{ title: t('users.phone'), dataIndex: 'phonenumber' },
{ title: t('users.email'), dataIndex: 'email' },
{
title: '角色',
title: t('users.role'),
render: (_, record) => {
const roles = record.roles || [];
if (roles.length === 0) return <Tag>未分配</Tag>;
return roles.map(item => (
<Tag color="blue" key={item.roleId}>
{item.roleName || item.roleKey || '角色'}
{item.roleName || item.roleKey || t('users.role')}
</Tag>
));
}
},
{
title: '状态',
title: t('roles.status'),
dataIndex: 'status',
render: (s) => s === '0' ?
<Tag color="green">正常</Tag> :
<Tag color="red">停用</Tag>
<Tag color="green">{t('roles.normal')}</Tag> :
<Tag color="red">{t('roles.stopped')}</Tag>
},
{
title: '操作',
title: t('roles.actions'),
render: (_, r) => (
<Space>
<Button type="text" icon={<EditOutlined />} onClick={() => handleEdit(r)}>编辑</Button>
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(r.userId)}>
<Button type="text" danger>删除</Button>
<Button type="text" icon={<EditOutlined />} onClick={() => handleEdit(r)}>{t('roles.edit')}</Button>
<Popconfirm title={t('common.confirmDelete')} onConfirm={() => handleDelete(r.userId)}>
<Button type="text" danger>roles.delete</Button>
</Popconfirm>
</Space>
)
@@ -180,7 +182,7 @@ export default function UserList() {
return (
<div style={{ padding: 20 }}>
{contextHolder}
<Card title="用户管理">
<Card title={t('router.users')}>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
新增用户
@@ -205,7 +207,7 @@ export default function UserList() {
{/* 新增/编辑弹窗 */}
<Modal
title={currentUser ? '编辑用户' : '新增用户'}
title={currentUser ? t('systemSetting.editUser') : t('users.addUser')}
open={modalVisible}
width={650}
onCancel={() => setModalVisible(false)}
@@ -215,12 +217,12 @@ export default function UserList() {
<Form form={form} layout="vertical">
<Row gutter={16}>
<Col span={12}>
<Form.Item name="userName" label="用户账号" rules={[{ required: true }]}>
<Form.Item name="userName" label={t('users.usernameLabel')} rules={[{ required: true }]}>
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="nickName" label="用户昵称" rules={[{ required: true }]}>
<Form.Item name="nickName" label={t('users.nicknameLabel')} rules={[{ required: true }]}>
<Input />
</Form.Item>
</Col>
@@ -228,12 +230,12 @@ export default function UserList() {
<Row gutter={16}>
<Col span={12}>
<Form.Item name="phonenumber" label="手机号码">
<Form.Item name="phonenumber" label={t('users.phoneLabel')}>
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="email" label="用户邮箱">
<Form.Item name="email" label={t('profile.email')}>
<Input />
</Form.Item>
</Col>
@@ -241,15 +243,15 @@ export default function UserList() {
<Row gutter={16}>
<Col span={12}>
<Form.Item name="password" label="密码">
<Input.Password placeholder="不填则不修改" />
<Form.Item name="password" label={t('users.passwordLabel')}>
<Input.Password placeholder={t('users.passwordPlaceholder')} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="language" label="语言">
<Form.Item name="language" label={t('users.languageLabel')}>
<Select placeholder="请选择语言">
<Option value="zh-CN">中文</Option>
<Option value="en">英语</Option>
<Option value="zh-CN">{t('users.chinese')}</Option>
<Option value="en">{t('users.english')}</Option>
</Select>
</Form.Item>
</Col>
@@ -258,7 +260,7 @@ export default function UserList() {
<Row gutter={16}>
<Col span={12}>
{/* 多角色选择 */}
<Form.Item name="roleIds" label="分配角色">
<Form.Item name="roleIds" label={t('users.assignRoleLabel')}>
<Select mode="multiple" placeholder="请选择角色">
{roles.map(item => (
<Option key={item.roleId} value={item.roleId}>
@@ -269,10 +271,10 @@ export default function UserList() {
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="status" label="状态" initialValue="0">
<Form.Item name="status" label={t('roles.status')} initialValue="0">
<Select>
<Option value="0">正常</Option>
<Option value="1">停用</Option>
<Option value="0">{t('roles.normal')}</Option>
<Option value="1">{t('roles.stopped')}</Option>
</Select>
</Form.Item>
</Col>
@@ -282,11 +284,11 @@ export default function UserList() {
<Input />
</Form.Item>
<Form.Item name="sex" label="性别" initialValue="2">
<Form.Item name="sex" label={t('users.genderLabel')} initialValue="2">
<Select>
<Option value="0">男</Option>
<Option value="1">女</Option>
<Option value="2">未知</Option>
<Option value="2">{t('constants.locationUnknown')}</Option>
</Select>
</Form.Item>
</Form>

View File

@@ -12,11 +12,13 @@ import {
ExpandOutlined, SettingOutlined, SearchOutlined, MoreOutlined
} from '@ant-design/icons';
import { Fence, Monitor, HardDrive, BellRing, History, Eye, Play, Square, Volume2, ChevronRight } from 'lucide-react';
import { useTranslation } from 'react-i18next';
const { Text, Title } = Typography;
const { Option } = Select;
export default function VideoMonitorPage() {
const { t } = useTranslation();
const cardStyle = {
borderRadius: 12,
border: '1px solid #f0f0f0',
@@ -25,26 +27,26 @@ export default function VideoMonitorPage() {
};
const [camList] = useState([
{ key: '1', name: 'CAM-01 1#阵列区球机', position: '1#阵列区', status: '在线', recStatus: '录制中', time: '2025-05-20 10:23:45' },
{ key: '2', name: 'CAM-02 2#逆变器室', position: '逆变器室', status: '在线', recStatus: '录制中', time: '2025-05-20 10:24:10' },
{ key: '3', name: 'CAM-03 3#电站大门', position: '电站大门', status: '在线', recStatus: '录制中', time: '2025-05-20 10:23:58' },
{ key: '4', name: 'CAM-04 4#周界北侧', position: '周界北侧', status: '在线', recStatus: '录制中', time: '2025-05-20 10:24:01' },
{ key: '5', name: 'CAM-05 5#气象站', position: '气象站', status: '在线', recStatus: '录制中', time: '2025-05-20 10:24:05' },
{ key: '6', name: 'CAM-06 6#设备区', position: '设备区', status: '在线', recStatus: '录制中', time: '2025-05-20 10:23:50' },
{ key: '7', name: 'CAM-07 7#储能电池舱', position: '储能区', status: '在线', recStatus: '录制中', time: '2025-05-20 10:23:47' },
{ key: '8', name: 'CAM-08 8#周界南侧', position: '周界南侧', status: '离线', recStatus: '停止', time: '2025-05-20 09:12:31' },
{ key: '9', name: 'CAM-09 9#阵列区西侧', position: '1#阵列区西侧', status: '离线', recStatus: '停止', time: '2025-05-20 08:55:02' },
{ key: '1', name: 'CAM-01 1#阵列区球机', position: '1#阵列区', status: t('devices.online'), recStatus: '录制中', time: '2025-05-20 10:23:45' },
{ key: '2', name: 'CAM-02 2#逆变器室', position: '逆变器室', status: t('devices.online'), recStatus: '录制中', time: '2025-05-20 10:24:10' },
{ key: '3', name: 'CAM-03 3#电站大门', position: '电站大门', status: t('devices.online'), recStatus: '录制中', time: '2025-05-20 10:23:58' },
{ key: '4', name: 'CAM-04 4#周界北侧', position: '周界北侧', status: t('devices.online'), recStatus: '录制中', time: '2025-05-20 10:24:01' },
{ key: '5', name: 'CAM-05 5#气象站', position: '气象站', status: t('devices.online'), recStatus: '录制中', time: '2025-05-20 10:24:05' },
{ key: '6', name: 'CAM-06 6#设备区', position: '设备区', status: t('devices.online'), recStatus: '录制中', time: '2025-05-20 10:23:50' },
{ key: '7', name: 'CAM-07 7#储能电池舱', position: '储能区', status: t('devices.online'), recStatus: '录制中', time: '2025-05-20 10:23:47' },
{ key: '8', name: 'CAM-08 8#周界南侧', position: '周界南侧', status: t('devices.offline'), recStatus: t('video.stop'), time: '2025-05-20 09:12:31' },
{ key: '9', name: 'CAM-09 9#阵列区西侧', position: '1#阵列区西侧', status: t('devices.offline'), recStatus: t('video.stop'), time: '2025-05-20 08:55:02' },
]);
const camColumns = [
{ title: '摄像头名称', dataIndex: 'name', key: 'name', width: 200 },
{ title: '位置', dataIndex: 'position', key: 'position', width: 120 },
{ title: t('aiAnalysis.location'), dataIndex: 'position', key: 'position', width: 120 },
{
title: '状态', dataIndex: 'status', key: 'status', width: 100,
title: t('roles.status'), dataIndex: 'status', key: 'status', width: 100,
render: (text: string) => (
<Space size={4}>
<div style={{ width: 6, height: 6, borderRadius: '50%', background: text === '在线' ? '#00B42A' : '#F53F3F' }}></div>
<span style={{ color: text === '在线' ? '#00B42A' : '#F53F3F' }}>{text}</span>
<div style={{ width: 6, height: 6, borderRadius: '50%', background: text === t('devices.online') ? '#00B42A' : '#F53F3F' }}></div>
<span style={{ color: text === t('devices.online') ? '#00B42A' : '#F53F3F' }}>{text}</span>
</Space>
)
},
@@ -59,7 +61,7 @@ export default function VideoMonitorPage() {
},
{ title: '最后事件时间', dataIndex: 'time', key: 'time', width: 180 },
{
title: '操作', key: 'action', width: 60,
title: t('roles.actions'), key: 'action', width: 60,
render: () => (
<Button type="text" icon={<PlayCircleOutlined />} size="small" />
)
@@ -197,7 +199,7 @@ export default function VideoMonitorPage() {
extra={
<Space size={16}>
<Button type="link" size="small" icon={<ExpandOutlined />}>自定义布局</Button>
<Button type="link" size="small" icon={<FullscreenOutlined />}>全屏</Button>
<Button type="link" size="small" icon={<FullscreenOutlined />}>{t('video.fullScreen')}</Button>
</Space>
}
bordered={false}
@@ -220,10 +222,10 @@ export default function VideoMonitorPage() {
<ScissorOutlined style={{ color: '#fff', fontSize: 18, cursor: 'pointer' }} />
<CameraOutlined style={{ color: '#fff', fontSize: 18, cursor: 'pointer' }} />
<ExpandOutlined style={{ color: '#fff', fontSize: 18, cursor: 'pointer' }} />
<Select defaultValue="高清" size="small" style={{ width: 80 }} bordered={false} className="video-select">
<Option value="超清">超清</Option>
<Option value="高清">高清</Option>
<Option value="标清">标清</Option>
<Select defaultValue={t('video.high')} size="small" style={{ width: 80 }} bordered={false} className="video-select">
<Option value={t('video.super')}>{t('video.super')}</Option>
<Option value={t('video.high')}>{t('video.high')}</Option>
<Option value={t('video.standard')}>{t('video.standard')}</Option>
</Select>
</Space>
<div style={{ color: '#fff', cursor: 'pointer' }}><MoreOutlined style={{ fontSize: 20 }} /></div>
@@ -302,8 +304,8 @@ export default function VideoMonitorPage() {
<Input placeholder="搜索摄像头名称/位置" prefix={<SearchOutlined />} style={{ width: 200 }} size="small" />
<Select defaultValue="全部状态" size="small" style={{ width: 120 }}>
<Option value="全部状态">全部状态</Option>
<Option value="在线">在线</Option>
<Option value="离线">离线</Option>
<Option value={t('devices.online')}>{t('devices.online')}</Option>
<Option value={t('devices.offline')}>{t('devices.offline')}</Option>
</Select>
</Space>
}
@@ -380,8 +382,8 @@ export default function VideoMonitorPage() {
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'center', gap: 8, marginTop: 10 }}>
<Tooltip title="播放"><Button type="text" icon={<PlayCircleOutlined />} size="small" /></Tooltip>
<Tooltip title="下载"><Button type="text" icon={<DownloadOutlined />} size="small" /></Tooltip>
<Tooltip title={t('video.play')}><Button type="text" icon={<PlayCircleOutlined />} size="small" /></Tooltip>
<Tooltip title={t('common.download')}><Button type="text" icon={<DownloadOutlined />} size="small" /></Tooltip>
<Tooltip title="详情"><Button type="text" icon={<Eye />} size="small" /></Tooltip>
</div>
</div>

View File

@@ -49,6 +49,7 @@ import { RootState } from '../store';
import { getUser } from '../api/user';
import utils, { errorLevelOptions, getWorkOrderTypeText, levelColorMap, levelNum2Text, levelText2Num, workOrderTypeOptions } from '../lib/utils';
import { getAlarmList } from '../api/alarmApi';
import { useTranslation } from 'react-i18next';
const { Option } = Select;
const { Search } = Input;
@@ -66,14 +67,9 @@ const priorityConfig = {
// 工单状态数字映射
const orderStatusConfig = {
1: { label: '待处理', textColor: '#1890ff' },
2: { label: '执行中', textColor: '#1890ff' },
4: { label: '挂起', textColor: '#faad14' },
3: { label: '已完成', textColor: '#52c41a' },
};
const WorkOrderPage = () => {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState('1');
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]); // 批量选中
const [selectedRowKey, setSelectedRowKey] = useState('');
@@ -112,13 +108,20 @@ const WorkOrderPage = () => {
{ value: 2, label: '中' },
{ value: 3, label: '低' }
];
const orderStatusConfig = {
1: { label: t('constants.pending'), textColor: '#1890ff' },
2: { label: t('workOrder.inProgress'), textColor: '#1890ff' },
4: { label: t('workOrder.suspend'), textColor: '#faad14' },
3: { label: t('workOrder.completed'), textColor: '#52c41a' },
};
// 状态选项
const statusOptions = [
{ value: 1, label: '待处理' },
{ value: 2, label: '执行中' },
{ value: 3, label: '已完成' },
{ value: 4, label: '已挂起' }
{ value: 1, label: t('constants.pending') },
{ value: 2, label: t('workOrder.inProgress') },
{ value: 3, label: t('workOrder.completed') },
{ value: 4, label: t('workOrder.suspended') }
];
// 分页状态
@@ -397,7 +400,7 @@ const WorkOrderPage = () => {
latitude: item.latitude || 0,
},
deviceAlias: item.drone_callsign || item.device_sn
|| '无人机',
|| t('constants.uav'),
}));
@@ -467,15 +470,15 @@ const WorkOrderPage = () => {
const getOrderStatusPieData = () => {
const statusMap = {
1: '待处理',
2: '执行中',
3: '已完成',
4: '已挂起',
1: t('constants.pending'),
2: t('workOrder.inProgress'),
3: t('workOrder.completed'),
4: t('workOrder.suspended'),
};
const countMap: Record<string, number> = {};
workOrderList.forEach((item: any) => {
const label = statusMap[item.orderStatus] || '其他';
const label = statusMap[item.orderStatus] || t('constants.others');
countMap[label] = (countMap[label] || 0) + 1;
});
@@ -770,10 +773,10 @@ const WorkOrderPage = () => {
if (!record?.id) return messageApi.warning('无效工单');
try {
await deleteWorkOrder(record.id);
messageApi.success('删除成功');
messageApi.success(t('common.deleteSuccess'));
getWorkOrderList();
} catch (e) {
messageApi.error('删除失败');
messageApi.error(t('common.deleteFail'));
console.error(e);
}
};
@@ -819,23 +822,23 @@ const WorkOrderPage = () => {
// 表格列
const columns = [
{ title: '工单编号', dataIndex: 'orderNo', key: 'orderNo', width: 150 },
{ title: '工单标题', dataIndex: 'orderTitle', key: 'orderTitle', width: 200, ellipsis: true },
{ title: t('workOrder.orderTitle'), dataIndex: 'orderTitle', key: 'orderTitle', width: 200, ellipsis: true },
{
title: '来源',
title: t('workOrder.source'),
dataIndex: 'sourceType',
key: 'sourceType',
width: 90,
render: (t) => sourceTypeMap[t] || t
},
{
title: '类型',
title: t('common.type'),
dataIndex: 'orderType',
key: 'orderType',
width: 80,
render: (val) => getWorkOrderTypeText(val)
},
{
title: '优先级',
title: t('workOrder.priority'),
dataIndex: 'priorityLevel',
key: 'priorityLevel',
width: 80,
@@ -846,9 +849,9 @@ const WorkOrderPage = () => {
)
},
{ title: '设备', dataIndex: 'deviceId', key: 'deviceId', width: 160, ellipsis: true },
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 160 },
{ title: t('common.createTime'), dataIndex: 'createTime', key: 'createTime', width: 160 },
{
title: '状态',
title: t('roles.status'),
dataIndex: 'orderStatus',
key: 'orderStatus',
width: 100,
@@ -860,15 +863,15 @@ const WorkOrderPage = () => {
)
},
{
title: '操作',
title: t('roles.actions'),
key: 'action',
width: 180,
fixed: "right",
render: (_, record) => (
<Space size={4}>
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => handleRowSelect(record)}>查看</Button>
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => { handleRowSelect(record); openEditModal(); }}>修改</Button>
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(record)}>
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => handleRowSelect(record)}>{t('common.view')}</Button>
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => { handleRowSelect(record); openEditModal(); }}>{t('workOrder.modify')}</Button>
<Popconfirm title={t('common.confirmDelete')} onConfirm={() => handleDelete(record)}>
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
删除
</Button>
@@ -942,11 +945,11 @@ const WorkOrderPage = () => {
<Form form={queryForm} layout="inline" initialValues={{
orderStatus: 1
}}>
<Form.Item name="orderType" label="工单类型">
<Form.Item name="orderType" label={t('workOrder.orderType')}>
<Select style={{ width: 120 }} size="small" options={workOrderTypeOptions} allowClear showSearch optionFilterProp="label" filterOption={(input, option) => option.label.toLowerCase().includes(input.toLowerCase())} />
</Form.Item>
<Form.Item name="priorityLevel" label="优先级">
<Form.Item name="priorityLevel" label={t('workOrder.priority')}>
<Select style={{ width: 120 }} size="small" options={errorLevelOptions} allowClear />
</Form.Item>
@@ -954,7 +957,7 @@ const WorkOrderPage = () => {
<RangePicker style={{ width: 320 }} size="small" />
</Form.Item>
<Form.Item name="orderStatus" label="状态" >
<Form.Item name="orderStatus" label={t('roles.status')} >
<Select style={{ width: 120 }} size="small" options={statusOptions} allowClear />
</Form.Item>
@@ -963,8 +966,8 @@ const WorkOrderPage = () => {
</Form.Item>
<Form.Item>
<Space>
<Button size="small" onClick={handleReset}>重置</Button>
<Button type="primary" size="small" icon={<SearchOutlined />} onClick={handleQuery}>查询</Button>
<Button size="small" onClick={handleReset}>{t('common.reset')}</Button>
<Button type="primary" size="small" icon={<SearchOutlined />} onClick={handleQuery}>{t('common.search2')}</Button>
</Space>
</Form.Item>
</Form>
@@ -1168,14 +1171,14 @@ const WorkOrderPage = () => {
size="small"
columns={[
{ title: '工单编号', dataIndex: 'orderNo', width: 260 },
{ title: '工单标题', dataIndex: 'orderTitle', ellipsis: true },
{ title: t('workOrder.orderTitle'), dataIndex: 'orderTitle', ellipsis: true },
{
title: '计划执行时间',
dataIndex: 'planStartTime',
render: t => dayjs(t).format('YYYY-MM-DD HH:mm')
},
{
title: '优先级',
title: t('workOrder.priority'),
dataIndex: 'priorityLevel',
render: p => (
<Tag
@@ -1190,7 +1193,7 @@ const WorkOrderPage = () => {
)
},
{
title: '状态',
title: t('roles.status'),
dataIndex: 'orderStatus',
render: s => orderStatusConfig[s]?.label
},
@@ -1314,10 +1317,10 @@ const WorkOrderPage = () => {
const status = currentDetail?.orderStatus;
// 流程节点
const steps = [
{ label: '已创建', step: 1 },
{ label: '执行中', step: 2 },
{ label: '已挂起', step: 3 },
{ label: '已完成', step: 4 },
{ label: t('workOrder.created'), step: 1 },
{ label: t('workOrder.inProgress'), step: 2 },
{ label: t('workOrder.suspended'), step: 3 },
{ label: t('workOrder.completed'), step: 4 },
];
// 判断节点是否激活
@@ -1433,11 +1436,11 @@ const WorkOrderPage = () => {
</Button>
</div>
<div style={{ flex: 1 }}>
<Button icon={<EditOutlined />} block onClick={openEditModal} style={{ borderRadius: 8, height: 36, fontWeight: 600 }}>修改</Button>
<Button icon={<EditOutlined />} block onClick={openEditModal} style={{ borderRadius: 8, height: 36, fontWeight: 600 }}>{t('workOrder.modify')}</Button>
</div>
<div style={{ flex: 1 }}>
<Popconfirm title="确定挂起?" onConfirm={handleSuspend}>
<Button icon={<PauseOutlined />} danger ghost block style={{ borderRadius: 8, height: 36, fontWeight: 600 }}>挂起</Button>
<Popconfirm title={t('workOrder.confirmSuspend')} onConfirm={handleSuspend}>
<Button icon={<PauseOutlined />} danger ghost block style={{ borderRadius: 8, height: 36, fontWeight: 600 }}>{t('workOrder.suspend')}</Button>
</Popconfirm>
</div>
<div style={{ flex: 1 }}>
@@ -1446,7 +1449,9 @@ const WorkOrderPage = () => {
style={{ background: '#00B42A', color: '#fff', border: 'none', borderRadius: 8, height: 36, fontWeight: 600 }}
block
onClick={openCompleteModal}
>完成</Button>
>
{t('workOrder.complete')}
</Button>
</div>
{isShowDispatch && (
<div style={{ flex: 1 }}>
@@ -1456,7 +1461,7 @@ const WorkOrderPage = () => {
onClick={openDispatchUserModal} style={{ borderRadius: 8, height: 36, fontWeight: 600 }}
icon={<PlayCircleFilled />}
>
派单
{t('workOrder.dispatch')}
</Button>
</div>
)}
@@ -1465,7 +1470,7 @@ const WorkOrderPage = () => {
</Card>
<Card bordered={false} title={<Text strong style={{ fontSize: 14, color: '#1D2129' }}>AI 建议与关联信息</Text>}
<Card bordered={false} title={<Text strong style={{ fontSize: 14, color: '#1D2129' }}>{t('workOrder.aiSuggestion')}</Text>}
style={{ ...cardStyle, marginTop: 8 }} bodyStyle={{ padding: '16px' }}>
<Row gutter={12} style={{ marginBottom: 16 }}>
<Col span={8}>
@@ -1473,8 +1478,8 @@ const WorkOrderPage = () => {
<div style={{ width: 32, height: 32, background: '#fff', borderRadius: 8, display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 10px', boxShadow: '0 2px 6px rgba(114,46,209,0.1)' }}>
<RobotOutlined style={{ fontSize: 18, color: '#722ED1' }} />
</div>
<div style={{ fontSize: 11, color: '#86909C', marginBottom: 4 }}>建议携带</div>
<div style={{ fontSize: 12, fontWeight: 700, color: '#1D2129' }}>红外热像设备</div>
<div style={{ fontSize: 11, color: '#86909C', marginBottom: 4 }}>{t('workOrder.suggestedEquipment')}</div>
<div style={{ fontSize: 12, fontWeight: 700, color: '#1D2129' }}>{t('workOrder.thermalCamera')}</div>
</div>
</Col>
<Col span={8}>
@@ -1482,7 +1487,7 @@ const WorkOrderPage = () => {
<div style={{ width: 32, height: 32, background: '#fff', borderRadius: 8, display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 10px', boxShadow: '0 2px 6px rgba(0,180,42,0.1)' }}>
<ThunderboltOutlined style={{ fontSize: 18, color: '#00B42A' }} />
</div>
<div style={{ fontSize: 11, color: '#86909C', marginBottom: 4 }}>优先复检组件</div>
<div style={{ fontSize: 11, color: '#86909C', marginBottom: 4 }}>{t('workOrder.priorityInspect')}</div>
<div style={{ fontSize: 12, fontWeight: 700, color: '#1D2129' }}>B-12-03 / B-12-05</div>
</div>
</Col>
@@ -1491,7 +1496,7 @@ const WorkOrderPage = () => {
<div style={{ width: 32, height: 32, background: '#fff', borderRadius: 8, display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 10px', boxShadow: '0 2px 6px rgba(247,186,30,0.1)' }}>
<RiseOutlined style={{ fontSize: 18, color: '#F7BA1E' }} />
</div>
<div style={{ fontSize: 11, color: '#86909C', marginBottom: 4 }}>预计影响发电</div>
<div style={{ fontSize: 11, color: '#86909C', marginBottom: 4 }}>{t('workOrder.estimatedImpact')}</div>
<div style={{ fontSize: 13, fontWeight: 800, color: '#1D2129' }}>2.1%</div>
</div>
</Col>
@@ -1499,7 +1504,7 @@ const WorkOrderPage = () => {
<Row gutter={12}>
<Col span={13}>
<div style={{ fontSize: 12, fontWeight: 700, color: '#1D2129', marginBottom: 12 }}>关联告警趋势 <Text type="secondary" style={{ fontSize: 11, fontWeight: 400 }}>(近 7 天)</Text></div>
<div style={{ fontSize: 12, fontWeight: 700, color: '#1D2129', marginBottom: 12 }}>{t('workOrder.relatedAlertTrend')} <Text type="secondary" style={{ fontSize: 11, fontWeight: 400 }}>{t('workOrder.near7Days')}</Text></div>
<UniversalLineChart
data={alarmChartData}
xKey="date"
@@ -1511,9 +1516,9 @@ const WorkOrderPage = () => {
smooth
dot
series={[
{ key: 'ERROR', name: '高告警', color: '#FF4D4F', strokeWidth: 2 },
{ key: 'WARNING', name: '中告警', color: '#FA8C16', strokeWidth: 2 },
{ key: 'INFO', name: '低告警', color: '#52c41a', strokeWidth: 2 },
{ key: 'ERROR', name: t('workOrder.highAlarm'), color: '#FF4D4F', strokeWidth: 2 },
{ key: 'WARNING', name: t('workOrder.midAlarm'), color: '#FA8C16', strokeWidth: 2 },
{ key: 'INFO', name: t('workOrder.lowAlarm'), color: '#52c41a', strokeWidth: 2 },
]}
/>
</Col>
@@ -1532,7 +1537,7 @@ const WorkOrderPage = () => {
)} </div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{latestThreeAlarm.length === 0 ? (
<Text type="secondary" style={{ fontSize: 11 }}>近7天无告警记录</Text>
<Text type="secondary" style={{ fontSize: 11 }}>{t('workOrder.noAlerts7d')}</Text>
) : (
<>
{latestThreeAlarm.map((item, idx) => (
@@ -1588,36 +1593,36 @@ const WorkOrderPage = () => {
</Row>
{/* 修改工单弹窗 */}
<Modal title="修改工单" open={editModalVisible} onCancel={() => setEditModalVisible(false)} footer={null} width={520}>
<Modal title={t('workOrder.modifyOrder')} open={editModalVisible} onCancel={() => setEditModalVisible(false)} footer={null} width={520}>
<Form form={form} layout="vertical" onFinish={submitEdit}>
<FormItem name="orderTitle" label="工单标题" rules={[{ required: true, message: '请输入工单标题' }]}>
<Input placeholder="请输入工单标题" />
<FormItem name="orderTitle" label={t('workOrder.orderTitle')} rules={[{ required: true, message: t('workOrder.pleaseInputTitle') }]}>
<Input placeholder={t('workOrder.pleaseInputTitle')} />
</FormItem>
<FormItem name="orderType" label="工单类型" rules={[{ required: true, message: '请选择工单类型' }]}>
<Select placeholder="请选择工单类型" options={workOrderTypeOptions} />
<FormItem name="orderType" label={t('workOrder.orderType')} rules={[{ required: true, message: t('workOrder.pleaseSelectType') }]}>
<Select placeholder={t('workOrder.pleaseSelectType')} options={workOrderTypeOptions} />
</FormItem>
<FormItem name="priorityLevel" label="优先级" rules={[{ required: true, message: '请选择优先级' }]}>
{/*<Select placeholder="请选择优先级">
<FormItem name="priorityLevel" label={t('workOrder.priority')} rules={[{ required: true, message: t('workOrder.pleaseSelectPriority') }]}>
{/*<Select placeholder={t('workOrder.pleaseSelectPriority')}>
<Option value={1}>ERROR(高)</Option><Option value={2}>WARNING</Option><Option value={3}>INFO</Option>
</Select>*/}
<Form.Item name="priorityLevel" label="优先级">
<Form.Item name="priorityLevel" label={t('workOrder.priority')}>
<Select options={errorLevelOptions} allowClear />
</Form.Item>
</FormItem>
<FormItem name="taskDescription" label="任务描述" rules={[{ required: true, message: '请输入任务描述' }]}>
<FormItem name="taskDescription" label={t('workOrder.taskDescription')} rules={[{ required: true, message: '请输入任务描述' }]}>
<Input.TextArea rows={4} placeholder="请输入任务描述" />
</FormItem>
<FormItem style={{ textAlign: 'right', marginBottom: 0 }}>
<Button onClick={() => setEditModalVisible(false)}>取消</Button>
<Button type="primary" htmlType="submit" style={{ marginLeft: 8 }}>确认修改</Button>
<Button onClick={() => setEditModalVisible(false)}>{t('common.cancel')}</Button>
<Button type="primary" htmlType="submit" style={{ marginLeft: 8 }}>{t('profile.confirmChange')}</Button>
</FormItem>
</Form>
</Modal>
{/* 派单弹窗:选择人员 */}
<Modal
title="工单派单"
title={t('workOrder.dispatchOrder')}
open={dispatchUserModalVisible}
onCancel={() => setDispatchUserModalVisible(false)}
footer={null}
@@ -1641,8 +1646,8 @@ const WorkOrderPage = () => {
</FormItem>
<FormItem
name="assignUserId"
label="选择接收人员"
rules={[{ required: true, message: '请选择派单人员' }]}
label={t('workOrder.selectReceiver')}
rules={[{ required: true, message: t('workOrder.pleaseSelectDispatcher') }]}
>
<Select
placeholder="请选择人员"
@@ -1659,15 +1664,15 @@ const WorkOrderPage = () => {
/>
</FormItem>
<FormItem style={{ textAlign: 'right', marginBottom: 0 }}>
<Button onClick={() => setDispatchUserModalVisible(false)}>取消</Button>
<Button type="primary" htmlType="submit" style={{ marginLeft: 8 }}>确认派单</Button>
<Button onClick={() => setDispatchUserModalVisible(false)}>{t('common.cancel')}</Button>
<Button type="primary" htmlType="submit" style={{ marginLeft: 8 }}>{t('workOrder.confirmDispatch')}</Button>
</FormItem>
</Form>
</Modal>
{/* 执行工单弹窗 */}
<Modal title="执行工单" open={dispatchModalVisible} onCancel={() => setDispatchModalVisible(false)} footer={null} width={550}>
<Modal title={t('workOrder.executeOrder')} open={dispatchModalVisible} onCancel={() => setDispatchModalVisible(false)} footer={null} width={550}>
<Form form={form} layout="vertical" onFinish={submitExecute}>
<FormItem name="orderIds" label="工单ID" hidden><Input /></FormItem>
<FormItem label="选中工单">
@@ -1693,7 +1698,7 @@ const WorkOrderPage = () => {
</FormItem>
<FormItem
name="deviceId"
label="选择执行设备"
label={t('workOrder.selectDevice')}
>
{/*<Select
style={{ width: "80%", marginBottom: 8 }}
@@ -1708,8 +1713,8 @@ const WorkOrderPage = () => {
String(option?.label || "").toLowerCase().includes(input.toLowerCase())
}
options={[
{ label: <span>机器人</span>, options: devices.map(item => ({ value: item.device_sn || item.serialNumber, label: item.deviceAlias || item.deviceName || '未知设备' })) },
{ label: <span>无人机</span>, options: planedevices.map(item => ({ value: item.device_sn, label: item.drone_callsign || item.callsign || '未知设备' })) },
{ label: <span>devices.robot</span>, options: devices.map(item => ({ value: item.device_sn || item.serialNumber, label: item.deviceAlias || item.deviceName || '未知设备' })) },
{ label: <span>constants.uav</span>, options: planedevices.map(item => ({ value: item.device_sn, label: item.drone_callsign || item.callsign || '未知设备' })) },
]}
/>*/}
@@ -1724,7 +1729,7 @@ const WorkOrderPage = () => {
}}
>
{/* 机器人设备分组 */}
<Select.OptGroup label="机器人">
<Select.OptGroup label={t('devices.robot')}>
{deviceAll
.filter(item => item.serialNumber)
.map(device => (
@@ -1738,7 +1743,7 @@ const WorkOrderPage = () => {
</Select.OptGroup>
{/* 无人机分组 */}
<Select.OptGroup label="无人机">
<Select.OptGroup label={t('constants.uav')}>
{deviceAll
.filter(item => item.device_sn)
.map(device => (
@@ -1755,7 +1760,7 @@ const WorkOrderPage = () => {
{/*<FormItem
name="startTime"
label="执行开始时间"
label={t('workOrder.execStartTime')}
>
<DatePicker
showTime
@@ -1765,33 +1770,33 @@ const WorkOrderPage = () => {
/>
</FormItem>*/}
<FormItem style={{ textAlign: 'right', marginBottom: 0 }}>
<Button onClick={() => setDispatchModalVisible(false)}>取消</Button>
<Button type="primary" htmlType="submit" style={{ marginLeft: 8 }}>确认执行</Button>
<Button onClick={() => setDispatchModalVisible(false)}>{t('common.cancel')}</Button>
<Button type="primary" htmlType="submit" style={{ marginLeft: 8 }}>{t('workOrder.confirmExecute')}</Button>
</FormItem>
</Form>
</Modal>
{/* 完成工单弹窗 */}
<Modal title="完成工单" open={completeModalVisible} onCancel={() => setCompleteModalVisible(false)} footer={null} width={500}>
<Modal title={t('workOrder.completeOrder')} open={completeModalVisible} onCancel={() => setCompleteModalVisible(false)} footer={null} width={500}>
<Form form={completeForm} layout="vertical" onFinish={submitComplete}>
<Form.Item name="id" hidden><Input /></Form.Item>
<Form.Item name="orderNo" label="工单编号"><Input disabled /></Form.Item>
<Form.Item name="handleResult" label="处理结果" rules={[{ required: true, message: '请输入处理结果' }]}>
<Form.Item name="handleResult" label={t('alerts.handleResult')} rules={[{ required: true, message: '请输入处理结果' }]}>
<Select placeholder="请选择处理结果">
<Option value="已处理完成">已处理完成</Option><Option value="已修复">已修复</Option><Option value="无需处理">无需处理</Option><Option value="问题已闭环">问题已闭环</Option>
</Select>
</Form.Item>
<Form.Item name="handleRemark" label="处理备注"><Input.TextArea rows={4} placeholder="请填写处理详情、备注信息" /></Form.Item>
<Form.Item name="handleRemark" label={t('alerts.handleRemark')}><Input.TextArea rows={4} placeholder="请填写处理详情、备注信息" /></Form.Item>
<Form.Item style={{ textAlign: 'right', marginBottom: 0 }}>
<Button onClick={() => setCompleteModalVisible(false)}>取消</Button>
<Button type="primary" htmlType="submit" style={{ marginLeft: 8 }}>确认完成</Button>
<Button onClick={() => setCompleteModalVisible(false)}>{t('common.cancel')}</Button>
<Button type="primary" htmlType="submit" style={{ marginLeft: 8 }}>{t('workOrder.confirmComplete')}</Button>
</Form.Item>
</Form>
</Modal>
{/* 近7天全部告警弹窗 */}
<Modal
title="近7天全部关联告警"
title={t('workOrder.relatedAlerts7d')}
open={alarmAllModalVisible}
onCancel={() => setAlarmAllModalVisible(false)}
width={900}
@@ -1816,33 +1821,33 @@ const WorkOrderPage = () => {
ellipsis: true
},
{
title: '告警级别',
title: t('alerts.alertLevel'),
dataIndex: 'alarmLevel',
width: 90,
render: (level) => {
const color = levelColorMap[level] || '#86909C';
const label = priorityConfig[level]?.label || '未知';
const label = priorityConfig[level]?.label || t('constants.locationUnknown');
return <Tag color={color}>{label}</Tag>;
}
},
{
title: '告警时间',
title: t('alerts.alertTime'),
dataIndex: 'alarmTime',
width: 170,
render: (t) => dayjs(t).format('YYYY-MM-DD HH:mm:ss')
},
{
title: '设备名称',
title: t('devices.deviceName'),
dataIndex: 'deviceName',
ellipsis: true,
width: 200
},
{
title: '处理状态',
title: t('alerts.handleStatus'),
dataIndex: 'handleStatus',
width: 100,
render: (s) => {
const map = { 1: '待处理', 2: '已关闭', };
const map = { 1: t('constants.pending'), 2: t('constants.closed'), };
let tagColor = 'default';
if (s === 1) tagColor = 'orange';
if (s === 2) tagColor = 'green';

View File

@@ -1,6 +1,9 @@
import React, { lazy, Suspense } from 'react';
import { createBrowserRouter } from 'react-router-dom';
import type { RouteObject } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
// 🔥 所有组件全部改为懒加载
const VideoMonitorPage = lazy(() => import('../pages/VideoMonitorPage'));
@@ -61,27 +64,27 @@ export const routeMeta: Record<string, {
roles: string[];
requiresAuth: boolean;
}> = {
'/': { title: '总览首页', roles: ['admin', 'operator', 'viewer'], requiresAuth: true },
'/overview': { title: '总览首页', roles: ['admin', 'operator', 'viewer'], requiresAuth: true },
'/devices': { title: '设备管理', roles: ['admin', 'operator'], requiresAuth: true },
'/video': { title: '在线视频', roles: ['admin', 'operator', 'viewer'], requiresAuth: true },
'/monitor': { title: '实时监控', roles: ['admin', 'operator', 'viewer'], requiresAuth: true },
'/alerts': { title: '告警中心', roles: ['admin', 'operator'], requiresAuth: true },
'/AIanalysis': { title: 'AI诊断分析', roles: ['admin', 'operator'], requiresAuth: true },
'/clean': { title: '清洗优化', roles: ['admin', 'operator'], requiresAuth: true },
'/task': { title: '工单任务', roles: ['admin', 'operator'], requiresAuth: true },
'/table': { title: '报表中心', roles: ['admin', 'operator'], requiresAuth: true },
'/systemSetting': { title: '系统设置', roles: ['admin'], requiresAuth: true },
'/systemSetting/log': { title: '日志管理', roles: ['admin'], requiresAuth: true },
'/systemSetting/videoManage': { title: '视频管理', roles: ['admin'], requiresAuth: true },
'/downloads': { title: '视频下载', roles: ['admin', 'operator'], requiresAuth: true },
'/manage/users': { title: '用户管理', roles: ['admin'], requiresAuth: true },
'/manage/roles': { title: '角色管理', roles: ['admin'], requiresAuth: true },
'/manage/menuList': { title: '菜单管理', roles: ['admin'], requiresAuth: true },
'/organization': { title: '组织管理', roles: ['admin'], requiresAuth: true },
'/station': { title: '场站管理', roles: ['admin'], requiresAuth: true },
'/RealtimeMonitor': { title: '实时监控', roles: ['admin', 'operator', 'viewer'], requiresAuth: true },
'/profile': { title: '个人中心', roles: ['admin', 'operator', 'viewer'], requiresAuth: true },
'/': { title: 'router.overview', roles: ['admin', 'operator', 'viewer'], requiresAuth: true },
'/overview': { title: 'router.overview', roles: ['admin', 'operator', 'viewer'], requiresAuth: true },
'/devices': { title: 'router.devices', roles: ['admin', 'operator'], requiresAuth: true },
'/video': { title: 'router.video', roles: ['admin', 'operator', 'viewer'], requiresAuth: true },
'/monitor': { title: 'router.monitor', roles: ['admin', 'operator', 'viewer'], requiresAuth: true },
'/alerts': { title: 'router.alerts', roles: ['admin', 'operator'], requiresAuth: true },
'/AIanalysis': { title: 'router.aiAnalysis', roles: ['admin', 'operator'], requiresAuth: true },
'/clean': { title: 'router.clean', roles: ['admin', 'operator'], requiresAuth: true },
'/task': { title: 'router.task', roles: ['admin', 'operator'], requiresAuth: true },
'/table': { title: 'router.table', roles: ['admin', 'operator'], requiresAuth: true },
'/systemSetting': { title: 'router.systemSetting', roles: ['admin'], requiresAuth: true },
'/systemSetting/log': { title: 'router.log', roles: ['admin'], requiresAuth: true },
'/systemSetting/videoManage': { title: 'router.videoManage', roles: ['admin'], requiresAuth: true },
'/downloads': { title: 'router.downloads', roles: ['admin', 'operator'], requiresAuth: true },
'/manage/users': { title: 'router.users', roles: ['admin'], requiresAuth: true },
'/manage/roles': { title: 'router.roles', roles: ['admin'], requiresAuth: true },
'/manage/menuList': { title: 'router.menuList', roles: ['admin'], requiresAuth: true },
'/organization': { title: 'router.organization', roles: ['admin'], requiresAuth: true },
'/station': { title: 'router.station', roles: ['admin'], requiresAuth: true },
'/RealtimeMonitor': { title: 'router.monitor', roles: ['admin', 'operator', 'viewer'], requiresAuth: true },
'/profile': { title: 'router.profile', roles: ['admin', 'operator', 'viewer'], requiresAuth: true },
};
// ====================== 全部懒加载的路由配置 ======================

542
tmpp7zw7soq.tsx Normal file
View File

@@ -0,0 +1,542 @@
import React, { useState, useRef, useEffect } from 'react';
import {
Table,
Input,
Button,
Form,
Select,
Space,
Tag,
Modal,
Card,
Typography,
InputNumber,
Row,
Col,
App,
Divider,
} from 'antd';
import {
SearchOutlined,
ReloadOutlined,
PlusOutlined,
EditOutlined,
DeleteOutlined,
EyeOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';
import {
getErrorListApi,
addErrorApi,
updateErrorApi,
deleteErrorApi,
getErrorDetailApi,
getErrorListAllApi
} from '@/api/systemSetting';
import utils from '@/lib/utils';
import { errorLevelOptions, errorSourceOptions, CompareEnumOptions } from '@/lib/utils';
import { useSelector } from 'react-redux';
import { RootState } from '@/src/store';
import { useTranslation } from 'react-i18next';
const { TextArea } = Input;
const { Text, Title } = Typography;
const { Option } = Select;
const AntdCard = Card as any;
export default function ErrorManagement() {
const { t } = useTranslation();
const { userInfo } = useSelector((state: RootState) => state.user);
const { stationId, currentStation } = useSelector((state: RootState) => state.station);
const { modal } = App.useApp();
const [form] = Form.useForm();
const [searchForm] = Form.useForm();
const [loading, setLoading] = useState(false);
const [errorList, setErrorList] = useState([]);
const [total, setTotal] = useState(0);
const [params, setParams] = useState({
errorCode: '',
errorName: '',
errorSource: '',
errorLevel: '',
orgId: currentStation?.orgId || '',
siteId: stationId || '',
});
const [modalVisible, setModalVisible] = useState(false);
const [modalType, setModalType] = useState<'add' | 'edit' | 'view'>('add');
const [currentRecord, setCurrentRecord] = useState<any>(null);
// 监听对比类型,判断是否是区间
const compareTypeWatch = Form.useWatch('compareType', form);
// 获取错误列表
const fetchErrorList = async () => {
setLoading(true);
try {
const requestParams = {
...params,
siteId: stationId || '',
};
const realParams = Object.fromEntries(
Object.entries(requestParams).filter(([_, val]) => val !== '')
);
const res = await getErrorListAllApi(realParams);
if (res.code === 200) {
setErrorList(res.rows || []);
setTotal(res.total || res.rows?.length || 0);
}
} catch (err) {
utils.message.error(t('systemSetting.getErrorListFailed'));
} finally {
setLoading(false);
}
};
useEffect(() => {
setParams(prev => ({
...prev,
siteId: stationId || ''
}));
}, [stationId]);
useEffect(() => {
fetchErrorList();
}, [params]);
// 打开弹窗
// 打开弹窗
const openModal = (type: 'add' | 'edit' | 'view', record?: any) => {
setModalType(type);
setCurrentRecord(record);
setModalVisible(true);
form.resetFields();
if (type === 'edit' || type === 'view') {
const fillData = { ...record };
// 区间规则拆分 compareValues 回填 min/max
const targetEnum = CompareEnumOptions.find(e => e.value === record.compareType);
if (targetEnum?.range && record.compareValues) {
const valArr = record.compareValues.split(',');
fillData.rangeMin = valArr[0];
fillData.rangeMax = valArr[1];
}
form.setFieldsValue(fillData);
}
};
// 保存错误(字段映射适配后端实体)
const handleSave = async (values: any) => {
console.log('提交表单数据', values);
//return;
// 组装后端需要的实体字段
const submitData = {
id: modalType == 'edit' ? currentRecord.id : undefined,
// 原有基础字段
errorCode: values.errorCode,
errorName: values.errorName,
errorSource: values.errorSource,
errorLevel: values.errorLevel,
errorDescription: values.errorDescription,
suggestion: values.suggestion,
// 新增校验规则字段
field: values.field,
compareType: values.compareType,
orgId: currentStation?.orgId || '',
siteId: stationId || '',
// 区间/普通值统一拼接逗号字符串传给 compareValues
compareValues: values.rangeMin && values.rangeMax
? `${values.rangeMin},${values.rangeMax}`
: values.compareValues || '',
};
console.log('提交表单数据', submitData);
try {
// 真实接口
const res = await addErrorApi(submitData);
if (res.code === 200) {
utils.message.success(modalType == 'edit' ? t('common.editSuccess') : t('common.addSuccess'));
setModalVisible(false);
fetchErrorList();
} else {
utils.message.error(res.msg || t('systemSetting.operationFailed'));
}
} catch (err) {
utils.message.error(t('systemSetting.operationFailed'));
}
};
// 删除错误
const handleDelete = (record: any) => {
modal.confirm({
title: t('common.confirmDelete'),
content: t('systemSetting.confirmDeleteError', { name: record.errorName }),
icon: <ExclamationCircleOutlined />,
okText: t('common.ok'),
cancelText: t('common.cancel'),
onOk: async () => {
try {
// 真实接口
const data = {
ids: [record.id]
}
console.log('删除请求参数', data);
const res = await deleteErrorApi(data.ids);
if (res.code == 200) {
utils.message.success(t('common.deleteSuccess'));
fetchErrorList();
} else {
utils.message.error(res.msg || t('common.deleteFail'));
}
} catch (err) {
utils.message.error(t('common.deleteFail'));
}
},
});
};
// 搜索
const handleSearch = () => {
searchForm.validateFields().then(values => {
setParams({
...params,
...values,
});
});
};
// 重置
const handleReset = () => {
searchForm.resetFields();
setParams({
errorCode: '',
errorName: '',
errorSource: '',
errorLevel: '',
});
};
// 标签颜色方法(原有不变)
const getTypeColor = (type: string) => {
const colorMap: Record<string, string> = {
ROBOT: 'blue',
DEVICE: 'green',
SYSTEM: 'purple',
COMMUNICATION: 'orange',
SENSOR: 'cyan',
OTHER: 'default',
};
return colorMap[type] || 'default';
};
const getSeverityColor = (severity: string) => {
const colorMap: Record<string, string> = {
MINOR: 'default',
NORMAL: 'blue',
SERIOUS: 'orange',
CRITICAL: 'red',
};
return colorMap[severity] || 'default';
};
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
PENDING: 'orange',
PROCESSING: 'blue',
RESOLVED: 'green',
IGNORED: 'default',
};
return colorMap[status] || 'default';
};
// 表格列(完全不变)
const columns = [
{
title: t('systemSetting.errorCode'),
dataIndex: 'errorCode',
key: 'errorCode',
width: 150,
fixed: 'left' as const,
},
{
title: t('systemSetting.errorName'),
dataIndex: 'errorName',
key: 'errorName',
width: 200,
},
{
title: t('systemSetting.errorSourceLabel'),
dataIndex: 'errorSource',
key: 'errorSource',
width: 120,
render: (text: string) => {
const typeObj = errorSourceOptions.find(t => t.value === text);
return <Tag color={getTypeColor(text)}>{typeObj?.label || text}</Tag>;
},
},
{
title: t('systemSetting.errorLevelLabel'),
dataIndex: 'errorLevel',
key: 'errorLevel',
width: 100,
render: (text: string) => {
const severityObj = errorLevelOptions.find(t => t.value === text);
return <Tag color={getSeverityColor(text)}>{severityObj?.label || text}</Tag>;
},
},
{
title: t('systemSetting.checkField'),
dataIndex: 'field',
key: 'field',
width: 130,
},
{
title: t('systemSetting.compareRule'),
dataIndex: 'compareType',
key: 'compareType',
width: 140,
render: (val) => {
const item = CompareEnumOptions.find(o => o.value === val);
return item?.label || val;
},
},
{
title: t('systemSetting.compareValueCol'),
dataIndex: 'compareValues',
key: 'compareValues',
width: 140,
},
{
title: t('common.description'),
dataIndex: 'errorDescription',
key: 'errorDescription',
ellipsis: true,
},
{
title: t('aiAnalysis.suggestion'),
dataIndex: 'suggestion',
key: 'suggestion',
width: 180,
},
{
title: t('roles.actions'),
key: 'action',
width: 300,
fixed: 'right' as const,
render: (_: any, record: any) => (
<Space>
<Button
type="link"
size="small"
icon={<EyeOutlined />}
onClick={() => openModal('view', record)}
>{t('common.view')}</Button>
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => openModal('edit', record)}
>{t('common.edit')}</Button>
<Button
type="link"
size="small"
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(record)}
>{t('common.delete')}</Button>
</Space>
),
},
];
return (
<div style={{ padding: '0px 0px', background: '#f5f7fa', minHeight: 'calc(100vh - 120px)' }}>
<AntdCard style={{ borderRadius: 8 }}>
{/* 搜索区域完全不变 */}
<Form form={searchForm} layout="inline" style={{ marginBottom: 0 }}>
<Form.Item name="errorCode" label={t("systemSetting.errorCode")}>
<Input placeholder={t("systemSetting.pleaseInputErrorCode")} style={{ width: 150 }} allowClear />
</Form.Item>
<Form.Item name="errorName" label={t("systemSetting.errorName")}>
<Input placeholder={t("systemSetting.pleaseInputErrorName")} style={{ width: 150 }} allowClear />
</Form.Item>
<Form.Item name="errorSource" label={t("systemSetting.errorSourceLabel")}>
<Select placeholder={t("systemSetting.pleaseSelectSource")} style={{ width: 120 }} allowClear >
{errorSourceOptions.map(option => (
<Option key={option.value} value={option.value}>{option.label}</Option>
))}
</Select>
</Form.Item>
<Form.Item name="errorLevel" label={t("systemSetting.errorLevelLabel")}>
<Select placeholder={t("systemSetting.pleaseSelectLevel")} style={{ width: 120 }} allowClear >
{errorLevelOptions.map(option => (
<Option key={option.value} value={option.value}>{option.label}</Option>
))}
</Select>
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>{t('common.search2')}</Button>
<Button icon={<ReloadOutlined />} onClick={handleReset}>{t('common.reset')}</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={() => openModal('add')}>{t('common.addNew')}</Button>
</Space>
</Form.Item>
</Form>
<Table
rowKey="id"
columns={columns}
dataSource={errorList}
loading={loading}
scroll={{ x: 1600, y: 1200 }}
pagination={{
current: params.pageNum,
pageSize: params.pageSize,
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${t('common.total')} ${total} ${t('common.items')}`,
onChange: (page, pageSize) => {
setParams({ ...params, pageNum: page, pageSize });
},
}}
/>
</AntdCard>
{/* 新增/编辑/查看弹窗(核心优化区域) */}
<Modal
title={
modalType === 'add'
? t('systemSetting.addErrorRule')
: modalType === 'edit'
? t('systemSetting.editErrorRule')
: t('systemSetting.viewErrorRuleDetail')
}
open={modalVisible}
onCancel={() => {
setModalVisible(false);
form.resetFields();
}}
width={780}
footer={
modalType === 'view'
? [
<Button key="close" onClick={() => setModalVisible(false)}>{t('common.close')}</Button>
]
: [
<div style={{ margin: '8px 0 24px 0' }}></div>,
<Button key="cancel" onClick={() => {
setModalVisible(false);
form.resetFields();
}}>{t('common.cancel')}</Button>,
<Button key="submit" type="primary" onClick={() => form.submit()}>{t('common.save')}</Button>
]
}
>
<Divider style={{ margin: '8px 0 16px 0' }} />
<Form
form={form}
layout="vertical"
onFinish={handleSave}
disabled={modalType === 'view'}
>
{/* 第一行:基础编码名称 */}
<Row gutter={16}>
<Col span={12}>
<Form.Item label={t("systemSetting.errorCode")} name="errorCode" rules={[{ required: true, message: t("systemSetting.pleaseInputErrorCode") }]}>
<Input placeholder={t("systemSetting.errorCodePlaceholder")} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label={t("systemSetting.errorName")} name="errorName" rules={[{ required: true, message: t("systemSetting.pleaseInputErrorName") }]}>
<Input placeholder={t("systemSetting.pleaseInputErrorName")} />
</Form.Item>
</Col>
</Row>
{/* 第二行:来源、等级、校验字段 */}
<Row gutter={16}>
<Col span={8}>
<Form.Item label={t("systemSetting.errorSourceLabel")} name="errorSource" rules={[{ required: true, message: t("systemSetting.pleaseSelectErrorSource") }]}>
<Select placeholder={t('common.pleaseSelect')}>
{errorSourceOptions.map(option => (
<Option key={option.value} value={option.value}>{option.label}</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label={t("systemSetting.errorLevelLabel")} name="errorLevel" rules={[{ required: true, message: t("systemSetting.pleaseSelectErrorLevel") }]}>
<Select placeholder={t('common.pleaseSelect')}>
{errorLevelOptions.map(option => (
<Option key={option.value} value={option.value}>{option.label}</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label={t("systemSetting.checkField")} name="field" rules={[{ required: true, message: t("systemSetting.pleaseInputCheckField") }]}>
<Input placeholder={t("systemSetting.checkFieldPlaceholder")} />
</Form.Item>
</Col>
</Row>
{/* 第三行:对比类型 + 对比值(自动切换单输入/区间双输入) */}
<Row gutter={16}>
<Col span={8}>
<Form.Item label={t("systemSetting.compareRule")} name="compareType" rules={[{ required: true, message: t("systemSetting.pleaseSelectCompareRule") }]}>
<Select placeholder={t("systemSetting.pleaseSelectCompareRule")}>
{CompareEnumOptions.map(item => (
<Option key={item.value} value={item.value}>{item.label} ({item.symbol})</Option>
))}
</Select>
</Form.Item>
</Col>
{/* 区间类型:BETWEEN / NOT_BETWEEN 显示两个输入框 */}
{CompareEnumOptions.find(o => o.value === compareTypeWatch)?.range ? (
<>
<Col span={8}>
<Form.Item label={t("systemSetting.rangeMin")} name="rangeMin" rules={[{ required: true, message: t("systemSetting.pleaseInputMinValue") }]}>
<InputNumber style={{ width: '100%' }} placeholder={t("systemSetting.minValuePlaceholder")} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label={t("systemSetting.rangeMax")} name="rangeMax" rules={[{ required: true, message: t("systemSetting.pleaseInputMaxValue") }]}>
<InputNumber style={{ width: '100%' }} placeholder={t("systemSetting.maxValuePlaceholder")} />
</Form.Item>
</Col>
</>
) : (
<Col span={16}>
<Form.Item label={t("systemSetting.compareValueLabel")} name="compareValues" rules={[{ required: true, message: t("systemSetting.pleaseInputCompareValue") }]}>
<Input placeholder={t("systemSetting.compareValuePlaceholder")} />
</Form.Item>
</Col>
)}
</Row>
{/* 错误描述 */}
<Form.Item label={t("systemSetting.errorDesc")} name="errorDescription">
<TextArea rows={3} placeholder={t("systemSetting.errorDescPlaceholder")} showCount maxLength={500} />
</Form.Item>
{/* 处理建议 */}
<Form.Item label={t('aiAnalysis.suggestion')} name="suggestion">
<TextArea rows={3} placeholder={t("systemSetting.suggestionPlaceholder")} showCount maxLength={500} />
</Form.Item>
</Form>
</Modal>
</div>
);
}

548
tmpv4ob8vxm.tsx Normal file
View File

@@ -0,0 +1,548 @@
import React, { useState, useRef, useEffect } from 'react';
import {
Table,
Input,
Button,
Form,
Select,
Space,
Tag,
Modal,
Card,
Typography,
InputNumber,
Row,
Col,
App,
Divider,
} from 'antd';
import {
SearchOutlined,
ReloadOutlined,
PlusOutlined,
EditOutlined,
DeleteOutlined,
EyeOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';
import {
getErrorListApi,
addErrorApi,
updateErrorApi,
deleteErrorApi,
getErrorDetailApi,
getErrorListAllApi
} from '@/api/systemSetting';
import utils from '@/lib/utils';
import { errorLevelOptions, errorSourceOptions, CompareEnumOptions } from '@/lib/utils';
import { useSelector } from 'react-redux';
import { RootState } from '@/src/store';
import { useTranslation } from 'react-i18next';
const { TextArea } = Input;
const { Text, Title } = Typography;
const { Option } = Select;
const AntdCard = Card as any;
export default function ErrorManagement() {
const { t } = useTranslation();
const { userInfo } = useSelector((state: RootState) => state.user);
const { stationId, currentStation } = useSelector((state: RootState) => state.station);
const { modal } = App.useApp();
const [form] = Form.useForm();
const [searchForm] = Form.useForm();
const [loading, setLoading] = useState(false);
const [errorList, setErrorList] = useState([]);
const [total, setTotal] = useState(0);
const [params, setParams] = useState({
errorCode: '',
errorName: '',
errorSource: '',
errorLevel: '',
orgId: currentStation?.orgId || '',
siteId: stationId || '',
});
const [modalVisible, setModalVisible] = useState(false);
const [modalType, setModalType] = useState<'add' | 'edit' | 'view'>('add');
const [currentRecord, setCurrentRecord] = useState<any>(null);
// 监听对比类型,判断是否是区间
const compareTypeWatch = Form.useWatch('compareType', form);
// 获取错误列表
const fetchErrorList = async () => {
setLoading(true);
try {
const requestParams = {
...params,
siteId: stationId || '',
};
const realParams = Object.fromEntries(
Object.entries(requestParams).filter(([_, val]) => val !== '')
);
const res = await getErrorListAllApi(realParams);
if (res.code === 200) {
setErrorList(res.rows || []);
setTotal(res.total || res.rows?.length || 0);
}
} catch (err) {
utils.message.error(t('systemSetting.getErrorListFailed'));
} finally {
setLoading(false);
}
};
useEffect(() => {
setParams(prev => ({
...prev,
siteId: stationId || ''
}));
}, [stationId]);
useEffect(() => {
fetchErrorList();
}, [params]);
// 打开弹窗
// 打开弹窗
const openModal = (type: 'add' | 'edit' | 'view', record?: any) => {
setModalType(type);
setCurrentRecord(record);
setModalVisible(true);
form.resetFields();
if (type === 'edit' || type === 'view') {
const fillData = { ...record };
// 区间规则拆分 compareValues 回填 min/max
const targetEnum = CompareEnumOptions.find(e => e.value === record.compareType);
if (targetEnum?.range && record.compareValues) {
const valArr = record.compareValues.split(',');
fillData.rangeMin = valArr[0];
fillData.rangeMax = valArr[1];
}
form.setFieldsValue(fillData);
}
};
// 保存错误(字段映射适配后端实体)
const handleSave = async (values: any) => {
console.log('提交表单数据', values);
//return;
// 组装后端需要的实体字段
const submitData = {
id: modalType == 'edit' ? currentRecord.id : undefined,
// 原有基础字段
errorCode: values.errorCode,
errorName: values.errorName,
errorSource: values.errorSource,
errorLevel: values.errorLevel,
errorDescription: values.errorDescription,
suggestion: values.suggestion,
// 新增校验规则字段
field: values.field,
compareType: values.compareType,
orgId: currentStation?.orgId || '',
siteId: stationId || '',
// 区间/普通值统一拼接逗号字符串传给 compareValues
compareValues: values.rangeMin && values.rangeMax
? `${values.rangeMin},${values.rangeMax}`
: values.compareValues || '',
};
console.log('提交表单数据', submitData);
try {
// 真实接口
const res = await addErrorApi(submitData);
if (res.code === 200) {
utils.message.success(modalType == 'edit' ? t('common.editSuccess') : t('common.addSuccess'));
setModalVisible(false);
fetchErrorList();
} else {
utils.message.error(res.msg || t('systemSetting.operationFailed'));
}
} catch (err) {
utils.message.error('操作失败');
}
};
// 删除错误
const handleDelete = (record: any) => {
modal.confirm({
title: t('common.confirmDelete'),
content: t('systemSetting.confirmDeleteError', { name: record.errorName }),
icon: <ExclamationCircleOutlined />,
okText: t('common.ok'),
cancelText: t('common.cancel'),
onOk: async () => {
try {
// 真实接口
const data = {
ids: [record.id]
}
console.log('删除请求参数', data);
const res = await deleteErrorApi(data.ids);
if (res.code == 200) {
utils.message.success(t('common.deleteSuccess'));
fetchErrorList();
} else {
utils.message.error(res.msg || t('common.deleteFail'));
}
} catch (err) {
utils.message.error(t('common.deleteFail'));
}
},
});
};
// 搜索
const handleSearch = () => {
searchForm.validateFields().then(values => {
setParams({
...params,
...values,
});
});
};
// 重置
const handleReset = () => {
searchForm.resetFields();
setParams({
errorCode: '',
errorName: '',
errorSource: '',
errorLevel: '',
});
};
// 标签颜色方法(原有不变)
const getTypeColor = (type: string) => {
const colorMap: Record<string, string> = {
ROBOT: 'blue',
DEVICE: 'green',
SYSTEM: 'purple',
COMMUNICATION: 'orange',
SENSOR: 'cyan',
OTHER: 'default',
};
return colorMap[type] || 'default';
};
const getSeverityColor = (severity: string) => {
const colorMap: Record<string, string> = {
MINOR: 'default',
NORMAL: 'blue',
SERIOUS: 'orange',
CRITICAL: 'red',
};
return colorMap[severity] || 'default';
};
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
PENDING: 'orange',
PROCESSING: 'blue',
RESOLVED: 'green',
IGNORED: 'default',
};
return colorMap[status] || 'default';
};
// 表格列(完全不变)
const columns = [
{
title: t('systemSetting.errorCode'),
dataIndex: 'errorCode',
key: 'errorCode',
width: 150,
fixed: 'left' as const,
},
{
title: t('systemSetting.errorName'),
dataIndex: 'errorName',
key: 'errorName',
width: 200,
},
{
title: t('systemSetting.errorSourceLabel'),
dataIndex: 'errorSource',
key: 'errorSource',
width: 120,
render: (text: string) => {
const typeObj = errorSourceOptions.find(t => t.value === text);
return <Tag color={getTypeColor(text)}>{typeObj?.label || text}</Tag>;
},
},
{
title: t('systemSetting.errorLevelLabel'),
dataIndex: 'errorLevel',
key: 'errorLevel',
width: 100,
render: (text: string) => {
const severityObj = errorLevelOptions.find(t => t.value === text);
return <Tag color={getSeverityColor(text)}>{severityObj?.label || text}</Tag>;
},
},
{
title: t('systemSetting.checkField'),
dataIndex: 'field',
key: 'field',
width: 130,
},
{
title: t('systemSetting.compareRule'),
dataIndex: 'compareType',
key: 'compareType',
width: 140,
render: (val) => {
const item = CompareEnumOptions.find(o => o.value === val);
return item?.label || val;
},
},
{
title: t('systemSetting.compareValueCol'),
dataIndex: 'compareValues',
key: 'compareValues',
width: 140,
},
{
title: t('common.description'),
dataIndex: 'errorDescription',
key: 'errorDescription',
ellipsis: true,
},
{
title: t('aiAnalysis.suggestion'),
dataIndex: 'suggestion',
key: 'suggestion',
width: 180,
},
{
title: t('roles.actions'),
key: 'action',
width: 300,
fixed: 'right' as const,
render: (_: any, record: any) => (
<Space>
<Button
type="link"
size="small"
icon={<EyeOutlined />}
onClick={() => openModal('view', record)}
>
查看
</Button>
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => openModal('edit', record)}
>
编辑
</Button>
<Button
type="link"
size="small"
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(record)}
>
删除
</Button>
</Space>
),
},
];
return (
<div style={{ padding: '0px 0px', background: '#f5f7fa', minHeight: 'calc(100vh - 120px)' }}>
<AntdCard style={{ borderRadius: 8 }}>
{/* 搜索区域完全不变 */}
<Form form={searchForm} layout="inline" style={{ marginBottom: 0 }}>
<Form.Item name="errorCode" label={t("systemSetting.errorCode")}>
<Input placeholder={t("systemSetting.pleaseInputErrorCode")} style={{ width: 150 }} allowClear />
</Form.Item>
<Form.Item name="errorName" label={t("systemSetting.errorName")}>
<Input placeholder={t("systemSetting.pleaseInputErrorName")} style={{ width: 150 }} allowClear />
</Form.Item>
<Form.Item name="errorSource" label={t("systemSetting.errorSourceLabel")}>
<Select placeholder={t("systemSetting.pleaseSelectSource")} style={{ width: 120 }} allowClear >
{errorSourceOptions.map(option => (
<Option key={option.value} value={option.value}>{option.label}</Option>
))}
</Select>
</Form.Item>
<Form.Item name="errorLevel" label={t("systemSetting.errorLevelLabel")}>
<Select placeholder={t("systemSetting.pleaseSelectLevel")} style={{ width: 120 }} allowClear >
{errorLevelOptions.map(option => (
<Option key={option.value} value={option.value}>{option.label}</Option>
))}
</Select>
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>{t('common.search2')}</Button>
<Button icon={<ReloadOutlined />} onClick={handleReset}>{t('common.reset')}</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={() => openModal('add')}>{t('common.addNew')}</Button>
</Space>
</Form.Item>
</Form>
<Table
rowKey="id"
columns={columns}
dataSource={errorList}
loading={loading}
scroll={{ x: 1600, y: 1200 }}
pagination={{
current: params.pageNum,
pageSize: params.pageSize,
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${t('common.total')} ${total} ${t('common.items')}`,
onChange: (page, pageSize) => {
setParams({ ...params, pageNum: page, pageSize });
},
}}
/>
</AntdCard>
{/* 新增/编辑/查看弹窗(核心优化区域) */}
<Modal
title={
modalType === 'add'
? t('systemSetting.addErrorRule')
: modalType === 'edit'
? '编辑错误规则'
: t('systemSetting.viewErrorRuleDetail')
}
open={modalVisible}
onCancel={() => {
setModalVisible(false);
form.resetFields();
}}
width={780}
footer={
modalType === 'view'
? [
<Button key="close" onClick={() => setModalVisible(false)}>{t('common.close')}</Button>
]
: [
<div style={{ margin: '8px 0 24px 0' }}></div>,
<Button key="cancel" onClick={() => {
setModalVisible(false);
form.resetFields();
}}>{t('common.cancel')}</Button>,
<Button key="submit" type="primary" onClick={() => form.submit()}>{t('common.save')}</Button>
]
}
>
<Divider style={{ margin: '8px 0 16px 0' }} />
<Form
form={form}
layout="vertical"
onFinish={handleSave}
disabled={modalType === 'view'}
>
{/* 第一行:基础编码名称 */}
<Row gutter={16}>
<Col span={12}>
<Form.Item label={t("systemSetting.errorCode")} name="errorCode" rules={[{ required: true, message: t("systemSetting.pleaseInputErrorCode") }]}>
<Input placeholder={t("systemSetting.errorCodePlaceholder")} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label={t("systemSetting.errorName")} name="errorName" rules={[{ required: true, message: t("systemSetting.pleaseInputErrorName") }]}>
<Input placeholder={t("systemSetting.pleaseInputErrorName")} />
</Form.Item>
</Col>
</Row>
{/* 第二行:来源、等级、校验字段 */}
<Row gutter={16}>
<Col span={8}>
<Form.Item label={t("systemSetting.errorSourceLabel")} name="errorSource" rules={[{ required: true, message: t("systemSetting.pleaseSelectErrorSource") }]}>
<Select placeholder={t('common.pleaseSelect')}>
{errorSourceOptions.map(option => (
<Option key={option.value} value={option.value}>{option.label}</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label={t("systemSetting.errorLevelLabel")} name="errorLevel" rules={[{ required: true, message: t("systemSetting.pleaseSelectErrorLevel") }]}>
<Select placeholder={t('common.pleaseSelect')}>
{errorLevelOptions.map(option => (
<Option key={option.value} value={option.value}>{option.label}</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label={t("systemSetting.checkField")} name="field" rules={[{ required: true, message: t("systemSetting.pleaseInputCheckField") }]}>
<Input placeholder={t("systemSetting.checkFieldPlaceholder")} />
</Form.Item>
</Col>
</Row>
{/* 第三行:对比类型 + 对比值(自动切换单输入/区间双输入) */}
<Row gutter={16}>
<Col span={8}>
<Form.Item label={t("systemSetting.compareRule")} name="compareType" rules={[{ required: true, message: t("systemSetting.pleaseSelectCompareRule") }]}>
<Select placeholder={t("systemSetting.pleaseSelectCompareRule")}>
{CompareEnumOptions.map(item => (
<Option key={item.value} value={item.value}>{item.label} ({item.symbol})</Option>
))}
</Select>
</Form.Item>
</Col>
{/* 区间类型:BETWEEN / NOT_BETWEEN 显示两个输入框 */}
{CompareEnumOptions.find(o => o.value === compareTypeWatch)?.range ? (
<>
<Col span={8}>
<Form.Item label={t("systemSetting.rangeMin")} name="rangeMin" rules={[{ required: true, message: t("systemSetting.pleaseInputMinValue") }]}>
<InputNumber style={{ width: '100%' }} placeholder={t("systemSetting.minValuePlaceholder")} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label={t("systemSetting.rangeMax")} name="rangeMax" rules={[{ required: true, message: t("systemSetting.pleaseInputMaxValue") }]}>
<InputNumber style={{ width: '100%' }} placeholder={t("systemSetting.maxValuePlaceholder")} />
</Form.Item>
</Col>
</>
) : (
<Col span={16}>
<Form.Item label={t("systemSetting.compareValueLabel")} name="compareValues" rules={[{ required: true, message: t("systemSetting.pleaseInputCompareValue") }]}>
<Input placeholder={t("systemSetting.compareValuePlaceholder")} />
</Form.Item>
</Col>
)}
</Row>
{/* 错误描述 */}
<Form.Item label={t("systemSetting.errorDesc")} name="errorDescription">
<TextArea rows={3} placeholder={t("systemSetting.errorDescPlaceholder")} showCount maxLength={500} />
</Form.Item>
{/* 处理建议 */}
<Form.Item label={t('aiAnalysis.suggestion')} name="suggestion">
<TextArea rows={3} placeholder={t("systemSetting.suggestionPlaceholder")} showCount maxLength={500} />
</Form.Item>
</Form>
</Modal>
</div>
);
}