调整部分前端布局和后端增删改逻辑
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
# 复制为根目录 .env 后再修改;根目录 .env 已加入 Git 忽略。
|
||||
|
||||
# 对外端口(默认仅绑定本机)
|
||||
FRONTEND_HOST_PORT=8080
|
||||
MYSQL_HOST_PORT=13307
|
||||
|
||||
# MySQL
|
||||
MYSQL_DATABASE=shierfeng-fastapi
|
||||
MYSQL_ROOT_PASSWORD=change-this-root-password
|
||||
DB_USERNAME=shierfeng
|
||||
DB_PASSWORD=change-this-app-password
|
||||
|
||||
# Redis 与登录令牌
|
||||
REDIS_DATABASE=2
|
||||
JWT_SECRET_KEY=replace-with-a-long-random-secret
|
||||
|
||||
# 本地 Compose 默认关闭传输层加密;正式环境启用时必须另行注入正式密钥。
|
||||
TRANSPORT_CRYPTO_ENABLED=false
|
||||
TRANSPORT_CRYPTO_MODE=off
|
||||
|
||||
TZ=Asia/Shanghai
|
||||
@@ -0,0 +1 @@
|
||||
/.env
|
||||
@@ -136,7 +136,7 @@ shierfeng app run --env=dev
|
||||
密码:请在目标环境安全配置中设置后手工输入,不在仓库中记录
|
||||
|
||||
# 浏览器访问
|
||||
地址:http://localhost:80
|
||||
地址:http://localhost:8080
|
||||
```
|
||||
|
||||
### 发布
|
||||
@@ -163,10 +163,25 @@ shierfeng app run --env=prod
|
||||
|
||||
### Docker Compose部署方式
|
||||
|
||||
> ⚠️ **警告:** 默认未做数据持久化配置,请注意数据备份或自行配置持久化
|
||||
> ⚠️ **说明:** 根目录 Compose 使用 MySQL 和 Redis 命名卷持久化数据。初始化 SQL 只会在 MySQL 数据目录首次为空时执行。
|
||||
|
||||
#### MySQL版本
|
||||
#### MySQL 全栈版本
|
||||
|
||||
```bash
|
||||
docker compose -f shierfeng-fastapi-test/docker-compose.test.my.yml up -d --build
|
||||
# 首次使用时创建本地配置文件
|
||||
cp .env.docker.example .env
|
||||
# 编辑 .env,至少修改 MYSQL_ROOT_PASSWORD、DB_PASSWORD 和 JWT_SECRET_KEY
|
||||
|
||||
# 启动前只做静态配置校验
|
||||
docker compose config --quiet
|
||||
|
||||
# 构建并启动前端、后端、MySQL、Redis
|
||||
docker compose up -d --build
|
||||
|
||||
# 查看服务状态
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
浏览器访问:http://localhost:8080;宿主机连接 MySQL 时使用 `127.0.0.1:13307`。
|
||||
|
||||
已有数据库不能通过全量初始化脚本升级。请先备份并核对目标库,再按照 `shierfeng-fastapi-backend/sql/patches/` 下对应的 MySQL 增量补丁和部署文档执行;不要通过删除 Compose 数据卷来代替迁移。
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
services:
|
||||
shierfeng-mysql:
|
||||
image: mysql:8.0
|
||||
container_name: shierfeng-mysql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?请在 .env 中设置 MYSQL_ROOT_PASSWORD}
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-shierfeng-fastapi}
|
||||
MYSQL_USER: ${DB_USERNAME:-shierfeng}
|
||||
MYSQL_PASSWORD: ${DB_PASSWORD:?请在 .env 中设置 DB_PASSWORD}
|
||||
TZ: ${TZ:-Asia/Shanghai}
|
||||
command:
|
||||
- --character-set-server=utf8mb4
|
||||
- --collation-server=utf8mb4_general_ci
|
||||
- --skip-character-set-client-handshake=1
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
- ./shierfeng-fastapi-backend/sql/shierfeng-fastapi.sql:/docker-entrypoint-initdb.d/01-shierfeng-fastapi.sql:ro
|
||||
ports:
|
||||
- 127.0.0.1:${MYSQL_HOST_PORT:-13307}:3306
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- >-
|
||||
mysqladmin ping -h 127.0.0.1 -uroot
|
||||
-p"$${MYSQL_ROOT_PASSWORD}" --silent
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
start_period: 20s
|
||||
networks:
|
||||
- shierfeng-network
|
||||
|
||||
shierfeng-redis:
|
||||
image: redis:7-alpine
|
||||
container_name: shierfeng-redis
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- redis-server
|
||||
- --appendonly
|
||||
- 'yes'
|
||||
- --save
|
||||
- '60'
|
||||
- '1000'
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
healthcheck:
|
||||
test: [CMD, redis-cli, ping]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
start_period: 5s
|
||||
networks:
|
||||
- shierfeng-network
|
||||
|
||||
shierfeng-backend-my:
|
||||
build:
|
||||
context: ./shierfeng-fastapi-backend
|
||||
dockerfile: Dockerfile.my
|
||||
image: shierfeng-backend-my:latest
|
||||
container_name: shierfeng-backend-my
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
APP_ENV: dockermy
|
||||
APP_HOST: 0.0.0.0
|
||||
APP_PORT: '9099'
|
||||
APP_ROOT_PATH: /docker-api
|
||||
APP_RELOAD: 'false'
|
||||
APP_WORKERS: '1'
|
||||
APP_DEMO_MODE: 'false'
|
||||
APP_DISABLE_SWAGGER: 'true'
|
||||
APP_DISABLE_REDOC: 'true'
|
||||
DB_TYPE: mysql
|
||||
DB_HOST: shierfeng-mysql
|
||||
DB_PORT: '3306'
|
||||
DB_USERNAME: ${DB_USERNAME:-shierfeng}
|
||||
DB_PASSWORD: ${DB_PASSWORD:?请在 .env 中设置 DB_PASSWORD}
|
||||
DB_DATABASE: ${MYSQL_DATABASE:-shierfeng-fastapi}
|
||||
DB_ECHO: 'false'
|
||||
REDIS_HOST: shierfeng-redis
|
||||
REDIS_PORT: '6379'
|
||||
REDIS_USERNAME: ${REDIS_USERNAME:-}
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD:-}
|
||||
REDIS_DATABASE: ${REDIS_DATABASE:-2}
|
||||
JWT_SECRET_KEY: ${JWT_SECRET_KEY:?请在 .env 中设置 JWT_SECRET_KEY}
|
||||
TRANSPORT_CRYPTO_ENABLED: ${TRANSPORT_CRYPTO_ENABLED:-false}
|
||||
TRANSPORT_CRYPTO_MODE: ${TRANSPORT_CRYPTO_MODE:-off}
|
||||
LOG_FILE_ENABLED: 'false'
|
||||
LOGURU_STDOUT: 'true'
|
||||
expose:
|
||||
- '9099'
|
||||
depends_on:
|
||||
shierfeng-mysql:
|
||||
condition: service_healthy
|
||||
shierfeng-redis:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- >-
|
||||
python -c "import socket; s=socket.create_connection(('127.0.0.1', 9099), 2); s.close()"
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
start_period: 20s
|
||||
networks:
|
||||
- shierfeng-network
|
||||
|
||||
shierfeng-frontend:
|
||||
build:
|
||||
context: ./shierfeng-fastapi-frontend
|
||||
dockerfile: Dockerfile
|
||||
image: shierfeng-frontend:latest
|
||||
container_name: shierfeng-frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 127.0.0.1:${FRONTEND_HOST_PORT:-8080}:80
|
||||
volumes:
|
||||
- ./shierfeng-fastapi-frontend/bin/nginx.dockermy.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
shierfeng-backend-my:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- shierfeng-network
|
||||
|
||||
volumes:
|
||||
mysql-data:
|
||||
redis-data:
|
||||
|
||||
networks:
|
||||
shierfeng-network:
|
||||
name: shierfeng-network
|
||||
driver: bridge
|
||||
@@ -0,0 +1,77 @@
|
||||
# MySQL Initialization and Docker Compose Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Keep the MySQL fresh-install script aligned with the current business model and provide a safe, persistent Compose deployment for the frontend, backend, MySQL, and Redis.
|
||||
|
||||
**Architecture:** The existing MySQL initializer remains the single bootstrap source for a new empty database and is mounted into MySQL's first-start initialization directory. Existing database volumes are never migrated automatically; operators continue to use the reviewed MySQL patches separately. Compose keeps MySQL and Redis on an internal network, persists their data in named volumes, and reads credentials from an ignored local `.env` file created from a checked-in example.
|
||||
|
||||
**Tech Stack:** MySQL 8.0, Redis 7, existing FastAPI and Vue/Nginx Dockerfiles, Docker Compose v2, Python `unittest` schema contracts.
|
||||
|
||||
**Spec:** User request; schema/seed contract in `shierfeng-fastapi-backend/tests/module_business/test_sql_contract.py`; operational migration guidance in `docs/test-data-batch-operations.md` and `docs/pallet-workflow-deployment.md`.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- MySQL remains the only database engine; do not add PostgreSQL configuration or SQL files.
|
||||
- Do not execute SQL, apply patches, start containers, or remove/replace any database volume as part of this change.
|
||||
- The full initializer contains `DROP TABLE`; mount it only as a first-start script for a fresh MySQL data directory.
|
||||
- Keep existing seed records and schema unchanged when the current contract tests confirm they are synchronized.
|
||||
- Do not place real credentials in Compose or the checked-in example environment file.
|
||||
- Preserve unrelated staged, unstaged, and untracked work already in the repository.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Mark the MySQL initializer's safe-use boundary
|
||||
|
||||
**Files:**
|
||||
- Modify: `shierfeng-fastapi-backend/sql/shierfeng-fastapi.sql`
|
||||
- Test: `shierfeng-fastapi-backend/tests/module_business/test_sql_contract.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: current SQLAlchemy ORM table metadata and the existing MySQL SQL contract tests.
|
||||
- Produces: the same schema and seed statements, with a clear warning that this destructive bootstrap is only for an empty/new database.
|
||||
|
||||
- [x] Add a Chinese header warning that the script drops/recreates tables and is for a fresh empty database only; direct existing-database users to reviewed files under `sql/patches/`.
|
||||
- [x] Run the existing SQL contract suite and confirm all table, field, constraint, menu, and test-batch contracts still pass.
|
||||
- [x] Review the diff to confirm no seed rows or DDL changed beyond the warning.
|
||||
|
||||
### Task 2: Add a persistent full-stack Docker Compose deployment
|
||||
|
||||
**Files:**
|
||||
- Create: `docker-compose.yml`
|
||||
- Create: `.env.docker.example`
|
||||
- Modify: `.gitignore`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `Dockerfile.my`, the frontend `Dockerfile`, `nginx.dockermy.conf`, `.env.dockermy`, and the MySQL initializer.
|
||||
- Produces: Compose services `shierfeng-mysql`, `shierfeng-redis`, `shierfeng-backend-my`, and `shierfeng-frontend`; named volumes `mysql-data` and `redis-data`; required secrets supplied by root `.env`.
|
||||
|
||||
- [x] Define MySQL 8.0 with the fresh-install SQL mounted read-only at `/docker-entrypoint-initdb.d/01-schema-and-seed.sql`, an app-scoped database user, a health check, and a named data volume.
|
||||
- [x] Define Redis 7 with append-only persistence, a health check, and a named data volume; do not publish Redis to the host.
|
||||
- [x] Build the backend and frontend from the existing Dockerfiles. Configure backend DB/Redis service hosts and credentials through Compose environment overrides; make backend wait for healthy dependencies.
|
||||
- [x] Route browser API requests through the existing Nginx config. Publish the frontend on configurable host port 8080 by default and MySQL only on loopback port 13307 by default.
|
||||
- [x] Create `.env.docker.example` with placeholders for MySQL root password, app DB username/password, and JWT secret; add only root `/.env` to `.gitignore`.
|
||||
- [x] Run `docker compose config --quiet` with test-only environment values and confirm configuration resolves without starting services.
|
||||
|
||||
### Task 3: Document first-run and existing-database workflows
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: root Compose file and `.env.docker.example`.
|
||||
- Produces: concise setup commands and explicit distinction between fresh database initialization and existing-database patches.
|
||||
|
||||
- [x] Replace the root deployment instructions that point to the test-only Compose file with instructions to copy `.env.docker.example` to `.env`, set strong local credentials, validate with `docker compose config`, and start with `docker compose up -d --build`.
|
||||
- [x] Document the default frontend/MySQL addresses and state that MySQL initialization scripts run only when its data directory is empty; existing databases must use reviewed patches after backup.
|
||||
- [x] Do not include `docker compose down -v` or any other volume-deletion command in the documentation.
|
||||
- [x] Re-run the SQL contract suite and Compose config validation; do not run `docker compose up`.
|
||||
|
||||
## Verification Commands
|
||||
|
||||
```bash
|
||||
PYTHONDONTWRITEBYTECODE=1 /Users/jiaolongyan/miniconda3/envs/ShiErFeng/bin/python -m unittest tests.module_business.test_sql_contract -v
|
||||
MYSQL_ROOT_PASSWORD=compose-validation DB_USERNAME=compose_validation DB_PASSWORD=compose-validation JWT_SECRET_KEY=compose-validation docker compose config --quiet
|
||||
```
|
||||
|
||||
Do not run the SQL initializer, migration patches, or `docker compose up` during implementation verification.
|
||||
@@ -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",
|
||||
},
|
||||
];
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
@@ -117,6 +117,7 @@ class Log:
|
||||
log_type: Literal['login', 'operation'] | None = 'operation',
|
||||
request_log_mode: Literal['full', 'none', 'summary', 'include', 'exclude'] = 'full',
|
||||
response_log_mode: Literal['full', 'none', 'summary', 'include', 'exclude'] = 'full',
|
||||
oper_url_mode: Literal['full', 'route'] = 'full',
|
||||
request_include_fields: tuple[str, ...] | None = None,
|
||||
response_include_fields: tuple[str, ...] | None = None,
|
||||
request_exclude_fields: tuple[str, ...] | None = None,
|
||||
@@ -130,6 +131,7 @@ class Log:
|
||||
:param log_type: 日志类型;`login`表示登录日志,仅落登录信息,`operation`表示操作日志,会落请求/响应摘要与操作人信息
|
||||
:param request_log_mode: 请求日志记录模式;`full`记录脱敏后的完整请求,`none`不记录请求体,`summary`记录请求摘要,`include`仅记录request_include_fields指定的字段,`exclude`记录完整请求后排除request_exclude_fields指定的字段
|
||||
:param response_log_mode: 响应日志记录模式;`full`记录脱敏后的完整响应,`none`不记录响应体,`summary`记录响应摘要,`include`仅记录response_include_fields指定的字段,`exclude`记录完整响应后排除response_exclude_fields指定的字段
|
||||
:param oper_url_mode: 操作URL记录模式;`full`记录实际请求路径,`route`记录路由模板,适合路径中包含敏感标识的接口
|
||||
:param request_include_fields: 请求日志白名单字段路径,仅在request_log_mode='include'时生效;推荐从path_params/query_params/json_body/form_data/files/raw_body开始,字段名支持 snake_case 与 camelCase 自动兼容
|
||||
:param response_include_fields: 响应日志白名单字段路径,仅在response_log_mode='include'时生效;推荐从code/msg/data/rows/success/time开始,字段名支持 snake_case 与 camelCase 自动兼容
|
||||
:param request_exclude_fields: 请求日志排除字段路径,仅在request_log_mode='exclude'时生效;推荐从path_params/query_params/json_body/form_data/files/raw_body开始,字段名支持 snake_case 与 camelCase 自动兼容
|
||||
@@ -141,6 +143,7 @@ class Log:
|
||||
self.log_type = log_type
|
||||
self.request_log_mode = request_log_mode
|
||||
self.response_log_mode = response_log_mode
|
||||
self.oper_url_mode = oper_url_mode
|
||||
self.request_include_fields = request_include_fields or ()
|
||||
self.response_include_fields = response_include_fields or ()
|
||||
self.request_exclude_fields = request_exclude_fields or ()
|
||||
@@ -166,7 +169,7 @@ class Log:
|
||||
# 获取操作类型
|
||||
operator_type = self._get_oper_type(user_agent)
|
||||
# 获取请求的url
|
||||
oper_url = request.url.path
|
||||
oper_url = self._get_oper_url(request)
|
||||
# 获取请求ip
|
||||
oper_ip = ClientIPUtil.get_client_ip(request)
|
||||
# 获取请求ip归属区域
|
||||
@@ -281,6 +284,21 @@ class Log:
|
||||
|
||||
return func_path
|
||||
|
||||
def _get_oper_url(self, request: Request) -> str:
|
||||
"""
|
||||
获取操作日志中的请求URL。
|
||||
|
||||
路由模板模式只保留路由结构,不把路径参数(例如二维码固定标识)写入日志。
|
||||
|
||||
:param request: 当前请求
|
||||
:return: 用于操作日志的URL
|
||||
"""
|
||||
if self.oper_url_mode == 'route':
|
||||
route_path = getattr(request.scope.get('route'), 'path', None)
|
||||
if route_path:
|
||||
return route_path
|
||||
return request.url.path
|
||||
|
||||
@staticmethod
|
||||
def _limit_log_text(log_text: str, max_length: int, overflow_text: str) -> str:
|
||||
"""
|
||||
|
||||
@@ -13,3 +13,20 @@
|
||||
2026-09-26 19:22:28.409 | 8f088061d64445eb83100393320bba81 | de6a6a19267e474195c7b3d33334dcc5 | 58249b4d816b4a3b8833981f5aadebca | 35286-85260c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-26 19:22:28.411 | 20c430d179d44806a414717830bf1c44 | 07c1721f324e447a9d121d6e78518182 | 286523eb9ad7456697859a49d3c85381 | 35286-85260c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-26 21:43:11.533 | f639f0f70cf841baa341fff7fda02dbf | c08cd9bcfe1447f9809115a22a51c909 | 4b0d65ad896c4f79b36636218ef526de | 35286-85260c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-26 23:43:55.134 | | | | 35286-85260c | ERROR | sqlalchemy.pool.base:_close_connection:376 - Exception closing connection <AdaptedConnection <asyncmy.connection.Connection object at 0x120ddf370>>
|
||||
Traceback (most recent call last):
|
||||
File "/Users/jiaolongyan/miniconda3/envs/ShiErFeng/lib/python3.10/site-packages/sqlalchemy/pool/base.py", line 374, in _close_connection
|
||||
self._dialect.do_close(connection)
|
||||
File "/Users/jiaolongyan/miniconda3/envs/ShiErFeng/lib/python3.10/site-packages/sqlalchemy/engine/default.py", line 721, in do_close
|
||||
dbapi_connection.close()
|
||||
File "/Users/jiaolongyan/miniconda3/envs/ShiErFeng/lib/python3.10/site-packages/sqlalchemy/dialects/mysql/asyncmy.py", line 114, in close
|
||||
self.await_(self._connection.ensure_closed())
|
||||
File "/Users/jiaolongyan/miniconda3/envs/ShiErFeng/lib/python3.10/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 132, in await_only
|
||||
return current.parent.switch(awaitable) # type: ignore[no-any-return,attr-defined] # noqa: E501
|
||||
File "/Users/jiaolongyan/miniconda3/envs/ShiErFeng/lib/python3.10/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 196, in greenlet_spawn
|
||||
value = await result
|
||||
File "asyncmy/connection.pyx", line 367, in ensure_closed
|
||||
File "/Users/jiaolongyan/miniconda3/envs/ShiErFeng/lib/python3.10/asyncio/streams.py", line 343, in wait_closed
|
||||
await self._protocol._get_close_waiter(self)
|
||||
BrokenPipeError: [Errno 32] Broken pipe
|
||||
2026-09-27 00:05:01.664 | a9662ac3b2254f2d88ec8edf6e409c8d | 3ced5ce006d845db8dbec0c9accad850 | 7841be0637224ec5b48e2defe6f48baa | 35794-8a000d | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,56 @@
|
||||
2026-09-27 01:20:39.570 | aeca06e98b2a457bae7a555ebab9c3e4 | c7b5cc2cd8954f239f72235abb121b38 | 1f860582c3a7413c835d1b2312151742 | 36967-fe3b0d | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-27 01:30:36.841 | | | | 36967-fe3b0d | WARNING | config.get_scheduler:on_lock_lost:273 - ⚠️ Worker 36967-fe3b0d 失去 Application 锁
|
||||
2026-09-27 09:17:40.878 | efa6272b9713467b980b5e2279502288 | 257ee926db59431baeae83aa56d1fd3d | 7b08ae862c674486a217cbccc2da56ca | 36967-fe3b0d | WARNING | module_admin.service.login_service:__check_login_captcha:167 - 验证码已失效
|
||||
2026-09-27 09:17:40.880 | efa6272b9713467b980b5e2279502288 | 257ee926db59431baeae83aa56d1fd3d | 7b08ae862c674486a217cbccc2da56ca | 36967-fe3b0d | WARNING | common.annotation.log_annotation:wrapper:197 - 验证码已失效
|
||||
2026-09-27 11:09:14.360 | 8b0be0a5114141bdb37b6431382fcdc2 | 4fc3e9a9e9ef4334b746ef8be065c54e | 3515f9adf1e64b50a5ee5c28b522ea42 | 62289-86935c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-27 11:50:06.114 | 8629920d3544439aa74e32d77b771ea7 | 2b71ec3764424e8595738c4263865922 | c1f4efbd1c8540e4827e7b543f3cf419 | 62289-86935c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-27 12:05:47.001 | | | | 62289-86935c | WARNING | config.get_scheduler:on_lock_lost:273 - ⚠️ Worker 62289-86935c 失去 Application 锁
|
||||
2026-09-27 12:07:47.946 | 2f3a94e35bfa40749edad4676fc7f829 | f1516386a6f54cffa9cbda8056afdb5f | 0539b6e3453d4245b3cbb995666e0250 | 62289-86935c | WARNING | module_admin.service.login_service:authenticate_user:133 - 密码错误
|
||||
2026-09-27 12:07:47.947 | 2f3a94e35bfa40749edad4676fc7f829 | f1516386a6f54cffa9cbda8056afdb5f | 0539b6e3453d4245b3cbb995666e0250 | 62289-86935c | WARNING | common.annotation.log_annotation:wrapper:197 - 密码错误
|
||||
2026-09-27 12:08:42.318 | f45d2036019848339490a295691a606a | ee76be6f453a45af83ae1f13a19a244e | 92414b750a2949b799c60542464253fc | 62289-86935c | WARNING | module_admin.service.login_service:__check_login_captcha:167 - 验证码已失效
|
||||
2026-09-27 12:08:42.319 | f45d2036019848339490a295691a606a | ee76be6f453a45af83ae1f13a19a244e | 92414b750a2949b799c60542464253fc | 62289-86935c | WARNING | common.annotation.log_annotation:wrapper:197 - 验证码已失效
|
||||
2026-09-27 12:38:43.360 | 93d8f14042c14439a1fc9af9781b3917 | ab93661ba45545d2b98611723aa80c97 | 2d7a40ea30bf4d96ba3f38d98add01d4 | 98878-57c13e | ERROR | common.annotation.log_annotation:wrapper:202 - 托盘不符合首次入库条件
|
||||
2026-09-27 12:38:51.959 | b647ecabff704381839c93c606148449 | f9860290d57f450f8c47e77082417e96 | ff9ad3724a044fb3b55f5494fe3e0636 | 98878-57c13e | ERROR | common.annotation.log_annotation:wrapper:202 - 托盘不符合首次入库条件
|
||||
2026-09-27 12:39:40.079 | e35e37edaf3349f2a483325410fb7d08 | c782c7bb104b4505bbf7b7570ed48dcc | e9a1da20ac1e460b919d068fc53f0c5c | 98878-57c13e | ERROR | common.annotation.log_annotation:wrapper:202 - 托盘不存在
|
||||
2026-09-27 12:39:55.747 | 8e6c37c17dee4acca97d81bc1c8df234 | 1f1439ce9ae24fd0857a25a6d5247b19 | 833774659e364bf0b953c733be109f02 | 98878-57c13e | ERROR | common.annotation.log_annotation:wrapper:202 - 托盘不存在
|
||||
2026-09-27 12:40:13.052 | 1b6fe92ad661496db94d6c545c079795 | ee5ec419404a4422b0bf8cabb3359e9e | 205e6790a7034d76a33d5c57597124d3 | 98878-57c13e | ERROR | common.annotation.log_annotation:wrapper:202 - 托盘不存在
|
||||
2026-09-27 12:41:12.508 | d0dc9c192e974f69884cb4658b93f435 | 4b462f3020ac481ba0a37b88210158f1 | edd50d81cede49cab7bbaf6028207b20 | 98878-57c13e | ERROR | common.annotation.log_annotation:wrapper:202 - 托盘不符合首次入库条件
|
||||
2026-09-27 12:41:15.159 | 89247b5e1c554337ac85f86a8e0ded6f | 4fe93c98b37943b1b5112297fa9b4ca6 | f32a9908ad73427bb834dcabc162726e | 98878-57c13e | ERROR | common.annotation.log_annotation:wrapper:202 - 托盘不符合首次入库条件
|
||||
2026-09-27 12:41:17.335 | e5ee875f98314e67b3fac45dd00855da | 098796711da540b2947eefb29c9870af | 15a558072c3740aba34b002e5000128f | 98878-57c13e | ERROR | common.annotation.log_annotation:wrapper:202 - 托盘不符合首次入库条件
|
||||
2026-09-27 12:42:07.755 | 84dde08dd86d45eab607fe861086fd39 | 63af6c1b8c7544808c5ef237972b2f78 | e631e73908cd4d68a8ae53a5a4789d66 | 98878-57c13e | ERROR | common.annotation.log_annotation:wrapper:202 - 托盘不符合仓库空托盘装填条件
|
||||
2026-09-27 12:43:25.899 | ea8ced2637674cbaaebe945011758d9c | 47742a638abb4382bd3084ba08361579 | 22477243875c4e9da7431c65f3244cd0 | 98878-57c13e | ERROR | common.annotation.log_annotation:wrapper:202 - 托盘不在仓库
|
||||
2026-09-27 12:43:55.961 | 80c5c99118c74ccf847411f43ff77904 | b550f418bfe3473bb1284d63dc6417ad | e921e40ad7e74ac9afa7e1b50dc1b8b1 | 98878-57c13e | ERROR | common.annotation.log_annotation:wrapper:202 - 托盘不在仓库
|
||||
2026-09-27 12:44:03.523 | 6e0c2aa14062420eaa30e3e8757d248b | 7fcdba6045554c039f2839d9c3334ddb | df6c3c3149804e879bba5f1dd72d3215 | 98878-57c13e | ERROR | common.annotation.log_annotation:wrapper:202 - 托盘不在仓库
|
||||
2026-09-27 12:45:06.004 | 071d1550da4e430e81f2829a2ea75518 | c87b19bd407048b998db61c27a7b79bb | b9ec0b3ae8e04b7fa9e2bebb57c63403 | 98878-57c13e | ERROR | common.annotation.log_annotation:wrapper:202 - 托盘不在仓库
|
||||
2026-09-27 12:48:34.536 | 72cde0024dbd4261bef45f58ef165ed6 | ce5921bbde4d439dad5f27119fe74cde | 05771f45855549ac81165ce682c1ed19 | 98878-57c13e | ERROR | common.annotation.log_annotation:wrapper:202 - 托盘不存在活动装填周期
|
||||
2026-09-27 12:48:40.629 | d94f732666cf4f4aa6c8e00b8a43519a | 26272550f15e47b2b8894cd99fad17f6 | d8c14cc3ca0d4b3ea598e77a5b2a5d8e | 98878-57c13e | ERROR | common.annotation.log_annotation:wrapper:202 - 托盘不存在活动装填周期
|
||||
2026-09-27 21:03:23.484 | 0d2daf1685da417a86cc18a1f1703671 | 8c9c0df2542c42788c1e3ef786304a34 | 8923f52c36684edf843eee43c241217e | 98878-57c13e | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-27 21:09:06.309 | aeaaba8686fd4bb0bcfe70ee37a7f99c | 87413968a9c94012bc4d0e1cb387c5ad | 5373a4b28b7548f1a12fe8fe09420c4e | 98878-57c13e | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-27 21:09:06.315 | b9416504567448838c27351ce695e790 | 6ef0ec202543402bbcbc607eea3dd33f | f40e81e63e7f4f0aace83731a84f1b3d | 98878-57c13e | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-27 21:09:06.315 | e39e66a972964ed5a16e02b9603c8753 | 9b9124ad9d0349368089d0d4a48732e7 | 75a317beb07f47498325c29e26217446 | 98878-57c13e | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-27 21:09:06.338 | 43408d4704e340109cd40500cc4a4220 | 3dcbaa0b65d148fd8379a106dd81494a | 73b40a6c8eb44ba69aafe016157a354a | 98878-57c13e | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-27 21:09:06.340 | 9d4a6be5b23740138c5af1ab33058e3a | 253c7ffeddf24306a0510286c905a83d | 0df07cefe8574b428b998058a6c8e24b | 98878-57c13e | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-27 21:09:06.341 | 02c79524aabe4f67a3f0b9e2dfc98426 | d4c55c75a03d4c4aa5548c739a0fc1c1 | 5681ef3b6b7542f295fffb65b114693c | 98878-57c13e | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-27 21:09:06.345 | 926cec89f53b480388fe87f49f79b813 | b188bf19996e4300898f12cdc06c49dd | 9bd98cf850bd4f3daf6d3eb394aad92d | 98878-57c13e | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-27 21:09:06.354 | 80d2f1d6cd7e46c98dad81b113050afb | ae00f600b34c4bd5a1aad6db2ad6b939 | ad24a21f231f4f1489b576aa0a561504 | 98878-57c13e | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-27 21:09:06.355 | abdcc7433f624efcafd72a30478fb322 | 110c133582754763983b73ecbff01ff5 | 780e5781ee4649658e64552c993c327c | 98878-57c13e | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-27 21:09:06.382 | fcc3821d12f942b6b33bb076ff1261a3 | da00c06f2769485d871c28ab604711fe | 2b9eb989432c49a08ed11653776a0f4f | 98878-57c13e | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-27 21:09:06.383 | 30cc18b2af334ef08dd0e9e42d58cdb6 | ff301c892b90449c9ad42d80ce341bc5 | 95a641d4e29844de9a5d02fa44584d5a | 98878-57c13e | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-27 21:54:38.854 | 7edc8b4a28f6452a8703e341a070d094 | 07efe432e63a40bea0a6cd21cd97fbd1 | be9759590e48460eaa4973827fdb329e | 98878-57c13e | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-27 22:05:12.088 | | | | 98878-57c13e | WARNING | config.get_scheduler:on_lock_lost:273 - ⚠️ Worker 98878-57c13e 失去 Application 锁
|
||||
2026-09-27 22:05:12.164 | | | | 98878-57c13e | ERROR | sqlalchemy.pool.base:_close_connection:376 - Exception closing connection <AdaptedConnection <asyncmy.connection.Connection object at 0x133919720>>
|
||||
Traceback (most recent call last):
|
||||
File "/Users/jiaolongyan/miniconda3/envs/ShiErFeng/lib/python3.10/site-packages/sqlalchemy/pool/base.py", line 374, in _close_connection
|
||||
self._dialect.do_close(connection)
|
||||
File "/Users/jiaolongyan/miniconda3/envs/ShiErFeng/lib/python3.10/site-packages/sqlalchemy/engine/default.py", line 721, in do_close
|
||||
dbapi_connection.close()
|
||||
File "/Users/jiaolongyan/miniconda3/envs/ShiErFeng/lib/python3.10/site-packages/sqlalchemy/dialects/mysql/asyncmy.py", line 114, in close
|
||||
self.await_(self._connection.ensure_closed())
|
||||
File "/Users/jiaolongyan/miniconda3/envs/ShiErFeng/lib/python3.10/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 132, in await_only
|
||||
return current.parent.switch(awaitable) # type: ignore[no-any-return,attr-defined] # noqa: E501
|
||||
File "/Users/jiaolongyan/miniconda3/envs/ShiErFeng/lib/python3.10/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 196, in greenlet_spawn
|
||||
value = await result
|
||||
File "asyncmy/connection.pyx", line 367, in ensure_closed
|
||||
File "/Users/jiaolongyan/miniconda3/envs/ShiErFeng/lib/python3.10/asyncio/streams.py", line 343, in wait_closed
|
||||
await self._protocol._get_close_waiter(self)
|
||||
BrokenPipeError: [Errno 32] Broken pipe
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,59 @@
|
||||
2026-09-28 09:00:15.337 | ba36c1c61a7244b9b65fb20c6a958f4b | bfb05dda36d44ff6ba420f064a88562b | 0cd6a97296f94e8eb3cc277ef7820515 | 62572-6b1f2c | WARNING | module_admin.service.login_service:__check_login_captcha:167 - 验证码已失效
|
||||
2026-09-28 09:00:15.339 | ba36c1c61a7244b9b65fb20c6a958f4b | bfb05dda36d44ff6ba420f064a88562b | 0cd6a97296f94e8eb3cc277ef7820515 | 62572-6b1f2c | WARNING | common.annotation.log_annotation:wrapper:197 - 验证码已失效
|
||||
2026-09-28 09:43:50.310 | d2b267db4ba8496a9df6021d1ba00300 | 6792967bd70f49aa9d408c6559c322c2 | af1fe648a9cb4179bee7c754859c46a9 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 10:03:42.880 | bdbd9aaa0d48441f9da094ea63c40e10 | 928c68eb45b94716b75a8548306e002b | e5eb4344cb14454a923416ac78753615 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:219 - 用户token已失效,请重新登录
|
||||
2026-09-28 10:09:14.545 | 09e6fff8a6614865af978bfc63633250 | e6b47ff360ed4cdd98110a7a6d5eb8d3 | c949b290f3294eef8ab226d00d1d8dc1 | 62572-6b1f2c | WARNING | module_admin.service.login_service:__check_login_captcha:167 - 验证码已失效
|
||||
2026-09-28 10:09:14.546 | 09e6fff8a6614865af978bfc63633250 | e6b47ff360ed4cdd98110a7a6d5eb8d3 | c949b290f3294eef8ab226d00d1d8dc1 | 62572-6b1f2c | WARNING | common.annotation.log_annotation:wrapper:197 - 验证码已失效
|
||||
2026-09-28 10:21:53.236 | bf6b4c6c058b430ba3ccb1708e07ee8a | a619e28862d7481cb4ee6a216802a05e | 52f7de72fe134ee0875aed8b38e73d0f | 62572-6b1f2c | ERROR | common.annotation.log_annotation:wrapper:202 - 托盘库存不存在或不在客户现场
|
||||
2026-09-28 10:24:08.862 | 4edf2b18b29f4c03a634d6b484e4fd67 | 2569f1304b6844d2a85cafac1c8918cf | aa800f8f383741f3b60f094c769104c8 | 62572-6b1f2c | ERROR | common.annotation.log_annotation:wrapper:202 - 托盘库存不存在或不在客户现场
|
||||
2026-09-28 10:38:31.064 | 53e207894a1a41f387538142ea33bc05 | 42cd65a6daff43a8a9147a4999b26d31 | c0ac80f51bea4fa6aa8df8a835f37fd1 | 62572-6b1f2c | WARNING | module_admin.service.login_service:authenticate_user:133 - 密码错误
|
||||
2026-09-28 10:38:31.065 | 53e207894a1a41f387538142ea33bc05 | 42cd65a6daff43a8a9147a4999b26d31 | c0ac80f51bea4fa6aa8df8a835f37fd1 | 62572-6b1f2c | WARNING | common.annotation.log_annotation:wrapper:197 - 密码错误
|
||||
2026-09-28 10:38:41.223 | 56fee336ebe64917993bfd7622cf200a | 6a6c67a9eb114ff389c2bd0702fd4c52 | 5a6c9a772ba24ec78b40e1a6e0bfbc10 | 62572-6b1f2c | WARNING | module_admin.service.login_service:__check_login_captcha:170 - 验证码错误
|
||||
2026-09-28 10:38:41.223 | 56fee336ebe64917993bfd7622cf200a | 6a6c67a9eb114ff389c2bd0702fd4c52 | 5a6c9a772ba24ec78b40e1a6e0bfbc10 | 62572-6b1f2c | WARNING | common.annotation.log_annotation:wrapper:197 - 验证码错误
|
||||
2026-09-28 10:39:08.437 | d8a40c7b112d4f2f9aaa1576fbe53fa8 | ed3211272ba242328f859f7c91d3e344 | 10d9499662d74069b50e25aaf8b37797 | 62572-6b1f2c | ERROR | common.annotation.log_annotation:wrapper:202 - 托盘不符合首次入库条件
|
||||
2026-09-28 10:46:03.508 | 5991967288fa43dfa5a6271bc1558787 | 519a68df38094734ad0ea2d5eb570bf6 | aabf21cb05f94f91923652108f9b4d6a | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 10:46:03.514 | 7b8b0647325446d3927a0eb3c1dcd243 | 55f399b0c1db44d393c8cd2d317758d5 | 2232c53932ae4c37bb4b74e365106987 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 10:46:03.517 | 03591e4d5132466bb2669406f52cef54 | d84d01d8e8dd402cb0538289efa17894 | 6d298dccae1b4b919a1cf468528ee47a | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 10:46:03.519 | 18c5f5c4b502457082d2fb5d6e6effbc | b44f7d08c28b4d4e81f95fdc60ddeb37 | 90f4596111c1480bb8e947f6b01f9435 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 10:46:03.521 | 432adc6dbc374fceadbe7600b872bd53 | 78a958eb833f48588cf89f33d4de5a1b | 10445daddd45421b88d5229944c31445 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 10:46:03.524 | e993ea887654458f81e9843ec9d07952 | 9e6d6c1343994bff8e3dece2d20bf0ee | 20d503a7eb7a4f35acd582591e1b9d19 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 10:46:03.572 | 8b09a80bfeaa4215b959e02563810dd0 | ab4294c904a249ad863972aeb01a4d29 | 61794f5ad6124b2a94bd07a94fe237d3 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 10:46:03.579 | d17003e74b47423aa5abb8e2d0906871 | 13d207f3938441ec9de043ea99371274 | cd490dd2c5664d7388aae81e21445b7d | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 10:46:03.580 | 9242f746036b49a9949d71718d1de355 | 756c01642fac45308a0ccaa11632b4c1 | fe9b872a3d4343ac94b258fcec5181dd | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 10:46:03.580 | cc55b76dad1741a4833ff540b6dc442b | 1868b345057d4034baece403d93e9e29 | 1635a88ad5504efd94453a112f8e29de | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 10:46:03.581 | 0a91ce3f831e49efab0965ca18b5dbc2 | 1c8c68b5fbbe425fa500be69d556cd2f | 05210e3976ac44c6b3da75b8fea98952 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 10:46:16.232 | f487e375148c457e8461f85f6149a369 | 3a5a5f02edec4bfba6944d250ae99ab7 | 5d22ece20d3f42ee90834292972f30f7 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 10:46:16.274 | 66a66a5342114b6081ec37161581f57e | dad420640f514875b674f4599fba05dd | 6acb0ea3c8104c7898fa3c099bf25d14 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 10:46:16.274 | 9000e1326b404b78abb9e7aeb30391f0 | 92d042571c854790a8487bd599daa1a9 | c00b72e95d67432591a22ebd900b7afb | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 10:46:16.283 | ae676d2179cf424889f77806e3ee4c9b | 30c80ed1d960492993480baae5d9040c | 62f8d2bf510947b28672756f70787e76 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 10:46:16.284 | d46d37aa27024b2685db5581253a029c | a5fa9a3cc50441d88c544aeaf96c1163 | 6d24c8ac5f304b6e9db1a02569055f1c | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 10:46:16.286 | b13930203e3745f29dc8b957ce7f302f | 3d381f1978384f02a67a251ad9ccb556 | 1b39dc0017a7453cafc9ef0a9c9efb95 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 10:46:16.334 | 6c4f35c7f97f4e74ade5f9ade9b1ac1c | 8242777acb314c53bb718c935df83932 | c8b6b445700549deb14838304ea24a00 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 10:46:16.341 | 1e374f012ecd4cf89a7dc79b0b72f559 | 116770f6222349bbbff40109d5f04a3f | 1aa2b9c5a6cb4222968c76fefdf4ad56 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 10:46:16.343 | 2da707d80ed2471fa487726b3c9f7af0 | 99cb8278c4314f86aeb53ca3714df758 | e7f48812d7614e22ae8f5848c438ab92 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 10:46:16.343 | 0d6378e8376a40c690cc04b8bff3866d | b06087c979a04726984cd8c879649539 | 783d035f0e2b4de1801743d5e3c76a7f | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 10:46:16.344 | 19d2731983c44b71a04efd514a579320 | 867e7a717cd64541904c16f49b8f05f7 | 8e625c800e7c4f27a12cea3ec6ac15d2 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 12:03:10.155 | cce423f37fb84cd29e0d6dc6303d9c03 | 56f5f2575b854dada7b81b12b15c2813 | 98511d06168940dab87f39e4f3e3b7b0 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 12:03:10.157 | 2bd0170f8bcf4374ab60c0db43a32447 | c508a0855b214114861a1c4384124487 | 32ff02da68ef442286cefa9f84185a0d | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 12:03:10.158 | 8d2a13462f724be39c4da1a6048c4ae8 | 53c0230422de445982668b36765187e6 | 2b827f6120e644048802922125ba2ff3 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 12:03:10.158 | 4d757a1a06ad4ef88f8c8ebcc01ca69a | 3c2ce29cca144acb9f82b08899c1805e | 1b25762b8394413e80db760cf923c96e | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 12:03:10.186 | 8715cea358f2403fa725cdda75bf253e | 9d89c144d998477591fead7a9df7331b | a153e664a0ec4426ae86609093828d5b | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 12:03:10.186 | 0f2e087daaa34c4289178ab647ff55d0 | 59fdd73713ae44a58a2548e1ff1e8ab6 | 99dd9cd9abdc436288405f6448659ebf | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 12:03:10.188 | 530a2a1dd70a48088eff6ffa02f98a39 | 393cfa5715a84f0cb6852d4ab07cdcae | 12708e07f86f4ee98cda8526b68ebd8c | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 12:03:10.188 | 082b2cf3465f49d4b39a09866c97ee8e | cb8a906c394743f1ace0e260422059cb | e21eeb4d4ac3461899fcb5efea9fd432 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 12:03:10.188 | 44df9a204ccf475483a55fb0d5c327c8 | a3a29eac131e4c359fe19f78437e998d | 140d7a57b6574cee812154218f1b5407 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 13:09:48.985 | 13f4cbcd83154d8ba991194959fa526a | 0a23c5e16ed44f3192cb93434bd2975f | 613ac50ed4a04b7fa78e234c98c84116 | 62572-6b1f2c | WARNING | module_admin.service.login_service:__check_login_captcha:170 - 验证码错误
|
||||
2026-09-28 13:09:48.990 | 13f4cbcd83154d8ba991194959fa526a | 0a23c5e16ed44f3192cb93434bd2975f | 613ac50ed4a04b7fa78e234c98c84116 | 62572-6b1f2c | WARNING | common.annotation.log_annotation:wrapper:197 - 验证码错误
|
||||
2026-09-28 13:53:49.708 | 73a6d08358304a178e9ac9089881e2b9 | 18c4db78e68641d6a35cae565589b239 | ae356b29372d46e9b6e5570691549c67 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 13:59:45.312 | 5ab6fb1312b444fcb388106469ce9873 | 7628b918777b4c0982903a773e18bc7e | 4c7e41ef83204be2874d60a71a5a8eca | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 13:59:45.334 | faf5e90044ef4668ac186cf84dabb6b9 | 3a100294e01f45058eabf594ddd5eca0 | f0b1df93eb224f92975da239f92930b1 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 13:59:45.354 | ae5e3f9d480946dd8d03a7738b32d440 | d1a965ca11a74dc688f7b79c2b57954b | abc10506edc24506921e3b6865b22a0a | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 13:59:45.369 | cabfdd3271cd49c1bfa4b2b5a31f27ec | 7f35b31a5b9740d1b060e233682494b6 | bcc3d7c9e2d741c9a3b75353b46c4e82 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 13:59:45.374 | c80a006255e54dd9afd2be79e243b544 | fa8750a6a7a64d8f93b3c16778d651b8 | c23d3d73eb1c42d989a052ab32b4495d | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 13:59:45.378 | c3bcf46a67d3408cac2fd35495c10764 | 56998f6264ce4700bca3c8e61538b19f | 8708aa7c8273479e8ac33fc1f55829a5 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 13:59:45.382 | 33fd5d51ec9649fe9956fc6fc50966a0 | c2886cea734245f48618354d4fa22a47 | 6bcc309f42184588865183daf6607a82 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 13:59:45.469 | 90ab69c370fa4dc882923c30f9e6a6c2 | 4c0d88ad77cd42dba9cc3fc5e8da49c1 | 153bc5411f9148aeb2b368f558e7d006 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 13:59:45.469 | 3354a5201c6e475c815b3e93774c60ab | cf62d6123fde46b7b3eaf36fa7750263 | 425febf1654348d288e38e22a35f6d20 | 62572-6b1f2c | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 15:44:48.546 | f3d841a80e0c49b6a2ef12e0fb03c0e9 | d09057ad24a24e7ea4a22f44065f962d | 40ab25364cde46b29603eec07bb0410f | 19536-893d88 | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 18:15:15.372 | 55fe7aed5a5049eabdae0edd5e13b9c6 | e70f5cad542640e5b84a4a8f7bcbf349 | c96b83685c474b3fa2eb47238c10b3c3 | 19536-893d88 | WARNING | module_admin.service.login_service:get_current_user:277 - 用户token已失效,请重新登录
|
||||
2026-09-28 18:24:37.733 | | | | 19536-893d88 | WARNING | config.get_scheduler:on_lock_lost:273 - ⚠️ Worker 19536-893d88 失去 Application 锁
|
||||
File diff suppressed because it is too large
Load Diff
@@ -60,6 +60,22 @@ class OperationLogDao:
|
||||
|
||||
return operation_log_list
|
||||
|
||||
@classmethod
|
||||
async def get_scan_exception_log_list(
|
||||
cls, db: AsyncSession, oper_name: str | None, page_size: int
|
||||
) -> PageModel:
|
||||
"""按扫码日志标题和精确操作人查询首页异常记录。"""
|
||||
query = (
|
||||
select(SysOperLog)
|
||||
.where(
|
||||
SysOperLog.title == '扫码操作',
|
||||
SysOperLog.status == 1,
|
||||
SysOperLog.oper_name == oper_name if oper_name is not None else True,
|
||||
)
|
||||
.order_by(desc(SysOperLog.oper_time))
|
||||
)
|
||||
return await PageUtil.paginate(db, query, page_num=1, page_size=page_size, is_page=True)
|
||||
|
||||
@classmethod
|
||||
async def add_operation_log_dao(cls, db: AsyncSession, operation_log: OperLogModel) -> SysOperLog:
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"""首页运营驾驶舱接口。"""
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Request, Response
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from common.aspect.db_seesion import DBSessionDependency
|
||||
from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency
|
||||
from common.router import APIRouterPro
|
||||
from common.vo import PageResponseModel
|
||||
from module_admin.entity.vo.user_vo import CurrentUserModel
|
||||
from module_business.entity.vo.dashboard_vo import ScanExceptionModel
|
||||
from module_business.service.dashboard_service import DashboardService
|
||||
from utils.response_util import ResponseUtil
|
||||
|
||||
|
||||
dashboard_controller = APIRouterPro(
|
||||
prefix='/business/dashboard',
|
||||
tags=['业务管理-运营驾驶舱'],
|
||||
dependencies=[PreAuthDependency()],
|
||||
)
|
||||
|
||||
|
||||
@dashboard_controller.get(
|
||||
'/scan-exceptions',
|
||||
summary='获取首页扫码异常分页列表接口',
|
||||
description='普通用户仅查看本人扫码异常,超级管理员查看全部扫码异常',
|
||||
response_model=PageResponseModel[ScanExceptionModel],
|
||||
)
|
||||
async def get_business_dashboard_scan_exceptions(
|
||||
request: Request,
|
||||
query_db: Annotated[AsyncSession, DBSessionDependency()],
|
||||
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
|
||||
) -> Response:
|
||||
result = await DashboardService.get_scan_exception_list_services(query_db, current_user)
|
||||
return ResponseUtil.success(model_content=result)
|
||||
@@ -78,6 +78,13 @@ async def get_business_filling_strain_options(
|
||||
response_model=DataResponseModel[PalletScanResponseModel],
|
||||
dependencies=[UserInterfaceAuthDependency('business:filling:query')],
|
||||
)
|
||||
@Log(
|
||||
title='扫码操作',
|
||||
business_type=BusinessType.OTHER,
|
||||
request_log_mode='none',
|
||||
response_log_mode='summary',
|
||||
oper_url_mode='route',
|
||||
)
|
||||
async def scan_business_filling_pallet(
|
||||
request: Request,
|
||||
qr_token: Annotated[
|
||||
|
||||
@@ -81,6 +81,13 @@ async def get_business_inbound_warehouse_options(
|
||||
response_model=DataResponseModel[PalletScanResponseModel],
|
||||
dependencies=[UserInterfaceAuthDependency('business:inbound:query')],
|
||||
)
|
||||
@Log(
|
||||
title='扫码操作',
|
||||
business_type=BusinessType.OTHER,
|
||||
request_log_mode='none',
|
||||
response_log_mode='summary',
|
||||
oper_url_mode='route',
|
||||
)
|
||||
async def scan_business_inbound_pallet(
|
||||
request: Request,
|
||||
qr_token: Annotated[
|
||||
|
||||
@@ -82,6 +82,13 @@ async def get_business_return_warehouse_options(
|
||||
response_model=DataResponseModel[PalletScanResponseModel],
|
||||
dependencies=[UserInterfaceAuthDependency('business:return:query')],
|
||||
)
|
||||
@Log(
|
||||
title='扫码操作',
|
||||
business_type=BusinessType.OTHER,
|
||||
request_log_mode='none',
|
||||
response_log_mode='summary',
|
||||
oper_url_mode='route',
|
||||
)
|
||||
async def scan_business_return_pallet(
|
||||
request: Request,
|
||||
qr_token: Annotated[
|
||||
|
||||
@@ -98,6 +98,13 @@ async def get_business_shipment_site_options(
|
||||
response_model=DataResponseModel[PalletScanResponseModel],
|
||||
dependencies=[UserInterfaceAuthDependency('business:shipment:query')],
|
||||
)
|
||||
@Log(
|
||||
title='扫码操作',
|
||||
business_type=BusinessType.OTHER,
|
||||
request_log_mode='none',
|
||||
response_log_mode='summary',
|
||||
oper_url_mode='route',
|
||||
)
|
||||
async def scan_business_shipment_pallet(
|
||||
request: Request,
|
||||
qr_token: Annotated[
|
||||
|
||||
@@ -82,6 +82,13 @@ async def get_business_usage_line_options(
|
||||
response_model=DataResponseModel[PalletScanResponseModel],
|
||||
dependencies=[UserInterfaceAuthDependency('business:usage:query')],
|
||||
)
|
||||
@Log(
|
||||
title='扫码操作',
|
||||
business_type=BusinessType.OTHER,
|
||||
request_log_mode='none',
|
||||
response_log_mode='summary',
|
||||
oper_url_mode='route',
|
||||
)
|
||||
async def scan_business_usage_pallet(
|
||||
request: Request,
|
||||
qr_token: Annotated[
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic.alias_generators import to_camel
|
||||
|
||||
|
||||
class ScanExceptionModel(BaseModel):
|
||||
"""首页扫码异常记录。"""
|
||||
|
||||
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True)
|
||||
|
||||
oper_id: int = Field(gt=0, description='异常日志ID')
|
||||
source: str = Field(min_length=1, max_length=50, description='扫码来源')
|
||||
error_message: str = Field(min_length=1, max_length=2000, description='异常消息')
|
||||
operator_name: str = Field(min_length=1, max_length=50, description='操作人员')
|
||||
occurred_at: datetime | None = Field(default=None, description='异常时间')
|
||||
@@ -0,0 +1,73 @@
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from common.vo import PageModel
|
||||
from exceptions.exception import ServiceException
|
||||
from module_admin.dao.log_dao import OperationLogDao
|
||||
from module_admin.entity.vo.user_vo import CurrentUserModel
|
||||
from module_business.entity.vo.dashboard_vo import ScanExceptionModel
|
||||
|
||||
|
||||
class DashboardService:
|
||||
"""首页运营驾驶舱数据服务。"""
|
||||
|
||||
_SCAN_EXCEPTION_LIMIT = 5
|
||||
_SCAN_SOURCE_LABELS = {
|
||||
'/business/inbound/scan/{qr_token}': '首次入库扫码',
|
||||
'/business/filling/scan/{qr_token}': '托盘装填扫码',
|
||||
'/business/shipment/scan/{qr_token}': '整托发货扫码',
|
||||
'/business/usage/scan/{qr_token}': '托盘使用扫码',
|
||||
'/business/return/scan/{qr_token}': '托盘退回扫码',
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _row_value(row: Any, *keys: str) -> Any:
|
||||
if isinstance(row, dict):
|
||||
for key in keys:
|
||||
if key in row:
|
||||
return row[key]
|
||||
return None
|
||||
for key in keys:
|
||||
value = getattr(row, key, None)
|
||||
if value is not None:
|
||||
return value
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _scan_source(cls, oper_url: str | None) -> str:
|
||||
if not oper_url:
|
||||
return '扫码接口'
|
||||
for route_path, label in cls._SCAN_SOURCE_LABELS.items():
|
||||
route_prefix = route_path.split('/{', 1)[0]
|
||||
if oper_url == route_path or oper_url.startswith(f'{route_prefix}/'):
|
||||
return label
|
||||
return '扫码接口'
|
||||
|
||||
@classmethod
|
||||
async def get_scan_exception_list_services(
|
||||
cls, query_db: AsyncSession, current_user: CurrentUserModel
|
||||
) -> PageModel[ScanExceptionModel]:
|
||||
"""按用户范围获取首页最近扫码异常。"""
|
||||
if current_user.user is None or not current_user.user.user_name:
|
||||
raise ServiceException(message='当前用户信息无效')
|
||||
|
||||
oper_name = None if current_user.user.admin else current_user.user.user_name
|
||||
page = await OperationLogDao.get_scan_exception_log_list(query_db, oper_name, cls._SCAN_EXCEPTION_LIMIT)
|
||||
rows = [
|
||||
ScanExceptionModel(
|
||||
oper_id=cls._row_value(row, 'operId', 'oper_id'),
|
||||
source=cls._scan_source(cls._row_value(row, 'operUrl', 'oper_url')),
|
||||
error_message=cls._row_value(row, 'errorMsg', 'error_msg') or '扫码请求失败',
|
||||
operator_name=cls._row_value(row, 'operName', 'oper_name') or '未知用户',
|
||||
occurred_at=cls._row_value(row, 'operTime', 'oper_time'),
|
||||
)
|
||||
for row in page.rows
|
||||
]
|
||||
return PageModel[ScanExceptionModel](
|
||||
rows=rows,
|
||||
pageNum=page.page_num,
|
||||
pageSize=page.page_size,
|
||||
total=page.total,
|
||||
hasNext=page.has_next,
|
||||
)
|
||||
@@ -1,3 +1,10 @@
|
||||
-- ============================================================================
|
||||
-- 重要说明:本文件是新建空 MySQL 数据库的全量初始化脚本。
|
||||
-- 脚本包含 DROP TABLE,会删除目标库中同名表及其数据;禁止直接用于已有业务库。
|
||||
-- 已有数据库请先完成备份和结构核对,再按 sql/patches/ 下对应的 MySQL 增量补丁执行。
|
||||
-- Docker Compose 仅会在 MySQL 数据卷首次为空时执行本文件。
|
||||
-- ============================================================================
|
||||
|
||||
-- ----------------------------
|
||||
-- 托盘业务表(须先于被引用的系统表按反向依赖删除)
|
||||
-- ----------------------------
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询首页扫码异常
|
||||
export function listScanExceptions() {
|
||||
return request({
|
||||
url: '/business/dashboard/scan-exceptions',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_图层_2" data-name="图层 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 516.1 194.52">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #008596;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #1a2736;
|
||||
}
|
||||
|
||||
.cls-sub {
|
||||
fill: #008596;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #91cdb9;
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: #43b69e;
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: #554d8a;
|
||||
}
|
||||
|
||||
.cls-6 {
|
||||
fill: #50bcba;
|
||||
}
|
||||
|
||||
.cls-7, .cls-8, .cls-9 {
|
||||
fill: #28225c;
|
||||
}
|
||||
|
||||
.cls-10 {
|
||||
fill: #009a91;
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.cls-10, .cls-11 {
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
|
||||
.cls-11 {
|
||||
fill: #a5d7d7;
|
||||
}
|
||||
|
||||
.cls-8 {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.cls-9 {
|
||||
opacity: .25;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="tekening">
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-2" d="M302.33,61.89l-18.13,63.56c-3.35,11.65-7.77,17.59-23.63,17.59-3.99,0-10.36-.86-13.06-1.73-1.19-.43-2.27-.76-2.27-1.94v-8.74c0-1.19,.97-2.05,2.16-2.05h.22c3.56,.21,9.93,.86,13.49,.86,3.88,0,5.83-1.29,7.12-5.61l.97-3.35h-1.62c-3.99,0-7.34-.65-10.04-10.04l-14.24-48.56c-.11-.32-.11-.54-.11-.76,0-1.08,.65-1.73,2.16-1.73h14.35c1.08,0,1.83,.97,2.05,2.05l9.71,40.58c.32,1.51,.65,2.05,1.29,2.05h.97l10.04-42.63c.21-1.08,1.08-2.05,2.27-2.05h14.35c1.08,0,2.05,.86,2.05,1.94,0,.22-.11,.33-.11,.54Z"/>
|
||||
<path class="cls-2" d="M326.72,121.44h-13.17c-1.19,0-2.27-1.08-2.27-2.27V40.83c0-1.19,1.08-2.16,2.27-2.16h13.17c1.19,0,2.05,.97,2.05,2.16V119.17c0,1.19-.86,2.27-2.05,2.27Z"/>
|
||||
<path class="cls-2" d="M396,61.89l-13.6,48.99c-2.7,9.6-9.28,11.33-15.65,11.33s-12.84-1.73-15.54-11.33l-13.49-48.99c-.11-.21-.11-.43-.11-.65,0-1.08,.76-1.84,1.73-1.84h14.24c1.08,0,1.94,.97,2.27,2.27l9.28,42.3c.33,1.62,.86,2.27,1.62,2.27s1.4-.65,1.73-2.27l9.28-42.3c.32-1.3,1.19-2.27,2.27-2.27h14.24c.97,0,1.84,.76,1.84,1.84,0,.21,0,.43-.11,.65Z"/>
|
||||
<path class="cls-2" d="M418.66,121.3c-9.39,0-18.45-4.53-18.45-18.56v-.97c0-12.63,8.09-18.24,26.55-18.24h5.83v-3.89c0-6.15-6.78-7.47-11.85-7.47,0,0-17.96,1.95-17.96-12.69,0,0,16.18-.15,23.3-.15,15.54,0,23.99,4.55,23.99,20.31v39.49c0,1.19-.97,2.16-2.16,2.16h-29.24Zm13.89-27.41h-6.15c-7.56,0-9.5,2.59-9.5,7.88v.97c0,4.53,2.27,5.39,6.8,5.39,3.24,0,8.68,0,8.68,0l.17-14.25Z"/>
|
||||
<path class="cls-2" d="M497.01,58.61c15.65,0,19.1,10.9,19.1,24.5v35.93c0,1.19-1.08,2.27-2.27,2.27h-13.06c-1.19,0-2.16-1.08-2.16-2.27v-35.93c-.1-6.03,.45-8.65-6.13-8.65-3.22,0-8.33,.12-11.99,2.06v42.52c0,1.19-.97,2.27-2.16,2.27h-13.06c-1.19,0-2.27-1.08-2.27-2.27V61.63c0-1.19,.97-2.16,2.16-2.16l31.83-.86Z"/>
|
||||
<path class="cls-2" d="M184.25,121.47c0-17.9,16.39-18.14,16.39-18.14,8.24,0,17.54-.5,17.98-6.18,.24-3.16-.41-4.44-6.02-7.82l-15.23-8.96c-10.04-6.01-12.73-12.89-12.73-20.87,0-12.02,6.69-21.3,27.63-21.3,1.66,0,23.24,.05,23.26,.07-.13,.14,.12,2.04,.12,3.24,0,14.93-20.16,13.71-20.16,13.71-6.15,0-9.85,2.38-9.85,6.31,0,3.06,1.26,4.7,7.09,7.87l13.81,6.38c11.22,6.12,12.5,12.79,12.5,21.64,0,11.58-6.73,24.27-24.96,24.11l-29.81-.05Z"/>
|
||||
<g>
|
||||
<path class="cls-sub" d="M365.04,163.65h-1.67c-.32,0-.58-.29-.58-.64v-22.95c0-.32,.23-.61,.58-.61h1.67c.32,0,.58,.26,.58,.61v22.95c0,.32-.23,.64-.58,.64Z"/>
|
||||
<path class="cls-sub" d="M373.88,143.11h-1.64c-.35,0-.64-.29-.64-.64v-2.57c0-.35,.29-.64,.64-.64h1.64c.35,0,.64,.29,.64,.64v2.57c0,.35-.29,.64-.64,.64Zm-.06,20.53h-1.54c-.35,0-.64-.29-.64-.64v-16.42c0-.35,.29-.64,.64-.64h1.54c.35,0,.64,.29,.64,.64v16.42c0,.35-.29,.64-.64,.64Z"/>
|
||||
<path class="cls-sub" d="M393.77,146.72l-4.35,15.32c-.42,1.45-1.51,1.84-2.9,1.84s-2.48-.39-2.9-1.84l-4.34-15.32c-.03-.1-.03-.16-.03-.23,0-.32,.19-.55,.51-.55h1.77c.26,0,.48,.19,.61,.61l3.7,14.03c.19,.77,.35,.93,.68,.93,.29,0,.48-.16,.68-.93l3.7-14.03c.13-.42,.35-.61,.61-.61h1.77c.32,0,.51,.23,.51,.55,0,.06,0,.13-.03,.23Z"/>
|
||||
<path class="cls-sub" d="M400.81,143.11h-1.64c-.35,0-.64-.29-.64-.64v-2.57c0-.35,.29-.64,.64-.64h1.64c.35,0,.64,.29,.64,.64v2.57c0,.35-.29,.64-.64,.64Zm-.06,20.53h-1.54c-.35,0-.64-.29-.64-.64v-16.42c0-.35,.29-.64,.64-.64h1.54c.35,0,.64,.29,.64,.64v16.42c0,.35-.29,.64-.64,.64Z"/>
|
||||
<path class="cls-sub" d="M420.03,163.65h-1.54c-.35,0-.64-.29-.64-.64v-10.59c0-3.25-1.09-4.25-3.54-4.25-1.35,0-2.86,.51-4.34,1.26v13.58c0,.32-.29,.64-.64,.64h-1.58c-.32,0-.61-.29-.61-.64v-16.42c0-.35,.26-.64,.61-.64h1.61c.35,0,.61,.29,.61,.64v.61c1.77-1,3.22-1.48,5.12-1.48,3.86,0,5.6,2.41,5.6,6.69v10.59c0,.35-.29,.64-.64,.64Z"/>
|
||||
<path class="cls-sub" d="M440.07,148.1l-1.16,.22c.58,.61,1.09,1.58,1.09,3.32v.26c0,3.54-2.19,5.5-7.15,5.5-1.71,0-2.8-.26-3.73-.64-.55,.26-.8,.61-.8,1.09,0,.55,.26,.84,1,1.03l4.8,1.22c3.15,.8,6.18,1.71,6.18,4.73v.26c0,3.09-2.06,4.99-7.34,4.99s-7.4-1.96-7.4-5.12v-.29c0-1.8,.97-3.28,2.03-4.09l-.23-.1c-1.03-.45-1.48-1.51-1.48-2.51v-.03c0-.87,.42-1.8,1.32-2.32-1-.93-1.48-2.09-1.48-3.73v-.26c0-2.86,1.61-5.7,7.14-5.7h7.05c.39,0,.74,.32,.74,.71v.84c0,.42-.23,.55-.58,.61Zm-2.61,16.74c0-1.26-1.03-1.77-3.93-2.51l-3.64-.93c-.93,.8-1.58,1.61-1.58,3.28v.29c0,1.8,1.06,2.93,4.63,2.93s4.51-1.03,4.51-2.8v-.26Zm-.26-13.2c0-1.93-.87-3.44-4.35-3.44s-4.34,1.64-4.34,3.44v.26c0,1.42,.64,3.28,4.34,3.28,3.48,0,4.35-1.35,4.35-3.28v-.26Z"/>
|
||||
<path class="cls-sub" d="M365.07,173.76h-1.64c-.35,0-.64-.29-.64-.64v-2.57c0-.35,.29-.64,.64-.64h1.64c.35,0,.64,.29,.64,.64v2.57c0,.35-.29,.64-.64,.64Zm-.06,20.53h-1.54c-.35,0-.64-.29-.64-.64v-16.42c0-.35,.29-.64,.64-.64h1.54c.35,0,.64,.29,.64,.64v16.42c0,.35-.29,.64-.64,.64Z"/>
|
||||
<path class="cls-sub" d="M384.28,194.29h-1.54c-.35,0-.64-.29-.64-.64v-10.59c0-3.25-1.09-4.25-3.54-4.25-1.35,0-2.86,.51-4.34,1.26v13.58c0,.32-.29,.64-.64,.64h-1.58c-.32,0-.61-.29-.61-.64v-16.42c0-.35,.26-.64,.61-.64h1.61c.35,0,.61,.29,.61,.64v.61c1.77-1,3.22-1.48,5.12-1.48,3.86,0,5.6,2.41,5.6,6.69v10.59c0,.35-.29,.64-.64,.64Z"/>
|
||||
<path class="cls-sub" d="M403.17,194.29h-1.54c-.35,0-.64-.29-.64-.64v-10.59c0-3.25-1.09-4.25-3.54-4.25-1.35,0-2.86,.51-4.34,1.26v13.58c0,.32-.29,.64-.64,.64h-1.58c-.32,0-.61-.29-.61-.64v-16.42c0-.35,.26-.64,.61-.64h1.61c.35,0,.61,.29,.61,.64v.61c1.77-1,3.22-1.48,5.12-1.48,3.86,0,5.6,2.41,5.6,6.69v10.59c0,.35-.29,.64-.64,.64Z"/>
|
||||
<path class="cls-sub" d="M415.71,194.52c-4.73,0-7.14-3.12-7.14-7.85v-2.45c0-4.73,2.41-7.85,7.14-7.85s7.18,3.12,7.18,7.85v2.45c0,4.73-2.45,7.85-7.18,7.85Zm4.35-10.3c0-3.28-1.32-5.38-4.35-5.38s-4.31,2.06-4.31,5.38v2.45c0,3.28,1.29,5.38,4.31,5.38s4.35-2.06,4.35-5.38v-2.45Z"/>
|
||||
<path class="cls-sub" d="M440.52,177.36l-4.35,15.32c-.42,1.45-1.51,1.84-2.9,1.84s-2.48-.39-2.9-1.84l-4.35-15.32c-.03-.1-.03-.16-.03-.23,0-.32,.19-.55,.51-.55h1.77c.26,0,.48,.19,.61,.61l3.7,14.03c.19,.77,.35,.93,.68,.93,.29,0,.48-.16,.68-.93l3.7-14.03c.13-.42,.35-.61,.61-.61h1.77c.32,0,.51,.23,.51,.55,0,.06,0,.13-.03,.23Z"/>
|
||||
<path class="cls-sub" d="M455.97,194.29h-1.48c-.35,0-.64-.29-.64-.64v-.71c-1.54,.9-3.15,1.58-4.96,1.58-2.74,0-5.25-1.19-5.25-5.21v-.39c0-3.48,2.03-5.21,7.76-5.21h2.38v-1.29c0-2.7-.93-3.64-3.44-3.64-2.09,0-3.64,.13-4.92,.26h-.16c-.29,0-.58-.1-.58-.51v-1.13c0-.35,.23-.55,.68-.61,1.32-.23,2.8-.42,4.99-.42,4.47,0,6.28,2.25,6.28,6.05v11.23c0,.35-.29,.64-.64,.64Zm-2.19-8.56h-2.38c-3.9,0-4.93,.87-4.93,3.15v.39c0,2.32,1.09,2.93,2.96,2.93,1.48,0,2.8-.45,4.35-1.29v-5.18Z"/>
|
||||
<path class="cls-sub" d="M469.99,194.36c-.64,.1-1.48,.16-2.09,.16-2.96,0-4.38-.77-4.38-4.31v-11.23l-3.12-.35c-.35-.03-.61-.22-.61-.58v-.87c0-.35,.26-.58,.61-.58h3.12v-3.15c0-.32,.22-.58,.58-.64l1.67-.26h.1c.26,0,.48,.22,.48,.55v3.51h3.48c.35,0,.64,.29,.64,.64v1.09c0,.35-.29,.64-.64,.64h-3.48v11.23c0,1.77,.45,2.03,1.74,2.03h1.8c.45,0,.68,.13,.68,.45v1.13c0,.29-.16,.48-.58,.55Z"/>
|
||||
<path class="cls-sub" d="M477.14,173.76h-1.64c-.35,0-.64-.29-.64-.64v-2.57c0-.35,.29-.64,.64-.64h1.64c.35,0,.64,.29,.64,.64v2.57c0,.35-.29,.64-.64,.64Zm-.06,20.53h-1.54c-.35,0-.64-.29-.64-.64v-16.42c0-.35,.29-.64,.64-.64h1.54c.35,0,.64,.29,.64,.64v16.42c0,.35-.29,.64-.64,.64Z"/>
|
||||
<path class="cls-sub" d="M490.01,194.52c-4.73,0-7.14-3.12-7.14-7.85v-2.45c0-4.73,2.41-7.85,7.14-7.85s7.18,3.12,7.18,7.85v2.45c0,4.73-2.45,7.85-7.18,7.85Zm4.35-10.3c0-3.28-1.32-5.38-4.35-5.38s-4.31,2.06-4.31,5.38v2.45c0,3.28,1.29,5.38,4.31,5.38s4.35-2.06,4.35-5.38v-2.45Z"/>
|
||||
<path class="cls-sub" d="M514.85,194.29h-1.54c-.35,0-.64-.29-.64-.64v-10.59c0-3.25-1.09-4.25-3.54-4.25-1.35,0-2.86,.51-4.34,1.26v13.58c0,.32-.29,.64-.64,.64h-1.58c-.32,0-.61-.29-.61-.64v-16.42c0-.35,.26-.64,.61-.64h1.61c.35,0,.61,.29,.61,.64v.61c1.77-1,3.22-1.48,5.12-1.48,3.86,0,5.6,2.41,5.6,6.69v10.59c0,.35-.29,.64-.64,.64Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-11" d="M57.78,67.44c-.03,5.02-.86,9.86-2.35,14.39,0,0,.01,.01,.02,.02,0,0,0,0,.01,0,10.82-31.09,38.25-54.41,71.67-59.3C130.83,15.86,132.95,8.18,132.95,0h0s-46.67,0-46.67,0H46.67C30.78,0,16.76,7.94,8.33,20.07c.88-.04,1.76-.07,2.65-.06,26.02,.17,46.98,21.41,46.8,47.43Z"/>
|
||||
<path class="cls-10" d="M0,46.67c0,5.62,1,11,2.82,15.99,5.22-.78,10.62-.87,16.11-.17,14.7,1.88,27.41,9.05,36.5,19.35,1.49-4.53,2.31-9.36,2.35-14.39,.17-26.02-20.78-47.26-46.8-47.43-.89,0-1.77,.02-2.65,.06C3.08,27.61,0,36.78,0,46.67H0Z"/>
|
||||
<path class="cls-6" d="M0,46.67c0,5.62,1,11,2.82,15.99,5.22-.78,10.62-.87,16.11-.17,14.7,1.88,27.41,9.05,36.5,19.35,1.49-4.53,2.31-9.36,2.35-14.39,.17-26.02-20.78-47.26-46.8-47.43-.89,0-1.77,.02-2.65,.06C3.08,27.61,0,36.78,0,46.67H0Z"/>
|
||||
<path class="cls-7" d="M55.46,81.85s-.01-.01-.02-.02c0,0,0,0,0,.01,0,0,.02,0,.02,0Z"/>
|
||||
<path class="cls-10" d="M55.47,81.85s0,0-.01,0c0,0,0,0,.01,.01,0,0,0,0,0,0Z"/>
|
||||
<path class="cls-8" d="M55.47,81.85s0,0-.01,0c0,0,0,0,.01,.01,0,0,0,0,0,0Z"/>
|
||||
<path class="cls-7" d="M55.42,81.88s.03,0,.05-.01c0,0,0,0,0,0,0,0,0,0-.01-.01,0,0-.02,0-.02,0,0,.01,0,.02-.01,.03Z"/>
|
||||
<path class="cls-8" d="M55.42,81.88s.03,0,.05-.01c0,0,0,0,0,0,0,0,0,0-.01-.01,0,0-.02,0-.02,0,0,.01,0,.02-.01,.03Z"/>
|
||||
<path class="cls-10" d="M55.47,81.85s0,0,.01,0c5.58-1.58,11.47-2.44,17.56-2.44,24,0,44.92,13.13,56.01,32.6,2.5-5.72,3.9-12.04,3.9-18.68h0c0-24.43-18.78-44.48-42.69-46.5-.22-.02-.22-.31,0-.33,15.91-1.34,29.53-10.67,36.88-23.95-33.42,4.9-60.85,28.21-71.67,59.3Z"/>
|
||||
<path class="cls-3" d="M55.47,81.85s0,0,.01,0c5.58-1.58,11.47-2.44,17.56-2.44,24,0,44.92,13.13,56.01,32.6,2.5-5.72,3.9-12.04,3.9-18.68h0c0-24.43-18.78-44.48-42.69-46.5-.22-.02-.22-.31,0-.33,15.91-1.34,29.53-10.67,36.88-23.95-33.42,4.9-60.85,28.21-71.67,59.3Z"/>
|
||||
<path class="cls-10" d="M55.47,81.86s.01,0,.02,0c0,0,0,0-.01,0,0,0,0,0,0,0,0,0,0,0,0,0Z"/>
|
||||
<path class="cls-8" d="M55.47,81.86s.01,0,.02,0c0,0,0,0-.01,0,0,0,0,0,0,0,0,0,0,0,0,0Z"/>
|
||||
<path class="cls-9" d="M55.47,81.86s.01,0,.02,0c0,0,0,0-.01,0,0,0,0,0,0,0,0,0,0,0,0,0Z"/>
|
||||
<polygon class="cls-7" points="55.47 81.86 55.47 81.86 55.47 81.86 55.47 81.86"/>
|
||||
<polygon class="cls-8" points="55.47 81.86 55.47 81.86 55.47 81.86 55.47 81.86"/>
|
||||
<polygon class="cls-9" points="55.47 81.86 55.47 81.86 55.47 81.86 55.47 81.86"/>
|
||||
<path class="cls-10" d="M73.04,79.42c-6.09,0-11.97,.86-17.56,2.44,24.27,7.47,41.91,30.06,41.91,56.78,0,0,0,.01,0,.02,14.23-3.48,25.89-13.47,31.65-26.64-11.08-19.46-32-32.6-56.01-32.6Z"/>
|
||||
<path class="cls-9" d="M73.04,79.42c-6.09,0-11.97,.86-17.56,2.44,24.27,7.47,41.91,30.06,41.91,56.78,0,0,0,.01,0,.02,14.23-3.48,25.89-13.47,31.65-26.64-11.08-19.46-32-32.6-56.01-32.6Z"/>
|
||||
<path class="cls-4" d="M73.04,79.42c-6.09,0-11.97,.86-17.56,2.44,24.27,7.47,41.91,30.06,41.91,56.78,0,0,0,.01,0,.02,14.23-3.48,25.89-13.47,31.65-26.64-11.08-19.46-32-32.6-56.01-32.6Z"/>
|
||||
<path class="cls-10" d="M55.48,81.86s-.01,0-.02,0c10.78,12.22,16.46,28.84,14.23,46.28-.53,4.12-1.59,8.08-3.11,11.86h19.69c3.83,0,7.55-.47,11.11-1.34,0,0,0-.01,0-.02,0-26.72-17.64-49.31-41.91-56.78Z"/>
|
||||
<path class="cls-8" d="M55.48,81.86s-.01,0-.02,0c10.78,12.22,16.46,28.84,14.23,46.28-.53,4.12-1.59,8.08-3.11,11.86h19.69c3.83,0,7.55-.47,11.11-1.34,0,0,0-.01,0-.02,0-26.72-17.64-49.31-41.91-56.78Z"/>
|
||||
<path class="cls-9" d="M55.48,81.86s-.01,0-.02,0c10.78,12.22,16.46,28.84,14.23,46.28-.53,4.12-1.59,8.08-3.11,11.86h19.69c3.83,0,7.55-.47,11.11-1.34,0,0,0-.01,0-.02,0-26.72-17.64-49.31-41.91-56.78Z"/>
|
||||
<path class="cls-1" d="M55.48,81.86s-.01,0-.02,0c10.78,12.22,16.46,28.84,14.23,46.28-.53,4.12-1.59,8.08-3.11,11.86h19.69c3.83,0,7.55-.47,11.11-1.34,0,0,0-.01,0-.02,0-26.72-17.64-49.31-41.91-56.78Z"/>
|
||||
<path class="cls-5" d="M55.47,81.86s0,0,0,0c-.02,0-.03,0-.05,.01,0-.01,0-.02,.01-.03,0,0,0,0,0-.01-9.09-10.29-21.81-17.47-36.5-19.35-5.49-.7-10.89-.6-16.11,.17,2.93,8.02,7.98,15.01,14.5,20.29,5.11,4.14,11.12,7.22,17.69,8.91,2.45,.63,4.96,1.08,7.55,1.3,.24,.02,.24,.34,0,.36-4.74,.41-9.26,1.54-13.49,3.27-8.71,3.55-16.1,9.65-21.25,17.38-4.93,7.4-7.82,16.28-7.82,25.84H0s8.74,0,8.74,0h57.85c1.51-3.77,2.58-7.74,3.11-11.86,2.23-17.44-3.45-34.06-14.23-46.28Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -86,15 +86,16 @@
|
||||
.el-table__header-wrapper, .el-table__fixed-header-wrapper {
|
||||
th {
|
||||
word-break: break-word;
|
||||
background-color: #f8f8f9 !important;
|
||||
color: #515a6e;
|
||||
height: 40px !important;
|
||||
background-color: var(--el-fill-color-light, #f5f7fa) !important;
|
||||
color: var(--el-text-color-regular, #606266);
|
||||
height: 46px !important;
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
}
|
||||
}
|
||||
.el-table__body-wrapper {
|
||||
.el-button [class*="el-icon-"] + span {
|
||||
margin-left: 1px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -306,3 +307,177 @@
|
||||
.splitpanes.default-theme .splitpanes__pane {
|
||||
background-color: var(--splitpanes-default-bg) !important;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 全局工作区与现代化容器体系
|
||||
// ==========================================
|
||||
.app-container {
|
||||
min-height: 100%;
|
||||
padding: 18px 20px 24px;
|
||||
background: var(--el-bg-color-page, #f5f7fb);
|
||||
color: var(--el-text-color-primary, #303133);
|
||||
box-sizing: border-box;
|
||||
|
||||
// 顶部内联检索卡片 (排除弹窗内部表单)
|
||||
& > .el-form--inline,
|
||||
.splitpanes .el-form--inline {
|
||||
margin: 0 0 14px;
|
||||
padding: 14px 16px 2px;
|
||||
border: 1px solid var(--el-border-color-lighter, #e4e7ed);
|
||||
border-radius: 12px;
|
||||
background: var(--el-bg-color, #fff);
|
||||
box-shadow: var(--el-box-shadow-lighter, 0 3px 14px rgba(31, 55, 88, 0.04));
|
||||
transition: box-shadow 0.25s ease, border-color 0.25s ease;
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 12px;
|
||||
margin-right: 14px;
|
||||
}
|
||||
|
||||
.el-input,
|
||||
.el-select,
|
||||
.el-input-number,
|
||||
.el-date-editor {
|
||||
min-width: 190px;
|
||||
}
|
||||
}
|
||||
|
||||
// 统一操作工具栏
|
||||
& > .el-row.mb8,
|
||||
.splitpanes .el-row.mb8 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 38px;
|
||||
margin: 0 0 12px !important;
|
||||
|
||||
.el-col {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.top-right-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// 统一现代化数据表格卡片容器
|
||||
& > .el-table,
|
||||
.splitpanes .el-table {
|
||||
width: 100%;
|
||||
border: 1px solid var(--el-border-color-lighter, #e4e7ed);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: var(--el-bg-color, #fff);
|
||||
box-shadow: var(--el-box-shadow-lighter, 0 3px 14px rgba(31, 55, 88, 0.04));
|
||||
|
||||
.el-table__header th {
|
||||
height: 46px !important;
|
||||
background: var(--el-fill-color-light, #f5f7fa) !important;
|
||||
color: var(--el-text-color-regular, #606266) !important;
|
||||
font-weight: 650;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter, #e4e7ed);
|
||||
}
|
||||
|
||||
.el-table__body td {
|
||||
height: 50px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter, #f0f2f5);
|
||||
}
|
||||
|
||||
.el-table__body tr:nth-child(even) > td {
|
||||
background: var(--el-fill-color-lighter, #fafafa);
|
||||
}
|
||||
|
||||
.el-table__body tr:hover > td {
|
||||
background-color: var(--el-fill-color-light, #f5f7fa) !important;
|
||||
}
|
||||
|
||||
.el-table__inner-wrapper::before {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 14px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗现代化规范
|
||||
.el-dialog {
|
||||
border-radius: 14px !important;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--el-border-color-lighter, #e4e7ed);
|
||||
box-shadow: 0 20px 60px rgba(15, 34, 58, 0.16) !important;
|
||||
|
||||
.el-dialog__header {
|
||||
padding: 16px 22px 14px;
|
||||
margin-right: 0;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter, #e4e7ed);
|
||||
|
||||
.el-dialog__title {
|
||||
font-size: 16px;
|
||||
font-weight: 650;
|
||||
color: var(--el-text-color-primary, #303133);
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 20px 22px;
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
padding: 12px 22px 16px;
|
||||
border-top: 1px solid var(--el-border-color-lighter, #e4e7ed);
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片现代化规范
|
||||
.el-card {
|
||||
border-radius: 14px !important;
|
||||
border: 1px solid var(--el-border-color-lighter, #e4e7ed) !important;
|
||||
box-shadow: var(--el-box-shadow-lighter, 0 3px 14px rgba(31, 55, 88, 0.04)) !important;
|
||||
|
||||
.el-card__header {
|
||||
padding: 16px 20px !important;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter, #e4e7ed) !important;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.el-card__body {
|
||||
padding: 18px 20px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 分割面板与树结构卡片化
|
||||
.splitpanes.default-theme {
|
||||
.splitpanes__pane {
|
||||
background-color: var(--el-bg-color, #fff) !important;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--el-border-color-lighter, #e4e7ed);
|
||||
padding: 16px;
|
||||
box-shadow: var(--el-box-shadow-lighter, 0 3px 14px rgba(31, 55, 88, 0.04));
|
||||
}
|
||||
|
||||
.splitpanes__splitter {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
width: 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 输入框与按钮微调
|
||||
.el-input__wrapper,
|
||||
.el-select__wrapper {
|
||||
border-radius: 8px !important;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.is-circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
left: 0;
|
||||
z-index: 1001;
|
||||
overflow: hidden;
|
||||
-webkit-box-shadow: 2px 0 6px rgba(0,21,41,.35);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
border-right: 1px solid var(--el-border-color-lighter, #e4e7ed);
|
||||
box-shadow: 2px 0 10px 0 rgba(15, 34, 58, 0.05);
|
||||
|
||||
// reset element-ui css
|
||||
.horizontal-collapse-transition {
|
||||
@@ -68,6 +68,24 @@
|
||||
border: none;
|
||||
height: 100%;
|
||||
width: 100% !important;
|
||||
|
||||
&:not(.el-menu--collapse) {
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
height: 46px;
|
||||
line-height: 46px;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.el-menu-item.is-active {
|
||||
font-weight: 600;
|
||||
background-color: rgba(0, 133, 150, 0.12) !important;
|
||||
color: var(--current-color, #008596) !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item, .menu-title {
|
||||
@@ -92,9 +110,18 @@
|
||||
color: vars.$base-menu-color-active !important;
|
||||
}
|
||||
|
||||
& .theme-dark .el-menu:not(.el-menu--collapse) .el-menu-item.is-active {
|
||||
background-color: rgba(0, 133, 150, 0.26) !important;
|
||||
color: #38bdf8 !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
& .nest-menu .el-sub-menu>.el-sub-menu__title,
|
||||
& .el-sub-menu .el-menu-item {
|
||||
min-width: vars.$base-sidebar-width !important;
|
||||
width: 100% !important;
|
||||
margin: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.06) !important;
|
||||
@@ -108,6 +135,11 @@
|
||||
&:hover {
|
||||
background-color: vars.$base-sub-menu-hover !important;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: rgba(0, 133, 150, 0.28) !important;
|
||||
color: #38bdf8 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,10 +88,21 @@ getBreadcrumb()
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
line-height: 50px;
|
||||
margin-left: 8px;
|
||||
|
||||
.no-redirect {
|
||||
color: #97a8be;
|
||||
color: var(--el-text-color-regular, #606266);
|
||||
cursor: text;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--el-text-color-secondary, #909399);
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary, #008596);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -96,7 +96,10 @@ function handleCurrentChange(val) {
|
||||
|
||||
<style scoped>
|
||||
.pagination-container {
|
||||
background: #fff;
|
||||
background: transparent;
|
||||
padding: 12px 2px 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.pagination-container.hidden {
|
||||
display: none;
|
||||
|
||||
@@ -52,6 +52,7 @@ async function syncTestBatchScope() {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-color: var(--el-bg-color-page, #f5f7fb);
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
@@ -123,11 +124,15 @@ async function syncTestBatchScope() {
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: #f1f1f1;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #c0c0c0;
|
||||
background-color: var(--el-border-color-darker, #c0c4cc);
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-text-color-secondary, #909399);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -156,7 +156,8 @@ async function toggleTheme(event) {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: var(--navbar-bg);
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
border-bottom: 1px solid var(--el-border-color-lighter, #e4e7ed);
|
||||
box-shadow: 0 1px 4px rgba(15, 34, 58, 0.04);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// padding: 0 8px;
|
||||
@@ -216,19 +217,22 @@ async function toggleTheme(event) {
|
||||
}
|
||||
|
||||
.right-menu-item {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 8px;
|
||||
height: 100%;
|
||||
font-size: 18px;
|
||||
color: #5a5e66;
|
||||
vertical-align: text-bottom;
|
||||
color: var(--el-text-color-regular, #5a5e66);
|
||||
vertical-align: middle;
|
||||
|
||||
&.hover-effect {
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
transition: background 0.3s, color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.025);
|
||||
background: rgba(0, 0, 0, 0.035);
|
||||
color: var(--el-color-primary, #008596);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,30 +251,36 @@ async function toggleTheme(event) {
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
margin-right: 0px;
|
||||
padding-right: 0px;
|
||||
margin-right: 4px;
|
||||
padding: 0 4px;
|
||||
|
||||
.avatar-wrapper {
|
||||
margin-top: 10px;
|
||||
right: 8px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 20px;
|
||||
transition: background 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
cursor: pointer;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-right: 8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
vertical-align: middle;
|
||||
border-style: none;
|
||||
object-fit: cover;
|
||||
border: 1.5px solid var(--el-color-primary-light-7, #b2e0e6);
|
||||
box-shadow: 0 2px 6px rgba(0, 133, 150, 0.12);
|
||||
}
|
||||
|
||||
.user-nickname{
|
||||
position: relative;
|
||||
left: 0px;
|
||||
// bottom: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
.user-nickname {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary, #303133);
|
||||
}
|
||||
|
||||
i {
|
||||
|
||||
@@ -44,6 +44,7 @@ const logoBackground = variables.menuBg;
|
||||
background: v-bind(logoBackground);
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
|
||||
& .sidebar-logo-link {
|
||||
display: flex;
|
||||
|
||||
@@ -87,8 +87,8 @@ function isActive(r) {
|
||||
function activeStyle(tag) {
|
||||
if (!isActive(tag)) return {};
|
||||
return {
|
||||
"background-color": theme.value,
|
||||
"border-color": theme.value
|
||||
"background-color": theme.value || '#008596',
|
||||
"border-color": theme.value || '#008596'
|
||||
};
|
||||
}
|
||||
function isAffix(tag) {
|
||||
@@ -244,8 +244,8 @@ function handleScroll() {
|
||||
height: 34px;
|
||||
width: 100%;
|
||||
background: var(--tags-bg, #fff);
|
||||
border-bottom: 1px solid var(--tags-item-border, #d8dce5);
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
|
||||
border-bottom: 1px solid var(--el-border-color-lighter, #e4e7ed);
|
||||
box-shadow: 0 1px 4px 0 rgba(15, 34, 58, 0.04);
|
||||
.tags-view-wrapper {
|
||||
.tags-view-item {
|
||||
display: inline-block;
|
||||
@@ -254,12 +254,14 @@ function handleScroll() {
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
border: 1px solid var(--tags-item-border, #d8dce5);
|
||||
border-radius: 6px;
|
||||
color: var(--tags-item-text, #495060);
|
||||
background: var(--tags-item-bg, #fff);
|
||||
padding: 0 8px;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
margin-left: 5px;
|
||||
margin-top: 4px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:first-of-type {
|
||||
margin-left: 15px;
|
||||
@@ -269,20 +271,27 @@ function handleScroll() {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
&:not(.active):hover {
|
||||
background-color: var(--el-fill-color-light, #f5f7fa);
|
||||
border-color: var(--el-color-primary-light-7, #b2e0e6);
|
||||
color: var(--el-color-primary, #008596);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #42b983;
|
||||
background-color: #008596;
|
||||
color: #fff;
|
||||
border-color: #42b983;
|
||||
border-color: #008596;
|
||||
box-shadow: 0 2px 6px rgba(0, 133, 150, 0.22);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
background: #fff;
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
margin-right: 5px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -298,21 +307,26 @@ function handleScroll() {
|
||||
z-index: 3000;
|
||||
position: absolute;
|
||||
list-style-type: none;
|
||||
padding: 5px 0;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--tags-item-text, #333);
|
||||
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
|
||||
border: 1px solid var(--el-border-color-light, #e4e7ed);
|
||||
padding: 6px 0;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 450;
|
||||
color: var(--el-text-color-primary, #333);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
|
||||
border: 1px solid var(--el-border-color-lighter, #e4e7ed);
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
padding: 7px 16px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--tags-item-hover, #eee);
|
||||
background: var(--el-fill-color-light, #f5f7fa);
|
||||
color: var(--el-color-primary, #008596);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ const useSettingsStore = defineStore(
|
||||
{
|
||||
state: () => ({
|
||||
title: '',
|
||||
theme: storageSetting.theme || '#409EFF',
|
||||
theme: storageSetting.theme || '#008596',
|
||||
sideTheme: storageSetting.sideTheme || sideTheme,
|
||||
showSettings: showSettings,
|
||||
navType: storageSetting.navType === undefined ? navType : storageSetting.navType,
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
<template>
|
||||
<div class="dashboard-page">
|
||||
<div class="dashboard-header">
|
||||
<div class="header-left">
|
||||
<h1 class="page-title">仓储运营总览</h1>
|
||||
<p class="page-desc">实时监控托盘流转、出入库动态与仓储核心 KPI</p>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-tag type="info" effect="plain" class="range-tag">数据范围:全部仓库</el-tag>
|
||||
<el-button type="primary" plain :icon="Refresh" :loading="state.loading" @click="loadDashboardData">刷新数据</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="state.partialError" class="dashboard-alert">
|
||||
<el-icon><Warning /></el-icon>
|
||||
<span>部分运营数据暂时无法加载,已保留可用模块。</span>
|
||||
@@ -36,7 +47,7 @@
|
||||
<div class="card-heading">
|
||||
<div>
|
||||
<h2>{{ trendRangeLabel }}入库 / 出库趋势</h2>
|
||||
<span>入库以单据数统计,出库以确认包数统计</span>
|
||||
<span>入库以单据数统计,出库以确认包数统计(默认近 7 天入库 / 出库趋势)</span>
|
||||
</div>
|
||||
<el-select
|
||||
v-model="selectedTrendDays"
|
||||
@@ -155,12 +166,31 @@
|
||||
|
||||
<el-card class="dashboard-card activity-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-heading"><div><h2>扫码异常</h2><span>扫码过程中的异常记录</span></div><el-tag type="success" effect="light">正常</el-tag></div>
|
||||
<div class="card-heading"><div><h2>扫码异常</h2><span>扫码过程中的异常记录</span></div><el-tag :type="scanExceptionTag.type" effect="light">{{ scanExceptionTag.label }}</el-tag></div>
|
||||
</template>
|
||||
<div class="exception-empty">
|
||||
<div v-if="state.loading || state.scanExceptionAvailable === null" class="exception-list">
|
||||
<el-skeleton :rows="4" animated />
|
||||
</div>
|
||||
<div v-else-if="state.scanExceptionAvailable === false" class="exception-empty">
|
||||
<div class="exception-icon exception-icon--warning"><el-icon><WarningFilled /></el-icon></div>
|
||||
<strong>扫码异常暂时无法加载</strong>
|
||||
<span>请稍后点击“重新加载”再试。</span>
|
||||
</div>
|
||||
<div v-else-if="scanExceptions.length" class="exception-list">
|
||||
<div v-for="item in scanExceptions" :key="item.operId" class="exception-item">
|
||||
<span class="exception-icon exception-icon--danger"><el-icon><WarningFilled /></el-icon></span>
|
||||
<span class="exception-copy">
|
||||
<strong>{{ item.source }}</strong>
|
||||
<small>{{ item.operatorName || '未知用户' }} · {{ formatTime(item.occurredAt) }}</small>
|
||||
<span class="exception-message" :title="item.errorMessage">{{ item.errorMessage }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="scanExceptionTotal > scanExceptions.length" class="exception-more">仅展示最近 {{ scanExceptions.length }} 条异常记录</div>
|
||||
</div>
|
||||
<div v-else class="exception-empty">
|
||||
<div class="exception-icon"><el-icon><CircleCheck /></el-icon></div>
|
||||
<strong>暂无扫码异常记录</strong>
|
||||
<span>当前系统没有提供独立异常日志接口,后续接入后将在此展示。</span>
|
||||
<span>最近没有失败的扫码请求。</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</section>
|
||||
@@ -169,7 +199,8 @@
|
||||
|
||||
<script setup name="DashBoard">
|
||||
import * as echarts from 'echarts'
|
||||
import { ArrowRight, Box, CircleCheck, Coin, Download, DocumentAdd, Operation, Upload, Van, Warning, WarningFilled } from '@element-plus/icons-vue'
|
||||
import { ArrowRight, Box, CircleCheck, Coin, Download, DocumentAdd, Operation, Refresh, Upload, Van, Warning, WarningFilled } from '@element-plus/icons-vue'
|
||||
import { listScanExceptions } from '@/api/business/dashboard'
|
||||
import { listInbound } from '@/api/business/inbound'
|
||||
import { listReturn } from '@/api/business/return'
|
||||
import { listShipment } from '@/api/business/shipment'
|
||||
@@ -198,6 +229,9 @@ const state = reactive({
|
||||
pendingItems: [],
|
||||
recentInbound: [],
|
||||
recentShipments: [],
|
||||
scanExceptionAvailable: null,
|
||||
scanExceptionTotal: 0,
|
||||
scanExceptions: [],
|
||||
trendInboundRows: [],
|
||||
trendShipmentRows: []
|
||||
})
|
||||
@@ -223,6 +257,14 @@ const pendingItems = computed(() => state.pendingItems)
|
||||
const pendingTotal = computed(() => state.pendingTotal)
|
||||
const recentInbound = computed(() => state.recentInbound)
|
||||
const recentShipments = computed(() => state.recentShipments)
|
||||
const scanExceptions = computed(() => state.scanExceptions)
|
||||
const scanExceptionTotal = computed(() => state.scanExceptionTotal)
|
||||
const scanExceptionTag = computed(() => {
|
||||
if (state.scanExceptionAvailable === null) return { type: 'info', label: '加载中' }
|
||||
if (state.scanExceptionAvailable === false) return { type: 'warning', label: '暂不可用' }
|
||||
if (scanExceptionTotal.value > 0) return { type: 'danger', label: `${scanExceptionTotal.value} 条` }
|
||||
return { type: 'success', label: '正常' }
|
||||
})
|
||||
const inventoryHasData = computed(() => state.inventoryRows.length > 0)
|
||||
const trendHasData = computed(() => state.trendInboundRows.length > 0 || state.trendShipmentRows.length > 0)
|
||||
const trendRangeLabel = computed(() => TREND_RANGE_OPTIONS.find((option) => option.value === selectedTrendDays.value)?.label || '近 7 天')
|
||||
@@ -352,9 +394,10 @@ async function handleTrendRangeChange() {
|
||||
async function loadDashboardData() {
|
||||
state.loading = true
|
||||
state.partialError = false
|
||||
state.scanExceptionAvailable = null
|
||||
const today = todayQuery()
|
||||
try {
|
||||
const [inventory, todayInbound, todayShipment, recentInboundResponse, recentShipmentResponse, trend, pendingInbound, pendingShipment, pendingReturn] = await Promise.all([
|
||||
const [inventory, todayInbound, todayShipment, recentInboundResponse, recentShipmentResponse, trend, pendingInbound, pendingShipment, pendingReturn, scanExceptionsResponse] = await Promise.all([
|
||||
requestSafely('托盘库存', () => loadAllPages(listPalletInventory), { rows: [], total: 0 }),
|
||||
requestSafely('今日入库', () => listInbound({ ...today, pageNum: 1, pageSize: 1 }), {}),
|
||||
requestSafely('今日出库', () => listShipment({ ...today, pageNum: 1, pageSize: 1 }), {}),
|
||||
@@ -363,7 +406,8 @@ async function loadDashboardData() {
|
||||
fetchTrendData(),
|
||||
requestSafely('待确认入库', () => listInbound({ status: 'DRAFT', pageNum: 1, pageSize: 5 }), {}),
|
||||
requestSafely('待确认出库', () => listShipment({ status: 'DRAFT', pageNum: 1, pageSize: 5 }), {}),
|
||||
requestSafely('待确认退回', () => listReturn({ status: 'REQUESTED', pageNum: 1, pageSize: 5 }), {})
|
||||
requestSafely('待确认退回', () => listReturn({ status: 'REQUESTED', pageNum: 1, pageSize: 5 }), {}),
|
||||
requestSafely('扫码异常', () => listScanExceptions(), null)
|
||||
])
|
||||
|
||||
state.inventoryRows = inventory.rows
|
||||
@@ -381,6 +425,9 @@ async function loadDashboardData() {
|
||||
...getRows(pendingReturn).map((row) => ({ key: `return-${row.returnId}`, title: row.returnNo, type: '退回申请', status: row.status, time: row.requestedAt, path: '/businessOperation/return', tone: 'green' }))
|
||||
].slice(0, 6)
|
||||
state.pendingTotal = getTotal(pendingInbound) + getTotal(pendingShipment) + getTotal(pendingReturn)
|
||||
state.scanExceptionAvailable = Boolean(scanExceptionsResponse)
|
||||
state.scanExceptions = getRows(scanExceptionsResponse)
|
||||
state.scanExceptionTotal = getTotal(scanExceptionsResponse)
|
||||
} finally {
|
||||
state.loading = false
|
||||
await nextTick()
|
||||
@@ -416,15 +463,50 @@ function renderCharts() {
|
||||
chart.clear()
|
||||
if (trendHasData.value) {
|
||||
chart.setOption({
|
||||
color: ['#1677ff', '#16a085'],
|
||||
tooltip: { trigger: 'axis' },
|
||||
color: ['#008596', '#0284c7'],
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: settingsStore.isDark ? 'rgba(23, 27, 34, 0.95)' : 'rgba(255, 255, 255, 0.96)',
|
||||
borderColor: settingsStore.isDark ? '#374151' : '#e2e8f0',
|
||||
textStyle: { color: settingsStore.isDark ? '#f1f5f9' : '#1e293b' }
|
||||
},
|
||||
legend: { bottom: 0, textStyle: { color: chartTextColor() } },
|
||||
grid: { top: 18, right: 20, bottom: 42, left: 42 },
|
||||
xAxis: { type: 'category', boundaryGap: false, data: trendData.value.labels, axisLine: { lineStyle: { color: chartGridColor() } }, axisLabel: { color: chartTextColor(), interval: selectedTrendDays.value > 14 ? 4 : 0, hideOverlap: true } },
|
||||
yAxis: { type: 'value', minInterval: 1, splitLine: { lineStyle: { color: chartGridColor() } }, axisLabel: { color: chartTextColor() } },
|
||||
series: [
|
||||
{ name: '入库单', type: 'line', smooth: true, symbol: 'circle', symbolSize: 7, data: trendData.value.inbound, areaStyle: { opacity: 0.08 } },
|
||||
{ name: '出库包数', type: 'line', smooth: true, symbol: 'circle', symbolSize: 7, data: trendData.value.outbound, areaStyle: { opacity: 0.06 } }
|
||||
{
|
||||
name: '入库单',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 7,
|
||||
itemStyle: { color: '#008596' },
|
||||
lineStyle: { width: 2.5, color: '#008596' },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(0, 133, 150, 0.25)' },
|
||||
{ offset: 1, color: 'rgba(0, 133, 150, 0.01)' }
|
||||
])
|
||||
},
|
||||
data: trendData.value.inbound
|
||||
},
|
||||
{
|
||||
name: '出库包数',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 7,
|
||||
itemStyle: { color: '#0284c7' },
|
||||
lineStyle: { width: 2.5, color: '#0284c7' },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(2, 132, 199, 0.2)' },
|
||||
{ offset: 1, color: 'rgba(2, 132, 199, 0.01)' }
|
||||
])
|
||||
},
|
||||
data: trendData.value.outbound
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
@@ -434,8 +516,14 @@ function renderCharts() {
|
||||
chart.clear()
|
||||
if (inventoryHasData.value) {
|
||||
chart.setOption({
|
||||
color: ['#1677ff', '#16a085', '#f59e0b', '#7c5cff', '#ef6a6a', '#8c9bab'],
|
||||
tooltip: { trigger: 'item', formatter: '{b}<br/>{c} 个托盘({d}%)' },
|
||||
color: ['#008596', '#0284c7', '#10b981', '#f59e0b', '#8b5cf6', '#64748b'],
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}<br/>{c} 个托盘({d}%)',
|
||||
backgroundColor: settingsStore.isDark ? 'rgba(23, 27, 34, 0.95)' : 'rgba(255, 255, 255, 0.96)',
|
||||
borderColor: settingsStore.isDark ? '#374151' : '#e2e8f0',
|
||||
textStyle: { color: settingsStore.isDark ? '#f1f5f9' : '#1e293b' }
|
||||
},
|
||||
legend: { bottom: 0, type: 'scroll', textStyle: { color: chartTextColor() } },
|
||||
series: [{ type: 'pie', radius: ['46%', '72%'], center: ['50%', '44%'], avoidLabelOverlap: true, itemStyle: { borderColor: settingsStore.isDark ? '#141414' : '#fff', borderWidth: 3 }, label: { color: chartTextColor(), formatter: '{b}\n{c}' }, data: inventoryStateSummary.value }]
|
||||
})
|
||||
@@ -481,26 +569,63 @@ onBeforeUnmount(() => {
|
||||
|
||||
.card-heading h2 { margin: 0; color: var(--el-text-color-primary); font-weight: 700; }
|
||||
.card-heading span { margin: 8px 0 0; color: var(--el-text-color-secondary); font-size: 13px; }
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
padding: 4px 2px;
|
||||
|
||||
.header-left {
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--el-text-color-primary, #1e293b);
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
margin: 6px 0 0;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary, #64748b);
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
.dashboard-alert { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; padding: 10px 14px; border: 1px solid var(--el-color-warning-light-5); border-radius: 10px; background: var(--el-color-warning-light-9); color: var(--el-color-warning-dark-2); font-size: 13px; }
|
||||
.dashboard-alert .el-button { margin-left: auto; }
|
||||
|
||||
.kpi-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 14px; margin-bottom: 20px; }
|
||||
.kpi-card { display: flex; align-items: flex-start; gap: 14px; min-height: 124px; padding: 20px; border: 1px solid var(--el-border-color-lighter); border-radius: 14px; background: var(--el-bg-color, #fff); box-shadow: 0 5px 18px rgba(31, 55, 88, .05); }
|
||||
.kpi-card { display: flex; align-items: flex-start; gap: 14px; min-height: 124px; padding: 20px; border: 1px solid var(--el-border-color-lighter); border-radius: 14px; background: var(--el-bg-color, #fff); box-shadow: 0 4px 16px rgba(15, 34, 58, 0.04); transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease; }
|
||||
.kpi-card:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(15, 34, 58, 0.08); border-color: var(--el-color-primary-light-7, #b2e0e6); }
|
||||
.kpi-icon, .quick-action-icon, .activity-icon, .exception-icon { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; border-radius: 12px; }
|
||||
.kpi-icon { width: 42px; height: 42px; font-size: 21px; }
|
||||
.kpi-card--blue .kpi-icon { background: #e9f2ff; color: #1677ff; }.kpi-card--green .kpi-icon { background: #e8f8f2; color: #16a085; }.kpi-card--cyan .kpi-icon { background: #e7f8fb; color: #0891b2; }.kpi-card--orange .kpi-icon { background: #fff2df; color: #d97706; }.kpi-card--red .kpi-icon { background: #ffeded; color: #e25555; }
|
||||
.kpi-content { min-width: 0; }.kpi-label { display: block; color: var(--el-text-color-secondary); font-size: 13px; }.kpi-content strong { display: block; margin: 7px 0 2px; font-size: 28px; line-height: 1; }.kpi-caption { color: var(--el-text-color-placeholder); font-size: 12px; }
|
||||
.kpi-card--blue .kpi-icon { background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%); color: #0284c7; }.kpi-card--green .kpi-icon { background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%); color: #16a34a; }.kpi-card--cyan .kpi-icon { background: linear-gradient(135deg, #e0f7fa 0%, #b2ebf2 100%); color: #008596; }.kpi-card--orange .kpi-icon { background: linear-gradient(135deg, #ffedd5 0%, #fed7aa 100%); color: #ea580c; }.kpi-card--red .kpi-icon { background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%); color: #dc2626; }
|
||||
html.dark {
|
||||
.kpi-card--blue .kpi-icon { background: rgba(2, 132, 199, 0.2); color: #38bdf8; }
|
||||
.kpi-card--green .kpi-icon { background: rgba(22, 163, 74, 0.2); color: #4ade80; }
|
||||
.kpi-card--cyan .kpi-icon { background: rgba(0, 133, 150, 0.2); color: #22d3ee; }
|
||||
.kpi-card--orange .kpi-icon { background: rgba(234, 88, 12, 0.2); color: #fb923c; }
|
||||
.kpi-card--red .kpi-icon { background: rgba(220, 38, 38, 0.2); color: #f87171; }
|
||||
}
|
||||
.kpi-content { min-width: 0; }.kpi-label { display: block; color: var(--el-text-color-secondary); font-size: 13px; }.kpi-content strong { display: block; margin: 7px 0 2px; font-size: 28px; line-height: 1; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-variant-numeric: tabular-nums; font-weight: 700; color: var(--el-text-color-primary); }.kpi-caption { color: var(--el-text-color-placeholder); font-size: 12px; }
|
||||
|
||||
.dashboard-section, .dashboard-card { border: 1px solid var(--el-border-color-lighter); border-radius: 14px; background: var(--el-bg-color, #fff); box-shadow: 0 5px 18px rgba(31, 55, 88, .04); }
|
||||
.quick-section { margin-bottom: 20px; padding: 20px; }.card-heading h2 { font-size: 17px; }
|
||||
.quick-action-grid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 10px; }.quick-action { display: flex; align-items: center; gap: 10px; min-width: 0; padding: 12px; border: 1px solid var(--el-border-color-lighter); border-radius: 10px; background: transparent; color: inherit; text-align: left; cursor: pointer; transition: border-color .2s ease, background-color .2s ease, transform .2s ease; }.quick-action:hover { border-color: var(--el-color-primary-light-5); background: var(--el-color-primary-light-9); transform: translateY(-1px); }.quick-action-icon { width: 34px; height: 34px; background: var(--el-color-primary-light-9); color: var(--el-color-primary); font-size: 18px; }.quick-action-copy { display: flex; flex-direction: column; min-width: 0; gap: 3px; }.quick-action-copy strong { font-size: 13px; white-space: nowrap; }.quick-action-copy small { overflow: hidden; color: var(--el-text-color-secondary); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }.quick-action-arrow { margin-left: auto; color: var(--el-text-color-placeholder); }
|
||||
.quick-action-grid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 10px; }.quick-action { display: flex; align-items: center; gap: 10px; min-width: 0; padding: 12px 14px; border: 1px solid var(--el-border-color-lighter); border-radius: 12px; background: var(--el-bg-color, #fff); color: inherit; text-align: left; cursor: pointer; transition: all 0.2s ease; }.quick-action:hover { border-color: var(--el-color-primary-light-5); background: var(--el-color-primary-light-9); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 133, 150, 0.08); }.quick-action:hover .quick-action-arrow { transform: translateX(3px); color: var(--el-color-primary); }.quick-action-icon { width: 34px; height: 34px; background: var(--el-color-primary-light-9); color: var(--el-color-primary); font-size: 18px; }.quick-action-copy { display: flex; flex-direction: column; min-width: 0; gap: 3px; }.quick-action-copy strong { font-size: 13px; white-space: nowrap; }.quick-action-copy small { overflow: hidden; color: var(--el-text-color-secondary); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }.quick-action-arrow { margin-left: auto; color: var(--el-text-color-placeholder); transition: transform 0.2s ease, color 0.2s ease; }
|
||||
|
||||
.dashboard-grid { display: grid; gap: 20px; margin-bottom: 20px; }.chart-grid { grid-template-columns: minmax(0, 1.55fr) minmax(340px, 1fr); }.operational-grid { grid-template-columns: minmax(0, 1.1fr) minmax(360px, .9fr); }.activity-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); margin-bottom: 0; }.dashboard-card :deep(.el-card__header) { padding: 18px 20px; border-bottom-color: var(--el-border-color-lighter); }.dashboard-card :deep(.el-card__body) { padding: 18px 20px; }.dashboard-card { overflow: hidden; }.card-heading { justify-content: space-between; gap: 12px; }.card-heading > div { min-width: 0; }.card-heading span { display: block; margin-top: 5px; font-size: 12px; }
|
||||
.trend-range-select { width: 112px; flex: 0 0 auto; }.chart-wrapper { position: relative; min-height: 290px; }.chart-wrapper--distribution { min-height: 290px; }.chart-canvas { width: 100%; height: 290px; }.chart-loading, .chart-empty { position: absolute; inset: 20px; }
|
||||
|
||||
.warehouse-ranking, .pending-list, .activity-list { min-height: 250px; }.warehouse-row { display: flex; align-items: center; gap: 12px; padding: 12px 0; }.warehouse-row + .warehouse-row { border-top: 1px solid var(--el-border-color-lighter); }.warehouse-rank { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 7px; background: var(--el-fill-color-light); color: var(--el-text-color-secondary); font-size: 12px; font-weight: 700; }.warehouse-rank--top { background: var(--el-color-primary-light-9); color: var(--el-color-primary); }.warehouse-main { flex: 1; min-width: 0; }.warehouse-title { justify-content: space-between; gap: 10px; margin-bottom: 8px; font-size: 13px; }.warehouse-title span { color: var(--el-text-color-placeholder); font-size: 12px; }.warehouse-value { flex: 0 0 84px; color: var(--el-text-color-primary); font-size: 13px; text-align: right; }
|
||||
.pending-item, .activity-item { display: flex; align-items: center; width: 100%; gap: 10px; padding: 12px 0; border: 0; border-bottom: 1px solid var(--el-border-color-lighter); background: transparent; color: inherit; text-align: left; cursor: pointer; }.pending-item:last-child, .activity-item:last-child { border-bottom: 0; }.pending-item:hover .pending-copy strong, .activity-item:hover .activity-copy strong { color: var(--el-color-primary); }.pending-dot { width: 8px; height: 8px; border-radius: 50%; }.pending-dot--orange { background: #f59e0b; }.pending-dot--blue { background: #1677ff; }.pending-dot--green { background: #16a085; }.pending-copy, .activity-copy { display: flex; flex: 1; flex-direction: column; min-width: 0; gap: 4px; }.pending-copy strong, .activity-copy strong { overflow: hidden; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }.pending-copy small, .activity-copy small { color: var(--el-text-color-secondary); font-size: 12px; }.activity-icon { width: 34px; height: 34px; font-size: 16px; }.activity-icon--inbound { background: #eaf3ff; color: #1677ff; }.activity-icon--outbound { background: #fff1e8; color: #d97706; }.activity-quantity { color: var(--el-color-primary); font-size: 12px; font-weight: 700; white-space: nowrap; }
|
||||
.exception-empty { display: flex; min-height: 250px; flex-direction: column; align-items: center; justify-content: center; gap: 8px; color: var(--el-text-color-secondary); text-align: center; }.exception-icon { width: 52px; height: 52px; margin-bottom: 5px; background: var(--el-color-success-light-9); color: var(--el-color-success); font-size: 26px; }.exception-empty strong { color: var(--el-text-color-primary); font-size: 14px; }.exception-empty span { max-width: 230px; font-size: 12px; line-height: 1.6; }
|
||||
.pending-item, .activity-item { display: flex; align-items: center; width: 100%; gap: 10px; padding: 12px 8px; border: 0; border-bottom: 1px solid var(--el-border-color-lighter); background: transparent; color: inherit; text-align: left; cursor: pointer; transition: all 0.2s ease; border-radius: 8px; }.pending-item:last-child, .activity-item:last-child { border-bottom: 0; }.pending-item:hover, .activity-item:hover { background: var(--el-fill-color-light, #f8fafc); }.pending-item:hover .pending-copy strong, .activity-item:hover .activity-copy strong { color: var(--el-color-primary); }.pending-dot { width: 8px; height: 8px; border-radius: 50%; }.pending-dot--orange { background: #f59e0b; }.pending-dot--blue { background: #1677ff; }.pending-dot--green { background: #16a085; }.pending-copy, .activity-copy { display: flex; flex: 1; flex-direction: column; min-width: 0; gap: 4px; }.pending-copy strong, .activity-copy strong { overflow: hidden; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }.pending-copy small, .activity-copy small { color: var(--el-text-color-secondary); font-size: 12px; }.activity-icon { width: 34px; height: 34px; font-size: 16px; }.activity-icon--inbound { background: #eaf3ff; color: #1677ff; }.activity-icon--outbound { background: #fff1e8; color: #d97706; }.activity-quantity { color: var(--el-color-primary); font-size: 12px; font-weight: 700; white-space: nowrap; }
|
||||
.exception-list { min-height: 250px; }.exception-item { display: flex; align-items: flex-start; gap: 10px; padding: 12px 0; border-bottom: 1px solid var(--el-border-color-lighter); }.exception-item:last-child { border-bottom: 0; }.exception-copy { display: flex; flex: 1; min-width: 0; flex-direction: column; gap: 4px; }.exception-copy strong { overflow: hidden; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }.exception-copy small { color: var(--el-text-color-secondary); font-size: 12px; }.exception-message { overflow: hidden; color: var(--el-text-color-secondary); font-size: 12px; line-height: 1.5; text-overflow: ellipsis; white-space: nowrap; }.exception-more { padding-top: 10px; color: var(--el-text-color-placeholder); font-size: 12px; text-align: center; }.exception-empty { display: flex; min-height: 250px; flex-direction: column; align-items: center; justify-content: center; gap: 8px; color: var(--el-text-color-secondary); text-align: center; }.exception-icon { width: 52px; height: 52px; margin-bottom: 5px; background: var(--el-color-success-light-9); color: var(--el-color-success); font-size: 26px; }.exception-icon--danger { width: 34px; height: 34px; margin: 0; border-radius: 10px; background: var(--el-color-danger-light-9); color: var(--el-color-danger); font-size: 17px; }.exception-icon--warning { background: var(--el-color-warning-light-9); color: var(--el-color-warning); }.exception-empty strong { color: var(--el-text-color-primary); font-size: 14px; }.exception-empty span { max-width: 230px; font-size: 12px; line-height: 1.6; }
|
||||
|
||||
@media (max-width: 1280px) { .kpi-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }.quick-action-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }.activity-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }.activity-card:last-child { grid-column: span 2; } }
|
||||
@media (max-width: 900px) { .dashboard-page { padding: 16px; }.chart-grid, .operational-grid { grid-template-columns: 1fr; } }
|
||||
|
||||
@@ -1,68 +1,101 @@
|
||||
<template>
|
||||
<div class="login">
|
||||
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
|
||||
<h3 class="title">{{ title }}</h3>
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
type="text"
|
||||
size="large"
|
||||
auto-complete="off"
|
||||
placeholder="账号"
|
||||
>
|
||||
<template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
size="large"
|
||||
auto-complete="off"
|
||||
placeholder="密码"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="code" v-if="captchaEnabled">
|
||||
<el-input
|
||||
v-model="loginForm.code"
|
||||
size="large"
|
||||
auto-complete="off"
|
||||
placeholder="验证码"
|
||||
style="width: 63%"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
|
||||
</el-input>
|
||||
<div class="login-code">
|
||||
<img :src="codeUrl" @click="getCode" class="login-code-img"/>
|
||||
<div class="login login-wrapper">
|
||||
<canvas ref="canvasRef" class="bg-canvas"></canvas>
|
||||
<div class="noise-overlay"></div>
|
||||
<div class="login-box">
|
||||
<div class="login-card">
|
||||
<div class="brand-header">
|
||||
<div class="brand-logo-wrap">
|
||||
<img :src="brandLogoLight" :alt="title" class="brand-logo brand-logo-light" />
|
||||
<img :src="brandLogoDark" :alt="title" class="brand-logo brand-logo-dark" />
|
||||
</div>
|
||||
<h3 class="system-title title">{{ title }}</h3>
|
||||
<div class="feature-badges">
|
||||
<span class="badge-item">托盘全流程追溯</span>
|
||||
<span class="badge-divider">·</span>
|
||||
<span class="badge-item">智能防错防漏</span>
|
||||
<span class="badge-divider">·</span>
|
||||
<span class="badge-item">动态精益运营</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
|
||||
<el-form-item style="width:100%;">
|
||||
<el-button
|
||||
:loading="loading"
|
||||
size="large"
|
||||
type="primary"
|
||||
style="width:100%;"
|
||||
@click.prevent="handleLogin"
|
||||
>
|
||||
<span v-if="!loading">登 录</span>
|
||||
<span v-else>登 录 中...</span>
|
||||
</el-button>
|
||||
<div style="float: right;" v-if="register">
|
||||
<router-link class="link-type" :to="'/register'">立即注册</router-link>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
|
||||
<el-form-item prop="username" class="custom-field">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
type="text"
|
||||
size="large"
|
||||
auto-complete="off"
|
||||
placeholder="账号"
|
||||
>
|
||||
<template #prefix>
|
||||
<svg-icon icon-class="user" class="input-icon" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password" class="custom-field">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
size="large"
|
||||
auto-complete="off"
|
||||
placeholder="密码"
|
||||
show-password
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<template #prefix>
|
||||
<svg-icon icon-class="password" class="input-icon" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="code" v-if="captchaEnabled" class="custom-field captcha-form-item">
|
||||
<div class="captcha-flex-row">
|
||||
<el-input
|
||||
v-model="loginForm.code"
|
||||
size="large"
|
||||
auto-complete="off"
|
||||
placeholder="验证码"
|
||||
class="captcha-input"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<template #prefix>
|
||||
<svg-icon icon-class="validCode" class="input-icon" />
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="login-code" @click="getCode" title="点击刷新验证码">
|
||||
<img :src="codeUrl" alt="验证码" class="login-code-img" />
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<div class="login-options-row">
|
||||
<el-checkbox v-model="loginForm.rememberMe">记住密码</el-checkbox>
|
||||
<router-link v-if="register" class="register-link" :to="'/register'">立即注册</router-link>
|
||||
</div>
|
||||
|
||||
<el-form-item class="submit-form-item">
|
||||
<el-button
|
||||
:loading="loading"
|
||||
size="large"
|
||||
type="primary"
|
||||
class="login-submit-btn"
|
||||
@click.prevent="handleLogin"
|
||||
>
|
||||
<span v-if="!loading">登 录</span>
|
||||
<span v-else>登 录 中...</span>
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 底部 -->
|
||||
<div class="el-login-footer">
|
||||
<span>{{ footerContent }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getCodeImg } from "@/api/login";
|
||||
@@ -70,6 +103,8 @@ import Cookies from "js-cookie";
|
||||
import { encrypt, decrypt } from "@/utils/jsencrypt";
|
||||
import useUserStore from '@/store/modules/user'
|
||||
import defaultSettings from '@/settings'
|
||||
import brandLogoDark from '@/assets/logo/sylvan-logo.svg'
|
||||
import brandLogoLight from '@/assets/logo/sylvan-logo-light.svg'
|
||||
|
||||
const title = import.meta.env.VITE_APP_TITLE;
|
||||
const footerContent = defaultSettings.footerContent
|
||||
@@ -162,83 +197,532 @@ function getCookie() {
|
||||
};
|
||||
}
|
||||
|
||||
const canvasRef = ref(null);
|
||||
let cleanupBackground = null;
|
||||
|
||||
class GeometricShape {
|
||||
constructor(anchorXRatio, anchorYRatio, radiusRatio, color, style = 'fill', ringCount = 1) {
|
||||
this.anchorXRatio = anchorXRatio;
|
||||
this.anchorYRatio = anchorYRatio;
|
||||
this.radiusRatio = radiusRatio;
|
||||
this.color = color;
|
||||
this.style = style;
|
||||
this.ringCount = ringCount;
|
||||
this.phaseX = Math.random() * Math.PI * 2;
|
||||
this.phaseY = Math.random() * Math.PI * 2;
|
||||
this.phaseR = Math.random() * Math.PI * 2;
|
||||
this.speed = 0.0003 + Math.random() * 0.0002;
|
||||
}
|
||||
|
||||
draw(ctx, w, h, t) {
|
||||
const baseX = w * this.anchorXRatio;
|
||||
const baseY = h * this.anchorYRatio;
|
||||
const maxDim = Math.max(w, h);
|
||||
const baseRadius = maxDim * this.radiusRatio;
|
||||
|
||||
const x = baseX + Math.sin(t * this.speed + this.phaseX) * (maxDim * 0.015);
|
||||
const y = baseY + Math.cos(t * this.speed * 0.8 + this.phaseY) * (maxDim * 0.015);
|
||||
const r = baseRadius + Math.sin(t * this.speed * 1.2 + this.phaseR) * (baseRadius * 0.03);
|
||||
|
||||
if (this.style === 'fill') {
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, r, 0, Math.PI * 2);
|
||||
ctx.fillStyle = this.color;
|
||||
ctx.fill();
|
||||
} else if (this.style === 'stroke') {
|
||||
for (let i = 0; i < this.ringCount; i++) {
|
||||
ctx.beginPath();
|
||||
const ringR = r + i * (maxDim * 0.025);
|
||||
ctx.arc(x, y, ringR, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = this.color;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AmbientGlow {
|
||||
constructor(xRatio, yRatio, radiusRatio, r, g, b, maxOpacity, speedX, speedY) {
|
||||
this.xRatio = xRatio;
|
||||
this.yRatio = yRatio;
|
||||
this.radiusRatio = radiusRatio;
|
||||
this.color = `${r}, ${g}, ${b}`;
|
||||
this.maxOpacity = maxOpacity;
|
||||
this.speedX = speedX;
|
||||
this.speedY = speedY;
|
||||
this.phaseX = Math.random() * Math.PI * 2;
|
||||
this.phaseY = Math.random() * Math.PI * 2;
|
||||
}
|
||||
|
||||
draw(ctx, w, h, t) {
|
||||
const maxDim = Math.max(w, h);
|
||||
const radius = maxDim * this.radiusRatio;
|
||||
const x = w * this.xRatio + Math.sin(t * this.speedX + this.phaseX) * (w * 0.2);
|
||||
const y = h * this.yRatio + Math.cos(t * this.speedY + this.phaseY) * (h * 0.2);
|
||||
|
||||
const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius);
|
||||
gradient.addColorStop(0, `rgba(${this.color}, ${this.maxOpacity})`);
|
||||
gradient.addColorStop(1, `rgba(${this.color}, 0)`);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
function initDynamicBackground() {
|
||||
const canvas = canvasRef.value;
|
||||
if (!canvas) return null;
|
||||
|
||||
const ctx = canvas.getContext('2d', { alpha: false });
|
||||
if (!ctx) return null;
|
||||
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
let time = 0;
|
||||
let animationFrameId = null;
|
||||
|
||||
function resizeCanvas() {
|
||||
if (!canvas) return;
|
||||
width = window.innerWidth;
|
||||
height = window.innerHeight;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = width * dpr;
|
||||
canvas.height = height * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
resizeCanvas();
|
||||
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
|
||||
const shapes = isDark ? [
|
||||
new GeometricShape(-0.02, 0.15, 0.35, '#162238', 'fill'),
|
||||
new GeometricShape(0.08, -0.05, 0.22, '#008596', 'fill'),
|
||||
new GeometricShape(-0.1, -0.1, 0.25, 'rgba(56, 189, 248, 0.08)', 'stroke', 6),
|
||||
new GeometricShape(0.85, 0.95, 0.25, '#162238', 'fill'),
|
||||
new GeometricShape(0.98, 0.88, 0.15, '#008596', 'fill'),
|
||||
new GeometricShape(1.05, 1.05, 0.15, 'rgba(56, 189, 248, 0.12)', 'stroke', 4),
|
||||
new GeometricShape(0.05, 1.0, 0.18, '#0e4e5e', 'fill'),
|
||||
new GeometricShape(0.95, -0.05, 0.2, '#008596', 'fill')
|
||||
] : [
|
||||
new GeometricShape(-0.02, 0.15, 0.35, '#262464', 'fill'),
|
||||
new GeometricShape(0.08, -0.05, 0.22, '#00A89D', 'fill'),
|
||||
new GeometricShape(-0.1, -0.1, 0.25, 'rgba(255, 255, 255, 0.08)', 'stroke', 6),
|
||||
new GeometricShape(0.85, 0.95, 0.25, '#262464', 'fill'),
|
||||
new GeometricShape(0.98, 0.88, 0.15, '#00A89D', 'fill'),
|
||||
new GeometricShape(1.05, 1.05, 0.15, 'rgba(0, 168, 157, 0.15)', 'stroke', 4),
|
||||
new GeometricShape(0.05, 1.0, 0.18, '#6ec6c2', 'fill'),
|
||||
new GeometricShape(0.95, -0.05, 0.2, '#00A89D', 'fill')
|
||||
];
|
||||
|
||||
const glows = isDark ? [
|
||||
new AmbientGlow(0.3, 0.7, 0.5, 0, 133, 150, 0.12, 0.0001, 0.00015),
|
||||
new AmbientGlow(0.7, 0.3, 0.6, 22, 34, 56, 0.18, 0.00012, 0.00008),
|
||||
new AmbientGlow(0.5, 0.5, 0.7, 14, 78, 94, 0.10, -0.00008, 0.00011)
|
||||
] : [
|
||||
new AmbientGlow(0.3, 0.7, 0.5, 0, 168, 157, 0.08, 0.0001, 0.00015),
|
||||
new AmbientGlow(0.7, 0.3, 0.6, 70, 80, 150, 0.06, 0.00012, 0.00008),
|
||||
new AmbientGlow(0.5, 0.5, 0.7, 110, 198, 194, 0.07, -0.00008, 0.00011)
|
||||
];
|
||||
|
||||
function drawBackground() {
|
||||
const isNowDark = document.documentElement.classList.contains('dark');
|
||||
ctx.fillStyle = isNowDark ? '#0b1420' : '#f0f6f7';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
function animate() {
|
||||
drawBackground();
|
||||
|
||||
glows.forEach(glow => {
|
||||
glow.draw(ctx, width, height, time);
|
||||
});
|
||||
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
|
||||
shapes.forEach(shape => {
|
||||
shape.draw(ctx, width, height, time);
|
||||
});
|
||||
|
||||
time += 16;
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
|
||||
return () => {
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
animationFrameId = null;
|
||||
}
|
||||
window.removeEventListener('resize', resizeCanvas);
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
cleanupBackground = initDynamicBackground();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (cleanupBackground) {
|
||||
cleanupBackground();
|
||||
cleanupBackground = null;
|
||||
}
|
||||
});
|
||||
|
||||
getCode();
|
||||
getCookie();
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.login {
|
||||
.login-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
background-color: #f0f6f7;
|
||||
}
|
||||
|
||||
html.dark .login-wrapper {
|
||||
background-color: #0b1420;
|
||||
}
|
||||
|
||||
.bg-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.noise-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
opacity: 0.04;
|
||||
background: url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.login-box {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: rgba(255, 255, 255, 0.90);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.85);
|
||||
border-radius: 20px;
|
||||
padding: 38px 36px 30px;
|
||||
box-shadow: 0 20px 60px rgba(15, 34, 58, 0.12), 0 4px 16px rgba(15, 34, 58, 0.05);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
html.dark .login-card {
|
||||
background: rgba(23, 27, 34, 0.92);
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.55), 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.brand-header {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.brand-logo-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
background-image: url("../assets/images/login-background.jpg");
|
||||
background-size: cover;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
html.dark .login {
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url("../assets/images/login-background.jpg");
|
||||
}
|
||||
.title {
|
||||
margin: 0px auto 30px auto;
|
||||
text-align: center;
|
||||
color: #707070;
|
||||
.brand-logo {
|
||||
height: 56px;
|
||||
max-width: 240px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 4px 10px rgba(15, 34, 58, 0.08));
|
||||
transition: transform 0.25s ease, filter 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
filter: drop-shadow(0 6px 14px rgba(15, 34, 58, 0.12));
|
||||
}
|
||||
}
|
||||
|
||||
html.dark .title {
|
||||
color: var(--el-text-color-primary);
|
||||
.brand-logo-light {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.brand-logo-dark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
.brand-logo-light {
|
||||
display: none;
|
||||
}
|
||||
.brand-logo-dark {
|
||||
display: block;
|
||||
filter: drop-shadow(0 4px 12px rgba(56, 189, 248, 0.16));
|
||||
|
||||
&:hover {
|
||||
filter: drop-shadow(0 6px 16px rgba(56, 189, 248, 0.22));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.system-title {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
color: #1a2736;
|
||||
}
|
||||
|
||||
html.dark .system-title {
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
.feature-badges {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.badge-item {
|
||||
font-size: 12px;
|
||||
color: #008596;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
html.dark .badge-item {
|
||||
color: #38bdf8;
|
||||
}
|
||||
|
||||
.badge-divider {
|
||||
color: #cbd5e1;
|
||||
font-size: 12px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html.dark .badge-divider {
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
width: 400px;
|
||||
padding: 25px 25px 5px 25px;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
.el-input {
|
||||
height: 40px;
|
||||
input {
|
||||
height: 40px;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
border-radius: 10px;
|
||||
padding: 2px 14px;
|
||||
height: 44px;
|
||||
box-shadow: 0 0 0 1px #dcdfe6 inset;
|
||||
transition: box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px #b0b8c4 inset;
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
box-shadow: 0 0 0 2px #008596 inset !important;
|
||||
}
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
height: 39px;
|
||||
width: 14px;
|
||||
margin-left: 0px;
|
||||
font-size: 16px;
|
||||
color: #008596;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-input__password) {
|
||||
color: #94a3b8;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #008596;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html.dark .login-form {
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
.login-tip {
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
.login-code {
|
||||
width: 33%;
|
||||
height: 40px;
|
||||
float: right;
|
||||
img {
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
:deep(.el-input__wrapper) {
|
||||
background-color: #1a202c;
|
||||
box-shadow: 0 0 0 1px #374151 inset;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px #4b5563 inset;
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
box-shadow: 0 0 0 2px #38bdf8 inset !important;
|
||||
}
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
color: #38bdf8;
|
||||
}
|
||||
|
||||
:deep(.el-input__password) {
|
||||
color: #64748b;
|
||||
|
||||
&:hover {
|
||||
color: #38bdf8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.captcha-flex-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.captcha-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.login-code {
|
||||
width: 120px;
|
||||
height: 44px;
|
||||
flex: 0 0 120px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #dcdfe6;
|
||||
background: #f8fafc;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: opacity 0.2s ease, border-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
border-color: #008596;
|
||||
}
|
||||
|
||||
.login-code-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
html.dark .login-code {
|
||||
background: #1a202c;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
.login-options-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: -4px 0 18px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.register-link {
|
||||
color: #008596;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #0ea5e9;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
html.dark .register-link {
|
||||
color: #38bdf8;
|
||||
}
|
||||
|
||||
.submit-form-item {
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
|
||||
.login-submit-btn {
|
||||
width: 100%;
|
||||
height: 46px;
|
||||
border-radius: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2px;
|
||||
border: none;
|
||||
background: linear-gradient(135deg, #008596 0%, #0284c7 100%);
|
||||
box-shadow: 0 8px 22px rgba(0, 133, 150, 0.32);
|
||||
transition: all 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #009cb0 0%, #0396dc 100%);
|
||||
box-shadow: 0 10px 26px rgba(0, 133, 150, 0.42);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 133, 150, 0.28);
|
||||
}
|
||||
}
|
||||
|
||||
.el-login-footer {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-family: Arial;
|
||||
color: #64748b;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
letter-spacing: 1px;
|
||||
letter-spacing: 0.5px;
|
||||
z-index: 2;
|
||||
}
|
||||
.login-code-img {
|
||||
height: 40px;
|
||||
padding-left: 12px;
|
||||
|
||||
html.dark .el-login-footer {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-box {
|
||||
padding: 14px;
|
||||
}
|
||||
.login-card {
|
||||
padding: 28px 20px 22px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
.system-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user