参考 B 站 --小霖同学-- 的教程入门一下quasar,然后我们给示例项目搞个页面。以下是前后端连接大概流程记录。

nvm alias default v24.12.0nvm use default  把默认node版本切换到这个lts版本,因为版本太老可能不行。

在合适的位置 npm init quasar 来创建 quasar项目,需要回答项目根目录名称、和vue3有关的配置等等。我们基本用默认选项,只是组件选择把状态管理 pinia、请求组件axios、i18n 和代码 linting 都选上,我们用 JavaScript (不用 TypeScript)。建完项目,根据提示安装完依赖,用 VSCode 添加项目根目录到工作区,准备工作差不多完成了。(默认用的是 vite 而非 webpack,务必注意,两个打包工具对应需要的在配置文件中的格式不一样!可以从quasar官网文档查看各自不同)

cd  PRJNAME,  npx  quasar  dev 启动本地web服务器就可以看到默认页面了。

我们先观察一下项目目录结构:src/router/routes.js 是项目包含的路由,只有 / 和 404。/ 对应的组件是布局模板 layouts/MainLayout.vue,子组件包含页面pages/IndexPage.vue,子组件对应 path 为空,也就是布局模板里面的<router-view />默认渲染这个子组件(其实就是首页index)。这里的 layouts 和 pages 划分和通常的想法一样,也就是 pages的东西是嵌入layouts的一部分(主体部分)。

参考 https://blog.csdn.net/weixin_43815091/article/details/124951953  修改 api 的基础URL:quasar.config.js 修改 (axios基础url用相对路径,如果用协议号http开始的绝对路径,开发服务器就不会捕获并执行proxy代理另外,我们路径额外加一个/api前缀并在proxy重写规则中去掉,如果用根目录/,配置会复杂一些。使用相对路径后,代理proxy会过滤请求,对于匹配上的请求,进行代理转发,即请求不再是浏览器到后端服务器,变成前端服务器到后端服务器,这样就避开了跨域问题)

build: {
.....................
     env: {
        apiBaseUrl: ctx.dev ? '/api' : 'http://localhost:8000',
      },
.....................
    devServer: {
      // https: true,
      open: true, // opens browser window automatically
      proxy: {
        '/api': {
          target: 'http://localhost:8000',
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/api/, ''),
        },
      },
    },

修改 boot/axios.js  (让 api 成为一个axios实例

const api = axios.create({
  baseURL: process.env.apiBaseUrl,
  timeout: 30000,
})

参考 https://help.crudapi.cn/guide.html  (源码网址 https://gitee.com/crudapi/crudapi-admin-web/blob/master/src/api/user.js),我们新建 src/api 目录,差不多就是把 GoFrame 的 *.http 文件中的各API请求包装一下方便使用。文件 src/api/post.js 如下:其中的Promise使用了 async/await 语法糖而非 then/catch

import { api } from 'boot/axios'

// axios 参考 https://axios-http.com/zh/docs/handling_errors
const post = {
  index: async function (params) {
    try {
      const response = await api.get('/post/index', {
        params: params,
      })
      console.log(response.data)
      return response.data
    } catch (error) {
      if (error.response) {
        // 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
        console.log(error.response.data)
        console.log(error.response.status)
        console.log(error.response.headers)
      } else if (error.request) {
        // 请求已经成功发起,但没有收到响应
        // `error.request` 在浏览器中是 XMLHttpRequest 的实例,
        // 而在node.js中是 http.ClientRequest 的实例
        console.log(error.request)
      } else {
        // 发送请求时出了点问题
        console.log('Error', error.message)
      }
      console.log(error.config)
    }
  },
  create: async function (params) {
    try {
      const response = await api.post('/post/create', params)
      console.log(response.data)
      return response.data
    } catch (error) {
      console.log(error)
    }
  },
  update: async function (id, params) {
    try {
      const response = await api.post('/post/update/' + id, params)
      console.log(response.data)
      return response.data
    } catch (error) {
      console.log(error)
    }
  },
  view: async function (id) {
    try {
      const response = await api.get('/post/view/' + id)
      console.log(response.data)
      return response.data
    } catch (error) {
      console.log(error)
    }
  },
  delete: async function (id) {
    try {
      const response = await api.post('/post/delete/' + id)
      console.log(response.data)
      return response.data
    } catch (error) {
      console.log(error)
    }
  },
}

export { post }

创建页面 src/pages/PostPage.vue (复习一下 <script setup>语法糖,ref响应对象创建,生命周期钩子 onMounted,异步函数使用,quasar表格基本使用)

<template>
  <q-page class="flex flex-center">
    <div class="q-pa-md">
      <q-table title="博客列表" :columns="columns" :rows="rows" row-key="id" />
    </div>
  </q-page>
</template>

<script setup>
import { post } from 'src/api/post'
import { onMounted, ref } from 'vue'

const columns = [
  {
    name: 'id',
    label: 'ID',
    field: 'id',
  },
  {
    name: 'title',
    label: '标题',
    field: 'title',
  },
  {
    name: 'content',
    label: '标题',
    field: 'content',
    format: (val) => {
      return val.substr(0, 20)
    },
  },
  {
    name: 'tags',
    label: '标签',
    field: 'tags',
  },
  {
    name: 'status',
    label: '状态',
    field: 'status',
  },
  {
    name: 'createdAt',
    label: '创建时间',
    field: 'createdAt',
    sortable: true,
  },
  {
    name: 'updatedAt',
    label: '更新时间',
    field: 'updatedAt',
  },
  {
    name: 'authorId',
    label: '作者',
    field: 'authorId',
  },
]
const rows = ref([])

onMounted(async () => {
  let res = await post.index()
  rows.value = res.data.list
})
</script>

路由 src/router/routes.js 添加

const routes = [
  {
    path: '/',
    component: () => import('layouts/MainLayout.vue'),
    children: [
      { path: '', component: () => import('pages/IndexPage.vue') },
      { path: '/post', component: () => import('pages/PostPage.vue') },
      { path: '/comment', component: () => import('pages/CommentPage.vue') },
    ],
  },
.........................

布局模板  src/layouts/MainLayout.vue中的链接改掉

const linksList = [
  {
    title: '博客',
    caption: '我的各种博客',
    icon: 'school',
    link: '#/post',
  },
  {
    title: '评论',
    caption: '用户浏览器后评论',
    icon: 'chat',
    link: '#/comment',
  },
...................

然后 npx  quasar   dev 启动本地服务器,浏览器自动打开(quasar.config.js中devServer部分莫默认设置是自动打开浏览器),点击“博客”菜单导航到 #/post,主视图中就用表格显示了博客。不过表格右下角关于页的提示信息默认是英文的,因为quasar框架的组件默认用英文,我么改一下配置文件 quasar.config.js  (如果要编写自己的支持i18n的组件,可以参考https://quasar.dev/options/app-internationalization

    framework: {
      config: {},
      lang: 'zh-CN',

作为GUI库,我们大部分时候关心的是样式风格、布局、可视组件,分别对应官网文档 (Documentation | Quasar FrameworkStyle and Identity、 Layout and Grid、Vue Components。我们大概了解一下有哪些可视组件(对应文档中 Vue Components):

  • Ajax Bar: 异步网络后台请求时,我们可以用这个让页面边缘显示进度(默认是页面底部)
  • Avatar:常见用于头像,也可用作展示性小图标,可以控制尺寸、颜色、方圆
  • Badge:徽章,主要是突出显示吸引注意的少量文字或图标,比如待阅读的数量,它是附属品,常和正文不在一个高度
  • Banner:旗帜条(常为上部长条),用于突出显示信息或者可选操作,如“登录成功”
  • Bar:贴顶条,用来容纳菜单,最大化、最小化、关闭等按钮
  • Breadcrumbs:面包屑,可以让人知道当前导航位置
  • Button、Button Group、Button Dropdown:按钮是最常用的可视化组件
  • Card:卡片,这是常见的信息展示容器
  • Carousel:转盘,可以在相对有限的空间用幻灯方式展示更多信息
  • Chat Message:聊天消息
  • Chip:一个界面块,用来容纳头像、文本、图标等东西,和徽章很像,但它是正文
  • Circular Progress:彩色的圆形进度指示器
  • Color Picker:颜色选择器
  • Dialog:对话框。它通常需要两个部分,对话框部分绑定一个模型变量,触发者让这个变量为true 则显示对话框。
  • Editor:编辑框,实现富文本编辑,WYSIWYG
  • Expansion Item:可展开的列表项,初始折叠,点击对应项目后可展示隐藏的详情内容
  • Floating Action Button:悬浮的动作按钮,在悬浮在主体内容的上面,常常显示一个小圆圈,点击可以弹出更多操作按钮
  • 表单组件: Input Textfield(文本框)、Select(下拉菜单)、File picker(文件选择)、Form(表单)、Field(装饰器、容器)、Radio(单选框)、Checkbox(复选框)、Toggle(开关指示)、Button Toggle(选中指示,类似Radio)、Option Group(用于分组radio/checkbox/toggle 的容器)、Slider(滑动条表示数量)、Ranger(滑动条指示范围)、Time Picker(时间选择)、Date Picker(日期选择)
  • Icon:方便在其他组件中嵌入图标
  • Img:方便嵌入图片,有加载进度指示
  • Infinite Scroll:无穷滚动让用户滚动时加载新的内容
  • Inner Loading:长加载耗时的时候显示动画,让用户可以看到应用在后台进行加载工作
  • Intersection:和 Intersection quasar指令相关,主要是DOM对象进入和移出视口过程中,能节约内存
  • Knob:圆形的数量展示,可以用鼠标或触控改变它的值,例如用于音量控制
  • Linear Progress:彩色的加载条,用来提醒用户某个动作在背后发生
  • List and List Items:用来进行列表展示,列表项可以是很复杂的
  • Markup Table:是对table的包装,呈现为Material风格,只用于少量简单的表格数据展示,需要复杂分页、排序、过滤功能的表格,应该用 QTable组件
  • Menu:菜单,通常是按钮弹出式
  • Resize Observer、Scroll Observer:尺寸改变、滚动时激发相应消息
  • Pagination:分页组件,外观上就是页码列表和上下翻页箭头
  • Parallax:视差滚动,滚动时背景图片滚动略慢造成一定视觉效果
  • Popup Edit:弹出式在地编辑,即点击要编辑目标,编辑器就在那个位置打开。可以和Table表格组件配合使用。
  • Popup Proxy:可以将弹出内容(如QMenu、QTooltip、QDialog、自定义浮层、上下文菜单等)与目标组件(按钮、输入框等)关联显示
  • Pull to refresh:下拉刷新
  • Rating:评级
  • Responsive:强制让内容保持纵横比
  • Scroll Area:可滚动区域,可以有自己的滚动条而不是浏览器提供的
  • Separator:分隔条,可以是横线,也可以是竖线
  • Skeleton:动态效果占位符,用于加载完之前显示
  • Slide Item:是列表项 QItem的增强版,可以向左、向右、向下、向上拖拉以便触发特定动作
  • Slide Transition:作用于单个DOM的滑动过渡效果
  • Space填充弹性盒子所有可用的空间,quasar组件都是弹性盒子
  • Spinners:动态视觉特性的图标,让用户感觉系统持续在工作
  • Splitter把容器分隔成可拖拉改变大小的部分
  • Stepper:用于展示步骤,让人知道当前做了哪几步,还剩几步
  • Table表格
  • Tabs:选项页,实现在同一个位置多个内容的切换显示
  • Tab Panels:选项面板,和Tabs有点像,但它针对面板而非整页,主要是为了在有限空间显示更多信息
  • Timeline:时间线,stepper是横向展示步骤,这个是纵向按时间展示事件信息
  • Toolbar:工具条,最常用于页头和页脚,和 bar 相比,它偏于布局。
  • Tooltip:工具提示,鼠标移上去展示提示信息
  • Tree树形结构,用来展示层次数据
  • Uploader:对于简单上传一个不大的文件,QFile更简单,QUploader主要是复杂的控制,例如多个文件,添加头部
  • Video:方便嵌入视频
  • Virtual Scroll:只显示长列表中部分数据,当用户在容器内滚动时更新可视数据,这样所需内存最小且渲染代价小

大概了解一下布局。quasar布局是基于弹性盒子(Flexbox)的,quasar提供了一些基于弹性盒子的CSS类,它们应用到父容器或者内容子项目完成布局。对于父容器,需要设定主轴方向(row [reverse]/row inline/column [reverse]/column inline)、包裹策略(溢出是否自动换行换列 wrap/no-wrap/reverse-wrap)、对齐方式(justify-xxx 内部子项间分布、items-xxx 沿主轴对齐、content-xxx整体内容在父容器的分布)。对于子项目,需要设定分布尺寸(quasar基于12列/行设计,col-n 表示占用n列,col-auto 尽量少占,col 有空间就多占,col-grow 至少占用所需空间,还有空间就增长,col-shrink 至多占用所需空间,空间不足就收缩)、自身对齐(self-xxx 可以让单个子项覆盖父容器的 items-xxx 对齐)、排序号(order-first/order-last/style="order: 2" 可以让布局排序不按源码中的顺序来)。quasar是基于响应式设计的,所以CSS类中有屏幕宽度大小(断点)设定:xs 超小屏是 [0px, 600px),sm 小屏是 [600px, 1024px),md 中屏是 [1024px, 1440px),lg 大屏是 [1440px, 1920px)xl 超大屏是 [1920px,+\infinity) 。屏幕断点也用于内外边距留空,例如 q-pa-xs、q-py-md-md 。quasar布局的核心概念是 行 row、列 col、间距 gutter。gutter的两种类型:q-gutter-{size}(需要间隔的元素不使用 col-*、offset-*)、q-col-gutter-{size}(需要间隔的元素使用了 col-* 、offset-*)其中尺寸后缀 -{size} 可以取 -none/-xs/-sm/-md/-lg/-xl,它表示屏幕尺寸,只是表示间隔相对大小q-gutter-{size}的逻辑是:额外用一个<div class="q-gutter-{size}"> 容器包裹需要间隔的子项,而 q-gutter-{size} 会对父容器应用负值的上边距和左边距,对每个子项应用正值(-none对应0)的上边距和左边距。q-col-gutter-{size}的逻辑类似。之所以需要额外的容器是因为会对父容器产生负值的top/left外边距,没有这个额外容器可能产生预期之外的副作用。总体布局的组件包括 Header、Footer、Drawer、Page (QPage必须被QPageContainer包裹,一般QPageContainer在布局文件,而 QPage在独立页面文件) 等。

大概了解一下样式。标题样式有 text-h1、text-h2、……、text-h6,副标题有 text-subtitle1、text-subtitle2。字体粗细有 text-weight-thin/light/regular/medium/bold/bolder。对齐辅助CSS类有 text-right/left/center/justify。字体风格辅助 text-bold/italic/no-wrap/strike/uppercase/lowercase/capitalize。品牌主色 primary蓝、secondary蓝绿、accent紫、dark黑、positive绿、negative红、info青、warning黄。颜色列表:red红、pink粉红、purple紫、deep-purple深紫、indigo靛蓝、blue蓝、light-blue浅蓝、cyan青、teal蓝绿、green绿、light-green浅绿、lime黄绿、yellow黄、amber琥珀色、orange橙、deep-orange橘红、brown棕、grey灰、blue-grey蓝灰。这些颜色用于字体(text-*)和背景(bg-*),颜色也可以使用定义了的变量,如 $primary、$success。阴影 no-shadow/shadow-{n}/shadow-transition/inset-shadow/inset-shadow-down。可见性 disabled/hidden/invisible/transparent/dimmed/ellipsis/z-top/z-max/print-only/print-hide。定位。变量。

quasar组件、布局、样式可以慢慢学,路由细节也可以慢慢学,还有两个问题:1、如何用pinia实现状态管理,哪些东西需要进行状态管理;2、登录获取令牌、在请求中携带令牌和令牌超时前刷新问题。

关于 pinia,主要概念是:1、stores/index.js 中 defineStore 中用 createPinia() 创建了pinia 实例。2、对于某个对象,例如 channel 对象,就是类似如下,store内是三个部分 state(管理的状态变量)、getters(只读函数)、actions(异步请求)

export const useChannelStore = defineStore('channel-store', {
  state: () => ({
    /** @type {{id:integer,networkVideoRecorderId:integer,enabled:boolean,order:integer,channelNo:integer,
     * channelName:string,filePrefix:string, createdAt:datetime, updatedAt:datetime }[]} */
    channels: [],
    /** @type {{
     * enabled: 'all' | 'enabled' | 'disabled',
     * nvrIds: integer[] | null
     * }} */
    filter: {
      enabled: 'all',
      nvrIds: null, // null = all
    },
  }),
  getters: {
    filteredChannels() {
      return this.channels.filter((channel) => {
        .........................
      })
    },
  },
  actions: {
    async fetchChannels() {
      try {
        let res = await channel.index()
        if (res.code === 0) {
          this.channels = res.data.list
        } else {
          console.error('error fetchChannels in channel.js', res.message)
        }
      } catch (error) {
        console.log(error)
      }
    },
    async updateChannel(id, params) {
      try {
        let res = await channel.update(id, params)
        if (res.code === 0) {
          if (res.data.rows_affected === 1) {
            return 1
          } else {
            console.error('0 rows updated in updateChannel')
            return 0
          }
        } else {
          console.error('error updating channel with id', id, ', error:', res.message)
        }
      } catch (e) {
        console.error(e)
      }
      return -1
    },
  },
})

3、使用 store:const storeChannel = useChannelStore()

本质上,状态管理就是既可以实现对变量值的缓存,又可以让变量值的变动从同一个入口进入,方便观察和诊断。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐