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
+109 -180
View File
@@ -1,6 +1,13 @@
<script setup lang="ts">
import { nextTick, ref } from 'vue';
import UiIcon from '@/components/UiIcon.vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import {
Button as AButton,
Card as ACard,
Flex as AFlex,
Input as AInput,
Space as ASpace,
} from 'ant-design-vue';
import PipelineColumn from './PipelineColumn.vue';
import type { PipelineDeal, PipelineStage, PipelineUiCopy } from '@/lib/pipeline-board';
import { DEAL_BOARD_COPY } from '@/lib/pipeline-board';
@@ -39,13 +46,40 @@ const emit = defineEmits<{
const adding = ref(false);
const newName = ref('');
const addInput = ref<HTMLInputElement | null>(null);
const addInput = ref<{ focus: () => void } | null>(null);
const shellStyle = {
boxSizing: 'border-box' as const,
flex: '1 1 auto',
height: 0,
minHeight: 0,
display: 'flex',
flexDirection: 'column' as const,
padding: 0,
minWidth: 0,
position: 'relative' as const,
overflow: 'hidden' as const,
};
const trackStyle = {
boxSizing: 'border-box' as const,
flex: '1 1 auto',
height: 0,
minHeight: 0,
display: 'flex',
flexDirection: 'row' as const,
alignItems: 'stretch' as const,
gap: '16px',
width: '100%',
overflowX: 'auto' as const,
overflowY: 'hidden' as const,
};
async function startAdd() {
adding.value = true;
newName.value = '';
await nextTick();
addInput.value?.focus();
addInput.value?.focus?.();
}
function cancelAdd() {
@@ -63,193 +97,88 @@ function submitAdd() {
</script>
<template>
<div class="board-shell" dir="rtl">
<div class="board">
<PipelineColumn
v-for="(stage, index) in stages"
:key="stage.id"
:stage="stage"
:dragging-id="draggingId"
:drop-active="dropStageId === stage.id"
:can-manage="canManage"
:copy="copy"
:select-mode="selectMode"
:selected-ids="selectedIds"
:is-first="index === 0"
:is-last="index === stages.length - 1"
@drag-start="(d, e) => emit('dragStart', d, e)"
@drag-end="emit('dragEnd')"
@drag-over="(id, e) => emit('dragOver', id, e)"
@drop="(id, e, index) => emit('drop', id, e, index)"
@add-deal="emit('addDeal', $event)"
@open-deal="emit('openDeal', $event)"
@menu-deal="emit('menuDeal', $event)"
@toggle-select="emit('toggleSelect', $event)"
@bulk-delete="emit('bulkDelete')"
@rename="(id, title) => emit('renameStage', id, title)"
@delete="emit('deleteStage', $event)"
@move="(id, dir) => emit('moveStage', id, dir)"
@column-drag-start="(id, e) => emit('columnDragStart', id, e)"
@column-drag-end="emit('columnDragEnd')"
@load-more="emit('loadMore', $event)"
/>
<div class="pipeline-board-shell" dir="rtl" :style="shellStyle">
<div class="pipeline-board-track" :style="trackStyle">
<PipelineColumn
v-for="(stage, index) in stages"
:key="stage.id"
:stage="stage"
:dragging-id="draggingId"
:drop-active="dropStageId === stage.id"
:can-manage="canManage"
:copy="copy"
:select-mode="selectMode"
:selected-ids="selectedIds"
:is-first="index === 0"
:is-last="index === stages.length - 1"
@drag-start="(d, e) => emit('dragStart', d, e)"
@drag-end="emit('dragEnd')"
@drag-over="(id, e) => emit('dragOver', id, e)"
@drop="(id, e, index) => emit('drop', id, e, index)"
@add-deal="emit('addDeal', $event)"
@open-deal="emit('openDeal', $event)"
@menu-deal="emit('menuDeal', $event)"
@toggle-select="emit('toggleSelect', $event)"
@bulk-delete="emit('bulkDelete')"
@rename="(id, title) => emit('renameStage', id, title)"
@delete="emit('deleteStage', $event)"
@move="(id, dir) => emit('moveStage', id, dir)"
@column-drag-start="(id, e) => emit('columnDragStart', id, e)"
@column-drag-end="emit('columnDragEnd')"
@load-more="emit('loadMore', $event)"
/>
<section v-if="canManage" class="add-col">
<form v-if="adding" class="add-form" @submit.prevent="submitAdd">
<input
ref="addInput"
v-model="newName"
maxlength="80"
placeholder="نام ستون جدید…"
aria-label="نام ستون جدید"
@keydown.esc.prevent="cancelAdd"
/>
<div class="add-actions">
<button class="ok" type="submit" :disabled="newName.trim().length < 1">
افزودن
</button>
<button class="cancel" type="button" @click="cancelAdd">انصراف</button>
</div>
</form>
<button v-else type="button" class="add-btn" @click="startAdd">
<UiIcon name="plus" :size="18" />
افزودن ستون
</button>
</section>
<div
v-if="canManage"
:style="{
flex: '0 0 280px',
width: '280px',
alignSelf: 'stretch',
display: 'flex',
alignItems: 'flex-start',
}"
>
<a-card v-if="adding" :style="{ width: '100%' }" :body-style="{ padding: '12px' }">
<a-flex vertical :gap="12">
<a-input
ref="addInput"
v-model:value="newName"
:maxlength="80"
placeholder="نام ستون جدید…"
aria-label="نام ستون جدید"
@pressEnter="submitAdd"
@keydown.esc.prevent="cancelAdd"
/>
<a-space>
<a-button
type="primary"
:disabled="newName.trim().length < 1"
@click="submitAdd"
>
افزودن
</a-button>
<a-button @click="cancelAdd">انصراف</a-button>
</a-space>
</a-flex>
</a-card>
<a-button v-else type="dashed" block :style="{ minHeight: '52px' }" @click="startAdd">
<template #icon><PlusOutlined /></template>
افزودن ستون
</a-button>
</div>
</div>
</div>
</template>
<style scoped>
.board-shell {
background: #fff;
border: 1px solid var(--line);
border-radius: var(--radius);
padding: var(--pad-modal);
min-width: 0;
}
.board {
display: flex;
flex-direction: row;
gap: 16px;
width: 100%;
min-width: 0;
max-width: 100%;
align-items: stretch;
overflow-x: auto;
overflow-y: hidden;
padding: 0.1rem 0.05rem 0.5rem;
touch-action: pan-x pan-y;
overscroll-behavior-x: contain;
-webkit-overflow-scrolling: touch;
.pipeline-board-track {
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--muted) 45%, transparent) transparent;
}
.board::-webkit-scrollbar {
.pipeline-board-track::-webkit-scrollbar {
height: 8px;
}
.board::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--muted) 38%, transparent);
.pipeline-board-track::-webkit-scrollbar-thumb {
border-radius: 999px;
}
.add-col {
flex: 0 0 17.5rem;
min-width: 17.5rem;
min-height: 420px;
display: flex;
align-items: flex-start;
padding-top: 0.15rem;
}
.add-btn {
width: 100%;
min-height: 3.2rem;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
border: 1.5px dashed var(--line);
border-radius: var(--radius);
background: #f8fafc;
color: var(--muted);
font: inherit;
font-weight: 700;
cursor: pointer;
}
.add-btn:hover {
border-color: var(--accent);
color: var(--accent-deep, #0f766e);
background: color-mix(in srgb, var(--accent) 6%, #fff);
}
.add-form {
width: 100%;
display: grid;
gap: 0.55rem;
padding: 0.75rem;
border: 1px solid var(--line);
border-radius: var(--radius);
background: #fff;
}
.add-form input {
width: 100%;
min-height: var(--control-h);
padding: 0 0.75rem;
border: 1px solid var(--line);
border-radius: var(--radius);
font: inherit;
}
.add-actions {
display: flex;
gap: 0.4rem;
}
.add-actions .ok,
.add-actions .cancel {
min-height: var(--control-h);
padding: 0 0.85rem;
border-radius: var(--radius);
font: inherit;
font-weight: 700;
cursor: pointer;
}
.add-actions .ok {
border: 0;
background: var(--accent);
color: #fff;
}
.add-actions .ok:disabled {
opacity: 0.5;
cursor: default;
}
.add-actions .cancel {
border: 1px solid var(--line);
background: #fff;
color: var(--ink);
}
@container pipeline (max-width: 720px) {
.board {
scroll-snap-type: x mandatory;
scroll-padding-inline: 0.25rem;
}
.add-col {
flex: 0 0 min(20rem, 86cqi);
width: min(20rem, 86cqi);
min-width: min(20rem, 86cqi);
min-height: 360px;
scroll-snap-align: start;
}
background: color-mix(in srgb, currentColor 28%, transparent);
}
</style>