<template>
|
<cl-page background-color="#f7f7f7">
|
<view class="record-page">
|
<!-- 顶部 Tabs -->
|
<cl-sticky>
|
<view class="tab-bar">
|
<view
|
v-for="tab in tabs"
|
:key="tab.value"
|
class="tab-item"
|
:class="{ 'is-active': activeTab === tab.value }"
|
@tap="switchTab(tab.value)"
|
>
|
<text>{{ tab.label }}</text>
|
<view v-if="activeTab === tab.value" class="tab-line"></view>
|
</view>
|
</view>
|
</cl-sticky>
|
|
<!-- 列表 -->
|
<scroll-view
|
class="record-scroll"
|
scroll-y
|
:lower-threshold="80"
|
@scrolltolower="loadMore"
|
:refresher-enabled="true"
|
:refresher-triggered="refreshing"
|
@refresherrefresh="onRefresh"
|
>
|
<view class="record-list">
|
<view
|
v-for="item in list"
|
:key="item.id"
|
class="record-card"
|
@tap="toggleDetail(item)"
|
>
|
<view class="card-header">
|
<text class="order-no">单号:{{ item.orderNo }}</text>
|
<text class="status" :class="`status-${item.status}`">
|
{{ item.status }}
|
</text>
|
</view>
|
<view class="card-body">
|
<view class="info-row">
|
<text class="info-label">回收类型</text>
|
<text class="info-value">{{ item.orderType || "-" }}</text>
|
</view>
|
<view class="info-row">
|
<text class="info-label">总重量</text>
|
<text class="info-value">{{ item.totalWeight ?? 0 }} kg</text>
|
</view>
|
<view class="info-row">
|
<text class="info-label">获得积分</text>
|
<text class="info-value text-green">+{{ item.totalCarbonPoint ?? 0 }}</text>
|
</view>
|
<view class="info-row">
|
<text class="info-label">回收时间</text>
|
<text class="info-value">{{ item.finishTime || item.createTime || "-" }}</text>
|
</view>
|
</view>
|
|
<view class="card-toggle">
|
<text>{{ isExpanded(item) ? "收起明细" : "查看明细" }}</text>
|
<cl-icon
|
:name="isExpanded(item) ? 'arrow-top' : 'arrow-bottom'"
|
:size="24"
|
color="#999"
|
/>
|
</view>
|
|
<view v-if="isExpanded(item)" class="item-detail" @tap.stop>
|
<view v-if="detailLoading[item.orderNo]" class="detail-tip">
|
加载中...
|
</view>
|
<view
|
v-else-if="!(detailMap[item.orderNo] || []).length"
|
class="detail-tip"
|
>
|
暂无明细
|
</view>
|
<view v-else class="detail-list">
|
<view
|
v-for="row in detailMap[item.orderNo]"
|
:key="row.id"
|
class="detail-row"
|
>
|
<view class="detail-main">
|
<text class="detail-name">{{ row.itemName || "-" }}</text>
|
<text class="detail-category">{{ row.category || "" }}</text>
|
</view>
|
<view class="detail-nums">
|
<text>{{ row.weight ?? 0 }} kg</text>
|
<text class="text-green">+{{ row.carbonPoint ?? 0 }}</text>
|
</view>
|
</view>
|
</view>
|
</view>
|
</view>
|
</view>
|
|
<cl-loadmore
|
v-if="list.length > 0"
|
:loading="loading"
|
:finish="finished"
|
:divider="false"
|
/>
|
|
<cl-empty
|
v-if="!loading && list.length === 0"
|
:fixed="false"
|
text="暂无回收记录"
|
/>
|
</scroll-view>
|
</view>
|
</cl-page>
|
</template>
|
|
<script lang="ts" setup>
|
import { ref } from "vue";
|
import { onReady, onPullDownRefresh } from "@dcloudio/uni-app";
|
import { useCool, useStore } from "/@/cool";
|
|
const { service } = useCool();
|
const { user } = useStore();
|
|
const tabs = [
|
{ label: "现场回收", value: "onsite" },
|
{ label: "预约回收", value: "appointment" },
|
] as const;
|
|
type TabValue = (typeof tabs)[number]["value"];
|
|
const activeTab = ref<TabValue>("onsite");
|
const list = ref<Eps.RecycleOrderEntity[]>([]);
|
const loading = ref(false);
|
const refreshing = ref(false);
|
const finished = ref(false);
|
const page = ref(1);
|
const size = 10;
|
|
const expandedOrderNo = ref("");
|
const detailMap = ref<Record<string, Eps.RecycleOrderItemEntity[]>>({});
|
const detailLoading = ref<Record<string, boolean>>({});
|
|
let reqSeq = 0;
|
|
function getUnionid() {
|
return user.info?.phone || "18918594752";
|
}
|
|
/** 现场:appointmentId 为空;预约:appointmentId 不为空 */
|
function buildQuery() {
|
const unionid = getUnionid();
|
const isOnsite = activeTab.value === "onsite";
|
|
return {
|
page: page.value,
|
size,
|
campusUserId: unionid,
|
unionid,
|
orderType: isOnsite ? "现场" : "预约",
|
// 供后端 pageQueryOp.where 判断 appointmentId 是否为空(标准 fieldEq 不会处理 IS NULL)
|
appointmentIdIsNull: isOnsite,
|
};
|
}
|
|
async function loadList(reset = false) {
|
if (loading.value && !reset) return;
|
if (!reset && finished.value) return;
|
|
if (reset) {
|
page.value = 1;
|
finished.value = false;
|
list.value = [];
|
expandedOrderNo.value = "";
|
detailMap.value = {};
|
detailLoading.value = {};
|
}
|
|
const current = ++reqSeq;
|
loading.value = true;
|
|
try {
|
const res = await service.shop.recycleorder.page(buildQuery());
|
if (current !== reqSeq) return;
|
|
console.log("【回收记录返回】", res);
|
|
const newList = res?.list || [];
|
list.value = page.value === 1 ? newList : [...list.value, ...newList];
|
|
const total = res?.pagination?.total;
|
finished.value =
|
newList.length < size || (typeof total === "number" && list.value.length >= total);
|
page.value++;
|
} catch (e) {
|
if (current !== reqSeq) return;
|
console.error("【回收记录加载失败】", e);
|
} finally {
|
if (current === reqSeq) {
|
loading.value = false;
|
refreshing.value = false;
|
}
|
}
|
}
|
|
function isExpanded(item: Eps.RecycleOrderEntity) {
|
return !!item.orderNo && expandedOrderNo.value === item.orderNo;
|
}
|
|
async function loadDetail(orderNo: string) {
|
if (detailMap.value[orderNo] || detailLoading.value[orderNo]) return;
|
|
detailLoading.value = { ...detailLoading.value, [orderNo]: true };
|
try {
|
const res = await service.shop.recycleorderitem.page({
|
page: 1,
|
size: 50,
|
orderNo,
|
});
|
console.log("【回收明细返回】", orderNo, res);
|
detailMap.value = {
|
...detailMap.value,
|
[orderNo]: res?.list || [],
|
};
|
} catch (e) {
|
console.error("【回收明细加载失败】", e);
|
detailMap.value = { ...detailMap.value, [orderNo]: [] };
|
} finally {
|
detailLoading.value = { ...detailLoading.value, [orderNo]: false };
|
}
|
}
|
|
function toggleDetail(item: Eps.RecycleOrderEntity) {
|
const orderNo = item.orderNo;
|
if (!orderNo) return;
|
|
if (expandedOrderNo.value === orderNo) {
|
expandedOrderNo.value = "";
|
return;
|
}
|
|
expandedOrderNo.value = orderNo;
|
loadDetail(orderNo);
|
}
|
|
function switchTab(val: TabValue) {
|
if (activeTab.value === val) return;
|
activeTab.value = val;
|
loadList(true);
|
}
|
|
function loadMore() {
|
if (!loading.value && !finished.value) {
|
loadList(false);
|
}
|
}
|
|
function onRefresh() {
|
refreshing.value = true;
|
loadList(true);
|
}
|
|
onReady(() => {
|
loadList(true);
|
});
|
|
onPullDownRefresh(() => {
|
loadList(true).finally(() => {
|
uni.stopPullDownRefresh();
|
});
|
});
|
</script>
|
|
<style lang="scss" scoped>
|
.record-page {
|
height: 100vh;
|
display: flex;
|
flex-direction: column;
|
|
.tab-bar {
|
display: flex;
|
background: #fff;
|
padding: 0 40rpx;
|
border-bottom: 1rpx solid #f0f0f0;
|
|
.tab-item {
|
position: relative;
|
padding: 28rpx 30rpx;
|
font-size: 28rpx;
|
color: #666;
|
transition: color 0.2s;
|
|
&.is-active {
|
color: #2ecc71;
|
font-weight: 600;
|
}
|
|
.tab-line {
|
position: absolute;
|
bottom: 0;
|
left: 50%;
|
transform: translateX(-50%);
|
width: 40rpx;
|
height: 4rpx;
|
background: #2ecc71;
|
border-radius: 2rpx;
|
}
|
}
|
}
|
|
.record-scroll {
|
flex: 1;
|
height: 0;
|
padding: 24rpx;
|
box-sizing: border-box;
|
}
|
|
.record-list {
|
display: flex;
|
flex-direction: column;
|
gap: 20rpx;
|
}
|
|
.record-card {
|
background: #fff;
|
border-radius: 20rpx;
|
padding: 28rpx;
|
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
|
.card-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
padding-bottom: 20rpx;
|
border-bottom: 1rpx solid #f5f5f5;
|
}
|
|
.order-no {
|
font-size: 26rpx;
|
color: #333;
|
font-weight: 500;
|
}
|
|
.status {
|
font-size: 22rpx;
|
padding: 6rpx 16rpx;
|
border-radius: 20rpx;
|
background: #f5f5f5;
|
color: #999;
|
|
&.status-待处理 {
|
background: #fef5e7;
|
color: #e67e22;
|
}
|
&.status-进行中 {
|
background: #eaf4fc;
|
color: #2980b9;
|
}
|
&.status-已完成 {
|
background: #e8f8f0;
|
color: #27ae60;
|
}
|
&.status-已取消 {
|
background: #fef0f0;
|
color: #e74c3c;
|
}
|
}
|
|
.card-body {
|
padding-top: 20rpx;
|
display: flex;
|
flex-direction: column;
|
gap: 14rpx;
|
}
|
|
.info-row {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
}
|
|
.info-label {
|
font-size: 24rpx;
|
color: #999;
|
}
|
|
.info-value {
|
font-size: 26rpx;
|
color: #333;
|
font-weight: 500;
|
}
|
|
.text-green {
|
color: #27ae60;
|
font-weight: 600;
|
}
|
|
.card-toggle {
|
margin-top: 16rpx;
|
padding-top: 16rpx;
|
border-top: 1rpx dashed #f0f0f0;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
gap: 8rpx;
|
font-size: 22rpx;
|
color: #999;
|
}
|
|
.item-detail {
|
margin-top: 16rpx;
|
padding: 16rpx;
|
background: #f8faf8;
|
border-radius: 12rpx;
|
}
|
|
.detail-tip {
|
text-align: center;
|
font-size: 22rpx;
|
color: #999;
|
padding: 12rpx 0;
|
}
|
|
.detail-list {
|
display: flex;
|
flex-direction: column;
|
gap: 12rpx;
|
}
|
|
.detail-row {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
gap: 16rpx;
|
}
|
|
.detail-main {
|
flex: 1;
|
min-width: 0;
|
display: flex;
|
flex-direction: column;
|
gap: 4rpx;
|
}
|
|
.detail-name {
|
font-size: 24rpx;
|
color: #333;
|
font-weight: 500;
|
}
|
|
.detail-category {
|
font-size: 20rpx;
|
color: #999;
|
}
|
|
.detail-nums {
|
display: flex;
|
align-items: center;
|
gap: 16rpx;
|
font-size: 22rpx;
|
color: #666;
|
flex-shrink: 0;
|
}
|
}
|
}
|
</style>
|