feat: add admin components for input, metrics, tables, and user forms

- Introduced AdminInput component for standardized input fields.
- Created AdminMetricCard for displaying metrics with customizable tones.
- Added AdminPlaceholderTable for loading states in tables.
- Developed AdminSectionCard for consistent section layouts.
- Implemented AdminSectionShell for organizing admin sections.
- Added AdminSelect for dropdown selections with v-model support.
- Created AdminTable for displaying tabular data with loading and empty states.
- Introduced AdminTextarea for multi-line text input.
- Developed AdminUserFormFields for user creation and editing forms.
- Added useAdminPageHeader composable for managing admin page header state.
This commit is contained in:
2026-03-24 07:08:44 +00:00
parent e854c68ad0
commit b60f65e4d1
100 changed files with 9270 additions and 2204 deletions

View File

@@ -1,7 +1,6 @@
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import { onMounted, ref, watch } from 'vue';
// Định nghĩa cấu trúc dữ liệu
interface SelectOption {
label: string;
value: string | number;
@@ -14,15 +13,14 @@ interface Props {
}
const props = withDefaults(defineProps<Props>(), {
placeholder: 'Vui lòng chọn...',
disabled: false
placeholder: 'Please select...',
disabled: false,
});
// Sử dụng defineModel thay cho props/emits thủ công
const modelValue = defineModel<string | number>();
const options = ref<SelectOption[]>([]);
const loading = ref<boolean>(false);
const loading = ref(false);
const error = ref<string | null>(null);
const fetchData = async () => {
@@ -30,55 +28,48 @@ const fetchData = async () => {
error.value = null;
try {
options.value = await props.loadOptions();
} catch (err) {
error.value = 'Lỗi kết nối';
} catch {
error.value = 'Failed to load options';
} finally {
loading.value = false;
}
};
onMounted(fetchData);
// Tự động load lại nếu hàm fetch thay đổi
watch(() => props.loadOptions, fetchData);
</script>
<template>
<div class="flex items-center gap-3">
<div class="relative w-full max-w-64">
<select
<div class="space-y-2">
<div class="relative w-full">
<select
v-model="modelValue"
:disabled="loading || disabled"
class="w-full appearance-none rounded-lg border border-gray-300 bg-white px-4 py-2 pr-10
text-gray-700 outline-none transition-all
focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20
disabled:bg-gray-50 disabled:text-gray-400 disabled:cursor-not-allowed"
class="w-full appearance-none rounded-md border border-border bg-header px-3 py-2 pr-10 text-sm text-foreground outline-none transition-all focus:border-primary/50 focus:ring-2 focus:ring-primary/30 disabled:cursor-not-allowed disabled:opacity-60"
>
<option value="" disabled>{{ placeholder }}</option>
<option
v-for="opt in options"
:key="opt.value"
<option
v-for="opt in options"
:key="opt.value"
:value="opt.value"
>
{{ opt.label }}
</option>
</select>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400">
<div class="i-carbon-chevron-down text-lg" />
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-foreground/40">
<div v-if="loading" class="h-4 w-4 animate-spin rounded-full border-2 border-current border-r-transparent" />
<div v-else class="i-carbon-chevron-down text-lg" />
</div>
</div>
<div v-if="loading" class="flex items-center text-blue-500">
<div class="i-carbon-circle-dash animate-spin text-xl" />
</div>
<button
v-if="error"
<button
v-if="error"
type="button"
@click="fetchData"
class="text-xs text-red-500 underline hover:text-red-600 transition"
class="text-xs font-medium text-danger transition hover:opacity-80"
>
Thử lại?
{{ error }} · Retry
</button>
</div>
</template>
</template>