From 70930071a49190f414c8d8bc9c9e9795a4096739 Mon Sep 17 00:00:00 2001
From: zhang <zc857179121@qq.com>
Date: 星期一, 23 三月 2026 16:08:27 +0800
Subject: [PATCH] Merge branch 'refs/heads/rcs_master' into jdxaj
---
zy-acs-flow/src/map/insight/code/CodeMain.jsx | 691 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 691 insertions(+), 0 deletions(-)
diff --git a/zy-acs-flow/src/map/insight/code/CodeMain.jsx b/zy-acs-flow/src/map/insight/code/CodeMain.jsx
new file mode 100644
index 0000000..b25ad7d
--- /dev/null
+++ b/zy-acs-flow/src/map/insight/code/CodeMain.jsx
@@ -0,0 +1,691 @@
+import React, { useMemo } from 'react';
+import { useTranslate, useRedirect } from 'react-admin';
+import {
+ Box,
+ Paper,
+ Typography,
+ Stack,
+ Grid,
+ Chip,
+ Button,
+ Divider,
+ Skeleton,
+ useTheme,
+} from '@mui/material';
+import { alpha } from '@mui/material/styles';
+import OpenInNewIcon from '@mui/icons-material/OpenInNew';
+import BoolValueIcon from '../BoolValueIcon';
+import { rotationToNum } from '../../tool';
+import CodeQr from './CodeQr';
+
+const DIR_RULE_ANGLES = [0, 90, 180, 270];
+const MAX_RELATION_ITEMS = 8;
+
+const CodeMain = ({ sprite, codeInfo, loading, code }) => {
+ const translate = useTranslate();
+ const redirect = useRedirect();
+ const theme = useTheme();
+
+ const info = codeInfo || {};
+
+ const displayCode = info?.data ?? info?.code ?? sprite?.data?.no ?? code;
+ const zoneName = info?.zoneName ?? info?.zoneId$ ?? sprite?.data?.zoneName;
+ const statusBool = detectBoolean(info?.statusBool ?? info?.status);
+ const statusLabel = info?.status$ || translate(statusBool ? 'common.enums.statusTrue' : 'common.enums.statusFalse');
+ const cornerBool = detectBoolean(info?.cornerBool ?? info?.corner ?? sprite?.data?.corner);
+ const spinText = getSpinText(info?.spin, translate);
+ const mapMetrics = useMemo(() => {
+ if (!sprite) {
+ return null;
+ }
+ const posX = sprite.position?.x;
+ const posY = sprite.position?.y;
+ const rotation = rotationToNum(sprite.rotation || 0);
+ const scaleX = sprite.scale?.x;
+ const scaleY = sprite.scale?.y;
+ return {
+ posX: isFiniteNumber(posX) ? posX : null,
+ posY: isFiniteNumber(posY) ? posY : null,
+ rotation: isFiniteNumber(rotation) ? rotation : null,
+ scaleX: isFiniteNumber(scaleX) ? scaleX : null,
+ scaleY: isFiniteNumber(scaleY) ? scaleY : null,
+ };
+ }, [sprite]);
+
+ const routeRelations = useMemo(
+ () => extractRelationItems(info, ['routeList', 'routes', 'routeRefs']),
+ [info]
+ );
+ const funcStaRelations = useMemo(
+ () => extractRelationItems(info, ['funcStaList', 'funcStas']),
+ [info]
+ );
+
+ const ruleList = useMemo(() => normalizeDirRule(info?.dirRule), [info?.dirRule]);
+
+ const spatialItems = [
+ {
+ label: translate('page.map.insight.code.fields.mapPosition', { _: '鍦板浘鍧愭爣' }),
+ render: () => (
+ <CoordinatePair
+ x={formatNumber(mapMetrics?.posX, 0)}
+ y={formatNumber(mapMetrics?.posY, 0)}
+ />
+ ),
+ always: mapMetrics?.posX != null || mapMetrics?.posY != null,
+ hideWhenEmpty: true,
+ },
+ {
+ label: translate('page.map.insight.code.fields.rotation', { _: '鏃嬭浆瑙掑害(掳)' }),
+ value: mapMetrics?.rotation != null ? `${formatNumber(mapMetrics.rotation, 1)}掳` : null,
+ hideWhenEmpty: true
+ },
+ {
+ label: translate('table.field.code.corner', { _: '鎷愯' }),
+ render: () => (
+ <BooleanDisplay
+ value={cornerBool}
+ label={
+ cornerBool == null
+ ? translate('common.enums.na')
+ : translate(cornerBool ? 'common.enums.true' : 'common.enums.false')
+ }
+ />
+ ),
+ always: true,
+ },
+ ];
+
+ const handleOpenDetail = () => {
+ if (info?.id) {
+ redirect('edit', 'code', info.id);
+ }
+ };
+
+ return (
+ <Box sx={{ pr: 1, pb: 3 }}>
+ <Grid container spacing={3} alignItems="flex-start">
+ <Grid item xs={12} md={5}>
+ <Stack spacing={3}>
+ <QrPreview
+ translate={translate}
+ value={displayCode}
+ loading={loading}
+ statusLabel={statusLabel}
+ statusBool={statusBool}
+ />
+ <Paper
+ sx={{
+ borderRadius: 4,
+ border: '1px solid',
+ borderColor: 'divider',
+ backgroundColor: theme.palette.background.paper,
+ p: { xs: 2.5, md: 3 },
+ }}
+ >
+ <Stack spacing={2}>
+ {zoneName && (
+ <Chip
+ label={zoneName}
+ variant="outlined"
+ size="small"
+ color="primary"
+ sx={{ alignSelf: 'flex-start' }}
+ />
+ )}
+ <FieldGrid items={spatialItems} loading={loading} />
+ </Stack>
+ </Paper>
+ <Button
+ variant="contained"
+ color="primary"
+ startIcon={<OpenInNewIcon />}
+ onClick={handleOpenDetail}
+ disabled={!info?.id}
+ sx={{ alignSelf: 'flex-start', textTransform: 'none', px: 3 }}
+ >
+ {translate('page.map.insight.code.actions.openDetail', { _: '缂栬緫' })}
+ </Button>
+ </Stack>
+ </Grid>
+ <Grid item xs={12} md={7}>
+ <Paper
+ sx={{
+ borderRadius: 4,
+ border: '1px solid',
+ borderColor: 'divider',
+ backgroundColor: theme.palette.background.paper,
+ p: { xs: 2.5, md: 3 },
+ }}
+ >
+ <Stack spacing={3}>
+ <InfoPanel title={translate('page.map.insight.code.sections.rules', { _: '閫氳瑙勫垯' })}>
+ <RulesSection
+ loading={loading}
+ cornerBool={cornerBool}
+ spinText={spinText}
+ rules={ruleList}
+ translate={translate}
+ />
+ </InfoPanel>
+ <InfoPanel title={translate('page.map.insight.code.relations.routes', { _: '鍏宠仈璺嚎' })}>
+ <RelationsList
+ items={routeRelations}
+ emptyLabel={translate('page.map.insight.code.relations.empty', { _: '鏆傛棤鍏宠仈淇℃伅' })}
+ />
+ </InfoPanel>
+ <InfoPanel title={translate('menu.funcSta', { _: '鍔熻兘绔�' })}>
+ <RelationsChips
+ items={funcStaRelations}
+ emptyLabel={translate('page.map.insight.code.relations.empty', { _: '鏆傛棤鍏宠仈淇℃伅' })}
+ />
+ </InfoPanel>
+ </Stack>
+ </Paper>
+ </Grid>
+ </Grid>
+ </Box>
+ );
+};
+
+const InfoPanel = ({ title, children }) => (
+ <Box
+ sx={{
+ border: '1px solid',
+ borderColor: 'divider',
+ borderRadius: 3,
+ p: { xs: 2, sm: 2.5 },
+ backgroundColor: 'background.paper',
+ }}
+ >
+ <Typography variant="subtitle2" color="text.secondary" sx={{ mb: 1 }}>
+ {title}
+ </Typography>
+ {children}
+ </Box>
+);
+
+const RelationsChips = ({ items, emptyLabel }) => {
+ if (!items?.length) {
+ return (
+ <Typography variant="body2" color="text.disabled">
+ {emptyLabel}
+ </Typography>
+ );
+ }
+ return (
+ <Stack direction="row" spacing={1} flexWrap="wrap">
+ {items.slice(0, MAX_RELATION_ITEMS).map((item, index) => (
+ <Chip
+ key={`${getRelationKey(item)}-${index}`}
+ label={getRelationLabel(item)}
+ variant="outlined"
+ size="small"
+ />
+ ))}
+ {items.length > MAX_RELATION_ITEMS && (
+ <Chip
+ label={`+${items.length - MAX_RELATION_ITEMS}`}
+ size="small"
+ />
+ )}
+ </Stack>
+ );
+};
+
+const RelationsList = ({ items, emptyLabel }) => {
+ if (!items?.length) {
+ return (
+ <Typography variant="body2" color="text.disabled">
+ {emptyLabel}
+ </Typography>
+ );
+ }
+
+ return (
+ <Stack spacing={1}>
+ {items.slice(0, MAX_RELATION_ITEMS).map((item, index) => (
+ <Box
+ key={`${getRelationKey(item)}-${index}`}
+ sx={{
+ px: 1.25,
+ py: 0.75,
+ borderRadius: 999,
+ border: '1px solid',
+ borderColor: 'divider',
+ backgroundColor: 'background.default',
+ }}
+ >
+ <Typography variant="body2" sx={{ lineHeight: 1.2 }}>
+ {getRelationLabel(item)}
+ </Typography>
+ </Box>
+ ))}
+ {items.length > MAX_RELATION_ITEMS && (
+ <Typography variant="caption" color="text.secondary">
+ +{items.length - MAX_RELATION_ITEMS}
+ </Typography>
+ )}
+ </Stack>
+ );
+};
+
+const QrPreview = ({ translate, value, loading, statusLabel, statusBool }) => (
+ <Paper
+ elevation={3}
+ sx={{
+ borderRadius: 4,
+ px: 3,
+ py: 3,
+ width: '100%',
+ maxWidth: 320,
+ alignSelf: { xs: 'center', lg: 'flex-start' },
+ minHeight: 320,
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 1,
+ }}
+ >
+ <Typography variant="caption" color="text.secondary" sx={{ letterSpacing: 1, textTransform: 'uppercase' }}>
+ {translate('page.map.insight.code.summary.qr', { _: '浜岀淮鐮侀瑙�' })}
+ </Typography>
+ <CodeQr value={value} loading={loading} />
+ <Typography variant="caption" color="text.secondary">
+ QR CODE
+ </Typography>
+ {!loading && (
+ <Stack spacing={0.5} alignItems="center" sx={{ mt: 0.5 }}>
+ <Typography variant="caption" color="text.secondary">
+ {translate('common.field.status', { _: '鐘舵��' })}
+ </Typography>
+ <Chip
+ label={statusLabel}
+ color={statusBool ? 'success' : 'default'}
+ variant="outlined"
+ size="small"
+ sx={{ fontWeight: 500 }}
+ />
+ </Stack>
+ )}
+ </Paper>
+);
+
+const FieldGrid = ({ items, loading }) => {
+ const candidates = (loading ? items : items.filter(item => {
+ if (!item) {
+ return false;
+ }
+ if (item.always) {
+ return true;
+ }
+ if (item.hideWhenEmpty) {
+ return hasDisplayValue(item.value);
+ }
+ return true;
+ })).filter(Boolean);
+
+ if (!candidates.length) {
+ return loading ? <Skeleton width="40%" /> : (
+ <Typography variant="body2" color="text.disabled">
+ -
+ </Typography>
+ );
+ }
+
+ return (
+ <Box
+ sx={{
+ display: 'grid',
+ gap: 2,
+ gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))',
+ }}
+ >
+ {candidates.map((item, index) => (
+ <Stack key={`${item.label}-${index}`} spacing={0.5}>
+ <Typography variant="caption" color="text.secondary">
+ {item.label}
+ </Typography>
+ <Box>
+ {item.render
+ ? item.render({ loading })
+ : loading
+ ? <Skeleton width="60%" />
+ : renderDisplayValue(item.value)}
+ </Box>
+ </Stack>
+ ))}
+ </Box>
+ );
+};
+
+const renderDisplayValue = (value) => {
+ if (React.isValidElement(value)) {
+ return value;
+ }
+ if (value === null || value === undefined || value === '') {
+ return (
+ <Typography variant="body2" color="text.disabled">
+ -
+ </Typography>
+ );
+ }
+ return (
+ <Typography variant="subtitle1" sx={{ wordBreak: 'break-word', fontWeight: 600 }}>
+ {value}
+ </Typography>
+ );
+};
+
+const hasDisplayValue = (value) => {
+ if (value === 0 || value === false) {
+ return true;
+ }
+ if (typeof value === 'number') {
+ return !Number.isNaN(value);
+ }
+ if (value instanceof Date) {
+ return true;
+ }
+ return value !== null && value !== undefined && String(value).trim() !== '';
+};
+
+const BooleanDisplay = ({ value, label }) => {
+ if (value == null) {
+ return (
+ <Typography variant="body2" color="text.disabled">
+ {label}
+ </Typography>
+ );
+ }
+ return (
+ <Stack direction="row" alignItems="center" spacing={1}>
+ <BoolValueIcon value={value} />
+ <Typography variant="body2">{label}</Typography>
+ </Stack>
+ );
+};
+
+const CoordinatePair = ({ x, y }) => {
+ if (!hasDisplayValue(x) && !hasDisplayValue(y)) {
+ return renderDisplayValue(null);
+ }
+
+ return (
+ <Stack direction="row" spacing={2.5} alignItems="center" flexWrap="wrap">
+ <Stack spacing={0.25}>
+ <Typography variant="caption" color="text.secondary">
+ X
+ </Typography>
+ <Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
+ {hasDisplayValue(x) ? x : '-'}
+ </Typography>
+ </Stack>
+ <Stack spacing={0.25}>
+ <Typography variant="caption" color="text.secondary">
+ Y
+ </Typography>
+ <Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
+ {hasDisplayValue(y) ? y : '-'}
+ </Typography>
+ </Stack>
+ </Stack>
+ );
+};
+
+const RulesSection = ({ cornerBool, spinText, rules, translate, loading }) => (
+ <Stack spacing={2}>
+ <Box
+ sx={{
+ display: 'grid',
+ gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, minmax(0, 1fr))' },
+ gap: 2,
+ }}
+ >
+ <RuleMetric
+ label={translate('table.field.code.corner')}
+ value={
+ <BooleanDisplay
+ value={cornerBool}
+ label={cornerBool == null
+ ? translate('common.enums.na')
+ : translate(cornerBool ? 'common.enums.true' : 'common.enums.false')}
+ />
+ }
+ />
+ <RuleMetric
+ label={translate('table.field.code.spin')}
+ value={
+ <Chip
+ label={spinText}
+ size="small"
+ color="primary"
+ variant="outlined"
+ />
+ }
+ />
+ </Box>
+ <Divider />
+ {loading ? (
+ <Skeleton variant="rounded" height={188} />
+ ) : (
+ <DirectionRuleCompass rules={rules} translate={translate} />
+ )}
+ </Stack>
+);
+
+const DirectionRuleCompass = ({ rules, translate }) => {
+ const theme = useTheme();
+ const enabledCount = rules.filter(rule => rule.enabled).length;
+ const topRule = rules.find(rule => rule.angle === 0);
+ const rightRule = rules.find(rule => rule.angle === 90);
+ const bottomRule = rules.find(rule => rule.angle === 180);
+ const leftRule = rules.find(rule => rule.angle === 270);
+
+ return (
+ <Stack spacing={0.85} alignItems="center">
+ <DirectionRuleCard rule={topRule} translate={translate} />
+ <Stack direction="row" spacing={0.6} alignItems="center" justifyContent="center">
+ <DirectionRuleCard rule={leftRule} translate={translate} />
+ <Box
+ sx={{
+ width: 52,
+ height: 42,
+ borderRadius: 2.5,
+ border: '1px dashed',
+ borderColor: 'divider',
+ backgroundColor: alpha(theme.palette.primary.main, 0.04),
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ textAlign: 'center',
+ flexShrink: 0,
+ }}
+ >
+ <Typography variant="h5" sx={{ lineHeight: 1, fontWeight: 700, fontSize: '0.98rem' }}>
+ {enabledCount}/{rules.length}
+ </Typography>
+ </Box>
+ <DirectionRuleCard rule={rightRule} translate={translate} />
+ </Stack>
+ <DirectionRuleCard rule={bottomRule} translate={translate} />
+ </Stack>
+ );
+};
+
+const DirectionRuleCard = ({ rule, translate }) => {
+ const theme = useTheme();
+
+ if (!rule) {
+ return null;
+ }
+
+ const statusText = translate(
+ rule.enabled ? 'page.code.dirRule.status.enabled' : 'page.code.dirRule.status.disabled'
+ );
+
+ return (
+ <Box
+ sx={{
+ width: 68,
+ height: 42,
+ borderRadius: 2.5,
+ border: '1px solid',
+ borderColor: rule.enabled ? 'success.light' : 'error.light',
+ backgroundColor: rule.enabled
+ ? alpha(theme.palette.success.main, 0.08)
+ : alpha(theme.palette.error.main, 0.08),
+ px: 0.5,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ textAlign: 'center',
+ flexShrink: 0,
+ }}
+ >
+ <Stack spacing={0.1}>
+ <Typography variant="subtitle2" sx={{ fontWeight: 700, lineHeight: 1, fontSize: '0.88rem' }}>
+ {rule.angle}掳
+ </Typography>
+ <Typography
+ variant="caption"
+ sx={{
+ lineHeight: 1.05,
+ fontSize: '0.64rem',
+ color: rule.enabled ? 'success.dark' : 'error.dark',
+ fontWeight: 600,
+ }}
+ >
+ {statusText}
+ </Typography>
+ </Stack>
+ </Box>
+ );
+};
+
+const RuleMetric = ({ label, value }) => (
+ <Stack spacing={0.5}>
+ <Typography variant="caption" color="text.secondary">
+ {label}
+ </Typography>
+ {value}
+ </Stack>
+);
+
+const normalizeDirRule = (value) => {
+ if (!value) {
+ return DIR_RULE_ANGLES.map(angle => ({ angle, enabled: true }));
+ }
+ let parsed = value;
+ if (typeof parsed === 'string') {
+ try {
+ parsed = JSON.parse(parsed);
+ } catch (error) {
+ parsed = [];
+ }
+ }
+ if (!Array.isArray(parsed)) {
+ parsed = [parsed];
+ }
+ const normalized = DIR_RULE_ANGLES.map(angle => ({ angle, enabled: true }));
+ parsed.forEach(rule => {
+ const angle = typeof rule?.angle === 'number' ? rule.angle : Number(rule?.angle);
+ if (!Number.isFinite(angle)) {
+ return;
+ }
+ const normalizedAngle = ((angle % 360) + 360) % 360;
+ const target = normalized.find(item => item.angle === normalizedAngle);
+ if (target) {
+ const disabled = rule?.enabled === false || rule?.enabled === 'false' || rule?.value === false || rule?.value === 'false';
+ target.enabled = !disabled;
+ }
+ });
+ return normalized;
+};
+
+const getRelationKey = (item) => {
+ if (item && typeof item === 'object') {
+ return item.id || item.uuid || item.code || item.data || item.no || JSON.stringify(item);
+ }
+ return item ?? 'relation';
+};
+
+const getRelationLabel = (item) => {
+ if (item == null) {
+ return '-';
+ }
+ if (typeof item === 'string' || typeof item === 'number') {
+ return item;
+ }
+ if (typeof item === 'object') {
+ if (item.startCode$ && item.endCode$) {
+ return `${item.startCode$} -> ${item.endCode$}`;
+ }
+ if (item.name || item.type || item.state) {
+ return [item.name, item.type, item.state].filter(Boolean).join(' / ');
+ }
+ return item.name || item.code || item.data || item.no || item.uuid || item.locNo || item.staNo || JSON.stringify(item);
+ }
+ return String(item);
+};
+
+const extractRelationItems = (info, keys) => {
+ if (!info) {
+ return [];
+ }
+ const dataset = keys
+ .map(key => info?.[key])
+ .find(value => Array.isArray(value) && value.length);
+ return Array.isArray(dataset) ? dataset : [];
+};
+
+const detectBoolean = (value) => {
+ if (typeof value === 'boolean') {
+ return value;
+ }
+ if (value === 1 || value === '1') {
+ return true;
+ }
+ if (value === 0 || value === '0') {
+ return false;
+ }
+ if (value === 'true') {
+ return true;
+ }
+ if (value === 'false') {
+ return false;
+ }
+ return null;
+};
+
+const getSpinText = (spin, translate) => {
+ switch (spin) {
+ case 1:
+ return translate('page.code.enums.spin.cw');
+ case 2:
+ return translate('page.code.enums.spin.ccw');
+ case 0:
+ return translate('page.code.enums.spin.na');
+ default:
+ return translate('page.code.enums.spin.na');
+ }
+};
+
+const isFiniteNumber = (value) => Number.isFinite(Number(value));
+
+const formatNumber = (value, precision = 2) => {
+ if (value === null || value === undefined || value === '') {
+ return null;
+ }
+ const num = Number(value);
+ if (!Number.isFinite(num)) {
+ return value;
+ }
+ if (precision === null) {
+ return num;
+ }
+ return num.toFixed(precision);
+};
+
+export default CodeMain;
--
Gitblit v1.9.1