2026-05-17 13:45:52 +03:30
|
|
|
import { baseURL, baseURLStorage } from '@root/config'
|
|
|
|
|
import { getConfigApp } from '@/utils/config'
|
|
|
|
|
import type { FileResource } from '@/api/file'
|
|
|
|
|
|
|
|
|
|
export function unwrapApiRecord(payload: unknown): Record<string, any> | null {
|
|
|
|
|
if (!payload || typeof payload !== 'object') return null
|
|
|
|
|
|
|
|
|
|
const data = payload as Record<string, unknown>
|
|
|
|
|
|
|
|
|
|
if (data.data && typeof data.data === 'object') {
|
|
|
|
|
return unwrapApiRecord(data.data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data.product && typeof data.product === 'object') {
|
|
|
|
|
return unwrapApiRecord(data.product)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data.row && typeof data.row === 'object') {
|
|
|
|
|
return unwrapApiRecord(data.row)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return data as Record<string, any>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function unwrapFileResource(payload: unknown): FileResource | null {
|
|
|
|
|
if (!payload || typeof payload !== 'object') return null
|
|
|
|
|
|
|
|
|
|
const data = payload as Record<string, unknown>
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
typeof data.full_src === 'string' ||
|
|
|
|
|
typeof data.src === 'string' ||
|
|
|
|
|
typeof data.path === 'string' ||
|
|
|
|
|
typeof data.url === 'string'
|
|
|
|
|
) {
|
|
|
|
|
return normalizeFileResource(data as Record<string, any>)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data.data && typeof data.data === 'object') {
|
|
|
|
|
return unwrapFileResource(data.data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data.file && typeof data.file === 'object') {
|
|
|
|
|
return unwrapFileResource(data.file)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Site domain + API base, e.g. https://example.ir/server */
|
|
|
|
|
export function getFileBaseUrl(): string {
|
|
|
|
|
const fromConfig =
|
|
|
|
|
getConfigApp('node_server') ||
|
|
|
|
|
getConfigApp('storage_url') ||
|
|
|
|
|
getConfigApp('storage') ||
|
|
|
|
|
getConfigApp('file_url') ||
|
|
|
|
|
getConfigApp('file_base')
|
|
|
|
|
|
|
|
|
|
if (typeof fromConfig === 'string' && fromConfig.trim()) {
|
2026-08-26 16:59:24 +03:30
|
|
|
return preferHttpsFileUrl(fromConfig.replace(/\/$/, ''))
|
2026-05-17 13:45:52 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const storageBase = baseURLStorage || baseURL
|
|
|
|
|
if (storageBase) {
|
2026-08-26 16:59:24 +03:30
|
|
|
return preferHttpsFileUrl(storageBase.replace(/\/$/, ''))
|
2026-05-17 13:45:52 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
|
return `${window.location.origin}/server`.replace(/\/$/, '')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-26 16:59:24 +03:30
|
|
|
/** صفحهٔ ادمین https است؛ URLهای http پشت CDN در پیشنمایش بلاک میشوند. */
|
|
|
|
|
export function preferHttpsFileUrl(url: string): string {
|
|
|
|
|
if (/localhost|127\.0\.0\.1/i.test(url)) return url
|
|
|
|
|
return url.replace(/^http:\/\//i, 'https://')
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 13:45:52 +03:30
|
|
|
/**
|
|
|
|
|
* Turns API paths like "storage/1405/02/27/file.jpg" into a full URL.
|
|
|
|
|
*/
|
|
|
|
|
export function toAbsoluteFileUrl(path?: string | null): string | undefined {
|
|
|
|
|
if (!path?.trim()) return undefined
|
|
|
|
|
|
|
|
|
|
const trimmed = path.trim()
|
|
|
|
|
|
|
|
|
|
if (/^https?:\/\//i.test(trimmed)) {
|
2026-08-26 16:59:24 +03:30
|
|
|
return preferHttpsFileUrl(trimmed)
|
2026-05-17 13:45:52 +03:30
|
|
|
}
|
|
|
|
|
|
2026-07-06 17:06:25 +03:30
|
|
|
const storageMatch = trimmed.match(/(?:^|\/)storage\/(.+)$/i)
|
|
|
|
|
if (storageMatch) {
|
|
|
|
|
const base = getFileBaseUrl()
|
|
|
|
|
if (base) {
|
2026-08-26 16:59:24 +03:30
|
|
|
return preferHttpsFileUrl(`${base}/storage/${storageMatch[1]}`)
|
2026-07-06 17:06:25 +03:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (/^[a-z0-9.-]+(?:\/server)?\/storage\//i.test(trimmed)) {
|
|
|
|
|
return `https://${trimmed.replace(/^\/+/, '')}`
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 13:45:52 +03:30
|
|
|
const base = getFileBaseUrl()
|
|
|
|
|
if (!base) return undefined
|
|
|
|
|
|
|
|
|
|
if (trimmed.startsWith('/')) {
|
2026-08-26 16:59:24 +03:30
|
|
|
return preferHttpsFileUrl(`${base}${trimmed}`)
|
2026-05-17 13:45:52 +03:30
|
|
|
}
|
|
|
|
|
|
2026-08-26 16:59:24 +03:30
|
|
|
return preferHttpsFileUrl(`${base}/${trimmed.replace(/^\//, '')}`)
|
2026-05-17 13:45:52 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isBareFileName(value: string): boolean {
|
|
|
|
|
return !value.includes('/') && /\.[a-z0-9]+$/i.test(value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function normalizeFileResource(record: Record<string, any>): FileResource | null {
|
|
|
|
|
const src =
|
|
|
|
|
typeof record.src === 'string'
|
|
|
|
|
? record.src
|
|
|
|
|
: typeof record.path === 'string'
|
|
|
|
|
? record.path
|
2026-07-07 10:21:12 +03:30
|
|
|
: typeof record.storage_path === 'string'
|
|
|
|
|
? record.storage_path
|
|
|
|
|
: undefined
|
2026-05-17 13:45:52 +03:30
|
|
|
|
|
|
|
|
if (!src && !record.full_src) return null
|
|
|
|
|
|
|
|
|
|
const absoluteUrl = toAbsoluteFileUrl(record.full_src || src)
|
|
|
|
|
if (!absoluteUrl) return null
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: record.id != null ? Number(record.id) : undefined,
|
|
|
|
|
name: record.title || record.original_name || record.name,
|
|
|
|
|
src,
|
|
|
|
|
full_src: absoluteUrl
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Normalize upload API response: { id, src: "storage/...", title, ... } */
|
|
|
|
|
export function normalizeUploadFileResponse(response: unknown): FileResource | null {
|
|
|
|
|
const record = unwrapApiRecord(response)
|
|
|
|
|
if (!record) return null
|
|
|
|
|
return normalizeFileResource(record)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function resolveFileImageUrl(file?: FileResource | null): string | undefined {
|
|
|
|
|
if (!file) return undefined
|
|
|
|
|
if (file.full_src) return file.full_src
|
|
|
|
|
return toAbsoluteFileUrl(file.src || file.path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function pickRawFileFromRecord(record: Record<string, any>): Record<string, any> | null {
|
|
|
|
|
for (const key of ['file', 'original_image', 'image', 'index_image', 'product_image']) {
|
|
|
|
|
const value = record[key]
|
|
|
|
|
if (value && typeof value === 'object') return value as Record<string, any>
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function resolveProductImageSrc(record: Record<string, any>): string | undefined {
|
|
|
|
|
const fromRelation = resolveFileImageUrl(pickImageFromRecord(record))
|
|
|
|
|
if (fromRelation) return fromRelation
|
|
|
|
|
|
|
|
|
|
const rawFile = pickRawFileFromRecord(record)
|
|
|
|
|
if (rawFile?.src) return toAbsoluteFileUrl(rawFile.src)
|
|
|
|
|
|
|
|
|
|
if (typeof record.file_src === 'string') return toAbsoluteFileUrl(record.file_src)
|
|
|
|
|
if (typeof record.image_src === 'string') return toAbsoluteFileUrl(record.image_src)
|
|
|
|
|
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function pickImageFromRecord(record: Record<string, any>): FileResource | null {
|
|
|
|
|
const keys = [
|
|
|
|
|
'file',
|
|
|
|
|
'original_image',
|
|
|
|
|
'image',
|
|
|
|
|
'index_image',
|
|
|
|
|
'product_image',
|
|
|
|
|
'image_file',
|
|
|
|
|
'uploaded_file'
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for (const key of keys) {
|
|
|
|
|
const value = record[key]
|
|
|
|
|
const resource = unwrapFileResource(value)
|
|
|
|
|
if (resource) return resource
|
|
|
|
|
|
|
|
|
|
if (value && typeof value === 'object') {
|
|
|
|
|
const normalized = normalizeFileResource(value as Record<string, any>)
|
|
|
|
|
if (normalized) return normalized
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof record.src === 'string') {
|
|
|
|
|
return normalizeFileResource(record)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function applyFileToUploadList(
|
|
|
|
|
fileId: number | string,
|
|
|
|
|
file?: FileResource | null,
|
|
|
|
|
fallbackName = 'image'
|
|
|
|
|
) {
|
|
|
|
|
const imageSrc = resolveFileImageUrl(file)
|
|
|
|
|
if (!imageSrc) return []
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
uid: String(fileId),
|
|
|
|
|
name: file?.name || fallbackName,
|
|
|
|
|
status: 'done',
|
|
|
|
|
url: imageSrc,
|
|
|
|
|
thumbUrl: imageSrc
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|