2026-09-13 21:02:25 +03:30
|
|
|
|
<template>
|
|
|
|
|
|
<!--
|
|
|
|
|
|
Ported from legacy henza_admin/src/views/manage/Const.vue (977 lines) — the
|
|
|
|
|
|
"مدیریت ثابت ها" screen, which manages several unrelated things in one card:
|
|
|
|
|
|
brands, tags, the mobile home slider, collections ("سینی") and site config.
|
|
|
|
|
|
|
|
|
|
|
|
Reorganised into tabs rather than one long scroll, because the legacy page is
|
|
|
|
|
|
a single card with five stacked sections and no way to navigate between them.
|
|
|
|
|
|
Every section keeps its own Farsi labels and its own rid gate.
|
|
|
|
|
|
|
|
|
|
|
|
The slider section is IMAGE-based in legacy (an uploader per slide), and is
|
|
|
|
|
|
one here too — each slide uses the shared ImageUploadInput, which posts to the
|
|
|
|
|
|
ported `file/create` endpoint and stores the returned URL.
|
|
|
|
|
|
-->
|
2026-10-13 10:30:25 +03:30
|
|
|
|
<a-card v-if="hasPermission('config.statics')" title="مدیریت ثابت ها" class="page-card table-scroll">
|
2026-09-13 21:02:25 +03:30
|
|
|
|
<a-spin :spinning="loading">
|
|
|
|
|
|
<a-tabs>
|
|
|
|
|
|
<!-- برندها -->
|
|
|
|
|
|
<a-tab-pane key="brands" tab="برند ها">
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<a-input
|
|
|
|
|
|
v-model:value="newBrand"
|
|
|
|
|
|
placeholder="نام برند"
|
|
|
|
|
|
style="max-width: 260px"
|
|
|
|
|
|
@pressEnter="addBrand"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
v-if="hasPermission('config.statics.brand.create')"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
@click="addBrand"
|
|
|
|
|
|
>
|
|
|
|
|
|
افزودن
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<a-table
|
|
|
|
|
|
class="mt-4"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
:data-source="brands"
|
|
|
|
|
|
:columns="brandColumns"
|
|
|
|
|
|
:pagination="false"
|
|
|
|
|
|
row-key="id"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #bodyCell="{ column, record }">
|
|
|
|
|
|
<template v-if="column.dataIndex === 'op'">
|
|
|
|
|
|
<a-tooltip v-if="hasPermission('config.statics.brand.edit')" title="ویرایش">
|
|
|
|
|
|
<EditOutlined class="cursor-pointer mx-1 text-primary" @click="editBrand(record)" />
|
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
|
<a-tooltip v-if="hasPermission('config.statics.brand.remove')" title="حذف">
|
|
|
|
|
|
<a-popconfirm
|
|
|
|
|
|
ok-text="بله"
|
|
|
|
|
|
cancel-text="خیر"
|
|
|
|
|
|
title="آیا مطمئن هستید؟"
|
|
|
|
|
|
@confirm="removeBrand(record.id)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<DeleteOutlined class="cursor-pointer mx-1 text-danger" />
|
|
|
|
|
|
</a-popconfirm>
|
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-table>
|
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- برچسبها -->
|
|
|
|
|
|
<a-tab-pane key="tags" tab="برچسب ها">
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<a-input
|
|
|
|
|
|
v-model:value="tagsInputValue"
|
|
|
|
|
|
placeholder="برچسب"
|
|
|
|
|
|
style="max-width: 260px"
|
|
|
|
|
|
@pressEnter="addTag"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
v-if="hasPermission('config.statics.tags.create')"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
@click="addTag"
|
|
|
|
|
|
>
|
|
|
|
|
|
افزودن
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="mt-4 tags">
|
|
|
|
|
|
<a-tag
|
|
|
|
|
|
v-for="(t, i) in tags"
|
|
|
|
|
|
:key="i"
|
|
|
|
|
|
:closable="hasPermission('config.statics.tags.remove')"
|
|
|
|
|
|
@close="removeTag(i)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ t }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
<a-empty v-if="tags.length === 0" description="برچسبی ثبت نشده است" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<a-button class="mt-4" type="primary" @click="saveTags">ذخیره برچسب ها</a-button>
|
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
|
|
2026-10-25 11:43:45 +03:30
|
|
|
|
<!-- اسلایدر موبایل — برای هر کسی که این صفحه را میبیند (config.statics) -->
|
|
|
|
|
|
<a-tab-pane key="slider" tab="اسلایدر موبایل">
|
2026-10-14 13:31:20 +03:30
|
|
|
|
<div class="slider-toolbar">
|
|
|
|
|
|
<span class="slider-count">{{ sliders.length }} اسلاید</span>
|
2026-09-13 21:02:25 +03:30
|
|
|
|
<div class="row">
|
2026-10-14 13:31:20 +03:30
|
|
|
|
<a-button @click="addSlide">
|
|
|
|
|
|
<template #icon><PlusOutlined /></template>
|
|
|
|
|
|
افزودن اسلاید
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<a-button type="primary" @click="saveSliders">ذخیره اسلایدر</a-button>
|
2026-09-13 21:02:25 +03:30
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-10-14 13:31:20 +03:30
|
|
|
|
<a-empty v-if="sliders.length === 0" description="اسلایدی ثبت نشده است" />
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else class="slider-grid">
|
|
|
|
|
|
<div v-for="(s, index) in sliders" :key="index" class="slide-card">
|
|
|
|
|
|
<div class="slide-card__head">
|
|
|
|
|
|
<span class="slide-card__title">اسلاید {{ index + 1 }}</span>
|
|
|
|
|
|
<a-popconfirm
|
|
|
|
|
|
ok-text="بله"
|
|
|
|
|
|
cancel-text="خیر"
|
|
|
|
|
|
title="این اسلاید حذف شود؟"
|
|
|
|
|
|
@confirm="removeSlide(index)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-button type="text" danger size="small" title="حذف">
|
|
|
|
|
|
<template #icon><DeleteOutlined /></template>
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</a-popconfirm>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="slide-card__media">
|
|
|
|
|
|
<ImageUploadInput v-model="s.image" purpose="slider" placeholder="تصویر اسلاید" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-input v-model:value="s.link" placeholder="لینک" />
|
|
|
|
|
|
</div>
|
2026-09-13 21:02:25 +03:30
|
|
|
|
</div>
|
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
|
|
2026-10-28 16:22:32 +03:30
|
|
|
|
<!-- دکمه سفارشی — یک دکمه با عنوان و لینک، قابل فعال/غیرفعال -->
|
|
|
|
|
|
<a-tab-pane key="custom-button" tab="دکمه سفارشی">
|
|
|
|
|
|
<a-form layout="vertical" class="custom-btn-form" @submit.prevent="saveCustomButton">
|
|
|
|
|
|
<a-form-item label="وضعیت">
|
|
|
|
|
|
<a-switch
|
|
|
|
|
|
v-model:checked="customButton.enabled"
|
|
|
|
|
|
checked-children="فعال"
|
|
|
|
|
|
un-checked-children="غیرفعال"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="عنوان دکمه">
|
|
|
|
|
|
<a-input
|
|
|
|
|
|
v-model:value="customButton.title"
|
|
|
|
|
|
placeholder="مثلاً پشتیبانی تلگرام"
|
|
|
|
|
|
style="max-width: 360px"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="لینک">
|
|
|
|
|
|
<a-input
|
|
|
|
|
|
v-model:value="customButton.link"
|
|
|
|
|
|
placeholder="https://..."
|
|
|
|
|
|
style="max-width: 360px"
|
|
|
|
|
|
dir="ltr"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-button type="primary" @click="saveCustomButton">ذخیره دکمه</a-button>
|
|
|
|
|
|
</a-form>
|
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
|
|
2026-10-14 04:33:24 +03:30
|
|
|
|
<!-- سینیها — super admin only -->
|
|
|
|
|
|
<a-tab-pane v-if="users.isSuperAdmin" key="collections" tab="سینی ها">
|
2026-09-13 21:02:25 +03:30
|
|
|
|
<div class="row">
|
|
|
|
|
|
<a-input
|
|
|
|
|
|
v-model:value="newCollection"
|
|
|
|
|
|
placeholder="نام سینی"
|
|
|
|
|
|
style="max-width: 260px"
|
|
|
|
|
|
@pressEnter="addCollection"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<a-button type="primary" @click="addCollection">
|
|
|
|
|
|
{{ collectionIsEdit ? 'ویرایش' : 'ثبت' }} سینی
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<a-button v-if="collectionIsEdit" @click="cancelCollectionEdit">انصراف</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<a-table
|
|
|
|
|
|
class="mt-4"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
:data-source="collections"
|
|
|
|
|
|
:columns="collectionColumns"
|
|
|
|
|
|
:pagination="false"
|
|
|
|
|
|
row-key="id"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #bodyCell="{ column, record }">
|
|
|
|
|
|
<template v-if="column.dataIndex === 'op'">
|
|
|
|
|
|
<a-tooltip title="ویرایش">
|
|
|
|
|
|
<EditOutlined
|
|
|
|
|
|
class="cursor-pointer mx-1 text-primary"
|
|
|
|
|
|
@click="editCollection(record)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
|
<a-tooltip title="حذف">
|
|
|
|
|
|
<a-popconfirm
|
|
|
|
|
|
ok-text="بله"
|
|
|
|
|
|
cancel-text="خیر"
|
|
|
|
|
|
title="آیا مطمئن هستید؟"
|
|
|
|
|
|
@confirm="removeCollection(record.id)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<DeleteOutlined class="cursor-pointer mx-1 text-danger" />
|
|
|
|
|
|
</a-popconfirm>
|
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-table>
|
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
|
</a-tabs>
|
|
|
|
|
|
</a-spin>
|
|
|
|
|
|
|
|
|
|
|
|
<a-modal v-model:open="brandModal" title="ویرایش برند" @ok="saveBrandEdit">
|
|
|
|
|
|
<a-input v-model:value="brandEditTitle" />
|
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
</a-card>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { onMounted, ref } from 'vue';
|
2026-09-14 15:18:17 +03:30
|
|
|
|
import { message } from 'ant-design-vue';
|
2026-10-14 13:31:20 +03:30
|
|
|
|
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons-vue';
|
2026-09-13 21:02:25 +03:30
|
|
|
|
import { useUsersStore } from '../../stores/users';
|
2026-09-14 15:18:17 +03:30
|
|
|
|
import { useDeleteHandler } from '../../composables/useDeleteHandler';
|
2026-09-13 21:02:25 +03:30
|
|
|
|
import api from '../../api/client';
|
|
|
|
|
|
import ImageUploadInput from '../../components/upload/ImageUploadInput.vue';
|
|
|
|
|
|
|
|
|
|
|
|
const users = useUsersStore();
|
|
|
|
|
|
const hasPermission = (rid: string) => Boolean(users.hasPermission(rid));
|
|
|
|
|
|
|
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
|
|
|
|
|
|
|
// ── brands
|
|
|
|
|
|
const brands = ref<{ id: string; title: string }[]>([]);
|
|
|
|
|
|
const newBrand = ref('');
|
|
|
|
|
|
const brandModal = ref(false);
|
|
|
|
|
|
const brandEditTitle = ref('');
|
|
|
|
|
|
const brandEditId = ref<string | undefined>(undefined);
|
|
|
|
|
|
const brandColumns = [
|
|
|
|
|
|
{ title: 'عنوان', dataIndex: 'title' },
|
|
|
|
|
|
{ title: 'عملیات', dataIndex: 'op' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// ── tags (stored as one setting key, as legacy did with __list__tags)
|
|
|
|
|
|
const tags = ref<string[]>([]);
|
|
|
|
|
|
const tagsInputValue = ref('');
|
|
|
|
|
|
|
|
|
|
|
|
// ── mobile slider (legacy key: mobile_main_slider)
|
|
|
|
|
|
const sliders = ref<{ image: string; link: string }[]>([]);
|
|
|
|
|
|
|
2026-10-28 16:22:32 +03:30
|
|
|
|
// ── custom button (Setting key: custom_button)
|
|
|
|
|
|
const customButton = ref({ enabled: false, title: '', link: '' });
|
|
|
|
|
|
|
2026-09-13 21:02:25 +03:30
|
|
|
|
// ── collections
|
|
|
|
|
|
const collections = ref<{ id: string; title: string }[]>([]);
|
|
|
|
|
|
const newCollection = ref('');
|
|
|
|
|
|
const collectionIsEdit = ref(false);
|
|
|
|
|
|
const collectionEditId = ref<string | undefined>(undefined);
|
|
|
|
|
|
const collectionColumns = [
|
|
|
|
|
|
{ title: 'عنوان', dataIndex: 'title' },
|
|
|
|
|
|
{ title: 'عملیات', dataIndex: 'op' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// ── brands
|
|
|
|
|
|
async function loadBrands() {
|
|
|
|
|
|
const { data } = await api.get('brand');
|
|
|
|
|
|
brands.value = data;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function addBrand() {
|
|
|
|
|
|
if (!newBrand.value.trim()) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.post('brand/create', { title: newBrand.value.trim() });
|
|
|
|
|
|
newBrand.value = '';
|
|
|
|
|
|
await loadBrands();
|
|
|
|
|
|
message.success('برند اضافه شد');
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e?.response?.data?.message ?? 'خطا در افزودن برند');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function editBrand(record: { id: string; title: string }) {
|
|
|
|
|
|
brandEditId.value = record.id;
|
|
|
|
|
|
brandEditTitle.value = record.title;
|
|
|
|
|
|
brandModal.value = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function saveBrandEdit() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.put(`brand/${brandEditId.value}`, { title: brandEditTitle.value });
|
|
|
|
|
|
brandModal.value = false;
|
|
|
|
|
|
await loadBrands();
|
|
|
|
|
|
message.success('تغییرات ثبت شد');
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e?.response?.data?.message ?? 'خطا در ویرایش');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-14 15:18:17 +03:30
|
|
|
|
// refused while products reference it
|
|
|
|
|
|
const { remove: removeBrandById } = useDeleteHandler({
|
|
|
|
|
|
onSuccess: loadBrands,
|
|
|
|
|
|
successMessage: 'برند حذف شد',
|
|
|
|
|
|
fallbackErrorMessage: 'خطا در حذف برند',
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function removeBrand(id: string) {
|
|
|
|
|
|
return removeBrandById(`brand/${id}`);
|
2026-09-13 21:02:25 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── tags
|
|
|
|
|
|
function addTag() {
|
|
|
|
|
|
const v = tagsInputValue.value.trim();
|
|
|
|
|
|
if (!v) return;
|
|
|
|
|
|
if (!tags.value.includes(v)) tags.value.push(v);
|
|
|
|
|
|
tagsInputValue.value = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function removeTag(index: number) {
|
|
|
|
|
|
tags.value.splice(index, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function saveTags() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.put('const/__list__tags', { val: tags.value });
|
|
|
|
|
|
message.success('برچسب ها ذخیره شد');
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e?.response?.data?.message ?? 'خطا در ذخیره');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── slider
|
2026-10-14 13:31:20 +03:30
|
|
|
|
function addSlide() {
|
|
|
|
|
|
sliders.value.push({ image: '', link: '' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function removeSlide(index: number) {
|
|
|
|
|
|
sliders.value.splice(index, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-25 11:43:45 +03:30
|
|
|
|
// Uses the dedicated `const/slider` route: the generic `const/:key` refuses the
|
|
|
|
|
|
// slider's key (admin-misc.controller.ts).
|
2026-09-13 21:02:25 +03:30
|
|
|
|
async function saveSliders() {
|
|
|
|
|
|
try {
|
2026-09-21 08:32:31 +03:30
|
|
|
|
await api.put('const/slider', { val: sliders.value });
|
2026-09-13 21:02:25 +03:30
|
|
|
|
message.success('اسلایدر ذخیره شد');
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e?.response?.data?.message ?? 'خطا در ذخیره');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-28 16:22:32 +03:30
|
|
|
|
async function saveCustomButton() {
|
|
|
|
|
|
const title = customButton.value.title.trim();
|
|
|
|
|
|
const link = customButton.value.link.trim();
|
|
|
|
|
|
if (customButton.value.enabled && (!title || !link)) {
|
|
|
|
|
|
message.warning('برای فعالسازی، عنوان و لینک را وارد کنید');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.put('const/custom_button', {
|
|
|
|
|
|
val: {
|
|
|
|
|
|
enabled: customButton.value.enabled,
|
|
|
|
|
|
title,
|
|
|
|
|
|
link,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
message.success('دکمه سفارشی ذخیره شد');
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e?.response?.data?.message ?? 'خطا در ذخیره');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-13 21:02:25 +03:30
|
|
|
|
// ── collections
|
|
|
|
|
|
async function loadCollections() {
|
|
|
|
|
|
const { data } = await api.post('ciny', { page: 1, limit: 200 });
|
|
|
|
|
|
collections.value = data.rows ?? data;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function addCollection() {
|
|
|
|
|
|
if (!newCollection.value.trim()) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (collectionIsEdit.value) {
|
|
|
|
|
|
await api.put(`ciny/${collectionEditId.value}`, { title: newCollection.value.trim() });
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await api.post('ciny/create', { title: newCollection.value.trim() });
|
|
|
|
|
|
}
|
|
|
|
|
|
cancelCollectionEdit();
|
|
|
|
|
|
await loadCollections();
|
|
|
|
|
|
message.success('عملیات انجام شد');
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e?.response?.data?.message ?? 'خطا');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function editCollection(record: { id: string; title: string }) {
|
|
|
|
|
|
collectionIsEdit.value = true;
|
|
|
|
|
|
collectionEditId.value = record.id;
|
|
|
|
|
|
newCollection.value = record.title;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function cancelCollectionEdit() {
|
|
|
|
|
|
collectionIsEdit.value = false;
|
|
|
|
|
|
collectionEditId.value = undefined;
|
|
|
|
|
|
newCollection.value = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function removeCollection(id: string) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.delete(`ciny/${id}`);
|
|
|
|
|
|
await loadCollections();
|
|
|
|
|
|
message.success('حذف شد');
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e?.response?.data?.message ?? 'خطا در حذف');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
loading.value = true;
|
2026-10-14 04:33:24 +03:30
|
|
|
|
await Promise.all([loadBrands(), users.isSuperAdmin ? loadCollections() : Promise.resolve()]);
|
2026-10-28 16:22:32 +03:30
|
|
|
|
const [t, s, btn] = await Promise.all([
|
|
|
|
|
|
api.get('const/__list__tags'),
|
|
|
|
|
|
api.get('const/slider'),
|
|
|
|
|
|
api.get('const/custom_button'),
|
|
|
|
|
|
]);
|
2026-09-13 21:02:25 +03:30
|
|
|
|
tags.value = Array.isArray(t.data) ? t.data : [];
|
2026-10-25 11:43:45 +03:30
|
|
|
|
sliders.value = Array.isArray(s.data) ? s.data : [];
|
2026-10-28 16:22:32 +03:30
|
|
|
|
if (btn.data && typeof btn.data === 'object' && !Array.isArray(btn.data)) {
|
|
|
|
|
|
customButton.value = {
|
|
|
|
|
|
enabled: Boolean(btn.data.enabled),
|
|
|
|
|
|
title: String(btn.data.title ?? ''),
|
|
|
|
|
|
link: String(btn.data.link ?? ''),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-09-13 21:02:25 +03:30
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.log(e);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.tags {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
2026-10-14 13:31:20 +03:30
|
|
|
|
.slider-toolbar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
margin-bottom: 1rem;
|
2026-09-13 21:02:25 +03:30
|
|
|
|
}
|
2026-10-14 13:31:20 +03:30
|
|
|
|
.slider-count {
|
|
|
|
|
|
color: rgba(0, 0, 0, 0.45);
|
|
|
|
|
|
}
|
|
|
|
|
|
/* one card per slide; min() keeps a card no wider than a narrow phone column */
|
|
|
|
|
|
.slider-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(min(100%, 220px), 1fr));
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.slide-card {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
border: 1px solid #f0f0f0;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
.slide-card__head {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.slide-card__title {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
/* Fixed-height media box: the empty upload tile, the uploading spinner and the
|
|
|
|
|
|
saved thumbnail all fill it, so the card keeps the same height when a photo
|
|
|
|
|
|
is added or removed (no jump). The shared upload tile is 104px square by
|
|
|
|
|
|
default; here it becomes a full-width banner preview. */
|
|
|
|
|
|
.slide-card__media {
|
|
|
|
|
|
height: 140px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.slide-card__media :deep(.image-upload),
|
|
|
|
|
|
.slide-card__media :deep(.ant-upload-wrapper),
|
|
|
|
|
|
.slide-card__media :deep(.ant-upload-list),
|
|
|
|
|
|
.slide-card__media :deep(.ant-upload-list-item-container),
|
|
|
|
|
|
.slide-card__media :deep(.ant-upload.ant-upload-select) {
|
|
|
|
|
|
width: 100% !important;
|
|
|
|
|
|
height: 100% !important;
|
|
|
|
|
|
margin: 0 !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
.slide-card__media :deep(.ant-upload-wrapper) {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
.slide-card__media :deep(.ant-upload-list-item-thumbnail img) {
|
|
|
|
|
|
object-fit: cover;
|
2026-09-13 21:02:25 +03:30
|
|
|
|
}
|
|
|
|
|
|
.mt-4 {
|
|
|
|
|
|
margin-top: 1rem;
|
|
|
|
|
|
}
|
2026-10-28 16:22:32 +03:30
|
|
|
|
.custom-btn-form {
|
|
|
|
|
|
max-width: 480px;
|
|
|
|
|
|
}
|
2026-10-13 10:30:25 +03:30
|
|
|
|
|
|
|
|
|
|
@media (max-width: 575px) {
|
|
|
|
|
|
/* the add-row input fills what the button leaves instead of a fixed 260px */
|
|
|
|
|
|
.row > .ant-input {
|
|
|
|
|
|
flex: 1 1 140px;
|
|
|
|
|
|
max-width: none !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
.mt-4 {
|
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-13 21:02:25 +03:30
|
|
|
|
</style>
|