feat(web): continue Ant settings migration and tighten page chrome

Migrate remaining settings screens to Ant Design Vue, densify teams/roles UI, drop page-header subtitles project-wide, and polish notes cards plus contacts row actions.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-04 10:05:28 +03:30
co-authored by Cursor
parent b11affbdfd
commit cb3a488b21
46 changed files with 6024 additions and 7679 deletions
+101 -74
View File
@@ -790,12 +790,7 @@ onBeforeUnmount(() => {
<template>
<a-flex vertical :gap="16">
<a-flex vertical :gap="4">
<a-typography-title :level="3" :style="{ margin: 0 }">یادداشت‌ها</a-typography-title>
<a-typography-text type="secondary">
فکر کنید، یادداشت کنید، هیچ چیز را فراموش نکنید.
</a-typography-text>
</a-flex>
<a-typography-title :level="3" :style="{ margin: 0 }">یادداشت‌ها</a-typography-title>
<a-alert v-if="error" type="error" show-icon :message="error" closable @close="error = ''" />
@@ -896,7 +891,7 @@ onBeforeUnmount(() => {
</a-button>
</a-empty>
<a-row v-else :gutter="[12, 12]" align="top">
<a-row v-else :gutter="[12, 12]" align="stretch">
<a-col
v-for="note in notes"
:key="note.id"
@@ -919,17 +914,27 @@ onBeforeUnmount(() => {
display: 'flex',
flexDirection: 'column',
gap: `${token.marginXS}px`,
minHeight: viewMode === 'list' ? undefined : '88px',
width: '100%',
aspectRatio: viewMode === 'list' ? undefined : '1 / 1',
overflow: 'hidden',
boxSizing: 'border-box',
}"
@click="openEditor('default', note)"
@keydown.enter="openEditor('default', note)"
>
<a-flex justify="space-between" align="flex-start" :gap="token.marginXXS">
<a-flex
justify="space-between"
align="flex-start"
:gap="token.marginXXS"
:style="{ flexShrink: 0 }"
>
<a-typography-text
v-if="note.title"
strong
:ellipsis="viewMode === 'grid'"
:style="{
flex: 1,
minWidth: 0,
fontSize: `${token.fontSizeLG}px`,
lineHeight: token.lineHeight,
wordBreak: 'break-word',
@@ -971,72 +976,89 @@ onBeforeUnmount(() => {
</a-space>
</a-flex>
<a-flex
v-if="note.checklist?.length"
vertical
:gap="token.marginXXS"
@click.stop
>
<a-checkbox
v-for="item in note.checklist.slice(0, 6)"
:key="item.id"
:checked="item.done"
@change="toggleCardChecklist(note, item.id)"
>
<a-typography-text
:delete="item.done"
:type="item.done ? 'secondary' : undefined"
>
{{ item.text }}
</a-typography-text>
</a-checkbox>
<a-typography-text
v-if="note.checklist.length > 6"
type="secondary"
>
+{{ toFaDigits(note.checklist.length - 6) }} مورد دیگر
</a-typography-text>
</a-flex>
<a-typography-paragraph
v-else-if="note.body"
:ellipsis="{ rows: 6 }"
:style="{
marginBottom: 0,
color: token.colorText,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}"
>
{{ note.body }}
</a-typography-paragraph>
<img
v-if="firstImage(note) && blobCache[blobKey(note.id, firstImage(note)!.id)]"
:src="blobCache[blobKey(note.id, firstImage(note)!.id)]"
:alt="firstImage(note)!.fileName"
:style="{
width: '100%',
maxHeight: '140px',
objectFit: 'cover',
borderRadius: `${token.borderRadius}px`,
}"
/>
<a-tag v-if="firstPdf(note)" :bordered="false">
<FileOutlined />
{{ firstPdf(note)!.fileName }}
</a-tag>
<div
v-if="firstAudio(note) && blobCache[blobKey(note.id, firstAudio(note)!.id)]"
@click.stop
:style="{
flex: 1,
minHeight: 0,
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
gap: `${token.marginXXS}px`,
}"
>
<audio
controls
preload="metadata"
:src="blobCache[blobKey(note.id, firstAudio(note)!.id)]"
:style="{ width: '100%', height: '32px' }"
<a-flex
v-if="note.checklist?.length"
vertical
:gap="token.marginXXS"
:style="{ overflow: 'hidden' }"
@click.stop
>
<a-checkbox
v-for="item in note.checklist.slice(0, viewMode === 'list' ? 6 : 4)"
:key="item.id"
:checked="item.done"
@change="toggleCardChecklist(note, item.id)"
>
<a-typography-text
:delete="item.done"
:type="item.done ? 'secondary' : undefined"
:ellipsis="viewMode === 'grid'"
>
{{ item.text }}
</a-typography-text>
</a-checkbox>
<a-typography-text
v-if="note.checklist.length > (viewMode === 'list' ? 6 : 4)"
type="secondary"
>
+{{
toFaDigits(note.checklist.length - (viewMode === 'list' ? 6 : 4))
}}
مورد دیگر
</a-typography-text>
</a-flex>
<a-typography-paragraph
v-else-if="note.body"
:ellipsis="{ rows: viewMode === 'list' ? 6 : 4 }"
:style="{
marginBottom: 0,
color: token.colorText,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}"
>
{{ note.body }}
</a-typography-paragraph>
<img
v-if="firstImage(note) && blobCache[blobKey(note.id, firstImage(note)!.id)]"
:src="blobCache[blobKey(note.id, firstImage(note)!.id)]"
:alt="firstImage(note)!.fileName"
:style="{
width: '100%',
maxHeight: viewMode === 'list' ? '140px' : '72px',
objectFit: 'cover',
borderRadius: `${token.borderRadius}px`,
flexShrink: 0,
}"
/>
<a-tag v-if="firstPdf(note)" :bordered="false">
<FileOutlined />
{{ firstPdf(note)!.fileName }}
</a-tag>
<div
v-if="firstAudio(note) && blobCache[blobKey(note.id, firstAudio(note)!.id)]"
@click.stop
>
<audio
controls
preload="metadata"
:src="blobCache[blobKey(note.id, firstAudio(note)!.id)]"
:style="{ width: '100%', height: '32px' }"
/>
</div>
</div>
<a-flex
@@ -1048,6 +1070,7 @@ onBeforeUnmount(() => {
"
wrap="wrap"
:gap="token.marginXXS"
:style="{ flexShrink: 0, overflow: 'hidden', maxHeight: '48px' }"
>
<a-tag
v-for="tag in note.tags ?? []"
@@ -1084,7 +1107,8 @@ onBeforeUnmount(() => {
<a-typography-text
type="secondary"
:style="{ fontSize: `${token.fontSizeSM}px` }"
:ellipsis="true"
:style="{ fontSize: `${token.fontSizeSM}px`, flexShrink: 0 }"
>
{{ formatNoteTime(note.updatedAt) }}
<template v-if="note.person"> · {{ note.person.fullName }}</template>
@@ -1107,13 +1131,16 @@ onBeforeUnmount(() => {
border: `1px dashed ${token.colorBorder}`,
padding: `${token.padding}px`,
cursor: 'pointer',
minHeight: '88px',
width: '100%',
aspectRatio: viewMode === 'list' ? undefined : '1 / 1',
minHeight: viewMode === 'list' ? '88px' : undefined,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: `${token.marginXS}px`,
background: token.colorBgContainer,
boxSizing: 'border-box',
}"
@click="openEditor()"
@keydown.enter="openEditor()"