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
+188 -152
View File
@@ -8,7 +8,6 @@ import {
Empty as AEmpty,
Flex as AFlex,
Row as ARow,
Spin as ASpin,
Typography,
theme,
} from 'ant-design-vue';
@@ -22,7 +21,13 @@ import {
FallOutlined,
RiseOutlined,
WalletOutlined,
} from '@ant-design/icons-vue';
} from '@/icons/ant-bridge';
import {
CrmSkeleton,
CrmSkeletonCard,
CrmSkeletonKpi,
CrmSkeletonList,
} from '@/components/skeleton';
import { use } from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
import { BarChart, PieChart } from 'echarts/charts';
@@ -314,17 +319,18 @@ onMounted(async () => {
description="وقتی سند و حساب واقعی ثبت شود، همین صفحه دادهٔ واقعی API را نشان می‌دهد."
/>
<a-spin :spinning="loading">
<template v-if="data">
<a-row :gutter="[token.marginSM, token.marginSM]">
<a-col
v-for="card in heroCards"
:key="card.key"
:xs="24"
:sm="12"
:lg="8"
>
<CrmSkeleton :loading="loading">
<a-row :gutter="[token.marginSM, token.marginSM]">
<a-col
v-for="card in heroCards"
:key="card.key"
:xs="24"
:sm="12"
:lg="8"
>
<CrmSkeletonKpi>
<a-card
v-if="data"
:bordered="false"
:body-style="{ padding: `${token.padding}px` }"
:style="softCardStyle"
@@ -360,19 +366,22 @@ onMounted(async () => {
</a-avatar>
</a-flex>
</a-card>
</a-col>
</a-row>
</CrmSkeletonKpi>
</a-col>
</a-row>
<a-row :gutter="[token.marginSM, token.marginSM]" :style="{ marginTop: `${token.margin}px` }">
<a-col
v-for="card in secondaryCards"
:key="card.key"
:xs="24"
:sm="12"
:md="8"
:lg="8"
>
<a-row :gutter="[token.marginSM, token.marginSM]" :style="{ marginTop: `${token.margin}px` }">
<a-col
v-for="card in secondaryCards"
:key="card.key"
:xs="24"
:sm="12"
:md="8"
:lg="8"
>
<CrmSkeletonKpi>
<a-card
v-if="data"
:bordered="false"
:body-style="{ padding: `${token.paddingSM}px` }"
:style="secondaryCardStyle"
@@ -406,162 +415,189 @@ onMounted(async () => {
</a-avatar>
</a-flex>
</a-card>
</a-col>
</a-row>
</CrmSkeletonKpi>
</a-col>
</a-row>
<template v-if="data">
<a-row :gutter="[token.marginSM, token.marginSM]" :style="{ marginTop: `${token.margin}px` }">
<a-col :xs="24" :lg="14">
<a-card
:bordered="false"
:style="secondaryCardStyle"
>
<a-typography-text strong>جریان تعهدی و نقدی</a-typography-text>
<a-typography-text
type="secondary"
:style="{ display: 'block', marginBottom: `${token.marginSM}px`, fontSize: '12px' }"
<CrmSkeletonCard :rows="6" :min-height="320">
<a-card
:bordered="false"
:style="secondaryCardStyle"
>
مقایسه درآمد/هزینه ثبت‌شده با دریافت و پرداخت واقعی
</a-typography-text>
<VChart
:option="flowChartOption"
autoresize
:style="{ height: '260px', width: '100%' }"
/>
</a-card>
<a-typography-text strong>جریان تعهدی و نقدی</a-typography-text>
<a-typography-text
type="secondary"
:style="{ display: 'block', marginBottom: `${token.marginSM}px`, fontSize: '12px' }"
>
مقایسه درآمد/هزینه ثبت‌شده با دریافت و پرداخت واقعی
</a-typography-text>
<VChart
:option="flowChartOption"
autoresize
:style="{ height: '260px', width: '100%' }"
/>
</a-card>
</CrmSkeletonCard>
</a-col>
<a-col :xs="24" :lg="10">
<a-card
:bordered="false"
:style="secondaryCardStyle"
>
<a-typography-text strong>ترکیب وضعیت مالی</a-typography-text>
<a-typography-text
type="secondary"
:style="{ display: 'block', marginBottom: `${token.marginSM}px`, fontSize: '12px' }"
<CrmSkeletonCard :rows="6" :min-height="320">
<a-card
:bordered="false"
:style="secondaryCardStyle"
>
سهم موجودی، مطالبات و تعهدات
</a-typography-text>
<VChart
:option="mixChartOption"
autoresize
:style="{ height: '260px', width: '100%' }"
/>
</a-card>
<a-typography-text strong>ترکیب وضعیت مالی</a-typography-text>
<a-typography-text
type="secondary"
:style="{ display: 'block', marginBottom: `${token.marginSM}px`, fontSize: '12px' }"
>
سهم موجودی، مطالبات و تعهدات
</a-typography-text>
<VChart
:option="mixChartOption"
autoresize
:style="{ height: '260px', width: '100%' }"
/>
</a-card>
</CrmSkeletonCard>
</a-col>
</a-row>
<a-row :gutter="[token.marginSM, token.marginSM]" :style="{ marginTop: `${token.margin}px` }">
<a-col :xs="24" :lg="8">
<a-card
:bordered="false"
title="اقساط نزدیک یا معوق"
:style="secondaryCardStyle"
:body-style="{ paddingBlock: `${token.paddingSM}px` }"
>
<a-empty
v-if="!data.upcomingInstallments.length"
description="قسط بازی نیست."
:image-style="{ height: '48px' }"
/>
<a-flex v-else vertical :gap="token.marginXS">
<a-flex
v-for="item in data.upcomingInstallments"
:key="item.id"
justify="space-between"
align="flex-start"
:gap="8"
>
<div :style="{ minWidth: 0 }">
<a-typography-text>قسط {{ item.position }}</a-typography-text>
<div>
<a-typography-text type="secondary" :style="{ fontSize: '12px' }">
{{ item.status === 'overdue' ? 'معوق' : item.status === 'partial' ? 'جزئی' : 'باز' }}
· مانده {{ toman(item.remaining) }}
</a-typography-text>
<CrmSkeletonList :rows="4" :avatar="false">
<a-card
:bordered="false"
title="اقساط نزدیک یا معوق"
:style="secondaryCardStyle"
:body-style="{ paddingBlock: `${token.paddingSM}px` }"
>
<a-empty
v-if="!data.upcomingInstallments.length"
description="قسط بازی نیست."
:image-style="{ height: '48px' }"
/>
<a-flex v-else vertical :gap="token.marginXS">
<a-flex
v-for="item in data.upcomingInstallments"
:key="item.id"
justify="space-between"
align="flex-start"
:gap="8"
>
<div :style="{ minWidth: 0 }">
<a-typography-text>قسط {{ item.position }}</a-typography-text>
<div>
<a-typography-text type="secondary" :style="{ fontSize: '12px' }">
{{ item.status === 'overdue' ? 'معوق' : item.status === 'partial' ? 'جزئی' : 'باز' }}
· مانده {{ toman(item.remaining) }}
</a-typography-text>
</div>
</div>
</div>
<a-typography-text strong :style="{ color: token.colorPrimary, whiteSpace: 'nowrap' }">
{{ toman(item.amount) }}
</a-typography-text>
<a-typography-text strong :style="{ color: token.colorPrimary, whiteSpace: 'nowrap' }">
{{ toman(item.amount) }}
</a-typography-text>
</a-flex>
</a-flex>
</a-flex>
</a-card>
</a-card>
</CrmSkeletonList>
</a-col>
<a-col :xs="24" :lg="8">
<a-card
:bordered="false"
title="پرداخت‌های آینده"
:style="secondaryCardStyle"
:body-style="{ paddingBlock: `${token.paddingSM}px` }"
>
<a-empty
v-if="!data.upcomingPayables.length"
description="تعهد پرداخت بازی نیست."
:image-style="{ height: '48px' }"
/>
<a-flex v-else vertical :gap="token.marginXS">
<a-flex
v-for="item in data.upcomingPayables"
:key="item.id"
justify="space-between"
align="flex-start"
:gap="8"
>
<div :style="{ minWidth: 0 }">
<a-typography-text>{{ item.title }}</a-typography-text>
<div>
<a-typography-text type="secondary" :style="{ fontSize: '12px' }">
مانده {{ toman(item.remaining) }}
</a-typography-text>
<CrmSkeletonList :rows="4" :avatar="false">
<a-card
:bordered="false"
title="پرداخت‌های آینده"
:style="secondaryCardStyle"
:body-style="{ paddingBlock: `${token.paddingSM}px` }"
>
<a-empty
v-if="!data.upcomingPayables.length"
description="تعهد پرداخت بازی نیست."
:image-style="{ height: '48px' }"
/>
<a-flex v-else vertical :gap="token.marginXS">
<a-flex
v-for="item in data.upcomingPayables"
:key="item.id"
justify="space-between"
align="flex-start"
:gap="8"
>
<div :style="{ minWidth: 0 }">
<a-typography-text>{{ item.title }}</a-typography-text>
<div>
<a-typography-text type="secondary" :style="{ fontSize: '12px' }">
مانده {{ toman(item.remaining) }}
</a-typography-text>
</div>
</div>
</div>
<a-typography-text strong :style="{ color: token.colorPrimary, whiteSpace: 'nowrap' }">
{{ toman(item.amount) }}
</a-typography-text>
<a-typography-text strong :style="{ color: token.colorPrimary, whiteSpace: 'nowrap' }">
{{ toman(item.amount) }}
</a-typography-text>
</a-flex>
</a-flex>
</a-flex>
</a-card>
</a-card>
</CrmSkeletonList>
</a-col>
<a-col :xs="24" :lg="8">
<a-card
:bordered="false"
title="آخرین اسناد"
:style="secondaryCardStyle"
:body-style="{ paddingBlock: `${token.paddingSM}px` }"
>
<a-empty
v-if="!data.recent.length"
description="هنوز سندی ثبت نشده است."
:image-style="{ height: '48px' }"
/>
<a-flex v-else vertical :gap="token.marginXS">
<a-flex
v-for="item in data.recent"
:key="item.id"
justify="space-between"
align="flex-start"
:gap="8"
>
<div :style="{ minWidth: 0 }">
<a-typography-text>{{ item.title }}</a-typography-text>
<div>
<a-typography-text type="secondary" :style="{ fontSize: '12px' }">
{{ item.kind === 'income' ? 'درآمد' : 'هزینه' }} · {{ item.status }}
</a-typography-text>
<CrmSkeletonList :rows="4" :avatar="false">
<a-card
:bordered="false"
title="آخرین اسناد"
:style="secondaryCardStyle"
:body-style="{ paddingBlock: `${token.paddingSM}px` }"
>
<a-empty
v-if="!data.recent.length"
description="هنوز سندی ثبت نشده است."
:image-style="{ height: '48px' }"
/>
<a-flex v-else vertical :gap="token.marginXS">
<a-flex
v-for="item in data.recent"
:key="item.id"
justify="space-between"
align="flex-start"
:gap="8"
>
<div :style="{ minWidth: 0 }">
<a-typography-text>{{ item.title }}</a-typography-text>
<div>
<a-typography-text type="secondary" :style="{ fontSize: '12px' }">
{{ item.kind === 'income' ? 'درآمد' : 'هزینه' }} · {{ item.status }}
</a-typography-text>
</div>
</div>
</div>
<a-typography-text strong :style="{ color: token.colorPrimary, whiteSpace: 'nowrap' }">
{{ toman(item.amount) }}
</a-typography-text>
<a-typography-text strong :style="{ color: token.colorPrimary, whiteSpace: 'nowrap' }">
{{ toman(item.amount) }}
</a-typography-text>
</a-flex>
</a-flex>
</a-flex>
</a-card>
</a-card>
</CrmSkeletonList>
</a-col>
</a-row>
</template>
</a-spin>
<template v-else-if="loading">
<a-row :gutter="[token.marginSM, token.marginSM]" :style="{ marginTop: `${token.margin}px` }">
<a-col :xs="24" :lg="14">
<CrmSkeletonCard :rows="6" :min-height="320" />
</a-col>
<a-col :xs="24" :lg="10">
<CrmSkeletonCard :rows="6" :min-height="320" />
</a-col>
</a-row>
<a-row :gutter="[token.marginSM, token.marginSM]" :style="{ marginTop: `${token.margin}px` }">
<a-col v-for="i in 3" :key="i" :xs="24" :lg="8">
<CrmSkeletonList :rows="4" :avatar="false" />
</a-col>
</a-row>
</template>
</CrmSkeleton>
</a-flex>
</div>
</template>
@@ -38,7 +38,7 @@ import {
PlusOutlined,
TagOutlined,
UserOutlined,
} from '@ant-design/icons-vue';
} from '@/icons/ant-bridge';
import { api } from '@/lib/api';
import { formatLocalDateInput } from '@/lib/jalali';
import type {
@@ -12,7 +12,7 @@ import {
Typography,
} from 'ant-design-vue';
import type { TableColumnsType } from 'ant-design-vue';
import { DownloadOutlined } from '@ant-design/icons-vue';
import { DownloadOutlined } from '@/icons/ant-bridge';
import { api } from '@/lib/api';
import { toman } from './treasury-format';