库存资产excel文件导出
This commit is contained in:
@ -1,7 +1,9 @@
|
|||||||
# 文件路径: app/api/v1/inbound/base.py
|
# 文件路径: app/api/v1/inbound/base.py
|
||||||
from flask import Blueprint, request, jsonify
|
|
||||||
|
from flask import Blueprint, request, jsonify, send_file
|
||||||
from app.services.inbound.base_service import MaterialBaseService
|
from app.services.inbound.base_service import MaterialBaseService
|
||||||
import traceback
|
import traceback
|
||||||
|
import datetime
|
||||||
|
|
||||||
inbound_base_bp = Blueprint('stock_base', __name__)
|
inbound_base_bp = Blueprint('stock_base', __name__)
|
||||||
|
|
||||||
@ -26,12 +28,13 @@ def search_base():
|
|||||||
@inbound_base_bp.route('/list', methods=['GET'])
|
@inbound_base_bp.route('/list', methods=['GET'])
|
||||||
def get_list():
|
def get_list():
|
||||||
try:
|
try:
|
||||||
page = request.args.get('pageNum', 1, type=int) # 前端传的是 pageNum
|
page = request.args.get('pageNum', 1, type=int)
|
||||||
limit = request.args.get('pageSize', 10, type=int)
|
limit = request.args.get('pageSize', 10, type=int)
|
||||||
|
|
||||||
# 构造筛选条件
|
# 构造筛选条件
|
||||||
filters = {
|
filters = {
|
||||||
'keyword': request.args.get('keyword', ''),
|
'keyword': request.args.get('keyword', ''),
|
||||||
|
'company': request.args.get('company', ''),
|
||||||
'category': request.args.get('category', ''),
|
'category': request.args.get('category', ''),
|
||||||
'type': request.args.get('type', ''),
|
'type': request.args.get('type', ''),
|
||||||
'isEnabled': request.args.get('isEnabled', None)
|
'isEnabled': request.args.get('isEnabled', None)
|
||||||
@ -45,7 +48,7 @@ def get_list():
|
|||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 2.1 选项接口 (GET /api/v1/inbound/base/options) [新增]
|
# 2.1 选项接口 (GET /api/v1/inbound/base/options)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@inbound_base_bp.route('/options', methods=['GET'])
|
@inbound_base_bp.route('/options', methods=['GET'])
|
||||||
def get_options():
|
def get_options():
|
||||||
@ -57,9 +60,45 @@ def get_options():
|
|||||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# 2.2 导出接口 (GET /api/v1/inbound/base/export)
|
||||||
|
# ==============================================================================
|
||||||
|
@inbound_base_bp.route('/export', methods=['GET'])
|
||||||
|
def export_data():
|
||||||
|
try:
|
||||||
|
# 获取筛选条件
|
||||||
|
filters = {
|
||||||
|
'keyword': request.args.get('keyword', ''),
|
||||||
|
'company': request.args.get('company', ''),
|
||||||
|
'category': request.args.get('category', ''),
|
||||||
|
'type': request.args.get('type', ''),
|
||||||
|
'isEnabled': request.args.get('isEnabled', None)
|
||||||
|
}
|
||||||
|
|
||||||
|
# 生成 Excel 文件流
|
||||||
|
file_stream = MaterialBaseService.export_excel(filters)
|
||||||
|
|
||||||
|
# 生成文件名:库存统计+年月日+时分秒 (北京时间 UTC+8)
|
||||||
|
# 简单处理:UTC时间 + 8小时
|
||||||
|
beijing_time = datetime.datetime.utcnow() + datetime.timedelta(hours=8)
|
||||||
|
filename = f"库存统计_{beijing_time.strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||||
|
|
||||||
|
# 发送文件
|
||||||
|
# 注意:download_name 仅在较新 Flask 版本有效,旧版本可能需要手动 header,
|
||||||
|
# 但通常浏览器下载名由前端 Blob 处理或 Content-Disposition 决定。
|
||||||
|
return send_file(
|
||||||
|
file_stream,
|
||||||
|
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=filename
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
return jsonify({"code": 500, "msg": f"导出失败: {str(e)}"}), 500
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 3. 新增接口 (POST /api/v1/inbound/base/)
|
# 3. 新增接口 (POST /api/v1/inbound/base/)
|
||||||
# 注意:前端 material_base.ts 可能会请求 / 或 /add,这里统一匹配
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@inbound_base_bp.route('/', methods=['POST'])
|
@inbound_base_bp.route('/', methods=['POST'])
|
||||||
def create():
|
def create():
|
||||||
|
|||||||
@ -4,11 +4,16 @@ from app.extensions import db
|
|||||||
from app.models.base import MaterialBase
|
from app.models.base import MaterialBase
|
||||||
from app.models.inbound.buy import StockBuy
|
from app.models.inbound.buy import StockBuy
|
||||||
from app.models.inbound.semi import StockSemi
|
from app.models.inbound.semi import StockSemi
|
||||||
# from app.models.inbound.product import StockProduct
|
from app.models.inbound.product import StockProduct
|
||||||
# from app.models.inbound.service import StockService
|
# from app.models.inbound.service import StockService
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_, and_
|
||||||
import traceback
|
import traceback
|
||||||
import json
|
import json
|
||||||
|
import io
|
||||||
|
import datetime
|
||||||
|
# 需要 pip install openpyxl
|
||||||
|
from openpyxl import Workbook
|
||||||
|
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
|
||||||
|
|
||||||
|
|
||||||
class MaterialBaseService:
|
class MaterialBaseService:
|
||||||
@ -91,7 +96,7 @@ class MaterialBaseService:
|
|||||||
|
|
||||||
for x in items:
|
for x in items:
|
||||||
# 1. 获取库存数 (兼容不同字段名)
|
# 1. 获取库存数 (兼容不同字段名)
|
||||||
q = getattr(x, 'stock_quantity', getattr(x, 'actual_quantity', getattr(x, 'quantity', 0)))
|
q = getattr(x, 'stock_quantity', getattr(x, 'in_quantity', 0)) # 优先取库存,其次入库
|
||||||
# 2. 获取可用数
|
# 2. 获取可用数
|
||||||
a = getattr(x, 'available_quantity', q)
|
a = getattr(x, 'available_quantity', q)
|
||||||
|
|
||||||
@ -137,7 +142,6 @@ class MaterialBaseService:
|
|||||||
query = query.filter_by(is_enabled=is_active)
|
query = query.filter_by(is_enabled=is_active)
|
||||||
|
|
||||||
# [修改3] 默认排序方式改为按 spec_model 排序
|
# [修改3] 默认排序方式改为按 spec_model 排序
|
||||||
# 如果需要更复杂的“/前内容”排序,通常直接按字符串排序也能满足前缀分组的需求
|
|
||||||
pagination = query.order_by(MaterialBase.spec_model.asc()).paginate(page=page, per_page=limit,
|
pagination = query.order_by(MaterialBase.spec_model.asc()).paginate(page=page, per_page=limit,
|
||||||
error_out=False)
|
error_out=False)
|
||||||
|
|
||||||
@ -280,14 +284,16 @@ class MaterialBaseService:
|
|||||||
|
|
||||||
buy_usage_count = StockBuy.query.filter_by(base_id=m_id).count()
|
buy_usage_count = StockBuy.query.filter_by(base_id=m_id).count()
|
||||||
semi_usage_count = StockSemi.query.filter_by(base_id=m_id).count()
|
semi_usage_count = StockSemi.query.filter_by(base_id=m_id).count()
|
||||||
|
prod_usage_count = StockProduct.query.filter_by(base_id=m_id).count()
|
||||||
|
|
||||||
total_usage = buy_usage_count + semi_usage_count
|
total_usage = buy_usage_count + semi_usage_count + prod_usage_count
|
||||||
|
|
||||||
if total_usage > 0:
|
if total_usage > 0:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"无法删除:该基础物料正被使用中。\n"
|
f"无法删除:该基础物料正被使用中。\n"
|
||||||
f"- 采购库存记录: {buy_usage_count} 条\n"
|
f"- 采购库存记录: {buy_usage_count} 条\n"
|
||||||
f"- 半成品库存记录: {semi_usage_count} 条\n"
|
f"- 半成品库存记录: {semi_usage_count} 条\n"
|
||||||
|
f"- 成品库存记录: {prod_usage_count} 条\n"
|
||||||
f"请先清理相关库存或仅‘禁用’此条目。"
|
f"请先清理相关库存或仅‘禁用’此条目。"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -298,4 +304,235 @@ class MaterialBaseService:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
print(f"删除基础信息失败: {e}")
|
print(f"删除基础信息失败: {e}")
|
||||||
|
raise e
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# [核心修改] 统一资产统计导出
|
||||||
|
# ==============================================================================
|
||||||
|
@staticmethod
|
||||||
|
def export_excel(filters=None):
|
||||||
|
"""
|
||||||
|
全口径资产统计报表:
|
||||||
|
逻辑:先查库存表 (Buy, Semi, Product),关联基础信息,只导出实际存在的库存记录。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 1. 构造基础信息的筛选条件 (用于过滤库存)
|
||||||
|
filter_conditions = []
|
||||||
|
if filters:
|
||||||
|
if filters.get('keyword'):
|
||||||
|
kw = f"%{filters['keyword']}%"
|
||||||
|
filter_conditions.append(or_(
|
||||||
|
MaterialBase.name.ilike(kw),
|
||||||
|
MaterialBase.common_name.ilike(kw),
|
||||||
|
MaterialBase.spec_model.ilike(kw),
|
||||||
|
MaterialBase.company_name.ilike(kw)
|
||||||
|
))
|
||||||
|
if filters.get('company'):
|
||||||
|
filter_conditions.append(MaterialBase.company_name == filters['company'])
|
||||||
|
if filters.get('category'):
|
||||||
|
filter_conditions.append(MaterialBase.category == filters['category'])
|
||||||
|
if filters.get('type'):
|
||||||
|
filter_conditions.append(MaterialBase.material_type == filters['type'])
|
||||||
|
if filters.get('isEnabled') is not None:
|
||||||
|
is_active = bool(int(filters['isEnabled']))
|
||||||
|
filter_conditions.append(MaterialBase.is_enabled == is_active)
|
||||||
|
|
||||||
|
# 2. 分别查询三个库存表,并 Join MaterialBase 进行筛选
|
||||||
|
# 2.1 采购库存 (StockBuy)
|
||||||
|
query_buy = db.session.query(StockBuy, MaterialBase).join(
|
||||||
|
MaterialBase, StockBuy.base_id == MaterialBase.id
|
||||||
|
)
|
||||||
|
for cond in filter_conditions:
|
||||||
|
query_buy = query_buy.filter(cond)
|
||||||
|
list_buy = query_buy.all()
|
||||||
|
|
||||||
|
# 2.2 半成品库存 (StockSemi)
|
||||||
|
query_semi = db.session.query(StockSemi, MaterialBase).join(
|
||||||
|
MaterialBase, StockSemi.base_id == MaterialBase.id
|
||||||
|
)
|
||||||
|
for cond in filter_conditions:
|
||||||
|
query_semi = query_semi.filter(cond)
|
||||||
|
list_semi = query_semi.all()
|
||||||
|
|
||||||
|
# 2.3 成品库存 (StockProduct)
|
||||||
|
query_product = db.session.query(StockProduct, MaterialBase).join(
|
||||||
|
MaterialBase, StockProduct.base_id == MaterialBase.id
|
||||||
|
)
|
||||||
|
for cond in filter_conditions:
|
||||||
|
query_product = query_product.filter(cond)
|
||||||
|
list_product = query_product.all()
|
||||||
|
|
||||||
|
# 3. 数据整合
|
||||||
|
all_rows = []
|
||||||
|
|
||||||
|
# 处理采购件
|
||||||
|
for stock, base in list_buy:
|
||||||
|
# 价格计算
|
||||||
|
unit_price = float(stock.unit_price or 0)
|
||||||
|
tax_rate = float(stock.tax_rate or 0)
|
||||||
|
price_incl = unit_price * (1 + tax_rate / 100.0)
|
||||||
|
qty = float(stock.stock_quantity or 0)
|
||||||
|
|
||||||
|
# 计算不含税总价 = 数量 * 不含税单价
|
||||||
|
total_val_excl = qty * unit_price
|
||||||
|
# 计算含税总价 = 数量 * 含税单价
|
||||||
|
total_val_incl = qty * price_incl
|
||||||
|
|
||||||
|
ident = stock.batch_number or stock.serial_number or stock.barcode or stock.sku
|
||||||
|
|
||||||
|
all_rows.append({
|
||||||
|
"base": base,
|
||||||
|
"type_name": "采购件",
|
||||||
|
"ident": ident,
|
||||||
|
"loc": stock.warehouse_location,
|
||||||
|
"source": stock.supplier_name,
|
||||||
|
"date": stock.in_date,
|
||||||
|
"qty": qty,
|
||||||
|
"avail": float(stock.available_quantity or 0),
|
||||||
|
"price_excl": unit_price,
|
||||||
|
"total_val_excl": total_val_excl, # [新增]
|
||||||
|
"tax": tax_rate,
|
||||||
|
"price_incl": price_incl,
|
||||||
|
"total_val": total_val_incl
|
||||||
|
})
|
||||||
|
|
||||||
|
# 处理半成品
|
||||||
|
for stock, base in list_semi:
|
||||||
|
cost = float(stock.raw_material_cost or 0) + float(stock.manual_cost or 0)
|
||||||
|
qty = float(stock.stock_quantity or 0)
|
||||||
|
|
||||||
|
# 半成品不含税总价 = 数量 * 成本
|
||||||
|
total_val_excl = qty * cost
|
||||||
|
# 含税总价同上 (税率0)
|
||||||
|
total_val_incl = qty * cost
|
||||||
|
|
||||||
|
ident = stock.batch_number or stock.serial_number or stock.barcode or stock.sku
|
||||||
|
|
||||||
|
all_rows.append({
|
||||||
|
"base": base,
|
||||||
|
"type_name": "半成品",
|
||||||
|
"ident": ident,
|
||||||
|
"loc": stock.warehouse_location,
|
||||||
|
"source": stock.production_manager,
|
||||||
|
"date": stock.production_date,
|
||||||
|
"qty": qty,
|
||||||
|
"avail": float(stock.available_quantity or 0),
|
||||||
|
"price_excl": cost,
|
||||||
|
"total_val_excl": total_val_excl, # [新增]
|
||||||
|
"tax": 0.0,
|
||||||
|
"price_incl": cost,
|
||||||
|
"total_val": total_val_incl
|
||||||
|
})
|
||||||
|
|
||||||
|
# 处理成品
|
||||||
|
for stock, base in list_product:
|
||||||
|
cost = float(stock.raw_material_cost or 0) + float(stock.manual_cost or 0)
|
||||||
|
qty = float(stock.stock_quantity or 0)
|
||||||
|
|
||||||
|
total_val_excl = qty * cost
|
||||||
|
total_val_incl = qty * cost
|
||||||
|
|
||||||
|
ident = stock.serial_number or stock.barcode or stock.sku
|
||||||
|
|
||||||
|
all_rows.append({
|
||||||
|
"base": base,
|
||||||
|
"type_name": "成品",
|
||||||
|
"ident": ident,
|
||||||
|
"loc": stock.warehouse_location,
|
||||||
|
"source": stock.production_manager,
|
||||||
|
"date": stock.production_date,
|
||||||
|
"qty": qty,
|
||||||
|
"avail": float(stock.available_quantity or 0),
|
||||||
|
"price_excl": cost,
|
||||||
|
"total_val_excl": total_val_excl, # [新增]
|
||||||
|
"tax": 0.0,
|
||||||
|
"price_incl": cost,
|
||||||
|
"total_val": total_val_incl
|
||||||
|
})
|
||||||
|
|
||||||
|
# 4. 排序:按公司 -> 规格型号 -> 基础ID -> 批号 排序
|
||||||
|
all_rows.sort(key=lambda x: (
|
||||||
|
x['base'].company_name or "",
|
||||||
|
x['base'].spec_model or "",
|
||||||
|
x['base'].id,
|
||||||
|
x['ident'] or ""
|
||||||
|
))
|
||||||
|
|
||||||
|
# 5. 生成 Excel
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "库存统计"
|
||||||
|
|
||||||
|
# 表头 [修改] 增加 "资产总额 (不含税)"
|
||||||
|
headers = [
|
||||||
|
"所属公司", "资产名称", "规格型号", "物料类型",
|
||||||
|
"类别一级", "类别二级", "类别三级", "类别四级", "类别五级",
|
||||||
|
"计量单位",
|
||||||
|
"库存性质", "唯一标识码 (批号/SN)", "仓库位置",
|
||||||
|
"资产来源", "入库/生产日期",
|
||||||
|
"库存数量", "可用数量",
|
||||||
|
"单价/成本 (不含税)", "资产总额 (不含税)", "税率 (%)", "单价/成本 (含税)", "资产总额 (含税)"
|
||||||
|
]
|
||||||
|
ws.append(headers)
|
||||||
|
|
||||||
|
# 样式
|
||||||
|
header_fill = PatternFill(start_color="D7E4BC", end_color="D7E4BC", fill_type="solid")
|
||||||
|
border_style = Border(left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'),
|
||||||
|
bottom=Side(style='thin'))
|
||||||
|
|
||||||
|
for cell in ws[1]:
|
||||||
|
cell.font = Font(bold=True, name='微软雅黑')
|
||||||
|
cell.alignment = Alignment(horizontal='center', vertical='center')
|
||||||
|
cell.fill = header_fill
|
||||||
|
cell.border = border_style
|
||||||
|
|
||||||
|
# 写入数据
|
||||||
|
for r in all_rows:
|
||||||
|
base = r['base']
|
||||||
|
# 类别拆分
|
||||||
|
cat_parts = (base.category or "").split('/')
|
||||||
|
while len(cat_parts) < 5:
|
||||||
|
cat_parts.append("")
|
||||||
|
|
||||||
|
# 日期格式化
|
||||||
|
date_str = r['date'].strftime('%Y-%m-%d') if isinstance(r['date'], datetime.date) else ""
|
||||||
|
|
||||||
|
row_val = [
|
||||||
|
base.company_name,
|
||||||
|
base.name,
|
||||||
|
base.spec_model,
|
||||||
|
base.material_type,
|
||||||
|
cat_parts[0], cat_parts[1], cat_parts[2], cat_parts[3], cat_parts[4],
|
||||||
|
base.unit,
|
||||||
|
r['type_name'],
|
||||||
|
r['ident'],
|
||||||
|
r['loc'],
|
||||||
|
r['source'],
|
||||||
|
date_str,
|
||||||
|
r['qty'],
|
||||||
|
r['avail'],
|
||||||
|
r['price_excl'],
|
||||||
|
r['total_val_excl'], # [新增] 对应列
|
||||||
|
r['tax'],
|
||||||
|
r['price_incl'],
|
||||||
|
r['total_val']
|
||||||
|
]
|
||||||
|
ws.append(row_val)
|
||||||
|
|
||||||
|
# 列宽调整
|
||||||
|
dims = {}
|
||||||
|
for row in ws.rows:
|
||||||
|
for cell in row:
|
||||||
|
if cell.value:
|
||||||
|
dims[cell.column_letter] = max((dims.get(cell.column_letter, 0), len(str(cell.value))))
|
||||||
|
for col, value in dims.items():
|
||||||
|
ws.column_dimensions[col].width = min(value + 2, 30)
|
||||||
|
|
||||||
|
output = io.BytesIO()
|
||||||
|
wb.save(output)
|
||||||
|
output.seek(0)
|
||||||
|
return output
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
raise e
|
raise e
|
||||||
@ -13,4 +13,6 @@ python-barcode>=0.14.0
|
|||||||
# [新增] 二维码生成库 (标签打印必需,包含PIL支持)
|
# [新增] 二维码生成库 (标签打印必需,包含PIL支持)
|
||||||
qrcode[pil]>=7.4.2
|
qrcode[pil]>=7.4.2
|
||||||
# [新增] 必须添加,用于处理 token 登录
|
# [新增] 必须添加,用于处理 token 登录
|
||||||
Flask-JWT-Extended==4.6.0
|
Flask-JWT-Extended==4.6.0
|
||||||
|
# [新增] Excel 处理库 (解决 No module named 'openpyxl' 报错)
|
||||||
|
openpyxl>=3.1.2
|
||||||
@ -9,9 +9,7 @@ export function listMaterialBase(params: any) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// 1.1 获取选项
|
||||||
// 1.1 获取基础信息筛选选项 (所有类别/类型) [新增]
|
|
||||||
// ==========================================
|
|
||||||
export function getMaterialBaseOptions() {
|
export function getMaterialBaseOptions() {
|
||||||
return request({
|
return request({
|
||||||
url: '/inbound/base/options',
|
url: '/inbound/base/options',
|
||||||
@ -19,7 +17,17 @@ export function getMaterialBaseOptions() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 新增基础信息
|
// 1.2 [新增] 导出全口径资产统计表
|
||||||
|
export function exportAssetStatistics(params: any) {
|
||||||
|
return request({
|
||||||
|
url: '/inbound/base/export',
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
responseType: 'blob' // 关键:必须声明为 blob 处理文件流
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 新增
|
||||||
export function addMaterialBase(data: any) {
|
export function addMaterialBase(data: any) {
|
||||||
return request({
|
return request({
|
||||||
url: '/inbound/base/',
|
url: '/inbound/base/',
|
||||||
@ -28,7 +36,7 @@ export function addMaterialBase(data: any) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 修改基础信息 (包含状态启用/禁用)
|
// 3. 修改
|
||||||
export function updateMaterialBase(data: any) {
|
export function updateMaterialBase(data: any) {
|
||||||
return request({
|
return request({
|
||||||
url: `/inbound/base/${data.id}`,
|
url: `/inbound/base/${data.id}`,
|
||||||
@ -37,18 +45,10 @@ export function updateMaterialBase(data: any) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 删除基础信息
|
// 4. 删除
|
||||||
export function delMaterialBase(id: number) {
|
export function delMaterialBase(id: number) {
|
||||||
return request({
|
return request({
|
||||||
url: `/inbound/base/${id}`,
|
url: `/inbound/base/${id}`,
|
||||||
method: 'delete'
|
method: 'delete'
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 获取详情 (可选,用于编辑回显)
|
|
||||||
export function getMaterialBase(id: number) {
|
|
||||||
return request({
|
|
||||||
url: `/inbound/base/${id}`,
|
|
||||||
method: 'get'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
@ -67,6 +67,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="right-toolbar">
|
<div class="right-toolbar">
|
||||||
|
<el-button type="success" plain @click="handleExport" :loading="exportLoading" style="margin-right: 10px">
|
||||||
|
<el-icon style="margin-right: 5px"><Download /></el-icon>导出库存统计
|
||||||
|
</el-button>
|
||||||
|
|
||||||
<el-button type="primary" @click="handleAdd" style="margin-right: 10px">
|
<el-button type="primary" @click="handleAdd" style="margin-right: 10px">
|
||||||
<el-icon style="margin-right: 5px"><Plus /></el-icon>新增
|
<el-icon style="margin-right: 5px"><Plus /></el-icon>新增
|
||||||
</el-button>
|
</el-button>
|
||||||
@ -412,8 +416,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted, nextTick, computed } from 'vue';
|
import { ref, reactive, onMounted, nextTick } from 'vue';
|
||||||
import { Plus, Picture, Document, Refresh, Setting, Rank, Camera, Link } from '@element-plus/icons-vue';
|
import { Plus, Document, Refresh, Setting, Rank, Camera, Link, Download } from '@element-plus/icons-vue';
|
||||||
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
|
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
|
||||||
import type { FormInstance, FormRules } from 'element-plus';
|
import type { FormInstance, FormRules } from 'element-plus';
|
||||||
|
|
||||||
@ -422,7 +426,8 @@ import {
|
|||||||
addMaterialBase,
|
addMaterialBase,
|
||||||
updateMaterialBase,
|
updateMaterialBase,
|
||||||
delMaterialBase,
|
delMaterialBase,
|
||||||
getMaterialBaseOptions
|
getMaterialBaseOptions,
|
||||||
|
exportAssetStatistics // 导入导出API
|
||||||
} from '@/api/material_base';
|
} from '@/api/material_base';
|
||||||
import { uploadFile, deleteFile } from '@/api/common/upload';
|
import { uploadFile, deleteFile } from '@/api/common/upload';
|
||||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue';
|
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue';
|
||||||
@ -464,6 +469,7 @@ interface CascaderOption {
|
|||||||
|
|
||||||
// --- 响应式数据 ---
|
// --- 响应式数据 ---
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const exportLoading = ref(false); // 导出加载状态
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
const tableData = ref<MaterialBaseVO[]>([]);
|
const tableData = ref<MaterialBaseVO[]>([]);
|
||||||
const submitLoading = ref(false);
|
const submitLoading = ref(false);
|
||||||
@ -481,7 +487,6 @@ const cameraRef = ref<InstanceType<typeof WebRtcCamera> | null>(null);
|
|||||||
const currentCameraField = ref<'generalImage' | 'generalManual'>('generalImage');
|
const currentCameraField = ref<'generalImage' | 'generalManual'>('generalImage');
|
||||||
|
|
||||||
const columns = reactive({
|
const columns = reactive({
|
||||||
// [修改] 默认隐藏 ID
|
|
||||||
id: { visible: false },
|
id: { visible: false },
|
||||||
companyName: { visible: true },
|
companyName: { visible: true },
|
||||||
name: { visible: true },
|
name: { visible: true },
|
||||||
@ -646,7 +651,51 @@ const getList = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let searchTimer: ReturnType<typeof setTimeout> | null = null;
|
// [修改] 导出处理函数:修正文件名格式
|
||||||
|
const handleExport = () => {
|
||||||
|
exportLoading.value = true;
|
||||||
|
const params = {
|
||||||
|
keyword: queryParams.keyword,
|
||||||
|
company: queryParams.company,
|
||||||
|
category: queryParams.category,
|
||||||
|
type: queryParams.type,
|
||||||
|
isEnabled: queryParams.isEnabled
|
||||||
|
};
|
||||||
|
|
||||||
|
exportAssetStatistics(params)
|
||||||
|
.then((response: any) => {
|
||||||
|
const blob = new Blob([response], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
|
||||||
|
// 构造文件名:库存统计_YYYYMMDD_HHMMSS
|
||||||
|
const now = new Date();
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(now.getDate()).padStart(2, '0');
|
||||||
|
const hour = String(now.getHours()).padStart(2, '0');
|
||||||
|
const minute = String(now.getMinutes()).padStart(2, '0');
|
||||||
|
const second = String(now.getSeconds()).padStart(2, '0');
|
||||||
|
const filename = `库存统计_${year}${month}${day}_${hour}${minute}${second}.xlsx`;
|
||||||
|
|
||||||
|
link.setAttribute('download', filename);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
ElMessage.success('导出成功');
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("导出失败", err);
|
||||||
|
ElMessage.error('导出失败');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
exportLoading.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let searchTimer: any = null;
|
||||||
const handleInputSearch = () => {
|
const handleInputSearch = () => {
|
||||||
if (searchTimer) clearTimeout(searchTimer);
|
if (searchTimer) clearTimeout(searchTimer);
|
||||||
searchTimer = setTimeout(() => {
|
searchTimer = setTimeout(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user