【山竹记账前端-vue】2.页面划分和布局


大纲链接 §

[toc]


页面划分与布局

1. 前端路由 ⇧

RESTful 风格

  • 4个欢迎界面
    • /welcome/1
    • /welcome/2
    • /welcome/3
    • /welcome/4
  • /start 开始记账
  • /items/new 记一笔
  • /tags/new 新建标签
  • /tags/:id 标签详情
  • /items 记录首页
    • 浮层菜单 overlay 未登录/已登录
  • /sessions/new 登录 -> sign_in
  • /statistics 统计图表

2. 组件规划 ⇧

组件分析结果

  • 页面
    • /welcome/1~4
    • /start
    • /items/new
    • /tags/new
    • /tags/:id
    • /items
    • /sessions/new
    • /statistics
  • 组件
    • layout/welcome
    • layout/main
    • tabs
    • button
    • overlay
    • lineChart
    • pieChart
    • ...

组件库

  • echarts
  • vant-ui

3. 工作排期原则 ⇧

  • 越细越好
    • 每个页面的工作量是不同的
    • 每个组件的工作量是不同的
    • 不能一概而论
  • 留好buffer
    • 你预估的时间乘以n(1.2 < n < 3.14)

4. 拆分路由,并添加欢迎页面路由 ⇧

拆分路由表 src/router/routes.ts ⇧

添加欢迎页面路由表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import {FooComp} from '@/views/FooComp.tsx'
import {BarComp} from '@/views/BarComp.tsx'
import type {RouteRecordRaw} from 'vue-router'

export const routes: RouteRecordRaw[] = [
  // {path: '/', ...},
  // {path: '/about', ...},
  {
    path: '/welcome',
    name: 'welcome',
    component: FooComp,
    children: [
      {path: '/1', component: FooComp,},
      {path: '/2', component: FooComp,},
      {path: '/3', component: FooComp,},
      {path: '/4', component: FooComp,},
    ],
  },
]
  • 先用component: FooComp,组件占位
  • 注意这里路由表需要更具体的类型声明,而不仅仅是数组 const routes: RouteRecordRaw[] = []
  • 可以点开 createRouter({...}) 的类型提示查看路由表类型 RouteRecordRaw
  • 可以直接将类型抄过去声明 routes
  • 之后写路由表属性都会有类型提示
  • 排查嵌套路由报警告 ...No match found for location with path "/welcome/1"
    • fix: Add a route matching this path or check for typos in the location
    • 嵌套路由中多写了前缀 /,需要去除

src/router/routerHistory.ts

1
2
3
import {createWebHistory} from 'vue-router'

export const routerHistory = createWebHistory(import.meta.env.BASE_URL)
  • 参数 base
    • 基准路径,它被预置到每个 URL 上
    • 允许在一个域名子文件夹中托管 SPA
    • 例如将 base 设置为 /sub-folder 使得其托管在 example.com/sub-folder
    • vite 可以使用 import.meta.env.BASE_URL 获取基准路径
  • createWebHashHistory 无需配置服务器,但不会被搜索引擎处理,SEO 的效果较差,大多用于静态站点
  • history 配置将会做单独的重定向处理
  • 注意区分全局变量 Window.history 已存在;只能重命名为 routerHistory

@/router/index.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import {createRouter} from 'vue-router'
import {routes} from '@/router/routes.ts'
import {routerHistory} from '@/router/routerHistory.ts'

const router = createRouter({
  history: routerHistory,
  routes,
})

export default router

简单地 代码/模块/组件 的划分依据

  • 根据通过搜索文件名称直接找到对应的 代码/模块/组件
  • 搜 routes
  • 搜 history

欢迎页面路由 ⇧

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import {defineComponent,} from 'vue'
import {RouterView} from 'vue-router'

export const WelcomeView = defineComponent({
  name: 'WelcomeView',
  props: {},
  components: {},
  setup(/*props, ctx*/) {

    return () => (
      <>
        <RouterView/>
      </>
    )
  },
})
  • 注意在 tsx 文件中朱支持原 vue 中的中划线组件命名,需要全部改为大写驼峰风格
    • <RouterView/> ✅️
    • <router-view/> ❌️,识别不出,无提示

5. CSS 初始化 ⇧

额外需要安装 pnpm add -D sass-embedded,几乎零配置;

  • 使用 scss 预编译样式

重命名 base.scss

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/* color palette from <https://github.com/vuejs/theme> */
:root {}

/* semantic color variables for this project */
:root {}

@media (prefers-color-scheme: dark) {
  :root {}
}

body {
  margin: 0;
  min-height: 100vh;
  font-size: 16px;
  color: #333;
  line-height: 1.5;
}

添加 reset.scss

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
html {
  box-sizing: border-box;
  line-height: 1.15;
  -webkit-text-size-adjust: 100%;
}

* {
  box-sizing: inherit;
  margin: 0;
  padding: 0;
}

*::before,
*::after {
  box-sizing: border-box;
  font-weight: normal;
}

ul {
  list-style: none;
}

a {
  text-decoration: none;
  color: inherit;
}

button, input {
  font: inherit;
}

重命名 main.scss

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@use './reset.scss' as *;
@use './base.scss' as *;

#app {
  max-width: 1280px;
  margin: 0 auto;
  font-weight: normal;
}

@media (hover: hover) {
}

@media (min-width: 1024px) {
  body {
    display: flex;
    place-items: center;
  }

  #app {}
}

字体库 base.scss

  • 抄 Fonts.css – 跨平台中文字体解决方案

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    body {
    // ...
    font-family: -apple-system, "Noto Sans", "Helvetica Neue",
    Helvetica, "Nimbus Sans L", Arial, "Liberation Sans",
    "PingFang SC", "Hiragino Sans GB", "Noto Sans CJK SC",
    "Source Han Sans SC", "Source Han Sans CN", "Microsoft YaHei",
    "Wenquanyi Micro Hei", "WenQuanYi Zen Hei", "ST Heiti",
    SimHei, "WenQuanYi Zen Hei Sharp", sans-serif;
    }

6. 使用 CSS Modules ⇧

src/modules/welcome/Welcome.module.scss

1
.wrapper {color: red;}

改写 src/views/WelcomeView.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import {defineComponent,} from 'vue'
import {RouterView} from 'vue-router'
import s from '@/modules/welcome/Welcome.module.scss'

export const WelcomeView = defineComponent({
  name: 'WelcomeView',
  props: {},
  components: {},
  setup(/*props, ctx*/) {
    return () => (
      <div class={s.wrapper}>
        <header>logo</header>
        <main>
          <RouterView/>
        </main>
        <footer>buttons</footer>
      </div>
    )
  },
})
  • module.scss 基本可以模拟 vue scoped style 的行为
    • 会添加 _wrapper_${hash} 的类名来隔离样式

CSS Modules 会拍平嵌套样式 src/modules/welcome/Welcome.module.scss

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.wrapper {
  color: red;

  &:hover {
    color: lightgreen;
  }

  & > .title {
    color: teal;
  }
}

src/modules/welcome/WelcomeView.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import { defineComponent } from 'vue'
import { RouterView } from 'vue-router'
import s from '@/modules/welcome/welcome.module.scss'

export const WelcomeView = defineComponent({
  setup: (/* props, context */) => {
    console.log('WelcomeView', s)

    return () => (
      <div class={s.wrapper}>
        <header class={s.title}>
          <h1>山竹记账</h1>
        </header>
        <main>
          <RouterView />
        </main>
      </div>
    )
  },
})
  • 打印结果为 {wrapper: '_wrapper_44dd5_1', title: '_title_44dd5_7'}
  • 因为 module 会为样式生成单独的哈希名,用来隔离样式,
    • 但是如果样式嵌套的话,子样式可能不会生效,那么可以使用:global包括子样式。

7. 使用 JSX Scoped CSS 插件 ⇧

  • 暂不支持 Vue,目前仅支持 React、Solid
  • 略

8. 使用 UnoCSS ⇧

安装 ⇧

  • pnpm install -D unocss @unocss/vite @unocss/preset-attributify @unocss/preset-wind4

  • 其中单独安装的预设配置 @unocss/preset-attributify @unocss/preset-wind4


配置文件 ⇧

vite.config.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import {fileURLToPath, URL} from 'node:url'

import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
import unoCSS from 'unocss/vite'

// https://vite.dev/config/
export default defineConfig({
  base: '/mangosteen-fe-0-publish/',
  plugins: [
    unoCSS(),
    vue(),
    vueJsx({
      transformOn: true,
      mergeProps: true,
    }),
    vueDevTools(),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
})
  • 在 plugins: [unoCSS(), ...] 中无需写额外配置,直接在 uno.config.ts 中配置即可

uno.config.ts

1
2
3
4
5
6
7
8
9
import {defineConfig, presetAttributify} from 'unocss'
import {presetWind4} from '@unocss/preset-wind4'

export default defineConfig({
  presets: [
    presetWind4(),
    presetAttributify(),
  ],
})

tsconfig.app.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
  "exclude": ["src/**/__tests__/*"],
  "compilerOptions": {
    "types": ["unocss/types/global"],
    "paths": {
      "@/*": ["./src/*"]
    },
    "jsx": "preserve",
    "jsxImportSource": "vue",
  }
}

src/main.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from '@/router'
import {App} from '@/App.tsx'
import '@/assets/styles/main.scss'
import 'uno.css'
// import 'virtual:uno.css'

const app = createApp(App)

app.use(createPinia())
app.use(router)

app.mount('#app')
  • UnoCSS 采用的是即时(On-demand)引擎,硬盘里没有一个叫 uno.css 的物理文件
  • 启动开发服务器(npm run dev)或打包项目时,UnoCSS 的构建插件会拦截 virtual:uno.css 的导入请求
  • 插件会扫描你代码中用到的所有样式类名(如 m-1, text-red-500),实时在内存中将它们转换为对应的 CSS 代码,最后把这些代码塞进这个“虚拟文件”中
  • import 'virtual:uno.css' 和 import 'uno.css' 本质上等价
  • virtual: 是 Vite 插件体系中约定俗成的虚拟模块前缀,显式地告诉开发者和编译器“这是一个内存中的模块”
  • uno.css 则是 UnoCSS 官方为了让代码看起来更简洁、更符合直觉而提供的一个别名(Alias)。无论你写哪一个,UnoCSS 插件在底层处理时都是一样的

通过这种虚拟层设计,UnoCSS 实现了两个关键特性

  • 极致的性能与 HMR(热更新):当修改一个类名时,UnoCSS 只需要更新内存中的虚拟模块,浏览器会瞬间秒级应用新样式,而不需要重新读写硬盘文件
  • 零体积浪费:打包时,生成的 CSS 文件只包含你实际在项目中写过的类名,不会带有一丝多余的代码

参考


UnoCss 初学者学习使用方法 ⇧

  • 写出原本的 css 代码
  • 丢到ai搜索,给出 unocss 写法
  • 查询对应写法 unocss interactive

安装类样式合并工具 cn ⇧

  • pnpm add cn
  • 使用 <div class={cn("px-2 py-1", isActive && "bg-blue-500", { "text-white": isActive })}></div>

参考


9. 完成第一个界面 ⇧

  • 注意文件名重命名,由小写(小驼峰)改成大写(大驼峰)时,需要先改成一个其他名称,如 fack,再改成大驼峰名称
    • 需要被 git 识别,除非设置 git config --global core.ignorecase false
  • 仅适配 440 x 956,无设计图不适配,画草图,等认可后再写适配代码

src/assets/styles/vars.scss

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
:root {
  --welcome-card-bg-color: white;
  --welcome-text: white;
  --title-text: #D4D4EE;
  --primary-color: #6035BF;
}

/* semantic color variables for this project */
// :root {}

@media (prefers-color-scheme: dark) {
  // :root {}
}

src/assets/styles/main.scss

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@use './reset.scss' as *;
@use './base.scss' as *;
@use './vars.scss' as *;

#app {
  max-width: 1280px;
  margin: 0 auto;
  font-weight: normal;
}

//@media (hover: hover) {}

@media (min-width: 1024px) {
  body {
    display: flex;
    place-items: center;
  }
}

uno.config.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { defineConfig, presetAttributify } from 'unocss'
import { presetWind4 } from '@unocss/preset-wind4'

export default defineConfig({
  presets: [
    presetWind4(),
    presetAttributify(),
  ],
  theme: {
    colors: {
      // 绑定你的 CSS 变量
      welcomeCardBg: 'var(--welcome-card-bg-color)',
      primaryColor: 'var(--primary-color)',
    }
  }
})

src/views/WelcomeView.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { defineComponent } from 'vue'
import { RouterView } from 'vue-router'
import s from './WelcomeView.module.scss'
import logo from '@/assets/icons/mangosteen.svg'
import { cn } from 'cn'

export const WelcomeView = defineComponent({
  setup: (/* props, context */) => {
    return () => (
      <div class={s.wrapper}>
        <header class={s.title}>
          <img src={logo} alt="logo" />
          <h1>山竹记账</h1>
        </header>
        <main
          class={cn([
            'bg-welcomeCardBg mb-62px ml-16px mr-16px rounded-lg',
            'flex flex-col flex-grow items-center justify-around',
          ])}>
          <RouterView />
        </main>
      </div>
    )
  },
})

src/modules/welcome/WelCome1stPage.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import { defineComponent } from 'vue'
import { RouterLink } from 'vue-router'
import pig from '@/assets/icons/pig.svg'

export const WelCome1stPage = defineComponent({
  setup: (/* props, context */) => {
    return () => (
      <>
        <img src={pig} alt="icon" class="w-128px h-130px mt-25%" />
        <div class="description flex flex-col items-center text-[2em]">
          <h2>会挣钱</h2>
          <h2>还要会省钱</h2>
        </div>
        <div class="go-next text-primaryColor mb-84px text-[2em] font-bold">
          <RouterLink to="/welcome/2">下一页</RouterLink>
        </div>
      </>
    )
  },
})

·未完待续·

参考文章

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名