change UI & fix bugs
This commit is contained in:
@@ -2,12 +2,13 @@
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import AdminPageSkeleton from '@/components/ui/AdminPageSkeleton.vue'
|
||||
import BaseModal from '@/components/ui/BaseModal.vue'
|
||||
import AdminImagePicker from '@/components/ui/AdminImagePicker.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 { catalogsApi, productsApi, settingsApi } from '@/services/api'
|
||||
import { ApiError } from '@/services/http'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { formatCurrency, formatNumber } from '@/utils/format'
|
||||
@@ -17,6 +18,17 @@ import { fileToCatalogImage } from '@/utils/catalogImage'
|
||||
|
||||
const productList = ref<Product[]>([])
|
||||
const catalogs = ref<Catalog[]>([])
|
||||
const usageOptions = ref<Array<{ value: string; label: string }>>([
|
||||
{ value: 'trousers', label: 'شلوار' },
|
||||
{ value: 'suit', label: 'کتوشلوار' },
|
||||
{ value: 'shirt', label: 'پیراهن' },
|
||||
{ value: 'other', label: 'سایر' },
|
||||
])
|
||||
const seasonOptions = ref<Array<{ value: string; label: string }>>([
|
||||
{ value: 'summer', label: 'تابستان' },
|
||||
{ value: 'winter', label: 'زمستان' },
|
||||
{ value: 'all_season', label: 'چهارفصل' },
|
||||
])
|
||||
const searchQuery = ref('')
|
||||
const catalogFilter = ref('all')
|
||||
const loading = ref(true)
|
||||
@@ -58,8 +70,8 @@ const emptyForm = () => ({
|
||||
subtitle: '',
|
||||
rollMeters: 50,
|
||||
isNew: false,
|
||||
colorsText: '',
|
||||
imageUrls: [] as string[],
|
||||
colorRows: [{ name: '', imageUrl: '' }] as Array<{ name: string; imageUrl: string }>,
|
||||
extraImageUrls: [] as string[],
|
||||
})
|
||||
|
||||
const form = reactive(emptyForm())
|
||||
@@ -92,12 +104,28 @@ const filteredProducts = computed(() => {
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [productsData, catalogsData] = await Promise.all([
|
||||
const [productsData, catalogsData, settings] = await Promise.all([
|
||||
productsApi.list(),
|
||||
catalogsApi.list(),
|
||||
settingsApi.get().catch(() => null),
|
||||
])
|
||||
productList.value = productsData
|
||||
catalogs.value = catalogsData
|
||||
if (settings?.categoryDefsJson?.usage?.length) {
|
||||
usageOptions.value = [
|
||||
...settings.categoryDefsJson.usage.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.title,
|
||||
})),
|
||||
{ value: 'other', label: 'سایر' },
|
||||
]
|
||||
}
|
||||
if (settings?.categoryDefsJson?.seasons?.length) {
|
||||
seasonOptions.value = settings.categoryDefsJson.seasons.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.title,
|
||||
}))
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(err instanceof ApiError ? err.message : 'بارگذاری محصولات انجام نشد.')
|
||||
} finally {
|
||||
@@ -139,13 +167,33 @@ function openEditModal(product: Product) {
|
||||
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 ?? [])]
|
||||
const colors = product.colors ?? []
|
||||
const colorImages = product.colorImages ?? []
|
||||
const gallery = [...(product.imageUrls ?? [])]
|
||||
if (colors.length) {
|
||||
form.colorRows = colors.map((name, index) => ({
|
||||
name,
|
||||
imageUrl: colorImages[index] || gallery[index] || '',
|
||||
}))
|
||||
const used = new Set(form.colorRows.map((row) => row.imageUrl).filter(Boolean))
|
||||
form.extraImageUrls = gallery.filter((url) => !used.has(url))
|
||||
} else {
|
||||
form.colorRows = [{ name: product.color || '', imageUrl: gallery[0] || '' }]
|
||||
form.extraImageUrls = gallery.slice(1)
|
||||
}
|
||||
formError.value = ''
|
||||
}
|
||||
|
||||
function parseListText(value: string): string[] {
|
||||
return value.split(',').map((part) => part.trim()).filter(Boolean)
|
||||
function addColorRow() {
|
||||
form.colorRows.push({ name: '', imageUrl: '' })
|
||||
}
|
||||
|
||||
function removeColorRow(index: number) {
|
||||
if (form.colorRows.length <= 1) {
|
||||
form.colorRows[0] = { name: '', imageUrl: '' }
|
||||
return
|
||||
}
|
||||
form.colorRows.splice(index, 1)
|
||||
}
|
||||
|
||||
const imageBusy = ref(false)
|
||||
@@ -159,7 +207,7 @@ async function addProductImages(event: Event) {
|
||||
formError.value = ''
|
||||
try {
|
||||
const next = await Promise.all(files.map((file) => fileToCatalogImage(file)))
|
||||
form.imageUrls = [...form.imageUrls, ...next]
|
||||
form.extraImageUrls = [...form.extraImageUrls, ...next]
|
||||
} catch (e) {
|
||||
formError.value = e instanceof Error ? e.message : 'بارگذاری تصویر ناموفق بود.'
|
||||
} finally {
|
||||
@@ -168,7 +216,7 @@ async function addProductImages(event: Event) {
|
||||
}
|
||||
|
||||
function removeProductImage(index: number) {
|
||||
form.imageUrls.splice(index, 1)
|
||||
form.extraImageUrls.splice(index, 1)
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
@@ -189,6 +237,12 @@ async function submitProduct() {
|
||||
return
|
||||
}
|
||||
|
||||
const colorEntries = form.colorRows
|
||||
.map((row) => ({
|
||||
name: row.name.trim(),
|
||||
imageUrl: row.imageUrl.trim() || undefined,
|
||||
}))
|
||||
.filter((row) => row.name)
|
||||
const catalogPayload = {
|
||||
usageCategory: form.usageCategory,
|
||||
season: form.season,
|
||||
@@ -197,8 +251,8 @@ async function submitProduct() {
|
||||
subtitle: form.subtitle.trim() || null,
|
||||
rollMeters: form.rollMeters,
|
||||
isNew: form.isNew,
|
||||
colorsJson: parseListText(form.colorsText),
|
||||
imageUrlsJson: form.imageUrls,
|
||||
colorsJson: colorEntries,
|
||||
imageUrlsJson: form.extraImageUrls,
|
||||
}
|
||||
const productCode = form.productCode.trim() || `${cat.code}-NEW`
|
||||
saving.value = true
|
||||
@@ -207,7 +261,7 @@ async function submitProduct() {
|
||||
const updated = await productsApi.update(editingId.value, {
|
||||
catalogId: form.catalogId,
|
||||
name: form.name.trim(),
|
||||
color: form.color.trim() || '—',
|
||||
color: form.color.trim() || colorEntries[0]?.name || '—',
|
||||
code: productCode,
|
||||
productCode,
|
||||
minMeterage: form.minMeterage,
|
||||
@@ -225,7 +279,7 @@ async function submitProduct() {
|
||||
companyId: cat.companyId,
|
||||
catalogId: form.catalogId,
|
||||
name: form.name.trim(),
|
||||
color: form.color.trim() || '—',
|
||||
color: form.color.trim() || colorEntries[0]?.name || '—',
|
||||
code: productCode,
|
||||
productCode,
|
||||
minMeterage: form.minMeterage,
|
||||
@@ -445,18 +499,17 @@ function clearFilters() {
|
||||
<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>
|
||||
<option v-for="opt in usageOptions" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</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>
|
||||
<option v-for="opt in seasonOptions" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -484,21 +537,37 @@ function clearFilters() {
|
||||
</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="مثلاً: سرمهای، خاکستری، بژ"
|
||||
/>
|
||||
<label>رنگها و تصویر هر رنگ</label>
|
||||
<p class="color-variants__hint">برای هر رنگ یک نام و یک عکس جدا بگذارید؛ در فروشگاه با انتخاب همان رنگ، همان عکس نمایش داده میشود.</p>
|
||||
<div class="color-variants">
|
||||
<div v-for="(row, index) in form.colorRows" :key="index" class="color-variant">
|
||||
<AdminImagePicker
|
||||
:model-value="row.imageUrl || null"
|
||||
@update:model-value="row.imageUrl = $event ?? ''"
|
||||
/>
|
||||
<div class="color-variant__fields">
|
||||
<input
|
||||
v-model="row.name"
|
||||
type="text"
|
||||
class="admin-input"
|
||||
:placeholder="`نام رنگ ${index + 1}`"
|
||||
/>
|
||||
<button type="button" class="admin-btn admin-btn--ghost admin-btn--xs" @click="removeColorRow(index)">
|
||||
حذف این رنگ
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="admin-btn admin-btn--outline" @click="addColorRow">
|
||||
+ افزودن رنگ
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<label>تصاویر محصول</label>
|
||||
<label>تصاویر عمومی محصول (اختیاری)</label>
|
||||
<div class="product-images">
|
||||
<div
|
||||
v-for="(src, index) in form.imageUrls"
|
||||
v-for="(src, index) in form.extraImageUrls"
|
||||
:key="`${index}-${src.slice(0, 24)}`"
|
||||
class="product-images__item"
|
||||
>
|
||||
@@ -580,6 +649,39 @@ function clearFilters() {
|
||||
background: #eef1f0;
|
||||
}
|
||||
|
||||
.color-variants {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.color-variants__hint {
|
||||
margin: 0 0 10px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.color-variant {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
background: var(--color-bg, #f7f8f7);
|
||||
}
|
||||
|
||||
.color-variant__fields {
|
||||
flex: 1;
|
||||
min-width: 180px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.product-images {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
Reference in New Issue
Block a user