feat: add sidebar entry + route + dashboard leave card + on_leave status

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-12 13:15:50 +08:00
parent 709685057b
commit 7fd0f9b706
3 changed files with 35 additions and 7 deletions
@@ -28,6 +28,7 @@ const menuGroups = computed<MenuGroup[]>(() => {
{ path: '/work-plans', label: '工作计划', icon: '<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line><polyline points="8 14 12 18 16 14"></polyline>' }, { path: '/work-plans', label: '工作计划', icon: '<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line><polyline points="8 14 12 18 16 14"></polyline>' },
{ path: '/mini-business', label: '商机跟单', icon: '<line x1="12" y1="1" x2="12" y2="23"></line><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>' }, { path: '/mini-business', label: '商机跟单', icon: '<line x1="12" y1="1" x2="12" y2="23"></line><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>' },
{ path: '/key-visits', label: '要客拜访', icon: '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>' }, { path: '/key-visits', label: '要客拜访', icon: '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>' },
{ path: '/leaves', label: '请假管理', icon: '<path d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>' },
], ],
}, },
] ]
+1
View File
@@ -55,6 +55,7 @@ const router = createRouter({
{ path: 'work-plans', name: 'WorkPlans', component: () => import('@/views/desktop/WorkPlans.vue') }, { path: 'work-plans', name: 'WorkPlans', component: () => import('@/views/desktop/WorkPlans.vue') },
{ path: 'mini-business', name: 'MiniBusiness', component: () => import('@/views/desktop/MiniBusiness.vue') }, { path: 'mini-business', name: 'MiniBusiness', component: () => import('@/views/desktop/MiniBusiness.vue') },
{ path: 'key-visits', name: 'KeyVisits', component: () => import('@/views/desktop/KeyVisits.vue') }, { path: 'key-visits', name: 'KeyVisits', component: () => import('@/views/desktop/KeyVisits.vue') },
{ path: 'leaves', name: 'Leaves', component: () => import('@/views/desktop/Leaves.vue') },
{ path: 'workspace', name: 'ManagerWorkspace', component: () => import('@/views/desktop/ManagerWorkspace.vue') }, { path: 'workspace', name: 'ManagerWorkspace', component: () => import('@/views/desktop/ManagerWorkspace.vue') },
{ path: 'customers', name: 'CustomerManage', component: () => import('@/views/desktop/CustomerManage.vue') }, { path: 'customers', name: 'CustomerManage', component: () => import('@/views/desktop/CustomerManage.vue') },
{ path: 'users', name: 'UserManage', component: () => import('@/views/desktop/UserManage.vue') }, { path: 'users', name: 'UserManage', component: () => import('@/views/desktop/UserManage.vue') },
+33 -7
View File
@@ -9,7 +9,7 @@ import api from '@/api/index'
const router = useRouter() const router = useRouter()
const auth = useAuthStore() const auth = useAuthStore()
const loading = ref(false) const loading = ref(false)
const stats = ref({ week_visits: 0, work_plans: 0, mini_business: 0, key_visits: 0, week_start: '', week_end: '' }) const stats = ref({ week_visits: 0, work_plans: 0, mini_business: 0, key_visits: 0, week_leaves: 0, week_start: '', week_end: '' })
const progress = ref<any[]>([]) const progress = ref<any[]>([])
const weekOffset = ref(0) // 0 = current week, -1 = last week, etc. const weekOffset = ref(0) // 0 = current week, -1 = last week, etc.
@@ -76,7 +76,8 @@ async function handleExport() {
} catch (e: any) { ElMessage.error('导出失败') } } catch (e: any) { ElMessage.error('导出失败') }
} }
function rowState(p: any): 'full' | 'catching' | 'missing' { function rowState(p: any): 'full' | 'catching' | 'missing' | 'on_leave' {
if (p.on_leave) return 'on_leave'
if (p.has_reported_today && p.completed) return 'full' if (p.has_reported_today && p.completed) return 'full'
if (p.has_reported_today && !p.completed) return 'catching' if (p.has_reported_today && !p.completed) return 'catching'
return 'missing' return 'missing'
@@ -170,6 +171,17 @@ function rowState(p: any): 'full' | 'catching' | 'missing' {
<span class="stat-label">要客拜访</span> <span class="stat-label">要客拜访</span>
</div> </div>
</div> </div>
<div class="stat-card stat-card--clickable stat-card--slate" @click="router.push('/leaves')">
<div class="stat-glyph stat-glyph--slate">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
</div>
<div class="stat-body">
<span class="stat-num">{{ stats.week_leaves }}</span>
<span class="stat-label">本周请假</span>
</div>
</div>
</div> </div>
<!-- Reporting Progress --> <!-- Reporting Progress -->
@@ -194,24 +206,26 @@ function rowState(p: any): 'full' | 'catching' | 'missing' {
> >
<!-- Chinese Seal Watermark --> <!-- Chinese Seal Watermark -->
<div class="seal-stamp" :class="`seal-stamp--${rowState(p)}`" aria-hidden="true"> <div class="seal-stamp" :class="`seal-stamp--${rowState(p)}`" aria-hidden="true">
<span class="seal-char">{{ { full: '满', catching: '追', missing: '未' }[rowState(p)] }}</span> <span class="seal-char">{{ { full: '满', catching: '追', missing: '未', on_leave: '假' }[rowState(p)] }}</span>
<span class="seal-char">{{ { full: '', catching: '', missing: '填' }[rowState(p)] }}</span> <span class="seal-char">{{ { full: '', catching: '', missing: '填', on_leave: '' }[rowState(p)] }}</span>
</div> </div>
<div class="progress-info"> <div class="progress-info">
<span class="progress-name"> <span class="progress-name">
<el-link type="primary" :underline="false" @click="goWeeklyReport(p.manager_id)">{{ p.manager_name }}</el-link> <el-link type="primary" :underline="false" @click="goWeeklyReport(p.manager_id)">{{ p.manager_name }}</el-link>
<span class="progress-status" :class="`progress-status--${rowState(p)}`"> <span class="progress-status" :class="`progress-status--${rowState(p)}`">
{{ { full: '本周已满', catching: '今日已填', missing: '今日未填' }[rowState(p)] }} {{ { full: '本周已满', catching: '今日已填', missing: '今日未填', on_leave: '请假中' }[rowState(p)] }}
</span> </span>
</span> </span>
<span class="progress-count">{{ p.visit_count }} / {{ p.expected }} </span> <span class="progress-count" v-if="!p.on_leave">{{ p.visit_count }} / {{ p.expected }} </span>
</div> </div>
<el-progress <el-progress
v-if="!p.on_leave"
:percentage="Math.min(100, Math.round((p.visit_count / Math.max(p.expected, 1)) * 100))" :percentage="Math.min(100, Math.round((p.visit_count / Math.max(p.expected, 1)) * 100))"
:color="rowState(p) === 'full' ? '#4A6741' : rowState(p) === 'catching' ? '#C4934A' : '#B8472E'" :color="rowState(p) === 'full' ? '#4A6741' : rowState(p) === 'catching' ? '#C4934A' : '#B8472E'"
:stroke-width="12" :stroke-width="12"
/> />
<span v-else class="leave-text">请假中</span>
</div> </div>
</div> </div>
</el-card> </el-card>
@@ -241,7 +255,7 @@ function rowState(p: any): 'full' | 'catching' | 'missing' {
/* ═══ Stat Grid ═══ */ /* ═══ Stat Grid ═══ */
.stat-grid { .stat-grid {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(5, 1fr);
gap: 16px; gap: 16px;
margin-bottom: 24px; margin-bottom: 24px;
} }
@@ -258,6 +272,7 @@ function rowState(p: any): 'full' | 'catching' | 'missing' {
.stat-card:hover { box-shadow: var(--shadow-md); } .stat-card:hover { box-shadow: var(--shadow-md); }
.stat-card--clickable { cursor: pointer; } .stat-card--clickable { cursor: pointer; }
.stat-card--clickable:hover { border-color: var(--gold); } .stat-card--clickable:hover { border-color: var(--gold); }
.stat-card--slate:hover { border-color: #5B7FA5; }
.stat-glyph { .stat-glyph {
width: 48px; height: 48px; width: 48px; height: 48px;
@@ -268,6 +283,7 @@ function rowState(p: any): 'full' | 'catching' | 'missing' {
.stat-glyph--sage { background: #EDF2EC; color: var(--sage); } .stat-glyph--sage { background: #EDF2EC; color: var(--sage); }
.stat-glyph--gold { background: #FBF6EE; color: var(--gold); } .stat-glyph--gold { background: #FBF6EE; color: var(--gold); }
.stat-glyph--vermilion { background: #FBF1EE; color: var(--vermilion); } .stat-glyph--vermilion { background: #FBF1EE; color: var(--vermilion); }
.stat-glyph--slate { color: #5B7FA5; }
.stat-body { display: flex; flex-direction: column; } .stat-body { display: flex; flex-direction: column; }
.stat-num { .stat-num {
@@ -308,6 +324,7 @@ function rowState(p: any): 'full' | 'catching' | 'missing' {
.progress-row--missing { border-left-color: var(--vermilion); background: rgba(184,71,46,0.02); } .progress-row--missing { border-left-color: var(--vermilion); background: rgba(184,71,46,0.02); }
.progress-row--catching { border-left-color: var(--gold); background: rgba(196,147,74,0.03); } .progress-row--catching { border-left-color: var(--gold); background: rgba(196,147,74,0.03); }
.progress-row--full { border-left-color: var(--sage); background: rgba(74,103,65,0.02); } .progress-row--full { border-left-color: var(--sage); background: rgba(74,103,65,0.02); }
.progress-row--on_leave { border-left-color: #5B7FA5; }
/* ═══ Chinese Seal Stamp Watermark ═══ */ /* ═══ Chinese Seal Stamp Watermark ═══ */
.seal-stamp { .seal-stamp {
@@ -357,6 +374,13 @@ function rowState(p: any): 'full' | 'catching' | 'missing' {
color: var(--sage); color: var(--sage);
} }
/* On-leave stamp — slate blue-gray ink */
.seal-stamp--on_leave {
border-color: #5B7FA5;
outline-color: #5B7FA5;
color: #5B7FA5;
}
.seal-char { .seal-char {
font-family: var(--font-heading); font-family: var(--font-heading);
font-size: 20px; font-size: 20px;
@@ -382,6 +406,7 @@ function rowState(p: any): 'full' | 'catching' | 'missing' {
.progress-status--missing { color: var(--vermilion); } .progress-status--missing { color: var(--vermilion); }
.progress-status--catching { color: var(--gold); } .progress-status--catching { color: var(--gold); }
.progress-status--full { color: var(--sage); } .progress-status--full { color: var(--sage); }
.progress-status--on_leave { color: #5B7FA5; }
.progress-count { .progress-count {
font-family: var(--font-mono); font-family: var(--font-mono);
@@ -389,5 +414,6 @@ function rowState(p: any): 'full' | 'catching' | 'missing' {
position: relative; z-index: 1; position: relative; z-index: 1;
} }
.leave-text { font-size: 13px; color: #5B7FA5; font-family: var(--font-body); }
.empty { text-align: center; color: var(--c-text-muted); padding: 40px 0; font-family: var(--font-body); } .empty { text-align: center; color: var(--c-text-muted); padding: 40px 0; font-family: var(--font-body); }
</style> </style>