Compare commits
2 Commits
b1e79e1e2b
...
5e77a63aff
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e77a63aff | ||
|
|
97d6d9adc6 |
18
Dockerfile
18
Dockerfile
@@ -1,3 +1,15 @@
|
|||||||
FROM eclipse-temurin:26-jre-alpine
|
# Этап 1: сборка jar внутри Docker (maven package отдельно запускать не нужно)
|
||||||
COPY torgi-0.0.1-SNAPSHOT.jar /home/torgi-0.0.1-SNAPSHOT.jar
|
FROM maven:3.9-eclipse-temurin-17 AS build
|
||||||
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75", "-XX:InitialRAMPercentage=50", "-Xms512m", "-Xmx4096m", "-jar", "/home/torgi-0.0.1-SNAPSHOT.jar"]
|
WORKDIR /app
|
||||||
|
COPY mvnw pom.xml ./
|
||||||
|
COPY .mvn .mvn
|
||||||
|
RUN chmod +x mvnw && ./mvnw -B dependency:go-offline
|
||||||
|
COPY src ./src
|
||||||
|
RUN ./mvnw -B package -DskipTests
|
||||||
|
|
||||||
|
# Этап 2: запуск (JRE 17 — та же версия, что java.version в pom.xml)
|
||||||
|
FROM eclipse-temurin:17-jre-alpine
|
||||||
|
WORKDIR /home
|
||||||
|
COPY --from=build /app/target/torgi-0.0.1-SNAPSHOT.jar /home/torgi-0.0.1-SNAPSHOT.jar
|
||||||
|
EXPOSE 8181
|
||||||
|
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75", "-XX:InitialRAMPercentage=50", "-Xms512m", "-Xmx4096m", "-jar", "/home/torgi-0.0.1-SNAPSHOT.jar"]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
services:
|
services:
|
||||||
client-backend:
|
client-backend:
|
||||||
image: client:0.0.1
|
image: gitea.rmevs.vip/rm/torgi2-backend:latest
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -44,6 +44,19 @@ services:
|
|||||||
- "5050:80"
|
- "5050:80"
|
||||||
volumes:
|
volumes:
|
||||||
- pgadmin-data:/var/lib/pgadmin
|
- pgadmin-data:/var/lib/pgadmin
|
||||||
|
profiles:
|
||||||
|
- tools
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
image: gitea.rmevs.vip/rm/torgi2-frontend:latest
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
depends_on:
|
||||||
|
- client-backend
|
||||||
|
restart: always
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db-data:
|
db-data:
|
||||||
|
|||||||
2
frontend/.gitignore
vendored
Normal file
2
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
12
frontend/index.html
Normal file
12
frontend/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Торги — события</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
22
frontend/package.json
Normal file
22
frontend/package.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "torgi-frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
|
"typescript": "^5.5.3",
|
||||||
|
"vite": "^5.4.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
frontend/src/App.tsx
Normal file
10
frontend/src/App.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import EventsPage from './pages/EventsPage';
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<main className="container">
|
||||||
|
<h1>Торги — события</h1>
|
||||||
|
<EventsPage />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
frontend/src/api.ts
Normal file
24
frontend/src/api.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { Page, TorgiEvent } from './types';
|
||||||
|
|
||||||
|
// В dev-режиме запросы идут через vite-proxy (см. vite.config.ts),
|
||||||
|
// поэтому BASE по умолчанию пустой. Для продакшна задать VITE_API_URL,
|
||||||
|
// например https://torgi.example.com
|
||||||
|
const BASE = import.meta.env.VITE_API_URL ?? '';
|
||||||
|
|
||||||
|
export interface EventFilter {
|
||||||
|
cadastral?: string;
|
||||||
|
status?: string;
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchEvents(filter: EventFilter): Promise<Page<TorgiEvent>> {
|
||||||
|
const q = new URLSearchParams();
|
||||||
|
if (filter.cadastral) q.set('cadastral', filter.cadastral);
|
||||||
|
if (filter.status) q.set('status', filter.status);
|
||||||
|
q.set('page', String(filter.page));
|
||||||
|
q.set('size', String(filter.size));
|
||||||
|
const res = await fetch(`${BASE}/api/events?${q.toString()}`);
|
||||||
|
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
||||||
|
return (await res.json()) as Page<TorgiEvent>;
|
||||||
|
}
|
||||||
54
frontend/src/index.css
Normal file
54
frontend/src/index.css
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
body {
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
background: #f6f6f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, button {
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 6px 8px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total {
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #b00020;
|
||||||
|
}
|
||||||
13
frontend/src/main.tsx
Normal file
13
frontend/src/main.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { StrictMode } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const root = document.getElementById('root');
|
||||||
|
if (!root) throw new Error('Root element not found');
|
||||||
|
|
||||||
|
createRoot(root).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
116
frontend/src/pages/EventsPage.tsx
Normal file
116
frontend/src/pages/EventsPage.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { fetchEvents } from '../api';
|
||||||
|
import type { Page, TorgiEvent } from '../types';
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20;
|
||||||
|
|
||||||
|
function cell(value: string | number | null | undefined): string {
|
||||||
|
if (value === null || value === undefined || value === '') return '—';
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EventsPage() {
|
||||||
|
const [cadastral, setCadastral] = useState('');
|
||||||
|
const [status, setStatus] = useState('');
|
||||||
|
const [applied, setApplied] = useState({ cadastral: '', status: '' });
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
const [data, setData] = useState<Page<TorgiEvent> | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const load = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const result = await fetchEvents({ ...applied, page, size: PAGE_SIZE });
|
||||||
|
setData(result);
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : 'Unknown error');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [applied, page]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void load();
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
const onSearch = () => {
|
||||||
|
setPage(0);
|
||||||
|
setApplied({ cadastral: cadastral.trim(), status: status.trim() });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div className="filters">
|
||||||
|
<input
|
||||||
|
placeholder="Кадастровый номер"
|
||||||
|
value={cadastral}
|
||||||
|
onChange={(e) => setCadastral(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
placeholder="Статус"
|
||||||
|
value={status}
|
||||||
|
onChange={(e) => setStatus(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button onClick={onSearch} disabled={loading}>
|
||||||
|
Найти
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading && <p>Загрузка…</p>}
|
||||||
|
{error && <p className="error">Ошибка: {error}</p>}
|
||||||
|
|
||||||
|
{data && (
|
||||||
|
<>
|
||||||
|
<p className="total">Всего: {data.totalElements}</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Извещение</th>
|
||||||
|
<th>Лот</th>
|
||||||
|
<th>Кадастр</th>
|
||||||
|
<th>Статус</th>
|
||||||
|
<th>Нач. цена</th>
|
||||||
|
<th>Итог. цена</th>
|
||||||
|
<th>Дата торгов</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.content.map((ev) => (
|
||||||
|
<tr key={ev.id}>
|
||||||
|
<td>{cell(ev.notice)}</td>
|
||||||
|
<td>{cell(ev.lotNumber)}</td>
|
||||||
|
<td>{cell(ev.cadastral)}</td>
|
||||||
|
<td>{cell(ev.status)}</td>
|
||||||
|
<td>{cell(ev.startPrice)}</td>
|
||||||
|
<td>{cell(ev.finalPrice)}</td>
|
||||||
|
<td>{cell(ev.tradesDate)}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{data.content.length === 0 && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={7}>Ничего не найдено</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div className="pagination">
|
||||||
|
<button disabled={page === 0 || loading} onClick={() => setPage((p) => p - 1)}>
|
||||||
|
← Назад
|
||||||
|
</button>
|
||||||
|
<span>
|
||||||
|
Стр. {data.number + 1} из {Math.max(data.totalPages, 1)}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
disabled={data.number + 1 >= data.totalPages || loading}
|
||||||
|
onClick={() => setPage((p) => p + 1)}
|
||||||
|
>
|
||||||
|
Вперед →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
frontend/src/types.ts
Normal file
26
frontend/src/types.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
export interface TorgiEvent {
|
||||||
|
id: number;
|
||||||
|
key: string | null;
|
||||||
|
status: string | null;
|
||||||
|
bidStart: string | null;
|
||||||
|
bidEnd: string | null;
|
||||||
|
tradesDate: string | null;
|
||||||
|
startPrice: string | null;
|
||||||
|
step: string | null;
|
||||||
|
initMoney: string | null;
|
||||||
|
finalPrice: string | null;
|
||||||
|
notice: string | null;
|
||||||
|
lotNumber: number | null;
|
||||||
|
cadastral: string | null;
|
||||||
|
type: string | null;
|
||||||
|
fullInfo: string | null;
|
||||||
|
createdAt: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Page<T> {
|
||||||
|
content: T[];
|
||||||
|
totalElements: number;
|
||||||
|
totalPages: number;
|
||||||
|
number: number;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
1
frontend/src/vite-env.d.ts
vendored
Normal file
1
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
20
frontend/tsconfig.json
Normal file
20
frontend/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
18
frontend/vite.config.ts
Normal file
18
frontend/vite.config.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
// Dev-proxy на бэкенд, чтобы не зависеть от CORS при локальной разработке.
|
||||||
|
// VITE_API_URL имеет приоритет, если задан (например, для продакшн-сборки).
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: process.env.VITE_API_URL ?? 'http://localhost:8080',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
5
pom.xml
5
pom.xml
@@ -83,6 +83,11 @@
|
|||||||
<groupId>org.springframework.retry</groupId>
|
<groupId>org.springframework.retry</groupId>
|
||||||
<artifactId>spring-retry</artifactId>
|
<artifactId>spring-retry</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
|
<version>2.8.17</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.fasterxml.jackson.core</groupId>
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
<artifactId>jackson-databind</artifactId>
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
|||||||
Reference in New Issue
Block a user