feat(admin): editable contact fields in sections and larger logos
Allow address, phone, map, and related site chrome to be edited from contact/settings for all editors, and enlarge brand marks on login, header, and preloader. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import type { SectionData } from "@imen/shared";
|
||||
import FormField from "@/components/FormField.vue";
|
||||
|
||||
/** Shared address / phone / email / hours fields from the settings section. */
|
||||
const data = defineModel<SectionData<"settings">>({ required: true });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** Zod path prefix when nested under another form context */
|
||||
pathPrefix?: string;
|
||||
}>(),
|
||||
{ pathPrefix: "" },
|
||||
);
|
||||
|
||||
function path(field: string) {
|
||||
return props.pathPrefix ? `${props.pathPrefix}.${field}` : field;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid gap-x-4 md:grid-cols-2">
|
||||
<FormField :path="path('phoneDisplay')" label="تلفن (نمایشی)" required>
|
||||
<a-input v-model:value="data.phoneDisplay" />
|
||||
</FormField>
|
||||
<FormField :path="path('phone')" label="تلفن (برای لینک و گوگل)" hint="با کد کشور، مثلاً +98-21-91008800" required>
|
||||
<a-input v-model:value="data.phone" class="ltr-input" />
|
||||
</FormField>
|
||||
<FormField :path="path('email')" label="ایمیل" required class="md:col-span-2">
|
||||
<a-input v-model:value="data.email" class="ltr-input" />
|
||||
</FormField>
|
||||
<FormField :path="path('address.locality')" label="شهر" required>
|
||||
<a-input v-model:value="data.address.locality" />
|
||||
</FormField>
|
||||
<FormField :path="path('address.country')" label="کد کشور" required>
|
||||
<a-input v-model:value="data.address.country" class="ltr-input" :maxlength="2" />
|
||||
</FormField>
|
||||
<FormField :path="path('address.street')" label="نشانی" required class="md:col-span-2">
|
||||
<a-input v-model:value="data.address.street" />
|
||||
</FormField>
|
||||
<FormField
|
||||
:path="path('mapUrl')"
|
||||
label="لینک نقشه"
|
||||
hint="اختیاری — لینک گوگلمپ، نشان و …"
|
||||
class="md:col-span-2"
|
||||
>
|
||||
<a-input v-model:value="data.mapUrl" class="ltr-input" placeholder="https://maps.google.com/..." />
|
||||
</FormField>
|
||||
<FormField :path="path('workingHours')" label="ساعات کاری (متن نمایشی)" required class="md:col-span-2">
|
||||
<a-input v-model:value="data.workingHours" />
|
||||
</FormField>
|
||||
</div>
|
||||
</template>
|
||||
@@ -107,9 +107,9 @@ async function submit() {
|
||||
|
||||
<div class="mt-8 flex items-center gap-4">
|
||||
<span
|
||||
class="flex size-14 items-center justify-center rounded-xl bg-white shadow-[0_0_28px_rgba(10,30,92,0.45)]"
|
||||
class="flex size-20 items-center justify-center overflow-hidden rounded-2xl bg-white shadow-[0_0_28px_rgba(10,30,92,0.45)]"
|
||||
>
|
||||
<img src="/logo.png" alt="ایمن پیشگامان" class="size-9 object-contain" />
|
||||
<img src="/logo.png" alt="ایمن پیشگامان" class="size-full scale-110 object-contain object-center" />
|
||||
</span>
|
||||
<div class="leading-tight">
|
||||
<div class="text-2xl font-black tracking-tight md:text-3xl">ایمن پیشگامان</div>
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { isSectionKey, SECTION_LABELS, type SectionKey, sectionSchemas } from "@imen/shared";
|
||||
import {
|
||||
isSectionKey,
|
||||
SECTION_LABELS,
|
||||
type SectionData,
|
||||
type SectionKey,
|
||||
sectionSchemas,
|
||||
settingsSectionSchema,
|
||||
} from "@imen/shared";
|
||||
import { message } from "ant-design-vue";
|
||||
import { type Component, computed, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
@@ -8,7 +15,6 @@ 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";
|
||||
@@ -38,7 +44,6 @@ const FORMS: Record<SectionKey, Component> = {
|
||||
footer: FooterForm,
|
||||
};
|
||||
|
||||
const auth = useAuthStore();
|
||||
const route = useRoute();
|
||||
const key = String(route.params.key);
|
||||
const valid = isSectionKey(key);
|
||||
@@ -49,6 +54,14 @@ const { model, validate, handleError, reset } = useZodForm(
|
||||
sectionSchemas[sectionKey] as (typeof sectionSchemas)["hero"],
|
||||
{} as never,
|
||||
);
|
||||
/** Contact page also edits site-wide contact info from settings */
|
||||
const settingsModel = ref<SectionData<"settings"> | null>(null);
|
||||
const contactModel = computed({
|
||||
get: () => model.value as unknown as SectionData<"contact">,
|
||||
set: (v: SectionData<"contact">) => {
|
||||
model.value = v as never;
|
||||
},
|
||||
});
|
||||
const loaded = ref(false);
|
||||
const saving = ref(false);
|
||||
const updatedAt = ref<string | null>(null);
|
||||
@@ -57,12 +70,20 @@ 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;
|
||||
const data = res.data as Record<string, unknown>;
|
||||
if (sectionKey !== "settings" && data.enabled === undefined) {
|
||||
data.enabled = true;
|
||||
}
|
||||
reset(data as never);
|
||||
updatedAt.value = res.updatedAt;
|
||||
|
||||
if (sectionKey === "contact") {
|
||||
const settingsRes = await api<SectionResponse>("/admin/sections/settings");
|
||||
settingsModel.value = settingsSectionSchema.parse(settingsRes.data);
|
||||
} else {
|
||||
settingsModel.value = null;
|
||||
}
|
||||
|
||||
loaded.value = true;
|
||||
} catch (e) {
|
||||
void message.error(errorMessage(e));
|
||||
@@ -72,8 +93,25 @@ async function load() {
|
||||
async function save() {
|
||||
const data = validate();
|
||||
if (!data) return;
|
||||
|
||||
if (sectionKey === "contact" && settingsModel.value) {
|
||||
const settingsParsed = settingsSectionSchema.safeParse(settingsModel.value);
|
||||
if (!settingsParsed.success) {
|
||||
void message.error("اطلاعات تماس معتبر نیست — نشانی، تلفن یا ایمیل را بررسی کنید");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
if (sectionKey === "contact" && settingsModel.value) {
|
||||
const settingsRes = await api<SectionResponse>("/admin/sections/settings", {
|
||||
method: "PUT",
|
||||
body: settingsSectionSchema.parse(settingsModel.value),
|
||||
});
|
||||
settingsModel.value = settingsSectionSchema.parse(settingsRes.data);
|
||||
}
|
||||
|
||||
const res = await api<SectionResponse>(`/admin/sections/${sectionKey}`, { method: "PUT", body: data });
|
||||
reset(res.data as never);
|
||||
updatedAt.value = res.updatedAt;
|
||||
@@ -87,9 +125,9 @@ async function save() {
|
||||
|
||||
const togglingVisibility = ref(false);
|
||||
|
||||
async function onVisibilityChange(val: any) {
|
||||
async function onVisibilityChange(val: unknown) {
|
||||
const nextVal = Boolean(val);
|
||||
(model.value as any).enabled = nextVal;
|
||||
(model.value as { enabled?: boolean }).enabled = nextVal;
|
||||
togglingVisibility.value = true;
|
||||
try {
|
||||
const res = await api<{ key: string; enabled: boolean; updatedAt: string }>(
|
||||
@@ -99,13 +137,13 @@ async function onVisibilityChange(val: any) {
|
||||
body: { enabled: nextVal },
|
||||
},
|
||||
);
|
||||
(model.value as any).enabled = res.enabled !== false;
|
||||
(model.value as { enabled?: boolean }).enabled = res.enabled !== false;
|
||||
updatedAt.value = res.updatedAt;
|
||||
void message.success(
|
||||
`وضعیت نمایش به ${nextVal ? "نمایش" : "عدم نمایش"} تغییر یافت`,
|
||||
);
|
||||
} catch (e) {
|
||||
(model.value as any).enabled = !nextVal;
|
||||
(model.value as { enabled?: boolean }).enabled = !nextVal;
|
||||
void message.error(errorMessage(e));
|
||||
} finally {
|
||||
togglingVisibility.value = false;
|
||||
@@ -126,7 +164,7 @@ if (valid) void load();
|
||||
<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"
|
||||
:checked="(model as { enabled?: boolean })?.enabled !== false"
|
||||
:loading="togglingVisibility"
|
||||
checked-children="نمایش"
|
||||
un-checked-children="مخفی"
|
||||
@@ -138,7 +176,7 @@ if (valid) void load();
|
||||
type="primary"
|
||||
class="max-lg:hidden"
|
||||
:loading="saving"
|
||||
:disabled="!loaded || (sectionKey === 'settings' && !auth.isAdmin)"
|
||||
:disabled="!loaded"
|
||||
@click="save"
|
||||
>
|
||||
<template #icon><VuesaxIcon name="save-2" :size="16" /></template>
|
||||
@@ -147,21 +185,19 @@ if (valid) void load();
|
||||
</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"
|
||||
v-if="loaded && sectionKey !== 'settings' && (model as { enabled?: boolean }).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" />
|
||||
<ContactForm
|
||||
v-if="sectionKey === 'contact'"
|
||||
v-model="contactModel"
|
||||
v-model:settings="settingsModel"
|
||||
/>
|
||||
<component v-else :is="FORMS[sectionKey]" v-model="model" />
|
||||
</a-form>
|
||||
</a-spin>
|
||||
|
||||
@@ -169,13 +205,13 @@ if (valid) void load();
|
||||
<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"
|
||||
:checked="(model as { enabled?: boolean })?.enabled !== false"
|
||||
:loading="togglingVisibility"
|
||||
size="small"
|
||||
@change="onVisibilityChange"
|
||||
/>
|
||||
<span class="text-xs font-medium text-gray-700">
|
||||
{{ (model as any)?.enabled !== false ? 'نمایش در سایت' : 'مخفی' }}
|
||||
{{ (model as { enabled?: boolean })?.enabled !== false ? 'نمایش در سایت' : 'مخفی' }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="text-xs text-gray-500 font-medium">تنظیمات پایه</div>
|
||||
@@ -191,7 +227,7 @@ if (valid) void load();
|
||||
size="middle"
|
||||
class="!font-bold !px-5"
|
||||
:loading="saving"
|
||||
:disabled="!loaded || (sectionKey === 'settings' && !auth.isAdmin)"
|
||||
:disabled="!loaded"
|
||||
@click="save"
|
||||
>
|
||||
<template #icon><VuesaxIcon name="save-2" :size="16" /></template>
|
||||
|
||||
@@ -6,13 +6,10 @@ import { useRouter } from "vue-router";
|
||||
import { api, errorMessage } from "@/api/http";
|
||||
import type { AdminSectionSummary } from "@/api/types";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { jalali } from "@/utils/format";
|
||||
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
// site-wide settings are editable by admins only
|
||||
const keys = SECTION_KEYS.filter((k) => k !== "settings" || auth.isAdmin);
|
||||
const keys = SECTION_KEYS;
|
||||
|
||||
const loading = ref(true);
|
||||
const sectionEnabled = reactive<Record<string, boolean>>({
|
||||
|
||||
@@ -103,7 +103,7 @@ onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeader title="کاربران پنل" subtitle="مدیر کل به همه بخشها دسترسی دارد؛ ویرایشگر به جز کاربران و تنظیمات سایت">
|
||||
<PageHeader title="کاربران پنل" subtitle="مدیر کل به همه بخشها دسترسی دارد؛ ویرایشگر به جز مدیریت کاربران و حذف دادهها">
|
||||
<a-button type="primary" @click="openNew">
|
||||
<template #icon><VuesaxIcon name="add" :size="16" /></template>
|
||||
کاربر جدید
|
||||
@@ -241,7 +241,6 @@ onMounted(load);
|
||||
</div>
|
||||
<ul class="mt-2 space-y-1 text-[11px] text-gray-600 list-disc list-inside">
|
||||
<li>مدیریت کاربران، نقشها و تنظیمات امنیتی</li>
|
||||
<li>تنظیمات پایه سایت، سئو، شبکهها و ساعات کاری</li>
|
||||
<li>حذف دادهها (محصولات، دستهها، پیامها، رسانهها)</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -253,7 +252,7 @@ onMounted(load);
|
||||
</div>
|
||||
<ul class="mt-2 space-y-1 text-[11px] text-gray-600 list-disc list-inside">
|
||||
<li>ایجاد، ویرایش و انتشار محصولات و دستهبندیها</li>
|
||||
<li>ویرایش محتوای بخشهای صفحه اصلی</li>
|
||||
<li>ویرایش بخشهای سایت، نشانی، تماس، سئو و شبکهها</li>
|
||||
<li>آپلود تصاویر در رسانه و مشاهده پیامهای تماس</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -269,7 +268,6 @@ onMounted(load);
|
||||
</div>
|
||||
<ul class="mt-2.5 space-y-1.5 text-xs text-gray-600 list-disc list-inside">
|
||||
<li>مدیریت کاربران، افزودن کاربر و تغییر نقشها و وضعیت حساب</li>
|
||||
<li>دسترسی کامل به تنظیمات پایه سایت، ساعات کاری، شبکهها و سئو</li>
|
||||
<li>مجوز حذف محصولات، دستهبندیها، رسانهها و پیامهای تماس</li>
|
||||
<li>مشاهده لاگهای امنیتی و رویدادهای کامل سامانه</li>
|
||||
</ul>
|
||||
@@ -282,7 +280,7 @@ onMounted(load);
|
||||
</div>
|
||||
<ul class="mt-2.5 space-y-1.5 text-xs text-gray-600 list-disc list-inside">
|
||||
<li>ایجاد، ویرایش، کپی و انتشار محصولات و گروهها</li>
|
||||
<li>ویرایش محتوای بخشهای صفحه اصلی و فعال/مخفیسازی نمایش آنها</li>
|
||||
<li>ویرایش بخشهای سایت، نشانی، تماس، سئو، شبکهها و نمایش/مخفیسازی</li>
|
||||
<li>آپلود و ویرایش اطلاعات رسانهها در کتابخانه رسانه</li>
|
||||
<li>مشاهده پیامهای تماس و بهروزرسانی وضعیت (بدون دسترسی حذف یا مدیریت کاربران)</li>
|
||||
</ul>
|
||||
@@ -319,7 +317,7 @@ onMounted(load);
|
||||
:options="ROLE_OPTIONS"
|
||||
/>
|
||||
<div class="mt-1.5 text-[11px] text-gray-500">
|
||||
{{ model.role === 'ADMIN' ? 'مدیر کل: دسترسی کامل سیستمی، حذف دادهها، تنظیمات و مدیریت کاربران' : 'ویرایشگر: ویرایش محتوا، محصولات، بخشها و پیامها (بدون حذف)' }}
|
||||
{{ model.role === 'ADMIN' ? 'مدیر کل: دسترسی کامل سیستمی، حذف دادهها و مدیریت کاربران' : 'ویرایشگر: ویرایش محتوا، محصولات، بخشهای سایت (از جمله نشانی و تماس)، پیامها (بدون حذف)' }}
|
||||
</div>
|
||||
</FormField>
|
||||
<FormField v-if="!isNew" path="isActive">
|
||||
|
||||
@@ -13,7 +13,7 @@ const data = defineModel<SectionData<"about">>({ required: true });
|
||||
<div class="grid gap-6 xl:grid-cols-3">
|
||||
<div class="space-y-6 xl:col-span-2">
|
||||
<a-card title="عنوان و متن">
|
||||
<SectionHeadFields v-model="data.head" :with-desc="false" />
|
||||
<SectionHeadFields v-model="data.head" />
|
||||
<FormField path="body" label="متن درباره ما" required>
|
||||
<a-textarea v-model:value="data.body" :auto-size="{ minRows: 4, maxRows: 10 }" />
|
||||
</FormField>
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import type { SectionData } from "@imen/shared";
|
||||
import ContactInfoFields from "@/components/ContactInfoFields.vue";
|
||||
import FormField from "@/components/FormField.vue";
|
||||
import SectionHeadFields from "@/components/SectionHeadFields.vue";
|
||||
|
||||
const data = defineModel<SectionData<"contact">>({ required: true });
|
||||
/** Site-wide contact info (settings); saved together with this section. */
|
||||
const settings = defineModel<SectionData<"settings"> | null>("settings", { default: null });
|
||||
|
||||
function onSettingsUpdate(value: SectionData<"settings">) {
|
||||
settings.value = value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<a-card title="عنوان بخش">
|
||||
<SectionHeadFields v-model="data.head" />
|
||||
<p class="text-sm text-gray-500">نشانی، تلفن، ایمیل و ساعات کاری از «تنظیمات سایت» خوانده میشوند.</p>
|
||||
</a-card>
|
||||
|
||||
<a-card v-if="settings" title="نشانی و اطلاعات تماس">
|
||||
<p class="mb-4 text-sm text-gray-500">
|
||||
این مقادیر در هدر، فوتر، بخش تماس و سئوی سایت مشترکاند و با ذخیرهٔ همین صفحه بهروز میشوند.
|
||||
</p>
|
||||
<ContactInfoFields :model-value="settings" @update:model-value="onSettingsUpdate" />
|
||||
</a-card>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-2">
|
||||
<a-card title="کادر پاسخگویی اضطراری">
|
||||
<FormField path="emergencyTitle" label="عنوان" required><a-input v-model:value="data.emergencyTitle" /></FormField>
|
||||
@@ -24,10 +38,16 @@ const data = defineModel<SectionData<"contact">>({ required: true });
|
||||
<FormField path="formTitle" label="عنوان فرم" hint="برای شکستن خط Enter بزنید" required>
|
||||
<a-textarea v-model:value="data.formTitle" :auto-size="{ minRows: 2 }" />
|
||||
</FormField>
|
||||
<FormField path="submitLabel" label="متن دکمه ارسال" required>
|
||||
<a-input v-model:value="data.submitLabel" />
|
||||
</FormField>
|
||||
<FormField path="successTitle" label="عنوان پیام موفقیت" required><a-input v-model:value="data.successTitle" /></FormField>
|
||||
<FormField path="successText" label="متن پیام موفقیت" required>
|
||||
<a-textarea v-model:value="data.successText" :auto-size="{ minRows: 2 }" />
|
||||
</FormField>
|
||||
<FormField path="resetLabel" label="متن دکمه بعد از موفقیت" required>
|
||||
<a-input v-model:value="data.resetLabel" />
|
||||
</FormField>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,15 +6,30 @@ const data = defineModel<SectionData<"footer">>({ required: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-card title="فوتر">
|
||||
<FormField path="about" label="متن معرفی" required>
|
||||
<a-textarea v-model:value="data.about" :auto-size="{ minRows: 2, maxRows: 5 }" />
|
||||
</FormField>
|
||||
<div class="grid gap-x-4 md:grid-cols-3">
|
||||
<FormField path="copyright" label="متن کپیرایت" required><a-input v-model:value="data.copyright" /></FormField>
|
||||
<FormField path="certLine" label="خط گواهینامهها"><a-input v-model:value="data.certLine" class="ltr-input" /></FormField>
|
||||
<FormField path="emergencyNote" label="یادداشت پشتیبانی"><a-input v-model:value="data.emergencyNote" /></FormField>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500">لینک دستهها و اطلاعات تماس فوتر بهصورت خودکار از دستهبندیها و تنظیمات سایت ساخته میشوند.</p>
|
||||
</a-card>
|
||||
<div class="space-y-6">
|
||||
<a-card title="فوتر">
|
||||
<FormField path="about" label="متن معرفی" required>
|
||||
<a-textarea v-model:value="data.about" :auto-size="{ minRows: 2, maxRows: 5 }" />
|
||||
</FormField>
|
||||
<div class="grid gap-x-4 md:grid-cols-3">
|
||||
<FormField path="linksTitle" label="عنوان ستون لینکها" required>
|
||||
<a-input v-model:value="data.linksTitle" />
|
||||
</FormField>
|
||||
<FormField path="productsTitle" label="عنوان ستون محصولات" required>
|
||||
<a-input v-model:value="data.productsTitle" />
|
||||
</FormField>
|
||||
<FormField path="contactTitle" label="عنوان ستون ارتباط" required>
|
||||
<a-input v-model:value="data.contactTitle" />
|
||||
</FormField>
|
||||
</div>
|
||||
<div class="grid gap-x-4 md:grid-cols-3">
|
||||
<FormField path="copyright" label="متن کپیرایت" required><a-input v-model:value="data.copyright" /></FormField>
|
||||
<FormField path="certLine" label="خط گواهینامهها"><a-input v-model:value="data.certLine" class="ltr-input" /></FormField>
|
||||
<FormField path="emergencyNote" label="یادداشت پشتیبانی"><a-input v-model:value="data.emergencyNote" /></FormField>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500">
|
||||
لینک دستهها و اطلاعات تماس فوتر بهصورت خودکار از دستهبندیها و تنظیمات سایت ساخته میشوند.
|
||||
</p>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { type MediaDto, type SectionData, SOCIAL_TYPES, type SocialType, WEEK_DAYS } from "@imen/shared";
|
||||
import ContactInfoFields from "@/components/ContactInfoFields.vue";
|
||||
import FormField from "@/components/FormField.vue";
|
||||
import ImageField from "@/components/ImageField.vue";
|
||||
import ListEditor from "@/components/ListEditor.vue";
|
||||
@@ -66,33 +67,22 @@ function pickOgImage(m: MediaDto) {
|
||||
</a-card>
|
||||
|
||||
<a-card title="اطلاعات تماس">
|
||||
<ContactInfoFields v-model="data" />
|
||||
</a-card>
|
||||
|
||||
<a-card title="دکمه هدر">
|
||||
<div class="grid gap-x-4 md:grid-cols-2">
|
||||
<FormField path="phoneDisplay" label="تلفن (نمایشی)" required>
|
||||
<a-input v-model:value="data.phoneDisplay" />
|
||||
<FormField path="headerCta.label" label="متن دکمه" required>
|
||||
<a-input v-model:value="data.headerCta.label" />
|
||||
</FormField>
|
||||
<FormField path="phone" label="تلفن (برای لینک و گوگل)" hint="با کد کشور، مثلاً +98-21-91008800" required>
|
||||
<a-input v-model:value="data.phone" class="ltr-input" />
|
||||
</FormField>
|
||||
<FormField path="email" label="ایمیل" required class="md:col-span-2">
|
||||
<a-input v-model:value="data.email" class="ltr-input" />
|
||||
</FormField>
|
||||
<FormField path="address.locality" label="شهر" required>
|
||||
<a-input v-model:value="data.address.locality" />
|
||||
</FormField>
|
||||
<FormField path="address.country" label="کد کشور" required>
|
||||
<a-input v-model:value="data.address.country" class="ltr-input" :maxlength="2" />
|
||||
</FormField>
|
||||
<FormField path="address.street" label="نشانی" required class="md:col-span-2">
|
||||
<a-input v-model:value="data.address.street" />
|
||||
<FormField path="headerCta.href" label="لینک" required>
|
||||
<a-input v-model:value="data.headerCta.href" class="ltr-input" placeholder="/#contact" />
|
||||
</FormField>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-card title="ساعات کاری">
|
||||
<FormField path="workingHours" label="متن نمایشی" required>
|
||||
<a-input v-model:value="data.workingHours" />
|
||||
</FormField>
|
||||
<FormField path="openingHours.days" label="روزهای کاری (برای گوگل)">
|
||||
<a-card title="ساعات کاری (برای گوگل)">
|
||||
<FormField path="openingHours.days" label="روزهای کاری">
|
||||
<a-checkbox-group
|
||||
v-model:value="data.openingHours.days"
|
||||
:options="DAY_OPTIONS"
|
||||
|
||||
@@ -10,7 +10,7 @@ const data = defineModel<SectionData<"testimonials">>({ required: true });
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<a-card title="عنوان بخش">
|
||||
<SectionHeadFields v-model="data.head" :with-highlight="false" :with-desc="false" />
|
||||
<SectionHeadFields v-model="data.head" />
|
||||
</a-card>
|
||||
<a-card title="نظرات" :body-style="{ background: '#fafafa' }">
|
||||
<ListEditor v-model="data.items" :create="() => ({ text: '', name: '', role: '' })" :max="12" add-label="افزودن نظر" :item-title="(q) => q.name">
|
||||
|
||||
Reference in New Issue
Block a user