feat(web): migrate CRM screens to Ant Design Vue with central theme
Adopt ant-design-vue and a shared antdTheme for leads, pipeline, invoices, and shared form controls under strict single-page governance. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,10 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
Alert as AAlert,
|
||||
Button as AButton,
|
||||
Card as ACard,
|
||||
Flex as AFlex,
|
||||
List as AList,
|
||||
Modal,
|
||||
Space as ASpace,
|
||||
Spin as ASpin,
|
||||
Textarea as ATextarea,
|
||||
Typography,
|
||||
} from 'ant-design-vue';
|
||||
import { api } from '@/lib/api';
|
||||
import { brand, type MemoryItemDto, type PaginatedResponse } from '@mento/shared';
|
||||
import { formatJalaliDateTime } from '@/lib/jalali';
|
||||
|
||||
const { Title: ATypographyTitle, Text: ATypographyText, Paragraph: ATypographyParagraph } =
|
||||
Typography;
|
||||
const AListItem = AList.Item;
|
||||
const AListItemMeta = AList.Item.Meta;
|
||||
|
||||
const props = defineProps<{
|
||||
personId: string;
|
||||
}>();
|
||||
@@ -86,15 +103,24 @@ async function saveEdit(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function removeItem(id: string) {
|
||||
if (!confirm('این مورد از حافظه حذف شود؟')) return;
|
||||
try {
|
||||
await api.delete(`/memory/${id}`);
|
||||
if (editingId.value === id) cancelEdit();
|
||||
await load();
|
||||
} catch {
|
||||
error.value = 'حذف ناموفق بود';
|
||||
}
|
||||
function removeItem(id: string) {
|
||||
Modal.confirm({
|
||||
title: 'حذف از حافظه',
|
||||
content: 'این مورد از حافظه حذف شود؟',
|
||||
okText: 'حذف',
|
||||
cancelText: 'انصراف',
|
||||
okButtonProps: { danger: true },
|
||||
async onOk() {
|
||||
try {
|
||||
await api.delete(`/memory/${id}`);
|
||||
if (editingId.value === id) cancelEdit();
|
||||
await load();
|
||||
} catch {
|
||||
error.value = 'حذف ناموفق بود';
|
||||
return Promise.reject();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function formatWhen(value: string) {
|
||||
@@ -110,152 +136,71 @@ onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="memory-panel">
|
||||
<header>
|
||||
<div>
|
||||
<p class="eyebrow">{{ brand.memoryEn }}</p>
|
||||
<h2>حافظه مشتری</h2>
|
||||
<p class="lead">نکات پایدار درباره این شخص — فقط با تأیید شما ذخیره میشود.</p>
|
||||
</div>
|
||||
</header>
|
||||
<a-card :bordered="false" :style="{ marginTop: '16px' }">
|
||||
<a-typography-text type="secondary" strong>{{ brand.memoryEn }}</a-typography-text>
|
||||
<a-typography-title :level="5" :style="{ margin: '4px 0' }">حافظه مشتری</a-typography-title>
|
||||
<a-typography-paragraph type="secondary" :style="{ marginBottom: '12px' }">
|
||||
نکات پایدار درباره این شخص — فقط با تأیید شما ذخیره میشود.
|
||||
</a-typography-paragraph>
|
||||
|
||||
<form class="add-form" @submit.prevent="addItem">
|
||||
<textarea
|
||||
v-model="draft"
|
||||
rows="2"
|
||||
<a-flex :gap="8" align="flex-start" :style="{ marginBottom: '12px' }">
|
||||
<a-textarea
|
||||
v-model:value="draft"
|
||||
:rows="2"
|
||||
placeholder="مثلاً: بودجه تا ۵۰۰ میلیون · ترجیح تماس صبحها"
|
||||
style="flex: 1"
|
||||
@pressEnter.ctrl="addItem"
|
||||
/>
|
||||
<button class="btn btn-ghost" type="submit" :disabled="saving || !draft.trim()">
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="saving || !draft.trim()"
|
||||
:loading="saving"
|
||||
@click="addItem"
|
||||
>
|
||||
افزودن
|
||||
</button>
|
||||
</form>
|
||||
</a-button>
|
||||
</a-flex>
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<p v-else-if="loading" class="muted">در حال بارگذاری…</p>
|
||||
<p v-else-if="!hasItems" class="muted">هنوز موردی در حافظه ثبت نشده.</p>
|
||||
<a-alert v-if="error" type="error" show-icon :message="error" :style="{ marginBottom: '12px' }" />
|
||||
|
||||
<ul v-else class="items">
|
||||
<li v-for="item in items" :key="item.id">
|
||||
<template v-if="editingId === item.id">
|
||||
<textarea v-model="editContent" rows="2" />
|
||||
<div class="row-actions">
|
||||
<button type="button" class="btn" :disabled="saving" @click="saveEdit(item.id)">
|
||||
ذخیره
|
||||
</button>
|
||||
<button type="button" class="btn btn-ghost" @click="cancelEdit">انصراف</button>
|
||||
</div>
|
||||
<a-spin :spinning="loading">
|
||||
<a-typography-text v-if="!loading && !hasItems" type="secondary">
|
||||
هنوز موردی در حافظه ثبت نشده.
|
||||
</a-typography-text>
|
||||
|
||||
<a-list v-else-if="hasItems" item-layout="vertical" :data-source="items">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<template v-if="editingId === item.id">
|
||||
<a-flex vertical :gap="8" style="width: 100%">
|
||||
<a-textarea v-model:value="editContent" :rows="2" />
|
||||
<a-space :size="8">
|
||||
<a-button type="primary" :loading="saving" @click="saveEdit(item.id)">ذخیره</a-button>
|
||||
<a-button @click="cancelEdit">انصراف</a-button>
|
||||
</a-space>
|
||||
</a-flex>
|
||||
</template>
|
||||
<a-list-item-meta v-else>
|
||||
<template #title>
|
||||
<a-typography-paragraph :style="{ marginBottom: 0, whiteSpace: 'pre-wrap' }">
|
||||
{{ item.content }}
|
||||
</a-typography-paragraph>
|
||||
</template>
|
||||
<template #description>
|
||||
<a-typography-text type="secondary">
|
||||
{{ sourceLabel[item.sourceType] ?? item.sourceType }}
|
||||
<template v-if="item.confirmedBy"> · {{ item.confirmedBy.fullName }}</template>
|
||||
· {{ formatWhen(item.updatedAt) }}
|
||||
</a-typography-text>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
<template v-if="editingId !== item.id" #actions>
|
||||
<a-button type="link" @click="startEdit(item)">ویرایش</a-button>
|
||||
<a-button type="link" danger @click="removeItem(item.id)">حذف</a-button>
|
||||
</template>
|
||||
</a-list-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p class="content">{{ item.content }}</p>
|
||||
<p class="meta">
|
||||
<span>{{ sourceLabel[item.sourceType] ?? item.sourceType }}</span>
|
||||
<span v-if="item.confirmedBy"> · {{ item.confirmedBy.fullName }}</span>
|
||||
<span> · {{ formatWhen(item.updatedAt) }}</span>
|
||||
</p>
|
||||
<div class="row-actions">
|
||||
<button type="button" @click="startEdit(item)">ویرایش</button>
|
||||
<button type="button" class="danger" @click="removeItem(item.id)">حذف</button>
|
||||
</div>
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</a-list>
|
||||
</a-spin>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.memory-panel {
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0;
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0.15rem 0 0;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.lead {
|
||||
margin: 0.35rem 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.add-form {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 0.6rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
textarea {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: 0.75rem 0.9rem;
|
||||
background: #fff;
|
||||
font: inherit;
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.items {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.items li {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: 0.75rem 0.85rem;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.meta {
|
||||
margin: 0.35rem 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.row-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.row-actions button {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.row-actions button.danger {
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #b42318;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user