470 lines
9.7 KiB
Vue
470 lines
9.7 KiB
Vue
<template>
|
||
<view class="page">
|
||
<!-- 顶部工具栏(系统导航栏已带返回按钮 + 标题,这里只放搜索/筛选) -->
|
||
<view class="toolbar">
|
||
<view class="search-box">
|
||
<FaIcon name="magnifying-glass" color="#94a3b8" :size="16" />
|
||
<input
|
||
class="search-input"
|
||
v-model="keyword"
|
||
placeholder="搜索单据标题"
|
||
confirm-type="search"
|
||
@confirm="onSearch"
|
||
/>
|
||
<text v-if="keyword" class="search-clear" @tap="clearKeyword">清空</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 状态下拉面板 -->
|
||
<view
|
||
v-for="tab in statusTabs"
|
||
:key="String(tab.value)"
|
||
class="dropdown-option"
|
||
:class="{ active: currentStatus === tab.value }"
|
||
hover-class="dropdown-option-active"
|
||
@tap="onStatusChange(tab.value)"
|
||
>
|
||
<text class="dropdown-option-text">{{ tab.label }}</text>
|
||
<FaIcon v-if="currentStatus === tab.value" name="check" color="#2563eb" :size="14" />
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 列表 -->
|
||
<scroll-view class="list-scroll" scroll-y :show-scrollbar="false" @scrolltolower="loadMore">
|
||
<view v-if="loading && !list.length" class="state-box">
|
||
<text class="state-text">加载中...</text>
|
||
</view>
|
||
<view v-else-if="!list.length" class="state-box">
|
||
<u-empty mode="list" text="暂无审批记录" />
|
||
</view>
|
||
<block v-else>
|
||
<view
|
||
v-for="row in list"
|
||
:key="row.id"
|
||
class="approval-card"
|
||
>
|
||
<view class="card-top">
|
||
<text class="card-title">{{ row.biz_title || '(无标题)' }}</text>
|
||
<u-tag
|
||
:text="row.status_label || ''"
|
||
:type="row.status_tag || 'info'"
|
||
size="mini"
|
||
/>
|
||
</view>
|
||
<view class="card-meta">
|
||
<text class="meta-text">{{ row.biz_type || '-' }}</text>
|
||
<text class="meta-text">发起人:{{ row.submit_user_name || '-' }}</text>
|
||
</view>
|
||
<view class="card-meta">
|
||
<text class="meta-text">提交:{{ row.submit_time || row.create_time || '-' }}</text>
|
||
<text v-if="row.finish_time" class="meta-text">完成:{{ row.finish_time }}</text>
|
||
</view>
|
||
<view v-if="row.current_node" class="card-meta">
|
||
<text class="meta-text">当前节点:{{ row.current_node }}</text>
|
||
</view>
|
||
<view class="card-actions">
|
||
<view class="op-btn op-view" hover-class="btn-active" @tap.stop="openDetail(row.id)">查看详情</view>
|
||
</view>
|
||
</view>
|
||
<view v-if="finished" class="load-done">
|
||
<text>{{ list.length ? '没有更多了' : '' }}</text>
|
||
</view>
|
||
<view v-else class="more-tip" @tap="loadMore">
|
||
<text>加载更多</text>
|
||
</view>
|
||
</block>
|
||
<view class="bottom-space" />
|
||
</scroll-view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted, nextTick } from 'vue'
|
||
import { onShow } from '@dcloudio/uni-app'
|
||
import {
|
||
fetchApprovalList,
|
||
APPROVAL_INSTANCE_STATUS_OPTIONS
|
||
} from '@/api/approval.js'
|
||
import { isLoggedIn } from '@/utils/auth.js'
|
||
|
||
const PAGE_SIZE = 20
|
||
|
||
/** 状态 tab(首个为「全部」,不展示「草稿」) */
|
||
const statusTabs = [
|
||
{ value: '', label: '全部' },
|
||
...APPROVAL_INSTANCE_STATUS_OPTIONS.filter(o => o.value !== 0)
|
||
]
|
||
|
||
const list = ref([])
|
||
const total = ref(0)
|
||
const page = ref(1)
|
||
const loading = ref(false)
|
||
const loadingMore = ref(false)
|
||
const finished = ref(false)
|
||
const keyword = ref('')
|
||
const currentStatus = ref('')
|
||
|
||
/** 当前展开的下拉:'status' | '' */
|
||
const openDropdown = ref('')
|
||
|
||
/** filter-bar 的底部坐标(用于下拉面板 fixed 定位) */
|
||
const filterBarBottom = ref(0)
|
||
|
||
const bizTypes = ref([])
|
||
|
||
/** 当前状态下拉框展示文案 */
|
||
const currentStatusLabel = computed(() => {
|
||
const hit = statusTabs.find(s => s.value === currentStatus.value)
|
||
return hit ? hit.label : '全部'
|
||
})
|
||
|
||
/** 下拉面板顶部偏移(紧贴 filter-bar 底部 + 8px 间隙) */
|
||
const panelTopStyle = computed(() => ({
|
||
top: (filterBarBottom.value + 8) + 'px'
|
||
}))
|
||
|
||
function toggleDropdown(name) {
|
||
openDropdown.value = openDropdown.value === name ? '' : name
|
||
if (openDropdown.value) {
|
||
// 打开后下一帧再取位置(filter-bar 还未渲染完成则取到 0)
|
||
nextTick(() => measureFilterBar())
|
||
}
|
||
}
|
||
|
||
function closeDropdown() {
|
||
openDropdown.value = ''
|
||
}
|
||
|
||
/** 动态获取 filter-bar 在视口中的底部坐标 */
|
||
function measureFilterBar() {
|
||
uni.createSelectorQuery()
|
||
.select('.filter-bar')
|
||
.boundingClientRect(rect => {
|
||
if (rect && typeof rect.bottom === 'number') {
|
||
filterBarBottom.value = Math.round(rect.bottom)
|
||
}
|
||
})
|
||
.exec()
|
||
}
|
||
|
||
onMounted(() => {
|
||
if (!isLoggedIn()) {
|
||
uni.reLaunch({ url: '/pages/login/login' })
|
||
return
|
||
}
|
||
refresh()
|
||
// 取 filter-bar 位置供下拉面板定位用
|
||
nextTick(() => measureFilterBar())
|
||
})
|
||
|
||
onShow(() => {
|
||
if (isLoggedIn()) refresh()
|
||
})
|
||
|
||
async function loadBizTypes() {
|
||
// 类型筛选已移除,保留此函数以避免潜在依赖
|
||
bizTypes.value = [{ value: '', label: '全部' }]
|
||
}
|
||
|
||
async function refresh() {
|
||
page.value = 1
|
||
finished.value = false
|
||
list.value = []
|
||
await loadPage()
|
||
}
|
||
|
||
async function loadMore() {
|
||
if (finished.value || loadingMore.value) return
|
||
loadingMore.value = true
|
||
page.value += 1
|
||
await loadPage()
|
||
}
|
||
|
||
async function loadPage() {
|
||
loading.value = true
|
||
try {
|
||
const data = await fetchApprovalList({
|
||
page: page.value,
|
||
pageSize: PAGE_SIZE,
|
||
keyword: keyword.value.trim(),
|
||
status: currentStatus.value
|
||
})
|
||
const rows = data.list || []
|
||
list.value = page.value === 1 ? rows : [...list.value, ...rows]
|
||
total.value = data.total || 0
|
||
finished.value = list.value.length >= total.value
|
||
} catch (e) {
|
||
if (page.value > 1) page.value -= 1
|
||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||
} finally {
|
||
loading.value = false
|
||
loadingMore.value = false
|
||
}
|
||
}
|
||
|
||
function onSearch() {
|
||
refresh()
|
||
}
|
||
|
||
function clearKeyword() {
|
||
keyword.value = ''
|
||
refresh()
|
||
}
|
||
|
||
function onStatusChange(value) {
|
||
currentStatus.value = value
|
||
closeDropdown()
|
||
refresh()
|
||
}
|
||
|
||
function openDetail(id) {
|
||
if (!id) {
|
||
uni.showToast({ title: '单据数据异常', icon: 'none' })
|
||
return
|
||
}
|
||
uni.navigateTo({ url: `/pages/approval/detail?id=${id}` })
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
@import '@/src/styles/page-common.scss';
|
||
|
||
.page {
|
||
min-height: 100vh;
|
||
background: $color-bg-page;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* ====== 顶部搜索 ====== */
|
||
.toolbar {
|
||
position: relative;
|
||
z-index: 10;
|
||
flex-shrink: 0;
|
||
padding: 20rpx $page-padding-x;
|
||
background: $color-card;
|
||
box-shadow: $shadow-header;
|
||
}
|
||
|
||
.search-box {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
height: 72rpx;
|
||
padding: 0 24rpx;
|
||
background: $color-bg-page;
|
||
border-radius: $radius-full;
|
||
box-shadow: $shadow-card;
|
||
|
||
.search-input {
|
||
flex: 1;
|
||
font-size: 26rpx;
|
||
color: $color-text;
|
||
}
|
||
|
||
.search-clear {
|
||
font-size: 24rpx;
|
||
color: $color-text-muted;
|
||
}
|
||
}
|
||
|
||
/* ====== 筛选条(顶部下拉触发器) ====== */
|
||
.filter-bar {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
gap: 16rpx;
|
||
padding: 12rpx $page-padding-x;
|
||
background: $color-card;
|
||
}
|
||
|
||
.filter-trigger {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
height: 72rpx;
|
||
padding: 0 20rpx;
|
||
border-radius: $radius-md;
|
||
background: $color-bg-page;
|
||
border: 1rpx solid transparent;
|
||
transition: background 0.2s, border-color 0.2s;
|
||
|
||
&.open {
|
||
background: $color-primary-bg;
|
||
border-color: $color-primary;
|
||
}
|
||
}
|
||
|
||
.filter-trigger-active {
|
||
background: #e2e8f0;
|
||
}
|
||
|
||
.filter-label {
|
||
font-size: 26rpx;
|
||
color: $color-text-muted;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.filter-value {
|
||
flex: 1;
|
||
min-width: 0;
|
||
font-size: 26rpx;
|
||
color: $color-text;
|
||
font-weight: 500;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.filter-trigger.open .filter-value {
|
||
color: $color-primary;
|
||
}
|
||
|
||
/* ====== 下拉面板 ====== */
|
||
.dropdown-mask {
|
||
position: fixed;
|
||
top: 0; left: 0; right: 0; bottom: 0;
|
||
background: transparent;
|
||
z-index: 50;
|
||
}
|
||
|
||
.dropdown-panel {
|
||
position: absolute;
|
||
left: $page-padding-x;
|
||
right: $page-padding-x;
|
||
background: $color-card;
|
||
border-radius: $radius-lg;
|
||
box-shadow: 0 12rpx 32rpx rgba(15, 23, 42, 0.16);
|
||
z-index: 60;
|
||
max-height: 480rpx;
|
||
overflow-y: auto;
|
||
padding: 8rpx 0;
|
||
}
|
||
|
||
.dropdown-option {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12rpx;
|
||
padding: 20rpx 28rpx;
|
||
font-size: 28rpx;
|
||
color: $color-text;
|
||
transition: background 0.15s;
|
||
|
||
&.active {
|
||
color: $color-primary;
|
||
font-weight: 500;
|
||
background: $color-primary-bg;
|
||
}
|
||
}
|
||
|
||
.dropdown-option-active {
|
||
background: $color-bg-page;
|
||
}
|
||
|
||
.dropdown-option-text {
|
||
flex: 1;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* ====== 列表 ====== */
|
||
.list-scroll {
|
||
flex: 1;
|
||
height: 0;
|
||
}
|
||
|
||
.approval-card {
|
||
@include card;
|
||
margin: 16rpx $page-padding-x 0;
|
||
padding: 24rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10rpx;
|
||
}
|
||
|
||
.card-active { opacity: 0.85; }
|
||
|
||
.card-top {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.card-title {
|
||
flex: 1;
|
||
min-width: 0;
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
color: $color-text;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.card-meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.meta-text {
|
||
font-size: 24rpx;
|
||
color: $color-text-muted;
|
||
}
|
||
|
||
.card-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 16rpx;
|
||
padding-top: 14rpx;
|
||
border-top: 1rpx solid $color-divider;
|
||
margin-top: 6rpx;
|
||
}
|
||
|
||
.op-btn {
|
||
padding: 14rpx 36rpx;
|
||
border-radius: $radius-md;
|
||
font-size: 26rpx;
|
||
font-weight: 500;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.op-view { background: $color-primary; }
|
||
|
||
.btn-active { opacity: 0.85; }
|
||
|
||
.more-tip {
|
||
padding: 12rpx 0 4rpx;
|
||
text-align: center;
|
||
font-size: 24rpx;
|
||
color: $color-primary;
|
||
}
|
||
|
||
.load-done {
|
||
padding: 12rpx 0 4rpx;
|
||
text-align: center;
|
||
font-size: 24rpx;
|
||
color: $color-text-muted;
|
||
}
|
||
|
||
.state-box {
|
||
display: flex;
|
||
justify-content: center;
|
||
padding: 140rpx 0;
|
||
}
|
||
|
||
.state-text {
|
||
font-size: 28rpx;
|
||
color: $color-text-muted;
|
||
}
|
||
|
||
.bottom-space {
|
||
height: 40rpx;
|
||
}
|
||
</style>
|