Files
gao-customer-h5/src/pages/point/PointRedeemSkuList.vue
T
2026-09-23 13:40:45 +08:00

249 lines
5.8 KiB
Vue

<template>
<div v-can="permissionCodes.pointRedeemSku.manage" class="redeem-sku-list">
<div class="filters glass">
<t-input
v-model.trim="keyword"
class="keyword"
type="search"
borderless
clearable
placeholder="搜索产品名称、编码或 SKU"
/>
</div>
<t-pull-down-refresh v-model="isRefreshing" :refresh-timeout="30000" @refresh="reload" @scrolltolower="load">
<t-cell-group v-if="result.list.length" theme="card">
<t-cell
v-for="(item, index) in result.list"
:key="item.id"
class="item"
arrow
v-entrance-transition="index * 30"
@click="toEdit(item)"
>
<template #left-icon>
<t-image
v-if="getProductPhoto(item)"
:src="getProductPhoto(item)"
:alt="item.product?.name || item.sku?.name || '产品图片'"
fit="cover"
class="avatar"
/>
<div
v-else
class="avatar fallback"
v-text="(item.product?.name || item.sku?.name || '产').slice(0, 1)"
/>
</template>
<template #title>
<span class="clip-text" v-text="item.product?.name || item.sku?.name || '未命名产品'" />
</template>
<template #note>
<span class="orange keep-text" v-text="formatPoint(item.defaultPointCost)" />
</template>
<template #description>
<div class="description">
<t-tag
size="small"
:theme="item.status === 'ACTIVE' ? 'success' : 'default'"
:content="statusText(item.status)"
/>
<product-sku-tags
:product-name="item.product?.name"
:fallback="item.sku?.specSummary || item.sku?.name || '默认 SKU'"
:unit="item.unit?.name"
/>
</div>
</template>
</t-cell>
</t-cell-group>
<t-empty v-else :description="isLoading ? '正在加载兑换配置' : '暂无兑换配置'" />
<div class="tips" v-text="isNoMore ? `共 ${result.total} 条配置` : '上拉加载更多'" />
</t-pull-down-refresh>
</div>
<nav-bar-action-menu
v-can="permissionCodes.pointRedeemSku.manage"
:actions="[{ key: 'create', icon: 'add', text: '新增兑换配置', theme: 'primary', action: toCreate }]"
/>
</template>
<script setup lang="ts">
import { Toolkit } from "timi-web";
import PointAPI from "@/api/PointAPI";
import ProductSkuTags from "@/components/ProductSkuTags.vue";
import { useNavBarStore } from "@/store/navBarStore";
import type { PointRedeemSku, PointRedeemSkuStatus } from "@/types/Point";
import { Message } from "@/utils/message";
import { formatWanInteger } from "@/utils/money";
import { getProductPhotoURL } from "@/utils/product";
import { permissionCodes } from "@/utils/permission";
defineOptions({ name: "PointRedeemSkuList" });
const router = useRouter();
const navBarStore = useNavBarStore();
const result = reactive({ total: 0, list: [] as PointRedeemSku[] });
const keyword = shallowRef("");
const index = ref(0);
const isLoading = ref(false);
const isRefreshing = ref(false);
const isNoMore = ref(false);
const pageSize = 20;
let reloadRequested = false;
async function reload(): Promise<void> {
if (isLoading.value) {
reloadRequested = true;
return;
}
index.value = 0;
result.total = 0;
result.list = [];
isNoMore.value = false;
try {
await load();
} finally {
isRefreshing.value = false;
}
}
async function load(): Promise<void> {
if (isLoading.value || isNoMore.value) {
return;
}
isLoading.value = true;
try {
const page = await PointAPI.redeemSkuList({
index: index.value,
size: pageSize,
keyword: keyword.value.trim() || undefined
});
const list = page.list || [];
result.list = result.list.concat(list);
result.total = page.total || 0;
index.value++;
isNoMore.value = result.total <= result.list.length || list.length < pageSize;
} catch {
Message.error("兑换配置加载失败");
} finally {
isLoading.value = false;
if (reloadRequested) {
reloadRequested = false;
await reload();
}
}
}
const reloadByKeyword = Toolkit.debounce(async () => {
await reload();
}, false);
function formatPoint(value?: number): string {
return `${formatWanInteger(value ?? 0)} 分`;
}
function getProductPhoto(item: PointRedeemSku): string {
const product = item.product || item.sku;
return product ? getProductPhotoURL(product) : "";
}
function statusText(status?: PointRedeemSkuStatus): string {
return ({
DRAFT: "草稿",
ACTIVE: "启用",
INACTIVE: "停用",
DELETED: "已删除"
} as Record<string, string>)[status || ""] || "未知";
}
async function toCreate(): Promise<void> {
await router.push({ name: "PointRedeemSkuCreate" });
}
async function toEdit(item: PointRedeemSku): Promise<void> {
if (item.id) {
await router.push({ name: "PointRedeemSkuEdit", params: { id: item.id } });
}
}
onMounted(async () => {
navBarStore.setCustomRightVisible(true);
await reload();
});
watch(keyword, () => {
reloadByKeyword();
});
onUnmounted(() => {
reloadByKeyword.cancel();
navBarStore.clearRight();
});
</script>
<style scoped lang="less">
.redeem-sku-list {
gap: 1rem;
display: flex;
padding: var(--app-nav-height) 0 5rem;
flex-direction: column;
.filters {
top: var(--app-nav-height);
left: 0;
right: 0;
z-index: 10;
display: flex;
padding: .75rem 1rem;
position: sticky;
flex-direction: column;
.keyword {
--td-input-vertical-padding: .25rem 1rem;
border: 1px solid var(--td-component-border);
border-radius: 1rem;
}
}
.item {
padding: 10px;
.avatar {
width: 3.5rem;
height: 3.5rem;
}
.description {
gap: .5rem;
display: flex;
align-items: center;
}
}
.title {
gap: .5rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.tips {
color: var(--td-text-color-secondary);
padding: 1rem;
text-align: center;
}
}
:global(html.theme-dark) {
.redeem-sku-list {
.filters {
background: #10161f;
}
.fallback {
color: #ccfbf1;
background: #115e59;
}
}
}
</style>