| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738 |
- <template>
- <view class="todo-page">
- <!-- 待办列表 -->
- <scroll-view class="todo-container" :scroll-y="needScroll" refresher-enabled :refresher-triggered="refreshing"
- @refresherrefresh="onRefresh" @refresherrestore="onRefreshRestore" @click="closeAllSwipeStates">
- <view class="todo-content">
- <!-- 草稿箱 - 固定顶部 -->
- <view v-if="draftList.length > 0" class="draft-container">
- <!-- 文件夹标签页效果 -->
- <view class="folder-tab">
- <!-- 底层:100%宽度的橙色线 -->
- <view class="tab-base-line orange"></view>
- <!-- 上层:左侧梯形白色区域 -->
- <view class="tab-trapezoid">
- <!-- 梯形内的橙色线 -->
- <view class="tab-inner-line orange" v-if="!draftExpanded"></view>
- </view>
- <!-- 右侧:二分之一高度的橙色线 -->
- <view class="tab-right-line orange"></view>
- </view>
- <!-- 草稿箱内容 -->
- <view class="draft-section">
- <view class="draft-header">
- <view class="draft-title">
- <Icon name="icon-caogaoxiang" size="38" color="#575d6d" />
- <text class="title-text">草稿箱</text>
- </view>
- <text class="count-badge" @click="toggleDraft">{{ draftList.length }}</text>
- </view>
- <!-- 草稿内容 -->
- <view v-if="draftExpanded" class="draft-content">
- <view v-for="(item, index) in draftList" :key="index" class="draft-item-wrapper"
- :class="{ 'swiped': swipeStates[`draft-${index}`] }"
- @touchstart="hasActions(item) ? handleTouchStart($event, `draft-${index}`) : null"
- @touchmove="hasActions(item) ? handleTouchMove($event, `draft-${index}`) : null"
- @touchend="hasActions(item) ? handleTouchEnd($event, `draft-${index}`) : null">
- <view class="draft-item" :class="{ 'has-actions': hasActions(item) }" @click="handleTodoClick(item)">
- <view class="draft-icon">
- <view class="dot"></view>
- </view>
- <view class="draft-title">{{ item.name }}</view>
- <view class="draft-time-wrapper">
- <view class="draft-time">
- <text>{{ formatTime(item.time) }}</text>
- <text>{{ formatDate(item.time) }}</text>
- </view>
- </view>
- </view>
- <!-- 左滑显示的操作按钮 -->
- <view class="swipe-actions">
- <view class="action-btn edit-btn" @click.stop="handleEditDraft(item)">
- 编辑
- </view>
- <view class="action-btn delete-btn" @click.stop="handleDeleteDraft(item)">
- 删除
- </view>
- </view>
- </view>
- </view>
- </view> <!-- 草稿箱内容结束 -->
- </view> <!-- 草稿箱容器结束 -->
- <!-- 审核分组列表 -->
- <view v-for="(group, groupIndex) in approvalGroups" :key="groupIndex" class="approval-group">
- <!-- 文件夹类型(多级) -->
- <view v-if="group.type === 'folder'" class="folder-container">
- <!-- 文件夹标签页效果 -->
- <view class="folder-tab">
- <!-- 底层:100%宽度的灰色线 -->
- <view class="tab-base-line gray"></view>
- <!-- 上层:左侧梯形白色区域 -->
- <view class="tab-trapezoid">
- <!-- 梯形内的灰色线 -->
- <view class="tab-inner-line gray" v-if="!group.expanded"></view>
- </view>
- <!-- 右侧:二分之一高度的灰色线 -->
- <view class="tab-right-line gray"></view>
- </view>
- <!-- 文件夹内容 -->
- <view class="folder-item">
- <view class="folder-header" @click="handleTodoClick(group)">
- <view class="folder-title">
- <Icon name="icon-gongwen" size="38" color="#575d6d" />
- <text class="title-text">{{ group.name }}</text>
- </view>
- <text class="count-badge" @click.stop="toggleGroup(groupIndex)">{{ group.count }}</text>
- </view>
- <!-- 展开的子项 -->
- <view v-if="group.expanded" class="folder-content">
- <view v-for="(item, itemIndex) in group.items" :key="itemIndex" class="sub-item-wrapper"
- :class="{ 'swiped': swipeStates[`folder-${groupIndex}-${itemIndex}`] }"
- @touchstart="hasActions(item) ? handleTouchStart($event, `folder-${groupIndex}-${itemIndex}`) : null"
- @touchmove="hasActions(item) ? handleTouchMove($event, `folder-${groupIndex}-${itemIndex}`) : null"
- @touchend="hasActions(item) ? handleTouchEnd($event, `folder-${groupIndex}-${itemIndex}`) : null">
- <view class="sub-item" :class="{ 'has-actions': hasActions(item) }" @click="handleTodoClick(item)">
- <view class="sub-icon">
- <view class="dot"></view>
- </view>
- <view class="sub-title">{{ item.name }}</view>
- <view class="sub-time-wrapper">
- <view class="sub-time">
- <text>{{ formatTime(item.time) }}</text>
- <text>{{ formatDate(item.time) }}</text>
- </view>
- </view>
- </view>
- <!-- 左滑显示的操作按钮 -->
- <view class="swipe-actions">
- <view v-for="(action, actionIndex) in item.actions" :key="actionIndex" class="action-btn"
- @click.stop="handleActionClick(action, item)">
- {{ action.name }}
- </view>
- </view>
- </view>
- </view>
- </view> <!-- 文件夹内容结束 -->
- </view> <!-- 文件夹容器结束 -->
- <!-- 文件类型(单级) -->
- <view v-else class="file-item">
- <view class="file-item-wrapper" :class="{ 'swiped': swipeStates[`file-${groupIndex}`] }"
- @touchstart="hasActions(group) ? handleTouchStart($event, `file-${groupIndex}`) : null"
- @touchmove="hasActions(group) ? handleTouchMove($event, `file-${groupIndex}`) : null"
- @touchend="hasActions(group) ? handleTouchEnd($event, `file-${groupIndex}`) : null">
- <view class="file-content" :class="{ 'has-actions': hasActions(group) }" @click="handleTodoClick(group)">
- <view class="file-icon">
- <Icon name="icon-daiban" size="38" color="#575d6d" />
- </view>
- <view class="file-title">{{ group.name }}</view>
- <view class="file-time-wrapper">
- <view class="file-time">
- <text>{{ formatTime(group.time) }}</text>
- <text>{{ formatDate(group.time) }}</text>
- </view>
- </view>
- </view>
- <!-- 左滑显示的操作按钮 -->
- <view class="swipe-actions">
- <view v-for="(action, actionIndex) in group.actions" :key="actionIndex" class="action-btn"
- @click.stop="handleActionClick(action, group)">
- {{ action.name }}
- </view>
- </view>
- </view>
- </view>
- </view>
- <!-- 空状态 -->
- <view v-if="draftList.length === 0 && approvalGroups.length === 0" class="empty-state">
- <Icon name="icon-wujilu" size="120" color="#ccc" />
- <text class="empty-text">暂无待办事项</text>
- </view>
- </view>
- </scroll-view>
- </view>
- </template>
- <script setup>
- import { ref, onMounted, computed } from 'vue'
- import { onShow } from '@dcloudio/uni-app'
- import { todoApi } from '@/api/todo'
- import Icon from '@/components/icon/index.vue'
- import { goTo } from '@/utils/navigation'
- import { formatDate as utilFormatDate } from '@/utils/date'
- // 待办数据
- const draftList = ref([]) // 草稿箱数据
- const approvalGroups = ref([]) // 审核分组数据
- const loading = ref(false)
- // 展开状态
- const draftExpanded = ref(true) // 草稿箱展开状态
- // 左滑状态管理
- const swipeStates = ref({}) // 记录每个项目的滑动状态
- const touchStartX = ref(0) // 触摸开始位置
- const touchStartY = ref(0) // 触摸开始Y位置
- // 刷新状态
- const refreshing = ref(false)
- // 计算是否需要滚动
- const needScroll = computed(() => {
- // 如果数据很少,就不允许滚动
- const totalItems = draftList.value.length + approvalGroups.value.length
- return totalItems > 5 // 超过5个项目才允许滚动
- })
- /**
- * 切换草稿箱展开/收起
- */
- const toggleDraft = () => {
- draftExpanded.value = !draftExpanded.value
- // 如果收起,关闭所有草稿项的左滑状态
- if (!draftExpanded.value) {
- draftList.value.forEach((_, index) => {
- swipeStates.value[`draft-${index}`] = false
- })
- }
- }
- /**
- * 切换分组展开/收起
- */
- const toggleGroup = (groupIndex) => {
- const group = approvalGroups.value[groupIndex]
- group.expanded = !group.expanded
- // 如果收起,关闭该分组内所有子项的左滑状态
- if (!group.expanded && group.items) {
- group.items.forEach((_, itemIndex) => {
- swipeStates.value[`folder-${groupIndex}-${itemIndex}`] = false
- })
- }
- // 同时关闭该分组本身的左滑状态(如果是文件类型)
- swipeStates.value[`file-${groupIndex}`] = false
- }
- /**
- * 格式化时间 - 使用工具函数
- */
- const formatTime = (timeStr) => {
- if (!timeStr) return ''
- try {
- // 使用工具函数解析时间,返回 HH:mm 格式
- return utilFormatDate(timeStr, 'HH:mm')
- } catch (error) {
- console.error('时间格式化错误:', error, timeStr)
- return ''
- }
- }
- /**
- * 格式化日期 - 使用工具函数
- */
- const formatDate = (timeStr) => {
- if (!timeStr) return ''
- try {
- // 使用工具函数解析日期,返回 MM/dd 格式
- return utilFormatDate(timeStr, 'MM/DD')
- } catch (error) {
- console.error('日期格式化错误:', error, timeStr)
- return ''
- }
- }
- /**
- * 处理待办项点击
- */
- const handleTodoClick = (item) => {
- //console.log('点击待办项:', item)
- // 根据项目类型选择合适的服务
- let targetService = null
- if (item.type === 'folder' && item.cservice) {
- // 批量处理项目使用 cservice
- targetService = item.cservice
- } else if (item.service) {
- // 单个审核项目使用 service
- targetService = item.service
- }
- if (targetService) {
- // 根据service配置跳转
- const parts = targetService.dest.split('_')
- const dir = parts[0]
- const mobilePage = `/pages/${dir}/${targetService.dest}`
- goTo(mobilePage, targetService.param || {})
- } else {
- console.warn('没有找到可用的服务配置:', item)
- }
- }
- /**
- * 处理操作按钮点击
- */
- const handleActionClick = (action, item) => {
- //console.log('点击操作按钮:', action, item)
- if (action.service) {
- const parts = action.service.dest.split('_')
- const dir = parts[0]
- const mobilePage = `/pages/${dir}/${action.service.dest}`
- goTo(mobilePage, action.service.param || {})
- }
- }
- /**
- * 编辑草稿
- */
- const handleEditDraft = (item) => {
- //console.log('编辑草稿:', item)
- handleTodoClick(item)
- }
- /**
- * 删除草稿
- */
- const handleDeleteDraft = (item) => {
- //console.log('删除草稿:', item)
- uni.showModal({
- title: '确认删除',
- content: `确定要删除草稿"${item.name}"吗?`,
- success: (res) => {
- if (res.confirm) {
- // TODO: 调用删除API
- const index = draftList.value.findIndex(draft => draft.id === item.id)
- if (index > -1) {
- draftList.value.splice(index, 1)
- uni.showToast({
- title: '删除成功',
- icon: 'success'
- })
- }
- }
- }
- })
- }
- /**
- * 处理触摸开始
- */
- const handleTouchStart = (event, itemId) => {
- touchStartX.value = event.touches[0].clientX
- touchStartY.value = event.touches[0].clientY
- }
- /**
- * 处理触摸移动
- */
- const handleTouchMove = (event, itemId) => {
- const deltaX = event.touches[0].clientX - touchStartX.value
- const deltaY = event.touches[0].clientY - touchStartY.value
- // 判断是否为水平滑动
- if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 30) {
- event.preventDefault()
- if (deltaX < -50) {
- // 左滑,先关闭其他所有项目,再显示当前项目
- closeAllSwipeStates()
- swipeStates.value[itemId] = true
- } else if (deltaX > 50) {
- // 右滑,隐藏操作按钮
- swipeStates.value[itemId] = false
- }
- }
- }
- /**
- * 关闭所有左滑状态
- */
- const closeAllSwipeStates = () => {
- Object.keys(swipeStates.value).forEach(key => {
- swipeStates.value[key] = false
- })
- }
- /**
- * 下拉刷新
- */
- const onRefresh = async () => {
- refreshing.value = true
- try {
- // 关闭所有左滑状态
- closeAllSwipeStates()
- // 关闭所有展开状态
- draftExpanded.value = false
- approvalGroups.value.forEach(group => {
- group.expanded = false
- })
- // 重新加载数据
- await loadTodoData()
- uni.showToast({
- title: '刷新成功',
- icon: 'success'
- })
- } catch (error) {
- console.error('刷新失败:', error)
- uni.showToast({
- title: '刷新失败',
- icon: 'error'
- })
- } finally {
- refreshing.value = false
- }
- }
- /**
- * 刷新恢复
- */
- const onRefreshRestore = () => {
- refreshing.value = false
- }
- /**
- * 判断项目是否有操作
- */
- const hasActions = (item) => {
- return item.actions && item.actions.length > 0
- }
- /**
- * 处理触摸结束
- */
- const handleTouchEnd = (event, itemId) => {
- // 可以在这里添加一些收尾逻辑
- }
- /**
- * 加载待办数据
- */
- const loadTodoData = async () => {
- try {
- loading.value = true
- // //console.log('加载待办数据')
- const res = await todoApi.getTaskHomep()
- //console.log('待办数据:', res.data)
- // 使用模拟数据(已注释,现在使用真实数据)
- // const mockData = getMockData()
- // //console.log('模拟数据:', mockData)
- // 分别处理草稿和审核数据
- // draftList.value = processDraftData(mockData.cgList)
- // approvalGroups.value = processApprovalData(mockData.shList)
- approvalGroups.value = processApprovalData(res.data.shList)
- // //console.log('处理后的草稿数据:', draftList.value)
- // //console.log('处理后的审核数据:', approvalGroups.value)
- } catch (error) {
- console.error('加载待办数据失败:', error)
- uni.showToast({
- title: '加载失败',
- icon: 'none'
- })
- // 设置空数据
- draftList.value = []
- approvalGroups.value = []
- } finally {
- loading.value = false
- }
- }
- /**
- * 处理草稿数据
- */
- const processDraftData = (cgList) => {
- return cgList.filter(item => !item.branch).map((item, index) => ({
- id: item.id,
- name: item.mc,
- time: item.sqsj,
- service: item.updateService,
- // 测试:只有部分草稿有操作(偶数索引的有操作)
- actions: index % 2 === 0 ? [
- { name: '编辑', type: 'edit' },
- { name: '删除', type: 'delete' }
- ] : []
- }))
- }
- /**
- * 处理审核数据 - 按照PC端的树形结构逻辑
- */
- const processApprovalData = (shList) => {
- //console.log('处理审核数据:', shList)
- if (!shList || !Array.isArray(shList)) {
- console.warn('shList 不是数组:', shList)
- return []
- }
- const groups = []
- const parentMap = new Map() // 存储批量处理项目作为父节点
- // 第一遍:收集所有批量处理项目作为父节点
- shList.forEach((innerList, groupIndex) => {
- if (innerList && Array.isArray(innerList) && innerList.length > 0) {
- innerList.forEach(item => {
- if (item.mark === 'pl') {
- const parentId = item.plid || item.index || `pl_${groupIndex}`
- parentMap.set(parentId, {
- id: parentId,
- name: item.name,
- type: 'folder',
- hasChildren: true,
- expanded: false,
- count: 0, // 稍后计算
- time: item.time, // 批量处理使用 time 字段
- cservice: item.cservice, // 批量操作服务
- items: [],
- // 批量处理项目通常没有单独的操作按钮
- actions: []
- })
- }
- })
- }
- })
- // 第二遍:处理所有项目,根据 plid 归类
- shList.forEach((innerList) => {
- if (innerList && Array.isArray(innerList) && innerList.length > 0) {
- innerList.forEach((item) => {
- if (item.mark === 'sh') {
- // 检查是否有对应的父节点
- const parentId = item.plid
- const parentNode = parentMap.get(parentId)
- if (parentNode) {
- // 作为子项目添加到对应的父节点
- parentNode.items.push({
- id: item.objectId || item.taskid,
- name: item.name,
- time: item.jzsj || item.fssj, // 优先使用截止时间,没有则用发送时间
- isOverdue: !!item.jzsj, // 有截止时间说明可能逾期
- service: item.service,
- // 根据是否有 iservice 决定是否显示操作按钮
- actions: item.iservice ? [
- { name: '查看', service: item.iservice }
- ] : []
- })
- parentNode.count = parentNode.items.length
- } else {
- // 独立的文件项目(没有对应的父节点)
- groups.push({
- id: item.objectId || item.taskid,
- name: item.name,
- type: 'file',
- hasChildren: false,
- expanded: true,
- count: 1,
- time: item.jzsj || item.fssj, // 优先使用截止时间,没有则用发送时间
- isOverdue: !!item.jzsj,
- service: item.service,
- // 根据是否有 iservice 决定是否显示操作按钮
- actions: item.iservice ? [
- { name: '查看', service: item.iservice }
- ] : []
- })
- }
- }
- })
- }
- })
- // 第三遍:将所有父节点添加到结果中
- parentMap.forEach(parent => {
- if (parent.items.length > 0) {
- groups.push(parent)
- }
- })
- //console.log('处理后的分组数据:', groups)
- //console.log('父节点映射:', parentMap)
- return groups
- }
- /**
- * 获取模拟数据
- */
- const getMockData = () => {
- return {
- // 草稿箱数据
- cgList: [
- {
- id: 'draft_1',
- mc: '车辆《粤A88888》的用车申请',
- sqsj: '2024-01-15 14:30:00',
- updateService: { dest: 'clyy_inp', param: {} }
- },
- {
- id: 'draft_2',
- mc: '于[部门]/单位名称]调整[某项工作]程的报批函',
- sqsj: '2024-01-14 09:15:00',
- updateService: { dest: 'qj_inp', param: {} }
- }
- ],
- // 审核列表数据
- shList: [
- // 单级审核项目
- [
- {
- objectId: 'sh_1',
- name: '车辆《粤A88888》的用车申请',
- beanName: '车辆申请',
- fssj: '2024-01-15 14:42:00',
- mark: 'sh',
- service: { dest: 'clyy_sh', param: {} },
- iservice: { name: 'clyy_view', title: '查看', dest: 'clyy_view', param: {} }
- }
- ],
- [
- {
- objectId: 'sh_2',
- name: '于[部门]/单位名称]调整[某项工作]程的报批函',
- beanName: '工作调整',
- fssj: '2024-01-15 14:42:00',
- mark: 'sh',
- service: { dest: 'gztz_sh', param: {} },
- iservice: { name: 'gztz_view', title: '查看', dest: 'gztz_view', param: {} }
- }
- ],
- // 多级审核项目(公文审批)
- [
- {
- objectId: 'pl_1',
- name: '申核《规章制度名称》(试行稿) 》并批准印发的请示',
- beanName: '公文审批',
- fssj: '2024-01-15 14:42:00',
- mark: 'pl',
- service: { dest: 'gw_sh', param: {} },
- iservice: { name: 'gw_view', title: '查看', dest: 'gw_view', param: {} }
- },
- {
- objectId: 'pl_2',
- name: '关于[设备采购/工程建设]项目预算及实施计划的申核报批',
- beanName: '公文审批',
- fssj: '2024-01-15 14:42:00',
- mark: 'pl',
- service: { dest: 'gw_sh2', param: {} },
- iservice: { name: 'gw_view2', title: '查看', dest: 'gw_view2', param: {} }
- },
- {
- objectId: 'pl_3',
- name: '关于[设备采购/工程建设]项目预算及实施计划的申核报批',
- beanName: '公文审批',
- fssj: '2024-01-15 14:42:00',
- mark: 'pl',
- service: { dest: 'gw_sh3', param: {} },
- iservice: { name: 'gw_view3', title: '查看', dest: 'gw_view3', param: {} }
- }
- ]
- ]
- }
- }
- // 生命周期处理函数
- const handleOnShow = () => {
- console.log('待办页面显示')
- // 页面显示时加载数据
- loadTodoData()
- }
- const handleOnHide = () => {
- console.log('待办页面隐藏')
- }
- const handleOnLoad = () => {
- console.log('待办页面加载')
- loadTodoData()
- }
- const handleOnUnload = () => {
- console.log('待办页面卸载')
- }
- // 页面加载时获取数据
- onMounted(() => {
- handleOnLoad()
- uni.loadFontFace({
- family: 'SSXinYiTi',
- source: 'url("https://m.hfdcschool.com/skin/mp_easy/fonts/056-ShangShouXinYiTi-2.woff")',
- success() {
- console.log('字体加载成功');
- },
- fail() {
- console.log('字体加载失败');
- },
- });
- })
- // 页面显示时更新数据
- onShow(() => {
- handleOnShow()
- uni.loadFontFace({
- family: 'SSXinYiTi',
- source: 'url("https://m.hfdcschool.com/skin/mp_easy/fonts/056-ShangShouXinYiTi-2.woff")',
- success() {
- console.log('字体加载成功');
- },
- fail() {
- console.log('字体加载失败');
- },
- });
- })
- // 下拉刷新
- const onPullDownRefresh = () => {
- loadTodoData().finally(() => {
- uni.stopPullDownRefresh()
- })
- }
- // 暴露生命周期方法供主容器调用
- defineExpose({
- onShow: handleOnShow,
- onHide: handleOnHide,
- onLoad: handleOnLoad,
- onUnload: handleOnUnload,
- onPullDownRefresh
- })
- </script>
- <style scoped lang="scss">
- @import './todo_list.scss';
- </style>
|