diff --git a/inventory-backend/app/__init__.py b/inventory-backend/app/__init__.py index 48955dd..b5bb80a 100644 --- a/inventory-backend/app/__init__.py +++ b/inventory-backend/app/__init__.py @@ -2,7 +2,6 @@ from flask import Flask from config import Config -# 【修改】增加 jwt 引入 from app.extensions import db, migrate, cors, jwt import os @@ -16,36 +15,44 @@ def create_app(): # ========================================================= db.init_app(app) migrate.init_app(app, db) + jwt.init_app(app) # 初始化 JWT - # 【新增】初始化 JWT (用于 Token 认证) - jwt.init_app(app) - - # 确保跨域配置 - # 允许 /api/ 开头的请求跨域 - cors.init_app(app, resources={r"/*": {"origins": "*"}}) + # 允许所有 /api/ 开头的请求跨域,支持 credentials + cors.init_app(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True) # ========================================================= # 2. 注册蓝图 (Blueprints) + # --------------------------------------------------------- + # 注意:为了解决前端请求不带 /v1 导致的 404 错误, + # 下面的模块都采用了 "双重注册" 策略: + # 1. 标准地址: /api/v1/... + # 2. 兼容地址: /api/... (name 参数必须不同) # ========================================================= # ----------------------------------------------------- - # 2.0 [新增] 注册权限与认证模块 (Auth) - 最关键修复 + # 2.0 注册权限与认证模块 (Auth) # ----------------------------------------------------- try: from app.api.v1.auth import auth_bp - # 前端请求地址: /api/v1/auth/login + # 标准 app.register_blueprint(auth_bp, url_prefix='/api/v1/auth') - print("✅ Auth (System & Login) 模块注册成功") + # 兼容 (防止前端忘记写 v1) + app.register_blueprint(auth_bp, url_prefix='/api/auth', name='auth_legacy') + print("✅ Auth 模块注册成功") except ImportError as e: print(f"❌ 错误: Auth 模块导入失败: {e}") # ----------------------------------------------------- - # 2.1 注册入库聚合模块 (Inbound) + # 2.1 注册入库聚合模块 (Inbound) - 【核心修复点】 # ----------------------------------------------------- try: from app.api.v1.inbound import inbound_bp + # 标准: /api/v1/inbound/base/list app.register_blueprint(inbound_bp, url_prefix='/api/v1/inbound') - print("✅ Inbound (Buy, Semi, Product, Base) 模块注册成功") + + # 兼容: /api/inbound/base/list (修复前端 404) + app.register_blueprint(inbound_bp, url_prefix='/api/inbound', name='inbound_legacy') + print("✅ Inbound 模块注册成功 (已启用兼容模式: /api/inbound)") except ImportError as e: print(f"❌ 错误: Inbound 模块导入失败: {e}") @@ -55,7 +62,8 @@ def create_app(): try: from app.api.v1.common.print import print_bp app.register_blueprint(print_bp, url_prefix='/api/v1/common/print') - print("✅ Print (Label Printing) 模块注册成功") + app.register_blueprint(print_bp, url_prefix='/api/common/print', name='print_legacy') + print("✅ Print 模块注册成功") except ImportError as e: print(f"❌ 错误: Print 模块导入失败: {e}") @@ -64,51 +72,44 @@ def create_app(): # ----------------------------------------------------- try: from app.api.v1.common.upload import upload_bp - # 注册方式 1: 标准路径 app.register_blueprint(upload_bp, url_prefix='/api/v1/common') - # 注册方式 2: 兼容路径 (防止反向代理剥离 /api) - app.register_blueprint(upload_bp, url_prefix='/v1/common', name='upload_fallback') - print("✅ Upload (File Storage) 模块注册成功 (双路径兼容模式)") + app.register_blueprint(upload_bp, url_prefix='/api/common', name='upload_legacy') + print("✅ Upload 模块注册成功") except ImportError as e: print(f"❌ 错误: Upload 模块导入失败: {e}") # ----------------------------------------------------- - # 2.4 [新增] 注册业务操作模块 (Transactions) + # 2.4 注册业务操作模块 (Transactions) # ----------------------------------------------------- try: - # 对应 borrow, return, scrap 等操作 from app.api.v1.transactions import trans_bp app.register_blueprint(trans_bp, url_prefix='/api/v1/trans') - print("✅ Transactions (Borrow, Return, Scrap) 模块注册成功") + app.register_blueprint(trans_bp, url_prefix='/api/trans', name='trans_legacy') + print("✅ Transactions 模块注册成功") except ImportError as e: - # 如果文件还没写好,这里会报错,但不影响主程序启动 - print(f"⚠️ 警告: Transaction 模块导入失败 (如果是新建项目可忽略): {e}") + # 允许模块不存在时不崩溃 + print(f"⚠️ 提示: Transaction 模块尚未创建或导入失败: {e}") # ========================================================= - # 3. 预加载数据模型 (解决 relationship 找不到模型的问题) + # 3. 预加载数据模型 # ========================================================= with app.app_context(): try: - # 1. 基础物料 + # 基础与库存模型 from app.models.base import MaterialBase - # 2. 采购入库 from app.models.inbound.buy import StockBuy - # 3. 半成品入库 from app.models.inbound.semi import StockSemi - # 4. 成品入库 from app.models.inbound.product import StockProduct - # 【新增】5. 系统用户 (关键:确保创建 user 表) + # 系统与业务模型 from app.models.system import SysUser, SysLog - - # 【新增】6. 业务流水 from app.models.transaction import TransBorrow, TransRepair, TransScrap - # 开发环境自动建表 (根据之前的对话,强烈建议在容器第一次启动时开启或手动调用) + # 首次运行时可取消注释自动建表 (但在生产环境建议使用 flask db upgrade) # db.create_all() except ImportError as e: - print(f"⚠️ 模型预加载失败: {e}") + print(f"⚠️ 模型预加载部分失败 (检查是否缺少文件): {e}") except Exception as e: print(f"⚠️ 模型预加载发生未知错误: {e}") diff --git a/inventory-backend/app/api/v1/auth.py b/inventory-backend/app/api/v1/auth.py index a87713b..0b981ad 100644 --- a/inventory-backend/app/api/v1/auth.py +++ b/inventory-backend/app/api/v1/auth.py @@ -1,5 +1,5 @@ # app/api/v1/auth.py -from flask import Blueprint, request, jsonify +from flask import Blueprint, request, jsonify, current_app from flask_jwt_extended import jwt_required, get_jwt from app.services.auth_service import AuthService @@ -10,13 +10,38 @@ auth_bp = Blueprint('auth', __name__) def login(): try: data = request.get_json() + if not data: + return jsonify({'msg': '无效的请求数据'}), 400 + if not data.get('username') or not data.get('password'): return jsonify({'msg': '请输入用户名和密码'}), 400 + # 调用 Service 层逻辑 result = AuthService.login(data) - return jsonify({'msg': '登录成功', 'data': result}), 200 + + # [关键修复] + # 前端 store 代码写的是: token.value = res.data.access_token + # 所以我们这里不能把 access_token 包裹在 data 字段里, + # 而是应该直接合并返回,或者让前端去 data.data 里面取。 + # 为了不改前端,我们这里做解构返回: + + response_data = { + 'msg': '登录成功', + 'access_token': result.get('access_token'), + 'user': result.get('user') + } + + return jsonify(response_data), 200 + + except ValueError as ve: + # 捕获已知的业务错误(如密码错误、用户不存在) + return jsonify({'msg': str(ve)}), 401 except Exception as e: - return jsonify({'msg': str(e)}), 401 + # [关键修复] 打印详细报错到控制台,方便排查 500 错误 + # (例如数据库连接失败、表不存在等) + current_app.logger.error(f"Login Failed Error: {str(e)}") + # 生产环境不建议直接把 error 返回给前端,但调试阶段很有用 + return jsonify({'msg': f'服务器内部错误: {str(e)}'}), 500 # 新增:创建用户 (替代了原来的注册) @@ -30,8 +55,13 @@ def create_user(): claims = get_jwt() operator_role = claims.get('role') + # 增加一个简单的权限判断(可选) + if operator_role not in ['super_admin', 'supervisor']: + return jsonify({'msg': '权限不足,无法创建用户'}), 403 + result = AuthService.create_user(data, operator_role) return jsonify({'msg': '用户创建成功', 'data': result}), 201 + except Exception as e: - # 这里虽然返回 400,但实际可能包含 403 的含义,具体看前端处理 + current_app.logger.error(f"User Create Failed: {str(e)}") return jsonify({'msg': str(e)}), 400 \ No newline at end of file diff --git a/inventory-backend/app/api/v1/transactions.py b/inventory-backend/app/api/v1/transactions.py index 8ad35cc..93da180 100644 --- a/inventory-backend/app/api/v1/transactions.py +++ b/inventory-backend/app/api/v1/transactions.py @@ -1,8 +1,12 @@ from flask import Blueprint, jsonify -# 定义蓝图,名字叫 'transactions' +# 定义蓝图 +# 注意:这个变量名 trans_bp 必须与 app/__init__.py 中注册时引用的名字一致 trans_bp = Blueprint('transactions', __name__) @trans_bp.route('/test', methods=['GET']) def test_transaction(): - return jsonify({"message": "Transaction module is working"}) \ No newline at end of file + """ + 测试接口:用于验证 Transaction 模块是否加载成功 + """ + return jsonify({"message": "Transaction module is working", "status": "success"}) \ No newline at end of file diff --git a/inventory-backend/app/models/base.py b/inventory-backend/app/models/base.py index c095b8f..b6cffcc 100644 --- a/inventory-backend/app/models/base.py +++ b/inventory-backend/app/models/base.py @@ -10,7 +10,8 @@ class MaterialBase(db.Model): # 1. 基础字段 id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(255), nullable=False, comment='基础信息名称') + name = db.Column(db.String(255), nullable=False, comment='名称') + common_name = db.Column(db.String(255), comment='俗名') # ✅ 新增字段 category = db.Column(db.String(100), comment='类别') material_type = db.Column(db.String(100), comment='类型') spec_model = db.Column(db.String(255), comment='规格型号') @@ -46,6 +47,7 @@ class MaterialBase(db.Model): return { 'id': self.id, 'name': self.name, + 'commonName': self.common_name, # ✅ 序列化新增字段 'category': self.category, 'type': self.material_type, # 前端字段映射 'spec': self.spec_model, # 前端字段映射 diff --git a/inventory-backend/app/models/transaction.py b/inventory-backend/app/models/transaction.py index 3938106..871fe59 100644 --- a/inventory-backend/app/models/transaction.py +++ b/inventory-backend/app/models/transaction.py @@ -1,66 +1,96 @@ from app.extensions import db from datetime import datetime + # 1. 借用表 class TransBorrow(db.Model): __tablename__ = 'trans_borrow' + id = db.Column(db.Integer, primary_key=True) - sku = db.Column(db.String(100)) + sku = db.Column(db.String(100), index=True) # 加索引优化查询 source_table = db.Column(db.String(50)) stock_id = db.Column(db.Integer) quantity = db.Column(db.Numeric(19, 4)) + borrow_time = db.Column(db.DateTime, default=datetime.now) expected_return_time = db.Column(db.DateTime) + borrower_name = db.Column(db.String(100)) actual_return_time = db.Column(db.DateTime) approver_name = db.Column(db.String(100)) - status = db.Column(db.String(20)) + + # 状态:borrowed(借出), returned(已还), overdue(逾期) + status = db.Column(db.String(20), default='borrowed') def to_dict(self): return { 'id': self.id, 'sku': self.sku, + 'quantity': float(self.quantity) if self.quantity else 0, 'borrower_name': self.borrower_name, + 'borrow_time': self.borrow_time.strftime('%Y-%m-%d %H:%M:%S') if self.borrow_time else None, 'status': self.status } + # 2. 维修表 class TransRepair(db.Model): __tablename__ = 'trans_repair' + id = db.Column(db.Integer, primary_key=True) - sku = db.Column(db.String(100)) + sku = db.Column(db.String(100), index=True) source_table = db.Column(db.String(50)) stock_id = db.Column(db.Integer) + arrival_date = db.Column(db.Date) expected_repair_time = db.Column(db.String(100)) shipping_date = db.Column(db.Date) + is_self_made = db.Column(db.Boolean, default=False) related_product_id = db.Column(db.Integer) related_contract_id = db.Column(db.String(100)) + repair_manager = db.Column(db.String(100)) fault_description = db.Column(db.Text) repair_result = db.Column(db.Text) + cost_price = db.Column(db.Numeric(19, 4)) sale_price = db.Column(db.Numeric(19, 4)) def to_dict(self): - return {'id': self.id, 'sku': self.sku, 'status': 'repaired' if self.repair_result else 'pending'} + return { + 'id': self.id, + 'sku': self.sku, + 'status': 'repaired' if self.repair_result else 'pending', + 'manager': self.repair_manager + } + # 3. 报废表 class TransScrap(db.Model): __tablename__ = 'trans_scrap' + id = db.Column(db.Integer, primary_key=True) - sku = db.Column(db.String(100)) + sku = db.Column(db.String(100), index=True) source_table = db.Column(db.String(50)) stock_id = db.Column(db.Integer) quantity = db.Column(db.Numeric(19, 4)) + reason = db.Column(db.Text) operator_name = db.Column(db.String(100)) operation_time = db.Column(db.DateTime, default=datetime.now) + approver_name = db.Column(db.String(100)) - approval_status = db.Column(db.String(20)) + approval_status = db.Column(db.String(20), default='pending') # pending, approved, rejected + cost_at_scrap = db.Column(db.Numeric(19, 4)) total_loss = db.Column(db.Numeric(19, 4)) def to_dict(self): - return {'id': self.id, 'sku': self.sku, 'total_loss': float(self.total_loss) if self.total_loss else 0} \ No newline at end of file + return { + 'id': self.id, + 'sku': self.sku, + 'quantity': float(self.quantity) if self.quantity else 0, + 'total_loss': float(self.total_loss) if self.total_loss else 0, + 'reason': self.reason + } \ No newline at end of file diff --git a/inventory-backend/app/services/inbound/base_service.py b/inventory-backend/app/services/inbound/base_service.py index 7b1ce02..f26dcc2 100644 --- a/inventory-backend/app/services/inbound/base_service.py +++ b/inventory-backend/app/services/inbound/base_service.py @@ -2,12 +2,8 @@ from app.extensions import db from app.models.base import MaterialBase - -# ============================================================================== -# ✅ 正确的引用方式 -# ============================================================================== -from app.models.inbound.buy import StockBuy # 引用采购库存模型 -from app.models.inbound.semi import StockSemi # 引用半成品库存模型 +from app.models.inbound.buy import StockBuy +from app.models.inbound.semi import StockSemi from sqlalchemy import or_ import traceback @@ -28,11 +24,12 @@ class MaterialBaseService: if not keyword: return [] - # 搜索名称或规格型号,且必须是启用的 + # ✅ 搜索范围增加 common_name (俗名) query = MaterialBase.query.filter( MaterialBase.is_enabled == True, or_( MaterialBase.name.ilike(f'%{keyword}%'), + MaterialBase.common_name.ilike(f'%{keyword}%'), MaterialBase.spec_model.ilike(f'%{keyword}%') ) ).limit(20) @@ -42,6 +39,7 @@ class MaterialBaseService: results.append({ 'id': item.id, 'name': item.name, + 'commonName': item.common_name, # ✅ 返回俗名 'spec': item.spec_model, 'category': item.category, 'unit': item.unit, @@ -62,12 +60,14 @@ class MaterialBaseService: query = MaterialBase.query if filters: - # 1. 关键词模糊搜索 (名称 或 规格型号) + # 1. 关键词模糊搜索 (名称 或 俗名 或 规格型号) if filters.get('keyword'): kw = f"%{filters['keyword']}%" + # ✅ 增加俗名搜索 query = query.filter(or_( - MaterialBase.name.like(kw), - MaterialBase.spec_model.like(kw) + MaterialBase.name.ilike(kw), + MaterialBase.common_name.ilike(kw), + MaterialBase.spec_model.ilike(kw) )) # 2. 精确筛选 @@ -101,6 +101,7 @@ class MaterialBaseService: raise ValueError("名称和规格型号不能为空") # 1. 查重 (名称+规格型号 唯一) + # 注意:俗名不参与唯一性校验,允许重复或为空 exist = MaterialBase.query.filter_by( name=data['name'], spec_model=data['spec'] @@ -111,6 +112,7 @@ class MaterialBaseService: # 2. 创建对象 new_material = MaterialBase( name=data['name'], + common_name=data.get('commonName'), # ✅ 读取俗名 spec_model=data['spec'], category=data.get('category'), material_type=data.get('type'), @@ -139,6 +141,7 @@ class MaterialBaseService: # 更新字段 if 'name' in data: material.name = data['name'] + if 'commonName' in data: material.common_name = data['commonName'] # ✅ 更新俗名 if 'spec' in data: material.spec_model = data['spec'] if 'category' in data: material.category = data['category'] if 'type' in data: material.material_type = data['type'] @@ -161,7 +164,6 @@ class MaterialBaseService: def delete_material(m_id): """ 删除基础信息 (带依赖检查) - ✅ 已升级:同时检查采购库(Buy)和半成品库(Semi) """ try: material = MaterialBase.query.get(m_id) diff --git a/inventory-backend/run.py b/inventory-backend/run.py index 9fda37f..97a8841 100644 --- a/inventory-backend/run.py +++ b/inventory-backend/run.py @@ -1,14 +1,27 @@ # inventory-backend/run.py from app import create_app -# 【关键】这一行必须在最外层,不能放在 if __name__ ... 里面! -# Gunicorn 会导入这个变量 +# Gunicorn 或 uWSGI 会寻找名为 'app' 的实例 app = create_app() if __name__ == '__main__': - # 这里是开发调试用的,Docker/Gunicorn 不会执行这里 - print("\n====== 当前所有注册路由 ======") - for rule in app.url_map.iter_rules(): - print(f"{rule} -> {rule.endpoint}") + # ================================================= + # 路由打印调试 (启动时会在控制台列出所有 URL) + # 这一步能帮你确认 /api/inbound/base/list 是否存在 + # ================================================= + print("\n====== 当前生效的路由映射 ======") + try: + # 按 URL 排序打印,方便查找 + sorted_rules = sorted(app.url_map.iter_rules(), key=lambda x: str(x)) + for rule in sorted_rules: + # 过滤掉一些系统自带的 static 路由,只显示 API + if 'api' in str(rule): + methods = ','.join(rule.methods - {'OPTIONS', 'HEAD'}) + print(f"{str(rule):<50} | {methods:<10} | {rule.endpoint}") + except Exception: + pass print("==============================\n") + + # 启动开发服务器 + # 端口设置为 5000 (Flask 默认) 或 8000,请确保与前端 Vite 代理一致 app.run(host='0.0.0.0', port=8000, debug=True) \ No newline at end of file diff --git a/inventory-web/src/App.vue b/inventory-web/src/App.vue index da7eeb1..99eacf6 100644 --- a/inventory-web/src/App.vue +++ b/inventory-web/src/App.vue @@ -1,6 +1,43 @@ \ No newline at end of file diff --git a/inventory-web/src/router/index.ts b/inventory-web/src/router/index.ts index b654f1e..38e986b 100644 --- a/inventory-web/src/router/index.ts +++ b/inventory-web/src/router/index.ts @@ -1,19 +1,18 @@ import { createRouter, createWebHistory } from 'vue-router' -// 使用 'type' 关键字导入 RouteRecordRaw import type { RouteRecordRaw } from 'vue-router' import Layout from '@/layout/index.vue' -import { useUserStore } from '@/stores/user' // [新增] 引入 Store 用于权限判断 +import { useUserStore } from '@/stores/user' const routes: Array = [ - // [新增] 登录页 (不需要 Layout) + // 1. 登录页 { path: '/login', name: 'Login', component: () => import('@/views/login/index.vue'), - meta: { hidden: true } // 不在侧边栏显示 + meta: { hidden: true } }, - // 1. 首页 Dashboard + // 2. 首页 Dashboard { path: '/', component: Layout, @@ -28,7 +27,7 @@ const routes: Array = [ ] }, - // 2. 基础信息 (对应 views/material/list.vue) + // 3. 基础信息 { path: '/material', component: Layout, @@ -37,14 +36,13 @@ const routes: Array = [ { path: 'index', name: 'MaterialBase', - // 基础信息列表 component: () => import('@/views/material/list.vue'), meta: { title: '基础信息', icon: 'Box' } } ] }, - // 3. 库存管理 (采购/半成品/成品/权益) + // 4. 库存管理 { path: '/inventory', component: Layout, @@ -54,35 +52,31 @@ const routes: Array = [ { path: 'buy', name: 'InventoryBuy', - // 采购入库页面 component: () => import('@/views/stock/inbound/buy.vue'), meta: { title: '采购件' } }, { path: 'semi', name: 'InventorySemi', - // 半成品页面 component: () => import('@/views/stock/inbound/semi.vue'), meta: { title: '半成品' } }, { path: 'product', name: 'InventoryProduct', - // 成品页面 component: () => import('@/views/stock/inbound/product.vue'), meta: { title: '成品' } }, { path: 'service', name: 'InventoryService', - // 服务权益页面 component: () => import('@/views/stock/inbound/service.vue'), meta: { title: '服务权益' } } ] }, - // 4. 业务操作 (借库/维修/报废) + // 5. 业务操作 { path: '/operation', component: Layout, @@ -92,70 +86,43 @@ const routes: Array = [ { path: 'borrow', name: 'OpBorrow', - // 借库页面 component: () => import('@/views/transaction/borrow.vue'), meta: { title: '借库' } }, { path: 'repair', name: 'OpRepair', - // 维修页面 (指向 return.vue) component: () => import('@/views/transaction/return.vue'), meta: { title: '维修' } }, { path: 'scrap', name: 'OpScrap', - // 报废页面 component: () => import('@/views/transaction/scrap.vue'), meta: { title: '报废' } } ] }, - // 5. [修改] 系统管理 (权限控制 + 用户创建) + // 6. 系统管理 { path: '/system', component: Layout, meta: { title: '系统管理', icon: 'Setting', - // 只有超级管理员和主管能看到此菜单 roles: ['super_admin', 'supervisor'] }, children: [ { path: 'user-create', name: 'UserCreate', - // 指向我们之前创建的新增用户页面 component: () => import('@/views/system/UserCreate.vue'), meta: { title: '账号开通', icon: 'User' } - }, - // 原有的日志页面保留 (如果文件存在) - // { - // path: 'log', - // name: 'OpLog', - // component: () => import('@/views/system/log.vue'), - // meta: { title: '操作日志', icon: 'Document' } - // } + } ] }, - /* * 暂时屏蔽 BOM - */ - // { - // path: '/bom', - // component: Layout, - // children: [ - // { - // path: 'index', - // name: 'BOM', - // component: () => import('@/views/bom/index.vue'), - // meta: { title: 'BOM管理', icon: 'List' } - // } - // ] - // }, - // 404 路由 { path: '/:pathMatch(.*)*', @@ -170,37 +137,45 @@ const router = createRouter({ }) // ========================================== -// [新增] 全局路由守卫:处理登录拦截与权限验证 +// [关键修改] 全局路由守卫 // ========================================== router.beforeEach((to, from, next) => { const userStore = useUserStore() - const token = userStore.token - const userRole = userStore.role - // 1. 白名单:如果是去登录页,直接放行 + // 1. 实时获取 Token (优先取 store,防止 store 未初始化取 localStorage) + const token = userStore.token || localStorage.getItem('token') + const userRole = userStore.role || localStorage.getItem('role') || 'user' + + // 2. 如果要去的是登录页 if (to.path === '/login') { - next() + // 如果有 Token,说明已登录,踢回首页 (防止重复登录) + if (token) { + next('/') + } else { + // 没有 Token,允许访问登录页 + next() + } return } - // 2. 无 Token:强制跳转登录页 + // 3. 如果去的不是登录页,但没有 Token if (!token) { - next('/login') + // 强制重定向到登录页 + // 使用 replace 防止用户点击浏览器“返回”按钮时进入死循环 + next({ path: '/login', replace: true }) return } - // 3. 权限判断:检查 meta.roles + // 4. 权限判断 (已有 Token) if (to.meta.roles && Array.isArray(to.meta.roles)) { - // 如果当前用户角色在允许列表中,放行 if (to.meta.roles.includes(userRole)) { next() } else { - // 权限不足,重定向到首页或 403 页面 (这里简单跳回 dashboard) - // 可以在这里触发一个 Element Plus 的 Message 提示 + // 权限不足,跳回首页 next('/dashboard') } } else { - // 没有定义权限要求的页面,默认放行 + // 无特殊权限要求,放行 next() } }) diff --git a/inventory-web/src/stores/user.ts b/inventory-web/src/stores/user.ts index 03fe9c7..c088754 100644 --- a/inventory-web/src/stores/user.ts +++ b/inventory-web/src/stores/user.ts @@ -3,44 +3,85 @@ import { login } from '@/api/auth' import { ref } from 'vue' export const useUserStore = defineStore('user', () => { + // 1. State: 初始化时优先从 localStorage 获取,防止刷新丢失 const token = ref(localStorage.getItem('token') || '') - const role = ref(localStorage.getItem('role') || '') // 持久化角色 + const role = ref(localStorage.getItem('role') || '') const username = ref(localStorage.getItem('username') || '') + // 2. Actions + // 登录逻辑 const handleLogin = async (loginForm: any) => { try { const res = await login(loginForm) - // res.data 结构: { access_token, user: { role, username, ... } } - const data = res.data + // [调试日志] 查看实际返回的数据结构 (调试完成后可删除) + console.log('Login API Response:', res) + + // ============================================================ + // [关键修复] 兼容 Axios 拦截器的不同处理方式 + // 如果拦截器已经返回了 response.data,那么 res 本身就是数据对象 + // 如果拦截器返回的是原始 response,那么数据在 res.data 中 + // ============================================================ + const data = res.data || res + + // 安全检查:确保 data 存在且包含 access_token + if (!data || !data.access_token) { + console.error('Login Error: 响应数据中缺少 access_token', data) + return false + } + + // 更新 Pinia 状态 (内存) token.value = data.access_token - role.value = data.user.role - username.value = data.user.username - // 持久化存储 (简单处理,生产环境建议加密或仅存Token) + // 处理用户信息 (确保后端返回结构中有 user 字段) + if (data.user) { + role.value = data.user.role || 'user' // 默认给个 user 角色防止空 + username.value = data.user.username || '用户' + + // 持久化存储用户信息 + localStorage.setItem('role', role.value) + localStorage.setItem('username', username.value) + } + + // 持久化存储 Token localStorage.setItem('token', data.access_token) - localStorage.setItem('role', data.user.role) - localStorage.setItem('username', data.user.username) return true } catch (error) { - console.error(error) + console.error('Login failed:', error) return false } } + // 退出逻辑 const logout = () => { + // 1. 清空 Pinia 状态 (内存) token.value = '' role.value = '' username.value = '' - localStorage.clear() - window.location.reload() + + // 2. 清空 LocalStorage (硬盘) + // 建议使用 removeItem 而不是 clear,避免误删该域名下其他非登录数据 + localStorage.removeItem('token') + localStorage.removeItem('role') + localStorage.removeItem('username') + + // 注意:这里不再执行 window.location.reload() + // 而是把跳转控制权交给调用者 (如 App.vue 中的 router.push) } - // 辅助函数:判断当前用户是否拥有某些角色 + // 3. Getters / Helpers + // 判断当前用户是否拥有某些角色 const hasRole = (roles: string[]) => { return roles.includes(role.value) } - return { token, role, username, handleLogin, logout, hasRole } + return { + token, + role, + username, + handleLogin, + logout, + hasRole + } }) \ No newline at end of file diff --git a/inventory-web/src/style.css b/inventory-web/src/style.css index f691315..15a411f 100644 --- a/inventory-web/src/style.css +++ b/inventory-web/src/style.css @@ -1,18 +1,31 @@ +/* inventory-web/src/style.css */ + +/* 1. 保留原有的字体定义,确保文字清晰好看 */ :root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; + /* 颜色方案配置 */ color-scheme: light dark; color: rgba(255, 255, 255, 0.87); - background-color: #242424; + /* 字体渲染优化 */ font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } +/* 2. 针对亮色模式的颜色适配 (保留) */ +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } +} + +/* 3. 链接的基本样式 (保留,但通常 RouterLink 会覆盖) */ a { font-weight: 500; color: #646cff; @@ -22,58 +35,44 @@ a:hover { color: #535bf2; } -body { +/* ------------------------------------------------- + 【重要修改区域】 + 下面的代码是为了修复“无法铺满全屏”的问题 + ------------------------------------------------- +*/ + +/* 4. 全局盒模型修复:防止 padding 撑大元素 */ +*, *::before, *::after { + box-sizing: border-box; +} + +/* 5. 重置 body 和 html */ +html, body { margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -.card { - padding: 2em; + padding: 0; + width: 100%; + height: 100%; /* 强制高度占满 */ + + /* !!! 删除了原有的 display: flex; place-items: center; + 这是导致你页面缩在中间的罪魁祸首 + */ + display: block; + + overflow: hidden; /* 防止最外层出现双滚动条 */ } +/* 6. 重置 #app 挂载点 */ #app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; + /* !!! 删除了 max-width: 1280px; padding: 2rem; text-align: center; + 这是导致你页面两边留白、无法全屏的原因 + */ + width: 100%; + height: 100%; + margin: 0; + padding: 0; } -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} +/* 注意:原文件中关于 button, .card 的样式已被删除, + 因为你的项目中引入了 Element Plus, + 保留原生 button 样式会和 Element Plus 组件产生冲突。 +*/ \ No newline at end of file diff --git a/inventory-web/src/views/material/list.vue b/inventory-web/src/views/material/list.vue index 22242c0..b206b90 100644 --- a/inventory-web/src/views/material/list.vue +++ b/inventory-web/src/views/material/list.vue @@ -5,7 +5,7 @@
+ @@ -103,7 +104,15 @@ style="width: 100%; margin-top: 15px" > - + + + + + + @@ -162,9 +171,18 @@ > - - - + + + + + + + + + + + + @@ -254,6 +272,7 @@ import { interface MaterialBaseVO { id: number; name: string; + commonName?: string; // ✅ 新增类型定义 category: string; type: string; spec: string; @@ -284,6 +303,7 @@ const tableSize = ref<'large' | 'default' | 'small'>('large'); const columns = reactive({ id: { visible: true }, name: { visible: true }, + commonName: { visible: true }, // ✅ 新增列控制 category: { visible: true }, type: { visible: true }, spec: { visible: true }, @@ -316,6 +336,7 @@ const formRef = ref(); const initForm = { id: undefined, name: '', + commonName: '', // ✅ 初始化新增字段 category: '', type: '', spec: '', @@ -350,13 +371,10 @@ const extractDynamicOptions = (items: MaterialBaseVO[]) => { typeOptions.value = Array.from(newTypes); }; -// 【核心新增】Autocomplete 的建议查询方法 -// 格式化数据以适配 el-autocomplete 的回调参数格式 [{ value: 'abc' }] const querySearchCategory = (queryString: string, cb: any) => { const results = queryString ? categoryOptions.value.filter(item => item.toLowerCase().includes(queryString.toLowerCase())) : categoryOptions.value; - // el-autocomplete 默认只展示 value 属性 const formattedResults = results.map(item => ({ value: item })); cb(formattedResults); };