feat: production-harden app and finalize merge-ready UX/theme baseline
- complete tokenized theme preset system and admin style guide visibility - refine dashboard layout/scroll behavior and shared async/request UI states - add user nav-visibility settings across API, migration, and frontend stores - harden security headers/CSP handling and related test coverage - enforce CI repository hygiene + frontend contract/theme/build quality gates - update docs/env references and make migration smoke test head-aware
This commit is contained in:
parent
ab1d29b203
commit
ae2ca8f149
66 changed files with 5655 additions and 853 deletions
67
frontend/src/features/scholars/components/ScholarAvatar.vue
Normal file
67
frontend/src/features/scholars/components/ScholarAvatar.vue
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
label?: string | null;
|
||||
scholarId: string;
|
||||
imageUrl?: string | null;
|
||||
size?: "sm" | "md";
|
||||
}>(),
|
||||
{
|
||||
label: null,
|
||||
imageUrl: null,
|
||||
size: "md",
|
||||
},
|
||||
);
|
||||
|
||||
const imageFailed = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.imageUrl,
|
||||
() => {
|
||||
imageFailed.value = false;
|
||||
},
|
||||
);
|
||||
|
||||
const initials = computed(() => {
|
||||
const source = (props.label || "").trim();
|
||||
if (!source) {
|
||||
return props.scholarId.slice(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
const tokens = source.split(/\s+/).filter(Boolean);
|
||||
if (tokens.length === 1) {
|
||||
return tokens[0].slice(0, 2).toUpperCase();
|
||||
}
|
||||
return `${tokens[0].charAt(0)}${tokens[1].charAt(0)}`.toUpperCase();
|
||||
});
|
||||
|
||||
const containerClass = computed(() =>
|
||||
props.size === "sm" ? "h-10 w-10 text-[11px]" : "h-12 w-12 text-xs",
|
||||
);
|
||||
|
||||
const canRenderImage = computed(() => Boolean(props.imageUrl) && !imageFailed.value);
|
||||
|
||||
function onImageError(): void {
|
||||
imageFailed.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex shrink-0 items-center justify-center overflow-hidden rounded-full border border-stroke-default bg-surface-card-muted font-semibold text-ink-secondary"
|
||||
:class="containerClass"
|
||||
>
|
||||
<img
|
||||
v-if="canRenderImage"
|
||||
:src="props.imageUrl || ''"
|
||||
:alt="`${props.label || props.scholarId} profile image`"
|
||||
class="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
@error="onImageError"
|
||||
/>
|
||||
<span v-else>{{ initials }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -4,6 +4,7 @@ export interface UserSettings {
|
|||
auto_run_enabled: boolean;
|
||||
run_interval_minutes: number;
|
||||
request_delay_seconds: number;
|
||||
nav_visible_pages: string[];
|
||||
}
|
||||
|
||||
export interface ChangePasswordPayload {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue