Files
yunzerwebsiteallinone/uniapp/pages/approval/index.vue
T
2026-10-16 22:40:02 +08:00

470 lines
9.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>