Files
freedom-web/src/view/todo/todo-list.vue
T
张家训 65c37d3b4a
CI/CD Deploy (Web) / 构建前端镜像并部署 (push) Successful in 19s
优化(todo): 添加/更新/删除/状态切换加loading+乐观更新,修复Switch类型抛错
2026-10-22 17:39:12 +08:00

445 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>