Compare commits

2 Commits

Author SHA1 Message Date
dxc
cd55a6aee1 Merge remote-tracking branch 'origin/1.0入库操作' into 1.0入库操作
# Conflicts:
#	inventory-backend/app/services/inbound/buy_service.py
#	inventory-web/src/views/stock/inbound/buy.vue
2026-01-28 11:50:55 +08:00
dxc
6f4917f57e 针对于采购页面进行优化逻辑 2026-01-28 11:49:59 +08:00
2 changed files with 391 additions and 302 deletions

View File

@ -1,23 +1,17 @@
# app/services/inbound/buy_service.py
from app.extensions import db from app.extensions import db
from app.models.material import MaterialBase from app.models.material import MaterialBase
from app.models.stock import StockBuy from app.models.stock import StockBuy
from datetime import datetime from datetime import datetime
from sqlalchemy import or_ from sqlalchemy import or_, func # 引入 func 用于聚合计算
import traceback import traceback
class BuyInboundService: class BuyInboundService:
@staticmethod @staticmethod
def search_base_material(keyword): def search_base_material(keyword):
"""
搜索基础物料库
"""
try: try:
if not keyword: if not keyword:
return [] return []
# 过滤条件:名称或规格包含关键词,且 is_enabled 为 True
query = MaterialBase.query.filter( query = MaterialBase.query.filter(
MaterialBase.is_enabled == True, MaterialBase.is_enabled == True,
or_( or_(
@ -44,11 +38,7 @@ class BuyInboundService:
@staticmethod @staticmethod
def handle_inbound(data): def handle_inbound(data):
"""
新增入库逻辑
"""
try: try:
# 1. 核心校验
base_id = data.get('base_id') base_id = data.get('base_id')
if not base_id: if not base_id:
raise ValueError("必须选择基础物料进行入库 (缺少 base_id)") raise ValueError("必须选择基础物料进行入库 (缺少 base_id)")
@ -57,24 +47,19 @@ class BuyInboundService:
if not material: if not material:
raise ValueError(f"ID为 {base_id} 的基础物料不存在") raise ValueError(f"ID为 {base_id} 的基础物料不存在")
# 2. 处理日期 (防止空字符串报错)
in_date_val = datetime.utcnow().date() in_date_val = datetime.utcnow().date()
if data.get('in_date'): if data.get('in_date'):
try: try:
date_str = str(data['in_date']) if len(str(data['in_date'])) > 10:
# 截取前10位 YYYY-MM-DD in_date_val = datetime.strptime(str(data['in_date'])[:10], '%Y-%m-%d').date()
if len(date_str) > 10:
in_date_val = datetime.strptime(date_str[:10], '%Y-%m-%d').date()
else: else:
in_date_val = datetime.strptime(date_str, '%Y-%m-%d').date() in_date_val = datetime.strptime(str(data['in_date']), '%Y-%m-%d').date()
except ValueError: except ValueError:
pass # 格式错误则使用当前日期 pass
# 3. 数据转换
in_qty = float(data.get('in_quantity') or 0) in_qty = float(data.get('in_quantity') or 0)
u_price = float(data.get('unit_price') or 0) u_price = float(data.get('unit_price') or 0)
# 4. 创建 StockBuy
new_stock = StockBuy( new_stock = StockBuy(
base_id=material.id, base_id=material.id,
sku=data.get('sku'), sku=data.get('sku'),
@ -82,27 +67,20 @@ class BuyInboundService:
serial_number=data.get('serial_number'), serial_number=data.get('serial_number'),
batch_number=data.get('batch_number'), batch_number=data.get('batch_number'),
barcode=data.get('barcode'), barcode=data.get('barcode'),
# --- 状态与数量强制逻辑 ---
status='在库', status='在库',
in_quantity=in_qty, in_quantity=in_qty,
stock_quantity=in_qty, # 初始库存 = 入库量 stock_quantity=in_qty,
available_quantity=in_qty, # 初始可用 = 入库量 available_quantity=in_qty,
inspection_status=data.get('inspection_status', '未检'), inspection_status=data.get('inspection_status', '未检'),
# ----------------
warehouse_location=data.get('warehouse_location'), warehouse_location=data.get('warehouse_location'),
unit_price=u_price, unit_price=u_price,
total_price=in_qty * u_price, total_price=in_qty * u_price,
currency=data.get('currency', 'CNY'), currency=data.get('currency', 'CNY'),
exchange_rate=data.get('exchange_rate', 1.0), exchange_rate=data.get('exchange_rate', 1.0),
supplier_name=data.get('supplier_name'), supplier_name=data.get('supplier_name'),
# [字段映射] 前端 -> DB
buyer_name=data.get('purchaser'), buyer_name=data.get('purchaser'),
buyer_email=data.get('purchaser_email'), buyer_email=data.get('purchaser_email'),
original_link=data.get('source_link'), original_link=data.get('source_link'),
detail_link=data.get('detail_link'), detail_link=data.get('detail_link'),
arrival_photo=data.get('arrival_photo'), arrival_photo=data.get('arrival_photo'),
remark=data.get('remark') remark=data.get('remark')
@ -114,23 +92,18 @@ class BuyInboundService:
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
print(f"Insert Error: {str(e)}")
raise e raise e
@staticmethod @staticmethod
def update_inbound(stock_id, data): def update_inbound(stock_id, data):
"""
更新入库逻辑
"""
try: try:
print(f"----- UPDATE DEBUG: ID={stock_id} -----") print(f"----- UPDATE DEBUG: ID={stock_id} -----")
print(f"Payload: {data}")
stock = StockBuy.query.get(stock_id) stock = StockBuy.query.get(stock_id)
if not stock: if not stock:
raise ValueError("记录不存在") raise ValueError("记录不存在")
# 1. 字段映射字典前端Key -> Model属性名
# 使用映射可以避免写大量 if...else且逻辑更清晰
field_mapping = { field_mapping = {
'sku': 'sku', 'sku': 'sku',
'barcode': 'barcode', 'barcode': 'barcode',
@ -145,50 +118,37 @@ class BuyInboundService:
'remark': 'remark', 'remark': 'remark',
'currency': 'currency', 'currency': 'currency',
'exchange_rate': 'exchange_rate', 'exchange_rate': 'exchange_rate',
# 关键映射
'purchaser': 'buyer_name', 'purchaser': 'buyer_name',
'purchaser_email': 'buyer_email', 'purchaser_email': 'buyer_email',
'source_link': 'original_link' 'source_link': 'original_link'
} }
# 遍历更新 (排除日期、数量、单价,下面单独处理)
for frontend_key, db_attr in field_mapping.items(): for frontend_key, db_attr in field_mapping.items():
if frontend_key in data: if frontend_key in data:
setattr(stock, db_attr, data[frontend_key]) setattr(stock, db_attr, data[frontend_key])
# 2. 核心数值逻辑 (数量 & 单价 & 总价)
qty_changed = False qty_changed = False
price_changed = False price_changed = False
# (A) 处理入库数量变更
if 'in_quantity' in data: if 'in_quantity' in data:
new_qty = float(data['in_quantity']) new_qty = float(data['in_quantity'])
old_qty = float(stock.in_quantity) old_qty = float(stock.in_quantity)
if new_qty != old_qty: if new_qty != old_qty:
print(f"Quantity Changed: {old_qty} -> {new_qty}")
diff = new_qty - old_qty diff = new_qty - old_qty
stock.in_quantity = new_qty stock.in_quantity = new_qty
# 联动更新库存和可用量
stock.stock_quantity = float(stock.stock_quantity) + diff stock.stock_quantity = float(stock.stock_quantity) + diff
stock.available_quantity = float(stock.available_quantity) + diff stock.available_quantity = float(stock.available_quantity) + diff
qty_changed = True qty_changed = True
# (B) 处理单价变更
if 'unit_price' in data: if 'unit_price' in data:
new_price = float(data['unit_price']) new_price = float(data['unit_price'])
old_price = float(stock.unit_price) old_price = float(stock.unit_price)
if new_price != old_price: if new_price != old_price:
print(f"Price Changed: {old_price} -> {new_price}")
stock.unit_price = new_price stock.unit_price = new_price
price_changed = True price_changed = True
# (C) 强制重算总价
if qty_changed or price_changed: if qty_changed or price_changed:
stock.total_price = float(stock.in_quantity) * float(stock.unit_price) stock.total_price = float(stock.in_quantity) * float(stock.unit_price)
print(f"New Total Price: {stock.total_price}")
db.session.commit() db.session.commit()
print("----- UPDATE SUCCESS -----") print("----- UPDATE SUCCESS -----")
@ -206,7 +166,6 @@ class BuyInboundService:
stock = StockBuy.query.get(stock_id) stock = StockBuy.query.get(stock_id)
if not stock: if not stock:
raise ValueError("记录不存在") raise ValueError("记录不存在")
db.session.delete(stock) db.session.delete(stock)
db.session.commit() db.session.commit()
return True return True
@ -216,14 +175,10 @@ class BuyInboundService:
@staticmethod @staticmethod
def get_list(page, limit, keyword=None): def get_list(page, limit, keyword=None):
"""
列表查询
"""
try: try:
# 使用 Outer Join 确保即使 MaterialBase 物理删除,库存记录也不会报错或消失 # 1. 查询分页数据
query = db.session.query(StockBuy).outerjoin(MaterialBase, StockBuy.base_id == MaterialBase.id) query = db.session.query(StockBuy).outerjoin(MaterialBase, StockBuy.base_id == MaterialBase.id)
# 搜索逻辑
if keyword: if keyword:
query = query.filter( query = query.filter(
or_( or_(
@ -237,16 +192,41 @@ class BuyInboundService:
pagination = query.order_by(StockBuy.id.desc()).paginate(page=page, per_page=limit, error_out=False) pagination = query.order_by(StockBuy.id.desc()).paginate(page=page, per_page=limit, error_out=False)
# ---------------------------------------------------------------------
# 新增逻辑:计算总库存
# ---------------------------------------------------------------------
# 2. 提取当前页所有涉及的 base_id
current_items = pagination.items
base_ids = list(set([item.base_id for item in current_items if item.base_id]))
# 3. 聚合查询:一次性查出这些 base_id 对应的 stock_quantity 和 available_quantity 总和
stock_map = {}
if base_ids:
# SELECT base_id, SUM(stock_quantity), SUM(available_quantity) FROM stock_buy WHERE base_id IN (...) GROUP BY base_id
aggregates = db.session.query(
StockBuy.base_id,
func.sum(StockBuy.stock_quantity).label('total_stock'),
func.sum(StockBuy.available_quantity).label('total_avail')
).filter(StockBuy.base_id.in_(base_ids)).group_by(StockBuy.base_id).all()
for agg in aggregates:
stock_map[agg.base_id] = {
'total_stock': float(agg.total_stock or 0),
'total_avail': float(agg.total_avail or 0)
}
# ---------------------------------------------------------------------
items = [] items = []
for item in pagination.items: for item in current_items:
# 获取关联的基础信息,如果关联不存在则给默认值
mat_name = item.material.name if item.material else '未知物料' mat_name = item.material.name if item.material else '未知物料'
mat_spec = item.material.spec_model if item.material else '' mat_spec = item.material.spec_model if item.material else ''
mat_cat = item.material.category if item.material else '' mat_cat = item.material.category if item.material else ''
mat_unit = item.material.unit if item.material else '' mat_unit = item.material.unit if item.material else ''
mat_type = item.material.material_type if item.material else '' mat_type = item.material.material_type if item.material else ''
# 构建返回字典 # 获取该物料的统计数据
stats = stock_map.get(item.base_id, {'total_stock': 0, 'total_avail': 0})
d = { d = {
'id': item.id, 'id': item.id,
'base_id': item.base_id, 'base_id': item.base_id,
@ -264,23 +244,24 @@ class BuyInboundService:
'status': item.status, 'status': item.status,
'inspection_status': item.inspection_status, 'inspection_status': item.inspection_status,
# --- 原始批次数据 (用于编辑) ---
'qty_inbound': float(item.in_quantity or 0), 'qty_inbound': float(item.in_quantity or 0),
'qty_stock': float(item.stock_quantity or 0), 'qty_stock': float(item.stock_quantity or 0),
'qty_available': float(item.available_quantity or 0), 'qty_available': float(item.available_quantity or 0),
# --- [新增] 聚合统计数据 (用于列表显示) ---
'sum_stock': stats['total_stock'],
'sum_available': stats['total_avail'],
'warehouse_loc': item.warehouse_location, 'warehouse_loc': item.warehouse_location,
'unit_price': float(item.unit_price or 0), 'unit_price': float(item.unit_price or 0),
'total_price': float(item.total_price or 0), 'total_price': float(item.total_price or 0),
'currency': item.currency, 'currency': item.currency,
'exchange_rate': float(item.exchange_rate or 1), 'exchange_rate': float(item.exchange_rate or 1),
'supplier_name': item.supplier_name, 'supplier_name': item.supplier_name,
# [关键映射] DB -> Frontend
'purchaser': item.buyer_name, 'purchaser': item.buyer_name,
'purchaser_email': item.buyer_email, 'purchaser_email': item.buyer_email,
'source_link': item.original_link, 'source_link': item.original_link,
'detail_link': item.detail_link, 'detail_link': item.detail_link,
'arrival_photo': item.arrival_photo, 'arrival_photo': item.arrival_photo,
'remark': item.remark 'remark': item.remark
@ -289,7 +270,6 @@ class BuyInboundService:
return {"total": pagination.total, "items": items} return {"total": pagination.total, "items": items}
except Exception as e: except Exception as e:
# 打印错误到 Docker 日志
print(f"List Error: {e}") print(f"List Error: {e}")
traceback.print_exc() traceback.print_exc()
return {"total": 0, "items": []} return {"total": 0, "items": []}

View File

@ -4,7 +4,7 @@
<div class="left-tools"> <div class="left-tools">
<el-input <el-input
v-model="queryParams.keyword" v-model="queryParams.keyword"
placeholder="搜索物料名称/规格/单号..." placeholder="🔍 搜索物料名称 / 规格 / 批号 / SN / 供应商..."
class="search-input" class="search-input"
clearable clearable
@clear="fetchData" @clear="fetchData"
@ -17,19 +17,21 @@
</div> </div>
<div class="right-tools"> <div class="right-tools">
<el-button type="primary" :icon="Plus" @click="handleCreate" class="big-font-btn">采购入库登记</el-button> <el-button type="primary" :icon="Plus" @click="handleCreate" class="action-btn">采购入库登记</el-button>
<el-button :icon="Refresh" @click="fetchData" class="big-font-btn">刷新数据</el-button> <el-button :icon="Refresh" @click="fetchData" class="action-btn">刷新</el-button>
<el-popover placement="bottom-end" title="显示列配置" :width="600" trigger="click"> <el-popover placement="bottom-end" title="列配置" :width="500" trigger="click">
<template #reference> <template #reference>
<el-button :icon="Setting" class="big-font-btn">自定义表头</el-button> <el-button :icon="Setting" class="action-btn">表头</el-button>
</template> </template>
<el-checkbox-group v-model="visibleColumnProps" class="column-selector"> <el-checkbox-group v-model="visibleColumnProps" class="column-selector">
<el-row> <div class="col-group-title">基础信息</div>
<el-col :span="24"><el-divider content-position="left">基础信息 (只读)</el-divider></el-col> <el-row :gutter="10">
<el-col :span="8" v-for="c in baseColumns" :key="c.prop"><el-checkbox :label="c.prop">{{ c.label }}</el-checkbox></el-col> <el-col :span="12" v-for="c in baseColumns" :key="c.prop"><el-checkbox :label="c.prop">{{ c.label }}</el-checkbox></el-col>
<el-col :span="24"><el-divider content-position="left">库存与商务</el-divider></el-col> </el-row>
<el-col :span="8" v-for="c in stockColumns" :key="c.prop"><el-checkbox :label="c.prop">{{ c.label }}</el-checkbox></el-col> <div class="col-group-title" style="margin-top:10px">库存与商务</div>
<el-row :gutter="10">
<el-col :span="12" v-for="c in stockColumns" :key="c.prop"><el-checkbox :label="c.prop">{{ c.label }}</el-checkbox></el-col>
</el-row> </el-row>
</el-checkbox-group> </el-checkbox-group>
</el-popover> </el-popover>
@ -42,8 +44,9 @@
border border
stripe stripe
style="width: 100%" style="width: 100%"
class="custom-big-table" class="modern-table"
highlight-current-row highlight-current-row
header-cell-class-name="table-header-gray"
> >
<template v-for="col in allColumns" :key="col.prop"> <template v-for="col in allColumns" :key="col.prop">
<el-table-column <el-table-column
@ -54,34 +57,46 @@
show-overflow-tooltip show-overflow-tooltip
> >
<template #default="scope" v-if="['serial_number', 'batch_number'].includes(col.prop)"> <template #default="scope" v-if="['serial_number', 'batch_number'].includes(col.prop)">
<span v-if="scope.row[col.prop]" :class="col.prop === 'serial_number' ? 'text-sn' : 'text-bn'"> <span v-if="scope.row[col.prop]" :class="col.prop === 'serial_number' ? 'tag-sn' : 'tag-bn'">
{{ scope.row[col.prop] }} {{ scope.row[col.prop] }}
</span> </span>
<span v-else>-</span> <span v-else class="text-placeholder">-</span>
</template>
<template #default="scope" v-else-if="col.prop === 'qty_stock'">
<span class="stock-num">{{ scope.row.sum_stock }}</span>
<el-tag size="small" type="info" effect="plain" class="sum-tag"></el-tag>
</template>
<template #default="scope" v-else-if="col.prop === 'qty_available'">
<span class="avail-num">{{ scope.row.sum_available }}</span>
<el-tag size="small" type="info" effect="plain" class="sum-tag"></el-tag>
</template> </template>
<template #default="scope" v-else-if="col.prop === 'status'"> <template #default="scope" v-else-if="col.prop === 'status'">
<el-tag :type="getStatusType(scope.row.status)" effect="dark" size="large"> <el-tag :type="getStatusType(scope.row.status)" effect="light" round>
{{ scope.row.status }} {{ scope.row.status }}
</el-tag> </el-tag>
</template> </template>
<template #default="scope" v-else-if="col.prop.includes('link')"> <template #default="scope" v-else-if="col.prop.includes('link')">
<el-link v-if="scope.row[col.prop]" type="primary" :href="scope.row[col.prop]" target="_blank">查看</el-link> <el-link v-if="scope.row[col.prop]" type="primary" :href="scope.row[col.prop]" target="_blank" :underline="false">
<el-icon><Link /></el-icon> 查看
</el-link>
</template> </template>
<template #default="scope" v-else-if="['unit_price', 'total_price'].includes(col.prop)"> <template #default="scope" v-else-if="['unit_price', 'total_price'].includes(col.prop)">
{{ formatMoney(scope.row[col.prop], scope.row.currency) }} <span class="money-text">{{ formatMoney(scope.row[col.prop], scope.row.currency) }}</span>
</template> </template>
</el-table-column> </el-table-column>
</template> </template>
<el-table-column label="操作" width="180" fixed="right" align="center"> <el-table-column label="操作" width="160" fixed="right" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-button link type="primary" size="large" @click="handleUpdate(row)">编辑</el-button> <el-button link type="primary" size="default" @click="handleUpdate(row)">编辑</el-button>
<el-popconfirm title="确定删除该条入库记录吗?" @confirm="handleDelete(row)"> <el-popconfirm title="确定删除该条记录吗?不可恢复。" @confirm="handleDelete(row)" width="220">
<template #reference> <template #reference>
<el-button link type="danger" size="large">删除</el-button> <el-button link type="danger" size="default">删除</el-button>
</template> </template>
</el-popconfirm> </el-popconfirm>
</template> </template>
@ -89,12 +104,13 @@
</el-table> </el-table>
<el-pagination <el-pagination
class="pagination-container" class="pagination-bar"
v-model:current-page="queryParams.page" v-model:current-page="queryParams.page"
v-model:page-size="queryParams.pageSize" v-model:page-size="queryParams.pageSize"
:total="total" :total="total"
:page-sizes="[15, 30, 50, 100]" :page-sizes="[15, 30, 50, 100]"
layout="total, sizes, prev, pager, next, jumper" layout="total, sizes, prev, pager, next, jumper"
background
@size-change="fetchData" @size-change="fetchData"
@current-change="fetchData" @current-change="fetchData"
/> />
@ -102,198 +118,207 @@
<el-dialog <el-dialog
v-model="visible" v-model="visible"
:title="dialogStatus === 'create' ? '新增采购入库' : '编辑入库信息'" :title="dialogStatus === 'create' ? '新增采购入库' : '编辑入库信息'"
width="1100px" width="1050px"
top="3vh" top="5vh"
destroy-on-close destroy-on-close
:close-on-click-modal="false" :close-on-click-modal="false"
class="stylish-dialog"
> >
<el-form :model="form" label-width="120px" ref="formRef" :rules="rules" size="large" class="custom-form"> <el-form :model="form" label-width="110px" ref="formRef" :rules="rules" size="large" class="stylish-form">
<div class="section-block"> <div class="form-card basic-card">
<el-divider content-position="left"><el-icon><Box /></el-icon> <b>1. 基础物料信息 (必须已存在)</b></el-divider> <div class="card-title">
<el-icon class="icon"><Box /></el-icon>
<span>1. 基础信息</span>
<span class="sub-title" v-if="dialogStatus === 'create'"> (请先搜索选择物料)</span>
</div>
<el-row :gutter="20"> <div class="card-content">
<el-col :span="12" v-if="dialogStatus === 'create'"> <el-row :gutter="24" v-if="dialogStatus === 'create'" style="margin-bottom: 20px;">
<el-form-item label="搜索关联" prop="base_id"> <el-col :span="14">
<el-select <el-form-item label="物料搜索" prop="base_id" class="highlight-label">
v-model="form.base_id" <el-select
filterable v-model="form.base_id"
remote filterable
reserve-keyword remote
placeholder="输入名称或规格型号搜索..." reserve-keyword
:remote-method="handleSearchMaterial" placeholder="输入名称 / 规格型号进行模糊搜索..."
:loading="searchLoading" :remote-method="handleSearchMaterial"
style="width: 100%" :loading="searchLoading"
@change="onMaterialSelected" style="width: 100%"
> @change="onMaterialSelected"
<el-option size="large"
v-for="item in materialOptions"
:key="item.id"
:label="item.name + ' - ' + item.spec"
:value="item.id"
> >
<span style="float: left; font-weight: bold;">{{ item.name }}</span> <el-option
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.spec }}</span> v-for="item in materialOptions"
</el-option> :key="item.id"
</el-select> :label="item.name + ' [' + item.spec + ']'"
</el-form-item> :value="item.id"
</el-col> >
<div class="option-item">
<el-col :span="12" v-if="dialogStatus === 'create'"> <span class="opt-name">{{ item.name }}</span>
<el-alert title="注:只能入库状态为'启用'的基础物料。" type="warning" :closable="false" show-icon /> <span class="opt-spec">{{ item.spec }}</span>
</el-col> </div>
</el-row> </el-option>
</el-select>
<el-row :gutter="20" class="read-only-area"> </el-form-item>
<el-col :span="8"><el-form-item label="名称"><el-input v-model="form.material_name" disabled /></el-form-item></el-col> </el-col>
<el-col :span="8"><el-form-item label="规格型号"><el-input v-model="form.spec_model" disabled /></el-form-item></el-col> <el-col :span="10">
<el-col :span="8"><el-form-item label="单位"><el-input v-model="form.unit" disabled /></el-form-item></el-col> <div class="info-alert">
<el-col :span="8"><el-form-item label="类别"><el-input v-model="form.category" disabled /></el-form-item></el-col> <el-icon><InfoFilled /></el-icon> 仅展示状态为启用的基础物料
<el-col :span="8"><el-form-item label="类型"><el-input v-model="form.material_type" disabled placeholder="自动关联" /></el-form-item></el-col>
</el-row>
</div>
<div class="section-block">
<el-divider content-position="left"><el-icon><House /></el-icon> <b>2. 库存实体信息</b></el-divider>
<el-row :gutter="20">
<el-col :span="6"><el-form-item label="编码/SKU" prop="sku"><el-input v-model="form.sku" /></el-form-item></el-col>
<el-col :span="6">
<el-form-item label="入库日期" prop="in_date">
<el-date-picker
v-model="form.in_date"
type="date"
value-format="YYYY-MM-DD"
style="width:100%"
disabled
placeholder="自动生成"
/>
</el-form-item>
</el-col>
<el-col :span="6"><el-form-item label="条码" prop="barcode"><el-input v-model="form.barcode" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="库位" prop="warehouse_location"><el-input v-model="form.warehouse_location" /></el-form-item></el-col>
</el-row>
<div class="sn-bn-row">
<el-row style="margin-bottom: 15px; padding-left: 20px;">
<el-col :span="24">
<el-radio-group v-model="entryMode" @change="handleEntryModeChange" :disabled="modeLocked">
<el-radio label="batch" size="large" border>按批号入库 (Batch)</el-radio>
<el-radio label="serial" size="large" border>按序列号入库 (SN)</el-radio>
</el-radio-group>
<div v-if="modeLocked" class="locked-tip">
<el-icon style="vertical-align: middle"><Lock /></el-icon>
该物料已有入库记录系统已自动匹配历史入库方式同物料同策略不可手动更改
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20"> <div class="read-only-grid">
<el-col :span="10"> <el-row :gutter="24">
<el-form-item label="批号" prop="batch_number"> <el-col :span="8"><el-form-item label="名称"><el-input v-model="form.material_name" disabled class="is-text-view" /></el-form-item></el-col>
<el-input <el-col :span="8"><el-form-item label="规格型号"><el-input v-model="form.spec_model" disabled class="is-text-view" /></el-form-item></el-col>
v-model="form.batch_number" <el-col :span="8"><el-form-item label="单位"><el-input v-model="form.unit" disabled class="is-text-view" /></el-form-item></el-col>
placeholder="系统自动生成" <el-col :span="8"><el-form-item label="类别"><el-input v-model="form.category" disabled class="is-text-view" /></el-form-item></el-col>
:disabled="entryMode === 'serial'" <el-col :span="8"><el-form-item label="类型"><el-input v-model="form.material_type" disabled class="is-text-view" /></el-form-item></el-col>
clearable </el-row>
> </div>
<template #prefix><span style="color:#67C23A; font-weight:bold">BN</span></template> </div>
</el-input> </div>
</el-form-item>
</el-col> <div class="form-card inbound-card">
<el-col :span="10"> <div class="card-title">
<el-form-item label="序列号" prop="serial_number"> <el-icon class="icon"><House /></el-icon>
<el-input <span>2. 入库详情</span>
v-model="form.serial_number"
placeholder="请扫描或输入设备SN"
:disabled="entryMode === 'batch'"
clearable
>
<template #prefix><span style="color:#409EFF; font-weight:bold">SN</span></template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="24" class="tip-col">
<span class="small-tip" v-if="entryMode === 'batch'">
* <b>批号模式</b>{{ modeLocked ? '根据历史记录' : '首次入库' }}系统已自动生成新批号
</span>
<span class="small-tip" v-else>
* <b>序列号模式</b>{{ modeLocked ? '根据历史记录' : '首次入库' }}请手动录入唯一SN码
</span>
</el-col>
</el-row>
</div> </div>
<el-row :gutter="20" style="margin-top: 15px;"> <div class="card-content">
<el-col :span="6"> <el-row :gutter="24">
<el-form-item label="入库量" prop="in_quantity"> <el-col :span="6"><el-form-item label="编码/SKU" prop="sku"><el-input v-model="form.sku" placeholder="选填" /></el-form-item></el-col>
<el-input-number v-model="form.in_quantity" :min="1" style="width:100%" controls-position="right" /> <el-col :span="6">
</el-form-item> <el-form-item label="入库日期" prop="in_date">
</el-col> <el-date-picker v-model="form.in_date" type="date" value-format="YYYY-MM-DD" style="width:100%" disabled />
</el-form-item>
</el-col>
<el-col :span="6"><el-form-item label="条码" prop="barcode"><el-input v-model="form.barcode" placeholder="扫描条码" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="库位" prop="warehouse_location"><el-input v-model="form.warehouse_location" placeholder="例如: A-01-02" /></el-form-item></el-col>
</el-row>
<template v-if="dialogStatus === 'update'"> <div class="identity-panel">
<el-row>
<el-col :span="24" style="margin-bottom: 12px;">
<el-radio-group v-model="entryMode" @change="handleEntryModeChange" :disabled="modeLocked" class="custom-radio-group">
<el-radio-button label="batch">按批号入库 (Batch)</el-radio-button>
<el-radio-button label="serial">按序列号入库 (SN)</el-radio-button>
</el-radio-group>
<span v-if="modeLocked" class="locked-msg"><el-icon><Lock /></el-icon> 根据历史记录已锁定入库方式</span>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="批号" prop="batch_number">
<el-input
v-model="form.batch_number"
:placeholder="entryMode === 'batch' ? '系统生成...' : '不可用'"
:disabled="entryMode === 'serial'"
clearable
>
<template #prefix><span class="prefix-tag bn">BN</span></template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="序列号" prop="serial_number">
<el-input
v-model="form.serial_number"
:placeholder="entryMode === 'serial' ? '请扫描 SN...' : '不可用'"
:disabled="entryMode === 'batch'"
clearable
>
<template #prefix><span class="prefix-tag sn">SN</span></template>
</el-input>
</el-form-item>
</el-col>
</el-row>
</div>
<el-row :gutter="24" style="margin-top: 15px;">
<el-col :span="6"> <el-col :span="6">
<el-form-item label="库数量" prop="stock_quantity"> <el-form-item label="库数量" prop="in_quantity">
<el-input-number v-model="form.stock_quantity" disabled style="width:100%" :controls="false" /> <el-input-number v-model="form.in_quantity" :min="1" controls-position="right" style="width:100%" class="strong-input" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<template v-if="dialogStatus === 'update'">
<el-col :span="6">
<el-form-item label="当前库存" prop="stock_quantity">
<el-input-number v-model="form.stock_quantity" disabled style="width:100%" :controls="false" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="当前可用" prop="available_quantity">
<el-input-number v-model="form.available_quantity" disabled style="width:100%" :controls="false" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="库存状态" prop="status">
<el-select v-model="form.status" style="width:100%">
<el-option label="在库" value="在库" />
<el-option label="出库" value="出库" />
<el-option label="损耗" value="损耗" />
</el-select>
</el-form-item>
</el-col>
</template>
<el-col :span="6"> <el-col :span="6">
<el-form-item label="可用数量" prop="available_quantity"> <el-form-item label="到检状态" prop="inspection_status">
<el-input-number v-model="form.available_quantity" disabled style="width:100%" :controls="false" /> <el-select v-model="form.inspection_status" style="width:100%">
</el-form-item> <el-option label="未检" value="未检"><span style="color:#909399"> 未检</span></el-option>
</el-col> <el-option label="合格" value="合格"><span style="color:#67C23A">🟢 合格</span></el-option>
<el-col :span="6"> <el-option label="不合格" value="不合格"><span style="color:#F56C6C">🔴 不合格</span></el-option>
<el-form-item label="库存状态" prop="status">
<el-select v-model="form.status" style="width:100%">
<el-option label="在库" value="在库" />
<el-option label="出库" value="出库" />
<el-option label="损耗" value="损耗" />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
</template> <el-col :span="12"><el-form-item label="到货图片" prop="arrival_photo"><el-input v-model="form.arrival_photo" placeholder="输入图片 URL" /></el-form-item></el-col>
</el-row>
<el-col :span="6"> <div class="divider-text">商务与采购信息</div>
<el-form-item label="到检状态" prop="inspection_status">
<el-select v-model="form.inspection_status" style="width:100%">
<el-option label="未检" value="未检" />
<el-option label="合格" value="合格" />
<el-option label="不合格" value="不合格" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12"><el-form-item label="到货图" prop="arrival_photo"><el-input v-model="form.arrival_photo" /></el-form-item></el-col>
</el-row>
</div>
<div class="section-block"> <el-row :gutter="24">
<el-divider content-position="left"><el-icon><Money /></el-icon> <b>3. 商务信息</b></el-divider> <el-col :span="6"><el-form-item label="币种"><el-input v-model="form.currency" /></el-form-item></el-col>
<el-row :gutter="20"> <el-col :span="6"><el-form-item label="汇率"><el-input-number v-model="form.exchange_rate" :precision="2" controls-position="right" style="width:100%"/></el-form-item></el-col>
<el-col :span="6"><el-form-item label="币种"><el-input v-model="form.currency" /></el-form-item></el-col> <el-col :span="6">
<el-col :span="6"><el-form-item label="汇率"><el-input-number v-model="form.exchange_rate" :precision="2" controls-position="right" style="width:100%"/></el-form-item></el-col> <el-form-item label="含税单价" prop="unit_price">
<el-col :span="6"><el-form-item label="单价" prop="unit_price"><el-input-number v-model="form.unit_price" :precision="4" controls-position="right" style="width:100%"/></el-form-item></el-col> <el-input-number v-model="form.unit_price" :precision="4" controls-position="right" style="width:100%" />
<el-col :span="6"><el-form-item label="总价"><el-input-number v-model="form.total_price" :precision="4" disabled :controls="false" style="width:100%"/></el-form-item></el-col> </el-form-item>
</el-row> </el-col>
<el-row :gutter="20"> <el-col :span="6">
<el-col :span="8"><el-form-item label="供应商"><el-input v-model="form.supplier_name" /></el-form-item></el-col> <el-form-item label="总价">
<el-col :span="8"><el-form-item label="采购人"><el-input v-model="form.purchaser" /></el-form-item></el-col> <el-input-number v-model="form.total_price" :precision="2" disabled :controls="false" style="width:100%" class="total-price-input"/>
<el-col :span="8"><el-form-item label="邮箱"><el-input v-model="form.purchaser_email" /></el-form-item></el-col> </el-form-item>
</el-row> </el-col>
<el-row :gutter="20"> </el-row>
<el-col :span="12"><el-form-item label="原始链接"><el-input v-model="form.source_link" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="详情链接"><el-input v-model="form.detail_link" /></el-form-item></el-col> <el-row :gutter="24">
</el-row> <el-col :span="8"><el-form-item label="供应商"><el-input v-model="form.supplier_name" placeholder="供应商全称" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="采购人"><el-input v-model="form.purchaser" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="采购邮箱"><el-input v-model="form.purchaser_email" /></el-form-item></el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12"><el-form-item label="原始链接"><el-input v-model="form.source_link" placeholder="http://" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="详情链接"><el-input v-model="form.detail_link" placeholder="http://" /></el-form-item></el-col>
</el-row>
</div>
</div> </div>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="visible = false" class="big-font-btn">取消</el-button> <div class="dialog-footer">
<el-button type="primary" :loading="submitting" @click="submitForm" class="big-font-btn"> <el-button @click="visible = false" size="large">取消</el-button>
{{ dialogStatus === 'create' ? '确认入库' : '保存修改' }} <el-button type="primary" :loading="submitting" @click="submitForm" size="large" class="confirm-btn">
</el-button> {{ dialogStatus === 'create' ? '确认入库' : '保存修改' }}
</el-button>
</div>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
@ -301,7 +326,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted, watch } from 'vue' import { ref, reactive, onMounted, watch } from 'vue'
import { Plus, Setting, Refresh, Search, Lock, Box, House, Money } from '@element-plus/icons-vue' import { Plus, Setting, Refresh, Search, Lock, Box, House, InfoFilled, Link } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { import {
@ -326,11 +351,10 @@ const formRef = ref()
const queryParams = reactive({ page: 1, pageSize: 15, keyword: '' }) const queryParams = reactive({ page: 1, pageSize: 15, keyword: '' })
const materialOptions = ref<any[]>([]) const materialOptions = ref<any[]>([])
// 新增:入库模式控制
const entryMode = ref('batch') const entryMode = ref('batch')
const modeLocked = ref(false) const modeLocked = ref(false)
// 列定义 (保持不变) // 列定义
const baseColumns = [ const baseColumns = [
{ prop: 'material_name', label: '名称' }, { prop: 'material_name', label: '名称' },
{ prop: 'category', label: '类别' }, { prop: 'category', label: '类别' },
@ -367,11 +391,29 @@ const stockColumns = [
const allColumns = [...baseColumns, ...stockColumns] const allColumns = [...baseColumns, ...stockColumns]
const visibleColumnProps = ref([ // 表头持久化
const STORAGE_KEY = 'stock_buy_visible_columns'
const defaultColumns = [
'material_name', 'category', 'material_type', 'spec_model', 'unit', 'material_name', 'category', 'material_type', 'spec_model', 'unit',
'inbound_date', 'serial_number', 'batch_number', 'status', 'inspection_status', 'inbound_date', 'serial_number', 'batch_number', 'status', 'inspection_status',
'unit_price', 'total_price', 'supplier_name', 'purchaser' 'unit_price', 'total_price', 'supplier_name', 'purchaser', 'qty_stock', 'qty_available'
]) ]
const getSavedColumns = () => {
try {
const saved = localStorage.getItem(STORAGE_KEY)
return saved ? JSON.parse(saved) : defaultColumns
} catch (e) {
return defaultColumns
}
}
const visibleColumnProps = ref(getSavedColumns())
watch(visibleColumnProps, (newVal) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(newVal))
}, { deep: true })
const form = reactive({ const form = reactive({
id: undefined, id: undefined,
@ -399,7 +441,7 @@ const validateUnique = (rule: any, value: string, callback: any) => {
return false return false
}) })
if (isDuplicate) { if (isDuplicate) {
callback(new Error('编号在当前页已存在')) callback(new Error('编号重复'))
} else { } else {
callback() callback()
} }
@ -407,18 +449,17 @@ const validateUnique = (rule: any, value: string, callback: any) => {
const validateIdentity = (rule: any, value: any, callback: any) => { const validateIdentity = (rule: any, value: any, callback: any) => {
if (entryMode.value === 'serial' && !form.serial_number && rule.field === 'serial_number') { if (entryMode.value === 'serial' && !form.serial_number && rule.field === 'serial_number') {
callback(new Error('序列号模式下必填')) callback(new Error('SN必填'))
} else if (entryMode.value === 'batch' && !form.batch_number && rule.field === 'batch_number') { } else if (entryMode.value === 'batch' && !form.batch_number && rule.field === 'batch_number') {
callback(new Error('批号模式下必填')) callback(new Error('批号必填'))
} else { } else {
callback() callback()
} }
} }
const rules = { const rules = {
base_id: [{ required: true, message: '请搜索并选择基础物料', trigger: 'change' }], base_id: [{ required: true, message: '请选择物料', trigger: 'change' }],
in_quantity: [{ required: true, message: '请输入入库数量', trigger: 'blur' }], in_quantity: [{ required: true, message: '请输入数量', trigger: 'blur' }],
in_date: [{ required: true, message: '请选择日期', trigger: 'change' }],
serial_number: [{ validator: validateIdentity, trigger: 'blur' }, { validator: validateUnique, trigger: 'blur' }], serial_number: [{ validator: validateIdentity, trigger: 'blur' }, { validator: validateUnique, trigger: 'blur' }],
batch_number: [{ validator: validateIdentity, trigger: 'blur' }, { validator: validateUnique, trigger: 'blur' }] batch_number: [{ validator: validateIdentity, trigger: 'blur' }, { validator: validateUnique, trigger: 'blur' }]
} }
@ -426,7 +467,6 @@ const rules = {
// ------------------------------------ // ------------------------------------
// 核心逻辑函数 // 核心逻辑函数
// ------------------------------------ // ------------------------------------
const checkHistoryAndSetMode = async (baseId: number) => { const checkHistoryAndSetMode = async (baseId: number) => {
try { try {
const res: any = await getBuyList({ page: 1, pageSize: 1000 }) const res: any = await getBuyList({ page: 1, pageSize: 1000 })
@ -436,7 +476,6 @@ const checkHistoryAndSetMode = async (baseId: number) => {
if (historyItems.length > 0) { if (historyItems.length > 0) {
modeLocked.value = true modeLocked.value = true
const latest = historyItems.sort((a: any, b: any) => b.id - a.id)[0] const latest = historyItems.sort((a: any, b: any) => b.id - a.id)[0]
if (latest.serial_number) { if (latest.serial_number) {
entryMode.value = 'serial' entryMode.value = 'serial'
form.serial_number = '' form.serial_number = ''
@ -512,7 +551,6 @@ watch(() => [form.in_quantity, form.unit_price], () => {
form.total_price = Number((form.in_quantity * form.unit_price).toFixed(4)) form.total_price = Number((form.in_quantity * form.unit_price).toFixed(4))
}) })
// CRUD 操作
const fetchData = async () => { const fetchData = async () => {
loading.value = true loading.value = true
try { try {
@ -525,20 +563,16 @@ const fetchData = async () => {
const handleCreate = () => { const handleCreate = () => {
dialogStatus.value = 'create' dialogStatus.value = 'create'
resetForm() resetForm()
// 新增时自动设置当前时间,不可修改
form.in_date = dayjs().format('YYYY-MM-DD') form.in_date = dayjs().format('YYYY-MM-DD')
modeLocked.value = false modeLocked.value = false
entryMode.value = 'batch' entryMode.value = 'batch'
form.batch_number = '' form.batch_number = ''
visible.value = true visible.value = true
} }
const handleUpdate = (row: any) => { const handleUpdate = (row: any) => {
dialogStatus.value = 'update' dialogStatus.value = 'update'
resetForm() resetForm()
modeLocked.value = true modeLocked.value = true
form.id = row.id form.id = row.id
@ -548,10 +582,8 @@ const handleUpdate = (row: any) => {
form.category = row.category form.category = row.category
form.unit = row.unit form.unit = row.unit
form.material_type = row.material_type form.material_type = row.material_type
form.sku = row.sku form.sku = row.sku
form.barcode = row.barcode form.barcode = row.barcode
// 编辑时回显原有时间disabled 属性确保不可修改
form.in_date = row.inbound_date form.in_date = row.inbound_date
form.warehouse_location = row.warehouse_loc form.warehouse_location = row.warehouse_loc
@ -585,7 +617,6 @@ const handleUpdate = (row: any) => {
visible.value = true visible.value = true
} }
// 核心修复:修复编辑不生效的问题
const submitForm = async () => { const submitForm = async () => {
if (!formRef.value) return if (!formRef.value) return
await formRef.value.validate(async (valid: boolean) => { await formRef.value.validate(async (valid: boolean) => {
@ -596,23 +627,16 @@ const submitForm = async () => {
await createBuyInbound(form) await createBuyInbound(form)
ElMessage.success('入库成功') ElMessage.success('入库成功')
} else { } else {
// 确保 ID 和数值类型正确传递
const payload = { const payload = {
...form, ...form,
in_quantity: Number(form.in_quantity), in_quantity: Number(form.in_quantity),
unit_price: Number(form.unit_price) unit_price: Number(form.unit_price)
} }
// 1. 等待更新完成
await updateBuyInbound(form.id!, payload) await updateBuyInbound(form.id!, payload)
ElMessage.success('更新成功') ElMessage.success('更新成功')
} }
// 2. 更新成功后,先刷新数据
await fetchData() await fetchData()
// 3. 数据刷新完毕,再关闭弹窗
visible.value = false visible.value = false
} catch (e: any) { } catch (e: any) {
ElMessage.error(e.msg || '操作失败') ElMessage.error(e.msg || '操作失败')
} finally { } finally {
@ -662,37 +686,122 @@ onMounted(() => fetchData())
</script> </script>
<style scoped> <style scoped>
.buy-module { background: #fff; padding: 15px; } /* 全局布局 */
.buy-module {
background: #f5f7fa; /* 整体背景微灰,突出内容 */
padding: 20px;
min-height: 100vh;
}
/* 头部布局优化:左搜右操作 */ /* 顶部工具栏 */
.header-tools { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .header-tools {
.left-tools { flex: 0 0 300px; /* 左侧固定宽度或自适应 */ } display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
background: #fff;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05);
}
.left-tools { flex: 0 0 350px; }
.right-tools { display: flex; gap: 10px; align-items: center; } .right-tools { display: flex; gap: 10px; align-items: center; }
.action-btn { font-weight: 500; }
.search-input { width: 100%; } /* 表格美化 */
.modern-table {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05);
}
:deep(.table-header-gray th) {
background-color: #f8f9fb !important;
color: #606266;
font-weight: 600;
height: 50px;
}
.tag-sn { color: #409EFF; font-weight: bold; font-family: monospace; }
.tag-bn { color: #67C23A; font-weight: bold; font-family: monospace; }
.money-text { font-family: 'Consolas', monospace; color: #303133; }
.stock-num { font-weight: bold; color: #333; font-size: 15px; }
.avail-num { font-weight: bold; color: #67C23A; font-size: 15px; }
.sum-tag { margin-left: 4px; transform: scale(0.9); }
.custom-big-table { font-size: 14px; } /* 弹窗与表单美化 */
.section-block { margin-bottom: 20px; border: 1px solid #ebeef5; padding: 15px; border-radius: 4px; } .stylish-form .form-card {
.read-only-area { background-color: #f5f7fa; padding: 10px; border-radius: 4px; } background: #fff;
border-radius: 8px;
border: 1px solid #e4e7ed;
margin-bottom: 20px;
overflow: hidden;
}
.sn-bn-row { .card-title {
background-color: #fffbf0; background: #fcfcfc;
padding: 12px 20px;
border-bottom: 1px solid #ebeef5;
font-weight: 600;
font-size: 15px;
color: #303133;
display: flex;
align-items: center;
}
.card-title .icon { margin-right: 8px; font-size: 18px; color: #409EFF; }
.card-title .sub-title { font-size: 12px; color: #909399; font-weight: normal; margin-left: 10px; }
.card-content { padding: 20px; }
/* 基础信息卡片 (蓝色调,示读) */
.basic-card { border-left: 4px solid #409EFF; }
.is-text-view :deep(.el-input__wrapper) {
box-shadow: none !important;
background-color: #f5f7fa;
border-bottom: 1px solid #dcdfe6;
border-radius: 0;
padding-left: 0;
}
.is-text-view :deep(.el-input__inner) { color: #606266; font-weight: 500; }
/* 入库信息卡片 */
.inbound-card { border-left: 4px solid #67C23A; }
/* 身份区域 (SN/BN) */
.identity-panel {
background: #fffbf0;
border: 1px dashed #e6a23c; border: 1px dashed #e6a23c;
padding: 15px 0 5px 0; border-radius: 6px;
border-radius: 4px; padding: 15px;
margin-top: 10px; margin-bottom: 20px;
} }
.text-sn { color: #409EFF; font-weight: bold; } .custom-radio-group { margin-bottom: 10px; }
.text-bn { color: #67C23A; font-weight: bold; } .locked-msg { font-size: 12px; color: #e6a23c; margin-left: 15px; }
.small-tip { font-size: 12px; color: #909399; margin-left: 10px; } .prefix-tag { font-weight: bold; font-size: 12px; padding: 0 5px; border-radius: 4px; }
.tip-col { padding-left: 20px; margin-bottom: 5px; margin-top: -10px;} .prefix-tag.bn { color: #67C23A; background: #f0f9eb; }
.locked-tip { .prefix-tag.sn { color: #409EFF; background: #ecf5ff; }
font-size: 12px;
color: #E6A23C; /* 分割线 */
margin-top: 5px; .divider-text {
background-color: #fdf6ec; display: flex;
padding: 5px 10px; align-items: center;
border-radius: 4px; text-align: center;
display: inline-block; margin: 30px 0 20px;
color: #909399;
font-size: 14px;
font-weight: 500;
} }
.divider-text::before, .divider-text::after {
content: '';
flex: 1;
border-bottom: 1px solid #ebeef5;
}
.divider-text::before { margin-right: 15px; }
.divider-text::after { margin-left: 15px; }
/* 底部按钮 */
.dialog-footer { display: flex; justify-content: flex-end; gap: 15px; margin-top: 20px; }
.info-alert { font-size: 12px; color: #909399; margin-top: 10px; display: flex; align-items: center; gap: 5px; }
.option-item { display: flex; justify-content: space-between; width: 100%; }
.opt-name { font-weight: bold; }
.opt-spec { color: #8492a6; font-size: 13px; }
.total-price-input :deep(.el-input__inner) { color: #F56C6C; font-weight: bold; }
</style> </style>