@@ -0,0 +1,736 @@
<!DOCTYPE html>
< html lang = "zh-CN" >
< head >
< meta charset = "UTF-8" >
< title > 营养管理 - 自由餐品库 | 健康CQ原型< / title >
< style >
* { box-sizing : border-box ; margin : 0 ; padding : 0 ; }
body { font-family : - apple-system , BlinkMacSystemFont , 'PingFang SC' , 'Microsoft YaHei' , sans-serif ; background : #F5F7FA ; color : #333 ; min-width : 1280 px ; }
: root {
--primary : #1890FF ; --primary-light : #E6F7FF ; --success : #52C41A ; --warning : #FAAD14 ; --danger : #FF4D4F ;
--text-primary : #333 ; --text-secondary : #666 ; --text-placeholder : #999 ; --border : #E8E8E8 ; --background : #F5F7FA ;
--font-xs : 11 px ; --font-sm : 13 px ; --font-base : 15 px ; --font-lg : 17 px ; --font-xl : 20 px ; --font-xxl : 24 px ;
--radius-sm : 4 px ; --radius-md : 8 px ; --radius-lg : 12 px ;
--shadow-sm : 0 1 px 4 px rgba ( 0 , 0 , 0 , 0.08 ) ; --shadow-md : 0 4 px 12 px rgba ( 0 , 0 , 0 , 0.12 ) ;
}
. top-bar { display : flex ; align-items : center ; height : 56 px ; background : #001529 ; padding : 0 24 px ; position : fixed ; top : 0 ; left : 0 ; right : 0 ; z-index : 100 ; }
. top-bar__logo { display : flex ; align-items : center ; gap : 8 px ; color : #fff ; font-size : var ( - - font - lg ) ; font-weight : 600 ; white-space : nowrap ; width : 240 px ; flex-shrink : 0 ; }
. top-bar__logo svg { width : 28 px ; height : 28 px ; }
. top-bar__nav { display : flex ; align-items : center ; gap : 4 px ; flex : 1 ; }
. top-bar__nav a { color : rgba ( 255 , 255 , 255 , 0.65 ) ; text-decoration : none ; padding : 6 px 16 px ; border-radius : var ( - - radius - sm ) ; font-size : var ( - - font - sm ) ; transition : all .2 s ; }
. top-bar__nav a : hover { color : #fff ; background : rgba ( 255 , 255 , 255 , 0.08 ) ; }
. top-bar__nav a . active { color : #fff ; background : var ( - - primary ) ; }
. top-bar__right { display : flex ; align-items : center ; gap : 16 px ; color : rgba ( 255 , 255 , 255 , 0.65 ) ; font-size : var ( - - font - sm ) ; }
. top-bar__right . btn-back { color : rgba ( 255 , 255 , 255 , 0.65 ) ; cursor : pointer ; display : flex ; align-items : center ; gap : 4 px ; text-decoration : none ; }
. top-bar__right . btn-back : hover { color : #fff ; }
. avatar { width : 32 px ; height : 32 px ; border-radius : 50 % ; background : #1890FF ; display : flex ; align-items : center ; justify-content : center ; color : #fff ; font-size : var ( - - font - sm ) ; cursor : pointer ; }
. sidebar { position : fixed ; top : 56 px ; left : 0 ; bottom : 0 ; width : 240 px ; background : #001529 ; overflow-y : auto ; z-index : 90 ; padding-top : 8 px ; }
. sidebar-menu__title { color : rgba ( 255 , 255 , 255 , 0.35 ) ; font-size : var ( - - font - xs ) ; padding : 16 px 24 px 8 px ; text-transform : uppercase ; letter-spacing : .5 px ; }
. sidebar-item { display : flex ; align-items : center ; gap : 8 px ; padding : 10 px 24 px ; color : rgba ( 255 , 255 , 255 , 0.65 ) ; font-size : var ( - - font - sm ) ; cursor : pointer ; transition : all .15 s ; text-decoration : none ; }
. sidebar-item : hover { color : #fff ; background : rgba ( 255 , 255 , 255 , 0.06 ) ; }
. sidebar-item . active { color : #fff ; background : var ( - - primary ) ; }
. mi { display : inline-block ; width : 20 px ; text-align : center ; filter : grayscale ( 1 ) ; opacity : 0.65 ; margin-right : 2 px ; }
. sidebar-item . active . mi { opacity : 0.9 ; }
. sidebar-subgroup__header { display : flex ; align-items : center ; gap : 6 px ; padding : 10 px 24 px ; color : rgba ( 255 , 255 , 255 , 0.65 ) ; font-size : var ( - - font - sm ) ; cursor : pointer ; transition : all .15 s ; user-select : none ; }
. sidebar-subgroup__header : hover { color : rgba ( 255 , 255 , 255 , 0.85 ) ; background : rgba ( 255 , 255 , 255 , 0.04 ) ; }
. sidebar-subgroup__header . arrow { width : 10 px ; height : 10 px ; transition : transform .2 s ; flex-shrink : 0 ; }
. sidebar-subgroup__header . arrow path { stroke : rgba ( 255 , 255 , 255 , 0.45 ) ; }
. sidebar-subgroup . open > . sidebar-subgroup__header { color : rgba ( 255 , 255 , 255 , 0.85 ) ; }
. sidebar-subgroup . open > . sidebar-subgroup__header . arrow { transform : rotate ( 90 deg ) ; }
. sidebar-subgroup__body { display : none ; padding-left : 16 px ; }
. sidebar-subgroup . open > . sidebar-subgroup__body { display : block ; }
. sidebar-subgroup__body . sidebar-item { padding-left : 36 px ; font-size : 12 px ; }
. main { margin-left : 240 px ; margin-top : 56 px ; padding : 24 px ; min-height : calc ( 100 vh - 56 px ) ; }
. stats-row { display : grid ; grid-template-columns : repeat ( 4 , 1 fr ) ; gap : 16 px ; margin-bottom : 16 px ; }
. stat-card { background : #fff ; border-radius : var ( - - radius - md ) ; padding : 20 px 24 px ; box-shadow : var ( - - shadow - sm ) ; display : flex ; align-items : center ; gap : 16 px ; }
. stat-card__icon { width : 48 px ; height : 48 px ; border-radius : var ( - - radius - md ) ; display : flex ; align-items : center ; justify-content : center ; font-size : 24 px ; }
. stat-card__icon . blue { background : var ( - - primary - light ) ; }
. stat-card__icon . green { background : #F6FFED ; }
. stat-card__icon . orange { background : #FFF7E6 ; }
. stat-card__icon . red { background : #FFF1F0 ; }
. stat-card__info { display : flex ; flex-direction : column ; gap : 4 px ; }
. stat-card__value { font-size : var ( - - font - xl ) ; font-weight : 700 ; color : var ( - - text - primary ) ; }
. stat-card__label { font-size : var ( - - font - sm ) ; color : var ( - - text - secondary ) ; }
. btn { height : 36 px ; padding : 0 16 px ; border-radius : var ( - - radius - sm ) ; font-size : var ( - - font - sm ) ; cursor : pointer ; border : none ; transition : all .2 s ; display : inline-flex ; align-items : center ; gap : 6 px ; text-decoration : none ; }
. btn-primary { background : var ( - - primary ) ; color : #fff ; } . btn-primary : hover { background : #40a9ff ; }
. btn-default { background : #fff ; color : var ( - - text - primary ) ; border : 1 px solid var ( - - border ) ; } . btn-default : hover { color : var ( - - primary ) ; border-color : var ( - - primary ) ; }
. tag { display : inline-flex ; align-items : center ; padding : 2 px 8 px ; border-radius : 10 px ; font-size : var ( - - font - xs ) ; margin-right : 4 px ; }
. tag-blue { background : var ( - - primary - light ) ; color : var ( - - primary ) ; }
. tag-green { background : #F6FFED ; color : var ( - - success ) ; }
. tag-orange { background : #FFF7E6 ; color : var ( - - warning ) ; }
. tag-red { background : #FFF1F0 ; color : var ( - - danger ) ; }
. tag-gray { background : #F5F5F5 ; color : #999 ; }
. link { color : var ( - - primary ) ; cursor : pointer ; font-size : var ( - - font - sm ) ; text-decoration : none ; } . link : hover { color : #40a9ff ; }
/* 左右分栏布局 */
. master-detail { display : flex ; gap : 16 px ; height : calc ( 100 vh - 56 px - 24 px - 24 px - 100 px ) ; }
. panel-left { width : 380 px ; flex-shrink : 0 ; background : #fff ; border-radius : var ( - - radius - md ) ; box-shadow : var ( - - shadow - sm ) ; display : flex ; flex-direction : column ; overflow : hidden ; }
. panel-right { flex : 1 ; background : #fff ; border-radius : var ( - - radius - md ) ; box-shadow : var ( - - shadow - sm ) ; display : flex ; flex-direction : column ; overflow : hidden ; }
. panel-left__header { padding : 16 px 20 px 12 px ; border-bottom : 1 px solid var ( - - border ) ; }
. panel-left__title { font-size : var ( - - font - base ) ; font-weight : 600 ; margin-bottom : 12 px ; display : flex ; align-items : center ; justify-content : space-between ; }
. panel-left__search { display : flex ; gap : 8 px ; margin-bottom : 10 px ; }
. panel-left__search input { flex : 1 ; height : 32 px ; border : 1 px solid var ( - - border ) ; border-radius : var ( - - radius - sm ) ; padding : 0 10 px ; font-size : var ( - - font - sm ) ; outline : none ; }
. panel-left__search input : focus { border-color : var ( - - primary ) ; }
. panel-left__filter { display : flex ; gap : 8 px ; }
. panel-left__filter select { height : 32 px ; border : 1 px solid var ( - - border ) ; border-radius : var ( - - radius - sm ) ; padding : 0 24 px 0 10 px ; font-size : var ( - - font - xs ) ; outline : none ; background : #fff url ( "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23999'/%3E%3C/svg%3E" ) no-repeat right 8 px center ; appearance : none ; cursor : pointer ; flex : 1 ; }
. panel-left__actions { display : flex ; gap : 8 px ; padding : 10 px 20 px ; border-bottom : 1 px solid var ( - - border ) ; flex-wrap : wrap ; }
. panel-left__actions . btn { height : 30 px ; font-size : var ( - - font - xs ) ; padding : 0 10 px ; }
. food-list { flex : 1 ; overflow-y : auto ; padding : 4 px 0 ; }
. food-item { display : flex ; align-items : center ; gap : 12 px ; padding : 12 px 20 px ; cursor : pointer ; transition : background .15 s ; border-left : 3 px solid transparent ; }
. food-item : hover { background : #F8FBFF ; }
. food-item . active { background : var ( - - primary - light ) ; border-left-color : var ( - - primary ) ; }
. food-item__icon { width : 36 px ; height : 36 px ; border-radius : var ( - - radius - sm ) ; background : #F5F7FA ; display : flex ; align-items : center ; justify-content : center ; font-size : 18 px ; flex-shrink : 0 ; }
. food-item__info { flex : 1 ; min-width : 0 ; }
. food-item__name { font-size : var ( - - font - sm ) ; font-weight : 500 ; color : var ( - - text - primary ) ; white-space : nowrap ; overflow : hidden ; text-overflow : ellipsis ; }
. food-item__meta { font-size : var ( - - font - xs ) ; color : var ( - - text - placeholder ) ; margin-top : 2 px ; display : flex ; gap : 8 px ; }
. panel-left__footer { padding : 10 px 20 px ; border-top : 1 px solid var ( - - border ) ; display : flex ; align-items : center ; justify-content : space-between ; }
. panel-left__footer span { font-size : var ( - - font - xs ) ; color : var ( - - text - placeholder ) ; }
. page-btn { width : 26 px ; height : 26 px ; border : 1 px solid var ( - - border ) ; border-radius : var ( - - radius - sm ) ; background : #fff ; cursor : pointer ; font-size : var ( - - font - xs ) ; display : inline-flex ; align-items : center ; justify-content : center ; transition : all .2 s ; }
. page-btn : hover , . page-btn . active { background : var ( - - primary ) ; color : #fff ; border-color : var ( - - primary ) ; }
/* 右侧详情面板 */
. panel-right__header { padding : 16 px 24 px 0 ; border-bottom : 1 px solid var ( - - border ) ; }
. panel-right__food-title { display : flex ; align-items : center ; gap : 12 px ; margin-bottom : 14 px ; }
. panel-right__food-title h2 { font-size : var ( - - font - lg ) ; font-weight : 600 ; }
. panel-right__food-title . code { font-size : var ( - - font - sm ) ; color : var ( - - text - placeholder ) ; }
. detail-tabs { display : flex ; gap : 0 ; flex-wrap : wrap ; }
. detail-tab { padding : 10 px 16 px ; font-size : var ( - - font - sm ) ; cursor : pointer ; color : var ( - - text - secondary ) ; border-bottom : 2 px solid transparent ; margin-bottom : -1 px ; transition : all .2 s ; display : flex ; align-items : center ; gap : 5 px ; }
. detail-tab : hover { color : var ( - - primary ) ; }
. detail-tab . active { color : var ( - - primary ) ; border-bottom-color : var ( - - primary ) ; font-weight : 500 ; }
. panel-right__body { flex : 1 ; overflow-y : auto ; padding : 20 px 24 px ; }
. detail-panel { display : none ; }
. detail-panel . active { display : block ; }
/* 详情网格 */
. info-grid { display : grid ; grid-template-columns : repeat ( 4 , 1 fr ) ; gap : 12 px ; margin-bottom : 20 px ; }
. info-cell { background : #FAFBFC ; border-radius : var ( - - radius - sm ) ; padding : 14 px 16 px ; border : 1 px solid #F0F0F0 ; }
. info-cell__label { font-size : var ( - - font - xs ) ; color : var ( - - text - placeholder ) ; margin-bottom : 4 px ; }
. info-cell__value { font-size : var ( - - font - sm ) ; font-weight : 500 ; color : var ( - - text - primary ) ; }
. info-section { margin-bottom : 20 px ; }
. info-section__title { font-size : var ( - - font - sm ) ; font-weight : 600 ; color : var ( - - text - primary ) ; margin-bottom : 10 px ; padding-bottom : 6 px ; border-bottom : 1 px solid #F0F0F0 ; display : flex ; align-items : center ; justify-content : space-between ; }
. info-table { width : 100 % ; border-collapse : collapse ; font-size : var ( - - font - sm ) ; }
. info-table th { background : #FAFAFA ; padding : 10 px 12 px ; text-align : left ; color : var ( - - text - secondary ) ; font-weight : 500 ; border-bottom : 1 px solid var ( - - border ) ; }
. info-table td { padding : 10 px 12 px ; border-bottom : 1 px solid var ( - - border ) ; color : var ( - - text - primary ) ; }
. info-table tr : last-child td { border-bottom : none ; }
. info-table tr : hover { background : #F8FBFF ; }
. spec-detail-table th { padding : 6 px 12 px ; font-size : 12 px ; }
. spec-detail-table td { padding : 6 px 12 px ; font-size : 12 px ; }
/* 外售净菜包胶囊开关 */
. sale-toggle { display : inline-flex ; align-items : center ; gap : 6 px ; cursor : pointer ; user-select : none ; }
. sale-toggle__switch { width : 32 px ; height : 18 px ; background : #D9D9D9 ; border-radius : 9 px ; position : relative ; transition : background .2 s ; flex-shrink : 0 ; }
. sale-toggle__switch :: after { content : '' ; position : absolute ; left : 2 px ; top : 2 px ; width : 14 px ; height : 14 px ; background : #fff ; border-radius : 50 % ; transition : transform .2 s ; box-shadow : 0 1 px 2 px rgba ( 0 , 0 , 0 , 0.15 ) ; }
. sale-toggle . active . sale-toggle__switch { background : var ( - - success ) ; }
. sale-toggle . active . sale-toggle__switch :: after { transform : translateX ( 14 px ) ; }
. sale-toggle__label { font-size : 11 px ; color : var ( - - text - placeholder ) ; }
. sale-toggle . active . sale-toggle__label { color : var ( - - success ) ; font-weight : 500 ; }
/* 图片/视频网格 */
. media-grid { display : grid ; grid-template-columns : repeat ( 3 , 1 fr ) ; gap : 16 px ; }
. media-card { background : #FAFBFC ; border-radius : var ( - - radius - md ) ; border : 1 px solid #F0F0F0 ; overflow : hidden ; }
. media-card__thumb { height : 120 px ; background : #E8E8E8 ; display : flex ; align-items : center ; justify-content : center ; font-size : 36 px ; color : #999 ; }
. media-card__info { padding : 10 px 12 px ; }
. media-card__title { font-size : var ( - - font - sm ) ; font-weight : 500 ; margin-bottom : 4 px ; }
. media-card__meta { font-size : var ( - - font - xs ) ; color : var ( - - text - placeholder ) ; }
/* 弹窗 */
. dialog-mask { display : none ; position : fixed ; top : 0 ; left : 0 ; right : 0 ; bottom : 0 ; background : rgba ( 0 , 0 , 0 , 0.45 ) ; z-index : 200 ; align-items : center ; justify-content : center ; }
. dialog-mask . open { display : flex ; }
. dialog { background : #fff ; border-radius : var ( - - radius - lg ) ; width : 560 px ; max-width : 90 vw ; box-shadow : var ( - - shadow - md ) ; }
. dialog__header { display : flex ; align-items : center ; justify-content : space-between ; padding : 20 px 24 px 16 px ; border-bottom : 1 px solid var ( - - border ) ; }
. dialog__title { font-size : var ( - - font - lg ) ; font-weight : 600 ; color : var ( - - text - primary ) ; }
. dialog__close { width : 32 px ; height : 32 px ; border : none ; background : none ; cursor : pointer ; display : flex ; align-items : center ; justify-content : center ; border-radius : var ( - - radius - sm ) ; color : var ( - - text - placeholder ) ; }
. dialog__close : hover { background : #F5F5F5 ; color : var ( - - text - primary ) ; }
. dialog__body { padding : 24 px ; }
. dialog__footer { display : flex ; justify-content : flex-end ; gap : 8 px ; padding : 16 px 24 px ; border-top : 1 px solid var ( - - border ) ; }
. form-group { margin-bottom : 16 px ; }
. form-group label { display : block ; font-size : var ( - - font - sm ) ; color : var ( - - text - primary ) ; margin-bottom : 8 px ; font-weight : 500 ; }
. form-group label . required { color : var ( - - danger ) ; margin-left : 2 px ; }
. form-input , . form-select { width : 100 % ; height : 36 px ; border : 1 px solid var ( - - border ) ; border-radius : var ( - - radius - sm ) ; padding : 0 12 px ; font-size : var ( - - font - sm ) ; outline : none ; background : #fff ; }
. form-input : focus , . form-select : focus { border-color : var ( - - primary ) ; }
. form-select { appearance : none ; background : #fff url ( "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23999'/%3E%3C/svg%3E" ) no-repeat right 10 px center ; padding-right : 28 px ; }
. form-row { display : grid ; grid-template-columns : 1 fr 1 fr ; gap : 16 px ; }
/* 操作列固定右侧 */
thead th : last-child , tbody td . td-actions { position : sticky ; right : 0 ; background : #fff ; box-shadow : -4 px 0 8 px rgba ( 0 , 0 , 0 , 0.04 ) ; z-index : 2 ; white-space : nowrap ; }
thead th : last-child { background : #FAFAFA ; }
tbody tr : hover td . td-actions { background : #F8FBFF ; }
/* 分类管理表格 */
. cat-table { width : 100 % ; border-collapse : collapse ; font-size : var ( - - font - sm ) ; }
. cat-table th { background : #FAFAFA ; padding : 10 px 12 px ; text-align : left ; color : var ( - - text - secondary ) ; font-weight : 500 ; border-bottom : 1 px solid var ( - - border ) ; white-space : nowrap ; }
. cat-table td { padding : 8 px 12 px ; border-bottom : 1 px solid #F5F5F5 ; color : var ( - - text - primary ) ; }
. cat-table tbody tr : hover { background : #F8FBFF ; }
. cat-toggle { display : inline-flex ; align-items : center ; justify-content : center ; width : 20 px ; height : 20 px ; border : 1 px solid var ( - - border ) ; border-radius : 3 px ; cursor : pointer ; margin-right : 6 px ; font-size : 12 px ; line-height : 1 ; background : #fff ; color : var ( - - text - secondary ) ; flex-shrink : 0 ; vertical-align : middle ; }
. cat-toggle : hover { border-color : var ( - - primary ) ; color : var ( - - primary ) ; }
. cat-toggle . leaf { visibility : hidden ; }
. cat-table td . td-actions { white-space : nowrap ; }
. cat-table td . td-actions . link { margin : 0 4 px ; }
/* 状态胶囊开关 */
. status-toggle { display : inline-flex ; align-items : center ; gap : 6 px ; cursor : pointer ; user-select : none ; }
. status-toggle__switch { width : 36 px ; height : 20 px ; background : #D9D9D9 ; border-radius : 10 px ; position : relative ; transition : background .2 s ; flex-shrink : 0 ; }
. status-toggle__switch :: after { content : '' ; position : absolute ; left : 2 px ; top : 2 px ; width : 16 px ; height : 16 px ; background : #fff ; border-radius : 50 % ; transition : transform .2 s ; box-shadow : 0 1 px 3 px rgba ( 0 , 0 , 0 , 0.15 ) ; }
. status-toggle . active . status-toggle__switch { background : var ( - - success ) ; }
. status-toggle . active . status-toggle__switch :: after { transform : translateX ( 16 px ) ; }
. status-toggle__label { font-size : var ( - - font - xs ) ; color : var ( - - text - placeholder ) ; }
. status-toggle . active . status-toggle__label { color : var ( - - success ) ; }
< / style >
< / head >
< body >
<!-- 顶部导航栏 -->
< header class = "top-bar" >
< div class = "top-bar__logo" >
< svg viewBox = "0 0 28 28" fill = "none" > < rect width = "28" height = "28" rx = "6" fill = "#1890FF" / > < path d = "M7 14h14M14 7v14" stroke = "#fff" stroke-width = "2.5" stroke-linecap = "round" / > < / svg >
营养管理
< / div >
< nav class = "top-bar__nav" >
< a href = "trace-query.html" > 溯源总览< / a >
< a href = "arc-employee.html" > 档案< / a >
< a href = "farm-land.html" > 绿色种采< / a >
< a href = "sup-hygiene-check.html" > 卫生供应< / a >
< a href = "prod-qc-daily.html" > 健康生产< / a >
< a href = "din-dict-mgmt.html" class = "active" > 营养用餐< / a >
< a href = "ano-dirty.html" > 异常事件< / a >
< / nav >
< div class = "top-bar__right" >
< a href = "../../index.html" class = "btn-back" > ← 返回首页< / a >
< div class = "avatar" > 管< / div >
< span > 系统管理员 ▾< / span >
< / div >
< / header >
<!-- 侧边栏 -->
< aside class = "sidebar" >
< div class = "sidebar-menu__title" > 营养用餐< / div >
< a href = "din-canteen.html" class = "sidebar-item" > < span class = "mi" > 🏢< / span > 场所管理< / a >
< a href = "din-dict-mgmt.html" class = "sidebar-item" > < span class = "mi" > 📖< / span > 字典管理< / a >
< a href = "prod-qc-nutrition.html" class = "sidebar-item" > < span class = "mi" > 🔬< / span > 营养检验< / a >
< a href = "din-ingredient.html" class = "sidebar-item" > < span class = "mi" > 🧪< / span > 食物成分< / a >
< a href = "din-dish-all.html" class = "sidebar-item" > < span class = "mi" > 🍽️< / span > 餐品总库< / a >
< a href = "din-dish-free.html" class = "sidebar-item active" > < span class = "mi" > 📦< / span > 自由餐品库< / a >
< div class = "sidebar-subgroup" >
< div class = "sidebar-subgroup__header" onclick = "toggleSubgroup(this)" >
< svg class = "arrow" viewBox = "0 0 10 10" fill = "none" > < path d = "M3 1l4 4-4 4" stroke-width = "1.5" / > < / svg >
📐 算法标准
< / div >
< div class = "sidebar-subgroup__body" >
< a href = "din-algo-population.html" class = "sidebar-item" > < span class = "mi" > 👥< / span > 人群基础标准< / a >
< a href = "din-algo-vip.html" class = "sidebar-item" > < span class = "mi" > 👑< / span > VIP定制标准< / a >
< / div >
< / div >
< a href = "din-recipe-mgmt.html" class = "sidebar-item" > < span class = "mi" > 📋< / span > 食谱管理< / a >
< a href = "din-order-meal.html" class = "sidebar-item" > < span class = "mi" > 🍱< / span > 营养点餐< / a >
< a href = "din-user-mgmt.html" class = "sidebar-item" > < span class = "mi" > 👤< / span > 用户管理< / a >
< a href = "din-order-line.html" class = "sidebar-item" > < span class = "mi" > 🍳< / span > 餐线管理< / a >
< a href = "din-order-pickup.html" class = "sidebar-item" > < span class = "mi" > 🎫< / span > 取餐管理< / a >
< a href = "din-data-consume.html" class = "sidebar-item" > < span class = "mi" > 💰< / span > 消费管理< / a >
< a href = "din-data-nutri.html" class = "sidebar-item" > < span class = "mi" > 📊< / span > 营养管理< / a >
< a href = "din-data-benefit.html" class = "sidebar-item" > < span class = "mi" > 📈< / span > 餐材效益< / a >
< a href = "din-terminal.html" class = "sidebar-item" > < span class = "mi" > 🖥️< / span > 终端管理< / a >
< div class = "sidebar-subgroup" >
< div class = "sidebar-subgroup__header" onclick = "toggleSubgroup(this)" >
< svg class = "arrow" viewBox = "0 0 10 10" fill = "none" > < path d = "M3 1l4 4-4 4" stroke-width = "1.5" / > < / svg >
🤖 模型管理
< / div >
< div class = "sidebar-subgroup__body" >
< a href = "din-model-llm.html" class = "sidebar-item" > < span class = "mi" > 🧠< / span > 大模型管理< / a >
< a href = "din-model-small.html" class = "sidebar-item" > < span class = "mi" > ⚙️< / span > 小模型管理< / a >
< / div >
< / div >
< a href = "din-innovation.html" class = "sidebar-item" > < span class = "mi" > 💡< / span > 味养创新< / a >
< / aside >
<!-- 主内容 -->
< main class = "main" >
<!-- 统计卡片 -->
< div class = "stats-row" >
< div class = "stat-card" > < div class = "stat-card__icon blue" > 🍽️< / div > < div class = "stat-card__info" > < div class = "stat-card__value" > 486< / div > < div class = "stat-card__label" > 餐品总数< / div > < / div > < / div >
< div class = "stat-card" > < div class = "stat-card__icon green" > ✅< / div > < div class = "stat-card__info" > < div class = "stat-card__value" > 312< / div > < div class = "stat-card__label" > 上架中< / div > < / div > < / div >
< div class = "stat-card" > < div class = "stat-card__icon orange" > 📊< / div > < div class = "stat-card__info" > < div class = "stat-card__value" > 312道< / div > < div class = "stat-card__label" > 已录入指标< / div > < / div > < / div >
< div class = "stat-card" > < div class = "stat-card__icon green" > 🆕< / div > < div class = "stat-card__info" > < div class = "stat-card__value" > 18< / div > < div class = "stat-card__label" > 本月新增< / div > < / div > < / div >
< / div >
<!-- 左右分栏 -->
< div class = "master-detail" >
<!-- 左侧:餐品列表 -->
< div class = "panel-left" >
< div class = "panel-left__header" >
< div class = "panel-left__title" > 餐品列表< / div >
< div class = "panel-left__search" >
< input type = "text" placeholder = "搜索餐品名称/编号..." >
< button class = "btn btn-primary" style = "height:32px;padding:0 12px;font-size:var(--font-xs);" > 搜索< / button >
< / div >
< div class = "panel-left__filter" >
< select > < option > 全部分类< / option > < option > 主食< / option > < option > 荤菜< / option > < option > 素菜< / option > < option > 汤品< / option > < option > 小吃< / option > < / select >
< select > < option > 全部状态< / option > < option > 启用< / option > < option > 停用< / option > < / select >
< / div >
< / div >
< div class = "panel-left__actions" >
< a href = "din-dish-add.html" class = "btn btn-primary" > ➕ 新增餐品< / a >
< button class = "btn btn-default" > 📤 批量导入< / button >
< button class = "btn btn-default" onclick = "openDishCategoryDialog()" > 📂 餐品分类管理< / button >
< / div >
< div class = "food-list" id = "dishList" > < / div >
< div class = "panel-left__footer" >
< span > 共 486 条< / span >
< div style = "display:flex;gap:4px;" >
< button class = "page-btn" > ‹ < / button > < button class = "page-btn active" > 1< / button > < button class = "page-btn" > 2< / button > < button class = "page-btn" > 3< / button > < button class = "page-btn" > › < / button >
< / div >
< / div >
< / div >
<!-- 右侧:餐品详情 -->
< div class = "panel-right" >
< div class = "panel-right__header" >
< div class = "panel-right__food-title" >
< h2 id = "detailName" > 红烧肉< / h2 >
< span class = "code" id = "detailCode" > DISH001< / span >
< span class = "tag tag-green" id = "detailStatus" > 上架< / span >
< / div >
< div class = "detail-tabs" >
< div class = "detail-tab active" onclick = "switchDetailTab(0)" > 🍽️ 基本档案< / div >
< div class = "detail-tab" onclick = "switchDetailTab(1)" > 🧩 餐品构成< / div >
< div class = "detail-tab" onclick = "switchDetailTab(2)" > 🍳 烹饪工艺< / div >
< div class = "detail-tab" onclick = "switchDetailTab(3)" > 📊 营养指标< / div >
< div class = "detail-tab" onclick = "switchDetailTab(4)" > 👅 味道档案< / div >
< div class = "detail-tab" onclick = "switchDetailTab(5)" > 🫦 口感档案< / div >
< div class = "detail-tab" onclick = "switchDetailTab(6)" > 🎨 色泽档案< / div >
< / div >
< / div >
< div class = "panel-right__body" id = "detailBody" > < / div >
< / div >
< / div >
< / main >
< script >
// 餐品数据
const DISHES = [
{ id : 'DISH001' , name : '红烧肉' , icon : '🥩' , category : '荤菜' , tags : [ '高蛋白' ] , specs : [ { name : '标准份' , weight : 200 , price : 12 , cleanPrice : 9 , cleanSale : false , isDefault : true } , { name : '小份' , weight : 120 , price : 8 , cleanPrice : 6 , cleanSale : false } , { name : '大份' , weight : 300 , price : 18 , cleanPrice : 13 , cleanSale : false } ] , method : '炖' , meal : '午/晚' , allergen : '无' , status : '启用' , source : '手动添加' , weightBasePrice : '6.00' , weightVipPrice : '5.40' , crowd : [ { name : '成人' , level : '推荐' } , { name : '高血脂人群' , level : '慎食' } , { name : '高血压人群' , level : '适量' } ] , crowdNote : '高血脂人群控制摄入量' ,
composition : [ { name : '猪五花肉' , netType : '红烧块' , amount : 160 , pct : '80%' , state : '净菜' , type : '主材' , allergen : '无' , note : '—' } , { name : '生姜' , netType : '姜片' , amount : 5 , pct : '2.5%' , state : '净菜' , type : '辅材' , allergen : '无' , note : '—' } , { name : '生抽' , netType : '调味汁' , amount : 15 , pct : '7.5%' , state : '原料' , type : '调料' , allergen : '含麸质' , note : '酱油' } , { name : '冰糖' , netType : '糖浆' , amount : 10 , pct : '5%' , state : '原料' , type : '调料' , allergen : '无' , note : '上色提味' } , { name : '料酒' , netType : '料酒汁' , amount : 10 , pct : '5%' , state : '原料' , type : '调料' , allergen : '无' , note : '去腥' } ] ,
nutri : { calorie : 385 , protein : 18.2 , carb : 12.4 , fat : 28.6 , vitC : 0.8 , mineral : 42 , fiber : 0.3 , sodium : 486 , nutriGrade : '偏差' , note : '铁2.4mg、锌1.8mg' } ,
cook : { mainMethod : '炖' , subMethod : '煎' , heat : '中火→小火' , duration : 45 , oil : 15 , salt : 3 , temp : '100→80' , output : 200 , steps : 6 } ,
taste : { sour : 1 , sweet : 3 , bitter : 1 , spicy : 1 , salty : 4 , smell : 4.2 , flavorScore : 3.8 , flavorGrade : '良好' } ,
color : { mainColor : '红褐色' , uniformity : 4 , gloss : 4 , freshness : 4 , appearance : 4 , colorScore : 4.0 , colorGrade : '良好' } ,
texture : { hardness : 2 , sticky : 3 , crispy : 1 , smooth : 4 , elastic : 3 , textureScore : 3.6 , textureGrade : '良好' }
} ,
{ id : 'DISH002' , name : '清炒时蔬' , icon : '🥬' , category : '素菜' , tags : [ '低脂' , '素食' ] , specs : [ { name : '标准份' , weight : 150 , price : 6 , cleanPrice : 4.5 , cleanSale : false , isDefault : true } , { name : '小份' , weight : 100 , price : 4 , cleanPrice : 3 , cleanSale : false } ] , method : '炒' , meal : '早/午/晚' , allergen : '无' , status : '启用' , source : '系统内置' , weightBasePrice : '3.00' , weightVipPrice : '2.70' , crowd : [ { name : '全人群' , level : '推荐' } , { name : '糖尿病人群' , level : '推荐' } , { name : '肾结石人群' , level : '慎食' } ] , crowdNote : '肾结石人群慎食(草酸含量高)' ,
composition : [ { name : '时令青菜' , netType : '青菜段' , amount : 120 , pct : '80%' , state : '净菜' , type : '主材' , allergen : '无' , note : '—' } , { name : '蒜末' , netType : '蒜末' , amount : 5 , pct : '3.3%' , state : '净菜' , type : '调料' , allergen : '无' , note : '—' } , { name : '食用油' , netType : '净油' , amount : 8 , pct : '5.3%' , state : '原料' , type : '调料' , allergen : '无' , note : '—' } , { name : '食盐' , netType : '盐粒' , amount : 2 , pct : '1.3%' , state : '原料' , type : '调料' , allergen : '无' , note : '—' } ] ,
nutri : { calorie : 68 , protein : 2.1 , carb : 8.6 , fat : 3.2 , vitC : 28.5 , mineral : 86 , fiber : 2.4 , sodium : 186 , nutriGrade : '达标' , note : '铁1.2mg、钙45mg' } ,
cook : { mainMethod : '炒' , subMethod : '—' , heat : '大火' , duration : 5 , oil : 8 , salt : 2 , temp : '200' , output : 150 , steps : 4 } ,
taste : { sour : 1 , sweet : 2 , bitter : 1 , spicy : 1 , salty : 2 , smell : 3.5 , flavorScore : 3.2 , flavorGrade : '一般' } ,
color : { mainColor : '翠绿色' , uniformity : 5 , gloss : 4 , freshness : 5 , appearance : 4 , colorScore : 4.5 , colorGrade : '优秀' } ,
texture : { hardness : 3 , sticky : 1 , crispy : 4 , smooth : 3 , elastic : 2 , textureScore : 3.4 , textureGrade : '良好' }
} ,
{ id : 'DISH003' , name : '番茄鸡蛋汤' , icon : '🍅' , category : '汤品' , tags : [ '低卡' ] , specs : [ { name : '标准份' , weight : 300 , price : 5 , cleanPrice : 3.5 , cleanSale : false , isDefault : true } ] , method : '煮' , meal : '午/晚' , allergen : '含蛋类' , status : '启用' , source : '系统内置' , weightBasePrice : '1.67' , weightVipPrice : '1.50' , crowd : [ { name : '全人群' , level : '推荐' } , { name : '儿童' , level : '推荐' } , { name : '老年人' , level : '推荐' } ] , crowdNote : '' ,
composition : [ { name : '番茄' , netType : '番茄块' , amount : 150 , pct : '50%' , state : '净菜' , type : '主材' , allergen : '无' , note : '—' } , { name : '鸡蛋' , netType : '鸡蛋液' , amount : 60 , pct : '20%' , state : '原料' , type : '主材' , allergen : '含蛋类' , note : '—' } , { name : '食用油' , netType : '净油' , amount : 5 , pct : '1.7%' , state : '原料' , type : '调料' , allergen : '无' , note : '—' } , { name : '食盐' , netType : '盐粒' , amount : 2 , pct : '0.7%' , state : '原料' , type : '调料' , allergen : '无' , note : '—' } ] ,
nutri : { calorie : 45 , protein : 3.8 , carb : 4.2 , fat : 2.1 , vitC : 18.2 , mineral : 32 , fiber : 1.1 , sodium : 280 , nutriGrade : '达标' , note : '铁0.8mg、钾210mg' } ,
cook : { mainMethod : '煮' , subMethod : '炒' , heat : '中火' , duration : 12 , oil : 5 , salt : 2 , temp : '100' , output : 300 , steps : 5 } ,
taste : { sour : 3 , sweet : 2 , bitter : 1 , spicy : 1 , salty : 2 , smell : 3.8 , flavorScore : 3.5 , flavorGrade : '良好' } ,
color : { mainColor : '橙红色' , uniformity : 4 , gloss : 3 , freshness : 5 , appearance : 4 , colorScore : 4.0 , colorGrade : '良好' } ,
texture : { hardness : 1 , sticky : 2 , crispy : 1 , smooth : 5 , elastic : 2 , textureScore : 3.8 , textureGrade : '良好' }
} ,
{ id : 'DISH004' , name : '米饭' , icon : '🍚' , category : '主食' , tags : [ '主食' ] , specs : [ { name : '标准份' , weight : 200 , price : 3 , cleanPrice : 2 , cleanSale : false , isDefault : true } , { name : '小份' , weight : 100 , price : 2 , cleanPrice : 1.5 , cleanSale : false } , { name : '大份' , weight : 300 , price : 4 , cleanPrice : 3 , cleanSale : false } ] , method : '蒸' , meal : '早/午/晚' , allergen : '无' , status : '启用' , source : '系统内置' , weightBasePrice : '1.50' , weightVipPrice : '1.35' , crowd : [ { name : '全人群' , level : '推荐' } , { name : '糖尿病人群' , level : '适量' } ] , crowdNote : '糖尿病人群注意控制量' ,
composition : [ { name : '大米' , netType : '熟米饭' , amount : 100 , pct : '100%' , state : '原料' , type : '主材' , allergen : '无' , note : '—' } ] ,
nutri : { calorie : 232 , protein : 4.8 , carb : 51.2 , fat : 0.6 , vitC : 0 , mineral : 12 , fiber : 0.4 , sodium : 3 , nutriGrade : '达标' , note : '—' } ,
cook : { mainMethod : '蒸' , subMethod : '—' , heat : '大火→中火' , duration : 30 , oil : 0 , salt : 0 , temp : '100' , output : 200 , steps : 2 } ,
taste : { sour : 1 , sweet : 2 , bitter : 1 , spicy : 1 , salty : 1 , smell : 3.0 , flavorScore : 2.8 , flavorGrade : '一般' } ,
color : { mainColor : '乳白色' , uniformity : 5 , gloss : 4 , freshness : 4 , appearance : 4 , colorScore : 4.2 , colorGrade : '良好' } ,
texture : { hardness : 2 , sticky : 3 , crispy : 1 , smooth : 3 , elastic : 2 , textureScore : 3.2 , textureGrade : '一般' }
} ,
{ id : 'DISH005' , name : '糖醋里脊' , icon : '🍖' , category : '荤菜' , tags : [ '高糖' ] , specs : [ { name : '标准份' , weight : 180 , price : 14 , cleanPrice : 10 , cleanSale : false , isDefault : true } , { name : '大份' , weight : 280 , price : 20 , cleanPrice : 14 , cleanSale : false } ] , method : '炸' , meal : '午' , allergen : '无' , status : '停用' , source : '味养创新' , weightBasePrice : '7.78' , weightVipPrice : '7.00' , crowd : [ { name : '成人' , level : '推荐' } , { name : '糖尿病人群' , level : '慎食' } , { name : '高血脂人群' , level : '适量' } ] , crowdNote : '糖尿病人群慎食,含糖量高' ,
composition : [ { name : '猪里脊肉' , netType : '里脊丝' , amount : 120 , pct : '66.7%' , state : '净菜' , type : '主材' , allergen : '无' , note : '—' } , { name : '淀粉' , netType : '淀粉糊' , amount : 20 , pct : '11.1%' , state : '原料' , type : '辅材' , allergen : '无' , note : '挂糊' } , { name : '白糖' , netType : '糖浆' , amount : 25 , pct : '13.9%' , state : '原料' , type : '调料' , allergen : '无' , note : '糖醋汁' } , { name : '食醋' , netType : '调味汁' , amount : 15 , pct : '8.3%' , state : '原料' , type : '调料' , allergen : '无' , note : '糖醋汁' } ] ,
nutri : { calorie : 420 , protein : 22.4 , carb : 38.2 , fat : 18.6 , vitC : 1.2 , mineral : 28 , fiber : 0.6 , sodium : 620 , nutriGrade : '未达标' , note : '铁1.8mg' } ,
cook : { mainMethod : '炸' , subMethod : '炒' , heat : '大火' , duration : 15 , oil : 50 , salt : 1 , temp : '180' , output : 180 , steps : 7 } ,
taste : { sour : 4 , sweet : 4 , bitter : 1 , spicy : 1 , salty : 3 , smell : 4.0 , flavorScore : 4.2 , flavorGrade : '良好' } ,
color : { mainColor : '金黄色' , uniformity : 4 , gloss : 5 , freshness : 4 , appearance : 5 , colorScore : 4.5 , colorGrade : '优秀' } ,
texture : { hardness : 3 , sticky : 1 , crispy : 5 , smooth : 4 , elastic : 3 , textureScore : 4.0 , textureGrade : '良好' }
}
] ;
let currentDish = 0 ;
let currentTab = 0 ;
function renderDishList ( ) {
document . getElementById ( 'dishList' ) . innerHTML = DISHES . map ( ( d , i ) => `
<div class="food-item ${ i === currentDish ? ' active' : '' } " onclick="selectDish( ${ i } )">
<div class="food-item__icon"> ${ d . icon } </div>
<div class="food-item__info">
<div class="food-item__name"> ${ d . name } </div>
<div class="food-item__meta"><span> ${ d . id } </span><span> ${ d . category } </span><span class="tag tag- ${ d . status === '启用' ? 'green' : 'gray' } " style="margin:0;padding:1px 6px;"> ${ d . status } </span></div>
</div>
</div> ` ) . join ( '' ) ;
}
function selectDish ( idx ) { currentDish = idx ; renderDishList ( ) ; renderDetail ( ) ; }
function levelTag ( level ) {
const map = { '推荐' : 'tag-green' , '适量' : 'tag-orange' , '慎食' : 'tag-red' } ;
return '<span class="tag ' + ( map [ level ] || 'tag-gray' ) + '">' + level + '</span>' ;
}
function switchDetailTab ( idx ) { currentTab = idx ; document . querySelectorAll ( '.detail-tab' ) . forEach ( ( t , i ) => t . classList . toggle ( 'active' , i === idx ) ) ; renderDetail ( ) ; }
function renderDetail ( ) {
const d = DISHES [ currentDish ] ;
document . getElementById ( 'detailName' ) . textContent = d . name ;
document . getElementById ( 'detailCode' ) . textContent = d . id ;
const s = document . getElementById ( 'detailStatus' ) ;
s . textContent = d . status ; s . className = 'tag tag-' + ( d . status === '启用' ? 'green' : 'gray' ) ;
const body = document . getElementById ( 'detailBody' ) ;
const renderers = [ renderBasic , renderComposition , renderCook , renderNutri , renderTaste , renderTexture , renderColor ] ;
body . innerHTML = renderers [ currentTab ] ( d ) ;
}
function renderBasic ( d ) {
var specRows = ( d . specs || [ ] ) . map ( function ( s ) {
var statusHtml = s . isDefault ? '<span class="tag tag-blue" style="font-size:11px;">默认</span>' : '' ;
var cleanPrice = ( s . cleanPrice !== undefined && s . cleanPrice !== null ) ? parseFloat ( s . cleanPrice ) . toFixed ( 2 ) : '—' ;
var cleanSaleActive = s . cleanSale === true ;
var cleanSaleHtml = '<div class="sale-toggle' + ( cleanSaleActive ? ' active' : '' ) + '" onclick="toggleCleanSale(this)"><span class="sale-toggle__switch"></span><span class="sale-toggle__label">' + ( cleanSaleActive ? '上架' : '下架' ) + '</span></div>' ;
return '<tr><td>' + s . name + '</td><td>' + s . weight + '</td><td>' + s . price . toFixed ( 2 ) + '</td><td>' + cleanPrice + '</td><td>' + cleanSaleHtml + '</td><td>' + statusHtml + '</td></tr>' ;
} ) . join ( '' ) ;
return ` <div class="info-section"><div class="info-section__title">基本信息 <span><a class="link" href="din-dish-add.html?mode=edit&tab=0">编辑</a></span></div>
<div class="info-grid">
<div class="info-cell"><div class="info-cell__label">餐品编号</div><div class="info-cell__value"> ${ d . id } </div></div>
<div class="info-cell"><div class="info-cell__label">餐品分类</div><div class="info-cell__value"> ${ d . category } </div></div>
<div class="info-cell"><div class="info-cell__label">适用餐次</div><div class="info-cell__value"> ${ d . meal } </div></div>
<div class="info-cell"><div class="info-cell__label">过敏原</div><div class="info-cell__value"> ${ d . allergen } </div></div>
<div class="info-cell"><div class="info-cell__label">餐品标签</div><div class="info-cell__value"> ${ d . tags . map ( t => '<span class="tag tag-blue">' + t + '</span>' ) . join ( '' ) } </div></div>
<div class="info-cell"><div class="info-cell__label">称重基础价(元/100g)</div><div class="info-cell__value"> ${ d . weightBasePrice || '—' } </div></div>
<div class="info-cell"><div class="info-cell__label">称重会员价(元/100g)</div><div class="info-cell__value"> ${ d . weightVipPrice || '—' } </div></div>
<div class="info-cell"><div class="info-cell__label">来源</div><div class="info-cell__value"><span class="tag ${ d . source === '系统内置' ? 'tag-blue' : d . source === '味养创新' ? 'tag-orange' : 'tag-gray' } "> ${ d . source || '—' } </span></div></div>
<div class="info-cell"><div class="info-cell__label">启用状态</div><div class="info-cell__value"><span class="tag tag- ${ d . status === '启用' ? 'green' : 'gray' } "> ${ d . status } </span></div></div>
</div></div>
<div class="info-section"><div class="info-section__title">👥 适配人群</div>
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px;"> ${ ( d . crowd || [ ] ) . map ( function ( c ) { return '<div style="display:flex;align-items:center;gap:4px;"><span style="font-size:var(--font-sm);">' + c . name + '</span>' + levelTag ( c . level ) + '</div>' ; } ).join('')}</div>
${ d . crowdNote ? '<div style="font-size:var(--font-sm);color:var(--text-secondary);background:#FFFBE6;padding:8px 12px;border-radius:var(--radius-sm);border:1px solid #FFE58F;">⚠️ ' + d . crowdNote + '</div>' : '' } </div>
<div class="info-section"><div class="info-section__title">📐 餐品规格(共 ${ ( d . specs || [ ] ) . length } 项)</div>
<table class="info-table spec-detail-table"><thead><tr><th>规格</th><th>单位重量(g)</th><th>餐品售价(元/份)</th><th>净菜包售价(元/份)</th><th>外售净菜包</th><th>状态</th></tr></thead><tbody> ${ specRows } </tbody></table></div>
<div class="info-section"><div class="info-section__title">📷 菜品展示</div>
<div class="media-grid">
<div class="media-card"><div class="media-card__thumb">图片</div></div>
<div class="media-card"><div class="media-card__thumb">图片</div></div>
<div class="media-card"><div class="media-card__thumb">图片</div></div>
</div></div>
<div class="info-section"><div class="info-section__title">🎬 制作流程</div>
<div class="media-grid">
<div class="media-card"><div class="media-card__thumb">🎬</div></div>
</div></div> ` ;
}
function renderComposition ( d ) {
const rows = ( d . composition || [ ] ) . map ( c => ` <tr><td> ${ c . name } </td><td> ${ c . netType } </td><td> ${ c . amount } </td><td> ${ c . pct } </td><td><span class="tag tag- ${ c . type === '主材' ? 'blue' : c . type === '调料' ? 'orange' : 'gray' } "> ${ c . type || '辅材' } </span></td><td> ${ c . allergen } </td><td> ${ c . note } </td></tr> ` ) . join ( '' ) ;
return ` <div class="info-section"><div class="info-section__title">食材构成(共 ${ ( d . composition || [ ] ) . length } 项) <span><a class="link" href="din-dish-add.html?mode=edit&tab=1">编辑</a></span></div>
<table class="info-table"><thead><tr><th>食材名称</th><th>净菜类型</th><th>用量(g/份)</th><th>占比(%)</th><th>食材类型</th><th>过敏原</th><th>备注</th></tr></thead><tbody> ${ rows } </tbody></table></div> ` ;
}
function renderCook ( d ) {
const c = d . cook ;
return ` <div class="info-section"><div class="info-section__title">烹饪工艺 <span><a class="link" href="din-dish-add.html?mode=edit&tab=2">编辑</a></span></div>
<div class="info-grid">
<div class="info-cell"><div class="info-cell__label">主烹饪法</div><div class="info-cell__value"> ${ c . mainMethod } </div></div>
<div class="info-cell"><div class="info-cell__label">辅烹饪法</div><div class="info-cell__value"> ${ c . subMethod } </div></div>
<div class="info-cell"><div class="info-cell__label">火候</div><div class="info-cell__value"> ${ c . heat } </div></div>
<div class="info-cell"><div class="info-cell__label">烹饪时长</div><div class="info-cell__value"> ${ c . duration } min</div></div>
<div class="info-cell"><div class="info-cell__label">油用量</div><div class="info-cell__value"> ${ c . oil } g</div></div>
<div class="info-cell"><div class="info-cell__label">盐用量</div><div class="info-cell__value"> ${ c . salt } g</div></div>
<div class="info-cell"><div class="info-cell__label">烹饪温度</div><div class="info-cell__value"> ${ c . temp } ℃</div></div>
<div class="info-cell"><div class="info-cell__label">出品量</div><div class="info-cell__value"> ${ c . output } g</div></div>
<div class="info-cell"><div class="info-cell__label">工艺步骤数</div><div class="info-cell__value"> ${ c . steps } 步</div></div>
</div></div> ` ;
}
function renderNutri ( d ) {
const n = d . nutri ;
return ` <div class="info-section"><div class="info-section__title">营养指标 <span><a class="link" href="din-dish-add.html?mode=edit&tab=3">编辑</a></span></div>
<div class="info-grid">
<div class="info-cell"><div class="info-cell__label">热量 (kcal/100g)</div><div class="info-cell__value"> ${ n . calorie } </div></div>
<div class="info-cell"><div class="info-cell__label">蛋白质 (g)</div><div class="info-cell__value"> ${ n . protein } </div></div>
<div class="info-cell"><div class="info-cell__label">碳水化合物 (g)</div><div class="info-cell__value"> ${ n . carb } </div></div>
<div class="info-cell"><div class="info-cell__label">脂肪 (g)</div><div class="info-cell__value"> ${ n . fat } </div></div>
<div class="info-cell"><div class="info-cell__label">维生素C (mg)</div><div class="info-cell__value"> ${ n . vitC } </div></div>
<div class="info-cell"><div class="info-cell__label">矿物质 (mg)</div><div class="info-cell__value"> ${ n . mineral } </div></div>
<div class="info-cell"><div class="info-cell__label">膳食纤维 (g)</div><div class="info-cell__value"> ${ n . fiber } </div></div>
<div class="info-cell"><div class="info-cell__label">钠 (mg)</div><div class="info-cell__value"> ${ n . sodium } </div></div>
<div class="info-cell"><div class="info-cell__label">营养等级</div><div class="info-cell__value"> ${ n . nutriGrade === '达标' ? '<span class="tag tag-green">达标</span>' : n . nutriGrade === '偏差' ? '<span class="tag tag-orange">偏差</span>' : '<span class="tag tag-red">未达标</span>' } </div></div>
</div></div> ` ;
}
function renderTaste ( d ) {
const t = d . taste ;
const gradeTag = t . flavorGrade === '优秀' ? 'green' : t . flavorGrade === '良好' ? 'blue' : t . flavorGrade === '一般' ? 'orange' : 'red' ;
return ` <div class="info-section"><div class="info-section__title">味道档案 <span><a class="link" href="din-dish-add.html?mode=edit&tab=4">编辑</a></span></div>
<div class="info-grid">
<div class="info-cell"><div class="info-cell__label">酸度 (1-5)</div><div class="info-cell__value"> ${ t . sour } </div></div>
<div class="info-cell"><div class="info-cell__label">甜度 (1-5)</div><div class="info-cell__value"> ${ t . sweet } </div></div>
<div class="info-cell"><div class="info-cell__label">苦度 (1-5)</div><div class="info-cell__value"> ${ t . bitter } </div></div>
<div class="info-cell"><div class="info-cell__label">辣度 (1-5)</div><div class="info-cell__value"> ${ t . spicy } </div></div>
<div class="info-cell"><div class="info-cell__label">咸度 (1-5)</div><div class="info-cell__value"> ${ t . salty } </div></div>
<div class="info-cell"><div class="info-cell__label">气味评分 (1-5)</div><div class="info-cell__value"> ${ t . smell } </div></div>
<div class="info-cell"><div class="info-cell__label">综合风味评分 (1-5)</div><div class="info-cell__value"> ${ t . flavorScore } </div></div>
<div class="info-cell"><div class="info-cell__label">综合评级</div><div class="info-cell__value"><span class="tag tag- ${ gradeTag } "> ${ t . flavorGrade } </span></div></div>
</div></div> ` ;
}
function renderColor ( d ) {
const c = d . color ;
const gradeTag = c . colorGrade === '优秀' ? 'green' : c . colorGrade === '良好' ? 'blue' : c . colorGrade === '一般' ? 'orange' : 'red' ;
return ` <div class="info-section"><div class="info-section__title">色泽档案 <span><a class="link" href="din-dish-add.html?mode=edit&tab=5">编辑</a></span></div>
<div class="info-grid">
<div class="info-cell"><div class="info-cell__label">主色调</div><div class="info-cell__value"> ${ c . mainColor } </div></div>
<div class="info-cell"><div class="info-cell__label">色泽均匀度 (1-5)</div><div class="info-cell__value"> ${ c . uniformity } </div></div>
<div class="info-cell"><div class="info-cell__label">光泽度 (1-5)</div><div class="info-cell__value"> ${ c . gloss } </div></div>
<div class="info-cell"><div class="info-cell__label">新鲜度 (1-5)</div><div class="info-cell__value"> ${ c . freshness } </div></div>
<div class="info-cell"><div class="info-cell__label">卖相评分 (1-5)</div><div class="info-cell__value"> ${ c . appearance } </div></div>
<div class="info-cell"><div class="info-cell__label">综合色泽评分 (1-5)</div><div class="info-cell__value"> ${ c . colorScore } </div></div>
<div class="info-cell"><div class="info-cell__label">综合评级</div><div class="info-cell__value"><span class="tag tag- ${ gradeTag } "> ${ c . colorGrade } </span></div></div>
</div></div> ` ;
}
function renderTexture ( d ) {
const t = d . texture ;
const gradeTag = t . textureGrade === '优秀' ? 'green' : t . textureGrade === '良好' ? 'blue' : t . textureGrade === '一般' ? 'orange' : 'red' ;
return ` <div class="info-section"><div class="info-section__title">口感档案 <span><a class="link" href="din-dish-add.html?mode=edit&tab=6">编辑</a></span></div>
<div class="info-grid">
<div class="info-cell"><div class="info-cell__label">软硬度 (1-5)</div><div class="info-cell__value"> ${ t . hardness } </div></div>
<div class="info-cell"><div class="info-cell__label">黏糯度 (1-5)</div><div class="info-cell__value"> ${ t . sticky } </div></div>
<div class="info-cell"><div class="info-cell__label">酥脆度 (1-5)</div><div class="info-cell__value"> ${ t . crispy } </div></div>
<div class="info-cell"><div class="info-cell__label">嫩滑度 (1-5)</div><div class="info-cell__value"> ${ t . smooth } </div></div>
<div class="info-cell"><div class="info-cell__label">弹性 (1-5)</div><div class="info-cell__value"> ${ t . elastic } </div></div>
<div class="info-cell"><div class="info-cell__label">综合口感评分 (1-5)</div><div class="info-cell__value"> ${ t . textureScore } </div></div>
<div class="info-cell"><div class="info-cell__label">综合评级</div><div class="info-cell__value"><span class="tag tag- ${ gradeTag } "> ${ t . textureGrade } </span></div></div>
</div></div> ` ;
}
function renderImages ( d ) {
return ` <div class="info-section"><div class="info-section__title">图片库 <span><button class="btn btn-primary" style="height:28px;font-size:var(--font-xs);padding:0 10px;">📤 上传图片</button></span></div>
<div class="media-grid">
<div class="media-card"><div class="media-card__thumb">📷</div><div class="media-card__info"><div class="media-card__meta">800× 600 · 128KB · <span class="link">预览</span></div></div></div>
<div class="media-card"><div class="media-card__thumb">📷</div><div class="media-card__info"><div class="media-card__meta">1200× 900 · 256KB · <span class="link">预览</span></div></div></div>
<div class="media-card"><div class="media-card__thumb">📷</div><div class="media-card__info"><div class="media-card__meta">800× 600 · 96KB · <span class="link">预览</span></div></div></div>
</div></div> ` ;
}
function renderVideos ( d ) {
return ` <div class="info-section"><div class="info-section__title">视频库 <span><button class="btn btn-primary" style="height:28px;font-size:var(--font-xs);padding:0 10px;">📤 上传视频</button></span></div>
<div class="media-grid">
<div class="media-card"><div class="media-card__thumb">🎬</div><div class="media-card__info"><div class="media-card__title">标准制作流程</div><div class="media-card__meta">8分32秒 · 256MB · <span class="link">播放</span></div></div></div>
<div class="media-card"><div class="media-card__thumb">🎬</div><div class="media-card__info"><div class="media-card__title">食材溯源故事</div><div class="media-card__meta">2分15秒 · 86MB · <span class="link">播放</span></div></div></div>
</div></div> ` ;
}
// 侧边栏折叠
function toggleSubgroup ( header ) { header . parentElement . classList . toggle ( 'open' ) ; }
// 切换外售净菜包上架/下架状态
function toggleCleanSale ( el ) {
var active = el . classList . toggle ( 'active' ) ;
var label = el . querySelector ( '.sale-toggle__label' ) ;
if ( label ) label . textContent = active ? '上架' : '下架' ;
}
( function ( ) { var a = document . querySelector ( '.sidebar-item.active' ) ; if ( ! a ) return ; var s = a . closest ( '.sidebar-subgroup' ) ; if ( s ) s . classList . add ( 'open' ) ; } ) ( ) ;
// 初始化
renderDishList ( ) ;
renderDetail ( ) ;
/* ========== 餐品分类管理 ========== */
var nextDishCatId = 100 ;
var dishCategories = [
{ id : 1 , name : '主食' , code : '01' , level : 1 , sort : 1 , status : '启用' , parentId : null } ,
{ id : 2 , name : '荤菜' , code : '02' , level : 1 , sort : 2 , status : '启用' , parentId : null } ,
{ id : 3 , name : '素菜' , code : '03' , level : 1 , sort : 3 , status : '启用' , parentId : null } ,
{ id : 4 , name : '汤品' , code : '04' , level : 1 , sort : 4 , status : '启用' , parentId : null } ,
{ id : 5 , name : '小吃' , code : '05' , level : 1 , sort : 5 , status : '启用' , parentId : null } ,
{ id : 6 , name : '饮品' , code : '06' , level : 1 , sort : 6 , status : '启用' , parentId : null } ,
{ id : 7 , name : '甜品' , code : '07' , level : 1 , sort : 7 , status : '启用' , parentId : null } ,
{ id : 8 , name : '套餐' , code : '08' , level : 1 , sort : 8 , status : '禁用' , parentId : null }
] ;
var expandedDishCats = { } ;
var editingDishCatId = null ;
var editingDishParentId = null ;
var MAX _DISH _LEVEL = 2 ;
function openDishCategoryDialog ( ) {
document . getElementById ( 'dishCategoryDialog' ) . classList . add ( 'open' ) ;
renderDishCatTable ( ) ;
}
function closeDishCategoryDialog ( ) {
document . getElementById ( 'dishCategoryDialog' ) . classList . remove ( 'open' ) ;
}
function toggleDishCatStatus ( id ) {
var cat = dishCategories . find ( function ( c ) { return c . id === id ; } ) ;
if ( ! cat ) return ;
cat . status = cat . status === '启用' ? '禁用' : '启用' ;
renderDishCatTable ( ) ;
}
function toggleDishCatExpand ( id ) {
expandedDishCats [ id ] = ! expandedDishCats [ id ] ;
renderDishCatTable ( ) ;
}
function getDishCatChildren ( parentId ) {
return dishCategories . filter ( function ( c ) { return c . parentId === parentId ; } ) . sort ( function ( a , b ) { return a . sort - b . sort ; } ) ;
}
function renderDishCatTable ( ) {
var tops = dishCategories . filter ( function ( c ) { return c . parentId === null ; } ) . sort ( function ( a , b ) { return a . sort - b . sort ; } ) ;
var html = '' ;
tops . forEach ( function ( cat ) {
html += renderDishCatRow ( cat ) ;
if ( expandedDishCats [ cat . id ] ) {
getDishCatChildren ( cat . id ) . forEach ( function ( child ) {
html += renderDishCatRow ( child , true ) ;
} ) ;
}
} ) ;
document . getElementById ( 'dishCatTableBody' ) . innerHTML = html || '<tr><td colspan="6" style="text-align:center;color:var(--text-placeholder);padding:40px;">暂无分类数据</td></tr>' ;
}
function renderDishCatRow ( cat , isChild ) {
var hasChildren = getDishCatChildren ( cat . id ) . length > 0 ;
var isExpanded = expandedDishCats [ cat . id ] ;
var toggleHtml = hasChildren
? '<span class="cat-toggle" onclick="event.stopPropagation();toggleDishCatExpand(' + cat . id + ')">' + ( isExpanded ? '− ' : '+' ) + '</span>'
: '<span class="cat-toggle leaf">+</span>' ;
var rowClass = isChild ? 'cat-row-l2' : '' ;
var activeClass = cat . status === '启用' ? ' active' : '' ;
var statusLabel = cat . status === '启用' ? '启用' : '禁用' ;
var statusHtml = '<div class="status-toggle' + activeClass + '" onclick="event.stopPropagation();toggleDishCatStatus(' + cat . id + ')"><span class="status-toggle__switch"></span><span class="status-toggle__label">' + statusLabel + '</span></div>' ;
var canAddChild = cat . level < MAX _DISH _LEVEL ;
var addChildBtn = canAddChild ? '<span class="link" onclick="openAddDishCat(' + cat . id + ')">新增下级</span>' : '' ;
return '<tr class="' + rowClass + '"><td class="cat-name">' + toggleHtml + cat . name + '</td><td>' + cat . code + '</td><td>' + cat . level + '</td><td>' + cat . sort + '</td><td>' + statusHtml + '</td><td class="td-actions"><span class="link" onclick="openEditDishCat(' + cat . id + ')">编辑</span><span class="link" style="color:var(--danger);" onclick="deleteDishCat(' + cat . id + ')">删除</span>' + addChildBtn + '</td></tr>' ;
}
function openAddDishCat ( parentId ) {
editingDishCatId = null ;
editingDishParentId = parentId || null ;
document . getElementById ( 'dishCatFormDialogTitle' ) . textContent = parentId ? '新增下级分类' : '新增分类' ;
document . getElementById ( 'dishCatFormName' ) . value = '' ;
document . getElementById ( 'dishCatFormCode' ) . value = '' ;
document . getElementById ( 'dishCatFormSort' ) . value = '1' ;
document . getElementById ( 'dishCatFormStatus' ) . value = '启用' ;
document . getElementById ( 'dishCatFormDialog' ) . classList . add ( 'open' ) ;
}
function openEditDishCat ( id ) {
var cat = dishCategories . find ( function ( c ) { return c . id === id ; } ) ;
if ( ! cat ) return ;
editingDishCatId = id ;
editingDishParentId = cat . parentId ;
document . getElementById ( 'dishCatFormDialogTitle' ) . textContent = '编辑分类' ;
document . getElementById ( 'dishCatFormName' ) . value = cat . name ;
document . getElementById ( 'dishCatFormCode' ) . value = cat . code ;
document . getElementById ( 'dishCatFormSort' ) . value = cat . sort ;
document . getElementById ( 'dishCatFormStatus' ) . value = cat . status ;
document . getElementById ( 'dishCatFormDialog' ) . classList . add ( 'open' ) ;
}
function closeDishCatFormDialog ( ) {
document . getElementById ( 'dishCatFormDialog' ) . classList . remove ( 'open' ) ;
editingDishCatId = null ;
editingDishParentId = null ;
}
function saveDishCatForm ( ) {
var name = document . getElementById ( 'dishCatFormName' ) . value . trim ( ) ;
var code = document . getElementById ( 'dishCatFormCode' ) . value . trim ( ) ;
var sort = parseInt ( document . getElementById ( 'dishCatFormSort' ) . value ) || 1 ;
var status = document . getElementById ( 'dishCatFormStatus' ) . value ;
if ( ! name || ! code ) { alert ( '请填写分类名称和分类编码' ) ; return ; }
if ( editingDishCatId ) {
var cat = dishCategories . find ( function ( c ) { return c . id === editingDishCatId ; } ) ;
if ( cat ) { cat . name = name ; cat . code = code ; cat . sort = sort ; cat . status = status ; }
} else {
var level = editingDishParentId ? 2 : 1 ;
dishCategories . push ( { id : nextDishCatId ++ , name : name , code : code , level : level , sort : sort , status : status , parentId : editingDishParentId } ) ;
if ( editingDishParentId ) expandedDishCats [ editingDishParentId ] = true ;
}
closeDishCatFormDialog ( ) ;
renderDishCatTable ( ) ;
}
function deleteDishCat ( id ) {
var cat = dishCategories . find ( function ( c ) { return c . id === id ; } ) ;
if ( ! cat ) return ;
if ( getDishCatChildren ( id ) . length > 0 ) { alert ( '该分类下存在子级分类,请先删除子级分类后再删除。' ) ; return ; }
if ( ! confirm ( '确定要删除分类「' + cat . name + '」吗?删除后不可恢复。' ) ) return ;
dishCategories = dishCategories . filter ( function ( c ) { return c . id !== id ; } ) ;
renderDishCatTable ( ) ;
}
< / script >
<!-- 新增食材弹窗 -->
< div class = "dialog-mask" id = "ingredientDialog" >
< div class = "dialog" >
< div class = "dialog__header" >
< div class = "dialog__title" > 新增食材< / div >
< button class = "dialog__close" onclick = "closeIngredientDialog()" >
< svg width = "14" height = "14" viewBox = "0 0 14 14" fill = "none" > < path d = "M1 1l12 12M13 1L1 13" stroke = "currentColor" stroke-width = "1.5" stroke-linecap = "round" / > < / svg >
< / button >
< / div >
< div class = "dialog__body" >
< div class = "form-row" >
< div class = "form-group" > < label > 食材名称< span class = "required" > *< / span > < / label > < input class = "form-input" type = "text" placeholder = "请输入食材名称" > < / div >
< div class = "form-group" > < label > 净菜类型< span class = "required" > *< / span > < / label > < select class = "form-select" > < option > 请选择< / option > < option > 土豆丝< / option > < option > 土豆块< / option > < option > 土豆泥< / option > < option > 番茄块< / option > < option > 蒜末< / option > < / select > < / div >
< / div >
< div class = "form-row" >
< div class = "form-group" > < label > 用量(g/份)< span class = "required" > *< / span > < / label > < input class = "form-input" type = "number" placeholder = "请输入用量" > < / div >
< div class = "form-group" > < label > 加工状态< / label > < select class = "form-select" > < option > 请选择< / option > < option > 原料< / option > < option > 净菜< / option > < option > 半成品< / option > < / select > < / div >
< / div >
< div class = "form-row" >
< div class = "form-group" > < label > 食材类型< / label > < select class = "form-select" > < option > 辅材< / option > < option > 主材< / option > < option > 调料< / option > < / select > < / div >
< div class = "form-group" > < label > 过敏原< / label > < input class = "form-input" type = "text" placeholder = "无则填无" > < / div >
< / div >
< div class = "form-group" > < label > 备注< / label > < input class = "form-input" type = "text" placeholder = "请输入备注信息" > < / div >
< / div >
< div class = "dialog__footer" >
< button class = "btn btn-default" onclick = "closeIngredientDialog()" > 取消< / button >
< button class = "btn btn-primary" > 确定< / button >
< / div >
< / div >
< / div >
< script >
function openIngredientDialog ( ) { document . getElementById ( 'ingredientDialog' ) . classList . add ( 'open' ) ; }
function closeIngredientDialog ( ) { document . getElementById ( 'ingredientDialog' ) . classList . remove ( 'open' ) ; }
< / script >
<!-- 餐品分类管理弹窗 -->
< div class = "dialog-mask" id = "dishCategoryDialog" >
< div class = "dialog" style = "width:820px;" >
< div class = "dialog__header" >
< div class = "dialog__title" > 餐品分类管理< / div >
< button class = "dialog__close" onclick = "closeDishCategoryDialog()" > ✕< / button >
< / div >
< div class = "dialog__body" >
<!-- 工具栏 -->
< div style = "margin-bottom:12px;" >
< button class = "btn btn-primary" onclick = "openAddDishCat()" style = "height:30px;font-size:var(--font-xs);" > + 新增一级分类< / button >
< / div >
<!-- 分类表格 -->
< div style = "overflow-x:auto;" >
< table class = "cat-table" >
< thead > < tr > < th style = "width:180px;" > 分类名称< / th > < th style = "width:100px;" > 分类编码< / th > < th style = "width:60px;" > 层级< / th > < th style = "width:60px;" > 排序< / th > < th style = "width:80px;" > 状态< / th > < th style = "width:180px;" > 操作< / th > < / tr > < / thead >
< tbody id = "dishCatTableBody" > < / tbody >
< / table >
< / div >
< / div >
< div class = "dialog__footer" >
< button class = "btn btn-default" onclick = "closeDishCategoryDialog()" > 关闭< / button >
< / div >
< / div >
< / div >
<!-- 餐品分类表单子弹窗 -->
< div class = "dialog-mask" id = "dishCatFormDialog" >
< div class = "dialog" style = "width:480px;" >
< div class = "dialog__header" >
< div class = "dialog__title" id = "dishCatFormDialogTitle" > 新增分类< / div >
< button class = "dialog__close" onclick = "closeDishCatFormDialog()" > ✕< / button >
< / div >
< div class = "dialog__body" >
< div class = "form-group" > < label > 分类名称< span class = "required" > *< / span > < / label > < input class = "form-input" id = "dishCatFormName" placeholder = "请输入分类名称" > < / div >
< div class = "form-row" >
< div class = "form-group" > < label > 分类编码< span class = "required" > *< / span > < / label > < input class = "form-input" id = "dishCatFormCode" placeholder = "请输入分类编码" > < / div >
< div class = "form-group" > < label > 排序< / label > < input class = "form-input" id = "dishCatFormSort" type = "number" value = "1" > < / div >
< / div >
< div class = "form-group" > < label > 状态< / label > < select class = "form-select" id = "dishCatFormStatus" > < option > 启用< / option > < option > 禁用< / option > < / select > < / div >
< / div >
< div class = "dialog__footer" >
< button class = "btn btn-default" onclick = "closeDishCatFormDialog()" > 取消< / button >
< button class = "btn btn-primary" onclick = "saveDishCatForm()" > 保存< / button >
< / div >
< / div >
< / div >
< / body >
< / html >