diff --git a/inventory-backend/app/api/v1/common/upload.py b/inventory-backend/app/api/v1/common/upload.py index 5fc6184..ed8ef06 100644 --- a/inventory-backend/app/api/v1/common/upload.py +++ b/inventory-backend/app/api/v1/common/upload.py @@ -5,21 +5,18 @@ import uuid from flask import Blueprint, request, jsonify, send_from_directory # 定义蓝图 +# 注意:在 app/__init__.py 或类似入口文件中,注册此蓝图时 url_prefix 通常应为 '/api/v1/common' upload_bp = Blueprint('upload', __name__) # ========================================================= -# 配置上传路径 (核心修改:确保路径绝对准确) +# 配置上传路径 # ========================================================= -# 向上寻找直到找到 inventory-backend 目录,或者默认为当前文件的上级目录的...上级 -# 这种方式比数 dirname 层级更稳健 - def get_project_root(): """获取项目根目录 inventory-backend""" current_path = os.path.abspath(__file__) - # 循环向上查找,直到找到名为 inventory-backend 的目录 - # 如果你的根目录名字不是 inventory-backend,请修改这里的判断逻辑 - # 或者直接使用相对路径回退 5 层: api/v1/common -> app -> inventory-backend + # 向上回退直到找到根目录,根据你的目录结构可能需要调整层级 + # 假设结构: inventory-backend/app/api/v1/common/upload.py (回退5层) base = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(current_path))))) return base @@ -28,7 +25,7 @@ BASE_DIR = get_project_root() UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads') # 允许上传的文件后缀 -ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'} +ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'pdf', 'doc', 'docx', 'xls', 'xlsx'} def allowed_file(filename): @@ -47,7 +44,7 @@ def ensure_upload_folder_exists(): # ------------------------------------------------------------------ # 1. 文件上传接口 -# URL: /api/v1/common/upload (POST) +# 完整 URL: /api/v1/common/upload (POST) # ------------------------------------------------------------------ @upload_bp.route('/upload', methods=['POST']) def upload_file(): @@ -63,6 +60,7 @@ def upload_file(): if file and allowed_file(file.filename): try: + # 获取后缀并生成唯一文件名 ext = file.filename.rsplit('.', 1)[1].lower() new_filename = f"{uuid.uuid4().hex}.{ext}" @@ -71,8 +69,9 @@ def upload_file(): print(f"💾 [Upload] 文件已保存: {save_path}") - # 生成访问 URL - # 这里的路径必须与 __init__.py 中注册的 url_prefix + 路由匹配 + # 生成访问 URL (返回给前端的相对路径) + # 前端展示时通常拼接 baseURL,或者直接使用此路径访问 + # 这里的 /api/v1/common 需与蓝图注册路径一致 file_url = f"/api/v1/common/files/{new_filename}" return jsonify({ @@ -92,13 +91,13 @@ def upload_file(): # ------------------------------------------------------------------ # 2. 静态文件访问接口 (回显) -# URL: /api/v1/common/files/ +# 完整 URL: /api/v1/common/files/ (GET) # ------------------------------------------------------------------ -@upload_bp.route('/files/') +@upload_bp.route('/files/', methods=['GET']) def uploaded_file(filename): - # 打印日志帮助调试 404 问题 full_path = os.path.join(UPLOAD_FOLDER, filename) if not os.path.exists(full_path): + # 尝试调试路径问题 print(f"❌ [File Access] 文件未找到: {full_path}") return jsonify({"code": 404, "msg": "文件不存在"}), 404 @@ -107,12 +106,12 @@ def uploaded_file(filename): # ------------------------------------------------------------------ # 3. 文件删除接口 (同步删除物理文件) -# URL: /api/v1/common/files/ (DELETE) +# 完整 URL: /api/v1/common/files/ (DELETE) # ------------------------------------------------------------------ @upload_bp.route('/files/', methods=['DELETE']) def delete_file(filename): try: - # 安全处理文件名 + # 安全处理文件名 (防止路径遍历) safe_filename = os.path.basename(filename) file_path = os.path.join(UPLOAD_FOLDER, safe_filename) @@ -124,7 +123,7 @@ def delete_file(filename): return jsonify({"code": 200, "msg": "文件已删除"}) else: print(f"⚠️ [Delete] 文件不存在,无需删除") - # 即使文件不存在也返回成功,保证前端流程继续 + # 即使文件不存在也返回成功,保证前端逻辑闭环 return jsonify({"code": 200, "msg": "文件不存在或已删除"}) except Exception as e: diff --git a/inventory-backend/app/models/base.py b/inventory-backend/app/models/base.py index b6cffcc..ab88f46 100644 --- a/inventory-backend/app/models/base.py +++ b/inventory-backend/app/models/base.py @@ -1,5 +1,6 @@ # app/models/base.py from app.extensions import db +import json class MaterialBase(db.Model): """ @@ -11,7 +12,7 @@ class MaterialBase(db.Model): # 1. 基础字段 id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(255), nullable=False, comment='名称') - common_name = db.Column(db.String(255), 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='规格型号') @@ -20,7 +21,7 @@ class MaterialBase(db.Model): # 可见等级 visibility_level = db.Column(db.Integer, default=0, comment='信息可见等级') - # 链接与图片 + # 链接与图片 (现在存储 JSON 字符串) manual_link = db.Column(db.Text, comment='通用说明书') product_image = db.Column(db.Text, comment='通用产品图') @@ -44,16 +45,29 @@ class MaterialBase(db.Model): """ 序列化方法 """ + # 辅助解析函数:将数据库存储的 JSON 字符串转为 List + def parse_list(json_str): + if not json_str: + return [] + try: + # 兼容旧数据:如果不是 JSON 格式(比如是单个 URL),则包装成 list + if not json_str.startswith('['): + return [json_str] + return json.loads(json_str) + except: + return [] + return { 'id': self.id, 'name': self.name, - 'commonName': self.common_name, # ✅ 序列化新增字段 + 'commonName': self.common_name, 'category': self.category, - 'type': self.material_type, # 前端字段映射 - 'spec': self.spec_model, # 前端字段映射 + 'type': self.material_type, + 'spec': self.spec_model, 'unit': self.unit, 'visibilityLevel': self.visibility_level, - 'generalManual': self.manual_link, - 'generalImage': self.product_image, + # 修改:解析为列表返回 + 'generalManual': parse_list(self.manual_link), + 'generalImage': parse_list(self.product_image), 'isEnabled': 1 if self.is_enabled else 0, } \ 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 f26dcc2..bed12f2 100644 --- a/inventory-backend/app/services/inbound/base_service.py +++ b/inventory-backend/app/services/inbound/base_service.py @@ -6,6 +6,7 @@ from app.models.inbound.buy import StockBuy from app.models.inbound.semi import StockSemi from sqlalchemy import or_ import traceback +import json class MaterialBaseService: @@ -24,7 +25,6 @@ class MaterialBaseService: if not keyword: return [] - # ✅ 搜索范围增加 common_name (俗名) query = MaterialBase.query.filter( MaterialBase.is_enabled == True, or_( @@ -39,7 +39,7 @@ class MaterialBaseService: results.append({ 'id': item.id, 'name': item.name, - 'commonName': item.common_name, # ✅ 返回俗名 + 'commonName': item.common_name, 'spec': item.spec_model, 'category': item.category, 'unit': item.unit, @@ -63,7 +63,6 @@ class MaterialBaseService: # 1. 关键词模糊搜索 (名称 或 俗名 或 规格型号) if filters.get('keyword'): kw = f"%{filters['keyword']}%" - # ✅ 增加俗名搜索 query = query.filter(or_( MaterialBase.name.ilike(kw), MaterialBase.common_name.ilike(kw), @@ -100,8 +99,7 @@ class MaterialBaseService: if not data.get('name') or not data.get('spec'): raise ValueError("名称和规格型号不能为空") - # 1. 查重 (名称+规格型号 唯一) - # 注意:俗名不参与唯一性校验,允许重复或为空 + # 1. 查重 exist = MaterialBase.query.filter_by( name=data['name'], spec_model=data['spec'] @@ -109,17 +107,18 @@ class MaterialBaseService: if exist: raise ValueError(f"已存在相同名称和规格的数据 (ID: {exist.id})") - # 2. 创建对象 + # 2. 创建对象 (列表转JSON字符串) new_material = MaterialBase( name=data['name'], - common_name=data.get('commonName'), # ✅ 读取俗名 + common_name=data.get('commonName'), spec_model=data['spec'], category=data.get('category'), material_type=data.get('type'), unit=data.get('unit'), visibility_level=data.get('visibilityLevel'), - manual_link=data.get('generalManual'), - product_image=data.get('generalImage'), + # 修改:将列表 dumps 为字符串 + manual_link=json.dumps(data.get('generalManual', [])), + product_image=json.dumps(data.get('generalImage', [])), is_enabled=True if data.get('isEnabled', 1) == 1 else False ) @@ -141,14 +140,18 @@ class MaterialBaseService: # 更新字段 if 'name' in data: material.name = data['name'] - if 'commonName' in data: material.common_name = data['commonName'] # ✅ 更新俗名 + 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'] if 'unit' in data: material.unit = data['unit'] if 'visibilityLevel' in data: material.visibility_level = data['visibilityLevel'] - if 'generalManual' in data: material.manual_link = data['generalManual'] - if 'generalImage' in data: material.product_image = data['generalImage'] + + # 修改:将列表 dumps 为字符串 + if 'generalManual' in data: + material.manual_link = json.dumps(data['generalManual']) + if 'generalImage' in data: + material.product_image = json.dumps(data['generalImage']) if 'isEnabled' in data: material.is_enabled = bool(int(data['isEnabled'])) @@ -170,12 +173,8 @@ class MaterialBaseService: if not material: raise ValueError("数据不存在") - # 1. 依赖检查:采购入库引用 buy_usage_count = StockBuy.query.filter_by(base_id=m_id).count() - - # 2. 依赖检查:半成品入库引用 semi_usage_count = StockSemi.query.filter_by(base_id=m_id).count() - total_usage = buy_usage_count + semi_usage_count if total_usage > 0: @@ -186,7 +185,6 @@ class MaterialBaseService: f"请先清理相关库存或仅‘禁用’此条目。" ) - # 3. 执行删除 db.session.delete(material) db.session.commit() return True diff --git a/inventory-web/src/api/common/upload.ts b/inventory-web/src/api/common/upload.ts index f24d158..51f5448 100644 --- a/inventory-web/src/api/common/upload.ts +++ b/inventory-web/src/api/common/upload.ts @@ -2,14 +2,23 @@ import request from '@/utils/request' /** * 上传文件通用接口 - * @param file File 对象 + * @param data File 对象 或 FormData 对象 + * 适配说明:list.vue 中 customUpload 已经封装了 FormData,所以这里支持直接传 FormData */ -export function uploadFile(file: File) { - const formData = new FormData() - formData.append('file', file) +export function uploadFile(data: File | FormData) { + let formData: FormData + + if (data instanceof FormData) { + formData = data + } else { + // 如果传入的是原始 File 对象,则手动封装 + formData = new FormData() + // @ts-ignore + formData.append('file', data) + } return request({ - // ★★★ [修改] 去掉开头的 /api,适配 request.ts 的 baseURL + // 注意:这里 /v1/common/upload 需要与后端 BluePrint 注册的 url_prefix 对应 url: '/v1/common/upload', method: 'post', data: formData, @@ -17,4 +26,16 @@ export function uploadFile(file: File) { 'Content-Type': 'multipart/form-data' } }) +} + +/** + * 删除文件通用接口 (新增) + * @param filename 文件名 (例如: a1b2c3d4.jpg) + */ +export function deleteFile(filename: string) { + return request({ + // 对应后端路由: @upload_bp.route('/files/', methods=['DELETE']) + url: `/v1/common/files/${filename}`, + method: 'delete' + }) } \ 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 bc30561..1ac4173 100644 --- a/inventory-web/src/views/material/list.vue +++ b/inventory-web/src/views/material/list.vue @@ -124,12 +124,43 @@ - -