add store tab
This commit is contained in:
@@ -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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user