445 lines
14 KiB
Vue
445 lines
14 KiB
Vue
<template>
|
||
<div>
|
||
<!-- 查询区域 start -->
|
||
<div class="option">
|
||
<Input v-model="search.title" class="short-input" clearable placeholder="标题"/>
|
||
<Select v-model="search.status" class="short-input" clearable placeholder="状态">
|
||
<Option :value="0">未完成</Option>
|
||
<Option :value="1">已完成</Option>
|
||
</Select>
|
||
<Select v-model="search.priority" class="short-input" clearable placeholder="优先级">
|
||
<Option :value="1">低</Option>
|
||
<Option :value="2">中</Option>
|
||
<Option :value="3">高</Option>
|
||
</Select>
|
||
<Button class="free" type="primary" icon="ios-search" @click="getDataList()">查询</Button>
|
||
<Button class="free" type="primary" icon="md-refresh" @click="refresh()">重置</Button>
|
||
</div>
|
||
<!-- 查询区域 end -->
|
||
|
||
<!-- 列表区域 start -->
|
||
<div class="table-button-list">
|
||
<Button class="free" type="info" icon="md-add" @click="toAdd">添加</Button>
|
||
</div>
|
||
<Table :height="fixedHeader ? autoHeight : ''"
|
||
border ref="table"
|
||
:columns="listColumns"
|
||
:data="dataList"
|
||
:loading="tableLoading">
|
||
<template slot-scope="{ row }" slot="status">
|
||
<!-- value 必须与 true-value/false-value 同类型(都是数字),否则组件内部会抛
|
||
'Value should be trueValue or falseValue.' 且状态不更新 -->
|
||
<i-switch :value="row.status" :true-value="1" :false-value="0"
|
||
:loading="!!rowLoading[row.id]"
|
||
@on-change="val => updateStatus(row, val)">
|
||
<span slot="open">完成</span>
|
||
<span slot="close">未完成</span>
|
||
</i-switch>
|
||
</template>
|
||
<template slot-scope="{ row }" slot="priority">
|
||
<Tag :color="priorityColor(row.priority)">{{ priorityName(row.priority) }}</Tag>
|
||
</template>
|
||
<template slot-scope="{ row, index }" slot="action">
|
||
<Button type="primary"
|
||
size="small"
|
||
style="margin-right: 5px"
|
||
icon="ios-create"
|
||
@click="toEdit(index)">编辑
|
||
</Button>
|
||
<Poptip
|
||
confirm
|
||
title="您确定要删除吗?"
|
||
:transfer="true"
|
||
@on-ok="toDelete(index)"
|
||
>
|
||
<Button type="error" size="small" icon="md-trash" :loading="!!rowLoading[row.id]">删除</Button>
|
||
</Poptip>
|
||
</template>
|
||
</Table>
|
||
<div class="pagination">
|
||
<Page show-elevator show-sizer show-total
|
||
:total="pageTotal" :page-size="pageParam.pageSize"
|
||
:current="pageParam.pageNum"
|
||
@on-page-size-change="changePageSize" @on-change="changePage"></Page>
|
||
</div>
|
||
<!-- 列表区域 end -->
|
||
|
||
<!-- Modal区域 start -->
|
||
<Modal v-model="modal" :title="modalTitle" :styles="{top: '20px'}" width="700" :mask-closable="false"
|
||
@on-cancel="cancel()">
|
||
<Form :rules="ruleCustom" :model="formItem" ref="formItem" :label-width="110" label-position="right">
|
||
<Row :gutter="32">
|
||
<Col span="24">
|
||
<FormItem label="标题" prop="title">
|
||
<Input type="text" class="form-input" v-model="formItem.title" placeholder="请输入待办标题"/>
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="12">
|
||
<FormItem label="优先级" prop="priority">
|
||
<RadioGroup v-model="formItem.priority">
|
||
<Radio :label="1"><span>低</span></Radio>
|
||
<Radio :label="2"><span>中</span></Radio>
|
||
<Radio :label="3"><span>高</span></Radio>
|
||
</RadioGroup>
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="12">
|
||
<FormItem label="状态" prop="status">
|
||
<RadioGroup v-model="formItem.status">
|
||
<Radio :label="0"><span>未完成</span></Radio>
|
||
<Radio :label="1"><span>已完成</span></Radio>
|
||
</RadioGroup>
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="12">
|
||
<FormItem label="截止日期" prop="dueDate">
|
||
<DatePicker type="date"
|
||
class="form-input"
|
||
format="yyyy-MM-dd"
|
||
v-model="formItem.dueDate"
|
||
placeholder="请选择截止日期">
|
||
</DatePicker>
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="24">
|
||
<FormItem label="备注" prop="remark">
|
||
<Input type="textarea" :rows="3" maxlength="200" show-word-limit class="form-input"
|
||
v-model="formItem.remark" placeholder="选填:补充说明"/>
|
||
</FormItem>
|
||
</Col>
|
||
</Row>
|
||
</Form>
|
||
<div slot="footer">
|
||
<Button type="text" size="large" @click="cancel">取消</Button>
|
||
<Button type="primary" size="large"
|
||
:loading="submitLoading"
|
||
@click="submit('formItem')">提交
|
||
</Button>
|
||
</div>
|
||
</Modal>
|
||
<!-- Modal区域 end -->
|
||
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { getDataList, add, update, updateStatus, remove } from '@/api/todo/todo'
|
||
import { objectUtil } from '@/common/js/util'
|
||
|
||
export default {
|
||
name: 'todo-list',
|
||
data () {
|
||
return {
|
||
fixedHeader: true, // 高度自适应
|
||
autoHeight: `${window.innerHeight} ` * 0.55, // 获取当前可视高度-定义Table高度
|
||
search: {
|
||
title: '',
|
||
status: '',
|
||
priority: ''
|
||
},
|
||
listColumns: [
|
||
{
|
||
title: '#',
|
||
type: 'index',
|
||
width: 60,
|
||
align: 'center',
|
||
indexMethod: (row) => {
|
||
return (row._index + 1) + ((this.pageParam.pageNum - 1) * this.pageParam.pageSize)
|
||
}
|
||
},
|
||
{
|
||
title: '标题',
|
||
key: 'title',
|
||
minWidth: 200
|
||
},
|
||
{
|
||
title: '状态',
|
||
slot: 'status',
|
||
width: 110,
|
||
align: 'center'
|
||
},
|
||
{
|
||
title: '优先级',
|
||
slot: 'priority',
|
||
width: 100,
|
||
align: 'center'
|
||
},
|
||
{
|
||
title: '截止日期',
|
||
key: 'dueDate',
|
||
width: 120,
|
||
align: 'center'
|
||
},
|
||
{
|
||
title: '备注',
|
||
key: 'remark',
|
||
minWidth: 180,
|
||
ellipsis: true,
|
||
tooltip: true
|
||
},
|
||
{
|
||
title: '创建时间',
|
||
key: 'createDate',
|
||
width: 160,
|
||
align: 'center'
|
||
},
|
||
{
|
||
title: '操作区',
|
||
slot: 'action',
|
||
width: 160,
|
||
fixed: 'right',
|
||
align: 'center'
|
||
}
|
||
],
|
||
dataList: [],
|
||
tableLoading: false,
|
||
pageTotal: 0, // 总页数
|
||
pageParam: { // 调用时候的分页参数
|
||
pageNum: 1,
|
||
pageSize: 10
|
||
},
|
||
modal: false,
|
||
modalTitle: '',
|
||
addModal: false, // 添加
|
||
editModal: false, // 编辑
|
||
submitLoading: false,
|
||
rowLoading: {}, // 行级 loading:{ 待办id: true },用于开关与删除按钮
|
||
formItem: {
|
||
id: '',
|
||
title: '',
|
||
status: 0,
|
||
priority: 2,
|
||
dueDate: '',
|
||
remark: ''
|
||
},
|
||
ruleCustom: {
|
||
title: [{ required: true, message: '请输入标题', trigger: 'blur' }]
|
||
}
|
||
}
|
||
},
|
||
methods: {
|
||
/**
|
||
* 拉取列表
|
||
* @param {Boolean} silent 静默刷新:不显示表格 loading,用于乐观更新后的后台同步
|
||
*/
|
||
getDataList (silent) {
|
||
let temp = JSON.parse(JSON.stringify(this.pageParam))
|
||
let data = objectUtil.trimJoin(temp, this.search)
|
||
if (!silent) {
|
||
this.tableLoading = true
|
||
}
|
||
const closeLoading = () => {
|
||
this.tableLoading = false
|
||
}
|
||
getDataList(data).then(res => {
|
||
let page = res.data.data
|
||
this.pageTotal = page.total
|
||
this.dataList = page.records
|
||
closeLoading()
|
||
}).catch(() => {
|
||
// 出错也要复位 loading,否则表格一直转圈
|
||
closeLoading()
|
||
})
|
||
},
|
||
toAdd () {
|
||
this.modalTitle = '添加'
|
||
this.modal = true
|
||
this.addModal = true
|
||
this.editModal = false
|
||
this.$nextTick(() => {
|
||
if (this.$refs.formItem) {
|
||
this.$refs.formItem.resetFields()
|
||
}
|
||
})
|
||
},
|
||
toEdit (index) {
|
||
this.modalTitle = '编辑'
|
||
let temp = this.dataList[index]
|
||
|
||
this.addModal = false
|
||
this.editModal = true
|
||
this.modal = true
|
||
|
||
this.$nextTick(() => {
|
||
// 清除上次校验状态,避免编辑回显报红
|
||
if (this.$refs.formItem) {
|
||
this.$refs.formItem.resetFields()
|
||
}
|
||
this.formItem.id = temp.id
|
||
this.formItem.title = temp.title
|
||
this.formItem.status = temp.status
|
||
this.formItem.priority = temp.priority
|
||
this.formItem.dueDate = temp.dueDate ? new Date(String(temp.dueDate).replace(/-/g, '/')) : ''
|
||
this.formItem.remark = temp.remark || ''
|
||
})
|
||
},
|
||
submit (formItem) {
|
||
// modal 提交
|
||
this.$refs[formItem].validate((valid) => {
|
||
if (valid) {
|
||
let data = Object.assign({}, this.formItem)
|
||
// DatePicker 绑定的是 Date 对象,后端按 yyyy-MM-dd 字符串接收
|
||
data.dueDate = this.formatDate(this.formItem.dueDate)
|
||
const isAdd = this.addModal
|
||
// 按钮转圈:公网穿透链路有延迟,没有反馈会像卡住
|
||
this.submitLoading = true
|
||
const req = isAdd ? add(data) : update(data)
|
||
req.then(res => {
|
||
// 编辑走本地合并,改完立刻看到结果,不等服务端回包
|
||
if (!isAdd) {
|
||
this.mergeRow(data)
|
||
}
|
||
this.$Message.success(isAdd ? '添加成功!' : '编辑成功!')
|
||
// 请求成功后再关弹窗,避免"点了没反应"
|
||
this.modal = false
|
||
this.resetForm()
|
||
this.submitLoading = false
|
||
// 新增的行 id 由服务端生成,静默拉一次同步(不显示表格 loading)
|
||
if (isAdd) {
|
||
this.getDataList(true)
|
||
}
|
||
}).catch(() => {
|
||
this.submitLoading = false
|
||
this.$Message.error(isAdd ? '添加失败,请重试' : '编辑失败,请重试')
|
||
})
|
||
}
|
||
})
|
||
},
|
||
/** 把提交的字段合并进当前列表对应行,实现即时更新 **/
|
||
mergeRow (data) {
|
||
let row = this.dataList.find(item => String(item.id) === String(data.id))
|
||
if (!row) {
|
||
return
|
||
}
|
||
row.title = data.title
|
||
row.status = data.status
|
||
row.priority = data.priority
|
||
row.dueDate = data.dueDate
|
||
row.remark = data.remark
|
||
},
|
||
cancel () {
|
||
// Modal cancel:只关弹窗并复位表单,不清空搜索条件与分页
|
||
this.modal = false
|
||
this.resetForm()
|
||
},
|
||
/** 复位弹窗表单 **/
|
||
resetForm () {
|
||
this.formItem = {
|
||
id: '',
|
||
title: '',
|
||
status: 0,
|
||
priority: 2,
|
||
dueDate: '',
|
||
remark: ''
|
||
}
|
||
this.submitLoading = false
|
||
},
|
||
toDelete (index) {
|
||
let temp = this.dataList[index]
|
||
let data = {
|
||
id: temp.id
|
||
}
|
||
// 删除按钮转圈,给即时反馈
|
||
this.$set(this.rowLoading, temp.id, true)
|
||
remove(data).then(res => {
|
||
// 先本地移除,画面立即响应,再静默同步分页数据
|
||
this.dataList.splice(index, 1)
|
||
this.pageTotal = Math.max(0, this.pageTotal - 1)
|
||
// 本页删空了就退回上一页,避免停在空页
|
||
if (this.dataList.length === 0 && this.pageParam.pageNum > 1) {
|
||
this.pageParam.pageNum -= 1
|
||
}
|
||
this.$set(this.rowLoading, temp.id, false)
|
||
this.$Message.success('删除成功!')
|
||
this.getDataList(true)
|
||
}).catch(() => {
|
||
this.$set(this.rowLoading, temp.id, false)
|
||
this.$Message.error('删除失败,请重试')
|
||
})
|
||
},
|
||
updateStatus (row, value) {
|
||
let status = value === true || value === 1 || value === '1' ? 1 : 0
|
||
if (row.status === status) {
|
||
return
|
||
}
|
||
let data = {
|
||
id: row.id,
|
||
status: status
|
||
}
|
||
const oldStatus = row.status
|
||
// 乐观更新:先改本地,开关立刻响应,不等网络
|
||
row.status = status
|
||
this.$set(this.rowLoading, row.id, true)
|
||
updateStatus(data).then(res => {
|
||
this.$set(this.rowLoading, row.id, false)
|
||
this.$Message.success(status === 1 ? '已完成!' : '已标记为未完成!')
|
||
// 当前按状态筛选且该行已不匹配条件时,静默刷新让它消失
|
||
if (this.search.status !== '' && Number(this.search.status) !== status) {
|
||
this.getDataList(true)
|
||
}
|
||
}).catch(() => {
|
||
// 失败回滚到原状态
|
||
this.$set(this.rowLoading, row.id, false)
|
||
row.status = oldStatus
|
||
this.$Message.error('状态更新失败,请重试')
|
||
})
|
||
},
|
||
refresh () {
|
||
// 重置查询条件:回到第一页并重新拉取(原来只清变量不刷列表,点了没变化)
|
||
this.pageParam = {
|
||
pageNum: 1,
|
||
pageSize: 10
|
||
}
|
||
this.search = {
|
||
title: '',
|
||
status: '',
|
||
priority: ''
|
||
}
|
||
this.resetForm()
|
||
this.getDataList()
|
||
},
|
||
changePage (index) {
|
||
// 需要显示开始数据的index,(因为数据是从0开始的,页码是从1开始的,需要-1)
|
||
this.pageParam.pageNum = index
|
||
this.getDataList()
|
||
},
|
||
changePageSize (page) {
|
||
// 切换分页条数
|
||
this.pageParam.pageNum = 1
|
||
this.pageParam.pageSize = page
|
||
this.getDataList()
|
||
},
|
||
/** 日期格式化为 yyyy-MM-dd,空值返回空串 **/
|
||
formatDate (date) {
|
||
if (!date) {
|
||
return ''
|
||
}
|
||
let d = date instanceof Date ? date : new Date(String(date).replace(/-/g, '/'))
|
||
if (isNaN(d.getTime())) {
|
||
return ''
|
||
}
|
||
let month = d.getMonth() + 1
|
||
let day = d.getDate()
|
||
return `${d.getFullYear()}-${month < 10 ? '0' + month : month}-${day < 10 ? '0' + day : day}`
|
||
},
|
||
priorityName (priority) {
|
||
let map = { 1: '低', 2: '中', 3: '高' }
|
||
return map[priority] || '中'
|
||
},
|
||
priorityColor (priority) {
|
||
let map = { 1: 'default', 2: 'blue', 3: 'red' }
|
||
return map[priority] || 'blue'
|
||
}
|
||
},
|
||
created () {
|
||
this.getDataList()
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.form-input {
|
||
width: 100%
|
||
}
|
||
</style>
|