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