Commit 12ec4980 by liangjianmin

feat(approve-process): 添加审批进度组件并优化审批页面展示

- 新增审批进度组件,整合审批节点展示逻辑
- 移除原有审批进度相关代码,提升代码可维护性
- 更新审批页面,使用新组件替代旧的审批进度展示
parent d77abe86
......@@ -170,231 +170,6 @@
word-break: break-all;
}
.process-scroll {
width: 100%;
padding: 4rpx 0 22rpx;
box-sizing: border-box;
white-space: nowrap;
}
.process-list {
display: inline-flex;
min-width: 100%;
padding: 22rpx 18rpx 8rpx 36rpx;
box-sizing: border-box;
}
.process-item {
position: relative;
display: inline-flex;
flex-direction: column;
width: 280rpx;
white-space: normal;
& + .process-item {
margin-left: 20rpx;
}
&.current {
.process-node {
background: #2f6bff;
border-color: #dbeafe;
box-shadow: 0 0 0 8rpx rgba(47, 107, 255, 0.13), 0 12rpx 24rpx rgba(47, 107, 255, 0.24);
}
.process-card {
background: linear-gradient(160deg, #f7fbff 0, #eaf2ff 100%);
border-color: rgba(47, 107, 255, 0.45);
box-shadow: 0 16rpx 34rpx rgba(47, 107, 255, 0.14);
}
.process-card::before {
opacity: 1;
}
.process-name {
color: #174ed9;
}
.process-status {
background: #e0ecff;
color: #1e5bff;
}
}
&.finished {
.process-node {
background: #21b96b;
border-color: #d7f8e6;
box-shadow: 0 8rpx 18rpx rgba(33, 185, 107, 0.2);
}
.process-line {
background: linear-gradient(90deg, #35c777 0, #45a3ff 100%);
}
.process-status {
background: #e7f8ee;
color: #16a05a;
}
}
&.rejected {
.process-node {
background: #ef4444;
border-color: #fee2e2;
box-shadow: 0 8rpx 18rpx rgba(239, 68, 68, 0.2);
}
.process-line {
background: linear-gradient(90deg, #35c777 0, #ef4444 100%);
}
.process-card {
background: linear-gradient(160deg, #ffffff 0, #fff5f5 100%);
border-color: rgba(239, 68, 68, 0.32);
}
.process-name {
color: #b91c1c;
}
.process-status {
background: #fee2e2;
color: #dc2626;
}
}
}
.process-line {
position: absolute;
left: -281rpx;
top: 17rpx;
width: 300rpx;
height: 6rpx;
border-radius: 999rpx;
background: #e2e8f0;
}
.process-node {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
width: 38rpx;
height: 38rpx;
margin-left: 0;
border: 8rpx solid #edf2f7;
border-radius: 50%;
background: #aebdce;
color: #ffffff;
box-sizing: border-box;
box-shadow: 0 8rpx 18rpx rgba(100, 116, 139, 0.18);
}
.process-index,
.process-check {
font-size: 19rpx;
font-weight: 700;
line-height: 1;
}
.process-card {
position: relative;
min-height: 196rpx;
margin-top: 16rpx;
padding: 16rpx;
border: 1rpx solid rgba(226, 232, 240, 0.88);
border-radius: 16rpx;
background: linear-gradient(160deg, #ffffff 0, #f6f9ff 100%);
box-sizing: border-box;
overflow: hidden;
box-shadow: 0 10rpx 26rpx rgba(15, 23, 42, 0.045);
}
.process-card::before {
content: '';
position: absolute;
right: -24rpx;
top: -28rpx;
width: 92rpx;
height: 92rpx;
border-radius: 50%;
background: radial-gradient(circle, rgba(47, 107, 255, 0.22) 0, rgba(47, 107, 255, 0) 68%);
opacity: 0;
}
.process-head {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
min-height: 86rpx;
}
.process-name {
max-width: 100%;
font-size: 25rpx;
font-weight: 700;
color: #18263c;
line-height: 32rpx;
word-break: break-all;
}
.process-status {
display: inline-flex;
align-items: center;
margin-top: 10rpx;
padding: 5rpx 12rpx;
border-radius: 999rpx;
background: #eef2f7;
font-size: 22rpx;
color: #64748b;
line-height: 26rpx;
text-align: center;
white-space: nowrap;
}
.process-user-row {
position: relative;
z-index: 1;
display: flex;
align-items: flex-start;
margin-top: 10rpx;
}
.process-user-label {
margin-right: 10rpx;
padding: 2rpx 8rpx;
border-radius: 6rpx;
background: rgba(148, 163, 184, 0.14);
font-size: 20rpx;
color: #64748b;
line-height: 28rpx;
flex-shrink: 0;
}
.process-user {
font-size: 23rpx;
font-weight: 500;
color: #334155;
line-height: 32rpx;
word-break: break-all;
}
.process-time {
position: relative;
z-index: 1;
display: block;
margin-top: 8rpx;
font-size: 23rpx;
color: #7b8ca5;
line-height: 30rpx;
word-break: break-all;
}
.timeline-wrap {
padding: 8rpx 20rpx 20rpx;
}
......
<template>
<view class="approve-process-section" v-if="items.length > 0">
<view class="section-title">{{ title }}</view>
<scroll-view class="process-scroll" scroll-x :show-scrollbar="false">
<view class="process-list">
<view :class="['process-item', getProcessClass(item, index)]" v-for="(item, index) in items" :key="index">
<view class="process-line" v-if="index > 0"></view>
<view class="process-node">
<text class="process-index" v-if="!isFinishedProcess(item)">{{ index + 1 }}</text>
<text class="process-check" v-else></text>
</view>
<view class="process-card">
<view class="process-head">
<text class="process-name">{{ item.node_name || '-' }}</text>
<text class="process-status">{{ item.approval_status_text || item.approve_status_cn || '-' }}</text>
</view>
<view class="process-user-row">
<text class="process-user-label">审批人</text>
<text class="process-user">{{ item.approval_names || item.auditor_name || '-' }}</text>
</view>
<text class="process-time">{{ item.approval_time || item.audit_time || '等待处理' }}</text>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
export default {
name: 'ApproveProcess',
props: {
title: {
type: String,
default: '审批进度'
},
items: {
type: Array,
default: () => []
}
},
methods: {
/**
* 获取审批状态文本
* @param {Object} item - 审批节点
* @return {string} 状态文本
*/
getStatusText(item) {
return item.approval_status_text || item.approve_status_cn || '';
},
/**
* 是否已通过节点
* @param {Object} item - 审批节点
* @return {boolean} 判断结果
*/
isFinishedProcess(item) {
return this.getStatusText(item) === '审核通过';
},
/**
* 是否驳回节点
* @param {Object} item - 审批节点
* @return {boolean} 判断结果
*/
isRejectedProcess(item) {
return this.getStatusText(item) === '审核拒绝';
},
/**
* 获取审批节点样式
* @param {Object} item - 审批节点
* @param {number} index - 节点索引
* @return {string} 样式名称
*/
getProcessClass(item, index) {
if (this.isFinishedProcess(item)) return 'finished';
if (this.isRejectedProcess(item)) return 'rejected';
if (Number(item.is_current) === 1) return 'current';
return index === 0 ? 'pending first' : 'pending';
}
}
};
</script>
<style lang="scss">
.approve-process-section {
margin-bottom: 12rpx;
background: rgba(255, 255, 255, 0.98);
border-radius: 18rpx;
box-shadow: 0 8rpx 22rpx rgba(15, 23, 42, 0.035);
overflow: hidden;
.section-title {
display: flex;
align-items: center;
padding: 20rpx 20rpx 6rpx;
font-size: 28rpx;
font-weight: 700;
color: #1e293b;
letter-spacing: 0.5rpx;
&::before {
content: '';
width: 6rpx;
height: 24rpx;
margin-right: 14rpx;
border-radius: 999rpx;
background: linear-gradient(180deg, #5a7fff 0, #3467ff 100%);
}
}
.process-scroll {
width: 100%;
padding: 4rpx 0 22rpx;
box-sizing: border-box;
white-space: nowrap;
}
.process-list {
display: inline-flex;
min-width: 100%;
padding: 22rpx 18rpx 8rpx 36rpx;
box-sizing: border-box;
}
.process-item {
position: relative;
display: inline-flex;
flex-direction: column;
width: 280rpx;
white-space: normal;
& + .process-item {
margin-left: 20rpx;
}
&.current {
.process-node {
background: #2f6bff;
border-color: #dbeafe;
box-shadow: 0 0 0 8rpx rgba(47, 107, 255, 0.13), 0 12rpx 24rpx rgba(47, 107, 255, 0.24);
}
.process-card {
background: linear-gradient(160deg, #f7fbff 0, #eaf2ff 100%);
border-color: rgba(47, 107, 255, 0.45);
box-shadow: 0 16rpx 34rpx rgba(47, 107, 255, 0.14);
}
.process-card::before {
opacity: 1;
}
.process-name {
color: #174ed9;
}
.process-status {
background: #e0ecff;
color: #1e5bff;
}
}
&.finished {
.process-node {
background: #21b96b;
border-color: #d7f8e6;
box-shadow: 0 8rpx 18rpx rgba(33, 185, 107, 0.2);
}
.process-line {
background: linear-gradient(90deg, #35c777 0, #45a3ff 100%);
}
.process-status {
background: #e7f8ee;
color: #16a05a;
}
}
&.rejected {
.process-node {
background: #ef4444;
border-color: #fee2e2;
box-shadow: 0 8rpx 18rpx rgba(239, 68, 68, 0.2);
}
.process-line {
background: linear-gradient(90deg, #35c777 0, #ef4444 100%);
}
.process-card {
background: linear-gradient(160deg, #ffffff 0, #fff5f5 100%);
border-color: rgba(239, 68, 68, 0.32);
}
.process-name {
color: #b91c1c;
}
.process-status {
background: #fee2e2;
color: #dc2626;
}
}
}
.process-line {
position: absolute;
left: -281rpx;
top: 17rpx;
width: 300rpx;
height: 6rpx;
border-radius: 999rpx;
background: #e2e8f0;
}
.process-node {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
width: 38rpx;
height: 38rpx;
margin-left: 0;
border: 8rpx solid #edf2f7;
border-radius: 50%;
background: #aebdce;
color: #ffffff;
box-sizing: border-box;
box-shadow: 0 8rpx 18rpx rgba(100, 116, 139, 0.18);
}
.process-index,
.process-check {
font-size: 19rpx;
font-weight: 700;
line-height: 1;
}
.process-card {
position: relative;
min-height: 196rpx;
margin-top: 16rpx;
padding: 16rpx;
border: 1rpx solid rgba(226, 232, 240, 0.88);
border-radius: 16rpx;
background: linear-gradient(160deg, #ffffff 0, #f6f9ff 100%);
box-sizing: border-box;
overflow: hidden;
box-shadow: 0 10rpx 26rpx rgba(15, 23, 42, 0.045);
}
.process-card::before {
content: '';
position: absolute;
right: -24rpx;
top: -28rpx;
width: 92rpx;
height: 92rpx;
border-radius: 50%;
background: radial-gradient(circle, rgba(47, 107, 255, 0.22) 0, rgba(47, 107, 255, 0) 68%);
opacity: 0;
}
.process-head {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
min-height: 86rpx;
}
.process-name {
max-width: 100%;
font-size: 25rpx;
font-weight: 700;
color: #18263c;
line-height: 32rpx;
word-break: break-all;
}
.process-status {
display: inline-flex;
align-items: center;
margin-top: 10rpx;
padding: 5rpx 12rpx;
border-radius: 999rpx;
background: #eef2f7;
font-size: 22rpx;
color: #64748b;
line-height: 26rpx;
text-align: center;
white-space: nowrap;
}
.process-user-row {
position: relative;
z-index: 1;
display: flex;
align-items: flex-start;
margin-top: 10rpx;
}
.process-user-label {
margin-right: 10rpx;
padding: 2rpx 8rpx;
border-radius: 6rpx;
background: rgba(148, 163, 184, 0.14);
font-size: 20rpx;
color: #64748b;
line-height: 28rpx;
flex-shrink: 0;
}
.process-user {
font-size: 23rpx;
font-weight: 500;
color: #334155;
line-height: 32rpx;
word-break: break-all;
}
.process-time {
position: relative;
z-index: 1;
display: block;
margin-top: 8rpx;
font-size: 23rpx;
color: #7b8ca5;
line-height: 30rpx;
word-break: break-all;
}
}
</style>
......@@ -79,31 +79,7 @@
</view>
</view>
<view class="section" v-if="approveProcess.length > 0">
<view class="section-title">审批进度</view>
<scroll-view class="process-scroll" scroll-x :show-scrollbar="false">
<view class="process-list">
<view :class="['process-item', getProcessClass(item, index)]" v-for="(item, index) in approveProcess" :key="index">
<view class="process-line" v-if="index > 0"></view>
<view class="process-node">
<text class="process-index" v-if="!isFinishedProcess(item)">{{ index + 1 }}</text>
<text class="process-check" v-else></text>
</view>
<view class="process-card">
<view class="process-head">
<text class="process-name">{{ item.node_name }}</text>
<text class="process-status">{{ item.approval_status_text }}</text>
</view>
<view class="process-user-row">
<text class="process-user-label">审批人</text>
<text class="process-user">{{ item.approval_names }}</text>
</view>
<text class="process-time">{{ item.approval_time || '等待处理' }}</text>
</view>
</view>
</view>
</scroll-view>
</view>
<approve-process :items="approveProcess"></approve-process>
<view class="section" v-if="detail.items && detail.items.length > 0">
<view class="section-title">审批日志</view>
......@@ -156,8 +132,12 @@
<script>
import { API } from '@/util/api';
import ApproveProcess from '@/components/approve-process/approve-process.vue';
export default {
components: {
ApproveProcess
},
data() {
return {
id: '',
......@@ -268,35 +248,6 @@
});
},
/**
* 是否已处理节点
* @param {Object} item - 审批节点
* @return {boolean} 判断结果
*/
isFinishedProcess(item) {
return item.approval_status_text === '审核通过';
},
/**
* 是否驳回节点
* @param {Object} item - 审批节点
* @return {boolean} 判断结果
*/
isRejectedProcess(item) {
return item.approval_status_text === '审核拒绝';
},
/**
* 获取审批节点样式
* @param {Object} item - 审批节点
* @param {number} index - 节点索引
* @return {string} 样式名称
*/
getProcessClass(item, index) {
if (this.isFinishedProcess(item)) return 'finished';
if (this.isRejectedProcess(item)) return 'rejected';
var currentClass = Number(item.is_current) === 1 ? ' current' : '';
if (currentClass) return 'current';
return index === 0 ? 'pending first' : 'pending';
},
/**
* 返回上一页
* @return {void}
*/
......
......@@ -175,6 +175,8 @@
</view>
</view>
<approve-process :items="approveProcess"></approve-process>
<view class="section" v-if="approveLogs.length">
<view class="section-title">审批日志</view>
<view class="timeline-wrap">
......@@ -302,16 +304,22 @@
<script>
import { API } from '@/util/api';
import ApproveProcess from '@/components/approve-process/approve-process.vue';
export default {
components: {
ApproveProcess
},
data() {
return {
order_id: '',
approve_id: '',
bill_id: '',
detail: {},
goodsList: [],
invoiceFiles: [],
approveMeta: {},
approveProcess: [],
loading: false,
showApproveModal: false,
modalStatus: 1,
......@@ -395,10 +403,12 @@
},
onLoad(options) {
this.order_id = options.order_id || options.orderId || options.source_id || options.sourceId || '';
this.approve_id = options.approve_id || options.approveId || '';
this.bill_id = options.bill_id || options.billId || '';
},
onShow() {
this.getData();
this.getApproveProcess();
},
methods: {
/**
......@@ -448,6 +458,20 @@
});
},
/**
* 获取审批进度
* @return {void}
*/
getApproveProcess() {
if (!this.approve_id) return;
this.request(API.getApproveProcess, 'GET', { approve_id: this.approve_id }).then(res => {
if (res.code === 0) {
this.approveProcess = res.data || [];
}
}).catch(() => {
this.approveProcess = [];
});
},
/**
* 从列表响应中取当前审批单
* @param {Object} res - 列表响应
* @return {Object} 审批单
......
......@@ -361,7 +361,7 @@
return;
}
uni.navigateTo({
url: `/pages/classify-price/detail?order_id=${item.order_id}&bill_id=${item.bill_id || ''}`
url: `/pages/classify-price/detail?order_id=${item.order_id}&bill_id=${item.bill_id || ''}&approve_id=${item.approve_id || ''}`
});
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment