From a28ba1ef578c519071e483c9f9656c67f362636a Mon Sep 17 00:00:00 2001 From: Hera Zhao Date: Tue, 14 Apr 2026 22:31:15 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E7=B2=BE=E6=B2=B9=E4=BB=B7?= =?UTF-8?q?=E7=9B=AE=E5=AF=BC=E5=87=BA=E6=94=B9=E4=B8=BA=20Excel=EF=BC=88.?= =?UTF-8?q?xlsx=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 按钮从「导出PDF」改「导出Excel」,动态 import xlsx - 列:精油/英文名/会员价/零售价/容量/单价/状态 - 文件名:精油价目表YYYY-MM-DD.xlsx - 新增 e2e(按钮可见 + 点击出 toast) Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/cypress/e2e/oil-price-export.cy.js | 28 ++++++++ frontend/src/views/OilReference.vue | 76 +++++++-------------- 2 files changed, 52 insertions(+), 52 deletions(-) create mode 100644 frontend/cypress/e2e/oil-price-export.cy.js 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..3728fd2 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, + [`单价(¥/${unit})`]: ppdNum ? Number(ppdNum.toFixed(2)) : '', + '状态': 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: 14 }, { 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) ──── From 2dca4d13b9eeff24c2dde7015fbabe13ab68f22b Mon Sep 17 00:00:00 2001 From: Hera Zhao Date: Tue, 14 Apr 2026 22:37:57 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E5=AF=BC=E5=87=BA=20Excel=20?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E3=80=8C=E5=8D=95=E4=BB=B7=E3=80=8D=E4=B8=BA?= =?UTF-8?q?=E4=B8=80=E5=88=97=EF=BC=8C=E5=80=BC=E5=BD=A2=E5=A6=82=20=C2=A5?= =?UTF-8?q?X.XX/ml|g|=E9=A2=97|=E6=BB=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/views/OilReference.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/OilReference.vue b/frontend/src/views/OilReference.vue index 3728fd2..df6a3a2 100644 --- a/frontend/src/views/OilReference.vue +++ b/frontend/src/views/OilReference.vue @@ -912,13 +912,13 @@ async function exportExcel() { '会员价': meta.bottlePrice != null ? Number(meta.bottlePrice.toFixed(2)) : '', '零售价': meta.retailPrice != null ? Number(meta.retailPrice.toFixed(2)) : '', '容量': vol, - [`单价(¥/${unit})`]: ppdNum ? Number(ppdNum.toFixed(2)) : '', + '单价': ppdNum ? `¥${ppdNum.toFixed(2)}/${unit}` : '', '状态': meta.isActive === false ? '下架' : '在售', }) } const ws = XLSX.utils.json_to_sheet(rows) - ws['!cols'] = [{ wch: 16 }, { wch: 28 }, { wch: 10 }, { wch: 10 }, { wch: 12 }, { wch: 14 }, { wch: 8 }] + 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`)