中英文切换 初始化
This commit is contained in:
212
.workbuddy/batch_i18n.py
Normal file
212
.workbuddy/batch_i18n.py
Normal 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
161
.workbuddy/fix_i18n.py
Normal 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
63
.workbuddy/fix_jsx_gt.py
Normal 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
82
.workbuddy/fix_jsx_gt2.py
Normal 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")
|
||||
29
.workbuddy/update_request.py
Normal file
29
.workbuddy/update_request.py
Normal 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")
|
||||
95
.workbuddy/update_router.py
Normal file
95
.workbuddy/update_router.py
Normal 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
119
.workbuddy/update_utils.py
Normal 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
78
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
53
src/App.tsx
53
src/App.tsx
@@ -1,8 +1,13 @@
|
||||
import React from 'react';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import { router } from './router';
|
||||
import { App as AntdApp } from 'antd';
|
||||
import { App as AntdApp, ConfigProvider, theme } from 'antd';
|
||||
import zhCN from 'antd/locale/zh_CN';
|
||||
import enUS from 'antd/locale/en_US';
|
||||
import utils from './lib/utils';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from './i18n';
|
||||
|
||||
// 用于静态注入 message/modal/notification
|
||||
const StaticAntd = () => {
|
||||
@@ -13,12 +18,46 @@ const StaticAntd = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
|
||||
const AntdConfigWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const { i18n } = useTranslation();
|
||||
const isEn = i18n.language?.startsWith('en');
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
46
src/components/LanguageSwitcher.tsx
Normal file
46
src/components/LanguageSwitcher.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)' }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: '摄像头管理' },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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
27
src/i18n/index.ts
Normal 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;
|
||||
@@ -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
870
src/locales/en/index.ts
Normal 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
895
src/locales/zh/index.ts
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}` },
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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 /> },
|
||||
];
|
||||
|
||||
|
||||
|
||||
@@ -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: () => (
|
||||
|
||||
@@ -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">更多 ></Button>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
|
||||
<Card title={<span style={{ fontWeight: 800 }}>{t('home.maintenanceProgress')}</span>} extra={<Button type="link" size="small">更多 ></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">更多 ></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">更多 ></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')}>更多 ></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')}>更多 ></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">更多 ></Button>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
|
||||
<Card title={<span style={{ fontWeight: 800 }}>{t('home.deviceOnlineStatus')}</span>} extra={<Button type="link" size="small">更多 ></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">详细 ></Button>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
|
||||
<Card title={<span style={{ fontWeight: 800 }}>{t('home.weatherInfo')}</span>} extra={<Button type="link" size="small">详细 ></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">更多 ></Button>} bordered={false} style={cardStyle} bodyStyle={{ padding: '12px' }}>
|
||||
<Card title={<span style={{ fontWeight: 800 }}>{t('clean.cleaningSuggestion')}</span>} extra={<Button type="link" size="small">更多 ></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%"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: '请确认新密码' }]}
|
||||
>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>*/}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
542
tmpp7zw7soq.tsx
Normal 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
548
tmpv4ob8vxm.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user