Initial project commit
This commit is contained in:
@@ -0,0 +1,427 @@
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { onPullDownRefresh, onReachBottom, onShow } from "@dcloudio/uni-app";
|
||||
import {
|
||||
getShootingList,
|
||||
getMyShootings,
|
||||
getMyApplications,
|
||||
} from "@/api/shooting";
|
||||
import { getEventList, getMyEvents, getMyRegistrations } from "@/api/event";
|
||||
import { checkLogin } from "@/utils/auth";
|
||||
|
||||
const tabs = [
|
||||
{ key: "shooting_plaza", label: "约拍广场", type: "shooting", mine: false },
|
||||
{ key: "shooting_mine", label: "我的约拍", type: "shooting", mine: true },
|
||||
{ key: "event_plaza", label: "活动广场", type: "event", mine: false },
|
||||
{ key: "event_mine", label: "我的活动", type: "event", mine: true },
|
||||
];
|
||||
|
||||
const activeTab = ref("shooting_plaza");
|
||||
|
||||
const shootingPlaza = ref({ list: [], page: 1, total: 0, finished: false, loading: false });
|
||||
const shootingMine = ref({ list: [], page: 1, total: 0, finished: false, loading: false });
|
||||
const eventPlaza = ref({ list: [], page: 1, total: 0, finished: false, loading: false });
|
||||
const eventMine = ref({ list: [], page: 1, total: 0, finished: false, loading: false });
|
||||
|
||||
const stateMap = {
|
||||
shooting_plaza: shootingPlaza,
|
||||
shooting_mine: shootingMine,
|
||||
event_plaza: eventPlaza,
|
||||
event_mine: eventMine,
|
||||
};
|
||||
|
||||
const currentState = computed(() => stateMap[activeTab.value].value);
|
||||
const currentItems = computed(() => currentState.value.list || []);
|
||||
const isCurrentLoading = computed(() => currentState.value.loading);
|
||||
const isCurrentFinished = computed(() => currentState.value.finished);
|
||||
const currentTabMeta = computed(() => tabs.find((t) => t.key === activeTab.value));
|
||||
|
||||
const shootingStatusLabels = {
|
||||
open: "招募中",
|
||||
matched: "已匹配",
|
||||
closed: "已关闭",
|
||||
};
|
||||
const shootingStatusColors = {
|
||||
open: "#22c55e",
|
||||
matched: "#f59e0b",
|
||||
closed: "#9ca3af",
|
||||
};
|
||||
|
||||
const eventStatusLabels = {
|
||||
upcoming: "即将开始",
|
||||
ongoing: "进行中",
|
||||
ended: "已结束",
|
||||
cancelled: "已取消",
|
||||
};
|
||||
const eventStatusColors = {
|
||||
upcoming: "#6366f1",
|
||||
ongoing: "#22c55e",
|
||||
ended: "#9ca3af",
|
||||
cancelled: "#ef4444",
|
||||
};
|
||||
|
||||
const roleLabels = {
|
||||
photographer: "找摄影",
|
||||
cosplayer: "找Coser",
|
||||
both: "不限",
|
||||
};
|
||||
|
||||
function formatDateTime(val) {
|
||||
if (!val) return "时间待定";
|
||||
const dt = new Date(val);
|
||||
const y = dt.getFullYear();
|
||||
const m = String(dt.getMonth() + 1).padStart(2, "0");
|
||||
const d = String(dt.getDate()).padStart(2, "0");
|
||||
const hh = String(dt.getHours()).padStart(2, "0");
|
||||
const mm = String(dt.getMinutes()).padStart(2, "0");
|
||||
return `${y}-${m}-${d} ${hh}:${mm}`;
|
||||
}
|
||||
|
||||
function formatBudget(item) {
|
||||
if (item.is_free) return "互免";
|
||||
if (item.budget_min && item.budget_max) return `¥${item.budget_min}-${item.budget_max}`;
|
||||
if (item.budget_min) return `¥${item.budget_min}起`;
|
||||
if (item.budget_max) return `¥${item.budget_max}以内`;
|
||||
return "面议";
|
||||
}
|
||||
|
||||
function ensureLoginForMineTab() {
|
||||
if (!currentTabMeta.value?.mine) return true;
|
||||
return checkLogin();
|
||||
}
|
||||
|
||||
async function fetchTab(tabKey, reset = false) {
|
||||
const stateRef = stateMap[tabKey];
|
||||
const state = stateRef.value;
|
||||
if (state.loading) return;
|
||||
if (!reset && state.finished) return;
|
||||
|
||||
const tabMeta = tabs.find((t) => t.key === tabKey);
|
||||
if (tabMeta?.mine && !checkLogin()) return;
|
||||
|
||||
if (reset) {
|
||||
state.page = 1;
|
||||
state.total = 0;
|
||||
state.finished = false;
|
||||
state.list = [];
|
||||
}
|
||||
|
||||
state.loading = true;
|
||||
try {
|
||||
const params = { page: state.page, page_size: 20 };
|
||||
let res;
|
||||
|
||||
if (tabKey === "shooting_plaza") {
|
||||
params.status = "open";
|
||||
res = await getShootingList(params);
|
||||
} else if (tabKey === "shooting_mine") {
|
||||
const [mineRes, applyRes] = await Promise.all([
|
||||
getMyShootings(params),
|
||||
getMyApplications({ page: 1, page_size: 5 }),
|
||||
]);
|
||||
const mixed = [...(mineRes.items || [])];
|
||||
if (state.page === 1) {
|
||||
(applyRes.items || []).forEach((app) => {
|
||||
mixed.push({
|
||||
id: `applied_${app.id}`,
|
||||
_application: true,
|
||||
request_id: app.request_id,
|
||||
title: `我报名的约拍 #${app.request_id}`,
|
||||
city: "",
|
||||
status: app.status,
|
||||
created_at: app.created_at,
|
||||
message: app.message,
|
||||
});
|
||||
});
|
||||
}
|
||||
res = { items: mixed, total: (mineRes.total || 0) + (state.page === 1 ? (applyRes.items || []).length : 0) };
|
||||
} else if (tabKey === "event_plaza") {
|
||||
res = await getEventList(params);
|
||||
} else {
|
||||
const [mineRes, regRes] = await Promise.all([
|
||||
getMyEvents(params),
|
||||
getMyRegistrations({ page: 1, page_size: 5 }),
|
||||
]);
|
||||
const mixed = [...(mineRes.items || [])];
|
||||
if (state.page === 1) {
|
||||
(regRes.items || []).forEach((reg) => {
|
||||
mixed.push({
|
||||
id: `joined_${reg.id}`,
|
||||
_registration: true,
|
||||
event_id: reg.event_id,
|
||||
title: `我参加的活动 #${reg.event_id}`,
|
||||
city: "",
|
||||
status: reg.status,
|
||||
created_at: reg.created_at,
|
||||
});
|
||||
});
|
||||
}
|
||||
res = { items: mixed, total: (mineRes.total || 0) + (state.page === 1 ? (regRes.items || []).length : 0) };
|
||||
}
|
||||
|
||||
const items = res.items || [];
|
||||
if (reset) state.list = items;
|
||||
else state.list.push(...items);
|
||||
|
||||
state.total = res.total || 0;
|
||||
state.finished = items.length < 20 || state.list.length >= state.total;
|
||||
state.page += 1;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function switchTab(key) {
|
||||
activeTab.value = key;
|
||||
if (!ensureLoginForMineTab()) return;
|
||||
if (stateMap[key].value.list.length === 0) {
|
||||
fetchTab(key, true);
|
||||
}
|
||||
}
|
||||
|
||||
function goCreate() {
|
||||
if (currentTabMeta.value?.type === "shooting") {
|
||||
uni.navigateTo({ url: "/pages/shooting/create" });
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({ url: "/pages/event/create" });
|
||||
}
|
||||
|
||||
function goDetail(item) {
|
||||
if (item._application) {
|
||||
uni.navigateTo({ url: `/pages/shooting/detail?id=${item.request_id}` });
|
||||
return;
|
||||
}
|
||||
if (item._registration) {
|
||||
uni.navigateTo({ url: `/pages/event/detail?id=${item.event_id}` });
|
||||
return;
|
||||
}
|
||||
if (currentTabMeta.value?.type === "shooting") {
|
||||
uni.navigateTo({ url: `/pages/shooting/detail?id=${item.id}` });
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({ url: `/pages/event/detail?id=${item.id}` });
|
||||
}
|
||||
|
||||
onShow(() => {
|
||||
if (stateMap[activeTab.value].value.list.length === 0) {
|
||||
fetchTab(activeTab.value, true);
|
||||
}
|
||||
});
|
||||
|
||||
onPullDownRefresh(async () => {
|
||||
await fetchTab(activeTab.value, true);
|
||||
uni.stopPullDownRefresh();
|
||||
});
|
||||
|
||||
onReachBottom(() => {
|
||||
fetchTab(activeTab.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="activity-page">
|
||||
<view class="page-header">
|
||||
<text class="page-title">活动</text>
|
||||
<view class="create-btn" @tap="goCreate">
|
||||
<uni-icons type="plusempty" size="16" color="#fff" />
|
||||
<text>发布</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tabs">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@tap="switchTab(tab.key)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="list-wrap">
|
||||
<view
|
||||
v-for="item in currentItems"
|
||||
:key="item.id"
|
||||
class="card"
|
||||
@tap="goDetail(item)"
|
||||
>
|
||||
<view class="card-top">
|
||||
<text class="card-title">{{ item.title }}</text>
|
||||
<view
|
||||
class="status"
|
||||
:style="{
|
||||
background: currentTabMeta.type === 'shooting'
|
||||
? (shootingStatusColors[item.status] || '#9ca3af')
|
||||
: (eventStatusColors[item.status] || '#9ca3af')
|
||||
}"
|
||||
>
|
||||
{{ currentTabMeta.type === "shooting" ? (shootingStatusLabels[item.status] || item.status) : (eventStatusLabels[item.status] || item.status) }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card-row" v-if="item.city">
|
||||
<uni-icons type="location" size="14" color="#6366f1" />
|
||||
<text>{{ item.city }}</text>
|
||||
</view>
|
||||
|
||||
<view class="card-row" v-if="currentTabMeta.type === 'shooting' && item.role_needed">
|
||||
<uni-icons type="person" size="14" color="#6366f1" />
|
||||
<text>{{ roleLabels[item.role_needed] || item.role_needed }}</text>
|
||||
<text class="dot">·</text>
|
||||
<text>{{ formatBudget(item) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="card-row" v-if="item.message">
|
||||
<uni-icons type="chat" size="14" color="#6366f1" />
|
||||
<text class="line1">{{ item.message }}</text>
|
||||
</view>
|
||||
|
||||
<view class="card-bottom">
|
||||
<text>{{ formatDateTime(item.start_time || item.shoot_date || item.created_at) }}</text>
|
||||
<text v-if="currentTabMeta.type === 'shooting' && !item._application">{{ item.application_count || 0 }}人报名</text>
|
||||
<text v-if="currentTabMeta.type === 'event' && !item._registration">{{ item.registration_count || 0 }}人报名</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="isCurrentLoading" class="status-tip">加载中...</view>
|
||||
<view v-else-if="isCurrentFinished && currentItems.length" class="status-tip">没有更多了</view>
|
||||
<view v-else-if="!isCurrentLoading && !currentItems.length" class="empty-tip">
|
||||
<uni-icons type="info" size="40" color="#d1d5db" />
|
||||
<text>暂无内容</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.activity-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6fa;
|
||||
padding-bottom: 20rpx;
|
||||
}
|
||||
.page-header {
|
||||
background: #fff;
|
||||
padding: 20rpx 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.page-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #1e1e2e;
|
||||
}
|
||||
.create-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
padding: 10rpx 20rpx;
|
||||
border-radius: 32rpx;
|
||||
background: #6366f1;
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
border-top: 1rpx solid #f1f5f9;
|
||||
border-bottom: 1rpx solid #e5e7eb;
|
||||
}
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 20rpx 0;
|
||||
font-size: 26rpx;
|
||||
color: #6b7280;
|
||||
position: relative;
|
||||
}
|
||||
.tab-item.active {
|
||||
color: #6366f1;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tab-item.active::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 26%;
|
||||
right: 26%;
|
||||
bottom: 0;
|
||||
height: 4rpx;
|
||||
background: #6366f1;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
.list-wrap {
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx 28rpx;
|
||||
margin-top: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.card-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #1e1e2e;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.status {
|
||||
font-size: 22rpx;
|
||||
color: #fff;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.card-row {
|
||||
margin-top: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
color: #6b7280;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.dot {
|
||||
margin: 0 4rpx;
|
||||
}
|
||||
.line1 {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.card-bottom {
|
||||
margin-top: 12rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 22rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
.status-tip {
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #9ca3af;
|
||||
padding: 30rpx 0;
|
||||
}
|
||||
.empty-tip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 120rpx 0;
|
||||
font-size: 28rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user