| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430 |
- <template>
- <view class="page">
- <view class="toolbar">
- <view class="toolbar-row">
- <view class="search-box" :class="{ 'is-focus': searchFocused }">
- <view class="search-ico" aria-hidden="true" />
- <input
- class="search-input"
- :value="appName"
- confirm-type="search"
- placeholder="搜索流程名称"
- placeholder-class="search-ph"
- @input="onSearchInput"
- @confirm="onSearchConfirm"
- @focus="searchFocused = true"
- @blur="searchFocused = false"
- />
- <view v-if="appName" class="search-clear" @click="clearAppName">
- <text class="search-clear__x">×</text>
- </view>
- </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 || row.status)"
- :status-tone="statusTone(row.instanceStatus || row.status)"
- :meta-lines="metaLines(row)"
- :actions="rowActions(row)"
- @open="openView(row)"
- @action="onCardAction($event, 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>
- import OaTaskCard from '../../../components/OaTaskCard.vue'
- import { fetchOaTasks } from '../../../utils/oa-api.js'
- import { forceRelogin, getToken } from '../../../utils/auth.js'
- import { stashOaDetailTitle, stashOaStartTitle } from '../../../utils/oa-h5.js'
- const PAGE_SIZE = 20
- const COPY_RESTART_STATUSES = ['returned', 'cancelled', 'rejected']
- const STATUS_MAP = {
- pending: '待处理',
- running: '进行中',
- approved: '已同意',
- rejected: '已拒绝',
- completed: '已完成',
- cancelled: '已撤销',
- done: '已结束',
- returned: '已打回',
- transferred: '已转交'
- }
- export default {
- components: { OaTaskCard },
- data() {
- return {
- appName: '',
- searchFocused: false,
- list: [],
- page: 1,
- total: 0,
- loading: false,
- loadingMore: false,
- hasMore: true,
- _searchTimer: null
- }
- },
- 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.applyTime) || '-'
- },
- {
- label: '当前节点',
- value: (row && row.currentNodeName) || '-'
- },
- {
- label: '当前审批人',
- value: (row && row.currentApproverNames) || '-'
- }
- ]
- },
- rowActions(row) {
- const actions = [
- { key: 'view', label: '查看', primary: true },
- { key: 'print', label: '打印' }
- ]
- const status = String((row && (row.instanceStatus || row.status)) || '')
- const appId = String((row && row.appId) || '').trim()
- if (COPY_RESTART_STATUSES.includes(status) && appId) {
- actions.push({ key: 'copyRestart', label: '复制并重新发起' })
- }
- return actions
- },
- onSearchInput(e) {
- const v =
- e && e.detail && e.detail.value != null
- ? String(e.detail.value)
- : e && e.target
- ? String(e.target.value || '')
- : ''
- this.appName = v
- if (this._searchTimer) clearTimeout(this._searchTimer)
- this._searchTimer = setTimeout(() => {
- this.loadList(true)
- }, 400)
- },
- onSearchConfirm() {
- if (this._searchTimer) {
- clearTimeout(this._searchTimer)
- this._searchTimer = null
- }
- this.loadList(true)
- },
- clearAppName() {
- this.appName = ''
- this.onSearchConfirm()
- },
- onCardAction(act, row) {
- if (!act || !row) return
- if (act.key === 'copyRestart') {
- this.openCopyRestart(row)
- return
- }
- if (act.key === 'print') {
- this.openView(row, { autoPrint: true })
- return
- }
- this.openView(row)
- },
- openView(row, opts) {
- if (!row) return
- // mine 行是实例:Web 用 row.id;兼容 instanceId
- const instanceId = String(row.instanceId || row.id || '').trim()
- if (!instanceId) {
- uni.showToast({ title: '缺少实例 ID', icon: 'none' })
- return
- }
- const autoPrint = !!(opts && opts.autoPrint)
- stashOaDetailTitle(row.appName || row.title)
- uni.navigateTo({
- url:
- '/pages/oa/webview/index?instanceId=' +
- encodeURIComponent(instanceId) +
- '&mode=view' +
- (autoPrint ? '&autoPrint=1' : '')
- })
- },
- openCopyRestart(row) {
- if (!row) return
- const appId = String(row.appId || '').trim()
- const fromInstanceId = String(row.instanceId || row.id || '').trim()
- if (!appId || !fromInstanceId) {
- uni.showToast({ title: '无法复制:缺少应用或实例', icon: 'none' })
- return
- }
- stashOaStartTitle(row.appName || row.title)
- uni.navigateTo({
- url:
- '/pages/oa/webview/index?appId=' +
- encodeURIComponent(appId) +
- '&fromInstanceId=' +
- encodeURIComponent(fromInstanceId)
- })
- },
- 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('mine', {
- appName: (this.appName || '').trim() || undefined,
- 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;
- }
- .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;
- }
- .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>
|