第二十八天:UI切版 & 元件-清单表格、弹出视窗

※ 今天的内容

一、清单表格:QTable、QMarkupTable
二、弹出视窗:QDialog、DialogPlugin
三、总结

一、清单表格:Table、QMarkupTable

用於清单资讯的呈现
Quasar 提供了两种表格

(一) QTable

根据定义的栏位、v-model的资料,自动产生表格的画面
可以依照自己的需求,使用分页、资料的格式与排序等等功能

QTable is a component that allows you to display data in a tabular manner. It’s generally called a datatable. It packs the following main features:

  • Filtering
  • Sorting
  • Single / Multiple rows selection with custom selection actions
  • Pagination (including server-side if required)
  • Grid mode (you can use for example QCards to display data in a non-tabular manner)
  • Total customization of rows and cells through scoped slots
  • Ability to add additional row(s) at top or bottom of data rows
  • Column picker (through QTableColumns component described in one of the sections)
  • Custom top and/or bottom Table controls
  • Responsive design

https://quasar.dev/vue-components/table#QTable-API

程序码示意:
https://ithelp.ithome.com.tw/upload/images/20201013/20120331lzwisQybG8.png

<template>
  <q-page class="q-pa-lg">
    <div class="full-width q-gutter-md">
      <h5 class="text-bold text-grey-9 q-mb-md">清单 (QTable)</h5>
      <q-table
        :data="table.data"
        :columns="table.columns"
        row-key="name"
        table-class="table"
        :pagination.sync="table.pagination"
        flat
      >
        <template v-slot:body-cell-status="props">
          <q-td :props="props">
            <div>
              <q-chip text-color="white" square 
                      :color="options.status.find(option => option.value === props.row.status).color" 
                      :label="options.status.find(option => option.value === props.row.status).label" />
            </div>
          </q-td>
        </template>
        <template v-slot:body-cell-operation="props">
          <q-td class="q-gutter-x-sm" :props="props">
            <q-btn unelevated color="green-7" @click="editDialog(props.row)">
              <span class="vertical_middle"><q-icon name="edit"></q-icon> 编辑</span>
            </q-btn>
            <q-btn unelevated color="red" @click="openCustomDialog('警告!', '确定要删除订单?', props.row)">
              <span class="vertical_middle"><q-icon name="delete"></q-icon> 删除</span>
            </q-btn>
          </q-td>
        </template>
      </q-table>
      <h5 class="text-bold text-grey-9 q-mb-md">清单 (QMarkupTable)</h5>
      <q-dialog v-model="editForm.isEdit">
        <q-card class="q-pa-lg" style="max-width: 500px; width: 100%">
          <h5 class="text-center text-bold q-mb-lg">编辑项目</h5>
          <div class="form">

          </div>
          <div class="row q-col-gutter-md">
            <div class="col-12">
              <q-input label="名称" stack-label outlined v-model="editForm.model.name"></q-input>
            </div>
            <div class="col-12">
              <q-input label="价格" stack-label outlined v-model="editForm.model.price"></q-input>
            </div>
            <div class="col-12">
              <q-select outlined v-model="editForm.model.status" :options="options.status" label="状态" emit-value map-options
      />
            </div>
            <div class="col-6">
              <q-btn v-close-popup unelevated color="primary" class="full-width" label="修改" @click="handleEdit"></q-btn>
            </div>
            <div class="col-6">
              <q-btn v-close-popup unelevated color="grey-7" class="full-width" label="取消"></q-btn>
            </div>
          </div>
        </q-card>
      </q-dialog>
    </div>
    
  </q-page>
</template>

<script>
import CustomDialog from 'src/components/CustomDialog.vue'

export default {
  data () {
    return {
      editForm: {
        data: null,
        model: {
          name: null,
          price: null,
          status: null
        },
        isEdit: false
      },
      table: {
        pagination: {
          sortBy: 'publish_date',
          descending: true,
          page: 1,
          rowsPerPage: 10
        },
        columns: [
          { name: 'name', field: 'name', align: 'center', label: '名称', sortable: true },
          { name: 'price', field: 'price', align: 'center', label: '商品价格', format: val => `$${val}`, sortable: true },
          { name: 'status', field: 'status', align: 'center', label: '上架状态', sortable: true },
          { name: 'publish_date', field: 'publish_date', align: 'center', label: '建立日期', sortable: true },
          { name: 'operation', field: 'operation', align: 'center', label: '操作' }
        ],
        data: [
          { id: 0, name: '名称1', price: 159, status: 0, publish_date: '2020-01-01' },
          { id: 1, name: '名称2', price: 237, status: 1, publish_date: '2020-01-11' },
          { id: 2, name: '名称3', price: 262, status: 0, publish_date: '2020-01-21' },
          { id: 3, name: '名称4', price: 305, status: 1, publish_date: '2020-01-31' },
          { id: 4, name: '名称5', price: 356, status: 0, publish_date: '2020-02-01' },
          { id: 5, name: '名称6', price: 375, status: 1, publish_date: '2020-04-01' },
          { id: 6, name: '名称7', price: 392, status: 0, publish_date: '2020-03-10' },
          { id: 7, name: '名称8', price: 408, status: 1, publish_date: '2020-07-01' },
          { id: 8, name: '名称9', price: 452, status: 0, publish_date: '2020-02-01' },
          { id: 9, name: '名称10', price: 518, status: 1, publish_date: '2020-03-01' }
        ]
      },
      options: {
        status: [
          { label: '下架', value: 0, color: 'grey-7' },
          { label: '上架', value: 1, color: 'cyan-8' }
        ]
      }
    }
  },
  methods: {
    editDialog (row) {
      this.editForm.isEdit = true

      for (let field in row) {
        this.editForm.model[field] = row[field]
      }
      this.editForm.data = row
    },
    handleEdit () {
      for (let field in this.editForm.model) {
        this.editForm.data[field] = this.editForm.model[field]
        this.editForm.model[field] = null
      }
    },
    openCustomDialog (title, text) {
      this.$q.dialog({
        component: CustomDialog,
        parent: this, 
        title: title,
        text: text
      }).onOk(() => {
        console.log('OK')

        let index = this.table.data.indexOf(row)
        this.table.data.splice(index, 1)
      }).onCancel(() => {
        console.log('Cancel')
      }).onDismiss(() => {
        console.log('Called on OK or Cancel')
      })
    }
  }
}
</script>

<style lang="scss" scoped>
  /deep/ .table {

  }
  /deep/ .table th {
    background-color: $grey-9;
    color: white;
    font-size: 16px;
  }

  /deep/ .table tbody td {
    font-size: 16px;
    color: $grey-9;
  }
</style>

其中几个比较重要的部分:
1.表格栏位

(1)栏位的属性名称:name、field
(2)栏位的显示文字:label
(3)内容对齐:align
(4)内容的格式处理:format
(5)栏位是否允许排序:sortable

<q-table
    :columns="table.columns"
>
columns: [
  { name: 'name', field: 'name', align: 'center', label: '名称', sortable: true },
  { name: 'price', field: 'price', align: 'center', label: '商品价格', format: val => `$${val}`, sortable: true },
  { name: 'status', field: 'status', align: 'center', label: '上架状态', sortable: true },
  { name: 'publish_date', field: 'publish_date', align: 'center', label: '建立日期', sortable: true },
  { name: 'operation', field: 'operation', align: 'center', label: '操作' }
],

2.表格资料
设定在QTable的data属性

<q-table
    :data="table.data"
>

每一个栏位(field)会对应到每一个资料的属性
只有定义在columns:[]的资料才会显示

data: [
  { id: 0, name: '名称1', price: 159, status: 0, publish_date: '2020-01-01' },
  { id: 1, name: '名称2', price: 237, status: 1, publish_date: '2020-01-11' },
  { id: 2, name: '名称3', price: 262, status: 0, publish_date: '2020-01-21' },
  { id: 3, name: '名称4', price: 305, status: 1, publish_date: '2020-01-31' },
  { id: 4, name: '名称5', price: 356, status: 0, publish_date: '2020-02-01' },
  { id: 5, name: '名称6', price: 375, status: 1, publish_date: '2020-04-01' },
  { id: 6, name: '名称7', price: 392, status: 0, publish_date: '2020-03-10' },
  { id: 7, name: '名称8', price: 408, status: 1, publish_date: '2020-07-01' },
  { id: 8, name: '名称9', price: 452, status: 0, publish_date: '2020-02-01' },
  { id: 9, name: '名称10', price: 518, status: 1, publish_date: '2020-03-01' }
]

3. 表格分页
设定在QTable的pagination属性

<q-table
    :pagination.sync="table.pagination"
>

(1)预设以什麽栏位排序: sortBy
(2)预设排序的方式:descending
(3)预设页数:page
(4)预设每一页的笔数:rowsPerPage

pagination: {
  sortBy: 'publish_date',
  descending: true,
  page: 1,
  rowsPerPage: 10
}

预设是clinet端处理分页的需求
如果要改成Server端处理分页
需要在QTable定义分页更换时、一页笔数更换时的查询方法(request event)

<q-table
    @request="onRequest"
>

@request触发後,可以透过props.pagination取得分页更换时、一页笔数更换时的分页参数
成功从後端查询後,必须自己写回在data定义的 pagination

并且在pagination当中设定rowsNumber

pagination: {
  rowsNumber:xx
}
onRequest (props) {
    const { page, rowsPerPage, sortBy, descending } = props.pagination
    
    // api request
    
    // update pagination in data ()
    this.pagination.page = page
    this.pagination.rowsPerPage = rowsPerPage
    this.pagination.rowsNumber = rowsNumber
    this.pagination.sortBy = sortBy
    this.pagination.descending = descending
}

4.自定义栏位内容
在<q-table>的slot里面使用 <template v-slot:body-cell-xxx="props">
透过props.row.xxx即可取得该列某个栏位的资料
xxx 是栏位的name

<template v-slot:body-cell-status="props">
  <q-td :props="props">
    <div>
      <q-chip text-color="white" square 
              :color="options.status.find(option => option.value === props.row.status).color" 
              :label="options.status.find(option => option.value === props.row.status).label" />
    </div>
  </q-td>
</template>

(二) QMarkupTable

相当於使用原生的<table>
没有任分页、资料的格式与排序等等功能

程序码示意:
https://ithelp.ithome.com.tw/upload/images/20201013/20120331pDm3O2eald.png

<template>
  <q-page class="q-pa-lg">
    <div class="full-width q-gutter-md">
      <h5 class="text-bold text-grey-9 q-mb-md">清单 (QMarkupTable)</h5>
      <q-markup-table class="table" flat>
        <thead>
          <tr>
            <th v-for="(field, index) in table.columns" :key="index">{{ field.label }}</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="row in table.data" :key="row.id">
            <td class="text-center">{{ row.name }}</td>
            <td class="text-center">{{ row.price }}</td>
            <td class="text-center">
              <q-chip text-color="white" square 
                      :color="options.status.find(option => option.value === row.status).color" 
                      :label="options.status.find(option => option.value === row.status).label" />
            </td>
            <td class="text-center">{{ row.publish_date }}</td>
            <td class="text-center q-gutter-x-sm">
              <q-btn unelevated color="green-7" @click="editDialog(row)">
                <span class="vertical_middle"><q-icon name="edit"></q-icon> 编辑</span>
              </q-btn>
              <q-btn unelevated color="red" @click="openCustomDialog('警告!', '确定要删除订单?', row)">
                <span class="vertical_middle"><q-icon name="delete"></q-icon> 删除</span>
              </q-btn>
            </td>
          </tr>
        </tbody>
      </q-markup-table>
      <q-dialog v-model="editForm.isEdit">
        <q-card class="q-pa-lg" style="max-width: 500px; width: 100%">
          <h5 class="text-center text-bold q-mb-lg">编辑项目</h5>
          <div class="form">

          </div>
          <div class="row q-col-gutter-md">
            <div class="col-12">
              <q-input label="名称" stack-label outlined v-model="editForm.model.name"></q-input>
            </div>
            <div class="col-12">
              <q-input label="价格" stack-label outlined v-model="editForm.model.price"></q-input>
            </div>
            <div class="col-12">
              <q-select outlined v-model="editForm.model.status" :options="options.status" label="状态" emit-value map-options
      />
            </div>
            <div class="col-6">
              <q-btn v-close-popup unelevated color="primary" class="full-width" label="修改" @click="handleEdit"></q-btn>
            </div>
            <div class="col-6">
              <q-btn v-close-popup unelevated color="grey-7" class="full-width" label="取消"></q-btn>
            </div>
          </div>
        </q-card>
      </q-dialog>
    </div>
    
  </q-page>
</template>

<script>
import CustomDialog from 'src/components/CustomDialog.vue'

export default {
  data () {
    return {
      editForm: {
        data: null,
        model: {
          name: null,
          price: null,
          status: null
        },
        isEdit: false
      },
      table: {
        columns: [
          { name: 'name', field: 'name', align: 'center', label: '名称', sortable: true },
          { name: 'price', field: 'price', align: 'center', label: '商品价格', format: val => `$${val}`, sortable: true },
          { name: 'status', field: 'status', align: 'center', label: '上架状态', sortable: true },
          { name: 'publish_date', field: 'publish_date', align: 'center', label: '建立日期', sortable: true },
          { name: 'operation', field: 'operation', align: 'center', label: '操作' }
        ],
        data: [
          { id: 0, name: '名称1', price: 159, status: 0, publish_date: '2020-01-01' },
          { id: 1, name: '名称2', price: 237, status: 1, publish_date: '2020-01-11' },
          { id: 2, name: '名称3', price: 262, status: 0, publish_date: '2020-01-21' },
          { id: 3, name: '名称4', price: 305, status: 1, publish_date: '2020-01-31' },
          { id: 4, name: '名称5', price: 356, status: 0, publish_date: '2020-02-01' },
          { id: 5, name: '名称6', price: 375, status: 1, publish_date: '2020-04-01' },
          { id: 6, name: '名称7', price: 392, status: 0, publish_date: '2020-03-10' },
          { id: 7, name: '名称8', price: 408, status: 1, publish_date: '2020-07-01' },
          { id: 8, name: '名称9', price: 452, status: 0, publish_date: '2020-02-01' },
          { id: 9, name: '名称10', price: 518, status: 1, publish_date: '2020-03-01' }
        ]
      },
      options: {
        status: [
          { label: '下架', value: 0, color: 'grey-7' },
          { label: '上架', value: 1, color: 'cyan-8' }
        ]
      }
    }
  },
  methods: {
    editDialog (row) {
      this.editForm.isEdit = true

      for (let field in row) {
        this.editForm.model[field] = row[field]
      }
      this.editForm.data = row
    },
    handleEdit () {
      for (let field in this.editForm.model) {
        this.editForm.data[field] = this.editForm.model[field]
        this.editForm.model[field] = null
      }
    },
    openCustomDialog (title, text) {
      this.$q.dialog({
        component: CustomDialog,
        parent: this, 
        title: title,
        text: text
      }).onOk(() => {
        console.log('OK')

        let index = this.table.data.indexOf(row)
        this.table.data.splice(index, 1)
      }).onCancel(() => {
        console.log('Cancel')
      }).onDismiss(() => {
        console.log('Called on OK or Cancel')
      })
    }
  }
}
</script>

<style lang="scss" scoped>
  /deep/ .table {

  }
  /deep/ .table th {
    background-color: $grey-9;
    color: white;
    font-size: 16px;
  }

  /deep/ .table tbody td {
    font-size: 16px;
    color: $grey-9;
  }
</style>

二、弹出视窗:QDialog、DialogPlugin

用於弹出资讯的视窗元件

(一) QDialog

你可以在页面里面使用<q-dialog>
使用v-model控制显示和隐藏

The QDialog component is a great way to offer the user the ability to choose a specific action or list of actions. They also can provide the user with important information, or require them to make a decision (or multiple decisions).

From a UI perspective, you can think of Dialogs as a type of floating modal, which covers only a portion of the screen. This means Dialogs should only be used for quick user actions, like verifying a password, getting a short App notification or selecting an option or options quickly.
https://quasar.dev/vue-components/dialog\

程序码如上面的范例:

<!-- src/pages/Index.vue -->

<q-dialog v-model="editForm.isEdit">
    <q-card class="q-pa-lg" style="max-width: 500px; width: 100%">
      <h5 class="text-center text-bold q-mb-lg">编辑项目</h5>
      <div class="form">

      </div>
      <div class="row q-col-gutter-md">
        <div class="col-12">
          <q-input label="名称" stack-label outlined v-model="editForm.model.name"></q-input>
        </div>
        <div class="col-12">
          <q-input label="价格" stack-label outlined v-model="editForm.model.price"></q-input>
        </div>
        <div class="col-12">
          <q-select outlined v-model="editForm.model.status" :options="options.status" label="状态" emit-value map-options
  />
        </div>
        <div class="col-6">
          <q-btn v-close-popup unelevated color="primary" class="full-width" label="修改" @click="handleEdit"></q-btn>
        </div>
        <div class="col-6">
          <q-btn v-close-popup unelevated color="grey-7" class="full-width" label="取消"></q-btn>
        </div>
      </div>
    </q-card>
</q-dialog>

除了设定Dialog 的 v-model=false关闭之外,
Quasar 有提供 「Close Popup Directive」
可以套用在按钮上面,按下时关闭Dialog,而不用修改QDialog的v-model

This directive is a helper when dealing with QDialog and QMenu components. When attached to a DOM element or component then that component will close the QDialog or QMenu (whichever is first parent) when clicked/tapped.
https://quasar.dev/vue-directives/close-popup#Introduction

<q-btn v-close-popup></q-btn>

(二) DialogPlugin

你也可以自订一个Dialog元件,使用DialogPlugin呼叫全域的Dialog

you can also supply a component for the Dialog Plugin to render (see the “Invoking custom component” section) which is a great way to avoid cluttering your Vue templates with inline dialogs (and it will also help you better organize your project files and also reuse dialogs).
https://quasar.dev/quasar-plugins/dialog

官方文件有示范DialogPlugin预设样式的范例
以下是呼叫自订全域Dialog的程序示意

呼叫方式:使用this.$q.dialog
在参数当中,必须指定要呼叫的Dialog元件
在元件当中,定义onOk、onCancel的按钮

// src/pages/Index.vue
openCustomDialog (title, text, row) {
  this.$q.dialog({
    component: CustomDialog,
    parent: this, 
    title: title,
    text: text
  }).onOk(() => {
    console.log('OK')

    let index = this.table.data.indexOf(row)
    this.table.data.splice(index, 1)
  }).onCancel(() => {
    console.log('Cancel')
  }).onDismiss(() => {
    console.log('Called on OK or Cancel')
  })
}

自订的Dialog元件:

// src/components/CustomDialog.vue
<template>
  <q-dialog ref="dialog" @hide="onDialogHide">
    <q-card class="" style="max-width: 500px; width: 100%;">
      <div class="text-h4 text-bold text-center text-white bg-red-7 q-pa-md">{{title}}</div>

      <q-card-section class="q-pt-lg">
        {{text}}
      </q-card-section>

      <!-- buttons example -->

      <div class="row q-pa-md q-col-gutter-sm">
        <div class="col-6">
          <q-btn unelevated class="full-width" color="red-7" label="确定" @click="onOKClick" />
        </div>
        <div class="col-6">
          <q-btn unelevated class="full-width" color="grey-8" label="取消" @click="onCancelClick"/>
        </div>
      </div>
    </q-card>
  </q-dialog>
</template>

<script>
export default {
  props: ['title', 'text'],

  methods: {
    // following method is REQUIRED
    // (don't change its name --> "show")
    show () {
      this.$refs.dialog.show()
    },

    // following method is REQUIRED
    // (don't change its name --> "hide")
    hide () {
      this.$refs.dialog.hide()
    },

    onDialogHide () {
      // required to be emitted
      // when QDialog emits "hide" event
      this.$emit('hide')
    },

    onOKClick () {
      // on OK, it is REQUIRED to
      // emit "ok" event (with optional payload)
      // before hiding the QDialog
      this.$emit('ok')
      // or with payload: this.$emit('ok', { ... })

      // then hiding dialog
      this.hide()
    },

    onCancelClick () {
      // we just need to hide dialog
      this.hide()
    }
  }
}
</script>

三、总结

明天将会介绍前端重要的Loading状态
包括Quasar的Loading Plugin以及 部分元件的loading属性


<<:  Day28 | 获取安装的extension进行操作

>>:  DAY28: 运算是什麽?

Longest Increasing Subsequence (最长递增子序列)

记录学习内容。看网路上大大们的文章和影片,做些纪录。 还不了解,内容可能有错误。 Longest I...

甲方 vs 乙方

如果不懂甲方或者乙方在专案中的意思,我猜你大概一开始就是待在做自己产品的公司吧。不过就算都是在做产品...

初学者跪着学JavaScript Day6 :template literals和 tagged template literals傻傻分不清楚

一日客家话:黑黑的 念法:五无 之前只学过 template literals,tagged tem...

Day 10 - 试用期过了但要继续下去吗?

很快的三个月过去了主管也敲一个时间跟我面谈了一会,确认後续的薪资调整以及工作的大方向目标後我点头说o...

Swift纯Code之旅 Day1. 「前置作业」

这次的挑战赛并不是什麽特别难的目标,由於我是转职写Swift的,因此也想写些比较基础入门的资讯提供...