Compare commits
2 Commits
feat/kit-e
...
feat/oil-p
| Author | SHA1 | Date | |
|---|---|---|---|
| 2dca4d13b9 | |||
| a28ba1ef57 |
28
frontend/cypress/e2e/oil-price-export.cy.js
Normal file
28
frontend/cypress/e2e/oil-price-export.cy.js
Normal file
@@ -0,0 +1,28 @@
|
||||
describe('Oil Reference Excel Export', () => {
|
||||
let adminToken
|
||||
|
||||
before(() => {
|
||||
cy.getAdminToken().then(token => { adminToken = token })
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('/oils', {
|
||||
onBeforeLoad(win) {
|
||||
win.localStorage.setItem('oil_auth_token', adminToken)
|
||||
}
|
||||
})
|
||||
cy.get('.oil-chip, .oils-grid', { timeout: 10000 }).should('exist')
|
||||
})
|
||||
|
||||
it('shows the Excel export button for admins', () => {
|
||||
cy.contains('button', '📥 导出Excel').should('be.visible')
|
||||
})
|
||||
|
||||
it('clicking Excel export shows success toast', () => {
|
||||
cy.window().then(win => {
|
||||
cy.stub(win.HTMLAnchorElement.prototype, 'click').returns(undefined)
|
||||
})
|
||||
cy.contains('button', '📥 导出Excel').click()
|
||||
cy.get('.toast', { timeout: 10000 }).should('contain', '导出成功')
|
||||
})
|
||||
})
|
||||
@@ -99,10 +99,10 @@
|
||||
</div>
|
||||
<!-- Desktop: text buttons -->
|
||||
<button v-if="auth.canManage" class="toolbar-btn-text" @click="showAddForm = !showAddForm">{{ showAddForm ? '收起' : '+ 新增' }}</button>
|
||||
<button v-if="auth.isAdmin" class="toolbar-btn-text" @click="exportPDF">📥 导出PDF</button>
|
||||
<button v-if="auth.isAdmin" class="toolbar-btn-text" @click="exportExcel">📥 导出Excel</button>
|
||||
<!-- Mobile: emoji-only buttons -->
|
||||
<button v-if="auth.canManage" class="toolbar-btn-icon" @click="showAddForm = !showAddForm" title="新增精油">➕</button>
|
||||
<button v-if="auth.isAdmin" class="toolbar-btn-icon" @click="exportPDF" title="导出PDF">📄</button>
|
||||
<button v-if="auth.isAdmin" class="toolbar-btn-icon" @click="exportExcel" title="导出Excel">📄</button>
|
||||
</div>
|
||||
|
||||
<!-- Add Oil Form (toggleable) -->
|
||||
@@ -891,66 +891,38 @@ async function removeOil(name) {
|
||||
}
|
||||
}
|
||||
|
||||
// PDF Export
|
||||
function exportPDF() {
|
||||
// Excel Export
|
||||
async function exportExcel() {
|
||||
const XLSX = (await import('xlsx')).default || await import('xlsx')
|
||||
const today = new Date()
|
||||
const dateStr = today.getFullYear() + String(today.getMonth()+1).padStart(2,'0') + String(today.getDate()).padStart(2,'0')
|
||||
const title = '精油价目表' + dateStr
|
||||
const dateStr = today.getFullYear() + '-' + String(today.getMonth()+1).padStart(2,'0') + '-' + String(today.getDate()).padStart(2,'0')
|
||||
|
||||
const sortedNames = [...oils.oilNames].sort((a, b) => a.localeCompare(b, 'zh'))
|
||||
let rows = ''
|
||||
const rows = []
|
||||
for (const name of sortedNames) {
|
||||
const meta = getMeta(name)
|
||||
if (!meta) continue
|
||||
const en = getEnglishName(name)
|
||||
const bp = meta.bottlePrice != null ? '¥' + meta.bottlePrice.toFixed(0) : '--'
|
||||
const rp = meta.retailPrice != null ? '¥' + meta.retailPrice.toFixed(0) : '--'
|
||||
const vol = volumeLabel(meta.dropCount, name)
|
||||
const unit = oilPriceUnit(name)
|
||||
const ppd = oils.pricePerDrop(name) ? '¥' + oils.pricePerDrop(name).toFixed(2) + '/' + unit : '--'
|
||||
rows += `<tr>
|
||||
<td>${name}</td>
|
||||
<td>${en}</td>
|
||||
<td>${bp}</td>
|
||||
<td>${rp}</td>
|
||||
<td>${vol}</td>
|
||||
<td>${ppd}</td>
|
||||
</tr>`
|
||||
const ppdNum = oils.pricePerDrop(name)
|
||||
rows.push({
|
||||
'精油': name,
|
||||
'英文名': en,
|
||||
'会员价': meta.bottlePrice != null ? Number(meta.bottlePrice.toFixed(2)) : '',
|
||||
'零售价': meta.retailPrice != null ? Number(meta.retailPrice.toFixed(2)) : '',
|
||||
'容量': vol,
|
||||
'单价': ppdNum ? `¥${ppdNum.toFixed(2)}/${unit}` : '',
|
||||
'状态': meta.isActive === false ? '下架' : '在售',
|
||||
})
|
||||
}
|
||||
const html = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>${title}</title>
|
||||
<style>
|
||||
body { font-family: 'PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif; padding: 20px; font-size: 11px; color: #333; }
|
||||
h1 { font-size: 18px; text-align: center; margin-bottom: 16px; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th { background: #7a9e7e; color: white; padding: 6px 8px; text-align: center; font-size: 11px; font-weight: 600; }
|
||||
td { padding: 5px 8px; border-bottom: 1px solid #e0e0e0; text-align: center; font-size: 11px; }
|
||||
td:first-child, th:first-child { text-align: left; font-weight: 500; }
|
||||
td:nth-child(2), th:nth-child(2) { text-align: left; }
|
||||
tr:nth-child(even) { background: #f9f9f9; }
|
||||
tr:hover { background: #e8f5e9; }
|
||||
@media print { body { padding: 10px; } h1 { font-size: 16px; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>doTERRA 精油价目表 ${dateStr}</h1>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>精油</th><th>英文名</th><th>会员价</th><th>零售价</th><th>容量</th><th>单价</th></tr>
|
||||
</thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
<p style="text-align:center;font-size:10px;color:#aaa;margin-top:12px">共 ${sortedNames.length} 种精油 · doTERRA 配方计算器导出</p>
|
||||
</body>
|
||||
</html>`
|
||||
const w = window.open('', '_blank')
|
||||
w.document.write(html)
|
||||
w.document.close()
|
||||
w.document.title = title
|
||||
setTimeout(() => w.print(), 500)
|
||||
|
||||
const ws = XLSX.utils.json_to_sheet(rows)
|
||||
ws['!cols'] = [{ wch: 16 }, { wch: 28 }, { wch: 10 }, { wch: 10 }, { wch: 12 }, { wch: 16 }, { wch: 8 }]
|
||||
const wb = XLSX.utils.book_new()
|
||||
XLSX.utils.book_append_sheet(wb, ws, '精油价目表')
|
||||
XLSX.writeFile(wb, `精油价目表${dateStr}.xlsx`)
|
||||
ui.showToast('导出成功')
|
||||
}
|
||||
|
||||
// ──── Save image logic (identical to RecipeDetailOverlay) ────
|
||||
|
||||
Reference in New Issue
Block a user