Initialize the admin app with banner management for app sections.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,623 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import AdminPageSkeleton from '@/components/ui/AdminPageSkeleton.vue'
|
||||
import BaseModal from '@/components/ui/BaseModal.vue'
|
||||
import BaseSwitch from '@/components/ui/BaseSwitch.vue'
|
||||
import BaseEmptyState from '@/components/ui/BaseEmptyState.vue'
|
||||
import ActiveSwitchCell from '@/components/ui/ActiveSwitchCell.vue'
|
||||
import AdminIcon from '@/components/icons/AdminIcon.vue'
|
||||
import AdminSelect from '@/components/ui/AdminSelect.vue'
|
||||
import { catalogsApi, productsApi } from '@/services/api'
|
||||
import { ApiError } from '@/services/http'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { formatCurrency, formatNumber } from '@/utils/format'
|
||||
import type { Catalog, Product } from '@/types'
|
||||
import AdminCurrencyInput from '@/components/ui/AdminCurrencyInput.vue'
|
||||
import { fileToCatalogImage } from '@/utils/catalogImage'
|
||||
|
||||
const productList = ref<Product[]>([])
|
||||
const catalogs = ref<Catalog[]>([])
|
||||
const searchQuery = ref('')
|
||||
const catalogFilter = ref('all')
|
||||
const loading = ref(true)
|
||||
const modalMode = ref<'add' | 'edit' | null>(null)
|
||||
const editingId = ref<string | null>(null)
|
||||
const formError = ref('')
|
||||
const saving = ref(false)
|
||||
const toast = useToast()
|
||||
|
||||
const catalogFilterOptions = computed(() => [
|
||||
{ value: 'all', label: 'همه کالیتهها' },
|
||||
...catalogs.value.map((catalog) => ({
|
||||
value: catalog.id,
|
||||
label: `${catalog.code} — ${catalog.name}`,
|
||||
})),
|
||||
])
|
||||
|
||||
const catalogFormOptions = computed(() =>
|
||||
catalogs.value.map((catalog) => ({
|
||||
value: catalog.id,
|
||||
label: `${catalog.code} — ${catalog.name} (${catalog.companyName})`,
|
||||
})),
|
||||
)
|
||||
|
||||
const emptyForm = () => ({
|
||||
catalogId: catalogs.value[0]?.id ?? '',
|
||||
name: '',
|
||||
color: '',
|
||||
productCode: '',
|
||||
minMeterage: 100,
|
||||
orderStep: 50,
|
||||
price: null as number | null,
|
||||
internalStock: 0,
|
||||
isActive: true,
|
||||
usageCategory: 'other',
|
||||
season: 'all_season',
|
||||
composition: '',
|
||||
weaveType: '',
|
||||
subtitle: '',
|
||||
rollMeters: 50,
|
||||
isNew: false,
|
||||
colorsText: '',
|
||||
imageUrls: [] as string[],
|
||||
})
|
||||
|
||||
const form = reactive(emptyForm())
|
||||
|
||||
const isModalOpen = computed(() => modalMode.value !== null)
|
||||
const modalTitle = computed(() => (modalMode.value === 'edit' ? 'ویرایش محصول' : 'افزودن محصول'))
|
||||
const isFormValid = computed(() => form.name.trim() && form.catalogId && form.minMeterage > 0)
|
||||
const hasActiveFilters = computed(
|
||||
() => searchQuery.value.trim().length > 0 || catalogFilter.value !== 'all',
|
||||
)
|
||||
|
||||
const filteredProducts = computed(() => {
|
||||
let list = productList.value
|
||||
if (catalogFilter.value !== 'all') {
|
||||
list = list.filter((p) => p.catalogId === catalogFilter.value)
|
||||
}
|
||||
const q = searchQuery.value.trim()
|
||||
if (q) {
|
||||
list = list.filter(
|
||||
(p) =>
|
||||
p.name.includes(q) ||
|
||||
p.color.includes(q) ||
|
||||
p.productCode.includes(q) ||
|
||||
p.catalogCode.includes(q),
|
||||
)
|
||||
}
|
||||
return list
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [productsData, catalogsData] = await Promise.all([
|
||||
productsApi.list(),
|
||||
catalogsApi.list(),
|
||||
])
|
||||
productList.value = productsData
|
||||
catalogs.value = catalogsData
|
||||
} catch (err) {
|
||||
toast.error(err instanceof ApiError ? err.message : 'بارگذاری محصولات انجام نشد.')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadData()
|
||||
})
|
||||
|
||||
function catalogMeta(catalogId: string) {
|
||||
return catalogs.value.find((c) => c.id === catalogId)
|
||||
}
|
||||
|
||||
function openAddModal() {
|
||||
modalMode.value = 'add'
|
||||
editingId.value = null
|
||||
Object.assign(form, emptyForm())
|
||||
formError.value = ''
|
||||
}
|
||||
|
||||
function openEditModal(product: Product) {
|
||||
modalMode.value = 'edit'
|
||||
editingId.value = product.id
|
||||
form.catalogId = product.catalogId
|
||||
form.name = product.name
|
||||
form.color = product.color
|
||||
form.productCode = product.productCode
|
||||
form.minMeterage = product.minMeterage
|
||||
form.orderStep = product.orderStep
|
||||
form.price = product.price
|
||||
form.internalStock = product.internalStock
|
||||
form.isActive = product.isActive
|
||||
form.usageCategory = (product as { usageCategory?: string }).usageCategory ?? 'other'
|
||||
form.season = (product as { season?: string }).season ?? 'all_season'
|
||||
form.composition = (product as { composition?: string }).composition ?? ''
|
||||
form.weaveType = (product as { weaveType?: string }).weaveType ?? ''
|
||||
form.subtitle = (product as { subtitle?: string }).subtitle ?? ''
|
||||
form.rollMeters = (product as { rollMeters?: number }).rollMeters ?? 50
|
||||
form.isNew = (product as { isNew?: boolean }).isNew ?? false
|
||||
form.colorsText = (product.colors ?? []).join(', ')
|
||||
form.imageUrls = [...(product.imageUrls ?? [])]
|
||||
formError.value = ''
|
||||
}
|
||||
|
||||
function parseListText(value: string): string[] {
|
||||
return value.split(',').map((part) => part.trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
const imageBusy = ref(false)
|
||||
|
||||
async function addProductImages(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const files = [...(input.files ?? [])]
|
||||
input.value = ''
|
||||
if (!files.length) return
|
||||
imageBusy.value = true
|
||||
formError.value = ''
|
||||
try {
|
||||
const next = await Promise.all(files.map((file) => fileToCatalogImage(file)))
|
||||
form.imageUrls = [...form.imageUrls, ...next]
|
||||
} catch (e) {
|
||||
formError.value = e instanceof Error ? e.message : 'بارگذاری تصویر ناموفق بود.'
|
||||
} finally {
|
||||
imageBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function removeProductImage(index: number) {
|
||||
form.imageUrls.splice(index, 1)
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modalMode.value = null
|
||||
editingId.value = null
|
||||
formError.value = ''
|
||||
}
|
||||
|
||||
async function submitProduct() {
|
||||
if (!isFormValid.value) {
|
||||
formError.value = 'نام محصول، کالیته و حداقل متراژ الزامی هستند.'
|
||||
return
|
||||
}
|
||||
|
||||
const cat = catalogMeta(form.catalogId)
|
||||
if (!cat) {
|
||||
formError.value = 'کالیته معتبر انتخاب کنید.'
|
||||
return
|
||||
}
|
||||
|
||||
const catalogPayload = {
|
||||
usageCategory: form.usageCategory,
|
||||
season: form.season,
|
||||
composition: form.composition.trim() || null,
|
||||
weaveType: form.weaveType.trim() || null,
|
||||
subtitle: form.subtitle.trim() || null,
|
||||
rollMeters: form.rollMeters,
|
||||
isNew: form.isNew,
|
||||
colorsJson: parseListText(form.colorsText),
|
||||
imageUrlsJson: form.imageUrls,
|
||||
}
|
||||
const productCode = form.productCode.trim() || `${cat.code}-NEW`
|
||||
saving.value = true
|
||||
try {
|
||||
if (modalMode.value === 'edit' && editingId.value) {
|
||||
const updated = await productsApi.update(editingId.value, {
|
||||
catalogId: form.catalogId,
|
||||
name: form.name.trim(),
|
||||
color: form.color.trim() || '—',
|
||||
code: productCode,
|
||||
productCode,
|
||||
minMeterage: form.minMeterage,
|
||||
orderStep: form.orderStep,
|
||||
price: form.price,
|
||||
internalStock: form.internalStock,
|
||||
isActive: form.isActive,
|
||||
...catalogPayload,
|
||||
})
|
||||
const index = productList.value.findIndex((p) => p.id === editingId.value)
|
||||
if (index !== -1) productList.value[index] = { ...productList.value[index]!, ...updated }
|
||||
toast.success('اطلاعات محصول بهروزرسانی شد.')
|
||||
} else {
|
||||
const created = await productsApi.create({
|
||||
companyId: cat.companyId,
|
||||
catalogId: form.catalogId,
|
||||
name: form.name.trim(),
|
||||
color: form.color.trim() || '—',
|
||||
code: productCode,
|
||||
productCode,
|
||||
minMeterage: form.minMeterage,
|
||||
orderStep: form.orderStep,
|
||||
price: form.price,
|
||||
internalStock: form.internalStock,
|
||||
isActive: form.isActive,
|
||||
...catalogPayload,
|
||||
})
|
||||
productList.value.push(created)
|
||||
toast.success('محصول جدید ثبت شد.')
|
||||
}
|
||||
closeModal()
|
||||
} catch (err) {
|
||||
toast.error(err instanceof ApiError ? err.message : 'ذخیره محصول انجام نشد.')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function persistActive(id: string, isActive: boolean) {
|
||||
await productsApi.setActive(id, isActive)
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
searchQuery.value = ''
|
||||
catalogFilter.value = 'all'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="products-page">
|
||||
<div class="admin-page-header">
|
||||
<div>
|
||||
<h1 class="admin-page-header__title">محصولات</h1>
|
||||
</div>
|
||||
<button type="button" class="admin-btn admin-btn--primary" @click="openAddModal">
|
||||
افزودن محصول
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="products-page__filters admin-card">
|
||||
<div class="products-page__search">
|
||||
<AdminIcon name="search" :size="18" />
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="search"
|
||||
class="products-page__search-input"
|
||||
placeholder="جستجو در نام، رنگ، کد..."
|
||||
/>
|
||||
</div>
|
||||
<AdminSelect
|
||||
v-model="catalogFilter"
|
||||
class="products-page__catalog-filter"
|
||||
aria-label="فیلتر کالیته"
|
||||
:options="catalogFilterOptions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AdminPageSkeleton v-if="loading" variant="table" />
|
||||
<BaseEmptyState
|
||||
v-else-if="!filteredProducts.length"
|
||||
class="admin-card"
|
||||
icon="products"
|
||||
:title="hasActiveFilters ? 'نتیجهای یافت نشد' : 'محصولی ثبت نشده'"
|
||||
:description="
|
||||
hasActiveFilters
|
||||
? 'عبارت جستجو را تغییر دهید یا فیلترها را پاک کنید.'
|
||||
: 'برای شروع، اولین محصول را ثبت کنید.'
|
||||
"
|
||||
>
|
||||
<button v-if="hasActiveFilters" type="button" class="admin-btn admin-btn--outline admin-btn--sm" @click="clearFilters">
|
||||
پاک کردن فیلترها
|
||||
</button>
|
||||
<button v-else type="button" class="admin-btn admin-btn--primary admin-btn--sm" @click="openAddModal">
|
||||
افزودن محصول
|
||||
</button>
|
||||
</BaseEmptyState>
|
||||
|
||||
<div v-else class="products-page__table admin-card desktop-only">
|
||||
<div class="admin-table-wrap">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>تصویر</th>
|
||||
<th>کد محصول</th>
|
||||
<th>نام</th>
|
||||
<th>رنگ</th>
|
||||
<th>کالیته</th>
|
||||
<th>شرکت</th>
|
||||
<th class="numeric">حداقل متراژ</th>
|
||||
<th class="numeric">ضریب</th>
|
||||
<th class="numeric">قیمت</th>
|
||||
<th class="numeric">موجودی</th>
|
||||
<th class="status">وضعیت</th>
|
||||
<th class="actions">عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="product in filteredProducts" :key="product.id">
|
||||
<td>
|
||||
<span
|
||||
class="product-thumb"
|
||||
:class="{ 'product-thumb--empty': !product.imageUrls?.[0] }"
|
||||
:style="product.imageUrls?.[0] ? { backgroundImage: `url(${product.imageUrls[0]})` } : undefined"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ product.productCode }}</td>
|
||||
<td>{{ product.name }}</td>
|
||||
<td>{{ product.color }}</td>
|
||||
<td>{{ product.catalogCode }}</td>
|
||||
<td>{{ product.companyName }}</td>
|
||||
<td class="numeric">{{ formatNumber(product.minMeterage) }} متر</td>
|
||||
<td class="numeric">{{ formatNumber(product.orderStep) }}</td>
|
||||
<td class="numeric">{{ product.price ? formatCurrency(product.price) : '—' }}</td>
|
||||
<td class="numeric">{{ formatNumber(product.internalStock) }} متر</td>
|
||||
<td class="status">
|
||||
<ActiveSwitchCell
|
||||
:id="product.id"
|
||||
v-model="product.isActive"
|
||||
:entity-name="product.name"
|
||||
entity-type="محصول"
|
||||
:persist="persistActive"
|
||||
/>
|
||||
</td>
|
||||
<td class="actions">
|
||||
<button type="button" class="admin-btn admin-btn--ghost admin-btn--xs" @click="openEditModal(product)">
|
||||
ویرایش
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!loading && filteredProducts.length" class="compact-only admin-card-list">
|
||||
<article v-for="product in filteredProducts" :key="product.id" class="admin-card-item">
|
||||
<div class="admin-card-item__header">
|
||||
<p class="admin-card-item__title">{{ product.name }}</p>
|
||||
<div class="admin-card-item__header-actions">
|
||||
<ActiveSwitchCell
|
||||
:id="product.id"
|
||||
v-model="product.isActive"
|
||||
:entity-name="product.name"
|
||||
entity-type="محصول"
|
||||
:persist="persistActive"
|
||||
/>
|
||||
<button type="button" class="admin-btn admin-btn--ghost admin-btn--xs" @click="openEditModal(product)">
|
||||
ویرایش
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-card-item__meta">
|
||||
<span>{{ product.productCode }}</span>
|
||||
<span>{{ product.color }}</span>
|
||||
<span>کالیته {{ product.catalogCode }}</span>
|
||||
<span>{{ product.price ? formatCurrency(product.price) : 'بدون قیمت' }}</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<BaseModal :open="isModalOpen" :title="modalTitle" size="lg" @close="closeModal">
|
||||
<form class="admin-form" @submit.prevent="submitProduct">
|
||||
<p v-if="formError" class="admin-form__error" role="alert">{{ formError }}</p>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<label>کالیته <span class="admin-form__required">*</span></label>
|
||||
<AdminSelect
|
||||
v-model="form.catalogId"
|
||||
block
|
||||
aria-label="انتخاب کالیته"
|
||||
placeholder="انتخاب کالیته"
|
||||
:options="catalogFormOptions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__row">
|
||||
<div class="admin-form__field">
|
||||
<label for="product-name">نام محصول <span class="admin-form__required">*</span></label>
|
||||
<input id="product-name" v-model="form.name" type="text" class="admin-input" />
|
||||
</div>
|
||||
<div class="admin-form__field">
|
||||
<label for="product-color">رنگ / نوع</label>
|
||||
<input id="product-color" v-model="form.color" type="text" class="admin-input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<label for="product-code">کد محصول</label>
|
||||
<input id="product-code" v-model="form.productCode" type="text" class="admin-input" dir="ltr" />
|
||||
</div>
|
||||
|
||||
<div class="admin-form__row">
|
||||
<div class="admin-form__field">
|
||||
<label for="product-min">حداقل متراژ</label>
|
||||
<input id="product-min" v-model.number="form.minMeterage" type="number" min="1" class="admin-input" />
|
||||
</div>
|
||||
<div class="admin-form__field">
|
||||
<label for="product-step">ضریب سفارش</label>
|
||||
<input id="product-step" v-model.number="form.orderStep" type="number" min="1" class="admin-input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__row">
|
||||
<div class="admin-form__field">
|
||||
<label for="product-price">قیمت (تومان)</label>
|
||||
<AdminCurrencyInput id="product-price" v-model="form.price" />
|
||||
</div>
|
||||
<div class="admin-form__field">
|
||||
<label for="product-stock">موجودی داخلی (متر)</label>
|
||||
<input id="product-stock" v-model.number="form.internalStock" type="number" min="0" class="admin-input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__row">
|
||||
<div class="admin-form__field">
|
||||
<label for="product-usage">کاربرد</label>
|
||||
<select id="product-usage" v-model="form.usageCategory" class="admin-input">
|
||||
<option value="trousers">شلوار</option>
|
||||
<option value="suit">کتوشلوار</option>
|
||||
<option value="shirt">پیراهن</option>
|
||||
<option value="other">سایر</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="admin-form__field">
|
||||
<label for="product-season">فصل</label>
|
||||
<select id="product-season" v-model="form.season" class="admin-input">
|
||||
<option value="summer">تابستان</option>
|
||||
<option value="winter">زمستان</option>
|
||||
<option value="all_season">چهارفصل</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__row">
|
||||
<div class="admin-form__field">
|
||||
<label for="product-composition">جنس / ترکیب</label>
|
||||
<input id="product-composition" v-model="form.composition" type="text" class="admin-input" />
|
||||
</div>
|
||||
<div class="admin-form__field">
|
||||
<label for="product-weave">نوع بافت</label>
|
||||
<input id="product-weave" v-model="form.weaveType" type="text" class="admin-input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__row">
|
||||
<div class="admin-form__field">
|
||||
<label for="product-subtitle">زیرعنوان</label>
|
||||
<input id="product-subtitle" v-model="form.subtitle" type="text" class="admin-input" />
|
||||
</div>
|
||||
<div class="admin-form__field">
|
||||
<label for="product-roll">متراژ طاقه</label>
|
||||
<input id="product-roll" v-model.number="form.rollMeters" type="number" min="1" class="admin-input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<label for="product-colors">رنگها (با ویرگول)</label>
|
||||
<input
|
||||
id="product-colors"
|
||||
v-model="form.colorsText"
|
||||
type="text"
|
||||
class="admin-input"
|
||||
placeholder="مثلاً: سرمهای، خاکستری، بژ"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<label>تصاویر محصول</label>
|
||||
<div class="product-images">
|
||||
<div
|
||||
v-for="(src, index) in form.imageUrls"
|
||||
:key="`${index}-${src.slice(0, 24)}`"
|
||||
class="product-images__item"
|
||||
>
|
||||
<span class="product-images__thumb" :style="{ backgroundImage: `url(${src})` }" />
|
||||
<button type="button" class="product-images__remove" @click="removeProductImage(index)">
|
||||
حذف
|
||||
</button>
|
||||
</div>
|
||||
<label class="product-images__add">
|
||||
<input type="file" accept="image/*" multiple hidden :disabled="imageBusy" @change="addProductImages" />
|
||||
{{ imageBusy ? '...' : '+ افزودن تصویر' }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<BaseSwitch v-model="form.isNew" label="محصول جدید" />
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<BaseSwitch v-model="form.isActive" label="وضعیت" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
<button type="button" class="admin-btn admin-btn--outline" @click="closeModal">انصراف</button>
|
||||
<button type="button" class="admin-btn admin-btn--primary" :disabled="!isFormValid || saving" @click="submitProduct">
|
||||
{{ modalMode === 'edit' ? 'ذخیره تغییرات' : 'ثبت محصول' }}
|
||||
</button>
|
||||
</template>
|
||||
</BaseModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.products-page__filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.products-page__search {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.products-page__search-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 0.875rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.products-page__catalog-filter {
|
||||
width: min(260px, 100%);
|
||||
}
|
||||
|
||||
.products-page__table {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.product-thumb {
|
||||
display: block;
|
||||
width: 44px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: #e8eceb center / cover no-repeat;
|
||||
}
|
||||
|
||||
.product-thumb--empty {
|
||||
background: #eef1f0;
|
||||
}
|
||||
|
||||
.product-images {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.product-images__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
width: 92px;
|
||||
}
|
||||
|
||||
.product-images__thumb {
|
||||
display: block;
|
||||
width: 92px;
|
||||
height: 72px;
|
||||
border-radius: 10px;
|
||||
background: #e8eceb center / cover no-repeat;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.product-images__remove,
|
||||
.product-images__add {
|
||||
border: 1px dashed var(--color-border);
|
||||
background: transparent;
|
||||
border-radius: 8px;
|
||||
padding: 6px 8px;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.product-images__add {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-width: 92px;
|
||||
min-height: 72px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user