调整部分前端布局和后端增删改逻辑

This commit is contained in:
焦龙言
2026-09-28 18:29:10 +08:00
parent 17d991a74a
commit a539b4eb71
57 changed files with 87382 additions and 895 deletions
@@ -0,0 +1,11 @@
{
"version" : "1.0",
"configurations" : [
{
"customPlaygroundType" : "device",
"packageName" : "",
"playground" : "standard",
"type" : "uni-app:app-android"
}
]
}
@@ -0,0 +1,8 @@
import request from "@/utils/request";
export function listScanExceptions() {
return request({
url: "/business/dashboard/scan-exceptions",
method: "get",
});
}
@@ -0,0 +1,9 @@
import request from "@/utils/request";
export function listPalletInventory(query) {
return request({
url: "/report/pallet-inventory/list",
method: "get",
params: query,
});
}
@@ -0,0 +1,9 @@
import request from "@/utils/request";
export function listShipmentReport(query) {
return request({
url: "/report/shipment/list",
method: "get",
params: query,
});
}
@@ -1,14 +1,11 @@
<template>
<view
v-if="testBatchStore.isTestMode"
class="flex items-start gap-3 rounded-xl border border-red-300 bg-red-50 px-3 py-3"
>
<view class="mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-full bg-red-500 text-white">
<view class="i-mdi-flask-outline text-sm"></view>
<view v-if="testBatchStore.isTestMode" class="test-batch-banner">
<view class="test-batch-icon">
<view class="i-mdi-flask-outline test-batch-icon-glyph"></view>
</view>
<view class="min-w-0">
<text class="block text-sm font-bold text-red-700">测试数据</text>
<text class="mt-0.5 block break-all text-xs leading-5 text-red-600">
<view class="test-batch-copy">
<text class="test-batch-title">测试数据</text>
<text class="test-batch-value">
当前批次:{{ testBatchStore.currentBatchName || "已选测试批次" }}
</text>
</view>
@@ -20,3 +17,62 @@ import { useTestBatchStore } from "@/store";
const testBatchStore = useTestBatchStore();
</script>
<style lang="scss" scoped>
.test-batch-banner {
display: flex;
width: 100%;
min-width: 0;
box-sizing: border-box;
align-items: flex-start;
gap: 12rpx;
margin-top: 18rpx;
border: 1rpx solid #f1b8b4;
border-radius: 16rpx;
padding: 16rpx;
background: #fff5f4;
}
.test-batch-icon {
display: flex;
width: 52rpx;
height: 52rpx;
flex: 0 0 52rpx;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #d9534f;
color: #ffffff;
}
.test-batch-icon-glyph {
font-size: 28rpx;
}
.test-batch-copy {
min-width: 0;
flex: 1;
}
.test-batch-title,
.test-batch-value {
display: block;
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
}
.test-batch-title {
color: #b42318;
font-size: 24rpx;
font-weight: 700;
line-height: 1.4;
}
.test-batch-value {
margin-top: 5rpx;
color: #c2413b;
font-size: 22rpx;
line-height: 1.55;
}
</style>
@@ -1,24 +1,24 @@
<template>
<view class="space-y-3 rounded-2xl bg-white p-4 shadow-sm">
<view class="flex items-center justify-between gap-3">
<view class="min-w-0">
<text class="block text-sm font-bold text-gray-800">数据模式</text>
<text class="mt-1 block text-xs text-gray-500">
<view class="test-batch-switch">
<view class="mode-switch-header">
<view class="mode-switch-copy">
<text class="mode-switch-title">数据模式</text>
<text class="mode-switch-description">
{{ testBatchStore.isTestMode ? "当前操作隔离在测试批次" : "当前使用正式数据" }}
</text>
</view>
<text
:class="testBatchStore.isTestMode ? 'border-red-300 bg-red-50 text-red-700' : 'border-gray-200 bg-gray-50 text-gray-600'"
class="shrink-0 rounded-full border px-2.5 py-1 text-xs font-semibold"
class="mode-switch-status"
:class="testBatchStore.isTestMode ? 'is-test' : 'is-formal'"
>
{{ testBatchStore.isTestMode ? "测试数据" : "正式数据" }}
</text>
</view>
<view class="flex rounded-xl bg-gray-100 p-1">
<view class="mode-switch-controls">
<button
class="flex-1 rounded-lg py-2 text-xs transition-colors"
:class="!testBatchStore.isTestMode ? 'bg-white font-semibold text-gray-800 shadow-sm' : 'text-gray-500'"
class="mode-switch-button"
:class="{ active: !testBatchStore.isTestMode }"
:disabled="loading"
size="mini"
@click="handleModeClick('formal')"
@@ -26,8 +26,8 @@
正式
</button>
<button
class="flex-1 rounded-lg py-2 text-xs transition-colors"
:class="testBatchStore.isTestMode ? 'bg-red-500 font-semibold text-white shadow-sm' : 'text-gray-500'"
class="mode-switch-button"
:class="{ 'active-test': testBatchStore.isTestMode }"
:disabled="loading || !activeOptions.length"
size="mini"
@click="handleModeClick('test')"
@@ -36,8 +36,11 @@
</button>
</view>
<view v-if="testBatchStore.isTestMode || activeOptions.length" class="space-y-1">
<text class="block text-xs text-gray-500">测试批次</text>
<view
v-if="testBatchStore.isTestMode || activeOptions.length"
class="test-batch-picker-row"
>
<text class="test-batch-picker-label-title">测试批次</text>
<picker
:range="activeOptions"
range-key="batchName"
@@ -45,15 +48,23 @@
:disabled="loading || !activeOptions.length"
@change="handleBatchChange"
>
<view class="rounded-xl bg-gray-50 px-3 py-3 text-sm text-gray-700">
{{ selectedBatch?.batchName || (loading ? "加载中..." : "暂无可用测试批次") }}
<view class="test-batch-picker">
<text class="test-batch-picker-label">
{{ selectedBatch?.batchName || (loading ? "加载中..." : "暂无可用测试批次") }}
</text>
<view class="i-mdi-chevron-down test-batch-picker-arrow"></view>
</view>
</picker>
</view>
<view v-if="loadError" class="flex items-center justify-between gap-2 rounded-lg bg-orange-50 px-3 py-2">
<text class="min-w-0 text-xs leading-5 text-orange-700">{{ loadError }}</text>
<button class="shrink-0 text-xs text-orange-700" size="mini" :disabled="loading" @click="loadOptions">
<view v-if="loadError" class="mode-switch-error">
<text class="mode-switch-error-text">{{ loadError }}</text>
<button
class="mode-switch-retry"
size="mini"
:disabled="loading"
@click="loadOptions"
>
重试
</button>
</view>
@@ -151,3 +162,191 @@ async function handleBatchChange(event) {
onMounted(loadOptions);
</script>
<style lang="scss" scoped>
.test-batch-switch {
width: 100%;
min-width: 0;
box-sizing: border-box;
border: 1rpx solid #e5eeee;
border-radius: 20rpx;
padding: 22rpx;
background: #ffffff;
box-shadow: 0 8rpx 24rpx rgba(40, 34, 92, 0.06);
}
.mode-switch-header {
display: flex;
min-width: 0;
align-items: flex-start;
justify-content: space-between;
gap: 14rpx;
}
.mode-switch-copy {
min-width: 0;
flex: 1;
}
.mode-switch-title,
.mode-switch-description,
.test-batch-picker-label-title,
.test-batch-picker-label,
.mode-switch-error-text {
display: block;
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
}
.mode-switch-title {
color: #28225c;
font-size: 26rpx;
font-weight: 700;
line-height: 1.4;
}
.mode-switch-description {
margin-top: 5rpx;
color: #8b969d;
font-size: 21rpx;
line-height: 1.5;
}
.mode-switch-status {
flex: 0 0 auto;
border: 1rpx solid #dce7e7;
border-radius: 999rpx;
padding: 7rpx 12rpx;
font-size: 20rpx;
line-height: 1.3;
}
.mode-switch-status.is-formal {
border-color: #dce7e7;
background: #f5f8f8;
color: #64727a;
}
.mode-switch-status.is-test {
border-color: #f1b8b4;
background: #fff1f0;
color: #b42318;
}
.mode-switch-controls {
display: flex;
width: 100%;
min-width: 0;
gap: 10rpx;
margin-top: 18rpx;
border-radius: 12rpx;
padding: 5rpx;
background: #f1f5f5;
}
.mode-switch-button {
width: auto;
min-width: 0;
height: 64rpx;
flex: 1;
margin: 0;
border: 0;
border-radius: 9rpx;
background: transparent;
color: #748087;
font-size: 23rpx;
line-height: 64rpx;
}
.mode-switch-button::after,
.mode-switch-retry::after {
border: 0;
}
.mode-switch-button.active {
background: #ffffff;
box-shadow: 0 4rpx 10rpx rgba(40, 34, 92, 0.08);
color: #28225c;
font-weight: 700;
}
.mode-switch-button.active-test {
background: #d9534f;
box-shadow: 0 4rpx 10rpx rgba(217, 83, 79, 0.18);
color: #ffffff;
font-weight: 700;
}
.test-batch-picker-row {
min-width: 0;
margin-top: 18rpx;
}
.test-batch-picker-label-title {
color: #6c7786;
font-size: 21rpx;
line-height: 1.4;
}
.test-batch-picker {
display: flex;
width: 100%;
min-width: 0;
box-sizing: border-box;
align-items: center;
justify-content: space-between;
gap: 12rpx;
margin-top: 8rpx;
border: 1rpx solid #e0eaea;
border-radius: 12rpx;
padding: 16rpx;
background: #fbfdfd;
}
.test-batch-picker-label {
flex: 1;
color: #47545c;
font-size: 23rpx;
line-height: 1.5;
}
.test-batch-picker-arrow {
flex: 0 0 30rpx;
color: #96a5a9;
font-size: 30rpx;
}
.mode-switch-error {
display: flex;
min-width: 0;
align-items: center;
gap: 10rpx;
margin-top: 14rpx;
border-radius: 10rpx;
padding: 10rpx 12rpx;
background: #fff8ed;
}
.mode-switch-error-text {
flex: 1;
color: #a15c00;
font-size: 21rpx;
line-height: 1.5;
}
.mode-switch-retry {
width: auto;
min-width: 74rpx;
height: 52rpx;
flex: 0 0 auto;
margin: 0;
padding: 0 12rpx;
border: 0;
border-radius: 8rpx;
background: #fff0d6;
color: #a15c00;
font-size: 20rpx;
line-height: 52rpx;
}
</style>
@@ -0,0 +1,42 @@
export const businessMenuEntries = [
{
path: "/pages/business/inbound/index",
title: "仓库入库",
description: "扫描托盘登记入库",
icon: "i-mdi-package-down",
iconClass: "bg-blue-50 text-blue-600",
permission: "business:inbound:list",
},
{
path: "/pages/business/filling/index",
title: "工厂装填",
description: "录入菌种和包数",
icon: "i-mdi-package-variant-closed-plus",
iconClass: "bg-green-50 text-green-600",
permission: "business:filling:list",
},
{
path: "/pages/business/shipment/index",
title: "发货出库",
description: "扫码创建发货单",
icon: "i-mdi-truck-delivery",
iconClass: "bg-orange-50 text-orange-600",
permission: "business:shipment:list",
},
{
path: "/pages/business/usage/index",
title: "客户现场使用",
description: "绑定产线记录消耗",
icon: "i-mdi-factory",
iconClass: "bg-purple-50 text-purple-600",
permission: "business:usage:list",
},
{
path: "/pages/business/return/index",
title: "退回仓库",
description: "申请和确认退回",
icon: "i-mdi-undo-variant",
iconClass: "bg-red-50 text-red-600",
permission: "business:return:list",
},
];
+67 -65
View File
@@ -1,70 +1,72 @@
{
"name": "智链云枢管理系统",
"appid": "__UNI__25A9D80",
"description": "",
"versionName": "1.9.0",
"versionCode": "100",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueCompiler": "uni-app",
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
"name" : "智链云枢管理系统",
"appid" : "__UNI__F8DBA93",
"description" : "",
"versionName" : "1.9.0",
"versionCode" : "100",
"transformPx" : false,
"app-plus" : {
"usingComponents" : true,
"nvueCompiler" : "uni-app",
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
"modules" : {},
"distribute" : {
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"ios" : {
"dSYMs" : false
},
"sdkConfigs" : {}
}
},
"modules": {},
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"ios": {
"dSYMs": false
},
"sdkConfigs": {}
}
},
"quickapp": {},
"mp-weixin": {
"appid": "wxccd7e2a0911b3397",
"setting": {
"urlCheck": false,
"es6": false,
"minified": true,
"postcss": true
"quickapp" : {},
"mp-weixin" : {
"appid" : "wxccd7e2a0911b3397",
"setting" : {
"urlCheck" : false,
"es6" : false,
"minified" : true,
"postcss" : true
},
"optimization" : {
"subPackages" : true
},
"usingComponents" : true
},
"optimization": {
"subPackages": true
"vueVersion" : "3",
"h5" : {
"devServer" : {
"port" : 9090,
"https" : false
},
"title" : "智链云枢管理系统",
"router" : {
"mode" : "hash",
"base" : "./"
}
},
"usingComponents": true
},
"vueVersion": "3",
"h5": {
"devServer": {
"port": 9090,
"https": false
},
"title": "智链云枢管理系统",
"router": {
"mode": "hash",
"base": "./"
}
}
"locale" : "zh-Hans",
"fallbackLocale" : "zh-Hans"
}
+11 -1
View File
@@ -34,7 +34,7 @@
{
"path": "pages/business/filling/index",
"style": {
"navigationBarTitleText": "托盘装填"
"navigationBarTitleText": "工厂装填"
}
},
{
@@ -158,5 +158,15 @@
"navigationBarTextStyle": "black",
"navigationBarTitleText": "智链云枢管理系统",
"navigationBarBackgroundColor": "#FFFFFF"
},
"condition" : { //模式配置,仅开发期间生效
"current": 0, //当前激活的模式(list 的索引项)
"list": [
{
"name": "", //模式名称
"path": "", //启动页面,必选
"query": "" //启动参数,在页面的onLoad函数里面得到
}
]
}
}
@@ -3,7 +3,7 @@
<scroll-view scroll-y class="h-screen">
<view class="space-y-4 p-4 pb-24">
<view>
<text class="block text-xl font-bold text-gray-800">托盘装填</text>
<text class="block text-xl font-bold text-gray-800">工厂装填</text>
<text class="mt-1 block text-xs text-gray-500">
扫描空托盘,录入菌种、批次、规格和初始包数
</text>
File diff suppressed because it is too large Load Diff
+332 -118
View File
@@ -1,118 +1,94 @@
<template>
<view
class="flex h-full flex-col items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 px-6 pb-20 overflow-hidden"
>
<!-- Logo Section -->
<view class="mb-6 flex flex-col items-center">
<view
class="mb-4 flex h-20 w-60 items-center justify-center rounded-2xl bg-gray-800 px-4 shadow-lg"
>
<image
class="h-16 w-full"
:src="globalConfig.appInfo.logo"
mode="aspectFit"
/>
</view>
<text class="text-xl font-bold tracking-wide text-gray-800"
>智链云枢管理系统登录</text
>
</view>
<view class="login-page">
<image
class="login-background"
src="/static/login-background.jpg"
mode="aspectFill"
/>
<view class="login-scrim" />
<!-- Form Section -->
<view class="w-full rounded-3xl bg-white/80 p-6 shadow-xl backdrop-blur-md">
<!-- Username -->
<view class="group relative mb-5">
<view
class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors group-focus-within:text-blue-500"
>
<view class="i-mdi-account text-xl"></view>
</view>
<input
v-model="loginForm.username"
class="h-12 w-full rounded-xl bg-gray-50 pl-12 pr-4 text-sm text-gray-700 outline-none transition-all focus:bg-white focus:ring-2 focus:ring-blue-400"
type="text"
placeholder="请输入账号"
maxlength="30"
/>
</view>
<!-- Password -->
<view class="group relative mb-5">
<view
class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors group-focus-within:text-blue-500"
>
<view class="i-mdi-lock text-xl"></view>
</view>
<input
v-model="loginForm.password"
type="password"
class="h-12 w-full rounded-xl bg-gray-50 pl-12 pr-4 text-sm text-gray-700 outline-none transition-all focus:bg-white focus:ring-2 focus:ring-blue-400"
placeholder="请输入密码"
maxlength="20"
/>
</view>
<!-- Captcha -->
<view
class="mb-8 flex items-center justify-between"
v-if="captchaEnabled"
>
<view class="group relative mr-3 flex-1">
<view
class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors group-focus-within:text-blue-500"
>
<view class="i-mdi-security text-xl"></view>
<view class="login-content">
<view class="login-card">
<view class="brand-lockup">
<view class="brand-logo-frame">
<image
class="brand-logo"
:src="globalConfig.appInfo.logo"
mode="aspectFit"
/>
</view>
<input
v-model="loginForm.code"
type="number"
class="h-12 w-full rounded-xl bg-gray-50 pl-12 pr-4 text-sm text-gray-700 outline-none transition-all focus:bg-white focus:ring-2 focus:ring-blue-400"
placeholder="验证码"
maxlength="4"
/>
<text class="brand-title">{{ globalConfig.appInfo.name }}</text>
<text class="brand-subtitle">仓储运营管理平台</text>
</view>
<view
class="h-12 w-28 overflow-hidden rounded-xl bg-gray-100 shadow-sm transition-opacity active:opacity-80"
@click="getCode"
>
<image :src="codeUrl" class="size-full object-cover"></image>
<view class="login-form">
<view class="field-control">
<view class="field-icon i-mdi-account" />
<input
v-model="loginForm.username"
class="field-input"
type="text"
placeholder="请输入您的账号"
maxlength="30"
confirm-type="next"
/>
</view>
<view class="field-control">
<view class="field-icon i-mdi-lock" />
<input
v-model="loginForm.password"
class="field-input"
type="password"
placeholder="请输入您的密码"
maxlength="20"
confirm-type="done"
@confirm="handleLogin"
/>
</view>
<view v-if="captchaEnabled" class="captcha-row">
<view class="field-control captcha-input">
<view class="field-icon i-mdi-security" />
<input
v-model="loginForm.code"
class="field-input"
type="number"
placeholder="请输入验证码"
maxlength="4"
confirm-type="done"
@confirm="handleLogin"
/>
</view>
<view class="captcha-image-wrap" @click="getCode">
<image
:src="codeUrl"
class="captcha-image"
mode="aspectFill"
/>
</view>
</view>
<button
class="login-button"
:loading="loading"
:disabled="loading"
@click="handleLogin"
>
{{ loading ? "登录中..." : "登 录" }}
</button>
</view>
</view>
<!-- Login Button -->
<button
@click="handleLogin"
class="flex h-12 w-full items-center justify-center rounded-xl bg-gradient-to-r from-blue-500 to-indigo-600 text-base font-semibold text-white shadow-lg shadow-blue-500/30 transition-transform active:scale-95"
>
登 录
</button>
<!-- Footer Links -->
<view class="mt-6 flex flex-col items-center space-y-3">
<view class="flex items-center text-sm text-gray-500" v-if="register">
<view v-if="register" class="register-tip">
<text>没有账号?</text>
<text
@click="handleUserRegister"
class="ml-1 font-medium text-blue-600 active:opacity-70"
>立即注册</text
>
<text class="action-link" @click="handleUserRegister">立即注册</text>
</view>
<view
class="flex flex-wrap items-center justify-center text-xs text-gray-400"
>
<view class="agreement-tip">
<text>登录即代表同意</text>
<text
@click="handleUserAgrement"
class="mx-1 text-blue-500 active:opacity-70"
>《用户协议》</text
>
<text class="action-link" @click="handleUserAgrement">《用户协议》</text>
<text>和</text>
<text
@click="handlePrivacy"
class="mx-1 text-blue-500 active:opacity-70"
>《隐私协议》</text
>
<text class="action-link" @click="handlePrivacy">《隐私协议》</text>
</view>
</view>
</view>
@@ -129,6 +105,7 @@ import { useConfigStore, useUserStore } from "@/store";
const { proxy } = getCurrentInstance();
const globalConfig = useConfigStore().config;
const codeUrl = ref("");
const loading = ref(false);
// 验证码开关
const captchaEnabled = ref(true);
// 用户注册开关
@@ -160,6 +137,8 @@ function getCode() {
getCodeImg().then((res) => {
captchaEnabled.value =
res.captchaEnabled === undefined ? true : res.captchaEnabled;
register.value =
res.registerEnabled === undefined ? false : res.registerEnabled;
if (captchaEnabled.value) {
codeUrl.value = "data:image/gif;base64," + res.img;
loginForm.value.uuid = res.uuid;
@@ -176,6 +155,7 @@ async function handleLogin() {
} else if (loginForm.value.code === "" && captchaEnabled.value) {
proxy.$modal.msgError("请输入验证码");
} else {
loading.value = true;
proxy.$modal.loading("登录中,请耐心等待...");
pwdLogin();
}
@@ -183,25 +163,26 @@ async function handleLogin() {
// 密码登录
async function pwdLogin() {
useUserStore()
.login(loginForm.value)
.then(() => {
proxy.$modal.closeLoading();
loginSuccess();
})
.catch(() => {
if (captchaEnabled.value) {
getCode();
}
});
try {
await useUserStore().login(loginForm.value);
proxy.$modal.closeLoading();
loading.value = false;
loginSuccess();
} catch (error) {
proxy.$modal.closeLoading();
loading.value = false;
if (captchaEnabled.value) {
getCode();
}
}
}
// 登录成功后,处理函数
function loginSuccess(result) {
function loginSuccess() {
// 设置用户信息
useUserStore()
.getInfo()
.then((res) => {
.then(() => {
proxy.$tab.reLaunch("/pages/index");
});
}
@@ -219,6 +200,239 @@ getCode();
<style lang="scss" scoped>
page {
background-color: #ffffff;
background-color: #eef8f8;
}
.login-page {
position: relative;
display: flex;
min-height: 100vh;
align-items: center;
justify-content: center;
overflow: hidden;
padding: 48rpx 32rpx;
background: #eef8f8;
}
.login-background,
.login-scrim {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.login-background {
z-index: 0;
}
.login-scrim {
z-index: 1;
background: rgba(241, 250, 250, 0.22);
}
.login-content {
position: relative;
z-index: 2;
display: flex;
width: 100%;
max-width: 720rpx;
justify-content: center;
}
.login-card {
width: 100%;
border: 1rpx solid rgba(255, 255, 255, 0.88);
border-radius: 20rpx;
padding: 56rpx 44rpx 42rpx;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 20rpx 60rpx rgba(40, 34, 92, 0.18);
}
.brand-lockup {
display: flex;
align-items: center;
margin-bottom: 46rpx;
text-align: center;
flex-direction: column;
}
.brand-logo-frame {
display: flex;
width: 400rpx;
max-width: 100%;
height: 136rpx;
box-sizing: border-box;
align-items: center;
justify-content: center;
border: 1rpx solid rgba(255, 255, 255, 0.12);
border-radius: 18rpx;
padding: 16rpx 22rpx;
background: #28225c;
box-shadow: 0 10rpx 22rpx rgba(40, 34, 92, 0.16);
}
.brand-logo {
width: 100%;
height: 100%;
}
.brand-title {
margin-top: 18rpx;
color: #28225c;
font-size: 34rpx;
font-weight: 600;
line-height: 1.35;
}
.brand-subtitle {
margin-top: 10rpx;
color: #6c7786;
font-size: 24rpx;
line-height: 1.4;
}
.login-form {
width: 100%;
}
.field-control {
display: flex;
align-items: center;
height: 96rpx;
margin-bottom: 24rpx;
border: 1rpx solid #d9e6e7;
border-radius: 10rpx;
padding: 0 24rpx;
background: #ffffff;
transition: border-color 160ms ease, box-shadow 160ms ease;
}
.field-control:focus-within {
border-color: #008596;
box-shadow: 0 0 0 4rpx rgba(0, 133, 150, 0.1);
}
.field-icon {
flex: 0 0 36rpx;
margin-right: 18rpx;
color: #008596;
font-size: 36rpx;
}
.field-input {
flex: 1;
min-width: 0;
height: 96rpx;
color: #28333f;
font-size: 28rpx;
line-height: 96rpx;
}
.captcha-row {
display: flex;
align-items: center;
gap: 18rpx;
}
.captcha-input {
flex: 1;
min-width: 0;
}
.captcha-image-wrap {
flex: 0 0 220rpx;
width: 220rpx;
height: 96rpx;
margin-bottom: 24rpx;
overflow: hidden;
border: 1rpx solid #d9e6e7;
border-radius: 10rpx;
background: #f4f8f8;
}
.captcha-image {
width: 100%;
height: 100%;
}
.login-button {
width: 100%;
height: 96rpx;
margin: 8rpx 0 0;
border: 0;
border-radius: 10rpx;
background: #008596;
box-shadow: 0 12rpx 24rpx rgba(0, 133, 150, 0.2);
color: #ffffff;
font-size: 30rpx;
font-weight: 600;
line-height: 96rpx;
transition: opacity 160ms ease, transform 160ms ease;
}
.login-button::after {
border: 0;
}
.login-button:active {
opacity: 0.84;
transform: translateY(1rpx);
}
.login-button[disabled] {
opacity: 0.72;
}
.register-tip,
.agreement-tip {
display: flex;
align-items: center;
justify-content: center;
color: #7b8791;
font-size: 24rpx;
line-height: 1.6;
text-align: center;
}
.register-tip {
margin-top: 30rpx;
}
.agreement-tip {
flex-wrap: wrap;
margin-top: 22rpx;
color: #9aa5ad;
font-size: 22rpx;
}
.action-link {
margin: 0 6rpx;
color: #008596;
}
.action-link:active {
opacity: 0.65;
}
@media screen and (max-width: 420px) {
.login-page {
padding: 32rpx 24rpx;
}
.login-card {
padding: 44rpx 28rpx 34rpx;
}
.brand-logo-frame {
width: 360rpx;
height: 122rpx;
padding: 14rpx 20rpx;
}
.captcha-image-wrap {
flex-basis: 190rpx;
width: 190rpx;
}
}
</style>
+320 -311
View File
@@ -1,231 +1,62 @@
<template>
<view class="flex h-full flex-col bg-gray-50 overflow-hidden">
<scroll-view scroll-y class="flex-1" :show-scrollbar="false">
<view class="p-4 pb-24 space-y-6">
<view class="work-page">
<scroll-view
class="work-scroll"
scroll-y
:show-scrollbar="false"
>
<view class="work-content">
<view class="work-heading">
<view class="work-heading-copy">
<text class="work-kicker">业务工作台</text>
<text class="work-subtitle">
根据当前账号权限,快速进入现场业务流程
</text>
</view>
<view class="work-permission-count">
<text class="work-permission-value">{{ visibleBusinessEntries.length }}</text>
<text class="work-permission-label">项可用</text>
</view>
</view>
<TestBatchModeSwitch />
<!-- Banner -->
<view
class="relative h-40 w-full overflow-hidden rounded-2xl shadow-lg shadow-indigo-100"
>
<swiper
class="h-40 w-full"
:current="swiperDotIndex"
@change="changeSwiper"
autoplay
interval="3000"
circular
>
<swiper-item v-for="(item, index) in data" :key="index">
<view class="h-full w-full" @click="clickBannerItem(item)">
<image
:src="item.image"
mode="scaleToFill"
class="block h-full w-full"
/>
</view>
</swiper-item>
</swiper>
<!-- Custom Dots -->
<view
class="absolute bottom-3 right-0 left-0 flex justify-center space-x-2 pointer-events-none"
>
<view
v-for="(item, index) in data"
:key="index"
class="h-1.5 rounded-full transition-all duration-300"
:class="current === index ? 'bg-white w-4' : 'bg-white/50 w-1.5'"
>
<view class="work-section">
<view class="work-section-header">
<view class="work-section-marker"></view>
<view class="work-section-copy">
<text class="work-section-title">业务管理</text>
<text class="work-section-description">选择需要执行的现场流程</text>
</view>
</view>
</view>
<!-- AI Tools Section -->
<view>
<view class="mb-4 flex items-center space-x-2">
<view class="h-4 w-1 rounded-full bg-indigo-500"></view>
<text class="text-base font-bold text-gray-800">AI 生产力</text>
</view>
<view class="grid grid-cols-2 gap-4">
<view v-if="visibleBusinessEntries.length" class="work-entry-list">
<view
class="group relative overflow-hidden rounded-2xl bg-white p-5 shadow-sm transition-all active:scale-95"
@click="handleToAiChat"
v-for="entry in visibleBusinessEntries"
:key="entry.path"
class="work-entry"
@click="handleBusinessEntry(entry)"
>
<view
class="absolute -right-4 -top-4 size-20 rounded-full bg-indigo-50 opacity-50"
></view>
<view class="relative z-10 flex flex-col">
<view
class="mb-3 flex size-10 items-center justify-center rounded-xl bg-indigo-500 text-white shadow-md shadow-indigo-200"
>
<view class="i-mdi-chat text-xl"></view>
</view>
<text class="font-bold text-gray-800">智能对话</text>
<text class="mt-1 text-xs text-gray-500">智能助手</text>
class="work-entry-icon"
:class="entry.iconClass"
>
<view :class="entry.icon" class="work-entry-icon-glyph"></view>
</view>
</view>
<view
class="group relative overflow-hidden rounded-2xl bg-white p-5 shadow-sm transition-all active:scale-95"
@click="handleBuilding"
>
<view
class="absolute -right-4 -top-4 size-20 rounded-full bg-purple-50 opacity-50"
></view>
<view class="relative z-10 flex flex-col">
<view
class="mb-3 flex size-10 items-center justify-center rounded-xl bg-purple-500 text-white shadow-md shadow-purple-200"
>
<view class="i-mdi-image-multiple text-xl"></view>
</view>
<text class="font-bold text-gray-800">图像生成</text>
<text class="mt-1 text-xs text-gray-500">创意工坊</text>
<view class="work-entry-copy">
<text class="work-entry-title">{{ entry.title }}</text>
<text class="work-entry-description">{{ entry.description }}</text>
</view>
<view class="i-mdi-chevron-right work-entry-arrow"></view>
</view>
</view>
</view>
<!-- PDA Business Section -->
<view v-if="visibleBusinessEntries.length">
<view class="mb-4 flex items-center space-x-2">
<view class="h-4 w-1 rounded-full bg-emerald-500"></view>
<text class="text-base font-bold text-gray-800">现场业务</text>
</view>
<view class="rounded-2xl bg-white p-5 shadow-sm">
<view class="grid grid-cols-3 gap-4">
<view
v-for="entry in visibleBusinessEntries"
:key="entry.path"
class="flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBusinessEntry(entry)"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-emerald-50 text-emerald-600"
>
<view :class="entry.icon" class="text-xl"></view>
</view>
<text class="text-center text-[11px] font-medium text-gray-600">
{{ entry.title }}
</text>
<text class="text-center text-[10px] text-gray-400">
{{ entry.description }}
</text>
</view>
</view>
</view>
</view>
<!-- System Management Section -->
<view>
<view class="mb-4 flex items-center space-x-2">
<view class="h-4 w-1 rounded-full bg-blue-500"></view>
<text class="text-base font-bold text-gray-800">系统管理</text>
</view>
<view class="rounded-2xl bg-white p-5 shadow-sm">
<view class="flex flex-wrap justify-between gap-y-4">
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleToUserList"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-blue-50 text-blue-500 transition-colors group-active:bg-blue-100"
>
<view class="i-mdi-account text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>用户管理</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-orange-50 text-orange-500 transition-colors group-active:bg-orange-100"
>
<view class="i-mdi-badge-account text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>角色管理</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-green-50 text-green-500 transition-colors group-active:bg-green-100"
>
<view class="i-mdi-office-building text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>部门管理</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-red-50 text-red-500 transition-colors group-active:bg-red-100"
>
<view class="i-mdi-cog text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>配置管理</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-cyan-50 text-cyan-500 transition-colors group-active:bg-cyan-100"
>
<view class="i-mdi-book-open-page-variant text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>字典管理</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-yellow-50 text-yellow-500 transition-colors group-active:bg-yellow-100"
>
<view class="i-mdi-bullhorn text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>通知公告</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-pink-50 text-pink-500 transition-colors group-active:bg-pink-100"
>
<view class="i-mdi-file-document-outline text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>日志管理</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-gray-50 text-gray-500 transition-colors group-active:bg-gray-100"
>
<view class="i-mdi-dots-horizontal text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600">更多</text>
</view>
<view v-else class="work-empty-state">
<view class="work-empty-icon">
<view class="i-mdi-lock-outline work-empty-icon-glyph"></view>
</view>
<text class="work-empty-title">暂无已授权业务</text>
<text class="work-empty-description">请联系管理员分配业务菜单权限</text>
</view>
</view>
</view>
@@ -234,113 +65,291 @@
</template>
<script setup>
import { computed, ref, getCurrentInstance } from "vue";
import { computed, getCurrentInstance } from "vue";
import TestBatchModeSwitch from "@/components/business/TestBatchModeSwitch.vue";
import { businessMenuEntries } from "@/config/businessMenu";
const { proxy } = getCurrentInstance();
const current = ref(0);
const swiperDotIndex = ref(0);
const data = ref([
{ image: "/static/images/banner/banner01.jpg" },
{ image: "/static/images/banner/banner02.jpg" },
{ image: "/static/images/banner/banner03.jpg" },
]);
const businessEntries = [
{
path: "/pages/business/inbound/index",
title: "仓库入库",
description: "扫描托盘登记入库",
icon: "i-mdi-package-down",
color: "blue",
permissions: [
"business:inbound:list",
"business:inbound:query",
"business:inbound:add",
"business:inbound:edit",
"business:inbound:confirm",
],
},
{
path: "/pages/business/filling/index",
title: "托盘装填",
description: "录入菌种和包数",
icon: "i-mdi-package-variant-closed-plus",
color: "green",
permissions: [
"business:filling:list",
"business:filling:query",
"business:filling:add",
],
},
{
path: "/pages/business/shipment/index",
title: "发货出库",
description: "扫码创建发货单",
icon: "i-mdi-truck-delivery",
color: "orange",
permissions: [
"business:shipment:list",
"business:shipment:query",
"business:shipment:add",
"business:shipment:edit",
"business:shipment:confirm",
],
},
{
path: "/pages/business/usage/index",
title: "现场使用",
description: "绑定产线记录消耗",
icon: "i-mdi-factory",
color: "purple",
permissions: [
"business:usage:list",
"business:usage:query",
"business:usage:add",
],
},
{
path: "/pages/business/return/index",
title: "退回仓库",
description: "申请和确认退回",
icon: "i-mdi-undo-variant",
color: "red",
permissions: [
"business:return:list",
"business:return:query",
"business:return:request",
"business:return:confirm",
],
},
];
const visibleBusinessEntries = computed(() =>
businessEntries.filter((entry) => proxy.$auth.hasPermiOr(entry.permissions)),
businessMenuEntries.filter((entry) => proxy.$auth.hasPermi(entry.permission)),
);
function clickBannerItem(item) {
console.info(item);
}
function changeSwiper(e) {
current.value = e.detail.current;
}
function handleToAiChat() {
handleBuilding();
}
function handleToUserList() {
handleBuilding();
}
function handleBusinessEntry(entry) {
if (!proxy.$auth.hasPermiOr(entry.permissions)) {
proxy.$modal.msgError("当前账号没有该业务权限");
if (!proxy.$auth.hasPermi(entry.permission)) {
proxy.$modal.msgError("当前账号没有该业务菜单权限");
return;
}
proxy.$tab.navigateTo(entry.path);
}
function handleBuilding() {
proxy.$modal.msg("模块建设中~");
}
</script>
<style lang="scss" scoped>
.work-page {
display: flex;
width: 100%;
height: 100%;
min-width: 0;
flex-direction: column;
overflow-x: hidden;
background: #f4f8f8;
}
.work-scroll {
width: 100%;
min-width: 0;
flex: 1;
}
.work-content {
width: 100%;
min-width: 0;
box-sizing: border-box;
padding: 24rpx 28rpx calc(140rpx + env(safe-area-inset-bottom));
}
.work-heading {
display: flex;
min-width: 0;
align-items: flex-start;
justify-content: space-between;
gap: 18rpx;
margin-bottom: 20rpx;
}
.work-heading-copy {
min-width: 0;
flex: 1;
}
.work-kicker {
display: block;
color: #28225c;
font-size: 38rpx;
font-weight: 700;
line-height: 1.3;
}
.work-subtitle {
display: block;
margin-top: 10rpx;
color: #6c7786;
font-size: 24rpx;
line-height: 1.55;
overflow-wrap: anywhere;
}
.work-permission-count {
display: flex;
flex: 0 0 auto;
align-items: baseline;
border-radius: 14rpx;
padding: 12rpx 16rpx;
background: #e9f7f6;
color: #008596;
flex-direction: column;
text-align: right;
}
.work-permission-value {
font-size: 34rpx;
font-weight: 700;
line-height: 1.1;
}
.work-permission-label {
margin-top: 4rpx;
font-size: 20rpx;
line-height: 1.2;
}
.work-section {
width: 100%;
min-width: 0;
box-sizing: border-box;
margin-top: 20rpx;
border-radius: 20rpx;
padding: 24rpx;
background: #ffffff;
box-shadow: 0 8rpx 24rpx rgba(40, 34, 92, 0.06);
}
.work-section-header {
display: flex;
min-width: 0;
align-items: flex-start;
gap: 14rpx;
}
.work-section-marker {
width: 8rpx;
height: 38rpx;
flex: 0 0 8rpx;
border-radius: 999rpx;
background: #008596;
}
.work-section-copy {
min-width: 0;
flex: 1;
}
.work-section-title {
display: block;
color: #28225c;
font-size: 30rpx;
font-weight: 700;
line-height: 1.35;
}
.work-section-description {
display: block;
margin-top: 6rpx;
color: #8b969d;
font-size: 22rpx;
line-height: 1.5;
}
.work-entry-list {
display: flex;
min-width: 0;
margin-top: 20rpx;
gap: 12rpx;
flex-direction: column;
}
.work-entry {
display: flex;
width: 100%;
min-width: 0;
box-sizing: border-box;
align-items: center;
gap: 16rpx;
border: 1rpx solid #e5eeee;
border-radius: 16rpx;
padding: 18rpx;
background: #fbfdfd;
}
.work-entry:active {
border-color: #8ccbc5;
background: #f0faf9;
}
.work-entry-icon {
display: flex;
width: 82rpx;
height: 82rpx;
flex: 0 0 82rpx;
align-items: center;
justify-content: center;
border-radius: 18rpx;
}
.work-entry-icon-glyph {
font-size: 42rpx;
}
.work-entry-copy {
min-width: 0;
flex: 1;
}
.work-entry-title,
.work-entry-description {
display: block;
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
}
.work-entry-title {
color: #27313a;
font-size: 27rpx;
font-weight: 700;
line-height: 1.4;
}
.work-entry-description {
margin-top: 7rpx;
color: #7b8791;
font-size: 22rpx;
line-height: 1.5;
}
.work-entry-arrow {
flex: 0 0 34rpx;
color: #a9b7bb;
font-size: 34rpx;
}
.work-empty-state {
display: flex;
align-items: center;
padding: 70rpx 16rpx 54rpx;
flex-direction: column;
text-align: center;
}
.work-empty-icon {
display: flex;
width: 84rpx;
height: 84rpx;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #f1f5f5;
color: #9aa5ad;
}
.work-empty-icon-glyph {
font-size: 42rpx;
}
.work-empty-title {
display: block;
margin-top: 20rpx;
color: #4b5563;
font-size: 26rpx;
font-weight: 600;
}
.work-empty-description {
display: block;
margin-top: 8rpx;
color: #9aa5ad;
font-size: 22rpx;
line-height: 1.5;
overflow-wrap: anywhere;
}
@media screen and (max-width: 420px) {
.work-content {
padding-right: 20rpx;
padding-left: 20rpx;
}
.work-kicker {
font-size: 34rpx;
}
.work-section {
padding: 20rpx;
}
.work-entry {
gap: 12rpx;
padding: 14rpx;
}
.work-entry-icon {
width: 72rpx;
height: 72rpx;
flex-basis: 72rpx;
}
.work-entry-icon-glyph {
font-size: 36rpx;
}
}
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

@@ -159,21 +159,95 @@ test("PDA 路由和工作台入口覆盖五类现场业务", async () => {
assert.equal(pagePaths.has(pagePath), true, `${pagePath} 路由缺失`);
}
const workSource = await readFile(
path.join(appRoot, "src/pages/work/index.vue"),
"utf8",
const [workSource, menuSource] = await Promise.all([
readFile(path.join(appRoot, "src/pages/work/index.vue"), "utf8"),
readFile(path.join(appRoot, "src/config/businessMenu.js"), "utf8"),
]);
assert.match(workSource, /hasPermi\(entry\.permission\)/);
assert.match(menuSource, /business:inbound:list/);
assert.match(menuSource, /business:filling:list/);
assert.match(menuSource, /business:shipment:list/);
assert.match(menuSource, /business:usage:list/);
assert.match(menuSource, /business:return:list/);
assert.match(menuSource, /pages\/business\/inbound\/index/);
assert.match(menuSource, /pages\/business\/filling\/index/);
assert.match(menuSource, /pages\/business\/shipment\/index/);
assert.match(menuSource, /pages\/business\/usage\/index/);
assert.match(menuSource, /pages\/business\/return\/index/);
});
test("首页展示真实运营驾驶舱,不保留模板入口和虚假统计", async () => {
const source = await readFile(path.join(appRoot, "src/pages/index.vue"), "utf8");
assert.match(source, /listScanExceptions/);
assert.match(source, /最近扫码异常/);
assert.match(source, /TestBatchBanner/);
assert.doesNotMatch(
source,
/快捷入口|新闻|计划|请假申请|周报|工作效率|新通知|待办任务|85%/,
);
assert.match(workSource, /hasPermiOr/);
assert.match(workSource, /business:inbound/);
assert.match(workSource, /business:filling/);
assert.match(workSource, /business:shipment/);
assert.match(workSource, /business:usage/);
assert.match(workSource, /business:return/);
assert.match(workSource, /pages\/business\/inbound\/index/);
assert.match(workSource, /pages\/business\/filling\/index/);
assert.match(workSource, /pages\/business\/shipment\/index/);
assert.match(workSource, /pages\/business\/usage\/index/);
assert.match(workSource, /pages\/business\/return\/index/);
});
test("工作台只展示真实业务入口并使用后端列表权限", async () => {
const [workSource, menuModule] = await Promise.all([
readFile(path.join(appRoot, "src/pages/work/index.vue"), "utf8"),
importSource("src/config/businessMenu.js"),
]);
assert.deepEqual(
menuModule.businessMenuEntries.map(({ title, permission, path: entryPath }) => ({
title,
permission,
path: entryPath,
})),
[
{
title: "仓库入库",
permission: "business:inbound:list",
path: "/pages/business/inbound/index",
},
{
title: "工厂装填",
permission: "business:filling:list",
path: "/pages/business/filling/index",
},
{
title: "发货出库",
permission: "business:shipment:list",
path: "/pages/business/shipment/index",
},
{
title: "客户现场使用",
permission: "business:usage:list",
path: "/pages/business/usage/index",
},
{
title: "退回仓库",
permission: "business:return:list",
path: "/pages/business/return/index",
},
],
);
assert.match(workSource, /businessMenuEntries/);
assert.match(workSource, /hasPermi\(entry\.permission\)/);
assert.match(workSource, /navigateTo\(entry\.path\)/);
assert.doesNotMatch(workSource, /AI 生产力|智能对话|图像生成|系统管理|handleBuilding|模块建设中/);
});
test("运营驾驶舱 API 对接现有扫码异常接口", async () => {
const source = await readFile(path.join(appRoot, "src/api/business/dashboard.js"), "utf8");
assert.match(source, /export function listScanExceptions\(/);
assert.match(source, /url:\s*["']\/business\/dashboard\/scan-exceptions["']/);
assert.match(source, /method:\s*["']get["']/);
});
test("APP 页面标题与后端业务菜单名称一致", async () => {
const [pages, fillingSource] = await Promise.all([
JSON.parse(await readFile(path.join(appRoot, "src/pages.json"), "utf8")),
readFile(path.join(appRoot, "src/pages/business/filling/index.vue"), "utf8"),
]);
const fillingPage = pages.pages.find((page) => page.path === "pages/business/filling/index");
assert.equal(fillingPage?.style?.navigationBarTitleText, "工厂装填");
assert.match(fillingSource, />工厂装填</);
});
test("PDA 测试模式附加批次请求头并在五个业务页持续提示", async () => {
@@ -0,0 +1,79 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";
const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const readSource = (relativePath) =>
readFileSync(resolve(appRoot, relativePath), "utf8");
test("APP 首页展示 PC 端核心运营统计并保护移动端横向空间", () => {
const source = readSource("src/pages/index.vue");
for (const className of [
"dashboard-overview",
"dashboard-kpi-grid",
"dashboard-trend-card",
"dashboard-distribution-card",
"dashboard-trend-range",
"dashboard-status-list",
]) {
assert.match(source, new RegExp(`class=\\"[^\\"]*${className}[^\\"]*\\"`));
}
for (const label of [
"当前库存托盘",
"剩余包数",
"今日入库单",
"今日出库单",
"待确认单据",
"入库 / 出库趋势",
"托盘流转状态分布",
]) {
assert.match(source, new RegExp(label));
}
assert.match(source, /listPalletInventory/);
assert.match(source, /listShipmentReport/);
assert.match(source, /listInbound/);
assert.match(source, /listShipment/);
assert.match(source, /listReturn/);
assert.match(source, /TREND_RANGE_OPTIONS/);
assert.match(source, /overflow-x:\s*hidden/);
assert.match(source, /min-width:\s*0/);
});
test("APP 首页统计保留加载、空数据和部分失败状态", () => {
const source = readSource("src/pages/index.vue");
assert.match(source, /statsLoading/);
assert.match(source, /statsError/);
assert.match(source, /dashboard-stat-empty/);
assert.match(source, /dashboard-stat-error/);
assert.match(source, /formatMetric/);
assert.match(source, /--/);
});
test("APP 首页扫码异常支持默认收起并通过标题展开", () => {
const source = readSource("src/pages/index.vue");
assert.match(source, /const exceptionsExpanded = ref\(false\)/);
assert.match(source, /function toggleExceptionList\(\)/);
assert.match(source, /class="[^"]*exception-header[^"]*"/);
assert.match(source, /@click="toggleExceptionList"/);
assert.match(source, /exception-toggle-icon/);
assert.match(source, /v-else-if="exceptionsExpanded" class="exception-list"/);
});
test("APP 首页报表 API 复用现有只读报表路径", () => {
const inventorySource = readSource("src/api/report/palletInventory.js");
const shipmentSource = readSource("src/api/report/shipment.js");
assert.match(inventorySource, /export function listPalletInventory\(/);
assert.match(inventorySource, /url:\s*["']\/report\/pallet-inventory\/list["']/);
assert.match(inventorySource, /method:\s*["']get["']/);
assert.match(shipmentSource, /export function listShipmentReport\(/);
assert.match(shipmentSource, /url:\s*["']\/report\/shipment\/list["']/);
assert.match(shipmentSource, /method:\s*["']get["']/);
});
@@ -0,0 +1,38 @@
import assert from "node:assert/strict";
import { existsSync, readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";
const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const loginPagePath = resolve(appRoot, "src/pages/login.vue");
const loginPageSource = readFileSync(loginPagePath, "utf8");
test("APP 登录页沿用 PC 端的品牌背景与登录卡片结构", () => {
assert.match(loginPageSource, /class="login-page"/);
assert.match(loginPageSource, /\/static\/login-background\.jpg/);
assert.match(loginPageSource, /class="login-card"/);
assert.match(loginPageSource, /globalConfig\.appInfo\.name/);
assert.doesNotMatch(
loginPageSource,
/bg-gradient-to-br from-blue-50 to-indigo-100/,
);
assert.ok(
existsSync(resolve(appRoot, "src/static/login-background.jpg")),
"APP 登录页需要包含与 PC 端一致的背景资源",
);
});
test("APP 登录页保留验证码刷新与回车登录交互", () => {
assert.match(loginPageSource, /@click="getCode"/);
assert.match(loginPageSource, /@confirm="handleLogin"/);
assert.match(loginPageSource, /captchaEnabled/);
});
test("APP 登录页为白色 Sylvan 字标提供深色承载层", () => {
assert.match(loginPageSource, /class="brand-logo-frame"/);
assert.match(
loginPageSource,
/\.brand-logo-frame\s*\{[\s\S]*background:\s*#28225c/,
);
});
@@ -0,0 +1,47 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";
const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const readSource = (relativePath) =>
readFileSync(resolve(appRoot, relativePath), "utf8");
const homeSource = readSource("src/pages/index.vue");
const workSource = readSource("src/pages/work/index.vue");
const bannerSource = readSource("src/components/business/TestBatchBanner.vue");
const modeSwitchSource = readSource(
"src/components/business/TestBatchModeSwitch.vue",
);
test("首页使用受约束的运营布局并保护横向空间", () => {
assert.match(homeSource, /class="dashboard-page"/);
assert.match(homeSource, /class="dashboard-scroll"/);
assert.match(homeSource, /class="dashboard-content"/);
assert.match(homeSource, /class="dashboard-hero"/);
assert.match(homeSource, /class="dashboard-metric-grid"/);
assert.match(homeSource, /class="exception-list"/);
assert.match(homeSource, /class="exception-item"/);
assert.match(homeSource, /overflow-x:\s*hidden/);
assert.match(homeSource, /overflow-wrap:\s*anywhere/);
});
test("工作台入口在窄屏下保持单列和可换行", () => {
assert.match(workSource, /class="work-page"/);
assert.match(workSource, /class="work-scroll"/);
assert.match(workSource, /class="work-content"/);
assert.match(workSource, /class="work-entry-list"/);
assert.match(workSource, /class="work-entry"/);
assert.match(workSource, /class="work-entry-copy"/);
assert.match(workSource, /overflow-x:\s*hidden/);
assert.match(workSource, /overflow-wrap:\s*anywhere/);
});
test("测试批次提示和切换区不会撑开页面", () => {
assert.match(bannerSource, /class="test-batch-banner"/);
assert.match(bannerSource, /overflow-wrap:\s*anywhere/);
assert.match(modeSwitchSource, /class="test-batch-switch"/);
assert.match(modeSwitchSource, /class="test-batch-picker"/);
assert.match(modeSwitchSource, /overflow-wrap:\s*anywhere/);
});