Files
gao-customer-h5/src/pages/user/UserList.vue
T
2026-08-21 15:02:28 +08:00

235 lines
6.0 KiB
Vue

<template>
<div
v-can="permissionCodes.user.read"
class="user-list"
>
<t-pull-down-refresh
v-model="isRefreshing"
:refresh-timeout="30000"
@refresh="refresh"
@scrolltolower="fetch"
>
<t-cell-group theme="card">
<t-cell
class="item"
v-for="item in pageResult.list"
:key="item.id"
arrow
@click="toDetail(item)"
>
<template #left-icon>
<t-image :src="buildUserAvatar(item)" :alt="getUserName(item)" fit="cover" class="avatar" />
</template>
<template #title>
<div class="header">
<span class="name" v-text="getUserName(item)" />
<t-tag class="meta" size="small" theme="success" variant="outline" :content="getRoleName(item)" />
</div>
</template>
<template #description>
<telephone-link :value="item.user?.phoneNo" />
</template>
</t-cell>
</t-cell-group>
<div v-if="!pageResult.list.length" class="load-state" v-text="isLoading ? '正在加载用户' : '暂无用户'" />
<div v-if="pageResult.list.length" class="load-state" v-text="isNoMore ? '已经到底了' : '上拉加载更多'" />
</t-pull-down-refresh>
<div class="actions">
<t-button v-can="permissionCodes.user.create" theme="light" type="button" @click="toCreate">新增用户</t-button>
<t-button v-can="permissionCodes.user.create" theme="primary" type="button" @click="inviteVisible = true">邀请注册</t-button>
</div>
</div>
<user-invite v-model:visible="inviteVisible" :store-id="currentStoreStore.currentStoreId || undefined" />
</template>
<script setup lang="ts">
import { Toast } from "tdesign-mobile-vue";
import { type Page, type PageResult, UserAPI, type User, useUserStore } from "timi-web";
import ManagedUserAPI from "@/api/ManagedUserAPI";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import type { ManagedUser } from "@/types/ManagedUser";
import { usePermissionSettingsStore } from "@/store/permissionSettingsStore";
import { buildFallbackAvatar } from "@/utils/avatar";
import { getManagedUserRoleList, getRoleDisplayName } from "@/utils/role";
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
import UserInvite from "@/pages/user/component/UserInvite.vue";
defineOptions({
name: "UserList"
});
const router = useRouter();
const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
const permissionStore = usePermissionSettingsStore();
const inviteVisible = ref(false);
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
const userListOptions = computed(() => ({ storeId: currentStoreStore.currentStoreId }));
// ---------- 列表数据 ----------
const isRefreshing = ref(false);
const isLoading = ref(false);
const isNoMore = ref(false);
const page = reactive<Page<ManagedUser>>({
index: 0,
size: 20
});
const pageResult = reactive<PageResult<ManagedUser>>({
total: 0,
list: []
});
// ---------- 展示文案 ----------
function getUserName(item: ManagedUser): string {
return item.user?.nick || item.user?.name || "未命名用户";
}
function getRoleName(item: ManagedUser): string {
return getRoleDisplayName(getManagedUserRoleList(item)[0], permissionStore.roleList);
}
// ---------- 头像 ----------
function buildUserAvatar(item: ManagedUser): string {
return isUser(item.user) ? UserAPI.getAvatarURL(item.user) || buildFallbackAvatar(getUserName(item)) : buildFallbackAvatar(getUserName(item));
}
function isUser(user?: Partial<User>): user is User {
return typeof user?.name === "string";
}
// ---------- 页面数据 ----------
async function loadStoreScope(): Promise<void> {
if (isGlobalManager.value) {
await currentStoreStore.loadStoreList();
return;
}
await currentStoreStore.loadLoginStore();
}
async function loadData(): Promise<void> {
await loadStoreScope();
const [userResult] = await Promise.all([
ManagedUserAPI.list(page, userListOptions.value),
permissionStore.loadAll()
]);
pageResult.total = userResult.total;
pageResult.list = userResult.list || [];
page.index = 1;
isNoMore.value = pageResult.list.length >= pageResult.total || pageResult.list.length < page.size;
}
async function refresh(): Promise<void> {
isRefreshing.value = true;
try {
page.index = 0;
pageResult.total = 0;
pageResult.list = [];
isNoMore.value = false;
await fetch();
} finally {
isRefreshing.value = false;
}
}
async function fetch(): Promise<void> {
if (isLoading.value || isNoMore.value) {
return;
}
isLoading.value = true;
try {
const result = await ManagedUserAPI.list(page, userListOptions.value);
pageResult.total = result.total;
pageResult.list = pageResult.list.concat(result.list || []);
page.index += 1;
isNoMore.value = pageResult.list.length >= pageResult.total || (result.list || []).length < page.size;
} finally {
isLoading.value = false;
}
}
// ---------- 导航 ----------
async function toCreate(): Promise<void> {
await router.push({
path: "/management/users/create",
query: currentStoreStore.currentStoreId ? { storeId: currentStoreStore.currentStoreId } : undefined
});
}
async function toDetail(item: ManagedUser): Promise<void> {
if (!item.id) {
return;
}
await router.push(`/management/users/${item.id}`);
}
onMounted(async () => {
try {
await loadData();
} catch {
Toast.error("用户列表加载失败");
}
});
</script>
<style scoped lang="less">
.user-list {
padding: calc(var(--app-nav-height) + 1rem) 0 6rem 0;
display: grid;
gap: .75rem;
.item {
padding: 10px;
.avatar {
width: 3.5rem;
height: 3.5rem;
border-radius: .5rem;
}
.header {
flex: 1;
gap: .5rem;
display: flex;
align-items: center;
justify-content: space-between;
}
}
.actions {
left: 1rem;
right: 1rem;
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
position: fixed;
gap: .75rem;
display: grid;
grid-template-columns: 1fr 1fr;
}
}
:global(html.theme-dark) {
.user-list {
.item {
background: rgba(30, 41, 59, .9);
.name {
color: #e2e8f0;
}
.meta {
color: #94a3b8;
}
}
}
}
</style>