mirror of https://github.com/halo-dev/halo-admin
feat: user management supports conditional filtering and sorting (#862)
#### What type of PR is this? /kind feature #### What this PR does / why we need it: 用户管理列表支持条件筛选和排序,适配:https://github.com/halo-dev/halo/pull/3320 #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/3290 #### Screenshots: <img width="922" alt="image" src="https://user-images.githubusercontent.com/21301288/219578426-de396dfb-7ece-496e-b740-d7a36321eafb.png"> #### Special notes for your reviewer: 测试方式: 1. Halo 需要切换到 https://github.com/halo-dev/halo/pull/3320 分支。 2. Console 需要 `pnpm build:packages` 3. 测试用户管理的关键词、条件、排序筛选是否工作正常。 #### Does this PR introduce a user-facing change? ```release-note Console 端的用户列表支持条件筛选和排序。 ```pull/867/head
parent
87d7592b29
commit
194ff7f4ad
|
@ -1,6 +1,5 @@
|
|||
.gitignore
|
||||
.npmignore
|
||||
.openapi-generator-ignore
|
||||
api.ts
|
||||
api/api-console-halo-run-v1alpha1-attachment-api.ts
|
||||
api/api-console-halo-run-v1alpha1-comment-api.ts
|
||||
|
|
|
@ -38,6 +38,8 @@ import { GrantRequest } from '../models'
|
|||
// @ts-ignore
|
||||
import { User } from '../models'
|
||||
// @ts-ignore
|
||||
import { UserList } from '../models'
|
||||
// @ts-ignore
|
||||
import { UserPermission } from '../models'
|
||||
/**
|
||||
* ApiConsoleHaloRunV1alpha1UserApi - axios parameter creator
|
||||
|
@ -223,6 +225,85 @@ export const ApiConsoleHaloRunV1alpha1UserApiAxiosParamCreator = function (confi
|
|||
options: localVarRequestOptions,
|
||||
}
|
||||
},
|
||||
/**
|
||||
* List users
|
||||
* @param {Array<string>} [sort] Sort property and direction of the list result. Supported fields: creationTimestamp
|
||||
* @param {string} [keyword]
|
||||
* @param {string} [role]
|
||||
* @param {number} [size] Size of one page. Zero indicates no limit.
|
||||
* @param {number} [page] The page number. Zero indicates no page.
|
||||
* @param {Array<string>} [labelSelector] Label selector for filtering.
|
||||
* @param {Array<string>} [fieldSelector] Field selector for filtering.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listUsers: async (
|
||||
sort?: Array<string>,
|
||||
keyword?: string,
|
||||
role?: string,
|
||||
size?: number,
|
||||
page?: number,
|
||||
labelSelector?: Array<string>,
|
||||
fieldSelector?: Array<string>,
|
||||
options: AxiosRequestConfig = {},
|
||||
): Promise<RequestArgs> => {
|
||||
const localVarPath = `/apis/api.console.halo.run/v1alpha1/users`
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL)
|
||||
let baseOptions
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options }
|
||||
const localVarHeaderParameter = {} as any
|
||||
const localVarQueryParameter = {} as any
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (sort) {
|
||||
localVarQueryParameter['sort'] = Array.from(sort)
|
||||
}
|
||||
|
||||
if (keyword !== undefined) {
|
||||
localVarQueryParameter['keyword'] = keyword
|
||||
}
|
||||
|
||||
if (role !== undefined) {
|
||||
localVarQueryParameter['role'] = role
|
||||
}
|
||||
|
||||
if (size !== undefined) {
|
||||
localVarQueryParameter['size'] = size
|
||||
}
|
||||
|
||||
if (page !== undefined) {
|
||||
localVarQueryParameter['page'] = page
|
||||
}
|
||||
|
||||
if (labelSelector) {
|
||||
localVarQueryParameter['labelSelector'] = labelSelector
|
||||
}
|
||||
|
||||
if (fieldSelector) {
|
||||
localVarQueryParameter['fieldSelector'] = fieldSelector
|
||||
}
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter)
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}
|
||||
localVarRequestOptions.headers = { ...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers }
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Update current user profile, but password.
|
||||
* @param {User} user
|
||||
|
@ -328,6 +409,40 @@ export const ApiConsoleHaloRunV1alpha1UserApiFp = function (configuration?: Conf
|
|||
const localVarAxiosArgs = await localVarAxiosParamCreator.grantPermission(name, grantRequest, options)
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)
|
||||
},
|
||||
/**
|
||||
* List users
|
||||
* @param {Array<string>} [sort] Sort property and direction of the list result. Supported fields: creationTimestamp
|
||||
* @param {string} [keyword]
|
||||
* @param {string} [role]
|
||||
* @param {number} [size] Size of one page. Zero indicates no limit.
|
||||
* @param {number} [page] The page number. Zero indicates no page.
|
||||
* @param {Array<string>} [labelSelector] Label selector for filtering.
|
||||
* @param {Array<string>} [fieldSelector] Field selector for filtering.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listUsers(
|
||||
sort?: Array<string>,
|
||||
keyword?: string,
|
||||
role?: string,
|
||||
size?: number,
|
||||
page?: number,
|
||||
labelSelector?: Array<string>,
|
||||
fieldSelector?: Array<string>,
|
||||
options?: AxiosRequestConfig,
|
||||
): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserList>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listUsers(
|
||||
sort,
|
||||
keyword,
|
||||
role,
|
||||
size,
|
||||
page,
|
||||
labelSelector,
|
||||
fieldSelector,
|
||||
options,
|
||||
)
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)
|
||||
},
|
||||
/**
|
||||
* Update current user profile, but password.
|
||||
* @param {User} user
|
||||
|
@ -403,6 +518,29 @@ export const ApiConsoleHaloRunV1alpha1UserApiFactory = function (
|
|||
.grantPermission(requestParameters.name, requestParameters.grantRequest, options)
|
||||
.then((request) => request(axios, basePath))
|
||||
},
|
||||
/**
|
||||
* List users
|
||||
* @param {ApiConsoleHaloRunV1alpha1UserApiListUsersRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listUsers(
|
||||
requestParameters: ApiConsoleHaloRunV1alpha1UserApiListUsersRequest = {},
|
||||
options?: AxiosRequestConfig,
|
||||
): AxiosPromise<UserList> {
|
||||
return localVarFp
|
||||
.listUsers(
|
||||
requestParameters.sort,
|
||||
requestParameters.keyword,
|
||||
requestParameters.role,
|
||||
requestParameters.size,
|
||||
requestParameters.page,
|
||||
requestParameters.labelSelector,
|
||||
requestParameters.fieldSelector,
|
||||
options,
|
||||
)
|
||||
.then((request) => request(axios, basePath))
|
||||
},
|
||||
/**
|
||||
* Update current user profile, but password.
|
||||
* @param {ApiConsoleHaloRunV1alpha1UserApiUpdateCurrentUserRequest} requestParameters Request parameters.
|
||||
|
@ -474,6 +612,62 @@ export interface ApiConsoleHaloRunV1alpha1UserApiGrantPermissionRequest {
|
|||
readonly grantRequest: GrantRequest
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for listUsers operation in ApiConsoleHaloRunV1alpha1UserApi.
|
||||
* @export
|
||||
* @interface ApiConsoleHaloRunV1alpha1UserApiListUsersRequest
|
||||
*/
|
||||
export interface ApiConsoleHaloRunV1alpha1UserApiListUsersRequest {
|
||||
/**
|
||||
* Sort property and direction of the list result. Supported fields: creationTimestamp
|
||||
* @type {Array<string>}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1UserApiListUsers
|
||||
*/
|
||||
readonly sort?: Array<string>
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1UserApiListUsers
|
||||
*/
|
||||
readonly keyword?: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1UserApiListUsers
|
||||
*/
|
||||
readonly role?: string
|
||||
|
||||
/**
|
||||
* Size of one page. Zero indicates no limit.
|
||||
* @type {number}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1UserApiListUsers
|
||||
*/
|
||||
readonly size?: number
|
||||
|
||||
/**
|
||||
* The page number. Zero indicates no page.
|
||||
* @type {number}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1UserApiListUsers
|
||||
*/
|
||||
readonly page?: number
|
||||
|
||||
/**
|
||||
* Label selector for filtering.
|
||||
* @type {Array<string>}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1UserApiListUsers
|
||||
*/
|
||||
readonly labelSelector?: Array<string>
|
||||
|
||||
/**
|
||||
* Field selector for filtering.
|
||||
* @type {Array<string>}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1UserApiListUsers
|
||||
*/
|
||||
readonly fieldSelector?: Array<string>
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for updateCurrentUser operation in ApiConsoleHaloRunV1alpha1UserApi.
|
||||
* @export
|
||||
|
@ -555,6 +749,31 @@ export class ApiConsoleHaloRunV1alpha1UserApi extends BaseAPI {
|
|||
.then((request) => request(this.axios, this.basePath))
|
||||
}
|
||||
|
||||
/**
|
||||
* List users
|
||||
* @param {ApiConsoleHaloRunV1alpha1UserApiListUsersRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1UserApi
|
||||
*/
|
||||
public listUsers(
|
||||
requestParameters: ApiConsoleHaloRunV1alpha1UserApiListUsersRequest = {},
|
||||
options?: AxiosRequestConfig,
|
||||
) {
|
||||
return ApiConsoleHaloRunV1alpha1UserApiFp(this.configuration)
|
||||
.listUsers(
|
||||
requestParameters.sort,
|
||||
requestParameters.keyword,
|
||||
requestParameters.role,
|
||||
requestParameters.size,
|
||||
requestParameters.page,
|
||||
requestParameters.labelSelector,
|
||||
requestParameters.fieldSelector,
|
||||
options,
|
||||
)
|
||||
.then((request) => request(this.axios, this.basePath))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update current user profile, but password.
|
||||
* @param {ApiConsoleHaloRunV1alpha1UserApiUpdateCurrentUserRequest} requestParameters Request parameters.
|
||||
|
|
|
@ -17,20 +17,25 @@ import {
|
|||
VStatusDot,
|
||||
VLoading,
|
||||
Toast,
|
||||
IconRefreshLine,
|
||||
VEmpty,
|
||||
} from "@halo-dev/components";
|
||||
import UserEditingModal from "./components/UserEditingModal.vue";
|
||||
import UserPasswordChangeModal from "./components/UserPasswordChangeModal.vue";
|
||||
import GrantPermissionModal from "./components/GrantPermissionModal.vue";
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import type { User, UserList } from "@halo-dev/api-client";
|
||||
import type { Role, User, UserList } from "@halo-dev/api-client";
|
||||
import { rbacAnnotations } from "@/constants/annotations";
|
||||
import { formatDatetime } from "@/utils/date";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import Fuse from "fuse.js";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { useRoleStore } from "@/stores/role";
|
||||
import { getNode } from "@formkit/core";
|
||||
import FilterTag from "@/components/filter/FilterTag.vue";
|
||||
import { useFetchRole } from "../roles/composables/use-role";
|
||||
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
||||
|
@ -53,16 +58,18 @@ const users = ref<UserList>({
|
|||
const loading = ref(false);
|
||||
const selectedUserNames = ref<string[]>([]);
|
||||
const selectedUser = ref<User>();
|
||||
const keyword = ref("");
|
||||
const refreshInterval = ref();
|
||||
|
||||
const userStore = useUserStore();
|
||||
const roleStore = useRoleStore();
|
||||
|
||||
let fuse: Fuse<User> | undefined = undefined;
|
||||
|
||||
const ANONYMOUSUSER_NAME = "anonymousUser";
|
||||
|
||||
const handleFetchUsers = async (options?: { mute?: boolean }) => {
|
||||
const handleFetchUsers = async (options?: {
|
||||
mute?: boolean;
|
||||
page?: number;
|
||||
}) => {
|
||||
try {
|
||||
clearInterval(refreshInterval.value);
|
||||
|
||||
|
@ -70,18 +77,22 @@ const handleFetchUsers = async (options?: { mute?: boolean }) => {
|
|||
loading.value = true;
|
||||
}
|
||||
|
||||
const { data } = await apiClient.extension.user.listv1alpha1User({
|
||||
if (options?.page) {
|
||||
users.value.page = options.page;
|
||||
}
|
||||
|
||||
const { data } = await apiClient.user.listUsers({
|
||||
page: users.value.page,
|
||||
size: users.value.size,
|
||||
keyword: keyword.value,
|
||||
fieldSelector: [`name!=${ANONYMOUSUSER_NAME}`],
|
||||
sort: [selectedSortItem.value?.value].filter(
|
||||
(item) => !!item
|
||||
) as string[],
|
||||
role: selectedRole.value?.metadata.name,
|
||||
});
|
||||
users.value = data;
|
||||
|
||||
fuse = new Fuse(data.items, {
|
||||
keys: ["spec.displayName", "metadata.name", "spec.email"],
|
||||
useExtendedSearch: true,
|
||||
threshold: 0.2,
|
||||
});
|
||||
users.value = data;
|
||||
|
||||
const deletedUsers = users.value.items.filter(
|
||||
(user) => !!user.metadata.deletionTimestamp
|
||||
|
@ -100,16 +111,6 @@ const handleFetchUsers = async (options?: { mute?: boolean }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const keyword = ref("");
|
||||
|
||||
const searchResults = computed(() => {
|
||||
if (!fuse || !keyword.value) {
|
||||
return users.value.items;
|
||||
}
|
||||
|
||||
return fuse?.search(keyword.value).map((item) => item.item);
|
||||
});
|
||||
|
||||
const handlePaginationChange = async ({
|
||||
page,
|
||||
size,
|
||||
|
@ -239,6 +240,62 @@ onMounted(() => {
|
|||
editingModal.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Filters
|
||||
function handleKeywordChange() {
|
||||
const keywordNode = getNode("keywordInput");
|
||||
if (keywordNode) {
|
||||
keyword.value = keywordNode._value as string;
|
||||
}
|
||||
handleFetchUsers({ page: 1 });
|
||||
}
|
||||
|
||||
function handleClearKeyword() {
|
||||
keyword.value = "";
|
||||
handleFetchUsers({ page: 1 });
|
||||
}
|
||||
|
||||
interface SortItem {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const SortItems: SortItem[] = [
|
||||
{
|
||||
label: "较近创建",
|
||||
value: "creationTimestamp,desc",
|
||||
},
|
||||
{
|
||||
label: "较早创建",
|
||||
value: "creationTimestamp,asc",
|
||||
},
|
||||
];
|
||||
|
||||
const selectedSortItem = ref<SortItem>();
|
||||
|
||||
function handleSortItemChange(sortItem?: SortItem) {
|
||||
selectedSortItem.value = sortItem;
|
||||
handleFetchUsers({ page: 1 });
|
||||
}
|
||||
|
||||
const { roles } = useFetchRole();
|
||||
const selectedRole = ref<Role>();
|
||||
|
||||
function handleRoleChange(role?: Role) {
|
||||
selectedRole.value = role;
|
||||
handleFetchUsers({ page: 1 });
|
||||
}
|
||||
|
||||
function handleClearFilters() {
|
||||
selectedRole.value = undefined;
|
||||
selectedSortItem.value = undefined;
|
||||
keyword.value = "";
|
||||
handleFetchUsers({ page: 1 });
|
||||
}
|
||||
|
||||
const hasFilters = computed(() => {
|
||||
return selectedRole.value || selectedSortItem.value || keyword.value;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<UserEditingModal
|
||||
|
@ -308,49 +365,53 @@ onMounted(() => {
|
|||
@change="handleCheckAllChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex w-full flex-1 sm:w-auto">
|
||||
<FormKit
|
||||
<div class="flex w-full flex-1 items-center sm:w-auto">
|
||||
<div
|
||||
v-if="!selectedUserNames.length"
|
||||
v-model="keyword"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<FormKit
|
||||
id="keywordInput"
|
||||
outer-class="!p-0"
|
||||
:model-value="keyword"
|
||||
name="keyword"
|
||||
placeholder="输入关键词搜索"
|
||||
type="text"
|
||||
@keyup.enter="handleKeywordChange"
|
||||
></FormKit>
|
||||
|
||||
<FilterTag v-if="keyword" @close="handleClearKeyword()">
|
||||
关键词:{{ keyword }}
|
||||
</FilterTag>
|
||||
|
||||
<FilterTag v-if="selectedRole" @close="handleRoleChange()">
|
||||
角色:{{
|
||||
selectedRole.metadata.annotations?.[
|
||||
rbacAnnotations.DISPLAY_NAME
|
||||
] || selectedRole.metadata.name
|
||||
}}
|
||||
</FilterTag>
|
||||
|
||||
<FilterTag
|
||||
v-if="selectedSortItem"
|
||||
@close="handleSortItemChange()"
|
||||
>
|
||||
排序:{{ selectedSortItem.label }}
|
||||
</FilterTag>
|
||||
|
||||
<FilterCleanButton
|
||||
v-if="hasFilters"
|
||||
@click="handleClearFilters"
|
||||
/>
|
||||
</div>
|
||||
<VSpace v-else>
|
||||
<VButton type="danger" @click="handleDeleteInBatch">
|
||||
删除
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</div>
|
||||
<div v-if="false" class="mt-4 flex sm:mt-0">
|
||||
<div class="mt-4 flex sm:mt-0">
|
||||
<VSpace spacing="lg">
|
||||
<FloatingDropdown>
|
||||
<div
|
||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
||||
>
|
||||
<span class="mr-0.5">状态</span>
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
</div>
|
||||
<template #popper>
|
||||
<div class="w-52 p-4">
|
||||
<ul class="space-y-1">
|
||||
<li
|
||||
v-close-popper
|
||||
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||
>
|
||||
<span class="truncate">正常</span>
|
||||
</li>
|
||||
<li
|
||||
v-close-popper
|
||||
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||
>
|
||||
<span class="truncate">已禁用</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</FloatingDropdown>
|
||||
<FloatingDropdown>
|
||||
<div
|
||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
||||
|
@ -364,28 +425,19 @@ onMounted(() => {
|
|||
<div class="w-52 p-4">
|
||||
<ul class="space-y-1">
|
||||
<li
|
||||
v-for="(role, index) in roles"
|
||||
:key="index"
|
||||
v-close-popper
|
||||
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||
@click="handleRoleChange(role)"
|
||||
>
|
||||
<span class="truncate">Super Administrator</span>
|
||||
</li>
|
||||
<li
|
||||
v-close-popper
|
||||
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||
>
|
||||
<span class="truncate">Administrator</span>
|
||||
</li>
|
||||
<li
|
||||
v-close-popper
|
||||
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||
>
|
||||
<span class="truncate">Editor</span>
|
||||
</li>
|
||||
<li
|
||||
v-close-popper
|
||||
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||
>
|
||||
<span class="truncate">Guest</span>
|
||||
<span class="truncate">
|
||||
{{
|
||||
role.metadata.annotations?.[
|
||||
rbacAnnotations.DISPLAY_NAME
|
||||
] || role.metadata.name
|
||||
}}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -404,33 +456,67 @@ onMounted(() => {
|
|||
<div class="w-72 p-4">
|
||||
<ul class="space-y-1">
|
||||
<li
|
||||
v-for="(sortItem, index) in SortItems"
|
||||
:key="index"
|
||||
v-close-popper
|
||||
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||
@click="handleSortItemChange(sortItem)"
|
||||
>
|
||||
<span class="truncate">较近登录</span>
|
||||
</li>
|
||||
<li
|
||||
v-close-popper
|
||||
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||
>
|
||||
<span class="truncate">较晚登录</span>
|
||||
<span class="truncate">{{ sortItem.label }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</FloatingDropdown>
|
||||
<div class="flex flex-row gap-2">
|
||||
<div
|
||||
class="group cursor-pointer rounded p-1 hover:bg-gray-200"
|
||||
@click="handleFetchUsers()"
|
||||
>
|
||||
<IconRefreshLine
|
||||
v-tooltip="`刷新`"
|
||||
:class="{ 'animate-spin text-gray-900': loading }"
|
||||
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VSpace>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<VLoading v-if="loading" />
|
||||
|
||||
<Transition v-else-if="!users.total" appear name="fade">
|
||||
<VEmpty
|
||||
message="当前没有符合筛选条件的用户,你可以尝试刷新或者创建新用户"
|
||||
title="当前没有符合筛选条件的用户"
|
||||
>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton @click="handleFetchUsers()">刷新</VButton>
|
||||
<VButton
|
||||
v-permission="['system:users:manage']"
|
||||
type="secondary"
|
||||
@click="editingModal = true"
|
||||
>
|
||||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
新建用户
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VEmpty>
|
||||
</Transition>
|
||||
|
||||
<Transition v-else appear name="fade">
|
||||
<ul
|
||||
class="box-border h-full w-full divide-y divide-gray-100"
|
||||
role="list"
|
||||
>
|
||||
<li v-for="(user, index) in searchResults" :key="index">
|
||||
<li v-for="(user, index) in users.items" :key="index">
|
||||
<VEntity :is-selected="checkSelection(user)">
|
||||
<template
|
||||
v-if="currentUserHasPermission(['system:users:manage'])"
|
||||
|
|
Loading…
Reference in New Issue