my-works.vue 8.39 KB
<template>
  <view class="page">
    <view class="nav">
      <view :style="{ height: statusBarHeight + 'px' }" />
      <view class="nav-inner">
        <view class="nav-btn" @click="goBack">
          <image
            class="nav-back-img"
            :src="backIcon"
            mode="aspectFit"
          />
        </view>
        <text class="nav-title">我的作品</text>
        <view class="nav-placeholder" />
      </view>
    </view>

    <view class="main" :style="{ paddingTop: contentTopPx + 'px' }">
      <view v-if="loading" class="state">
        <text class="state-txt">加载中…</text>
      </view>
      <view v-else-if="works.length" class="list">
        <view
          v-for="w in works"
          :key="w.id"
          class="card"
          @click="openDetail(w.id)"
        >
          <image class="cover" :src="w.imageUrl" mode="aspectFill" />
          <view class="info">
            <text class="title">{{ w.title }}</text>
            <text class="meta">{{ w.publishedAt }}</text>
            <view
              v-if="w.status === 'rejected' && w.rejectReason"
              class="reject-snippet"
              @click.stop="goResubmit(w.id)"
            >
              <text class="reject-snippet-txt">{{ w.rejectReason }}</text>
              <text class="reject-edit">去修改 ›</text>
            </view>
            <view
              class="row"
              :class="{ 'row-no-likes': w.status !== 'approved' }"
            >
              <text
                :class="[
                  'status',
                  w.status === 'approved'
                    ? 'st-ok'
                    : w.status === 'rejected'
                      ? 'st-reject'
                      : 'st-wait',
                ]"
              >
                {{
                  w.status === "approved"
                    ? "已通过"
                    : w.status === "rejected"
                      ? "未通过"
                      : "审核中"
                }}
              </text>
              <view v-if="w.status === 'approved'" class="likes">
                <image
                  class="heart"
                  :src="w.likes > 0 ? likeOnIcon : likeOffIcon"
                  mode="aspectFit"
                />
                <text class="likes-num">{{ w.likes }}</text>
              </view>
            </view>
          </view>
        </view>
      </view>
      <view v-else class="empty">
        <text class="empty-txt">{{
          loadError || "暂无作品,去上传页发布吧"
        }}</text>
      </view>
    </view>
  </view>
</template>

<script setup lang="ts">
import { onMounted, ref } from "vue";
import { myWorksList, type MyWorkItem } from "@/data/userWorks";
import { apiEnabled } from "@/config/api";
import { get, getMpToken, isMpSessionExpiredError } from "@/utils/api/http";
import { ensureMpSession, getMpLoginChannel } from "@/utils/api/mpSession";
import { resolveMediaUrl } from "@/utils/mediaUrl";
import { setUploadResubmitWorkId } from "@/utils/uploadResubmit";
import { staticAsset } from "@/utils/staticAsset";

const statusBarHeight = ref(20);
const backIcon = staticAsset("back.png");
const likeOnIcon = staticAsset("cang_on.png");
const likeOffIcon = staticAsset("cang.png");
const contentTopPx = ref(64);
const apiOn = apiEnabled();
/** 接接口:首屏不展示静态列表,等请求结束 */
const works = ref<MyWorkItem[]>(apiOn ? [] : myWorksList);
const loading = ref(apiOn);
const loadError = ref("");

onMounted(() => {
  const info = uni.getSystemInfoSync();
  statusBarHeight.value = info.statusBarHeight || 20;
  contentTopPx.value =
    statusBarHeight.value + uni.upx2px(88) + uni.upx2px(32);
  if (apiOn) {
    void loadWorks();
  }
});

async function loadWorks() {
  if (!apiEnabled()) {
    works.value = myWorksList;
    loading.value = false;
    return;
  }
  // #ifdef MP-WEIXIN
  if (!getMpToken() || getMpLoginChannel() !== "weixin") {
    loading.value = false;
    uni.reLaunch({ url: "/pages/login/login?tab=user" });
    return;
  }
  // #endif
  loading.value = true;
  loadError.value = "";
  try {
    // #ifndef MP-WEIXIN
    await ensureMpSession();
    // #endif
    const res = await get<{
      list: Array<{
        id: number;
        imageUrl: string;
        title: string;
        likes: number;
        status: string;
        rejectReason?: string;
        publishedAt: string;
      }>;
    }>("/api/v1/me/works");
    works.value = res.data.list.map((w) => {
      const st =
        w.status === "approved"
          ? "approved"
          : w.status === "rejected"
            ? "rejected"
            : "pending";
      return {
        id: w.id,
        imageUrl: resolveMediaUrl(w.imageUrl),
        title: w.title,
        likes: w.likes,
        status: st,
        rejectReason: typeof w.rejectReason === "string" ? w.rejectReason : "",
        publishedAt: w.publishedAt,
      };
    });
  } catch (e) {
    if (isMpSessionExpiredError(e)) {
      return;
    }
    works.value = [];
    const msg =
      e && typeof e === "object" && "msg" in e && typeof (e as { msg: string }).msg === "string"
        ? (e as { msg: string }).msg
        : "加载失败,请稍后重试";
    loadError.value = msg;
    uni.showToast({ title: msg.length > 20 ? msg.slice(0, 20) + "…" : msg, icon: "none" });
  } finally {
    loading.value = false;
  }
}

function goBack() {
  uni.navigateBack({ delta: 1 });
}

function openDetail(id: number) {
  uni.navigateTo({
    url: `/pages/photo-detail/photo-detail?id=${id}`,
  });
}

function goResubmit(id: number) {
  if (!Number.isInteger(id) || id <= 0) {
    uni.showToast({ title: "作品信息异常,请稍后重试", icon: "none" });
    return;
  }
  setUploadResubmitWorkId(id);
  uni.switchTab({
    url: "/pages/upload/upload",
    fail: () => {
      uni.showToast({ title: "跳转失败,请稍后重试", icon: "none" });
    },
  });
}
</script>

<style scoped>
.page {
  min-height: 100vh;
  background: #f9fafb;
}

.nav {
  position: fixed;
  top: 0;
  right: 0;
  left: 0;
  z-index: 50;
  background: rgba(255, 255, 255, 0.98);
  border-bottom: 1rpx solid #e5e7eb;
}

.nav-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 88rpx;
}

.nav-btn {
  display: flex;
  width: 72rpx;
  height: 72rpx;
  align-items: center;
  justify-content: center;
}

.nav-back-img {
  width: 44rpx;
  height: 44rpx;
}

.nav-title {
  font-size: 30rpx;
  color: #111827;
}

.nav-placeholder {
  width: 72rpx;
}

.main {
  padding: 0 16rpx 48rpx;
  box-sizing: border-box;
}

.state {
  padding: 120rpx 16rpx;
  text-align: center;
}

.state-txt {
  font-size: 28rpx;
  color: #9ca3af;
}

.list {
  display: flex;
  flex-direction: column;
  gap: 24rpx;
}

.card {
  display: flex;
  overflow: hidden;
  border-radius: 24rpx;
  background: #fff;
  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
}

.cover {
  width: 220rpx;
  height: 220rpx;
  flex-shrink: 0;
  background: #f3f4f6;
}

.info {
  display: flex;
  flex: 1;
  flex-direction: column;
  justify-content: center;
  padding: 24rpx 28rpx;
  min-width: 0;
}

.title {
  font-size: 30rpx;
  font-weight: 500;
  color: #111827;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.meta {
  margin-top: 12rpx;
  font-size: 24rpx;
  color: #6b7280;
}

.row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 20rpx;
}

.row-no-likes {
  justify-content: flex-start;
}

.status {
  padding: 8rpx 16rpx;
  font-size: 22rpx;
  border-radius: 8rpx;
}

.st-ok {
  color: #fff;
  background: #4a90e2;
}

.st-wait {
  color: #fff;
  background: #eab308;
}

.st-reject {
  color: #fff;
  background: #ef4444;
}

.reject-snippet {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16rpx;
  margin-top: 12rpx;
  padding: 12rpx 16rpx;
  border-radius: 12rpx;
  background: #fef2f2;
}

.reject-snippet-txt {
  flex: 1;
  min-width: 0;
  font-size: 22rpx;
  line-height: 1.45;
  color: #991b1b;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.reject-edit {
  flex-shrink: 0;
  font-size: 22rpx;
  color: #4a90e2;
}

.likes {
  display: flex;
  align-items: center;
  gap: 8rpx;
}

.heart {
  width: 28rpx;
  height: 28rpx;
}

.likes-num {
  font-size: 26rpx;
  color: #374151;
}

.empty {
  padding: 120rpx 16rpx;
  text-align: center;
}

.empty-txt {
  font-size: 28rpx;
  color: #9ca3af;
}
</style>