feat: 新增文件上传支持,优化移动端UI与巡检功能

1.  新增全局上传配置,设置默认20MB请求体上限与上传目录
2.  重构上传文件处理,按天自动创建子目录存储文件
3.  修复巡检上传表单移动端强制调用相机的问题
4.  为MES与库房客户端添加响应式侧边栏,支持桌面折叠与移动端抽屉模式
5.  补充PAD端巡检页面的独立访问说明文档
6.  删除冗余的前端静态首页文件
7.  更新库房客户端静态资源哈希值
This commit is contained in:
SunYF
2026-08-31 18:54:15 +08:00
parent ae4c292487
commit bb1bb2c3f5
10 changed files with 174 additions and 64 deletions
+10
View File
@@ -254,6 +254,16 @@ export const helpInspect = {
overview:
'检验员/线长用手持平板(PAD)或电脑浏览器做巡检与检验确认,PC 与 PAD 访问同一地址、前端自适应。\n\n数据来源:全部记录提交后写入 MES 巡检记录表(inspection_record),用于追溯和流程卡输出,也可在看板触发报警。\n\n权限:菜单「巡检终端」+ 各功能独立按钮权限码(sys.inspect:checkin/point/process/done/alarm/view),由【角色权限管理】统一分配。操作工通常无此菜单。',
sections: [
{
title: '打开方式(PAD / 手机独立访问)',
overview:
'电脑端:左侧菜单「巡检终端」打开。\n\nPAD / 手机独立打开:在浏览器地址栏输入\n' +
window.location.origin + '/inspect\n' +
'(如上所示即本机当前访问地址,例如 http://192.168.1.100:8888/inspect)。\n\n' +
'首次打开会跳转登录页,登录一次后(浏览器记住登录态)再次打开将直接进入巡检页;PAD 与电脑需在同一局域网。\n\n' +
'功能按权限显示:无「巡检终端」菜单或按钮权限的账号无法使用。',
fields: []
},
{
title: '开工签到',
fields: [
@@ -1,9 +1,16 @@
<template>
<el-container class="layout" style="min-height:100vh">
<el-aside width="210px" class="aside">
<div class="logo">MES 产线控制</div>
<!-- 移动端遮罩 -->
<div v-if="isMobile && mobileOpen" class="drawer-mask" @click="mobileOpen = false"></div>
<el-aside :width="asideWidth" class="aside"
:class="{ 'drawer': isMobile, 'drawer-open': isMobile && mobileOpen }">
<div class="logo">
<el-icon :size="22"><Monitor /></el-icon>
<span v-show="!isCollapse">MES 产线控制</span>
</div>
<el-menu v-if="menus.length" :default-active="$route.path" router background-color="#001529"
text-color="#a6adb4" active-text-color="#409eff" style="border-right:none">
text-color="#a6adb4" active-text-color="#409eff" style="border-right:none"
:collapse="isCollapse && !isMobile">
<el-menu-item v-for="m in menus" :key="m.path" :index="m.path">
<el-icon style="margin-right:8px"><component :is="m.icon" /></el-icon>
<span>{{ m.name }}</span>
@@ -16,7 +23,13 @@
</el-aside>
<el-container>
<el-header class="header">
<span class="page-title">{{ $route.meta.title || '' }}</span>
<div class="header-left">
<el-icon class="collapse-btn" @click="toggleAside">
<Expand v-if="isMobile ? !mobileOpen : isCollapse" />
<Fold v-else />
</el-icon>
<span class="page-title">{{ $route.meta.title || '' }}</span>
</div>
<div class="spacer"></div>
<div class="header-right">
<el-button class="help-btn" text @click="openHelp">帮助</el-button>
@@ -39,17 +52,50 @@
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
import { useRouter } from 'vue-router'
import {
Document, Calendar, Box, Goods, ShoppingCart, Monitor, Tools,
Operation, Link, DataLine, EditPen, Search, CircleCheck, Clock, User
Operation, Link, DataLine, EditPen, Search, CircleCheck, Clock, User,
Fold, Expand
} from '@element-plus/icons-vue'
import { getUser, logout } from '../utils/auth'
import request from '../utils/request'
const router = useRouter()
// ===== 响应式侧边栏:桌面可折叠,移动端抽屉 =====
const isCollapse = ref(false) // 桌面端折叠(仅图标)
const isMobile = ref(false) // 移动端(≤767px
const mobileOpen = ref(false) // 移动端抽屉展开
const asideWidth = computed(() => (isMobile.value ? '210px' : isCollapse.value ? '64px' : '210px'))
function toggleAside() {
if (isMobile.value) {
mobileOpen.value = !mobileOpen.value
} else {
isCollapse.value = !isCollapse.value
}
}
function onScreenChange() {
const mq = window.matchMedia('(max-width: 767px)')
isMobile.value = mq.matches
if (!isMobile.value) mobileOpen.value = false
}
// 路由切换后自动收起移动端抽屉
watch(() => router.currentRoute.value.path, () => {
if (isMobile.value) mobileOpen.value = false
})
onMounted(() => {
onScreenChange()
window.addEventListener('resize', onScreenChange)
})
onBeforeUnmount(() => window.removeEventListener('resize', onScreenChange))
const menus = computed(() => {
const me = getUser()
if (!me) return defaultMenus
@@ -59,21 +105,21 @@ const menus = computed(() => {
})
const defaultMenus = [
{ code: 'produce.workorder', name: '工单管理', path: '/work-order' },
{ code: 'produce.dailyplan', name: '日排产', path: '/daily-plan' },
{ code: 'produce.bom', name: '物料清单', path: '/bom' },
{ code: 'produce.material', name: '备料单', path: '/material-request' },
{ code: 'produce.plc', name: '工位组合下发', path: '/plc-send' },
{ code: 'produce.torque', name: '拧紧查询', path: '/torque' },
{ code: 'produce.processflow', name: '工艺流程', path: '/process-flow' },
{ code: 'produce.station', name: '关联工位', path: '/station' },
{ code: 'produce.performance', name: '绩效报表', path: '/performance' },
{ code: 'produce.scan', name: '手动报工', path: '/scan' },
{ code: 'produce.trace', name: '工件追溯', path: '/trace' },
{ code: 'produce.product', name: '产品类型', path: '/product-type' },
{ code: 'sys.inspect', name: '巡检终端', path: '/inspect' },
{ code: 'sys.eventlog', name: '操作日志', path: '/event-log' },
{ code: 'sys.rbac', name: '角色权限管理', path: '/rbac' }
{ code: 'produce.workorder', name: '工单管理', path: '/work-order', icon: Document },
{ code: 'produce.dailyplan', name: '日排产', path: '/daily-plan', icon: Calendar },
{ code: 'produce.bom', name: '物料清单', path: '/bom', icon: Goods },
{ code: 'produce.material', name: '备料单', path: '/material-request', icon: ShoppingCart },
{ code: 'produce.plc', name: '工位组合下发', path: '/plc-send', icon: Monitor },
{ code: 'produce.torque', name: '拧紧查询', path: '/torque', icon: Tools },
{ code: 'produce.processflow', name: '工艺流程', path: '/process-flow', icon: Operation },
{ code: 'produce.station', name: '关联工位', path: '/station', icon: Link },
{ code: 'produce.performance', name: '绩效报表', path: '/performance', icon: DataLine },
{ code: 'produce.scan', name: '手动报工', path: '/scan', icon: EditPen },
{ code: 'produce.trace', name: '工件追溯', path: '/trace', icon: Search },
{ code: 'produce.product', name: '产品类型', path: '/product-type', icon: Box },
{ code: 'sys.inspect', name: '巡检终端', path: '/inspect', icon: CircleCheck },
{ code: 'sys.eventlog', name: '操作日志', path: '/event-log', icon: Clock },
{ code: 'sys.rbac', name: '角色权限管理', path: '/rbac', icon: User }
]
const userName = computed(() => {
@@ -108,14 +154,20 @@ function onUserCmd(cmd) {
</script>
<style scoped>
.aside { background: #001529; }
.logo { color: #fff; font-size: 17px; font-weight: 600; text-align: center; line-height: 60px; height: 60px; }
.aside { background: #001529; transition: width .2s ease; }
.drawer { position: fixed; left: 0; top: 0; bottom: 0; z-index: 1001; transform: translateX(-100%); transition: transform .3s ease; }
.drawer-open { transform: translateX(0); }
.drawer-mask { position: fixed; inset: 0; background: rgba(0, 0, 0, .45); z-index: 1000; }
.logo { display: flex; align-items: center; justify-content: center; gap: 8px; color: #fff; font-size: 17px; font-weight: 600; height: 60px; white-space: nowrap; }
.no-menu { padding: 24px 10px; text-align: center; color: #a6adb4; font-size: 14px; }
.no-menu .sub { margin-top: 6px; font-size: 12px; color: #6b7684; }
.header { display: flex; align-items: center; justify-content: space-between; background: #fff; border-bottom: 1px solid #eee; }
.page-title { font-size: 16px; font-weight: 600; color: #303133; }
.header-left { display: flex; align-items: center; gap: 8px; min-width: 0; }
.collapse-btn { font-size: 20px; cursor: pointer; color: #303133; display: flex; align-items: center; flex-shrink: 0; }
.collapse-btn:hover { color: #1668dc; }
.page-title { font-size: 16px; font-weight: 600; color: #303133; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.spacer { flex: 1; }
.header-right { display: flex; align-items: center; gap: 6px; }
.header-right { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
.help-btn { font-size: 13px; color: #606266; }
.user-name { display: inline-flex; align-items: center; gap: 4px; cursor: pointer; font-size: 14px; color: #303133; outline: none; padding: 4px 6px; border-radius: 4px; }
.user-name:hover { color: #1668dc; background: #f2f6fc; }
+3 -3
View File
@@ -15,7 +15,7 @@
</el-radio-group>
</el-form-item>
<el-form-item label="签到拍照">
<input type="file" accept="image/*" capture="environment" class="file-input" @change="handlePhoto($event, checkin)">
<input type="file" accept="image/*" class="file-input" @change="handlePhoto($event, checkin)">
<div class="photo-box">
<el-image v-if="checkin.photo" :src="checkin.photo" :preview-src-list="[checkin.photo]" fit="cover" class="photo-preview" preview-teleported />
<span v-else class="photo-tip">点击上方按钮拍照上传</span>
@@ -41,7 +41,7 @@
</el-checkbox-group>
</el-form-item>
<el-form-item label="点检拍照">
<input type="file" accept="image/*" capture="environment" class="file-input" @change="handlePhoto($event, point)">
<input type="file" accept="image/*" class="file-input" @change="handlePhoto($event, point)">
<div class="photo-box">
<el-image v-if="point.photo" :src="point.photo" :preview-src-list="[point.photo]" fit="cover" class="photo-preview" preview-teleported />
<span v-else class="photo-tip">点击上方按钮拍照上传</span>
@@ -115,7 +115,7 @@
</el-checkbox-group>
</el-form-item>
<el-form-item label="异常拍照">
<input type="file" accept="image/*" capture="environment" class="file-input" @change="handlePhoto($event, alarm)">
<input type="file" accept="image/*" class="file-input" @change="handlePhoto($event, alarm)">
<div class="photo-box">
<el-image v-if="alarm.photo" :src="alarm.photo" :preview-src-list="[alarm.photo]" fit="cover" class="photo-preview" preview-teleported />
<span v-else class="photo-tip">点击上方按钮拍照上传</span>