<template>
<view class="page-container">
<scroll-view style="flex:1" scroll-y="true" :scroll-bottom="scrollBottom" @scroll="onScroll">
<view class="work-container">
<view class="card-list">
<view class="card-item" v-for="(item, index) in approvalList" :key="index">
<view class="card-content">
<view class="item-type">{{ item.type }}</view>
<view class="item-status" :data-status="item.status">{{ getStatusText(item.status) }}</view>
<view class="item-detail" @tap="handleDetailClick(item)">详情</view>
<view class="checkbox-wrapper">
<view class="custom-checkbox" :class="{ 'checked': selectedItems[index] }"
@tap="toggleSelect(index)">
</view>
</view>
</view>
</view>
<!-- 列表底部占位,给签核区域留空间 -->
<view class="list-bottom-placeholder" :style="{ height: hasSelected ? '180rpx' : '0' }"></view>
</view>
</view>
</scroll-view>
<!-- 独立悬浮在tabbar上方的签核区域 -->
<view class="floating-action-bar" :class="{ 'show': hasSelected }">
<view class="floating-content">
<!-- 关闭按钮 -->
<view class="close-btn" @tap="clearSelection">
<text class="close-icon">×</text>
</view>
<!-- 左侧驳回按钮 -->
<view class="action-btn reject-btn" @tap="handleReject">
<text class="btn-icon">✗</text>
<text class="btn-text">驳回</text>
</view>
<!-- 中间分隔和信息 -->
<view class="action-center">
<view class="selected-info">
<view class="info-dot"></view>
<text class="info-text">已选 {{ selectedCount }} 项</text>
</view>
</view>
<!-- 右侧通过按钮 -->
<view class="action-btn approve-btn" @tap="handleApprove">
<text class="btn-icon">✓</text>
<text class="btn-text">通过</text>
</view>
</view>
<!-- 悬浮区域底部的装饰 -->
<view class="floating-bottom"></view>
</view>
</view>
</template>
<script lang="uts">
import { getApprovalList } from '@/api/approval/review'
interface ApprovalItem {
id : number
type : string
status : string
data : string
}
export default {
data() {
return {
approvalList: [] as ApprovalItem[],
selectedItems: [] as boolean[],
scrollBottom: '0'
}
},
computed: {
// 计算是否有选中的项目
hasSelected() : boolean {
return this.selectedItems.some(item => item)
},
// 计算选中数量
selectedCount() : number {
return this.selectedItems.filter(item => item).length
}
},
onShow() {
this.fetchApprovalList()
},
methods: {
async fetchApprovalList() {
try {
const data = await getApprovalList()
if (data && data.data) {
this.approvalList = data.data as ApprovalItem[]
// 初始化选择状态数组
this.selectedItems = new Array(this.approvalList.length).fill(false)
}
} catch (error) {
console.error('获取审批列表失败:', error)
}
},
getStatusText(status : string) : string {
const statusMap : Map<string, string> = new Map([
["0", "待审批"],
["1", "已通过"],
["2", "已拒绝"],
["11", "处理中"]
])
return statusMap.get(status.trim()) || status
},
toggleSelect(index : number) {
this.selectedItems[index] = !this.selectedItems[index]
// 使用 $set 确保响应式更新
this.$set(this.selectedItems, index, this.selectedItems[index])
// 如果有选中项目,自动滚动到底部
if (this.hasSelected) {
setTimeout(() => {
this.scrollBottom = '99999'
}, 100)
}
},
handleDetailClick(item : ApprovalItem) {
const params = {
id: item.id,
type: item.type,
status: item.status,
data: item.data
}
const paramsStr = encodeURIComponent(JSON.stringify(params))
uni.navigateTo({
url: `/pages/approval/type/info?params=${paramsStr}`,
success: () => {
console.log('跳转到审批详情成功', item.id)
},
fail: (err) => {
console.error('跳转到审批详情失败', err)
uni.showToast({
title: '跳转失败',
icon: 'none'
})
}
});
},
// 清空所有选择
clearSelection() {
this.selectedItems = new Array(this.selectedItems.length).fill(false)
},
onScroll(e : any) {
// 可以添加滚动逻辑
},
// 处理通过
handleApprove() {
const selectedIds = this.approvalList
.filter((_, index) => this.selectedItems[index])
.map(item => item.id)
if (selectedIds.length === 0) return
uni.showModal({
title: '确认通过',
content: `确定要通过选中的 ${selectedIds.length} 个审批项吗?`,
success: (res) => {
if (res.confirm) {
// 这里调用通过审批的API
console.log('通过审批项:', selectedIds)
uni.showToast({
title: '审批通过',
icon: 'success'
})
// 清空选中状态
this.clearSelection()
}
}
})
},
// 处理驳回
handleReject() {
const selectedIds = this.approvalList
.filter((_, index) => this.selectedItems[index])
.map(item => item.id)
if (selectedIds.length === 0) return
uni.showModal({
title: '确认驳回',
content: `确定要驳回选中的 ${selectedIds.length} 个审批项吗?`,
success: (res) => {
if (res.confirm) {
// 这里调用驳回审批的API
console.log('驳回审批项:', selectedIds)
uni.showToast({
title: '已驳回',
icon: 'none'
})
// 清空选中状态
this.clearSelection()
}
}
})
}
}
}
</script>
<style lang="scss">
.page-container {
display: flex;
flex-direction: column;
height: 89vh;
position: relative;
}
.work-container {
padding: 24rpx;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
flex: 1;
.card-list {
.card-item {
background: linear-gradient(135deg, #ffffff 0%, #fcfdff 100%);
border-radius: 28rpx;
padding: 36rpx 32rpx;
margin-bottom: 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow:
0 8rpx 32rpx rgba(0, 0, 0, 0.08),
0 3rpx 12rpx rgba(0, 0, 0, 0.04),
inset 0 1rpx 0 rgba(255, 255, 255, 0.8);
border: 1rpx solid rgba(229, 231, 235, 0.8);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
/* 左侧装饰条 */
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 6rpx;
height: 60%;
background: linear-gradient(to bottom, #1890ff, #36cfc9);
border-radius: 0 8rpx 8rpx 0;
}
&:active {
transform: translateY(-2rpx) scale(0.995);
box-shadow:
0 12rpx 40rpx rgba(0, 0, 0, 0.12),
0 4rpx 16rpx rgba(0, 0, 0, 0.06),
inset 0 1rpx 0 rgba(255, 255, 255, 0.8);
}
.card-content {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
gap: 32rpx;
padding-left: 16rpx;
.item-type {
font-size: 34rpx;
font-weight: 700;
color: #1e293b;
letter-spacing: 0.2rpx;
min-width: 140rpx;
position: relative;
padding-left: 4rpx;
/* 类型文字装饰 */
&::after {
content: '';
position: absolute;
left: 0;
bottom: -4rpx;
width: 40rpx;
height: 3rpx;
background: linear-gradient(to right, #1890ff, transparent);
border-radius: 2rpx;
}
}
.item-status {
font-size: 26rpx;
padding: 10rpx 24rpx;
border-radius: 32rpx;
font-weight: 600;
letter-spacing: 0.2rpx;
min-width: 96rpx;
text-align: center;
box-shadow:
0 2rpx 8rpx rgba(0, 0, 0, 0.08),
inset 0 1rpx 0 rgba(255, 255, 255, 0.4);
border: 1rpx solid transparent;
transition: all 0.2s ease;
&[data-status="0"] {
background: linear-gradient(135deg, #ffd666, #ffc53d);
color: #d46b08;
border-color: rgba(255, 197, 61, 0.3);
text-shadow: 0 1rpx 1rpx rgba(255, 255, 255, 0.5);
}
&[data-status="1"] {
background: linear-gradient(135deg, #b7eb8f, #95de64);
color: #389e0d;
border-color: rgba(149, 222, 100, 0.3);
text-shadow: 0 1rpx 1rpx rgba(255, 255, 255, 0.5);
}
&[data-status="2"] {
background: linear-gradient(135deg, #ffccc7, #ffa39e);
color: #cf1322;
border-color: rgba(255, 163, 158, 0.3);
text-shadow: 0 1rpx 1rpx rgba(255, 255, 255, 0.5);
}
&[data-status="11"] {
background: linear-gradient(135deg, #bae7ff, #91d5ff);
color: #096dd9;
border-color: rgba(145, 213, 255, 0.3);
text-shadow: 0 1rpx 1rpx rgba(255, 255, 255, 0.5);
}
}
.item-detail {
font-size: 28rpx;
color: #64748b;
font-weight: 500;
padding: 10rpx 24rpx;
background: linear-gradient(135deg, #f8fafc, #f1f5f9);
border-radius: 24rpx;
border: 1rpx solid rgba(226, 232, 240, 0.8);
transition: all 0.2s ease;
min-width: 80rpx;
text-align: center;
&:active {
background: linear-gradient(135deg, #e2e8f0, #cbd5e1);
color: #475569;
}
}
}
.checkbox-wrapper {
margin-left: 32rpx;
position: relative;
.custom-checkbox {
width: 44rpx;
height: 44rpx;
border-radius: 12rpx;
border: 2rpx solid #e2e8f0;
background: linear-gradient(135deg, #ffffff, #f8fafc);
position: relative;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow:
0 3rpx 10rpx rgba(0, 0, 0, 0.05),
inset 0 1rpx 0 rgba(255, 255, 255, 0.8);
/* 复选框内阴影 */
&::before {
content: '';
position: absolute;
top: 2rpx;
left: 2rpx;
right: 2rpx;
bottom: 2rpx;
border-radius: 8rpx;
background: linear-gradient(135deg, transparent, rgba(255, 255, 255, 0.4));
}
&.checked {
border-color: #1890ff;
background: linear-gradient(135deg, #1890ff, #36cfc9);
transform: rotate(90deg) scale(1.05);
box-shadow:
0 4rpx 15rpx rgba(24, 144, 255, 0.3),
0 2rpx 6rpx rgba(24, 144, 255, 0.2);
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20rpx;
height: 20rpx;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E") no-repeat center;
background-size: contain;
opacity: 0;
animation: checkAppear 0.2s ease 0.1s forwards;
}
}
}
}
/* 卡片悬停效果 */
&:hover {
transform: translateY(-3rpx);
box-shadow:
0 12rpx 40rpx rgba(0, 0, 0, 0.12),
0 4rpx 16rpx rgba(0, 0, 0, 0.08),
inset 0 1rpx 0 rgba(255, 255, 255, 0.8);
border-color: rgba(148, 163, 184, 0.6);
.item-detail {
transform: translateY(-1rpx);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
}
}
}
/* 列表底部占位符 */
.list-bottom-placeholder {
transition: height 0.3s ease;
}
}
}
/* 悬浮在tabbar上方的签核区域 */
.floating-action-bar {
position: fixed;
left: 24rpx;
right: 24rpx;
bottom: calc(100rpx + 24rpx);
/* 在tabbar上方24rpx */
z-index: 1000;
/* 高于tabbar */
transform: translateY(200rpx);
opacity: 0;
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
pointer-events: none;
&.show {
transform: translateY(0);
opacity: 1;
pointer-events: auto;
}
.floating-content {
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
border-radius: 24rpx;
padding: 70rpx 24rpx 30rpx 24rpx;
box-shadow:
0 8rpx 40rpx rgba(0, 0, 0, 0.15),
0 4rpx 20rpx rgba(0, 0, 0, 0.08),
0 2rpx 10rpx rgba(0, 0, 0, 0.04);
border: 1rpx solid rgba(229, 231, 235, 0.9);
display: flex;
flex-direction: row;
align-items: center;
position: relative;
overflow: hidden;
/* 顶部的装饰条 */
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4rpx;
background: linear-gradient(90deg, #ff6b6b, #1890ff, #1dd1a1);
border-radius: 24rpx 24rpx 0 0;
}
/* 关闭按钮 */
.close-btn {
position: absolute;
top: 12rpx;
right: 12rpx;
width: 36rpx;
height: 36rpx;
border-radius: 50%;
background: rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
cursor: pointer;
z-index: 10;
&:active {
background: rgba(0, 0, 0, 0.1);
transform: scale(0.9);
}
.close-icon {
font-size: 28rpx;
font-weight: 300;
color: #64748b;
line-height: 1;
}
}
/* 按钮样式 */
.action-btn {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 24rpx 20rpx;
border-radius: 20rpx;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
min-width: 0;
position: relative;
z-index: 1;
&:active {
transform: translateY(4rpx) scale(0.98);
}
.btn-icon {
font-size: 44rpx;
font-weight: 300;
margin-bottom: 8rpx;
line-height: 1;
transition: transform 0.2s ease;
}
.btn-text {
font-size: 32rpx;
font-weight: 700;
letter-spacing: 1rpx;
}
/* 驳回按钮样式 */
&.reject-btn {
background: linear-gradient(135deg, #ff6b6b, #ff4757);
color: white;
box-shadow: 0 4rpx 16rpx rgba(255, 107, 107, 0.3);
&:active {
background: linear-gradient(135deg, #ff4757, #ff3838);
box-shadow: 0 2rpx 8rpx rgba(255, 107, 107, 0.4);
.btn-icon {
transform: scale(0.9);
}
}
}
/* 通过按钮样式 */
&.approve-btn {
background: linear-gradient(135deg, #1dd1a1, #10ac84);
color: white;
box-shadow: 0 4rpx 16rpx rgba(29, 209, 161, 0.3);
&:active {
background: linear-gradient(135deg, #10ac84, #0a8f6d);
box-shadow: 0 2rpx 8rpx rgba(29, 209, 161, 0.4);
.btn-icon {
transform: scale(0.9);
}
}
}
}
/* 中间信息区域 */
.action-center {
flex: 0 0 auto;
padding: 0 24rpx;
min-width: 180rpx;
.selected-info {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.info-dot {
width: 16rpx;
height: 16rpx;
background: linear-gradient(135deg, #1890ff, #36cfc9);
border-radius: 50%;
margin-bottom: 10rpx;
position: relative;
&::after {
content: '';
position: absolute;
top: -4rpx;
left: -4rpx;
right: -4rpx;
bottom: -4rpx;
border-radius: 50%;
border: 2rpx solid rgba(24, 144, 255, 0.3);
animation: ripple 2s infinite;
}
}
.info-text {
font-size: 26rpx;
font-weight: 600;
color: #1e293b;
text-align: center;
line-height: 1.2;
}
}
}
}
/* 底部装饰 */
.floating-bottom {
height: 8rpx;
background: linear-gradient(135deg, #ffffff, #f8fafc);
border-radius: 0 0 24rpx 24rpx;
margin: 0 24rpx;
opacity: 0.8;
filter: blur(4rpx);
transform: translateY(-2rpx);
}
}
/* 动画 */
@keyframes checkAppear {
from {
opacity: 0;
transform: translate(-50%, -50%) scale(0.8);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
@keyframes ripple {
0% {
transform: scale(1);
opacity: 1;
}
100% {
transform: scale(1.8);
opacity: 0;
}
}
/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
.work-container {
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
.card-item {
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
border-color: rgba(71, 85, 105, 0.6);
box-shadow:
0 8rpx 32rpx rgba(0, 0, 0, 0.3),
0 3rpx 12rpx rgba(0, 0, 0, 0.2),
inset 0 1rpx 0 rgba(255, 255, 255, 0.05);
&::before {
background: linear-gradient(to bottom, #36cfc9, #0891b2);
}
.card-content {
.item-type {
color: #f1f5f9;
&::after {
background: linear-gradient(to right, #36cfc9, transparent);
}
}
.item-status {
box-shadow:
0 2rpx 8rpx rgba(0, 0, 0, 0.3),
inset 0 1rpx 0 rgba(255, 255, 255, 0.1);
text-shadow: 0 1rpx 1rpx rgba(0, 0, 0, 0.3);
&[data-status="0"] {
background: linear-gradient(135deg, #ffa940, #fa8c16);
color: #fff2e8;
border-color: rgba(255, 169, 64, 0.4);
}
&[data-status="1"] {
background: linear-gradient(135deg, #73d13d, #52c41a);
color: #f6ffed;
border-color: rgba(115, 209, 61, 0.4);
}
&[data-status="2"] {
background: linear-gradient(135deg, #ff7875, #ff4d4f);
color: #fff2f0;
border-color: rgba(255, 120, 117, 0.4);
}
&[data-status="11"] {
background: linear-gradient(135deg, #69c0ff, #1890ff);
color: #e6f7ff;
border-color: rgba(105, 192, 255, 0.4);
}
}
.item-detail {
color: #cbd5e1;
background: linear-gradient(135deg, #334155, #475569);
border-color: rgba(71, 85, 105, 0.8);
&:active {
background: linear-gradient(135deg, #475569, #64748b);
color: #e2e8f0;
}
}
}
.checkbox-wrapper {
.custom-checkbox {
border-color: #475569;
background: linear-gradient(135deg, #334155, #1e293b);
box-shadow:
0 3rpx 10rpx rgba(0, 0, 0, 0.3),
inset 0 1rpx 0 rgba(255, 255, 255, 0.05);
&::before {
background: linear-gradient(135deg, transparent, rgba(255, 255, 255, 0.1));
}
&.checked {
border-color: #36cfc9;
background: linear-gradient(135deg, #36cfc9, #0891b2);
box-shadow:
0 4rpx 15rpx rgba(54, 207, 201, 0.3),
0 2rpx 6rpx rgba(54, 207, 201, 0.2);
}
}
}
&:hover {
border-color: rgba(100, 116, 139, 0.8);
box-shadow:
0 12rpx 40rpx rgba(0, 0, 0, 0.4),
0 4rpx 16rpx rgba(0, 0, 0, 0.3),
inset 0 1rpx 0 rgba(255, 255, 255, 0.05);
}
}
}
.floating-action-bar {
.floating-content {
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
border-color: rgba(71, 85, 105, 0.6);
box-shadow:
0 8rpx 40rpx rgba(0, 0, 0, 0.4),
0 4rpx 20rpx rgba(0, 0, 0, 0.3),
0 2rpx 10rpx rgba(0, 0, 0, 0.2);
&::before {
background: linear-gradient(90deg, #ff6b6b, #36cfc9, #1dd1a1);
}
.close-btn {
background: rgba(255, 255, 255, 0.1);
&:active {
background: rgba(255, 255, 255, 0.2);
}
.close-icon {
color: #cbd5e1;
}
}
.action-btn {
&.reject-btn {
background: linear-gradient(135deg, #ff6b6b, #ff3838);
box-shadow: 0 4rpx 16rpx rgba(255, 107, 107, 0.4);
&:active {
background: linear-gradient(135deg, #ff3838, #d63031);
box-shadow: 0 2rpx 8rpx rgba(255, 107, 107, 0.5);
}
}
&.approve-btn {
background: linear-gradient(135deg, #1dd1a1, #0a8f6d);
box-shadow: 0 4rpx 16rpx rgba(29, 209, 161, 0.4);
&:active {
background: linear-gradient(135deg, #0a8f6d, #076552);
box-shadow: 0 2rpx 8rpx rgba(29, 209, 161, 0.5);
}
}
}
.action-center {
.selected-info {
.info-dot {
background: linear-gradient(135deg, #36cfc9, #0891b2);
&::after {
border-color: rgba(54, 207, 201, 0.4);
}
}
.info-text {
color: #f1f5f9;
}
}
}
}
.floating-bottom {
background: linear-gradient(135deg, #1e293b, #0f172a);
}
}
}
</style>
这段代码是uniapp x项目中的uvue页面内容,其实就是移动端app的一个页面,当前有一个小问题就是approvalList中数据不只有几条,而当前页面最多只能显示9条,以至于超过9条的部分页面无法展示,分析一下页面结构,能不能以滚动条的形式看到其他数据,注意整个页面的头部是存在一个head的,底部有一个可以切换页面的tabbar组件
最新发布