feat(web): adopt vuesax icons and shared skeleton loading

Replace the inline SVG icon set with the vuesax linear pack, add skeleton primitives for loading states, and keep page scroll inside the app shell.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-05 14:46:35 +03:30
co-authored by Cursor
parent 41f310f3a2
commit 6c6e32e099
133 changed files with 9069 additions and 7416 deletions
+280 -234
View File
@@ -2,6 +2,26 @@
import { computed, onMounted, ref } from 'vue';
import { RouterLink, useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import {
Alert as AAlert,
Breadcrumb as ABreadcrumb,
BreadcrumbItem as ABreadcrumbItem,
Button as AButton,
Card as ACard,
Col as ACol,
Divider as ADivider,
Flex as AFlex,
Form as AForm,
FormItem as AFormItem,
Input as AInput,
Modal,
Row as ARow,
Space as ASpace,
Textarea as ATextarea,
Typography,
} from 'ant-design-vue';
import { CheckOutlined, DeleteOutlined } from '@/icons/ant-bridge';
import { CrmSkeleton, CrmSkeletonCard } from '@/components/skeleton';
import { api } from '@/lib/api';
import type { CompanyDto, CreateCompanyRequest } from '@mento/shared';
import CustomFieldsSection from '@/components/CustomFieldsSection.vue';
@@ -10,15 +30,8 @@ import RelatedTasksPanel from '@/components/routines/RelatedTasksPanel.vue';
import JalaliDateInput from '@/components/JalaliDateInput.vue';
import CustomerGroupPicker from '@/components/CustomerGroupPicker.vue';
import { formatLocalDateInput, parseLocalInput } from '@/lib/jalali';
import {
UiButton,
UiCard,
UiField,
UiInput,
UiPageHeader,
UiTextarea,
} from '@/components/ui';
import UiIcon from '@/components/UiIcon.vue';
const { Title: ATypographyTitle, Text: ATypographyText } = Typography;
const props = withDefaults(
defineProps<{
@@ -181,13 +194,22 @@ async function submit() {
}
async function remove() {
if (!id.value || !confirm('این شرکت حذف شود؟ افراد متصل بدون شرکت می‌مانند.')) return;
try {
await api.delete(`/companies/${id.value}`);
await router.push({ name: 'contacts', query: { tab: 'companies' } });
} catch {
error.value = 'حذف ناموفق بود';
}
if (!id.value) return;
Modal.confirm({
title: 'حذف شرکت',
content: 'این شرکت حذف شود؟ افراد متصل بدون شرکت می‌مانند.',
okText: 'حذف',
okType: 'danger',
cancelText: 'انصراف',
async onOk() {
try {
await api.delete(`/companies/${id.value}`);
await router.push({ name: 'contacts', query: { tab: 'companies' } });
} catch {
error.value = 'حذف ناموفق بود';
}
},
});
}
function cancel() {
@@ -202,232 +224,256 @@ onMounted(load);
</script>
<template>
<section class="crm-page company-form-page" :class="{ embedded }" dir="rtl">
<UiPageHeader
<a-flex vertical :gap="16" dir="rtl">
<a-flex
v-if="!embedded"
:title="pageTitle"
description="ثبت و ویرایش مشخصات حقوقی، اطلاعات تماس و فیلدهای سفارشی شرکت"
justify="space-between"
align="center"
wrap="wrap"
:gap="12"
>
<template #breadcrumb>
<RouterLink :to="{ name: 'contacts', query: { tab: 'companies' } }" class="breadcrumb-link">
شرکت‌ها
</RouterLink>
<span class="breadcrumb-separator">/</span>
<span>{{ pageTitle }}</span>
</template>
<template #actions>
<UiButton :loading="saving" @click="submit">
<template #icon>
<UiIcon name="check-plain" />
</template>
<a-flex vertical :gap="4">
<a-breadcrumb>
<a-breadcrumb-item>
<RouterLink :to="{ name: 'contacts', query: { tab: 'companies' } }">
شرکت‌ها
</RouterLink>
</a-breadcrumb-item>
<a-breadcrumb-item>{{ pageTitle }}</a-breadcrumb-item>
</a-breadcrumb>
<a-typography-title :level="3" :style="{ margin: 0 }">
{{ pageTitle }}
</a-typography-title>
<a-typography-text type="secondary">
ثبت و ویرایش مشخصات حقوقی، اطلاعات تماس و فیلدهای سفارشی شرکت
</a-typography-text>
</a-flex>
<a-space :size="8" wrap>
<a-button type="primary" :loading="saving" @click="submit">
<template #icon><CheckOutlined /></template>
ذخیره
</UiButton>
<UiButton variant="ghost" :disabled="saving" @click="cancel">
انصراف
</UiButton>
<UiButton v-if="!isNew" variant="danger" :disabled="saving" @click="remove">
<template #icon>
<UiIcon name="trash" />
</template>
</a-button>
<a-button :disabled="saving" @click="cancel">انصراف</a-button>
<a-button
v-if="!isNew"
danger
:disabled="saving"
@click="remove"
>
<template #icon><DeleteOutlined /></template>
حذف
</UiButton>
</template>
</UiPageHeader>
</a-button>
</a-space>
</a-flex>
<div v-if="error" class="crm-alert crm-alert-danger">{{ error }}</div>
<a-alert v-if="error" type="error" show-icon :message="error" />
<form class="crm-form" @submit.prevent="submit">
<UiCard title="اطلاعات اصلی شرکت" class="section-card">
<div class="crm-form-grid-2">
<UiField label="نام شرکت" required class="col-span-2">
<UiInput
id="name"
v-model="name"
required
minlength="2"
placeholder="مثال: داده‌پردازی سپهر"
/>
</UiField>
<CrmSkeleton :loading="loading">
<CrmSkeletonCard :rows="8">
<a-form layout="vertical" @finish="submit">
<a-card :bordered="false">
<a-divider orientation="right">اطلاعات اصلی شرکت</a-divider>
<a-row :gutter="[16, 0]">
<a-col :span="24">
<a-form-item label="نام شرکت" required>
<a-input
id="name"
v-model:value="name"
:minlength="2"
placeholder="مثال: داده‌پردازی سپهر"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="کد شرکت">
<a-input id="code" v-model:value="code" placeholder="کد داخلی" dir="ltr" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="کد دیدار شرکت">
<a-input
id="sourceRef"
v-model:value="sourceRef"
placeholder="مثال: 5277"
dir="ltr"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="گروه‌های شرکت">
<CustomerGroupPicker v-model="groupIds" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="تاریخ ثبت شرکت">
<JalaliDateInput id="registeredAt" v-model="registeredAt" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="برچسب‌ها">
<a-input
id="company-tags"
v-model:value="tagsRaw"
placeholder="شرکتی، VIP (با کاما جدا کنید)"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="توضیحات شرکت">
<a-textarea
id="notes"
v-model:value="notes"
:rows="4"
placeholder="توضیحات مهم، حوزه‌های همکاری..."
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
<UiField label="کد شرکت">
<UiInput id="code" v-model="code" placeholder="کد داخلی" dir="ltr" />
</UiField>
<a-card :bordered="false" :style="{ marginTop: '16px' }">
<a-divider orientation="right">اطلاعات تماس و آدرس</a-divider>
<a-row :gutter="[16, 0]">
<a-col :xs="24" :md="12">
<a-form-item label="تلفن شرکت">
<a-input
id="phone"
v-model:value="phone"
type="tel"
placeholder="۰۲۱..."
dir="ltr"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="تلفن های دیگر شرکت">
<a-input
id="otherPhones"
v-model:value="otherPhones"
placeholder="با کاما جدا کنید"
dir="ltr"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="ایمیل شرکت">
<a-input
id="email"
v-model:value="email"
type="email"
placeholder="info@company.ir"
dir="ltr"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="ایمیل های دیگر شرکت">
<a-input
id="otherEmails"
v-model:value="otherEmails"
placeholder="با کاما جدا کنید"
dir="ltr"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="شماره فکس">
<a-input id="fax" v-model:value="fax" type="tel" dir="ltr" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="سایت های شرکت">
<a-input
id="website"
v-model:value="website"
placeholder="https://..."
dir="ltr"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="استان">
<a-input id="province" v-model:value="province" placeholder="مثال: تهران" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="شهرستان">
<a-input id="city" v-model:value="city" placeholder="مثال: تهران" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="کد پستی">
<a-input id="postalCode" v-model:value="postalCode" dir="ltr" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="آدرس">
<a-input
id="address"
v-model:value="address"
placeholder="نشانی، طبقه، پلاک..."
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
<UiField label="کد دیدار شرکت">
<UiInput id="sourceRef" v-model="sourceRef" placeholder="مثال: 5277" dir="ltr" />
</UiField>
<a-card :bordered="false" :style="{ marginTop: '16px' }">
<a-divider orientation="right">اطلاعات حقوقی و ثبتی</a-divider>
<a-row :gutter="[16, 0]">
<a-col :xs="24" :md="12">
<a-form-item label="شناسه ملی">
<a-input
id="nationalId"
v-model:value="nationalId"
placeholder="شناسه ملی ۱۱ رقمی"
dir="ltr"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="شماره اقتصادی">
<a-input id="economicCode" v-model:value="economicCode" dir="ltr" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="شماره ثبت">
<a-input
id="registrationNumber"
v-model:value="registrationNumber"
dir="ltr"
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
<UiField label="گروه‌های شرکت" class="col-span-2">
<CustomerGroupPicker v-model="groupIds" />
</UiField>
<CustomFieldsSection entity-type="company" v-model="customFields" />
<RelatedTasksPanel
v-if="!isNew && id"
:entity="{ type: 'company', id, label: name }"
/>
<EntityTimeline v-if="!isNew && id" entity-type="company" :entity-id="id" />
<UiField label="تاریخ ثبت شرکت">
<JalaliDateInput id="registeredAt" v-model="registeredAt" />
</UiField>
<UiField label="برچسب‌ها" class="col-span-2">
<UiInput
id="company-tags"
v-model="tagsRaw"
placeholder="شرکتی، VIP (با کاما جدا کنید)"
/>
</UiField>
<UiField label="توضیحات شرکت" class="col-span-2">
<UiTextarea
id="notes"
v-model="notes"
:rows="4"
placeholder="توضیحات مهم، حوزه‌های همکاری..."
/>
</UiField>
</div>
</UiCard>
<UiCard title="اطلاعات تماس و آدرس" class="section-card">
<div class="crm-form-grid-2">
<UiField label="تلفن شرکت">
<UiInput id="phone" v-model="phone" type="tel" placeholder="۰۲۱..." dir="ltr" />
</UiField>
<UiField label="تلفن های دیگر شرکت">
<UiInput id="otherPhones" v-model="otherPhones" placeholder="با کاما جدا کنید" dir="ltr" />
</UiField>
<UiField label="ایمیل شرکت">
<UiInput
id="email"
v-model="email"
type="email"
placeholder="info@company.ir"
dir="ltr"
/>
</UiField>
<UiField label="ایمیل های دیگر شرکت">
<UiInput id="otherEmails" v-model="otherEmails" placeholder="با کاما جدا کنید" dir="ltr" />
</UiField>
<UiField label="شماره فکس">
<UiInput id="fax" v-model="fax" type="tel" dir="ltr" />
</UiField>
<UiField label="سایت های شرکت">
<UiInput id="website" v-model="website" placeholder="https://..." dir="ltr" />
</UiField>
<UiField label="استان">
<UiInput id="province" v-model="province" placeholder="مثال: تهران" />
</UiField>
<UiField label="شهرستان">
<UiInput id="city" v-model="city" placeholder="مثال: تهران" />
</UiField>
<UiField label="کد پستی">
<UiInput id="postalCode" v-model="postalCode" dir="ltr" />
</UiField>
<UiField label="آدرس" class="col-span-2">
<UiInput id="address" v-model="address" placeholder="نشانی، طبقه، پلاک..." />
</UiField>
</div>
</UiCard>
<UiCard title="اطلاعات حقوقی و ثبتی" class="section-card">
<div class="crm-form-grid-2">
<UiField label="شناسه ملی">
<UiInput id="nationalId" v-model="nationalId" placeholder="شناسه ملی ۱۱ رقمی" dir="ltr" />
</UiField>
<UiField label="شماره اقتصادی">
<UiInput id="economicCode" v-model="economicCode" dir="ltr" />
</UiField>
<UiField label="شماره ثبت">
<UiInput id="registrationNumber" v-model="registrationNumber" dir="ltr" />
</UiField>
</div>
</UiCard>
<CustomFieldsSection entity-type="company" v-model="customFields" />
<RelatedTasksPanel v-if="!isNew && id" :entity="{ type: 'company', id, label: name }" />
<EntityTimeline v-if="!isNew && id" entity-type="company" :entity-id="id" />
<div class="form-bottom-actions">
<UiButton type="submit" :loading="saving">
<template #icon>
<UiIcon name="check-plain" />
</template>
ذخیره اطلاعات
</UiButton>
<UiButton variant="ghost" type="button" :disabled="saving" @click="cancel">
انصراف
</UiButton>
<UiButton v-if="!isNew" variant="danger" type="button" :disabled="saving" @click="remove">
حذف شرکت
</UiButton>
</div>
</form>
</section>
<a-flex justify="flex-end" :style="{ marginTop: '16px' }">
<a-space :size="8" wrap>
<a-button type="primary" html-type="submit" :loading="saving">
<template #icon><CheckOutlined /></template>
ذخیره اطلاعات
</a-button>
<a-button :disabled="saving" @click="cancel">انصراف</a-button>
<a-button
v-if="!isNew"
danger
:disabled="saving"
@click="remove"
>
حذف شرکت
</a-button>
</a-space>
</a-flex>
</a-form>
</CrmSkeletonCard>
</CrmSkeleton>
</a-flex>
</template>
<style scoped>
.company-form-page {
display: flex;
flex-direction: column;
gap: var(--sp-6);
width: 100%;
max-width: 100%;
}
.company-form-page.embedded {
max-width: none;
gap: var(--sp-4);
}
.embedded-actions {
display: flex;
gap: var(--sp-2);
}
.breadcrumb-link {
color: var(--color-text-secondary);
text-decoration: none;
}
.breadcrumb-link:hover {
color: var(--color-brand-600);
}
.breadcrumb-separator {
margin: 0 var(--sp-2);
color: var(--color-text-muted);
}
.section-card {
padding: var(--sp-6);
}
.col-span-2 {
grid-column: span 2;
}
.form-bottom-actions {
display: flex;
gap: var(--sp-3);
justify-content: flex-end;
margin-top: var(--sp-4);
}
@media (max-width: 640px) {
.col-span-2 {
grid-column: span 1;
}
.form-bottom-actions {
flex-direction: column;
}
}
</style>