feat(todo): 待办清单页面改版——卡片式列表+统计看板+飞书表格视图
- 卡片式任务列表:圆点勾选、优先级色点、截止日标签(逾期/今天高亮) - 顶部统计看板:全部/未完成/已完成,点击卡片即筛选 - 快捷添加:回车即创建,可选优先级 - 新增卡片/表格视图切换,表格视图内嵌飞书表格(SHEET_URL 待配置) - 手机端响应式适配(≤768px) - 后端与接口层无改动
This commit is contained in:
+569
-303
@@ -1,378 +1,644 @@
|
||||
<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 class="todo-page">
|
||||
<!-- 顶部:标题 + 统计看板 -->
|
||||
<div class="todo-header">
|
||||
<div class="todo-heading">
|
||||
<h2 class="todo-title">待办清单</h2>
|
||||
<span class="todo-date">{{ todayText }}</span>
|
||||
</div>
|
||||
<div class="stat-cards" v-if="viewMode === 'card'">
|
||||
<div class="stat-card" :class="{ active: statusTab === 'all' }" @click="setTab('all')">
|
||||
<div class="stat-num">{{ stats.total }}</div>
|
||||
<div class="stat-label">全部</div>
|
||||
</div>
|
||||
<div class="stat-card warn" :class="{ active: statusTab === 'undone' }" @click="setTab('undone')">
|
||||
<div class="stat-num">{{ stats.undone }}</div>
|
||||
<div class="stat-label">未完成</div>
|
||||
</div>
|
||||
<div class="stat-card ok" :class="{ active: statusTab === 'done' }" @click="setTab('done')">
|
||||
<div class="stat-num">{{ stats.done }}</div>
|
||||
<div class="stat-label">已完成</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 查询区域 end -->
|
||||
|
||||
<!-- 列表区域 start -->
|
||||
<div class="table-button-list">
|
||||
<Button class="free" type="info" icon="md-add" @click="toAdd">添加</Button>
|
||||
<!-- 快捷添加(仅卡片视图) -->
|
||||
<div class="quick-add" v-if="viewMode === 'card'">
|
||||
<Input v-model="quickTitle" class="quick-input" clearable size="large"
|
||||
placeholder="想到什么就记下来,回车快速添加…" @on-enter="quickAdd"/>
|
||||
<Select v-model="quickPriority" class="quick-priority" size="large" placement="bottom-end">
|
||||
<Option :value="1">低优先级</Option>
|
||||
<Option :value="2">中优先级</Option>
|
||||
<Option :value="3">高优先级</Option>
|
||||
</Select>
|
||||
<Button type="primary" class="quick-btn" size="large" icon="md-add" @click="quickAdd">添加</Button>
|
||||
</div>
|
||||
<Table :height="fixedHeader ? autoHeight : ''"
|
||||
border ref="table"
|
||||
:columns="listColumns"
|
||||
:data="dataList"
|
||||
:loading="tableLoading">
|
||||
<template slot-scope="{ row }" slot="status">
|
||||
<i-switch :value="row.status === 1" true-value="1" false-value="0"
|
||||
@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">删除</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>
|
||||
<!-- 筛选栏 -->
|
||||
<div class="filter-bar">
|
||||
<RadioGroup v-model="statusTab" type="button" button-style="solid" @on-change="onTabChange"
|
||||
v-if="viewMode === 'card'">
|
||||
<Radio label="all">全部</Radio>
|
||||
<Radio label="undone">未完成</Radio>
|
||||
<Radio label="done">已完成</Radio>
|
||||
</RadioGroup>
|
||||
<span v-else class="sheet-hint">飞书共享表格,数据在飞书侧维护</span>
|
||||
<div class="filter-right">
|
||||
<!-- 视图切换:卡片 / 飞书表格 -->
|
||||
<RadioGroup v-model="viewMode" type="button" class="view-switch">
|
||||
<Radio label="card"><Icon type="md-list-box"/> 卡片</Radio>
|
||||
<Radio label="sheet"><Icon type="md-grid"/> 表格</Radio>
|
||||
</RadioGroup>
|
||||
<template v-if="viewMode === 'card'">
|
||||
<Select v-model="search.priority" clearable class="filter-priority" placeholder="优先级" @on-change="onSearch">
|
||||
<Option :value="1">低</Option>
|
||||
<Option :value="2">中</Option>
|
||||
<Option :value="3">高</Option>
|
||||
</Select>
|
||||
<Input v-model="search.title" class="filter-search" search clearable
|
||||
placeholder="搜索标题,回车确认" @on-search="onSearch" @on-clear="onSearch"/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 飞书表格视图 -->
|
||||
<div v-if="viewMode === 'sheet'" class="sheet-view">
|
||||
<iframe v-if="sheetUrl" :src="sheetUrl" class="sheet-frame" allowfullscreen></iframe>
|
||||
<div v-else class="sheet-empty">
|
||||
<Icon type="md-grid" size="48"/>
|
||||
<p>尚未配置飞书表格链接</p>
|
||||
<p class="sheet-tip">在代码顶部 SHEET_URL 填入飞书表格分享链接(表格需开启「获得链接的人可编辑」)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务列表(卡片视图) -->
|
||||
<template v-if="viewMode === 'card'">
|
||||
<Spin v-if="tableLoading" class="todo-spin"/>
|
||||
<div v-else-if="dataList.length" class="todo-list">
|
||||
<div v-for="(item, index) in dataList" :key="item.id"
|
||||
class="todo-item" :class="{ 'is-done': item.status === 1 }">
|
||||
<span class="check-circle" :title="item.status === 1 ? '标记未完成' : '标记完成'" @click="toggleStatus(item)">
|
||||
<Icon v-if="item.status === 1" type="md-checkmark" size="14"/>
|
||||
</span>
|
||||
<div class="item-main" @click="toEdit(index)">
|
||||
<div class="item-title">
|
||||
<span class="prio-dot" :class="'prio-' + (item.priority || 2)"></span>
|
||||
<span class="title-text">{{ item.title }}</span>
|
||||
<span v-if="item.dueDate" class="due-chip" :class="dueClass(item)">
|
||||
<Icon type="md-time" size="12"/> {{ dueText(item) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="item.remark" class="item-remark">{{ item.remark }}</div>
|
||||
<div class="item-meta">创建于 {{ formatCreate(item.createDate) }}</div>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<Button type="text" icon="ios-create-outline" size="small" @click="toEdit(index)"/>
|
||||
<Poptip confirm transfer title="确定删除这条待办吗?" @on-ok="toDelete(index)">
|
||||
<Button type="text" class="del-btn" icon="md-trash" size="small"/>
|
||||
</Poptip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="todo-empty">
|
||||
<Icon type="md-clipboard" size="48"/>
|
||||
<p v-if="statusTab === 'done'">还没有已完成的待办</p>
|
||||
<p v-else-if="statusTab === 'undone'">太棒了,没有未完成的待办</p>
|
||||
<p v-else>暂无待办,从上方输入框添加第一条吧</p>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div v-if="pageTotal > pageParam.pageSize" class="pagination">
|
||||
<Page show-total
|
||||
:total="pageTotal" :page-size="pageParam.pageSize" :current="pageParam.pageNum"
|
||||
@on-page-size-change="changePageSize" @on-change="changePage"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<Modal v-model="modal" :title="modalTitle" width="520" class-name="todo-modal"
|
||||
:mask-closable="false" @on-cancel="cancel()">
|
||||
<Form :model="formItem" ref="formItem" :rules="ruleCustom" :label-width="70" label-position="left">
|
||||
<FormItem label="标题" prop="title">
|
||||
<Input v-model="formItem.title" maxlength="200" placeholder="待办标题"/>
|
||||
</FormItem>
|
||||
<FormItem label="优先级">
|
||||
<RadioGroup v-model="formItem.priority" type="button" button-style="solid">
|
||||
<Radio :label="1">低</Radio>
|
||||
<Radio :label="2">中</Radio>
|
||||
<Radio :label="3">高</Radio>
|
||||
</RadioGroup>
|
||||
</FormItem>
|
||||
<FormItem label="状态">
|
||||
<i-switch v-model="doneSwitch" size="large">
|
||||
<span slot="open">已完成</span>
|
||||
<span slot="close">未完成</span>
|
||||
</i-switch>
|
||||
</FormItem>
|
||||
<FormItem label="截止日期">
|
||||
<DatePicker type="date" format="yyyy-MM-dd" v-model="formItem.dueDate"
|
||||
:clearable="true" class="full-input" placeholder="选填"/>
|
||||
</FormItem>
|
||||
<FormItem label="备注">
|
||||
<Input type="textarea" :rows="3" maxlength="200" show-word-limit
|
||||
v-model="formItem.remark" class="full-input" placeholder="选填:补充说明"/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
<div slot="footer">
|
||||
<Button type="text" size="large" @click="cancel">取消</Button>
|
||||
<Button type="primary" size="large"
|
||||
:loading="submitLoading"
|
||||
@click="submit('formItem')">提交
|
||||
</Button>
|
||||
<Button type="primary" size="large" :loading="submitLoading" @click="submit">保存</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
<!-- Modal区域 end -->
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getDataList, add, update, updateStatus, remove } from '@/api/todo/todo'
|
||||
import { objectUtil } from '@/common/js/util'
|
||||
|
||||
// 飞书表格分享链接(表格视图内嵌展示);需开启「获得链接的人可编辑/阅读」
|
||||
const SHEET_URL = ''
|
||||
|
||||
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'
|
||||
}
|
||||
],
|
||||
// 视图模式:card 卡片 / sheet 飞书表格
|
||||
viewMode: 'card',
|
||||
sheetUrl: SHEET_URL,
|
||||
// 快捷添加
|
||||
quickTitle: '',
|
||||
quickPriority: 2,
|
||||
// 筛选
|
||||
statusTab: 'all',
|
||||
search: { title: '', priority: null },
|
||||
// 统计(由独立请求获取总数)
|
||||
stats: { total: 0, undone: 0, done: 0 },
|
||||
// 列表
|
||||
dataList: [],
|
||||
tableLoading: false,
|
||||
pageTotal: 0, // 总页数
|
||||
pageParam: { // 调用时候的分页参数
|
||||
pageNum: 1,
|
||||
pageSize: 10
|
||||
},
|
||||
pageTotal: 0,
|
||||
pageParam: { pageNum: 1, pageSize: 20 },
|
||||
// 弹窗
|
||||
modal: false,
|
||||
modalTitle: '',
|
||||
addModal: false, // 添加
|
||||
editModal: false, // 编辑
|
||||
modalTitle: '编辑待办',
|
||||
submitLoading: false,
|
||||
doneSwitch: false,
|
||||
formItem: {
|
||||
id: '',
|
||||
title: '',
|
||||
status: 0,
|
||||
priority: 2,
|
||||
dueDate: '',
|
||||
remark: ''
|
||||
id: '', title: '', status: 0, priority: 2, dueDate: '', remark: ''
|
||||
},
|
||||
ruleCustom: {
|
||||
title: [{ required: true, message: '请输入标题', trigger: 'blur' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
todayText () {
|
||||
const d = new Date()
|
||||
const week = ['日', '一', '二', '三', '四', '五', '六'][d.getDay()]
|
||||
return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日 · 星期${week}`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// ===== 数据加载 =====
|
||||
getDataList () {
|
||||
let temp = JSON.parse(JSON.stringify(this.pageParam))
|
||||
let data = objectUtil.trimJoin(temp, this.search)
|
||||
const data = {
|
||||
pageNum: this.pageParam.pageNum,
|
||||
pageSize: this.pageParam.pageSize
|
||||
}
|
||||
if (this.search.title) data.title = this.search.title.trim()
|
||||
if (this.search.priority) data.priority = this.search.priority
|
||||
if (this.statusTab === 'undone') data.status = 0
|
||||
if (this.statusTab === 'done') data.status = 1
|
||||
this.tableLoading = true
|
||||
getDataList(data).then(res => {
|
||||
let page = res.data.data
|
||||
const page = res.data.data
|
||||
this.pageTotal = page.total
|
||||
this.dataList = page.records
|
||||
// 延迟300毫秒,给它更流畅的感觉
|
||||
setTimeout(() => {
|
||||
this.tableLoading = false
|
||||
}, 300)
|
||||
this.tableLoading = false
|
||||
}).catch(() => { this.tableLoading = false })
|
||||
},
|
||||
// 统计卡片:分别请求三种状态的总数(pageSize=1 只取 total)
|
||||
loadStats () {
|
||||
const count = (extra) => getDataList(Object.assign({ pageNum: 1, pageSize: 1 }, extra)).then(res => res.data.data.total)
|
||||
Promise.all([count({}), count({ status: 0 }), count({ status: 1 })]).then(([total, undone, done]) => {
|
||||
this.stats = { total, undone, done }
|
||||
}).catch(() => {})
|
||||
},
|
||||
reloadAll () {
|
||||
this.loadStats()
|
||||
this.getDataList()
|
||||
},
|
||||
// ===== 筛选交互 =====
|
||||
setTab (tab) {
|
||||
this.statusTab = tab
|
||||
this.pageParam.pageNum = 1
|
||||
this.getDataList()
|
||||
},
|
||||
onTabChange () {
|
||||
this.pageParam.pageNum = 1
|
||||
this.getDataList()
|
||||
},
|
||||
onSearch () {
|
||||
this.pageParam.pageNum = 1
|
||||
this.getDataList()
|
||||
},
|
||||
// ===== 快捷添加 =====
|
||||
quickAdd () {
|
||||
const title = (this.quickTitle || '').trim()
|
||||
if (!title) {
|
||||
this.$Message.warning('先输入待办内容再添加')
|
||||
return
|
||||
}
|
||||
add({ title, priority: this.quickPriority }).then(() => {
|
||||
this.quickTitle = ''
|
||||
this.$Message.success('已添加')
|
||||
this.pageParam.pageNum = 1
|
||||
this.reloadAll()
|
||||
})
|
||||
},
|
||||
toAdd () {
|
||||
this.modalTitle = '添加'
|
||||
this.modal = true
|
||||
this.addModal = true
|
||||
this.editModal = false
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.formItem) {
|
||||
this.$refs.formItem.resetFields()
|
||||
}
|
||||
})
|
||||
// ===== 勾选完成 =====
|
||||
toggleStatus (row) {
|
||||
const status = row.status === 1 ? 0 : 1
|
||||
updateStatus({ id: row.id, status }).then(() => {
|
||||
this.$Message.success(status === 1 ? '已完成' : '已标记为未完成')
|
||||
this.reloadAll()
|
||||
}).catch(() => this.getDataList())
|
||||
},
|
||||
// ===== 编辑弹窗 =====
|
||||
toEdit (index) {
|
||||
this.modalTitle = '编辑'
|
||||
let temp = this.dataList[index]
|
||||
|
||||
this.addModal = false
|
||||
this.editModal = true
|
||||
const temp = this.dataList[index]
|
||||
this.modalTitle = '编辑待办'
|
||||
this.modal = true
|
||||
|
||||
this.$nextTick(() => {
|
||||
// 清除上次校验状态,避免编辑回显报红
|
||||
if (this.$refs.formItem) {
|
||||
this.$refs.formItem.resetFields()
|
||||
if (this.$refs.formItem) this.$refs.formItem.resetFields()
|
||||
this.formItem = {
|
||||
id: temp.id,
|
||||
title: temp.title,
|
||||
status: temp.status,
|
||||
priority: temp.priority || 2,
|
||||
dueDate: temp.dueDate ? new Date(String(temp.dueDate).replace(/-/g, '/')) : '',
|
||||
remark: temp.remark || ''
|
||||
}
|
||||
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 || ''
|
||||
this.doneSwitch = temp.status === 1
|
||||
})
|
||||
},
|
||||
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)
|
||||
if (this.addModal) { // 添加
|
||||
add(data).then(res => {
|
||||
this.$Message.success('添加成功!')
|
||||
this.refresh()
|
||||
this.getDataList()
|
||||
})
|
||||
} else { // 编辑
|
||||
update(data).then(res => {
|
||||
this.$Message.success('编辑成功!')
|
||||
this.refresh()
|
||||
this.getDataList()
|
||||
})
|
||||
}
|
||||
submit () {
|
||||
this.$refs.formItem.validate((valid) => {
|
||||
if (!valid) return
|
||||
this.submitLoading = true
|
||||
const data = Object.assign({}, this.formItem)
|
||||
data.dueDate = this.formatDate(this.formItem.dueDate)
|
||||
data.status = this.doneSwitch ? 1 : 0
|
||||
update(data).then(() => {
|
||||
this.$Message.success('保存成功')
|
||||
this.cancel()
|
||||
}
|
||||
this.reloadAll()
|
||||
}).finally(() => { this.submitLoading = false })
|
||||
})
|
||||
},
|
||||
cancel () {
|
||||
// Modal cancel
|
||||
this.modal = false
|
||||
this.refresh()
|
||||
if (this.$refs.formItem) this.$refs.formItem.resetFields()
|
||||
},
|
||||
toDelete (index) {
|
||||
let temp = this.dataList[index]
|
||||
let data = {
|
||||
id: temp.id
|
||||
}
|
||||
remove(data).then(res => {
|
||||
this.refresh()
|
||||
this.getDataList()
|
||||
this.$Message.success('删除成功!')
|
||||
remove({ id: this.dataList[index].id }).then(() => {
|
||||
this.$Message.success('删除成功')
|
||||
// 删除后若当前页空了则回退一页
|
||||
if (this.dataList.length === 1 && this.pageParam.pageNum > 1) {
|
||||
this.pageParam.pageNum -= 1
|
||||
}
|
||||
this.reloadAll()
|
||||
})
|
||||
},
|
||||
updateStatus (row, value) {
|
||||
let status = value === true || value === 1 || value === '1' ? 1 : 0
|
||||
let data = {
|
||||
id: row.id,
|
||||
status: status
|
||||
}
|
||||
updateStatus(data).then(res => {
|
||||
this.$Message.success(status === 1 ? '已完成!' : '已标记为未完成!')
|
||||
this.getDataList()
|
||||
}).catch(() => {
|
||||
this.getDataList()
|
||||
})
|
||||
},
|
||||
refresh () {
|
||||
this.pageParam = {
|
||||
pageNum: 1,
|
||||
pageSize: 10
|
||||
}
|
||||
this.search = {
|
||||
title: '',
|
||||
status: '',
|
||||
priority: ''
|
||||
}
|
||||
this.formItem = {
|
||||
id: '',
|
||||
title: '',
|
||||
status: 0,
|
||||
priority: 2,
|
||||
dueDate: '',
|
||||
remark: ''
|
||||
}
|
||||
},
|
||||
// ===== 分页 =====
|
||||
changePage (index) {
|
||||
// 需要显示开始数据的index,(因为数据是从0开始的,页码是从1开始的,需要-1)
|
||||
this.pageParam.pageNum = index
|
||||
this.getDataList()
|
||||
},
|
||||
changePageSize (page) {
|
||||
// 切换分页条数
|
||||
changePageSize (size) {
|
||||
this.pageParam.pageNum = 1
|
||||
this.pageParam.pageSize = page
|
||||
this.pageParam.pageSize = size
|
||||
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}`
|
||||
if (!date) return ''
|
||||
const d = date instanceof Date ? date : new Date(String(date).replace(/-/g, '/'))
|
||||
if (isNaN(d.getTime())) return ''
|
||||
const m = d.getMonth() + 1
|
||||
const day = d.getDate()
|
||||
return `${d.getFullYear()}-${m < 10 ? '0' + m : m}-${day < 10 ? '0' + day : day}`
|
||||
},
|
||||
priorityName (priority) {
|
||||
let map = { 1: '低', 2: '中', 3: '高' }
|
||||
return map[priority] || '中'
|
||||
formatCreate (val) {
|
||||
if (!val) return ''
|
||||
return String(val).substring(0, 10)
|
||||
},
|
||||
priorityColor (priority) {
|
||||
let map = { 1: 'default', 2: 'blue', 3: 'red' }
|
||||
return map[priority] || 'blue'
|
||||
dueDateObj (item) {
|
||||
if (!item.dueDate) return null
|
||||
const d = new Date(String(item.dueDate).replace(/-/g, '/'))
|
||||
d.setHours(0, 0, 0, 0)
|
||||
return d
|
||||
},
|
||||
// 截止日展示:已逾期 / 今天 / 明天 / 具体日期
|
||||
dueText (item) {
|
||||
const d = this.dueDateObj(item)
|
||||
if (!d) return ''
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const diff = Math.round((d - today) / 86400000)
|
||||
if (item.status === 0 && diff < 0) return `已逾期${-diff}天`
|
||||
if (diff === 0) return '今天截止'
|
||||
if (diff === 1) return '明天截止'
|
||||
return this.formatDate(d)
|
||||
},
|
||||
dueClass (item) {
|
||||
const d = this.dueDateObj(item)
|
||||
if (!d || item.status === 1) return ''
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const diff = Math.round((d - today) / 86400000)
|
||||
if (diff < 0) return 'overdue'
|
||||
if (diff === 0) return 'today'
|
||||
return ''
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.getDataList()
|
||||
this.reloadAll()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.form-input {
|
||||
width: 100%
|
||||
@primary: #2d8cf0;
|
||||
@text-main: #17233d;
|
||||
@text-sub: #808695;
|
||||
@radius: 10px;
|
||||
|
||||
.todo-page {
|
||||
padding: 16px;
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ===== 顶部 ===== */
|
||||
.todo-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.todo-heading {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.todo-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: @text-main;
|
||||
margin: 0;
|
||||
}
|
||||
.todo-date {
|
||||
font-size: 13px;
|
||||
color: @text-sub;
|
||||
}
|
||||
.stat-cards {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border: 1px solid #e8eaec;
|
||||
border-radius: @radius;
|
||||
padding: 14px 16px;
|
||||
cursor: pointer;
|
||||
transition: all .2s;
|
||||
&:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, .08);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
&.active {
|
||||
border-color: @primary;
|
||||
box-shadow: 0 0 0 2px fade(@primary, 15%);
|
||||
}
|
||||
&.warn .stat-num { color: #ff9900; }
|
||||
&.ok .stat-num { color: #19be6b; }
|
||||
}
|
||||
.stat-num {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: @text-main;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: @text-sub;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* ===== 快捷添加 ===== */
|
||||
.quick-add {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
background: #fff;
|
||||
border: 1px solid #e8eaec;
|
||||
border-radius: @radius;
|
||||
padding: 12px;
|
||||
margin-bottom: 14px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, .04);
|
||||
}
|
||||
.quick-input { flex: 1; }
|
||||
.quick-priority { width: 110px; flex-shrink: 0; }
|
||||
.quick-btn { flex-shrink: 0; }
|
||||
|
||||
/* ===== 筛选栏 ===== */
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.filter-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.filter-priority { width: 90px; }
|
||||
.filter-search { width: 200px; }
|
||||
.view-switch { margin-right: 8px; }
|
||||
.sheet-hint { font-size: 13px; color: @text-sub; }
|
||||
|
||||
/* ===== 飞书表格视图 ===== */
|
||||
.sheet-view {
|
||||
background: #fff;
|
||||
border: 1px solid #e8eaec;
|
||||
border-radius: @radius;
|
||||
overflow: hidden;
|
||||
}
|
||||
.sheet-frame {
|
||||
width: 100%;
|
||||
height: calc(100vh - 320px);
|
||||
min-height: 480px;
|
||||
border: none;
|
||||
display: block;
|
||||
}
|
||||
.sheet-empty {
|
||||
text-align: center;
|
||||
padding: 72px 16px;
|
||||
color: #c5c8ce;
|
||||
p { margin-top: 12px; font-size: 14px; }
|
||||
.sheet-tip { font-size: 12px; color: #b0b4bc; }
|
||||
}
|
||||
|
||||
/* ===== 任务列表 ===== */
|
||||
.todo-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.todo-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #e8eaec;
|
||||
border-radius: @radius;
|
||||
padding: 14px 16px;
|
||||
transition: all .2s;
|
||||
&:hover {
|
||||
border-color: #dcdee2;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, .06);
|
||||
.item-actions { opacity: 1; }
|
||||
}
|
||||
&.is-done {
|
||||
background: #fafafa;
|
||||
.title-text {
|
||||
color: @text-sub;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.item-remark, .item-meta { opacity: .7; }
|
||||
}
|
||||
}
|
||||
.check-circle {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
border: 2px solid #c5c8ce;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: all .15s;
|
||||
&:hover { border-color: @primary; }
|
||||
.is-done & {
|
||||
background: #19be6b;
|
||||
border-color: #19be6b;
|
||||
}
|
||||
}
|
||||
.item-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.item-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.prio-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
&.prio-1 { background: #c5c8ce; }
|
||||
&.prio-2 { background: #2d8cf0; }
|
||||
&.prio-3 { background: #ed4014; }
|
||||
}
|
||||
.title-text {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: @text-main;
|
||||
word-break: break-all;
|
||||
}
|
||||
.due-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-size: 12px;
|
||||
color: @text-sub;
|
||||
background: #f5f7f9;
|
||||
border-radius: 4px;
|
||||
padding: 1px 7px;
|
||||
white-space: nowrap;
|
||||
&.today {
|
||||
color: #ff9900;
|
||||
background: fade(#ff9900, 12%);
|
||||
}
|
||||
&.overdue {
|
||||
color: #ed4014;
|
||||
background: fade(#ed4014, 10%);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
.item-remark {
|
||||
font-size: 13px;
|
||||
color: @text-sub;
|
||||
margin-top: 6px;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.item-meta {
|
||||
font-size: 12px;
|
||||
color: #c5c8ce;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.item-actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
opacity: 0;
|
||||
transition: opacity .2s;
|
||||
.del-btn { color: #ed4014; }
|
||||
}
|
||||
|
||||
/* ===== 空状态 / 分页 / 加载 ===== */
|
||||
.todo-empty {
|
||||
text-align: center;
|
||||
padding: 56px 0;
|
||||
color: #c5c8ce;
|
||||
p { margin-top: 12px; font-size: 14px; }
|
||||
}
|
||||
.todo-spin { display: block; margin: 60px auto; }
|
||||
.pagination {
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
.full-input { width: 100%; }
|
||||
|
||||
/* ===== 手机端适配(≤768px) ===== */
|
||||
@media (max-width: 768px) {
|
||||
.todo-page { padding: 10px; }
|
||||
.todo-title { font-size: 20px; }
|
||||
.stat-card { padding: 10px 12px; }
|
||||
.stat-num { font-size: 20px; }
|
||||
.quick-add {
|
||||
flex-wrap: wrap;
|
||||
.quick-input { flex: 1 1 100%; }
|
||||
.quick-priority { flex: 1; width: auto; }
|
||||
.quick-btn { flex: 1; }
|
||||
}
|
||||
.filter-bar { flex-direction: column; align-items: stretch; }
|
||||
.filter-right { width: 100%; }
|
||||
.filter-priority { width: 100px; }
|
||||
.filter-search { flex: 1; width: auto; }
|
||||
.todo-item { padding: 12px; }
|
||||
.item-actions { opacity: 1; } // 手机端常显操作按钮
|
||||
.sheet-frame { height: 60vh; min-height: 360px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user