deploy
This commit is contained in:
@@ -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}>
|
||||
Загрузить файлы
|
||||
|
||||
@@ -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))} МБ`
|
||||
}
|
||||
@@ -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
@@ -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 при необходимости
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ await fastify.register(jwt, {
|
||||
await fastify.register(multipart, {
|
||||
limits: {
|
||||
files: 10,
|
||||
/** Совпадает с лимитом одного файла для `POST /api/admin/uploads` (товары, галерея). */
|
||||
fileSize: getProductImageMaxFileBytes(),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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 |
Reference in New Issue
Block a user