Files
CRM_Montzar/apps/web/src/views/FaqSettingsView.vue
T
s.esmaeili 1ae87c97b7 feat(ui): standardize design system, modals, sidebar, and form inputs
- Implement unified design system tokens, 10px radius, and IRANSans typography
- Modernize sidebar with Gemini-style collapsed expander, square 1:1 items, and elevated profile flyout
- Standardize all modal dialogs with 16px padding, fixed header/footer, 16px medium title, and sticky bottom actions
- Refactor DealFormView and CustomFieldsSection with standard UiField, UiInput, and UiSelect components
- Add draggable bottom-left QuickCreate floating action button
2026-09-12 01:33:07 +03:30

331 lines
8.4 KiB
Vue

<script setup lang="ts">
import { onMounted, ref } from 'vue';
import axios from 'axios';
import { api } from '@/lib/api';
import type {
CreateFaqArticleRequest,
FaqArticleDto,
UpdateFaqArticleRequest,
} from '@mento/shared';
import {
UiBadge,
UiButton,
UiCard,
UiEmptyState,
UiField,
UiInput,
UiPageHeader,
UiTextarea,
} from '@/components/ui';
import UiIcon from '@/components/UiIcon.vue';
const loading = ref(false);
const saving = ref(false);
const error = ref('');
const ok = ref('');
const items = ref<FaqArticleDto[]>([]);
const editingId = ref<string | null>(null);
const question = ref('');
const answer = ref('');
const category = ref('');
const isPublished = ref(true);
const position = ref(0);
function resetForm() {
editingId.value = null;
question.value = '';
answer.value = '';
category.value = '';
isPublished.value = true;
position.value = 0;
}
function startEdit(row: FaqArticleDto) {
editingId.value = row.id;
question.value = row.question;
answer.value = row.answer;
category.value = row.category ?? '';
isPublished.value = row.isPublished;
position.value = row.position;
ok.value = '';
}
async function load() {
loading.value = true;
error.value = '';
try {
const { data } = await api.get<FaqArticleDto[]>('/faq', {
params: { publishedOnly: false },
});
items.value = data;
} catch (e) {
if (axios.isAxiosError(e) && !e.response) {
error.value = 'اتصال به API برقرار نشد.';
} else {
error.value = 'بارگذاری FAQ ناموفق بود.';
}
} finally {
loading.value = false;
}
}
async function save() {
if (!question.value.trim() || !answer.value.trim()) {
error.value = 'سؤال و پاسخ را وارد کنید.';
return;
}
saving.value = true;
error.value = '';
ok.value = '';
try {
if (editingId.value) {
const body: UpdateFaqArticleRequest = {
question: question.value.trim(),
answer: answer.value.trim(),
category: category.value.trim() || null,
isPublished: isPublished.value,
position: position.value,
};
await api.patch(`/faq/${editingId.value}`, body);
ok.value = 'مقاله به‌روز شد.';
} else {
const body: CreateFaqArticleRequest = {
question: question.value.trim(),
answer: answer.value.trim(),
category: category.value.trim() || undefined,
isPublished: isPublished.value,
position: position.value,
};
await api.post('/faq', body);
ok.value = 'مقاله اضافه شد.';
}
resetForm();
await load();
} catch (e) {
if (axios.isAxiosError(e)) {
const msg = (e.response?.data as { message?: string | string[] })?.message;
error.value = Array.isArray(msg)
? msg.join('، ')
: (msg ?? 'ذخیره ناموفق بود');
} else {
error.value = 'ذخیره ناموفق بود';
}
} finally {
saving.value = false;
}
}
async function remove(row: FaqArticleDto) {
if (!confirm(`حذف «${row.question}»؟`)) return;
await api.delete(`/faq/${row.id}`);
if (editingId.value === row.id) resetForm();
await load();
}
onMounted(load);
</script>
<template>
<section class="crm-page faq-page" dir="rtl">
<UiPageHeader
title="پایگاه دانش و سؤالات متداول"
description="سؤالات پرتکرار و پاسخ‌های جامع برای تسریع در پاسخگویی پشتیبانان و کاهش تیکت‌های مکرر"
/>
<div v-if="error" class="crm-alert crm-alert-danger">{{ error }}</div>
<div v-if="ok" class="crm-alert crm-alert-success">{{ ok }}</div>
<UiCard :title="editingId ? 'ویرایش مقاله' : 'مقاله جدید'" class="editor-card">
<form class="crm-form" @submit.prevent="save">
<UiField label="پرسش / عنوان مقاله" required>
<UiInput v-model="question" required maxlength="500" placeholder="مثلاً نحوه فعال‌سازی حساب کاربری..." />
</UiField>
<UiField label="پاسخ کامل" required>
<UiTextarea v-model="answer" :rows="4" required placeholder="توضیحات و راهنمای گام‌به‌گام..." />
</UiField>
<div class="form-grid-extra">
<UiField label="دسته‌بندی">
<UiInput v-model="category" maxlength="100" placeholder="مثلاً مالی، فنی، نصب" />
</UiField>
<UiField label="ترتیب نمایش">
<UiInput v-model.number="position" type="number" min="0" />
</UiField>
<div class="publish-toggle-wrap">
<label class="publish-label">
<input v-model="isPublished" type="checkbox" />
<span>منتشرشده در پورتال</span>
</label>
</div>
</div>
<div class="form-actions-row">
<UiButton type="submit" :loading="saving">
{{ editingId ? 'ذخیره تغییرات' : 'افزودن مقاله' }}
</UiButton>
<UiButton v-if="editingId" variant="ghost" type="button" @click="resetForm">
انصراف
</UiButton>
</div>
</form>
</UiCard>
<UiEmptyState
v-if="!loading && !items.length"
title="هنوز مقاله‌ای ثبت نشده است"
description="با ثبت اولین سؤال و پاسخ، پایگاه دانش سازمان را تکمیل کنید."
icon="document"
/>
<div v-else class="articles-list">
<UiCard v-for="row in items" :key="row.id" class="article-card">
<div class="article-content">
<div class="article-title-row">
<strong class="article-question">{{ row.question }}</strong>
<div class="article-badges">
<UiBadge :variant="row.isPublished ? 'success' : 'neutral'">
{{ row.isPublished ? 'منتشرشده' : 'پیش‌نویس' }}
</UiBadge>
<UiBadge v-if="row.category" variant="info">
{{ row.category }}
</UiBadge>
</div>
</div>
<p class="article-answer">{{ row.answer }}</p>
</div>
<div class="article-actions">
<UiButton variant="secondary" size="sm" @click="startEdit(row)">
<template #icon>
<UiIcon name="pencil" />
</template>
ویرایش
</UiButton>
<UiButton variant="danger" size="sm" @click="remove(row)">
<template #icon>
<UiIcon name="trash" />
</template>
حذف
</UiButton>
</div>
</UiCard>
</div>
</section>
</template>
<style scoped>
.faq-page {
display: flex;
flex-direction: column;
gap: var(--sp-6);
width: 100%;
max-width: 100%;
}
.editor-card {
padding: var(--sp-6);
}
.form-grid-extra {
display: grid;
grid-template-columns: 1fr 1fr auto;
gap: var(--sp-4);
align-items: flex-end;
}
.publish-toggle-wrap {
display: flex;
align-items: center;
height: var(--control-h, 40px);
}
.publish-label {
display: inline-flex;
align-items: center;
gap: var(--sp-2);
font-size: var(--fs-sm);
color: var(--color-text-secondary);
cursor: pointer;
user-select: none;
}
.form-actions-row {
display: flex;
gap: var(--sp-2);
margin-top: var(--sp-2);
}
.articles-list {
display: flex;
flex-direction: column;
gap: var(--sp-4);
}
.article-card {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: var(--sp-4);
padding: var(--sp-4) var(--sp-5);
}
.article-content {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--sp-2);
}
.article-title-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--sp-3);
}
.article-question {
font-size: var(--fs-md);
font-weight: 700;
color: var(--color-text-primary);
}
.article-badges {
display: inline-flex;
align-items: center;
gap: var(--sp-2);
}
.article-answer {
margin: 0;
font-size: var(--fs-sm);
color: var(--color-text-secondary);
line-height: var(--lh-relaxed);
white-space: pre-wrap;
}
.article-actions {
display: flex;
gap: var(--sp-2);
flex-shrink: 0;
}
@media (max-width: 640px) {
.form-grid-extra {
grid-template-columns: 1fr;
}
.article-card {
flex-direction: column;
align-items: stretch;
}
.article-actions {
justify-content: flex-end;
}
}
</style>