完成积分商品
This commit is contained in:
@@ -13,10 +13,17 @@
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<text class="label-txt">上架状态:</text>
|
||||
<view class="select-mock">
|
||||
<text class="select-val">请选择</text>
|
||||
<text class="arrow-down">▼</text>
|
||||
</view>
|
||||
<picker
|
||||
mode="selector"
|
||||
:range="statusOptions"
|
||||
range-key="label"
|
||||
@change="handleStatusChange"
|
||||
>
|
||||
<view class="select-mock">
|
||||
<text class="select-val">{{ currentStatusLabel }}</text>
|
||||
<text class="arrow-down">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<text class="label-txt">商品搜索:</text>
|
||||
@@ -52,13 +59,27 @@
|
||||
</view>
|
||||
|
||||
<view class="table-body">
|
||||
<view v-for="(item, index) in productList" :key="item.id" class="table-row">
|
||||
<view class="td td-id"><text class="td-txt">{{ item.id }}</text></view>
|
||||
<!-- 加载中状态 -->
|
||||
<view v-if="loading" class="empty-row">
|
||||
<text class="empty-txt">加载中...</text>
|
||||
</view>
|
||||
<!-- 空状态 -->
|
||||
<view v-else-if="productList.length === 0" class="empty-row">
|
||||
<text class="empty-txt">暂无积分商品</text>
|
||||
</view>
|
||||
<!-- 数据行 -->
|
||||
<view v-else v-for="(item, index) in productList" :key="item.id" class="table-row">
|
||||
<view class="td td-id"><text class="td-txt-small td-id-val">{{ item.id }}</text></view>
|
||||
<view class="td td-img">
|
||||
<image class="product-thumb" :src="item.image" mode="aspectFill"></image>
|
||||
</view>
|
||||
<view class="td td-title">
|
||||
<text class="title-txt line-clamp-2">{{ item.title }}</text>
|
||||
<view class="title-cell">
|
||||
<text class="title-txt line-clamp-2">{{ item.title }}</text>
|
||||
<view v-if="item.productType" class="type-tag">
|
||||
<text class="type-tag-txt">{{ getProductTypeLabel(item.productType) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="td td-integral"><text class="td-txt">{{ item.integral }}</text></view>
|
||||
<view class="td td-limit"><text class="td-txt">{{ item.limit }}</text></view>
|
||||
@@ -108,68 +129,94 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
interface ProductItem {
|
||||
id: number
|
||||
image: string
|
||||
title: string
|
||||
integral: number
|
||||
limit: number
|
||||
remain: number
|
||||
createTime: string
|
||||
sort: number
|
||||
status: boolean
|
||||
// ──────────────────────────────────────────────
|
||||
// 数据结构(对应 ml_point_products 真实字段)
|
||||
// ──────────────────────────────────────────────
|
||||
interface PointProductItem {
|
||||
id: string // UUID
|
||||
image: string // image_url,为空时用默认占位
|
||||
title: string // name
|
||||
productType: string // product_type:physical/coupon/virtual
|
||||
integral: number // points_required
|
||||
originalPrice: string // original_price,格式化后
|
||||
limit: number // stock
|
||||
remain: string // 数据库无此字段,固定显示 '-'
|
||||
createTime: string // created_at,格式化后
|
||||
sort: number // sort_order
|
||||
status: boolean // status === 1
|
||||
}
|
||||
|
||||
const searchQuery = ref('')
|
||||
const total = ref(3)
|
||||
// 商品类型标签映射
|
||||
function getProductTypeLabel(type: string): string {
|
||||
if (type === 'physical') return '实物'
|
||||
if (type === 'coupon') return '优惠券'
|
||||
if (type === 'virtual') return '虚拟'
|
||||
return type
|
||||
}
|
||||
|
||||
const productList = ref<ProductItem[]>([
|
||||
{
|
||||
id: 48,
|
||||
image: 'https://img14.360buyimg.com/n1/jfs/t1/172605/32/17036/114175/609a473eE6997455c/df82c6168e36712b.jpg',
|
||||
title: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤UWG440060',
|
||||
integral: 0,
|
||||
limit: 4,
|
||||
remain: 0,
|
||||
createTime: '2025-10-24 14:29:19',
|
||||
sort: 9999,
|
||||
status: true
|
||||
},
|
||||
{
|
||||
id: 43,
|
||||
image: 'https://img12.360buyimg.com/n1/jfs/t1/185449/19/11995/4379/60d96d27E6a877c8e/3c38d4e92a2a7a5a.jpg',
|
||||
title: '阿迪达斯官网 adidas BBALL CAP COT 男女训练运动帽子FQ5270 传奇水蓝/传...',
|
||||
integral: 100,
|
||||
limit: 1,
|
||||
remain: 0,
|
||||
createTime: '2025-05-13 15:37:46',
|
||||
sort: 9998,
|
||||
status: true
|
||||
},
|
||||
{
|
||||
id: 44,
|
||||
image: 'https://img13.360buyimg.com/n1/jfs/t1/192173/5/11913/21447/60e57e95Ef82688f3/bc875f643e8c95a3.jpg',
|
||||
title: '劳伦斯意式极简大平层设计师款直排真皮沙发简约客厅别墅大小户型',
|
||||
integral: 6860,
|
||||
limit: 1,
|
||||
remain: 0,
|
||||
createTime: '2025-05-13 15:38:02',
|
||||
sort: 9996,
|
||||
status: true
|
||||
// 时间格式化:2026-03-05T09:27:20.672132+00:00 → 2026-03-05 17:27:20
|
||||
function formatDateTime(raw: string): string {
|
||||
if (!raw) return '-'
|
||||
try {
|
||||
const d = new Date(raw)
|
||||
const pad = (n: number): string => n < 10 ? '0' + n : '' + n
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
} catch (_) {
|
||||
return raw
|
||||
}
|
||||
])
|
||||
|
||||
const handleSearch = () => { console.log('Searching...') }
|
||||
const handleAdd = () => { console.log('Adding...') }
|
||||
const handleEdit = (item: ProductItem) => { console.log('Editing...', item.id) }
|
||||
const toggleStatus = (index: number) => {
|
||||
productList.value[index].status = !productList.value[index].status
|
||||
}
|
||||
|
||||
// 分页适配状态
|
||||
// 将数据库原始行映射为页面展示对象(统一转换层)
|
||||
function mapPointProduct(row: UTSJSONObject): PointProductItem {
|
||||
const rawPrice = row.get('original_price')
|
||||
const priceNum = rawPrice != null ? Number(rawPrice) : 0
|
||||
return {
|
||||
id: (row.get('id') as string) ?? '-',
|
||||
image: (row.get('image_url') as string) || '/static/image/default-product.png',
|
||||
title: (row.get('name') as string) || '未命名商品',
|
||||
productType: (row.get('product_type') as string) || '',
|
||||
integral: row.get('points_required') != null ? Number(row.get('points_required')) : 0,
|
||||
originalPrice: priceNum > 0 ? '¥' + priceNum.toFixed(2) : '-',
|
||||
limit: row.get('stock') != null ? Number(row.get('stock')) : 0,
|
||||
remain: '-', // 数据库暂无剩余库存字段
|
||||
createTime: formatDateTime((row.get('created_at') as string) ?? ''),
|
||||
sort: row.get('sort_order') != null ? Number(row.get('sort_order')) : 0,
|
||||
status: Number(row.get('status')) === 1,
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 搜索条件状态
|
||||
// ──────────────────────────────────────────────
|
||||
const searchQuery = ref('') // 商品名称搜索
|
||||
const statusFilter = ref(-1) // -1 = 全部;1 = 上架;0 = 下架
|
||||
|
||||
// 状态下拉选项
|
||||
const statusOptions = [
|
||||
{ label: '全部', value: -1 },
|
||||
{ label: '上架', value: 1 },
|
||||
{ label: '下架', value: 0 },
|
||||
]
|
||||
const currentStatusLabel = computed(() => {
|
||||
const found = statusOptions.find((o) => o.value === statusFilter.value)
|
||||
return found != null ? found.label : '请选择'
|
||||
})
|
||||
const handleStatusChange = (e: any) => {
|
||||
const idx = Number(e.detail.value)
|
||||
statusFilter.value = statusOptions[idx]?.value ?? -1
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 列表 & 分页状态
|
||||
// ──────────────────────────────────────────────
|
||||
const productList = ref<PointProductItem[]>([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
let jumpPageInput = ''
|
||||
@@ -188,16 +235,114 @@ const visiblePages = computed(() => {
|
||||
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
|
||||
return [1, -1, cur - 1, cur, cur + 1, -1, t]
|
||||
})
|
||||
const handlePageChange = (p: number) => { currentPage.value = p }
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 核心:服务端分页查询(只请求当前页)
|
||||
// ──────────────────────────────────────────────
|
||||
async function fetchProducts() {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const query = supa
|
||||
.from('ml_point_products')
|
||||
.select('id, name, image_url, product_type, points_required, original_price, stock, status, sort_order, created_at')
|
||||
|
||||
// 状态筛选
|
||||
if (statusFilter.value === 1) {
|
||||
query.eq('status', 1)
|
||||
} else if (statusFilter.value === 0) {
|
||||
query.neq('status', 1)
|
||||
}
|
||||
|
||||
// 商品名称模糊搜索
|
||||
if (searchQuery.value.trim() !== '') {
|
||||
query.ilike('name', '%' + searchQuery.value.trim() + '%')
|
||||
}
|
||||
|
||||
const { data, error, total: serverTotal } = await query
|
||||
.order('sort_order', { ascending: true })
|
||||
.limit(pageSize.value)
|
||||
.page(currentPage.value)
|
||||
.execute()
|
||||
|
||||
if (error) {
|
||||
uni.showToast({ title: '加载失败: ' + (error as UTSJSONObject).getString('message'), icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (data != null) {
|
||||
const rows = data as Array<UTSJSONObject>
|
||||
productList.value = rows.map((row: UTSJSONObject): PointProductItem => mapPointProduct(row))
|
||||
total.value = serverTotal > 0 ? serverTotal : rows.length
|
||||
} else {
|
||||
productList.value = []
|
||||
total.value = 0
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('[PointProducts] 加载失败:', err)
|
||||
uni.showToast({ title: '加载失败: ' + (err.message || '请检查网络'), icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 状态切换(同步写回数据库)
|
||||
// ──────────────────────────────────────────────
|
||||
async function toggleStatus(index: number) {
|
||||
const item = productList.value[index]
|
||||
const newStatus = item.status ? 0 : 1
|
||||
try {
|
||||
const { error } = await supa
|
||||
.from('ml_point_products')
|
||||
.update({ status: newStatus } as UTSJSONObject)
|
||||
.eq('id', item.id)
|
||||
.execute()
|
||||
if (error) {
|
||||
uni.showToast({ title: '状态更新失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
productList.value[index].status = !item.status
|
||||
} catch (err: any) {
|
||||
console.error('[PointProducts] 状态更新失败:', err)
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 搜索 / 翻页 / 切换 pageSize
|
||||
// ──────────────────────────────────────────────
|
||||
const handleSearch = () => {
|
||||
currentPage.value = 1
|
||||
fetchProducts()
|
||||
}
|
||||
const handleAdd = () => { console.log('Adding...') }
|
||||
const handleEdit = (item: PointProductItem) => { console.log('Editing...', item.id) }
|
||||
|
||||
const handlePageChange = (p: number) => {
|
||||
currentPage.value = p
|
||||
fetchProducts()
|
||||
}
|
||||
const handlePageSizeChange = (e: any) => {
|
||||
const idx = Number(e.detail.value)
|
||||
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
|
||||
currentPage.value = 1
|
||||
fetchProducts()
|
||||
}
|
||||
const handleJumpPage = () => {
|
||||
const p = parseInt(jumpPageInput)
|
||||
if (!isNaN(p) && p >= 1 && p <= totalPage.value) currentPage.value = p
|
||||
if (!isNaN(p) && p >= 1 && p <= totalPage.value) {
|
||||
currentPage.value = p
|
||||
fetchProducts()
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 生命周期
|
||||
// ──────────────────────────────────────────────
|
||||
onMounted(() => {
|
||||
fetchProducts()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -266,7 +411,7 @@ const handleJumpPage = () => {
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.select-val { font-size: 14px; color: #c0c4cc; }
|
||||
.select-val { font-size: 14px; color: #606266; }
|
||||
.arrow-down { font-size: 10px; color: #c0c4cc; }
|
||||
|
||||
.search-input {
|
||||
@@ -340,7 +485,7 @@ const handleJumpPage = () => {
|
||||
.td-txt-small { font-size: 13px; color: #515a6e; }
|
||||
|
||||
/* 列表各列宽度控制 */
|
||||
.th-id, .td-id { width: 60px; }
|
||||
/* .th-id, .td-id 宽度在下方覆盖为 160px(UUID 展示需要) */
|
||||
.th-img, .td-img { width: 80px; }
|
||||
.th-title, .td-title { flex: 1; min-width: 200px; }
|
||||
.th-integral, .td-integral { width: 100px; }
|
||||
@@ -396,5 +541,35 @@ const handleJumpPage = () => {
|
||||
|
||||
/* 分页区域已迁至 CommonPagination 组件 */
|
||||
|
||||
/* ID 列:UUID 需要更多宽度 */
|
||||
.th-id, .td-id { width: 160px; }
|
||||
.td-id-val { font-size: 11px; color: #aaa; word-break: break-all; }
|
||||
|
||||
/* 商品标题单元格:名称 + 类型标签 */
|
||||
.title-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.type-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background-color: #ecf5ff;
|
||||
border-radius: 3px;
|
||||
padding: 1px 6px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
.type-tag-txt { font-size: 11px; color: #409eff; }
|
||||
|
||||
/* 空状态 / 加载中行 */
|
||||
.empty-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px 0;
|
||||
width: 100%;
|
||||
}
|
||||
.empty-txt { font-size: 14px; color: #999; }
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user