MainLayout.vue 8.93 KB
<template>
  <div class="layout-shell">
    <aside class="soft-sider" :class="{ collapsed }">
      <button class="sider-toggle" type="button" @click="collapsed = !collapsed">
        <menu-fold-outlined v-if="!collapsed" />
        <menu-unfold-outlined v-else />
      </button>

      <div class="brand-block">
        <div class="brand-mark">DL</div>
        <div v-if="!collapsed" class="brand-copy">
          <strong>地利骑手中台</strong>
          <span>Soft operations cockpit</span>
        </div>
      </div>

      <div class="menu-scroll">
        <a-menu
          v-model:selectedKeys="selectedKeys"
          mode="inline"
          @click="onMenuClick"
        >
          <a-menu-item key="/dashboard">
            <template #icon><home-outlined /></template>
            工作台
          </a-menu-item>
          <a-menu-item key="/city">
            <template #icon><global-outlined /></template>
            租户管理
          </a-menu-item>
          <a-menu-item key="/substation">
            <template #icon><apartment-outlined /></template>
            分站管理
          </a-menu-item>
          <a-sub-menu key="merchant">
            <template #icon><shop-outlined /></template>
            <template #title>商家管理</template>
            <a-menu-item key="/merchant/enter">入驻申请</a-menu-item>
            <a-menu-item key="/merchant/store">店铺管理</a-menu-item>
          </a-sub-menu>
          <a-menu-item key="/rider">
            <template #icon><user-outlined /></template>
            骑手管理
          </a-menu-item>
          <a-menu-item key="/rider/evaluate">
            <template #icon><star-outlined /></template>
            骑手评价
          </a-menu-item>
          <a-sub-menu key="orders">
            <template #icon><unordered-list-outlined /></template>
            <template #title>订单管理</template>
            <a-menu-item key="/order">订单列表</a-menu-item>
            <a-menu-item key="/refund">退款管理</a-menu-item>
            <a-menu-item key="/delivery/order">配送订单</a-menu-item>
          </a-sub-menu>
          <a-sub-menu key="open">
            <template #icon><api-outlined /></template>
            <template #title>开放平台</template>
            <a-menu-item key="/open">应用管理</a-menu-item>
            <a-menu-item key="/open/mock-delivery">模拟推单</a-menu-item>
          </a-sub-menu>
        </a-menu>
      </div>

      <div v-if="!collapsed" class="sider-foot">
        <div class="soft-chip soft-chip-green">在线协作</div>
        <p>面向运营、分站与骑手管理的统一轻量工作台。</p>
      </div>
    </aside>

    <main class="content-column">
      <header class="soft-topbar">
        <div>
          <p class="eyebrow">Soft-Neo Admin</p>
          <h1>{{ currentTitle }}</h1>
        </div>
        <div class="topbar-actions">
          <div class="date-pill">
            <calendar-outlined />
            <span>{{ todayLabel }}</span>
          </div>
          <a-dropdown>
            <a-button type="text" class="profile-button">
              <span class="profile-avatar">{{ avatarText }}</span>
              <span class="profile-copy">
                <strong>{{ auth.userInfo?.userNickname || '管理员' }}</strong>
                <small>{{ auth.userInfo?.role === 'admin' ? '超级管理员' : '分站管理员' }}</small>
              </span>
              <down-outlined />
            </a-button>
            <template #overlay>
              <a-menu>
                <a-menu-item @click="handleLogout">退出登录</a-menu-item>
              </a-menu>
            </template>
          </a-dropdown>
        </div>
      </header>

      <div class="soft-page-shell">
        <router-view />
      </div>
    </main>
  </div>
</template>

<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import {
  GlobalOutlined, ApartmentOutlined, ShopOutlined,
  UserOutlined, UnorderedListOutlined, ApiOutlined, DownOutlined, StarOutlined,
  CalendarOutlined, MenuFoldOutlined, MenuUnfoldOutlined, HomeOutlined
} from '@ant-design/icons-vue'

const router = useRouter()
const route = useRoute()
const auth = useAuthStore()
const collapsed = ref(false)
const selectedKeys = ref([route.path])
const titleMap: Record<string, string> = {
  '/dashboard': '工作台',
  '/city': '租户管理',
  '/substation': '分站管理',
  '/merchant/enter': '商家入驻',
  '/merchant/store': '店铺管理',
  '/rider': '骑手管理',
  '/rider/evaluate': '骑手评价',
  '/order': '订单列表',
  '/refund': '退款管理',
  '/delivery/order': '配送订单',
  '/open': '开放平台',
  '/open/mock-delivery': '模拟推单',
}

watch(() => route.path, (p) => { selectedKeys.value = [p] })

const currentTitle = computed(() => titleMap[route.path] || '外卖管理')
const avatarText = computed(() => (auth.userInfo?.userNickname || '管理员').slice(0, 1))
const todayLabel = computed(() => new Intl.DateTimeFormat('zh-CN', {
  month: 'long',
  day: 'numeric',
  weekday: 'short',
}).format(new Date()))

function onMenuClick({ key }: { key: string }) {
  router.push(key)
}

function handleLogout() {
  auth.logout()
  router.push('/login')
}
</script>

<style scoped>
.layout-shell {
  min-height: 100vh;
  display: grid;
  grid-template-columns: 280px minmax(0, 1fr);
  gap: 22px;
  padding: 22px;
}

.soft-sider,
.soft-topbar {
  border: 1px solid rgba(255, 255, 255, 0.58);
  background: rgba(255, 255, 255, 0.74);
  backdrop-filter: blur(22px);
  box-shadow: 0 16px 40px rgba(130, 110, 218, 0.12);
}

.soft-sider {
  position: sticky;
  top: 22px;
  height: calc(100vh - 44px);
  border-radius: 34px;
  padding: 18px 14px 18px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.menu-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  padding-right: 4px;
}

.menu-scroll::-webkit-scrollbar {
  width: 8px;
}

.menu-scroll::-webkit-scrollbar-thumb {
  border-radius: 999px;
  background: rgba(140, 124, 240, 0.24);
}

.soft-sider.collapsed {
  width: 88px;
}

.sider-toggle {
  align-self: flex-end;
  width: 44px;
  height: 44px;
  border-radius: 16px;
  border: none;
  background: rgba(246, 242, 255, 0.9);
  color: #7f6de5;
  cursor: pointer;
  margin-bottom: 12px;
}

.brand-block {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 10px 20px;
}

.brand-mark {
  width: 52px;
  height: 52px;
  border-radius: 18px;
  background: linear-gradient(145deg, #8c7cf0, #e6b5dc);
  color: white;
  font-family: 'Outfit', sans-serif;
  font-weight: 700;
  display: grid;
  place-items: center;
  box-shadow: 0 14px 24px rgba(140, 124, 240, 0.28);
}

.brand-copy {
  display: flex;
  flex-direction: column;
}

.brand-copy strong,
.profile-copy strong,
.hero-copy h2,
.insight-card strong,
h1 {
  font-family: 'Outfit', sans-serif;
  color: #2f2946;
}

.brand-copy span,
.profile-copy small,
.eyebrow,
.hero-copy p,
.insight-card p,
.note-list {
  color: #8d88a4;
}

:deep(.ant-menu) {
  min-height: 100%;
  background: transparent;
}

.sider-foot {
  margin-top: 14px;
  flex-shrink: 0;
  border-radius: 24px;
  background: linear-gradient(180deg, rgba(245, 241, 255, 0.85), rgba(255, 247, 250, 0.92));
  padding: 16px;
}

.soft-chip {
  display: inline-flex;
  align-items: center;
  border-radius: 999px;
  padding: 6px 12px;
  font-size: 12px;
  font-weight: 700;
}

.soft-chip-green {
  background: rgba(139, 212, 167, 0.22);
  color: #3d8f63;
}

.content-column {
  min-width: 0;
}

.soft-topbar {
  border-radius: 30px;
  padding: 18px 24px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
}

.soft-topbar h1 {
  margin: 4px 0 0;
  font-size: 30px;
}

.eyebrow {
  margin: 0;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-size: 11px;
  font-weight: 700;
}

.topbar-actions {
  display: flex;
  align-items: center;
  gap: 14px;
}

.date-pill,
.profile-button {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.82);
  border: 1px solid rgba(194, 184, 237, 0.38);
  padding: 10px 14px;
}

.profile-button {
  height: auto;
  padding-right: 12px;
}

.profile-avatar {
  width: 38px;
  height: 38px;
  border-radius: 14px;
  display: grid;
  place-items: center;
  background: linear-gradient(135deg, #a48ef4, #f1bfd8);
  color: white;
  font-weight: 700;
}

.profile-copy {
  display: flex;
  flex-direction: column;
  text-align: left;
}

@media (max-width: 960px) {
  .layout-shell {
    grid-template-columns: 1fr;
    padding: 14px;
  }

  .soft-sider {
    position: relative;
    top: 0;
    height: auto;
  }

  .menu-scroll {
    overflow: visible;
    padding-right: 0;
  }
  .soft-topbar {
    flex-direction: column;
    align-items: flex-start;
  }
}

@media (max-width: 720px) {
  .soft-sider.collapsed {
    width: auto;
  }

  .soft-topbar h1 {
    font-size: 26px;
  }

  .hero-copy h2 {
    font-size: 28px;
  }
}
</style>