feat: user creation support

Signed-off-by: Ryan Wang <i@ryanc.cc>
pull/588/head
Ryan Wang 2 years ago
parent efad1d8268
commit ce03f4c923

@ -34,11 +34,11 @@
"floating-vue": "2.0.0-beta.16",
"lodash.clonedeep": "^4.5.0",
"pinia": "^2.0.14",
"uuid": "^8.3.2",
"vue": "^3.2.37",
"vue-filepond": "^7.0.3",
"vue-grid-layout": "3.0.0-beta1",
"vue-router": "^4.0.16",
"vue-starport": "^0.3.0"
"vue-router": "^4.0.16"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.1.3",

@ -1,9 +1,8 @@
<script lang="ts" setup>
import { BasicLayout } from "@/layouts";
import { IconUpload, VButton, VTabbar } from "@halo-dev/components";
import { onMounted, provide, ref } from "vue";
import { onMounted, provide, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { Starport } from "vue-starport";
import { axiosInstance } from "@/utils/api-client";
import type { User } from "@/types/extension";
@ -49,16 +48,24 @@ provide("user", user);
const activeTab = ref();
const { name: currentRouteName } = useRoute();
const route = useRoute();
const router = useRouter();
// set default active tab
onMounted(() => {
handleFetchUser();
const tab = tabs.find((tab) => tab.routeName === currentRouteName);
const tab = tabs.find((tab) => tab.routeName === route.name);
activeTab.value = tab ? tab.id : tabs[0].id;
});
watch(
() => route.name,
async (newRouteName) => {
const tab = tabs.find((tab) => tab.routeName === newRouteName);
activeTab.value = tab ? tab.id : tabs[0].id;
}
);
const handleTabChange = (id: string) => {
const tab = tabs.find((tab) => tab.id === id);
if (tab) {
@ -73,17 +80,13 @@ const handleTabChange = (id: string) => {
<div class="px-4 sm:px-6 lg:px-8">
<div class="-mt-12 flex items-end space-x-5 sm:-mt-16">
<div class="flex">
<Starport
:duration="400"
:port="`user-profile-${user?.metadata?.name}`"
class="h-24 w-24 sm:h-32 sm:w-32"
>
<div class="h-24 w-24 sm:h-32 sm:w-32">
<img
:src="user?.spec?.avatar"
alt="Avatar"
class="h-full w-full rounded-full ring-4 ring-white drop-shadow-lg"
/>
</Starport>
</div>
</div>
<div
class="mt-6 sm:flex sm:min-w-0 sm:flex-1 sm:items-center sm:justify-end sm:space-x-6 sm:pb-1"

@ -42,6 +42,7 @@ importers:
tailwindcss-safe-area: ^0.2.2
tailwindcss-themeable: ^1.3.0
typescript: ~4.7.4
uuid: ^8.3.2
vite: ^2.9.12
vite-compression-plugin: ^0.0.4
vite-plugin-externals: ^0.5.0
@ -53,7 +54,6 @@ importers:
vue-filepond: ^7.0.3
vue-grid-layout: 3.0.0-beta1
vue-router: ^4.0.16
vue-starport: ^0.3.0
vue-tsc: ^0.34.17
dependencies:
'@halo-dev/admin-api': 1.1.0
@ -65,11 +65,11 @@ importers:
floating-vue: 2.0.0-beta.16_vue@3.2.37
lodash.clonedeep: 4.5.0
pinia: 2.0.14_j6bzmzd4ujpabbp5objtwxyjp4
uuid: 8.3.2
vue: 3.2.37
vue-filepond: 7.0.3_filepond@4.30.4+vue@3.2.37
vue-grid-layout: 3.0.0-beta1
vue-router: 4.0.16_vue@3.2.37
vue-starport: 0.3.0
devDependencies:
'@rushstack/eslint-patch': 1.1.3
'@tailwindcss/aspect-ratio': 0.4.0_tailwindcss@3.1.4
@ -6723,7 +6723,6 @@ packages:
/uuid/8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
dev: true
/v8-compile-cache/2.3.0:
resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==}
@ -7011,15 +7010,6 @@ packages:
'@vue/devtools-api': 6.1.4
vue: 3.2.37
/vue-starport/0.3.0:
resolution: {integrity: sha512-CfwYVxJDFqj7zoDw0TAMdNdpefuTdUH3rtupsadSa1je5Z7S/XwUCdxN0vVjBEEvWh33HmqjdK0IRQMWDlV7VQ==}
dependencies:
'@vueuse/core': 8.7.5_vue@3.2.37
vue: 3.2.37
transitivePeerDependencies:
- '@vue/composition-api'
dev: false
/vue-tsc/0.34.17_typescript@4.7.4:
resolution: {integrity: sha512-jzUXky44ZLHC4daaJag7FQr3idlPYN719/K1eObGljz5KaS2UnVGTU/XSYCd7d6ampYYg4OsyalbHyJIxV0aEQ==}
hasBin: true

@ -1,12 +1,9 @@
<script lang="ts" setup>
import { RouterView } from "vue-router";
import { StarportCarrier } from "vue-starport";
</script>
<template>
<StarportCarrier>
<RouterView />
</StarportCarrier>
<RouterView />
</template>
<style lang="scss">

@ -13,7 +13,6 @@ import { useRoute, useRouter } from "vue-router";
import { roles } from "@/modules/system/roles/roles-mock";
import { ref } from "vue";
import { users } from "@/modules/system/users/users-mock";
import { Starport } from "vue-starport";
const route = useRoute();
@ -128,11 +127,7 @@ const handleRouteToUser = (username: string) => {
<div class="flex items-center px-4 py-4">
<div class="flex min-w-0 flex-1 items-center">
<div class="flex-shrink-0">
<Starport
:duration="400"
:port="`user-profile-${user.name}`"
class="h-12 w-12"
>
<div class="h-12 w-12">
<div
class="overflow-hidden rounded border bg-white hover:shadow-sm"
>
@ -142,7 +137,7 @@ const handleRouteToUser = (username: string) => {
class="h-full w-full"
/>
</div>
</Starport>
</div>
</div>
<div
class="min-w-0 flex-1 px-4 md:grid md:grid-cols-2 md:gap-4"

@ -12,12 +12,13 @@ import {
VSpace,
VTag,
} from "@halo-dev/components";
import UserCreationModal from "./components/UserCreationModal.vue";
import { ref } from "vue";
import { Starport } from "vue-starport";
import { axiosInstance } from "@halo-dev/admin-shared";
import type { User } from "@/types/extension";
const checkAll = ref(false);
const creationModal = ref<boolean>(false);
const users = ref<User[]>([]);
const handleFetchUsers = async () => {
@ -32,6 +33,11 @@ const handleFetchUsers = async () => {
handleFetchUsers();
</script>
<template>
<UserCreationModal
v-model:visible="creationModal"
@close="handleFetchUsers"
/>
<VPageHeader title="用户">
<template #icon>
<IconUserSettings class="mr-2 self-center" />
@ -44,7 +50,7 @@ handleFetchUsers();
</template>
角色管理
</VButton>
<VButton type="secondary">
<VButton type="secondary" @click="creationModal = true">
<template #icon>
<IconAddCircle class="h-full w-full" />
</template>
@ -204,17 +210,13 @@ handleFetchUsers();
/>
</div>
<div v-if="user.spec.avatar" class="mr-4">
<Starport
:duration="400"
:port="`user-profile-${user.metadata.name}`"
class="h-12 w-12"
>
<div class="h-12 w-12">
<img
:alt="user.spec.displayName"
:src="user.spec.avatar"
class="h-full w-full overflow-hidden rounded border bg-white hover:shadow-sm"
/>
</Starport>
</div>
</div>
<div class="flex-1">
<div class="flex flex-row items-center">

@ -0,0 +1,167 @@
<script lang="ts" name="UserCreationModal" setup>
import type { PropType } from "vue";
import { computed, ref } from "vue";
import { axiosInstance } from "@halo-dev/admin-shared";
import {
IconSave,
VButton,
VInput,
VModal,
VTextarea,
} from "@halo-dev/components";
import type { User } from "@/types/extension";
import { v4 as uuid } from "uuid";
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
user: {
type: Object as PropType<User | null>,
default: null,
},
});
const emit = defineEmits(["update:visible", "close"]);
interface creationFormState {
user: User;
saving: boolean;
}
const creationForm = ref<creationFormState>({
user: {
spec: {
displayName: "",
avatar: "",
email: "",
phone: "",
password: "",
bio: "",
disabled: false,
loginHistoryLimit: 0,
},
apiVersion: "v1alpha1",
kind: "User",
metadata: {
name: uuid(),
},
},
saving: false,
});
const isUpdateMode = computed(() => {
return !!creationForm.value.user.metadata.creationTimestamp;
});
const creationModalTitle = computed(() => {
return isUpdateMode.value ? "编辑用户" : "新增用户";
});
const handleVisibleChange = (visible: boolean) => {
emit("update:visible", visible);
if (!visible) {
emit("close");
}
};
const handleCreateUser = async () => {
try {
creationForm.value.saving = true;
await axiosInstance.post("/api/v1alpha1/users", creationForm.value.user);
handleVisibleChange(false);
} catch (e) {
console.error(e);
} finally {
creationForm.value.saving = false;
}
};
</script>
<template>
<VModal
:title="creationModalTitle"
:visible="visible"
:width="700"
@update:visible="handleVisibleChange"
>
<form>
<div class="space-y-6 divide-y-0 sm:divide-y sm:divide-gray-200">
<div class="sm:grid sm:grid-cols-3 sm:items-start sm:gap-4 sm:pt-5">
<label
class="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"
>
用户名
</label>
<div class="mt-1 sm:col-span-2 sm:mt-0">
<VInput v-model="creationForm.user.metadata.name"></VInput>
</div>
</div>
<div class="sm:grid sm:grid-cols-3 sm:items-start sm:gap-4 sm:pt-5">
<label
class="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"
>
昵称
</label>
<div class="mt-1 sm:col-span-2 sm:mt-0">
<VInput v-model="creationForm.user.spec.displayName"></VInput>
</div>
</div>
<div class="sm:grid sm:grid-cols-3 sm:items-center sm:gap-4 sm:pt-5">
<label class="block text-sm font-medium text-gray-700">
电子邮箱
</label>
<div class="mt-1 sm:col-span-2 sm:mt-0">
<VInput v-model="creationForm.user.spec.email"></VInput>
</div>
</div>
<div class="sm:grid sm:grid-cols-3 sm:items-start sm:gap-4 sm:pt-5">
<label
class="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"
>
手机号
</label>
<div class="mt-1 sm:col-span-2 sm:mt-0">
<VInput v-model="creationForm.user.spec.phone"></VInput>
</div>
</div>
<div class="sm:grid sm:grid-cols-3 sm:items-start sm:gap-4 sm:pt-5">
<label
class="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"
>
头像
</label>
<div class="mt-1 sm:col-span-2 sm:mt-0">
<VInput v-model="creationForm.user.spec.avatar"></VInput>
</div>
</div>
<div class="sm:grid sm:grid-cols-3 sm:items-start sm:gap-4 sm:pt-5">
<label
class="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"
>
描述
</label>
<div class="mt-1 sm:col-span-2 sm:mt-0">
<VTextarea v-model="creationForm.user.spec.bio"></VTextarea>
</div>
</div>
</div>
</form>
<template #footer>
<VButton
:loading="creationForm.saving"
type="secondary"
@click="handleCreateUser"
>
<template #icon>
<IconSave class="h-full w-full" />
</template>
保存
</VButton>
</template>
</VModal>
</template>
Loading…
Cancel
Save