Files
CosScene/clients/pages/mine/my-spots.vue
T
2026-05-09 16:40:29 +08:00

356 lines
7.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { ref } from "vue";
import { onPullDownRefresh, onReachBottom, onShow } from "@dcloudio/uni-app";
import { getMySpots, deleteSpot } from "@/api/spot";
import { extractList } from "@/utils/request";
import { resolveImageUrl } from "@/utils/image";
import { checkLogin } from "@/utils/auth";
import { formatSpotPrice } from "@/utils/spot";
onShow(() => {
if (checkLogin()) fetchSpots(true);
});
const spots = ref([]);
const page = ref(1);
const pageSize = 10;
const hasMore = ref(true);
const loading = ref(false);
const statusMap = {
pending: { text: "待审核", color: "#f59e0b", bg: "rgba(245,158,11,0.1)" },
approved: { text: "已通过", color: "#22c55e", bg: "rgba(34,197,94,0.1)" },
rejected: { text: "已拒绝", color: "#ef4444", bg: "rgba(239,68,68,0.1)" },
};
const getStatus = (s) => statusMap[s] || { text: s, color: "#94a3b8", bg: "#f5f6fa" };
const fetchSpots = async (reset = false) => {
if (loading.value) return;
if (!reset && !hasMore.value) return;
loading.value = true;
if (reset) {
page.value = 1;
hasMore.value = true;
}
try {
const res = await getMySpots({ page: page.value, page_size: pageSize });
const list = extractList(res);
if (reset) {
spots.value = list;
} else {
spots.value.push(...list);
}
if (list.length < pageSize) {
hasMore.value = false;
} else {
page.value++;
}
} catch (e) {
console.error(e);
} finally {
loading.value = false;
}
};
const goDetail = (id) => {
uni.navigateTo({ url: `/pages/spot/detail?id=${id}` });
};
const goEdit = (id) => {
uni.navigateTo({ url: `/pages/spot/edit?id=${id}` });
};
const handleDelete = (item) => {
uni.showModal({
title: "确认删除",
content: `确定要删除「${item.title}」吗?此操作不可撤销。`,
confirmColor: "#ef4444",
success: async (res) => {
if (res.confirm) {
try {
await deleteSpot(item.id);
uni.showToast({ title: "已删除", icon: "success" });
spots.value = spots.value.filter((s) => s.id !== item.id);
} catch (e) {
console.error(e);
}
}
},
});
};
onPullDownRefresh(async () => {
await fetchSpots(true);
uni.stopPullDownRefresh();
});
onReachBottom(() => {
fetchSpots();
});
fetchSpots(true);
</script>
<template>
<view class="my-spots-page">
<view class="list">
<view
v-for="item in spots"
:key="item.id"
class="spot-card"
@tap="goDetail(item.id)"
>
<image
v-if="item.cover_image_url"
class="cover"
:src="resolveImageUrl(item.cover_image_url)"
mode="aspectFill"
/>
<view v-else class="cover cover-placeholder">
<uni-icons type="camera" size="32" color="#94a3b8" class="placeholder-icon" />
</view>
<view class="info">
<view class="title-row">
<text class="title">{{ item.title }}</text>
<view
class="status-badge"
:style="{ background: getStatus(item.audit_status).bg }"
>
<text
class="status-text"
:style="{ color: getStatus(item.audit_status).color }"
>
{{ getStatus(item.audit_status).text }}
</text>
</view>
</view>
<view class="meta-row">
<view class="city"><uni-icons type="location" size="14" color="#64748b" /> {{ item.city || "未知城市" }}</view>
<text
class="price-text"
:class="{ free: formatSpotPrice(item).isFree, paid: !formatSpotPrice(item).isFree }"
>
{{ formatSpotPrice(item).label }}
</text>
</view>
<view
v-if="item.audit_status === 'rejected' && item.reject_reason"
class="reject-row"
>
<text class="reject-label">拒绝原因</text>
<text class="reject-reason">{{ item.reject_reason }}</text>
</view>
<view class="action-row">
<view class="action-btn edit-btn" @tap.stop="goEdit(item.id)">
<uni-icons type="compose" size="16" color="#6366f1" />
<text class="action-text edit-text">编辑</text>
</view>
<view class="action-btn delete-btn" @tap.stop="handleDelete(item)">
<uni-icons type="trash" size="16" color="#ef4444" />
<text class="action-text delete-text">删除</text>
</view>
</view>
</view>
</view>
<view v-if="loading" class="status-tip">
<text>加载中...</text>
</view>
<view v-else-if="!hasMore && spots.length > 0" class="status-tip">
<text>没有更多了</text>
</view>
<view v-else-if="!loading && spots.length === 0" class="empty-state">
<uni-icons type="location" size="48" color="#6366f1" class="empty-icon" />
<text class="empty-title">还没有投稿地点</text>
<text class="empty-desc">去投稿你发现的取景地吧</text>
</view>
</view>
</view>
</template>
<style scoped>
.my-spots-page {
min-height: 100vh;
background: #f5f6fa;
}
.list {
padding: 24rpx 32rpx;
}
.spot-card {
background: #ffffff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
margin-bottom: 24rpx;
}
.cover {
width: 100%;
height: 280rpx;
}
.cover-placeholder {
background: #e2e8f0;
display: flex;
align-items: center;
justify-content: center;
}
.placeholder-icon {
font-size: 64rpx;
}
.info {
padding: 20rpx 24rpx 24rpx;
}
.title-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
}
.title {
font-size: 30rpx;
font-weight: 600;
color: #1e293b;
flex: 1;
margin-right: 16rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-badge {
padding: 6rpx 16rpx;
border-radius: 8rpx;
flex-shrink: 0;
}
.status-text {
font-size: 22rpx;
font-weight: 600;
}
.meta-row {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 8rpx;
flex-wrap: wrap;
}
.city {
font-size: 24rpx;
color: #64748b;
}
.price-text {
font-size: 24rpx;
font-weight: 600;
}
.price-text.free {
color: #16a34a;
}
.price-text.paid {
color: #d97706;
}
.reject-row {
margin-top: 12rpx;
background: rgba(239, 68, 68, 0.06);
padding: 16rpx 20rpx;
border-radius: 10rpx;
}
.reject-label {
font-size: 24rpx;
color: #ef4444;
font-weight: 500;
}
.reject-reason {
font-size: 24rpx;
color: #64748b;
line-height: 1.5;
}
.action-row {
display: flex;
gap: 16rpx;
margin-top: 16rpx;
padding-top: 16rpx;
border-top: 1rpx solid #f1f5f9;
}
.action-btn {
display: flex;
align-items: center;
gap: 6rpx;
padding: 10rpx 24rpx;
border-radius: 8rpx;
}
.edit-btn {
background: rgba(99, 102, 241, 0.08);
}
.delete-btn {
background: rgba(239, 68, 68, 0.08);
}
.action-text {
font-size: 24rpx;
font-weight: 500;
}
.edit-text {
color: #6366f1;
}
.delete-text {
color: #ef4444;
}
.status-tip {
text-align: center;
padding: 40rpx 0;
color: #94a3b8;
font-size: 26rpx;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 160rpx 0;
}
.empty-icon {
font-size: 96rpx;
margin-bottom: 24rpx;
}
.empty-title {
font-size: 32rpx;
font-weight: 600;
color: #1e293b;
margin-bottom: 12rpx;
}
.empty-desc {
font-size: 26rpx;
color: #94a3b8;
}
</style>