change UI & fix bugs

This commit is contained in:
2026-09-14 16:10:30 +03:30
parent 33a3828aaf
commit a349526385
78 changed files with 1214 additions and 506 deletions
+133 -31
View File
@@ -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;