feat(mobile-h5): show corner index on all five OA task list cards.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
df941a3450
commit
e229525a61
5 files changed
+1990
-636
No files matched your search
+439
-193
@@ -1,60 +1,50 @@
|
||||
<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 class="page-head" :style="{ paddingTop: statusBarPx + 'px' }">
|
||||
<view class="nav__inner">
|
||||
<view class="nav__back" @click="goBack">
|
||||
<view class="nav__back-ico" aria-hidden="true" />
|
||||
</view>
|
||||
<text class="nav__title">抄送我的</text>
|
||||
<view
|
||||
class="manage-btn"
|
||||
:class="{ 'is-active': manageMode }"
|
||||
class="nav__action"
|
||||
:class="{ 'is-done': manageMode }"
|
||||
@click="toggleManage"
|
||||
>
|
||||
{{ manageMode ? '完成' : '管理' }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="toolbar-row toolbar-row--sub">
|
||||
<view class="search-box" :class="{ 'is-focus': initiatorFocused }">
|
||||
<view class="toolbar">
|
||||
<view class="search-box" :class="{ 'is-focus': searchFocused }">
|
||||
<view class="search-ico" aria-hidden="true" />
|
||||
<input
|
||||
class="search-input"
|
||||
:value="initiatorName"
|
||||
:value="keyword"
|
||||
confirm-type="search"
|
||||
placeholder="搜索申请人"
|
||||
placeholder="搜索流程名称、申请人"
|
||||
placeholder-class="search-ph"
|
||||
@input="onInitiatorInput"
|
||||
@input="onSearchInput"
|
||||
@confirm="onSearchConfirm"
|
||||
@focus="initiatorFocused = true"
|
||||
@blur="initiatorFocused = false"
|
||||
@focus="searchFocused = true"
|
||||
@blur="searchFocused = false"
|
||||
/>
|
||||
<view v-if="initiatorName" class="search-clear" @click="clearInitiator">
|
||||
<view v-if="keyword" class="search-clear" @click="clearKeyword">
|
||||
<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>
|
||||
<scroll-view scroll-x class="tabs" :show-scrollbar="false">
|
||||
<view class="tabs-inner">
|
||||
<view
|
||||
v-for="tab in readTabs"
|
||||
:key="tab.value"
|
||||
class="tab"
|
||||
:class="{ active: readStatus === tab.value }"
|
||||
@click="setReadStatus(tab.value)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -78,11 +68,20 @@
|
||||
<text class="state-loading__text">加载抄送…</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!list.length" class="oa-task-list__empty">暂无抄送</view>
|
||||
<view v-else-if="!list.length" class="oa-empty-wrap">
|
||||
<VanEmpty
|
||||
:description="keyword ? '未找到相关抄送' : '暂无抄送'"
|
||||
:tip="
|
||||
keyword
|
||||
? '试试其他关键词,或清空搜索'
|
||||
: '抄送给你的审批会出现在这里'
|
||||
"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view v-else class="oa-task-list">
|
||||
<view v-else class="pending-list">
|
||||
<view
|
||||
v-for="row in list"
|
||||
v-for="(row, idx) in list"
|
||||
:key="row.id"
|
||||
class="cc-row"
|
||||
:class="{ 'is-manage': manageMode, 'is-selected': isSelected(row.id) }"
|
||||
@@ -96,17 +95,45 @@
|
||||
<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
|
||||
class="dd-card"
|
||||
:class="{ 'is-unread': !row.isRead, 'is-selected': isSelected(row.id) }"
|
||||
@click="onCardOpen(row)"
|
||||
@longpress="onCardLongPress(row)"
|
||||
>
|
||||
<view class="dd-card__corner" aria-hidden="true">
|
||||
<text class="dd-card__corner-num">{{ cornerIndex(idx) }}</text>
|
||||
</view>
|
||||
<view class="dd-card__head">
|
||||
<view class="dd-card__title-wrap">
|
||||
<view v-if="!row.isRead" class="dd-card__unread" />
|
||||
<text class="dd-card__title">{{ cardTitle(row) }}</text>
|
||||
</view>
|
||||
<text class="dd-card__time">{{
|
||||
formatRelativeTime(row.applyTime || row.createTime)
|
||||
}}</text>
|
||||
</view>
|
||||
<view v-if="metaLines(row).length" class="dd-card__meta">
|
||||
<text
|
||||
v-for="(line, idx) in metaLines(row)"
|
||||
:key="idx"
|
||||
class="dd-card__meta-line"
|
||||
>
|
||||
{{ line }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="dd-card__foot">
|
||||
<view class="dd-card__user">
|
||||
<view class="dd-card__avatar">{{ avatarText(row) }}</view>
|
||||
<text class="dd-card__user-text">{{ submitterText(row) }}</text>
|
||||
</view>
|
||||
<text
|
||||
class="dd-card__status"
|
||||
:class="'is-' + statusTone(row.instanceStatus)"
|
||||
>
|
||||
{{ statusText(row.instanceStatus) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loadingMore" class="oa-task-list__end">加载更多…</view>
|
||||
@@ -116,7 +143,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import OaTaskCard from '../../../components/OaTaskCard.vue'
|
||||
import VanEmpty from '../../../components/van-empty/van-empty.vue'
|
||||
import {
|
||||
fetchBatchMarkOaCcTasksRead,
|
||||
fetchMarkOaCcTaskRead,
|
||||
@@ -124,8 +151,9 @@
|
||||
} from '../../../utils/oa-api.js'
|
||||
import { forceRelogin, getToken } from '../../../utils/auth.js'
|
||||
import { stashOaDetailTitle } from '../../../utils/oa-h5.js'
|
||||
import { formatCornerIndex } from '../../../utils/oa-corner-index.js'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
const STATUS_MAP = {
|
||||
pending: '待处理',
|
||||
@@ -139,15 +167,25 @@
|
||||
transferred: '已转交'
|
||||
}
|
||||
|
||||
function pad2(n) {
|
||||
return n < 10 ? '0' + n : String(n)
|
||||
}
|
||||
|
||||
function parseDateTime(str) {
|
||||
if (!str) return null
|
||||
const s = String(str).trim().replace(/-/g, '/')
|
||||
const d = new Date(s)
|
||||
return Number.isNaN(d.getTime()) ? null : d
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { OaTaskCard },
|
||||
components: { VanEmpty },
|
||||
data() {
|
||||
return {
|
||||
appName: '',
|
||||
initiatorName: '',
|
||||
statusBarPx: 0,
|
||||
keyword: '',
|
||||
readStatus: 'unread',
|
||||
appNameFocused: false,
|
||||
initiatorFocused: false,
|
||||
searchFocused: false,
|
||||
list: [],
|
||||
page: 1,
|
||||
total: 0,
|
||||
@@ -161,9 +199,9 @@
|
||||
batchMarking: false,
|
||||
_searchTimer: null,
|
||||
readTabs: [
|
||||
{ value: 'unread', label: '未读' },
|
||||
{ value: 'all', label: '全部' },
|
||||
{ value: 'read', label: '已读' },
|
||||
{ value: 'all', label: '全部' }
|
||||
{ value: 'unread', label: '未读' }
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -176,6 +214,14 @@
|
||||
)
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
try {
|
||||
const sys = uni.getSystemInfoSync() || {}
|
||||
this.statusBarPx = Number(sys.statusBarHeight) || 0
|
||||
} catch (_) {
|
||||
this.statusBarPx = 0
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
if (!getToken()) {
|
||||
forceRelogin('请先登录')
|
||||
@@ -201,9 +247,28 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
uni.navigateBack({
|
||||
fail: () => {
|
||||
uni.reLaunch({ url: '/pages/index/index' })
|
||||
}
|
||||
})
|
||||
},
|
||||
cornerIndex(idx) {
|
||||
return formatCornerIndex(Number(idx) + 1)
|
||||
},
|
||||
cardTitle(row) {
|
||||
return (row && (row.appName || row.title)) || '-'
|
||||
},
|
||||
avatarText(row) {
|
||||
const name = (row && row.initiatorUserName) || ''
|
||||
if (!name) return '申'
|
||||
return String(name).slice(0, 2)
|
||||
},
|
||||
submitterText(row) {
|
||||
const name = (row && row.initiatorUserName) || ''
|
||||
return name ? '由' + name + '提交' : '抄送'
|
||||
},
|
||||
statusText(s) {
|
||||
if (!s) return '-'
|
||||
return STATUS_MAP[s] || s
|
||||
@@ -215,55 +280,61 @@
|
||||
return 'info'
|
||||
},
|
||||
metaLines(row) {
|
||||
return [
|
||||
{
|
||||
label: '申请人',
|
||||
value: (row && row.initiatorUserName) || '-'
|
||||
},
|
||||
{
|
||||
label: '申请时间',
|
||||
value: (row && row.applyTime) || '-'
|
||||
},
|
||||
{
|
||||
label: '阅读',
|
||||
value: row && row.isRead ? '已读' : '未读'
|
||||
}
|
||||
]
|
||||
const lines = []
|
||||
const fields = (row && row.previewFields) || []
|
||||
for (let i = 0; i < fields.length && i < 3; i++) {
|
||||
const f = fields[i] || {}
|
||||
const label = String(f.label || '').trim() || '字段'
|
||||
const value =
|
||||
f.value != null && String(f.value).trim() !== ''
|
||||
? String(f.value)
|
||||
: '-'
|
||||
lines.push(label + ':' + value)
|
||||
}
|
||||
return lines
|
||||
},
|
||||
cardActionsFor(row) {
|
||||
if (this.manageMode) return []
|
||||
return [
|
||||
{ key: 'view', label: '查看', primary: true },
|
||||
{
|
||||
key: 'read',
|
||||
label: '已读',
|
||||
disabled: !!(row && row.isRead)
|
||||
},
|
||||
{ key: 'print', label: '打印' }
|
||||
]
|
||||
formatRelativeTime(str) {
|
||||
const d = parseDateTime(str)
|
||||
if (!d) return str || '-'
|
||||
const now = new Date()
|
||||
const startToday = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate()
|
||||
)
|
||||
const startThat = new Date(d.getFullYear(), d.getMonth(), d.getDate())
|
||||
const dayDiff = Math.round(
|
||||
(startToday.getTime() - startThat.getTime()) / 86400000
|
||||
)
|
||||
if (dayDiff === 0) {
|
||||
return pad2(d.getHours()) + ':' + pad2(d.getMinutes())
|
||||
}
|
||||
if (dayDiff === 1) return '昨天'
|
||||
if (dayDiff === 2) return '前天'
|
||||
if (dayDiff > 2 && dayDiff < 7) return dayDiff + '天前'
|
||||
return (
|
||||
d.getFullYear() +
|
||||
'.' +
|
||||
pad2(d.getMonth() + 1) +
|
||||
'.' +
|
||||
pad2(d.getDate())
|
||||
)
|
||||
},
|
||||
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 ''
|
||||
},
|
||||
onSearchInput(e) {
|
||||
this.keyword = this._inputVal(e)
|
||||
if (this._searchTimer) clearTimeout(this._searchTimer)
|
||||
this._searchTimer = setTimeout(() => {
|
||||
this.loadList(true)
|
||||
}, 400)
|
||||
},
|
||||
onSearchConfirm() {
|
||||
if (this._searchTimer) {
|
||||
clearTimeout(this._searchTimer)
|
||||
@@ -271,12 +342,8 @@
|
||||
}
|
||||
this.loadList(true)
|
||||
},
|
||||
clearAppName() {
|
||||
this.appName = ''
|
||||
this.onSearchConfirm()
|
||||
},
|
||||
clearInitiator() {
|
||||
this.initiatorName = ''
|
||||
clearKeyword() {
|
||||
this.keyword = ''
|
||||
this.onSearchConfirm()
|
||||
},
|
||||
setReadStatus(value) {
|
||||
@@ -304,11 +371,20 @@
|
||||
}
|
||||
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 })
|
||||
onCardLongPress(row) {
|
||||
if (!row || this.manageMode) return
|
||||
const itemList = []
|
||||
if (!row.isRead) itemList.push('标记已读')
|
||||
itemList.push('打印')
|
||||
uni.showActionSheet({
|
||||
itemList,
|
||||
success: (res) => {
|
||||
const tap = res && res.tapIndex
|
||||
const label = itemList[tap]
|
||||
if (label === '标记已读') this.markOneRead(row)
|
||||
else if (label === '打印') this.openView(row, { autoPrint: true })
|
||||
}
|
||||
})
|
||||
},
|
||||
async markOneRead(row) {
|
||||
if (!row || row.isRead || this.markingId) return
|
||||
@@ -402,8 +478,7 @@
|
||||
}
|
||||
try {
|
||||
const res = await fetchOaTasks('cc', {
|
||||
appName: (this.appName || '').trim() || undefined,
|
||||
initiatorName: (this.initiatorName || '').trim() || undefined,
|
||||
keyword: (this.keyword || '').trim() || undefined,
|
||||
readStatus: this.readStatus || 'unread',
|
||||
page: this.page,
|
||||
pageSize: PAGE_SIZE
|
||||
@@ -439,30 +514,78 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
.page-head {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: #fff;
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 1rpx solid #f1f5f9;
|
||||
z-index: 20;
|
||||
background: #ecfdf5;
|
||||
border-bottom: 1rpx solid #e8eeeb;
|
||||
}
|
||||
|
||||
.toolbar-row {
|
||||
.nav__inner {
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 20rpx 24rpx 0;
|
||||
padding: 0 16rpx 0 8rpx;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
|
||||
.nav__back {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav__back-ico {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-left: 4rpx solid #0f291e;
|
||||
border-bottom: 4rpx solid #0f291e;
|
||||
transform: rotate(45deg);
|
||||
margin-left: 8rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.toolbar-row--sub {
|
||||
padding-top: 12rpx;
|
||||
.nav__title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 32rpx;
|
||||
font-weight: 650;
|
||||
color: #0f291e;
|
||||
}
|
||||
|
||||
.nav__action {
|
||||
margin-left: auto;
|
||||
padding: 0 16rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 28rpx;
|
||||
font-weight: 650;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.nav__action.is-done {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.nav__action:active {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
background: #fff;
|
||||
padding: 16rpx 24rpx 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
@@ -536,59 +659,47 @@
|
||||
margin-top: -2rpx;
|
||||
}
|
||||
|
||||
.manage-btn {
|
||||
flex-shrink: 0;
|
||||
min-width: 96rpx;
|
||||
height: 72rpx;
|
||||
padding: 0 16rpx;
|
||||
display: flex;
|
||||
.tabs {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
margin-top: 8rpx;
|
||||
padding: 0 8rpx;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1rpx solid #f1f5f9;
|
||||
}
|
||||
|
||||
.tabs-inner {
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
.tab {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 80rpx;
|
||||
padding: 0 28rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #166534;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.manage-btn.is-active {
|
||||
color: #64748b;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.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 {
|
||||
.tab.active {
|
||||
color: #166534;
|
||||
background: #ecfdf5;
|
||||
border-color: #a7f3d0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.read-tab:active {
|
||||
opacity: 0.78;
|
||||
.tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 28rpx;
|
||||
right: 28rpx;
|
||||
bottom: 0;
|
||||
height: 4rpx;
|
||||
border-radius: 4rpx 4rpx 0 0;
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.batch-bar {
|
||||
@@ -600,13 +711,14 @@
|
||||
padding: 20rpx 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #fff;
|
||||
border: 1rpx solid #d8f3e5;
|
||||
border: 1rpx solid #e8eaed;
|
||||
box-shadow: 0 4rpx 20rpx rgba(15, 23, 42, 0.04);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.batch-bar__hint {
|
||||
font-size: 26rpx;
|
||||
color: #5f7a6d;
|
||||
color: #646a73;
|
||||
}
|
||||
|
||||
.batch-bar__btn {
|
||||
@@ -618,9 +730,8 @@
|
||||
border-radius: 14rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #166534;
|
||||
background: #ecfdf5;
|
||||
border: 1rpx solid #a7f3d0;
|
||||
color: #fff;
|
||||
background: #0a9f6d;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -633,28 +744,27 @@
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.pending-list {
|
||||
padding: 20rpx 24rpx 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.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);
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.cc-check {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 40rpx 0 0 4rpx;
|
||||
margin-right: 12rpx;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cc-check__box {
|
||||
@@ -670,8 +780,8 @@
|
||||
}
|
||||
|
||||
.cc-check__box.on {
|
||||
border-color: #22c55e;
|
||||
background: #22c55e;
|
||||
border-color: #0a9f6d;
|
||||
background: #0a9f6d;
|
||||
}
|
||||
|
||||
.cc-check__mark {
|
||||
@@ -681,6 +791,142 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dd-card {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx 28rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 4rpx 20rpx rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.dd-card:active {
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.dd-card.is-selected {
|
||||
box-shadow: 0 4rpx 20rpx rgba(10, 159, 109, 0.16);
|
||||
outline: 2rpx solid rgba(10, 159, 109, 0.35);
|
||||
}
|
||||
|
||||
.dd-card__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.dd-card__title-wrap {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.dd-card__unread {
|
||||
flex-shrink: 0;
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
margin-top: 12rpx;
|
||||
border-radius: 50%;
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
.dd-card__title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.dd-card__time {
|
||||
flex-shrink: 0;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.4;
|
||||
color: #8f959e;
|
||||
padding-top: 4rpx;
|
||||
}
|
||||
|
||||
.dd-card__meta {
|
||||
margin-top: 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.dd-card__meta-line {
|
||||
font-size: 26rpx;
|
||||
line-height: 1.45;
|
||||
color: #8f959e;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dd-card__foot {
|
||||
margin-top: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.dd-card__user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dd-card__avatar {
|
||||
width: auto;
|
||||
min-width: 52rpx;
|
||||
height: 52rpx;
|
||||
padding: 0 12rpx;
|
||||
border-radius: 12rpx;
|
||||
background: linear-gradient(145deg, #0a9f6d, #087f57);
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dd-card__user-text {
|
||||
font-size: 24rpx;
|
||||
color: #646a73;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dd-card__status {
|
||||
flex-shrink: 0;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.dd-card__status.is-success {
|
||||
color: #0a9f6d;
|
||||
}
|
||||
|
||||
.dd-card__status.is-danger {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.dd-card__status.is-info {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.state {
|
||||
padding: 80rpx 24rpx;
|
||||
text-align: center;
|
||||
|
||||
+262
-107
@@ -1,41 +1,21 @@
|
||||
<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>
|
||||
<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 class="search-box" :class="{ 'is-focus': searchFocused }">
|
||||
<view class="search-ico" aria-hidden="true" />
|
||||
<input
|
||||
class="search-input"
|
||||
:value="keyword"
|
||||
confirm-type="search"
|
||||
placeholder="搜索流程名称、申请人"
|
||||
placeholder-class="search-ph"
|
||||
@input="onSearchInput"
|
||||
@confirm="onSearchConfirm"
|
||||
@focus="searchFocused = true"
|
||||
@blur="searchFocused = false"
|
||||
/>
|
||||
<view v-if="keyword" class="search-clear" @click="clearKeyword">
|
||||
<text class="search-clear__x">×</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -49,20 +29,55 @@
|
||||
<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)"
|
||||
:meta-lines="metaLines(row)"
|
||||
:actions="cardActions"
|
||||
@open="openView(row)"
|
||||
@action="openView(row)"
|
||||
<view v-else-if="!list.length" class="oa-empty-wrap">
|
||||
<VanEmpty
|
||||
:description="keyword ? '未找到相关已办' : '暂无已办'"
|
||||
:tip="
|
||||
keyword
|
||||
? '试试其他关键词,或清空搜索'
|
||||
: '你处理过的审批会出现在这里'
|
||||
"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view v-else class="pending-list">
|
||||
<view
|
||||
v-for="(row, idx) in list"
|
||||
:key="row.id"
|
||||
class="dd-card"
|
||||
@click="openView(row)"
|
||||
>
|
||||
<view class="dd-card__corner" aria-hidden="true">
|
||||
<text class="dd-card__corner-num">{{ cornerIndex(idx) }}</text>
|
||||
</view>
|
||||
<view class="dd-card__head">
|
||||
<text class="dd-card__title">{{ cardTitle(row) }}</text>
|
||||
<text class="dd-card__time">{{
|
||||
formatRelativeTime(row.applyTime || row.createTime)
|
||||
}}</text>
|
||||
</view>
|
||||
<view v-if="metaLines(row).length" class="dd-card__meta">
|
||||
<text
|
||||
v-for="(line, idx) in metaLines(row)"
|
||||
:key="idx"
|
||||
class="dd-card__meta-line"
|
||||
>
|
||||
{{ line }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="dd-card__foot">
|
||||
<view class="dd-card__user">
|
||||
<view class="dd-card__avatar">{{ avatarText(row) }}</view>
|
||||
<text class="dd-card__user-text">{{ submitterText(row) }}</text>
|
||||
</view>
|
||||
<text
|
||||
class="dd-card__status"
|
||||
:class="'is-' + statusTone(row.instanceStatus)"
|
||||
>
|
||||
{{ statusText(row.instanceStatus) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loadingMore" class="oa-task-list__end">加载更多…</view>
|
||||
<view v-else-if="!hasMore" class="oa-task-list__end">已经到底了</view>
|
||||
</view>
|
||||
@@ -70,12 +85,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import OaTaskCard from '../../../components/OaTaskCard.vue'
|
||||
import VanEmpty from '../../../components/van-empty/van-empty.vue'
|
||||
import { fetchOaTasks } from '../../../utils/oa-api.js'
|
||||
import { forceRelogin, getToken } from '../../../utils/auth.js'
|
||||
import { stashOaDetailTitle } from '../../../utils/oa-h5.js'
|
||||
import { formatCornerIndex } from '../../../utils/oa-corner-index.js'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
const STATUS_MAP = {
|
||||
pending: '待处理',
|
||||
@@ -89,22 +105,30 @@
|
||||
transferred: '已转交'
|
||||
}
|
||||
|
||||
function pad2(n) {
|
||||
return n < 10 ? '0' + n : String(n)
|
||||
}
|
||||
|
||||
function parseDateTime(str) {
|
||||
if (!str) return null
|
||||
const s = String(str).trim().replace(/-/g, '/')
|
||||
const d = new Date(s)
|
||||
return Number.isNaN(d.getTime()) ? null : d
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { OaTaskCard },
|
||||
components: { VanEmpty },
|
||||
data() {
|
||||
return {
|
||||
appName: '',
|
||||
initiatorName: '',
|
||||
appNameFocused: false,
|
||||
initiatorFocused: false,
|
||||
keyword: '',
|
||||
searchFocused: false,
|
||||
list: [],
|
||||
page: 1,
|
||||
total: 0,
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
hasMore: true,
|
||||
_searchTimer: null,
|
||||
cardActions: [{ key: 'view', label: '查看', primary: true }]
|
||||
_searchTimer: null
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
@@ -132,9 +156,21 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cornerIndex(idx) {
|
||||
return formatCornerIndex(Number(idx) + 1)
|
||||
},
|
||||
cardTitle(row) {
|
||||
return (row && (row.appName || row.title)) || '-'
|
||||
},
|
||||
avatarText(row) {
|
||||
const name = (row && row.initiatorUserName) || ''
|
||||
if (!name) return '申'
|
||||
return String(name).slice(0, 2)
|
||||
},
|
||||
submitterText(row) {
|
||||
const name = (row && row.initiatorUserName) || ''
|
||||
return name ? '由' + name + '提交' : '已办'
|
||||
},
|
||||
statusText(s) {
|
||||
if (!s) return '-'
|
||||
return STATUS_MAP[s] || s
|
||||
@@ -146,40 +182,58 @@
|
||||
return 'info'
|
||||
},
|
||||
metaLines(row) {
|
||||
return [
|
||||
{
|
||||
label: '申请人',
|
||||
value: (row && row.initiatorUserName) || '-'
|
||||
},
|
||||
{
|
||||
label: '申请时间',
|
||||
value: (row && row.applyTime) || '-'
|
||||
},
|
||||
{
|
||||
label: '当前节点',
|
||||
value: (row && row.currentNodeName) || '-'
|
||||
}
|
||||
]
|
||||
const lines = []
|
||||
const fields = (row && row.previewFields) || []
|
||||
for (let i = 0; i < fields.length && i < 3; i++) {
|
||||
const f = fields[i] || {}
|
||||
const label = String(f.label || '').trim() || '字段'
|
||||
const value =
|
||||
f.value != null && String(f.value).trim() !== ''
|
||||
? String(f.value)
|
||||
: '-'
|
||||
lines.push(label + ':' + value)
|
||||
}
|
||||
return lines
|
||||
},
|
||||
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()
|
||||
formatRelativeTime(str) {
|
||||
const d = parseDateTime(str)
|
||||
if (!d) return str || '-'
|
||||
const now = new Date()
|
||||
const startToday = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate()
|
||||
)
|
||||
const startThat = new Date(d.getFullYear(), d.getMonth(), d.getDate())
|
||||
const dayDiff = Math.round(
|
||||
(startToday.getTime() - startThat.getTime()) / 86400000
|
||||
)
|
||||
if (dayDiff === 0) {
|
||||
return pad2(d.getHours()) + ':' + pad2(d.getMinutes())
|
||||
}
|
||||
if (dayDiff === 1) return '昨天'
|
||||
if (dayDiff === 2) return '前天'
|
||||
if (dayDiff > 2 && dayDiff < 7) return dayDiff + '天前'
|
||||
return (
|
||||
d.getFullYear() +
|
||||
'.' +
|
||||
pad2(d.getMonth() + 1) +
|
||||
'.' +
|
||||
pad2(d.getDate())
|
||||
)
|
||||
},
|
||||
_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 ''
|
||||
},
|
||||
onSearchInput(e) {
|
||||
this.keyword = this._inputVal(e)
|
||||
if (this._searchTimer) clearTimeout(this._searchTimer)
|
||||
this._searchTimer = setTimeout(() => {
|
||||
this.loadList(true)
|
||||
}, 400)
|
||||
},
|
||||
onSearchConfirm() {
|
||||
if (this._searchTimer) {
|
||||
clearTimeout(this._searchTimer)
|
||||
@@ -187,12 +241,8 @@
|
||||
}
|
||||
this.loadList(true)
|
||||
},
|
||||
clearAppName() {
|
||||
this.appName = ''
|
||||
this.onSearchConfirm()
|
||||
},
|
||||
clearInitiator() {
|
||||
this.initiatorName = ''
|
||||
clearKeyword() {
|
||||
this.keyword = ''
|
||||
this.onSearchConfirm()
|
||||
},
|
||||
openView(row) {
|
||||
@@ -225,8 +275,7 @@
|
||||
}
|
||||
try {
|
||||
const res = await fetchOaTasks('done', {
|
||||
appName: (this.appName || '').trim() || undefined,
|
||||
initiatorName: (this.initiatorName || '').trim() || undefined,
|
||||
keyword: (this.keyword || '').trim() || undefined,
|
||||
page: this.page,
|
||||
pageSize: PAGE_SIZE
|
||||
})
|
||||
@@ -266,25 +315,11 @@
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: #fff;
|
||||
padding-bottom: 16rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
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;
|
||||
@@ -358,6 +393,126 @@
|
||||
margin-top: -2rpx;
|
||||
}
|
||||
|
||||
.pending-list {
|
||||
padding: 20rpx 24rpx 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dd-card {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx 28rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 4rpx 20rpx rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.dd-card:active {
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.dd-card__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.dd-card__title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.dd-card__time {
|
||||
flex-shrink: 0;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.4;
|
||||
color: #8f959e;
|
||||
padding-top: 4rpx;
|
||||
}
|
||||
|
||||
.dd-card__meta {
|
||||
margin-top: 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.dd-card__meta-line {
|
||||
font-size: 26rpx;
|
||||
line-height: 1.45;
|
||||
color: #8f959e;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dd-card__foot {
|
||||
margin-top: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.dd-card__user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dd-card__avatar {
|
||||
width: auto;
|
||||
min-width: 52rpx;
|
||||
height: 52rpx;
|
||||
padding: 0 12rpx;
|
||||
border-radius: 12rpx;
|
||||
background: linear-gradient(145deg, #0a9f6d, #087f57);
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dd-card__user-text {
|
||||
font-size: 24rpx;
|
||||
color: #646a73;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dd-card__status {
|
||||
flex-shrink: 0;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.dd-card__status.is-success {
|
||||
color: #0a9f6d;
|
||||
}
|
||||
|
||||
.dd-card__status.is-danger {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.dd-card__status.is-info {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.state {
|
||||
padding: 80rpx 24rpx;
|
||||
text-align: center;
|
||||
|
||||
+288
-148
@@ -1,7 +1,20 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="toolbar">
|
||||
<view class="toolbar-row">
|
||||
<view class="page-head" :style="{ paddingTop: statusBarPx + 'px' }">
|
||||
<view class="nav__inner">
|
||||
<view class="nav__back" @click="goBack">
|
||||
<view class="nav__back-ico" aria-hidden="true" />
|
||||
</view>
|
||||
<text class="nav__title">草稿箱</text>
|
||||
<view
|
||||
class="nav__action"
|
||||
:class="{ 'is-done': manageMode }"
|
||||
@click="toggleManage"
|
||||
>
|
||||
{{ manageMode ? '完成' : '管理' }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="toolbar">
|
||||
<view class="search-box" :class="{ 'is-focus': searchFocused }">
|
||||
<view class="search-ico" aria-hidden="true" />
|
||||
<input
|
||||
@@ -19,13 +32,6 @@
|
||||
<text class="search-clear__x">×</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="manage-btn"
|
||||
:class="{ 'is-active': manageMode }"
|
||||
@click="toggleManage"
|
||||
>
|
||||
{{ manageMode ? '完成' : '管理' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -43,34 +49,49 @@
|
||||
<text class="state-loading__text">加载草稿…</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!list.length" class="oa-task-list__empty">暂无草稿</view>
|
||||
<view v-else-if="!list.length" class="oa-empty-wrap">
|
||||
<VanEmpty
|
||||
description="暂无草稿"
|
||||
tip="保存过的审批草稿会出现在这里"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view v-else class="oa-task-list">
|
||||
<view v-else class="draft-list">
|
||||
<view
|
||||
v-for="row in list"
|
||||
v-for="(row, idx) in list"
|
||||
:key="row.id"
|
||||
class="draft-row"
|
||||
:class="{ 'is-manage': manageMode, 'is-selected': isSelected(row.id) }"
|
||||
@click="onRowClick(row)"
|
||||
>
|
||||
<view
|
||||
v-if="manageMode"
|
||||
class="draft-check"
|
||||
@click.stop="toggleSelect(row.id)"
|
||||
>
|
||||
<view v-if="manageMode" class="draft-check" @click.stop="toggleSelect(row.id)">
|
||||
<view class="draft-check__box" :class="{ on: isSelected(row.id) }">
|
||||
<text v-if="isSelected(row.id)" class="draft-check__mark">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="draft-card-wrap">
|
||||
<OaTaskCard
|
||||
:title="cardTitle(row)"
|
||||
status-text="草稿"
|
||||
status-tone="warn"
|
||||
:meta-lines="metaLines(row)"
|
||||
:actions="manageMode ? [] : cardActions"
|
||||
@open="onCardOpen(row)"
|
||||
@action="onCardAction($event, row)"
|
||||
/>
|
||||
<view class="dd-card">
|
||||
<view class="dd-card__corner" aria-hidden="true">
|
||||
<text class="dd-card__corner-num">{{ cornerIndex(idx) }}</text>
|
||||
</view>
|
||||
<view class="dd-card__head">
|
||||
<text class="dd-card__title">{{ cardTitle(row) }}</text>
|
||||
<text class="dd-card__time">{{ formatRelativeTime(row.updateTime) }}</text>
|
||||
</view>
|
||||
<view v-if="metaLines(row).length" class="dd-card__meta">
|
||||
<text
|
||||
v-for="(line, idx) in metaLines(row)"
|
||||
:key="idx"
|
||||
class="dd-card__meta-line"
|
||||
>
|
||||
{{ line }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="dd-card__foot">
|
||||
<view class="dd-card__user">
|
||||
<text class="dd-card__user-text">待提交草稿</text>
|
||||
</view>
|
||||
<text class="dd-card__status">草稿</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loadingMore" class="oa-task-list__end">加载更多…</view>
|
||||
@@ -80,23 +101,34 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import OaTaskCard from '../../../components/OaTaskCard.vue'
|
||||
import {
|
||||
fetchBatchDeleteOaInstanceDrafts,
|
||||
fetchDeleteOaInstanceDraft,
|
||||
fetchOaInstanceDraftCount,
|
||||
fetchOaInstanceDraftList,
|
||||
fetchSubmitOaInstanceDraft
|
||||
fetchOaInstanceDraftList
|
||||
} from '../../../utils/oa-api.js'
|
||||
import { forceRelogin, getToken } from '../../../utils/auth.js'
|
||||
import { stashOaStartTitle } from '../../../utils/oa-h5.js'
|
||||
import { formatCornerIndex } from '../../../utils/oa-corner-index.js'
|
||||
import VanEmpty from '../../../components/van-empty/van-empty.vue'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
function pad2(n) {
|
||||
return n < 10 ? '0' + n : String(n)
|
||||
}
|
||||
|
||||
function parseDateTime(str) {
|
||||
if (!str) return null
|
||||
const s = String(str).trim().replace(/-/g, '/')
|
||||
const d = new Date(s)
|
||||
return Number.isNaN(d.getTime()) ? null : d
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { OaTaskCard },
|
||||
components: { VanEmpty },
|
||||
data() {
|
||||
return {
|
||||
statusBarPx: 0,
|
||||
keyword: '',
|
||||
searchFocused: false,
|
||||
list: [],
|
||||
@@ -107,13 +139,15 @@
|
||||
hasMore: true,
|
||||
manageMode: false,
|
||||
selectedIds: [],
|
||||
submittingId: '',
|
||||
_searchTimer: null,
|
||||
cardActions: [
|
||||
{ key: 'edit', label: '编辑', primary: true },
|
||||
{ key: 'submit', label: '提交审批' },
|
||||
{ key: 'delete', label: '删除', danger: true }
|
||||
]
|
||||
_searchTimer: null
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
try {
|
||||
const sys = uni.getSystemInfoSync() || {}
|
||||
this.statusBarPx = Number(sys.statusBarHeight) || 0
|
||||
} catch (_) {
|
||||
this.statusBarPx = 0
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
@@ -142,18 +176,62 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
uni.navigateBack({
|
||||
fail: () => {
|
||||
uni.reLaunch({ url: '/pages/index/index' })
|
||||
}
|
||||
})
|
||||
},
|
||||
cornerIndex(idx) {
|
||||
return formatCornerIndex(Number(idx) + 1)
|
||||
},
|
||||
cardTitle(row) {
|
||||
return (row && (row.appName || row.title)) || '-'
|
||||
},
|
||||
metaLines(row) {
|
||||
const ver =
|
||||
row && row.versionNo != null && row.versionNo !== ''
|
||||
? `v${row.versionNo}`
|
||||
: '-'
|
||||
return [
|
||||
{ label: '最近保存', value: (row && row.updateTime) || '-' },
|
||||
{ label: '版本', value: ver }
|
||||
]
|
||||
const lines = []
|
||||
const fields = (row && row.previewFields) || []
|
||||
for (let i = 0; i < fields.length && i < 3; i++) {
|
||||
const f = fields[i] || {}
|
||||
const label = String(f.label || '').trim() || '字段'
|
||||
const value =
|
||||
f.value != null && String(f.value).trim() !== ''
|
||||
? String(f.value)
|
||||
: '-'
|
||||
lines.push(label + ':' + value)
|
||||
}
|
||||
if (row && row.updateTime) {
|
||||
lines.push('最近保存:' + row.updateTime)
|
||||
}
|
||||
return lines
|
||||
},
|
||||
formatRelativeTime(str) {
|
||||
const d = parseDateTime(str)
|
||||
if (!d) return str || '-'
|
||||
const now = new Date()
|
||||
const startToday = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate()
|
||||
)
|
||||
const startThat = new Date(d.getFullYear(), d.getMonth(), d.getDate())
|
||||
const dayDiff = Math.round(
|
||||
(startToday.getTime() - startThat.getTime()) / 86400000
|
||||
)
|
||||
if (dayDiff === 0) {
|
||||
return pad2(d.getHours()) + ':' + pad2(d.getMinutes())
|
||||
}
|
||||
if (dayDiff === 1) return '昨天'
|
||||
if (dayDiff === 2) return '前天'
|
||||
if (dayDiff > 2 && dayDiff < 7) return dayDiff + '天前'
|
||||
return (
|
||||
d.getFullYear() +
|
||||
'.' +
|
||||
pad2(d.getMonth() + 1) +
|
||||
'.' +
|
||||
pad2(d.getDate())
|
||||
)
|
||||
},
|
||||
isSelected(id) {
|
||||
return this.selectedIds.indexOf(id) >= 0
|
||||
@@ -199,19 +277,13 @@
|
||||
this.selectedIds = this.selectedIds.concat([id])
|
||||
}
|
||||
},
|
||||
onCardOpen(row) {
|
||||
onRowClick(row) {
|
||||
if (this.manageMode) {
|
||||
this.toggleSelect(row && row.id)
|
||||
return
|
||||
}
|
||||
this.openEdit(row)
|
||||
},
|
||||
onCardAction(act, row) {
|
||||
if (!act || !row) return
|
||||
if (act.key === 'edit') this.openEdit(row)
|
||||
else if (act.key === 'submit') this.submitRow(row)
|
||||
else if (act.key === 'delete') this.deleteRow(row)
|
||||
},
|
||||
openEdit(row) {
|
||||
if (!row || !row.id) return
|
||||
stashOaStartTitle(row.appName || row.title)
|
||||
@@ -278,51 +350,7 @@
|
||||
try {
|
||||
await fetchOaInstanceDraftCount()
|
||||
} catch (_) {
|
||||
/* 角标 UI 在 Task 11;此处仅预热 count API */
|
||||
}
|
||||
},
|
||||
async submitRow(row) {
|
||||
if (!row || !row.id || this.submittingId) return
|
||||
const ok = await this.confirm(
|
||||
'提交审批',
|
||||
'确认提交该草稿并发起审批吗?',
|
||||
'提交'
|
||||
)
|
||||
if (!ok) return
|
||||
this.submittingId = row.id
|
||||
try {
|
||||
await fetchSubmitOaInstanceDraft(row.id)
|
||||
uni.showToast({ title: '已提交', icon: 'success' })
|
||||
await this.loadList(true)
|
||||
this.refreshDraftCount()
|
||||
} catch (e) {
|
||||
uni.showToast({
|
||||
title: (e && e.message) || '提交失败,请完善后重试',
|
||||
icon: 'none'
|
||||
})
|
||||
this.openEdit(row)
|
||||
} finally {
|
||||
this.submittingId = ''
|
||||
}
|
||||
},
|
||||
async deleteRow(row) {
|
||||
if (!row || !row.id) return
|
||||
const ok = await this.confirm(
|
||||
'删除草稿',
|
||||
'确认删除该草稿吗?删除后不可恢复。',
|
||||
'删除'
|
||||
)
|
||||
if (!ok) return
|
||||
try {
|
||||
await fetchDeleteOaInstanceDraft(row.id)
|
||||
uni.showToast({ title: '已删除', icon: 'success' })
|
||||
await this.loadList(true)
|
||||
this.refreshDraftCount()
|
||||
} catch (e) {
|
||||
uni.showToast({
|
||||
title: (e && e.message) || '删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
/* 角标预热 */
|
||||
}
|
||||
},
|
||||
async batchDelete() {
|
||||
@@ -360,26 +388,77 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
.page-head {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: #fff;
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 1rpx solid #f1f5f9;
|
||||
z-index: 20;
|
||||
background: #ecfdf5;
|
||||
border-bottom: 1rpx solid #e8eeeb;
|
||||
}
|
||||
|
||||
.toolbar-row {
|
||||
.nav__inner {
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 20rpx 24rpx 0;
|
||||
padding: 0 16rpx 0 8rpx;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
|
||||
.nav__back {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav__back-ico {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-left: 4rpx solid #0f291e;
|
||||
border-bottom: 4rpx solid #0f291e;
|
||||
transform: rotate(45deg);
|
||||
margin-left: 8rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.nav__title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 32rpx;
|
||||
font-weight: 650;
|
||||
color: #0f291e;
|
||||
}
|
||||
|
||||
.nav__action {
|
||||
margin-left: auto;
|
||||
padding: 0 16rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 28rpx;
|
||||
font-weight: 650;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.nav__action.is-done {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.nav__action:active {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
background: #fff;
|
||||
padding: 16rpx 24rpx;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
@@ -453,28 +532,6 @@
|
||||
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;
|
||||
}
|
||||
|
||||
.batch-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -512,28 +569,27 @@
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.draft-list {
|
||||
padding: 20rpx 24rpx 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.draft-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.draft-row.is-manage .draft-card-wrap {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.draft-row.is-selected .draft-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);
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.draft-check {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 40rpx 0 0 4rpx;
|
||||
margin-right: 12rpx;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.draft-check__box {
|
||||
@@ -560,6 +616,90 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dd-card {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx 28rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 4rpx 20rpx rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.draft-row.is-selected .dd-card {
|
||||
box-shadow: 0 8rpx 28rpx rgba(22, 163, 74, 0.12);
|
||||
outline: 2rpx solid rgba(34, 197, 94, 0.4);
|
||||
}
|
||||
|
||||
.dd-card__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.dd-card__title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.dd-card__time {
|
||||
flex-shrink: 0;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.4;
|
||||
color: #8f959e;
|
||||
padding-top: 4rpx;
|
||||
}
|
||||
|
||||
.dd-card__meta {
|
||||
margin-top: 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.dd-card__meta-line {
|
||||
font-size: 26rpx;
|
||||
line-height: 1.45;
|
||||
color: #8f959e;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dd-card__foot {
|
||||
margin-top: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.dd-card__user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dd-card__user-text {
|
||||
font-size: 24rpx;
|
||||
color: #646a73;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dd-card__status {
|
||||
flex-shrink: 0;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.state {
|
||||
padding: 80rpx 24rpx;
|
||||
text-align: center;
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
<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 class="search-box" :class="{ 'is-focus': searchFocused }">
|
||||
<view class="search-ico" aria-hidden="true" />
|
||||
<input
|
||||
class="search-input"
|
||||
:value="keyword"
|
||||
confirm-type="search"
|
||||
placeholder="搜索流程名称"
|
||||
placeholder-class="search-ph"
|
||||
@input="onSearchInput"
|
||||
@confirm="onSearchConfirm"
|
||||
@focus="searchFocused = true"
|
||||
@blur="searchFocused = false"
|
||||
/>
|
||||
<view v-if="keyword" class="search-clear" @click="clearKeyword">
|
||||
<text class="search-clear__x">×</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -31,20 +29,58 @@
|
||||
<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-else-if="!list.length" class="oa-empty-wrap">
|
||||
<VanEmpty
|
||||
:description="keyword ? '未找到相关记录' : '暂无发起记录'"
|
||||
:tip="
|
||||
keyword
|
||||
? '试试其他关键词,或清空搜索'
|
||||
: '你发起的审批会出现在这里'
|
||||
"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view v-else class="pending-list">
|
||||
<view
|
||||
v-for="(row, idx) in list"
|
||||
:key="row.id"
|
||||
class="dd-card"
|
||||
@click="openView(row)"
|
||||
@longpress="onCardLongPress(row)"
|
||||
>
|
||||
<view class="dd-card__corner" aria-hidden="true">
|
||||
<text class="dd-card__corner-num">{{ cornerIndex(idx) }}</text>
|
||||
</view>
|
||||
<view class="dd-card__head">
|
||||
<text class="dd-card__title">{{ cardTitle(row) }}</text>
|
||||
<text class="dd-card__time">{{
|
||||
formatRelativeTime(row.applyTime || row.createTime)
|
||||
}}</text>
|
||||
</view>
|
||||
<view v-if="metaLines(row).length" class="dd-card__meta">
|
||||
<text
|
||||
v-for="(line, idx) in metaLines(row)"
|
||||
:key="idx"
|
||||
class="dd-card__meta-line"
|
||||
>
|
||||
{{ line }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="dd-card__foot">
|
||||
<text
|
||||
class="dd-card__hint-tag"
|
||||
:class="'is-' + statusTone(row.instanceStatus || row.status)"
|
||||
>
|
||||
{{ submitterText(row) }}
|
||||
</text>
|
||||
<text
|
||||
class="dd-card__status"
|
||||
:class="'is-' + statusTone(row.instanceStatus || row.status)"
|
||||
>
|
||||
{{ statusText(row.instanceStatus || row.status) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loadingMore" class="oa-task-list__end">加载更多…</view>
|
||||
<view v-else-if="!hasMore" class="oa-task-list__end">已经到底了</view>
|
||||
</view>
|
||||
@@ -52,12 +88,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import OaTaskCard from '../../../components/OaTaskCard.vue'
|
||||
import VanEmpty from '../../../components/van-empty/van-empty.vue'
|
||||
import { fetchOaTasks } from '../../../utils/oa-api.js'
|
||||
import { forceRelogin, getToken } from '../../../utils/auth.js'
|
||||
import { stashOaDetailTitle, stashOaStartTitle } from '../../../utils/oa-h5.js'
|
||||
import { formatCornerIndex } from '../../../utils/oa-corner-index.js'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
const COPY_RESTART_STATUSES = ['returned', 'cancelled', 'rejected']
|
||||
|
||||
@@ -73,11 +110,22 @@
|
||||
transferred: '已转交'
|
||||
}
|
||||
|
||||
function pad2(n) {
|
||||
return n < 10 ? '0' + n : String(n)
|
||||
}
|
||||
|
||||
function parseDateTime(str) {
|
||||
if (!str) return null
|
||||
const s = String(str).trim().replace(/-/g, '/')
|
||||
const d = new Date(s)
|
||||
return Number.isNaN(d.getTime()) ? null : d
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { OaTaskCard },
|
||||
components: { VanEmpty },
|
||||
data() {
|
||||
return {
|
||||
appName: '',
|
||||
keyword: '',
|
||||
searchFocused: false,
|
||||
list: [],
|
||||
page: 1,
|
||||
@@ -113,9 +161,27 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cornerIndex(idx) {
|
||||
return formatCornerIndex(Number(idx) + 1)
|
||||
},
|
||||
cardTitle(row) {
|
||||
return (row && (row.appName || row.title)) || '-'
|
||||
},
|
||||
rowStatus(row) {
|
||||
return String((row && (row.instanceStatus || row.status)) || '')
|
||||
},
|
||||
canCopyRestart(row) {
|
||||
const status = this.rowStatus(row)
|
||||
const appId = String((row && row.appId) || '').trim()
|
||||
return COPY_RESTART_STATUSES.includes(status) && !!appId
|
||||
},
|
||||
submitterText(row) {
|
||||
const names = String((row && row.currentApproverNames) || '').trim()
|
||||
if (names) return '待' + names + '处理'
|
||||
const node = String((row && row.currentNodeName) || '').trim()
|
||||
if (node) return node
|
||||
return '我发起的'
|
||||
},
|
||||
statusText(s) {
|
||||
if (!s) return '-'
|
||||
return STATUS_MAP[s] || s
|
||||
@@ -127,41 +193,53 @@
|
||||
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: '复制并重新发起' })
|
||||
const lines = []
|
||||
const fields = (row && row.previewFields) || []
|
||||
for (let i = 0; i < fields.length && i < 3; i++) {
|
||||
const f = fields[i] || {}
|
||||
const label = String(f.label || '').trim() || '字段'
|
||||
const value =
|
||||
f.value != null && String(f.value).trim() !== ''
|
||||
? String(f.value)
|
||||
: '-'
|
||||
lines.push(label + ':' + value)
|
||||
}
|
||||
return actions
|
||||
return lines
|
||||
},
|
||||
formatRelativeTime(str) {
|
||||
const d = parseDateTime(str)
|
||||
if (!d) return str || '-'
|
||||
const now = new Date()
|
||||
const startToday = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate()
|
||||
)
|
||||
const startThat = new Date(d.getFullYear(), d.getMonth(), d.getDate())
|
||||
const dayDiff = Math.round(
|
||||
(startToday.getTime() - startThat.getTime()) / 86400000
|
||||
)
|
||||
if (dayDiff === 0) {
|
||||
return pad2(d.getHours()) + ':' + pad2(d.getMinutes())
|
||||
}
|
||||
if (dayDiff === 1) return '昨天'
|
||||
if (dayDiff === 2) return '前天'
|
||||
if (dayDiff > 2 && dayDiff < 7) return dayDiff + '天前'
|
||||
return (
|
||||
d.getFullYear() +
|
||||
'.' +
|
||||
pad2(d.getMonth() + 1) +
|
||||
'.' +
|
||||
pad2(d.getDate())
|
||||
)
|
||||
},
|
||||
_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 ''
|
||||
},
|
||||
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
|
||||
this.keyword = this._inputVal(e)
|
||||
if (this._searchTimer) clearTimeout(this._searchTimer)
|
||||
this._searchTimer = setTimeout(() => {
|
||||
this.loadList(true)
|
||||
@@ -174,21 +252,27 @@
|
||||
}
|
||||
this.loadList(true)
|
||||
},
|
||||
clearAppName() {
|
||||
this.appName = ''
|
||||
clearKeyword() {
|
||||
this.keyword = ''
|
||||
this.onSearchConfirm()
|
||||
},
|
||||
onCardAction(act, row) {
|
||||
if (!act || !row) return
|
||||
if (act.key === 'copyRestart') {
|
||||
this.openCopyRestart(row)
|
||||
return
|
||||
onCardLongPress(row) {
|
||||
if (!row) return
|
||||
const itemList = ['打印']
|
||||
if (this.canCopyRestart(row)) {
|
||||
itemList.push('复制并重新发起')
|
||||
}
|
||||
if (act.key === 'print') {
|
||||
this.openView(row, { autoPrint: true })
|
||||
return
|
||||
}
|
||||
this.openView(row)
|
||||
uni.showActionSheet({
|
||||
itemList,
|
||||
success: (res) => {
|
||||
const tap = res && res.tapIndex
|
||||
if (tap === 0) {
|
||||
this.openView(row, { autoPrint: true })
|
||||
} else if (tap === 1 && this.canCopyRestart(row)) {
|
||||
this.openCopyRestart(row)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
openView(row, opts) {
|
||||
if (!row) return
|
||||
@@ -240,7 +324,7 @@
|
||||
}
|
||||
try {
|
||||
const res = await fetchOaTasks('mine', {
|
||||
appName: (this.appName || '').trim() || undefined,
|
||||
keyword: (this.keyword || '').trim() || undefined,
|
||||
page: this.page,
|
||||
pageSize: PAGE_SIZE
|
||||
})
|
||||
@@ -280,21 +364,11 @@
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: #fff;
|
||||
padding-bottom: 16rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
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;
|
||||
@@ -368,6 +442,125 @@
|
||||
margin-top: -2rpx;
|
||||
}
|
||||
|
||||
.pending-list {
|
||||
padding: 20rpx 24rpx 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dd-card {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx 28rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 4rpx 20rpx rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.dd-card:active {
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.dd-card__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.dd-card__title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.dd-card__time {
|
||||
flex-shrink: 0;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.4;
|
||||
color: #8f959e;
|
||||
padding-top: 4rpx;
|
||||
}
|
||||
|
||||
.dd-card__meta {
|
||||
margin-top: 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.dd-card__meta-line {
|
||||
font-size: 26rpx;
|
||||
line-height: 1.45;
|
||||
color: #8f959e;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dd-card__foot {
|
||||
margin-top: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.dd-card__hint-tag {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
max-width: 70%;
|
||||
font-size: 22rpx;
|
||||
font-weight: 650;
|
||||
line-height: 1.2;
|
||||
padding: 8rpx 18rpx;
|
||||
border-radius: 999rpx;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
background: rgba(217, 119, 6, 0.12);
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.dd-card__hint-tag.is-success {
|
||||
background: rgba(10, 159, 109, 0.12);
|
||||
color: #0a9f6d;
|
||||
}
|
||||
|
||||
.dd-card__hint-tag.is-danger {
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.dd-card__hint-tag.is-info {
|
||||
background: rgba(100, 106, 115, 0.12);
|
||||
color: #646a73;
|
||||
}
|
||||
|
||||
.dd-card__status {
|
||||
flex-shrink: 0;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.dd-card__status.is-success {
|
||||
color: #0a9f6d;
|
||||
}
|
||||
|
||||
.dd-card__status.is-danger {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.dd-card__status.is-info {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.state {
|
||||
padding: 80rpx 24rpx;
|
||||
text-align: center;
|
||||
|
||||
@@ -1,41 +1,48 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="toolbar">
|
||||
<view class="toolbar-row">
|
||||
<view class="search-box" :class="{ 'is-focus': appNameFocused }">
|
||||
<view class="page-head" :style="{ paddingTop: statusBarPx + 'px' }">
|
||||
<view class="nav__inner">
|
||||
<view class="nav__back" @click="goBack">
|
||||
<view class="nav__back-ico" aria-hidden="true" />
|
||||
</view>
|
||||
<text class="nav__title">待办中心</text>
|
||||
<view
|
||||
class="nav__action"
|
||||
:class="{ 'is-done': manageMode }"
|
||||
@click="toggleManage"
|
||||
>
|
||||
{{ manageMode ? '完成' : '管理' }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="toolbar">
|
||||
<view class="search-box" :class="{ 'is-focus': searchFocused }">
|
||||
<view class="search-ico" aria-hidden="true" />
|
||||
<input
|
||||
class="search-input"
|
||||
:value="appName"
|
||||
:value="keyword"
|
||||
confirm-type="search"
|
||||
placeholder="搜索流程名称"
|
||||
placeholder="搜索流程名称、申请人"
|
||||
placeholder-class="search-ph"
|
||||
@input="onAppNameInput"
|
||||
@input="onSearchInput"
|
||||
@confirm="onSearchConfirm"
|
||||
@focus="appNameFocused = true"
|
||||
@blur="appNameFocused = false"
|
||||
@focus="searchFocused = true"
|
||||
@blur="searchFocused = false"
|
||||
/>
|
||||
<view v-if="appName" class="search-clear" @click="clearAppName">
|
||||
<view v-if="keyword" class="search-clear" @click="clearKeyword">
|
||||
<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 v-if="manageMode && selectedIds.length" class="batch-bar">
|
||||
<text class="batch-bar__hint">已选 {{ selectedIds.length }} 项</text>
|
||||
<view class="batch-bar__actions">
|
||||
<view class="batch-bar__btn is-return" @click="openBatchSheet('return')">
|
||||
批量打回
|
||||
</view>
|
||||
<view class="batch-bar__btn is-ok" @click="openBatchSheet('approve')">
|
||||
批量通过
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -49,33 +56,120 @@
|
||||
<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)"
|
||||
:meta-lines="metaLines(row)"
|
||||
:actions="cardActions"
|
||||
@open="openApprove(row)"
|
||||
@action="openApprove(row)"
|
||||
<view v-else-if="!list.length" class="oa-empty-wrap">
|
||||
<VanEmpty
|
||||
:description="keyword ? '未找到相关待办' : '暂无待办'"
|
||||
:tip="
|
||||
keyword
|
||||
? '试试其他关键词,或清空搜索'
|
||||
: '当前没有需要你处理的审批'
|
||||
"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view v-else class="pending-list">
|
||||
<view
|
||||
v-for="(row, idx) in list"
|
||||
:key="row.id"
|
||||
class="pending-row"
|
||||
:class="{ 'is-manage': manageMode, 'is-selected': isSelected(row.id) }"
|
||||
@click="onRowClick(row)"
|
||||
>
|
||||
<view
|
||||
v-if="manageMode"
|
||||
class="pending-check"
|
||||
@click.stop="toggleSelect(row.id)"
|
||||
>
|
||||
<view class="pending-check__box" :class="{ on: isSelected(row.id) }">
|
||||
<text v-if="isSelected(row.id)" class="pending-check__mark">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="dd-card" :class="{ 'is-selected': isSelected(row.id) }">
|
||||
<view class="dd-card__corner" aria-hidden="true">
|
||||
<text class="dd-card__corner-num">{{ cornerIndex(idx) }}</text>
|
||||
</view>
|
||||
<view class="dd-card__head">
|
||||
<text class="dd-card__title">{{ cardTitle(row) }}</text>
|
||||
<text class="dd-card__time">{{
|
||||
formatRelativeTime(row.applyTime || row.createTime)
|
||||
}}</text>
|
||||
</view>
|
||||
<view v-if="metaLines(row).length" class="dd-card__meta">
|
||||
<text
|
||||
v-for="(line, idx) in metaLines(row)"
|
||||
:key="idx"
|
||||
class="dd-card__meta-line"
|
||||
>
|
||||
{{ line }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="dd-card__foot">
|
||||
<view class="dd-card__user">
|
||||
<view class="dd-card__avatar">{{ avatarText(row) }}</view>
|
||||
<text class="dd-card__user-text">{{ submitterText(row) }}</text>
|
||||
</view>
|
||||
<text
|
||||
class="dd-card__status"
|
||||
:class="'is-' + statusTone(row.instanceStatus)"
|
||||
>
|
||||
{{ statusText(row.instanceStatus) }}
|
||||
</text>
|
||||
</view>
|
||||
</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
|
||||
v-if="sheetVisible"
|
||||
class="sheet-mask"
|
||||
@click="closeBatchSheet"
|
||||
/>
|
||||
<view class="sheet" :class="{ 'is-show': sheetVisible }">
|
||||
<view class="sheet__hd">
|
||||
<text class="sheet__title">{{ sheetTitle }}</text>
|
||||
<text class="sheet__close" @click="closeBatchSheet">取消</text>
|
||||
</view>
|
||||
<view class="sheet__hint">
|
||||
将对
|
||||
<text class="sheet__hint-em">{{ selectedIds.length }}</text>
|
||||
条待办执行操作
|
||||
</view>
|
||||
<textarea
|
||||
class="sheet__ta"
|
||||
:value="sheetComment"
|
||||
:placeholder="sheetPlaceholder"
|
||||
:maxlength="500"
|
||||
@input="onSheetInput"
|
||||
/>
|
||||
<view class="sheet__ft">
|
||||
<view class="sheet__btn is-cancel" @click="closeBatchSheet">取消</view>
|
||||
<view
|
||||
class="sheet__btn"
|
||||
:class="sheetKind === 'return' ? 'is-return' : 'is-ok'"
|
||||
@click="confirmBatch"
|
||||
>
|
||||
{{ batchActing ? '处理中…' : '确定' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import OaTaskCard from '../../../components/OaTaskCard.vue'
|
||||
import { fetchOaTasks } from '../../../utils/oa-api.js'
|
||||
import VanEmpty from '../../../components/van-empty/van-empty.vue'
|
||||
import {
|
||||
fetchApproveOaTask,
|
||||
fetchOaTasks,
|
||||
fetchReturnOaTask
|
||||
} from '../../../utils/oa-api.js'
|
||||
import { forceRelogin, getToken } from '../../../utils/auth.js'
|
||||
import { stashOaDetailTitle } from '../../../utils/oa-h5.js'
|
||||
import { formatCornerIndex } from '../../../utils/oa-corner-index.js'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
const STATUS_MAP = {
|
||||
pending: '待处理',
|
||||
@@ -89,22 +183,55 @@
|
||||
transferred: '已转交'
|
||||
}
|
||||
|
||||
function pad2(n) {
|
||||
return n < 10 ? '0' + n : String(n)
|
||||
}
|
||||
|
||||
function parseDateTime(str) {
|
||||
if (!str) return null
|
||||
const s = String(str).trim().replace(/-/g, '/')
|
||||
const d = new Date(s)
|
||||
return Number.isNaN(d.getTime()) ? null : d
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { OaTaskCard },
|
||||
components: { VanEmpty },
|
||||
data() {
|
||||
return {
|
||||
appName: '',
|
||||
initiatorName: '',
|
||||
appNameFocused: false,
|
||||
initiatorFocused: false,
|
||||
statusBarPx: 0,
|
||||
keyword: '',
|
||||
searchFocused: false,
|
||||
list: [],
|
||||
page: 1,
|
||||
total: 0,
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
hasMore: true,
|
||||
_searchTimer: null,
|
||||
cardActions: [{ key: 'approve', label: '办理', primary: true }]
|
||||
manageMode: false,
|
||||
selectedIds: [],
|
||||
sheetVisible: false,
|
||||
sheetKind: 'approve',
|
||||
sheetComment: '',
|
||||
batchActing: false,
|
||||
_searchTimer: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sheetTitle() {
|
||||
return this.sheetKind === 'return' ? '批量打回' : '批量通过'
|
||||
},
|
||||
sheetPlaceholder() {
|
||||
return this.sheetKind === 'return'
|
||||
? '请填写打回意见(必填)'
|
||||
: '审批意见(可选)'
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
try {
|
||||
const sys = uni.getSystemInfoSync() || {}
|
||||
this.statusBarPx = Number(sys.statusBarHeight) || 0
|
||||
} catch (_) {
|
||||
this.statusBarPx = 0
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
@@ -132,54 +259,116 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
uni.navigateBack({
|
||||
fail: () => {
|
||||
uni.reLaunch({ url: '/pages/index/index' })
|
||||
}
|
||||
})
|
||||
},
|
||||
cornerIndex(idx) {
|
||||
return formatCornerIndex(Number(idx) + 1)
|
||||
},
|
||||
toggleManage() {
|
||||
this.manageMode = !this.manageMode
|
||||
if (!this.manageMode) this.selectedIds = []
|
||||
this.closeBatchSheet()
|
||||
},
|
||||
isSelected(id) {
|
||||
return this.selectedIds.indexOf(id) >= 0
|
||||
},
|
||||
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])
|
||||
}
|
||||
},
|
||||
onRowClick(row) {
|
||||
if (!row) return
|
||||
if (this.manageMode) {
|
||||
this.toggleSelect(row.id)
|
||||
return
|
||||
}
|
||||
this.openApprove(row)
|
||||
},
|
||||
cardTitle(row) {
|
||||
return (row && (row.appName || row.title)) || '-'
|
||||
},
|
||||
avatarText(row) {
|
||||
const name = (row && row.initiatorUserName) || ''
|
||||
if (!name) return '申'
|
||||
return String(name).slice(0, 2)
|
||||
},
|
||||
submitterText(row) {
|
||||
const name = (row && row.initiatorUserName) || ''
|
||||
return name ? '由' + name + '提交' : '待处理'
|
||||
},
|
||||
statusText(s) {
|
||||
if (!s) return '-'
|
||||
if (!s) return '待处理'
|
||||
return STATUS_MAP[s] || s
|
||||
},
|
||||
statusTone(s) {
|
||||
if (s === 'pending' || s === 'running') return 'warn'
|
||||
if (s === 'pending' || s === 'running' || !s) 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.currentNodeName) || '-'
|
||||
}
|
||||
]
|
||||
const lines = []
|
||||
const fields = (row && row.previewFields) || []
|
||||
for (let i = 0; i < fields.length && i < 3; i++) {
|
||||
const f = fields[i] || {}
|
||||
const label = String(f.label || '').trim() || '字段'
|
||||
const value =
|
||||
f.value != null && String(f.value).trim() !== ''
|
||||
? String(f.value)
|
||||
: '-'
|
||||
lines.push(label + ':' + value)
|
||||
}
|
||||
return lines
|
||||
},
|
||||
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()
|
||||
formatRelativeTime(str) {
|
||||
const d = parseDateTime(str)
|
||||
if (!d) return str || '-'
|
||||
const now = new Date()
|
||||
const startToday = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate()
|
||||
)
|
||||
const startThat = new Date(d.getFullYear(), d.getMonth(), d.getDate())
|
||||
const dayDiff = Math.round(
|
||||
(startToday.getTime() - startThat.getTime()) / 86400000
|
||||
)
|
||||
if (dayDiff === 0) {
|
||||
return pad2(d.getHours()) + ':' + pad2(d.getMinutes())
|
||||
}
|
||||
if (dayDiff === 1) return '昨天'
|
||||
if (dayDiff === 2) return '前天'
|
||||
if (dayDiff > 2 && dayDiff < 7) return dayDiff + '天前'
|
||||
return (
|
||||
d.getFullYear() +
|
||||
'.' +
|
||||
pad2(d.getMonth() + 1) +
|
||||
'.' +
|
||||
pad2(d.getDate())
|
||||
)
|
||||
},
|
||||
_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 ''
|
||||
},
|
||||
onSearchInput(e) {
|
||||
this.keyword = this._inputVal(e)
|
||||
if (this._searchTimer) clearTimeout(this._searchTimer)
|
||||
this._searchTimer = setTimeout(() => {
|
||||
this.loadList(true)
|
||||
}, 400)
|
||||
},
|
||||
onSearchConfirm() {
|
||||
if (this._searchTimer) {
|
||||
clearTimeout(this._searchTimer)
|
||||
@@ -187,12 +376,8 @@
|
||||
}
|
||||
this.loadList(true)
|
||||
},
|
||||
clearAppName() {
|
||||
this.appName = ''
|
||||
this.onSearchConfirm()
|
||||
},
|
||||
clearInitiator() {
|
||||
this.initiatorName = ''
|
||||
clearKeyword() {
|
||||
this.keyword = ''
|
||||
this.onSearchConfirm()
|
||||
},
|
||||
openApprove(row) {
|
||||
@@ -202,7 +387,6 @@
|
||||
uni.showToast({ title: '缺少实例 ID', icon: 'none' })
|
||||
return
|
||||
}
|
||||
// row.id = 待办审批任务 id(对齐办理动作 taskId;Web pending 仅跳实例页,移动端需带 taskId)
|
||||
const taskId = String(row.id || '').trim()
|
||||
stashOaDetailTitle(row.appName || row.title)
|
||||
let url =
|
||||
@@ -214,6 +398,61 @@
|
||||
}
|
||||
uni.navigateTo({ url })
|
||||
},
|
||||
openBatchSheet(kind) {
|
||||
if (!this.selectedIds.length || this.batchActing) return
|
||||
this.sheetKind = kind === 'return' ? 'return' : 'approve'
|
||||
this.sheetComment = kind === 'return' ? '' : '同意'
|
||||
this.sheetVisible = true
|
||||
},
|
||||
closeBatchSheet() {
|
||||
if (this.batchActing) return
|
||||
this.sheetVisible = false
|
||||
},
|
||||
onSheetInput(e) {
|
||||
this.sheetComment = this._inputVal(e)
|
||||
},
|
||||
async confirmBatch() {
|
||||
if (this.batchActing) return
|
||||
const ids = (this.selectedIds || []).filter(Boolean)
|
||||
if (!ids.length) return
|
||||
const comment = String(this.sheetComment || '').trim()
|
||||
if (this.sheetKind === 'return' && !comment) {
|
||||
uni.showToast({ title: '打回意见必填', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.batchActing = true
|
||||
let ok = 0
|
||||
let fail = 0
|
||||
try {
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const id = ids[i]
|
||||
try {
|
||||
if (this.sheetKind === 'return') {
|
||||
await fetchReturnOaTask(id, comment)
|
||||
} else {
|
||||
await fetchApproveOaTask(id, comment || undefined)
|
||||
}
|
||||
ok += 1
|
||||
} catch (_) {
|
||||
fail += 1
|
||||
}
|
||||
}
|
||||
this.sheetVisible = false
|
||||
this.selectedIds = []
|
||||
this.manageMode = false
|
||||
let title = ''
|
||||
if (fail === 0) {
|
||||
title =
|
||||
(this.sheetKind === 'return' ? '已打回 ' : '已通过 ') + ok + ' 条'
|
||||
} else {
|
||||
title = '成功 ' + ok + ',失败 ' + fail
|
||||
}
|
||||
uni.showToast({ title: title, icon: fail === 0 ? 'success' : 'none' })
|
||||
await this.loadList(true)
|
||||
} finally {
|
||||
this.batchActing = false
|
||||
}
|
||||
},
|
||||
async loadList(reset) {
|
||||
if (!getToken()) {
|
||||
forceRelogin('请先登录')
|
||||
@@ -229,8 +468,7 @@
|
||||
}
|
||||
try {
|
||||
const res = await fetchOaTasks('pending', {
|
||||
appName: (this.appName || '').trim() || undefined,
|
||||
initiatorName: (this.initiatorName || '').trim() || undefined,
|
||||
keyword: (this.keyword || '').trim() || undefined,
|
||||
page: this.page,
|
||||
pageSize: PAGE_SIZE
|
||||
})
|
||||
@@ -242,6 +480,14 @@
|
||||
const loaded = this.list.length
|
||||
this.hasMore = loaded < total && rows.length > 0
|
||||
if (rows.length > 0) this.page += 1
|
||||
// 刷新后去掉已不在列表中的勾选
|
||||
if (isReset && this.selectedIds.length) {
|
||||
const idSet = {}
|
||||
this.list.forEach((r) => {
|
||||
if (r && r.id) idSet[r.id] = true
|
||||
})
|
||||
this.selectedIds = this.selectedIds.filter((id) => idSet[id])
|
||||
}
|
||||
} catch (e) {
|
||||
if (isReset) this.list = []
|
||||
uni.showToast({
|
||||
@@ -265,30 +511,77 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
.page-head {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: #fff;
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 1rpx solid #f1f5f9;
|
||||
z-index: 20;
|
||||
background: #ecfdf5;
|
||||
border-bottom: 1rpx solid #e8eeeb;
|
||||
}
|
||||
|
||||
.toolbar-row {
|
||||
.nav__inner {
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 20rpx 24rpx 0;
|
||||
padding: 0 16rpx 0 8rpx;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
|
||||
.nav__back {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav__back-ico {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-left: 4rpx solid #0f291e;
|
||||
border-bottom: 4rpx solid #0f291e;
|
||||
transform: rotate(45deg);
|
||||
margin-left: 8rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.toolbar-row--sub {
|
||||
padding-top: 12rpx;
|
||||
.nav__title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 32rpx;
|
||||
font-weight: 650;
|
||||
color: #0f291e;
|
||||
}
|
||||
|
||||
.nav__action {
|
||||
margin-left: auto;
|
||||
padding: 0 16rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 28rpx;
|
||||
font-weight: 650;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.nav__action.is-done {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.nav__action:active {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
background: #fff;
|
||||
padding: 16rpx 24rpx;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
@@ -362,6 +655,226 @@
|
||||
margin-top: -2rpx;
|
||||
}
|
||||
|
||||
.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 #e8eaed;
|
||||
box-shadow: 0 4rpx 20rpx rgba(15, 23, 42, 0.04);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.batch-bar__hint {
|
||||
font-size: 26rpx;
|
||||
color: #646a73;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.batch-bar__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.batch-bar__btn {
|
||||
min-height: 64rpx;
|
||||
padding: 0 24rpx;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 14rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.batch-bar__btn.is-ok {
|
||||
color: #fff;
|
||||
background: #0a9f6d;
|
||||
}
|
||||
|
||||
.batch-bar__btn.is-return {
|
||||
color: #d97706;
|
||||
background: #fff7e8;
|
||||
border: 1rpx solid #fde68a;
|
||||
}
|
||||
|
||||
.batch-bar__btn:active {
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.pending-list {
|
||||
padding: 20rpx 24rpx 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pending-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pending-check {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pending-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;
|
||||
}
|
||||
|
||||
.pending-check__box.on {
|
||||
background: #0a9f6d;
|
||||
border-color: #0a9f6d;
|
||||
}
|
||||
|
||||
.pending-check__mark {
|
||||
font-size: 22rpx;
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dd-card {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx 28rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 4rpx 20rpx rgba(15, 23, 42, 0.04);
|
||||
border: 1rpx solid transparent;
|
||||
}
|
||||
|
||||
.dd-card.is-selected {
|
||||
border-color: #86efac;
|
||||
box-shadow: 0 0 0 4rpx rgba(10, 159, 109, 0.16);
|
||||
}
|
||||
|
||||
.dd-card:active {
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.dd-card__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.dd-card__title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.dd-card__time {
|
||||
flex-shrink: 0;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.4;
|
||||
color: #8f959e;
|
||||
padding-top: 4rpx;
|
||||
}
|
||||
|
||||
.dd-card__meta {
|
||||
margin-top: 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.dd-card__meta-line {
|
||||
font-size: 26rpx;
|
||||
line-height: 1.45;
|
||||
color: #8f959e;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dd-card__foot {
|
||||
margin-top: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.dd-card__user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dd-card__avatar {
|
||||
width: auto;
|
||||
min-width: 52rpx;
|
||||
height: 52rpx;
|
||||
padding: 0 12rpx;
|
||||
border-radius: 12rpx;
|
||||
background: linear-gradient(145deg, #0a9f6d, #087f57);
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dd-card__user-text {
|
||||
font-size: 24rpx;
|
||||
color: #646a73;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dd-card__status {
|
||||
flex-shrink: 0;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.dd-card__status.is-success {
|
||||
color: #0a9f6d;
|
||||
}
|
||||
|
||||
.dd-card__status.is-danger {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.dd-card__status.is-info {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.state {
|
||||
padding: 80rpx 24rpx;
|
||||
text-align: center;
|
||||
@@ -421,4 +934,111 @@
|
||||
transform: translateY(-12rpx);
|
||||
}
|
||||
}
|
||||
|
||||
.sheet-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 90;
|
||||
background: rgba(15, 23, 42, 0.45);
|
||||
}
|
||||
|
||||
.sheet {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 91;
|
||||
padding: 28rpx 32rpx calc(28rpx + env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
border-radius: 28rpx 28rpx 0 0;
|
||||
box-shadow: 0 -8rpx 32rpx rgba(15, 23, 42, 0.12);
|
||||
transform: translateY(110%);
|
||||
transition: transform 0.22s ease;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sheet.is-show {
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.sheet__hd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.sheet__title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #0f291e;
|
||||
}
|
||||
|
||||
.sheet__close {
|
||||
font-size: 26rpx;
|
||||
color: #64748b;
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
|
||||
.sheet__hint {
|
||||
margin-bottom: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.sheet__hint-em {
|
||||
color: #166534;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sheet__ta {
|
||||
width: 100%;
|
||||
min-height: 180rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
border-radius: 16rpx;
|
||||
border: 1rpx solid #e2e8f0;
|
||||
background: #f8fafc;
|
||||
font-size: 28rpx;
|
||||
color: #1f2329;
|
||||
line-height: 1.5;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sheet__ft {
|
||||
margin-top: 24rpx;
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.sheet__btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 30rpx;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.sheet__btn.is-cancel {
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.sheet__btn.is-ok {
|
||||
background: #0a9f6d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sheet__btn.is-return {
|
||||
background: #d97706;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sheet__btn:active {
|
||||
opacity: 0.82;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user