پوسته، ظاهر سایت است. محتوای سایت در پایگاههای داده نگهداری میشود و با تغییر پوسته، فقط نحوه نمایش آن عوض میشود؛ مطالب، دستهها، دیدگاهها و تنظیمات دستنخورده میمانند.
برای نمایش هر صفحه، موتور فایل html مربوط به آن صفحه را از پوسته فعال میخواند، کلیدواژههای درصددار را با مقدار واقعی جایگزین میکند و نتیجه را به مرورگر میفرستد.
مثال — این سطر در فایل قالب:
<h2>%slogan</h2>
اینگونه به مرورگر تحویل میشود:
<h2>شعار سایت اینجا نمایش داده میشود.</h2>
theme.json تعریف میکنید، و کلیدواژههای آماده موتور که از قبل تعریف شدهاند.index.html و category.html و post.html.سریعترین راه، ویرایش یکی از پوستههای موجود است:
mytheme) کپی کنید.pages/theme.json را باز کنید و theme_name را به نام جدید تغییر دهید.pages/*.html و style.css را مطابق سلیقه خود ویرایش کنید.themes بارگذاری کنید.فایل «پرامپت پوسته» که همراه موتور منتشر میشود، مشخصات کامل طراحی پوسته را در خود دارد. متن آن را در ابتدای یک گفتگوی تازه با هر سرویس هوش مصنوعی قرار دهید و در بخش پایانی آن، نوع پوسته و ویژگیهای دلخواه را بنویسید. فایلهای تحویلی را مطابق مسیرهای اعلامشده ذخیره و نصب کنید.
| روش | نحوه فعالسازی |
|---|---|
| بارگذاری فایل ZIP از بخش «پوسته» در مدیریت | نصب و فعالسازی، خودکار |
| دریافت از مخزن پوستهها | نصب و فعالسازی، خودکار |
| بارگذاری پوشه با FTP یا مدیریت پرونده | فعالسازی دستی از بخش «پوسته» |
با تغییر پوسته، تنظیمات هر پوسته در فایل theme.json همان پوسته ذخیره میشود؛ پس اگر بعداً به پوسته قبلی برگردید، تنظیمات آن سر جای خود خواهد بود.
هر پوسته یک پوشه در مسیر themes است:
themes/mytheme/
├── screenshot.jpg پیشنمایش پوسته، ۳۰۰×۴۰۰ پیکسل
├── style.css شیوهنامه اصلی
├── script.js جاوااسکریپت پوسته
├── img/
│ └── logo.jpg تصاویر و دیگر داراییها
└── pages/
├── .htaccess جلوگیری از دسترسی وب (حذف نکنید)
├── theme.json مشخصات و تنظیمات پوسته
├── index.html صفحه اصلی (الزامی)
├── category.html دستهها، برچسبها، نتایج جستجو (الزامی)
├── post.html صفحه اختصاصی مطلب (الزامی)
├── tag.html برچسبها (اختیاری)
├── search.html نتایج جستجو (اختیاری)
├── 404.html صفحه خطای یافت نشد (اختیاری)
└── *.html قالبهای دلخواه با نام آزاد
| فایل | نقش | الزامی؟ |
|---|---|---|
pages/index.html | صفحه اصلی سایت | بله |
pages/category.html | مطالب یک دسته | بله |
pages/post.html | صفحه اختصاصی هر مطلب | بله |
pages/tag.html | مطالب یک برچسب | خیر — در نبودش category.html |
pages/search.html | نتایج جستجو | خیر — در نبودش category.html |
pages/404.html | صفحه خطای ۴۰۴ | خیر — در نبودش صفحه پیشفرض موتور |
pages/*.html | قالب اختصاصی برای مطلب یا دسته خاص | خیر — با «قالب نمایش» مطلب انتخاب میشود |
screenshot.jpg | پیشنمایش در بخش پوسته مدیریت | توصیه میشود |
قواعد نامگذاری: نام پوسته و نام فایلهای قالب فقط میتواند شامل حروف انگلیسی، رقم، خط تیره (-) و زیرخط (_) باشد. قالبهایی که با این قاعده نامگذاری شوند، در ردیف «قالب نمایش» صفحه ویرایش مطلب قابل انتخاباند.
مسیر داراییها: فایلهای CSS و JS و تصاویر پوسته با نشانی کامل فراخوانی میشوند:
<link rel="stylesheet" href="/themes/mytheme/style.css"> <script src="/themes/mytheme/script.js"></script>
الزامات هر فایل قالب:
<body> در همه صفحات الزامی است. در حالت فروشگاهی، موتور دادههای فروشگاه را پیش از این تگ درج میکند.head، عنوان و توضیحات سئو را با کلیدواژههای آماده درج کنید:<title>%mtitle</title> <meta name="description" content="%mdescription"> <meta name="keywords" content="%mkeywords">
نکته: موتور هنگام پردازش، ابتدا متغیرهای
theme.jsonرا جایگزین میکند و سپس حلقههای مطالب را پردازش میکند. بنابراین میتوانید متغیر پوسته را درون مشخصههای حلقه هم به کار ببرید.
این فایل دو گروه کلید دارد:
theme_ شروع میشوند: مشخصات و رفتار پوسته را بیان میکنند، در html جایگزین نمیشوند و بیشترشان در مدیریت نمایش داده نمیشوند.%نامکلید جایگزین میشود.هر متغیر آرایهای دو عضوی است: [برچسب در مدیریت، مقدار پیشفرض]
"slogan": [
"شعار سایت",
"شعار سایت اینجا نمایش داده میشود."
]
%slogan با مقدار نهایی جایگزین میشود.نوع ردیف در مدیریت (و رفتار مقدار) با پسوند آخر نام کلید تعیین میشود:
| پسوند | ردیف در مدیریت | کاربرد نمونه |
|---|---|---|
| بدون پسوند | ورودی تکخطی | عنوان بخش، متن کوتاه |
_text | جعبه متن چندخطی | معرفی سایت (هر خط جدید به <br> تبدیل میشود) |
_html | ویرایشگر غنی (WYSIWYG) | بلوک معرفی با تصویر و قالببندی |
_code | جعبه متن کد خام | کد درج ویدئو یا نقشه (iframe) |
_img | انتخاب تصویر | نشان سایت |
_imgset | چند تصویر، جدا شده با | | تصاویر لغزانه |
_color | انتخاب رنگ از جدول طیف | رنگ مضمون |
_email | ورودی ایمیل | ایمیل دریافت پیام فرم تماس |
_number | ورودی عددی | تعداد مطالب یک بخش |
_url | ورودی نشانی وب | نشانی شبکه اجتماعی |
_menu | ویرایشگر فهرست | فهرست اصلی سایت (بخش ۴) |
_category | انتخاب یک دسته | دسته مطالب صفحه اصلی |
_posts | انتخاب چند مطلب | مطالب منتخب لغزانه |
نکته درباره
_imgset: مقدار این متغیر متن خامی از نشانی تصاویر است که با|از هم جدا شدهاند؛ موتور آن را عیناً جایگزین میکند. تبدیل به کد تصاویر، با جاوااسکریپت و متدsplit('|')به عهده پوسته است.
نکته درباره
_color: الگوی رایج، تزریق مقدار رنگ به متغیر CSS است:<style>:root{--primary-color:%primary_color}</style>
{
"theme_name": "mytheme",
"theme_ver": "1.0",
"theme_info": "پوسته نمونه وبسایت موتور.",
"primary_color": ["رنگ مضمون", "#439792"],
"logo_img": ["تصویر نشان سایت", "/themes/mytheme/img/logo.jpg"],
"main_menu": ["فهرست اصلی", "صفحه اصلی|/\nدسته نمونه|/دسته-نمونه-1/\n"],
"home_category": ["دسته مطالب صفحه اصلی", "4"],
"copyright_text": ["متن حقوق محفوظ", "تمامی حقوق این سایت محفوظ است."]
}
در این نمونه، سه کلید اول فنیاند و پنج کلید بعدی متغیر پوستهاند که هر کدام یک ردیف قابل تنظیم در مدیریت میسازند.
| کلید | کاربرد | شرح کامل |
|---|---|---|
theme_name | نام پوسته؛ در مدیریت بهعنوان مشخصات نمایش داده میشود | — |
theme_ver | نسخه پوسته | — |
theme_url | نشانی صفحه معرفی پوسته | — |
theme_info | توضیح پوسته (متن html مجاز است) | — |
theme_main_menu_classing | کلاسهای CSS منو | بخش ۴ |
theme_vars | تعریف فیلدهای اختصاصی مطلب | بخش ۷ |
theme_tags_img | فعالکردن تصویر شاخص برچسبها (yes) | بخش ۷ |
theme_categories_img | فعالکردن تصویر شاخص دستهها (yes) | بخش ۷ |
theme_similar_posts | فعالکردن انتخاب مطالب مشابه (yes) | بخش ۷ |
theme_php | پردازش فرمهای اختصاصی پوسته | بخش ۷ |
theme_dev | نسخهبندی خودکار style.css و script.js در زمان توسعه (yes) | بخش ۱۱ |
متغیری که نامش با _menu پایان مییابد، یک فهرست است. مقدار آن متن سادهای است که هر سطرش یک گزینه را تعریف میکند:
عنوان گزینه|نشانی
در theme.json سطرها با کاراکتر \n از هم جدا میشوند:
"main_menu": [
"فهرست اصلی",
"صفحه اصلی|/\nنمونه مطلب|/نمونه-مطلب-1/\nدسته نمونه|/دسته-نمونه-1/\nپیوند نمونه|https://example.com/\n"
]
فراخوانی در قالب:
<nav>%main_menu</nav>
خروجی:
<nav><ul class="navbar-nav">
<li class="nav-item active"><a class="nav-link" href="/">صفحه اصلی</a></li>
<li class="nav-item"><a class="nav-link" href="/نمونه-مطلب-1/">نمونه مطلب</a></li>
<li class="nav-item"><a class="nav-link" href="/دسته-نمونه-1/">دسته نمونه</a></li>
<li class="nav-item"><a class="nav-link" href="https://example.com/" target="_blank">پیوند نمونه</a></li>
</ul></nav>
خروجی شامل خودِ <ul> است؛ پس پوسته فقط باید محل قرارگیری آن (مثلاً درون <nav>) و ظاهرش با CSS را تعیین کند.
دسته-نمونه-1 معادل /دسته-نمونه-1/ است).://) بهطور خودکار target="_blank" میگیرد.# بهصورت پیوند درونصفحهای ساخته میشود.عنوان|)، پیوند بدون href ساخته میشود؛ برای گزینهای که فقط بازکننده زیرمنو است کاربرد دارد.گزینههای تودرتو فقط با تورفتگی فاصله (یا tab) در ابتدای سطر ساخته میشوند. اندازه تورفتگی نسبت به سطر اول سنجیده میشود؛ تعداد دقیق فاصلهها مهم نیست، نسبت سطرها به هم مهم است:
"main_menu": [
"فهرست اصلی",
"صفحه اصلی|/\nمحصولات|\n موبایل|/موبایل/\n لپتاپ|/لپ-تاپ/\nتماس با ما|/تماس-با-ما/\n"
]
گزینه «محصولات» بدون نشانی، والدِ زیرمنویی است که دو گزینه دارد.
گزینهای که نشانیاش با نشانی صفحه جاری برابر باشد، کلاس active میگیرد. در صفحاتی که پارامتر کوئری دارند (مثل صفحه نتایج جستجو)، هیچ گزینهای active نمیشود.
کلاسهای خروجی منو با کلید فنی theme_ناممنو_classing تنظیم میشود — شش مقدار، جداشده با کاما:
| ردیف | کلاسِ | پیشفرض |
|---|---|---|
| ۱ | <ul> اصلی | navbar-nav |
| ۲ | <li> گزینهها | nav-item |
| ۳ | <a> گزینهها | nav-link |
| ۴ | کلاس اضافه <li> والدِ زیرمنو | dropdown |
| ۵ | کلاس اضافه <a> والدِ زیرمنو | dropdown-toggle |
| ۶ | <ul> زیرمنو | dropdown-menu |
پیشفرض، همان مجموعه کلاسهای بوتاسترپ است؛ بنابراین اگر طراحی شما بوتاسترپی نیست، کلاسهای خودتان را تعریف کنید:
"theme_main_menu_classing": "menu,menu-item,menu-link,,menu-parent,submenu",
مقدار خالی یعنی آن عنصر کلاس اضافهای نمیگیرد.
دو نکته درباره پیوندِ والد زیرمنو:
<li class="menu-item menu-parent">.<a> والد زیرمنو، صفتهای role="button" و data-toggle="dropdown" اضافه میشود؛ باز و بسته شدن زیرمنو را با جاوااسکریپت پوسته مدیریت کنید.هر تعداد متغیر _menu میتوانید تعریف کنید (مثلاً منوی اصلی و منوی پانویس) و کلاسبندی هر کدام مستقل است: برای منویی به نام footer_menu، کلید theme_footer_menu_classing.
این کلیدواژهها از قبل تعریف شدهاند، نیازی به تعریف در theme.json ندارند و در همه صفحات مقدار میگیرند. درون حلقه ITEMS هم قابل استفادهاند.
| کلیدواژه | مقدار |
|---|---|
%route | نوع صفحه جاری: index | category | post | tag | search | 404 |
%sitename | نام سایت (از تنظیمات) |
%siteurl | نشانی اصلی سایت |
%sitelink | کد آمادهٔ <a href="%siteurl">%sitename</a> |
%mtitle | عنوان سئو |
%mdescription | توضیح سئو |
%mkeywords | کلیدواژههای سئو |
%post_id | شناسه محتوای جاری صفحه |
%post_url | نشانی یکتای محتوای جاری |
%post_title | عنوان محتوای جاری |
%post_excerpt | چکیده محتوای جاری |
%search_query | عبارت جستجو — فقط در صفحه جستجو مقدار دارد؛ بقیه صفحات خالی |
%captchano | شمار عملیات باقیمانده تا نمایش مجدد کپچا (بخش ۹) |
%URL(n) | اسلاگ مطلب با شناسه n، بدون اسلش — برای پیوند: href="/%URL(3)/" |
%TITLE(n) | عنوان مطلب با شناسه n |
%user_id | شناسه عددی عضو واردشده؛ برای مهمان 0 |
%user_name | نام عضو واردشده؛ برای مهمان خالی |
%user_role | نقش عضو: admin | editor | author؛ برای مهمان خالی |
%comment_display_class | فقط در post.html — بخش ۹ |
%site_store | yes اگر فروشگاه فعال است، وگرنه no |
%post_title و %post_excerpt را خودش بازنویسی میکند: «N یافته در جستجوی «عبارت»» و «صفحه x از y»؛ در 404.html متن خطا.%mtitle و %mdescription را از فیلدهای سئوی اختصاصی مطلب، و در نبود آنها از عنوان/چکیده مطلب یا تنظیمات سایت مقداردهی میکند. پوسته فقط آنها را در جای مناسب درج میکند.%URL(n) و %TITLE(n) برای شناسهای که وجود ندارد، خالی جایگزین میشوند.class="search_form_%route" و سپس .search_form_category{display:none}.ITEMS محدوده یک حلقه تکرار است. موتور بر اساس مشخصههای برچسب، مطالب را از پایگاه میخواند و در هر تکرار، کلیدواژههای درون بدنه را با دادههای همان مطلب جایگزین میکند:
<ITEMS i="4" limit="3">
<h2>%title</h2>
%img
</ITEMS>
الگوی رایج post.html: کل صفحه مطلب درون <ITEMS> ... </ITEMS> بدون مقدار i قرار میگیرد.
ترتیب پردازش: موتور ابتدا متغیرهای theme.json و کلیدواژههای سراسری را جایگزین میکند و سپس حلقهها را پردازش میکند. بنابراین استفاده از متغیر درون مشخصهها معتبر است: i="{%home_posts}".
| مقدار | نتیجه |
|---|---|
| خالی | post.html: مطلب جاری — category/tag/search: مطالب همان صفحه (با صفحهبندی) — index.html: آخرین مطالب |
"post" | مطلب جاری |
| عدد | مطالب ذیل آن شناسه (دسته یا برچسب): i="4" یا i="%home_category" |
{5,6,7} | مطالب با آن شناسهها — از متغیر پوسته: i="{%home_posts}" |
recent_posts | آخرین مطالب |
recent_top_visits | پربازدیدترین مطالب ۱۰ روز اخیر |
top_visits | پربازدیدترین مطالب کل |
recent_top_comments | پرنظرترین مطالب ۱۰ روز اخیر |
top_comments | پرنظرترین مطالب کل |
similar | مطالب مشابه انتخابی (نیازمند theme_similar_posts=yes) |
same_category | همدستههای مطلب جاری |
same_tags | همبرچسبهای مطلب جاری |
tags | برچسبهای مطلب جاری |
category | دستههای مطلب جاری |
limit="3" — سه مورد (پیشفرض ۱۰)limit="3,10" — پرش از ۳ مورد اول و نمایش ۱۰ مورد بعدی؛ برای حالت «مطلب ویژه» بههمراه «سایر اخبار»<ITEMS display="post"> ... </ITEMS>
اگر %route جاری برابر مقدار display بود، بدنه بلوک چاپ میشود و در غیر این صورت کل بلوک حذف میشود. کلیدواژههای حلقه (%title و مانند آن) درون بلوک display پردازش نمیشوند؛ متغیرهای پوسته و کلیدواژههای سراسری (که پیش از ITEMS جایگزین شدهاند) کار میکنند.
شناسه و نشانی
| کلیدواژه | مقدار |
|---|---|
%index | شماره تکرار، از ۱ |
%first_* | در تکرار اول، مقدار پس از first_؛ در بقیه خالی — مثال: <div class="carousel-item %first_active"> |
%all_items | تعداد کل نتایج حلقه |
%id | شناسه مطلب |
%url و %href | نشانی مطلب: /نمونه-مطلب-1/ |
%link | کد آماده <a href="..." title="عنوان">عنوان</a> |
%title | عنوان مطلب |
متن و تصویر
| کلیدواژه | مقدار |
|---|---|
%excerpt | چکیده (خط جدید به <br> تبدیل میشود) |
%content | متن کامل مطلب |
%src | نشانی تصویر شاخص، یا خالی اگر مطلب تصویر ندارد |
%img | کد آماده <img loading="lazy" src="..." alt="عنوان"> — بدون تصویر شاخص، خالی |
%thumb | کد <img> بندانگشتی: src="/...-thumb.jpg" |
بندانگشتی حداکثر ۱۰۰×۱۰۰ پیکسل است و کنار تصویر اصلی با پسوند -thumb.jpg ساخته میشود. توصیه: در فهرستهای عمومی %img و در ابزارکهای کمحجم %thumb بههمراه پسزمینه placeholder در CSS.
زمان — قالبها ثابتاند؛ قالب دلخواه با جاوااسکریپت و تبدیل %date (تابع pdate در پیوست الف):
| کلیدواژه | قالب |
|---|---|
%date | میلادی Y-m-d |
%time | H:i:s |
%pdate | شمسی Y/m/d |
%htime | زمان نسبی: «۲ ساعت پیش» |
دسته، برچسب و بازخورد
| کلیدواژه | مقدار |
|---|---|
%category_links | پیوندهای کلیکپذیر دستههای مطلب |
%category_names | عناوین دستهها، جداشده با tab (برای پردازش JS) |
%tag_links / %tag_names | مانند بالا برای برچسبها |
%comments_no | شمار دیدگاههای سطح اولِ منتشرشده |
%comments_all_no | شمار همه دیدگاههای منتشرشده (شامل پاسخها) |
%visit | بازدید یکتا (بدون اعضا و خزندهها) |
%votes | رشته آرا مثل 1,0,0,2,1,0 یا خالی (بخش ۹) |
دستهها و برچسبهایی که نشانی ندارند (ذخیرهشده با #) در خروجی نمیآیند.
نویسنده، فروشگاه و فیلدهای خام
| کلیدواژه | مقدار |
|---|---|
%author_name / %author_url / %author_link | نام، نشانی معرفی، کد <a> نویسنده |
%buy | کد دکمه خرید — فقط اگر فروشگاه فعال و قیمت بزرگتر از صفر باشد (بخش ۱۰) |
%price | قیمت با جداکننده هزارگان |
%has_price | yes / no |
%type %category %tags %template %commenting %created %modified | مقادیر خام جدول مطالب |
%نامفیلد | فیلدهای اختصاصی مطلب (theme_vars — بخش ۷) |
%paging() ← در category/tag/search با i خالی %paging(4) ← اگر حلقه i="4" دارد %link(4) ← پیوند کلیکپذیر صفحه دسته، همراه %paging(4)
خروجی فقط <li>هاست — شامل اعداد صفحات، «قبلی/بعدی»، سهنقطه برای فاصلهها و کلاس active روی صفحه جاری. خودتان با <ul> بپیچید:
<nav><ul class="pagination">%paging()</ul></nav>
در index.html با i خالی، %paging() خروجی ندارد. در صفحاتی که صفحهبندی معنا ندارد از درج %paging پرهیز کنید؛ چون جایگزین نمیشود و عیناً در خروجی میماند.
%search_finds (تعداد یافتهها) | %search_page (شماره صفحه جاری) | %search_pages (تعداد صفحات)
<div class="news_area">
<ITEMS i="%news_category" limit="3">
<div class="news_row">
<h2>%title</h2>
%img
<p>%excerpt</p>
</div>
</ITEMS>
</div>
اگر %news_category متغیری از نوع _category با مقدار ۴ باشد، سه مطلب آخر دسته ۴ با عنوان و تصویر و چکیده نمایش داده میشود.
این امکانات با کلیدهای فنی theme_* در theme.json فعال میشوند.
"theme_vars": "pretitle,پیشعنوان;pre_text,پیشمتن;"
; از هم جدا میشوند.pretitle ورودی تکخطی، pre_text چندخطی.%pretitle و %pre_text — در post.html که کل صفحه درون <ITEMS> بدون i است، سراسری به نظر میرسد."theme_categories_img": "yes", "theme_tags_img": "yes"
با هر مقدار yes، در فرم ایجاد/ویرایش دستهها یا برچسبها ردیف «تصویر شاخص» نمایش داده میشود. نمایش تصویر دسته جاری در category.html:
<ITEMS>%img</ITEMS>
(بدون i — یعنی «محتوای جاری صفحه» = خود دسته.)
"theme_similar_posts": "yes"
در فرم ویرایش مطلب، ردیفی برای انتخاب دستی مطالب مرتبط (مطلب، دسته یا برچسب) اضافه میشود. فراخوانی:
<ITEMS i="similar" limit="6"> ... </ITEMS>
اگر پوسته فرمی دارد که با فرمهای آماده موتور (تماس، دیدگاه) قابل انجام نیست — مثلاً ثبت ایمیل خبرنامه — مقدار theme_php یک کد PHP است که هنگام هر POST به نشانی ریشه سایت، پس از خواندن تنظیمات پوسته اجرا میشود:
"theme_php": "if(isset($_POST['newsletter']) and isset($_POST['email'])){file_put_contents(DATA_FOLDER.'/newsletter.log', date('Y-m-d H:i:s').'\\t'.$_POST['email'].'\\n', FILE_APPEND); die('OK.');}"
DATA_FOLDER به پوشه دادههای سایت اشاره میکند.die() کنید؛ متن پاسخ به جاوااسکریپت فرم برمیگردد.⚠ هشدار: نصب پوستهای که
theme_phpدارد، یعنی اجرای کد PHP دلخواه روی سایت شما. پوسته را فقط از منبع مورد اعتماد نصب کنید و به محتوایtheme.jsonآن توجه داشته باشید.
<form action="/" method="GET">
<input name="search" value="%search_query">
<button type="submit">جستجو</button>
</form>
search و ارسال با GET.class="search_form_%route".فیلدهای فرم با پیشوند contact به نشانی ریشه سایت POST میشوند، همراه با کد امنیتی. نمونه کامل HTML و جاوااسکریپت در پیوست پ:
<form method="POST" onsubmit="return sendform()">
<input name="contact[name]" placeholder="نام شما" required>
<input name="contact[email]" type="email" required>
<input name="contact[subject]" placeholder="موضوع">
<textarea name="contact[message]" required></textarea>
<input type="number" name="captcha" required>
<img src="/captcha/" alt="کد امنیتی" onclick="this.src='/captcha/?r='+Math.random()">
<button type="submit">ارسال</button>
</form>
_email در theme.json است (رایج: contact_email).contact مجاز است؛ فیلدهای شناختهشده (name، email، subject، message) در ایمیل با برچسب فارسی میآیند و بقیه با نام اصلی.OK. — خطا: ERROR|پیام?r=عدد تصادفی به نشانی).دیدگاه و بازخورد فقط برای مطالب (نه دستهها و برچسبها) و در post.html است. دریافت و نمایش دیدگاهها با جاوااسکریپت (AJAX) انجام میشود — حلقه ITEMS برای دیدگاهها وجود ندارد.
وضعیت دیدگاه هر مطلب (ردیف «وضعیت دیدگاه» در فرم ویرایش مطلب):
| مقدار | رفتار |
|---|---|
C | غیرفعال — فرم نمایش داده نمیشود |
O | عمومی، انتشار فوری |
M | فقط اعضا، انتشار فوری |
| خالی | عمومی با تأیید مدیر (دیدگاه اعضا فوری منتشر میشود) |
وقتی دیدگاه مجاز نیست، %comment_display_class مقدار no-display میدهد تا بخش بازخورد و دیدگاه با CSS پنهان شود. اگر فرم به هر حال ارسال شود، سرور خطای مناسب برمیگرداند.
/captcha/ با چهار رقم فارسی است.%captchano در بارگذاری صفحه مقدار اولیه را میدهد و با هر عملیات کم میشود.go_captcha در پیوست الف ساخته میشود.| عملیات | فیلدهای POST | پاسخ موفق |
|---|---|---|
| دریافت دیدگاهها | readcomments=<شناسه مطلب> و last_cid=<آخرین شناسه بارگیریشده> و parent=<0 یا شناسه دیدگاه والد> |
OK\n سپس هر دیدگاه در یک سطر با ۷ فیلد جداشده با tab: شناسه، نام، متن، تاریخ (میلادی Y-m-d H:i:s)، مثبت، منفی، تعداد پاسخها — حداکثر ۵۰ مورد در هر درخواست؛ ادامه فهرست با last_cid |
| رأی به دیدگاه | comment_id=<id> و vote=<1|-1> (+captcha) | OK\n<مثبت>,<منفی>,<captchano> |
| رأی به مطلب | post_id=<id> و vote=<0..5> (+captcha) | OK\n<v0>,...,<v5>,<captchano> |
| ثبت دیدگاه | post_id و comment-name و comment-text و parent (+captcha) | OK|دیدگاه شما منتشر شد. یا OK|دیدگاه شما دریافت شد و پس از بازبینی مدیر سایت، منتشر خواهد شد. |
هر خطا با پیشوند ERROR|پیام فارسی آماده برمیگردد.
قواعد سمت سرور (برای طراحی فرم): نام تکخطی و بدون کاراکترهای < > " ' / | % @ — پیشنهاد maxlength="30". متن بدون تگ و حداکثر ۲۰۴۸ کاراکتر — پیشنهاد maxlength="2048". عضو غیرمدیر با نام ثبتشده خودش دیدگاه میدهد؛ مهمان نمیتواند از نام اعضای سایت استفاده کند.
<div class="post-media-votes %comment_display_class">
<div class="post-media-title">بازخورد</div>
<div class="votes_area" votes="%votes" post_id="%post_id"></div>
</div>
<div class="comment-section %comment_display_class">
<div class="post-media-title comment-section-title">دیدگاهها</div>
<div id="comments-container" captchano="%captchano" post_id="%post_id" comments_no="%comments_no"></div>
<div class="comment-form">
<form onsubmit="send_comment();return false" method="POST">
<div class="comment-form-info"></div>
<input type="hidden" name="post_id" value="%post_id">
<input type="hidden" name="parent" value="0">
<input class="form-control comment-form-name" name="comment-name" placeholder="نام شما" value="%user_name" required>
<textarea class="form-control" name="comment-text" placeholder="دیدگاه شما" required></textarea>
<button type="submit">ارسال دیدگاه</button>
</form>
</div>
</div>
#comments-container و .comment-section-title الزامیاند؛ اسکریپت مرجع به آنها وابسته است.votes، post_id، captchano، comments_no.value="%user_name" نام عضو واردشده را در فرم میگذارد؛ اسکریپت مرجع نام مهمان را در localStorage (کلید comment-form-name) نگه میدارد و در بازدید بعدی خودکار پر میکند.کد پیوست الف را عیناً در script.js قرار دهید و فقط HTML و CSS تولید کنید. اسکریپت، دکمههای رأی و بدنه دیدگاهها را میسازد؛ CSS پوسته باید این کلاسها را پوشش دهد:
| گروه | کلاسها |
|---|---|
| دکمههای رأی | .vote_btns و .vote-plus .vote-minus .vote-haha .vote-wow .vote-sad .vote-angry — حالت انتخابشده: .active |
| برچسب رأی مطلب | موافق، مخالف، خندهدار، تعجبآور، غمگین، ظالمانه — در اسکریپت مرجع قابل تغییر است |
| دیدگاه | .comment .comment-inf .comment-name .comment-date .comment-votes .comment-plus .comment-minus .comment-text .comment-buttons .comment-load-replies (شمار پاسخها) .comment-reply-toggle (بازکردن فرم پاسخ) .readcomments-more («N دیدگاه دیگر») .comments-loading |
| فرم و خطاها | .comment-form .comment-form-info (با حالتهای success / error / wait) #form_reply #captcha_modal |
تاریخ شمسی دیدگاه با pdate(p[3])+p[3].substring(10) ساخته میشود؛ تابع pdate سراسری است و در پیوست الف تعریف شده.
وقتی قابلیت فروشگاه در تنظیمات سایت فعال شود، موتور پیش از تگ <body> این اسکریپت را به خروجی اضافه میکند:
<script id="wotor_store_data">var STORE={sitename:"نام سایت", store_desc:"توضیحات سبد خرید",
store_cond:"توضیحات فرم سفارش", STORE_CURRENCY:"تومان",
catalog:{2:["نمونه-مطلب-2","نمونه مطلب 2",20000,"عدد"]},
payment_methods:{0:["پیپینگ","پرداخت به وسیله کلیه کارتهای عضو شتاب..."]},
receipts:[1263357]};</script>
catalog: شناسه مطلب ← [اسلاگ، عنوان، قیمت، واحد]. واحد خالی یعنی کالای شمارشناپذیر و تعدادش در سبد روی ۱ قفل میشود.STORE_CURRENCY واحد پول است؛ آن را در کد هاردکد نکنید.store_desc و store_cond از تنظیمات فروشگاه در مدیریت میآیند.receipts شماره رسیدهای نشست جاری است؛ برای نمایش «رسیدهای اخیر».پوسته این تگ را نمیسازد و بازنویسی نمیکند. کد پیوست ب با گارد if(typeof(STORE)=='object') نوشته شده و در سایت غیرفروشگاهی بیخطر است؛ میتوانید عیناً استفاده کنید یا با همان قراردادها بازنویسی کنید.
%buy در حلقه ITEMS کد دکمه را میدهد (فقط اگر فروشگاه فعال و قیمت بزرگتر از صفر باشد):
<div class="site-store site-store-post" post_id="12" price="250000">
<span class="site-store-price">250,000</span>
<button class="site-store-btn" onclick="buy(12)"></button>
</div>
دکمه بدون متن است؛ برچسب یا آیکون با CSS ساخته میشود: .site-store-btn:before{content:'افزودن به سبد'}. کلاس دوم site-store-{route} برای تفاوت نمایش دکمه در فهرستها و صفحه مطلب است.
localStorage با کلید cart_str و قالب pid,تعداد;pid,تعداد; نگهداری میشود و تا پایان نشست باقی میماند./checkout/ با فیلدهای cart و customer[name] و customer[email] و customer[phone] و customer[comment] و pay_method (شماره روش پرداخت از STORE.payment_methods)./receipt/شماره/) هم از سمت موتور رندر میشود و بخشی از پوسته نیست.اگر از کد مرجع پیوست ب استفاده میکنید، این کلاسها را استایل دهید: #store_cart_button #store_cart_modal .store_cart_modal_inner .store_cart_modal_header .close_cart_btn .store_cart_modal_table .store_cart_modal_row .store_cart_modal_footer .store_cart_modal_buttons .store_cart_modal_over .store_cart_modal_cond .store-customer-form .store_cart_modal_payment_methods .store_cart_modal_payment_method_desc .catalog_modal_form #filterinp .catalog_data_table_head #catalog_data_table .catalog_table_row #recent_receipts_button .close_receipts_btn — مودال کپچا (#captcha_modal) مشترک با بخش ۹ است.
در زمان طراحی، این کلید را در theme.json بگذارید:
"theme_dev": "yes"
موتور به انتهای نشانی style.css و script.js علامت نسخه زمانی اضافه میکند (?v=...) تا تغییرات بدون درگیری با حافظه نهان مرورگر دیده شود. در نسخه نهایی پوسته، این کلید را حذف کنید.
| علامت | علت محتمل | راهحل |
|---|---|---|
| صفحه سفید یا خطای PHP | خطای اجرایی | وارد مدیریت شوید (نمایش خطا آنجا فعال است) یا لاگ سرور را ببینید |
| کلیدواژهای جایگزین نمیشود | غلط املایی؛ یا متغیر در theme.json پوسته فعال تعریف نشده؛ یا کلیدواژه حلقه خارج از ITEMS به کار رفته | نام کلیدواژه و محل استفاده را چک کنید |
| تغییرات CSS اعمال نمیشود | حافظه نهان مرورگر | theme_dev یا بارگذاری اجباری (Ctrl+F5) |
| «پرونده پوسته یافت نشد» | قالب تعیینشده در «قالب نمایش» مطلب، در پوشه pages موجود نیست | فایل را اضافه کنید یا قالب مطلب را اصلاح کنید |
| صفحه 404 برای مطلب تازه | نشانی یکتا یا قالب انتخابی ناموجود | نشانی/قالب را بررسی کنید |
| دیدگاهها بارگیری نمیشوند | حذف عناصر الزامی (#comments-container، .comment-section-title) یا خطای جاوااسکریپت | کنسول مرورگر را ببینید |
| theme.json نادیده گرفته میشود | JSON نامعتبر — مثلاً کامای اضافی | با یک اعتبارسنج JSON بررسی کنید |
عیبیابی AJAX: همه پاسخهای موتور (OK... و ERROR|...) را در تب Network ابزار توسعهدهنده مرورگر میبینید.
برای پوسته سبک و مستقل، فونت را درون CSS جاسازی کنید:
@font-face{font-family:WebYekan;src:url(data:application/x-font-woff;charset=utf-8;base64,...) format('woff')}
اگر پوسته را با هوش مصنوعی میسازید، مدل نمیتواند base64 واقعی فونت تولید کند؛ از آن بخواهید فقط اسکلت @font-face را با علامتگذاری /* FONT_BASE64 */ بسازد و سپس خودتان رشته base64 فونت را جایگزین کنید. در غیر این صورت فونت سیستم (مثل Tahoma) کافی است.
- و _screenshot.jpg با ابعاد ۳۰۰×۴۰۰pages/.htaccess با محتوای Require all deniedtheme.json معتبر؛ theme_name برابر نام پوشه؛ theme_info تکمیلشدهindex.html و category.html و post.html؛ و در صورت لزوم tag.html و search.html و 404.html<body> در همه صفحات%mtitle و %mdescription و %mkeywords) در head همه صفحات.no-display و .pagination و کلاسهای دیدگاه/آرا (اگر استفاده شده) و .site-store-* (اگر فروشگاه در نظر گرفته شده)%img و %votes نباید ظاهر را بشکند)theme_dev از theme.jsonاین کد، پیادهسازی مرجع بخش ۹ است. آن را عیناً در فایل script.js پوسته قرار دهید؛ کار شما فقط ساخت HTML و استایلدادن کلاسهای معرفیشده است.
دو نکته مهم:
- تابع
pdate()در ابتدای این کد و بهصورت سراسری تعریف شده است؛ پیوست ب (فروشگاه) نیز به همین تابع وابسته است. بنابراین هر دو کد باید در یک فایل (script.js) باشند و این تابع پیش از هر دو قرار گیرد.- در نمایش تاریخ دیدگاهها از قالب شمسی استفاده میشود:
pdate(p[3])+p[3].substring(10)— بخش تاریخ شمسی و بخش ساعتِ رشته میلادی سرور، کنار هم.
// -------- { Shared helper — تابع سراسری تاریخ شمسی --------
function pdate(datestr){
var y=0;
var m=0;
var d=0;
if(!datestr){
var today = new Date();
y=today.getFullYear();
m=today.getMonth()+1;
d=today.getDate();
}else{
var p=datestr.replace(/-/g,' ').replace(/\//g,' ').split(' ');
y=parseInt(p[0],10);
m=parseInt(p[1],10);
d=parseInt(p[2],10);
}
var g_days_in_month=[31,28,31,30,31,30,31,31,30,31,30,31];
var m_days_in_month=[31,31,31,31,31,31,30,30,30,30,30,29];
var gy = y-1600;
var g_day_no = 365*gy+Math.floor((gy+3)/4)-Math.floor((gy+99)/100)+Math.floor((gy+399)/400);
for(var j=0; j<m-1;j++){
g_day_no += g_days_in_month[j];
}
if(m>2 && ((gy%4==0 && gy%100!=0) || (gy%400==0))){
g_day_no++;
}
var m_day_no = g_day_no + d - 80;
var j_np = Math.floor(m_day_no/12053);
m_day_no = m_day_no % 12053;
var py = 979 + 33*j_np + 4*Math.floor(m_day_no/1461);
m_day_no %= 1461;
if(m_day_no >= 366){
py += Math.floor((m_day_no-1)/365);
m_day_no = (m_day_no-1)%365;
}
for(var j=0; j<11 && m_day_no >= m_days_in_month[j];j++){
m_day_no -= m_days_in_month[j];
}
var pm=j+1;
var pd=m_day_no+1;
return py+'/'+(pm<10?'0':'')+pm+'/'+(pd<10?'0':'')+pd;
}
// -------- Shared helper } --------
// -------- { Comment and Vote Script --------
var captchano = 0;
if(document.querySelector('#comments-container') || document.querySelector('.votes_area')){
var e=document.getElementById('comments-container');
if(e){
readcomments(0);
captchano=parseInt(e.getAttribute('captchano'));
}
if(document.querySelector('.comment-form-name') && document.querySelector('.comment-form-name').value.length==0 && localStorage.getItem('comment-form-name')){
document.querySelector('.comment-form-name').value=localStorage.getItem('comment-form-name');
}
var vas=document.querySelectorAll('.votes_area');
if(vas){
for(var i=0;i<vas.length;i++){
var v6=[0,0,0,0,0,0];
var s=vas[i].getAttribute('votes');
if(s){
var a=s.split(',');
for(var j=0;j<v6.length;j++){
if(a[j] && a[j].match(/\d+/)){
v6[j]=parseInt(a[j]);
}
}
}
s='<div class="vote_btns" id="post-'+vas[i].getAttribute('post_id')+'-votes" >';
for(var j=0;j<v6.length;j++){
s+='<i onclick="vote()" class="vote-'+['plus','minus','haha','wow','sad','angry'][j]+'" vote="'+j+'" title="'+['موافق','مخالف','خندهدار','تعجبآور','غمگین','ظالمانه'][j]+'">'+v6[j]+'</i>';
}
s+='</div>';
vas[i].innerHTML=s;
}
function vote(captcha){
var e=captcha?document.querySelector('.triggered'):event.target;
if(e.className.indexOf('active')!=-1){return;}
var et=document.querySelector('.triggered');
if(et){et.classList.remove('triggered');}
e.classList.add('triggered');
if(!captcha && captchano<=0){return go_captcha('vote');}
e.style.pointerEvents='none';
e.style.opacity=0.6;
var xhr = new XMLHttpRequest();
xhr.onload = function(){
e.style.pointerEvents='auto';
e.style.opacity=1;
if(xhr.responseText.indexOf('ERROR')===0){
go_captcha('vote');
}else if(xhr.responseText.indexOf('OK')===0){
var ac=e.parentNode.querySelector('.active');
if(ac){ac.classList.remove('active');}
e.classList.add('active');
var p=xhr.responseText.substring(3).split(',');
if(e.parentNode.id.indexOf('post')==0){
captchano=parseInt(p[6]);
var a=e.parentNode.querySelectorAll('i');
for(var j=0;j<a.length;j++){a[j].innerHTML=p[j];}
}else{
e.parentNode.querySelector('.comment-plus').innerHTML=(parseInt(p[0])?'+':' ')+p[0];
e.parentNode.querySelector('.comment-minus').innerHTML=(parseInt(p[1])?'-':' ')+p[1];
captchano=parseInt(p[2]);
}
}
}
xhr.open('POST', '/');
xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
xhr.send(e.parentNode.id.replace('-votes','').replace('-','_id=')+'&vote='+e.getAttribute('vote')+(captcha?('&captcha='+captcha):''));
}
}
function readcomments(n){
var e=document.getElementById(n?('comment-'+n):'comments-container');
var last_cid=0;
if(!n){
var cs=document.querySelectorAll('#comments-container .comment');
if(cs.length){last_cid=cs[cs.length-1].id.replace('comment-','');}
var b=document.querySelector('.readcomments-more button');
if(b){
b.classList.add('comments-loading');
}else{
document.querySelector('.comment-section-title').classList.add('comments-loading');
}
}else{
var b=e.querySelector('.comment-load-replies');
if(b){b.classList.add('comments-loading');}
}
var xhr = new XMLHttpRequest();
xhr.onload = function(){
document.querySelector('.comment-section-title').classList.remove('comments-loading');
if(xhr.responseText.indexOf('OK')===0){
var h='';
var a=xhr.responseText.substring(3).split("\n");
for(var i=0;i<a.length;i++){
var p=a[i].split("\t");
if(p.length==7){
//0id|1name|2text|3date|4plus|5minus|6children
h+='<div class="comment" id="comment-'+p[0]+'">'+
'<div class="comment-inf">'+
'<div class="comment-name">'+p[1]+'</div>'+
'<div class="comment-date">'+pdate(p[3])+p[3].substring(10)+'</div>'+
'<div class="comment-votes" id="comment-'+p[0]+'-votes">'+
'<i onclick="vote()" class="comment-plus" vote="1">'+(parseInt(p[4])?'+':' ')+p[4]+'</i>'+
'<i onclick="vote()" class="comment-minus" vote="-1">'+(parseInt(p[5])?'-':' ')+p[5]+'</i></div>'+
'</div>'+
'<div class="comment-text">'+p[2]+'</div>'+
'<div class="comment-buttons">'+(parseInt(p[6])?('<i onclick="readcomments('+p[0]+')" class="comment-load-replies" >'+p[6] +' پاسخ </i>'):'')+
'<i onclick="reply('+p[0]+')" class="comment-reply-toggle"> پاسخ</i>'+
'</div>'+
'</div>';
}
}
e.insertAdjacentHTML('beforeend',h);
if(n){
e.querySelector('.comment-load-replies').style.display='none';
}else{
var b=document.querySelector('.readcomments-more');
if(b){b.parentNode.removeChild(b);}
var m=parseInt(document.getElementById('comments-container').getAttribute('comments_no'))-document.querySelectorAll('#comments-container > .comment').length;
if(m>0){
e.insertAdjacentHTML('beforeend','<div class="readcomments-more"><button onclick="readcomments(0)">'+m+' دیدگاه دیگر</button></div>');
}
}
if(window.location.hash.indexOf('#comment-')==0){
setTimeout(function(){var c=document.querySelector(window.location.hash);if(c){c.scrollIntoView();}},500);
}
}
}
xhr.open('POST', '/');
xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
xhr.send('readcomments='+document.getElementById('comments-container').getAttribute('post_id')+'&last_cid='+last_cid+'&parent='+n);
}
function reply(n){
var e=document.getElementById('form_reply');
if(e){
e.parentNode.removeChild(e);
if(e.getAttribute('reply_to')==n){
return;
}
}
var h='<div class="comment-form" id="form_reply" reply_to="'+n+'" >'+document.querySelector('.comment-form').innerHTML+'</div>';
var name=document.querySelector('.comment-form input[name=comment-name]').value;
document.getElementById('comment-'+n).insertAdjacentHTML('beforeend',h);
document.querySelector('#form_reply input[name=comment-name]').value=name;
document.querySelector('#form_reply input[name=parent]').value=n;
document.querySelector('#form_reply textarea').focus();
}
function go_captcha(cb){
var h='<div id="captcha_modal" onclick="if(this==event.target){document.body.removeChild(this)}" style="position:fixed;z-index:9999;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)"><div style="direction:rtl;position:relative;background-color:#fff;margin:0 auto auto auto;padding:0;border:1px solid #fff;width:315px;max-width:90%;box-shadow:1px 1px 5px #000;transition:0.5s all"><span style="padding:5px;color:#f00;cursor:pointer;font-size:32px;line-height:1" onclick="document.body.removeChild(document.getElementById(\'captcha_modal\'))">×</span><div style="padding:5px 15px"><p>لطفاً کد چهار رقمی زیر را وارد کنید.</p><form onsubmit="'+cb+'(this.querySelector(\'input\').value);document.body.removeChild(document.getElementById(\'captcha_modal\'));return false"><img src="/captcha/?'+Math.random()+'" style="max-width:85%;margin:15px auto;display:block;cursor:pointer" onclick="this.src=\'/captcha/?\'+Math.random();this.parentNode.querySelector(\'input\').value=\'\'" /><input name="cptinp" class="form-control" type="number" style="display:block;margin:15px auto;text-align:center;width:175px" required /><button type="submit" style="background:seagreen;color:#fff;border:none;cursor:pointer;width:100px;padding:5px;border-radius:2px;margin:15px 0;font-family:inherit"> تأیید</button></form></div></div></div>';
document.body.insertAdjacentHTML('beforeend',h);
var mdl=document.querySelector('#captcha_modal > div');
mdl.style.marginTop=(mdl.parentNode.clientHeight-mdl.clientHeight)/3.5+'px';
mdl.querySelector('input').focus();
}
function send_comment(captcha){
var f=captcha?document.querySelector('.triggered'):event.target;
var p=f.querySelector('.comment-form-info');
p.innerHTML='';
p.className='comment-form-info';
var et=document.querySelector('.triggered');
if(et){et.classList.remove('triggered');}
f.classList.add('triggered');
if(!captcha && captchano<=0){return go_captcha('send_comment');}
var formdata = new FormData(f);
if(captcha){formdata.append('captcha',captcha);}
p.innerHTML='<i class="comments-loading"></i> در حال ارسال...';
p.classList.add('wait');
f.style.pointerEvents='none';
var xhr = new XMLHttpRequest();
xhr.onload = function(){
f.style.pointerEvents='auto';
captchano=0;
if(xhr.response.indexOf('OK')===0){
p.innerHTML=xhr.response.substr(3);
p.className='comment-form-info success';
f.querySelector('Textarea').value='';
localStorage.setItem('comment-form-name',f.querySelector('.comment-form-name').value);
readcomments(parseInt(document.querySelector('input[name=parent]').value));
var r=document.getElementById('form_reply');
if(r){r.outerHTML='<p class="success">'+xhr.response.substr(3)+'</p>';}
}else if(xhr.responseText.indexOf('ERROR|')===0){
p.innerHTML=xhr.response.substr(6);
p.className='comment-form-info error';
}
};
xhr.onerror = function(){
f.style.pointerEvents='auto';
p.innerHTML='دیدگاه ارسال نشد.';
p.className='comment-form-info error';
}
xhr.open('POST', '/');
xhr.send(formdata);
}
}
// -------- Comment and Vote Script } --------
این کد، پیادهسازی مرجع بخش ۱۰ است: کلید سبد خرید، مودال سبد، کاتالوگ کالا، فرم سفارش با روشهای پرداخت و فهرست رسیدهای اخیر. با گارد if(typeof(STORE)=='object') در سایت غیرفروشگاهی کاملاً بیاثر است.
دو نکته نسبت به نسخههای پیشین:
- واحد پول از
STORE.STORE_CURRENCYخوانده میشود و هاردکد نیست.- تابع
pdate()از این کد به ابتدای پیوست الف منتقل شده (سراسری) و برای «تاریخ» در سرصفحه سبد خرید از همان استفاده میشود.
// -------- { Store Script --------
if(typeof(STORE)=='object'){
if(localStorage.getItem('cart_str')){buy(-1);}
if(typeof(STORE.receipts)=='object' && STORE.receipts.length>0 && localStorage.getItem('dismiss_recent_receipts')!='yes'){
var h='<div id="recent_receipts_button"><span class="close_receipts_btn" onclick="localStorage.setItem(\'dismiss_recent_receipts\',\'yes\');document.body.removeChild(this.parentNode)">×</span><div class="label">رسیدهای اخیر</div>';
for(var i=(STORE.receipts.length-1);i>=0;i--){
h+='<a target="_blank" href="/receipt/'+STORE.receipts[i]+'/">سفارش '+
(STORE.receipts[i]+'').substr(0,(STORE.receipts[i]+'').length-6) +'</a>';
}
h+='</div>';
document.body.insertAdjacentHTML('beforeend',h);
}
function buy(pid){
var e=document.getElementById('store_cart_button');
if(!e){
document.body.insertAdjacentHTML('beforeend',
'<div id="store_cart_button" onclick="buy(0)" >سبد خرید </div>'+
'<div id="store_cart_modal" style="display:none" onclick="if(this==event.target && !this.querySelector(\'.store_cart_modal_inner_over\')){document.getElementById(\'store_cart_button\').style.display=\'block\';document.getElementById(\'store_cart_modal\').style.display=\'none\';}">'+
'<div class="store_cart_modal_inner">'+
'<div class="store_cart_modal_header"><h1>'+STORE.sitename+'</h1><div class="inf"> تاریخ: '+pdate()+'</div><p>'+STORE.store_desc.replace(/\n/g,'<br>')+'</p></div><span class="close_cart_btn" onclick="if(document.querySelector(\'.store_cart_modal_inner.store_cart_modal_inner_over\')){document.querySelector(\'.store_cart_modal_inner\').classList.remove(\'store_cart_modal_inner_over\');justify_store_cart_modal();}else{document.getElementById(\'store_cart_button\').style.display=\'block\';document.getElementById(\'store_cart_modal\').style.display=\'none\';}">×</span>'+
'<table class="store_cart_modal_table"></table><div class="store_cart_modal_footer"> قابل پرداخت: <span>0</span> '+STORE.STORE_CURRENCY+'</div><div class="store_cart_modal_buttons"><button onclick="catalog_modal()">افزودن</button> <button onClick="recaclc();var w = window.open("", "MsgWindow");w.document.open();w.document.write(\'<!DOCTYPE html><html lang="fa"><head>\'+document.head.innerHTML+\'</head><body><style>@media print{@page{size:landscape}body{background:#fff}}</style><div class="store_cart_print">\'+document.querySelector(\'.store_cart_modal_inner\').innerHTML.replace(\'close_cart_btn\',\'" style="display:none\')+\'</div><script>document.addEventListener("DOMContentLoaded",function(){print();}); </script></body></html>\');w.document.close();">پیشفاکتور</button> <button onclick="pay_method_modal()" >تکمیل سفارش</button></div>'+
'<div class="store_cart_modal_over"></div>'+
'</div></div>');
}
if(pid!=-1){
document.getElementById('store_cart_button').style.display='none';
document.getElementById('store_cart_modal').style.display='block';
var cart_str=(localStorage.getItem('cart_str'))?(localStorage.getItem('cart_str')):'';
var a=cart_str.split(';');
var pids=[];
var ords=[];
for(i=0;i<a.length;i++){
var p=a[i].split(',');
if(p.length==2){
var pi=parseInt(p[0]);
var pn=parseInt(p[1]);
if(pid==pi){pid=0;pn++;}
if(pids.indexOf(pi)==-1){
pids.push(pi);
ords.push(pn);
}else{
ords[pids.indexOf(pi)]++;
}
}
}
if(pid>0){
pids.push(pid);
ords.push(1);
}
var h='<tr><th>ردیف</th><th>شناسه</th><th>شرح</th><th>تعداد</th><th>واحد</th><th>فی</th><th>جمع</th></tr>';
var rows=1;
for(var j=0;j<pids.length;j++){
if(STORE.catalog[pids[j]]){
h+='<tr class="store_cart_modal_row fadeIn" pid="'+pids[j]+'"><td><span>'+(rows++)+
'</span><i title="حذف ردیف" class="store_cart_modal_row_del_btn" onclick="this.parentNode.parentNode.parentNode.removeChild(this.parentNode.parentNode);recaclc();">×</i>'+
'</td><td>'+pids[j]+'</td><td><a href="/'+STORE.catalog[pids[j]][0]+'/" target="_blank">'+STORE.catalog[pids[j]][1]+'</a></td>'+
'<td><input name="num" type="number" min="1" onchange="recaclc()" class="form-control" value="'+ords[j]+'"></td><td>'+STORE.catalog[pids[j]][3]+'</td>'+
'<td>'+ (STORE.catalog[pids[j]][2]+'').replace(/\d(?=(\d{3})+$)/g, '$&,')+'</td>'+
'<td class="st"></td></tr>';
}
}
document.querySelector('.store_cart_modal_table').innerHTML='<tbody>'+h+'</tbody>';
recaclc();
}
document.querySelector('.store_cart_modal_inner').classList.remove('store_cart_modal_inner_over');
justify_store_cart_modal();
}
function recaclc(){
var cart_str='';
var a=document.querySelectorAll('.store_cart_modal_table .store_cart_modal_row');
var gt=0;
for(i=0;i<a.length;i++){
var no=parseInt(a[i].querySelector('input').value);
var pid=parseInt(a[i].getAttribute('pid'));
if(no<1 || STORE.catalog[pid][3].length==0){no=1;}
a[i].querySelector('input').value=no;
cart_str+=pid+','+no+';';
var st=STORE.catalog[pid][2]*no;
gt+=st;
a[i].querySelector('td span').innerHTML=(i+1);
a[i].querySelector('.st').innerHTML=(st+'').replace(/\d(?=(\d{3})+$)/g, '$&,');
}
document.querySelector('.store_cart_modal_footer span').innerHTML=(gt+'').replace(/\d(?=(\d{3})+$)/g, '$&,');
localStorage.setItem('cart_str',cart_str);
if(localStorage.getItem('dismiss_recent_receipts')){
localStorage.removeItem('dismiss_recent_receipts');
}
}
function catalog_modal(){
document.querySelector('.store_cart_modal_inner').classList.add('store_cart_modal_inner_over');
var h='<div class="catalog_modal_form"><input class="form-control" type="text" id="filterinp" onkeyup="refilter()" placeholder="جستجوی کالا به نام یا شناسه"><span onclick="document.getElementById(\'filterinp\').value=\'\';refilter()">×</span></div><div class="catalog_data_table_head" style="padding:2px;background:#eee"><span>شناسه</span><span>شرح</span><span>واحد</span><span>فی</span></div><div id="catalog_data_table" style="overflow:auto;max-height:75vh;margin-bottom:15px;">';
var ks=Object.keys(STORE.catalog);
if(ks.length==0){
h+='<p style="width:90%;margin:auto;">موردی یافت نشد.</p>';
}
for(var i=0;i<ks.length;i++){
h+='<div class="catalog_table_row" onclick="buy('+ks[i]+')"><span>'+ks[i]+'</span><span>'+STORE.catalog[ks[i]][1]+'</span><span>'+STORE.catalog[ks[i]][3]+'</span><span fee="'+STORE.catalog[ks[i]][2]+'">'+(STORE.catalog[ks[i]][2]+'').replace(/\d(?=(\d{3})+$)/g, '$&,') +'</span></div>';
}
h+='</div>';
document.querySelector('.store_cart_modal_over').innerHTML=h;
justify_store_cart_modal();
}
function refilter() {
var q=document.getElementById('filterinp').value.toUpperCase();
document.getElementById('filterinp').parentNode.querySelector('span').style.display=(q.length==0)?'none':'block';
var a=document.querySelectorAll('.catalog_table_row');
for(var i=0;i<a.length;i++){
var p=a[i].querySelectorAll('span');
if(p[0].innerText.toUpperCase().indexOf(q)==-1 && p[1].innerText.toUpperCase().indexOf(q)==-1 ){
a[i].style.display='none';
}else{
a[i].style.display='block';
}
}
}
function justify_store_cart_modal(){
var m=document.querySelector('.store_cart_modal_inner');
m.style.marginTop=(m.parentNode.clientHeight-m.clientHeight)/3.5+'px';
}
function pay_method_modal(){
var cart_str=localStorage.getItem('cart_str');
if(cart_str.length<3){
return;
}
document.querySelector('.store_cart_modal_inner').classList.add('store_cart_modal_inner_over');
var h='';
h+='<div class="store_cart_modal_cond">'+STORE.store_cond.replace(/\n/g,'<br>')+'</div>';
if(Object.keys(STORE.payment_methods).length==0){
h+='<p>روشی برای پرداخت تعیین نشده است.</p>';
}else{
h+='<form class="store_cart_modal_form" method="POST" action="/checkout/">'+
'<div class="store-customer-form">'+
'<input type="hidden" name="cart" value="'+cart_str+'">'+
'<input class="form-control" type="text" name="customer[name]" placeholder="نام و نام خانوادگی" value="" required>'+
'<input class="form-control" dir="ltr" style="font-family:sans-serif" type="email" name="customer[email]" placeholder="ایمیل" value="" required>'+
'<input class="form-control" dir="ltr" type="tel" name="customer[phone]" placeholder="تلفن تماس" value="" required>'+
'<textarea class="form-control" type="text" name="customer[comment]" placeholder="توضیحات"></textarea>'+
'</div>'+
'<div class="store_cart_modal_form_row2"><div class="label">روش پرداخت:</div>'+
'<div class="store_cart_modal_payment_methods">';
for(var i=0;i<Object.keys(STORE.payment_methods).length;i++){
h+='<div class="label"><input type="radio" name="pay_method" value="'+i+'" onchange="document.querySelector(\'.store_cart_modal_payment_method_desc\').innerHTML=STORE.payment_methods['+i+'][1];" '+((i==0)?'checked':'')+'> '+STORE.payment_methods[i][0]+'</div>';
}
h+='<div class="store_cart_modal_payment_method_desc">'+STORE.payment_methods[0][1]+'</div>';
h+='</div>';
h+='</div><div class="store_cart_modal_pay"><button type="submit" onclick="localStorage.removeItem(\'cart_str\')" >پرداخت</button></div></form>';
}
document.querySelector('.store_cart_modal_over').innerHTML=h;
justify_store_cart_modal();
}
}
// -------- Store Script } --------
پیادهسازی مرجع بخش ۸. کلاسهای چیدمان (row، col-lg-6 و…) از نمونه اصلی نگه داشته شدهاند؛ در پوسته خود میتوانید ساختار چیدمان را آزادانه عوض کنید — مهم، نام فیلدها و اسکریپت ارسال است.
اصلاح نسبت به نمونه اولیه:
type="name"وtype="subject"بهtype="text"تغییر کرد (این دو، نوع معتبر HTML نیستند) وreturn false;در انتهای تابع اضافه شد.
<form id="contact-form" method="post" onsubmit="return sendform()">
<div class="row">
<div class="col-lg-6">
<fieldset>
<input type="text" name="contact[name]" id="name" placeholder="نام شما..." autocomplete="name" required>
</fieldset>
</div>
<div class="col-lg-6">
<fieldset>
<input type="text" name="contact[email]" id="email" pattern="[^ @]*@[^ @]*" placeholder="E-mail..." required="" style="font-family:sans-serif;direction:ltr" autocomplete="email">
</fieldset>
</div>
<div class="col-lg-6">
<fieldset>
<input type="text" name="contact[subject]" id="subject" placeholder="موضوع..." autocomplete="on">
</fieldset>
</div>
<div class="col-lg-6 captcha">
<input type="number" name="captcha" class="form-control" placeholder="کد امنیتی" autocomplete="off" required>
<img src="/captcha/" alt="کد امنیتی" onclick="this.src='/captcha/?r='+Math.random()" style="filter:none">
</div>
<div class="col-lg-12">
<fieldset>
<textarea name="contact[message]" id="message" placeholder="پیام شما..." required></textarea>
</fieldset>
</div>
<div class="col-lg-12">
<div class="info" style="color:#fff;margin:5px 0;padding:5px"></div>
<fieldset>
<button type="submit" id="form-submit" class="orange-button">ارسال</button>
</fieldset>
</div>
</div>
</form>
<script>
function sendform(){
event.preventDefault();
var f=event.target;
var formdata = new FormData(f);
var p=f.querySelector('.info');
p.innerHTML='در حال ارسال...';
p.style.background ='teal';
f.style.pointerEvents='none';
var xhr = new XMLHttpRequest();
xhr.onload = function(){
f.querySelector('img').src='/captcha/?'+Math.random();
f.style.pointerEvents='auto';
if(xhr.status == 200){
if(xhr.response.startsWith('OK')){
p.innerHTML="پیام شما با موفقیت ثبت شد.";
p.style.background ='green';
return;
}else if(xhr.responseText.startsWith('ERROR|')){
p.innerHTML=xhr.response.substr(6);
p.style.background='red';
return;
}
}
p.innerHTML='خطا در ارتباط با سرور';
p.style.background ='red';
};
xhr.open('POST', '/');
xhr.send(formdata);
return false;
}
</script>
نکته: تابع
sendformنام عمومی دارد و باsend_commentپیوست الف تداخلی ندارد؛ پوستهای که هم فرم تماس و هم دیدگاه دارد میتواند هر دو را در یکscript.jsداشته باشد.