profile-setup.vue 7.04 KB
<template>
  <view class="page">
    <view class="header">
      <text class="title">完善个人资料</text>
      <text class="sub">设置头像、昵称和手机号,让作品更有辨识度</text>
    </view>

    <!-- 头像 -->
    <view class="avatar-section">
      <button class="avatar-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
        <image
          v-if="avatarTemp"
          class="avatar-img"
          :src="avatarTemp"
          mode="aspectFill"
        />
        <view v-else class="avatar-placeholder">
          <text class="avatar-placeholder-icon">+</text>
          <text class="avatar-placeholder-text">点击设置头像</text>
        </view>
      </button>
    </view>

    <!-- 昵称 -->
    <view class="name-section">
      <text class="label">昵称</text>
      <input
        class="name-input"
        type="nickname"
        v-model="nickname"
        maxlength="16"
        placeholder="填写昵称"
        placeholder-class="name-ph"
      />
      <view class="line" />
    </view>

    <!-- 手机号 -->
    <!-- #ifdef MP-WEIXIN -->
    <view class="phone-section">
      <text class="label">手机号</text>
      <button
        class="phone-btn"
        open-type="getPhoneNumber"
        @getphonenumber="onGetPhoneNumber"
      >
        <text v-if="phoneNumber" class="phone-val">{{ phoneNumber }}</text>
        <text v-else class="phone-ph">点击获取微信绑定手机号</text>
      </button>
      <view class="line" />
    </view>
    <!-- #endif -->

    <view class="actions">
      <button class="btn-save" :disabled="saving" @click="onSubmit">
        <text v-if="saving">保存中…</text>
        <text v-else>完成</text>
      </button>
      <text class="skip-link" @click="goHome">跳过,以后再设</text>
    </view>
  </view>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { loadUserProfile, saveUserProfile } from "@/utils/userProfileStorage";
import { uploadImage, post } from "@/utils/api/http";
import { isAlreadyPersistedAvatarUrl } from "@/utils/avatarUpload";
import { bindWechatPhone } from "@/utils/api/mpSession";

const nickname = ref("");
const avatarTemp = ref("");
const phoneNumber = ref("");
const saving = ref(false);

onLoad(() => {
  const profile = loadUserProfile();
  nickname.value = profile.nickname || "";
  avatarTemp.value = profile.avatarUrl || "";
  phoneNumber.value = profile.phone || "";
});

function onChooseAvatar(e: any) {
  const url = e?.detail?.avatarUrl;
  if (url) {
    avatarTemp.value = url;
  }
}

async function onGetPhoneNumber(e: any) {
  const code = e?.detail?.code;
  if (!code) {
    uni.showToast({ title: "未获取到手机号授权", icon: "none" });
    return;
  }
  try {
    const phone = await bindWechatPhone(code);
    phoneNumber.value = phone;
    uni.showToast({ title: "手机号已获取", icon: "success" });
  } catch (err: unknown) {
    const msg =
      err && typeof err === "object" && "msg" in err
        ? (err as { msg: string }).msg
        : "手机号绑定失败";
    uni.showToast({ title: msg as string, icon: "none" });
  }
}

function goHome() {
  uni.switchTab({ url: "/pages/home/home" });
}

async function onSubmit() {
  const nick = nickname.value.trim();
  // #ifdef MP-WEIXIN
  const phone = phoneNumber.value.trim();
  if (!phone) {
    uni.showToast({ title: "请先绑定手机号", icon: "none" });
    return;
  }
  // #endif

  saving.value = true;
  try {
    let avatarUrl = avatarTemp.value.trim();
    if (avatarUrl && !isAlreadyPersistedAvatarUrl(avatarUrl)) {
      try {
        avatarUrl = await uploadImage(avatarUrl);
      } catch {
        uni.showToast({ title: "头像上传失败,请重试", icon: "none" });
        saving.value = false;
        return;
      }
    }

    // #ifdef MP-WEIXIN
    if (phone) {
      saveUserProfile({
        nickname: nick,
        avatarUrl,
        bio: loadUserProfile().bio,
        gender: loadUserProfile().gender,
        phone,
      });
    }
    // #endif

    await post("/api/v1/me/profile", {
      nickname: nick || undefined,
      avatarUrl: avatarUrl || undefined,
    });

    // #ifndef MP-WEIXIN
    saveUserProfile({
      nickname: nick,
      avatarUrl,
      bio: loadUserProfile().bio,
      gender: loadUserProfile().gender,
      phone: loadUserProfile().phone,
    });
    // #endif

    uni.showToast({ title: "已保存", icon: "success" });
    setTimeout(() => goHome(), 500);
  } catch {
    uni.showToast({ title: "保存失败,请重试", icon: "none" });
  } finally {
    saving.value = false;
  }
}
</script>

<style scoped>
.page {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 100rpx 48rpx 0;
  box-sizing: border-box;
  background: #fff;
}

.header {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-bottom: 60rpx;
}

.title {
  font-size: 40rpx;
  font-weight: 700;
  color: #111827;
  margin-bottom: 16rpx;
}

.sub {
  font-size: 26rpx;
  color: #9ca3af;
}

.avatar-section {
  margin-bottom: 48rpx;
}

.avatar-btn {
  width: 160rpx;
  height: 160rpx;
  padding: 0;
  margin: 0;
  border: none;
  border-radius: 50%;
  background: transparent;
  overflow: visible;
  line-height: 1;
}

.avatar-btn::after {
  display: none;
}

.avatar-img {
  width: 160rpx;
  height: 160rpx;
  border-radius: 50%;
}

.avatar-placeholder {
  width: 160rpx;
  height: 160rpx;
  border-radius: 50%;
  border: 4rpx dashed #d1d5db;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: #f9fafb;
}

.avatar-placeholder-icon {
  font-size: 48rpx;
  color: #9ca3af;
  line-height: 1;
  margin-bottom: 4rpx;
}

.avatar-placeholder-text {
  font-size: 20rpx;
  color: #9ca3af;
}

.name-section,
.phone-section {
  width: 100%;
  margin-bottom: 40rpx;
}

.label {
  display: block;
  font-size: 28rpx;
  font-weight: 600;
  color: #374151;
  margin-bottom: 16rpx;
}

.name-input {
  display: block;
  width: 100%;
  height: 80rpx;
  padding: 0 4rpx;
  font-size: 32rpx;
  color: #111827;
  background: transparent;
}

.name-ph {
  color: #c4c4c4;
  font-size: 28rpx;
}

.phone-btn {
  display: flex;
  align-items: center;
  width: 100%;
  height: 80rpx;
  padding: 0 4rpx;
  margin: 0;
  border: none;
  background: transparent;
  text-align: left;
  font-size: 32rpx;
  line-height: 1;
}

.phone-btn::after {
  display: none;
}

.phone-val {
  color: #111827;
}

.phone-ph {
  color: #c4c4c4;
  font-size: 28rpx;
}

.line {
  height: 1rpx;
  margin-top: 8rpx;
  background: #e5e7eb;
}

.actions {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: 20rpx;
}

.btn-save {
  width: 100%;
  height: 88rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 24rpx;
  padding: 0;
  font-size: 32rpx;
  font-weight: 500;
  color: #fff;
  background: #2185ff;
  border: none;
  border-radius: 999rpx;
}

.btn-save::after {
  display: none;
}

.btn-save[disabled] {
  opacity: 0.6;
}

.skip-link {
  font-size: 26rpx;
  color: #9ca3af;
  padding: 16rpx;
}
</style>