feat: 新增文件上传支持,优化移动端UI与巡检功能
1. 新增全局上传配置,设置默认20MB请求体上限与上传目录 2. 重构上传文件处理,按天自动创建子目录存储文件 3. 修复巡检上传表单移动端强制调用相机的问题 4. 为MES与库房客户端添加响应式侧边栏,支持桌面折叠与移动端抽屉模式 5. 补充PAD端巡检页面的独立访问说明文档 6. 删除冗余的前端静态首页文件 7. 更新库房客户端静态资源哈希值
This commit is contained in:
@@ -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; }
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user