Ver código fonte

feat: 页面框架

lxf 1 semana atrás
pai
commit
c660e5f517

+ 28 - 29
README.md

@@ -27,43 +27,42 @@ npm run dev
 
 ```
 src/
+├── layout/                # 后台框架(侧栏 + 顶栏)
+│   ├── index.vue
+│   ├── menu.js            # 左侧菜单配置
+│   └── MenuIcon.vue
 ├── api/
-│   ├── config.js          # 接口域名 / CDN / 忽略提示关键字
-│   └── modules/           # 接口模块(自动挂载为 VE_API.xxx)
-├── common/
-│   └── ol_common.js       # 地图公共方法(WMTS / WKT / fit)
-├── plugins/
-│   ├── axios.js           # 请求封装(token 走 localStorage)
-│   ├── element.js
-│   └── permission.js      # 路由守卫(title / token)
+│   ├── config.js
+│   └── modules/
 ├── router/
-│   ├── helper.js          # createPage(默认 keepAlive: true)
-│   ├── routes.js          # 路由
+│   ├── helper.js
+│   ├── routes.js          # Layout + children 业务路由
 │   └── index.js
-├── utils/auth.js          # token 读写
-├── views/home/            # 脚手架占位页(可替换)
-├── styles/
-├── App.vue                # 全局 keep-alive
-└── main.js
+├── views/
+│   ├── device/            # 设备管理
+│   ├── planting/          # 种植方案
+│   └── personnel/         # 人员管理
+└── ...
 ```
 
-## 新增页面(推荐写法)
+## 新增页面
 
-1. 新建 `src/views/xxx/index.vue`(纯 `<script setup>`)
-2. 在 `src/router/routes.js` 注册
+1. 新建 `src/views/xxx/index.vue`
+2. 在 `src/router/routes.js` 的 `children` 中追加
 
 ```js
-import { createPage } from "./helper";
-
-export default [
-  createPage({
-    path: "/xxx",
-    name: "Xxx",
-    component: () => import("@/views/xxx/index.vue"),
-    title: "页面标题",
-    // keepAlive: false, // 默认 true,不需要缓存时关闭
-  }),
-];
+createPage({
+  path: "xxx",
+  name: "Xxx",
+  component: () => import("@/views/xxx/index.vue"),
+  title: "页面标题",
+}),
+```
+
+3. 在 `src/layout/menu.js` 追加菜单项(图标可选):
+
+```js
+{ path: "/xxx", title: "页面标题", icon: "device" }
 ```
 
 ## 新增接口

+ 1 - 9
src/App.vue

@@ -1,14 +1,6 @@
 <template>
-  <router-view v-slot="{ Component }">
-    <keep-alive v-if="route.meta && route.meta.keepAlive !== false">
-      <component :is="Component" />
-    </keep-alive>
-    <component v-else :is="Component" />
-  </router-view>
+  <router-view />
 </template>
 
 <script setup>
-import { useRoute } from "vue-router";
-
-const route = useRoute();
 </script>

BIN
src/assets/font/HANTI.TTF


BIN
src/assets/font/PangMenZhengDao.TTF


BIN
src/assets/font/SOURCEHANSANSCN-NORMAL.OTF


BIN
src/assets/layout/device.png


BIN
src/assets/layout/leaf.png


BIN
src/assets/layout/logo.png


BIN
src/assets/layout/person.png


+ 59 - 0
src/layout/MenuIcon.vue

@@ -0,0 +1,59 @@
+<template>
+  <svg v-if="name === 'device'" viewBox="0 0 24 24" width="22" height="22" fill="none">
+    <path
+      d="M4 10c2.5-1 5-4.5 6.5-8 .8 3.5 2.8 6.5 6.5 8-3 .5-5.5 2-7 5-1.2-2-3.2-3.8-6-5z"
+      stroke="currentColor"
+      stroke-width="1.6"
+      stroke-linejoin="round"
+    />
+    <circle cx="9" cy="10" r="1.2" fill="currentColor" />
+    <circle cx="14" cy="10" r="1.2" fill="currentColor" />
+    <path d="M8 14.5c1 .8 2.2 1.3 3.5 1.3s2.5-.5 3.5-1.3" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" />
+  </svg>
+
+  <svg v-else-if="name === 'planting'" viewBox="0 0 24 24" width="22" height="22" fill="none">
+    <path
+      d="M12 21v-7"
+      stroke="currentColor"
+      stroke-width="1.6"
+      stroke-linecap="round"
+    />
+    <path
+      d="M12 14c-4.5 0-7-3-7-7 4.5 0 7 3 7 7z"
+      stroke="currentColor"
+      stroke-width="1.6"
+      stroke-linejoin="round"
+    />
+    <path
+      d="M12 11c2.2-2.5 5.5-3.5 8-3.2-1 4-4 6.2-8 6.2"
+      stroke="currentColor"
+      stroke-width="1.6"
+      stroke-linejoin="round"
+    />
+  </svg>
+
+  <svg v-else-if="name === 'personnel'" viewBox="0 0 24 24" width="22" height="22" fill="none">
+    <circle cx="12" cy="9" r="3.2" stroke="currentColor" stroke-width="1.6" />
+    <path
+      d="M6.5 19c.8-3 3-4.5 5.5-4.5S16.7 16 17.5 19"
+      stroke="currentColor"
+      stroke-width="1.6"
+      stroke-linecap="round"
+    />
+    <path
+      d="M8 6.5c1-1.5 2.5-2.3 4-2.3s3 .8 4 2.3"
+      stroke="currentColor"
+      stroke-width="1.6"
+      stroke-linecap="round"
+    />
+  </svg>
+</template>
+
+<script setup>
+defineProps({
+  name: {
+    type: String,
+    required: true,
+  },
+});
+</script>

+ 211 - 0
src/layout/index.vue

@@ -0,0 +1,211 @@
+<template>
+  <div class="layout">
+    <aside class="layout-aside">
+      <div class="brand">
+        <img class="brand-logo" :src="logo" alt="logo">
+        <span class="brand-title">飞鸟后台数据平台</span>
+      </div>
+
+      <nav class="side-menu">
+        <div v-for="item in sideMenus" :key="item.path" class="side-menu__item"
+          :class="{ 'is-active': isActive(item.path) }" @click="go(item.path)">
+          <span class="side-menu__icon">
+            <MenuIcon :name="item.icon" />
+          </span>
+          <span class="side-menu__text">{{ item.title }}</span>
+        </div>
+      </nav>
+    </aside>
+
+    <div class="layout-main">
+      <header class="layout-header">
+        <div class="header-right">
+          <el-dropdown trigger="click">
+            <div class="user-entry">
+              <el-avatar :size="32" class="user-avatar">
+                <el-icon :size="18">
+                  <UserFilled />
+                </el-icon>
+              </el-avatar>
+              <span class="user-name">用户001</span>
+              <el-icon class="user-arrow">
+                <ArrowDown />
+              </el-icon>
+            </div>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item disabled>个人中心</el-dropdown-item>
+                <el-dropdown-item divided>退出登录</el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+        </div>
+      </header>
+
+      <main class="layout-content">
+        <router-view v-slot="{ Component }">
+          <keep-alive v-if="route.meta && route.meta.keepAlive !== false">
+            <component :is="Component" />
+          </keep-alive>
+          <component v-else :is="Component" />
+        </router-view>
+      </main>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { useRoute, useRouter } from "vue-router";
+import { UserFilled, ArrowDown } from "@element-plus/icons-vue";
+import { sideMenus } from "./menu";
+import MenuIcon from "./MenuIcon.vue";
+
+import logo from "@/assets/layout/logo.png";
+
+const route = useRoute();
+const router = useRouter();
+
+const isActive = (path) => route.path === path || route.path.startsWith(path + "/");
+
+const go = (path) => {
+  if (route.path !== path) {
+    router.push(path);
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.layout {
+  display: flex;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  background: #f0f2f5;
+}
+
+.layout-aside {
+  width: 280px;
+  flex-shrink: 0;
+  background: #1a1a1a;
+  display: flex;
+  flex-direction: column;
+  color: #cfcfcf;
+}
+
+.brand {
+  height: 64px;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 0 16px;
+  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
+}
+
+.brand-logo {
+  width: 29px;
+  height: 32px;
+  object-fit: contain;
+}
+
+.brand-title {
+  font-size: 24px;
+  letter-spacing: 2px;
+  font-family: "PangMenZhengDao";
+  background: linear-gradient(to bottom, #F3C11D, #ffffff);
+  background-clip: text;
+  color: transparent;
+  text-decoration: none;
+}
+
+.side-menu {
+  padding: 16px 12px;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.side-menu__item {
+  height: 48px;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 0 16px;
+  border-radius: 8px;
+  cursor: pointer;
+  color: #9a9a9a;
+  transition: background 0.2s, color 0.2s;
+
+  &:hover {
+    color: #e8d48b;
+    background: rgba(212, 175, 55, 0.08);
+  }
+
+  &.is-active {
+    color: #d4af37;
+    background: linear-gradient(90deg, rgba(212, 175, 55, 0.28), rgba(212, 175, 55, 0.08));
+  }
+}
+
+.side-menu__icon {
+  display: inline-flex;
+  width: 22px;
+  height: 22px;
+}
+
+.side-menu__text {
+  font-size: 15px;
+}
+
+.layout-main {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+}
+
+.layout-header {
+  height: 64px;
+  flex-shrink: 0;
+  background: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  padding: 0 24px;
+  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.04);
+}
+
+.header-right {
+  display: flex;
+  align-items: center;
+}
+
+.user-entry {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  cursor: pointer;
+  outline: none;
+}
+
+.user-avatar {
+  background: #e8e8e8;
+  color: #8c8c8c;
+}
+
+.user-name {
+  font-size: 14px;
+  color: #333;
+}
+
+.user-arrow {
+  color: #999;
+  font-size: 12px;
+}
+
+.layout-content {
+  flex: 1;
+  min-height: 0;
+  overflow: auto;
+  background: #f5f5f5;
+}
+</style>

+ 17 - 0
src/layout/menu.js

@@ -0,0 +1,17 @@
+export const sideMenus = [
+  {
+    path: "/device",
+    title: "设备管理",
+    icon: "device",
+  },
+  {
+    path: "/planting",
+    title: "种植方案",
+    icon: "planting",
+  },
+  {
+    path: "/personnel",
+    title: "人员管理",
+    icon: "personnel",
+  },
+];

+ 28 - 17
src/router/routes.js

@@ -1,24 +1,35 @@
+import Layout from "@/layout/index.vue";
 import { createPage } from "./helper";
 
 /**
- * 页面路由表
- * - 默认 keepAlive: true,无需每个路由手写
- * - 不需要缓存时传 keepAlive: false
- * - 新增页面:在此追加 createPage(...) 即可
+ * 后台布局路由
+ * - Layout 负责侧栏 + 顶栏
+ * - children 为业务页面,左侧菜单点击切换
  */
 export default [
-  createPage({
+  {
     path: "/",
-    name: "Home",
-    component: () => import("@/views/home/index.vue"),
-    title: "首页",
-  }),
-
-  // 示例(按需取消注释):
-  // createPage({
-  //   path: "/demo",
-  //   name: "Demo",
-  //   component: () => import("@/views/demo/index.vue"),
-  //   title: "示例页",
-  // }),
+    component: Layout,
+    redirect: "/device",
+    children: [
+      createPage({
+        path: "device",
+        name: "Device",
+        component: () => import("@/views/device/index.vue"),
+        title: "设备管理",
+      }),
+      createPage({
+        path: "planting",
+        name: "Planting",
+        component: () => import("@/views/planting/index.vue"),
+        title: "种植方案",
+      }),
+      createPage({
+        path: "personnel",
+        name: "Personnel",
+        component: () => import("@/views/personnel/index.vue"),
+        title: "人员管理",
+      }),
+    ],
+  },
 ];

+ 6 - 0
src/styles/common.scss

@@ -32,3 +32,9 @@ h6 {
 #nprogress .bar {
   background: $base-color !important;
 }
+
+
+@font-face {
+  font-family: 'PangMenZhengDao';
+  src: url('@/assets/font/PangMenZhengDao.TTF') format('truetype');
+}

+ 7 - 7
src/styles/variables.scss.js

@@ -1,12 +1,12 @@
 const variables = {
   main_bg_color: "#f5f5f5",
-  base_color: "#409EFF",
-  nav_height: "80px",
-  side_close_width: "65px",
-  side_open_width: "160px",
-  sideBgColor: "#121213",
-  sideTextColor: "#fff",
-  sideActiveTextColor: "#ffd04b",
+  base_color: "#d4af37",
+  nav_height: "64px",
+  side_close_width: "64px",
+  side_open_width: "220px",
+  sideBgColor: "#1a1a1a",
+  sideTextColor: "#9a9a9a",
+  sideActiveTextColor: "#d4af37",
 };
 
 export default variables;

+ 29 - 0
src/views/device/index.vue

@@ -0,0 +1,29 @@
+<template>
+  <div class="page-placeholder">
+    <h2>设备管理</h2>
+    <p>页面内容区域,后续在此完善业务。</p>
+  </div>
+</template>
+
+<script setup>
+</script>
+
+<style lang="scss" scoped>
+.page-placeholder {
+  box-sizing: border-box;
+  min-height: 100%;
+  padding: 24px;
+  background: #fff;
+
+  h2 {
+    margin: 0 0 8px;
+    font-size: 20px;
+    color: #303133;
+  }
+
+  p {
+    margin: 0;
+    color: #909399;
+  }
+}
+</style>

+ 0 - 37
src/views/home/index.vue

@@ -1,37 +0,0 @@
-<template>
-  <div class="scaffold-home">
-    <h1>feiniao</h1>
-    <p>脚手架已就绪,在 <code>src/router/routes.js</code> 添加页面路由即可。</p>
-  </div>
-</template>
-
-<script setup>
-// 业务页面在 views 下新建,并通过 createPage 注册到 routes.js
-</script>
-
-<style lang="scss" scoped>
-.scaffold-home {
-  box-sizing: border-box;
-  min-height: 100%;
-  padding: 48px 24px;
-  background: linear-gradient(180deg, #eef5ff 0%, $main-bg-color 40%);
-
-  h1 {
-    margin: 0 0 12px;
-    font-size: 28px;
-    color: #1f2d3d;
-  }
-
-  p {
-    margin: 0;
-    color: #606266;
-  }
-
-  code {
-    padding: 2px 6px;
-    border-radius: 4px;
-    background: #fff;
-    color: $base-color;
-  }
-}
-</style>

+ 29 - 0
src/views/personnel/index.vue

@@ -0,0 +1,29 @@
+<template>
+  <div class="page-placeholder">
+    <h2>人员管理</h2>
+    <p>页面内容区域,后续在此完善业务。</p>
+  </div>
+</template>
+
+<script setup>
+</script>
+
+<style lang="scss" scoped>
+.page-placeholder {
+  box-sizing: border-box;
+  min-height: 100%;
+  padding: 24px;
+  background: #fff;
+
+  h2 {
+    margin: 0 0 8px;
+    font-size: 20px;
+    color: #303133;
+  }
+
+  p {
+    margin: 0;
+    color: #909399;
+  }
+}
+</style>

+ 29 - 0
src/views/planting/index.vue

@@ -0,0 +1,29 @@
+<template>
+  <div class="page-placeholder">
+    <h2>种植方案</h2>
+    <p>页面内容区域,后续在此完善业务。</p>
+  </div>
+</template>
+
+<script setup>
+</script>
+
+<style lang="scss" scoped>
+.page-placeholder {
+  box-sizing: border-box;
+  min-height: 100%;
+  padding: 24px;
+  background: #fff;
+
+  h2 {
+    margin: 0 0 8px;
+    font-size: 20px;
+    color: #303133;
+  }
+
+  p {
+    margin: 0;
+    color: #909399;
+  }
+}
+</style>