账单导出
This commit is contained in:
+149
-1
@@ -4,6 +4,11 @@
|
||||
padding: 20rpx 24rpx 60rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
// 多选模式为底部导出栏留出空间
|
||||
&--select {
|
||||
padding-bottom: 160rpx;
|
||||
}
|
||||
|
||||
// ===== 待支付汇总 =====
|
||||
.bill-summary {
|
||||
display: flex;
|
||||
@@ -37,7 +42,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 状态筛选 =====
|
||||
// ===== 状态筛选 + 导出入口 =====
|
||||
.bill-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.status-scroll {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__export {
|
||||
flex-shrink: 0;
|
||||
margin-left: 16rpx;
|
||||
padding: 12rpx 28rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #fff;
|
||||
border: 1rpx solid #ee0a24;
|
||||
color: #ee0a24;
|
||||
font-size: 26rpx;
|
||||
|
||||
&.active {
|
||||
background: #ee0a24;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status-scroll {
|
||||
white-space: nowrap;
|
||||
margin-bottom: 20rpx;
|
||||
@@ -85,6 +119,16 @@
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
&--select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -155,4 +199,108 @@
|
||||
color: #07c160;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 勾选圆圈 =====
|
||||
.bill-check {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid #c8c9cc;
|
||||
margin-right: 20rpx;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.on {
|
||||
background: #ee0a24;
|
||||
border-color: #ee0a24;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 导出操作栏 =====
|
||||
.export-bar {
|
||||
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);
|
||||
|
||||
&__count {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 26rpx;
|
||||
color: #646566;
|
||||
}
|
||||
|
||||
&__all {
|
||||
padding: 12rpx 24rpx;
|
||||
margin-right: 16rpx;
|
||||
border: 1rpx solid #dcdee0;
|
||||
border-radius: 999rpx;
|
||||
font-size: 26rpx;
|
||||
color: #323233;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
padding: 14rpx 40rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #ee0a24;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 分类选择弹层 =====
|
||||
.category-popup {
|
||||
padding: 32rpx 32rpx 24rpx;
|
||||
|
||||
&__title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #969799;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__list {
|
||||
max-height: 560rpx;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 8rpx;
|
||||
border-bottom: 1rpx solid #f2f3f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+196
-37
@@ -1,14 +1,19 @@
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import Taro, { useDidShow, useReachBottom } from '@tarojs/taro'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import { Empty } from '@antmjs/vantui'
|
||||
import { Empty, Icon, Popup } from '@antmjs/vantui'
|
||||
import useAuthStore from '@/stores/auth/useAuthStore'
|
||||
import { getBillListApi } from '@/services/bill'
|
||||
import { getBillExportUrl, getBillListApi } from '@/services/bill'
|
||||
import { getCategoriesApi } from '@/services/product'
|
||||
import { downloadExportFile } from '@/utils/download'
|
||||
import type { Bill, BillSummary } from '@/services/bill'
|
||||
import './index.less'
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
/** 单次导出上限(后端限制 1~100 张) */
|
||||
const EXPORT_MAX = 100
|
||||
|
||||
/** 状态筛选(接口 status 为原始支付状态:0 未支付含审核中 / 1 已支付) */
|
||||
const STATUS_FILTERS: Array<{ value: 0 | 1 | undefined; label: string }> = [
|
||||
{ value: undefined, label: '全部' },
|
||||
@@ -16,9 +21,16 @@ const STATUS_FILTERS: Array<{ value: 0 | 1 | undefined; label: string }> = [
|
||||
{ value: 1, label: '已支付' },
|
||||
]
|
||||
|
||||
/** 导出分类选项(id=0 全部分类) */
|
||||
interface CategoryOption {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 账单列表页
|
||||
* 采购单完成后由后台按门店生成(只读);头部 summary 为门店口径待支付汇总(含审核中,不受筛选影响)
|
||||
* 支持多选账单合并导出 Excel(可按一级分类过滤商品明细)
|
||||
*/
|
||||
export default function BillListPage() {
|
||||
const token = useAuthStore(s => s.token)
|
||||
@@ -31,6 +43,12 @@ export default function BillListPage() {
|
||||
const [finished, setFinished] = useState(false)
|
||||
const loadingRef = useRef(false)
|
||||
|
||||
/** 导出:多选模式 + 已选账单 + 分类弹层 */
|
||||
const [selectMode, setSelectMode] = useState(false)
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>([])
|
||||
const [showCategory, setShowCategory] = useState(false)
|
||||
const [categories, setCategories] = useState<CategoryOption[]>([])
|
||||
|
||||
const loggedIn = !!token
|
||||
|
||||
/** 拉取账单列表(summary 每次随响应刷新) */
|
||||
@@ -84,8 +102,86 @@ export default function BillListPage() {
|
||||
Taro.navigateTo({ url: `/pages/bill-detail/index?id=${id}` })
|
||||
}, [])
|
||||
|
||||
/** 进入/退出多选导出模式 */
|
||||
const toggleSelectMode = useCallback(() => {
|
||||
setSelectMode(prev => !prev)
|
||||
setSelectedIds([])
|
||||
}, [])
|
||||
|
||||
/** 账单点击:多选模式切换勾选,否则进详情 */
|
||||
const handleItemTap = useCallback(
|
||||
(bill: Bill) => {
|
||||
if (!selectMode) {
|
||||
goDetail(bill.id)
|
||||
return
|
||||
}
|
||||
setSelectedIds(prev => {
|
||||
if (prev.includes(bill.id)) return prev.filter(i => i !== bill.id)
|
||||
if (prev.length >= EXPORT_MAX) {
|
||||
Taro.showToast({ title: `最多导出 ${EXPORT_MAX} 张`, icon: 'none' })
|
||||
return prev
|
||||
}
|
||||
return [...prev, bill.id]
|
||||
})
|
||||
},
|
||||
[selectMode, goDetail],
|
||||
)
|
||||
|
||||
/** 全选当前已加载账单(受导出上限约束) */
|
||||
const handleSelectAll = useCallback(() => {
|
||||
setSelectedIds(prev => {
|
||||
if (prev.length === bills.length) return []
|
||||
if (bills.length > EXPORT_MAX) {
|
||||
Taro.showToast({ title: `最多导出 ${EXPORT_MAX} 张,已选前 ${EXPORT_MAX} 张`, icon: 'none' })
|
||||
return bills.slice(0, EXPORT_MAX).map(b => b.id)
|
||||
}
|
||||
return bills.map(b => b.id)
|
||||
})
|
||||
}, [bills])
|
||||
|
||||
/** 打开分类选择弹层(首次加载分类树根节点) */
|
||||
const handleExportTap = useCallback(async () => {
|
||||
if (selectedIds.length === 0) {
|
||||
Taro.showToast({ title: '请先勾选要导出的账单', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (categories.length === 0) {
|
||||
try {
|
||||
const res = await getCategoriesApi()
|
||||
setCategories([
|
||||
{ id: 0, name: '全部分类' },
|
||||
...res.data.map(c => ({ id: c.id, name: c.name })),
|
||||
])
|
||||
} catch {
|
||||
return // 错误已由 request 层 toast
|
||||
}
|
||||
}
|
||||
setShowCategory(true)
|
||||
}, [selectedIds, categories])
|
||||
|
||||
/** 按所选分类导出合并 Excel */
|
||||
const handleExport = useCallback(
|
||||
async (categoryId: number) => {
|
||||
const isH5 = process.env.TARO_ENV === 'h5'
|
||||
Taro.showLoading({ title: '导出中...', mask: true })
|
||||
try {
|
||||
await downloadExportFile(getBillExportUrl(selectedIds, categoryId), '门店账单.xlsx')
|
||||
setShowCategory(false)
|
||||
toggleSelectMode()
|
||||
if (isH5) {
|
||||
Taro.showToast({ title: '导出成功', icon: 'success' })
|
||||
}
|
||||
} catch (e: any) {
|
||||
Taro.showToast({ title: e?.message || '导出失败,请稍后重试', icon: 'none' })
|
||||
} finally {
|
||||
Taro.hideLoading()
|
||||
}
|
||||
},
|
||||
[selectedIds, toggleSelectMode],
|
||||
)
|
||||
|
||||
return (
|
||||
<View className='bill-page'>
|
||||
<View className={`bill-page ${selectMode ? 'bill-page--select' : ''}`}>
|
||||
{/* ========== 待支付汇总(门店口径,含审核中) ========== */}
|
||||
{loggedIn && summary && summary.unpaid_count > 0 && (
|
||||
<View className='bill-summary'>
|
||||
@@ -97,18 +193,25 @@ export default function BillListPage() {
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* ========== 状态筛选 ========== */}
|
||||
<ScrollView scrollX className='status-scroll'>
|
||||
{STATUS_FILTERS.map(item => (
|
||||
<View
|
||||
key={item.label}
|
||||
className={`status-chip ${status === item.value ? 'active' : ''}`}
|
||||
onClick={() => handleStatusTap(item.value)}
|
||||
>
|
||||
<Text>{item.label}</Text>
|
||||
{/* ========== 状态筛选 + 导出入口 ========== */}
|
||||
<View className='bill-toolbar'>
|
||||
<ScrollView scrollX className='status-scroll'>
|
||||
{STATUS_FILTERS.map(item => (
|
||||
<View
|
||||
key={item.label}
|
||||
className={`status-chip ${status === item.value ? 'active' : ''}`}
|
||||
onClick={() => handleStatusTap(item.value)}
|
||||
>
|
||||
<Text>{item.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
{loggedIn && bills.length > 0 && (
|
||||
<View className={`bill-toolbar__export ${selectMode ? 'active' : ''}`} onClick={toggleSelectMode}>
|
||||
<Text>{selectMode ? '取消' : '导出'}</Text>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* ========== 账单列表 ========== */}
|
||||
{!loggedIn ? (
|
||||
@@ -122,36 +225,92 @@ export default function BillListPage() {
|
||||
<Empty description='暂无账单' className='bill-empty' />
|
||||
)
|
||||
) : (
|
||||
bills.map(bill => (
|
||||
<View key={bill.id} className='bill-item' onClick={() => goDetail(bill.id)}>
|
||||
<View className='bill-item__header'>
|
||||
<Text className='bill-item__no'>{bill.bill_no}</Text>
|
||||
<Text className={`bill-item__status bill-item__status--${bill.pay_state}`}>
|
||||
{bill.pay_state_name}
|
||||
</Text>
|
||||
</View>
|
||||
<View className='bill-item__body'>
|
||||
<View className='bill-item__meta'>
|
||||
<Text className='bill-item__date'>账单日期 {bill.bill_date}</Text>
|
||||
{bill.purchase && (
|
||||
<Text className='bill-item__purchase'>采购单 {bill.purchase.purchase_no}</Text>
|
||||
)}
|
||||
</View>
|
||||
<Text className='bill-item__amount'>¥{bill.total_amount}</Text>
|
||||
</View>
|
||||
<View className='bill-item__footer'>
|
||||
<Text className='bill-item__settle'>应结算 {bill.settlement_date}</Text>
|
||||
{bill.pay_state === 2 && bill.paid_at && (
|
||||
<Text className='bill-item__paid'>已于 {bill.paid_at} 支付</Text>
|
||||
bills.map(bill => {
|
||||
const checked = selectMode && selectedIds.includes(bill.id)
|
||||
return (
|
||||
<View
|
||||
key={bill.id}
|
||||
className={`bill-item ${selectMode ? 'bill-item--select' : ''}`}
|
||||
onClick={() => handleItemTap(bill)}
|
||||
>
|
||||
{selectMode && (
|
||||
<View className={`bill-check ${checked ? 'on' : ''}`}>
|
||||
{checked && <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>
|
||||
<Text className={`bill-item__status bill-item__status--${bill.pay_state}`}>
|
||||
{bill.pay_state_name}
|
||||
</Text>
|
||||
</View>
|
||||
<View className='bill-item__body'>
|
||||
<View className='bill-item__meta'>
|
||||
<Text className='bill-item__date'>账单日期 {bill.bill_date}</Text>
|
||||
{bill.purchase && (
|
||||
<Text className='bill-item__purchase'>采购单 {bill.purchase.purchase_no}</Text>
|
||||
)}
|
||||
</View>
|
||||
<Text className='bill-item__amount'>¥{bill.total_amount}</Text>
|
||||
</View>
|
||||
<View className='bill-item__footer'>
|
||||
<Text className='bill-item__settle'>应结算 {bill.settlement_date}</Text>
|
||||
{bill.pay_state === 2 && bill.paid_at && (
|
||||
<Text className='bill-item__paid'>已于 {bill.paid_at} 支付</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
)
|
||||
})
|
||||
)}
|
||||
|
||||
{loggedIn && finished && bills.length > 0 && (
|
||||
<View className='bill-loading'><Text>没有更多了</Text></View>
|
||||
)}
|
||||
|
||||
{/* ========== 导出操作栏 ========== */}
|
||||
{selectMode && (
|
||||
<View className='export-bar'>
|
||||
<Text className='export-bar__count'>已选 {selectedIds.length} 张</Text>
|
||||
<View className='export-bar__all' onClick={handleSelectAll}>
|
||||
{selectedIds.length === bills.length && bills.length > 0 ? '取消全选' : '全选'}
|
||||
</View>
|
||||
<View
|
||||
className={`export-bar__btn ${selectedIds.length === 0 ? 'disabled' : ''}`}
|
||||
onClick={handleExportTap}
|
||||
>
|
||||
导出 Excel
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* ========== 分类选择弹层 ========== */}
|
||||
<Popup
|
||||
show={showCategory}
|
||||
position='bottom'
|
||||
round
|
||||
closeable
|
||||
closeOnClickOverlay
|
||||
safeAreaInsetBottom
|
||||
onClose={() => setShowCategory(false)}
|
||||
>
|
||||
<View className='category-popup'>
|
||||
<Text className='category-popup__title'>选择商品分类</Text>
|
||||
<Text className='category-popup__desc'>
|
||||
仅过滤商品明细行,配送费与附加金额仍全额汇总
|
||||
</Text>
|
||||
<ScrollView scrollY className='category-popup__list'>
|
||||
{categories.map(c => (
|
||||
<View key={c.id} className='category-popup__item' onClick={() => handleExport(c.id)}>
|
||||
<Text className='category-popup__name'>{c.name}</Text>
|
||||
<Icon name='arrow' size={16} color='#c8c9cc' />
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</Popup>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { get } from '@/utils/request'
|
||||
import { BASE_URL, get } from '@/utils/request'
|
||||
import type { PaginatedData } from '@/types/api'
|
||||
|
||||
/** 账单支付进度:0 待支付 / 1 审核中 / 2 已支付(由后端推导,展示以此为准) */
|
||||
@@ -115,3 +115,11 @@ export function getBillListApi(params: BillListParams = {}) {
|
||||
export function getBillDetailApi(id: number) {
|
||||
return get<BillDetail>(`/mini/bill/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 账单合并导出文件地址:GET /mini/bill/export
|
||||
* 返回 xlsx 文件流(非 JSON,需带 token 下载);categoryId 仅过滤商品明细,费用仍全额汇总
|
||||
*/
|
||||
export function getBillExportUrl(ids: number[], categoryId = 0): string {
|
||||
return `${BASE_URL}/mini/bill/export?ids=${ids.join(',')}&category_id=${categoryId}`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import { getToken } from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 导出文件下载(双端)
|
||||
* 后端导出接口成功返回 xlsx 文件流,业务失败时仍以文件流形式返回 JSON
|
||||
* (blob.type 为 application/json),两端都需探测并解析 msg 提示
|
||||
*/
|
||||
|
||||
/** 从 Content-Disposition 解析文件名(优先 RFC 5987 filename*=utf-8''...) */
|
||||
export function parseExportFilename(disposition?: string | null): string {
|
||||
if (!disposition) return ''
|
||||
const star = disposition.match(/filename\*=utf-8''([^;]+)/i)
|
||||
if (star?.[1]) {
|
||||
try {
|
||||
return decodeURIComponent(star[1])
|
||||
} catch {
|
||||
// 编码异常时退化为普通 filename
|
||||
}
|
||||
}
|
||||
const plain = disposition.match(/filename="?([^";]+)"?/i)
|
||||
return plain?.[1] ?? ''
|
||||
}
|
||||
|
||||
/** 解析 JSON 文本中的业务错误信息 */
|
||||
function parseJsonError(text: string): Error {
|
||||
try {
|
||||
const json = JSON.parse(text)
|
||||
return new Error(json.msg || '导出失败,请稍后重试')
|
||||
} catch {
|
||||
return new Error('导出失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
/** H5:fetch 带鉴权拉取 blob → <a download> 触发保存 */
|
||||
async function downloadFileH5(url: string, fallbackName: string): Promise<void> {
|
||||
const token = getToken()
|
||||
const res = await fetch(url, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error(`下载失败(${res.status})`)
|
||||
}
|
||||
const blob = await res.blob()
|
||||
|
||||
// 业务失败:返回的是 JSON blob
|
||||
if (blob.type.includes('application/json')) {
|
||||
throw parseJsonError(await blob.text())
|
||||
}
|
||||
|
||||
const filename =
|
||||
parseExportFilename(res.headers.get('Content-Disposition')) || fallbackName
|
||||
const objectUrl = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = objectUrl
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(objectUrl)
|
||||
}
|
||||
|
||||
/** 读取小程序本地文件内容(utf8) */
|
||||
function readTempFile(filePath: string, length?: number): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
Taro.getFileSystemManager().readFile({
|
||||
filePath,
|
||||
encoding: 'utf8',
|
||||
...(length !== undefined ? { position: 0, length } : {}),
|
||||
success: r => resolve(r.data as string),
|
||||
fail: reject,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** 小程序:downloadFile 下载后 openDocument 打开;业务失败时下载"成功"但内容是 JSON */
|
||||
async function downloadFileWeapp(url: string): Promise<void> {
|
||||
const token = getToken()
|
||||
const { tempFilePath } = await Taro.downloadFile({
|
||||
url,
|
||||
header: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
})
|
||||
|
||||
// 探测前 200 字节:以 '{' 开头即为 JSON 错误响应
|
||||
const head = await readTempFile(tempFilePath, 200)
|
||||
if (head.trimStart().startsWith('{')) {
|
||||
throw parseJsonError(await readTempFile(tempFilePath))
|
||||
}
|
||||
|
||||
await Taro.openDocument({
|
||||
filePath: tempFilePath,
|
||||
fileType: 'xlsx',
|
||||
showMenu: true,
|
||||
fail: () => {
|
||||
Taro.showToast({ title: '文件已下载,打开失败', icon: 'none' })
|
||||
},
|
||||
} as Parameters<typeof Taro.openDocument>[0])
|
||||
}
|
||||
|
||||
/**
|
||||
* 带鉴权下载导出文件
|
||||
* - H5:保存到浏览器下载目录,返回保存的文件名
|
||||
* - 小程序:直接调起系统文档预览(showMenu 支持转发/保存)
|
||||
*/
|
||||
export async function downloadExportFile(url: string, fallbackName: string): Promise<void> {
|
||||
if (process.env.TARO_ENV === 'h5') {
|
||||
return downloadFileH5(url, fallbackName)
|
||||
}
|
||||
return downloadFileWeapp(url)
|
||||
}
|
||||
Reference in New Issue
Block a user