| New file |
| | |
| | | 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; |