diff --git a/frontend/cypress/e2e/oil-price-export.cy.js b/frontend/cypress/e2e/oil-price-export.cy.js new file mode 100644 index 0000000..1dbaa90 --- /dev/null +++ b/frontend/cypress/e2e/oil-price-export.cy.js @@ -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', '导出成功') + }) +}) diff --git a/frontend/src/views/OilReference.vue b/frontend/src/views/OilReference.vue index b371b2e..df6a3a2 100644 --- a/frontend/src/views/OilReference.vue +++ b/frontend/src/views/OilReference.vue @@ -99,10 +99,10 @@ - + - + @@ -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 += ` - ${name} - ${en} - ${bp} - ${rp} - ${vol} - ${ppd} - ` + 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 = ` - - - -${title} - - - -

doTERRA 精油价目表 ${dateStr}

- - - - - ${rows} -
精油英文名会员价零售价容量单价
-

共 ${sortedNames.length} 种精油 · doTERRA 配方计算器导出

- -` - 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) ────