204 lines
7.2 KiB
Vue
204 lines
7.2 KiB
Vue
<script setup lang="ts">
|
|
import { isSectionKey, SECTION_LABELS, type SectionKey, sectionSchemas } from "@imen/shared";
|
|
import { message } from "ant-design-vue";
|
|
import { type Component, computed, ref } from "vue";
|
|
import { useRoute } from "vue-router";
|
|
import { api, errorMessage } from "@/api/http";
|
|
import VuesaxIcon from "@/components/VuesaxIcon.vue";
|
|
import type { SectionResponse } from "@/api/types";
|
|
import PageHeader from "@/components/PageHeader.vue";
|
|
import { useZodForm } from "@/composables/useZodForm";
|
|
import { useAuthStore } from "@/stores/auth";
|
|
import { jalali, SITE_URL } from "@/utils/format";
|
|
import AboutForm from "@/sections/AboutForm.vue";
|
|
import ContactForm from "@/sections/ContactForm.vue";
|
|
import FooterForm from "@/sections/FooterForm.vue";
|
|
import HeroForm from "@/sections/HeroForm.vue";
|
|
import IndustriesForm from "@/sections/IndustriesForm.vue";
|
|
import ProductsIntroForm from "@/sections/ProductsIntroForm.vue";
|
|
import ServicesForm from "@/sections/ServicesForm.vue";
|
|
import SettingsForm from "@/sections/SettingsForm.vue";
|
|
import StandardsForm from "@/sections/StandardsForm.vue";
|
|
import StatsForm from "@/sections/StatsForm.vue";
|
|
import TapeForm from "@/sections/TapeForm.vue";
|
|
import TestimonialsForm from "@/sections/TestimonialsForm.vue";
|
|
|
|
const FORMS: Record<SectionKey, Component> = {
|
|
settings: SettingsForm,
|
|
hero: HeroForm,
|
|
tape: TapeForm,
|
|
stats: StatsForm,
|
|
productsIntro: ProductsIntroForm,
|
|
about: AboutForm,
|
|
services: ServicesForm,
|
|
standards: StandardsForm,
|
|
industries: IndustriesForm,
|
|
testimonials: TestimonialsForm,
|
|
contact: ContactForm,
|
|
footer: FooterForm,
|
|
};
|
|
|
|
const auth = useAuthStore();
|
|
const route = useRoute();
|
|
const key = String(route.params.key);
|
|
const valid = isSectionKey(key);
|
|
const sectionKey = (valid ? key : "hero") as SectionKey;
|
|
|
|
// the page is re-created per key (RouterView keys by path), so one form instance per section
|
|
const { model, validate, handleError, reset } = useZodForm(
|
|
sectionSchemas[sectionKey] as (typeof sectionSchemas)["hero"],
|
|
{} as never,
|
|
);
|
|
const loaded = ref(false);
|
|
const saving = ref(false);
|
|
const updatedAt = ref<string | null>(null);
|
|
const label = computed(() => (valid ? SECTION_LABELS[sectionKey] : null));
|
|
|
|
async function load() {
|
|
try {
|
|
const res = await api<SectionResponse>(`/admin/sections/${sectionKey}`);
|
|
const data = res.data as any;
|
|
if (sectionKey !== "settings" && data.enabled === undefined) {
|
|
data.enabled = true;
|
|
}
|
|
reset(data as never);
|
|
updatedAt.value = res.updatedAt;
|
|
loaded.value = true;
|
|
} catch (e) {
|
|
void message.error(errorMessage(e));
|
|
}
|
|
}
|
|
|
|
async function save() {
|
|
const data = validate();
|
|
if (!data) return;
|
|
saving.value = true;
|
|
try {
|
|
const res = await api<SectionResponse>(`/admin/sections/${sectionKey}`, { method: "PUT", body: data });
|
|
reset(res.data as never);
|
|
updatedAt.value = res.updatedAt;
|
|
void message.success("ذخیره شد؛ سایت تا چند لحظه دیگر بهروز میشود");
|
|
} catch (e) {
|
|
handleError(e);
|
|
} finally {
|
|
saving.value = false;
|
|
}
|
|
}
|
|
|
|
const togglingVisibility = ref(false);
|
|
|
|
async function onVisibilityChange(val: any) {
|
|
const nextVal = Boolean(val);
|
|
(model.value as any).enabled = nextVal;
|
|
togglingVisibility.value = true;
|
|
try {
|
|
const res = await api<{ key: string; enabled: boolean; updatedAt: string }>(
|
|
`/admin/sections/${sectionKey}/visibility`,
|
|
{
|
|
method: "PATCH",
|
|
body: { enabled: nextVal },
|
|
},
|
|
);
|
|
(model.value as any).enabled = res.enabled !== false;
|
|
updatedAt.value = res.updatedAt;
|
|
void message.success(
|
|
`وضعیت نمایش به ${nextVal ? "نمایش" : "عدم نمایش"} تغییر یافت`,
|
|
);
|
|
} catch (e) {
|
|
(model.value as any).enabled = !nextVal;
|
|
void message.error(errorMessage(e));
|
|
} finally {
|
|
togglingVisibility.value = false;
|
|
}
|
|
}
|
|
|
|
if (valid) void load();
|
|
</script>
|
|
|
|
<template>
|
|
<a-result v-if="!valid" status="404" title="بخش پیدا نشد" />
|
|
<template v-else>
|
|
<PageHeader
|
|
:title="label!.title"
|
|
:subtitle="updatedAt ? `آخرین ذخیره: ${jalali(updatedAt)}` : label!.desc"
|
|
back="/sections"
|
|
>
|
|
<div v-if="sectionKey !== 'settings'" class="max-lg:hidden flex items-center gap-2 me-2">
|
|
<span class="text-sm font-medium text-gray-700">نمایش در سایت:</span>
|
|
<a-switch
|
|
:checked="(model as any)?.enabled !== false"
|
|
:loading="togglingVisibility"
|
|
checked-children="نمایش"
|
|
un-checked-children="مخفی"
|
|
@change="onVisibilityChange"
|
|
/>
|
|
</div>
|
|
<a :href="SITE_URL" target="_blank" rel="noopener" class="max-lg:hidden"><a-button>مشاهده سایت</a-button></a>
|
|
<a-button
|
|
type="primary"
|
|
class="max-lg:hidden"
|
|
:loading="saving"
|
|
:disabled="!loaded || (sectionKey === 'settings' && !auth.isAdmin)"
|
|
@click="save"
|
|
>
|
|
<template #icon><VuesaxIcon name="save-2" :size="16" /></template>
|
|
ذخیره
|
|
</a-button>
|
|
</PageHeader>
|
|
<a-spin :spinning="!loaded">
|
|
<a-alert
|
|
v-if="loaded && sectionKey === 'settings' && !auth.isAdmin"
|
|
type="error"
|
|
show-icon
|
|
message="فقط مدیر کل میتواند تنظیمات سایت را تغییر دهد."
|
|
class="mb-4"
|
|
/>
|
|
<a-alert
|
|
v-else-if="loaded && sectionKey !== 'settings' && (model as any).enabled === false"
|
|
type="warning"
|
|
show-icon
|
|
message="این بخش در حال حاضر در سایت نمایش داده نمیشود (مخفی است)."
|
|
class="mb-4"
|
|
/>
|
|
<a-form v-if="loaded" layout="vertical" class="pb-20 lg:pb-0" @submit.prevent="save">
|
|
<component :is="FORMS[sectionKey]" v-model="model" />
|
|
</a-form>
|
|
</a-spin>
|
|
|
|
<!-- Mobile Sticky Bottom Action Bar -->
|
|
<div class="lg:hidden fixed bottom-0 inset-x-0 z-30 border-t border-gray-200 bg-white/95 px-4 py-2.5 backdrop-blur-md shadow-lg flex items-center justify-between gap-3">
|
|
<div v-if="sectionKey !== 'settings'" class="flex items-center gap-2">
|
|
<a-switch
|
|
:checked="(model as any)?.enabled !== false"
|
|
:loading="togglingVisibility"
|
|
size="small"
|
|
@change="onVisibilityChange"
|
|
/>
|
|
<span class="text-xs font-medium text-gray-700">
|
|
{{ (model as any)?.enabled !== false ? 'نمایش در سایت' : 'مخفی' }}
|
|
</span>
|
|
</div>
|
|
<div v-else class="text-xs text-gray-500 font-medium">تنظیمات پایه</div>
|
|
|
|
<div class="flex items-center gap-2">
|
|
<a :href="SITE_URL" target="_blank" rel="noopener">
|
|
<a-button size="middle" class="!px-3" title="مشاهده سایت">
|
|
<template #icon><VuesaxIcon name="eye" :size="16" /></template>
|
|
</a-button>
|
|
</a>
|
|
<a-button
|
|
type="primary"
|
|
size="middle"
|
|
class="!font-bold !px-5"
|
|
:loading="saving"
|
|
:disabled="!loaded || (sectionKey === 'settings' && !auth.isAdmin)"
|
|
@click="save"
|
|
>
|
|
<template #icon><VuesaxIcon name="save-2" :size="16" /></template>
|
|
ذخیره تغییرات
|
|
</a-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</template>
|