Files
tehran_tala/apps/admin-panel/src/views/manage/Const.vue
T

374 lines
12 KiB
Vue
Raw Normal View History

<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.
-->
<a-card v-if="hasPermission('config.statics')" title="مدیریت ثابت ها">
<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>
<!-- اسلایدر موبایل -->
<a-tab-pane key="slider" tab="اسلایدر موبایل">
<div v-for="(s, index) in sliders" :key="index" class="slide">
<h6 class="mobile-c">اسلاید {{ index + 1 }}</h6>
<div class="row">
<ImageUploadInput v-model="s.image" purpose="slider" placeholder="تصویر اسلاید" />
<a-input v-model:value="s.link" placeholder="لینک" />
<a-button danger @click="sliders.splice(index, 1)">حذف</a-button>
</div>
</div>
<div class="row mt-4">
<a-button @click="sliders.push({ image: '', link: '' })">افزودن اسلاید</a-button>
<a-button type="primary" @click="saveSliders">ذخیره اسلایدر</a-button>
</div>
</a-tab-pane>
<!-- سینی‌ها -->
<a-tab-pane key="collections" tab="سینی ها">
<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';
import { message } from 'ant-design-vue';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
import { useUsersStore } from '../../stores/users';
import { useDeleteHandler } from '../../composables/useDeleteHandler';
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 }[]>([]);
// ── 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 ?? 'خطا در ویرایش');
}
}
// refused while products reference it
const { remove: removeBrandById } = useDeleteHandler({
onSuccess: loadBrands,
successMessage: 'برند حذف شد',
fallbackErrorMessage: 'خطا در حذف برند',
});
function removeBrand(id: string) {
return removeBrandById(`brand/${id}`);
}
// ── 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
async function saveSliders() {
try {
await api.put('const/mobile_main_slider', { val: sliders.value });
message.success('اسلایدر ذخیره شد');
} catch (e: any) {
message.error(e?.response?.data?.message ?? 'خطا در ذخیره');
}
}
// ── 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;
await Promise.all([loadBrands(), loadCollections()]);
const [t, s] = await Promise.all([
api.get('const/__list__tags'),
api.get('const/mobile_main_slider'),
]);
tags.value = Array.isArray(t.data) ? t.data : [];
sliders.value = Array.isArray(s.data) ? s.data : [];
} 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;
}
.slide {
border: 1px solid #f0f0f0;
border-radius: 6px;
padding: 0.75rem;
margin-bottom: 0.75rem;
}
.mobile-c {
margin: 0 0 0.5rem;
}
.mt-4 {
margin-top: 1rem;
}
</style>