| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743 |
- <template>
- <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
- 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 }">
- <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="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" />
- <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">
- <view
- v-for="row in list"
- :key="row.id"
- 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>
- </view>
- </template>
- <script>
- import OaTaskCard from '../../../components/OaTaskCard.vue'
- import {
- fetchBatchMarkOaCcTasksRead,
- 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,
- manageMode: false,
- selectedIds: [],
- openingId: '',
- markingId: '',
- batchMarking: false,
- _searchTimer: null,
- readTabs: [
- { value: 'unread', label: '未读' },
- { value: 'read', label: '已读' },
- { value: 'all', label: '全部' }
- ]
- }
- },
- 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() {
- 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 ? '已读' : '未读'
- }
- ]
- },
- cardActionsFor(row) {
- if (this.manageMode) return []
- return [
- { key: 'view', label: '查看', primary: true },
- {
- key: 'read',
- label: '已读',
- disabled: !!(row && row.isRead)
- },
- { key: 'print', label: '打印' }
- ]
- },
- isSelected(id) {
- return this.selectedIds.indexOf(id) >= 0
- },
- 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)
- },
- 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)
- else if (act.key === 'print') this.openView(row, { autoPrint: true })
- },
- 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, opts) {
- 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
- }
- const autoPrint = !!(opts && opts.autoPrint)
- 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' +
- (autoPrint ? '&autoPrint=1' : '')
- })
- } finally {
- this.openingId = ''
- }
- },
- async loadList(reset) {
- if (!getToken()) {
- forceRelogin('请先登录')
- return
- }
- const isReset = !!reset
- if (isReset) {
- this.page = 1
- this.hasMore = true
- this.loading = true
- this.selectedIds = []
- } 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;
- }
- .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;
- 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;
- }
- .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;
- 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>
|