[Admin] Redesign Settings as sidebar tabs + style password/toggle fields
Split the long settings page into 7 sidebar tabs (publish+AI, sources, channels, SMS, push, map, demo) with a single form so one Save persists everything; seed/clear/test are submit buttons targeting their handlers via asp-page-handler. Boolean settings now render as clean .toggle-row cards. CSS fix: the form input rule omitted input[type=password] (and url/email/search), so API-key/VAPID/token fields were unstyled — added them, plus accent-color + sizing for checkboxes/radios. Active tab persists across handler posts via sessionStorage; layout collapses to a horizontal tab strip on mobile. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,233 +1,238 @@
|
|||||||
@page
|
@page
|
||||||
@model JobsMedical.Web.Pages.Admin.SettingsModel
|
@model JobsMedical.Web.Pages.Admin.SettingsModel
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "تنظیمات جمعآوری و هوش مصنوعی";
|
ViewData["Title"] = "تنظیمات سامانه";
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="page-head">
|
<div class="page-head">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>تنظیمات جمعآوری و هوش مصنوعی</h1>
|
<h1>تنظیمات سامانه</h1>
|
||||||
<p class="muted"><a asp-page="/Admin/Index">← بازگشت به صف</a></p>
|
<p class="muted"><a asp-page="/Admin/Index">← بازگشت به صف</a></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container section" style="max-width:680px;">
|
<div class="container section">
|
||||||
|
@if (Model.Saved is not null) { <div class="alert alert-success">✓ @Model.Saved</div> }
|
||||||
@if (Model.DemoMsg is not null) { <div class="alert alert-success">@Model.DemoMsg</div> }
|
@if (Model.DemoMsg is not null) { <div class="alert alert-success">@Model.DemoMsg</div> }
|
||||||
<div class="card card-pad" style="margin-bottom:14px;">
|
|
||||||
<h3 style="margin-top:0;">حالت نمایشی (Demo)</h3>
|
|
||||||
<p class="muted" style="font-size:13px; margin-top:0;">دادههای نمونهی تهران را برای نمایش/دمو روی سایت قرار بده یا حذف کن. (تیک «حالت نمایشی» پایین را هم بزن تا پس از هر استقرار دوباره ساخته شود.)</p>
|
|
||||||
<div style="display:flex; gap:8px; flex-wrap:wrap;">
|
|
||||||
<form method="post"><button asp-page-handler="SeedDemo" class="btn btn-outline">ساخت داده نمونه</button></form>
|
|
||||||
<form method="post" onsubmit="return confirm('همه دادههای نمونه حذف شوند؟');"><button asp-page-handler="ClearDemo" class="btn btn-outline" style="color:var(--danger); border-color:var(--danger);">حذف داده نمونه</button></form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@if (Model.SmsTest is not null) { <div class="alert alert-success">@Model.SmsTest</div> }
|
@if (Model.SmsTest is not null) { <div class="alert alert-success">@Model.SmsTest</div> }
|
||||||
<form method="post" class="card card-pad" style="margin-bottom:14px; display:flex; gap:8px; align-items:end; flex-wrap:wrap;">
|
|
||||||
<div class="filter-group" style="margin:0; flex:1; min-width:160px;">
|
|
||||||
<label>ارسال پیامک آزمایشی به</label>
|
|
||||||
<input type="tel" name="TestPhone" dir="ltr" placeholder="۰۹۱۲ ..." />
|
|
||||||
</div>
|
|
||||||
<button type="submit" asp-page-handler="TestSms" class="btn btn-outline">ارسال آزمایشی</button>
|
|
||||||
</form>
|
|
||||||
@if (Model.Saved is not null)
|
|
||||||
{
|
|
||||||
<div class="alert alert-success">✓ @Model.Saved</div>
|
|
||||||
}
|
|
||||||
<form method="post" class="card card-pad">
|
|
||||||
<h3 style="margin-top:0;">کانالهای اعلان (فعال / غیرفعال)</h3>
|
|
||||||
<p class="muted" style="font-size:12px; margin-top:0;">روشن/خاموشکردن هر کانال ارسال اعلان به کاربران. کلیدها و تنظیمات هر کانال در بخشهای پایینتر.</p>
|
|
||||||
<div class="filter-group">
|
|
||||||
<label style="display:flex; align-items:center; gap:8px; font-weight:700;">
|
|
||||||
<input type="checkbox" name="WebNotificationsEnabled" value="true" style="width:auto;" checked="@Model.WebNotificationsEnabled" />
|
|
||||||
اعلانهای وب / درونبرنامهای (زنگوله + نوتیف زنده) — توصیهشده برای ایران
|
|
||||||
</label>
|
|
||||||
<p class="muted" style="font-size:12px; margin:4px 0 0;">از طریق سرور خودمان ارسال میشود؛ نیازی به سرویسهای گوگل ندارد و در ایران کار میکند.</p>
|
|
||||||
</div>
|
|
||||||
<div class="filter-group">
|
|
||||||
<label style="display:flex; align-items:center; gap:8px; font-weight:700;">
|
|
||||||
<input type="checkbox" name="SmsEnabled" value="true" style="width:auto;" checked="@Model.SmsEnabled" />
|
|
||||||
پیامک (SMS) — کاوهنگار
|
|
||||||
</label>
|
|
||||||
<p class="muted" style="font-size:12px; margin:4px 0 0;">برای کد ورود و اعلانهای مهم. کلید و تمپلیت را در بخش «پیامک ورود» پایین وارد کن.</p>
|
|
||||||
</div>
|
|
||||||
<div class="filter-group">
|
|
||||||
<label style="display:flex; align-items:center; gap:8px; font-weight:700;">
|
|
||||||
<input type="checkbox" name="PushEnabled" value="true" style="width:auto;" checked="@Model.PushEnabled" />
|
|
||||||
پوش مرورگر (Web Push) — بهترین تلاش
|
|
||||||
</label>
|
|
||||||
<p class="muted" style="font-size:12px; margin:4px 0 0;">برای اعلان هنگام بستهبودن برنامه؛ ولی از سرویس مرورگر (گوگل) عبور میکند که در ایران اغلب فیلتر است.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr style="border:none; border-top:1px solid var(--line); margin:18px 0;" />
|
<form method="post">
|
||||||
|
<div class="settings-layout">
|
||||||
|
|
||||||
<h3 style="margin-top:0;">حالت انتشار</h3>
|
<!-- Sidebar tabs -->
|
||||||
<div class="filter-group">
|
<nav class="settings-tabs" id="settingsTabs">
|
||||||
<label>نحوه افزودن آگهیها به سایت</label>
|
<button type="button" data-tab="publish" class="active"><span class="tab-ico">📢</span> انتشار و هوش مصنوعی</button>
|
||||||
<select name="Mode">
|
<button type="button" data-tab="sources"><span class="tab-ico">📡</span> منابع جمعآوری</button>
|
||||||
<option value="0" selected="@(Model.Mode == JobsMedical.Web.Models.IngestionMode.Manual)">دستی — همه به صف بررسی میروند</option>
|
<button type="button" data-tab="channels"><span class="tab-ico">🔔</span> کانالهای اعلان</button>
|
||||||
<option value="1" selected="@(Model.Mode == JobsMedical.Web.Models.IngestionMode.Automatic)">خودکار — موارد تأییدشده مستقیم منتشر میشوند</option>
|
<button type="button" data-tab="sms"><span class="tab-ico">✉️</span> پیامک</button>
|
||||||
</select>
|
<button type="button" data-tab="push"><span class="tab-ico">📲</span> پوش مرورگری</button>
|
||||||
</div>
|
<button type="button" data-tab="map"><span class="tab-ico">🗺️</span> نقشه</button>
|
||||||
<div class="filter-group">
|
<button type="button" data-tab="demo"><span class="tab-ico">🧪</span> حالت نمایشی</button>
|
||||||
<label>حداقل درصد اطمینان برای انتشار خودکار (بدون هوش مصنوعی)</label>
|
</nav>
|
||||||
<input type="number" name="AutoPublishMinConfidence" min="0" max="100" value="@Model.AutoPublishMinConfidence" dir="ltr" />
|
|
||||||
<p class="muted" style="font-size:12px; margin:4px 0 0;">در حالت خودکار و بدون AI، آگهیهایی با اطمینان بالاتر از این مقدار خودکار منتشر میشوند.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr style="border:none; border-top:1px solid var(--line); margin:18px 0;" />
|
<!-- Panels -->
|
||||||
|
<div>
|
||||||
|
<!-- PUBLISH + AI -->
|
||||||
|
<section class="card card-pad settings-panel active" data-panel="publish">
|
||||||
|
<h3>حالت انتشار</h3>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>نحوه افزودن آگهیها به سایت</label>
|
||||||
|
<select name="Mode">
|
||||||
|
<option value="0" selected="@(Model.Mode == JobsMedical.Web.Models.IngestionMode.Manual)">دستی — همه به صف بررسی میروند</option>
|
||||||
|
<option value="1" selected="@(Model.Mode == JobsMedical.Web.Models.IngestionMode.Automatic)">خودکار — موارد تأییدشده مستقیم منتشر میشوند</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>حداقل درصد اطمینان برای انتشار خودکار (بدون هوش مصنوعی)</label>
|
||||||
|
<input type="number" name="AutoPublishMinConfidence" min="0" max="100" value="@Model.AutoPublishMinConfidence" dir="ltr" />
|
||||||
|
<p class="muted" style="font-size:12px; margin:4px 0 0;">در حالت خودکار و بدون AI، آگهیهایی با اطمینان بالاتر از این مقدار خودکار منتشر میشوند.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3 style="margin-top:0;">لایه هوش مصنوعی (اختیاری)</h3>
|
<hr style="border:none; border-top:1px solid var(--line); margin:18px 0;" />
|
||||||
<div class="filter-group">
|
<h3>لایه هوش مصنوعی (اختیاری)</h3>
|
||||||
<label style="display:flex; align-items:center; gap:8px; font-weight:700;">
|
<label class="toggle-row">
|
||||||
<input type="checkbox" name="AiEnabled" value="true" style="width:auto;" checked="@Model.AiEnabled" />
|
<input type="checkbox" name="AiEnabled" value="true" checked="@Model.AiEnabled" />
|
||||||
فعالسازی بررسی با هوش مصنوعی قبل از انتشار
|
<span class="t-body"><span>فعالسازی بررسی با هوش مصنوعی قبل از انتشار</span>
|
||||||
</label>
|
<span class="t-hint">هر آگهی پیش از انتشار توسط مدل بررسی و تأیید/رد/ساختارمند میشود.</span></span>
|
||||||
<p class="muted" style="font-size:12px; margin:4px 0 0;">در صورت فعال بودن، هر آگهی پیش از انتشار توسط مدل بررسی و تأیید/رد/ساختارمند میشود.</p>
|
</label>
|
||||||
</div>
|
<div class="filter-group">
|
||||||
<div class="filter-group">
|
<label>آدرس سرویس (سازگار با OpenAI)</label>
|
||||||
<label>آدرس سرویس (سازگار با OpenAI)</label>
|
<input type="text" name="AiEndpoint" value="@Model.AiEndpoint" placeholder="https://host/v1/chat/completions" dir="ltr" />
|
||||||
<input type="text" name="AiEndpoint" value="@Model.AiEndpoint" placeholder="https://host/v1/chat/completions" dir="ltr" />
|
<p class="muted" style="font-size:12px; margin:4px 0 0;">میتواند یک مدل self-hosted یا سرویس داخلی باشد (OpenAI/Anthropic در ایران مسدودند).</p>
|
||||||
<p class="muted" style="font-size:12px; margin:4px 0 0;">میتواند یک مدل self-hosted یا سرویس داخلی باشد (OpenAI/Anthropic در ایران مسدودند).</p>
|
</div>
|
||||||
</div>
|
<div class="filter-group" style="display:flex; gap:8px;">
|
||||||
<div class="filter-group" style="display:flex; gap:8px;">
|
<div style="flex:1;"><label>کلید API</label><input type="password" name="AiApiKey" value="@Model.AiApiKey" dir="ltr" /></div>
|
||||||
<div style="flex:1;"><label>کلید API</label><input type="password" name="AiApiKey" value="@Model.AiApiKey" dir="ltr" /></div>
|
<div style="flex:1;"><label>نام مدل</label><input type="text" name="AiModel" value="@Model.AiModel" dir="ltr" /></div>
|
||||||
<div style="flex:1;"><label>نام مدل</label><input type="text" name="AiModel" value="@Model.AiModel" dir="ltr" /></div>
|
</div>
|
||||||
</div>
|
<div class="filter-group">
|
||||||
<div class="filter-group">
|
<label>دستور و چارچوب هوش مصنوعی (Prompt / Framework)</label>
|
||||||
<label>دستور و چارچوب هوش مصنوعی (Prompt / Framework)</label>
|
<textarea name="AiSystemPrompt" rows="8" dir="rtl">@Model.AiSystemPrompt</textarea>
|
||||||
<textarea name="AiSystemPrompt" rows="10" dir="rtl">@Model.AiSystemPrompt</textarea>
|
<p class="muted" style="font-size:12px; margin:4px 0 0;">به مدل بگو چطور تأیید/رد کند و چه فیلدهایی را استخراج کند. خروجی باید JSON باشد.</p>
|
||||||
<p class="muted" style="font-size:12px; margin:4px 0 0;">به مدل بگو چطور تأیید/رد کند و چه فیلدهایی را استخراج کند. خروجی باید JSON باشد.</p>
|
</div>
|
||||||
</div>
|
<label class="toggle-row">
|
||||||
<div class="filter-group">
|
<input type="checkbox" name="AiAutoApprove" value="true" checked="@Model.AiAutoApprove" />
|
||||||
<label style="display:flex; align-items:center; gap:8px; font-weight:700;">
|
<span class="t-body"><span>در حالت خودکار، آگهیهایی که AI تأیید میکند مستقیم منتشر شوند</span></span>
|
||||||
<input type="checkbox" name="AiAutoApprove" value="true" style="width:auto;" checked="@Model.AiAutoApprove" />
|
</label>
|
||||||
در حالت خودکار، آگهیهایی که AI تأیید میکند مستقیم منتشر شوند
|
</section>
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr style="border:none; border-top:1px solid var(--line); margin:18px 0;" />
|
<!-- SOURCES -->
|
||||||
|
<section class="card card-pad settings-panel" data-panel="sources">
|
||||||
|
<h3>منابع جمعآوری</h3>
|
||||||
|
<label class="toggle-row">
|
||||||
|
<input type="checkbox" name="AutoIngestEnabled" value="true" checked="@Model.AutoIngestEnabled" />
|
||||||
|
<span class="t-body"><span>اجرای خودکار جمعآوری روی زمانبند</span></span>
|
||||||
|
</label>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>فاصله اجرای خودکار (دقیقه)</label>
|
||||||
|
<input type="number" name="IngestIntervalMinutes" min="1" value="@Model.IngestIntervalMinutes" dir="ltr" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3 style="margin-top:0;">منابع جمعآوری (اسکرپ کانالها)</h3>
|
<label class="toggle-row">
|
||||||
<div class="filter-group">
|
<input type="checkbox" name="TelegramEnabled" value="true" checked="@Model.TelegramEnabled" />
|
||||||
<label style="display:flex; align-items:center; gap:8px; font-weight:700;">
|
<span class="t-body"><span>تلگرام (کانالهای عمومی — بدون توکن)</span></span>
|
||||||
<input type="checkbox" name="AutoIngestEnabled" value="true" style="width:auto;" checked="@Model.AutoIngestEnabled" />
|
</label>
|
||||||
اجرای خودکار جمعآوری روی زمانبند
|
<div class="filter-group">
|
||||||
</label>
|
<label>یوزرنیم کانالها (هر خط یک کانال)</label>
|
||||||
</div>
|
<textarea name="TelegramChannels" rows="3" dir="ltr" placeholder="shift_channel another_channel">@Model.TelegramChannels</textarea>
|
||||||
<div class="filter-group">
|
</div>
|
||||||
<label>فاصله اجرای خودکار (دقیقه)</label>
|
|
||||||
<input type="number" name="IngestIntervalMinutes" min="1" value="@Model.IngestIntervalMinutes" dir="ltr" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filter-group">
|
<label class="toggle-row">
|
||||||
<label style="display:flex; align-items:center; gap:8px; font-weight:700;">
|
<input type="checkbox" name="BaleEnabled" value="true" checked="@Model.BaleEnabled" />
|
||||||
<input type="checkbox" name="TelegramEnabled" value="true" style="width:auto;" checked="@Model.TelegramEnabled" />
|
<span class="t-body"><span>بله (بات باید عضو کانال باشد)</span></span>
|
||||||
تلگرام (کانالهای عمومی — بدون توکن)
|
</label>
|
||||||
</label>
|
<div class="filter-group"><label>توکن بات بله</label><input type="password" name="BaleBotToken" value="@Model.BaleBotToken" dir="ltr" /></div>
|
||||||
<label style="margin-top:6px;">یوزرنیم کانالها (هر خط یک کانال)</label>
|
|
||||||
<textarea name="TelegramChannels" rows="3" dir="ltr" placeholder="shift_channel another_channel">@Model.TelegramChannels</textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filter-group">
|
<label class="toggle-row">
|
||||||
<label style="display:flex; align-items:center; gap:8px; font-weight:700;">
|
<input type="checkbox" name="DivarEnabled" value="true" checked="@Model.DivarEnabled" />
|
||||||
<input type="checkbox" name="BaleEnabled" value="true" style="width:auto;" checked="@Model.BaleEnabled" />
|
<span class="t-body"><span>دیوار</span></span>
|
||||||
بله (بات باید عضو کانال باشد)
|
</label>
|
||||||
</label>
|
<div class="filter-group" style="display:flex; gap:8px;">
|
||||||
<label style="margin-top:6px;">توکن بات بله</label>
|
<div style="flex:0 0 120px;"><label>شهر (slug)</label><input type="text" name="DivarCity" value="@Model.DivarCity" dir="ltr" placeholder="tehran" /></div>
|
||||||
<input type="password" name="BaleBotToken" value="@Model.BaleBotToken" dir="ltr" />
|
<div style="flex:1;"><label>عبارتهای جستجو (هر خط یکی)</label><textarea name="DivarQueries" rows="3">@Model.DivarQueries</textarea></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="filter-group">
|
<label class="toggle-row">
|
||||||
<label style="display:flex; align-items:center; gap:8px; font-weight:700;">
|
<input type="checkbox" name="MedjobsEnabled" value="true" checked="@Model.MedjobsEnabled" />
|
||||||
<input type="checkbox" name="DivarEnabled" value="true" style="width:auto;" checked="@Model.DivarEnabled" />
|
<span class="t-body"><span>مدجابز (medjobs.ir)</span><span class="t-hint">خواندن کامل آگهیها از سایتمپ.</span></span>
|
||||||
دیوار
|
</label>
|
||||||
</label>
|
<div class="filter-group"><label>حداکثر آگهی در هر اجرا</label><input type="number" name="MedjobsMaxAds" min="1" max="500" value="@Model.MedjobsMaxAds" dir="ltr" /></div>
|
||||||
<div style="display:flex; gap:8px; margin-top:6px;">
|
|
||||||
<div style="flex:0 0 120px;"><label>شهر (slug)</label><input type="text" name="DivarCity" value="@Model.DivarCity" dir="ltr" placeholder="tehran" /></div>
|
<label class="toggle-row">
|
||||||
<div style="flex:1;"><label>عبارتهای جستجو (هر خط یکی)</label><textarea name="DivarQueries" rows="3">@Model.DivarQueries</textarea></div>
|
<input type="checkbox" name="WebsitesEnabled" value="true" checked="@Model.WebsitesEnabled" />
|
||||||
|
<span class="t-body"><span>وبسایتها (آدرسهای دلخواه)</span></span>
|
||||||
|
</label>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>آدرس صفحهها (هر خط یک URL)</label>
|
||||||
|
<textarea name="WebsiteUrls" rows="3" dir="ltr" placeholder="https://example.ir/jobs/123">@Model.WebsiteUrls</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr style="border:none; border-top:1px solid var(--line); margin:18px 0;" />
|
||||||
|
<label class="toggle-row">
|
||||||
|
<input type="checkbox" name="IngestProxyEnabled" value="true" checked="@Model.IngestProxyEnabled" />
|
||||||
|
<span class="t-body"><span>ارسال جمعآوری از طریق پروکسی</span>
|
||||||
|
<span class="t-hint">برای دسترسی به تلگرام و … در ایران (Xray/V2Ray).</span></span>
|
||||||
|
</label>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>آدرس پروکسی محلی</label>
|
||||||
|
<input type="text" name="IngestProxyUrl" value="@Model.IngestProxyUrl" dir="ltr" placeholder="socks5://xray:10808" />
|
||||||
|
<p class="muted" style="font-size:12px; margin:4px 0 0;">یک کلاینت Xray/V2Ray کانفیگ vmess/vless/trojan تو را به یک پروکسی محلی تبدیل میکند (socks5:// یا socks4:// یا http://).</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- CHANNELS -->
|
||||||
|
<section class="card card-pad settings-panel" data-panel="channels">
|
||||||
|
<h3>کانالهای اعلان (فعال / غیرفعال)</h3>
|
||||||
|
<p class="muted" style="font-size:12px; margin-top:0;">روشن/خاموشکردن هر کانال ارسال اعلان به کاربران. کلیدها در تبهای «پیامک» و «پوش مرورگری».</p>
|
||||||
|
<label class="toggle-row">
|
||||||
|
<input type="checkbox" name="WebNotificationsEnabled" value="true" checked="@Model.WebNotificationsEnabled" />
|
||||||
|
<span class="t-body"><span>اعلانهای وب / درونبرنامهای (زنگوله + نوتیف زنده)</span>
|
||||||
|
<span class="t-hint">توصیهشده برای ایران — از سرور خودمان، بدون سرویس گوگل.</span></span>
|
||||||
|
</label>
|
||||||
|
<label class="toggle-row">
|
||||||
|
<input type="checkbox" name="SmsEnabled" value="true" checked="@Model.SmsEnabled" />
|
||||||
|
<span class="t-body"><span>پیامک (SMS) — کاوهنگار</span>
|
||||||
|
<span class="t-hint">برای کد ورود و اعلانهای مهم.</span></span>
|
||||||
|
</label>
|
||||||
|
<label class="toggle-row">
|
||||||
|
<input type="checkbox" name="PushEnabled" value="true" checked="@Model.PushEnabled" />
|
||||||
|
<span class="t-body"><span>پوش مرورگر (Web Push)</span>
|
||||||
|
<span class="t-hint">برای اعلان هنگام بستهبودن برنامه؛ از سرویس مرورگر (گوگل) عبور میکند و در ایران اغلب فیلتر است.</span></span>
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- SMS -->
|
||||||
|
<section class="card card-pad settings-panel" data-panel="sms">
|
||||||
|
<h3>پیامک ورود (OTP) — کاوهنگار</h3>
|
||||||
|
<p class="muted" style="font-size:12px; margin-top:0;">روشن/خاموشکردن این کانال در تب «کانالهای اعلان». (در صورت خاموش بودن، کد ورود روی صفحه نمایش داده میشود.)</p>
|
||||||
|
<div class="filter-group"><label>کلید API کاوهنگار</label><input type="password" name="SmsApiKey" value="@Model.SmsApiKey" dir="ltr" /></div>
|
||||||
|
<div class="filter-group" style="display:flex; gap:8px;">
|
||||||
|
<div style="flex:1;"><label>نام تمپلیت verify (ترجیحی)</label><input type="text" name="SmsTemplate" value="@Model.SmsTemplate" dir="ltr" placeholder="otp" /></div>
|
||||||
|
<div style="flex:1;"><label>خط ارسال (در نبود تمپلیت)</label><input type="text" name="SmsSender" value="@Model.SmsSender" dir="ltr" placeholder="10008..." /></div>
|
||||||
|
</div>
|
||||||
|
<p class="muted" style="font-size:12px;">روش پیشنهادی: تمپلیت verify/lookup با متغیر %token. اگر تمپلیت خالی باشد، پیامک ساده با خط ارسال فرستاده میشود.</p>
|
||||||
|
<hr style="border:none; border-top:1px solid var(--line); margin:14px 0;" />
|
||||||
|
<div class="filter-group" style="display:flex; gap:8px; align-items:end; flex-wrap:wrap;">
|
||||||
|
<div style="flex:1; min-width:160px;"><label>ارسال پیامک آزمایشی به</label><input type="tel" name="TestPhone" dir="ltr" placeholder="۰۹۱۲ ..." /></div>
|
||||||
|
<button type="submit" asp-page-handler="TestSms" class="btn btn-outline">ارسال آزمایشی</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- PUSH -->
|
||||||
|
<section class="card card-pad settings-panel" data-panel="push">
|
||||||
|
<h3>اعلانها (Web Push / PWA)</h3>
|
||||||
|
<p class="muted" style="font-size:12px; margin-top:0;">روشن/خاموشکردن این کانال در تب «کانالهای اعلان». اینجا فقط کلیدهای VAPID را وارد کن.</p>
|
||||||
|
<div class="filter-group"><label>VAPID Public Key</label><input type="text" name="VapidPublicKey" value="@Model.VapidPublicKey" dir="ltr" /></div>
|
||||||
|
<div class="filter-group"><label>VAPID Private Key</label><input type="password" name="VapidPrivateKey" value="@Model.VapidPrivateKey" dir="ltr" /></div>
|
||||||
|
<div class="filter-group"><label>VAPID Subject</label><input type="text" name="VapidSubject" value="@Model.VapidSubject" dir="ltr" placeholder="mailto:admin@@hamkadr.ir" /></div>
|
||||||
|
<p class="muted" style="font-size:12px;">جفتکلید VAPID را یکبار بساز (web-push). بدون آن، اعلان محلی روی دستگاه کار میکند ولی ارسال از سرور نیاز به کلید دارد.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- MAP -->
|
||||||
|
<section class="card card-pad settings-panel" data-panel="map">
|
||||||
|
<h3>نقشه (نشان)</h3>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>کلید API نقشه نشان (web map.js)</label>
|
||||||
|
<input type="text" name="NeshanMapKey" value="@Model.NeshanMapKey" dir="ltr" placeholder="web.xxxxx" />
|
||||||
|
<p class="muted" style="font-size:12px; margin:4px 0 0;">برای انتخاب موقعیت روی نقشه در فرم ثبت مرکز. بدون کلید، فقط دکمه «موقعیت فعلی من» نمایش داده میشود.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- DEMO -->
|
||||||
|
<section class="card card-pad settings-panel" data-panel="demo">
|
||||||
|
<h3>حالت نمایشی (Demo)</h3>
|
||||||
|
<label class="toggle-row">
|
||||||
|
<input type="checkbox" name="DemoMode" value="true" checked="@Model.DemoMode" />
|
||||||
|
<span class="t-body"><span>حالت نمایشی فعال باشد</span>
|
||||||
|
<span class="t-hint">دادههای نمونه پس از هر استقرار بهصورت خودکار ساخته شوند.</span></span>
|
||||||
|
</label>
|
||||||
|
<p class="muted" style="font-size:13px;">ساخت/حذف فوری دادههای نمونهی تهران:</p>
|
||||||
|
<div style="display:flex; gap:8px; flex-wrap:wrap;">
|
||||||
|
<button type="submit" asp-page-handler="SeedDemo" class="btn btn-outline">ساخت داده نمونه</button>
|
||||||
|
<button type="submit" asp-page-handler="ClearDemo" class="btn btn-outline" style="color:var(--danger); border-color:var(--danger);" onclick="return confirm('همه دادههای نمونه حذف شوند؟');">حذف داده نمونه</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="settings-save">
|
||||||
|
<button type="submit" class="btn btn-accent btn-block btn-lg">ذخیره تنظیمات</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="filter-group">
|
|
||||||
<label style="display:flex; align-items:center; gap:8px; font-weight:700;">
|
|
||||||
<input type="checkbox" name="MedjobsEnabled" value="true" style="width:auto;" checked="@Model.MedjobsEnabled" />
|
|
||||||
مدجابز (medjobs.ir) — خواندن کامل آگهیها از سایتمپ
|
|
||||||
</label>
|
|
||||||
<label style="margin-top:6px;">حداکثر آگهی در هر اجرا</label>
|
|
||||||
<input type="number" name="MedjobsMaxAds" min="1" max="500" value="@Model.MedjobsMaxAds" dir="ltr" />
|
|
||||||
<p class="muted" style="font-size:12px; margin:4px 0 0;">آگهیهای تکراری بهصورت خودکار رد میشوند؛ هر اجرا فقط آگهیهای جدید را میآورد.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filter-group">
|
|
||||||
<label style="display:flex; align-items:center; gap:8px; font-weight:700;">
|
|
||||||
<input type="checkbox" name="WebsitesEnabled" value="true" style="width:auto;" checked="@Model.WebsitesEnabled" />
|
|
||||||
وبسایتها (آدرسهای دلخواه)
|
|
||||||
</label>
|
|
||||||
<label style="margin-top:6px;">آدرس صفحهها (هر خط یک URL)</label>
|
|
||||||
<textarea name="WebsiteUrls" rows="3" dir="ltr" placeholder="https://example.ir/jobs/123">@Model.WebsiteUrls</textarea>
|
|
||||||
<p class="muted" style="font-size:12px; margin:4px 0 0;">موتور هر آدرس را میخواند و متن آگهی را استخراج میکند (عنوان og + بدنه محتوا). برای هر صفحه شغلی، آرشیو کانال یا آگهی طبقهبندی.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filter-group">
|
|
||||||
<label style="display:flex; align-items:center; gap:8px; font-weight:700;">
|
|
||||||
<input type="checkbox" name="IngestProxyEnabled" value="true" style="width:auto;" checked="@Model.IngestProxyEnabled" />
|
|
||||||
ارسال جمعآوری از طریق پروکسی (برای دسترسی به تلگرام و … در ایران)
|
|
||||||
</label>
|
|
||||||
<label style="margin-top:6px;">آدرس پروکسی محلی</label>
|
|
||||||
<input type="text" name="IngestProxyUrl" value="@Model.IngestProxyUrl" dir="ltr" placeholder="socks5://xray:10808" />
|
|
||||||
<p class="muted" style="font-size:12px; margin:4px 0 0;">یک کلاینت Xray/V2Ray (سرویس جانبی) کانفیگ vmess/vless/trojan تو را به یک پروکسی محلی SOCKS تبدیل میکند؛ آدرس همان را اینجا بگذار (socks5:// یا socks4:// یا http://).</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr style="border:none; border-top:1px solid var(--line); margin:18px 0;" />
|
|
||||||
|
|
||||||
<h3 style="margin-top:0;">حالت نمایشی (Demo)</h3>
|
|
||||||
<div class="filter-group">
|
|
||||||
<label style="display:flex; align-items:center; gap:8px; font-weight:700;">
|
|
||||||
<input type="checkbox" name="DemoMode" value="true" style="width:auto;" checked="@Model.DemoMode" />
|
|
||||||
حالت نمایشی فعال باشد — دادههای نمونه پس از هر استقرار بهصورت خودکار ساخته شوند
|
|
||||||
</label>
|
|
||||||
<p class="muted" style="font-size:12px; margin:4px 0 0;">برای ساخت/حذف فوری دادههای نمونه از کارت بالای همین صفحه استفاده کن.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr style="border:none; border-top:1px solid var(--line); margin:18px 0;" />
|
|
||||||
|
|
||||||
<h3 style="margin-top:0;">پیامک ورود (OTP) — کاوهنگار</h3>
|
|
||||||
<p class="muted" style="font-size:12px; margin-top:0;">روشن/خاموشکردن این کانال در بخش «کانالهای اعلان» بالا. (در صورت خاموش بودن، کد ورود روی صفحه نمایش داده میشود.)</p>
|
|
||||||
<div class="filter-group">
|
|
||||||
<label>کلید API کاوهنگار</label>
|
|
||||||
<input type="password" name="SmsApiKey" value="@Model.SmsApiKey" dir="ltr" />
|
|
||||||
</div>
|
|
||||||
<div class="filter-group" style="display:flex; gap:8px;">
|
|
||||||
<div style="flex:1;"><label>نام تمپلیت verify (ترجیحی)</label><input type="text" name="SmsTemplate" value="@Model.SmsTemplate" dir="ltr" placeholder="otp" /></div>
|
|
||||||
<div style="flex:1;"><label>خط ارسال (در نبود تمپلیت)</label><input type="text" name="SmsSender" value="@Model.SmsSender" dir="ltr" placeholder="10008..." /></div>
|
|
||||||
</div>
|
|
||||||
<p class="muted" style="font-size:12px;">روش پیشنهادی: تمپلیت verify/lookup با متغیر %token. اگر تمپلیت خالی باشد، پیامک ساده با خط ارسال فرستاده میشود.</p>
|
|
||||||
|
|
||||||
<hr style="border:none; border-top:1px solid var(--line); margin:18px 0;" />
|
|
||||||
<h3 style="margin-top:0;">نقشه (نشان)</h3>
|
|
||||||
<div class="filter-group">
|
|
||||||
<label>کلید API نقشه نشان (web map.js)</label>
|
|
||||||
<input type="text" name="NeshanMapKey" value="@Model.NeshanMapKey" dir="ltr" placeholder="web.xxxxx" />
|
|
||||||
<p class="muted" style="font-size:12px; margin:4px 0 0;">برای انتخاب موقعیت روی نقشه در فرم ثبت مرکز. بدون کلید، فقط دکمه «موقعیت فعلی من» نمایش داده میشود.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr style="border:none; border-top:1px solid var(--line); margin:18px 0;" />
|
|
||||||
<h3 style="margin-top:0;">اعلانها (Web Push / PWA)</h3>
|
|
||||||
<p class="muted" style="font-size:12px; margin-top:0;">روشن/خاموشکردن این کانال در بخش «کانالهای اعلان» بالا. اینجا فقط کلیدهای VAPID را وارد کن.</p>
|
|
||||||
<div class="filter-group">
|
|
||||||
<label>VAPID Public Key</label>
|
|
||||||
<input type="text" name="VapidPublicKey" value="@Model.VapidPublicKey" dir="ltr" />
|
|
||||||
</div>
|
|
||||||
<div class="filter-group">
|
|
||||||
<label>VAPID Private Key</label>
|
|
||||||
<input type="password" name="VapidPrivateKey" value="@Model.VapidPrivateKey" dir="ltr" />
|
|
||||||
</div>
|
|
||||||
<div class="filter-group">
|
|
||||||
<label>VAPID Subject</label>
|
|
||||||
<input type="text" name="VapidSubject" value="@Model.VapidSubject" dir="ltr" placeholder="mailto:admin@hamkadr.ir" />
|
|
||||||
</div>
|
|
||||||
<p class="muted" style="font-size:12px;">جفتکلید VAPID را یکبار بساز (web-push). بدون آن، اعلان محلی روی دستگاه کار میکند ولی ارسال از سرور نیاز به کلید دارد.</p>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-accent btn-block btn-lg">ذخیره تنظیمات</button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var tabs = document.querySelectorAll('#settingsTabs button');
|
||||||
|
var panels = document.querySelectorAll('.settings-panel');
|
||||||
|
function show(name) {
|
||||||
|
tabs.forEach(function (t) { t.classList.toggle('active', t.dataset.tab === name); });
|
||||||
|
panels.forEach(function (p) { p.classList.toggle('active', p.dataset.panel === name); });
|
||||||
|
try { sessionStorage.setItem('adminTab', name); } catch (e) {}
|
||||||
|
}
|
||||||
|
tabs.forEach(function (t) { t.addEventListener('click', function () { show(t.dataset.tab); }); });
|
||||||
|
var saved; try { saved = sessionStorage.getItem('adminTab'); } catch (e) {}
|
||||||
|
if (saved && document.querySelector('[data-panel="' + saved + '"]')) show(saved);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
|||||||
@@ -143,10 +143,13 @@ button, input, select, textarea, optgroup { font-family: inherit; }
|
|||||||
.search-card label { font-size: 12px; font-weight: 700; color: var(--muted); }
|
.search-card label { font-size: 12px; font-weight: 700; color: var(--muted); }
|
||||||
|
|
||||||
/* ---------- Forms ---------- */
|
/* ---------- Forms ---------- */
|
||||||
select, input[type="text"], input[type="tel"], input[type="number"], textarea {
|
select, textarea,
|
||||||
|
input[type="text"], input[type="tel"], input[type="number"], input[type="password"],
|
||||||
|
input[type="email"], input[type="url"], input[type="search"] {
|
||||||
width: 100%; padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px;
|
width: 100%; padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px;
|
||||||
font-family: inherit; font-size: 14px; background: #fff; color: var(--ink);
|
font-family: inherit; font-size: 14px; background: #fff; color: var(--ink);
|
||||||
}
|
}
|
||||||
|
input[type="checkbox"], input[type="radio"] { accent-color: var(--primary); width: 17px; height: 17px; }
|
||||||
select:focus, input:focus, textarea:focus { outline: none; border-color: var(--primary); }
|
select:focus, input:focus, textarea:focus { outline: none; border-color: var(--primary); }
|
||||||
label { font-size: 13px; }
|
label { font-size: 13px; }
|
||||||
|
|
||||||
@@ -253,6 +256,44 @@ label { font-size: 13px; }
|
|||||||
.tour-count { font-size: 12px; color: var(--muted); }
|
.tour-count { font-size: 12px; color: var(--muted); }
|
||||||
.tour-btns { display: flex; gap: 6px; }
|
.tour-btns { display: flex; gap: 6px; }
|
||||||
|
|
||||||
|
/* ---------- Admin settings: sidebar tabs ---------- */
|
||||||
|
.settings-layout { display: grid; grid-template-columns: 220px 1fr; gap: 18px; align-items: start; }
|
||||||
|
.settings-tabs {
|
||||||
|
position: sticky; top: 80px; display: flex; flex-direction: column; gap: 4px;
|
||||||
|
background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow); padding: 8px;
|
||||||
|
}
|
||||||
|
.settings-tabs button {
|
||||||
|
display: flex; align-items: center; gap: 8px; width: 100%; text-align: start;
|
||||||
|
padding: 11px 12px; border: none; background: transparent; border-radius: 10px;
|
||||||
|
font-family: inherit; font-size: 14px; font-weight: 600; color: var(--muted); cursor: pointer;
|
||||||
|
}
|
||||||
|
.settings-tabs button:hover { background: var(--primary-soft); color: var(--primary-dark); }
|
||||||
|
.settings-tabs button.active { background: var(--primary); color: #fff; }
|
||||||
|
.settings-tabs .tab-ico { font-size: 16px; }
|
||||||
|
|
||||||
|
.settings-panel { display: none; }
|
||||||
|
.settings-panel.active { display: block; animation: fadeIn .15s ease; }
|
||||||
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
||||||
|
.settings-panel h3:first-child { margin-top: 0; }
|
||||||
|
|
||||||
|
/* Toggle rows — give each boolean field a clean, card-like row. */
|
||||||
|
.toggle-row {
|
||||||
|
display: flex; align-items: flex-start; gap: 10px; font-weight: 700;
|
||||||
|
padding: 12px 14px; border: 1px solid var(--line); border-radius: 12px; background: var(--bg);
|
||||||
|
cursor: pointer; margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.toggle-row input[type="checkbox"] { margin-top: 2px; flex: 0 0 auto; }
|
||||||
|
.toggle-row .t-body { display: flex; flex-direction: column; gap: 3px; }
|
||||||
|
.toggle-row .t-hint { font-size: 12px; font-weight: 500; color: var(--muted); }
|
||||||
|
.settings-save { position: sticky; bottom: 0; padding-top: 12px; background: linear-gradient(transparent, var(--bg) 40%); }
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.settings-layout { grid-template-columns: 1fr; }
|
||||||
|
.settings-tabs { position: static; flex-direction: row; overflow-x: auto; }
|
||||||
|
.settings-tabs button { white-space: nowrap; }
|
||||||
|
}
|
||||||
|
|
||||||
/* Legal/policy pages (privacy, rules, terms) — comfortable long-form reading. */
|
/* Legal/policy pages (privacy, rules, terms) — comfortable long-form reading. */
|
||||||
.legal { line-height: 2; }
|
.legal { line-height: 2; }
|
||||||
.legal h2 { font-size: 17px; margin: 22px 0 8px; color: var(--primary-dark); }
|
.legal h2 { font-size: 17px; margin: 22px 0 8px; color: var(--primary-dark); }
|
||||||
|
|||||||
Reference in New Issue
Block a user