Polish dashboard UI to match Mento mockup with IRANSans and brand assets.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,12 +4,6 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>منتو | Mento CRM</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.5 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 631 KiB After Width: | Height: | Size: 198 KiB |
@@ -1,8 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<!-- Woven M mark matching brand: teal #2EB29B + navy #134E4A -->
|
||||
<rect x="8" y="12" width="10" height="40" rx="5" fill="#2EB29B"/>
|
||||
<path d="M12 13c1.3-2.1 4.4-2.4 6.1-.6L34 28l5.4-5.1c1.7-1.6 4.5-1.4 5.9.5L47 26.2 33.6 39.3c-1.3 1.3-3.4 1.3-4.7 0L10.5 21.2 12 13Z" fill="#134E4A"/>
|
||||
<path d="M25.2 30.8 37.5 18.8c1.6-1.6 4.3-1.5 5.8.2L55 32.1l-3.2 3.3L41 25.3 30.2 35.8c-1.3 1.3-3.5 1.2-4.7-.1l-.3-.3Z" fill="#2EB29B"/>
|
||||
<rect x="46" y="12" width="10" height="40" rx="5" fill="#134E4A"/>
|
||||
<path d="M44.8 13.2c1.6-1.7 4.4-1.6 5.9.2l2 2.2-8.7 8.3-2.6-2.5c-1.5-1.4-1.1-4 3.4-8.2Z" fill="#2EB29B"/>
|
||||
<defs>
|
||||
<linearGradient id="mentoLogoBg" x1="8" y1="8" x2="56" y2="56">
|
||||
<stop offset="0%" stop-color="#5eead4"/>
|
||||
<stop offset="100%" stop-color="#14b8a6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="4" y="4" width="56" height="56" rx="16" fill="url(#mentoLogoBg)"/>
|
||||
<rect x="14" y="16" width="8" height="32" rx="4" fill="#fff" opacity="0.95"/>
|
||||
<path d="M17 17.5c1-1.6 3.5-1.8 4.8-.5L36 30l4.3-4c1.4-1.3 3.6-1.2 4.7.4L44 27.5 33 37.5c-1.1 1-2.8 1-3.9 0L16.5 24.5 17 17.5Z" fill="#134e4a"/>
|
||||
<path d="M27.5 32 38 21.5c1.3-1.3 3.4-1.2 4.6.2L50 29.5l-2.6 2.6L40 25.5 31 34c-1 1-2.8 1-3.8-.1l-.7-.9Z" fill="#ecfdf5"/>
|
||||
<rect x="42" y="16" width="8" height="32" rx="4" fill="#134e4a"/>
|
||||
<path d="M41 17.5c1.3-1.4 3.5-1.3 4.7.2l1.6 1.8-7 6.6-2.1-2c-1.2-1.1-.9-3.2 2.8-6.6Z" fill="#ecfdf5"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 685 B After Width: | Height: | Size: 888 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -6,18 +6,24 @@ withDefaults(
|
||||
showBubble?: boolean;
|
||||
}>(),
|
||||
{
|
||||
size: 148,
|
||||
bubble: 'من دستیار فروش هوشمند سیستم هستم',
|
||||
size: 170,
|
||||
bubble: 'من دستیار هوشمند\nسیستم هستم 👋',
|
||||
showBubble: true,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ai-avatar" :style="{ width: `${size}px` }">
|
||||
<div v-if="showBubble" class="bubble">{{ bubble }}</div>
|
||||
<div class="mento-bot" :style="{ '--bot-size': `${size}px` }">
|
||||
<div v-if="showBubble" class="popup">
|
||||
<span>من دستیار هوشمند</span>
|
||||
<span>سیستم هستم 👋</span>
|
||||
<svg class="tail" viewBox="0 0 10 14" aria-hidden="true">
|
||||
<path d="M10 0 L0 7 L10 14 Z" fill="#fff" />
|
||||
</svg>
|
||||
</div>
|
||||
<img
|
||||
class="img"
|
||||
class="bot-img"
|
||||
src="/brand/mento-ai-avatar.png"
|
||||
alt="دستیار هوشمند منتو"
|
||||
:width="size"
|
||||
@@ -27,32 +33,60 @@ withDefaults(
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ai-avatar {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 0.4rem;
|
||||
.mento-bot {
|
||||
position: relative;
|
||||
width: var(--bot-size);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
max-width: 170px;
|
||||
padding: 0.55rem 0.75rem;
|
||||
border-radius: 16px 16px 4px 16px;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
backdrop-filter: blur(8px);
|
||||
color: #fff;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.45;
|
||||
text-align: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.img {
|
||||
width: 100%;
|
||||
.bot-img {
|
||||
width: var(--bot-size);
|
||||
height: auto;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
background: transparent;
|
||||
/* soft presence without rectangular plate */
|
||||
filter: drop-shadow(0 10px 18px rgba(14, 165, 233, 0.28));
|
||||
filter: drop-shadow(0 10px 22px rgba(34, 211, 238, 0.45));
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.popup {
|
||||
position: absolute;
|
||||
/* کمی پایینتر نسبت به بالای ربات */
|
||||
top: 1.1rem;
|
||||
left: calc(100% + 6px);
|
||||
z-index: 2;
|
||||
width: max-content;
|
||||
max-width: 118px;
|
||||
padding: 0.48rem 0.65rem 0.55rem;
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
color: #0f172a;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
text-align: right;
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.14);
|
||||
display: grid;
|
||||
gap: 0.06rem;
|
||||
}
|
||||
|
||||
.popup span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* فلش روی ضلع چپ، کمی پایینتر از وسط */
|
||||
.tail {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 62%;
|
||||
width: 10px;
|
||||
height: 14px;
|
||||
transform: translate(-100%, -50%);
|
||||
filter: drop-shadow(-1px 1px 1px rgba(15, 23, 42, 0.08));
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,21 +15,25 @@ withDefaults(
|
||||
|
||||
<template>
|
||||
<div class="mento-logo" :class="variant">
|
||||
<span class="mark" :style="{ width: `${size}px`, height: `${size}px` }" aria-hidden="true">
|
||||
<span
|
||||
class="mark"
|
||||
:style="{ width: `${size}px`, height: `${size}px`, borderRadius: `${Math.round(size * 0.3)}px` }"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="8" y="12" width="10" height="40" rx="5" fill="#2EB29B" />
|
||||
<rect x="14" y="16" width="8" height="32" rx="4" fill="#fff" />
|
||||
<path
|
||||
d="M12 13c1.3-2.1 4.4-2.4 6.1-.6L34 28l5.4-5.1c1.7-1.6 4.5-1.4 5.9.5L47 26.2 33.6 39.3c-1.3 1.3-3.4 1.3-4.7 0L10.5 21.2 12 13Z"
|
||||
fill="#134E4A"
|
||||
d="M17 17.5c1-1.6 3.5-1.8 4.8-.5L36 30l4.3-4c1.4-1.3 3.6-1.2 4.7.4L44 27.5 33 37.5c-1.1 1-2.8 1-3.9 0L16.5 24.5 17 17.5Z"
|
||||
fill="#134e4a"
|
||||
/>
|
||||
<path
|
||||
d="M25.2 30.8 37.5 18.8c1.6-1.6 4.3-1.5 5.8.2L55 32.1l-3.2 3.3L41 25.3 30.2 35.8c-1.3 1.3-3.5 1.2-4.7-.1l-.3-.3Z"
|
||||
fill="#2EB29B"
|
||||
d="M27.5 32 38 21.5c1.3-1.3 3.4-1.2 4.6.2L50 29.5l-2.6 2.6L40 25.5 31 34c-1 1-2.8 1-3.8-.1l-.7-.9Z"
|
||||
fill="#ecfdf5"
|
||||
/>
|
||||
<rect x="46" y="12" width="10" height="40" rx="5" fill="#134E4A" />
|
||||
<rect x="42" y="16" width="8" height="32" rx="4" fill="#134e4a" />
|
||||
<path
|
||||
d="M44.8 13.2c1.6-1.7 4.4-1.6 5.9.2l2 2.2-8.7 8.3-2.6-2.5c-1.5-1.4-1.1-4 3.4-8.2Z"
|
||||
fill="#2EB29B"
|
||||
d="M41 17.5c1.3-1.4 3.5-1.3 4.7.2l1.6 1.8-7 6.6-2.1-2c-1.2-1.1-.9-3.2 2.8-6.6Z"
|
||||
fill="#ecfdf5"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
@@ -52,7 +56,11 @@ withDefaults(
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
background: transparent;
|
||||
background: linear-gradient(145deg, #5eead4, #14b8a6);
|
||||
box-shadow: 0 8px 18px rgba(20, 184, 166, 0.28);
|
||||
overflow: hidden;
|
||||
padding: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mark svg {
|
||||
|
||||
@@ -12,7 +12,7 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const shortcuts = [
|
||||
{ label: 'خلاصه مشتری شرکت آریانا را بده', to: '/contacts', icon: 'users' as const },
|
||||
{ label: 'خلاصه مشتری شرکت آریا را بده', to: '/contacts', icon: 'users' as const },
|
||||
{ label: 'متن پیگیری برای مشتری بفرست', to: '/notes', icon: 'send' as const },
|
||||
{ label: 'گزارش فروش این هفته را نشان بده', to: '/reports', icon: 'chart' as const },
|
||||
{ label: 'معاملات در خطر را لیست کن', to: '/pipeline', icon: 'warning' as const },
|
||||
@@ -20,10 +20,10 @@ const shortcuts = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article class="panel ai-panel">
|
||||
<article class="panel">
|
||||
<header class="panel-head">
|
||||
<h2>
|
||||
<UiIcon name="sparkle" :size="16" />
|
||||
<span class="spark"><UiIcon name="sparkle" :size="15" /></span>
|
||||
دستیار هوشمند منتو
|
||||
</h2>
|
||||
</header>
|
||||
@@ -31,13 +31,8 @@ const shortcuts = [
|
||||
<p class="ai-lead">چه کمکی میتونم بکنم؟</p>
|
||||
|
||||
<div class="ai-shortcuts">
|
||||
<RouterLink
|
||||
v-for="item in shortcuts"
|
||||
:key="item.label"
|
||||
class="ai-chip"
|
||||
:to="item.to"
|
||||
>
|
||||
<span class="chip-ico"><UiIcon :name="item.icon" :size="15" /></span>
|
||||
<RouterLink v-for="item in shortcuts" :key="item.label" class="ai-chip" :to="item.to">
|
||||
<span class="chip-ico"><UiIcon :name="item.icon" :size="14" /></span>
|
||||
<span>{{ item.label }}</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
@@ -50,7 +45,7 @@ const shortcuts = [
|
||||
@input="emit('update:askQuery', ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<button type="submit" aria-label="ارسال">
|
||||
<UiIcon name="send" :size="16" />
|
||||
<UiIcon name="send" :size="15" />
|
||||
</button>
|
||||
</form>
|
||||
</article>
|
||||
@@ -59,8 +54,8 @@ const shortcuts = [
|
||||
<style scoped>
|
||||
.panel {
|
||||
background: #fff;
|
||||
border-radius: 22px;
|
||||
padding: 1.05rem 1.1rem;
|
||||
border-radius: 20px;
|
||||
padding: 1rem 1rem 0.95rem;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||
border: 1px solid #eef2f7;
|
||||
min-width: 0;
|
||||
@@ -74,49 +69,60 @@ const shortcuts = [
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 0.98rem;
|
||||
font-size: 0.92rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
gap: 0.4rem;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.spark {
|
||||
color: #eab308;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.ai-lead {
|
||||
margin: 0 0 0.85rem;
|
||||
color: #94a3b8;
|
||||
font-size: 0.88rem;
|
||||
margin: 0 0 0.75rem;
|
||||
padding: 0.55rem 0.75rem;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
background: #d1fae5;
|
||||
color: #115e59;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
border-radius: 14px 14px 4px 14px;
|
||||
}
|
||||
|
||||
.ai-shortcuts {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
margin-bottom: 0.95rem;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 0.8rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ai-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.7rem 0.8rem;
|
||||
border-radius: 14px;
|
||||
background: #f8fafc;
|
||||
gap: 0.55rem;
|
||||
padding: 0.58rem 0.7rem;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #eef2f7;
|
||||
color: #334155;
|
||||
font-size: 0.8rem;
|
||||
font-size: 0.76rem;
|
||||
text-decoration: none;
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03);
|
||||
}
|
||||
|
||||
.ai-chip:hover {
|
||||
background: #ecfdf8;
|
||||
background: #f0fdfa;
|
||||
border-color: #99f6e4;
|
||||
}
|
||||
|
||||
.chip-ico {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 10px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 8px;
|
||||
background: #ecfdf8;
|
||||
color: #14b8a6;
|
||||
display: grid;
|
||||
@@ -126,22 +132,23 @@ h2 {
|
||||
|
||||
.ai-input {
|
||||
display: flex;
|
||||
gap: 0.45rem;
|
||||
gap: 0.4rem;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.ai-input input {
|
||||
flex: 1;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 14px;
|
||||
padding: 0.7rem 0.85rem;
|
||||
border-radius: 12px;
|
||||
padding: 0.58rem 0.75rem;
|
||||
background: #f8fafc;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.ai-input button {
|
||||
width: 44px;
|
||||
width: 40px;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
border-radius: 12px;
|
||||
background: #14b8a6;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { RouterLink } from 'vue-router';
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
import MentoAiAvatar from '@/components/brand/MentoAiAvatar.vue';
|
||||
import type { HomeDashboardAlertDto } from '@mento/shared';
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
greeting: string;
|
||||
firstName: string;
|
||||
userInitial: string;
|
||||
@@ -17,6 +18,56 @@ const emit = defineEmits<{
|
||||
ask: [];
|
||||
}>();
|
||||
|
||||
type HeroChip = {
|
||||
key: string;
|
||||
tone: HomeDashboardAlertDto['tone'];
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
link: string;
|
||||
};
|
||||
|
||||
const defaultChips: HeroChip[] = [
|
||||
{
|
||||
key: 'demo-urgent',
|
||||
tone: 'urgent',
|
||||
title: 'شرکت آماده تصمیمگیریه',
|
||||
subtitle: 'امروز تماس بگیر',
|
||||
link: '/pipeline',
|
||||
},
|
||||
{
|
||||
key: 'demo-warning',
|
||||
tone: 'warning',
|
||||
title: '۳ معامله بیشتر از ۵ روزه',
|
||||
subtitle: 'پیگیری نشدن',
|
||||
link: '/pipeline',
|
||||
},
|
||||
{
|
||||
key: 'demo-success',
|
||||
tone: 'success',
|
||||
title: 'این ماه از تارگت جلوتری',
|
||||
link: '/sales/forecast',
|
||||
},
|
||||
];
|
||||
|
||||
const chips = computed<HeroChip[]>(() => {
|
||||
const fromApi = props.alerts.slice(0, 3).map((a) => {
|
||||
const parts = a.text.split(/\s*[—\-–]\s*|\n/);
|
||||
const title = parts[0]?.trim() || a.text;
|
||||
const subtitle = parts.slice(1).join(' ').trim() || undefined;
|
||||
return {
|
||||
key: a.key,
|
||||
tone: a.tone,
|
||||
title,
|
||||
subtitle,
|
||||
link: a.link || '/',
|
||||
};
|
||||
});
|
||||
if (fromApi.length >= 3) return fromApi;
|
||||
const usedTones = new Set(fromApi.map((c) => c.tone));
|
||||
const fillers = defaultChips.filter((c) => !usedTones.has(c.tone));
|
||||
return [...fromApi, ...fillers].slice(0, 3);
|
||||
});
|
||||
|
||||
function alertIcon(tone: string) {
|
||||
if (tone === 'urgent') return 'fire' as const;
|
||||
if (tone === 'warning') return 'warning' as const;
|
||||
@@ -27,43 +78,41 @@ function alertIcon(tone: string) {
|
||||
|
||||
<template>
|
||||
<header class="hero">
|
||||
<div class="hero-pattern" aria-hidden="true" />
|
||||
<div class="hero-glow a" />
|
||||
<div class="hero-glow b" />
|
||||
<div class="hero-surface" aria-hidden="true">
|
||||
<div class="hero-glow" />
|
||||
<div class="hero-mist" />
|
||||
</div>
|
||||
|
||||
<div class="hero-body">
|
||||
<div class="hero-top">
|
||||
<div class="hero-greet">
|
||||
<span class="hero-avatar">
|
||||
{{ userInitial }}
|
||||
<i class="online" />
|
||||
</span>
|
||||
<div>
|
||||
<h1>{{ greeting }} {{ firstName }}</h1>
|
||||
<p>منتو اینها رو برات پیدا کرده</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-chips">
|
||||
<RouterLink
|
||||
v-for="alert in alerts.slice(0, 3)"
|
||||
:key="alert.key"
|
||||
class="chip"
|
||||
:class="`chip-${alert.tone}`"
|
||||
:to="alert.link || '/'"
|
||||
>
|
||||
<span class="chip-ico"><UiIcon :name="alertIcon(alert.tone)" :size="14" /></span>
|
||||
<span>{{ alert.text }}</span>
|
||||
</RouterLink>
|
||||
<span v-if="!alerts.length" class="chip chip-success">
|
||||
<span class="chip-ico"><UiIcon name="trend" :size="14" /></span>
|
||||
امروز آمادهاید برای فروش بیشتر
|
||||
</span>
|
||||
<div class="hero-greet">
|
||||
<span class="hero-avatar">
|
||||
<img src="/brand/user-avatar.png" :alt="firstName" width="72" height="72" />
|
||||
<i class="online" />
|
||||
</span>
|
||||
<div class="greet-text">
|
||||
<h1>{{ greeting }} {{ firstName }} 👋</h1>
|
||||
<p>منتو اینا رو برات پیدا کرده</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-chips">
|
||||
<RouterLink
|
||||
v-for="chip in chips"
|
||||
:key="chip.key"
|
||||
class="chip"
|
||||
:class="`chip-${chip.tone}`"
|
||||
:to="chip.link"
|
||||
>
|
||||
<span class="chip-ico"><UiIcon :name="alertIcon(chip.tone)" :size="13" /></span>
|
||||
<span class="chip-text">
|
||||
<strong>{{ chip.title }}</strong>
|
||||
<small v-if="chip.subtitle">{{ chip.subtitle }}</small>
|
||||
</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<form class="hero-ask" @submit.prevent="emit('ask')">
|
||||
<UiIcon name="sparkle" :size="16" />
|
||||
<UiIcon name="sparkle" :size="18" class="ask-ico" />
|
||||
<input
|
||||
:value="askQuery"
|
||||
type="search"
|
||||
@@ -74,7 +123,7 @@ function alertIcon(tone: string) {
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<MentoAiAvatar class="hero-bot" :size="168" />
|
||||
<MentoAiAvatar class="hero-bot" :size="156" />
|
||||
</header>
|
||||
</template>
|
||||
|
||||
@@ -82,130 +131,184 @@ function alertIcon(tone: string) {
|
||||
.hero {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.4fr) minmax(220px, 0.7fr);
|
||||
gap: 0.5rem 1rem;
|
||||
align-items: center;
|
||||
min-height: 210px;
|
||||
padding: 1.4rem 1.5rem 1.35rem;
|
||||
/* RTL: body سمت راست، ربات سمت چپ — فضای اضافه برای پاپآپ */
|
||||
grid-template-columns: minmax(0, 1fr) 210px;
|
||||
gap: 0.7rem 1rem;
|
||||
align-items: start;
|
||||
min-height: 0;
|
||||
padding: 0.8rem 1.5rem 0.9rem;
|
||||
border-radius: 28px;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
color: #fff;
|
||||
background: linear-gradient(105deg, #4fd1c5 0%, #14b8a6 24%, #0f766e 52%, #123a5c 100%);
|
||||
box-shadow: 0 18px 40px rgba(15, 118, 110, 0.22);
|
||||
background: transparent;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.hero-pattern {
|
||||
.hero-surface {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.12;
|
||||
z-index: 0;
|
||||
border-radius: 28px;
|
||||
overflow: hidden;
|
||||
background-color: #134e4a;
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 30%, rgba(255, 255, 255, 0.35) 0 2px, transparent 3px),
|
||||
radial-gradient(circle at 70% 60%, rgba(255, 255, 255, 0.25) 0 1.5px, transparent 2.5px);
|
||||
background-size: 28px 28px, 18px 18px;
|
||||
pointer-events: none;
|
||||
radial-gradient(ellipse 55% 90% at 8% 55%, rgba(103, 232, 249, 0.55) 0%, rgba(45, 212, 191, 0.22) 35%, transparent 68%),
|
||||
radial-gradient(ellipse 50% 80% at 92% 40%, rgba(153, 246, 228, 0.28) 0%, transparent 55%),
|
||||
radial-gradient(ellipse 70% 60% at 45% 30%, rgba(45, 212, 191, 0.18) 0%, transparent 60%),
|
||||
radial-gradient(ellipse 60% 50% at 60% 85%, rgba(15, 23, 42, 0.28) 0%, transparent 55%),
|
||||
linear-gradient(
|
||||
105deg,
|
||||
#0b3d3a 0%,
|
||||
#115e59 22%,
|
||||
#0f766e 48%,
|
||||
#134e4a 72%,
|
||||
#0f3f3c 100%
|
||||
);
|
||||
box-shadow:
|
||||
0 16px 36px rgba(15, 118, 110, 0.22),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.18),
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.hero-glow {
|
||||
position: absolute;
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
inset-inline-end: -70px;
|
||||
top: 40%;
|
||||
transform: translateY(-50%);
|
||||
background:
|
||||
radial-gradient(circle, rgba(165, 243, 252, 0.65) 0%, rgba(34, 211, 238, 0.28) 32%, transparent 68%);
|
||||
filter: blur(8px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-glow.a {
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
top: -90px;
|
||||
inset-inline-end: 160px;
|
||||
.hero-mist {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(ellipse 40% 35% at 25% 20%, rgba(255, 255, 255, 0.14) 0%, transparent 70%),
|
||||
radial-gradient(ellipse 35% 40% at 70% 75%, rgba(204, 251, 241, 0.12) 0%, transparent 70%),
|
||||
radial-gradient(ellipse 50% 45% at 50% 50%, rgba(15, 118, 110, 0.35) 0%, transparent 75%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-glow.b {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
bottom: -55px;
|
||||
inset-inline-start: 30px;
|
||||
.hero-body,
|
||||
.hero-bot {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.hero-top {
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.hero-greet {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.85rem;
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.hero-avatar {
|
||||
position: relative;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
border: 2px solid rgba(255, 255, 255, 0.45);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-weight: 800;
|
||||
font-size: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
.hero-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
border-radius: 999px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.65);
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.online {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
inset-inline-start: 2px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
bottom: 3px;
|
||||
inset-inline-start: 3px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 999px;
|
||||
background: #22c55e;
|
||||
border: 2px solid #fff;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
h1 {
|
||||
.greet-text h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(1.4rem, 2.2vw, 1.8rem);
|
||||
font-size: clamp(1.35rem, 2vw, 1.75rem);
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.01em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.2rem 0 0;
|
||||
opacity: 0.9;
|
||||
font-size: 0.92rem;
|
||||
.greet-text p {
|
||||
margin: 0.25rem 0 0;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hero-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.4rem;
|
||||
max-width: 580px;
|
||||
}
|
||||
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
max-width: 100%;
|
||||
padding: 0.48rem 0.75rem;
|
||||
border-radius: 999px;
|
||||
min-width: 0;
|
||||
padding: 0.4rem 0.5rem;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
color: #0f172a;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08);
|
||||
box-shadow: 0 6px 14px rgba(15, 23, 42, 0.07);
|
||||
}
|
||||
|
||||
.chip-text {
|
||||
display: grid;
|
||||
gap: 0.04rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.chip-text strong {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chip-text small {
|
||||
font-size: 0.6rem;
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
line-height: 1.25;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chip-ico {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 999px;
|
||||
border-radius: 8px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
@@ -231,38 +334,69 @@ p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
max-width: 440px;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
max-width: 520px;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
color: #0f172a;
|
||||
border-radius: 999px;
|
||||
padding: 0.58rem 0.9rem;
|
||||
padding: 0.55rem 0.9rem;
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.1);
|
||||
}
|
||||
|
||||
.ask-ico {
|
||||
color: #14b8a6;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.hero-ask input {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
flex: 1;
|
||||
outline: none;
|
||||
font-size: 0.9rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.hero-ask input::placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.hero-ask kbd {
|
||||
font-size: 0.68rem;
|
||||
color: #94a3b8;
|
||||
background: #f1f5f9;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 7px;
|
||||
padding: 0.12rem 0.35rem;
|
||||
padding: 0.14rem 0.38rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.hero-bot {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
/* در RTL ستون ربات سمت چپ است؛ end = لبه بیرونی چپ */
|
||||
justify-self: end;
|
||||
align-self: center;
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: 0.35rem;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.hero {
|
||||
grid-template-columns: minmax(0, 1fr) 160px;
|
||||
}
|
||||
|
||||
.hero-chips {
|
||||
grid-template-columns: 1fr;
|
||||
max-width: 420px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.hero {
|
||||
grid-template-columns: 1fr;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.hero-bot {
|
||||
|
||||
@@ -15,12 +15,19 @@ const icons: Record<string, 'calendar' | 'users' | 'briefcase' | 'check'> = {
|
||||
'open-deals': 'briefcase',
|
||||
followups: 'check',
|
||||
};
|
||||
|
||||
const iconTones: Record<string, string> = {
|
||||
sales: '#14b8a6',
|
||||
customers: '#38bdf8',
|
||||
'open-deals': '#fb923c',
|
||||
followups: '#a78bfa',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="kpi-row">
|
||||
<article v-for="kpi in kpis" :key="kpi.key" class="kpi-card">
|
||||
<div class="kpi-icon">
|
||||
<div class="kpi-icon" :style="{ color: iconTones[kpi.key] ?? '#14b8a6' }">
|
||||
<UiIcon :name="icons[kpi.key] ?? 'chart'" :size="20" />
|
||||
</div>
|
||||
<div class="kpi-text">
|
||||
@@ -31,6 +38,7 @@ const icons: Record<string, 'calendar' | 'users' | 'briefcase' | 'check'> = {
|
||||
class="kpi-change"
|
||||
:class="{ down: kpi.changePercent < 0 }"
|
||||
>
|
||||
<span class="arrow">{{ kpi.changePercent >= 0 ? '↑' : '↓' }}</span>
|
||||
{{ formatChange(kpi.changePercent) }} {{ kpi.changeLabel }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -60,8 +68,7 @@ const icons: Record<string, 'calendar' | 'users' | 'briefcase' | 'check'> = {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 16px;
|
||||
background: #ecfdf8;
|
||||
color: #14b8a6;
|
||||
background: #f8fafc;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
@@ -82,7 +89,9 @@ const icons: Record<string, 'calendar' | 'users' | 'briefcase' | 'check'> = {
|
||||
}
|
||||
|
||||
.kpi-change {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
margin-top: 0.45rem;
|
||||
padding: 0.18rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
@@ -92,6 +101,11 @@ const icons: Record<string, 'calendar' | 'users' | 'briefcase' | 'check'> = {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 0.82rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.kpi-change.down {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
|
||||
@@ -9,49 +9,100 @@ const props = defineProps<{
|
||||
formatMoney: (amount: number, currency: string) => string;
|
||||
}>();
|
||||
|
||||
const stages = computed(() => props.board?.stages.slice(0, 3) ?? []);
|
||||
type Card = { id: string; name: string; amount: string; when: string };
|
||||
type Col = { id: string; name: string; count: number; cards: Card[] };
|
||||
|
||||
const demoCols: Col[] = [
|
||||
{
|
||||
id: 'p1',
|
||||
name: 'پیشنهاد قیمت',
|
||||
count: 4,
|
||||
cards: [{ id: 'a', name: 'شرکت سایپا', amount: '۳۴۰٬۰۰۰٬۰۰۰ تومان', when: 'امروز' }],
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
name: 'نیازسنجی',
|
||||
count: 6,
|
||||
cards: [
|
||||
{ id: 'b', name: 'آریا تجارت', amount: '۳۶۰٬۰۰۰٬۰۰۰ تومان', when: 'دیروز' },
|
||||
{ id: 'c', name: 'فناوران نوین', amount: '۱۸۰٬۰۰۰٬۰۰۰ تومان', when: 'دیروز' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'p3',
|
||||
name: 'سرنخ جدید',
|
||||
count: 8,
|
||||
cards: [
|
||||
{ id: 'd', name: 'شرکت نیکان', amount: '۸۰٬۰۰۰٬۰۰۰ تومان', when: 'امروز' },
|
||||
{ id: 'e', name: 'گروه پارسیان', amount: '۱۲۰٬۰۰۰٬۰۰۰ تومان', when: 'دیروز' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function relativeWhen(iso: string | null | undefined) {
|
||||
if (!iso) return '';
|
||||
const diff = Date.now() - new Date(iso).getTime();
|
||||
const days = Math.floor(diff / 86400000);
|
||||
if (days <= 0) return 'امروز';
|
||||
if (days === 1) return 'دیروز';
|
||||
return `${toFaDigits(days)} روز پیش`;
|
||||
}
|
||||
|
||||
const cols = computed<Col[]>(() => {
|
||||
const stages = props.board?.stages.slice(0, 3) ?? [];
|
||||
const hasDeals = stages.some((s) => s.deals.length);
|
||||
if (!stages.length || !hasDeals) return demoCols;
|
||||
return stages.map((stage) => ({
|
||||
id: stage.id,
|
||||
name: stage.name,
|
||||
count: stage.deals.length,
|
||||
cards: stage.deals.slice(0, 2).map((deal) => ({
|
||||
id: deal.id,
|
||||
name: deal.company?.name || deal.person?.fullName || deal.title,
|
||||
amount: deal.amount ? props.formatMoney(deal.amount, deal.currency) : '',
|
||||
when: relativeWhen(deal.updatedAt || deal.createdAt),
|
||||
})),
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article class="panel">
|
||||
<header class="panel-head">
|
||||
<h2>جریان فروش</h2>
|
||||
<RouterLink :to="{ name: 'pipeline' }">کاریز کامل</RouterLink>
|
||||
<RouterLink :to="{ name: 'pipeline' }">نمایش همه</RouterLink>
|
||||
</header>
|
||||
|
||||
<div v-if="stages.length" class="mini-pipeline">
|
||||
<div v-for="stage in stages" :key="stage.id" class="pipe-col">
|
||||
<div class="mini-pipeline">
|
||||
<div v-for="col in cols" :key="col.id" class="pipe-col">
|
||||
<header>
|
||||
<strong>{{ stage.name }}</strong>
|
||||
<span>{{ toFaDigits(stage.deals.length) }}</span>
|
||||
<strong>{{ col.name }} ({{ toFaDigits(col.count) }})</strong>
|
||||
</header>
|
||||
<div class="pipe-cards">
|
||||
<RouterLink
|
||||
v-for="deal in stage.deals.slice(0, 3)"
|
||||
:key="deal.id"
|
||||
v-for="card in col.cards"
|
||||
:key="card.id"
|
||||
class="pipe-card"
|
||||
:to="{ name: 'deal-detail', params: { id: deal.id } }"
|
||||
:to="card.id.length > 4 ? { name: 'deal-detail', params: { id: card.id } } : { name: 'pipeline' }"
|
||||
>
|
||||
<span class="deal-mark">
|
||||
{{ (deal.company?.name || deal.person?.fullName || deal.title).slice(0, 1) }}
|
||||
</span>
|
||||
<span class="deal-mark">{{ card.name.slice(0, 1) }}</span>
|
||||
<div>
|
||||
<strong>{{ deal.company?.name || deal.person?.fullName || deal.title }}</strong>
|
||||
<span v-if="deal.amount">{{ formatMoney(deal.amount, deal.currency) }}</span>
|
||||
<strong>{{ card.name }}</strong>
|
||||
<span v-if="card.amount">{{ card.amount }}</span>
|
||||
<em>{{ card.when }}</em>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="muted">کاریزی برای نمایش نیست.</p>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.panel {
|
||||
background: #fff;
|
||||
border-radius: 22px;
|
||||
padding: 1.05rem 1.1rem;
|
||||
border-radius: 20px;
|
||||
padding: 1rem 1.05rem;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||
border: 1px solid #eef2f7;
|
||||
min-width: 0;
|
||||
@@ -62,65 +113,54 @@ const stages = computed(() => props.board?.stages.slice(0, 3) ?? []);
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.85rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 0.98rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #14b8a6;
|
||||
font-size: 0.82rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.mini-pipeline {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(140px, 1fr));
|
||||
gap: 0.75rem;
|
||||
grid-template-columns: repeat(3, minmax(120px, 1fr));
|
||||
gap: 0.65rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.pipe-col > header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.55rem;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.pipe-col > header span {
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.78rem;
|
||||
color: #64748b;
|
||||
background: #f1f5f9;
|
||||
border-radius: 999px;
|
||||
padding: 0.05rem 0.45rem;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pipe-cards {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.pipe-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
padding: 0.65rem 0.7rem;
|
||||
border-radius: 14px;
|
||||
gap: 0.5rem;
|
||||
padding: 0.55rem 0.6rem;
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #eef2f7;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.pipe-card div {
|
||||
display: grid;
|
||||
gap: 0.12rem;
|
||||
gap: 0.1rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -128,17 +168,24 @@ a {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.pipe-card span {
|
||||
color: #94a3b8;
|
||||
font-size: 0.72rem;
|
||||
font-size: 0.68rem;
|
||||
}
|
||||
|
||||
.pipe-card em {
|
||||
color: #cbd5e1;
|
||||
font-size: 0.66rem;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.deal-mark {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 10px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
background: #ecfdf8;
|
||||
color: #0f766e;
|
||||
display: grid;
|
||||
@@ -147,10 +194,6 @@ a {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.mini-pipeline {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -1,13 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { RouterLink } from 'vue-router';
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
import type { ActivityDto } from '@mento/shared';
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
activities: ActivityDto[];
|
||||
formatWhen: (value: string | null) => string;
|
||||
}>();
|
||||
|
||||
type Row = {
|
||||
id: string;
|
||||
title: string;
|
||||
when: string;
|
||||
type: string;
|
||||
tone: 'done' | 'pending' | 'held' | 'late';
|
||||
label: string;
|
||||
};
|
||||
|
||||
const demoRows: Row[] = [
|
||||
{
|
||||
id: 'd1',
|
||||
title: 'تماس با شرکت آریا',
|
||||
when: 'امروز، ۱۰:۴۵',
|
||||
type: 'call',
|
||||
tone: 'done',
|
||||
label: 'انجام شد',
|
||||
},
|
||||
{
|
||||
id: 'd2',
|
||||
title: 'ارسال پیشنهاد قیمت به پارسیان',
|
||||
when: 'دیروز، ۱۴:۴۰',
|
||||
type: 'mail',
|
||||
tone: 'pending',
|
||||
label: 'در انتظار پاسخ',
|
||||
},
|
||||
{
|
||||
id: 'd3',
|
||||
title: 'جلسه با مشتری جدید',
|
||||
when: 'پریروز، ۱۱:۰۰',
|
||||
type: 'meeting',
|
||||
tone: 'held',
|
||||
label: 'برگزار شد',
|
||||
},
|
||||
];
|
||||
|
||||
const typeLabel: Record<string, string> = {
|
||||
call: 'تماس',
|
||||
meeting: 'جلسه',
|
||||
@@ -16,11 +53,11 @@ const typeLabel: Record<string, string> = {
|
||||
};
|
||||
|
||||
function activityStatus(act: ActivityDto) {
|
||||
if (act.status === 'done') return { label: 'انجام شد', tone: 'done' };
|
||||
if (act.status === 'done') return { label: 'انجام شد', tone: 'done' as const };
|
||||
if (act.dueAt && new Date(act.dueAt) < new Date()) {
|
||||
return { label: 'عقبافتاده', tone: 'late' };
|
||||
return { label: 'عقبافتاده', tone: 'late' as const };
|
||||
}
|
||||
return { label: 'در انتظار پاسخ', tone: 'pending' };
|
||||
return { label: 'در انتظار پاسخ', tone: 'pending' as const };
|
||||
}
|
||||
|
||||
function activityIcon(type: string) {
|
||||
@@ -29,6 +66,21 @@ function activityIcon(type: string) {
|
||||
if (type === 'task') return 'check' as const;
|
||||
return 'mail' as const;
|
||||
}
|
||||
|
||||
const rows = computed<Row[]>(() => {
|
||||
if (!props.activities.length) return demoRows;
|
||||
return props.activities.slice(0, 3).map((act) => {
|
||||
const st = activityStatus(act);
|
||||
return {
|
||||
id: act.id,
|
||||
title: act.title,
|
||||
when: `${typeLabel[act.type] ?? act.type} · ${props.formatWhen(act.dueAt || act.createdAt)}`,
|
||||
type: act.type,
|
||||
tone: st.tone,
|
||||
label: st.label,
|
||||
};
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -38,32 +90,26 @@ function activityIcon(type: string) {
|
||||
<RouterLink :to="{ name: 'activities' }">مشاهده همه</RouterLink>
|
||||
</header>
|
||||
|
||||
<ul v-if="activities.length" class="activity-list">
|
||||
<li v-for="act in activities" :key="act.id">
|
||||
<ul class="activity-list">
|
||||
<li v-for="act in rows" :key="act.id">
|
||||
<span class="act-icon">
|
||||
<UiIcon :name="activityIcon(act.type)" :size="16" />
|
||||
</span>
|
||||
<div class="act-body">
|
||||
<strong>{{ act.title }}</strong>
|
||||
<span>
|
||||
{{ typeLabel[act.type] ?? act.type }} ·
|
||||
{{ formatWhen(act.dueAt || act.createdAt) }}
|
||||
</span>
|
||||
<span>{{ act.when }}</span>
|
||||
</div>
|
||||
<span class="status-pill" :class="activityStatus(act).tone">
|
||||
{{ activityStatus(act).label }}
|
||||
</span>
|
||||
<span class="status-pill" :class="act.tone">{{ act.label }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="muted">فعالیتی ثبت نشده.</p>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.panel {
|
||||
background: #fff;
|
||||
border-radius: 22px;
|
||||
padding: 1.05rem 1.1rem;
|
||||
border-radius: 20px;
|
||||
padding: 1rem 1.05rem;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||
border: 1px solid #eef2f7;
|
||||
min-width: 0;
|
||||
@@ -79,12 +125,12 @@ function activityIcon(type: string) {
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 0.98rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #14b8a6;
|
||||
font-size: 0.82rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -93,19 +139,19 @@ a {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
.activity-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.act-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 14px;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 12px;
|
||||
background: #ecfdf8;
|
||||
color: #14b8a6;
|
||||
display: grid;
|
||||
@@ -117,27 +163,31 @@ a {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
gap: 0.12rem;
|
||||
}
|
||||
|
||||
.act-body strong {
|
||||
font-size: 0.88rem;
|
||||
font-size: 0.84rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.act-body span {
|
||||
color: #94a3b8;
|
||||
font-size: 0.74rem;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
font-size: 0.7rem;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
padding: 0.24rem 0.55rem;
|
||||
padding: 0.22rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-pill.done {
|
||||
.status-pill.done,
|
||||
.status-pill.held {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
@@ -151,8 +201,4 @@ a {
|
||||
background: #fee2e2;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #94a3b8;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,17 +9,27 @@ const props = defineProps<{
|
||||
formatAmount: (value: number) => string;
|
||||
}>();
|
||||
|
||||
const demoTrend: HomeDashboardTrendPointDto[] = [
|
||||
{ monthKey: '01', monthLabel: 'فروردین', amount: 980_000_000 },
|
||||
{ monthKey: '02', monthLabel: 'اردیبهشت', amount: 1_250_000_000 },
|
||||
{ monthKey: '03', monthLabel: 'خرداد', amount: 1_480_000_000 },
|
||||
{ monthKey: '04', monthLabel: 'تیر', amount: 1_860_000_000 },
|
||||
{ monthKey: '05', monthLabel: 'مرداد', amount: 2_450_000_000 },
|
||||
];
|
||||
|
||||
const trend = computed(() => (props.trend.length ? props.trend : demoTrend));
|
||||
const change = computed(() => props.changePercent ?? 14);
|
||||
|
||||
const points = computed(() => {
|
||||
const trend = props.trend;
|
||||
if (!trend.length) return [];
|
||||
const max = Math.max(...trend.map((p) => p.amount), 1);
|
||||
const rows = trend.value;
|
||||
const max = Math.max(...rows.map((p) => p.amount), 1);
|
||||
const w = 360;
|
||||
const h = 150;
|
||||
const padX = 16;
|
||||
const padY = 18;
|
||||
return trend.map((p, i) => ({
|
||||
const padY = 36;
|
||||
return rows.map((p, i) => ({
|
||||
...p,
|
||||
x: padX + (i / Math.max(trend.length - 1, 1)) * (w - padX * 2),
|
||||
x: padX + (i / Math.max(rows.length - 1, 1)) * (w - padX * 2),
|
||||
y: h - padY - (p.amount / max) * (h - padY * 2),
|
||||
}));
|
||||
});
|
||||
@@ -43,12 +53,8 @@ const highlight = computed(() => {
|
||||
<article class="panel">
|
||||
<header class="panel-head">
|
||||
<h2>روند فروش</h2>
|
||||
<span
|
||||
v-if="changePercent != null"
|
||||
class="mini-badge"
|
||||
:class="{ down: changePercent < 0 }"
|
||||
>
|
||||
{{ formatChange(changePercent) }} نسبت به ماه قبل
|
||||
<span class="mini-badge" :class="{ down: change < 0 }">
|
||||
{{ formatChange(change) }} نسبت به ماه قبل
|
||||
</span>
|
||||
</header>
|
||||
|
||||
@@ -56,7 +62,7 @@ const highlight = computed(() => {
|
||||
<svg viewBox="0 0 360 170" class="line-chart" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="homeTrendFill" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#14b8a6" stop-opacity="0.3" />
|
||||
<stop offset="0%" stop-color="#14b8a6" stop-opacity="0.28" />
|
||||
<stop offset="100%" stop-color="#14b8a6" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
@@ -76,29 +82,29 @@ const highlight = computed(() => {
|
||||
:points="line"
|
||||
fill="none"
|
||||
stroke="#14b8a6"
|
||||
stroke-width="3.4"
|
||||
stroke-width="3.2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<g v-for="(p, i) in points" :key="i">
|
||||
<circle :cx="p.x" :cy="p.y" r="5" fill="#fff" stroke="#14b8a6" stroke-width="2.6" />
|
||||
<circle :cx="p.x" :cy="p.y" r="4.5" fill="#fff" stroke="#14b8a6" stroke-width="2.4" />
|
||||
</g>
|
||||
<g v-if="highlight">
|
||||
<rect
|
||||
:x="highlight.x - 52"
|
||||
:y="highlight.y - 36"
|
||||
:x="Math.min(Math.max(highlight.x - 52, 4), 256)"
|
||||
:y="highlight.y - 32"
|
||||
width="104"
|
||||
height="26"
|
||||
rx="10"
|
||||
height="24"
|
||||
rx="8"
|
||||
fill="#0f766e"
|
||||
/>
|
||||
<text
|
||||
:x="highlight.x"
|
||||
:y="highlight.y - 18"
|
||||
:x="Math.min(Math.max(highlight.x, 56), 308)"
|
||||
:y="highlight.y - 16"
|
||||
text-anchor="middle"
|
||||
fill="#fff"
|
||||
font-size="10"
|
||||
font-family="Vazirmatn, Tahoma, sans-serif"
|
||||
font-size="9.5"
|
||||
font-family="IRANSans, Tahoma, sans-serif"
|
||||
>
|
||||
{{ formatAmount(highlight.amount) }}
|
||||
</text>
|
||||
@@ -114,11 +120,14 @@ const highlight = computed(() => {
|
||||
<style scoped>
|
||||
.panel {
|
||||
background: #fff;
|
||||
border-radius: 22px;
|
||||
padding: 1.05rem 1.15rem;
|
||||
border-radius: 20px;
|
||||
padding: 1rem 1.1rem 0.9rem;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||
border: 1px solid #eef2f7;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
@@ -126,22 +135,22 @@ const highlight = computed(() => {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.65rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 0.98rem;
|
||||
font-size: 0.95rem;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.mini-badge {
|
||||
font-size: 0.72rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
color: #16a34a;
|
||||
background: #dcfce7;
|
||||
border-radius: 999px;
|
||||
padding: 0.2rem 0.55rem;
|
||||
padding: 0.18rem 0.5rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -152,7 +161,9 @@ h2 {
|
||||
|
||||
.chart-wrap {
|
||||
display: grid;
|
||||
gap: 0.2rem;
|
||||
gap: 0.15rem;
|
||||
margin-top: auto;
|
||||
padding-top: 2.25rem;
|
||||
}
|
||||
|
||||
.line-chart {
|
||||
@@ -164,7 +175,7 @@ h2 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #94a3b8;
|
||||
font-size: 0.74rem;
|
||||
padding: 0 0.4rem;
|
||||
font-size: 0.72rem;
|
||||
padding: 0 0.35rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,18 +7,28 @@ const props = defineProps<{
|
||||
sources: HomeDashboardSourceDto[];
|
||||
}>();
|
||||
|
||||
const colors = ['#14b8a6', '#38bdf8', '#fb923c', '#fbbf24', '#94a3b8', '#a78bfa'];
|
||||
const demoSources: HomeDashboardSourceDto[] = [
|
||||
{ label: 'معرفی دوستان', count: 44, percent: 34 },
|
||||
{ label: 'وبسایت', count: 33, percent: 26 },
|
||||
{ label: 'شبکههای اجتماعی', count: 23, percent: 18 },
|
||||
{ label: 'تبلیغات', count: 18, percent: 14 },
|
||||
{ label: 'سایر', count: 10, percent: 8 },
|
||||
];
|
||||
|
||||
const colors = ['#2563eb', '#14b8a6', '#22c55e', '#fb923c', '#94a3b8'];
|
||||
|
||||
const rows = computed(() => (props.sources.length ? props.sources : demoSources));
|
||||
|
||||
const segments = computed(() => {
|
||||
let offset = 0;
|
||||
return props.sources.map((s, i) => {
|
||||
return rows.value.map((s, i) => {
|
||||
const seg = { ...s, color: colors[i % colors.length], offset };
|
||||
offset += s.percent;
|
||||
return seg;
|
||||
});
|
||||
});
|
||||
|
||||
const total = computed(() => props.sources.reduce((a, s) => a + s.count, 0));
|
||||
const total = computed(() => rows.value.reduce((a, s) => a + s.count, 0));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -66,39 +76,39 @@ const total = computed(() => props.sources.reduce((a, s) => a + s.count, 0));
|
||||
<style scoped>
|
||||
.panel {
|
||||
background: #fff;
|
||||
border-radius: 22px;
|
||||
padding: 1.05rem 1.15rem;
|
||||
border-radius: 20px;
|
||||
padding: 1rem 1.1rem;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||
border: 1px solid #eef2f7;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
margin-bottom: 0.85rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 0.98rem;
|
||||
font-size: 0.95rem;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.donut-wrap {
|
||||
display: grid;
|
||||
grid-template-columns: 140px 1fr;
|
||||
gap: 1rem;
|
||||
grid-template-columns: 132px 1fr;
|
||||
gap: 0.85rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.donut-chart-box {
|
||||
position: relative;
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
width: 132px;
|
||||
height: 132px;
|
||||
}
|
||||
|
||||
.donut-chart {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
width: 132px;
|
||||
height: 132px;
|
||||
}
|
||||
|
||||
.donut-center {
|
||||
@@ -112,12 +122,12 @@ h2 {
|
||||
}
|
||||
|
||||
.donut-center strong {
|
||||
font-size: 1.2rem;
|
||||
font-size: 1.15rem;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.donut-center span {
|
||||
font-size: 0.72rem;
|
||||
font-size: 0.7rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
@@ -126,14 +136,14 @@ h2 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.82rem;
|
||||
gap: 0.48rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.donut-legend li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.lbl {
|
||||
@@ -148,8 +158,8 @@ em {
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { RouterLink } from 'vue-router';
|
||||
import { toFaDigits } from '@/lib/jalali';
|
||||
import type { HomeDashboardTopCustomerDto } from '@mento/shared';
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
customers: HomeDashboardTopCustomerDto[];
|
||||
}>();
|
||||
|
||||
const demo: HomeDashboardTopCustomerDto[] = [
|
||||
{ id: '1', name: 'شرکت آریا', dealCount: 8, rank: 1 },
|
||||
{ id: '2', name: 'گروه پارسیان', dealCount: 4, rank: 2 },
|
||||
{ id: '3', name: 'فناوران نوین', dealCount: 3, rank: 3 },
|
||||
];
|
||||
|
||||
const rows = computed(() => (props.customers.length ? props.customers.slice(0, 3) : demo));
|
||||
|
||||
function trophy(rank: number) {
|
||||
if (rank === 1) return '🥇';
|
||||
if (rank === 2) return '🥈';
|
||||
@@ -18,10 +28,11 @@ function trophy(rank: number) {
|
||||
<article class="panel">
|
||||
<header class="panel-head">
|
||||
<h2>مشتریان برتر</h2>
|
||||
<RouterLink :to="{ name: 'contacts' }">مشاهده همه</RouterLink>
|
||||
</header>
|
||||
|
||||
<ul v-if="customers.length" class="top-list">
|
||||
<li v-for="row in customers" :key="row.id">
|
||||
<ul class="top-list">
|
||||
<li v-for="row in rows" :key="row.id">
|
||||
<span class="cust-avatar">{{ row.name.slice(0, 1) }}</span>
|
||||
<div>
|
||||
<strong>{{ row.name }}</strong>
|
||||
@@ -30,27 +41,35 @@ function trophy(rank: number) {
|
||||
<span class="rank">{{ trophy(row.rank) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="muted">هنوز داده کافی برای رتبهبندی نیست.</p>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.panel {
|
||||
background: #fff;
|
||||
border-radius: 22px;
|
||||
padding: 1.05rem 1.1rem;
|
||||
border-radius: 20px;
|
||||
padding: 1rem 1.05rem;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||
border: 1px solid #eef2f7;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.85rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 0.98rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #14b8a6;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.top-list {
|
||||
@@ -58,19 +77,19 @@ h2 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
.top-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.cust-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 14px;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(145deg, #ccfbf1, #99f6e4);
|
||||
color: #0f766e;
|
||||
display: grid;
|
||||
@@ -83,23 +102,19 @@ h2 {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 0.12rem;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
|
||||
.top-list strong {
|
||||
font-size: 0.88rem;
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.top-list span {
|
||||
color: #94a3b8;
|
||||
font-size: 0.74rem;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.rank {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #94a3b8;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -349,13 +349,19 @@ onUnmounted(() => {
|
||||
<div class="profile-meta">
|
||||
<strong>{{ auth.user?.fullName }}</strong>
|
||||
<span>{{ roleLabel || 'کاربر' }}</span>
|
||||
<span class="online-label">
|
||||
<i class="online-dot" />
|
||||
آنلاین
|
||||
</span>
|
||||
</div>
|
||||
<span class="online-dot" title="آنلاین" />
|
||||
</div>
|
||||
|
||||
<div class="promo-card">
|
||||
<span class="promo-icon"><UiIcon name="megaphone" :size="18" /></span>
|
||||
<p>با منتو فروشات رو چند برابر کن</p>
|
||||
<span class="promo-icons" aria-hidden="true">
|
||||
<UiIcon name="send" :size="16" />
|
||||
<UiIcon name="sparkle" :size="14" />
|
||||
</span>
|
||||
<p>با منتو فروشات را چند برابر کن</p>
|
||||
</div>
|
||||
|
||||
<button class="logout-link" type="button" @click="onLogout">خروج از حساب</button>
|
||||
@@ -454,10 +460,11 @@ onUnmounted(() => {
|
||||
<style scoped>
|
||||
.shell {
|
||||
display: grid;
|
||||
grid-template-columns: 248px minmax(0, 1fr);
|
||||
grid-template-columns: 268px minmax(0, 1fr);
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
background: #f5f7fa;
|
||||
background: #eef2f6;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.workspace {
|
||||
@@ -477,18 +484,21 @@ onUnmounted(() => {
|
||||
|
||||
.sidebar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
top: 14px;
|
||||
align-self: start;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
height: calc(100dvh - 28px);
|
||||
margin: 14px;
|
||||
margin-inline-end: 10px;
|
||||
background: #fff;
|
||||
border-inline-start: 1px solid #e8eef3;
|
||||
padding: 1.15rem 0.9rem;
|
||||
border: 0;
|
||||
border-radius: 28px;
|
||||
padding: 1.55rem 1.05rem 1.2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
gap: 1.35rem;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.brand-row {
|
||||
@@ -496,7 +506,7 @@ onUnmounted(() => {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
padding: 0 0.35rem;
|
||||
padding: 0.15rem 0.45rem 0.35rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -541,11 +551,12 @@ onUnmounted(() => {
|
||||
|
||||
.side-nav {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
gap: 0.45rem;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
scrollbar-width: none;
|
||||
padding-inline: 0.15rem;
|
||||
}
|
||||
|
||||
.side-nav::-webkit-scrollbar {
|
||||
@@ -555,19 +566,19 @@ onUnmounted(() => {
|
||||
.nav-primary,
|
||||
.nav-sub {
|
||||
display: grid;
|
||||
gap: 0.22rem;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
padding: 0.78rem 0.9rem;
|
||||
border-radius: 14px;
|
||||
color: #64748b;
|
||||
gap: 0.75rem;
|
||||
padding: 0.82rem 1rem;
|
||||
border-radius: 18px;
|
||||
color: #334155;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
font-size: 0.92rem;
|
||||
font-size: 0.94rem;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
@@ -577,7 +588,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.nav-item.router-link-active {
|
||||
background: #d1faf2;
|
||||
background: #d8faf3;
|
||||
color: #0f766e;
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -605,7 +616,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.nav-group {
|
||||
margin-top: 0.35rem;
|
||||
margin-top: 0.55rem;
|
||||
}
|
||||
|
||||
.nav-group-btn {
|
||||
@@ -619,8 +630,8 @@ onUnmounted(() => {
|
||||
font: inherit;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
padding: 0.5rem 0.9rem;
|
||||
border-radius: 10px;
|
||||
padding: 0.55rem 1rem;
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -639,15 +650,15 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.nav-sub {
|
||||
margin-inline-end: 0.75rem;
|
||||
padding-inline-end: 0.35rem;
|
||||
margin-inline-end: 0.55rem;
|
||||
padding-inline-end: 0.25rem;
|
||||
border-inline-end: 1px solid #eef2f7;
|
||||
}
|
||||
|
||||
.nav-sub a {
|
||||
display: block;
|
||||
padding: 0.55rem 0.8rem;
|
||||
border-radius: 10px;
|
||||
padding: 0.58rem 0.9rem;
|
||||
border-radius: 14px;
|
||||
color: #64748b;
|
||||
text-decoration: none;
|
||||
font-size: 0.88rem;
|
||||
@@ -671,20 +682,23 @@ onUnmounted(() => {
|
||||
.sidebar-foot {
|
||||
margin-top: auto;
|
||||
display: grid;
|
||||
gap: 0.7rem;
|
||||
gap: 0.85rem;
|
||||
flex-shrink: 0;
|
||||
padding-top: 0.35rem;
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
padding: 0.55rem 0.65rem;
|
||||
gap: 0.75rem;
|
||||
padding: 0.85rem 0.9rem;
|
||||
border-radius: 18px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(145deg, #99f6e4, #5eead4);
|
||||
color: #0f766e;
|
||||
@@ -697,54 +711,59 @@ onUnmounted(() => {
|
||||
.profile-meta {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 0.08rem;
|
||||
gap: 0.12rem;
|
||||
}
|
||||
|
||||
.profile-meta strong {
|
||||
font-size: 0.88rem;
|
||||
font-size: 0.9rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.profile-meta span {
|
||||
.profile-meta > span {
|
||||
color: #94a3b8;
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.online-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
color: #64748b !important;
|
||||
font-size: 0.74rem !important;
|
||||
}
|
||||
|
||||
.online-dot {
|
||||
margin-inline-start: auto;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: #22c55e;
|
||||
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.18);
|
||||
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.16);
|
||||
}
|
||||
|
||||
.promo-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
padding: 0.95rem 1rem;
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(145deg, #0f766e, #115e59 55%, #134e4a);
|
||||
color: #ecfdf5;
|
||||
box-shadow: 0 14px 28px rgba(15, 118, 110, 0.25);
|
||||
padding: 1.05rem 1.05rem;
|
||||
border-radius: 22px;
|
||||
background: linear-gradient(105deg, #c4e84a 0%, #2dd4bf 42%, #1d4ed8 100%);
|
||||
color: #fff;
|
||||
box-shadow: 0 12px 26px rgba(29, 78, 216, 0.18);
|
||||
}
|
||||
|
||||
.promo-icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
.promo-icons {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.promo-card p {
|
||||
margin: 0;
|
||||
font-size: 0.84rem;
|
||||
font-size: 0.86rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
}
|
||||
@@ -754,42 +773,55 @@ onUnmounted(() => {
|
||||
background: transparent;
|
||||
color: #94a3b8;
|
||||
font: inherit;
|
||||
font-size: 0.8rem;
|
||||
font-size: 0.78rem;
|
||||
cursor: pointer;
|
||||
text-align: right;
|
||||
padding: 0 0.35rem;
|
||||
padding: 0 0.45rem 0.15rem;
|
||||
}
|
||||
|
||||
.global-search {
|
||||
flex: 1;
|
||||
max-width: 620px;
|
||||
margin-inline: auto;
|
||||
width: min(100%, 420px);
|
||||
max-width: 420px;
|
||||
justify-self: start;
|
||||
/* همتراز با عنوان هیرو: padding هیرو + آواتار خوشآمد + فاصله */
|
||||
margin-inline-start: calc(1.4rem + 48px + 0.8rem);
|
||||
margin-inline-end: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
background: #eef2f6;
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
padding: 0.78rem 1.1rem;
|
||||
padding: 0.7rem 1.05rem;
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.desktop-topbar {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
align-items: start;
|
||||
gap: 1rem;
|
||||
padding: 1.05rem 1.5rem 0.85rem;
|
||||
/* همان فاصله بالای سایدبار شناور (14px) */
|
||||
padding: 14px 1.5rem 0.85rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
background: rgba(245, 247, 250, 0.92);
|
||||
background: rgba(238, 242, 246, 0.92);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
/* مرکز عمودی نسبت به ارتفاع جستجو (~49px) */
|
||||
min-height: 49px;
|
||||
}
|
||||
|
||||
.global-search:focus-within {
|
||||
background: #fff;
|
||||
border-color: #cbd5e1;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
|
||||
border-color: #e2e8f0;
|
||||
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.global-search input {
|
||||
@@ -813,18 +845,12 @@ onUnmounted(() => {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.72rem;
|
||||
color: #94a3b8;
|
||||
background: #fff;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 0.18rem 0.45rem;
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.round-btn {
|
||||
position: relative;
|
||||
width: 42px;
|
||||
@@ -1057,6 +1083,9 @@ onUnmounted(() => {
|
||||
bottom: 0;
|
||||
inset-inline-start: 0;
|
||||
width: min(300px, 86vw);
|
||||
height: 100dvh;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
z-index: 35;
|
||||
transform: translateX(110%);
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/* IRANSans (FaNum) — local self-hosted */
|
||||
@font-face {
|
||||
font-family: "IRANSans";
|
||||
src:
|
||||
url("../assets/fonts/iranSans/woff2/IRANSansWeb(FaNum)_UltraLight.woff2") format("woff2"),
|
||||
url("../assets/fonts/iranSans/woff/IRANSansWeb(FaNum)_UltraLight.woff") format("woff");
|
||||
font-weight: 200;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANSans";
|
||||
src:
|
||||
url("../assets/fonts/iranSans/woff2/IRANSansWeb(FaNum)_Light.woff2") format("woff2"),
|
||||
url("../assets/fonts/iranSans/woff/IRANSansWeb(FaNum)_Light.woff") format("woff");
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANSans";
|
||||
src:
|
||||
url("../assets/fonts/iranSans/woff2/IRANSansWeb(FaNum).woff2") format("woff2"),
|
||||
url("../assets/fonts/iranSans/woff/IRANSansWeb(FaNum).woff") format("woff");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANSans";
|
||||
src:
|
||||
url("../assets/fonts/iranSans/woff2/IRANSansWeb(FaNum)_Medium.woff2") format("woff2"),
|
||||
url("../assets/fonts/iranSans/woff/IRANSansWeb(FaNum)_Medium.woff") format("woff");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANSans";
|
||||
src:
|
||||
url("../assets/fonts/iranSans/woff2/IRANSansWeb(FaNum)_Bold.woff2") format("woff2"),
|
||||
url("../assets/fonts/iranSans/woff/IRANSansWeb(FaNum)_Bold.woff") format("woff");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANSans";
|
||||
src:
|
||||
url("../assets/fonts/iranSans/woff2/IRANSansWeb(FaNum)_Black.woff2") format("woff2"),
|
||||
url("../assets/fonts/iranSans/woff/IRANSansWeb(FaNum)_Black.woff") format("woff");
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* map common 600 → Bold so font-weight:600 works */
|
||||
@font-face {
|
||||
font-family: "IRANSans";
|
||||
src:
|
||||
url("../assets/fonts/iranSans/woff2/IRANSansWeb(FaNum)_Bold.woff2") format("woff2"),
|
||||
url("../assets/fonts/iranSans/woff/IRANSansWeb(FaNum)_Bold.woff") format("woff");
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* map 800 → Black */
|
||||
@font-face {
|
||||
font-family: "IRANSans";
|
||||
src:
|
||||
url("../assets/fonts/iranSans/woff2/IRANSansWeb(FaNum)_Black.woff2") format("woff2"),
|
||||
url("../assets/fonts/iranSans/woff/IRANSansWeb(FaNum)_Black.woff") format("woff");
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
@import "./fonts.css";
|
||||
|
||||
:root {
|
||||
--bg: #f4f7f8;
|
||||
--surface: #ffffff;
|
||||
@@ -9,7 +11,7 @@
|
||||
--danger: #b42318;
|
||||
--shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
|
||||
--radius: 18px;
|
||||
--font: "Vazirmatn", Tahoma, sans-serif;
|
||||
--font: "IRANSans", Tahoma, sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
|
||||
@@ -148,15 +148,15 @@ onMounted(async () => {
|
||||
|
||||
.mid-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.3fr 1fr 0.95fr;
|
||||
gap: 0.9rem;
|
||||
grid-template-columns: 1.25fr 1fr 0.88fr;
|
||||
gap: 0.8rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.bottom-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.35fr 0.85fr;
|
||||
gap: 0.9rem;
|
||||
grid-template-columns: 1fr 1.35fr 0.78fr;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.muted {
|
||||
|
||||
Generated
+23
-4
@@ -11,8 +11,14 @@
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@rollup/rollup-win32-x64-msvc": "*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-win32-x64-msvc": "^4.63.1"
|
||||
}
|
||||
},
|
||||
"apps/api": {
|
||||
@@ -1890,13 +1896,12 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.62.5",
|
||||
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.5.tgz",
|
||||
"integrity": "sha512-/gDJaRs4gl0NPIwqCz+6PkpmhhjRAD2j6P4rSNHBzUkO3naEx2mIU0pRle1vUNRQ7mE/+8OOeXLTv/J56FKiQg==",
|
||||
"version": "4.63.1",
|
||||
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz",
|
||||
"integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6305,6 +6310,20 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup/node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.62.5",
|
||||
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.5.tgz",
|
||||
"integrity": "sha512-/gDJaRs4gl0NPIwqCz+6PkpmhhjRAD2j6P4rSNHBzUkO3naEx2mIU0pRle1vUNRQ7mE/+8OOeXLTv/J56FKiQg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/router": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/router/-/router-2.2.0.tgz",
|
||||
|
||||
@@ -20,5 +20,8 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-win32-x64-msvc": "^4.63.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Cut black background only via edge flood-fill — keep black face screen."""
|
||||
from collections import deque
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
src = Path(r"e:\projeha\crm\apps\web\public\brand\Mento-ai.png")
|
||||
dst = Path(r"e:\projeha\crm\apps\web\public\brand\mento-ai-avatar.png")
|
||||
|
||||
img = Image.open(src).convert("RGBA")
|
||||
w, h = img.size
|
||||
px = img.load()
|
||||
|
||||
def is_bg(r, g, b, a):
|
||||
# near-black background plate (not cyan glow)
|
||||
if a < 10:
|
||||
return True
|
||||
if r < 40 and g < 40 and b < 45:
|
||||
return True
|
||||
if r < 55 and g < 55 and b < 60 and (r + g + b) < 150:
|
||||
# still bg if not cyan-tinted
|
||||
if b < g + 15 and g < r + 15:
|
||||
return True
|
||||
return False
|
||||
|
||||
visited = [[False] * w for _ in range(h)]
|
||||
q = deque()
|
||||
|
||||
# seed from all edge pixels that look like background
|
||||
for x in range(w):
|
||||
for y in (0, h - 1):
|
||||
r, g, b, a = px[x, y]
|
||||
if is_bg(r, g, b, a):
|
||||
q.append((x, y))
|
||||
visited[y][x] = True
|
||||
for y in range(h):
|
||||
for x in (0, w - 1):
|
||||
if not visited[y][x]:
|
||||
r, g, b, a = px[x, y]
|
||||
if is_bg(r, g, b, a):
|
||||
q.append((x, y))
|
||||
visited[y][x] = True
|
||||
|
||||
while q:
|
||||
x, y = q.popleft()
|
||||
px[x, y] = (0, 0, 0, 0)
|
||||
for nx, ny in ((x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)):
|
||||
if 0 <= nx < w and 0 <= ny < h and not visited[ny][nx]:
|
||||
r, g, b, a = px[nx, ny]
|
||||
if is_bg(r, g, b, a):
|
||||
visited[ny][nx] = True
|
||||
q.append((nx, ny))
|
||||
|
||||
bbox = img.getbbox()
|
||||
if bbox:
|
||||
pad = 20
|
||||
l, t, r, btm = bbox
|
||||
img = img.crop(
|
||||
(max(0, l - pad), max(0, t - pad), min(w, r + pad), min(h, btm + pad))
|
||||
)
|
||||
|
||||
# fit into square canvas keeping aspect
|
||||
ow, oh = img.size
|
||||
side = max(ow, oh)
|
||||
canvas = Image.new("RGBA", (side, side), (0, 0, 0, 0))
|
||||
canvas.paste(img, ((side - ow) // 2, (side - oh) // 2))
|
||||
canvas = canvas.resize((512, 512), Image.Resampling.LANCZOS)
|
||||
canvas.save(dst, "PNG", optimize=True)
|
||||
|
||||
# stats
|
||||
a = [p[3] for p in canvas.getdata()]
|
||||
print(
|
||||
"saved",
|
||||
dst,
|
||||
"zero_a",
|
||||
sum(1 for x in a if x == 0),
|
||||
"corner",
|
||||
canvas.getpixel((0, 0)),
|
||||
"center",
|
||||
canvas.getpixel((256, 256)),
|
||||
)
|
||||
Reference in New Issue
Block a user