|
|
@@ -19,6 +19,13 @@
|
|
|
<text class="search-clear__x">×</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
+ <view
|
|
|
+ class="manage-btn"
|
|
|
+ :class="{ 'is-active': manageMode }"
|
|
|
+ @click="toggleManage"
|
|
|
+ >
|
|
|
+ {{ manageMode ? '完成' : '管理' }}
|
|
|
+ </view>
|
|
|
</view>
|
|
|
<view class="toolbar-row toolbar-row--sub">
|
|
|
<view class="search-box" :class="{ 'is-focus': initiatorFocused }">
|
|
|
@@ -51,6 +58,17 @@
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
+ <view v-if="manageMode && selectedIds.length" class="batch-bar">
|
|
|
+ <text class="batch-bar__hint">已选 {{ selectedIds.length }} 项</text>
|
|
|
+ <view
|
|
|
+ class="batch-bar__btn"
|
|
|
+ :class="{ 'is-disabled': batchReadDisabled }"
|
|
|
+ @click="batchMarkRead"
|
|
|
+ >
|
|
|
+ 批量已读
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
<view v-if="loading && !list.length" class="state state-loading">
|
|
|
<view class="pulse-dots" aria-hidden="true">
|
|
|
<view class="pulse-dot" />
|
|
|
@@ -63,18 +81,34 @@
|
|
|
<view v-else-if="!list.length" class="oa-task-list__empty">暂无抄送</view>
|
|
|
|
|
|
<view v-else class="oa-task-list">
|
|
|
- <OaTaskCard
|
|
|
+ <view
|
|
|
v-for="row in list"
|
|
|
:key="row.id"
|
|
|
- :title="cardTitle(row)"
|
|
|
- :status-text="statusText(row.instanceStatus)"
|
|
|
- :status-tone="statusTone(row.instanceStatus)"
|
|
|
- :unread="!row.isRead"
|
|
|
- :meta-lines="metaLines(row)"
|
|
|
- :actions="cardActions"
|
|
|
- @open="openView(row)"
|
|
|
- @action="openView(row)"
|
|
|
- />
|
|
|
+ class="cc-row"
|
|
|
+ :class="{ 'is-manage': manageMode, 'is-selected': isSelected(row.id) }"
|
|
|
+ >
|
|
|
+ <view
|
|
|
+ v-if="manageMode"
|
|
|
+ class="cc-check"
|
|
|
+ @click.stop="toggleSelect(row.id)"
|
|
|
+ >
|
|
|
+ <view class="cc-check__box" :class="{ on: isSelected(row.id) }">
|
|
|
+ <text v-if="isSelected(row.id)" class="cc-check__mark">✓</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="cc-card-wrap">
|
|
|
+ <OaTaskCard
|
|
|
+ :title="cardTitle(row)"
|
|
|
+ :status-text="statusText(row.instanceStatus)"
|
|
|
+ :status-tone="statusTone(row.instanceStatus)"
|
|
|
+ :unread="!row.isRead"
|
|
|
+ :meta-lines="metaLines(row)"
|
|
|
+ :actions="cardActionsFor(row)"
|
|
|
+ @open="onCardOpen(row)"
|
|
|
+ @action="onCardAction($event, row)"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
<view v-if="loadingMore" class="oa-task-list__end">加载更多…</view>
|
|
|
<view v-else-if="!hasMore" class="oa-task-list__end">已经到底了</view>
|
|
|
</view>
|
|
|
@@ -83,7 +117,11 @@
|
|
|
|
|
|
<script>
|
|
|
import OaTaskCard from '../../../components/OaTaskCard.vue'
|
|
|
- import { fetchMarkOaCcTaskRead, fetchOaTasks } from '../../../utils/oa-api.js'
|
|
|
+ import {
|
|
|
+ fetchBatchMarkOaCcTasksRead,
|
|
|
+ fetchMarkOaCcTaskRead,
|
|
|
+ fetchOaTasks
|
|
|
+ } from '../../../utils/oa-api.js'
|
|
|
import { forceRelogin, getToken } from '../../../utils/auth.js'
|
|
|
import { stashOaDetailTitle } from '../../../utils/oa-h5.js'
|
|
|
|
|
|
@@ -116,14 +154,26 @@
|
|
|
loading: false,
|
|
|
loadingMore: false,
|
|
|
hasMore: true,
|
|
|
+ manageMode: false,
|
|
|
+ selectedIds: [],
|
|
|
openingId: '',
|
|
|
+ markingId: '',
|
|
|
+ batchMarking: false,
|
|
|
_searchTimer: null,
|
|
|
readTabs: [
|
|
|
{ value: 'unread', label: '未读' },
|
|
|
{ value: 'read', label: '已读' },
|
|
|
{ value: 'all', label: '全部' }
|
|
|
- ],
|
|
|
- cardActions: [{ key: 'view', label: '查看', primary: true }]
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ batchReadDisabled() {
|
|
|
+ const ids = this.selectedIds || []
|
|
|
+ if (!ids.length) return true
|
|
|
+ return !this.list.some(
|
|
|
+ (r) => r && ids.indexOf(r.id) >= 0 && !r.isRead
|
|
|
+ )
|
|
|
}
|
|
|
},
|
|
|
onShow() {
|
|
|
@@ -180,6 +230,20 @@
|
|
|
}
|
|
|
]
|
|
|
},
|
|
|
+ cardActionsFor(row) {
|
|
|
+ if (this.manageMode) return []
|
|
|
+ return [
|
|
|
+ { key: 'view', label: '查看', primary: true },
|
|
|
+ {
|
|
|
+ key: 'read',
|
|
|
+ label: '已读',
|
|
|
+ disabled: !!(row && row.isRead)
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ isSelected(id) {
|
|
|
+ return this.selectedIds.indexOf(id) >= 0
|
|
|
+ },
|
|
|
scheduleSearch() {
|
|
|
if (this._searchTimer) clearTimeout(this._searchTimer)
|
|
|
this._searchTimer = setTimeout(() => {
|
|
|
@@ -219,6 +283,73 @@
|
|
|
this.readStatus = value
|
|
|
this.loadList(true)
|
|
|
},
|
|
|
+ toggleManage() {
|
|
|
+ this.manageMode = !this.manageMode
|
|
|
+ if (!this.manageMode) this.selectedIds = []
|
|
|
+ },
|
|
|
+ toggleSelect(id) {
|
|
|
+ if (!id) return
|
|
|
+ const i = this.selectedIds.indexOf(id)
|
|
|
+ if (i >= 0) {
|
|
|
+ this.selectedIds = this.selectedIds.filter((x) => x !== id)
|
|
|
+ } else {
|
|
|
+ this.selectedIds = this.selectedIds.concat([id])
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onCardOpen(row) {
|
|
|
+ if (this.manageMode) {
|
|
|
+ this.toggleSelect(row && row.id)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.openView(row)
|
|
|
+ },
|
|
|
+ onCardAction(act, row) {
|
|
|
+ if (!act || !row) return
|
|
|
+ if (act.key === 'view') this.openView(row)
|
|
|
+ else if (act.key === 'read') this.markOneRead(row)
|
|
|
+ },
|
|
|
+ async markOneRead(row) {
|
|
|
+ if (!row || row.isRead || this.markingId) return
|
|
|
+ const taskId = String(row.id || '').trim()
|
|
|
+ if (!taskId) return
|
|
|
+ this.markingId = taskId
|
|
|
+ try {
|
|
|
+ await fetchMarkOaCcTaskRead(taskId)
|
|
|
+ uni.showToast({ title: '已标记已读', icon: 'success' })
|
|
|
+ await this.loadList(true)
|
|
|
+ } catch (e) {
|
|
|
+ uni.showToast({
|
|
|
+ title: (e && e.message) || '标记已读失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ } finally {
|
|
|
+ this.markingId = ''
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async batchMarkRead() {
|
|
|
+ if (this.batchReadDisabled || this.batchMarking) return
|
|
|
+ const selected = this.selectedIds || []
|
|
|
+ const ids = this.list
|
|
|
+ .filter((r) => r && selected.indexOf(r.id) >= 0 && !r.isRead)
|
|
|
+ .map((r) => String(r.id || '').trim())
|
|
|
+ .filter(Boolean)
|
|
|
+ if (!ids.length) return
|
|
|
+ this.batchMarking = true
|
|
|
+ try {
|
|
|
+ await fetchBatchMarkOaCcTasksRead(ids)
|
|
|
+ uni.showToast({ title: '已标记已读', icon: 'success' })
|
|
|
+ this.selectedIds = []
|
|
|
+ this.manageMode = false
|
|
|
+ await this.loadList(true)
|
|
|
+ } catch (e) {
|
|
|
+ uni.showToast({
|
|
|
+ title: (e && e.message) || '批量已读失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ } finally {
|
|
|
+ this.batchMarking = false
|
|
|
+ }
|
|
|
+ },
|
|
|
async openView(row) {
|
|
|
if (!row || this.openingId) return
|
|
|
const instanceId = String(row.instanceId || '').trim()
|
|
|
@@ -261,6 +392,7 @@
|
|
|
this.page = 1
|
|
|
this.hasMore = true
|
|
|
this.loading = true
|
|
|
+ this.selectedIds = []
|
|
|
} else {
|
|
|
this.loadingMore = true
|
|
|
}
|
|
|
@@ -400,6 +532,28 @@
|
|
|
margin-top: -2rpx;
|
|
|
}
|
|
|
|
|
|
+ .manage-btn {
|
|
|
+ flex-shrink: 0;
|
|
|
+ min-width: 96rpx;
|
|
|
+ height: 72rpx;
|
|
|
+ padding: 0 16rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #166534;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ .manage-btn.is-active {
|
|
|
+ color: #64748b;
|
|
|
+ }
|
|
|
+
|
|
|
+ .manage-btn:active {
|
|
|
+ opacity: 0.72;
|
|
|
+ }
|
|
|
+
|
|
|
.read-tabs {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
@@ -433,6 +587,96 @@
|
|
|
opacity: 0.78;
|
|
|
}
|
|
|
|
|
|
+ .batch-bar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 16rpx;
|
|
|
+ margin: 16rpx 24rpx 0;
|
|
|
+ padding: 20rpx 24rpx;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ background: #fff;
|
|
|
+ border: 1rpx solid #d8f3e5;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ .batch-bar__hint {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #5f7a6d;
|
|
|
+ }
|
|
|
+
|
|
|
+ .batch-bar__btn {
|
|
|
+ min-height: 64rpx;
|
|
|
+ padding: 0 28rpx;
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border-radius: 14rpx;
|
|
|
+ font-size: 26rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #166534;
|
|
|
+ background: #ecfdf5;
|
|
|
+ border: 1rpx solid #a7f3d0;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ .batch-bar__btn.is-disabled {
|
|
|
+ opacity: 0.45;
|
|
|
+ pointer-events: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .batch-bar__btn:active {
|
|
|
+ opacity: 0.78;
|
|
|
+ }
|
|
|
+
|
|
|
+ .cc-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: stretch;
|
|
|
+ gap: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .cc-row.is-manage .cc-card-wrap {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .cc-row.is-selected .cc-card-wrap :deep(.oa-task-card) {
|
|
|
+ border-color: rgba(34, 197, 94, 0.45);
|
|
|
+ box-shadow: 0 8rpx 32rpx rgba(22, 163, 74, 0.12);
|
|
|
+ }
|
|
|
+
|
|
|
+ .cc-check {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ padding: 40rpx 0 0 4rpx;
|
|
|
+ margin-right: 12rpx;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .cc-check__box {
|
|
|
+ width: 40rpx;
|
|
|
+ height: 40rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ border: 2rpx solid #cbd5e1;
|
|
|
+ background: #fff;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ .cc-check__box.on {
|
|
|
+ border-color: #22c55e;
|
|
|
+ background: #22c55e;
|
|
|
+ }
|
|
|
+
|
|
|
+ .cc-check__mark {
|
|
|
+ font-size: 22rpx;
|
|
|
+ line-height: 1;
|
|
|
+ color: #fff;
|
|
|
+ font-weight: 700;
|
|
|
+ }
|
|
|
+
|
|
|
.state {
|
|
|
padding: 80rpx 24rpx;
|
|
|
text-align: center;
|