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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user