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,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>
|
||||
|
||||
Reference in New Issue
Block a user