feat(oa): add mobile draft box list with edit submit delete
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
387e82426d
commit
6b0fb7b128
2 files changed
+627
-7
No files matched your search
@@ -69,7 +69,9 @@
|
||||
"style": {
|
||||
"navigationBarTitleText": "草稿箱",
|
||||
"navigationBarBackgroundColor": "#ecfdf5",
|
||||
"navigationBarTextStyle": "black"
|
||||
"navigationBarTextStyle": "black",
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundTextStyle": "dark"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,12 +1,630 @@
|
||||
<template>
|
||||
<view class="page-shell" style="padding:24rpx">
|
||||
<text>{{ title }}</text>
|
||||
<view class="page">
|
||||
<view class="toolbar">
|
||||
<view class="toolbar-row">
|
||||
<view class="search-box" :class="{ 'is-focus': 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
|
||||
class="manage-btn"
|
||||
:class="{ 'is-active': manageMode }"
|
||||
@click="toggleManage"
|
||||
>
|
||||
{{ manageMode ? '完成' : '管理' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="manageMode && selectedIds.length" class="batch-bar">
|
||||
<text class="batch-bar__hint">已选 {{ selectedIds.length }} 项</text>
|
||||
<view class="batch-bar__btn" @click="batchDelete">批量删除</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading && !list.length" class="state state-loading">
|
||||
<view class="pulse-dots" aria-hidden="true">
|
||||
<view class="pulse-dot" />
|
||||
<view class="pulse-dot" />
|
||||
<view class="pulse-dot" />
|
||||
</view>
|
||||
<text class="state-loading__text">加载草稿…</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!list.length" class="oa-task-list__empty">暂无草稿</view>
|
||||
|
||||
<view v-else class="oa-task-list">
|
||||
<view
|
||||
v-for="row in list"
|
||||
:key="row.id"
|
||||
class="draft-row"
|
||||
:class="{ 'is-manage': manageMode, 'is-selected': isSelected(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>
|
||||
</view>
|
||||
<view v-if="loadingMore" class="oa-task-list__end">加载更多…</view>
|
||||
<view v-else-if="!hasMore" class="oa-task-list__end">已经到底了</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return { title: '草稿箱' }
|
||||
import OaTaskCard from '../../../components/OaTaskCard.vue'
|
||||
import {
|
||||
fetchBatchDeleteOaInstanceDrafts,
|
||||
fetchDeleteOaInstanceDraft,
|
||||
fetchOaInstanceDraftCount,
|
||||
fetchOaInstanceDraftList,
|
||||
fetchSubmitOaInstanceDraft
|
||||
} from '../../../utils/oa-api.js'
|
||||
import { forceRelogin, getToken } from '../../../utils/auth.js'
|
||||
import { stashOaStartTitle } from '../../../utils/oa-h5.js'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export default {
|
||||
components: { OaTaskCard },
|
||||
data() {
|
||||
return {
|
||||
keyword: '',
|
||||
searchFocused: false,
|
||||
list: [],
|
||||
page: 1,
|
||||
total: 0,
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
hasMore: true,
|
||||
manageMode: false,
|
||||
selectedIds: [],
|
||||
submittingId: '',
|
||||
_searchTimer: null,
|
||||
cardActions: [
|
||||
{ key: 'edit', label: '编辑', primary: true },
|
||||
{ key: 'submit', label: '提交审批' },
|
||||
{ key: 'delete', label: '删除', danger: true }
|
||||
]
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
if (!getToken()) {
|
||||
forceRelogin('请先登录')
|
||||
return
|
||||
}
|
||||
this.loadList(true)
|
||||
this.refreshDraftCount()
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.loadList(true)
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
onReachBottom() {
|
||||
if (!this.hasMore || this.loading || this.loadingMore) return
|
||||
this.loadList(false)
|
||||
},
|
||||
onUnload() {
|
||||
if (this._searchTimer) {
|
||||
clearTimeout(this._searchTimer)
|
||||
this._searchTimer = null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cardTitle(row) {
|
||||
return (row && (row.appName || row.title)) || '-'
|
||||
},
|
||||
metaLines(row) {
|
||||
const ver =
|
||||
row && row.versionNo != null && row.versionNo !== ''
|
||||
? `v${row.versionNo}`
|
||||
: '-'
|
||||
return [
|
||||
{ label: '最近保存', value: (row && row.updateTime) || '-' },
|
||||
{ label: '版本', value: ver }
|
||||
]
|
||||
},
|
||||
isSelected(id) {
|
||||
return this.selectedIds.indexOf(id) >= 0
|
||||
},
|
||||
onSearchInput(e) {
|
||||
const v =
|
||||
e && e.detail && e.detail.value != null
|
||||
? String(e.detail.value)
|
||||
: e && e.target
|
||||
? String(e.target.value || '')
|
||||
: ''
|
||||
this.keyword = v
|
||||
if (this._searchTimer) clearTimeout(this._searchTimer)
|
||||
this._searchTimer = setTimeout(() => {
|
||||
this.loadList(true)
|
||||
}, 400)
|
||||
},
|
||||
onSearchConfirm() {
|
||||
if (this._searchTimer) {
|
||||
clearTimeout(this._searchTimer)
|
||||
this._searchTimer = null
|
||||
}
|
||||
this.loadList(true)
|
||||
},
|
||||
clearKeyword() {
|
||||
this.keyword = ''
|
||||
if (this._searchTimer) {
|
||||
clearTimeout(this._searchTimer)
|
||||
this._searchTimer = null
|
||||
}
|
||||
this.loadList(true)
|
||||
},
|
||||
toggleManage() {
|
||||
this.manageMode = !this.manageMode
|
||||
if (!this.manageMode) this.selectedIds = []
|
||||
},
|
||||
toggleSelect(id) {
|
||||
if (!id) return
|
||||
const i = this.selectedIds.indexOf(id)
|
||||
if (i >= 0) {
|
||||
this.selectedIds = this.selectedIds.filter((x) => x !== id)
|
||||
} else {
|
||||
this.selectedIds = this.selectedIds.concat([id])
|
||||
}
|
||||
},
|
||||
onCardOpen(row) {
|
||||
if (this.manageMode) {
|
||||
this.toggleSelect(row && row.id)
|
||||
return
|
||||
}
|
||||
this.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)
|
||||
uni.navigateTo({
|
||||
url:
|
||||
'/pages/oa/webview/index?draftId=' +
|
||||
encodeURIComponent(row.id) +
|
||||
'&appId=' +
|
||||
encodeURIComponent(row.appId || '')
|
||||
})
|
||||
},
|
||||
confirm(title, content, confirmText) {
|
||||
return new Promise((resolve) => {
|
||||
uni.showModal({
|
||||
title: title || '提示',
|
||||
content: content || '',
|
||||
confirmText: confirmText || '确定',
|
||||
cancelText: '取消',
|
||||
success: (res) => resolve(!!(res && res.confirm)),
|
||||
fail: () => resolve(false)
|
||||
})
|
||||
})
|
||||
},
|
||||
async loadList(reset) {
|
||||
if (!getToken()) {
|
||||
forceRelogin('请先登录')
|
||||
return
|
||||
}
|
||||
const isReset = !!reset
|
||||
if (isReset) {
|
||||
this.page = 1
|
||||
this.hasMore = true
|
||||
this.loading = true
|
||||
this.selectedIds = []
|
||||
} else {
|
||||
this.loadingMore = true
|
||||
}
|
||||
try {
|
||||
const res = await fetchOaInstanceDraftList({
|
||||
keyword: (this.keyword || '').trim() || undefined,
|
||||
page: this.page,
|
||||
pageSize: PAGE_SIZE
|
||||
})
|
||||
const rows = (res && res.list) || []
|
||||
const total = (res && res.total) != null ? Number(res.total) : 0
|
||||
this.total = total
|
||||
if (isReset) this.list = rows
|
||||
else this.list = this.list.concat(rows)
|
||||
const loaded = this.list.length
|
||||
this.hasMore = loaded < total && rows.length > 0
|
||||
if (rows.length > 0) this.page += 1
|
||||
} catch (e) {
|
||||
if (isReset) this.list = []
|
||||
uni.showToast({
|
||||
title: (e && e.message) || '加载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.loading = false
|
||||
this.loadingMore = false
|
||||
}
|
||||
},
|
||||
async refreshDraftCount() {
|
||||
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 {
|
||||
const data = await fetchSubmitOaInstanceDraft(row.id)
|
||||
uni.showToast({ title: '已提交', icon: 'success' })
|
||||
await this.loadList(true)
|
||||
this.refreshDraftCount()
|
||||
if (data && data.id) {
|
||||
uni.navigateTo({
|
||||
url:
|
||||
'/pages/oa/webview/index?instanceId=' +
|
||||
encodeURIComponent(data.id) +
|
||||
'&mode=view'
|
||||
})
|
||||
}
|
||||
} 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() {
|
||||
const ids = (this.selectedIds || []).filter(Boolean)
|
||||
if (!ids.length) return
|
||||
const ok = await this.confirm(
|
||||
'批量删除',
|
||||
'确认删除选中的 ' + ids.length + ' 条草稿吗?',
|
||||
'删除'
|
||||
)
|
||||
if (!ok) return
|
||||
try {
|
||||
await fetchBatchDeleteOaInstanceDrafts(ids)
|
||||
uni.showToast({ title: '已删除', icon: 'success' })
|
||||
this.selectedIds = []
|
||||
this.manageMode = false
|
||||
await this.loadList(true)
|
||||
this.refreshDraftCount()
|
||||
} catch (e) {
|
||||
uni.showToast({
|
||||
title: (e && e.message) || '批量删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
padding-bottom: 48rpx;
|
||||
background: #f5f6f8;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: #fff;
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 1rpx solid #f1f5f9;
|
||||
}
|
||||
|
||||
.toolbar-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 20rpx 24rpx 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
height: 72rpx;
|
||||
padding: 0 24rpx;
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid #e8eaed;
|
||||
background: #fafafa;
|
||||
box-sizing: border-box;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
background 0.15s,
|
||||
box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.search-box.is-focus {
|
||||
border-color: #cbd5e1;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 0 6rpx rgba(148, 163, 184, 0.18);
|
||||
}
|
||||
|
||||
.search-ico {
|
||||
position: relative;
|
||||
width: 26rpx;
|
||||
height: 26rpx;
|
||||
border: 3rpx solid #94a3b8;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-ico::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 12rpx;
|
||||
height: 3rpx;
|
||||
right: -8rpx;
|
||||
bottom: -5rpx;
|
||||
background: #94a3b8;
|
||||
border-radius: 3rpx;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
font-size: 28rpx;
|
||||
color: #1f2937;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.search-ph {
|
||||
color: #b0b7c3;
|
||||
}
|
||||
|
||||
.search-clear {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 50%;
|
||||
background: #e5e7eb;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-clear__x {
|
||||
font-size: 28rpx;
|
||||
line-height: 1;
|
||||
color: #6b7280;
|
||||
margin-top: -2rpx;
|
||||
}
|
||||
|
||||
.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;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
margin: 16rpx 24rpx 0;
|
||||
padding: 20rpx 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #fff;
|
||||
border: 1rpx solid #d8f3e5;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.batch-bar__hint {
|
||||
font-size: 26rpx;
|
||||
color: #5f7a6d;
|
||||
}
|
||||
|
||||
.batch-bar__btn {
|
||||
min-height: 64rpx;
|
||||
padding: 0 28rpx;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 14rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.06);
|
||||
border: 1rpx solid rgba(239, 68, 68, 0.35);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.batch-bar__btn:active {
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.draft-check {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 40rpx 0 0 4rpx;
|
||||
margin-right: 12rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.draft-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;
|
||||
}
|
||||
|
||||
.draft-check__box.on {
|
||||
border-color: #22c55e;
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.draft-check__mark {
|
||||
font-size: 22rpx;
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.state {
|
||||
padding: 80rpx 24rpx;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.state-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 28rpx;
|
||||
min-height: 50vh;
|
||||
padding: 96rpx 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pulse-dots {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.pulse-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--brand-green, #22c55e);
|
||||
animation: oa-pulse-bounce 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.pulse-dot:nth-child(2) {
|
||||
animation-delay: 0.15s;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.pulse-dot:nth-child(3) {
|
||||
animation-delay: 0.3s;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.state-loading__text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted, #5f7a6d);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@keyframes oa-pulse-bounce {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-12rpx);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user