@@ -0,0 +1,223 @@
< template >
< el-card >
< el-tabs v-model = "tab" >
< ! - - = = = = = = = = = = = = 成本估算 = = = = = = = = = = = = - - >
< el-tab-pane label = "成本估算" name = "estimate" >
< el-row :gutter = "14" >
< el-col :span = "7" >
< el-card shadow = "never" header = "系列成本模板" >
< div class = "series-list" v-loading = "loading" >
< div v-for = "s in series" :key="s.id" class="series-item"
: class = "{ active: current?.id === s.id }" @click ="pick(s)" >
< div class = "series-name" > { { s . name } } < / div >
< div class = "series-meta" > 系列 { { s . series } } · 壁厚 { { s . wall _thick } } mm
< el-tag v-if = "s.source_file" size="small" type="info" effect="plain" > Excel < / el -tag >
< / div >
< / div >
< el-empty v-if = "!series.length && !loading" description="暂无系列模板,请先导入核算 Excel" :image-size="70" / >
< / div >
< el-upload :show-file-list = "false" :before-upload = "onImport" accept = ".xlsx" style = "margin-top:12px" >
< el-button type = "primary" plain style = "width:100%" :loading = "importing" > 导入成本核算 Excel < / el-button >
< / el-upload >
< div class = "tip" > 支持鲁铭 12 表格式工作簿 : 价格库 8 表自动入产品价格本 , 铝成本页生成系列模板 。 < / div >
< / el-card >
< / el-col >
< el-col :span = "17" >
< el-card shadow = "never" header = "估算参数" >
< el-form inline >
< el-form-item label = "窗宽 mm" > < el-input-number v-model = "form.width" :min="300" :max="6000" :step="100" / > < / el-form-item >
< el-form-item label = "窗高 mm" > < el-input-number v-model = "form.height" :min="300" :max="3000" :step="100" / > < / el-form-item >
< el-form-item label = "樘数" > < el-input-number v-model = "form.count" :min="1" :max="999" / > < / el-form-item >
< el-form-item label = "开启扇数" > < el-input-number v-model = "form.sash_count" :min="0" :max="20" / > < / el-form-item >
< el-form-item > < el-button type = "primary" :disabled = "!current" @click ="run" > 估算成本 < / el -button > < / el-form-item >
< / el-form >
< / el-card >
< template v-if = "result" >
< el -row :gutter = "10" class = "stat-row" >
< el-col :span = "6" > < el-card shadow = "never" > < el-statistic title = "总成本(元)" :value = "result.total" :precision = "2" / > < / el-card > < / el-col >
< el-col :span = "6" > < el-card shadow = "never" > < el-statistic title = "每平方成本(元/㎡)" :value = "result.per_sqm" :precision = "2" / > < / el-card > < / el-col >
< el-col :span = "6" > < el-card shadow = "never" > < el-statistic title = "总面积(㎡)" :value = "result.area" :precision = "2" / > < / el-card > < / el-col >
< el-col :span = "6" > < el-card shadow = "never" > < el-statistic title = "建议售价(50%毛利)" :value = "result.margin_ladder['50%']" :precision = "2" / > < / el-card > < / el-col >
< / el-row >
< el-card shadow = "never" header = "用料与成本明细" class = "detail-card" >
< el-collapse v-model = "opened" >
< el -collapse -item v-for = "sec in sections" :key="sec.key" :name="sec.key" >
< template # title >
< span class = "sec-title" > { { sec . label } } < / span >
< el-tag size = "small" : type = "sec.key === 'total' ? 'danger' : 'info'" effect = "plain" >
{ { sec . key === 'total' ? result . total : result [ sec . key + '_cost' ] } } 元
< / el-tag >
< / template >
< el-table :data = "result[sec.key]" size = "small" v-if = "result[sec.key]?.length" >
< el -table -column v-for = "col in sec.cols" :key="col.k" :prop="col.k" :label="col.l" :width="col.w" / >
< / el-table >
< el-empty v-else description = "该系列无此项" :image-size = "48" / >
< / el-collapse-item >
< / el-collapse >
< el-table :data = "ladderRows" size = "small" class = "ladder" >
< el-table-column v-for = "r in ladderRows" :key="r.k" :prop="r.k" :label="r.l" align="center" / >
< / el-table >
< / el-card >
< / template >
< el-empty v-else description = "选择系列后点击「估算成本」" / >
< / el-col >
< / el-row >
< / el-tab-pane >
<!-- === === === === 价格库 === === === === -- >
< el-tab-pane label = "价格库" name = "lib" >
< div class = "toolbar" >
< el-input v-model = "lib.keyword" placeholder="名称/规格" clearable style="width:180px" @keyup.enter="loadLib" / >
< el-select v-model = "lib.material" placeholder="全部类型" clearable style="width:120px" @change="loadLib" >
< el -option value = "profile" label = "型材" / > < el-option value = "glass" label = "玻璃" / >
< el-option value = "hardware" label = "五金" / > < el-option value = "addon" label = "配件" / >
< / el-select >
< el-button type = "primary" @click ="loadLib" > 查询 < / el -button >
< span class = "tip" > 共 { { lib . total } } 条 · 来自核算 Excel 价格库 8 表 ( 华健型材模具 / 丝吉利娅 / 索宾柯五金 / 隔热条 / 胶条 / 赛遨辅材 / 螺丝 / 其他 ) < / span >
< / div >
< el-table :data = "lib.rows" v-loading = "lib.loading" stripe size="small" >
< el -table -column prop = "name" label = "名称" min -width = " 180 " show -overflow -tooltip / >
< el-table-column label = "类型" width = "75" >
< template # default = "{ row }" > { { libLabel ( row . material ) } } < / template >
< / el-table-column >
< el-table-column prop = "spec" label = "规格" width = "110" / >
< el-table-column prop = "unit" label = "单位" width = "70" / >
< el-table-column prop = "price" label = "单价" width = "90" / >
< el-table-column prop = "note" label = "备注" min -width = " 150 " show -overflow -tooltip / >
< / el-table >
< el-pagination style = "margin-top:12px; justify-content:flex-end" background layout = "total, prev, pager, next"
:total = "lib.total" :page-size = "lib.page_size" v -model :current-page = "lib.page" @ current -change = " loadLib " / >
< / el-tab-pane >
< / el-tabs >
< / el-card >
< / template >
< script setup lang = "ts" >
import { computed , onMounted , reactive , ref } from 'vue'
import { ElMessage } from 'element-plus'
import { api , apiUpload } from '../api'
const tab = ref ( 'estimate' )
const series = ref < any [ ] > ( [ ] )
const current = ref < any > ( null )
const loading = ref ( false )
const importing = ref ( false )
const result = ref < any > ( null )
const opened = ref ( [ 'profile' , 'glass' , 'hardware' ] )
const form = reactive ( { width : 1600 , height : 1600 , count : 1 , sash _count : 0 } )
const sections = [
{ key : 'profile' , label : '铝型材成本' , cols : [
{ k : 'code' , l : '型材编号' , w : 120 } , { k : 'name' , l : '名称' , w : 110 } ,
{ k : 'qty' , l : '使用数量(m)' , w : 100 } , { k : 'weight_per_m' , l : '米重(kg/m)' , w : 95 } ,
{ k : 'weight' , l : '总重量(kg)' , w : 95 } , { k : 'unit_price' , l : '单价(元/kg)' , w : 95 } ,
{ k : 'amount' , l : '价格' , w : 85 } ] } ,
{ key : 'addon' , label : '型材辅件成本' , cols : [
{ k : 'name' , l : '名称' , w : 130 } , { k : 'qty' , l : '使用数量(m)' , w : 100 } ,
{ k : 'weight' , l : '总重量(kg)' , w : 95 } , { k : 'unit_price' , l : '单价(元/kg)' , w : 95 } ,
{ k : 'amount' , l : '价格' , w : 85 } ] } ,
{ key : 'strip' , label : '隔热条成本' , cols : [
{ k : 'code' , l : '编号' , w : 120 } , { k : 'total' , l : '合计(米)' , w : 85 } ,
{ k : 'unit_price' , l : '单价(元/米)' , w : 100 } , { k : 'amount' , l : '价格' , w : 85 } ] } ,
{ key : 'seal' , label : '胶条成本' , cols : [
{ k : 'name' , l : '胶条名称' , w : 150 } , { k : 'qty' , l : '使用米数' , w : 90 } ,
{ k : 'unit_price' , l : '单价(元/m)' , w : 95 } , { k : 'amount' , l : '价格' , w : 85 } ] } ,
{ key : 'glass' , label : '玻璃成本' , cols : [
{ k : 'blocks' , l : '玻璃数量' , w : 90 } , { k : 'config' , l : '玻璃配置' , w : 190 } ,
{ k : 'area' , l : '玻璃面积(㎡)' , w : 110 } , { k : 'unit_price' , l : '单价(元/㎡)' , w : 95 } ,
{ k : 'amount' , l : '价格' , w : 85 } ] } ,
{ key : 'extra' , label : '辅材成本' , cols : [
{ k : 'name' , l : '名称' , w : 150 } , { k : 'qty' , l : '数量' , w : 80 } ,
{ k : 'unit_price' , l : '单价' , w : 85 } , { k : 'amount' , l : '价格' , w : 85 } ] } ,
{ key : 'hardware' , label : '五金成本' , cols : [
{ k : 'vendor' , l : '五金厂家' , w : 110 } , { k : 'name' , l : '名称' , w : 200 } ,
{ k : 'qty' , l : '数量' , w : 80 } , { k : 'unit_price' , l : '单价' , w : 85 } ,
{ k : 'amount' , l : '价格' , w : 85 } ] } ,
]
const ladderRows = computed ( ( ) => {
if ( ! result . value ) return [ ]
const ladder = result . value . margin _ladder || { }
const keys = Object . keys ( ladder )
const row1 : Record < string , any > = { k : '毛利' , l : '毛利阶梯' }
const row2 : Record < string , any > = { k : '售价' , l : '建议售价' }
keys . forEach ( k => { row1 [ k ] = k . replace ( '%' , '' ) ; row2 [ k ] = ladder [ k ] } )
return [ row1 , row2 ]
} )
async function load ( ) {
loading . value = true
try {
series . value = await api ( 'GET' , '/api/costbook/series' )
if ( series . value . length && ! current . value ) pick ( series . value [ 0 ] )
} finally { loading . value = false }
}
function pick ( s : any ) {
current . value = s
result . value = null
const win = s . window || s . data ? . window
if ( win ) {
form . width = win . w || 1600
form . height = win . h || 1600
// 开启扇数:按基准开启面积/玻璃块数推 1,无开启信息则 0
form . sash _count = win . sash _area ? 1 : 0
}
}
async function run ( ) {
if ( ! current . value ) return
result . value = await api ( 'POST' , '/api/costbook/estimate' , {
series _id : current . value . id , width : form . width , height : form . height ,
count : form . count , sash _count : form . sash _count ,
} )
}
async function onImport ( file : File ) {
importing . value = true
try {
const d = await apiUpload < any > ( '/api/costbook/import-excel' , file )
ElMessage . success ( d . message || '导入完成' )
current . value = null
await load ( )
await loadLib ( )
} finally { importing . value = false }
return false
}
const lib = reactive ( { rows : [ ] , total : 0 , page : 1 , page _size : 15 , keyword : '' , material : '' , loading : false } )
async function loadLib ( ) {
lib . loading = true
try {
const d = await api < any > ( 'GET' , '/api/products' , undefined , {
params : { page : lib . page , page _size : lib . page _size , keyword : lib . keyword , material : lib . material || undefined } ,
} )
lib . rows = d . items ; lib . total = d . total
} finally { lib . loading = false }
}
function libLabel ( m : string ) {
return ( { profile : '型材' , glass : '玻璃' , hardware : '五金' , addon : '配件' , subbar : '副梃' } as any ) [ m ] || m
}
onMounted ( ( ) => { load ( ) ; loadLib ( ) } )
< / script >
< style scoped >
. series - list { min - height : 120 px ; }
. series - item { padding : 10 px 12 px ; border : 1 px solid var ( -- el - border - color - light ) ; border - radius : 8 px ; margin - bottom : 8 px ; cursor : pointer ; transition : all .15 s ; }
. series - item : hover { border - color : var ( -- el - color - primary - light - 5 ) ; }
. series - item . active { border - color : var ( -- el - color - primary ) ; background : var ( -- el - color - primary - light - 9 ) ; }
. series - name { font - weight : 600 ; }
. series - meta { font - size : 12 px ; color : var ( -- el - text - color - secondary ) ; margin - top : 2 px ; }
. tip { font - size : 12 px ; color : var ( -- el - text - color - secondary ) ; margin - top : 8 px ; line - height : 1.5 ; }
. toolbar { display : flex ; gap : 8 px ; align - items : center ; margin - bottom : 12 px ; flex - wrap : wrap ; }
. stat - row { margin : 12 px 0 ; }
. detail - card { margin - top : 4 px ; }
. sec - title { font - weight : 600 ; margin - right : 10 px ; }
. ladder { margin - top : 14 px ; }
< / style >