add store tab

This commit is contained in:
Timi
2026-08-11 19:53:04 +08:00
parent 1f8183034d
commit e612cfd382
4 changed files with 203 additions and 108 deletions
+5
View File
@@ -116,6 +116,7 @@ const navBarHeight = computed(() => {
const tabVal = ref(route.path); const tabVal = ref(route.path);
const list = [ const list = [
{ value: "/customers", icon: "usergroup", text: "客户" }, { value: "/customers", icon: "usergroup", text: "客户" },
{ value: "/store", icon: "shop", text: "门店" },
{ value: "/management", icon: "setting-1", text: "管理" } { value: "/management", icon: "setting-1", text: "管理" }
]; ];
@@ -128,6 +129,10 @@ function resolveTabValue(path: string): string {
return "/customers"; return "/customers";
} }
if (path.startsWith("/store")) {
return "/store";
}
if (path.startsWith("/management")) { if (path.startsWith("/management")) {
return "/management"; return "/management";
} }
+1 -108
View File
@@ -26,19 +26,6 @@
/> />
</template> </template>
</t-cell> </t-cell>
<t-cell
v-if="canViewStore"
class="cell"
title="当前门店"
:note="currentStoreStore.currentStoreName"
:arrow="canSelectStore"
:hover="canSelectStore"
@click="openStorePicker"
>
<template #leftIcon>
<t-icon name="shop" size="1.75rem" />
</template>
</t-cell>
</t-cell-group> </t-cell-group>
<t-cell-group v-for="group in visibleMenuGroupList" :key="group.title" :title="group.title" theme="card"> <t-cell-group v-for="group in visibleMenuGroupList" :key="group.title" :title="group.title" theme="card">
<t-cell <t-cell
@@ -60,34 +47,20 @@
<p>技术支持:夜雨</p> <p>技术支持:夜雨</p>
<p>Copyright © 2026 imyeyu.com</p> <p>Copyright © 2026 imyeyu.com</p>
</footer> </footer>
<t-popup v-model="storePickerVisible" placement="bottom">
<t-picker
v-model="storePickerDraft"
:columns="storePickerColumns"
title="当前门店"
@cancel="storePickerVisible = false"
@confirm="confirmStorePicker"
/>
</t-popup>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { UserAPI, useUserStore } from "timi-web"; import { UserAPI, useUserStore } from "timi-web";
import { Toast } from "tdesign-mobile-vue";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import { buildFallbackAvatar } from "@/utils/avatar"; import { buildFallbackAvatar } from "@/utils/avatar";
import { corePermissionCodes, gaoRoleCodes, gaoStoreScopePermissionCodes, permissionCodes } from "@/utils/permission"; import { corePermissionCodes, gaoRoleCodes, permissionCodes } from "@/utils/permission";
type UserAvatarSource = Parameters<typeof UserAPI.getAvatarURL>[0]; type UserAvatarSource = Parameters<typeof UserAPI.getAvatarURL>[0];
type PickerValue = string | number;
type MenuItem = { type MenuItem = {
icon: string; icon: string;
path: string; path: string;
title: string; title: string;
visible: boolean; visible: boolean;
action?: "store";
}; };
type MenuGroup = { type MenuGroup = {
@@ -101,15 +74,10 @@ defineOptions({
const router = useRouter(); const router = useRouter();
const userStore = useUserStore(); const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
const currentUser = computed(() => userStore.loginUser.user); const currentUser = computed(() => userStore.loginUser.user);
const currentUserAvatar = computed(() => buildUserAvatar(currentUser.value)); const currentUserAvatar = computed(() => buildUserAvatar(currentUser.value));
const canSelectStore = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
const canLoadLoginStore = computed(() => userStore.hasPermissions(gaoStoreScopePermissionCodes, "or"));
const canViewStore = computed(() => canSelectStore.value || canLoadLoginStore.value);
const canManageStore = computed(() => userStore.hasRoles([gaoRoleCodes.globalManager, gaoRoleCodes.storeManager], "or"));
const canManagePermission = computed(() => userStore.hasPermissions([ const canManagePermission = computed(() => userStore.hasPermissions([
corePermissionCodes.role.read, corePermissionCodes.role.read,
corePermissionCodes.permission.read corePermissionCodes.permission.read
@@ -122,53 +90,10 @@ const visibleMenuGroupList = computed<MenuGroup[]>(() => menuGroupList.value
})) }))
.filter(group => group.items.length)); .filter(group => group.items.length));
// ---------- 当前门店 ----------
const storePickerVisible = ref(false);
const storePickerDraft = ref<PickerValue[]>([]);
const storePickerColumns = computed(() => [
currentStoreStore.storeList.map(item => ({
label: item.name || item.code || "未命名门店",
value: item.id || ""
})).filter(item => item.value)
]);
function openStorePicker(): void {
if (!canSelectStore.value) {
return;
}
if (!currentStoreStore.storeList.length) {
Toast.warning("暂无可选门店");
return;
}
storePickerDraft.value = [currentStoreStore.currentStoreId];
storePickerVisible.value = true;
}
function confirmStorePicker(): void {
const storeId = String(storePickerDraft.value[0] || "");
if (!storeId) {
return;
}
currentStoreStore.selectStore(storeId);
storePickerVisible.value = false;
Toast.success("当前门店已切换");
}
const menuGroupList = computed<MenuGroup[]>(() => [ const menuGroupList = computed<MenuGroup[]>(() => [
{ {
title: "客户", title: "客户",
items: [ items: [
{
icon: "shop",
path: "/management/stores",
title: "门店管理",
action: "store",
visible: canManageStore.value && userStore.hasPermissions(permissionCodes.store.read)
},
{ {
icon: "usergroup", icon: "usergroup",
path: "/management/customers", path: "/management/customers",
@@ -192,12 +117,6 @@ const menuGroupList = computed<MenuGroup[]>(() => [
path: "/management/customer-data-charts", path: "/management/customer-data-charts",
title: "客户统计数据", title: "客户统计数据",
visible: userStore.hasPermissions(permissionCodes.stat.read) visible: userStore.hasPermissions(permissionCodes.stat.read)
},
{
icon: "chart",
path: "/management/data-charts",
title: "全局统计数据",
visible: userStore.hasPermissions(permissionCodes.stat.read)
} }
] ]
}, },
@@ -242,27 +161,8 @@ async function go(item: MenuItem | string): Promise<void> {
return; return;
} }
if (item.action === "store" && !canSelectStore.value) {
const store = await currentStoreStore.loadLoginStore();
if (!store?.id) {
Toast.warning("未绑定所属门店");
return;
}
await router.push(`/management/stores/${store.id}`);
return;
}
await router.push(item.path); await router.push(item.path);
} }
onMounted(async () => {
if (canSelectStore.value) {
await currentStoreStore.loadStoreList();
} else if (canLoadLoginStore.value) {
await currentStoreStore.loadLoginStore();
}
});
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
@@ -291,13 +191,6 @@ onMounted(async () => {
} }
} }
.store-card {
.cell {
padding: 10px;
}
}
.footer { .footer {
padding: 1.25rem 1rem calc(1rem + env(safe-area-inset-bottom, 0)); padding: 1.25rem 1rem calc(1rem + env(safe-area-inset-bottom, 0));
text-align: center; text-align: center;
+182
View File
@@ -0,0 +1,182 @@
<template>
<div class="store-tab">
<section v-if="canViewStore || canManageStore" class="store-header">
<t-cell-group class="store-card" theme="card">
<t-cell
v-if="canViewStore"
class="info"
title="当前门店"
:note="currentStoreStore.currentStoreName"
>
<template #description>
<div v-if="currentStore?.code" v-text="`编码:${currentStore.code}`" />
<div v-if="currentStore?.address" v-text="currentStore.address" />
<div v-if="currentStore?.telephone" v-text="`电话:${currentStore.telephone}`" />
</template>
<template #leftIcon>
<t-icon name="shop" size="1.75rem" />
</template>
</t-cell>
<t-cell
v-if="canSelectStore"
class="action"
title="切换门店"
note="选择数据统计门店"
arrow
hover
@click="openStorePicker"
>
<template #leftIcon>
<t-icon name="swap" size="1.75rem" />
</template>
</t-cell>
<t-cell
v-if="canManageStore"
class="action"
title="门店管理"
note="管理门店资料和营业日"
arrow
hover
@click="goStoreManagement"
>
<template #leftIcon>
<t-icon name="shop" size="1.75rem" />
</template>
</t-cell>
</t-cell-group>
</section>
<store-data-charts :key="currentStoreStore.currentStoreId" />
<t-popup v-model="storePickerVisible" placement="bottom">
<t-picker
v-model="storePickerDraft"
:columns="storePickerColumns"
title="切换门店"
@cancel="storePickerVisible = false"
@confirm="confirmStorePicker"
/>
</t-popup>
</div>
</template>
<script setup lang="ts">
import { Toast } from "tdesign-mobile-vue";
import { useUserStore } from "timi-web";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import { gaoRoleCodes, gaoStoreScopePermissionCodes, permissionCodes } from "@/utils/permission";
type PickerValue = string | number;
const StoreDataCharts = defineAsyncComponent(() => import("@/pages/store/StoreDataCharts.vue"));
defineOptions({
name: "StoreTab"
});
const router = useRouter();
const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
const currentStore = computed(() => currentStoreStore.currentStore);
const canSelectStore = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
const canLoadLoginStore = computed(() => userStore.hasPermissions(gaoStoreScopePermissionCodes, "or"));
const canViewStore = computed(() => canSelectStore.value || canLoadLoginStore.value);
const canManageStore = computed(() => userStore.hasRoles([
gaoRoleCodes.globalManager,
gaoRoleCodes.storeManager
], "or") && userStore.hasPermissions(permissionCodes.store.read));
// ---------- 门店切换 ----------
const storePickerVisible = ref(false);
const storePickerDraft = ref<PickerValue[]>([]);
const storePickerColumns = computed(() => [
currentStoreStore.storeList.map(item => ({
label: item.name || item.code || "未命名门店",
value: item.id || ""
})).filter(item => item.value)
]);
function openStorePicker(): void {
if (!canSelectStore.value) {
return;
}
if (!currentStoreStore.storeList.length) {
Toast.warning("暂无可选门店");
return;
}
storePickerDraft.value = [currentStoreStore.currentStoreId];
storePickerVisible.value = true;
}
function confirmStorePicker(): void {
const storeId = String(storePickerDraft.value[0] || "");
if (!storeId) {
return;
}
currentStoreStore.selectStore(storeId);
storePickerVisible.value = false;
Toast.success("当前门店已切换");
}
// ---------- 门店管理 ----------
async function goStoreManagement(): Promise<void> {
if (canSelectStore.value) {
await router.push("/management/stores");
return;
}
const store = await currentStoreStore.loadLoginStore();
if (!store?.id) {
Toast.warning("未绑定所属门店");
return;
}
await router.push(`/management/stores/${store.id}`);
}
onMounted(async () => {
if (canSelectStore.value) {
await currentStoreStore.loadStoreList();
} else if (canLoadLoginStore.value) {
await currentStoreStore.loadLoginStore();
}
});
</script>
<style scoped lang="less">
.store-tab {
.store-header {
padding: calc(var(--app-nav-height) + 1rem) 1rem 0 1rem;
.store-card {
.info {
:deep(.t-cell__description) {
gap: .25rem;
display: flex;
flex-direction: column;
}
}
}
}
:deep(.data-charts-page) {
padding-top: 1rem;
}
}
:global(html.theme-dark) {
.store-tab {
.store-card {
.info {
:deep(.t-cell__description) {
color: #94a3b8;
}
}
}
}
}
</style>
+15
View File
@@ -1,6 +1,7 @@
import type { RouteRecordRaw } from "vue-router"; import type { RouteRecordRaw } from "vue-router";
import HomeTab from "@/pages/tabs/HomeTab.vue"; import HomeTab from "@/pages/tabs/HomeTab.vue";
import SettingTab from "@/pages/tabs/SettingTab.vue"; import SettingTab from "@/pages/tabs/SettingTab.vue";
import StoreTab from "@/pages/tabs/StoreTab.vue";
const tabRoutes: RouteRecordRaw[] = [ const tabRoutes: RouteRecordRaw[] = [
{ {
@@ -17,6 +18,20 @@ const tabRoutes: RouteRecordRaw[] = [
darkBodyBackground: "#10161f" darkBodyBackground: "#10161f"
} }
}, },
{
path: "/store",
name: "StoreTab",
component: StoreTab,
meta: {
depth: 2,
navBarVisible: true,
navBarCanBack: false,
navBarTitle: "门店",
tabBarVisible: true,
bodyBackground: "#F5F5F8",
darkBodyBackground: "#10161f"
}
},
{ {
path: "/management", path: "/management",
name: "SettingTab", name: "SettingTab",