|
|
@@ -1,12 +1,495 @@
|
|
|
<template>
|
|
|
- <view class="page-shell" style="padding:24rpx">
|
|
|
- <text>{{ title }}</text>
|
|
|
+ <view class="page">
|
|
|
+ <view class="toolbar">
|
|
|
+ <view class="toolbar-row">
|
|
|
+ <view class="search-box" :class="{ 'is-focus': appNameFocused }">
|
|
|
+ <view class="search-ico" aria-hidden="true" />
|
|
|
+ <input
|
|
|
+ class="search-input"
|
|
|
+ :value="appName"
|
|
|
+ confirm-type="search"
|
|
|
+ placeholder="搜索流程名称"
|
|
|
+ placeholder-class="search-ph"
|
|
|
+ @input="onAppNameInput"
|
|
|
+ @confirm="onSearchConfirm"
|
|
|
+ @focus="appNameFocused = true"
|
|
|
+ @blur="appNameFocused = false"
|
|
|
+ />
|
|
|
+ <view v-if="appName" class="search-clear" @click="clearAppName">
|
|
|
+ <text class="search-clear__x">×</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="toolbar-row toolbar-row--sub">
|
|
|
+ <view class="search-box" :class="{ 'is-focus': initiatorFocused }">
|
|
|
+ <input
|
|
|
+ class="search-input"
|
|
|
+ :value="initiatorName"
|
|
|
+ confirm-type="search"
|
|
|
+ placeholder="搜索申请人"
|
|
|
+ placeholder-class="search-ph"
|
|
|
+ @input="onInitiatorInput"
|
|
|
+ @confirm="onSearchConfirm"
|
|
|
+ @focus="initiatorFocused = true"
|
|
|
+ @blur="initiatorFocused = false"
|
|
|
+ />
|
|
|
+ <view v-if="initiatorName" class="search-clear" @click="clearInitiator">
|
|
|
+ <text class="search-clear__x">×</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="read-tabs">
|
|
|
+ <view
|
|
|
+ v-for="tab in readTabs"
|
|
|
+ :key="tab.value"
|
|
|
+ class="read-tab"
|
|
|
+ :class="{ 'is-active': readStatus === tab.value }"
|
|
|
+ @click="setReadStatus(tab.value)"
|
|
|
+ >
|
|
|
+ {{ tab.label }}
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="loading && !list.length" class="state state-loading">
|
|
|
+ <view class="pulse-dots" aria-hidden="true">
|
|
|
+ <view class="pulse-dot" />
|
|
|
+ <view class="pulse-dot" />
|
|
|
+ <view class="pulse-dot" />
|
|
|
+ </view>
|
|
|
+ <text class="state-loading__text">加载抄送…</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-else-if="!list.length" class="oa-task-list__empty">暂无抄送</view>
|
|
|
+
|
|
|
+ <view v-else class="oa-task-list">
|
|
|
+ <OaTaskCard
|
|
|
+ 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)"
|
|
|
+ />
|
|
|
+ <view v-if="loadingMore" class="oa-task-list__end">加载更多…</view>
|
|
|
+ <view v-else-if="!hasMore" class="oa-task-list__end">已经到底了</view>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
</template>
|
|
|
+
|
|
|
<script>
|
|
|
-export default {
|
|
|
- data() {
|
|
|
- return { title: '抄送我的' }
|
|
|
+ import OaTaskCard from '../../../components/OaTaskCard.vue'
|
|
|
+ import { fetchMarkOaCcTaskRead, fetchOaTasks } from '../../../utils/oa-api.js'
|
|
|
+ import { forceRelogin, getToken } from '../../../utils/auth.js'
|
|
|
+ import { stashOaDetailTitle } from '../../../utils/oa-h5.js'
|
|
|
+
|
|
|
+ const PAGE_SIZE = 20
|
|
|
+
|
|
|
+ const STATUS_MAP = {
|
|
|
+ pending: '待处理',
|
|
|
+ running: '进行中',
|
|
|
+ approved: '已同意',
|
|
|
+ rejected: '已拒绝',
|
|
|
+ completed: '已完成',
|
|
|
+ cancelled: '已撤销',
|
|
|
+ done: '已结束',
|
|
|
+ returned: '已打回',
|
|
|
+ transferred: '已转交'
|
|
|
+ }
|
|
|
+
|
|
|
+ export default {
|
|
|
+ components: { OaTaskCard },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ appName: '',
|
|
|
+ initiatorName: '',
|
|
|
+ readStatus: 'unread',
|
|
|
+ appNameFocused: false,
|
|
|
+ initiatorFocused: false,
|
|
|
+ list: [],
|
|
|
+ page: 1,
|
|
|
+ total: 0,
|
|
|
+ loading: false,
|
|
|
+ loadingMore: false,
|
|
|
+ hasMore: true,
|
|
|
+ openingId: '',
|
|
|
+ _searchTimer: null,
|
|
|
+ readTabs: [
|
|
|
+ { value: 'unread', label: '未读' },
|
|
|
+ { value: 'read', label: '已读' },
|
|
|
+ { value: 'all', label: '全部' }
|
|
|
+ ],
|
|
|
+ cardActions: [{ key: 'view', label: '查看', primary: true }]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onShow() {
|
|
|
+ if (!getToken()) {
|
|
|
+ forceRelogin('请先登录')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.loadList(true)
|
|
|
+ },
|
|
|
+ onPullDownRefresh() {
|
|
|
+ this.loadList(true)
|
|
|
+ .catch(() => {})
|
|
|
+ .finally(() => {
|
|
|
+ uni.stopPullDownRefresh()
|
|
|
+ })
|
|
|
+ },
|
|
|
+ onReachBottom() {
|
|
|
+ if (!this.hasMore || this.loading || this.loadingMore) return
|
|
|
+ this.loadList(false)
|
|
|
+ },
|
|
|
+ onUnload() {
|
|
|
+ if (this._searchTimer) {
|
|
|
+ clearTimeout(this._searchTimer)
|
|
|
+ this._searchTimer = null
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ cardTitle(row) {
|
|
|
+ return (row && (row.appName || row.title)) || '-'
|
|
|
+ },
|
|
|
+ statusText(s) {
|
|
|
+ if (!s) return '-'
|
|
|
+ return STATUS_MAP[s] || s
|
|
|
+ },
|
|
|
+ statusTone(s) {
|
|
|
+ if (s === 'pending' || s === 'running') return 'warn'
|
|
|
+ if (s === 'approved' || s === 'completed' || s === 'done') return 'success'
|
|
|
+ if (s === 'rejected' || s === 'cancelled' || s === 'returned') return 'danger'
|
|
|
+ return 'info'
|
|
|
+ },
|
|
|
+ metaLines(row) {
|
|
|
+ return [
|
|
|
+ {
|
|
|
+ label: '申请人',
|
|
|
+ value: (row && row.initiatorUserName) || '-'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '申请时间',
|
|
|
+ value: (row && row.applyTime) || '-'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '阅读',
|
|
|
+ value: row && row.isRead ? '已读' : '未读'
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ scheduleSearch() {
|
|
|
+ if (this._searchTimer) clearTimeout(this._searchTimer)
|
|
|
+ this._searchTimer = setTimeout(() => {
|
|
|
+ this.loadList(true)
|
|
|
+ }, 400)
|
|
|
+ },
|
|
|
+ onAppNameInput(e) {
|
|
|
+ this.appName = this._inputVal(e)
|
|
|
+ this.scheduleSearch()
|
|
|
+ },
|
|
|
+ onInitiatorInput(e) {
|
|
|
+ this.initiatorName = this._inputVal(e)
|
|
|
+ this.scheduleSearch()
|
|
|
+ },
|
|
|
+ _inputVal(e) {
|
|
|
+ if (e && e.detail && e.detail.value != null) return String(e.detail.value)
|
|
|
+ if (e && e.target) return String(e.target.value || '')
|
|
|
+ return ''
|
|
|
+ },
|
|
|
+ onSearchConfirm() {
|
|
|
+ if (this._searchTimer) {
|
|
|
+ clearTimeout(this._searchTimer)
|
|
|
+ this._searchTimer = null
|
|
|
+ }
|
|
|
+ this.loadList(true)
|
|
|
+ },
|
|
|
+ clearAppName() {
|
|
|
+ this.appName = ''
|
|
|
+ this.onSearchConfirm()
|
|
|
+ },
|
|
|
+ clearInitiator() {
|
|
|
+ this.initiatorName = ''
|
|
|
+ this.onSearchConfirm()
|
|
|
+ },
|
|
|
+ setReadStatus(value) {
|
|
|
+ if (this.readStatus === value) return
|
|
|
+ this.readStatus = value
|
|
|
+ this.loadList(true)
|
|
|
+ },
|
|
|
+ async openView(row) {
|
|
|
+ if (!row || this.openingId) return
|
|
|
+ const instanceId = String(row.instanceId || '').trim()
|
|
|
+ const taskId = String(row.id || '').trim()
|
|
|
+ if (!instanceId) {
|
|
|
+ uni.showToast({ title: '缺少实例 ID', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.openingId = taskId || instanceId
|
|
|
+ try {
|
|
|
+ if (taskId && !row.isRead) {
|
|
|
+ try {
|
|
|
+ await fetchMarkOaCcTaskRead(taskId)
|
|
|
+ row.isRead = true
|
|
|
+ } catch (e) {
|
|
|
+ uni.showToast({
|
|
|
+ title: (e && e.message) || '标记已读失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ stashOaDetailTitle(row.appName || row.title)
|
|
|
+ uni.navigateTo({
|
|
|
+ url:
|
|
|
+ '/pages/oa/webview/index?instanceId=' +
|
|
|
+ encodeURIComponent(instanceId) +
|
|
|
+ '&mode=view'
|
|
|
+ })
|
|
|
+ } finally {
|
|
|
+ this.openingId = ''
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async loadList(reset) {
|
|
|
+ if (!getToken()) {
|
|
|
+ forceRelogin('请先登录')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const isReset = !!reset
|
|
|
+ if (isReset) {
|
|
|
+ this.page = 1
|
|
|
+ this.hasMore = true
|
|
|
+ this.loading = true
|
|
|
+ } else {
|
|
|
+ this.loadingMore = true
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const res = await fetchOaTasks('cc', {
|
|
|
+ appName: (this.appName || '').trim() || undefined,
|
|
|
+ initiatorName: (this.initiatorName || '').trim() || undefined,
|
|
|
+ readStatus: this.readStatus || 'unread',
|
|
|
+ page: this.page,
|
|
|
+ pageSize: PAGE_SIZE
|
|
|
+ })
|
|
|
+ const rows = (res && res.list) || []
|
|
|
+ const total = (res && res.total) != null ? Number(res.total) : 0
|
|
|
+ this.total = total
|
|
|
+ if (isReset) this.list = rows
|
|
|
+ else this.list = this.list.concat(rows)
|
|
|
+ const loaded = this.list.length
|
|
|
+ this.hasMore = loaded < total && rows.length > 0
|
|
|
+ if (rows.length > 0) this.page += 1
|
|
|
+ } catch (e) {
|
|
|
+ if (isReset) this.list = []
|
|
|
+ uni.showToast({
|
|
|
+ title: (e && e.message) || '加载失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ } finally {
|
|
|
+ this.loading = false
|
|
|
+ this.loadingMore = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
-}
|
|
|
</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+ .page {
|
|
|
+ min-height: 100vh;
|
|
|
+ padding-bottom: 48rpx;
|
|
|
+ background: #f5f6f8;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toolbar {
|
|
|
+ position: sticky;
|
|
|
+ top: 0;
|
|
|
+ z-index: 10;
|
|
|
+ background: #fff;
|
|
|
+ padding-bottom: 16rpx;
|
|
|
+ border-bottom: 1rpx solid #f1f5f9;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toolbar-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16rpx;
|
|
|
+ padding: 20rpx 24rpx 0;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toolbar-row--sub {
|
|
|
+ padding-top: 12rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-box {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16rpx;
|
|
|
+ height: 72rpx;
|
|
|
+ padding: 0 24rpx;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ border: 1rpx solid #e8eaed;
|
|
|
+ background: #fafafa;
|
|
|
+ box-sizing: border-box;
|
|
|
+ transition:
|
|
|
+ border-color 0.15s,
|
|
|
+ background 0.15s,
|
|
|
+ box-shadow 0.15s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-box.is-focus {
|
|
|
+ border-color: #cbd5e1;
|
|
|
+ background: #fff;
|
|
|
+ box-shadow: 0 0 0 6rpx rgba(148, 163, 184, 0.18);
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-ico {
|
|
|
+ position: relative;
|
|
|
+ width: 26rpx;
|
|
|
+ height: 26rpx;
|
|
|
+ border: 3rpx solid #94a3b8;
|
|
|
+ border-radius: 50%;
|
|
|
+ box-sizing: border-box;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-ico::after {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ width: 12rpx;
|
|
|
+ height: 3rpx;
|
|
|
+ right: -8rpx;
|
|
|
+ bottom: -5rpx;
|
|
|
+ background: #94a3b8;
|
|
|
+ border-radius: 3rpx;
|
|
|
+ transform: rotate(45deg);
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-input {
|
|
|
+ flex: 1;
|
|
|
+ height: 72rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #1f2937;
|
|
|
+ min-width: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-ph {
|
|
|
+ color: #b0b7c3;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-clear {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 40rpx;
|
|
|
+ height: 40rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #e5e7eb;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-clear__x {
|
|
|
+ font-size: 28rpx;
|
|
|
+ line-height: 1;
|
|
|
+ color: #6b7280;
|
|
|
+ margin-top: -2rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .read-tabs {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12rpx;
|
|
|
+ padding: 16rpx 24rpx 0;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ .read-tab {
|
|
|
+ flex: 1;
|
|
|
+ height: 64rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border-radius: 14rpx;
|
|
|
+ font-size: 26rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #64748b;
|
|
|
+ background: #f8fafc;
|
|
|
+ border: 1rpx solid #e2e8f0;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ .read-tab.is-active {
|
|
|
+ color: #166534;
|
|
|
+ background: #ecfdf5;
|
|
|
+ border-color: #a7f3d0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .read-tab:active {
|
|
|
+ opacity: 0.78;
|
|
|
+ }
|
|
|
+
|
|
|
+ .state {
|
|
|
+ padding: 80rpx 24rpx;
|
|
|
+ text-align: center;
|
|
|
+ color: #999;
|
|
|
+ font-size: 28rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .state-loading {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 28rpx;
|
|
|
+ min-height: 50vh;
|
|
|
+ padding: 96rpx 32rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pulse-dots {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pulse-dot {
|
|
|
+ width: 16rpx;
|
|
|
+ height: 16rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: var(--brand-green, #22c55e);
|
|
|
+ animation: oa-pulse-bounce 1.2s ease-in-out infinite;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pulse-dot:nth-child(2) {
|
|
|
+ animation-delay: 0.15s;
|
|
|
+ opacity: 0.75;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pulse-dot:nth-child(3) {
|
|
|
+ animation-delay: 0.3s;
|
|
|
+ opacity: 0.55;
|
|
|
+ }
|
|
|
+
|
|
|
+ .state-loading__text {
|
|
|
+ font-size: 26rpx;
|
|
|
+ font-weight: 500;
|
|
|
+ color: var(--text-muted, #5f7a6d);
|
|
|
+ line-height: 1.4;
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes oa-pulse-bounce {
|
|
|
+ 0%,
|
|
|
+ 80%,
|
|
|
+ 100% {
|
|
|
+ transform: translateY(0);
|
|
|
+ }
|
|
|
+ 40% {
|
|
|
+ transform: translateY(-12rpx);
|
|
|
+ }
|
|
|
+ }
|
|
|
+</style>
|