2026-09-07 16:56:52 +03:30
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { onMounted, reactive, ref } from 'vue'
|
|
|
|
|
|
import { useRouter } from 'vue-router'
|
|
|
|
|
|
import { message } from 'ant-design-vue'
|
2026-09-08 14:56:10 +03:30
|
|
|
|
import { ArrowRightOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue'
|
2026-09-07 16:56:52 +03:30
|
|
|
|
import api from '@/api/http'
|
|
|
|
|
|
import MoneyInput from '@/components/MoneyInput.vue'
|
|
|
|
|
|
import { useAuthStore } from '@/stores/auth'
|
2026-09-08 14:56:10 +03:30
|
|
|
|
import { SUPERADMIN_SYSTEM_SETTING_KEYS } from '@/config/superAdminSettingKeys'
|
|
|
|
|
|
import { rewriteLegacyUploadUrl } from '@/utils/rewriteLegacyUploadUrl'
|
|
|
|
|
|
import {
|
|
|
|
|
|
resolveSettingDescription,
|
|
|
|
|
|
resolveSettingTitle,
|
|
|
|
|
|
} from '@/config/khanegiSettingCatalog'
|
2026-09-07 16:56:52 +03:30
|
|
|
|
|
2026-09-08 11:01:00 +03:30
|
|
|
|
withDefaults(
|
|
|
|
|
|
defineProps<{
|
|
|
|
|
|
embedded?: boolean
|
|
|
|
|
|
}>(),
|
|
|
|
|
|
{ embedded: false },
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-09-07 16:56:52 +03:30
|
|
|
|
type PriceRow = {
|
|
|
|
|
|
durationDays: number
|
|
|
|
|
|
enabled: boolean
|
|
|
|
|
|
originalPriceRials: number
|
|
|
|
|
|
finalPriceRials: number
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type CatalogItem = {
|
|
|
|
|
|
id: string
|
|
|
|
|
|
settingKey: string
|
|
|
|
|
|
titleFa: string
|
2026-09-08 14:56:10 +03:30
|
|
|
|
description: string
|
|
|
|
|
|
images: string[]
|
2026-09-07 16:56:52 +03:30
|
|
|
|
inStore: boolean
|
|
|
|
|
|
prices: PriceRow[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-08 14:56:10 +03:30
|
|
|
|
type TemplateRow = {
|
|
|
|
|
|
key?: string
|
|
|
|
|
|
title_fa?: string
|
|
|
|
|
|
description?: string
|
|
|
|
|
|
type?: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const DURATION_DAYS = [30, 90, 180, 360] as const
|
|
|
|
|
|
|
2026-09-07 16:56:52 +03:30
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
const auth = useAuthStore()
|
|
|
|
|
|
const canWrite = auth.can('plugins:write')
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const savingId = ref<string | null>(null)
|
2026-09-08 14:56:10 +03:30
|
|
|
|
const uploadingKey = ref<string | null>(null)
|
2026-09-07 16:56:52 +03:30
|
|
|
|
const items = ref<CatalogItem[]>([])
|
|
|
|
|
|
const showCreate = ref(false)
|
|
|
|
|
|
const creating = ref(false)
|
|
|
|
|
|
const createForm = reactive({
|
|
|
|
|
|
settingKey: '',
|
|
|
|
|
|
titleFa: '',
|
|
|
|
|
|
description: '',
|
|
|
|
|
|
inStore: true,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const durationLabel: Record<number, string> = {
|
|
|
|
|
|
30: '۳۰ روزه',
|
|
|
|
|
|
90: '۹۰ روزه',
|
|
|
|
|
|
180: '۱۸۰ روزه',
|
|
|
|
|
|
360: '۳۶۰ روزه',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-08 14:56:10 +03:30
|
|
|
|
function emptyPrices(): PriceRow[] {
|
|
|
|
|
|
return DURATION_DAYS.map((durationDays) => ({
|
|
|
|
|
|
durationDays,
|
|
|
|
|
|
enabled: false,
|
|
|
|
|
|
originalPriceRials: 0,
|
|
|
|
|
|
finalPriceRials: 0,
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function asCatalog(data: unknown): CatalogItem[] {
|
|
|
|
|
|
if (!Array.isArray(data)) return []
|
|
|
|
|
|
return (data as Array<Record<string, any>>).map((row) => ({
|
|
|
|
|
|
id: String(row?.id || ''),
|
|
|
|
|
|
settingKey: String(row?.settingKey || ''),
|
|
|
|
|
|
titleFa: String(row?.titleFa || ''),
|
|
|
|
|
|
description: String(row?.description || ''),
|
|
|
|
|
|
images: Array.isArray(row?.images)
|
|
|
|
|
|
? row.images.map((url: unknown) => rewriteLegacyUploadUrl(url)).filter(Boolean).slice(0, 3)
|
|
|
|
|
|
: [],
|
|
|
|
|
|
inStore: row?.inStore !== false,
|
|
|
|
|
|
prices: Array.isArray(row?.prices) && row.prices.length ? row.prices : emptyPrices(),
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildCards(catalogRows: CatalogItem[], templates: TemplateRow[]): CatalogItem[] {
|
|
|
|
|
|
const byKey = new Map(catalogRows.map((row) => [row.settingKey, row]))
|
|
|
|
|
|
const templateByKey = new Map(
|
|
|
|
|
|
templates.filter((row) => row.key).map((row) => [String(row.key), row]),
|
|
|
|
|
|
)
|
|
|
|
|
|
const extra = catalogRows
|
|
|
|
|
|
.map((row) => row.settingKey)
|
|
|
|
|
|
.filter((key) => key && !SUPERADMIN_SYSTEM_SETTING_KEYS.has(key))
|
|
|
|
|
|
const keys = [...SUPERADMIN_SYSTEM_SETTING_KEYS, ...extra]
|
|
|
|
|
|
return keys.map((key) => {
|
|
|
|
|
|
const hit = byKey.get(key)
|
|
|
|
|
|
const tpl = templateByKey.get(key)
|
|
|
|
|
|
const titleFa = resolveSettingTitle({ key, title_fa: hit?.titleFa || tpl?.title_fa || '' })
|
|
|
|
|
|
const description = resolveSettingDescription({
|
|
|
|
|
|
key,
|
|
|
|
|
|
description: hit?.description || tpl?.description || '',
|
|
|
|
|
|
type: tpl?.type || 'switch',
|
|
|
|
|
|
})
|
|
|
|
|
|
if (hit) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
...hit,
|
|
|
|
|
|
titleFa: hit.titleFa || titleFa,
|
|
|
|
|
|
description: hit.description || description,
|
|
|
|
|
|
images: hit.images || [],
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: '',
|
|
|
|
|
|
settingKey: key,
|
|
|
|
|
|
titleFa,
|
|
|
|
|
|
description,
|
|
|
|
|
|
images: [],
|
|
|
|
|
|
inStore: true,
|
|
|
|
|
|
prices: emptyPrices(),
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-07 16:56:52 +03:30
|
|
|
|
async function load() {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
try {
|
2026-09-08 14:56:10 +03:30
|
|
|
|
const [{ data: catalog }, templatesRes] = await Promise.all([
|
|
|
|
|
|
api.get('/plugins/store/catalog'),
|
|
|
|
|
|
api.get('/setting-templates').catch(() => ({ data: { items: [] } })),
|
|
|
|
|
|
])
|
|
|
|
|
|
const templateRows = Array.isArray(templatesRes.data?.items) ? templatesRes.data.items : []
|
|
|
|
|
|
items.value = buildCards(asCatalog(catalog), templateRows)
|
2026-09-07 16:56:52 +03:30
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e?.response?.data?.message || 'خطا در دریافت کاتالوگ')
|
2026-09-08 14:56:10 +03:30
|
|
|
|
items.value = buildCards([], [])
|
2026-09-07 16:56:52 +03:30
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-08 14:56:10 +03:30
|
|
|
|
async function ensureCatalogId(item: CatalogItem): Promise<string> {
|
|
|
|
|
|
if (item.id) return item.id
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await api.post('/plugins/store/catalog', {
|
|
|
|
|
|
settingKey: item.settingKey,
|
|
|
|
|
|
titleFa: item.titleFa,
|
|
|
|
|
|
description: item.description,
|
|
|
|
|
|
inStore: item.inStore,
|
|
|
|
|
|
})
|
|
|
|
|
|
item.id = String(data?.id || '')
|
|
|
|
|
|
return item.id
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
const { data } = await api.get('/plugins/store/catalog')
|
|
|
|
|
|
const found = asCatalog(data).find((row) => row.settingKey === item.settingKey)
|
|
|
|
|
|
if (found?.id) {
|
|
|
|
|
|
item.id = found.id
|
|
|
|
|
|
return found.id
|
|
|
|
|
|
}
|
|
|
|
|
|
throw e
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-07 16:56:52 +03:30
|
|
|
|
async function save(item: CatalogItem) {
|
|
|
|
|
|
for (const price of item.prices) {
|
|
|
|
|
|
if (price.enabled && (price.originalPriceRials == null || price.finalPriceRials == null)) {
|
|
|
|
|
|
message.warning(`برای ${durationLabel[price.durationDays]} باید قیمت بزنید`)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-08 14:56:10 +03:30
|
|
|
|
savingId.value = item.settingKey
|
2026-09-07 16:56:52 +03:30
|
|
|
|
try {
|
2026-09-08 14:56:10 +03:30
|
|
|
|
const id = await ensureCatalogId(item)
|
|
|
|
|
|
await api.patch(`/plugins/store/catalog/${id}`, {
|
2026-09-07 16:56:52 +03:30
|
|
|
|
inStore: item.inStore,
|
|
|
|
|
|
titleFa: item.titleFa,
|
|
|
|
|
|
description: item.description,
|
2026-09-08 14:56:10 +03:30
|
|
|
|
images: item.images || [],
|
2026-09-07 16:56:52 +03:30
|
|
|
|
prices: item.prices,
|
|
|
|
|
|
})
|
|
|
|
|
|
message.success('ذخیره شد')
|
|
|
|
|
|
await load()
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
const msg = e?.response?.data?.message
|
|
|
|
|
|
message.error(Array.isArray(msg) ? msg.join('، ') : msg || 'ذخیره ناموفق')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
savingId.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function createSetting() {
|
|
|
|
|
|
if (!createForm.settingKey.trim() || !createForm.titleFa.trim()) {
|
|
|
|
|
|
message.warning('کلید و عنوان لازم است')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
creating.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.post('/plugins/store/catalog', { ...createForm })
|
|
|
|
|
|
message.success('تنظیم اضافه شد')
|
|
|
|
|
|
showCreate.value = false
|
|
|
|
|
|
createForm.settingKey = ''
|
|
|
|
|
|
createForm.titleFa = ''
|
|
|
|
|
|
createForm.description = ''
|
|
|
|
|
|
createForm.inStore = true
|
|
|
|
|
|
await load()
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
const msg = e?.response?.data?.message
|
|
|
|
|
|
message.error(Array.isArray(msg) ? msg.join('، ') : msg || 'ثبت ناموفق')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
creating.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-08 14:56:10 +03:30
|
|
|
|
async function uploadSlot(
|
|
|
|
|
|
item: CatalogItem,
|
|
|
|
|
|
options: { file: string | Blob | File; onSuccess?: (body?: unknown) => void; onError?: (e: Error) => void },
|
|
|
|
|
|
) {
|
|
|
|
|
|
const file = options.file
|
|
|
|
|
|
if (!(file instanceof File) && !(file instanceof Blob)) return
|
|
|
|
|
|
if ((item.images || []).length >= 3) {
|
|
|
|
|
|
message.warning('حداکثر سه عکس')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
uploadingKey.value = item.settingKey
|
|
|
|
|
|
try {
|
|
|
|
|
|
const body = new FormData()
|
|
|
|
|
|
body.append('file', file)
|
|
|
|
|
|
const { data } = await api.post<{ url: string }>('/plugins/store/upload-image', body)
|
|
|
|
|
|
const url = rewriteLegacyUploadUrl(data.url)
|
|
|
|
|
|
if (url) item.images = [...(item.images || []), url].slice(0, 3)
|
|
|
|
|
|
options.onSuccess?.(data)
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
const msg = e?.response?.data?.message
|
|
|
|
|
|
options.onError?.(e instanceof Error ? e : new Error(String(msg || 'upload failed')))
|
|
|
|
|
|
message.error(Array.isArray(msg) ? msg.join('، ') : msg || 'آپلود ناموفق')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
uploadingKey.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function removeSlot(item: CatalogItem, index: number) {
|
|
|
|
|
|
item.images = (item.images || []).filter((_, i) => i !== index)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function slotUploader(item: CatalogItem) {
|
|
|
|
|
|
return (options: {
|
|
|
|
|
|
file: string | Blob | File
|
|
|
|
|
|
onSuccess?: (body?: unknown) => void
|
|
|
|
|
|
onError?: (e: Error) => void
|
|
|
|
|
|
}) => uploadSlot(item, options)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-07 16:56:52 +03:30
|
|
|
|
onMounted(load)
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-09-08 11:01:00 +03:30
|
|
|
|
<div :class="embedded ? 'catalog-embed' : 'page-shell'">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="!embedded"
|
|
|
|
|
|
style="display: flex; align-items: flex-start; gap: 8px; margin-bottom: 16px"
|
|
|
|
|
|
>
|
2026-09-07 16:56:52 +03:30
|
|
|
|
<a-button type="text" @click="router.push({ name: 'plugin-panel' })">
|
|
|
|
|
|
<ArrowRightOutlined />
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<div style="flex: 1">
|
|
|
|
|
|
<h1 class="page-title">قیمت فروشگاه خانگی</h1>
|
|
|
|
|
|
<p class="page-sub">برای همهٔ نسخههای خانگی یکی است. مدت خاموش در ادمین دیده نمیشود.</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-button v-if="canWrite" type="primary" @click="showCreate = true">
|
|
|
|
|
|
<PlusOutlined /> تنظیم جدید
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
2026-09-08 11:01:00 +03:30
|
|
|
|
<div v-else class="catalog-embed-bar">
|
|
|
|
|
|
<p class="catalog-hint">
|
2026-09-08 14:56:10 +03:30
|
|
|
|
همهٔ کلیدهای دسترسی قالب خانگی. مدت خاموش در ادمین ماژول دیده نمیشود.
|
2026-09-08 11:01:00 +03:30
|
|
|
|
</p>
|
|
|
|
|
|
<a-button v-if="canWrite" type="primary" size="small" @click="showCreate = true">
|
|
|
|
|
|
<PlusOutlined /> تنظیم جدید
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
2026-09-07 16:56:52 +03:30
|
|
|
|
|
|
|
|
|
|
<a-spin :spinning="loading">
|
|
|
|
|
|
<div class="catalog-grid">
|
2026-09-08 14:56:10 +03:30
|
|
|
|
<article v-for="item in items" :key="item.settingKey" class="catalog-card">
|
|
|
|
|
|
<a-form layout="vertical" class="catalog-form">
|
|
|
|
|
|
<a-form-item label="عنوان" required>
|
|
|
|
|
|
<a-input v-model:value="item.titleFa" :disabled="!canWrite" placeholder="مثلاً ماژول جدید" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="کلید تنظیمات">
|
|
|
|
|
|
<a-input :value="item.settingKey" disabled dir="ltr" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="توضیح">
|
|
|
|
|
|
<a-textarea v-model:value="item.description" :rows="4" :maxlength="4000" :disabled="!canWrite" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="عکسها">
|
|
|
|
|
|
<p class="img-hint">حداکثر سه عکس. در ادمین ماژول عکس اول روی کارت است.</p>
|
|
|
|
|
|
<div class="img-slots">
|
|
|
|
|
|
<div v-for="(url, index) in (item.images || [])" :key="url + index" class="img-slot">
|
|
|
|
|
|
<img :src="url" alt="" />
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
v-if="canWrite"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
danger
|
|
|
|
|
|
class="img-slot-del"
|
|
|
|
|
|
@click="removeSlot(item, index)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<DeleteOutlined />
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-upload
|
|
|
|
|
|
v-if="canWrite && (item.images || []).length < 3"
|
|
|
|
|
|
name="file"
|
|
|
|
|
|
list-type="picture-card"
|
|
|
|
|
|
:show-upload-list="false"
|
|
|
|
|
|
accept="image/jpeg,image/png,image/gif,image/webp"
|
|
|
|
|
|
:custom-request="slotUploader(item)"
|
|
|
|
|
|
:disabled="uploadingKey === item.settingKey"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="img-add">
|
|
|
|
|
|
<PlusOutlined />
|
|
|
|
|
|
<span>{{ uploadingKey === item.settingKey ? '…' : 'آپلود' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-upload>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="به فروشگاه افزونه اضافه شود">
|
|
|
|
|
|
<a-switch v-model:checked="item.inStore" :disabled="!canWrite" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-form>
|
2026-09-07 16:56:52 +03:30
|
|
|
|
<div v-for="price in item.prices" :key="price.durationDays" class="price-row">
|
|
|
|
|
|
<a-switch v-model:checked="price.enabled" :disabled="!canWrite" />
|
|
|
|
|
|
<span class="dur">{{ durationLabel[price.durationDays] }}</span>
|
|
|
|
|
|
<div class="price-fields" :class="{ dim: !price.enabled }">
|
|
|
|
|
|
<label>
|
|
|
|
|
|
اصلی
|
|
|
|
|
|
<MoneyInput v-model:value="price.originalPriceRials" :disabled="!canWrite || !price.enabled" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label>
|
|
|
|
|
|
نهایی
|
|
|
|
|
|
<MoneyInput v-model:value="price.finalPriceRials" :disabled="!canWrite || !price.enabled" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
v-if="canWrite"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
block
|
2026-09-08 14:56:10 +03:30
|
|
|
|
:loading="savingId === item.settingKey"
|
2026-09-07 16:56:52 +03:30
|
|
|
|
@click="save(item)"
|
|
|
|
|
|
>
|
|
|
|
|
|
ذخیره
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</article>
|
2026-09-08 14:56:10 +03:30
|
|
|
|
<div v-if="!items.length" class="empty-hint">کلید دسترسیای در قالب نیست</div>
|
2026-09-07 16:56:52 +03:30
|
|
|
|
</div>
|
|
|
|
|
|
</a-spin>
|
|
|
|
|
|
|
|
|
|
|
|
<a-modal
|
|
|
|
|
|
v-model:open="showCreate"
|
|
|
|
|
|
title="تنظیم جدید"
|
|
|
|
|
|
ok-text="ثبت"
|
|
|
|
|
|
cancel-text="انصراف"
|
|
|
|
|
|
:confirm-loading="creating"
|
|
|
|
|
|
@ok="createSetting"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-form layout="vertical" style="margin-top: 12px">
|
|
|
|
|
|
<a-form-item label="عنوان" required>
|
|
|
|
|
|
<a-input v-model:value="createForm.titleFa" placeholder="مثلاً ماژول جدید" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="کلید تنظیمات" required>
|
|
|
|
|
|
<a-input v-model:value="createForm.settingKey" placeholder="enable_something" dir="ltr" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="توضیح">
|
2026-09-08 14:56:10 +03:30
|
|
|
|
<a-textarea v-model:value="createForm.description" :rows="4" :maxlength="4000" />
|
2026-09-07 16:56:52 +03:30
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="به فروشگاه افزونه اضافه شود">
|
|
|
|
|
|
<a-switch v-model:checked="createForm.inStore" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-form>
|
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-09-08 11:01:00 +03:30
|
|
|
|
.catalog-embed-bar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.catalog-hint {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 12rem;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
|
}
|
2026-09-07 16:56:52 +03:30
|
|
|
|
.catalog-grid {
|
|
|
|
|
|
display: grid;
|
2026-09-08 14:56:10 +03:30
|
|
|
|
grid-template-columns: 1fr;
|
2026-09-07 16:56:52 +03:30
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
2026-09-08 14:56:10 +03:30
|
|
|
|
@media (min-width: 640px) {
|
|
|
|
|
|
.catalog-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
|
|
|
|
}
|
|
|
|
|
|
@media (min-width: 1024px) {
|
|
|
|
|
|
.catalog-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
|
|
|
|
}
|
2026-09-07 16:56:52 +03:30
|
|
|
|
.catalog-card {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1px solid #e8eef5;
|
|
|
|
|
|
border-radius: 18px;
|
|
|
|
|
|
padding: 14px;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
2026-09-08 14:56:10 +03:30
|
|
|
|
.catalog-form :deep(.ant-form-item) {
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.catalog-form :deep(.ant-form-item:last-child) {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.empty-hint {
|
|
|
|
|
|
grid-column: 1 / -1;
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
font-size: 0.85rem;
|
2026-09-07 16:56:52 +03:30
|
|
|
|
}
|
|
|
|
|
|
.price-row {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: auto 4.5rem 1fr;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.dur { font-weight: 700; font-size: 0.82rem; }
|
|
|
|
|
|
.price-fields {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.price-fields.dim { opacity: 0.45; }
|
|
|
|
|
|
.price-fields label {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
}
|
2026-09-08 14:56:10 +03:30
|
|
|
|
.img-hint {
|
|
|
|
|
|
margin: 0 0 6px;
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
}
|
|
|
|
|
|
.img-slots {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.img-slot {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
aspect-ratio: 1;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
border: 1px solid #e8eef5;
|
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
|
}
|
|
|
|
|
|
.img-slot img {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
.img-slot-del {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 4px;
|
|
|
|
|
|
left: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.img-add {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.img-slots :deep(.ant-upload.ant-upload-select-picture-card) {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
aspect-ratio: 1;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
2026-09-07 16:56:52 +03:30
|
|
|
|
</style>
|