feat(admin): implement initial admin dashboard and member management
This commit introduces the foundational structure for the admin dashboard, focusing on the member management feature. Key additions include: - A new page at `/admin/manage/members` to display and manage members. - An `AddModal` component with a comprehensive form for creating new members, featuring validation with Zod and input masking. - A reusable `PhoneInput` component with country code selection, backed by a new `useCountries` composable and a full country dataset. - A custom, user-friendly global error page (`error.vue`) to handle application errors gracefully. - Updated dashboard sidebar navigation to include the new member management section. - Added recommended VS Code extensions and settings to improve developer experience.
This commit is contained in:
64
app/components/PhoneInput.vue
Normal file
64
app/components/PhoneInput.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<!-- ~/components/PhoneInput.vue -->
|
||||
<script setup lang="ts">
|
||||
import { USelectMenu, UInput } from "#components";
|
||||
import { useCountries } from "~/composables/useCountries";
|
||||
|
||||
interface Props {
|
||||
modelValue?: string;
|
||||
defaultDial?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits(["update:modelValue", "update:country"]);
|
||||
|
||||
const { getAll } = useCountries();
|
||||
|
||||
// 当前选中国家
|
||||
const selectedCountry = ref(
|
||||
getAll().find((c) => c.dial === props.defaultDial)?.dial || getAll()[0]?.dial
|
||||
);
|
||||
|
||||
console.table(getAll().map(c => ({ name: c.name.cn, dial: c.dial })));
|
||||
|
||||
// 用户输入的电话号码
|
||||
const phone = ref(props.modelValue || "");
|
||||
|
||||
// 计算选项
|
||||
const countryOptions = computed(() =>
|
||||
getAll()
|
||||
.filter((c) => c.dial && c.dial.trim() !== "")
|
||||
.map((c) => ({
|
||||
label: `${c.name.cn} (${c.dial || "未知"})`,
|
||||
id: c.dial || "unknown",
|
||||
}))
|
||||
);
|
||||
|
||||
// 完整号码输出
|
||||
const fullNumber = computed(() => {
|
||||
const dial = selectedCountry.value || "";
|
||||
return `${dial}${phone.value}`;
|
||||
});
|
||||
|
||||
// 双向绑定
|
||||
watch(phone, () => emit("update:modelValue", fullNumber.value));
|
||||
watch(selectedCountry, () => emit("update:country", selectedCountry.value));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- 国家区号选择 -->
|
||||
<USelectMenu
|
||||
v-model="selectedCountry"
|
||||
value-key="id"
|
||||
:items="countryOptions"
|
||||
/>
|
||||
|
||||
<!-- 电话号码输入 -->
|
||||
<UInput
|
||||
v-model="phone"
|
||||
placeholder="输入电话号码"
|
||||
type="tel"
|
||||
class="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user