Просмотр исходного кода

feat(oa): add cc read filters batch mark and unread styles

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 2 месяцев назад
Родитель
Сommit
06b1468bf1

+ 9 - 2
mobile-h5/components/OaTaskCard.vue

@@ -34,9 +34,10 @@
         class="oa-task-card__btn"
         :class="{
           'is-primary': !!act.primary,
-          'is-danger': !!act.danger && !act.primary
+          'is-danger': !!act.danger && !act.primary,
+          'is-disabled': !!act.disabled
         }"
-        @click.stop="$emit('action', act)"
+        @click.stop="onActionClick(act)"
       >
         {{ act.label }}
       </view>
@@ -63,6 +64,12 @@
         const t = String(this.statusTone || 'info').toLowerCase()
         return TONES.includes(t) ? t : 'info'
       }
+    },
+    methods: {
+      onActionClick(act) {
+        if (!act || act.disabled) return
+        this.$emit('action', act)
+      }
     }
   }
 </script>

+ 257 - 13
mobile-h5/pages/oa/cc/index.vue

@@ -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;

+ 5 - 0
mobile-h5/styles/components/oa-task-list.scss

@@ -166,3 +166,8 @@
   background: rgba(239, 68, 68, 0.06);
   border-color: rgba(239, 68, 68, 0.35);
 }
+
+.oa-task-card__btn.is-disabled {
+  opacity: 0.45;
+  pointer-events: none;
+}