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:
2026-10-18 19:39:59 +03:30
co-authored by Cursor
parent ab538b8304
commit 4abab55a6b
40 changed files with 7529 additions and 11795 deletions
+48 -154
View File
@@ -1,9 +1,18 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import UiIcon from '@/components/UiIcon.vue';
import { computed, ref } from 'vue';
import {
Button as AButton,
Checkbox as ACheckbox,
Flex as AFlex,
Popover as APopover,
Typography,
} from 'ant-design-vue';
import { SettingOutlined } from '@ant-design/icons-vue';
import { useTableColumns } from '@/lib/tableColumns';
import { toFaDigits } from '@/lib/jalali';
const { Text: ATypographyText } = Typography;
const props = withDefaults(
defineProps<{
tableKey: string;
@@ -14,164 +23,49 @@ const props = withDefaults(
);
const open = ref(false);
const root = ref<HTMLElement | null>(null);
const cols = useTableColumns(() => props.tableKey);
const columns = computed(() => cols.columns());
const hiddenCount = computed(() => cols.hiddenCount());
function onDocClick(e: MouseEvent) {
if (open.value && root.value && !root.value.contains(e.target as Node)) open.value = false;
}
onMounted(() => document.addEventListener('click', onDocClick));
onBeforeUnmount(() => document.removeEventListener('click', onDocClick));
const placement = computed(() =>
props.align === 'end' ? 'bottomLeft' : 'bottomRight',
);
</script>
<template>
<div ref="root" class="tc-pop">
<button
class="tc-tool"
type="button"
:class="{ active: hiddenCount > 0 }"
:title="label"
@click.stop="open = !open"
>
<UiIcon name="sliders" :size="16" />
<span class="tc-label">{{ label }}</span>
<em v-if="hiddenCount" class="tc-badge">{{ toFaDigits(hiddenCount) }} مخفی</em>
</button>
<div v-if="open" class="tc-menu" :class="align" @click.stop>
<p class="tc-head">نمایش ستون‌ها</p>
<p v-if="!columns.length" class="tc-empty">ستونی برای نمایش وجود ندارد.</p>
<label v-for="col in columns" :key="col" class="tc-item">
<input type="checkbox" :checked="cols.isVisible(col)" @change="cols.toggle(col)" />
<span>{{ col }}</span>
</label>
<button v-if="hiddenCount" type="button" class="tc-reset" @click="cols.showAll()">
نمایش همه ستون‌ها
</button>
</div>
</div>
<a-popover v-model:open="open" trigger="click" :placement="placement">
<template #content>
<a-flex
vertical
:gap="8"
style="min-width: 13rem; max-width: calc(100vw - 1.5rem)"
>
<a-typography-text type="secondary" strong>
نمایش ستون‌ها
</a-typography-text>
<a-typography-text v-if="!columns.length" type="secondary">
ستونی برای نمایش وجود ندارد.
</a-typography-text>
<a-checkbox
v-for="col in columns"
:key="col"
:checked="cols.isVisible(col)"
@click.prevent="cols.toggle(col)"
>
{{ col }}
</a-checkbox>
<a-button v-if="hiddenCount" type="link" @click="cols.showAll()">
نمایش همه ستون‌ها
</a-button>
</a-flex>
</template>
<a-button :type="hiddenCount > 0 ? 'primary' : 'default'" :ghost="hiddenCount > 0">
<template #icon><SettingOutlined /></template>
{{ label }}
<template v-if="hiddenCount">
· {{ toFaDigits(hiddenCount) }} مخفی
</template>
</a-button>
</a-popover>
</template>
<style scoped>
.tc-pop {
position: relative;
}
.tc-tool {
min-height: var(--control-h, 2.4rem);
height: var(--control-h, 2.4rem);
border: 1px solid var(--line);
background: #fff;
border-radius: var(--radius);
padding: 0 0.75rem;
box-sizing: border-box;
display: inline-flex;
align-items: center;
gap: 0.35rem;
font: inherit;
font-size: var(--fs-sm);
color: var(--ink-secondary);
cursor: pointer;
white-space: nowrap;
}
.tc-tool:hover {
background: var(--surface-subtle);
border-color: var(--line-strong);
}
.tc-tool.active {
border-color: #5eead4;
color: #0f766e;
}
.tc-badge {
font-style: normal;
font-size: 0.72rem;
background: #f0fdfa;
color: #0f766e;
border-radius: 999px;
padding: 0.05rem 0.45rem;
}
.tc-menu {
position: absolute;
top: calc(100% + 6px);
inset-inline-start: 0;
z-index: 20;
background: #fff;
border-radius: var(--radius);
box-shadow: 0 14px 36px rgba(15, 23, 42, 0.14);
padding: 0.45rem;
min-width: 13rem;
max-width: calc(100vw - 1.5rem);
max-height: min(24rem, 70vh);
overflow-y: auto;
display: grid;
gap: 0.1rem;
}
.tc-menu.end {
inset-inline-start: auto;
inset-inline-end: 0;
}
.tc-head {
margin: 0 0 0.25rem;
padding: 0.25rem 0.5rem;
font-size: var(--fs-xs, 0.75rem);
color: var(--ink-muted, #64748b);
font-weight: 700;
}
.tc-empty {
margin: 0;
padding: 0.4rem 0.5rem;
font-size: var(--fs-sm);
color: var(--ink-muted, #64748b);
}
.tc-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.42rem 0.5rem;
border-radius: var(--radius);
cursor: pointer;
font-size: var(--fs-sm);
user-select: none;
}
.tc-item:hover {
background: #f0fdfa;
}
.tc-item input {
width: 1rem;
height: 1rem;
accent-color: #0d9488;
margin: 0;
}
.tc-reset {
margin-top: 0.3rem;
border: 0;
border-top: 1px solid var(--line);
background: none;
padding: 0.5rem;
font: inherit;
font-size: var(--fs-sm);
color: #0f766e;
cursor: pointer;
text-align: start;
}
@media (max-width: 640px) {
.tc-label {
display: none;
}
}
</style>