656 lines
16 KiB
Vue
Raw Normal View History

2025-08-04 09:59:12 +08:00
<template>
<div class="app-container">
<div class="content-wrapper">
<!-- 操作区域 -->
<div class="operation-header">
<h2>我的酒单</h2>
<div class="operation-buttons">
<el-button type="primary" :icon="Plus" @click="showAddBeerDialog">
新酒上枪
</el-button>
<el-button type="success" :icon="Download" @click="showExportMenuDialog">
导出酒单
</el-button>
<el-button type="info" :icon="Refresh" @click="refreshData">
刷新
</el-button>
</div>
</div>
<!-- 在售酒款列表 -->
<div class="tap-list" v-loading="loading">
<div v-if="tapList.length === 0" class="empty-state">
<el-empty description="暂无在售酒款" />
</div>
<div v-else class="tap-items">
<div v-for="item in tapList" :key="item.id" class="tap-item">
<!-- 酒头编号 -->
<div class="tap-number">
<span class="tap-badge">{{ item.tapNo }}</span>
</div>
<!-- 酒款信息 -->
<div class="beer-info">
<div class="beer-image">
<img
:src="item.beerCover || '/images/default-beer.png'"
:alt="item.beerName"
/>
</div>
<div class="beer-details">
<h3 class="beer-name">{{ item.beerName }}</h3>
<p class="beer-brand">{{ item.brandName }}</p>
<p class="beer-style">
{{ item.beerStyles || item.customStyle }}
</p>
<p class="beer-abv">ABV {{ item.beerAbv }}%</p>
</div>
</div>
<!-- 规格价格 -->
<div class="specs-info">
<div
v-for="spec in item.specList"
:key="spec.id"
class="spec-item"
>
<span class="spec-name">{{ spec.specName }}</span>
<span class="spec-price">{{ spec.specPrice }}</span>
</div>
</div>
<!-- 操作按钮 -->
<div class="actions">
<el-button size="small" type="primary" @click="editSpecs(item)" class="action-btn">
修改规格
</el-button>
<el-button size="small" type="success" @click="replaceBeer(item)" class="action-btn">
替换
</el-button>
<el-button size="small" type="warning" @click="offTap(item)" class="action-btn">
下架
</el-button>
</div>
</div>
</div>
</div>
</div>
<!-- 新酒上枪对话框 -->
<el-dialog
v-model="addBeerDialog.visible"
title="新酒上枪"
width="1000px"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<BeerSelectionWizard
@submit="handleAddBeer"
@cancel="addBeerDialog.visible = false"
/>
</el-dialog>
<!-- 导出酒单对话框 -->
<el-dialog v-model="exportMenuDialog.visible" title="导出酒单" width="900px">
<MenuExportWizard
:tap-list="tapList"
@submit="handleExportMenu"
@cancel="exportMenuDialog.visible = false"
/>
</el-dialog>
<!-- 编辑规格价格对话框 -->
<el-dialog v-model="editSpecDialog.visible" title="编辑规格价格" width="500px">
<EditSpecForm
:tap-beer="editSpecDialog.data"
@submit="handleUpdateSpecs"
@cancel="editSpecDialog.visible = false"
/>
</el-dialog>
<!-- 替换酒款对话框 -->
<el-dialog v-model="replaceDialog.visible" title="替换酒款" width="500px">
<BeerSelectionWizard
@submit="handleReplaceBeer"
@cancel="replaceDialog.visible = false"
/>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Download, Refresh } from '@element-plus/icons-vue'
import {
getTapList,
addBeerToTap,
offTapBeer,
updateTapBeerSpecs
} from '@/api/barmgr/tap'
import { formatTime } from '@/utils/date-util'
import { generateWineMenu } from '@/api/template'
import BeerSelectionWizard from './components/BeerSelectionWizard.vue'
import EditSpecForm from './components/EditSpecForm.vue'
import MenuExportWizard from './components/MenuExportWizard.vue'
// 响应式数据
const loading = ref(false)
const tapList = ref([])
const addBeerDialog = reactive({
visible: false
})
const exportMenuDialog = reactive({
visible: false
})
const editSpecDialog = reactive({
visible: false,
data: null
})
const replaceDialog = reactive({
visible: false,
tapNo: null
})
// 获取在售酒款列表
const fetchTapList = async () => {
try {
loading.value = true
const response = await getTapList({ pageNum: 1, pageSize: 1000 })
console.log('API Response:', response)
// axios 返回的数据在 response.data 中
const responseData = response.data
console.log('Response Data:', responseData)
if (responseData && responseData.code === 200) {
// TableDataInfo 结构:{code: 200, msg: "查询成功", rows: [...], total: 1}
tapList.value = responseData.rows || []
console.log('Processed tapList:', tapList.value)
} else {
console.error('API Error:', responseData?.msg || '未知错误')
ElMessage.error('获取酒款列表失败: ' + (responseData?.msg || '未知错误'))
}
} catch (error) {
console.error('Fetch Error:', error)
ElMessage.error('获取酒款列表失败: ' + error.message)
} finally {
loading.value = false
}
}
// 刷新数据
const refreshData = async () => {
await fetchTapList()
ElMessage.success('数据刷新成功')
}
// 显示新酒上枪对话框
const showAddBeerDialog = () => {
addBeerDialog.visible = true
}
// 显示导出酒单对话框
const showExportMenuDialog = () => {
exportMenuDialog.visible = true
}
// 处理新酒上枪
const handleAddBeer = async (data) => {
try {
console.log('上枪请求数据:', data)
const response = await addBeerToTap(data)
console.log('上枪响应数据:', response)
// 检查不同的响应数据结构
if (response.data && response.data.code === 200) {
ElMessage.success('上枪成功')
addBeerDialog.visible = false
await refreshData()
} else if (response.code === 200) {
ElMessage.success('上枪成功')
addBeerDialog.visible = false
await refreshData()
} else {
const errorMsg = (response.data && response.data.msg) || response.msg || '上枪失败'
throw new Error(errorMsg)
}
} catch (error) {
console.error('上枪失败详情:', error)
ElMessage.error('上枪失败: ' + error.message)
}
}
// 处理导出酒单
const handleExportMenu = async (exportData) => {
try {
console.log('导出酒单数据:', exportData)
// 构建请求参数,按照整版酒单编辑器的数据结构要求
const params = {
uuid: exportData.template.uuid || exportData.template.posterId || String(exportData.template.id), // 优先使用uuid字段
format: exportData.format || 'PDF'
}
// 按照 beer{index}_{field} 格式传递酒款数据
exportData.generateParams.beerList.forEach((beer, index) => {
params[`beer${index}_name`] = beer.beerName || ''
params[`beer${index}_style`] = beer.beerStyles || ''
params[`beer${index}_abv`] = beer.beerAbv ? String(beer.beerAbv) : ''
params[`beer${index}_price`] = beer.specs && beer.specs[0] ? String(beer.specs[0].specPrice) : ''
params[`beer${index}_spec`] = beer.specs && beer.specs[0] ? beer.specs[0].specName : ''
params[`beer${index}_img`] = beer.cover || ''
params[`beer${index}_brewery_name`] = beer.brandName || ''
params[`beer${index}_tapno`] = beer.tapNo ? String(beer.tapNo) : ''
// 如果有多个规格,添加第二个规格
if (beer.specs && beer.specs[1]) {
params[`beer${index}_spec1`] = beer.specs[1].specName
params[`beer${index}_price1`] = String(beer.specs[1].specPrice)
}
})
console.log('发送给后端的参数:', params)
ElMessage({
message: '正在生成酒单,请稍候...',
type: 'info',
duration: 0
})
// 调用现有的整版酒单生成接口
const response = await generateWineMenu(params)
console.log('后端响应:', response)
if (response.data && response.data.code === 200 && response.data.data && response.data.data.image) {
const result = response.data
// 现有接口返回base64图片需要转换为可下载的文件
const base64Image = result.data.image
// 将base64转换为blob
const byteCharacters = atob(base64Image)
const byteNumbers = new Array(byteCharacters.length)
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i)
}
const byteArray = new Uint8Array(byteNumbers)
// 根据格式设置正确的MIME类型和文件扩展名
let mimeType = 'image/png'
let fileExtension = 'png'
if (result.data.format === 'base64_pdf') {
mimeType = 'application/pdf'
fileExtension = 'pdf'
}
const blob = new Blob([byteArray], { type: mimeType })
// 创建下载链接
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `酒单_${new Date().getTime()}.${fileExtension}`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
ElMessage.closeAll() // 关闭loading消息
if (result.data.format === 'base64_pdf') {
ElMessage.success('PDF酒单导出成功')
} else {
ElMessage.success('PNG酒单导出成功')
}
exportMenuDialog.visible = false
} else {
// axios响应错误处理
const errorMsg = response.data?.msg || response.msg || '生成失败'
throw new Error(errorMsg)
}
} catch (error) {
console.error('导出酒单失败:', error)
ElMessage.closeAll() // 关闭loading消息
ElMessage.error('导出酒单失败: ' + error.message)
}
}
// 下架酒款
const offTap = async (row) => {
try {
await ElMessageBox.confirm(
`确定要下架酒头 ${row.tapNo} 上的"${row.beerName}"吗?`,
'下架确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
const response = await offTapBeer(row.tapId)
console.log('Off tap response:', response)
// axios 返回的数据在 response.data 中
const responseData = response.data || response
if (responseData && responseData.code === 200) {
ElMessage.success('下架成功')
await refreshData()
} else {
throw new Error(responseData?.msg || '下架失败')
}
} catch (error) {
if (error !== 'cancel') {
console.error('Off tap error:', error)
ElMessage.error('下架失败: ' + (error.message || error))
}
}
}
// 编辑规格价格
const editSpecs = (row) => {
console.log('Edit specs for:', row)
editSpecDialog.data = { ...row }
editSpecDialog.visible = true
}
// 替换酒款
const replaceBeer = (row) => {
replaceDialog.tapNo = row.tapNo
replaceDialog.visible = true
}
// 处理替换酒款
const handleReplaceBeer = async (data) => {
try {
// 先下架当前酒款
const offResponse = await offTapBeer(data.tapId)
if (offResponse.code === 200) {
// 再上架新酒款
const addResponse = await addBeerToTap(data)
if (addResponse.code === 200) {
ElMessage.success('替换成功')
replaceDialog.visible = false
await refreshData()
} else {
throw new Error(addResponse.msg || '上架失败')
}
} else {
throw new Error(offResponse.msg || '下架失败')
}
} catch (error) {
ElMessage.error('替换失败: ' + error.message)
}
}
// 处理更新规格价格
const handleUpdateSpecs = async (data) => {
try {
console.log('Update specs data:', data)
const response = await updateTapBeerSpecs(data)
console.log('Update specs response:', response)
// axios 返回的数据在 response.data 中
const responseData = response.data || response
if (responseData && responseData.code === 200) {
ElMessage.success('价格更新成功')
editSpecDialog.visible = false
await refreshData()
} else {
throw new Error(responseData?.msg || '价格更新失败')
}
} catch (error) {
console.error('Update specs error:', error)
ElMessage.error('价格更新失败: ' + error.message)
}
}
// 组件挂载时获取数据
onMounted(() => {
refreshData()
})
</script>
<style scoped>
.app-container {
min-height: 100vh;
background: #f5f7fa;
padding: 20px 0;
}
.content-wrapper {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.operation-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px 32px;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
}
.operation-header h2 {
margin: 0;
color: #303133;
font-size: 20px;
font-weight: 600;
}
.operation-buttons {
display: flex;
gap: 12px;
}
.tap-list {
padding: 0;
}
.empty-state {
padding: 60px 32px;
text-align: center;
}
.tap-items {
padding: 0;
}
.tap-item {
display: flex;
align-items: center;
padding: 24px 32px;
border-bottom: 1px solid #f0f0f0;
transition: background-color 0.2s;
}
.tap-item:hover {
background-color: #fafafa;
}
.tap-item:last-child {
border-bottom: none;
}
.tap-number {
margin-right: 24px;
}
.tap-badge {
display: inline-block;
width: 48px;
height: 48px;
line-height: 48px;
text-align: center;
background: #409eff;
color: white;
border-radius: 50%;
font-weight: bold;
font-size: 14px;
}
.beer-info {
display: flex;
align-items: center;
flex: 1;
margin-right: 24px;
}
.beer-image {
margin-right: 16px;
}
.beer-image img {
width: 60px;
height: 90px;
border-radius: 8px;
object-fit: cover;
border: 2px solid #f0f0f0;
}
.beer-details {
flex: 1;
}
.beer-name {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
color: #303133;
line-height: 1.4;
}
.beer-brand {
margin: 0 0 2px 0;
font-size: 14px;
color: #606266;
line-height: 1.4;
}
.beer-style {
margin: 0 0 2px 0;
font-size: 12px;
color: #67c23a;
line-height: 1.4;
}
.beer-abv {
margin: 0;
font-size: 12px;
color: #909399;
line-height: 1.4;
}
.specs-info {
margin-right: 24px;
min-width: 120px;
}
.spec-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding: 8px 12px;
background: #f8f9fa;
border-radius: 6px;
font-size: 14px;
width: 200px;
margin-right: 100px;
}
.spec-item:last-child {
margin-bottom: 0;
}
.spec-name {
font-size: 14px;
color: #606266;
font-weight: 500;
}
.spec-price {
font-size: 16px;
font-weight: 600;
color: #e6a23c;
}
.actions {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 80px;
}
.actions .el-button {
width: 100%;
height: 32px;
font-size: 12px;
}
.action-btn {
width: 100% !important;
height: 32px !important;
font-size: 12px !important;
margin: 0 !important;
}
/* 响应式设计 */
@media (max-width: 768px) {
.content-wrapper {
margin: 0 16px;
border-radius: 8px;
}
.operation-header {
padding: 16px 20px;
flex-direction: column;
gap: 16px;
align-items: stretch;
}
.operation-buttons {
justify-content: center;
}
.tap-item {
padding: 16px 20px;
flex-direction: column;
align-items: stretch;
gap: 16px;
}
.tap-number {
margin-right: 0;
text-align: center;
}
.beer-info {
margin-right: 0;
justify-content: center;
}
.specs-info {
margin-right: 0;
min-width: auto;
}
.actions {
flex-direction: row;
justify-content: center;
}
}
</style>