Refactor App and EmbeddedActiveEquipmentPage for theme support
- Renamed the main component from `App` to `ToirAdmin` for clarity. - Integrated theme support using `useEmbeddedParentTheme` to dynamically adjust the UI based on the parent theme. - Updated `EmbeddedActiveEquipmentPage` to utilize the new theme and improved styling for better user experience. - Added a new utility hook `useEmbeddedParentTheme` to manage theme changes via postMessage from parent origins. - Enhanced the `vite-env.d.ts` file to include environment variable definitions for parent origins.
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import { createTheme } from '@mui/material/styles';
|
||||
import { useMemo } from 'react';
|
||||
import { Admin, Resource } from 'react-admin';
|
||||
import { authProvider } from './auth/authProvider';
|
||||
import { dataProvider } from './dataProvider';
|
||||
import { useEmbeddedParentTheme } from './embed/useEmbeddedParentTheme';
|
||||
import { EmbeddedActiveEquipmentPage } from './pages/EmbeddedActiveEquipmentPage';
|
||||
import { EquipmentCreate } from './resources/equipment/EquipmentCreate';
|
||||
import { EquipmentEdit } from './resources/equipment/EquipmentEdit';
|
||||
@@ -11,13 +14,18 @@ import { EquipmentStatusChangeEdit } from './resources/equipment-status-change/E
|
||||
import { EquipmentStatusChangeList } from './resources/equipment-status-change/EquipmentStatusChangeList';
|
||||
import { EquipmentStatusChangeShow } from './resources/equipment-status-change/EquipmentStatusChangeShow';
|
||||
|
||||
function App() {
|
||||
if (window.location.pathname === '/embedded/equipment-active') {
|
||||
return <EmbeddedActiveEquipmentPage />;
|
||||
}
|
||||
function ToirAdmin() {
|
||||
const paletteMode = useEmbeddedParentTheme();
|
||||
const theme = useMemo(
|
||||
() =>
|
||||
createTheme({
|
||||
palette: { mode: paletteMode },
|
||||
}),
|
||||
[paletteMode],
|
||||
);
|
||||
|
||||
return (
|
||||
<Admin dataProvider={dataProvider} authProvider={authProvider}>
|
||||
<Admin dataProvider={dataProvider} authProvider={authProvider} theme={theme}>
|
||||
<Resource
|
||||
name="equipment"
|
||||
list={EquipmentList}
|
||||
@@ -36,4 +44,12 @@ function App() {
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
if (window.location.pathname === '/embedded/equipment-active') {
|
||||
return <EmbeddedActiveEquipmentPage />;
|
||||
}
|
||||
|
||||
return <ToirAdmin />;
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
48
client/src/embed/useEmbeddedParentTheme.ts
Normal file
48
client/src/embed/useEmbeddedParentTheme.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export type EmbedPaletteMode = 'light' | 'dark';
|
||||
|
||||
const MESSAGE_TYPE = 'greact-theme' as const;
|
||||
|
||||
function parseThemeFromSearch(): EmbedPaletteMode {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'light';
|
||||
}
|
||||
const t = new URLSearchParams(window.location.search).get('theme');
|
||||
return t === 'dark' ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
/** Comma-separated list of parent origins allowed to send theme via postMessage. If unset, all origins are accepted (theme-only payload). */
|
||||
function getAllowedOrigins(): string[] {
|
||||
const raw = import.meta.env.VITE_TOIR_EMBED_PARENT_ORIGINS;
|
||||
if (typeof raw === 'string' && raw.trim()) {
|
||||
return raw
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function useEmbeddedParentTheme(): EmbedPaletteMode {
|
||||
const [mode, setMode] = useState<EmbedPaletteMode>(parseThemeFromSearch);
|
||||
|
||||
useEffect(() => {
|
||||
const allowed = getAllowedOrigins();
|
||||
const handler = (event: MessageEvent) => {
|
||||
if (allowed.length > 0 && !allowed.includes(event.origin)) {
|
||||
return;
|
||||
}
|
||||
const data = event.data as { type?: string; theme?: string } | null;
|
||||
if (data && typeof data === 'object' && data.type === MESSAGE_TYPE) {
|
||||
if (data.theme === 'dark' || data.theme === 'light') {
|
||||
setMode(data.theme);
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener('message', handler);
|
||||
return () => window.removeEventListener('message', handler);
|
||||
}, []);
|
||||
|
||||
return mode;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Box from '@mui/material/Box';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import Link from '@mui/material/Link';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
@@ -9,9 +11,12 @@ import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEmbeddedParentTheme } from '../embed/useEmbeddedParentTheme';
|
||||
import { env } from '../config/env';
|
||||
import { ensureFreshToken, getAccessToken } from '../auth/keycloak';
|
||||
import { downloadEquipmentAttachmentFile } from '../resources/equipment/attachmentDownload';
|
||||
|
||||
type EquipmentRecord = {
|
||||
id: string;
|
||||
@@ -19,11 +24,15 @@ type EquipmentRecord = {
|
||||
serialNumber: string;
|
||||
dateOfInspection: string | null;
|
||||
commissionedAt: string | null;
|
||||
attachment?: {
|
||||
objectKey?: string;
|
||||
originalFileName?: string | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
function formatDate(value: string | null) {
|
||||
if (!value) {
|
||||
return '-';
|
||||
return '—';
|
||||
}
|
||||
|
||||
const date = new Date(value);
|
||||
@@ -35,6 +44,24 @@ function formatDate(value: string | null) {
|
||||
}
|
||||
|
||||
export function EmbeddedActiveEquipmentPage() {
|
||||
const paletteMode = useEmbeddedParentTheme();
|
||||
const muiTheme = useMemo(
|
||||
() =>
|
||||
createTheme({
|
||||
palette: { mode: paletteMode },
|
||||
components: {
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundImage: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
[paletteMode],
|
||||
);
|
||||
|
||||
const [data, setData] = useState<EquipmentRecord[]>([]);
|
||||
const [total, setTotal] = useState<number | null>(null);
|
||||
const [isPending, setIsPending] = useState(true);
|
||||
@@ -105,52 +132,98 @@ export function EmbeddedActiveEquipmentPage() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box sx={{ minHeight: '100vh', boxSizing: 'border-box', p: { xs: 2, md: 3 }, bgcolor: '#f3f6fa' }}>
|
||||
<Paper elevation={0} sx={{ overflow: 'hidden', borderRadius: 3, border: '1px solid #d7e0ea' }}>
|
||||
<Box sx={{ px: 3, py: 2, borderBottom: '1px solid #d7e0ea', bgcolor: '#fff' }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, color: '#10233a' }}>
|
||||
Оборудование в эксплуатации
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mt: 0.5, color: '#5b7087' }}>
|
||||
Отображаются записи со статусом `Active`
|
||||
{typeof total === 'number' ? `: ${total}` : ''}
|
||||
</Typography>
|
||||
</Box>
|
||||
const pageBg =
|
||||
paletteMode === 'dark' ? 'linear-gradient(180deg, #0d1117 0%, #0a0c10 100%)' : '#f3f6fa';
|
||||
const headerBg = paletteMode === 'dark' ? '#161b22' : '#fff';
|
||||
const headerBorder = paletteMode === 'dark' ? '1px solid #30363d' : '1px solid #d7e0ea';
|
||||
const titleColor = paletteMode === 'dark' ? '#e6edf3' : '#10233a';
|
||||
const subtitleColor = paletteMode === 'dark' ? '#8b949e' : '#5b7087';
|
||||
|
||||
{isPending ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
|
||||
<CircularProgress />
|
||||
return (
|
||||
<ThemeProvider theme={muiTheme}>
|
||||
<CssBaseline />
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
boxSizing: 'border-box',
|
||||
p: { xs: 2, md: 3 },
|
||||
bgcolor: pageBg,
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
borderRadius: 3,
|
||||
border: headerBorder,
|
||||
bgcolor: paletteMode === 'dark' ? '#161b22' : '#fff',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ px: 3, py: 2, borderBottom: headerBorder, bgcolor: headerBg }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, color: titleColor }}>
|
||||
Оборудование в эксплуатации
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mt: 0.5, color: subtitleColor }}>
|
||||
Отображаются записи со статусом 'Active'
|
||||
{typeof total === 'number' ? `: ${total}` : ''}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : error ? (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Alert severity="error">Не удалось загрузить активное оборудование.</Alert>
|
||||
</Box>
|
||||
) : (
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{ fontWeight: 700 }}>Наименование</TableCell>
|
||||
<TableCell sx={{ fontWeight: 700 }}>Заводской номер</TableCell>
|
||||
<TableCell sx={{ fontWeight: 700 }}>Дата изготовления</TableCell>
|
||||
<TableCell sx={{ fontWeight: 700 }}>Дата поверки</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{(data ?? []).map((item) => (
|
||||
<TableRow key={item.id} hover>
|
||||
<TableCell>{item.name}</TableCell>
|
||||
<TableCell>{item.serialNumber}</TableCell>
|
||||
<TableCell>{formatDate(item.dateOfInspection)}</TableCell>
|
||||
<TableCell>{formatDate(item.commissionedAt)}</TableCell>
|
||||
|
||||
{isPending ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : error ? (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Alert severity="error">Не удалось загрузить активное оборудование.</Alert>
|
||||
</Box>
|
||||
) : (
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{ fontWeight: 700 }}>Наименование</TableCell>
|
||||
<TableCell sx={{ fontWeight: 700 }}>Заводской номер</TableCell>
|
||||
<TableCell sx={{ fontWeight: 700 }}>Дата изготовления</TableCell>
|
||||
<TableCell sx={{ fontWeight: 700 }}>Дата поверки</TableCell>
|
||||
<TableCell sx={{ fontWeight: 700 }}>Файл</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</Paper>
|
||||
</Box>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{(data ?? []).map((item) => (
|
||||
<TableRow key={item.id} hover>
|
||||
<TableCell>{item.name}</TableCell>
|
||||
<TableCell>{item.serialNumber}</TableCell>
|
||||
<TableCell>{formatDate(item.commissionedAt)}</TableCell>
|
||||
<TableCell>{formatDate(item.dateOfInspection)}</TableCell>
|
||||
<TableCell>
|
||||
{item?.attachment?.objectKey ? (
|
||||
<Link
|
||||
component="button"
|
||||
type="button"
|
||||
underline="hover"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const label = item.attachment?.originalFileName?.trim() || 'файл';
|
||||
void downloadEquipmentAttachmentFile(item.id, label).catch(() => {});
|
||||
}}
|
||||
sx={{ cursor: 'pointer', verticalAlign: 'inherit' }}
|
||||
>
|
||||
{item.attachment?.originalFileName?.trim() || 'Скачать'}
|
||||
</Link>
|
||||
) : (
|
||||
'—'
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</Paper>
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
8
client/src/vite-env.d.ts
vendored
8
client/src/vite-env.d.ts
vendored
@@ -1 +1,9 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_TOIR_EMBED_PARENT_ORIGINS?: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user