forked from ddrilling/asb_cloud_front
На странице Наработка добавлен сброс даты и выделение данных при наведении
This commit is contained in:
commit
bbf15c1f35
4
.gitignore
vendored
4
.gitignore
vendored
@ -11,8 +11,10 @@
|
|||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
# production
|
# build directories
|
||||||
/build
|
/build
|
||||||
|
/dev_build
|
||||||
|
/prod_build
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
3518
package-lock.json
generated
3518
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@ -16,10 +16,15 @@
|
|||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "webpack-dev-server --mode=development --open --hot",
|
|
||||||
"build": "webpack --mode=production",
|
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"dev": "webpack-dev-server --mode=development --open --hot",
|
"build": "webpack --env=\"ENV=prod\"",
|
||||||
|
"dev_build": "webpack --env=\"ENV=dev\"",
|
||||||
|
"prod_build": "webpack --env=\"ENV=prod\"",
|
||||||
|
|
||||||
|
"start": "webpack-dev-server --env=\"ENV=dev\" --open --hot",
|
||||||
|
"prod": "webpack-dev-server --env=\"ENV=prod\" --open --hot",
|
||||||
|
"dev": "webpack-dev-server --env=\"ENV=dev\" --open --hot",
|
||||||
|
|
||||||
"oul": "npx openapi -i http://127.0.0.1:5000/swagger/v1/swagger.json -o src/services/api",
|
"oul": "npx openapi -i http://127.0.0.1:5000/swagger/v1/swagger.json -o src/services/api",
|
||||||
"oud": "npx openapi -i http://192.168.1.113:5000/swagger/v1/swagger.json -o src/services/api",
|
"oud": "npx openapi -i http://192.168.1.113:5000/swagger/v1/swagger.json -o src/services/api",
|
||||||
"oug": "npx openapi -i https://cloud.digitaldrilling.ru/swagger/v1/swagger.json -o src/services/api",
|
"oug": "npx openapi -i https://cloud.digitaldrilling.ru/swagger/v1/swagger.json -o src/services/api",
|
||||||
@ -84,24 +89,30 @@
|
|||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"babel-jest": "^28.1.0",
|
"babel-jest": "^28.1.0",
|
||||||
"babel-loader": "^8.2.5",
|
"babel-loader": "^8.2.5",
|
||||||
|
"colors": "^1.4.0",
|
||||||
"css-loader": "^6.7.1",
|
"css-loader": "^6.7.1",
|
||||||
|
"css-minimizer-webpack-plugin": "^4.2.0",
|
||||||
|
"extract-loader": "^5.1.0",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"html-webpack-plugin": "^5.5.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"interpolate-html-plugin": "^4.0.0",
|
"interpolate-html-plugin": "^4.0.0",
|
||||||
"jest": "^28.1.0",
|
"jest": "^28.1.0",
|
||||||
"less": "^4.1.3",
|
"less": "^4.1.3",
|
||||||
"less-loader": "^11.0.0",
|
"less-loader": "^11.0.0",
|
||||||
|
"mini-css-extract-plugin": "^2.6.1",
|
||||||
"openapi-typescript": "^5.4.0",
|
"openapi-typescript": "^5.4.0",
|
||||||
"openapi-typescript-codegen": "^0.23.0",
|
"openapi-typescript-codegen": "^0.23.0",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"react-test-renderer": "^18.1.0",
|
"react-test-renderer": "^18.1.0",
|
||||||
"source-map-loader": "^3.0.1",
|
"source-map-loader": "^3.0.1",
|
||||||
"style-loader": "^3.3.1",
|
"style-loader": "^3.3.1",
|
||||||
|
"terser-webpack-plugin": "^5.3.6",
|
||||||
"ts-loader": "^9.3.0",
|
"ts-loader": "^9.3.0",
|
||||||
"typescript": "^4.7.4",
|
"typescript": "^4.7.4",
|
||||||
"url-loader": "^4.1.1",
|
"url-loader": "^4.1.1",
|
||||||
"webpack": "^5.73.0",
|
"webpack": "^5.73.0",
|
||||||
"webpack-cli": "^4.9.2",
|
"webpack-cli": "^4.9.2",
|
||||||
"webpack-dev-server": "^4.9.1"
|
"webpack-dev-server": "^4.9.1",
|
||||||
|
"webpack-merge": "^5.8.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import Register from '@pages/Register'
|
|||||||
import FileDownload from '@pages/FileDownload'
|
import FileDownload from '@pages/FileDownload'
|
||||||
|
|
||||||
import '@styles/App.less'
|
import '@styles/App.less'
|
||||||
|
import '@styles/include/antd_theme.less'
|
||||||
|
|
||||||
//OpenAPI.BASE = 'http://localhost:3000'
|
//OpenAPI.BASE = 'http://localhost:3000'
|
||||||
OpenAPI.TOKEN = async () => getUserToken() ?? ''
|
OpenAPI.TOKEN = async () => getUserToken() ?? ''
|
||||||
|
@ -30,7 +30,7 @@ export type DataType<T = any> = Record<string, T>
|
|||||||
export type RenderMethod<T = any> = (value: T, dataset?: DataType<T>, index?: number) => ReactNode
|
export type RenderMethod<T = any> = (value: T, dataset?: DataType<T>, index?: number) => ReactNode
|
||||||
export type SorterMethod<T = any> = (a?: DataType<T> | null, b?: DataType<T> | null) => number
|
export type SorterMethod<T = any> = (a?: DataType<T> | null, b?: DataType<T> | null) => number
|
||||||
|
|
||||||
export type columnPropsOther<T = any> = ColumnProps<T> & {
|
export type columnPropsOther<T = any> = ColumnProps<DataType<T>> & {
|
||||||
// редактируемая колонка
|
// редактируемая колонка
|
||||||
editable?: boolean
|
editable?: boolean
|
||||||
// react компонента редактора
|
// react компонента редактора
|
||||||
|
@ -2,11 +2,14 @@ import { InputNumber } from 'antd'
|
|||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
import { makeNumericSorter } from '../sorters'
|
import { makeNumericSorter } from '../sorters'
|
||||||
import { columnPropsOther, makeGroupColumn, RenderMethod } from '.'
|
import makeColumn, { columnPropsOther, DataType, makeGroupColumn, RenderMethod } from '.'
|
||||||
|
import { ColumnFilterItem, CompareFn } from 'antd/lib/table/interface'
|
||||||
|
|
||||||
export const RegExpIsFloat = /^[-+]?\d+\.?\d*$/
|
export const RegExpIsFloat = /^[-+]?\d+\.?\d*$/
|
||||||
|
|
||||||
export const makeNumericRender = <T extends unknown>(fixed?: number): RenderMethod<T> => (value) => {
|
type FilterMethod<T> = (value: string | number | boolean, record: DataType<T>) => boolean
|
||||||
|
|
||||||
|
export const makeNumericRender = <T extends unknown>(fixed?: number): RenderMethod<T> => (value: T) => {
|
||||||
let val = '-'
|
let val = '-'
|
||||||
if ((value ?? null) !== null && Number.isFinite(+value)) {
|
if ((value ?? null) !== null && Number.isFinite(+value)) {
|
||||||
val = (fixed ?? null) !== null
|
val = (fixed ?? null) !== null
|
||||||
@ -21,77 +24,74 @@ export const makeNumericRender = <T extends unknown>(fixed?: number): RenderMeth
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeNumericColumnOptions = (fixed?: number, sorterKey?: string): columnPropsOther => ({
|
export const makeNumericColumnOptions = <T extends unknown = any>(fixed?: number, sorterKey?: string): columnPropsOther<T> => ({
|
||||||
editable: true,
|
editable: true,
|
||||||
initialValue: 0,
|
initialValue: 0,
|
||||||
width: 100,
|
width: 100,
|
||||||
sorter: sorterKey ? makeNumericSorter(sorterKey) : undefined,
|
sorter: sorterKey ? makeNumericSorter<T>(sorterKey) : undefined,
|
||||||
formItemRules: [{
|
formItemRules: [{
|
||||||
required: true,
|
required: true,
|
||||||
message: 'Введите число',
|
message: 'Введите число',
|
||||||
pattern: RegExpIsFloat,
|
pattern: RegExpIsFloat,
|
||||||
}],
|
}],
|
||||||
render: makeNumericRender(fixed),
|
render: makeNumericRender<T>(fixed),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const makeNumericColumn = (
|
export const makeNumericColumn = <T extends unknown = any>(
|
||||||
title: ReactNode,
|
title: ReactNode,
|
||||||
dataIndex: string,
|
key: string,
|
||||||
filters: object[],
|
filters?: ColumnFilterItem[],
|
||||||
filterDelegate: (key: string | number) => any,
|
filterDelegate?: (key: string | number) => FilterMethod<T>,
|
||||||
renderDelegate: (_: any, row: object) => any,
|
renderDelegate?: RenderMethod<T>,
|
||||||
width: string,
|
width?: string | number,
|
||||||
other?: columnPropsOther
|
other?: columnPropsOther,
|
||||||
) => ({
|
) => makeColumn(title, key, {
|
||||||
title: title,
|
filters,
|
||||||
dataIndex: dataIndex,
|
onFilter: filterDelegate ? filterDelegate(key) : undefined,
|
||||||
key: dataIndex,
|
sorter: makeNumericSorter(key),
|
||||||
filters: filters,
|
width,
|
||||||
onFilter: filterDelegate ? filterDelegate(dataIndex) : null,
|
|
||||||
sorter: makeNumericSorter(dataIndex),
|
|
||||||
width: width,
|
|
||||||
input: <InputNumber style={{ width: '100%' }}/>,
|
input: <InputNumber style={{ width: '100%' }}/>,
|
||||||
render: renderDelegate ?? makeNumericRender(),
|
render: renderDelegate ?? makeNumericRender<T>(2),
|
||||||
align: 'right',
|
align: 'right',
|
||||||
...other
|
...other
|
||||||
})
|
})
|
||||||
|
|
||||||
export const makeNumericColumnPlanFact = (
|
export const makeNumericColumnPlanFact = <T extends unknown = any>(
|
||||||
title: ReactNode,
|
title: ReactNode,
|
||||||
dataIndex: string,
|
key: string,
|
||||||
filters: object[],
|
filters?: ColumnFilterItem[],
|
||||||
filterDelegate: (key: string | number) => any,
|
filterDelegate?: (key: string | number) => FilterMethod<T>,
|
||||||
renderDelegate: (_: any, row: object) => any,
|
renderDelegate?: RenderMethod<T>,
|
||||||
width: string
|
width?: string | number
|
||||||
) => makeGroupColumn(title, [
|
) => makeGroupColumn(title, [
|
||||||
makeNumericColumn('п', dataIndex + 'Plan', filters, filterDelegate, renderDelegate, width),
|
makeNumericColumn<T>('п', key + 'Plan', filters, filterDelegate, renderDelegate, width),
|
||||||
makeNumericColumn('ф', dataIndex + 'Fact', filters, filterDelegate, renderDelegate, width),
|
makeNumericColumn<T>('ф', key + 'Fact', filters, filterDelegate, renderDelegate, width),
|
||||||
])
|
])
|
||||||
|
|
||||||
export const makeNumericStartEnd = (
|
export const makeNumericStartEnd = <T extends unknown = any>(
|
||||||
title: ReactNode,
|
title: ReactNode,
|
||||||
dataIndex: string,
|
key: string,
|
||||||
fixed: number,
|
fixed: number,
|
||||||
filters: object[],
|
filters?: ColumnFilterItem[],
|
||||||
filterDelegate: (key: string | number) => any,
|
filterDelegate?: (key: string | number) => FilterMethod<T>,
|
||||||
renderDelegate: (_: any, row: object) => any,
|
renderDelegate?: RenderMethod<T>,
|
||||||
width: string,
|
width?: string | number,
|
||||||
) => makeGroupColumn(title, [
|
) => makeGroupColumn(title, [
|
||||||
makeNumericColumn('старт', dataIndex + 'Start', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Start')),
|
makeNumericColumn<T>('старт', key + 'Start', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, key + 'Start')),
|
||||||
makeNumericColumn('конец', dataIndex + 'End', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'End'))
|
makeNumericColumn<T>('конец', key + 'End', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, key + 'End'))
|
||||||
])
|
])
|
||||||
|
|
||||||
export const makeNumericMinMax = (
|
export const makeNumericMinMax = <T extends unknown = any>(
|
||||||
title: ReactNode,
|
title: ReactNode,
|
||||||
dataIndex: string,
|
key: string,
|
||||||
fixed: number,
|
fixed: number,
|
||||||
filters: object[],
|
filters?: ColumnFilterItem[],
|
||||||
filterDelegate: (key: string | number) => any,
|
filterDelegate?: (key: string | number) => FilterMethod<T>,
|
||||||
renderDelegate: (_: any, row: object) => any,
|
renderDelegate?: RenderMethod<T>,
|
||||||
width: string,
|
width?: string | number,
|
||||||
) => makeGroupColumn(title, [
|
) => makeGroupColumn(title, [
|
||||||
makeNumericColumn('мин', dataIndex + 'Min', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Min')),
|
makeNumericColumn<T>('мин', key + 'Min', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, key + 'Min')),
|
||||||
makeNumericColumn('макс', dataIndex + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Max')),
|
makeNumericColumn<T>('макс', key + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, key + 'Max')),
|
||||||
])
|
])
|
||||||
|
|
||||||
export default makeNumericColumn
|
export default makeNumericColumn
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import { ColumnFilterItem } from 'antd/lib/table/interface'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
import { columnPropsOther, DataType, RenderMethod, SorterMethod } from '.'
|
import makeColumn, { columnPropsOther, DataType, RenderMethod, SorterMethod } from '.'
|
||||||
import { makeStringSorter } from '../sorters'
|
import { makeStringSorter } from '../sorters'
|
||||||
|
|
||||||
export const makeFilterTextMatch = <T extends unknown>(key: keyof DataType<T>) =>
|
export const makeFilterTextMatch = <T extends unknown>(key: keyof DataType<T>) =>
|
||||||
@ -8,18 +9,15 @@ export const makeFilterTextMatch = <T extends unknown>(key: keyof DataType<T>) =
|
|||||||
|
|
||||||
export const makeTextColumn = <T extends unknown = any>(
|
export const makeTextColumn = <T extends unknown = any>(
|
||||||
title: ReactNode,
|
title: ReactNode,
|
||||||
dataIndex: string,
|
key: string,
|
||||||
filters: object[],
|
filters?: ColumnFilterItem[],
|
||||||
sorter?: SorterMethod<T>,
|
sorter?: SorterMethod<T>,
|
||||||
render?: RenderMethod<T>,
|
render?: RenderMethod<T>,
|
||||||
other?: columnPropsOther
|
other?: columnPropsOther
|
||||||
) => ({
|
) => makeColumn(title, key, {
|
||||||
title: title,
|
filters,
|
||||||
dataIndex: dataIndex,
|
onFilter: filters ? makeFilterTextMatch(key) : undefined,
|
||||||
key: dataIndex,
|
sorter: sorter ?? makeStringSorter(key),
|
||||||
filters: filters,
|
|
||||||
onFilter: filters ? makeFilterTextMatch(dataIndex) : null,
|
|
||||||
sorter: sorter ?? makeStringSorter(dataIndex),
|
|
||||||
render: render,
|
render: render,
|
||||||
...other
|
...other
|
||||||
})
|
})
|
||||||
|
@ -10,18 +10,17 @@ import { tryAddKeys } from './EditableTable'
|
|||||||
|
|
||||||
import '@styles/index.css'
|
import '@styles/index.css'
|
||||||
|
|
||||||
export type BaseTableColumn<T = any> = ColumnGroupType<T> | ColumnType<T>
|
export type BaseTableColumn<T> = ColumnGroupType<T> | ColumnType<T>
|
||||||
export type TableColumns<T = any> = OmitExtends<BaseTableColumn<T>, TableColumnSettings>[]
|
export type TableColumns<T> = OmitExtends<BaseTableColumn<T>, TableColumnSettings>[]
|
||||||
|
|
||||||
export type TableContainer = TableProps<any> & {
|
export type TableContainer<T> = TableProps<T> & {
|
||||||
columns: TableColumns
|
columns: TableColumns<T>
|
||||||
dataSource: any[]
|
|
||||||
tableName?: string
|
tableName?: string
|
||||||
showSettingsChanger?: boolean
|
showSettingsChanger?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Table = memo<TableContainer>(({ columns, dataSource, tableName, showSettingsChanger, ...other }) => {
|
const _Table = <T extends object>({ columns, dataSource, tableName, showSettingsChanger, ...other }: TableContainer<T>) => {
|
||||||
const [newColumns, setNewColumns] = useState<TableColumns>([])
|
const [newColumns, setNewColumns] = useState<TableColumns<T>>([])
|
||||||
const [settings, setSettings] = useState<TableSettings>({})
|
const [settings, setSettings] = useState<TableSettings>({})
|
||||||
|
|
||||||
const onSettingsChanged = useCallback((settings?: TableSettings | null) => {
|
const onSettingsChanged = useCallback((settings?: TableSettings | null) => {
|
||||||
@ -52,6 +51,8 @@ export const Table = memo<TableContainer>(({ columns, dataSource, tableName, sho
|
|||||||
{...other}
|
{...other}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
export const Table = memo(_Table) as typeof _Table
|
||||||
|
|
||||||
export default Table
|
export default Table
|
||||||
|
@ -7,7 +7,7 @@ import { TableColumnSettings, makeTableSettings, mergeTableSettings, TableSettin
|
|||||||
import { TableColumns } from './Table'
|
import { TableColumns } from './Table'
|
||||||
import { makeColumn } from '.'
|
import { makeColumn } from '.'
|
||||||
|
|
||||||
const parseSettings = (columns?: TableColumns, settings?: TableSettings | null): TableColumnSettings[] => {
|
const parseSettings = <T extends object>(columns?: TableColumns<T>, settings?: TableSettings | null): TableColumnSettings[] => {
|
||||||
const newSettings = mergeTableSettings(makeTableSettings(columns ?? []), settings ?? {})
|
const newSettings = mergeTableSettings(makeTableSettings(columns ?? []), settings ?? {})
|
||||||
return Object.values(newSettings).map((set, i) => ({ ...set, key: i }))
|
return Object.values(newSettings).map((set, i) => ({ ...set, key: i }))
|
||||||
}
|
}
|
||||||
@ -15,14 +15,14 @@ const parseSettings = (columns?: TableColumns, settings?: TableSettings | null):
|
|||||||
const unparseSettings = (columns: TableColumnSettings[]): TableSettings =>
|
const unparseSettings = (columns: TableColumnSettings[]): TableSettings =>
|
||||||
Object.fromEntries(columns.map((column) => [column.columnName, column]))
|
Object.fromEntries(columns.map((column) => [column.columnName, column]))
|
||||||
|
|
||||||
export type TableSettingsChangerProps = {
|
export type TableSettingsChangerProps<T extends object> = {
|
||||||
title?: string
|
title?: string
|
||||||
columns?: TableColumns
|
columns?: TableColumns<T>
|
||||||
settings?: TableSettings | null
|
settings?: TableSettings | null
|
||||||
onChange: (settings: TableSettings | null) => void
|
onChange: (settings: TableSettings | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TableSettingsChanger = memo<TableSettingsChangerProps>(({ title, columns, settings, onChange }) => {
|
const _TableSettingsChanger = <T extends object>({ title, columns, settings, onChange }: TableSettingsChangerProps<T>) => {
|
||||||
const [visible, setVisible] = useState<boolean>(false)
|
const [visible, setVisible] = useState<boolean>(false)
|
||||||
const [newSettings, setNewSettings] = useState<TableColumnSettings[]>(parseSettings(columns, settings))
|
const [newSettings, setNewSettings] = useState<TableColumnSettings[]>(parseSettings(columns, settings))
|
||||||
const [tableColumns, setTableColumns] = useState<ColumnsType<TableColumnSettings>>([])
|
const [tableColumns, setTableColumns] = useState<ColumnsType<TableColumnSettings>>([])
|
||||||
@ -36,10 +36,12 @@ export const TableSettingsChanger = memo<TableSettingsChangerProps>(({ title, co
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const toogleAll = useCallback((show: boolean) => {
|
const toogleAll = useCallback((show: boolean) => {
|
||||||
setNewSettings((oldSettings) => oldSettings.map((column) => {
|
setNewSettings((oldSettings) =>
|
||||||
column.visible = show
|
oldSettings.map((column) => {
|
||||||
return column
|
column.visible = show
|
||||||
}))
|
return column
|
||||||
|
})
|
||||||
|
)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -49,7 +51,9 @@ export const TableSettingsChanger = memo<TableSettingsChangerProps>(({ title, co
|
|||||||
title: () => (
|
title: () => (
|
||||||
<>
|
<>
|
||||||
Показать
|
Показать
|
||||||
<Button type={'link'} onClick={() => toogleAll(true)}>Показать все</Button>
|
<Button type={'link'} onClick={() => toogleAll(true)}>
|
||||||
|
Показать все
|
||||||
|
</Button>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
render: (visible: boolean, _?: TableColumnSettings, index: number = NaN) => (
|
render: (visible: boolean, _?: TableColumnSettings, index: number = NaN) => (
|
||||||
@ -59,7 +63,7 @@ export const TableSettingsChanger = memo<TableSettingsChangerProps>(({ title, co
|
|||||||
unCheckedChildren={'Скрыт'}
|
unCheckedChildren={'Скрыт'}
|
||||||
onChange={(visible) => onVisibilityChange(index, visible)}
|
onChange={(visible) => onVisibilityChange(index, visible)}
|
||||||
/>
|
/>
|
||||||
)
|
),
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
}, [toogleAll, onVisibilityChange])
|
}, [toogleAll, onVisibilityChange])
|
||||||
@ -88,9 +92,17 @@ export const TableSettingsChanger = memo<TableSettingsChangerProps>(({ title, co
|
|||||||
>
|
>
|
||||||
<Table columns={tableColumns} dataSource={newSettings} />
|
<Table columns={tableColumns} dataSource={newSettings} />
|
||||||
</Modal>
|
</Modal>
|
||||||
<Button size={'small'} style={{ position: 'absolute', left: 0, top: 0, opacity: .5 }} type={'link'} onClick={() => setVisible(true)} icon={<SettingOutlined />}/>
|
<Button
|
||||||
|
size={'small'}
|
||||||
|
style={{ position: 'absolute', left: 0, top: 0, opacity: 0.5 }}
|
||||||
|
type={'link'}
|
||||||
|
onClick={() => setVisible(true)}
|
||||||
|
icon={<SettingOutlined />}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
export const TableSettingsChanger = memo(_TableSettingsChanger) as typeof _TableSettingsChanger
|
||||||
|
|
||||||
export default TableSettingsChanger
|
export default TableSettingsChanger
|
||||||
|
@ -3,8 +3,9 @@ import { isRawDate } from '@utils'
|
|||||||
import { TimeDto } from '@api'
|
import { TimeDto } from '@api'
|
||||||
|
|
||||||
import { DataType } from './Columns'
|
import { DataType } from './Columns'
|
||||||
|
import { CompareFn } from 'antd/lib/table/interface'
|
||||||
|
|
||||||
export const makeNumericSorter = <T extends unknown>(key: keyof DataType<T>) =>
|
export const makeNumericSorter = <T extends unknown>(key: keyof DataType<T>): CompareFn<DataType<T>> =>
|
||||||
(a: DataType<T>, b: DataType<T>) => Number(a[key]) - Number(b[key])
|
(a: DataType<T>, b: DataType<T>) => Number(a[key]) - Number(b[key])
|
||||||
|
|
||||||
export const makeNumericObjSorter = (key: [string, string]) =>
|
export const makeNumericObjSorter = (key: [string, string]) =>
|
||||||
|
@ -1,144 +0,0 @@
|
|||||||
import React, { memo, useEffect } from 'react'
|
|
||||||
import * as d3 from 'd3'
|
|
||||||
|
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
|
||||||
import { useElementSize } from 'usehooks-ts'
|
|
||||||
|
|
||||||
|
|
||||||
import '@styles/d3.less'
|
|
||||||
|
|
||||||
export type HorizontalChartDataType = {
|
|
||||||
name: string
|
|
||||||
percent: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type D3HorizontalChartProps = {
|
|
||||||
width?: string
|
|
||||||
height?: string
|
|
||||||
data: HorizontalChartDataType[]
|
|
||||||
colors?: string[]
|
|
||||||
selected?: string | null
|
|
||||||
onMouseOver?: (e: MouseEvent, d: HorizontalChartDataType) => void
|
|
||||||
onMouseOut?: (e: MouseEvent, d: HorizontalChartDataType) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const D3HorizontalChart = memo((
|
|
||||||
{
|
|
||||||
width: givenWidth = '100%',
|
|
||||||
height: givenHeight = '100%',
|
|
||||||
selected,
|
|
||||||
data,
|
|
||||||
colors,
|
|
||||||
onMouseOver,
|
|
||||||
onMouseOut,
|
|
||||||
}: D3HorizontalChartProps) => {
|
|
||||||
|
|
||||||
const [rootRef, { width, height }] = useElementSize()
|
|
||||||
|
|
||||||
const margin = { top: 50, right: 100, bottom: 50, left: 100 }
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (width < 100 || height < 100) return
|
|
||||||
const chartWidth = width - margin.left - margin.right
|
|
||||||
const chartHeight = height - margin.top - margin.bottom
|
|
||||||
|
|
||||||
const svg = d3.select('#d3-horizontal-chart')
|
|
||||||
.attr('width', '100%')
|
|
||||||
.attr('height', '100%')
|
|
||||||
.append('g')
|
|
||||||
.attr('transform', `translate(${margin.left},${margin.top})`)
|
|
||||||
|
|
||||||
const names = data.map(d => d.name)
|
|
||||||
const xMax = 100
|
|
||||||
|
|
||||||
// scales
|
|
||||||
|
|
||||||
const x = d3.scaleLinear()
|
|
||||||
.domain([0, xMax])
|
|
||||||
.range([0, chartWidth])
|
|
||||||
|
|
||||||
const y = d3.scaleBand()
|
|
||||||
.domain(names)
|
|
||||||
.range([0, chartHeight])
|
|
||||||
.padding(0.25)
|
|
||||||
|
|
||||||
// axes
|
|
||||||
|
|
||||||
const tickValues = [0, 25, 50, 75, 100]
|
|
||||||
|
|
||||||
const xAxisTop = d3.axisTop(x)
|
|
||||||
.tickValues(tickValues)
|
|
||||||
.tickFormat(d => d + '%')
|
|
||||||
|
|
||||||
const xAxisBottom = d3.axisBottom(x)
|
|
||||||
.tickValues(tickValues)
|
|
||||||
.tickFormat(d => d + '%')
|
|
||||||
|
|
||||||
const yAxisLeft = d3.axisLeft(y)
|
|
||||||
|
|
||||||
const gridlines = d3.axisBottom(x)
|
|
||||||
.tickValues(tickValues)
|
|
||||||
.tickFormat(() => '')
|
|
||||||
.tickSize(chartHeight)
|
|
||||||
|
|
||||||
const yAxisRight = d3.axisRight(y)
|
|
||||||
.ticks(0)
|
|
||||||
.tickValues([])
|
|
||||||
.tickFormat(() => '')
|
|
||||||
|
|
||||||
svg.append('g')
|
|
||||||
.attr('transform', `translate(0,0)`)
|
|
||||||
.attr("class", "grid-line")
|
|
||||||
.call(g => g.select('.domain').remove())
|
|
||||||
.call(gridlines)
|
|
||||||
|
|
||||||
svg.append('g')
|
|
||||||
.attr('transform', `translate(0,0)`)
|
|
||||||
.call(xAxisTop)
|
|
||||||
|
|
||||||
svg.append("g")
|
|
||||||
.call(yAxisLeft)
|
|
||||||
|
|
||||||
svg.append('g')
|
|
||||||
.attr('transform', `translate(0,${chartHeight})`)
|
|
||||||
.call(xAxisBottom)
|
|
||||||
|
|
||||||
svg.append('g')
|
|
||||||
.attr('transform', `translate(${chartWidth},0)`)
|
|
||||||
.call(yAxisRight)
|
|
||||||
|
|
||||||
const layers = svg.append('g')
|
|
||||||
.selectAll('g')
|
|
||||||
.data(data)
|
|
||||||
.join('g')
|
|
||||||
|
|
||||||
layers.each(function() {
|
|
||||||
d3.select(this)
|
|
||||||
.selectAll('rect')
|
|
||||||
.data(data)
|
|
||||||
.join('rect')
|
|
||||||
.attr('fill', (d, i) => colors ? colors[i] : 'black')
|
|
||||||
.attr('y', d => String(y(d.name)))
|
|
||||||
.attr('height', y.bandwidth())
|
|
||||||
.attr('width', d => d.percent >= 0 ? x(d.percent) : 0)
|
|
||||||
.attr('stroke', d => selected && d.name === selected ? 'black' : '')
|
|
||||||
.attr('stroke-width', d => selected && d.name === selected ? '2' : '0')
|
|
||||||
.on('mouseover', onMouseOver ? onMouseOver : () => {})
|
|
||||||
.on('mouseout', onMouseOut ? onMouseOut : () => {})
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
svg.remove()
|
|
||||||
}
|
|
||||||
}, [width, height, data, colors, selected, onMouseOver, onMouseOut])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LoaderPortal show={false} style={{width: givenWidth, height: givenHeight}}>
|
|
||||||
<div ref={rootRef} style={{width: '100%', height: '100%'}}>
|
|
||||||
<svg id={'d3-horizontal-chart'}></svg>
|
|
||||||
</div>
|
|
||||||
</LoaderPortal>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default D3HorizontalChart
|
|
106
src/components/d3/D3HorizontalPercentChart.tsx
Normal file
106
src/components/d3/D3HorizontalPercentChart.tsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { memo, useEffect, useMemo, useRef } from 'react'
|
||||||
|
import { useElementSize } from 'usehooks-ts'
|
||||||
|
import { Property } from 'csstype'
|
||||||
|
import * as d3 from 'd3'
|
||||||
|
|
||||||
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
|
import { ChartOffset } from './types'
|
||||||
|
|
||||||
|
import '@styles/d3.less'
|
||||||
|
import { usePartialProps } from '@asb/utils'
|
||||||
|
|
||||||
|
export type PercentChartDataType = {
|
||||||
|
name: string
|
||||||
|
percent: number
|
||||||
|
color?: Property.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
export type D3HorizontalChartProps = {
|
||||||
|
width?: Property.Width
|
||||||
|
height?: Property.Height
|
||||||
|
data: PercentChartDataType[]
|
||||||
|
offset?: Partial<ChartOffset>
|
||||||
|
selected?: string | null
|
||||||
|
onMouseOver?: (e: MouseEvent, d: PercentChartDataType) => void
|
||||||
|
onMouseOut?: (e: MouseEvent, d: PercentChartDataType) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultOffset = { top: 50, right: 100, bottom: 50, left: 100 }
|
||||||
|
|
||||||
|
export const D3HorizontalPercentChart = memo<D3HorizontalChartProps>(({
|
||||||
|
width: givenWidth = '100%',
|
||||||
|
height: givenHeight = '100%',
|
||||||
|
offset: givenOffset,
|
||||||
|
data,
|
||||||
|
selected = null,
|
||||||
|
onMouseOver = () => {},
|
||||||
|
onMouseOut= () => {},
|
||||||
|
}) => {
|
||||||
|
const offset = usePartialProps<ChartOffset>(givenOffset, defaultOffset)
|
||||||
|
|
||||||
|
const [divRef, { width, height }] = useElementSize()
|
||||||
|
const rootRef = useRef<SVGGElement | null>(null)
|
||||||
|
|
||||||
|
const root = useMemo(() => rootRef.current ? d3.select(rootRef.current) : null, [rootRef.current])
|
||||||
|
|
||||||
|
const inlineWidth = useMemo(() => width - offset.left - offset.right, [width])
|
||||||
|
const inlineHeight = useMemo(() => height - offset.top - offset.bottom, [height])
|
||||||
|
|
||||||
|
const xScale = useMemo(() => d3.scaleLinear().domain([0, 100]).range([0, inlineWidth]), [inlineWidth])
|
||||||
|
const yScale = useMemo(() => d3.scaleBand().domain(data.map((d) => d.name)).range([0, inlineHeight]).padding(0.25), [data, inlineHeight])
|
||||||
|
|
||||||
|
useEffect(() => { /// Отрисовываем оси X сверху и снизу
|
||||||
|
if (width < 100 || height < 100 || !root) return
|
||||||
|
const xAxisTop = d3.axisTop(xScale).tickFormat((d) => `${d}%`).tickValues([0, 25, 50, 75, 100]).tickSize(-inlineHeight)
|
||||||
|
const xAxisBottom = d3.axisBottom(xScale).tickFormat((d) => `${d}%`).tickValues([0, 25, 50, 75, 100])
|
||||||
|
|
||||||
|
root.selectChild<SVGGElement>('.axis.x.bottom').call(xAxisBottom)
|
||||||
|
root.selectChild<SVGGElement>('.axis.x.top').call(xAxisTop)
|
||||||
|
.selectAll('.tick')
|
||||||
|
.attr('class', 'tick grid-line')
|
||||||
|
}, [root, width, height, xScale, inlineHeight])
|
||||||
|
|
||||||
|
useEffect(() => { /// Отрисовываем ось Y слева
|
||||||
|
if (width < 100 || height < 100 || !root) return
|
||||||
|
root.selectChild<SVGGElement>('.axis.y.left').call(d3.axisLeft(yScale))
|
||||||
|
}, [root, width, height, yScale])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (width < 100 || height < 100 || !root) return
|
||||||
|
|
||||||
|
const delay = d3.transition().duration(500).ease(d3.easeLinear)
|
||||||
|
|
||||||
|
const rects = root.selectChild('.data').selectAll('rect').data(data)
|
||||||
|
rects.enter().append('rect')
|
||||||
|
rects.exit().remove()
|
||||||
|
root.selectChild<SVGGElement>('.data')
|
||||||
|
.selectAll<SVGRectElement, PercentChartDataType>('rect')
|
||||||
|
.attr('fill', (d) => d.color || 'black')
|
||||||
|
.attr('y', (d) => yScale(d.name) ?? null)
|
||||||
|
.attr('stroke', d => selected && d.name === selected ? 'black' : '')
|
||||||
|
.attr('stroke-width', d => selected && d.name === selected ? '2' : '0')
|
||||||
|
.attr('height', yScale.bandwidth())
|
||||||
|
.on('mouseover', onMouseOver)
|
||||||
|
.on('mouseout', onMouseOut)
|
||||||
|
.transition(delay)
|
||||||
|
.attr('width', (d) => d.percent > 0 ? xScale(d.percent) : 0)
|
||||||
|
|
||||||
|
}, [data, width, height, root, yScale, xScale, selected])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LoaderPortal show={false} style={{ width: givenWidth, height: givenHeight }}>
|
||||||
|
<div ref={divRef} style={{ width: '100%', height: '100%' }}>
|
||||||
|
<svg id={'d3-horizontal-chart'} width={'100%'} height={'100%'}>
|
||||||
|
<g ref={rootRef} transform={`translate(${offset.left}, ${offset.top})`}>
|
||||||
|
<g className={'axis x top'}></g>
|
||||||
|
<g className={'axis x bottom'} transform={`translate(0, ${inlineHeight})`}></g>
|
||||||
|
<g className={'data'}></g>
|
||||||
|
<g className={'axis y left'}></g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</LoaderPortal>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default D3HorizontalPercentChart
|
@ -559,7 +559,6 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
|
|||||||
<D3MonitoringCurrentValues<DataType>
|
<D3MonitoringCurrentValues<DataType>
|
||||||
groups={groups}
|
groups={groups}
|
||||||
data={data}
|
data={data}
|
||||||
left={offset.left}
|
|
||||||
sizes={sizes}
|
sizes={sizes}
|
||||||
/>
|
/>
|
||||||
<D3MouseZone width={width} height={height} offset={{ ...offset, top: sizes.chartsTop }}>
|
<D3MouseZone width={width} height={height} offset={{ ...offset, top: sizes.chartsTop }}>
|
||||||
|
@ -5,20 +5,24 @@ import { ChartGroup, ChartSizes } from '@components/d3/monitoring/D3MonitoringCh
|
|||||||
import { makeDisplayValue } from '@utils'
|
import { makeDisplayValue } from '@utils'
|
||||||
|
|
||||||
export type D3MonitoringCurrentValuesProps<DataType extends BaseDataType> = {
|
export type D3MonitoringCurrentValuesProps<DataType extends BaseDataType> = {
|
||||||
|
/** Группы графиков */
|
||||||
groups: ChartGroup<DataType>[]
|
groups: ChartGroup<DataType>[]
|
||||||
|
/** Массив данных графика */
|
||||||
data: DataType[]
|
data: DataType[]
|
||||||
left: number
|
/** Объект, хранящий полезные размеры и отступы графика (нужен только groupWidth, chartsTop и groupLeft) */
|
||||||
sizes: ChartSizes
|
sizes: ChartSizes
|
||||||
}
|
}
|
||||||
|
|
||||||
const display = makeDisplayValue({ def: '---', fixed: 2 })
|
const display = makeDisplayValue({ def: '---', fixed: 2 })
|
||||||
|
|
||||||
const _D3MonitoringCurrentValues = <DataType extends BaseDataType>({ groups, data, left, sizes }: D3MonitoringCurrentValuesProps<DataType>) => (
|
/// `Array.at` вместе с `??` возвращает странный тип, поэтому его пока пришлось пометить как `any`
|
||||||
<g transform={`translate(${left}, ${sizes.chartsTop})`} pointerEvents={'none'}>
|
/// TODO: Исправить тип
|
||||||
|
const _D3MonitoringCurrentValues = <DataType extends BaseDataType>({ groups, data, sizes }: D3MonitoringCurrentValuesProps<DataType>) => (
|
||||||
|
<g transform={`translate(${sizes.left}, ${sizes.chartsTop})`} pointerEvents={'none'}>
|
||||||
{groups.map((group) => (
|
{groups.map((group) => (
|
||||||
<g key={group.key} transform={`translate(${sizes.groupLeft(group.key)}, 0)`}>
|
<g key={group.key} transform={`translate(${sizes.groupLeft(group.key)}, 0)`}>
|
||||||
{group.charts.filter((chart) => chart.showCurrentValue).map((chart, i) => (
|
{group.charts.filter((chart) => chart.showCurrentValue).map((chart, i) => (
|
||||||
<g key={chart.key} stroke={'white'} fill={chart.color} strokeWidth={3} paintOrder={'stroke'}>
|
<g key={chart.key} stroke={'white'} fill={chart.color} strokeWidth={4} paintOrder={'stroke'} style={{ fontWeight: 600 }}>
|
||||||
<text x={sizes.groupWidth / 2 - 10} textAnchor={'end'} y={15 + i * 20}>{chart.shortLabel ?? chart.label}:</text>
|
<text x={sizes.groupWidth / 2 - 10} textAnchor={'end'} y={15 + i * 20}>{chart.shortLabel ?? chart.label}:</text>
|
||||||
<text x={sizes.groupWidth / 2 + 10} textAnchor={'start'} y={15 + i * 20}>{display(chart.x((data.at(-1) ?? {}) as any))}</text>
|
<text x={sizes.groupWidth / 2 + 10} textAnchor={'start'} y={15 + i * 20}>{display(chart.x((data.at(-1) ?? {}) as any))}</text>
|
||||||
</g>
|
</g>
|
||||||
@ -28,6 +32,15 @@ const _D3MonitoringCurrentValues = <DataType extends BaseDataType>({ groups, dat
|
|||||||
</g>
|
</g>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отрисовывает последние значения графиков
|
||||||
|
*
|
||||||
|
* @typeParam DataType - тип данных для отрисовки графиков
|
||||||
|
*
|
||||||
|
* @param groups - Массив групп графиков
|
||||||
|
* @param data - Массив данных графиков
|
||||||
|
* @param sizes - Объект с полезными размерами и отступами внутри svg
|
||||||
|
*/
|
||||||
export const D3MonitoringCurrentValues = memo(_D3MonitoringCurrentValues) as typeof _D3MonitoringCurrentValues
|
export const D3MonitoringCurrentValues = memo(_D3MonitoringCurrentValues) as typeof _D3MonitoringCurrentValues
|
||||||
|
|
||||||
export default D3MonitoringCurrentValues
|
export default D3MonitoringCurrentValues
|
||||||
|
@ -164,7 +164,7 @@ const _D3MonitoringEditor = <DataType extends BaseDataType>({
|
|||||||
}}
|
}}
|
||||||
height={250}
|
height={250}
|
||||||
/>
|
/>
|
||||||
<Button onClick={() => setMode('limit')}>Ограничение подачи</Button>
|
{/* <Button onClick={() => setMode('limit')}>Ограничение подачи</Button> */}
|
||||||
</div>
|
</div>
|
||||||
<Divider type={'vertical'} style={{ height: '100%', padding: '0 5px' }} />
|
<Divider type={'vertical'} style={{ height: '100%', padding: '0 5px' }} />
|
||||||
<div style={divStyle}>
|
<div style={divStyle}>
|
||||||
|
@ -30,7 +30,7 @@ export const notify = (body?: ReactNode, notifyType: NotifyType = 'info', well?:
|
|||||||
const message = (
|
const message = (
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<span>{instance.message}</span>
|
<span>{instance.message}</span>
|
||||||
<WellView well={well} />
|
<WellView placement={'leftBottom'} well={well} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -99,6 +99,8 @@ export const WellTreeSelector = memo<WellTreeSelectorProps>(({ show, expand, cur
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|
||||||
|
console.log(location.pathname)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (current) setSelected([current])
|
if (current) setSelected([current])
|
||||||
}, [current])
|
}, [current])
|
||||||
@ -156,7 +158,18 @@ export const WellTreeSelector = memo<WellTreeSelectorProps>(({ show, expand, cur
|
|||||||
}, [wellsTree])
|
}, [wellsTree])
|
||||||
|
|
||||||
const onSelect = useCallback((value: Key[]): void => {
|
const onSelect = useCallback((value: Key[]): void => {
|
||||||
navigate(String(value), { state: { from: location.pathname }})
|
const newRoot = /\/(\w+)\/\d+/.exec(String(value))
|
||||||
|
const oldRoot = /\/(\w+)(?:\/\d+)?/.exec(location.pathname)
|
||||||
|
if (!newRoot || !oldRoot) return
|
||||||
|
|
||||||
|
let newPath = newRoot[0]
|
||||||
|
if (oldRoot[1] === newRoot[1]) {
|
||||||
|
/// Если типы страницы одинаковые (deposit, cluster, well), добавляем остаток старого пути
|
||||||
|
const url = location.pathname.substring(oldRoot[0].length)
|
||||||
|
newPath = newPath + url
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(newPath, { state: { from: location.pathname }})
|
||||||
}, [navigate, location])
|
}, [navigate, location])
|
||||||
|
|
||||||
useEffect(() => onChange(location.pathname), [onChange, location])
|
useEffect(() => onChange(location.pathname), [onChange, location])
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import { Tooltip } from 'antd'
|
import { Tooltip, TooltipProps } from 'antd'
|
||||||
|
|
||||||
import { Grid, GridItem } from '@components/Grid'
|
import { Grid, GridItem } from '@components/Grid'
|
||||||
import { WellIcon, WellIconState } from '@components/icons'
|
import { WellIcon, WellIconState } from '@components/icons'
|
||||||
@ -13,12 +13,12 @@ const wellState: Record<number, { enum: WellIconState, label: string }> = {
|
|||||||
2: { enum: 'inactive', label: 'Завершена' },
|
2: { enum: 'inactive', label: 'Завершена' },
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WellViewProps = {
|
export type WellViewProps = TooltipProps & {
|
||||||
well?: WellDto
|
well?: WellDto
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WellView = memo<WellViewProps>(({ well }) => well ? (
|
export const WellView = memo<WellViewProps>(({ well, ...other }) => well ? (
|
||||||
<Tooltip title={(
|
<Tooltip {...other} title={(
|
||||||
<Grid style={{ columnGap: '8px' }}>
|
<Grid style={{ columnGap: '8px' }}>
|
||||||
<GridItem row={1} col={1}>Название:</GridItem>
|
<GridItem row={1} col={1}>Название:</GridItem>
|
||||||
<GridItem row={1} col={2}>{well.caption ?? '---'}</GridItem>
|
<GridItem row={1} col={2}>{well.caption ?? '---'}</GridItem>
|
||||||
|
@ -40,7 +40,7 @@ export const DocumentsTemplate = ({ idCategory, well: givenWell, mimeTypes, head
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
makeDateColumn('Дата загрузки', 'uploadDate'),
|
makeDateColumn('Дата загрузки', 'uploadDate'),
|
||||||
makeNumericColumn('Размер', 'size', null, null, formatBytes),
|
makeNumericColumn('Размер', 'size', null, null, (value) => formatBytes(value)),
|
||||||
makeColumn('Автор', 'author', { render: item => <UserView user={item}/> }),
|
makeColumn('Автор', 'author', { render: item => <UserView user={item}/> }),
|
||||||
makeColumn('Компания', 'company', { render: (_, record) => <CompanyView company={record?.author?.company}/> }),
|
makeColumn('Компания', 'company', { render: (_, record) => <CompanyView company={record?.author?.company}/> }),
|
||||||
...(customColumns ?? [])
|
...(customColumns ?? [])
|
||||||
@ -49,12 +49,8 @@ export const DocumentsTemplate = ({ idCategory, well: givenWell, mimeTypes, head
|
|||||||
const filenames = useMemo(() => files.map(file => file.name).filter(Boolean).filter(unique), [files])
|
const filenames = useMemo(() => files.map(file => file.name).filter(Boolean).filter(unique), [files])
|
||||||
|
|
||||||
const update = useCallback(() => {
|
const update = useCallback(() => {
|
||||||
let begin = null
|
const begin = filterDataRange?.length > 1 ? filterDataRange[0].toISOString() : null
|
||||||
let end = null
|
const end = filterDataRange?.length > 1 ? filterDataRange[1].toISOString() : null
|
||||||
if (filterDataRange?.length > 1) {
|
|
||||||
begin = filterDataRange[0].toISOString()
|
|
||||||
end = filterDataRange[1].toISOString()
|
|
||||||
}
|
|
||||||
|
|
||||||
invokeWebApiWrapperAsync(
|
invokeWebApiWrapperAsync(
|
||||||
async () => {
|
async () => {
|
||||||
|
@ -11,7 +11,8 @@ import { makeColumn, makeDateColumn, makeNumericColumn, makeNumericSorter, makeT
|
|||||||
import { wrapPrivateComponent } from '@utils'
|
import { wrapPrivateComponent } from '@utils'
|
||||||
import { MessageService } from '@api'
|
import { MessageService } from '@api'
|
||||||
|
|
||||||
import '@styles/message.css'
|
import '@styles/filter.less'
|
||||||
|
import '@styles/message.less'
|
||||||
|
|
||||||
const pageSize = 26
|
const pageSize = 26
|
||||||
const { Search } = Input
|
const { Search } = Input
|
||||||
@ -103,22 +104,25 @@ const Messages = memo(() => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={'filter-group'}>
|
<div className={'filter-group'}>
|
||||||
<h3 className={'filter-group-heading'}>Фильтр сообщений</h3>
|
<h3 className={'head'}>Фильтр сообщений</h3>
|
||||||
<Select
|
<div className={'body'}>
|
||||||
mode={'multiple'}
|
<Select
|
||||||
allowClear
|
allowClear
|
||||||
placeholder={'Фильтр сообщений'}
|
mode={'multiple'}
|
||||||
className={'filter-selector'}
|
className={'type-filter'}
|
||||||
value={categories}
|
placeholder={'Фильтр сообщений'}
|
||||||
onChange={setCategories}>
|
onChange={setCategories}
|
||||||
{children}
|
value={categories}
|
||||||
</Select>
|
>
|
||||||
<RangePicker showTime onChange={(range) => setRange(range)} />
|
{children}
|
||||||
<Search
|
</Select>
|
||||||
className={'filter-selector'}
|
<RangePicker showTime onChange={(range) => setRange(range)} />
|
||||||
placeholder={'Фильтр сообщений по тексту'}
|
<Search
|
||||||
onSearch={onChangeSearchString}
|
className={'filter-selector'}
|
||||||
/>
|
placeholder={'Фильтр сообщений по тексту'}
|
||||||
|
onSearch={onChangeSearchString}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<LoaderPortal show={showLoader}>
|
<LoaderPortal show={showLoader}>
|
||||||
<Table
|
<Table
|
||||||
|
159
src/pages/Telemetry/OperationTime/index.jsx
Normal file
159
src/pages/Telemetry/OperationTime/index.jsx
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
import { Select } from 'antd'
|
||||||
|
import moment from 'moment'
|
||||||
|
|
||||||
|
import { useWell } from '@asb/context'
|
||||||
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
|
import D3HorizontalPercentChart from '@components/d3/D3HorizontalPercentChart'
|
||||||
|
import { DateRangeWrapper, makeColumn, makeNumericColumn, makeNumericRender, makeTextColumn, Table } from '@components/Table'
|
||||||
|
import { arrayOrDefault, range, wrapPrivateComponent } from '@utils'
|
||||||
|
import { SubsystemOperationTimeService } from '@api'
|
||||||
|
|
||||||
|
import '@styles/filter.less'
|
||||||
|
import '@styles/operation_time.less'
|
||||||
|
|
||||||
|
const subsystemColors = [
|
||||||
|
'#1abc9c', '#16a085', '#2ecc71', '#27ae60',
|
||||||
|
'#3498db', '#2980b9', '#9b59b6', '#8e44ad',
|
||||||
|
'#34495e', '#2c3e50', '#f1c40f', '#f39c12',
|
||||||
|
'#e67e22', '#d35400', '#e74c3c', '#c0392b',
|
||||||
|
'#ecf0f1', '#bdc3c7', '#95a5a6', '#7f8c8d',
|
||||||
|
]
|
||||||
|
|
||||||
|
const tableColumns = [
|
||||||
|
makeColumn('Цвет', 'color', { width: 60, render: (backgroundColor) => (
|
||||||
|
<div className={'table_color'} style={{ backgroundColor }} />
|
||||||
|
)}),
|
||||||
|
makeTextColumn('Подсистема', 'subsystemName'),
|
||||||
|
makeNumericColumn('Использование, %', 'kUsage', undefined, undefined, val => (+val * 100).toFixed(2), 200),
|
||||||
|
makeNumericColumn('Проходка, м', 'sumDepthInterval', undefined, undefined, undefined, 200),
|
||||||
|
makeNumericColumn('Время работы, ч', 'usedTimeHours', undefined, undefined, undefined, 200),
|
||||||
|
makeNumericColumn('Кол-во запусков', 'operationCount', undefined, undefined, makeNumericRender(0), 200),
|
||||||
|
]
|
||||||
|
|
||||||
|
// Выбор доступен только до текущей даты
|
||||||
|
const disabledDates = (current) => current && moment(current).isAfter(moment(), 'day', '[]')
|
||||||
|
// Выбор доступен только до текущего времени
|
||||||
|
const disabledTimes = (date) => ({
|
||||||
|
disabledHours: () => range(24).filter(h => date && moment(date).hours(h).isAfter(moment(), 'hour', '[]')),
|
||||||
|
disabledMinutes: () => range(60).filter(m => date && moment(date).minutes(m).isAfter(moment(), 'minute', '[]')),
|
||||||
|
disabledSeconds: () => range(60).filter(s => date && moment(date).seconds(s).isBetween(moment(), 'second', '[]'))
|
||||||
|
})
|
||||||
|
|
||||||
|
export const OperationTime = memo(() => {
|
||||||
|
const [showLoader, setShowLoader] = useState(false)
|
||||||
|
const [data, setData] = useState([])
|
||||||
|
const [selected, setSelected] = useState([])
|
||||||
|
const [selectedOnHover, setSelectedOnHover] = useState(null)
|
||||||
|
const [dateRange, setDateRange] = useState([])
|
||||||
|
const [well] = useWell()
|
||||||
|
|
||||||
|
// Создаём массив пунктов для селектора подсистем
|
||||||
|
const typeOptions = useMemo(() => data.map((d) => ({ label: d.subsystemName, value: d.idSubsystem })), [data])
|
||||||
|
// Фильтруем данные по выбранным подсистемам
|
||||||
|
const selectedData = useMemo(() => data.filter((d) => selected.includes(d.idSubsystem)), [selected, data])
|
||||||
|
// Подготавливаем данные для отображения на графике
|
||||||
|
const chartData = useMemo(() => selectedData.map((d) => ({
|
||||||
|
name: d.subsystemName,
|
||||||
|
percent: d.kUsage * 100,
|
||||||
|
color: d.color,
|
||||||
|
})), [selectedData])
|
||||||
|
|
||||||
|
const onRow = useCallback((item) => {
|
||||||
|
const out = {
|
||||||
|
onMouseEnter: () => {
|
||||||
|
setSelectedOnHover(item.subsystemName)
|
||||||
|
},
|
||||||
|
onMouseLeave: () => {
|
||||||
|
setSelectedOnHover(null)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.subsystemName === selectedOnHover) {
|
||||||
|
out.style = { background: '#FAFAFA', fontSize: '16px', fontWeight: '600' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}, [selectedOnHover])
|
||||||
|
|
||||||
|
const onMouseOver = useCallback(function (e, d) {
|
||||||
|
setSelectedOnHover(d.name)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onMouseOut = useCallback(function (e, d) {
|
||||||
|
setSelectedOnHover(null)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
invokeWebApiWrapperAsync(
|
||||||
|
async () => {
|
||||||
|
if (!well.id) return
|
||||||
|
|
||||||
|
// Ограничение задаётся только если выбраны обе даты
|
||||||
|
const startDate = dateRange && dateRange[1] ? dateRange[0]?.toISOString() : undefined
|
||||||
|
const endDate = dateRange && dateRange[1] ? dateRange[1]?.toISOString() : undefined
|
||||||
|
|
||||||
|
const data = await SubsystemOperationTimeService.getStat(well.id, undefined, startDate, endDate)
|
||||||
|
// Выбираем цвета для подсистем (если цветов не хватает начинаем сначала)
|
||||||
|
const coloredData = arrayOrDefault(data).map((d, i) => ({ ...d, color: subsystemColors[i % subsystemColors.length] }))
|
||||||
|
|
||||||
|
setData(coloredData)
|
||||||
|
setSelected(data?.map((d) => d.idSubsystem)) // По-умолчанию выбираем все подсистемы
|
||||||
|
},
|
||||||
|
setShowLoader,
|
||||||
|
`Не удалось загрузить данные`,
|
||||||
|
{ actionName: 'Получение данных по скважине', well }
|
||||||
|
)
|
||||||
|
}, [dateRange, well])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LoaderPortal show={showLoader}>
|
||||||
|
<div className={'filter-group'}>
|
||||||
|
<h3 className={'head'}>Фильтр подсистем</h3>
|
||||||
|
<div className={'body'}>
|
||||||
|
<Select
|
||||||
|
allowClear
|
||||||
|
mode={'multiple'}
|
||||||
|
options={typeOptions}
|
||||||
|
className={'filter-selector'}
|
||||||
|
onChange={setSelected}
|
||||||
|
value={selected}
|
||||||
|
/>
|
||||||
|
<DateRangeWrapper
|
||||||
|
allowClear
|
||||||
|
onCalendarChange={setDateRange}
|
||||||
|
disabledDate={disabledDates}
|
||||||
|
disabledTime={disabledTimes}
|
||||||
|
value={dateRange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<D3HorizontalPercentChart
|
||||||
|
data={chartData}
|
||||||
|
colors={subsystemColors}
|
||||||
|
selected={selectedOnHover}
|
||||||
|
width={'100%'}
|
||||||
|
height={'50vh'}
|
||||||
|
onMouseOver={onMouseOver}
|
||||||
|
onMouseOut={onMouseOut}
|
||||||
|
/>
|
||||||
|
<Table
|
||||||
|
bordered
|
||||||
|
size={'small'}
|
||||||
|
columns={tableColumns}
|
||||||
|
dataSource={selectedData}
|
||||||
|
scroll={{ y: '25vh', x: true }}
|
||||||
|
pagination={false}
|
||||||
|
onRow={onRow}
|
||||||
|
/>
|
||||||
|
</LoaderPortal>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default wrapPrivateComponent(OperationTime, {
|
||||||
|
requirements: [], // SubsystemOperationTime.get
|
||||||
|
title: 'Наработка',
|
||||||
|
route: 'operation_time',
|
||||||
|
key: 'operation_time',
|
||||||
|
})
|
@ -1,187 +0,0 @@
|
|||||||
import React, { memo, ReactNode, useCallback, useEffect, useState } from 'react'
|
|
||||||
import { Col, Row, Select } from 'antd'
|
|
||||||
import { Moment } from 'moment'
|
|
||||||
|
|
||||||
import { DateRangeWrapper, makeColumn, makeNumericRender, Table } from '@components/Table'
|
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
|
||||||
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
|
|
||||||
import { D3HorizontalChart, HorizontalChartDataType } from '@components/d3/D3HorizontalChart'
|
|
||||||
import { useWell } from '@asb/context'
|
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
|
||||||
import { SubsystemOperationTimeService } from '@api'
|
|
||||||
|
|
||||||
const { Option } = Select;
|
|
||||||
|
|
||||||
type DataType = {
|
|
||||||
idSubsystem: number
|
|
||||||
subsystemName: string
|
|
||||||
usedTimeHours: number
|
|
||||||
kUsage: number
|
|
||||||
sumDepthInterval: number
|
|
||||||
operationCount: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const subsystemColors = [
|
|
||||||
'#1abc9c',
|
|
||||||
'#16a085',
|
|
||||||
'#2ecc71',
|
|
||||||
'#27ae60',
|
|
||||||
'#3498db',
|
|
||||||
'#2980b9',
|
|
||||||
'#9b59b6',
|
|
||||||
'#8e44ad',
|
|
||||||
'#34495e',
|
|
||||||
'#2c3e50',
|
|
||||||
'#f1c40f',
|
|
||||||
'#f39c12',
|
|
||||||
'#e67e22',
|
|
||||||
'#d35400',
|
|
||||||
'#e74c3c',
|
|
||||||
'#c0392b',
|
|
||||||
'#ecf0f1',
|
|
||||||
'#bdc3c7',
|
|
||||||
'#95a5a6',
|
|
||||||
'#7f8c8d',
|
|
||||||
]
|
|
||||||
|
|
||||||
const tableColumns = [
|
|
||||||
makeColumn('Цвет', 'color', { width: 50, render: (color) => (
|
|
||||||
<div style={{ backgroundColor: color, padding: '5px 0' }} />
|
|
||||||
) }),
|
|
||||||
makeColumn('Подсистема', 'subsystemName'),
|
|
||||||
makeColumn('% использования', 'kUsage', { width: 200, render: val => makeNumericRender(2)(+val * 100) }),
|
|
||||||
makeColumn('Проходка, м', 'sumDepthInterval', { width: 200, render: makeNumericRender(2) }),
|
|
||||||
makeColumn('Время работы, ч', 'usedTimeHours', { width: 200, render: makeNumericRender(2) }),
|
|
||||||
makeColumn('Кол-во запусков', 'operationCount', { width: 200, render: makeNumericRender(0) }),
|
|
||||||
]
|
|
||||||
|
|
||||||
const OperationTime = memo(() => {
|
|
||||||
const [showLoader, setShowLoader] = useState(false)
|
|
||||||
const [data, setData] = useState<DataType[]>([])
|
|
||||||
const [selectedData, setSelectedData] = useState<DataType[]>([])
|
|
||||||
const [dateRange, setDateRange] = useState<Moment[] | null>([])
|
|
||||||
const [selected, setSelected] = useState<string | null>(null)
|
|
||||||
const [childrenData, setChildrenData] = useState<ReactNode[]>([])
|
|
||||||
const [well] = useWell()
|
|
||||||
|
|
||||||
const errorNotifyText = `Не удалось загрузить данные`
|
|
||||||
|
|
||||||
const onRow = useCallback((item: DataType) => {
|
|
||||||
const out = {
|
|
||||||
onMouseEnter: () => {
|
|
||||||
setSelected(item.subsystemName)
|
|
||||||
},
|
|
||||||
onMouseLeave: () => {
|
|
||||||
setSelected(null)
|
|
||||||
},
|
|
||||||
style: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.subsystemName === selected) {
|
|
||||||
out.style = { background: '#FAFAFA', fontSize: '16px', fontWeight: '600' }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}, [selected])
|
|
||||||
|
|
||||||
const onMouseOver = useCallback(function (e: MouseEvent, d: HorizontalChartDataType) {
|
|
||||||
setSelected(d.name)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const onMouseOut = useCallback(function (e: MouseEvent, d: HorizontalChartDataType) {
|
|
||||||
setSelected(null)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
invokeWebApiWrapperAsync(
|
|
||||||
async () => {
|
|
||||||
if (!well.id) return
|
|
||||||
try {
|
|
||||||
const responseData:DataType[] = arrayOrDefault(await SubsystemOperationTimeService.getStat(
|
|
||||||
well.id,
|
|
||||||
undefined,
|
|
||||||
dateRange && dateRange[1] ? dateRange[0]?.toISOString() : undefined,
|
|
||||||
dateRange && dateRange[1] ? dateRange[1].toISOString() : undefined,
|
|
||||||
))
|
|
||||||
setData(responseData)
|
|
||||||
setSelectedData(responseData)
|
|
||||||
} catch(e) {
|
|
||||||
setData([])
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setShowLoader,
|
|
||||||
errorNotifyText,
|
|
||||||
{ actionName: 'Получение данных по скважине', well }
|
|
||||||
)
|
|
||||||
}, [dateRange])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setChildrenData(data.map((item) => (
|
|
||||||
<Option key={item.subsystemName}>
|
|
||||||
{item.subsystemName}
|
|
||||||
</Option>
|
|
||||||
)))
|
|
||||||
}, [data])
|
|
||||||
|
|
||||||
const selectChange = useCallback((value: string[]) => {
|
|
||||||
setSelectedData(selectedData.reduce((previousValue: DataType[], currentValue) => {
|
|
||||||
if (value.includes(currentValue.subsystemName)) {
|
|
||||||
previousValue.push(currentValue)
|
|
||||||
}
|
|
||||||
return previousValue
|
|
||||||
}, []))
|
|
||||||
|
|
||||||
},[data]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LoaderPortal show={showLoader}>
|
|
||||||
<h3 className={'filter-group-heading'}>Фильтр подсистем</h3>
|
|
||||||
<Row align={'middle'} justify={'space-between'} wrap={false} style={{ backgroundColor: 'white' }}>
|
|
||||||
<Col span={18}>
|
|
||||||
<Select
|
|
||||||
allowClear
|
|
||||||
mode="multiple"
|
|
||||||
value={selectedData.map(d => d.subsystemName)}
|
|
||||||
onChange={selectChange}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
>
|
|
||||||
{childrenData}
|
|
||||||
</Select>
|
|
||||||
</Col>
|
|
||||||
<Col span={6}>
|
|
||||||
<DateRangeWrapper
|
|
||||||
allowClear
|
|
||||||
allowEmpty={[true, true]}
|
|
||||||
onCalendarChange={(dateRange: any) => setDateRange(dateRange)}
|
|
||||||
value={[dateRange && dateRange[0], dateRange && dateRange[1]]}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<D3HorizontalChart
|
|
||||||
data={selectedData.map(item => ({name: item.subsystemName, percent: item.kUsage * 100}))}
|
|
||||||
colors={subsystemColors}
|
|
||||||
selected={selected}
|
|
||||||
width={'100%'}
|
|
||||||
height={'50vh'}
|
|
||||||
onMouseOver={onMouseOver}
|
|
||||||
onMouseOut={onMouseOut}
|
|
||||||
/>
|
|
||||||
<Table
|
|
||||||
size={'small'}
|
|
||||||
columns={tableColumns}
|
|
||||||
dataSource={selectedData.map((d, i) => ({...d, color: subsystemColors[i]}))}
|
|
||||||
scroll={{ y: '25vh', x: true }}
|
|
||||||
pagination={false}
|
|
||||||
onRow={onRow}
|
|
||||||
/>
|
|
||||||
</LoaderPortal>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default wrapPrivateComponent(OperationTime, {
|
|
||||||
requirements: [],
|
|
||||||
title: 'Наработка',
|
|
||||||
route: 'operation_time',
|
|
||||||
})
|
|
@ -9,7 +9,7 @@ import { MessageService } from '@api'
|
|||||||
|
|
||||||
import { makeMessageColumns } from '../Messages'
|
import { makeMessageColumns } from '../Messages'
|
||||||
|
|
||||||
import '@styles/message.css'
|
import '@styles/message.less'
|
||||||
|
|
||||||
export const ActiveMessagesOnline = memo(({ well: givenWell }) => {
|
export const ActiveMessagesOnline = memo(({ well: givenWell }) => {
|
||||||
const [messages, setMessages] = useState([])
|
const [messages, setMessages] = useState([])
|
||||||
|
@ -32,14 +32,14 @@ import SpinPicEnabled from '@images/SpinEnabled.png'
|
|||||||
import SpinPicDisabled from '@images/SpinDisabled.png'
|
import SpinPicDisabled from '@images/SpinDisabled.png'
|
||||||
|
|
||||||
import '@styles/monitoring.less'
|
import '@styles/monitoring.less'
|
||||||
import '@styles/message.css'
|
import '@styles/message.less'
|
||||||
|
|
||||||
const { Option } = Select
|
const { Option } = Select
|
||||||
|
|
||||||
export const yAxis = {
|
export const yAxis = {
|
||||||
type: 'time',
|
type: 'time',
|
||||||
accessor: (d) => new Date(d.date),
|
accessor: (d) => new Date(d.date),
|
||||||
format: (d) => formatDate(d, undefined, 'YYYY-MM-DD HH:mm:ss'),
|
format: (d) => formatDate(d, undefined, 'DD.MM.YYYY HH:mm:ss'),
|
||||||
}
|
}
|
||||||
|
|
||||||
const dash = [7, 3]
|
const dash = [7, 3]
|
||||||
|
@ -1,18 +1,4 @@
|
|||||||
@import '~antd/dist/antd.less';
|
|
||||||
@import './loader.css';
|
@import './loader.css';
|
||||||
/*
|
|
||||||
* ЭТО ФАЙЛ НАСТРОЙКИ ТЕМЫ И КОМПОНЕНТОВ ТЕМЫ.
|
|
||||||
* НЕ ПИШИТЕ ТУТ СТИЛИ ДЛЯ КАСТОМНЫХ КОМПОНЕНТОВ.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Переменные для темы тут:
|
|
||||||
// https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less
|
|
||||||
|
|
||||||
//@primary-color: rgba(124, 124, 124, 0.3);
|
|
||||||
@primary-color: rgb(195, 40,40);
|
|
||||||
//@primary-color:rgb(65, 63, 61);
|
|
||||||
//@layout-header-background: rgb(195, 40,40);
|
|
||||||
@layout-header-background: rgb(65, 63, 61);
|
|
||||||
|
|
||||||
@header-height: 64px;
|
@header-height: 64px;
|
||||||
@layout-min-height: calc(100vh - @header-height);
|
@layout-min-height: calc(100vh - @header-height);
|
||||||
@ -154,9 +140,3 @@ html {
|
|||||||
tr.table_row_size {
|
tr.table_row_size {
|
||||||
height: 10px;
|
height: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-selector {
|
|
||||||
width: 25%;
|
|
||||||
margin-right: 5px;
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
|
@ -116,9 +116,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-line line {
|
.grid-line:not(:first-of-type):not(:last-of-type) line {
|
||||||
stroke: #ddd;
|
stroke: #ddd;
|
||||||
stroke-dasharray: 4
|
stroke-dasharray: 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1800px) {
|
@media (max-width: 1800px) {
|
||||||
|
22
src/styles/filter.less
Normal file
22
src/styles/filter.less
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
.filter-group {
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
|
||||||
|
& .head {
|
||||||
|
margin: 5px auto;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .body {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
& .type-filter {
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .filter-selector {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
src/styles/include/antd_theme.less
Normal file
14
src/styles/include/antd_theme.less
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
@import '~antd/dist/antd.less';
|
||||||
|
/*
|
||||||
|
* ЭТО ФАЙЛ НАСТРОЙКИ ТЕМЫ И КОМПОНЕНТОВ ТЕМЫ.
|
||||||
|
* НЕ ПИШИТЕ ТУТ СТИЛИ ДЛЯ КАСТОМНЫХ КОМПОНЕНТОВ.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Переменные для темы тут:
|
||||||
|
// https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less
|
||||||
|
|
||||||
|
//@primary-color: rgba(124, 124, 124, 0.3);
|
||||||
|
@primary-color: rgb(195, 40,40);
|
||||||
|
//@primary-color:rgb(65, 63, 61);
|
||||||
|
//@layout-header-background: rgb(195, 40,40);
|
||||||
|
@layout-header-background: rgb(65, 63, 61);
|
9
src/styles/message.css → src/styles/message.less
Executable file → Normal file
9
src/styles/message.css → src/styles/message.less
Executable file → Normal file
@ -39,15 +39,6 @@
|
|||||||
background: #505060;
|
background: #505060;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-group {
|
|
||||||
margin: 0 0 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-group-heading {
|
|
||||||
margin: 5px auto;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
td.ant-table-column-sort {
|
td.ant-table-column-sort {
|
||||||
color: black;
|
color: black;
|
||||||
background-color: #fafafa;
|
background-color: #fafafa;
|
3
src/styles/operation_time.less
Normal file
3
src/styles/operation_time.less
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.table_color {
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
@ -3,6 +3,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
|
min-height: 80vh;
|
||||||
|
|
||||||
.tvd-top {
|
.tvd-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -57,7 +58,6 @@
|
|||||||
left: 55px;
|
left: 55px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tvd-right {
|
.tvd-right {
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
* @param data Копируемые данные
|
* @param data Копируемые данные
|
||||||
* @returns Полная копия `data`
|
* @returns Полная копия `data`
|
||||||
*/
|
*/
|
||||||
export const deepCopy = <T extends any>(data: T): T => JSON.parse(JSON.stringify(data ?? null))
|
export const deepCopy = <T,>(data: T): T => JSON.parse(JSON.stringify(data ?? null))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Маппинг полей объекта
|
* Маппинг полей объекта
|
||||||
|
@ -21,7 +21,7 @@ export const getArrayFromLocalStorage = <T extends string = string>(name: string
|
|||||||
return raw.split(sep).map<T>(elm => elm as T)
|
return raw.split(sep).map<T>(elm => elm as T)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getJSON = <T extends any>(name: StorageNames): T | null => {
|
export const getJSON = <T,>(name: StorageNames): T | null => {
|
||||||
const raw = localStorage.getItem(name)
|
const raw = localStorage.getItem(name)
|
||||||
if (!raw) return null
|
if (!raw) return null
|
||||||
try {
|
try {
|
||||||
@ -30,7 +30,7 @@ export const getJSON = <T extends any>(name: StorageNames): T | null => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setJSON = <T extends any>(name: StorageNames, data: T | null): boolean => {
|
export const setJSON = <T,>(name: StorageNames, data: T | null): boolean => {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(name, JSON.stringify(data))
|
localStorage.setItem(name, JSON.stringify(data))
|
||||||
return true
|
return true
|
||||||
|
@ -9,7 +9,7 @@ export type TableColumnSettings = {
|
|||||||
export type TableSettings = Record<string, TableColumnSettings>
|
export type TableSettings = Record<string, TableColumnSettings>
|
||||||
export type TableSettingsStore = Record<string, TableSettings | null>
|
export type TableSettingsStore = Record<string, TableSettings | null>
|
||||||
|
|
||||||
export const makeTableSettings = (columns: TableColumns): TableSettings => {
|
export const makeTableSettings = <T extends object>(columns: TableColumns<T>): TableSettings => {
|
||||||
const settings: TableSettings = {}
|
const settings: TableSettings = {}
|
||||||
columns.forEach((column) => {
|
columns.forEach((column) => {
|
||||||
if (!column.key) return
|
if (!column.key) return
|
||||||
@ -47,8 +47,8 @@ export const optimizeTableColumn = (column: TableColumnSettings): TableColumnSet
|
|||||||
visible: column.visible ?? true,
|
visible: column.visible ?? true,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const applyTableSettings = (columns: TableColumns, settings: TableSettings): TableColumns => {
|
export const applyTableSettings = <T extends object>(columns: TableColumns<T>, settings: TableSettings): TableColumns<T> => {
|
||||||
let newColumns: TableColumns = columns.map((column) => ({ ...column }))
|
let newColumns: TableColumns<T> = columns.map((column) => ({ ...column }))
|
||||||
newColumns = newColumns.filter((column) => {
|
newColumns = newColumns.filter((column) => {
|
||||||
const name = String(column.key)
|
const name = String(column.key)
|
||||||
return !(name in settings) || settings[name]?.visible
|
return !(name in settings) || settings[name]?.visible
|
||||||
|
106
webpack.config.base.js
Normal file
106
webpack.config.base.js
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
const path = require('path')
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||||
|
const InterpolateHtmlPlugin = require('interpolate-html-plugin')
|
||||||
|
|
||||||
|
const proxy = require('./package.json').proxy
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: './src/index.tsx',
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'build'),
|
||||||
|
filename: '[name].[contenthash:8].js',
|
||||||
|
publicPath: '/',
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
historyApiFallback: true,
|
||||||
|
port: 3000,
|
||||||
|
proxy: {
|
||||||
|
context: ['/api', '/auth', '/hubs'],
|
||||||
|
target: proxy,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
splitChunks: {
|
||||||
|
cacheGroups: {
|
||||||
|
commons: {
|
||||||
|
test: /[\\/]node_modules[\\/]/,
|
||||||
|
name: 'vendors',
|
||||||
|
chunks: 'all',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
removeAvailableModules: true,
|
||||||
|
removeEmptyChunks: true,
|
||||||
|
mergeDuplicateChunks: true,
|
||||||
|
providedExports: true,
|
||||||
|
runtimeChunk: true,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
fallback: { 'path': require.resolve('path-browserify') }, // TODO: Remove
|
||||||
|
modules: [path.join(__dirname, 'src'), 'node_modules'],
|
||||||
|
alias: {
|
||||||
|
react: path.join(__dirname, 'node_modules', 'react'),
|
||||||
|
|
||||||
|
'@asb': path.resolve(__dirname, 'src'),
|
||||||
|
'@api': path.resolve(__dirname, 'src/services/api'),
|
||||||
|
'@components': path.resolve(__dirname, 'src/components'),
|
||||||
|
'@services': path.resolve(__dirname, 'src/services'),
|
||||||
|
'@pages': path.resolve(__dirname, 'src/pages'),
|
||||||
|
'@utils': path.resolve(__dirname, 'src/utils'),
|
||||||
|
'@images': path.resolve(__dirname, 'src/images'),
|
||||||
|
'@styles': path.resolve(__dirname, 'src/styles'),
|
||||||
|
},
|
||||||
|
extensions: [ '', '.tsx', '.ts', '.jsx', '.js', '.json', '.d.ts', '.svg', '.png' ],
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
enforce: 'pre',
|
||||||
|
use: [ 'source-map-loader' ],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.m?jsx?$/i,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: [ 'babel-loader' ],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.m?tsx?$/i,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: [ 'babel-loader', 'ts-loader' ],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(jpe?g|gif|png)$/i,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'file-loader',
|
||||||
|
options: {
|
||||||
|
publicPath: '/images/',
|
||||||
|
outputPath: 'images',
|
||||||
|
name: '[name].[contenthash:8].[ext]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.svg$/i,
|
||||||
|
issuer: /\.m?[jt]sx?$/,
|
||||||
|
use: [
|
||||||
|
'@svgr/webpack',
|
||||||
|
{
|
||||||
|
loader: 'file-loader',
|
||||||
|
options: {
|
||||||
|
publicPath: '/images/svg/',
|
||||||
|
outputPath: 'images/svg',
|
||||||
|
name: '[name].[contenthash:8].[ext]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new InterpolateHtmlPlugin({ PUBLIC_URL: 'static' }),
|
||||||
|
new HtmlWebpackPlugin({ template: './public/index.html' }),
|
||||||
|
],
|
||||||
|
}
|
76
webpack.config.dev.js
Normal file
76
webpack.config.dev.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
const { HotModuleReplacementPlugin, SourceMapDevToolPlugin } = require('webpack')
|
||||||
|
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
|
||||||
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||||
|
const TerserPlugin = require('terser-webpack-plugin')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const lessLoader = {
|
||||||
|
loader: 'less-loader',
|
||||||
|
options: {
|
||||||
|
lessOptions: {
|
||||||
|
javascriptEnabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
cache: true,
|
||||||
|
performance: false,
|
||||||
|
mode: 'development',
|
||||||
|
devtool: 'source-map',
|
||||||
|
optimization: {
|
||||||
|
minimizer: [
|
||||||
|
new CssMinimizerPlugin(),
|
||||||
|
new TerserPlugin(),
|
||||||
|
],
|
||||||
|
flagIncludedChunks: false,
|
||||||
|
usedExports: false,
|
||||||
|
sideEffects: false,
|
||||||
|
concatenateModules: false,
|
||||||
|
emitOnErrors: true,
|
||||||
|
nodeEnv: 'development',
|
||||||
|
minimize: false,
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'dev_build'),
|
||||||
|
pathinfo: true,
|
||||||
|
},
|
||||||
|
stats: 'errors-warnings',
|
||||||
|
module: {
|
||||||
|
unsafeCache: true,
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.css$/i,
|
||||||
|
use: [
|
||||||
|
MiniCssExtractPlugin.loader,
|
||||||
|
'css-loader'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.less$/i,
|
||||||
|
exclude: path.resolve(__dirname, 'src/styles/include/'),
|
||||||
|
use: [
|
||||||
|
MiniCssExtractPlugin.loader,
|
||||||
|
'css-loader',
|
||||||
|
lessLoader,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.less$/i,
|
||||||
|
include: path.resolve(__dirname, 'src/styles/include/'),
|
||||||
|
use: [
|
||||||
|
'style-loader',
|
||||||
|
'css-loader',
|
||||||
|
lessLoader,
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: '[name].[contenthash:8].css',
|
||||||
|
}),
|
||||||
|
new HotModuleReplacementPlugin(),
|
||||||
|
new SourceMapDevToolPlugin({ filename: 'maps/[file].map' }),
|
||||||
|
],
|
||||||
|
}
|
@ -1,141 +1,55 @@
|
|||||||
|
const { merge } = require('webpack-merge')
|
||||||
|
const process = require('process')
|
||||||
|
const colors = require('colors')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
const fs = require('fs')
|
||||||
const { HotModuleReplacementPlugin, SourceMapDevToolPlugin } = require('webpack')
|
|
||||||
const InterpolateHtmlPlugin = require('interpolate-html-plugin')
|
|
||||||
|
|
||||||
const proxy = require('./package.json').proxy
|
colors.enable()
|
||||||
|
|
||||||
module.exports = {
|
const baseConfigPath = path.resolve(__dirname, 'webpack.config.base.js')
|
||||||
entry: './src/index.tsx',
|
|
||||||
devtool: 'source-map',
|
|
||||||
output: {
|
|
||||||
path: path.resolve(__dirname, 'build'),
|
|
||||||
filename: '[name].[contenthash:8].js',
|
|
||||||
publicPath: '/',
|
|
||||||
},
|
|
||||||
optimization: {
|
|
||||||
splitChunks: {
|
|
||||||
cacheGroups: {
|
|
||||||
commons: {
|
|
||||||
test: /[\\/]node_modules[\\/]/,
|
|
||||||
name: 'vendors',
|
|
||||||
chunks: 'all',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
fallback: { 'path': require.resolve('path-browserify') }, // TODO: Remove
|
|
||||||
modules: [path.join(__dirname, 'src'), 'node_modules'],
|
|
||||||
alias: {
|
|
||||||
react: path.join(__dirname, 'node_modules', 'react'),
|
|
||||||
|
|
||||||
'@asb': path.resolve(__dirname, 'src'),
|
const quit = (error = false) => {
|
||||||
'@api': path.resolve(__dirname, 'src/services/api'),
|
console.log('Exiting...')
|
||||||
'@components': path.resolve(__dirname, 'src/components'),
|
process.exit(error ? 1 : 0)
|
||||||
'@services': path.resolve(__dirname, 'src/services'),
|
}
|
||||||
'@pages': path.resolve(__dirname, 'src/pages'),
|
|
||||||
'@utils': path.resolve(__dirname, 'src/utils'),
|
module.exports = (env) => {
|
||||||
'@images': path.resolve(__dirname, 'src/images'),
|
if (!fs.existsSync(path.resolve(__dirname, 'src/services/api'))) {
|
||||||
'@styles': path.resolve(__dirname, 'src/styles'),
|
console.error(`Error: Services not found! Generate it first!`.bold.red)
|
||||||
},
|
console.info('To generate services you can use node scripts.')
|
||||||
extensions: [ '', '.tsx', '.ts', '.jsx', '.js', '.json', '.d.ts', '.svg', '.png' ],
|
console.info('For example: `npm run oug_dev` for generate services from dev-server.')
|
||||||
},
|
|
||||||
devServer: {
|
console.error(`Ошибка: Сервисы не найдены! Сначала сгенерируйте их!`.bold.red)
|
||||||
historyApiFallback: true,
|
console.info('Для генерации сервисов воспользуйтесь node-скриптами.')
|
||||||
port: 3000,
|
console.info('Например: `npm run oug_dev` для генерации от dev-сервера.')
|
||||||
proxy: {
|
quit(1)
|
||||||
context: ['/api', '/auth', '/hubs'],
|
}
|
||||||
target: proxy,
|
|
||||||
},
|
if (!fs.existsSync(baseConfigPath)) {
|
||||||
},
|
console.error(`Error: Base webpack config file not found!`.bold.red)
|
||||||
stats: 'errors-warnings',
|
console.error(`Ошибка: базовый конфигурационный файл webpack не найден!`.bold.red)
|
||||||
// {
|
quit(1)
|
||||||
// colors: true,
|
}
|
||||||
// hash: false,
|
|
||||||
// version: false,
|
const mode = String(env.ENV)
|
||||||
// timings: false,
|
const configPath = path.resolve(__dirname, `webpack.config.${env.ENV}.js`)
|
||||||
// assets: false,
|
|
||||||
// chunks: false,
|
if (!fs.existsSync(configPath)) {
|
||||||
// modules: false,
|
console.error(`Error: Webpack config file for mode "${mode.underline}" not found!`.bold.red)
|
||||||
// reasons: false,
|
console.error(`Ошибка: конфигурационный файл webpack для режима "${mode.underline}" не найден!`.bold.red)
|
||||||
// children: false,
|
quit(1)
|
||||||
// source: false,
|
}
|
||||||
// errors: false,
|
|
||||||
// errorDetails: false,
|
console.info('Make sure you update your npm packages and services!'.bold.yellow)
|
||||||
// warnings: false,
|
console.info('Убедитесь, что вы обновили npm-пакеты и сервисы!'.bold.yellow)
|
||||||
// publicPath: false,
|
|
||||||
// },
|
console.info(`Building in mode: ${String(env.ENV).blue.bold}`)
|
||||||
module: {
|
console.info(`Using base config: ${baseConfigPath.underline.italic}`)
|
||||||
rules: [
|
console.info(`Using main config: ${configPath.underline.italic}`)
|
||||||
{
|
|
||||||
test: /\.js$/,
|
console.info(`Собрка в режиме: ${String(env.ENV).blue.bold}`)
|
||||||
enforce: 'pre',
|
console.info(`Базовый конфигурационный файл: ${baseConfigPath.underline.italic}`)
|
||||||
use: [ 'source-map-loader' ],
|
console.info(`Режимный конфигурационный файл: ${configPath.underline.italic}`)
|
||||||
},
|
|
||||||
{
|
return merge(require(baseConfigPath), require(configPath))
|
||||||
test: /\.m?jsx?$/i,
|
|
||||||
exclude: /node_modules/,
|
|
||||||
use: [ 'babel-loader' ],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.m?tsx?$/i,
|
|
||||||
exclude: /node_modules/,
|
|
||||||
use: [ 'babel-loader', 'ts-loader' ],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.css$/i,
|
|
||||||
use: [ 'style-loader', 'css-loader' ],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.less$/i,
|
|
||||||
use: [
|
|
||||||
'style-loader',
|
|
||||||
'css-loader',
|
|
||||||
{
|
|
||||||
loader: 'less-loader',
|
|
||||||
options: {
|
|
||||||
lessOptions: {
|
|
||||||
javascriptEnabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(jpe?g|gif|png)$/i,
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: 'file-loader',
|
|
||||||
options: {
|
|
||||||
publicPath: '/images/',
|
|
||||||
outputPath: 'images',
|
|
||||||
name: '[name].[contenthash:8].[ext]',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.svg$/i,
|
|
||||||
issuer: /\.m?[jt]sx?$/,
|
|
||||||
use: [
|
|
||||||
'@svgr/webpack',
|
|
||||||
{
|
|
||||||
loader: 'file-loader',
|
|
||||||
options: {
|
|
||||||
publicPath: '/images/svg/',
|
|
||||||
outputPath: 'images/svg',
|
|
||||||
name: '[name].[contenthash:8].[ext]',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new HotModuleReplacementPlugin(),
|
|
||||||
new InterpolateHtmlPlugin({ PUBLIC_URL: 'static' }),
|
|
||||||
new SourceMapDevToolPlugin({ filename: '[file].map' }),
|
|
||||||
new HtmlWebpackPlugin({ template: './public/index.html' }),
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
58
webpack.config.prod.js
Normal file
58
webpack.config.prod.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
const path = require('path')
|
||||||
|
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
|
||||||
|
const TerserPlugin = require('terser-webpack-plugin')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mode: 'production',
|
||||||
|
performance: {
|
||||||
|
assetFilter: (assetFilename) => !/\.map$/.test(assetFilename),
|
||||||
|
hints: 'error',
|
||||||
|
maxAssetSize: 300 * 1024, // 300KB
|
||||||
|
},
|
||||||
|
cache: false,
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'prod_build'),
|
||||||
|
pathinfo: false,
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
minimizer: [
|
||||||
|
new CssMinimizerPlugin(),
|
||||||
|
new TerserPlugin(),
|
||||||
|
],
|
||||||
|
flagIncludedChunks: true,
|
||||||
|
usedExports: true,
|
||||||
|
sideEffects: true,
|
||||||
|
concatenateModules: true,
|
||||||
|
emitOnErrors: false,
|
||||||
|
nodeEnv: 'production',
|
||||||
|
minimize: true,
|
||||||
|
},
|
||||||
|
stats: 'errors-warnings',
|
||||||
|
module: {
|
||||||
|
unsafeCache: false,
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.css$/i,
|
||||||
|
use: [
|
||||||
|
'style-loader',
|
||||||
|
'css-loader'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.less$/i,
|
||||||
|
use: [
|
||||||
|
'style-loader',
|
||||||
|
'css-loader',
|
||||||
|
{
|
||||||
|
loader: 'less-loader',
|
||||||
|
options: {
|
||||||
|
lessOptions: {
|
||||||
|
javascriptEnabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user