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:
zlwuandCursor committed 2026-08-13 11:37:10 +08:00
1 parent df941a3450
commit e229525a61
5 files changed
+1990 -636

No files matched your search

+439 -193
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+283 -90
View File
@@ -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;
+718 -98
View File
@@ -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>