| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852 |
- <template>
- <div class="restaurant-order-page">
- <NTabs v-model:value="activeTab" type="line" animated>
- <!-- ==================== Tab 1: 全部订单 ==================== -->
- <NTabPane name="all" tab="全部订单">
- <div class="tab-content">
- <!-- 筛选栏 -->
- <NForm inline label-placement="left" class="filter-form mb-3">
- <NFormItem label="订单日期">
- <NDatePicker
- v-model:value="filterDate"
- type="date"
- clearable
- placeholder="请选择日期"
- style="width: 200px"
- />
- </NFormItem>
- <NFormItem>
- <NSpace>
- <NButton type="primary" @click="handleFilterSearch">
- <template #icon>
- <i class="i-mdi:magnify" />
- </template>
- 查询
- </NButton>
- <NButton @click="handleFilterReset">
- <template #icon>
- <i class="i-mdi:refresh" />
- </template>
- 重置
- </NButton>
- </NSpace>
- </NFormItem>
- </NForm>
- <!-- 统计卡片 -->
- <div class="stats-row">
- <div class="stat-card stat-pending">
- <span class="stat-value">{{ dashboard.pendingCount }}</span>
- <span class="stat-label">待接单</span>
- </div>
- <div class="stat-card stat-progress">
- <span class="stat-value">{{ inProgressCount }}</span>
- <span class="stat-label">进行中</span>
- </div>
- <div class="stat-card stat-completed">
- <span class="stat-value">{{ dashboard.todayCompletedCount }}</span>
- <span class="stat-label">今日完成</span>
- </div>
- <div class="stat-card stat-rejected">
- <span class="stat-value">{{ rejectedOrders.length }}</span>
- <span class="stat-label">已拒单</span>
- </div>
- <div v-if="connected" class="sse-indicator" title="实时通知已连接">
- <span class="sse-dot" />
- </div>
- </div>
- <!-- 待接单列表 -->
- <div v-if="pendingOrders.length > 0" class="order-section">
- <h3 class="section-title">
- <span class="section-dot pending" />
- 待接单
- <span class="section-count">{{ pendingOrders.length }}</span>
- </h3>
- <div class="order-list">
- <div v-for="order in pendingOrders" :key="order.id" class="order-row order-row-pending" @click="openDetail(order)">
- <div class="order-room">
- {{ order.roomNo || '--' }}
- </div>
- <div class="order-info">
- <div class="order-no">
- {{ order.orderNo }}
- </div>
- <div class="order-items">
- {{ buildItemSummary(order) }}
- </div>
- <div class="order-meta">
- <span class="order-time">{{ formatWaitTime(order.createTime) }}</span>
- <span class="order-amount">¥{{ order.totalAmount }}</span>
- </div>
- </div>
- <div class="order-actions" @click.stop>
- <NButton size="small" type="success" @click="handleAccept(order)">
- 接单
- </NButton>
- <NButton size="small" type="error" @click="handleReject(order)">
- 拒单
- </NButton>
- </div>
- </div>
- </div>
- </div>
- <!-- 进行中列表 -->
- <div v-if="inProgressOrders.length > 0" class="order-section">
- <h3 class="section-title">
- <span class="section-dot progress" />
- 进行中
- <span class="section-count">{{ inProgressOrders.length }}</span>
- </h3>
- <div class="order-list">
- <div v-for="order in inProgressOrders" :key="order.id" class="order-row" @click="openDetail(order)">
- <div class="order-room">
- {{ order.roomNo || '--' }}
- </div>
- <div class="order-info">
- <div class="order-no">
- {{ order.orderNo }}
- </div>
- <div class="order-items">
- {{ buildItemSummary(order) }}
- </div>
- <div class="order-meta">
- <span class="order-status-tag">
- <DictTag dict-type="hotel_order_status" :value="String(order.status)" size="small" />
- </span>
- <span class="order-amount">¥{{ order.totalAmount }}</span>
- </div>
- </div>
- <div class="order-actions" @click.stop>
- <!-- 前厅只有出餐后的配送/完成操作,备餐/出餐归厨房端 -->
- <template v-if="order.status === 4">
- <NButton size="small" type="primary" @click="handleDeliver(order)">
- 配送
- </NButton>
- </template>
- <template v-if="order.status === 5">
- <NButton size="small" type="success" @click="handleComplete(order)">
- 完成
- </NButton>
- </template>
- </div>
- </div>
- </div>
- </div>
- <!-- 已拒单列表 -->
- <div v-if="rejectedOrders.length > 0" class="order-section">
- <h3 class="section-title">
- <span class="section-dot rejected" />
- 已拒单
- <span class="section-count">{{ rejectedOrders.length }}</span>
- </h3>
- <div class="order-list">
- <div v-for="order in rejectedOrders" :key="order.id" class="order-row order-row-rejected" @click="openDetail(order)">
- <div class="order-room">
- {{ order.roomNo || '--' }}
- </div>
- <div class="order-info">
- <div class="order-no">
- {{ order.orderNo }}
- </div>
- <div class="order-items">
- {{ buildItemSummary(order) }}
- </div>
- <div class="order-meta">
- <DictTag dict-type="hotel_order_status" :value="String(order.status)" size="small" />
- <span class="order-amount">¥{{ order.totalAmount }}</span>
- </div>
- <div v-if="order.rejectReason" class="order-reject-reason text-xs mt-1 text-gray-400">
- 拒单原因:{{ order.rejectReason }}
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 空状态 -->
- <div v-if="pendingOrders.length === 0 && inProgressOrders.length === 0 && rejectedOrders.length === 0" class="empty-state">
- <span class="text-gray-400">暂无活跃订单</span>
- </div>
- </div>
- </NTabPane>
- <!-- ==================== Tab 2: 订单看板 ==================== -->
- <NTabPane name="kanban" tab="订单看板">
- <div class="tab-content kanban-content">
- <!-- 筛选栏 -->
- <NForm inline label-placement="left" class="filter-form mb-3">
- <NFormItem label="订单日期">
- <NDatePicker
- v-model:value="filterDate"
- type="date"
- clearable
- placeholder="请选择日期"
- style="width: 200px"
- />
- </NFormItem>
- <NFormItem>
- <NSpace>
- <NButton type="primary" @click="handleFilterSearch">
- <template #icon>
- <i class="i-mdi:magnify" />
- </template>
- 查询
- </NButton>
- <NButton @click="handleFilterReset">
- <template #icon>
- <i class="i-mdi:refresh" />
- </template>
- 重置
- </NButton>
- </NSpace>
- </NFormItem>
- </NForm>
- <div class="kanban-board">
- <div v-for="col in kanbanColumns" :key="col.key" class="kanban-column">
- <div class="kanban-header" :style="{ borderTopColor: col.color }">
- <span class="kanban-count" :style="{ color: col.color }">{{ getKanbanCount(col.key) }}</span>
- <span class="kanban-title">{{ col.label }}</span>
- </div>
- <div class="kanban-body">
- <div v-if="getKanbanOrders(col.key).length === 0" class="kanban-empty">
- <span class="text-xs text-gray-300">暂无订单</span>
- </div>
- <div
- v-for="order in getKanbanOrders(col.key)"
- :key="order.id"
- class="kanban-card"
- :style="{ borderTopColor: col.color }"
- @click="openDetail(order)"
- >
- <div class="kc-top flex items-center justify-between">
- <span class="kc-room">{{ order.roomNo || '--' }}</span>
- <DictTag dict-type="hotel_order_status" :value="String(order.status)" size="tiny" />
- </div>
- <div class="kc-no text-xs text-gray-400">
- {{ order.orderNo }}
- </div>
- <div class="kc-items text-xs">
- {{ buildItemSummary(order) }}
- </div>
- <div class="kc-bottom flex items-center justify-between">
- <span class="kc-amount text-xs">¥{{ order.totalAmount }}</span>
- <span class="kc-time text-xs text-gray-400">{{ formatShortTime(order.createTime) }}</span>
- </div>
- <div v-if="col.key === 'rejected' && order.rejectReason" class="kc-reject-reason text-xs mt-1 text-gray-400">
- 拒单原因:{{ order.rejectReason }}
- </div>
- <div class="kc-actions" @click.stop>
- <!-- 前厅只有接单/拒单 + 出餐后的配送/完成,备餐/出餐归厨房端 -->
- <template v-if="col.key === 'pending'">
- <NButton size="tiny" type="success" @click="handleAccept(order)">
- 接单
- </NButton>
- <NButton size="tiny" type="error" @click="handleReject(order)">
- 拒单
- </NButton>
- </template>
- <template v-if="col.key === 'ready'">
- <NButton size="tiny" type="primary" @click="handleDeliver(order)">
- 配送
- </NButton>
- </template>
- <template v-if="col.key === 'delivering'">
- <NButton size="tiny" type="success" @click="handleComplete(order)">
- 完成
- </NButton>
- </template>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </NTabPane>
- <!-- ==================== Tab 3: 营业时间 ==================== -->
- <NTabPane name="hours" tab="营业时间">
- <div class="tab-content hours-content">
- <!-- 营业时段列表 -->
- <h3 class="hours-section-title">
- <span class="text-base">📅</span> 每日营业时段
- </h3>
- <div class="hours-list">
- <div v-for="bh in sortedBusinessHours" :key="bh.id" class="hours-card">
- <div class="hours-card-left flex items-center gap-3">
- <span class="hours-icon">{{ mealIconMap[bh.mealType] || '🕐' }}</span>
- <div>
- <div class="hours-name">
- {{ bh.mealName || mealLabelMap[bh.mealType] || bh.mealType }}
- </div>
- <div class="hours-time">
- {{ bh.startTime }} — {{ bh.endTime }}
- </div>
- </div>
- </div>
- <div class="hours-card-right flex items-center gap-2">
- <NTag :type="bh.status === 'ENABLED' ? 'success' : 'default'" size="small" round>
- {{ bh.status === 'ENABLED' ? '启用' : '未启用' }}
- </NTag>
- <NButton size="tiny" quaternary type="primary" @click="openEditHours(bh)">
- 编辑
- </NButton>
- <NButton
- size="tiny"
- quaternary
- :type="bh.status === 'ENABLED' ? 'warning' : 'success'"
- @click="handleToggleHours(bh)"
- >
- {{ bh.status === 'ENABLED' ? '禁用' : '启用' }}
- </NButton>
- <NButton size="tiny" quaternary type="error" @click="handleDeleteHours(bh)">
- 删除
- </NButton>
- </div>
- </div>
- <div v-if="businessHours.length === 0" class="hours-empty text-sm py-6 text-center text-gray-400">
- 暂无营业时段,请点击下方按钮添加
- </div>
- </div>
- <NButton type="primary" dashed block style="margin-top: 12px" @click="openAddHours">
- + 添加营业时段
- </NButton>
- <!-- 紧急控制 -->
- <h3 class="hours-section-title mt-6">
- <span class="text-base" /> 紧急控制
- </h3>
- <div class="emergency-card">
- <div class="emergency-row flex items-center justify-between">
- <div>
- <div class="emergency-title">
- 临时暂停接单
- </div>
- <div class="emergency-desc">
- 突发情况(厨房故障等)时一键暂停
- </div>
- </div>
- <NSwitch v-model:value="isPaused" @update:value="handleTogglePause" />
- </div>
- <NDivider style="margin: 12px 0" />
- <div class="emergency-row flex items-center justify-between">
- <div>
- <div class="emergency-title">
- 当前状态
- </div>
- <div class="emergency-desc">
- {{ currentStatusText }}
- </div>
- </div>
- <NTag :type="isPaused ? 'error' : 'success'" size="small" round>
- {{ isPaused ? '已暂停接单' : '正常接单' }}
- </NTag>
- </div>
- </div>
- </div>
- </NTabPane>
- </NTabs>
- <!-- ==================== 营业时段编辑弹窗 ==================== -->
- <NModal
- v-model:show="hoursModalVisible"
- preset="card"
- :title="hoursModalTitle"
- style="width: 480px"
- :mask-closable="false"
- >
- <NForm label-placement="left" label-width="80px" size="medium">
- <NFormItem label="餐段类型" required>
- <NSelect
- v-model:value="hoursForm.mealType"
- placeholder="请选择餐段类型"
- :options="mealTypeOptions"
- @update:value="onMealTypeChange"
- />
- </NFormItem>
- <NFormItem label="餐段名称" required>
- <NInput v-model:value="hoursForm.mealName" placeholder="如:早餐、午餐" />
- </NFormItem>
- <NFormItem label="适用星期">
- <NSelect
- v-model:value="hoursForm.dayOfWeekArr"
- placeholder="不选则每天"
- multiple
- clearable
- :options="weekDayOptions"
- />
- </NFormItem>
- <NFormItem label="开始时间" required>
- <NSelect v-model:value="hoursForm.startTime" placeholder="请选择" :options="timeOptions" />
- </NFormItem>
- <NFormItem label="结束时间" required>
- <NSelect v-model:value="hoursForm.endTime" placeholder="请选择" :options="timeOptions" />
- </NFormItem>
- <NFormItem label="排序">
- <NInputNumber v-model:value="hoursForm.sortOrder" :min="0" :step="1" placeholder="排序号" />
- </NFormItem>
- </NForm>
- <template #footer>
- <div class="flex justify-end gap-2">
- <NButton size="small" @click="hoursModalVisible = false">
- 取消
- </NButton>
- <NButton size="small" type="primary" :loading="hoursSaving" @click="confirmSaveHours">
- 保存
- </NButton>
- </div>
- </template>
- </NModal>
- <!-- 原因输入弹窗(拒单用) -->
- <NModal
- v-model:show="reasonVisible"
- title="拒单原因"
- preset="dialog"
- style="width: 420px"
- :show-icon="false"
- :mask-closable="false"
- positive-text="确认拒单"
- negative-text="取消"
- @positive-click="handleReasonConfirm"
- >
- <NInput
- v-model:value="reasonText"
- type="textarea"
- placeholder="请输入拒单原因(选填)"
- :rows="3"
- />
- </NModal>
- <!-- ==================== 订单详情弹窗 ==================== -->
- <NModal
- v-model:show="showDetail"
- preset="card"
- :title="null"
- style="width: 720px; max-width: 95vw;"
- :bordered="false"
- :segmented="{ content: true }"
- class="order-detail-modal"
- >
- <div v-if="detailOrder" class="receipt-full">
- <!-- 顶部标题栏 -->
- <div class="receipt-header-bar mb-6 rounded-t-lg p-4 flex items-center justify-between">
- <div class="flex items-center gap-3">
- <i class="i-mdi:arrow-left text-xl cursor-pointer hover:text-white/80" @click="showDetail = false" />
- <span class="text-white text-base font-semibold">订单详情</span>
- </div>
- <DictTag dict-type="hotel_order_status" :value="String(detailOrder.status)" />
- </div>
- <!-- 房间号区域 -->
- <div class="room-hero-section text-center mb-6">
- <div class="room-icon-wrapper inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-100 mb-3">
- <i class="i-mdi:home-city text-3xl text-blue-600" />
- </div>
- <div class="room-number text-5xl font-bold text-gray-800 mb-2">
- {{ detailOrder.roomNo || '--' }}
- </div>
- <div class="room-meta text-sm text-gray-500">
- {{ detailOrder.orderNo }} · {{ formatDetailTime(detailOrder.createTime) }}下单
- </div>
- </div>
- <!-- 小票主体(虚线边框) -->
- <div class="receipt-body mb-6 rounded-lg border-2 border-dashed border-gray-300 p-6 bg-white">
- <!-- 小票头部 -->
- <div class="receipt-title text-center mb-4 pb-4 border-b border-dashed border-gray-300">
- <div class="text-lg font-bold text-gray-800 mb-1">璞石酒店 · 前台联</div>
- <div class="text-sm text-gray-500">{{ detailOrder.orderNo }}</div>
- <div class="text-xs text-gray-400 mt-1">{{ formatDateTime(detailOrder.createTime) }}</div>
- </div>
- <!-- 桌号与备注 -->
- <div class="receipt-info mb-4 pb-4 border-b border-dashed border-gray-300">
- <div class="flex items-center gap-2 mb-2">
- <span class="text-sm text-gray-600">桌号:</span>
- <span class="text-base font-semibold text-gray-800">{{ detailOrder.roomNo }}</span>
- <span class="text-sm text-gray-600 ml-2">{{ detailOrder.roomType || '' }}</span>
- </div>
- <div v-if="detailOrder.contactName" class="flex items-center gap-2 mb-2">
- <span class="text-sm text-gray-600">联系人:</span>
- <span class="text-sm text-gray-700">{{ detailOrder.contactName }}</span>
- </div>
- <div class="flex items-center gap-2">
- <span class="text-sm text-gray-600">备注:</span>
- <span class="text-sm text-gray-700">{{ detailOrder.note || '无' }}</span>
- </div>
- </div>
- <!-- 菜品明细 -->
- <div class="receipt-items mb-4 pb-4 border-b border-dashed border-gray-300">
- <div v-for="(item, idx) in (detailOrder.items || [])" :key="item.id" class="receipt-item-row mb-3 last:mb-0">
- <div class="item-header flex justify-between items-start mb-1">
- <div class="flex-1">
- <span class="item-index text-sm text-gray-500 mr-2">[{{ idx + 1 }}]</span>
- <span class="item-name text-base font-medium text-gray-800">{{ item.dishName }}</span>
- </div>
- <span class="item-price text-right font-semibold text-gray-800 whitespace-nowrap ml-3">
- ¥{{ item.subtotal || '0.00' }}
- </span>
- </div>
- <div v-if="item.specDesc || item.additionsDesc" class="item-details text-xs text-gray-500 ml-6">
- <span v-if="item.specDesc">{{ item.specDesc }}</span>
- <span v-if="item.specDesc && item.additionsDesc"> / </span>
- <span v-if="item.additionsDesc">{{ item.additionsDesc }}</span>
- </div>
- <div v-if="item.note" class="item-note text-xs text-orange-600 ml-6 mt-1">
- 备注: {{ item.note }}
- </div>
- </div>
- </div>
- <!-- 合计 -->
- <div class="receipt-total text-right text-lg font-bold text-gray-800">
- 合计:¥{{ detailOrder.totalAmount || '0.00' }}
- </div>
- </div>
- <!-- 底部操作按钮(前厅视角) -->
- <div class="receipt-actions">
- <NButton
- v-if="detailOrder.status === 1"
- type="success"
- block
- size="large"
- class="action-btn-primary"
- @click="handleAcceptFromDetail"
- >
- <template #icon><i class="i-mdi:check-circle" /></template>
- 接单
- </NButton>
- <NButton
- v-if="detailOrder.status === 1"
- type="error"
- block
- size="large"
- class="mt-3"
- @click="handleRejectFromDetail"
- >
- <template #icon><i class="i-mdi:close-circle" /></template>
- 拒单
- </NButton>
- <NButton
- v-if="detailOrder.status === 4"
- type="primary"
- block
- size="large"
- class="action-btn-primary"
- @click="handleDeliverFromDetail"
- >
- <template #icon><i class="i-mdi:truck-delivery" /></template>
- 开始配送
- </NButton>
- <NButton
- v-if="detailOrder.status === 5"
- type="success"
- block
- size="large"
- class="action-btn-primary"
- @click="handleCompleteFromDetail"
- >
- <template #icon><i class="i-mdi:flag-checkered" /></template>
- 完成配送
- </NButton>
- <div v-if="detailOrder.status >= 2 && detailOrder.status <= 3" class="status-hint text-center text-sm text-gray-400 py-3">
- <i class="i-mdi:chef-hat mr-1" />订单正在厨房制作中,请稍候
- </div>
- <div v-if="detailOrder.status === 6" class="status-hint text-center text-sm text-green-600 py-3">
- <i class="i-mdi:check-circle mr-1" />订单已完成
- </div>
- <div v-if="detailOrder.status === 7" class="status-hint text-center text-sm text-gray-400 py-3">
- <i class="i-mdi:close-circle mr-1" />订单已拒单
- </div>
- </div>
- </div>
- </NModal>
- </div>
- </template>
- <script setup>
- import {
- NButton,
- NDatePicker,
- NDivider,
- NForm,
- NFormItem,
- NInput,
- NInputNumber,
- NModal,
- NSelect,
- NSpace,
- NSwitch,
- NTabPane,
- NTabs,
- NTag,
- useDialog,
- useMessage,
- } from 'naive-ui'
- import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
- import {
- acceptOrder,
- completeOrder,
- createBusinessHours,
- deleteBusinessHours,
- deliverOrder,
- getBusinessHoursAll,
- getOrderDashboard,
- getOrderPage,
- getPauseStatus,
- prepareOrder,
- readyOrder,
- rejectOrder,
- toggleBusinessHours,
- togglePause,
- updateBusinessHours,
- } from '@/api/hotel'
- import DictTag from '@/components/DictTag.vue'
- import { useOrderNotification } from '@/composables/useOrderNotification'
- defineOptions({ name: 'RestaurantOrder' })
- const message = useMessage()
- const dialog = useDialog()
- const activeTab = ref('all')
- // ==================== 日期筛选 ====================
- const filterDate = ref(Date.now())
- function getFilterDateStr() {
- if (!filterDate.value)
- return null
- const d = new Date(filterDate.value)
- const y = d.getFullYear()
- const m = String(d.getMonth() + 1).padStart(2, '0')
- const day = String(d.getDate()).padStart(2, '0')
- return `${y}-${m}-${day}`
- }
- // ==================== 看板列定义 ====================
- const kanbanColumns = [
- { key: 'pending', label: '待接单', color: '#d03050', statuses: [1] },
- { key: 'preparing', label: '备餐中', color: '#f0a020', statuses: [2, 3] },
- { key: 'ready', label: '待配送', color: '#2080f0', statuses: [4] },
- { key: 'delivering', label: '配送中', color: '#18a058', statuses: [5] },
- { key: 'completed', label: '已结单', color: '#999', statuses: [6] },
- { key: 'rejected', label: '已拒单', color: '#999', statuses: [7] },
- ]
- // ==================== 数据状态 ====================
- const dashboard = ref({
- pendingCount: 0,
- preparingCount: 0,
- deliveringCount: 0,
- todayTotal: 0,
- todayRevenue: '0.00',
- todayCompletedCount: 0,
- })
- const allOrders = ref([])
- const businessHours = ref([])
- const isPaused = ref(false)
- // ==================== SSE 通知 ====================
- const { connected, connect, disconnect, playSound } = useOrderNotification({
- onNewOrder: () => {
- refreshAll()
- },
- onRefundRequest: () => {
- refreshAll()
- },
- })
- function handleHotelNotification(e) {
- const { type, message: msg, level } = e.detail
- const nType = level === 'warning' ? 'warning' : 'info'
- $notification[nType]({
- title: type === 'new-order' ? '新订单' : '退单申请',
- content: msg,
- duration: 5000,
- })
- }
- // ==================== 数据加载 ====================
- async function loadDashboard() {
- try {
- const res = await getOrderDashboard({ queryDate: getFilterDateStr() })
- if (res.code === 200 && res.data) {
- dashboard.value = {
- pendingCount: res.data.pendingCount || 0,
- preparingCount: res.data.preparingCount || 0,
- deliveringCount: res.data.deliveringCount || 0,
- todayTotal: res.data.todayTotal || 0,
- todayRevenue: res.data.todayRevenue || '0.00',
- todayCompletedCount: res.data.todayCompletedCount || 0,
- }
- }
- }
- catch (err) {
- void err
- }
- }
- async function loadOrders() {
- try {
- const dateStr = getFilterDateStr()
- const statuses = [1, 2, 3, 4, 5, 6, 7]
- const promises = statuses.map((s) => {
- return getOrderPage({ pageNum: 1, pageSize: 100, status: s, queryDate: dateStr })
- })
- const results = await Promise.all(promises)
- let orders = []
- for (let i = 0; i < results.length; i++) {
- const res = results[i]
- if (res.code === 200 && res.data && res.data.records) {
- orders = orders.concat(res.data.records)
- }
- }
- allOrders.value = orders
- }
- catch (err) {
- console.error('[餐厅订单] 加载订单失败:', err)
- }
- }
- async function loadBusinessHours() {
- try {
- const res = await getBusinessHoursAll()
- if (res.code === 200) {
- businessHours.value = res.data || []
- }
- }
- catch (err) {
- console.error('[餐厅订单] 加载营业时段失败:', err)
- }
- }
- async function loadPauseStatus() {
- try {
- const res = await getPauseStatus()
- if (res.code === 200) {
- isPaused.value = res.data
- }
- }
- catch (err) {
- console.error('[餐厅订单] 加载暂停状态失败:', err)
- }
- }
- function refreshAll() {
- loadDashboard()
- loadOrders()
- }
- onMounted(() => {
- loadDashboard()
- loadOrders()
- loadBusinessHours()
- loadPauseStatus()
- connect()
- window.addEventListener('hotel-notification', handleHotelNotification)
- })
- onUnmounted(() => {
- disconnect()
- window.removeEventListener('hotel-notification', handleHotelNotification)
- })
- watch(connected, (newVal) => {
- if (newVal) {
- checkPendingOrders()
- }
- })
- watch(filterDate, () => {
- refreshAll()
- })
- function handleFilterSearch() {
- refreshAll()
- }
- function handleFilterReset() {
- filterDate.value = Date.now()
- refreshAll()
- }
- async function checkPendingOrders() {
- try {
- const res = await getOrderPage({ pageNum: 1, pageSize: 1, status: 1 })
- if (res.code === 200 && res.data && res.data.total > 0) {
- playSound('new-order')
- window.dispatchEvent(new CustomEvent('hotel-notification', {
- detail: { type: 'new-order', message: `您有 ${res.data.total} 个待接单订单`, level: 'info' },
- }))
- }
- }
- catch (err) {
- console.error('[餐厅订单] 检查待接单订单失败:', err)
- }
- }
- // ==================== Tab 1: 全部订单 ====================
- const pendingOrders = computed(() => {
- return allOrders.value.filter((o) => {
- return o.status === 1
- })
- })
- const inProgressOrders = computed(() => {
- return allOrders.value.filter((o) => {
- return o.status >= 2 && o.status <= 5
- })
- })
- const inProgressCount = computed(() => {
- return inProgressOrders.value.length
- })
- const rejectedOrders = computed(() => {
- return allOrders.value.filter((o) => {
- return o.status === 7
- })
- })
- function buildItemSummary(order) {
- if (order.items && order.items.length > 0) {
- const names = []
- for (let i = 0; i < order.items.length; i++) {
- names.push(order.items[i].dishName)
- }
- return names.join('、')
- }
- return ''
- }
- function formatWaitTime(createTime) {
- if (!createTime)
- return ''
- const now = new Date()
- const created = new Date(createTime)
- const diff = Math.floor((now - created) / 1000 / 60)
- if (diff <= 0)
- return '刚刚'
- if (diff < 60)
- return `等待 ${diff} 分钟`
- const hours = Math.floor(diff / 60)
- const mins = diff % 60
- return `等待 ${hours} 小时${mins > 0 ? `${mins} 分钟` : ''}`
- }
- // ==================== Tab 2: 订单看板 ====================
- function getKanbanOrders(key) {
- let col = null
- for (let i = 0; i < kanbanColumns.length; i++) {
- if (kanbanColumns[i].key === key) {
- col = kanbanColumns[i]
- break
- }
- }
- if (!col)
- return []
- return allOrders.value.filter((o) => {
- return col.statuses.includes(o.status)
- })
- }
- function getKanbanCount(key) {
- return getKanbanOrders(key).length
- }
- function formatShortTime(createTime) {
- if (!createTime)
- return ''
- const d = new Date(createTime)
- const h = String(d.getHours()).padStart(2, '0')
- const m = String(d.getMinutes()).padStart(2, '0')
- return `${h}:${m}`
- }
- // ==================== Tab 3: 营业时间 ====================
- const mealIconMap = { breakfast: '', lunch: '☀️', dinner: '🌙', night: '🌜' }
- const mealLabelMap = { breakfast: '早餐时段', lunch: '午餐时段', dinner: '晚餐时段', night: '夜宵时段' }
- const mealTypeOptions = [
- { label: '早餐', value: 'breakfast' },
- { label: '午餐', value: 'lunch' },
- { label: '晚餐', value: 'dinner' },
- { label: '夜宵', value: 'night' },
- ]
- const weekDayOptions = [
- { label: '周一', value: 1 },
- { label: '周二', value: 2 },
- { label: '周三', value: 3 },
- { label: '周四', value: 4 },
- { label: '周五', value: 5 },
- { label: '周六', value: 6 },
- { label: '周日', value: 0 },
- ]
- const timeOptions = computed(() => {
- const options = []
- for (let h = 6; h <= 23; h++) {
- for (let m = 0; m < 60; m += 30) {
- const label = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`
- options.push({ label, value: label })
- }
- }
- return options
- })
- const sortedBusinessHours = computed(() => {
- return businessHours.value.slice().sort((a, b) => {
- return (a.sortOrder || 0) - (b.sortOrder || 0)
- })
- })
- const currentStatusText = computed(() => {
- if (isPaused.value)
- return '已暂停接单'
- return '正常营业中'
- })
- // 营业时段编辑弹窗
- const hoursModalVisible = ref(false)
- const hoursModalTitle = ref('')
- const hoursSaving = ref(false)
- const hoursForm = ref({
- id: null,
- mealType: '',
- mealName: '',
- dayOfWeekArr: [],
- dayOfWeek: null,
- startTime: '',
- endTime: '',
- sortOrder: 0,
- })
- function openAddHours() {
- hoursModalTitle.value = '添加营业时段'
- hoursForm.value = {
- id: null,
- mealType: '',
- mealName: '',
- dayOfWeekArr: [],
- dayOfWeek: null,
- startTime: '',
- endTime: '',
- sortOrder: 0,
- }
- hoursModalVisible.value = true
- }
- function openEditHours(bh) {
- hoursModalTitle.value = '编辑营业时段'
- let dayArr = []
- if (bh.dayOfWeek) {
- dayArr = String(bh.dayOfWeek).split(',').map((v) => {
- return Number(v.trim())
- })
- }
- hoursForm.value = {
- id: bh.id,
- mealType: bh.mealType || '',
- mealName: bh.mealName || '',
- dayOfWeekArr: dayArr,
- dayOfWeek: bh.dayOfWeek || null,
- startTime: bh.startTime || '',
- endTime: bh.endTime || '',
- sortOrder: bh.sortOrder || 0,
- }
- hoursModalVisible.value = true
- }
- function onMealTypeChange(value) {
- if (value && mealLabelMap[value] && !hoursForm.value.mealName) {
- hoursForm.value.mealName = mealLabelMap[value]
- }
- }
- async function confirmSaveHours() {
- const form = hoursForm.value
- if (!form.mealType) {
- message.warning('请选择餐段类型')
- return
- }
- if (!form.mealName) {
- message.warning('请输入餐段名称')
- return
- }
- if (!form.startTime) {
- message.warning('请选择开始时间')
- return
- }
- if (!form.endTime) {
- message.warning('请选择结束时间')
- return
- }
- const data = {
- mealType: form.mealType,
- mealName: form.mealName,
- dayOfWeek: form.dayOfWeekArr && form.dayOfWeekArr.length > 0 ? form.dayOfWeekArr.join(',') : null,
- startTime: form.startTime,
- endTime: form.endTime,
- sortOrder: form.sortOrder || 0,
- }
- hoursSaving.value = true
- try {
- let res
- if (form.id) {
- data.id = form.id
- res = await updateBusinessHours(data)
- }
- else {
- res = await createBusinessHours(data)
- }
- if (res.code === 200) {
- message.success(form.id ? '修改成功' : '添加成功')
- hoursModalVisible.value = false
- loadBusinessHours()
- }
- else {
- message.error(res.msg || '操作失败')
- }
- }
- catch (err) {
- message.error('操作失败')
- console.error('[餐厅订单] 保存营业时段失败:', err)
- }
- finally {
- hoursSaving.value = false
- }
- }
- async function handleToggleHours(bh) {
- try {
- await toggleBusinessHours(bh.id)
- message.success(bh.status === 'ENABLED' ? '已禁用' : '已启用')
- loadBusinessHours()
- }
- catch (err) {
- message.error('操作失败')
- console.error('[餐厅订单] 切换营业时段状态失败:', err)
- }
- }
- function handleDeleteHours(bh) {
- dialog.warning({
- title: '确认删除',
- content: `确定要删除营业时段「${bh.mealName || bh.mealType}」吗?`,
- positiveText: '删除',
- negativeText: '取消',
- async onPositiveClick() {
- try {
- await deleteBusinessHours(bh.id)
- message.success('已删除')
- loadBusinessHours()
- }
- catch (err) {
- message.error('删除失败')
- console.error('[餐厅订单] 删除营业时段失败:', err)
- }
- },
- })
- }
- // ==================== 暂停接单 ====================
- async function handleTogglePause() {
- const willPause = isPaused.value
- const title = willPause ? '暂停接单' : '恢复接单'
- const content = willPause
- ? '暂停后顾客扫码将无法下单,确定要暂停接单吗?'
- : '恢复后顾客可以正常扫码下单,确定要恢复接单吗?'
- dialog.warning({
- title,
- content,
- positiveText: '确定',
- negativeText: '取消',
- async onPositiveClick() {
- try {
- const res = await togglePause()
- if (res.code === 200) {
- isPaused.value = res.data
- message.success(isPaused.value ? '已暂停接单' : '已恢复接单')
- }
- }
- catch (err) {
- message.error('操作失败')
- console.error('[餐厅订单] 切换暂停状态失败:', err)
- }
- },
- onNegativeClick() {
- isPaused.value = !willPause
- },
- })
- }
- // ==================== 订单操作 ====================
- const reasonVisible = ref(false)
- const reasonText = ref('')
- let rejectOrderId = null
- function handleAccept(order) {
- dialog.warning({
- title: '确认接单',
- content: `确认接受订单 ${order.orderNo} 吗?`,
- positiveText: '确认接单',
- negativeText: '取消',
- async onPositiveClick() {
- try {
- await acceptOrder(order.id)
- message.success('接单成功')
- refreshAll()
- }
- catch (err) {
- message.error('接单失败')
- console.error('[餐厅订单] 接单失败:', err)
- }
- },
- })
- }
- function handleReject(order) {
- rejectOrderId = order.id
- reasonText.value = ''
- reasonVisible.value = true
- }
- async function handleReasonConfirm() {
- if (rejectOrderId) {
- try {
- await rejectOrder(rejectOrderId, reasonText.value || undefined)
- message.success('已拒单')
- }
- catch (err) {
- message.error('拒单失败')
- console.error('[餐厅订单] 拒单失败:', err)
- }
- rejectOrderId = null
- refreshAll()
- }
- }
- function handlePrepare(order) {
- dialog.warning({
- title: '开始备餐',
- content: `确认开始备餐 ${order.orderNo}?`,
- positiveText: '确认',
- negativeText: '取消',
- async onPositiveClick() {
- try {
- await prepareOrder(order.id)
- message.success('已开始备餐')
- refreshAll()
- }
- catch (err) {
- void err
- message.error('操作失败')
- }
- },
- })
- }
- function handleReady(order) {
- dialog.warning({
- title: '出餐完成',
- content: `确认订单 ${order.orderNo} 出餐完成?`,
- positiveText: '确认出餐',
- negativeText: '取消',
- async onPositiveClick() {
- try {
- await readyOrder(order.id)
- message.success('已出餐')
- refreshAll()
- }
- catch (err) {
- void err
- message.error('操作失败')
- }
- },
- })
- }
- function handleDeliver(order) {
- dialog.warning({
- title: '开始配送',
- content: `确认开始配送订单 ${order.orderNo}?`,
- positiveText: '确认配送',
- negativeText: '取消',
- async onPositiveClick() {
- try {
- await deliverOrder(order.id)
- message.success('已开始配送')
- refreshAll()
- }
- catch (err) {
- void err
- message.error('操作失败')
- }
- },
- })
- }
- function handleComplete(order) {
- dialog.warning({
- title: '完成配送',
- content: `确认订单 ${order.orderNo} 配送完成?`,
- positiveText: '确认完成',
- negativeText: '取消',
- async onPositiveClick() {
- try {
- await completeOrder(order.id)
- message.success('订单已完成')
- refreshAll()
- }
- catch (err) {
- void err
- message.error('操作失败')
- }
- },
- })
- }
- // ==================== 订单详情弹窗 ====================
- const showDetail = ref(false)
- const detailOrder = ref(null)
- function openDetail(order) {
- detailOrder.value = order
- showDetail.value = true
- }
- function formatDetailTime(dateStr) {
- if (!dateStr) return '--'
- const d = new Date(dateStr)
- return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
- }
- function formatDateTime(dateStr) {
- if (!dateStr) return '--'
- const d = new Date(dateStr)
- const hh = String(d.getHours()).padStart(2, '0')
- const mm = String(d.getMinutes()).padStart(2, '0')
- const ss = String(d.getSeconds()).padStart(2, '0')
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${hh}:${mm}:${ss}`
- }
- function handleAcceptFromDetail() {
- if (!detailOrder.value) return
- const order = detailOrder.value
- dialog.warning({
- title: '确认接单',
- content: `确认接受订单 ${order.orderNo} 吗?`,
- positiveText: '确认接单',
- negativeText: '取消',
- async onPositiveClick() {
- try {
- await acceptOrder(order.id)
- message.success('接单成功')
- showDetail.value = false
- refreshAll()
- }
- catch (err) {
- message.error('接单失败')
- console.error('[餐厅订单] 接单失败:', err)
- }
- },
- })
- }
- function handleRejectFromDetail() {
- if (!detailOrder.value) return
- rejectOrderId = detailOrder.value.id
- reasonText.value = ''
- reasonVisible.value = true
- }
- function handleDeliverFromDetail() {
- if (!detailOrder.value) return
- const order = detailOrder.value
- dialog.warning({
- title: '开始配送',
- content: `确认开始配送订单 ${order.orderNo}?`,
- positiveText: '确认配送',
- negativeText: '取消',
- async onPositiveClick() {
- try {
- await deliverOrder(order.id)
- message.success('已开始配送')
- showDetail.value = false
- refreshAll()
- }
- catch (err) {
- message.error('操作失败')
- console.error('[餐厅订单] 配送失败:', err)
- }
- },
- })
- }
- function handleCompleteFromDetail() {
- if (!detailOrder.value) return
- const order = detailOrder.value
- dialog.warning({
- title: '完成配送',
- content: `确认订单 ${order.orderNo} 配送完成?`,
- positiveText: '确认完成',
- negativeText: '取消',
- async onPositiveClick() {
- try {
- await completeOrder(order.id)
- message.success('订单已完成')
- showDetail.value = false
- refreshAll()
- }
- catch (err) {
- message.error('操作失败')
- console.error('[餐厅订单] 完成失败:', err)
- }
- },
- })
- }
- </script>
- <style scoped>
- .restaurant-order-page {
- height: 100%;
- display: flex;
- flex-direction: column;
- }
- .restaurant-order-page :deep(.n-tabs) {
- height: 100%;
- display: flex;
- flex-direction: column;
- }
- .restaurant-order-page :deep(.n-tabs-pane-wrapper) {
- flex: 1;
- min-height: 0;
- }
- .tab-content {
- height: 100%;
- overflow-y: auto;
- padding: 16px;
- }
- /* ==================== 筛选栏 ==================== */
- .filter-form {
- background: #fff;
- border-radius: 8px;
- padding: 12px 16px;
- border: 1px solid #f0f0f0;
- }
- /* ==================== 统计卡片 ==================== */
- .stats-row {
- display: flex;
- align-items: center;
- gap: 16px;
- margin-bottom: 20px;
- }
- .stat-card {
- flex: 1;
- max-width: 200px;
- background: #fff;
- border-radius: 10px;
- padding: 16px 20px;
- text-align: center;
- border: 1px solid #f0f0f0;
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
- }
- .stat-value {
- display: block;
- font-size: 28px;
- font-weight: 700;
- line-height: 1.2;
- }
- .stat-label {
- display: block;
- font-size: 13px;
- color: #999;
- margin-top: 4px;
- }
- .stat-pending .stat-value {
- color: #d03050;
- }
- .stat-progress .stat-value {
- color: #f0a020;
- }
- .stat-completed .stat-value {
- color: #18a058;
- }
- .stat-rejected .stat-value {
- color: #999;
- }
- .sse-indicator {
- display: flex;
- align-items: center;
- margin-left: 4px;
- }
- .sse-dot {
- display: inline-block;
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background: #18a058;
- animation: sse-pulse 2s ease-in-out infinite;
- }
- @keyframes sse-pulse {
- 0%,
- 100% {
- opacity: 1;
- }
- 50% {
- opacity: 0.4;
- }
- }
- /* ==================== 订单分组列表 ==================== */
- .order-section {
- margin-bottom: 20px;
- }
- .section-title {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 15px;
- font-weight: 600;
- color: #333;
- margin: 0 0 10px 0;
- }
- .section-dot {
- display: inline-block;
- width: 8px;
- height: 8px;
- border-radius: 50%;
- }
- .section-dot.pending {
- background: #d03050;
- }
- .section-dot.progress {
- background: #f0a020;
- }
- .section-dot.rejected {
- background: #999;
- }
- .section-count {
- font-size: 12px;
- color: #999;
- font-weight: 400;
- }
- .order-list {
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
- .order-row {
- display: flex;
- align-items: center;
- background: #fff;
- border-radius: 8px;
- padding: 12px 16px;
- border: 1px solid #f0f0f0;
- border-left: 3px solid #e8e8ec;
- gap: 16px;
- transition: box-shadow 0.2s;
- }
- .order-row:hover {
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
- }
- .order-row-pending {
- border-left-color: #d03050;
- background: #fffafa;
- }
- .order-row-rejected {
- border-left-color: #999;
- background: #fafafa;
- opacity: 0.85;
- }
- .order-room {
- font-size: 20px;
- font-weight: 700;
- color: #333;
- min-width: 50px;
- text-align: center;
- background: #f5f5f5;
- border-radius: 6px;
- padding: 6px 10px;
- }
- .order-info {
- flex: 1;
- min-width: 0;
- }
- .order-no {
- font-size: 12px;
- color: #999;
- margin-bottom: 2px;
- }
- .order-items {
- font-size: 14px;
- color: #333;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .order-meta {
- display: flex;
- align-items: center;
- gap: 12px;
- margin-top: 4px;
- }
- .order-time {
- font-size: 12px;
- color: #f0a020;
- }
- .order-amount {
- font-size: 14px;
- font-weight: 600;
- color: #333;
- }
- .order-status-tag {
- display: inline-flex;
- }
- .order-actions {
- display: flex;
- gap: 8px;
- flex-shrink: 0;
- }
- .empty-state {
- text-align: center;
- padding: 60px 0;
- font-size: 14px;
- }
- /* ==================== 看板 ==================== */
- .kanban-content {
- padding: 0;
- }
- .kanban-board {
- display: flex;
- gap: 12px;
- height: 100%;
- overflow-x: auto;
- padding: 16px;
- }
- .kanban-column {
- flex: 1;
- min-width: 220px;
- max-width: 280px;
- background: #f7f8fa;
- border-radius: 8px;
- display: flex;
- flex-direction: column;
- max-height: 100%;
- }
- .kanban-header {
- padding: 10px 12px;
- border-top: 3px solid;
- display: flex;
- align-items: center;
- gap: 8px;
- flex-shrink: 0;
- }
- .kanban-count {
- font-size: 18px;
- font-weight: 700;
- }
- .kanban-title {
- font-size: 14px;
- font-weight: 600;
- color: #333;
- }
- .kanban-body {
- flex: 1;
- overflow-y: auto;
- padding: 8px;
- display: flex;
- flex-direction: column;
- gap: 8px;
- min-height: 0;
- }
- .kanban-empty {
- text-align: center;
- padding: 20px 0;
- }
- .kanban-card {
- background: #fff;
- border-radius: 8px;
- padding: 10px 12px;
- border: 1px solid #e8e8ec;
- border-top: 3px solid;
- transition:
- box-shadow 0.2s,
- transform 0.15s;
- }
- .kanban-card:hover {
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
- transform: translateY(-1px);
- }
- .kc-room {
- font-size: 16px;
- font-weight: 700;
- color: #333;
- }
- .kc-no {
- margin-top: 2px;
- }
- .kc-items {
- margin-top: 6px;
- color: #555;
- line-height: 1.4;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
- }
- .kc-bottom {
- margin-top: 8px;
- padding-top: 6px;
- border-top: 1px dashed #eee;
- }
- .kc-amount {
- font-weight: 600;
- color: #333;
- }
- .kc-actions {
- margin-top: 8px;
- display: flex;
- flex-wrap: wrap;
- gap: 4px;
- }
- /* ==================== 营业时间 ==================== */
- .hours-content {
- max-width: 720px;
- }
- .hours-section-title {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 15px;
- font-weight: 600;
- color: #333;
- margin: 0 0 12px 0;
- }
- .hours-list {
- display: flex;
- flex-direction: column;
- gap: 10px;
- }
- .hours-card {
- display: flex;
- align-items: center;
- justify-content: space-between;
- background: #fff;
- border-radius: 10px;
- padding: 14px 18px;
- border: 1px solid #f0f0f0;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
- }
- .hours-icon {
- font-size: 28px;
- line-height: 1;
- }
- .hours-name {
- font-size: 15px;
- font-weight: 600;
- color: #333;
- }
- .hours-time {
- font-size: 13px;
- color: #999;
- margin-top: 2px;
- }
- .hours-empty {
- text-align: center;
- }
- /* 紧急控制 */
- .emergency-card {
- background: #fff;
- border-radius: 10px;
- padding: 16px 18px;
- border: 1px solid #f0f0f0;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
- }
- .emergency-title {
- font-size: 14px;
- font-weight: 600;
- color: #333;
- }
- .emergency-desc {
- font-size: 12px;
- color: #999;
- margin-top: 2px;
- }
- /* ==================== 小票详情弹窗 ==================== */
- .order-detail-modal :deep(.n-card__content) {
- padding: 0 !important;
- }
- .receipt-full {
- background: #f5f7fa;
- min-height: 500px;
- }
- .receipt-header-bar {
- background: linear-gradient(135deg, #1a6b8c 0%, #0d526e);
- color: white;
- }
- .room-hero-section {
- padding-top: 12px;
- }
- .room-icon-wrapper {
- box-shadow: 0 4px 12px rgba(32, 128, 240, 0.15);
- }
- .room-number {
- letter-spacing: 2px;
- text-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
- }
- .receipt-body {
- background: #fff;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
- }
- .receipt-title {
- font-family: 'Microsoft YaHei', sans-serif;
- }
- .receipt-info {
- line-height: 1.8;
- }
- .receipt-item-row {
- padding-bottom: 12px;
- margin-bottom: 12px;
- border-bottom: 1px dashed #e5e7eb;
- }
- .receipt-item-row:last-child {
- border-bottom: none;
- margin-bottom: 0;
- padding-bottom: 0;
- }
- .item-index {
- font-weight: 600;
- color: #f0a020;
- }
- .item-name {
- font-size: 15px;
- }
- .item-price {
- font-size: 15px;
- }
- .item-details {
- line-height: 1.6;
- }
- .item-note {
- line-height: 1.6;
- }
- .receipt-total {
- padding-top: 16px;
- border-top: 2px solid #e5e7eb;
- font-size: 18px;
- }
- .receipt-actions {
- margin-top: 8px;
- }
- .action-btn-primary {
- height: 52px;
- font-size: 16px;
- font-weight: 600;
- border-radius: 12px;
- transition: all 0.3s ease;
- }
- .action-btn-primary:hover {
- transform: translateY(-2px);
- box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
- }
- .action-btn-primary:active {
- transform: translateY(0);
- }
- .status-hint {
- padding: 12px 0;
- }
- </style>
|