после загрузки основного функционала

This commit is contained in:
Mikan
2025-06-03 01:48:46 +03:00
parent 6ede1ca15f
commit 5e198c8b56
5 changed files with 345 additions and 85 deletions

View File

@@ -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 для каждого элемента")

View File

@@ -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 }
```
### Авторизация ### Авторизация

View File

@@ -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

View 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>
```

View File

@@ -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()
}
```