refactor(ui): unify frontend around compact, desktop-first design
Extract shared utilities (color functions, icon registry), replace hero banners with compact PageHeader, add TrimesterSelector/ConfirmDialog/ Breadcrumb components, consolidate off-palette colors to design tokens, convert AssessmentListView to table layout, compress ResultsView stats into horizontal bar, and inline ClassFormView as a modal in ClassListView. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,74 +1,22 @@
|
||||
<template>
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="px-4 sm:px-6 lg:px-8 py-8">
|
||||
<LoadingSpinner v-if="loading" text="Chargement des résultats..." fullPage />
|
||||
|
||||
<template v-else-if="results">
|
||||
<!-- Header amélioré avec métadonnées -->
|
||||
<div class="bg-gradient-to-r from-success-500 to-emerald-600 rounded-2xl p-8 mb-8 text-white">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold mb-2">{{ results.assessment_title }}</h1>
|
||||
<p class="text-success-100 mb-4">Résultats de l'évaluation</p>
|
||||
|
||||
<!-- Métadonnées avec icônes -->
|
||||
<div class="flex flex-wrap gap-4 text-sm text-success-100">
|
||||
<div class="flex items-center gap-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<span>{{ gradedStudents.length }}/{{ results.students_scores?.length || 0 }} élèves évalués</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
<span>Moy: {{ calculatedStats?.mean?.toFixed(1) || '-' }}/20</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
|
||||
</svg>
|
||||
<span>Max: {{ calculatedStats?.max?.toFixed(1) || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bouton retour -->
|
||||
<router-link
|
||||
:to="`/assessments/${results.assessment_id}`"
|
||||
class="btn bg-white/20 hover:bg-white/30 text-white"
|
||||
>
|
||||
Retour
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<PageHeader
|
||||
:title="results.assessment_title"
|
||||
:subtitle="`Résultats \u00b7 ${gradedStudents.length}/${results.students_scores?.length || 0} élèves évalués`"
|
||||
:breadcrumbs="[{ label: 'Évaluations', to: '/assessments' }, { label: results.assessment_title, to: `/assessments/${results.assessment_id}` }, { label: 'Résultats' }]"
|
||||
/>
|
||||
|
||||
<!-- Statistics -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-6 gap-4 mb-8">
|
||||
<div class="card card-body text-center">
|
||||
<p class="text-sm text-gray-500">Évalués</p>
|
||||
<p class="text-2xl font-bold">{{ gradedStudents.length }}<span class="text-sm text-gray-400">/{{ results.students_scores?.length }}</span></p>
|
||||
</div>
|
||||
<div class="card card-body text-center">
|
||||
<p class="text-sm text-gray-500">Moyenne</p>
|
||||
<p class="text-2xl font-bold">{{ calculatedStats?.mean?.toFixed(2) || '-' }}</p>
|
||||
</div>
|
||||
<div class="card card-body text-center">
|
||||
<p class="text-sm text-gray-500">Médiane</p>
|
||||
<p class="text-2xl font-bold">{{ calculatedStats?.median?.toFixed(2) || '-' }}</p>
|
||||
</div>
|
||||
<div class="card card-body text-center">
|
||||
<p class="text-sm text-gray-500">Écart-type</p>
|
||||
<p class="text-2xl font-bold">{{ calculatedStats?.std_dev?.toFixed(2) || '-' }}</p>
|
||||
</div>
|
||||
<div class="card card-body text-center">
|
||||
<p class="text-sm text-gray-500">Min</p>
|
||||
<p class="text-2xl font-bold">{{ calculatedStats?.min?.toFixed(2) || '-' }}</p>
|
||||
</div>
|
||||
<div class="card card-body text-center">
|
||||
<p class="text-sm text-gray-500">Max</p>
|
||||
<p class="text-2xl font-bold">{{ calculatedStats?.max?.toFixed(2) || '-' }}</p>
|
||||
</div>
|
||||
<!-- Statistics bar -->
|
||||
<div class="flex items-center gap-6 px-4 py-3 bg-white rounded-lg border border-gray-200 mb-6 text-sm">
|
||||
<div><span class="text-gray-500">Évalués</span> <span class="font-bold">{{ gradedStudents.length }}<span class="text-gray-400">/{{ results.students_scores?.length }}</span></span></div>
|
||||
<div><span class="text-gray-500">Moy</span> <span class="font-bold">{{ calculatedStats?.mean?.toFixed(2) || '-' }}</span></div>
|
||||
<div><span class="text-gray-500">Med</span> <span class="font-bold">{{ calculatedStats?.median?.toFixed(2) || '-' }}</span></div>
|
||||
<div><span class="text-gray-500">E-T</span> <span class="font-bold">{{ calculatedStats?.std_dev?.toFixed(2) || '-' }}</span></div>
|
||||
<div><span class="text-gray-500">Min</span> <span class="font-bold">{{ calculatedStats?.min?.toFixed(2) || '-' }}</span></div>
|
||||
<div><span class="text-gray-500">Max</span> <span class="font-bold">{{ calculatedStats?.max?.toFixed(2) || '-' }}</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Avertissement si élèves non évalués -->
|
||||
@@ -368,6 +316,7 @@ import { Bar } from 'vue-chartjs'
|
||||
import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js'
|
||||
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'
|
||||
import SendReportsModal from '@/components/assessment/SendReportsModal.vue'
|
||||
import PageHeader from '@/components/common/PageHeader.vue'
|
||||
|
||||
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend)
|
||||
|
||||
@@ -379,86 +328,7 @@ const configStore = useConfigStore()
|
||||
const selectedStudents = ref([])
|
||||
const showSendModal = ref(false)
|
||||
|
||||
// Color interpolation functions
|
||||
function hexToRgb(hex) {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : { r: 0, g: 0, b: 0 }
|
||||
}
|
||||
|
||||
function rgbToHex(r, g, b) {
|
||||
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)
|
||||
}
|
||||
|
||||
function rgbToHsl(r, g, b) {
|
||||
r /= 255; g /= 255; b /= 255
|
||||
const max = Math.max(r, g, b), min = Math.min(r, g, b)
|
||||
let h, s, l = (max + min) / 2
|
||||
|
||||
if (max === min) {
|
||||
h = s = 0
|
||||
} else {
|
||||
const d = max - min
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
||||
switch (max) {
|
||||
case r: h = (g - b) / d + (g < b ? 6 : 0); break
|
||||
case g: h = (b - r) / d + 2; break
|
||||
case b: h = (r - g) / d + 4; break
|
||||
}
|
||||
h /= 6
|
||||
}
|
||||
return { h: h * 360, s: s * 100, l: l * 100 }
|
||||
}
|
||||
|
||||
function hslToRgb(h, s, l) {
|
||||
h /= 360; s /= 100; l /= 100
|
||||
const hue2rgb = (p, q, t) => {
|
||||
if (t < 0) t += 1
|
||||
if (t > 1) t -= 1
|
||||
if (t < 1/6) return p + (q - p) * 6 * t
|
||||
if (t < 1/2) return q
|
||||
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6
|
||||
return p
|
||||
}
|
||||
let r, g, b
|
||||
if (s === 0) {
|
||||
r = g = b = l
|
||||
} else {
|
||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s
|
||||
const p = 2 * l - q
|
||||
r = hue2rgb(p, q, h + 1/3)
|
||||
g = hue2rgb(p, q, h)
|
||||
b = hue2rgb(p, q, h - 1/3)
|
||||
}
|
||||
return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) }
|
||||
}
|
||||
|
||||
function interpolateColorHSL(color1, color2, factor) {
|
||||
const rgb1 = hexToRgb(color1)
|
||||
const rgb2 = hexToRgb(color2)
|
||||
const hsl1 = rgbToHsl(rgb1.r, rgb1.g, rgb1.b)
|
||||
const hsl2 = rgbToHsl(rgb2.r, rgb2.g, rgb2.b)
|
||||
|
||||
let deltaH = hsl2.h - hsl1.h
|
||||
if (deltaH > 180) hsl2.h -= 360
|
||||
else if (deltaH < -180) hsl2.h += 360
|
||||
|
||||
const h = ((hsl1.h + (hsl2.h - hsl1.h) * factor) % 360 + 360) % 360
|
||||
const s = hsl1.s + (hsl2.s - hsl1.s) * factor
|
||||
const l = hsl1.l + (hsl2.l - hsl1.l) * factor
|
||||
|
||||
const rgb = hslToRgb(h, s, l)
|
||||
return rgbToHex(rgb.r, rgb.g, rgb.b)
|
||||
}
|
||||
|
||||
function getTextColorForBg(bgColor) {
|
||||
const rgb = hexToRgb(bgColor)
|
||||
const brightness = (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000
|
||||
return brightness > 128 ? '#000000' : '#ffffff'
|
||||
}
|
||||
import { interpolateColorHSL, getTextColorForBg } from '@/utils/colors'
|
||||
|
||||
// Get gradient color based on percentage
|
||||
function getGradientColor(percentage) {
|
||||
|
||||
Reference in New Issue
Block a user