Files
KCGL/inventory-web/src/views/stock/inbound/buy.vue

807 lines
30 KiB
Vue

<template>
<div class="buy-module">
<div class="header-tools">
<div class="left-tools">
<el-input
v-model="queryParams.keyword"
placeholder="🔍 搜索物料名称 / 规格 / 批号 / SN / 供应商..."
class="search-input"
clearable
@clear="fetchData"
@keyup.enter="fetchData"
>
<template #append>
<el-button :icon="Search" @click="fetchData" />
</template>
</el-input>
</div>
<div class="right-tools">
<el-button type="primary" :icon="Plus" @click="handleCreate" class="action-btn">采购入库登记</el-button>
<el-button :icon="Refresh" @click="fetchData" class="action-btn">刷新</el-button>
<el-popover placement="bottom-end" title="列配置" :width="500" trigger="click">
<template #reference>
<el-button :icon="Setting" class="action-btn">表头</el-button>
</template>
<el-checkbox-group v-model="visibleColumnProps" class="column-selector">
<div class="col-group-title">基础信息</div>
<el-row :gutter="10">
<el-col :span="12" v-for="c in baseColumns" :key="c.prop"><el-checkbox :label="c.prop">{{ c.label }}</el-checkbox></el-col>
</el-row>
<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-checkbox-group>
</el-popover>
</div>
</div>
<el-table
v-loading="loading"
:data="tableData"
border
stripe
style="width: 100%"
class="modern-table"
highlight-current-row
header-cell-class-name="table-header-gray"
>
<template v-for="col in allColumns" :key="col.prop">
<el-table-column
v-if="visibleColumnProps.includes(col.prop)"
:prop="col.prop"
:label="col.label"
:min-width="col.minWidth || '140'"
show-overflow-tooltip
>
<template #default="scope" v-if="['serial_number', 'batch_number'].includes(col.prop)">
<span v-if="scope.row[col.prop]" :class="col.prop === 'serial_number' ? 'tag-sn' : 'tag-bn'">
{{ scope.row[col.prop] }}
</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 #default="scope" v-else-if="col.prop === 'status'">
<el-tag :type="getStatusType(scope.row.status)" effect="light" round>
{{ scope.row.status }}
</el-tag>
</template>
<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" :underline="false">
<el-icon><Link /></el-icon> 查看
</el-link>
</template>
<template #default="scope" v-else-if="['unit_price', 'total_price'].includes(col.prop)">
<span class="money-text">{{ formatMoney(scope.row[col.prop], scope.row.currency) }}</span>
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="160" fixed="right" align="center">
<template #default="{ row }">
<el-button link type="primary" size="default" @click="handleUpdate(row)">编辑</el-button>
<el-popconfirm title="确定删除该条记录吗?不可恢复。" @confirm="handleDelete(row)" width="220">
<template #reference>
<el-button link type="danger" size="default">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pagination-bar"
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[15, 30, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="fetchData"
@current-change="fetchData"
/>
<el-dialog
v-model="visible"
:title="dialogStatus === 'create' ? '新增采购入库' : '编辑入库信息'"
width="1050px"
top="5vh"
destroy-on-close
:close-on-click-modal="false"
class="stylish-dialog"
>
<el-form :model="form" label-width="110px" ref="formRef" :rules="rules" size="large" class="stylish-form">
<div class="form-card basic-card">
<div class="card-title">
<el-icon class="icon"><Box /></el-icon>
<span>1. 基础信息</span>
<span class="sub-title" v-if="dialogStatus === 'create'"> (请先搜索选择物料)</span>
</div>
<div class="card-content">
<el-row :gutter="24" v-if="dialogStatus === 'create'" style="margin-bottom: 20px;">
<el-col :span="14">
<el-form-item label="物料搜索" prop="base_id" class="highlight-label">
<el-select
v-model="form.base_id"
filterable
remote
reserve-keyword
placeholder="输入名称 / 规格型号进行模糊搜索..."
:remote-method="handleSearchMaterial"
:loading="searchLoading"
style="width: 100%"
@change="onMaterialSelected"
size="large"
>
<el-option
v-for="item in materialOptions"
:key="item.id"
:label="item.name + ' [' + item.spec + ']'"
:value="item.id"
>
<div class="option-item">
<span class="opt-name">{{ item.name }}</span>
<span class="opt-spec">{{ item.spec }}</span>
</div>
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="10">
<div class="info-alert">
<el-icon><InfoFilled /></el-icon> 仅展示状态为启用的基础物料
</div>
</el-col>
</el-row>
<div class="read-only-grid">
<el-row :gutter="24">
<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-col :span="8"><el-form-item label="规格型号"><el-input v-model="form.spec_model" disabled class="is-text-view" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="单位"><el-input v-model="form.unit" disabled class="is-text-view" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="类别"><el-input v-model="form.category" disabled class="is-text-view" /></el-form-item></el-col>
<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>
</el-row>
</div>
</div>
</div>
<div class="form-card inbound-card">
<div class="card-title">
<el-icon class="icon"><House /></el-icon>
<span>2. 入库详情</span>
</div>
<div class="card-content">
<el-row :gutter="24">
<el-col :span="6"><el-form-item label="编码/SKU" prop="sku"><el-input v-model="form.sku" placeholder="选填" /></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 />
</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>
<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-form-item label="入库数量" prop="in_quantity">
<el-input-number v-model="form.in_quantity" :min="1" controls-position="right" style="width:100%" class="strong-input" />
</el-form-item>
</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-form-item label="到检状态" prop="inspection_status">
<el-select v-model="form.inspection_status" style="width:100%">
<el-option label="未检" value="未检"><span style="color:#909399"> 未检</span></el-option>
<el-option label="合格" value="合格"><span style="color:#67C23A">🟢 合格</span></el-option>
<el-option label="不合格" value="不合格"><span style="color:#F56C6C">🔴 不合格</span></el-option>
</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" placeholder="输入图片 URL" /></el-form-item></el-col>
</el-row>
<div class="divider-text">商务与采购信息</div>
<el-row :gutter="24">
<el-col :span="6"><el-form-item label="币种"><el-input v-model="form.currency" /></el-form-item></el-col>
<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="含税单价" 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-col :span="6">
<el-form-item label="总价">
<el-input-number v-model="form.total_price" :precision="2" disabled :controls="false" style="width:100%" class="total-price-input"/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<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>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false" size="large">取消</el-button>
<el-button type="primary" :loading="submitting" @click="submitForm" size="large" class="confirm-btn">
{{ dialogStatus === 'create' ? '确认入库' : '保存修改' }}
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, watch } from 'vue'
import { Plus, Setting, Refresh, Search, Lock, Box, House, InfoFilled, Link } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import dayjs from 'dayjs'
import {
getBuyList,
createBuyInbound,
updateBuyInbound,
deleteBuyInbound,
searchMaterialBase
} from '@/api/inbound/buy'
// ------------------------------------
// 状态与变量
// ------------------------------------
const loading = ref(false)
const submitting = ref(false)
const visible = ref(false)
const searchLoading = ref(false)
const dialogStatus = ref<'create' | 'update'>('create')
const tableData = ref([])
const total = ref(0)
const formRef = ref()
const queryParams = reactive({ page: 1, pageSize: 15, keyword: '' })
const materialOptions = ref<any[]>([])
const entryMode = ref('batch')
const modeLocked = ref(false)
// 列定义
const baseColumns = [
{ prop: 'material_name', label: '名称' },
{ prop: 'category', label: '类别' },
{ prop: 'material_type', label: '类型' },
{ prop: 'spec_model', label: '规格型号' },
{ prop: 'unit', label: '单位' },
]
const stockColumns = [
{ prop: 'id', label: 'ID', minWidth: '60' },
{ prop: 'base_id', label: 'BaseID', minWidth: '80' },
{ prop: 'sku', label: 'SKU', minWidth: '120' },
{ prop: 'inbound_date', label: '入库日期', minWidth: '120' },
{ prop: 'barcode', label: '条码', minWidth: '120' },
{ prop: 'serial_number', label: '序列号', minWidth: '150' },
{ prop: 'batch_number', label: '批号', minWidth: '150' },
{ prop: 'status', label: '状态', minWidth: '100' },
{ prop: 'inspection_status', label: '到检', minWidth: '100' },
{ prop: 'qty_inbound', label: '入库量', minWidth: '100' },
{ prop: 'qty_stock', label: '库存数', minWidth: '100' },
{ prop: 'qty_available', label: '可用数', minWidth: '100' },
{ prop: 'warehouse_loc', label: '库位', minWidth: '120' },
{ prop: 'unit_price', label: '单价', minWidth: '120' },
{ prop: 'total_price', label: '总价', minWidth: '120' },
{ prop: 'currency', label: '币种', minWidth: '80' },
{ prop: 'exchange_rate', label: '汇率', minWidth: '80' },
{ prop: 'supplier_name', label: '供应商', minWidth: '150' },
{ prop: 'purchaser', label: '采购人', minWidth: '100' },
{ prop: 'purchaser_email', label: '邮箱', minWidth: '150' },
{ prop: 'source_link', label: '采购链接', minWidth: '100' },
{ prop: 'detail_link', label: '详情链接', minWidth: '100' },
{ prop: 'arrival_photo', label: '到货图', minWidth: '100' }
]
const allColumns = [...baseColumns, ...stockColumns]
// 表头持久化
const STORAGE_KEY = 'stock_buy_visible_columns'
const defaultColumns = [
'material_name', 'category', 'material_type', 'spec_model', 'unit',
'inbound_date', 'serial_number', 'batch_number', 'status', 'inspection_status',
'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({
id: undefined,
base_id: undefined as number | undefined,
material_name: '', spec_model: '', category: '', unit: '', material_type: '',
sku: '', barcode: '', in_date: '',
serial_number: '', batch_number: '',
status: '在库', inspection_status: '未检',
in_quantity: 1, stock_quantity: 1, available_quantity: 1,
warehouse_location: '',
unit_price: 0, total_price: 0, currency: 'CNY', exchange_rate: 1.00,
supplier_name: '', purchaser: '', purchaser_email: '',
source_link: '', detail_link: '', arrival_photo: ''
})
// ------------------------------------
// 逻辑校验规则
// ------------------------------------
const validateUnique = (rule: any, value: string, callback: any) => {
if (!value) return callback()
const isDuplicate = tableData.value.some((row: any) => {
if (dialogStatus.value === 'update' && row.id === form.id) return false
if (rule.field === 'serial_number' && row.serial_number === value) return true
if (rule.field === 'batch_number' && row.batch_number === value) return true
return false
})
if (isDuplicate) {
callback(new Error('编号重复'))
} else {
callback()
}
}
const validateIdentity = (rule: any, value: any, callback: any) => {
if (entryMode.value === 'serial' && !form.serial_number && rule.field === 'serial_number') {
callback(new Error('SN必填'))
} else if (entryMode.value === 'batch' && !form.batch_number && rule.field === 'batch_number') {
callback(new Error('批号必填'))
} else {
callback()
}
}
const rules = {
base_id: [{ required: true, message: '请选择物料', trigger: 'change' }],
in_quantity: [{ required: true, message: '请输入数量', trigger: 'blur' }],
serial_number: [{ validator: validateIdentity, trigger: 'blur' }, { validator: validateUnique, trigger: 'blur' }],
batch_number: [{ validator: validateIdentity, trigger: 'blur' }, { validator: validateUnique, trigger: 'blur' }]
}
// ------------------------------------
// 核心逻辑函数
// ------------------------------------
const checkHistoryAndSetMode = async (baseId: number) => {
try {
const res: any = await getBuyList({ page: 1, pageSize: 1000 })
const allItems = res.data.items || []
const historyItems = allItems.filter((item: any) => item.base_id === baseId)
if (historyItems.length > 0) {
modeLocked.value = true
const latest = historyItems.sort((a: any, b: any) => b.id - a.id)[0]
if (latest.serial_number) {
entryMode.value = 'serial'
form.serial_number = ''
form.batch_number = ''
} else {
entryMode.value = 'batch'
form.serial_number = ''
const lastBatch = latest.batch_number || '000000'
form.batch_number = incrementBatchNumber(lastBatch)
}
} else {
modeLocked.value = false
entryMode.value = 'batch'
form.batch_number = '000001'
}
if(formRef.value) {
formRef.value.clearValidate('serial_number')
formRef.value.clearValidate('batch_number')
}
} catch (e) {
console.error(e)
modeLocked.value = false
entryMode.value = 'batch'
form.batch_number = '000001'
}
}
const incrementBatchNumber = (batchStr: string) => {
if (!batchStr || !/^\d+$/.test(batchStr)) return '000001'
const num = parseInt(batchStr, 10)
return (num + 1).toString().padStart(6, '0')
}
const handleEntryModeChange = (val: string) => {
if (val === 'batch') {
form.serial_number = ''
form.batch_number = '000001'
if(formRef.value) formRef.value.clearValidate('serial_number')
} else {
form.batch_number = ''
if(formRef.value) formRef.value.clearValidate('batch_number')
}
}
const handleSearchMaterial = async (query: string) => {
if (query) {
searchLoading.value = true
try {
const res: any = await searchMaterialBase(query)
materialOptions.value = res.data || []
} finally {
searchLoading.value = false
}
} else {
materialOptions.value = []
}
}
const onMaterialSelected = (val: number) => {
const item = materialOptions.value.find(i => i.id === val)
if (item) {
form.material_name = item.name
form.spec_model = item.spec
form.category = item.category
form.unit = item.unit
form.material_type = item.type
checkHistoryAndSetMode(item.id)
}
}
watch(() => [form.in_quantity, form.unit_price], () => {
form.total_price = Number((form.in_quantity * form.unit_price).toFixed(4))
})
const fetchData = async () => {
loading.value = true
try {
const res: any = await getBuyList(queryParams)
tableData.value = res.data.items || []
total.value = res.data.total || 0
} finally { loading.value = false }
}
const handleCreate = () => {
dialogStatus.value = 'create'
resetForm()
form.in_date = dayjs().format('YYYY-MM-DD')
modeLocked.value = false
entryMode.value = 'batch'
form.batch_number = ''
visible.value = true
}
const handleUpdate = (row: any) => {
dialogStatus.value = 'update'
resetForm()
modeLocked.value = true
form.id = row.id
form.base_id = row.base_id
form.material_name = row.material_name
form.spec_model = row.spec_model
form.category = row.category
form.unit = row.unit
form.material_type = row.material_type
form.sku = row.sku
form.barcode = row.barcode
form.in_date = row.inbound_date
form.warehouse_location = row.warehouse_loc
if (row.serial_number) {
entryMode.value = 'serial'
form.serial_number = row.serial_number
form.batch_number = ''
} else {
entryMode.value = 'batch'
form.batch_number = row.batch_number
form.serial_number = ''
}
form.status = row.status
form.inspection_status = row.inspection_status
form.in_quantity = Number(row.qty_inbound) || 0
form.stock_quantity = Number(row.qty_stock) || 0
form.available_quantity = Number(row.qty_available) || 0
form.unit_price = Number(row.unit_price) || 0
form.total_price = Number(row.total_price) || 0
form.currency = row.currency
form.exchange_rate = Number(row.exchange_rate)
form.supplier_name = row.supplier_name
form.purchaser = row.purchaser
form.purchaser_email = row.purchaser_email
form.source_link = row.source_link
form.detail_link = row.detail_link
form.arrival_photo = row.arrival_photo
visible.value = true
}
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid: boolean) => {
if (valid) {
submitting.value = true
try {
if (dialogStatus.value === 'create') {
await createBuyInbound(form)
ElMessage.success('入库成功')
} else {
const payload = {
...form,
in_quantity: Number(form.in_quantity),
unit_price: Number(form.unit_price)
}
await updateBuyInbound(form.id!, payload)
ElMessage.success('更新成功')
}
await fetchData()
visible.value = false
} catch (e: any) {
ElMessage.error(e.msg || '操作失败')
} finally {
submitting.value = false
}
}
})
}
const handleDelete = async (row: any) => {
try {
await deleteBuyInbound(row.id)
ElMessage.success('删除成功')
fetchData()
} catch (e) {
ElMessage.error('删除失败')
}
}
const resetForm = () => {
materialOptions.value = []
Object.assign(form, {
id: undefined, base_id: undefined,
material_name: '', spec_model: '', category: '', unit: '', material_type: '',
sku: '', barcode: '', in_date: '',
serial_number: '', batch_number: '',
status: '在库', inspection_status: '未检',
in_quantity: 1, stock_quantity: 1, available_quantity: 1,
warehouse_location: '',
unit_price: 0, total_price: 0, currency: 'CNY', exchange_rate: 1.00,
supplier_name: '', purchaser: '', purchaser_email: '',
source_link: '', detail_link: '', arrival_photo: ''
})
}
const getStatusType = (status: string) => {
const map: any = { '在库': 'success', '出库': 'info', '损耗': 'danger' }
return map[status] || 'warning'
}
const formatMoney = (val: any, currency = '¥') => {
const num = Number(val)
return isNaN(num) ? '-' : `${currency} ${num.toFixed(2)}`
}
onMounted(() => fetchData())
</script>
<style scoped>
/* 全局布局 */
.buy-module {
background: #f5f7fa; /* 整体背景微灰,突出内容 */
padding: 20px;
min-height: 100vh;
}
/* 顶部工具栏 */
.header-tools {
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; }
.action-btn { font-weight: 500; }
/* 表格美化 */
.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); }
/* 弹窗与表单美化 */
.stylish-form .form-card {
background: #fff;
border-radius: 8px;
border: 1px solid #e4e7ed;
margin-bottom: 20px;
overflow: hidden;
}
.card-title {
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-radius: 6px;
padding: 15px;
margin-bottom: 20px;
}
.custom-radio-group { margin-bottom: 10px; }
.locked-msg { font-size: 12px; color: #e6a23c; margin-left: 15px; }
.prefix-tag { font-weight: bold; font-size: 12px; padding: 0 5px; border-radius: 4px; }
.prefix-tag.bn { color: #67C23A; background: #f0f9eb; }
.prefix-tag.sn { color: #409EFF; background: #ecf5ff; }
/* 分割线 */
.divider-text {
display: flex;
align-items: center;
text-align: center;
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>