This commit is contained in:
@kirill.komarov
2026-05-11 20:42:26 +05:00
parent 212484d062
commit 130c12a1d3
9 changed files with 48 additions and 9 deletions
+10 -1
View File
@@ -1,6 +1,7 @@
import type { Category, Product } from '@/entities/product/model/types'
import { apiClient } from '@/shared/api/client'
import { apiBaseURL } from '@/shared/config'
import { ADMIN_UPLOAD_IMAGE_MAX_BYTES, formatAdminImageMaxSizeHint } from '@/shared/constants/upload-limits'
export type PublicProductsResponse = {
items: Product[]
@@ -119,8 +120,16 @@ export async function deleteAdminCategory(id: string): Promise<void> {
/** FormData: не задавать Content-Type вручную (boundary задаёт браузер). */
export async function uploadAdminProductImages(files: FileList | readonly File[]): Promise<string[]> {
const list = Array.from(files)
for (const f of list) {
if (f.size > ADMIN_UPLOAD_IMAGE_MAX_BYTES) {
throw new Error(
`Файл «${f.name}» слишком большой (максимум ${formatAdminImageMaxSizeHint()} на одно изображение).`,
)
}
}
const fd = new FormData()
for (const f of Array.from(files)) {
for (const f of list) {
fd.append('files', f, f.name)
}
const token = localStorage.getItem('craftshop_auth_token')
@@ -11,6 +11,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { fetchAdminCatalogSlider } from '@/entities/catalog-slider/api/catalog-slider-api'
import { deleteGalleryImage, fetchAdminGallery } from '@/entities/gallery/api/gallery-api'
import { uploadAdminProductImages } from '@/entities/product/api/product-api'
import { formatAdminImageMaxSizeHint } from '@/shared/constants/upload-limits'
import { invalidateQueryKeys } from '@/shared/lib/invalidate-query-keys'
import { GallerySliderSection } from './GallerySliderSection'
@@ -80,6 +81,10 @@ export function AdminGalleryPage() {
<Divider sx={{ mb: 3 }} />
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Форматы: PNG, JPEG, WebP. На один файл до {formatAdminImageMaxSizeHint()}.
</Typography>
<Stack direction="row" spacing={2} sx={{ mb: 3, alignItems: 'center', flexWrap: 'wrap' }}>
<Button variant="contained" component="label" disabled={uploadMut.isPending}>
Загрузить файлы
+3 -1
View File
@@ -40,6 +40,7 @@ import {
uploadAdminProductImages,
} from '@/entities/product/api/product-api'
import type { Category, Product } from '@/entities/product/model/types'
import { formatAdminImageMaxSizeHint } from '@/shared/constants/upload-limits'
import { formatPriceRub } from '@/shared/lib/format-price'
import { getErrorMessage } from '@/shared/lib/get-error-message'
import { invalidateQueryKeys } from '@/shared/lib/invalidate-query-keys'
@@ -535,7 +536,8 @@ export function AdminPage() {
Фото (загрузка)
</Typography>
<FormHelperText sx={{ mt: 0, mb: 1 }}>
Крестик на превью убирает фото только из карточки; файл остаётся на сервере и в галерее.
PNG, JPEG или WebP, до {formatAdminImageMaxSizeHint()} на файл. Крестик на превью убирает фото только из
карточки; файл остаётся на сервере и в галерее.
</FormHelperText>
<Box
sx={{
@@ -0,0 +1,6 @@
/** Должно совпадать с `getProductImageMaxFileBytes()` на сервере (по умолчанию 20 МБ). */
export const ADMIN_UPLOAD_IMAGE_MAX_BYTES = 20 * 1024 * 1024
export function formatAdminImageMaxSizeHint(): string {
return `${Math.round(ADMIN_UPLOAD_IMAGE_MAX_BYTES / (1024 * 1024))} МБ`
}
+1 -1
View File
@@ -23,7 +23,7 @@
Актуально для загрузок файлов (если нужны нестандартные лимиты):
- `PRODUCT_IMAGE_MAX_FILE_BYTES`фото товаров в админке (по умолчанию 20 МБ).
- `ADMIN_IMAGE_MAX_FILE_BYTES` (или устаревшее `PRODUCT_IMAGE_MAX_FILE_BYTES`)одно изображение в админке: товары, галерея (по умолчанию 20 МБ).
- `OTHER_UPLOAD_MAX_FILE_BYTES` — отзывы, чек оплаты и т.п. (по умолчанию 2 МБ).
- `MAX_UPLOAD_BODY_BYTES` — весь POST multipart (по умолчанию рассчитывается от лимита фото товара × 10 + запас).
+4 -2
View File
@@ -3,8 +3,10 @@ PORT=3333
ADMIN_EMAIL=admin@example.com
JWT_SECRET=замените-на-секрет-jwt
# Загрузки (байты). Фото товара в админке: по умолчанию 20 МБ; отзывы/чек и т.п.: 2 МБ.
# PRODUCT_IMAGE_MAX_FILE_BYTES=20971520
# Загрузки (байты). Админ: одно изображение (товары, галерея) — по умолчанию 20 МБ.
# ADMIN_IMAGE_MAX_FILE_BYTES=20971520
# (устаревшее имя, то же значение) PRODUCT_IMAGE_MAX_FILE_BYTES=20971520
# Отзывы, чек оплаты и т.п.: 2 МБ.
# OTHER_UPLOAD_MAX_FILE_BYTES=2097152
# MAX_UPLOAD_BODY_BYTES=… — весь POST multipart при необходимости
+1
View File
@@ -36,6 +36,7 @@ await fastify.register(jwt, {
await fastify.register(multipart, {
limits: {
files: 10,
/** Совпадает с лимитом одного файла для `POST /api/admin/uploads` (товары, галерея). */
fileSize: getProductImageMaxFileBytes(),
},
})
+18 -4
View File
@@ -1,14 +1,28 @@
const MB = 1024 * 1024
/** Фото товаров в админке (на файл). По умолчанию 20 МБ. */
export const PRODUCT_IMAGE_MAX_FILE_BYTES = 20 * MB
/**
* Один файл изображения в админке: товары, галерея (`POST /api/admin/uploads`).
* Должно совпадать с лимитом плагина multipart в `server/src/index.js`.
*/
export const ADMIN_UPLOAD_IMAGE_MAX_FILE_BYTES_DEFAULT = 20 * MB
/** @deprecated используйте ADMIN_UPLOAD_IMAGE_MAX_FILE_BYTES_DEFAULT; оставлено для совместимости импортов */
export const PRODUCT_IMAGE_MAX_FILE_BYTES = ADMIN_UPLOAD_IMAGE_MAX_FILE_BYTES_DEFAULT
/** Отзывы, чек оплаты и прочие загрузки (на файл). По умолчанию 2 МБ. */
export const OTHER_UPLOAD_MAX_FILE_BYTES = 2 * MB
/** Лимит одного файла для админских изображений (байты). Env: `ADMIN_IMAGE_MAX_FILE_BYTES` или `PRODUCT_IMAGE_MAX_FILE_BYTES`. */
export function getProductImageMaxFileBytes() {
const n = Number(process.env.PRODUCT_IMAGE_MAX_FILE_BYTES)
return Number.isFinite(n) && n > 0 ? Math.floor(n) : PRODUCT_IMAGE_MAX_FILE_BYTES
const fromAdmin = Number(process.env.ADMIN_IMAGE_MAX_FILE_BYTES)
const fromLegacy = Number(process.env.PRODUCT_IMAGE_MAX_FILE_BYTES)
const n =
Number.isFinite(fromAdmin) && fromAdmin > 0
? fromAdmin
: Number.isFinite(fromLegacy) && fromLegacy > 0
? fromLegacy
: NaN
return Number.isFinite(n) && n > 0 ? Math.floor(n) : ADMIN_UPLOAD_IMAGE_MAX_FILE_BYTES_DEFAULT
}
export function getOtherUploadMaxFileBytes() {
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 MiB