feat: 新增文件上传支持,优化移动端UI与巡检功能
1. 新增全局上传配置,设置默认20MB请求体上限与上传目录 2. 重构上传文件处理,按天自动创建子目录存储文件 3. 修复巡检上传表单移动端强制调用相机的问题 4. 为MES与库房客户端添加响应式侧边栏,支持桌面折叠与移动端抽屉模式 5. 补充PAD端巡检页面的独立访问说明文档 6. 删除冗余的前端静态首页文件 7. 更新库房客户端静态资源哈希值
This commit is contained in:
@@ -53,4 +53,9 @@ Wms:
|
||||
Plc:
|
||||
Enable: false
|
||||
Host: 127.0.0.1
|
||||
Port: 102
|
||||
Port: 102
|
||||
|
||||
# 文件上传(工艺图纸 PDF / 巡检照片):Dir 相对服务启动目录,可改绝对路径;按天自动分子目录
|
||||
Upload:
|
||||
Dir: uploads
|
||||
MaxMB: 20
|
||||
@@ -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>
|
||||
|
||||
@@ -53,7 +53,7 @@ func ListInspectionsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
// UploadPhotoHandler POST /inspections/upload(巡检拍照上传,返回文件名)
|
||||
func UploadPhotoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseMultipartForm(32 << 20); err != nil {
|
||||
if err := r.ParseMultipartForm(svcCtx.Config.Upload.MaxMB << 20); err != nil {
|
||||
httpx.BadRequest(w, "上传文件过大或格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -11,9 +11,11 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// saveUploadFile 保存上传文件(工艺图纸PDF / 巡检照片),返回生成的文件名
|
||||
// saveUploadFile 保存上传文件(工艺图纸PDF / 巡检照片),返回生成的相对文件名(含日期子目录)
|
||||
func saveUploadFile(r *http.Request, field, dir string) (string, error) {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
sub := time.Now().Format("2006-01-02")
|
||||
full := filepath.Join(dir, sub)
|
||||
if err := os.MkdirAll(full, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
file, header, err := r.FormFile(field)
|
||||
@@ -27,7 +29,7 @@ func saveUploadFile(r *http.Request, field, dir string) (string, error) {
|
||||
ext = ".bin"
|
||||
}
|
||||
name := time.Now().Format("20060102150405") + "_" + randHex(6) + ext
|
||||
dst, err := os.Create(filepath.Join(dir, name))
|
||||
dst, err := os.Create(filepath.Join(full, name))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -35,7 +37,7 @@ func saveUploadFile(r *http.Request, field, dir string) (string, error) {
|
||||
if _, err := io.Copy(dst, file); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return name, nil
|
||||
return filepath.ToSlash(filepath.Join(sub, name)), nil
|
||||
}
|
||||
|
||||
func randHex(n int) string {
|
||||
@@ -44,26 +46,30 @@ func randHex(n int) string {
|
||||
return hex.EncodeToString(b)[:n*2]
|
||||
}
|
||||
|
||||
// serveUploadFile 下发上传目录中的文件(限制为单段文件名,防路径穿越)
|
||||
// serveUploadFile 下发上传目录中的文件(仅允许 单层日期子目录/文件名,防路径穿越)
|
||||
func serveUploadFile(w http.ResponseWriter, r *http.Request, dir, name string) {
|
||||
name = strings.TrimSpace(filepath.Base(name))
|
||||
if name == "" || name == "." || name == ".." || strings.Contains(name, "..") {
|
||||
name = strings.TrimSpace(name)
|
||||
clean := filepath.Clean(filepath.FromSlash(name))
|
||||
if clean == "." || clean == ".." ||
|
||||
strings.HasPrefix(clean, ".."+string(filepath.Separator)) ||
|
||||
filepath.IsAbs(clean) || strings.ContainsRune(name, '\x00') {
|
||||
http.Error(w, "非法文件名", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
f, err := os.Open(filepath.Join(dir, name))
|
||||
f, err := os.Open(filepath.Join(dir, clean))
|
||||
if err != nil {
|
||||
http.Error(w, "文件不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
ct := "application/octet-stream"
|
||||
base := strings.ToLower(clean)
|
||||
switch {
|
||||
case strings.HasSuffix(strings.ToLower(name), ".pdf"):
|
||||
case strings.HasSuffix(base, ".pdf"):
|
||||
ct = "application/pdf"
|
||||
case strings.HasSuffix(strings.ToLower(name), ".jpg"), strings.HasSuffix(strings.ToLower(name), ".jpeg"):
|
||||
case strings.HasSuffix(base, ".jpg"), strings.HasSuffix(base, ".jpeg"):
|
||||
ct = "image/jpeg"
|
||||
case strings.HasSuffix(strings.ToLower(name), ".png"):
|
||||
case strings.HasSuffix(base, ".png"):
|
||||
ct = "image/png"
|
||||
}
|
||||
w.Header().Set("Content-Type", ct)
|
||||
|
||||
@@ -167,6 +167,7 @@ func main() {
|
||||
server := rest.MustNewServer(
|
||||
c.RestConf,
|
||||
rest.WithCors("*"),
|
||||
rest.WithMaxBytes(c.Upload.MaxMB<<20), // 请求体上限跟随 Upload.MaxMB(默认20MB),否则 go-zero 默认 1MB 会 413
|
||||
rest.WithNotFoundHandler(http.HandlerFunc(spaHandler.ServeHTTP)),
|
||||
)
|
||||
defer server.Stop()
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
|
||||
<title>MES 产线控制</title>
|
||||
<script type="module" crossorigin src="./assets/index-CEKQX1B2.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-HmZIfQ_J.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
HomeFilled, Download, Upload, Search, CircleCheck, List,
|
||||
Box, Tickets, Monitor, EditPen
|
||||
Box, Tickets, Monitor, EditPen, Fold, Expand
|
||||
} from '@element-plus/icons-vue'
|
||||
import { getUser, logout } from '../utils/auth'
|
||||
|
||||
@@ -15,6 +15,38 @@ const user = getUser()
|
||||
const userName = computed(() => user?.realName || user?.username || '未登录')
|
||||
const pageTitle = computed(() => route.meta?.title || '')
|
||||
|
||||
// ===== 响应式侧边栏:桌面可折叠,移动端抽屉 =====
|
||||
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(() => route.path, () => {
|
||||
if (isMobile.value) mobileOpen.value = false
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
onScreenChange()
|
||||
window.addEventListener('resize', onScreenChange)
|
||||
})
|
||||
onBeforeUnmount(() => window.removeEventListener('resize', onScreenChange))
|
||||
|
||||
const menus = [
|
||||
{ path: '/', title: '工作台', icon: HomeFilled },
|
||||
{ path: '/inbound', title: '入库管理', icon: Download },
|
||||
@@ -50,13 +82,17 @@ function onLogout() {
|
||||
|
||||
<template>
|
||||
<el-container class="layout">
|
||||
<el-aside width="210px" class="aside">
|
||||
<!-- 移动端遮罩 -->
|
||||
<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="brand">
|
||||
<el-icon :size="22"><Box /></el-icon>
|
||||
<span>库房客户端</span>
|
||||
<span v-show="!isCollapse">库房客户端</span>
|
||||
</div>
|
||||
<el-menu :default-active="route.path" class="menu" background-color="#001529"
|
||||
text-color="#c2cad8" active-text-color="#ffffff" router>
|
||||
text-color="#c2cad8" active-text-color="#ffffff" router
|
||||
:collapse="isCollapse && !isMobile">
|
||||
<el-menu-item v-for="m in menus" :key="m.path" :index="m.path">
|
||||
<el-icon><component :is="m.icon" /></el-icon>
|
||||
<span>{{ m.title }}</span>
|
||||
@@ -66,7 +102,13 @@ function onLogout() {
|
||||
|
||||
<el-container>
|
||||
<el-header height="56px" class="header">
|
||||
<span class="title">{{ pageTitle }}</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="title">{{ pageTitle }}</span>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<el-button text type="primary" @click="router.push('/display')">
|
||||
<el-icon style="margin-right:4px"><Monitor /></el-icon>免登录大屏
|
||||
@@ -91,19 +133,26 @@ function onLogout() {
|
||||
|
||||
<style scoped>
|
||||
.layout { height: 100vh; }
|
||||
.aside { background: #001529; }
|
||||
.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; }
|
||||
.brand {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
height: 56px; padding: 0 16px;
|
||||
color: #fff; font-weight: 600; font-size: 15px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.menu { border-right: none; }
|
||||
.header {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
background: #fff; border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
.title { font-size: 16px; font-weight: 600; }
|
||||
.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; }
|
||||
.title { font-size: 16px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.spacer { flex: 1; }
|
||||
.help-btn { font-size: 13px; color: #606266; }
|
||||
.user-name {
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>库房客户端</title>
|
||||
<script type="module" crossorigin src="/assets/index-C_gZ2k2u.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Bi2AMikI.css">
|
||||
<script type="module" crossorigin src="/assets/index-BytlUM1G.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Bc2zCoxT.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
Reference in New Issue
Block a user