增添分页配置

This commit is contained in:
2026-03-09 09:44:04 +08:00
parent 1673022ea3
commit e5ad13d6f5
6 changed files with 368 additions and 149 deletions

View File

@@ -122,23 +122,46 @@
</view>
<!-- 分页 -->
<view class="pagination-footer">
<text class="total-txt">共 {{ dataList.length }} 条</text>
<view class="page-select">
<text class="page-val">10 条/页 ▼</text>
</view>
<view class="pagination-footer" v-if="dataList.length > 0 || pageState.loading">
<text class="total-txt">共 {{ pageState.total }} 条</text>
<picker class="page-select" :range="pageSizeOptionLabels" :value="pageSizeIndex" @change="handlePageSizeChange">
<view class="page-select-inner">
<text class="page-val">{{ pageState.pageSize }} 条/页</text>
<text class="arrow-down">▼</text>
</view>
</picker>
<view class="page-btns">
<view class="p-btn disabled"><text></text></view>
<view class="p-btn active"><text>1</text></view>
<view class="p-btn"><text>2</text></view>
<view class="p-btn"><text></text></view>
<view class="p-btn" :class="{ disabled: pageState.currentPage <= 1 }" @click="handlePageChange(pageState.currentPage - 1)">
<text></text>
</view>
<view
v-for="(p, index) in visiblePages"
:key="index"
class="p-btn"
:class="{ active: p === pageState.currentPage, 'ellipsis-btn': p === -1 }"
@click="p !== -1 && handlePageChange(p)">
<text>{{ p === -1 ? '...' : p }}</text>
</view>
<view class="p-btn" :class="{ disabled: pageState.currentPage >= totalPage }" @click="handlePageChange(pageState.currentPage + 1)">
<text></text>
</view>
</view>
<view class="page-jump">
<text class="jump-txt">前往</text>
<input class="jump-input" placeholder="1" />
<input class="jump-input" type="number" v-model="pageState.jumpPageInput" @confirm="handleJumpPage" @blur="handleJumpPage" placeholder="页码" />
<text class="jump-txt">页</text>
</view>
</view>
<!-- 空数据状态 -->
<view class="table-empty" v-if="dataList.length === 0 && !pageState.loading">
<text class="empty-txt">暂无数据</text>
</view>
</view>
</view>
@@ -175,7 +198,7 @@
</template>
<script setup lang="uts">
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, onMounted, computed } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
interface CouponItem {
@@ -220,30 +243,127 @@ const filter = reactive({
const dataList = ref<CouponItem[]>([])
// --- 分页与列表状态管理 ---
const pageState = reactive({
currentPage: 1,
pageSize: 10,
total: 0,
loading: false,
jumpPageInput: ''
})
const pageSizeOptions = [10, 20, 30, 50, 100]
const pageSizeOptionLabels = computed((): string[] => pageSizeOptions.map((size: number): string => `${size} 条/页`))
const pageSizeIndex = computed((): number => {
const index = pageSizeOptions.indexOf(pageState.pageSize)
return index === -1 ? 0 : index
})
const totalPage = computed((): number => {
return Math.ceil(pageState.total / pageState.pageSize)
})
const visiblePages = computed((): number[] => {
const current = pageState.currentPage
const total = totalPage.value
if (total <= 7) {
const pages: number[] = []
for (let i = 1; i <= total; i++) {
pages.push(i)
}
return pages
}
if (current <= 4) {
return [1, 2, 3, 4, 5, -1, total]
}
if (current >= total - 3) {
return [1, -1, total - 4, total - 3, total - 2, total - 1, total]
}
return [1, -1, current - 1, current, current + 1, -1, total]
})
const handlePageSizeChange = (e: any) => {
if (pageState.loading) return
let val = 0
if (typeof e.detail.value === 'string') {
val = parseInt(e.detail.value)
} else {
val = e.detail.value as number
}
pageState.pageSize = pageSizeOptions[val]
pageState.currentPage = 1
fetchCouponTemplates()
}
const handlePageChange = (p: number) => {
if (pageState.loading || p < 1 || p > totalPage.value || p === pageState.currentPage) return
pageState.currentPage = p
pageState.jumpPageInput = ''
fetchCouponTemplates()
}
const handleJumpPage = () => {
if (pageState.loading) return
let jumpTo = parseInt(pageState.jumpPageInput)
if (isNaN(jumpTo)) return
if (jumpTo < 1) jumpTo = 1
if (jumpTo > totalPage.value) jumpTo = totalPage.value
pageState.jumpPageInput = String(jumpTo)
if (jumpTo !== pageState.currentPage) {
pageState.currentPage = jumpTo
fetchCouponTemplates()
}
}
const fetchCouponTemplates = async () => {
pageState.loading = true
uni.showLoading({ title: '加载中...', mask: true })
try {
let query = supa.from('ml_coupon_templates').select('*')
// 兼容使用 { count: 'exact' } 获取精确总数
let query = supa.from('ml_coupon_templates').select('*', { count: 'exact' })
// 如果有名称筛选
if (filter.name.trim() != '') {
query = query.like('name', `%${filter.name}%`)
}
const res = await query.order('created_at', { ascending: false }).execute()
// 计算分页 range
const start = (pageState.currentPage - 1) * pageState.pageSize
const end = pageState.currentPage * pageState.pageSize - 1
const res = await query.order('created_at', { ascending: false }).range(start, end).execute()
if (res.error != null || res.status >= 400) {
const msg = res.error?.message ?? `获取数据失败 (${res.status})`
uni.showToast({ title: msg, icon: 'none' })
return
}
// 尝试安全获取总数。如果有的封装库剥离了 count 属性,则以当前返回的数据长度作为保底
let fCount = 0
if (res.count != null) {
fCount = res.count as number
} else if (res['total'] != null) {
fCount = res['total'] as number
}
if (!Array.isArray(res.data)) {
console.warn('Expected array but got:', res.data)
dataList.value = []
pageState.total = fCount
return
}
const rawData = res.data as Array<UTSJSONObject>
// 如果获取到的总数为 0 但实际有数据,说明接口 count 丢了,用当前拉取的数据量兜底防止分页区坍塌
pageState.total = Math.max(fCount, rawData.length)
dataList.value = rawData.map((item : UTSJSONObject) : CouponItem => {
// 优先获取 cid (自增 ID),如果没有则取 id (UUID) 的后几位或 0
const displayId = (item.get('cid') as number | null) ?? 0
@@ -290,10 +410,14 @@ const fetchCouponTemplates = async () => {
} catch (e) {
console.error('Fetch Coupons Error:', e)
uni.showToast({ title: '访问数据库异常', icon: 'none' })
} finally {
pageState.loading = false
uni.hideLoading()
}
}
const handleQuery = () => {
pageState.currentPage = 1 // 重置到第一页
fetchCouponTemplates()
}
const handleAdd = () => {
@@ -379,9 +503,15 @@ const handleDelete = (item: CouponItem) => {
return
}
// 移除本地列表数据
dataList.value = dataList.value.filter(i => i.id_uuid !== item.id_uuid)
uni.showToast({ title: '删除成功', icon: 'success' })
// 如果当前页只有一条数据,且不是第一页,则退回上一页
if (dataList.value.length === 1 && pageState.currentPage > 1) {
pageState.currentPage--
}
// 重新拉取当前页数据
fetchCouponTemplates()
} catch (e) {
uni.showToast({ title: '操作数据库异常', icon: 'none' })
}
@@ -626,21 +756,80 @@ onMounted(() => {
/* 分页 */
.pagination-footer {
padding: 24px;
padding: 16px 24px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
gap: 12px;
border-top: 1px solid #f0f0f0;
gap: 16px;
border-top: 1px solid #e8eaec;
background-color: #fff;
}
.total-txt { font-size: 14px; color: #606266; }
.page-btns { display: flex; flex-direction: row; gap: 8px; }
.total-txt { font-size: 14px; color: #515a6e; }
.page-select {
border: 1px solid #dcdee2;
border-radius: 4px;
background-color: #fff;
cursor: pointer;
transition: border 0.2s;
}
.page-select:hover { border-color: #2d8cf0; }
.page-select-inner {
display: flex;
flex-direction: row;
align-items: center;
padding: 0 12px;
height: 32px;
gap: 8px;
}
.page-val { font-size: 14px; color: #515a6e; }
.page-btns { display: flex; flex-direction: row; gap: 4px; }
.p-btn {
width: 32px; height: 32px; border: 1px solid #dcdfe6; border-radius: 4px;
display: flex; align-items: center; justify-content: center; font-size: 14px; color: #666;
min-width: 32px; height: 32px; padding: 0 4px; border: 1px solid #dcdee2; border-radius: 4px;
display: flex; align-items: center; justify-content: center; font-size: 14px; color: #515a6e;
background-color: #fff; cursor: pointer; transition: all 0.2s;
}
.p-btn:hover:not(.disabled):not(.active):not(.ellipsis-btn) {
border-color: #2d8cf0; color: #2d8cf0;
}
.p-btn.active { background-color: #2d8cf0; border-color: #2d8cf0; color: #fff; }
.p-btn.disabled { color: #c5c8ce; background-color: #f7f7f7; cursor: not-allowed; border-color: #dcdee2; }
.p-btn.ellipsis-btn { border: none; cursor: default; }
.page-jump {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.jump-txt { font-size: 14px; color: #515a6e; }
.jump-input {
width: 50px;
height: 32px;
border: 1px solid #dcdee2;
border-radius: 4px;
text-align: center;
font-size: 14px;
color: #515a6e;
transition: border 0.2s;
}
.jump-input:focus { border-color: #2d8cf0; outline: none; }
/* 空数据状态 */
.table-empty {
padding: 60px 0;
display: flex;
justify-content: center;
align-items: center;
background-color: #fff;
border-top: 1px solid #e8eaec;
}
.empty-txt {
font-size: 14px;
color: #909399;
}
/* 🎭 模态框样式 */
.modal-mask {