pages.json
"easycom": {
"autoscan": true,
"custom": {
"^u--(.*)": "@/uni_modules/uview-plus/components/u-$1/u-$1.vue",
"^up-(.*)": "@/uni_modules/uview-plus/components/u-$1/u-$1.vue",
"^u-([^-].*)": "@/uni_modules/uview-plus/components/u-$1/u-$1.vue"
}
},
main.js
import uviewPlus, { setConfig } from './uni_modules/uview-plus'
export function createApp() {
const app = createSSRApp(App)
app.use(uviewPlus)
setConfig({
config: {
iconUrl: '/static/font/uicon-iconfont.ttf',
loadFontOnce: true
}
})
return {
app
}
}
App.vue
@import '@/uni_modules/uni-scss/index.scss';
vite.config.js
import { defineConfig } from "vite";
// 注意:UniUpRoot 必须在 uni() 之前注册
import UniUpRoot from "./uni_modules/uview-plus/libs/root/index.js";
import uni from "@dcloudio/vite-plugin-uni";
export default defineConfig({
server: {
host: true,
// 开发服务器端口,改为你想要的端口即可,如 3000、8080、9527
port: 3000,
// 端口被占用时是否自动换端口(false 则直接报错)
strictPort: false,
// 自动打开浏览器
open: true,
},
plugins: [
UniUpRoot({
// 根文件名(内部会自动归一化为 App.up.vue)
rootFileName: "App.up",
// 若 App.up.vue 不存在则自动创建一个最小版本
autoCreateRootFile: true,
}),
uni(),
],
});
uni.scss
@import '@/uni_modules/uni-scss/variables.scss';
@import '@/uni_modules/uview-plus/theme.scss';
App.up.vue
<template>
<UpRootView />
</template>
uView-plus tabs 组件
<view class="process-tabs-wrap">
<up-tabs :list="list1" :current="currentTabIndex" :scrollable="true" @click="activeTabs"></up-tabs>
</view>
<script setup>
const list1 = ref([]);
const currentTabIndex = ref(0);
const activeTabs = (item, index) => {
const tabIndex = typeof index === 'number' ? index : item?.index ?? 0;
};
</script>
<style lang="scss" scoped>
.process-tabs-wrap :deep(.u-tabs) {
width: 100%;
}
.process-tabs-wrap {
margin: 16rpx 24rpx 0;
background: #ffffff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(47, 123, 255, 0.06);
/* App端 scroll-view 必须有明确高度才能渲染,否则会塌陷为0 */
height: 80rpx;
display: flex;
align-items: center;
}
</style>
uni-popup 组件components/work-instruction-preview/work-instruction-preview.vue
uni-popup 真机运行默认未隐藏,加了 v-if="popupVisible" ,解决默认未隐藏的问题;点击取消触发closePopup函数,无法关闭弹窗;
<template>
<uni-popup v-if="popupVisible" ref="popupRef" type="bottom">
<view class="instruction-popup">
<view class="instruction-popup-title">作业指导书列表</view>
<view
v-for="(item, idx) in instructions"
:key="idx"
class="instruction-item"
@click="onPickInstruction(item)"
>
<text class="instruction-icon">?</text>
<text class="instruction-name">{{ item.src_name }}</text>
<text class="instruction-arrow">›</text>
</view>
<view class="instruction-cancel" @click="closePopup">
<text>取消</text>
</view>
</view>
</uni-popup>
</template>
<script setup>
import { ref, nextTick } from 'vue';
import { getToken } from '@/utils/auth.js';
const props = defineProps({
// 作业指导书列表,每项含 src_name(PDF名称)、fileurl(PDF路径)
instructions: { type: Array, default: () => [] },
});
// 控制弹窗组件是否挂载(避免 uni-popup 在 App 端初始化时序问题导致内容直接显示在页面流中)
const popupVisible = ref(false);
// uni-popup 通过 ref 调用 open/close 控制显隐
const popupRef = ref(null);
const openPopup = () => {
// 先挂载组件,再在下一帧调用 open,确保 ref 可用
popupVisible.value = true;
nextTick(() => {
popupRef.value?.open('bottom');
});
};
const closePopup = () => {
// 先挂载组件,再在下一帧调用 open,确保 ref 可用
popupRef.value?.close();
// 关闭动画结束后再卸载组件,避免动画被截断
setTimeout(() => {
popupVisible.value = false;
}, 300);
};
// 构建 PDF 文件真实下载地址(H5 交给 pdf.js 阅读器渲染)
const buildFileUrl = (fileurl) => {
const ticket = getToken() || '';
return `https://api.100xsys.cn:443/file/download?filePath=${encodeURIComponent(fileurl)}&bx_auth_ticket=${ticket}`;
};
// 构建 pdf.js 在线预览页地址(APP 交给系统原生浏览器打开)
const buildPdfUrl = (fileurl) => {
const ticket = getToken() || '';
const downloadUrl = buildFileUrl(fileurl);
return `https://saas.100xsys.cn/vpages/#/viewpdf?pdfsrc=${encodeURIComponent(downloadUrl)}&bx_ticket=${ticket}`;
};
// 打开 PDF
const openPdf = (fileurl, name) => {
if (!fileurl) {
uni.showToast({ title: 'PDF文件路径无效', icon: 'none' });
return;
}
// #ifdef H5
// H5:跳转 uview-plus PDF阅读器页面(pdf.js 应用内渲染),透传 PDF 名称作为页面标题
uni.navigateTo({
url: `/pages/pdfview/index?url=${encodeURIComponent(buildFileUrl(fileurl))}&name=${encodeURIComponent(name || '')}`
});
// #endif
// #ifdef APP-PLUS
// APP:调用系统原生浏览器打开
plus.runtime.openURL(buildPdfUrl(fileurl));
// #endif
};
// 弹窗中点击某个作业指导书
const onPickInstruction = (item) => {
closePopup();
openPdf(item.fileurl, item.src_name);
};
// 解析 work_instruction_json 字符串为数组
const parseWorkInstructions = (jsonStr) => {
if (!jsonStr) return [];
try {
const parsed = JSON.parse(jsonStr);
return Array.isArray(parsed) ? parsed : [];
} catch (e) {
console.error('解析作业指导书JSON失败:', e);
return [];
}
};
// 预览入口:0个提示/1个直接打开/多个弹窗列表
const preview = () => {
const list = props.instructions;
if (!list || !list.length) {
uni.showToast({ title: '暂无作业指导书', icon: 'none' });
return;
}
if (list.length === 1) {
openPdf(list[0].fileurl, list[0].src_name);
return;
}
openPopup();
};
defineExpose({ preview, parseWorkInstructions });
</script>
<style lang="scss" scoped>
.instruction-popup {
padding: 32rpx 28rpx;
padding-bottom: calc(32rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
background: #ffffff;
border-radius: 32rpx 32rpx 0 0;
overflow: hidden;
}
.instruction-popup-title {
font-size: 32rpx;
font-weight: 700;
color: #1a1a1a;
text-align: center;
margin-bottom: 24rpx;
}
.instruction-item {
display: flex;
flex-direction: row;
align-items: center;
padding: 28rpx 0;
border-bottom: 2rpx solid #f0f2f5;
}
.instruction-icon {
font-size: 36rpx;
margin-right: 16rpx;
}
.instruction-name {
flex: 1;
font-size: 28rpx;
color: #1a1a1a;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.instruction-arrow {
font-size: 36rpx;
color: #c0c4cc;
margin-left: 8rpx;
}
.instruction-cancel {
margin-top: 24rpx;
padding: 24rpx 0;
text-align: center;
font-size: 28rpx;
color: #999;
}
</style>
引入 work-instruction-preview 组件
<template>
<view class="guide-right" @click="onPreview">
<text class="preview-text">预览</text>
</view>
<work-instruction-preview ref="instructionRef" :instructions="workInstructions"></work-instruction-preview>
</template>
<script setup>
const instructionRef = ref();
const workInstructions = ref([]);
const onPreview = () => {
instructionRef.value?.preview();
};
</script>