商品列表状态保存
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+147
-6
@@ -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<Bill | null>(null)
|
||||
const [showCategory, setShowCategory] = useState(false)
|
||||
const [categories, setCategories] = useState<CategoryOption[]>([])
|
||||
const [showWeChatGuide, setShowWeChatGuide] = useState(false)
|
||||
|
||||
/** 批量导出:选择模式与已选账单 ID */
|
||||
const [batchSelecting, setBatchSelecting] = useState(false)
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>([])
|
||||
|
||||
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 (
|
||||
<View className={`bill-page ${showPayBar ? 'bill-page--pay' : ''}`}>
|
||||
<View className={`bill-page ${showPayBar || batchSelecting ? 'bill-page--pay' : ''}`}>
|
||||
{/* ========== 状态筛选 ========== */}
|
||||
<View className='bill-toolbar'>
|
||||
<ScrollView scrollX className='status-scroll'>
|
||||
@@ -162,6 +235,11 @@ export default function BillListPage() {
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
{loggedIn && bills.length > 0 && (
|
||||
<View className={`batch-entry ${batchSelecting ? 'active' : ''}`} onClick={toggleBatchSelecting}>
|
||||
{batchSelecting ? '取消' : '批量导出'}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* ========== 账单列表 ========== */}
|
||||
@@ -177,7 +255,16 @@ export default function BillListPage() {
|
||||
)
|
||||
) : (
|
||||
bills.map(bill => (
|
||||
<View key={bill.id} className='bill-item' onClick={() => goDetail(bill.id)}>
|
||||
<View
|
||||
key={bill.id}
|
||||
className={`bill-item ${batchSelecting && selectedIds.includes(bill.id) ? 'bill-item--selected' : ''}`}
|
||||
onClick={() => batchSelecting ? toggleBillSelection(bill.id) : goDetail(bill.id)}
|
||||
>
|
||||
{batchSelecting && (
|
||||
<View className={`bill-item__checkbox ${selectedIds.includes(bill.id) ? 'checked' : ''}`}>
|
||||
{selectedIds.includes(bill.id) && <Icon name='success' size={14} color='#fff' />}
|
||||
</View>
|
||||
)}
|
||||
<View className='bill-item__content'>
|
||||
<View className='bill-item__header'>
|
||||
<Text className='bill-item__no'>{bill.bill_no}</Text>
|
||||
@@ -194,6 +281,7 @@ export default function BillListPage() {
|
||||
</View>
|
||||
<View className='bill-item__side'>
|
||||
<Text className='bill-item__amount'>¥{bill.total_amount}</Text>
|
||||
{!batchSelecting && (
|
||||
<View
|
||||
className='bill-item__export'
|
||||
onClick={e => {
|
||||
@@ -203,6 +291,7 @@ export default function BillListPage() {
|
||||
>
|
||||
导出
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<View className='bill-item__footer'>
|
||||
@@ -231,6 +320,25 @@ export default function BillListPage() {
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* ========== 批量导出操作栏 ========== */}
|
||||
{batchSelecting && (
|
||||
<View className='bill-batchbar'>
|
||||
<View className='bill-batchbar__select-all' onClick={handleSelectAll}>
|
||||
<View className={`bill-item__checkbox ${allSelected ? 'checked' : ''}`}>
|
||||
{allSelected && <Icon name='success' size={14} color='#fff' />}
|
||||
</View>
|
||||
<Text>{allSelected ? '清空' : '全选'}</Text>
|
||||
</View>
|
||||
<Text className='bill-batchbar__count'>已选 {selectedIds.length}/100</Text>
|
||||
<View
|
||||
className={`bill-batchbar__button ${selectedIds.length === 0 ? 'disabled' : ''}`}
|
||||
onClick={handleBatchExport}
|
||||
>
|
||||
导出
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* ========== 分类选择弹层 ========== */}
|
||||
<Popup
|
||||
show={showCategory}
|
||||
@@ -256,6 +364,39 @@ export default function BillListPage() {
|
||||
</ScrollView>
|
||||
</View>
|
||||
</Popup>
|
||||
|
||||
{/* ========== 微信 H5 下载兜底指引 ========== */}
|
||||
<Popup
|
||||
show={showWeChatGuide}
|
||||
position='bottom'
|
||||
round
|
||||
closeable
|
||||
closeOnClickOverlay
|
||||
safeAreaInsetBottom
|
||||
onClose={() => setShowWeChatGuide(false)}
|
||||
>
|
||||
<View className='wechat-guide'>
|
||||
<Text className='wechat-guide__title'>账单下载提示</Text>
|
||||
<Text className='wechat-guide__desc'>
|
||||
已尝试开始下载。若文件未下载或无法打开,请使用系统浏览器重新导出:
|
||||
</Text>
|
||||
<View className='wechat-guide__step'>
|
||||
<Text className='wechat-guide__number'>1</Text>
|
||||
<Text className='wechat-guide__text'>点击本页右上角的「···」</Text>
|
||||
</View>
|
||||
<View className='wechat-guide__step'>
|
||||
<Text className='wechat-guide__number'>2</Text>
|
||||
<Text className='wechat-guide__text'>选择「在浏览器打开」(iPhone 可选择 Safari)</Text>
|
||||
</View>
|
||||
<View className='wechat-guide__step'>
|
||||
<Text className='wechat-guide__number'>3</Text>
|
||||
<Text className='wechat-guide__text'>在浏览器中登录后,重新导出账单</Text>
|
||||
</View>
|
||||
<View className='wechat-guide__button' onClick={() => setShowWeChatGuide(false)}>
|
||||
我知道了
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
|
||||
@@ -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 → <a download> 触发保存 */
|
||||
async function downloadFileH5(url: string, fallbackName: string): Promise<void> {
|
||||
const token = getToken()
|
||||
@@ -57,7 +64,8 @@ async function downloadFileH5(url: string, fallbackName: string): Promise<void>
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(objectUrl)
|
||||
// 立即回收会让部分微信 WebView 尚未读取 Blob 就失去下载地址
|
||||
setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000)
|
||||
}
|
||||
|
||||
/** 读取小程序本地文件内容(utf8) */
|
||||
|
||||
@@ -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 状态码 → 错误提示映射
|
||||
|
||||
Reference in New Issue
Block a user