после загрузки основного функционала
This commit is contained in:
@@ -2,5 +2,5 @@ from StaticHelper.ai_interface import AIInterface
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
ai = AIInterface()
|
ai = AIInterface()
|
||||||
ai.work("")
|
ai.work("")#Создай экран. Используй данные из документации. Создавай className для каждого элемента")
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ export interface PageControl<T>{
|
|||||||
nextPage(): void;
|
nextPage(): void;
|
||||||
previousPage(): void;
|
previousPage(): void;
|
||||||
}
|
}
|
||||||
|
export interface FieldInfo {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
format?: string;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
`types/survey.ts`
|
`types/survey.ts`
|
||||||
```ts
|
```ts
|
||||||
@@ -113,12 +119,10 @@ export interface ContactData{
|
|||||||
phone?: string;
|
phone?: string;
|
||||||
email?:string;
|
email?:string;
|
||||||
}
|
}
|
||||||
interface Role extends UniqueItem{
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
export interface Account extends UniqueItem, ContactData{
|
export interface Account extends UniqueItem, ContactData{
|
||||||
login: string;
|
login: string;
|
||||||
role?: Role;
|
role?: number;
|
||||||
|
roleTitle?: string;
|
||||||
is_staff: boolean;
|
is_staff: boolean;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
}
|
}
|
||||||
@@ -235,6 +239,21 @@ export function ProfessionPage() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Автодокументация
|
||||||
|
`api/hooks.ts`
|
||||||
|
```ts
|
||||||
|
/**
|
||||||
|
* Хук для получения документации swagger JSON и извлечения структур.
|
||||||
|
* @param applicationName Название модуля/приложения.
|
||||||
|
* @param endpointName Название эндпоинта.
|
||||||
|
* @returns Массив структур без ID с полями: название, тип, title, format.
|
||||||
|
*/
|
||||||
|
export function useApiDocumentation(
|
||||||
|
applicationName: string,
|
||||||
|
endpointName: string
|
||||||
|
): { schema: FieldInfo[] | null; loading: boolean; error: Error | null }
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Авторизация
|
### Авторизация
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
|
||||||
|
|
||||||
|
```ts
|
||||||
|
|
||||||
|
import { useState, useMemo, useCallback } from 'react'
|
||||||
|
import { FieldInfo } from '../types/common'
|
||||||
|
import {FiltersState, VisibilityState} from "../types/filters.ts";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Хук для управления фильтрами и видимостью колонок таблицы.
|
||||||
|
* @param schema Схема полей таблицы.
|
||||||
|
* @param initialData
|
||||||
|
* @returns Объекты состояния фильтров и видимости, а также функции для их изменения.
|
||||||
|
*/
|
||||||
|
export function useTableFilters(schema: FieldInfo[]| null, initialData: unknown[] | null) {
|
||||||
|
const [filters, setFilters] = useState<FiltersState>({})
|
||||||
|
const [visibleColumns, setVisibleColumns] = useState<VisibilityState>({})
|
||||||
|
|
||||||
|
// Инициализация видимости колонок
|
||||||
|
useMemo(() => {
|
||||||
|
if (!schema){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialVisibility: VisibilityState = {}
|
||||||
|
schema.forEach(field => {
|
||||||
|
initialVisibility[field.name] = true
|
||||||
|
})
|
||||||
|
setVisibleColumns(initialVisibility)
|
||||||
|
}, [schema])
|
||||||
|
|
||||||
|
// Обновление фильтров
|
||||||
|
const handleFilterChange = useCallback((name: string, filterPart: unknown) => {
|
||||||
|
setFilters(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: {
|
||||||
|
...prev[name],
|
||||||
|
...filterPart
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Переключатель видимости колонки
|
||||||
|
const handleColumnToggle = useCallback((name: string) => {
|
||||||
|
setVisibleColumns(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: !prev[name]
|
||||||
|
}))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Логика фильтрации данных, вынесенная из компонента
|
||||||
|
const filteredData = useMemo(() => {
|
||||||
|
if (!initialData || !schema){
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
return initialData.filter(item => {
|
||||||
|
return schema.every(field => {
|
||||||
|
const value = item[field.name]
|
||||||
|
const filter = filters[field.name]
|
||||||
|
if (!filter) return true
|
||||||
|
|
||||||
|
// Обработка по типу
|
||||||
|
if (field.type === 'string') {
|
||||||
|
if (filter.searchText) {
|
||||||
|
if (typeof value !== 'string') return false
|
||||||
|
if (!value.toLowerCase().includes(filter.searchText.toLowerCase())) return false
|
||||||
|
}
|
||||||
|
if (filter.exactMatch && filter.searchText) {
|
||||||
|
return value.toLowerCase() === filter.searchText.toLowerCase()
|
||||||
|
}
|
||||||
|
if (filter.empty !== undefined) {
|
||||||
|
const isEmpty = value === '' || value === null || value === undefined
|
||||||
|
if (filter.empty && !isEmpty) return false
|
||||||
|
if (filter.notEmpty && isEmpty) return false
|
||||||
|
}
|
||||||
|
} else if (field.format === 'date') {
|
||||||
|
const dateValue = new Date(value as string).getTime()
|
||||||
|
if (filter.fromDate) {
|
||||||
|
const fromTime = new Date(filter.fromDate).getTime()
|
||||||
|
if (dateValue < fromTime) return false
|
||||||
|
}
|
||||||
|
if (filter.toDate) {
|
||||||
|
const toTime = new Date(filter.toDate).getTime()
|
||||||
|
if (dateValue > toTime) return false
|
||||||
|
}
|
||||||
|
} else if (field.type === 'number') {
|
||||||
|
const numValue = Number(value)
|
||||||
|
if (filter.gte !== undefined && numValue < filter.gte) return false
|
||||||
|
if (filter.lte !== undefined && numValue > filter.lte) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [initialData, filters, schema])
|
||||||
|
|
||||||
|
return {
|
||||||
|
filters,
|
||||||
|
setFilters,
|
||||||
|
visibleColumns,
|
||||||
|
setVisibleColumns,
|
||||||
|
handleFilterChange,
|
||||||
|
handleColumnToggle,
|
||||||
|
filteredData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function FilteredTable({ schema, filteredData, visibleColumns}: FilteredTableProps) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="filtered-table">
|
||||||
|
|
||||||
|
|
||||||
|
<table className="filtered-table-main" style={{ overflowX: 'auto', display: 'block' }}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{schema.map(
|
||||||
|
(field) =>
|
||||||
|
visibleColumns[field.name] && (
|
||||||
|
<th key={field.name} className="column-header">
|
||||||
|
{field.title}
|
||||||
|
</th>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
|
<tr key={(item as {id: number}).id}>
|
||||||
|
{schema.map(
|
||||||
|
(field) =>
|
||||||
|
visibleColumns[field.name] && (
|
||||||
|
<td key={field.name} className="data-cell">
|
||||||
|
{item[field.name] ?? "-"}
|
||||||
|
</td>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={schema.length} className="no-data">
|
||||||
|
Нет данных для отображения
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const {
|
||||||
|
filters,
|
||||||
|
handleFilterChange,
|
||||||
|
visibleColumns,
|
||||||
|
handleColumnToggle,
|
||||||
|
filteredData
|
||||||
|
} = useTableFilters(schema, dataPage.items)
|
||||||
|
|
||||||
|
|
||||||
|
const handleExport = async () => {
|
||||||
|
if (!filteredData || filteredData.length === 0) {
|
||||||
|
alert('Нет данных для экспорта')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const blob = new Blob([buffer], {
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
})
|
||||||
|
saveAs(blob, "Отчёт.")
|
||||||
|
};
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
в handleExport сохранять csv
|
||||||
9
StaticHelper/source/temp.md
Normal file
9
StaticHelper/source/temp.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
`API/users.ts`
|
||||||
|
```ts
|
||||||
|
|
||||||
|
export async function login(data: TokenObtainPair): Promise<JwtTokenResponse | null>
|
||||||
|
|
||||||
|
export async function refreshToken(data: TokenRefresh): Promise<JwtTokenResponse | null>
|
||||||
|
|
||||||
|
export async function verifyToken(data: TokenVerify): Promise<boolean>
|
||||||
|
```
|
||||||
@@ -1,95 +1,138 @@
|
|||||||
`API/users.ts`
|
`components/useTableFilters.tsx`
|
||||||
```ts
|
```ts
|
||||||
|
// добавьте новую функцию для экспорта CSV
|
||||||
|
export function useTableFilters(schema: FieldInfo[]| null, initialData: unknown[] | null) {
|
||||||
|
const [filters, setFilters] = useState<FiltersState>({})
|
||||||
|
const [visibleColumns, setVisibleColumns] = useState<VisibilityState>({})
|
||||||
|
|
||||||
export async function login(data: TokenObtainPair): Promise<JwtTokenResponse | null>
|
// Инициализация видимости колонок
|
||||||
|
useMemo(() => {
|
||||||
|
if (!schema){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
export async function refreshToken(data: TokenRefresh): Promise<JwtTokenResponse | null>
|
const initialVisibility: VisibilityState = {}
|
||||||
|
schema.forEach(field => {
|
||||||
|
initialVisibility[field.name] = true
|
||||||
|
})
|
||||||
|
setVisibleColumns(initialVisibility)
|
||||||
|
}, [schema])
|
||||||
|
|
||||||
export async function verifyToken(data: TokenVerify): Promise<boolean>
|
// Обновление фильтров
|
||||||
```
|
const handleFilterChange = useCallback((name: string, filterPart: unknown) => {
|
||||||
|
setFilters(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: {
|
||||||
|
...prev[name],
|
||||||
|
...filterPart
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}, [])
|
||||||
|
|
||||||
`utils/users/UserProvider.tsx`
|
// Переключатель видимости колонки
|
||||||
```tsx
|
const handleColumnToggle = useCallback((name: string) => {
|
||||||
export function UserProvider({ children }: { children: ReactNode }) {
|
setVisibleColumns(prev => ({
|
||||||
const [user, setUser] = useState<Account | undefined>(() => {
|
...prev,
|
||||||
const storedUser = localStorage.getItem('user');
|
[name]: !prev[name]
|
||||||
return storedUser ? JSON.parse(storedUser) : undefined;
|
}))
|
||||||
});
|
}, [])
|
||||||
const [tokens, setTokens] = useState<JwtTokenResponse | undefined>(() => {
|
|
||||||
const storedTokens = localStorage.getItem('tokens');
|
|
||||||
return storedTokens ? JSON.parse(storedTokens) : undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
// Функция экспорта данных в CSV
|
||||||
if (tokens?.access) {
|
const handleExportCSV = useCallback(() => {
|
||||||
localStorage.setItem('tokens', JSON.stringify(tokens));
|
if (!initialData || !schema || initialData.length === 0) {
|
||||||
} else {
|
alert('Нет данных для экспорта')
|
||||||
localStorage.removeItem('tokens');
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleFields = schema.filter(field => visibleColumns[field.name])
|
||||||
|
const header = visibleFields.map(field => `"${field.title.replace(/"/g, '""')}"`).join(';')
|
||||||
|
|
||||||
|
const rows = initialData
|
||||||
|
.filter(item => schema.every(field => {
|
||||||
|
const value = item[field.name]
|
||||||
|
const filter = filters[field.name]
|
||||||
|
if (!filter) return true
|
||||||
|
|
||||||
|
if (field.type === 'string') {
|
||||||
|
if (filter.searchText) {
|
||||||
|
if (typeof value !== 'string') return false
|
||||||
|
if (!value.toLowerCase().includes(filter.searchText.toLowerCase())) return false
|
||||||
|
}
|
||||||
|
if (filter.exactMatch && filter.searchText) {
|
||||||
|
return value.toLowerCase() === filter.searchText.toLowerCase()
|
||||||
|
}
|
||||||
|
if (filter.empty !== undefined) {
|
||||||
|
const isEmpty = value === '' || value === null || value === undefined
|
||||||
|
if (filter.empty && !isEmpty) return false
|
||||||
|
if (filter.notEmpty && isEmpty) return false
|
||||||
|
}
|
||||||
|
} else if (field.format === 'date') {
|
||||||
|
const dateValue = new Date(value as string).getTime()
|
||||||
|
if (filter.fromDate) {
|
||||||
|
const fromTime = new Date(filter.fromDate).getTime()
|
||||||
|
if (dateValue < fromTime) return false
|
||||||
|
}
|
||||||
|
if (filter.toDate) {
|
||||||
|
const toTime = new Date(filter.toDate).getTime()
|
||||||
|
if (dateValue > toTime) return false
|
||||||
|
}
|
||||||
|
} else if (field.type === 'number') {
|
||||||
|
const numValue = Number(value)
|
||||||
|
if (filter.gte !== undefined && numValue < filter.gte) return false
|
||||||
|
if (filter.lte !== undefined && numValue > filter.lte) return false
|
||||||
}
|
}
|
||||||
}, [tokens]);
|
return true
|
||||||
|
}))
|
||||||
|
.map(item => {
|
||||||
|
const row = visibleFields.map(field => {
|
||||||
|
const val = item[field.name]
|
||||||
|
// Обработка null/undefined
|
||||||
|
const strVal = val !== undefined && val !== null ? String(val) : ''
|
||||||
|
// Экранирование кавычек
|
||||||
|
return `"${strVal.replace(/"/g, '""')}"`
|
||||||
|
|
||||||
useEffect(() => {
|
})
|
||||||
if (user) {
|
return row.join(';')
|
||||||
localStorage.setItem('user', JSON.stringify(user));
|
})
|
||||||
} else {
|
|
||||||
localStorage.removeItem('user');
|
|
||||||
}
|
|
||||||
}, [user]);
|
|
||||||
|
|
||||||
const handleLogin = async (credentials: TokenObtainPair) => {
|
const csvContent = [header, ...rows].join('\n')
|
||||||
const tokens = await apiLogin(credentials);
|
// Создаем ссылку для скачивания
|
||||||
if (tokens) {
|
const blob = new Blob([csvContent], { type: 'text/csv' })
|
||||||
setTokens(tokens);
|
const url = URL.createObjectURL(blob)
|
||||||
// Здесь можно добавить запрос для получения данных пользователя
|
const link = document.createElement('a')
|
||||||
setUser({ id: 1, login: credentials.login, is_staff: false, is_active: true } as Account); // TODO
|
link.href = url
|
||||||
}
|
link.download = 'export.csv'
|
||||||
};
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}, [initialData, schema, filters, visibleColumns])
|
||||||
|
|
||||||
const handleLogout = () => {
|
return {
|
||||||
setUser(undefined);
|
filters,
|
||||||
setTokens(undefined);
|
setFilters,
|
||||||
};
|
visibleColumns,
|
||||||
|
setVisibleColumns,
|
||||||
const validateToken = async () => {
|
handleFilterChange,
|
||||||
if (tokens?.access) {
|
handleColumnToggle,
|
||||||
const isValid = await verifyToken({ token: tokens.access });
|
filteredData,
|
||||||
if (!isValid && tokens.refresh) {
|
handleExportCSV // добавьте сюда
|
||||||
const newTokens = await refreshToken({ refresh: tokens.refresh });
|
}
|
||||||
if (newTokens) {
|
|
||||||
setTokens(newTokens);
|
|
||||||
} else {
|
|
||||||
handleLogout();
|
|
||||||
}
|
|
||||||
} else if (!isValid) {
|
|
||||||
handleLogout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
validateToken().then();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UserContext.Provider value={{ user, login: handleLogin, logout: handleLogout }}>
|
|
||||||
{children}
|
|
||||||
</UserContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`Ваш файл, где вызывается handleExport` (примеры использования)
|
||||||
В частности обрати внимание на эту функцию
|
|
||||||
```ts
|
```ts
|
||||||
const handleLogin = async (credentials: TokenObtainPair) => {
|
const {
|
||||||
const tokens = await apiLogin(credentials);
|
filters,
|
||||||
if (tokens) {
|
handleFilterChange,
|
||||||
setTokens(tokens);
|
visibleColumns,
|
||||||
// Здесь можно добавить запрос для получения данных пользователя
|
handleColumnToggle,
|
||||||
setUser({ id: 1, login: credentials.login, is_staff: false, is_active: true } as Account); // TODO
|
filteredData,
|
||||||
}
|
handleExportCSV // новая функция
|
||||||
};
|
} = useTableFilters(schema, dataPage.items)
|
||||||
```
|
|
||||||
|
|
||||||
Воспользуйся эндпоинтами /users/auth/users/me/ для получения id текущего пользователя
|
|
||||||
|
|
||||||
|
const handleExport = async () => {
|
||||||
|
handleExportCSV()
|
||||||
|
}
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user