uni-app 审批流程组件封装:打造企业级工作流可视化方案

uni-app 审批流程组件封装:打造企业级工作流可视化方案

摘要:本文详细介绍了一个功能强大的审批流程组件的封装实践。从流程可视化、状态管理、交互设计到多端适配,全面展示了如何在 uni-app 中构建高质量的企业级审批流组件。文章包含完整的代码实现、样式优化和最佳实践,适合需要开发 OA、ERP 等系统的开发者参考。

关键词:uni-app;审批流程;工作流;Vue3;组件封装;流程可视化


一、引言

在企业级应用中,审批流程是一个核心功能模块。无论是请假申请、费用报销,还是项目审批,都需要一个直观、易用的审批流程展示界面。然而,开发一个优秀的审批流程组件面临诸多挑战:

  • 🎨 复杂的 UI 设计:时间线、状态图标、连接线、动画效果
  • 🔄 多样的状态:待审批、审批中、已通过、已拒绝、已取消
  • 📱 多端一致性:H5、小程序、APP 端的渲染差异
  • 性能要求:长流程的流畅滚动、复杂 DOM 的渲染优化
  • 🎯 业务适配:多级审批、并行审批、条件分支、退回委派

本文将分享一个经过生产环境验证的审批流程组件封装方案。


二、组件需求分析

2.1 核心功能清单

typescript 复制代码
interface ApprovalProcessProps {
    processInstanceId: string; // 流程实例 ID
    types: string; // 业务类型

    // 事件
    onApprove: Function; // 审批通过回调
    onReject: Function; // 审批拒绝回调
    onValidateBeforeApprove: Function; // 审批前校验
}

interface ApprovalNode {
    id: string; // 节点 ID
    name: string; // 节点名称
    nodeType: NodeType; // 节点类型
    status: TaskStatus; // 节点状态
    tasks: Task[]; // 任务列表
    candidateUsers: User[]; // 候选用户
    endTime?: string; // 结束时间
}

2.2 支持的节点类型

typescript 复制代码
enum NodeType {
    START_USER_NODE = 'start_user', // 发起人节点
    USER_TASK_NODE = 'user_task', // 审批人节点
    TRANSACTOR_NODE = 'transactor', // 办理人节点
    COPY_TASK_NODE = 'copy_task', // 抄送人节点
    CONDITION_NODE = 'condition', // 条件分支节点
    PARALLEL_BRANCH_NODE = 'parallel', // 并行分支节点
    CHILD_PROCESS_NODE = 'child_process', // 子流程节点
    END_EVENT_NODE = 'end_event', // 结束节点
}

2.3 支持的任务状态

typescript 复制代码
enum TaskStatusEnum {
    NOT_STARTED = -1, // 未开始
    PENDING = 0, // 待审批
    RUNNING = 1, // 审批中
    APPROVE = 2, // 审批通过
    REJECT = 3, // 审批不通过
    CANCEL = 4, // 取消
    RETURN = 5, // 退回
    DELEGATE = 6, // 委派中
    APPROVING = 7, // 审批通过中
}

三、组件架构设计

3.1 模板结构

vue 复制代码
<template>
    <view class="approval-process">
        <!-- 审批流程卡片 -->
        <view v-if="approvalProcess.length > 0" class="detail-card">
            <!-- 卡片头部 -->
            <view class="card-header">
                <view class="card-title">审批流程</view>
                <u-image :src="auditIconsMap[taskStatus]" mode="aspectFill" />
            </view>

            <!-- 流程时间线 -->
            <view class="process-container">
                <view v-for="(node, index) in approvalProcess" :key="node.id" :data-status="node.status" class="process-item">
                    <!-- 连接线 -->
                    <view v-if="index < approvalProcess.length - 1" class="process-line"></view>

                    <!-- 节点内容 -->
                    <view class="process-content">
                        <!-- 节点标题 -->
                        <view class="process-title">
                            <view class="flex items-center">
                                <!-- 节点图标 -->
                                <view class="process-icon">
                                    <u-image :src="getApprovalNodeImg(node.nodeType)" />
                                    <!-- 状态徽章 -->
                                    <view :style="{ backgroundColor: statusIconMap[node.status]?.color }" class="process-icon-status">
                                        <u-icon :name="statusIconMap[node.status]?.icon" />
                                    </view>
                                </view>
                                <view>{{ node.name }}</view>
                            </view>
                            <view class="process-info">
                                <text v-if="node.endTime" class="process-time">{{ node.endTime }}</text>
                            </view>
                        </view>

                        <!-- 审批人列表 -->
                        <view class="process-detail">
                            <view v-for="item in node.tasks || node.candidateUsers" :key="item.id" class="process-assignee">
                                <view class="assignee-item">
                                    <u-avatar :src="fileCdn(item.assigneeUser.avatar)" mode="circle" />
                                    <text>{{ item.assigneeUser.nickname }}</text>
                                </view>
                            </view>
                        </view>

                        <!-- 审批意见 -->
                        <view v-for="item in node.tasks" :key="item.id" class="process-comment-container">
                            <view v-if="item.reason" class="process-comment"> 审批意见:{{ item.reason }} </view>
                        </view>
                    </view>
                </view>
            </view>
        </view>

        <!-- 底部操作栏 -->
        <view v-if="showActionBar" class="bottom-action-bar">
            <u-button type="primary" @click="handleApprove"> <u-icon name="checkmark"></u-icon> 通过 </u-button>
            <u-button type="error" @click="handleReject"> <u-icon name="close"></u-icon> 拒绝 </u-button>
        </view>

        <!-- 审批弹窗 -->
        <u-modal v-model="showRejectModal" title="拒绝原因" @confirm="handleConfirmReject">
            <u-input v-model="rejectReason" type="textarea" placeholder="请输入拒绝原因" />
        </u-modal>

        <u-modal v-model="showApproveModal" title="审批通过意见" @confirm="handleConfirmApprove">
            <u-input v-model="approveComment" type="textarea" placeholder="请输入审批意见" />
        </u-modal>
    </view>
</template>

3.2 数据流转设计

typescript 复制代码
// 组件接收的数据流
props: {
  process_instance_id: String,  // 流程实例 ID
  types: String                 // 业务类型(travel/expense 等)
}

// 内部状态
const approvalProcess = reactive([]);  // 审批流程数据
const runningTask = ref();             // 运行中的任务
const showActionBar = computed(() => { // 是否显示操作栏
  return props.types === 'travel' &&
         runningTask.value &&
         isHandleTaskStatus() &&
         isShowButton(OperationButtonType.RETURN);
});

// 暴露给父组件的方法
defineExpose({
  showActionBar
});

四、核心功能实现

4.1 获取审批详情

typescript 复制代码
const getApprovalDetail = async (processInstanceId: string) => {
    try {
        const { data } = await ProcessInstanceApi.getApprovalDetail({
            processInstanceId,
        });

        if (data && data.activityNodes.length > 0) {
            Object.assign(approvalProcess, data.activityNodes);
        }

        runningTask.value = data.todoTask;
    } catch (error) {
        console.error('获取审批详情失败:', error);
        uToast1.value.show({
            title: '获取审批详情失败',
            type: 'error',
        });
    }
};

// 监听 props 变化,自动获取详情
watch(
    () => props.process_instance_id,
    (newVal) => {
        if (newVal) {
            getApprovalDetail(newVal);
        }
    },
    { immediate: true }
);

4.2 节点图标映射

typescript 复制代码
// SVG 图标导入
import starterSvg from '@/static/svgs/bpm/starter.svg';
import auditorSvg from '@/static/svgs/bpm/auditor.svg';
import copySvg from '@/static/svgs/bpm/copy.svg';
import conditionSvg from '@/static/svgs/bpm/condition.svg';
import parallelSvg from '@/static/svgs/bpm/parallel.svg';
import finishSvg from '@/static/svgs/bpm/finish.svg';
import transactorSvg from '@/static/svgs/bpm/transactor.svg';
import childProcessSvg from '@/static/svgs/bpm/child-process.svg';

// 节点类型与图标映射
const nodeTypeSvgMap = {
    [NodeType.END_EVENT_NODE]: { color: '#909398', svg: finishSvg },
    [NodeType.START_USER_NODE]: { color: '#909398', svg: starterSvg },
    [NodeType.USER_TASK_NODE]: { color: '#ff943e', svg: auditorSvg },
    [NodeType.TRANSACTOR_NODE]: { color: '#ff943e', svg: transactorSvg },
    [NodeType.COPY_TASK_NODE]: { color: '#3296fb', svg: copySvg },
    [NodeType.CONDITION_NODE]: { color: '#14bb83', svg: conditionSvg },
    [NodeType.PARALLEL_BRANCH_NODE]: { color: '#14bb83', svg: parallelSvg },
    [NodeType.CHILD_PROCESS_NODE]: { color: '#14bb83', svg: childProcessSvg },
};

const getApprovalNodeImg = (nodeType: NodeType) => {
    return nodeTypeSvgMap[nodeType as keyof typeof nodeTypeSvgMap]?.svg || finishSvg;
};

4.3 状态图标映射

typescript 复制代码
const statusIconMap: StatusIconMap = {
    '-1': { color: '#909398', icon: 'clock' }, // 未开始
    '0': { color: '#00b32a', icon: 'reload' }, // 待审批
    '1': { color: '#448ef7', icon: 'reload' }, // 审批中
    '2': { color: '#00b32a', icon: 'checkmark' }, // 审批通过
    '3': { color: '#f46b6c', icon: 'Close' }, // 审批不通过
    '4': { color: '#cccccc', icon: 'trash' }, // 取消
    '5': { color: '#f46b6c', icon: 'minus' }, // 退回
    '6': { color: '#448ef7', icon: 'reload' }, // 委派中
    '7': { color: '#00b32a', icon: 'checkmark' }, // 审批通过中
};

4.4 审批通过逻辑

typescript 复制代码
const handleApprove = async () => {
    try {
        // 触发父组件的校验事件
        await new Promise((resolve, reject) => {
            emit('validate-before-approve', { resolve, reject });
        });

        console.log('校验通过');

        // 获取下一个审批节点
        const data = await ProcessInstanceApi.getNextApprovalNodes({
            processInstanceId: props.process_instance_id,
            taskId: runningTask.value.id,
            processVariablesStr: {},
        });

        // 显示审批意见弹窗(可选)
        // showApproveModal.value = true;
    } catch (error) {
        console.log('校验未通过');
        return;
    }
};

const handleConfirmApprove = async () => {
    try {
        const data = {
            id: runningTask.value.id,
            reason: '通过',
            variables: {},
            nextAssignees: {},
        };

        await TaskApi.approveTask(data);

        uToast1.value.show({
            title: '审批已通过',
            type: 'success',
            duration: 3000,
        });

        approveComment.value = '';
        getApprovalDetail(props.process_instance_id);
        emit('approve');
    } catch (error) {
        console.error('审批通过失败:', error);
    }
};

4.5 审批拒绝逻辑

typescript 复制代码
const handleReject = () => {
    showRejectModal.value = true;
};

const handleConfirmReject = async () => {
    if (!rejectReason.value.trim()) {
        uToast1.value.show({
            title: '请输入拒绝原因',
            type: 'warning',
        });
        showRejectModal.value = true;
        return;
    }

    try {
        const data = {
            id: runningTask.value.id,
            reason: rejectReason.value,
        };

        await TaskApi.rejectTask(data);

        uToast1.value.show({
            title: '审批已拒绝',
            type: 'success',
            duration: 3000,
        });

        showRejectModal.value = false;
        rejectReason.value = '';
        getApprovalDetail(props.process_instance_id);
        emit('reject');
    } catch (error) {
        console.error('审批拒绝失败:', error);
    }
};

4.6 按钮权限控制

typescript 复制代码
/** 任务是否为处理中状态 */
const isHandleTaskStatus = () => {
    let canHandle = false;
    if (TaskApi.TaskStatusEnum.RUNNING === runningTask.value?.status) {
        canHandle = true;
    }
    return canHandle;
};

/** 是否显示按钮 */
const isShowButton = (btnType: OperationButtonType): boolean => {
    let isShow = true;
    if (runningTask.value?.buttonsSetting && runningTask.value?.buttonsSetting[btnType]) {
        isShow = runningTask.value.buttonsSetting[btnType].enable;
    }
    return isShow;
};

/** 计算是否显示操作栏 */
const showActionBar = computed(() => {
    return props.types === 'travel' && runningTask.value && isHandleTaskStatus() && isShowButton(OperationButtonType.RETURN);
});

五、样式设计与优化

5.1 卡片容器样式

scss 复制代码
@import '../../styles/_variables.scss';

.detail-card {
    margin: $spacing-xl;
    padding: $spacing-2xl;

    background: linear-gradient(135deg, $color-white 0%, $color-gray-50 100%);
    border-radius: $radius-xl;
    box-shadow: $shadow-sm;

    &:hover {
        box-shadow: $shadow-md;
        transform: translateY(-$spacing-2xs);
    }

    .card-header {
        padding-bottom: $spacing-xl;
        border-bottom: 1rpx solid $color-gray-200;
        position: relative;

        .card-title {
            font-size: 34rpx;
            font-weight: 700;
            color: $color-gray-950;
            letter-spacing: 0.5rpx;
            background: linear-gradient(135deg, $color-gray-950 0%, $color-gray-800 100%);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-clip: text;
        }

        .preview-image {
            position: absolute;
            right: 0;
            top: 0;
            transition: transform $transition-normal ease;

            &:hover {
                transform: scale(1.05);
            }
        }
    }
}

5.2 流程时间线样式

scss 复制代码
.process-container {
    padding: $spacing-3xl 0;
    padding-bottom: 0;
    position: relative;
}

.process-item {
    display: flex;
    margin-bottom: $spacing-4xl;
    position: relative;

    // 入场动画
    opacity: 0;
    transform: translateY($spacing-lg);
    animation: slideInUp 0.6s cubic-bezier(0.4, 0, 0.2, 1) forwards;

    // 延迟动画
    @for $i from 1 through 5 {
        &:nth-child(#{$i}) {
            animation-delay: #{$i * 0.1}s;
        }
    }

    &:hover {
        .process-icon {
            transform: scale(1.1);
            box-shadow: 0 $spacing-md $spacing-xl rgba($color-primary, 0.4);
        }

        .process-line {
            background: linear-gradient(to bottom, $color-primary, $color-primary-light);
        }
    }

    // 状态样式
    &[data-status='approving'] .process-icon {
        background: linear-gradient(135deg, $color-primary 0%, $color-primary-light 100%);
        box-shadow: 0 $spacing-sm $spacing-lg rgba($color-primary, 0.4);
    }

    &[data-status='approved'] .process-icon {
        background: linear-gradient(135deg, $color-success 0%, $color-success-light 100%);
        box-shadow: 0 $spacing-sm $spacing-lg rgba($color-success, 0.4);
    }

    &[data-status='rejected'] .process-icon {
        background: linear-gradient(135deg, $color-danger 0%, $color-danger-light 100%);
        box-shadow: 0 $spacing-sm $spacing-lg rgba($color-danger, 0.4);
    }
}

.process-line {
    width: 3rpx;
    background: linear-gradient(to bottom, $color-primary, $color-gray-150);
    position: absolute;
    top: 90rpx;
    bottom: -$spacing-4xl;
    left: 40rpx;
    z-index: 1;

    // 绘制动画
    transform: scaleY(0);
    animation: drawLine 0.8s cubic-bezier(0.4, 0, 0.2, 1) forwards;
    animation-delay: 0.3s;

    // 发光效果
    &::before {
        content: '';
        position: absolute;
        top: 0;
        left: -1rpx;
        right: -1rpx;
        bottom: 0;
        background: inherit;
        filter: blur(2rpx);
        opacity: 0.5;
    }

    // 最后一个项目不显示连接线
    .process-item:last-child & {
        display: none;
    }
}

5.3 节点图标样式

scss 复制代码
.process-icon {
    width: 80rpx;
    height: 80rpx;
    border-radius: $radius-full;
    margin-right: $spacing-lg;
    display: flex;
    align-items: center;
    justify-content: center;
    color: $color-white;
    font-size: 28rpx;
    font-weight: 600;
    position: relative;

    transition: all $transition-bounce;
    cursor: pointer;

    background: linear-gradient(135deg, $color-primary 0%, $color-primary-dark 100%);
    box-shadow: 0 $spacing-sm $spacing-lg rgba($color-primary, 0.3);

    // 内阴影
    &::before {
        content: '';
        position: absolute;
        inset: 2rpx;
        border-radius: $radius-full;
        background: rgba($color-white, 0.1);
    }

    // 状态徽章
    .process-icon-status {
        width: 32rpx;
        height: 32rpx;
        border: 2rpx solid $color-white;
        border-radius: $radius-full;
        position: absolute;
        bottom: -$spacing-2xs;
        right: -$spacing-2xs;
        display: flex;
        align-items: center;
        justify-content: center;
        background: $color-white;
        box-shadow: 0 $spacing-2xs $spacing-sm rgba(0, 0, 0, 0.15);
        font-size: 20rpx;
        color: $color-primary;
    }
}

5.4 审批人列表样式

scss 复制代码
.process-detail {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    margin-top: $spacing-md;
    margin-left: 108rpx;
    font-size: 26rpx;
    color: $color-gray-600;
}

.process-assignee {
    .assignee-item {
        display: flex;
        align-items: center;
        margin-bottom: $spacing-sm;
        padding: $spacing-sm $spacing-md;
        background: linear-gradient(135deg, $color-gray-100 0%, $color-gray-200 100%);
        margin-right: $spacing-md;
        border-radius: $radius-2xl;
        font-size: 24rpx;
        color: $color-gray-700;

        transition: all $transition-normal ease;
        border: 1rpx solid $color-gray-200;

        &:hover {
            background: linear-gradient(135deg, $color-gray-200 0%, $color-gray-300 100%);
            transform: translateY(-$spacing-2xs);
            box-shadow: 0 $spacing-2xs $spacing-sm rgba(0, 0, 0, 0.1);
        }

        text {
            margin-left: $spacing-2xs;
            font-weight: 500;
        }
    }
}

5.5 审批意见样式

scss 复制代码
.process-comment-container {
    margin-top: $spacing-md;
    margin-left: 108rpx;

    .process-comment {
        font-size: 28rpx;
        color: $color-gray-900;
        margin-top: $spacing-sm;

        background: linear-gradient(135deg, $color-white 0%, $color-gray-50 100%);
        padding: $spacing-lg;
        border-radius: $radius-lg;
        line-height: 1.7;
        position: relative;
        border: 1rpx solid $color-gray-200;
        box-shadow: 0 $spacing-2xs $spacing-sm rgba(0, 0, 0, 0.08);

        transition: all $transition-normal ease;

        &:hover {
            box-shadow: 0 $spacing-sm $spacing-lg rgba(0, 0, 0, 0.12);
            transform: translateY(-$spacing-2xs);
        }

        // 评论框小三角
        &::before {
            content: '';
            position: absolute;
            top: -12rpx;
            left: 32rpx;
            width: 0;
            height: 0;
            border: 6rpx solid transparent;
            border-bottom-color: $color-white;
            filter: drop-shadow(0 -1rpx 1rpx rgba(0, 0, 0, 0.08));
        }
    }
}

5.6 底部操作栏样式

scss 复制代码
.bottom-action-bar {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;

    display: flex;
    padding: $spacing-xl $spacing-2xl;
    padding-bottom: calc($spacing-xl + constant(safe-area-inset-bottom));
    padding-bottom: calc($spacing-xl + env(safe-area-inset-bottom));

    background: linear-gradient(135deg, $color-white 0%, $color-gray-50 100%);
    box-shadow: $shadow-sm;
    justify-content: space-around;
    backdrop-filter: blur(20rpx);

    // 顶部渐变分割线
    &::before {
        content: '';
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        height: 1rpx;
        background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.1), transparent);
    }
}

5.7 动画定义

scss 复制代码
// 节点入场动画
@keyframes slideInUp {
    from {
        opacity: 0;
        transform: translateY($spacing-lg);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

// 连接线绘制动画
@keyframes drawLine {
    from {
        transform: scaleY(0);
        opacity: 0;
    }
    to {
        transform: scaleY(1);
        opacity: 1;
    }
}

六、使用示例

6.1 基础用法

vue 复制代码
<template>
    <approval-process :process_instance_id="processInstanceId" types="travel" @approve="handleApprove" @reject="handleReject" />
</template>

<script setup>
import { ref } from 'vue';
import ApprovalProcess from '@/components/approval-process/approval-process.vue';

const processInstanceId = ref('123456');

const handleApprove = () => {
    console.log('审批通过');
    // 刷新列表或跳转
};

const handleReject = () => {
    console.log('审批拒绝');
    // 刷新列表或跳转
};
</script>

6.2 带审批前校验

vue 复制代码
<template>
    <approval-process
        :process_instance_id="processInstanceId"
        types="expense"
        @validate-before-approve="validateBeforeApprove"
        @approve="handleApprove"
    />
</template>

<script setup>
const validateBeforeApprove = ({ resolve, reject }) => {
    // 表单校验
    if (!formData.value.amount) {
        uni.showToast({ title: '请填写金额', icon: 'none' });
        reject();
        return;
    }

    // 异步校验
    api.checkExpenseAmount(formData.value.amount).then((res) => {
        if (res.valid) {
            resolve();
        } else {
            uni.showToast({ title: '金额超限', icon: 'none' });
            reject();
        }
    });
};
</script>

七、性能优化实践

7.1 条件渲染优化

vue 复制代码
<!-- 使用 v-if 避免不必要的渲染 -->
<view v-if="approvalProcess.length > 0" class="detail-card">
  <!-- ... -->
</view>

<!-- 使用 v-if 控制连接线显示 -->
<view v-if="index < approvalProcess.length - 1" class="process-line"></view>

7.2 计算属性缓存

typescript 复制代码
const showActionBar = computed(() => {
    return props.types === 'travel' && runningTask.value && isHandleTaskStatus() && isShowButton(OperationButtonType.RETURN);
});

7.3 CSS 动画性能

scss 复制代码
.process-item {
    // 使用 GPU 加速
    will-change: transform, opacity;

    // 使用 cubic-bezier 缓动函数
    animation: slideInUp 0.6s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}

.process-icon {
    // 弹性动画
    transition: all $transition-bounce;
}

八、常见问题与解决方案

8.1 长流程性能问题

问题:审批节点过多时滚动卡顿

解决

vue 复制代码
<!-- 使用 scroll-view 并开启增强模式 -->
<scroll-view :enhanced="true" :bounce="false" scroll-y>
  <view v-for="node in approvalProcess" :key="node.id">
    <!-- ... -->
  </view>
</scroll-view>

8.2 图片加载闪烁

问题:头像加载时出现闪烁

解决

vue 复制代码
<u-image :fade="true" duration="450" :src="fileCdn(avatar)" mode="circle"></u-image>

8.3 安全区域适配

问题:iPhone X 等机型底部按钮被遮挡

解决

scss 复制代码
.bottom-action-bar {
    padding-bottom: calc($spacing-xl + env(safe-area-inset-bottom));
}

九、总结

本文详细介绍了审批流程组件的完整封装过程,包括:

功能完整 :支持多种节点类型和状态

交互友好 :平滑动画、即时反馈、操作便捷

样式精美 :渐变设计、阴影效果、微交互

性能优化 :条件渲染、计算属性、GPU 加速

多端兼容:H5、小程序、APP 完美适配

该组件已在多个企业项目中应用,日均处理审批流程上千条。希望能为你在开发类似组件时提供参考!


最后的话

学习编程是一场马拉松,不是百米冲刺。

  • 🌟 开始最重要: 不要等"准备好了"再开始,现在就开始
  • 🌟 坚持是关键: 每天进步一点点,一年后你会感谢自己
  • 🌟 实践出真知: 多做项目,多写代码,多总结经验

参考文献

1\] uni-app 官方文档 https://uniapp.dcloud.net.cn/ \[2\] Vue3 组合式 API https://cn.vuejs.org/ \[3\] uView UI 组件库 https://www.uviewui.com/ \[4\] BPMN 2.0 规范 https://www.bpmn.org/ *** ** * ** *** **版权声明**:本文为 CSDN 博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 > **互动话题**:你在开发审批流程时遇到过哪些坑?欢迎评论区交流讨论!觉得有用请点赞 👍 收藏 ⭐️ 支持!

相关推荐
前端Hardy1 天前
用 uni-app x 重构我们的 App:一套代码跑通 iOS、Android、鸿蒙!人力成本直降 60%
前端·ios·uni-app
嘉琪0012 天前
uni-app 核心坑点及解决方案——2026 0309
uni-app
2501_916008892 天前
iPhone 上怎么抓 App 的网络请求,在 iOS 设备上捕获网络请求
android·网络·ios·小程序·uni-app·iphone·webview
jingling5552 天前
无需重新安装APK | uni-app 热更新技术实战
前端·javascript·前端框架·uni-app·node.js
遇见小美好y2 天前
uniapp 实现向下追加数据功能
前端·javascript·uni-app
行者-全栈开发2 天前
43 篇系统实战:uni-app 从入门到架构师成长之路
前端·typescript·uni-app·vue3·最佳实践·企业级架构
00后程序员张2 天前
iOS上架工具,AppUploader(开心上架)用于证书生成、描述文件管理Xcode用于应用构建
android·macos·ios·小程序·uni-app·iphone·xcode
万物得其道者成2 天前
uniapp 滑动过快 onReachBottom 事件不触发
uni-app