ess-main-template 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.env.example +7 -0
  2. package/.prettierrc +8 -0
  3. package/README.md +73 -0
  4. package/commitlint.config.js +6 -0
  5. package/eslint.config.js +32 -0
  6. package/index.html +22 -0
  7. package/mock/auth.mock.ts +39 -0
  8. package/mock/user.mock.ts +83 -0
  9. package/package.json +64 -0
  10. package/public/favicon.svg +1 -0
  11. package/public/icons.svg +24 -0
  12. package/src/App.css +182 -0
  13. package/src/App.tsx +29 -0
  14. package/src/assets/fonts/.gitkeep +0 -0
  15. package/src/assets/hero.png +0 -0
  16. package/src/assets/react.svg +1 -0
  17. package/src/assets/vite.svg +1 -0
  18. package/src/components/AuthButton.tsx +23 -0
  19. package/src/components/LangSwitch.tsx +31 -0
  20. package/src/components/RouteProgress.tsx +25 -0
  21. package/src/components/StationPicker.tsx +24 -0
  22. package/src/components/ThemeSwitch.tsx +18 -0
  23. package/src/constants/bus-events.ts +15 -0
  24. package/src/constants/index.ts +25 -0
  25. package/src/hooks/useAuth.ts +17 -0
  26. package/src/i18n/index.ts +32 -0
  27. package/src/i18n/locales/en/auth.json +15 -0
  28. package/src/i18n/locales/en/common.json +33 -0
  29. package/src/i18n/locales/en/menu.json +33 -0
  30. package/src/i18n/locales/zh/auth.json +15 -0
  31. package/src/i18n/locales/zh/common.json +33 -0
  32. package/src/i18n/locales/zh/menu.json +33 -0
  33. package/src/index.css +109 -0
  34. package/src/layouts/BasicLayout.tsx +73 -0
  35. package/src/layouts/BlankLayout.tsx +7 -0
  36. package/src/layouts/components/RightContent.tsx +43 -0
  37. package/src/main.tsx +13 -0
  38. package/src/pages/403.tsx +23 -0
  39. package/src/pages/404.tsx +23 -0
  40. package/src/pages/Dashboard/index.tsx +15 -0
  41. package/src/pages/Login/index.module.less +23 -0
  42. package/src/pages/Login/index.tsx +58 -0
  43. package/src/router/AppEntry.tsx +93 -0
  44. package/src/router/AuthGuard.tsx +19 -0
  45. package/src/router/componentMap.ts +14 -0
  46. package/src/router/generateRoutes.tsx +55 -0
  47. package/src/router/index.tsx +18 -0
  48. package/src/services/auth.ts +42 -0
  49. package/src/store/appStore.ts +51 -0
  50. package/src/store/userStore.ts +81 -0
  51. package/src/styles/global.less +40 -0
  52. package/src/styles/mixins.less +56 -0
  53. package/src/styles/reset.less +49 -0
  54. package/src/styles/variables.less +50 -0
  55. package/src/types/i18next.d.ts +16 -0
  56. package/src/types/less.d.ts +6 -0
  57. package/src/types/wujie-react.d.ts +37 -0
  58. package/src/utils/auth.ts +28 -0
  59. package/src/utils/request.ts +118 -0
  60. package/src/wujie/SubApp.tsx +72 -0
  61. package/src/wujie/bus.ts +38 -0
  62. package/src/wujie/config.ts +33 -0
  63. package/src/wujie/props.ts +42 -0
  64. package/src/wujie/useBusSync.ts +67 -0
  65. package/tsconfig.app.json +34 -0
  66. package/tsconfig.json +4 -0
  67. package/tsconfig.node.json +24 -0
  68. package/vite.config.ts +26 -0
@@ -0,0 +1,23 @@
1
+ import { Button, Result } from 'antd'
2
+ import { useNavigate } from 'react-router-dom'
3
+ import { useTranslation } from 'react-i18next'
4
+
5
+ const NotFound: React.FC = () => {
6
+ const navigate = useNavigate()
7
+ const { t } = useTranslation('common')
8
+
9
+ return (
10
+ <Result
11
+ status="404"
12
+ title="404"
13
+ subTitle={t('noData')}
14
+ extra={
15
+ <Button type="primary" onClick={() => navigate('/')}>
16
+ {t('backHome')}
17
+ </Button>
18
+ }
19
+ />
20
+ )
21
+ }
22
+
23
+ export default NotFound
@@ -0,0 +1,15 @@
1
+ import { useTranslation } from 'react-i18next'
2
+
3
+ const Dashboard: React.FC = () => {
4
+ const { t } = useTranslation('menu')
5
+
6
+ return (
7
+ <div>
8
+ <h2>{t('dashboard')}</h2>
9
+ <p>Dashboard placeholder</p>
10
+ </div>
11
+ )
12
+ }
13
+
14
+ export default Dashboard
15
+ export const Component = Dashboard
@@ -0,0 +1,23 @@
1
+ .container {
2
+ display: flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ min-height: 100vh;
6
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
7
+ }
8
+
9
+ .card {
10
+ width: 400px;
11
+ padding: 40px;
12
+ background: #fff;
13
+ border-radius: 8px;
14
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
15
+ }
16
+
17
+ .title {
18
+ margin-bottom: 32px;
19
+ font-size: 24px;
20
+ font-weight: 600;
21
+ text-align: center;
22
+ color: #333;
23
+ }
@@ -0,0 +1,58 @@
1
+ import React, { useState } from 'react'
2
+ import { Button, Form, Input, message } from 'antd'
3
+ import { LockOutlined, UserOutlined } from '@ant-design/icons'
4
+ import { useNavigate } from 'react-router-dom'
5
+ import { useTranslation } from 'react-i18next'
6
+ import { useUserStore } from '@/store/userStore'
7
+ import { login } from '@/services/auth'
8
+ import type { LoginParams } from '@/services/auth'
9
+ import styles from './index.module.less'
10
+
11
+ const LoginPage: React.FC = () => {
12
+ const navigate = useNavigate()
13
+ const { t } = useTranslation('auth')
14
+ const setAuth = useUserStore((s) => s.setAuth)
15
+ const [form] = Form.useForm<LoginParams>()
16
+ const [loading, setLoading] = useState(false)
17
+
18
+ const handleSubmit = async (values: LoginParams) => {
19
+ setLoading(true)
20
+ try {
21
+ const { data: res } = await login(values)
22
+ if (res.code === 0) {
23
+ setAuth(res.data.accessToken, res.data.refreshToken)
24
+ message.success(t('loginSuccess'))
25
+ navigate('/', { replace: true })
26
+ } else {
27
+ message.error(res.message || t('loginFailed'))
28
+ }
29
+ } catch {
30
+ message.error(t('loginFailed'))
31
+ } finally {
32
+ setLoading(false)
33
+ }
34
+ }
35
+
36
+ return (
37
+ <div className={styles.container}>
38
+ <div className={styles.card}>
39
+ <h1 className={styles.title}>{t('loginTitle')}</h1>
40
+ <Form form={form} onFinish={handleSubmit} size="large" autoComplete="off">
41
+ <Form.Item name="username" rules={[{ required: true, message: t('usernameRequired') }]}>
42
+ <Input prefix={<UserOutlined />} placeholder={t('usernamePlaceholder')} />
43
+ </Form.Item>
44
+ <Form.Item name="password" rules={[{ required: true, message: t('passwordRequired') }]}>
45
+ <Input.Password prefix={<LockOutlined />} placeholder={t('passwordPlaceholder')} />
46
+ </Form.Item>
47
+ <Form.Item>
48
+ <Button type="primary" htmlType="submit" loading={loading} block>
49
+ {t('loginButton')}
50
+ </Button>
51
+ </Form.Item>
52
+ </Form>
53
+ </div>
54
+ </div>
55
+ )
56
+ }
57
+
58
+ export default LoginPage
@@ -0,0 +1,93 @@
1
+ import { useEffect, useState, useMemo } from 'react'
2
+ import { Navigate, useRoutes } from 'react-router-dom'
3
+ import { Spin } from 'antd'
4
+ import { useUserStore } from '@/store/userStore'
5
+ import { getUserInfo, getUserRoutes, getUserStations } from '@/services/auth'
6
+ import BasicLayout from '@/layouts/BasicLayout'
7
+ import { generateRoutes } from './generateRoutes'
8
+
9
+ const AppEntry: React.FC = () => {
10
+ const token = useUserStore((s) => s.token)
11
+ const dynamicRoutes = useUserStore((s) => s.dynamicRoutes)
12
+ const setUserInfo = useUserStore((s) => s.setUserInfo)
13
+ const setPermissions = useUserStore((s) => s.setPermissions)
14
+ const setDynamicRoutes = useUserStore((s) => s.setDynamicRoutes)
15
+ const setAuthorizedStations = useUserStore((s) => s.setAuthorizedStations)
16
+ const [loading, setLoading] = useState(true)
17
+
18
+ useEffect(() => {
19
+ if (!token) {
20
+ setLoading(false)
21
+ return
22
+ }
23
+
24
+ let cancelled = false
25
+
26
+ async function fetchUserData() {
27
+ try {
28
+ const [infoRes, routesRes, stationsRes] = await Promise.all([
29
+ getUserInfo(),
30
+ getUserRoutes(),
31
+ getUserStations(),
32
+ ])
33
+ if (cancelled) return
34
+
35
+ if (infoRes.data.code === 0) {
36
+ setUserInfo(infoRes.data.data)
37
+ setPermissions(infoRes.data.data.roles)
38
+ }
39
+ if (routesRes.data.code === 0) {
40
+ setDynamicRoutes(routesRes.data.data)
41
+ }
42
+ if (stationsRes.data.code === 0) {
43
+ setAuthorizedStations(stationsRes.data.data)
44
+ }
45
+ } catch {
46
+ // token 无效等异常由 axios 拦截器处理
47
+ } finally {
48
+ if (!cancelled) setLoading(false)
49
+ }
50
+ }
51
+
52
+ fetchUserData()
53
+ return () => {
54
+ cancelled = true
55
+ }
56
+ }, [token, setUserInfo, setPermissions, setDynamicRoutes, setAuthorizedStations])
57
+
58
+ const childRoutes = useMemo(() => {
59
+ if (!dynamicRoutes.length) return []
60
+ const routes = generateRoutes(dynamicRoutes)
61
+ // 默认重定向到第一条路由
62
+ const firstPath = dynamicRoutes[0]?.path || '/dashboard'
63
+ routes.unshift({ index: true, element: <Navigate to={firstPath} replace /> })
64
+ routes.push({ path: '*', element: <Navigate to={firstPath} replace /> })
65
+ return routes
66
+ }, [dynamicRoutes])
67
+
68
+ const element = useRoutes([
69
+ {
70
+ path: '/',
71
+ element: <BasicLayout />,
72
+ children: childRoutes,
73
+ },
74
+ ])
75
+
76
+ if (!token) {
77
+ return <Navigate to="/login" replace />
78
+ }
79
+
80
+ if (loading) {
81
+ return (
82
+ <div
83
+ style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}
84
+ >
85
+ <Spin size="large" />
86
+ </div>
87
+ )
88
+ }
89
+
90
+ return element
91
+ }
92
+
93
+ export default AppEntry
@@ -0,0 +1,19 @@
1
+ import { Navigate, useLocation } from 'react-router-dom'
2
+ import { useUserStore } from '@/store/userStore'
3
+
4
+ interface AuthGuardProps {
5
+ children: React.ReactNode
6
+ }
7
+
8
+ const AuthGuard: React.FC<AuthGuardProps> = ({ children }) => {
9
+ const token = useUserStore((s) => s.token)
10
+ const location = useLocation()
11
+
12
+ if (!token) {
13
+ return <Navigate to="/login" state={{ from: location }} replace />
14
+ }
15
+
16
+ return <>{children}</>
17
+ }
18
+
19
+ export default AuthGuard
@@ -0,0 +1,14 @@
1
+ import { lazy } from 'react'
2
+ import type { ComponentType } from 'react'
3
+
4
+ type LazyComponent = React.LazyExoticComponent<ComponentType>
5
+
6
+ /** 页面组件映射:后端返回的 component 字段 → React.lazy 组件 */
7
+ const componentMap: Record<string, LazyComponent> = {
8
+ 'pages/Dashboard': lazy(() => import('@/pages/Dashboard')),
9
+ // 子应用占位:后续步骤中添加 SubApp 容器组件
10
+ // 'sub-operation': lazy(() => import('@/pages/SubApp/Operation')),
11
+ // 'sub-analysis': lazy(() => import('@/pages/SubApp/Analysis')),
12
+ }
13
+
14
+ export default componentMap
@@ -0,0 +1,55 @@
1
+ /* eslint-disable react-refresh/only-export-components */
2
+ import { Suspense } from 'react'
3
+ import { Navigate } from 'react-router-dom'
4
+ import type { RouteObject } from 'react-router-dom'
5
+ import type { RouteItem } from '@/store/userStore'
6
+ import componentMap from './componentMap'
7
+
8
+ /** 403 占位(后续步骤替换为真实 403 页面) */
9
+ const Forbidden = () => <div style={{ padding: 48, textAlign: 'center' }}>403 - 无权访问</div>
10
+
11
+ /** 将后端路由数据转换为 react-router RouteObject */
12
+ export function generateRoutes(routes: RouteItem[]): RouteObject[] {
13
+ return routes.map((route) => {
14
+ // 取路径最后一段作为相对路径,确保嵌套路由正确匹配
15
+ // /dashboard → dashboard, /operation/devices → devices
16
+ const segments = route.path.split('/').filter(Boolean)
17
+ const relativePath = segments[segments.length - 1] || route.path
18
+
19
+ const routeObj: RouteObject = {
20
+ path: relativePath,
21
+ }
22
+
23
+ // 叶子节点:匹配组件
24
+ if (route.component) {
25
+ const LazyComp = componentMap[route.component]
26
+ if (LazyComp) {
27
+ routeObj.element = (
28
+ <Suspense fallback={<div>Loading...</div>}>
29
+ <LazyComp />
30
+ </Suspense>
31
+ )
32
+ } else {
33
+ // 组件未注册,显示 403
34
+ routeObj.element = <Forbidden />
35
+ }
36
+ }
37
+
38
+ // 有子路由
39
+ if (route.children?.length) {
40
+ routeObj.children = generateRoutes(route.children)
41
+ // 父级路由默认重定向到第一个子路由(使用相对路径)
42
+ if (!route.component && route.children[0]) {
43
+ const firstChildSegments = route.children[0].path.split('/').filter(Boolean)
44
+ const firstChildRelative =
45
+ firstChildSegments[firstChildSegments.length - 1] || route.children[0].path
46
+ routeObj.children.unshift({
47
+ index: true,
48
+ element: <Navigate to={firstChildRelative} replace />,
49
+ })
50
+ }
51
+ }
52
+
53
+ return routeObj
54
+ })
55
+ }
@@ -0,0 +1,18 @@
1
+ import { createBrowserRouter } from 'react-router-dom'
2
+ import BlankLayout from '@/layouts/BlankLayout'
3
+ import LoginPage from '@/pages/Login'
4
+ import AppEntry from './AppEntry'
5
+
6
+ const router = createBrowserRouter([
7
+ {
8
+ path: '/login',
9
+ element: <BlankLayout />,
10
+ children: [{ index: true, element: <LoginPage /> }],
11
+ },
12
+ {
13
+ path: '/*',
14
+ element: <AppEntry />,
15
+ },
16
+ ])
17
+
18
+ export default router
@@ -0,0 +1,42 @@
1
+ import request from '@/utils/request'
2
+
3
+ export interface LoginParams {
4
+ username: string
5
+ password: string
6
+ }
7
+
8
+ export interface LoginResult {
9
+ accessToken: string
10
+ refreshToken: string
11
+ }
12
+
13
+ export interface ApiResponse<T = unknown> {
14
+ code: number
15
+ message: string
16
+ data: T
17
+ }
18
+
19
+ /** 登录 */
20
+ export function login(data: LoginParams) {
21
+ return request.post<ApiResponse<LoginResult>>('/auth/login', data)
22
+ }
23
+
24
+ /** 获取用户信息 */
25
+ export function getUserInfo() {
26
+ return request.get<ApiResponse<import('@/store/userStore').UserInfo>>('/user/info')
27
+ }
28
+
29
+ /** 获取权限路由 */
30
+ export function getUserRoutes() {
31
+ return request.get<ApiResponse<import('@/store/userStore').RouteItem[]>>('/user/routes')
32
+ }
33
+
34
+ /** 获取授权站点 */
35
+ export function getUserStations() {
36
+ return request.get<ApiResponse<import('@/store/userStore').Station[]>>('/user/stations')
37
+ }
38
+
39
+ /** 刷新 Token */
40
+ export function refreshToken() {
41
+ return request.post<ApiResponse<LoginResult>>('/auth/refresh')
42
+ }
@@ -0,0 +1,51 @@
1
+ import { create } from 'zustand'
2
+ import { STORAGE_KEYS } from '@/constants'
3
+
4
+ export type Theme = 'light' | 'dark'
5
+ export type Locale = 'zh' | 'en'
6
+
7
+ interface AppState {
8
+ theme: Theme
9
+ locale: Locale
10
+ sidebarCollapsed: boolean
11
+ currentStation: string | null
12
+ }
13
+
14
+ interface AppActions {
15
+ setTheme: (theme: Theme) => void
16
+ toggleTheme: () => void
17
+ setLocale: (locale: Locale) => void
18
+ setSidebarCollapsed: (collapsed: boolean) => void
19
+ toggleSidebar: () => void
20
+ setCurrentStation: (stationId: string | null) => void
21
+ }
22
+
23
+ export const useAppStore = create<AppState & AppActions>()((set) => ({
24
+ theme: (localStorage.getItem(STORAGE_KEYS.THEME) as Theme) || 'light',
25
+ locale: (localStorage.getItem(STORAGE_KEYS.LOCALE) as Locale) || 'zh',
26
+ sidebarCollapsed: false,
27
+ currentStation: null,
28
+
29
+ setTheme: (theme) => {
30
+ localStorage.setItem(STORAGE_KEYS.THEME, theme)
31
+ set({ theme })
32
+ },
33
+
34
+ toggleTheme: () =>
35
+ set((state) => {
36
+ const next = state.theme === 'light' ? 'dark' : 'light'
37
+ localStorage.setItem(STORAGE_KEYS.THEME, next)
38
+ return { theme: next }
39
+ }),
40
+
41
+ setLocale: (locale) => {
42
+ localStorage.setItem(STORAGE_KEYS.LOCALE, locale)
43
+ set({ locale })
44
+ },
45
+
46
+ setSidebarCollapsed: (collapsed) => set({ sidebarCollapsed: collapsed }),
47
+
48
+ toggleSidebar: () => set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })),
49
+
50
+ setCurrentStation: (stationId) => set({ currentStation: stationId }),
51
+ }))
@@ -0,0 +1,81 @@
1
+ import { create } from 'zustand'
2
+ import { STORAGE_KEYS } from '@/constants'
3
+ import { setTokens, clearTokens } from '@/utils/auth'
4
+
5
+ export interface Station {
6
+ id: string
7
+ name: string
8
+ }
9
+
10
+ export interface UserInfo {
11
+ id: string
12
+ username: string
13
+ realName: string
14
+ avatar?: string
15
+ roles: string[]
16
+ }
17
+
18
+ export interface RouteItem {
19
+ path: string
20
+ name: string
21
+ icon?: string
22
+ component?: string
23
+ children?: RouteItem[]
24
+ meta?: {
25
+ title: string
26
+ hideInMenu?: boolean
27
+ permissions?: string[]
28
+ }
29
+ }
30
+
31
+ interface UserState {
32
+ token: string | null
33
+ refreshToken: string | null
34
+ userInfo: UserInfo | null
35
+ permissions: string[]
36
+ dynamicRoutes: RouteItem[]
37
+ authorizedStations: Station[]
38
+ }
39
+
40
+ interface UserActions {
41
+ setAuth: (token: string, refreshToken: string) => void
42
+ setUserInfo: (info: UserInfo) => void
43
+ setPermissions: (permissions: string[]) => void
44
+ setDynamicRoutes: (routes: RouteItem[]) => void
45
+ setAuthorizedStations: (stations: Station[]) => void
46
+ logout: () => void
47
+ }
48
+
49
+ const initialState: UserState = {
50
+ token: localStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN),
51
+ refreshToken: localStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN),
52
+ userInfo: null,
53
+ permissions: [],
54
+ dynamicRoutes: [],
55
+ authorizedStations: [],
56
+ }
57
+
58
+ export const useUserStore = create<UserState & UserActions>()((set) => ({
59
+ ...initialState,
60
+
61
+ setAuth: (token, refreshToken) => {
62
+ setTokens(token, refreshToken)
63
+ set({ token, refreshToken })
64
+ },
65
+
66
+ setUserInfo: (info) => {
67
+ localStorage.setItem(STORAGE_KEYS.USER_INFO, JSON.stringify(info))
68
+ set({ userInfo: info })
69
+ },
70
+
71
+ setPermissions: (permissions) => set({ permissions }),
72
+
73
+ setDynamicRoutes: (routes) => set({ dynamicRoutes: routes }),
74
+
75
+ setAuthorizedStations: (stations) => set({ authorizedStations: stations }),
76
+
77
+ logout: () => {
78
+ clearTokens()
79
+ set({ ...initialState, token: null, refreshToken: null })
80
+ },
81
+ }))
@@ -0,0 +1,40 @@
1
+ @import './variables.less';
2
+ @import './mixins.less';
3
+ @import './reset.less';
4
+
5
+ // 自定义字体声明
6
+ @font-face {
7
+ font-family: 'CustomFont';
8
+ src:
9
+ url('../assets/fonts/CustomFont.woff2') format('woff2'),
10
+ url('../assets/fonts/CustomFont.woff') format('woff');
11
+ font-weight: normal;
12
+ font-style: normal;
13
+ font-display: swap;
14
+ }
15
+
16
+ // 全局基础样式
17
+ body {
18
+ font-family: @font-family;
19
+ font-size: @font-size-base;
20
+ color: @text-color;
21
+ background: @bg-layout;
22
+ }
23
+
24
+ // 字体加载状态管理
25
+ .fonts-loading {
26
+ // 字体加载中使用系统字体(保持布局稳定)
27
+ body {
28
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
29
+ }
30
+ }
31
+
32
+ // 全局滚动条美化
33
+ body {
34
+ .custom-scrollbar();
35
+ }
36
+
37
+ // 全局选中色
38
+ ::selection {
39
+ background: fade(@color-primary, 20%);
40
+ }
@@ -0,0 +1,56 @@
1
+ // 文字省略
2
+ .text-ellipsis() {
3
+ overflow: hidden;
4
+ text-overflow: ellipsis;
5
+ white-space: nowrap;
6
+ }
7
+
8
+ // 多行省略
9
+ .text-ellipsis-lines(@lines: 2) {
10
+ display: -webkit-box;
11
+ -webkit-box-orient: vertical;
12
+ -webkit-line-clamp: @lines;
13
+ overflow: hidden;
14
+ }
15
+
16
+ // Flex 居中
17
+ .flex-center() {
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ }
22
+
23
+ // Flex 两端对齐
24
+ .flex-between() {
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: space-between;
28
+ }
29
+
30
+ // 滚动条美化
31
+ .custom-scrollbar() {
32
+ &::-webkit-scrollbar {
33
+ width: 6px;
34
+ height: 6px;
35
+ }
36
+
37
+ &::-webkit-scrollbar-thumb {
38
+ background: rgba(0, 0, 0, 0.15);
39
+ border-radius: 3px;
40
+
41
+ &:hover {
42
+ background: rgba(0, 0, 0, 0.25);
43
+ }
44
+ }
45
+
46
+ &::-webkit-scrollbar-track {
47
+ background: transparent;
48
+ }
49
+ }
50
+
51
+ // 卡片样式
52
+ .card-base() {
53
+ background: @bg-container;
54
+ border-radius: @border-radius-base;
55
+ box-shadow: @box-shadow-card;
56
+ }
@@ -0,0 +1,49 @@
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ box-sizing: border-box;
5
+ margin: 0;
6
+ padding: 0;
7
+ }
8
+
9
+ html,
10
+ body {
11
+ height: 100%;
12
+ -webkit-font-smoothing: antialiased;
13
+ -moz-osx-font-smoothing: grayscale;
14
+ }
15
+
16
+ body {
17
+ line-height: 1.5;
18
+ }
19
+
20
+ img,
21
+ picture,
22
+ video,
23
+ canvas,
24
+ svg {
25
+ display: block;
26
+ max-width: 100%;
27
+ }
28
+
29
+ input,
30
+ button,
31
+ textarea,
32
+ select {
33
+ font: inherit;
34
+ }
35
+
36
+ a {
37
+ text-decoration: none;
38
+ color: inherit;
39
+ }
40
+
41
+ ul,
42
+ ol {
43
+ list-style: none;
44
+ }
45
+
46
+ #root {
47
+ height: 100%;
48
+ isolation: isolate;
49
+ }