add NavBarActionMenu.vue
This commit is contained in:
@@ -0,0 +1,116 @@
|
|||||||
|
<template>
|
||||||
|
<teleport v-if="navBarStore.customRightVisible && visibleActionList.length" to="#app-nav-right">
|
||||||
|
<div class="nav-actions">
|
||||||
|
<t-popover
|
||||||
|
v-model:visible="actionVisible"
|
||||||
|
placement="bottom-right"
|
||||||
|
theme="light"
|
||||||
|
>
|
||||||
|
<t-button
|
||||||
|
class="trigger"
|
||||||
|
variant="text"
|
||||||
|
:aria-label="triggerLabel"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<t-icon :name="triggerIcon" />
|
||||||
|
</template>
|
||||||
|
<span v-if="triggerText" v-text="triggerText" />
|
||||||
|
</t-button>
|
||||||
|
<template #content>
|
||||||
|
<div class="menu">
|
||||||
|
<t-button
|
||||||
|
v-for="action in visibleActionList"
|
||||||
|
:key="action.key"
|
||||||
|
class="menu-item"
|
||||||
|
variant="text"
|
||||||
|
type="button"
|
||||||
|
:theme="action.theme"
|
||||||
|
:loading="action.loading"
|
||||||
|
:disabled="action.disabled || action.loading"
|
||||||
|
@click="handleAction(action)"
|
||||||
|
>
|
||||||
|
<template v-if="action.icon" #icon>
|
||||||
|
<t-icon :name="action.icon" />
|
||||||
|
</template>
|
||||||
|
<span v-text="action.text" />
|
||||||
|
</t-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</t-popover>
|
||||||
|
</div>
|
||||||
|
</teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
|
import type { NavBarAction } from "@/types/NavBarAction";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "NavBarActionMenu"
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
actions: readonly NavBarAction[];
|
||||||
|
triggerIcon?: string;
|
||||||
|
triggerLabel?: string;
|
||||||
|
triggerText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
triggerIcon: "ellipsis",
|
||||||
|
triggerLabel: "更多操作",
|
||||||
|
triggerText: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const navBarStore = useNavBarStore();
|
||||||
|
const actionVisible = ref(false);
|
||||||
|
const visibleActionList = computed(() => props.actions.filter(action => action.visible !== false));
|
||||||
|
|
||||||
|
async function handleAction(action: NavBarAction): Promise<void> {
|
||||||
|
actionVisible.value = false;
|
||||||
|
await action.action();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.nav-actions {
|
||||||
|
--td-popover-padding: 0 .5rem;
|
||||||
|
|
||||||
|
:deep(.t-popover__content) {
|
||||||
|
border-radius: .875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
gap: .25rem;
|
||||||
|
display: grid;
|
||||||
|
padding: .5rem 0;
|
||||||
|
min-width: 9rem;
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
--td-button-medium-height: 32px;
|
||||||
|
--td-button-medium-padding-horizontal: 10px;
|
||||||
|
|
||||||
|
gap: .25rem;
|
||||||
|
width: 100%;
|
||||||
|
font-weight: unset;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.theme-dark) .nav-actions {
|
||||||
|
:deep(.t-popover__content) {
|
||||||
|
color: #e2e8f0;
|
||||||
|
background: rgba(30, 41, 59, .98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
v-if="navBarStore.isShowing"
|
v-if="navBarStore.isShowing"
|
||||||
class="nav-bar"
|
class="nav-bar"
|
||||||
:left-arrow="!!navBarStore.canBack"
|
:left-arrow="!!navBarStore.canBack"
|
||||||
:z-index="10"
|
:z-index="20"
|
||||||
@left-click="doBack"
|
@left-click="doBack"
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
|
|||||||
@@ -72,28 +72,13 @@
|
|||||||
<div v-if="!pageResult.list.length" class="load-state" v-text="isLoading ? '正在加载客户' : '暂无客户数据'" />
|
<div v-if="!pageResult.list.length" class="load-state" v-text="isLoading ? '正在加载客户' : '暂无客户数据'" />
|
||||||
<div v-if="pageResult.list.length" class="load-state" v-text="isLoading ? '正在加载更多' : isNoMore ? '已经到底了' : '上拉加载更多'" />
|
<div v-if="pageResult.list.length" class="load-state" v-text="isLoading ? '正在加载更多' : isNoMore ? '已经到底了' : '上拉加载更多'" />
|
||||||
</t-pull-down-refresh>
|
</t-pull-down-refresh>
|
||||||
<div class="bottom-actions">
|
|
||||||
<t-button
|
|
||||||
v-can="permissionCodes.customer.export"
|
|
||||||
theme="primary"
|
|
||||||
variant="outline"
|
|
||||||
class="export bg-white"
|
|
||||||
:loading="isExporting"
|
|
||||||
:disabled="isExporting"
|
|
||||||
@click="doExport"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<t-icon name="download" />
|
|
||||||
</template>
|
|
||||||
导出
|
|
||||||
</t-button>
|
|
||||||
</div>
|
|
||||||
<tag-selector
|
<tag-selector
|
||||||
v-model="selectedTagIdList"
|
v-model="selectedTagIdList"
|
||||||
v-model:visible="tagSelectorVisible"
|
v-model:visible="tagSelectorVisible"
|
||||||
@change="handleTagFilterChanged"
|
@change="handleTagFilterChanged"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<nav-bar-action-menu v-if="canShowActionMenu" :actions="actionList" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -105,6 +90,7 @@ import TagSelector from "@/components/TagSelector.vue";
|
|||||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import type { Customer } from "@/types/Customer";
|
import type { Customer } from "@/types/Customer";
|
||||||
|
import type { NavBarAction } from "@/types/NavBarAction";
|
||||||
import type { Tag } from "@/types/Tag";
|
import type { Tag } from "@/types/Tag";
|
||||||
import { getCustomerPhotoURL } from "@/utils/customer";
|
import { getCustomerPhotoURL } from "@/utils/customer";
|
||||||
import { corePermissionCodes, gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
import { corePermissionCodes, gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||||
@@ -121,6 +107,32 @@ const isRefreshing = ref(false);
|
|||||||
const isExporting = ref(false);
|
const isExporting = ref(false);
|
||||||
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
|
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
|
||||||
const canReadTag = computed(() => userStore.hasPermissions(corePermissionCodes.tag.read));
|
const canReadTag = computed(() => userStore.hasPermissions(corePermissionCodes.tag.read));
|
||||||
|
const canCreateCustomer = computed(() => userStore.hasPermissions(permissionCodes.customer.create));
|
||||||
|
const canExportCustomer = computed(() => userStore.hasPermissions(permissionCodes.customer.export));
|
||||||
|
const canShowActionMenu = computed(() => canCreateCustomer.value || canExportCustomer.value);
|
||||||
|
|
||||||
|
const actionList = computed<NavBarAction[]>(() => {
|
||||||
|
const list: NavBarAction[] = [];
|
||||||
|
if (canCreateCustomer.value) {
|
||||||
|
list.push({
|
||||||
|
action: toCreate,
|
||||||
|
theme: "primary",
|
||||||
|
icon: "add",
|
||||||
|
key: "create",
|
||||||
|
text: "新增客户"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (canExportCustomer.value) {
|
||||||
|
list.push({
|
||||||
|
action: doExport,
|
||||||
|
icon: "download",
|
||||||
|
key: "export",
|
||||||
|
loading: isExporting.value,
|
||||||
|
text: "导出 Excel"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
|
||||||
// ---------- 标签筛选 ----------
|
// ---------- 标签筛选 ----------
|
||||||
|
|
||||||
@@ -199,9 +211,7 @@ onMounted(loadData);
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
navBarStore.clearRight();
|
navBarStore.clearRight();
|
||||||
if (userStore.hasPermissions(permissionCodes.customer.create)) {
|
navBarStore.setCustomRightVisible(canShowActionMenu.value);
|
||||||
navBarStore.setRightText("添加", "var(--td-brand-color)", toCreate);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(keyword, async () => {
|
watch(keyword, async () => {
|
||||||
@@ -244,7 +254,7 @@ async function doExport(): Promise<void> {
|
|||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.customer-list {
|
.customer-list {
|
||||||
padding: var(--app-nav-height) 0 5.5rem 0;
|
padding: var(--app-nav-height) 0 1rem 0;
|
||||||
|
|
||||||
.filter {
|
.filter {
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
@@ -288,7 +298,7 @@ async function doExport(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.list-wrapper {
|
.list-wrapper {
|
||||||
padding: 1rem 0 4rem 0;
|
padding: 1rem 0;
|
||||||
|
|
||||||
.items {
|
.items {
|
||||||
|
|
||||||
@@ -318,19 +328,6 @@ async function doExport(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-actions {
|
|
||||||
gap: .75rem;
|
|
||||||
left: 1rem;
|
|
||||||
right: 1rem;
|
|
||||||
display: flex;
|
|
||||||
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
|
|
||||||
position: fixed;
|
|
||||||
|
|
||||||
.export {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(html.theme-dark) {
|
:global(html.theme-dark) {
|
||||||
|
|||||||
+23
-76
@@ -45,50 +45,13 @@
|
|||||||
<div v-if="pageResult.list.length" class="load-state" v-text="isNoMore ? '已经到底了' : '上拉加载更多'" />
|
<div v-if="pageResult.list.length" class="load-state" v-text="isNoMore ? '已经到底了' : '上拉加载更多'" />
|
||||||
</t-pull-down-refresh>
|
</t-pull-down-refresh>
|
||||||
</div>
|
</div>
|
||||||
<teleport v-if="navBarStore.customRightVisible" to="#app-nav-right">
|
<nav-bar-action-menu
|
||||||
<div v-if="canCreateUser" class="user-actions">
|
v-if="canCreateUser"
|
||||||
<t-popover
|
:actions="actionList"
|
||||||
v-model:visible="actionVisible"
|
trigger-icon="add"
|
||||||
placement="bottom-right"
|
trigger-label="新增"
|
||||||
theme="light"
|
trigger-text="新增"
|
||||||
>
|
/>
|
||||||
<t-button class="trigger" theme="primary" variant="text">
|
|
||||||
<template #icon>
|
|
||||||
<t-icon name="add" />
|
|
||||||
</template>
|
|
||||||
<span>新增</span>
|
|
||||||
</t-button>
|
|
||||||
<template #content>
|
|
||||||
<div class="menu">
|
|
||||||
<t-button
|
|
||||||
v-can="permissionCodes.user.create"
|
|
||||||
class="menu-item"
|
|
||||||
variant="text"
|
|
||||||
type="button"
|
|
||||||
@click="toCreate"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<t-icon name="add" />
|
|
||||||
</template>
|
|
||||||
新增用户
|
|
||||||
</t-button>
|
|
||||||
<t-button
|
|
||||||
v-can="permissionCodes.user.create"
|
|
||||||
class="menu-item"
|
|
||||||
variant="text"
|
|
||||||
type="button"
|
|
||||||
@click="openInvite"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<t-icon name="qrcode" />
|
|
||||||
</template>
|
|
||||||
邀请注册
|
|
||||||
</t-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</t-popover>
|
|
||||||
</div>
|
|
||||||
</teleport>
|
|
||||||
<user-invite v-model:visible="inviteVisible" :store-id="currentStoreStore.currentStoreId || undefined" />
|
<user-invite v-model:visible="inviteVisible" :store-id="currentStoreStore.currentStoreId || undefined" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -102,6 +65,7 @@ import type { ManagedUser } from "@/types/ManagedUser";
|
|||||||
import { usePermissionSettingsStore } from "@/store/permissionSettingsStore";
|
import { usePermissionSettingsStore } from "@/store/permissionSettingsStore";
|
||||||
import { buildFallbackAvatar } from "@/utils/avatar";
|
import { buildFallbackAvatar } from "@/utils/avatar";
|
||||||
import { getManagedUserRoleList, getRoleDisplayName } from "@/utils/role";
|
import { getManagedUserRoleList, getRoleDisplayName } from "@/utils/role";
|
||||||
|
import type { NavBarAction } from "@/types/NavBarAction";
|
||||||
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||||
import UserInvite from "@/pages/user/component/UserInvite.vue";
|
import UserInvite from "@/pages/user/component/UserInvite.vue";
|
||||||
|
|
||||||
@@ -114,12 +78,26 @@ const userStore = useUserStore();
|
|||||||
const currentStoreStore = useCurrentStoreStore();
|
const currentStoreStore = useCurrentStoreStore();
|
||||||
const navBarStore = useNavBarStore();
|
const navBarStore = useNavBarStore();
|
||||||
const permissionStore = usePermissionSettingsStore();
|
const permissionStore = usePermissionSettingsStore();
|
||||||
const actionVisible = ref(false);
|
|
||||||
const inviteVisible = ref(false);
|
const inviteVisible = ref(false);
|
||||||
|
|
||||||
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
|
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
|
||||||
const canCreateUser = computed(() => userStore.hasPermissions(permissionCodes.user.create));
|
const canCreateUser = computed(() => userStore.hasPermissions(permissionCodes.user.create));
|
||||||
|
|
||||||
|
const actionList: NavBarAction[] = [
|
||||||
|
{
|
||||||
|
action: toCreate,
|
||||||
|
icon: "add",
|
||||||
|
key: "create",
|
||||||
|
text: "新增用户"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: openInvite,
|
||||||
|
icon: "qrcode",
|
||||||
|
key: "invite",
|
||||||
|
text: "邀请注册"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
const userListOptions = computed(() => ({ storeId: currentStoreStore.currentStoreId }));
|
const userListOptions = computed(() => ({ storeId: currentStoreStore.currentStoreId }));
|
||||||
|
|
||||||
// ---------- 列表数据 ----------
|
// ---------- 列表数据 ----------
|
||||||
@@ -226,7 +204,6 @@ async function fetch(): Promise<void> {
|
|||||||
// ---------- 导航 ----------
|
// ---------- 导航 ----------
|
||||||
|
|
||||||
async function toCreate(): Promise<void> {
|
async function toCreate(): Promise<void> {
|
||||||
actionVisible.value = false;
|
|
||||||
await router.push({
|
await router.push({
|
||||||
path: "/management/users/create",
|
path: "/management/users/create",
|
||||||
query: currentStoreStore.currentStoreId ? { storeId: currentStoreStore.currentStoreId } : undefined
|
query: currentStoreStore.currentStoreId ? { storeId: currentStoreStore.currentStoreId } : undefined
|
||||||
@@ -234,7 +211,6 @@ async function toCreate(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openInvite(): void {
|
function openInvite(): void {
|
||||||
actionVisible.value = false;
|
|
||||||
inviteVisible.value = true;
|
inviteVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,35 +266,6 @@ onUnmounted(() => navBarStore.clearRight());
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-actions {
|
|
||||||
--td-popover-padding: 0 .5rem;
|
|
||||||
|
|
||||||
:deep(.t-popover__content) {
|
|
||||||
border-radius: .875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trigger {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu {
|
|
||||||
gap: .25rem;
|
|
||||||
min-width: 8rem;
|
|
||||||
display: grid;
|
|
||||||
padding: .5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item {
|
|
||||||
--td-button-medium-height: 32px;
|
|
||||||
--td-button-medium-padding-horizontal: 10px;
|
|
||||||
|
|
||||||
gap: .25rem;
|
|
||||||
width: 100%;
|
|
||||||
font-weight: unset;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(html.theme-dark) {
|
:global(html.theme-dark) {
|
||||||
.user-list {
|
.user-list {
|
||||||
.item {
|
.item {
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { ButtonProps } from "tdesign-mobile-vue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顶部导航右侧菜单项。
|
||||||
|
*/
|
||||||
|
export type NavBarAction = {
|
||||||
|
action: () => void | Promise<void>;
|
||||||
|
disabled?: boolean;
|
||||||
|
icon?: string;
|
||||||
|
key: string;
|
||||||
|
loading?: boolean;
|
||||||
|
text: string;
|
||||||
|
theme?: ButtonProps["theme"];
|
||||||
|
visible?: boolean;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user