From 854d98c9974bb111ef151a569d75f540102912e2 Mon Sep 17 00:00:00 2001 From: xinen <2302563948@qq.com> Date: Sun, 1 Nov 2026 16:02:18 +0800 Subject: [PATCH] =?UTF-8?q?=E5=95=86=E5=93=81=E5=88=97=E8=A1=A8=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E4=BF=9D=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/bill/index.less | 142 +++++++++++++++++++++++++++++++ src/pages/bill/index.tsx | 171 ++++++++++++++++++++++++++++++++++---- src/services/bill.ts | 6 ++ src/utils/download.ts | 10 ++- src/utils/request.ts | 4 +- 5 files changed, 315 insertions(+), 18 deletions(-) diff --git a/src/pages/bill/index.less b/src/pages/bill/index.less index 4ec2b84..693499b 100644 --- a/src/pages/bill/index.less +++ b/src/pages/bill/index.less @@ -64,6 +64,18 @@ } } + .batch-entry { + flex-shrink: 0; + margin-left: 16rpx; + padding: 12rpx 0 12rpx 20rpx; + color: #ee0a24; + font-size: 26rpx; + + &.active { + color: #646566; + } + } + .status-scroll { white-space: nowrap; margin-bottom: 20rpx; @@ -106,11 +118,36 @@ // ===== 账单单项 ===== .bill-item { + display: flex; + align-items: center; background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; + &--selected { + background: #fff8f8; + box-shadow: inset 0 0 0 2rpx #ee0a24; + } + + &__checkbox { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 36rpx; + height: 36rpx; + margin-right: 20rpx; + border: 2rpx solid #c8c9cc; + border-radius: 50%; + box-sizing: border-box; + + &.checked { + border-color: #ee0a24; + background: #ee0a24; + } + } + &__content { flex: 1; min-width: 0; @@ -204,6 +241,53 @@ } } + // ===== 批量导出操作栏 ===== + .bill-batchbar { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 99; + display: flex; + align-items: center; + padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom)); + background: #fff; + box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.06); + + &__select-all { + display: flex; + align-items: center; + flex-shrink: 0; + color: #323233; + font-size: 26rpx; + + .bill-item__checkbox { + margin-right: 12rpx; + } + } + + &__count { + flex: 1; + margin: 0 24rpx; + color: #969799; + font-size: 24rpx; + text-align: right; + } + + &__button { + flex-shrink: 0; + padding: 16rpx 52rpx; + border-radius: 999rpx; + background: #ee0a24; + color: #fff; + font-size: 28rpx; + + &.disabled { + opacity: 0.5; + } + } + } + // ===== 分类选择弹层 ===== .category-popup { padding: 32rpx 32rpx 24rpx; @@ -245,4 +329,62 @@ color: #323233; } } + + // ===== 微信 H5 下载指引 ===== + .wechat-guide { + padding: 36rpx 32rpx 32rpx; + + &__title { + display: block; + text-align: center; + font-size: 32rpx; + font-weight: 600; + color: #323233; + } + + &__desc { + display: block; + margin: 24rpx 0; + font-size: 26rpx; + line-height: 1.6; + color: #646566; + } + + &__step { + display: flex; + align-items: flex-start; + margin-top: 18rpx; + } + + &__number { + flex-shrink: 0; + width: 36rpx; + height: 36rpx; + margin-top: 3rpx; + border-radius: 50%; + background: #fdecee; + color: #ee0a24; + font-size: 22rpx; + line-height: 36rpx; + text-align: center; + } + + &__text { + flex: 1; + margin-left: 16rpx; + font-size: 26rpx; + line-height: 1.6; + color: #323233; + } + + &__button { + margin-top: 32rpx; + padding: 20rpx 0; + border-radius: 999rpx; + background: #ee0a24; + color: #fff; + font-size: 28rpx; + text-align: center; + } + } } diff --git a/src/pages/bill/index.tsx b/src/pages/bill/index.tsx index 19214b7..80ef3f7 100644 --- a/src/pages/bill/index.tsx +++ b/src/pages/bill/index.tsx @@ -3,13 +3,14 @@ import Taro, { useDidShow, useReachBottom } from '@tarojs/taro' import { View, Text, ScrollView } from '@tarojs/components' import { Empty, Icon, Popup } from '@antmjs/vantui' import useAuthStore from '@/stores/auth/useAuthStore' -import { getBillExportUrl, getBillListApi } from '@/services/bill' +import { getBillBatchExportUrl, getBillExportUrl, getBillListApi } from '@/services/bill' import { getCategoriesApi } from '@/services/product' -import { downloadExportFile } from '@/utils/download' +import { downloadExportFile, isWeChatH5 } from '@/utils/download' import type { Bill, BillSummary } from '@/services/bill' import './index.less' const PAGE_SIZE = 10 +const MAX_BATCH_EXPORT = 100 /** 状态筛选(接口 status 为原始支付状态:0 未支付含审核中 / 1 已支付) */ const STATUS_FILTERS: Array<{ value: 0 | 1 | undefined; label: string }> = [ @@ -44,6 +45,11 @@ export default function BillListPage() { const [exportBill, setExportBill] = useState(null) const [showCategory, setShowCategory] = useState(false) const [categories, setCategories] = useState([]) + const [showWeChatGuide, setShowWeChatGuide] = useState(false) + + /** 批量导出:选择模式与已选账单 ID */ + const [batchSelecting, setBatchSelecting] = useState(false) + const [selectedIds, setSelectedIds] = useState([]) const loggedIn = !!token @@ -85,6 +91,7 @@ export default function BillListPage() { (value?: 0 | 1) => { setStatus(value) setFinished(false) + setSelectedIds([]) loadList(1, true, value) }, [loadList], @@ -132,7 +139,10 @@ export default function BillListPage() { try { await downloadExportFile(getBillExportUrl(exportBill.id, categoryId), `账单-${exportBill.bill_no}.xlsx`) setShowCategory(false) - if (isH5) { + if (isWeChatH5()) { + // 微信可能静默忽略 a[download],不能据此提示“导出成功” + setShowWeChatGuide(true) + } else if (isH5) { Taro.showToast({ title: '导出成功', icon: 'success' }) } } catch (e: any) { @@ -144,11 +154,74 @@ export default function BillListPage() { [exportBill], ) + /** 进入/退出批量选择模式 */ + const toggleBatchSelecting = useCallback(() => { + setBatchSelecting(prev => !prev) + setSelectedIds([]) + }, []) + + /** 选择或取消选择一张账单 */ + const toggleBillSelection = useCallback( + (id: number) => { + if (selectedIds.includes(id)) { + setSelectedIds(selectedIds.filter(selectedId => selectedId !== id)) + return + } + if (selectedIds.length >= MAX_BATCH_EXPORT) { + Taro.showToast({ title: '一次最多导出100张账单', icon: 'none' }) + return + } + setSelectedIds([...selectedIds, id]) + }, + [selectedIds], + ) + + const allSelected = bills.length > 0 + && selectedIds.length === Math.min(bills.length, MAX_BATCH_EXPORT) + && bills.slice(0, MAX_BATCH_EXPORT).every(bill => selectedIds.includes(bill.id)) + + /** 全选当前已加载账单(超过 100 张时选择前 100 张) */ + const handleSelectAll = useCallback(() => { + if (allSelected) { + setSelectedIds([]) + return + } + const ids = bills.slice(0, MAX_BATCH_EXPORT).map(bill => bill.id) + setSelectedIds(ids) + if (bills.length > MAX_BATCH_EXPORT) { + Taro.showToast({ title: '已选择前100张账单', icon: 'none' }) + } + }, [allSelected, bills]) + + /** 批量导出所选账单 */ + const handleBatchExport = useCallback(async () => { + if (selectedIds.length === 0) { + Taro.showToast({ title: '请先选择账单', icon: 'none' }) + return + } + const isH5 = process.env.TARO_ENV === 'h5' + Taro.showLoading({ title: '导出中...', mask: true }) + try { + await downloadExportFile(getBillBatchExportUrl(selectedIds), '门店账单_批量导出.xlsx') + setBatchSelecting(false) + setSelectedIds([]) + if (isWeChatH5()) { + setShowWeChatGuide(true) + } else if (isH5) { + Taro.showToast({ title: '导出成功', icon: 'success' }) + } + } catch (e: any) { + Taro.showToast({ title: e?.message || '导出失败,请稍后重试', icon: 'none' }) + } finally { + Taro.hideLoading() + } + }, [selectedIds]) + /** 底部待支付汇总栏是否可见 */ - const showPayBar = loggedIn && !!summary && summary.unpaid_count > 0 + const showPayBar = loggedIn && !!summary && summary.unpaid_count > 0 && !batchSelecting return ( - + {/* ========== 状态筛选 ========== */} @@ -162,6 +235,11 @@ export default function BillListPage() { ))} + {loggedIn && bills.length > 0 && ( + + {batchSelecting ? '取消' : '批量导出'} + + )} {/* ========== 账单列表 ========== */} @@ -177,7 +255,16 @@ export default function BillListPage() { ) ) : ( bills.map(bill => ( - goDetail(bill.id)}> + batchSelecting ? toggleBillSelection(bill.id) : goDetail(bill.id)} + > + {batchSelecting && ( + + {selectedIds.includes(bill.id) && } + + )} {bill.bill_no} @@ -194,15 +281,17 @@ export default function BillListPage() { ¥{bill.total_amount} - { - e.stopPropagation() - handleExportTap(bill) - }} - > - 导出 - + {!batchSelecting && ( + { + e.stopPropagation() + handleExportTap(bill) + }} + > + 导出 + + )} @@ -231,6 +320,25 @@ export default function BillListPage() { )} + {/* ========== 批量导出操作栏 ========== */} + {batchSelecting && ( + + + + {allSelected && } + + {allSelected ? '清空' : '全选'} + + 已选 {selectedIds.length}/100 + + 导出 + + + )} + {/* ========== 分类选择弹层 ========== */} + + {/* ========== 微信 H5 下载兜底指引 ========== */} + setShowWeChatGuide(false)} + > + + 账单下载提示 + + 已尝试开始下载。若文件未下载或无法打开,请使用系统浏览器重新导出: + + + 1 + 点击本页右上角的「···」 + + + 2 + 选择「在浏览器打开」(iPhone 可选择 Safari) + + + 3 + 在浏览器中登录后,重新导出账单 + + setShowWeChatGuide(false)}> + 我知道了 + + + ) } diff --git a/src/services/bill.ts b/src/services/bill.ts index c553cb1..c0151a5 100644 --- a/src/services/bill.ts +++ b/src/services/bill.ts @@ -129,3 +129,9 @@ export function getBillDetailApi(id: number) { export function getBillExportUrl(id: number, categoryId = 0): string { return `${BASE_URL}/mini/bill/${id}/export?category_id=${categoryId}` } + +/** 批量导出账单文件地址:GET /mini/bill/batch-export,最多 100 张 */ +export function getBillBatchExportUrl(ids: number[]): string { + const query = ids.map(id => `ids[]=${id}`).join('&') + return `${BASE_URL}/mini/bill/batch-export?${query}` +} diff --git a/src/utils/download.ts b/src/utils/download.ts index 9683d03..fed44b0 100644 --- a/src/utils/download.ts +++ b/src/utils/download.ts @@ -32,6 +32,13 @@ function parseJsonError(text: string): Error { } } +/** 是否为微信内置浏览器(小程序 web-view 的 UA 同样包含 MicroMessenger) */ +export function isWeChatH5(): boolean { + return process.env.TARO_ENV === 'h5' + && typeof navigator !== 'undefined' + && /MicroMessenger/i.test(navigator.userAgent) +} + /** H5:fetch 带鉴权拉取 blob → 触发保存 */ async function downloadFileH5(url: string, fallbackName: string): Promise { const token = getToken() @@ -57,7 +64,8 @@ async function downloadFileH5(url: string, fallbackName: string): Promise document.body.appendChild(link) link.click() document.body.removeChild(link) - URL.revokeObjectURL(objectUrl) + // 立即回收会让部分微信 WebView 尚未读取 Blob 就失去下载地址 + setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000) } /** 读取小程序本地文件内容(utf8) */ diff --git a/src/utils/request.ts b/src/utils/request.ts index af654e8..c2a0c0f 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -10,8 +10,8 @@ const LOGIN_PATH = '/pages/login/index' /** 默认请求超时(ms) */ const DEFAULT_TIMEOUT = 15000 /** 接口根地址(uploadFile 等原生请求同样使用) */ -export const BASE_URL = "http://localhost:8000" -// export const BASE_URL = "https://purchase.henanklkj.com/index.php" +// export const BASE_URL = "http://localhost:8000" +export const BASE_URL = "https://purchase.henanklkj.com/index.php" /** * HTTP 状态码 → 错误提示映射