|
|
@@ -2,24 +2,26 @@
|
|
|
import { useUserStore } from "@/store/user";
|
|
|
import { usePriceStore } from "@/store/price";
|
|
|
import { computed, ref, onMounted, watch } from "vue";
|
|
|
-import { upgradeGuest, purchaseMember, userQueryOrder } from "@/services/api";
|
|
|
+import {
|
|
|
+ upgradeGuest,
|
|
|
+ purchaseMember,
|
|
|
+ userQueryOrder,
|
|
|
+ updateProfile,
|
|
|
+ resetPassword,
|
|
|
+} from "@/services/api";
|
|
|
import { vipLevelToText, VipLevel } from "@/types/vip";
|
|
|
|
|
|
const userStore = useUserStore();
|
|
|
const priceStore = usePriceStore();
|
|
|
-const isLoggedIn = computed(() => !!userStore.token);
|
|
|
-const isGuest = computed(() => {
|
|
|
- return userStore.userInfo?.vipLevel === "guest";
|
|
|
-});
|
|
|
-const isVip = computed(() => {
|
|
|
- const level = userStore.userInfo?.vipLevel;
|
|
|
- return level && level !== VipLevel.GUEST && level !== VipLevel.FREE;
|
|
|
-});
|
|
|
+
|
|
|
+const isLoginUser = computed(() => userStore.isLoginUser);
|
|
|
+const isVipUser = computed(() => userStore.isVipUser);
|
|
|
+const isGuestUser = computed(() => userStore.isGuestUser);
|
|
|
|
|
|
// 是否显示VIP到期时间
|
|
|
const shouldShowExpireTime = computed(() => {
|
|
|
return (
|
|
|
- isVip.value &&
|
|
|
+ isVipUser.value &&
|
|
|
userStore.userInfo?.vipLevel !== VipLevel.LIFETIME &&
|
|
|
userStore.userInfo?.vipExpireTime
|
|
|
);
|
|
|
@@ -31,6 +33,8 @@ const showMembershipDialog = ref(false);
|
|
|
const showPaymentWaitingDialog = ref(false);
|
|
|
const showErrorDialog = ref(false);
|
|
|
const showSuccessDialog = ref(false);
|
|
|
+const showEditUserDialog = ref(false);
|
|
|
+const showResetPasswordDialog = ref(false);
|
|
|
const errorMessage = ref("");
|
|
|
const successMessage = ref("");
|
|
|
const currentOrderNo = ref("");
|
|
|
@@ -38,8 +42,20 @@ const upgradeForm = ref({
|
|
|
password: null,
|
|
|
email: null,
|
|
|
});
|
|
|
+const editUserForm = ref({
|
|
|
+ name: "",
|
|
|
+ email: "",
|
|
|
+});
|
|
|
+const resetPasswordForm = ref({
|
|
|
+ password: "",
|
|
|
+ confirmPassword: "",
|
|
|
+});
|
|
|
const isLoading = ref(false);
|
|
|
const isPaymentLoading = ref(false);
|
|
|
+const isEditUserLoading = ref(false);
|
|
|
+const isResetPasswordLoading = ref(false);
|
|
|
+const editUserError = ref("");
|
|
|
+const resetPasswordError = ref("");
|
|
|
|
|
|
// 使用动态价格配置
|
|
|
const membershipPlans = computed(() => priceStore.getMembershipPlans);
|
|
|
@@ -55,6 +71,120 @@ const showLoginDialog = () => {
|
|
|
emit("show-login");
|
|
|
};
|
|
|
|
|
|
+// 显示会员购买弹窗
|
|
|
+const showMembership = async () => {
|
|
|
+ // 确保价格配置已加载
|
|
|
+ if (!priceStore.isPriceConfigLoaded) {
|
|
|
+ try {
|
|
|
+ await priceStore.fetchPriceConfig();
|
|
|
+ } catch (error) {
|
|
|
+ console.error("价格配置加载失败:", error);
|
|
|
+ showError("价格配置加载失败,请重试");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ showMembershipDialog.value = true;
|
|
|
+};
|
|
|
+
|
|
|
+// 显示修改用户信息弹窗
|
|
|
+const showEditUser = () => {
|
|
|
+ // 清除错误信息
|
|
|
+ editUserError.value = "";
|
|
|
+ // 初始化表单数据
|
|
|
+ editUserForm.value = {
|
|
|
+ name: userStore.userInfo?.name || "",
|
|
|
+ email: userStore.userInfo?.email || "",
|
|
|
+ };
|
|
|
+ showEditUserDialog.value = true;
|
|
|
+};
|
|
|
+
|
|
|
+// 显示重置密码弹窗
|
|
|
+const showResetPassword = () => {
|
|
|
+ // 清除错误信息
|
|
|
+ resetPasswordError.value = "";
|
|
|
+ // 重置表单数据
|
|
|
+ resetPasswordForm.value = {
|
|
|
+ password: "",
|
|
|
+ confirmPassword: "",
|
|
|
+ };
|
|
|
+ showResetPasswordDialog.value = true;
|
|
|
+};
|
|
|
+
|
|
|
+// 处理重置密码
|
|
|
+const handleResetPassword = async () => {
|
|
|
+ // 清除之前的错误信息
|
|
|
+ resetPasswordError.value = "";
|
|
|
+
|
|
|
+ if (!resetPasswordForm.value.password.trim()) {
|
|
|
+ resetPasswordError.value = "请输入新密码";
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (resetPasswordForm.value.password.length < 6) {
|
|
|
+ resetPasswordError.value = "密码长度至少6位";
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (
|
|
|
+ resetPasswordForm.value.password !== resetPasswordForm.value.confirmPassword
|
|
|
+ ) {
|
|
|
+ resetPasswordError.value = "两次输入的密码不一致";
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ isResetPasswordLoading.value = true;
|
|
|
+ try {
|
|
|
+ const response = await resetPassword(resetPasswordForm.value.password);
|
|
|
+
|
|
|
+ if (response.message) {
|
|
|
+ showResetPasswordDialog.value = false;
|
|
|
+ showSuccess(response.message);
|
|
|
+ }
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error("重置密码失败", error);
|
|
|
+ resetPasswordError.value = error.message || "重置密码失败,请重试";
|
|
|
+ } finally {
|
|
|
+ isResetPasswordLoading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 处理修改用户信息
|
|
|
+const handleEditUser = async () => {
|
|
|
+ // 清除之前的错误信息
|
|
|
+ editUserError.value = "";
|
|
|
+
|
|
|
+ if (!editUserForm.value.name.trim()) {
|
|
|
+ editUserError.value = "请输入用户名";
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ isEditUserLoading.value = true;
|
|
|
+ try {
|
|
|
+ const response = await updateProfile(
|
|
|
+ editUserForm.value.name,
|
|
|
+ editUserForm.value.email || undefined
|
|
|
+ );
|
|
|
+
|
|
|
+ if (response.message) {
|
|
|
+ // 更新本地用户信息
|
|
|
+ const updatedUserInfo = {
|
|
|
+ ...userStore.userInfo,
|
|
|
+ name: editUserForm.value.name,
|
|
|
+ email: editUserForm.value.email,
|
|
|
+ };
|
|
|
+ userStore.setUserInfo(updatedUserInfo);
|
|
|
+
|
|
|
+ showEditUserDialog.value = false;
|
|
|
+ showSuccess(response.message);
|
|
|
+ }
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error("修改用户信息失败", error);
|
|
|
+ editUserError.value = error.message || "修改失败,请重试";
|
|
|
+ } finally {
|
|
|
+ isEditUserLoading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
// 格式化VIP到期时间
|
|
|
const formatVipExpireTime = (expireTime: string | null | undefined): string => {
|
|
|
if (!expireTime) return "";
|
|
|
@@ -83,21 +213,6 @@ const showSuccess = (message: string) => {
|
|
|
showSuccessDialog.value = true;
|
|
|
};
|
|
|
|
|
|
-// Safari兼容性处理:打开支付页面
|
|
|
-const openPaymentPage = (url: string) => {
|
|
|
- // 检测是否为Safari浏览器
|
|
|
- const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
|
-
|
|
|
- if (isSafari) {
|
|
|
- // Safari浏览器
|
|
|
- setTimeout(() => window.open(url, "_blank"));
|
|
|
- // window.location.href = url;
|
|
|
- } else {
|
|
|
- // 其他浏览器:正常使用window.open
|
|
|
- window.open(url, "_blank");
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
const handleMembershipPurchase = async () => {
|
|
|
if (!selectedPlan.value) {
|
|
|
alert("请选择会员套餐");
|
|
|
@@ -236,7 +351,7 @@ const handleQueryOrder = async () => {
|
|
|
|
|
|
// 加载价格配置的函数
|
|
|
const loadPriceConfig = async () => {
|
|
|
- if (isLoggedIn.value && !priceStore.isPriceConfigLoaded) {
|
|
|
+ if (isLoginUser.value && !priceStore.isPriceConfigLoaded) {
|
|
|
try {
|
|
|
await priceStore.fetchPriceConfig();
|
|
|
} catch (error) {
|
|
|
@@ -247,7 +362,7 @@ const loadPriceConfig = async () => {
|
|
|
|
|
|
// 监听登录状态变化
|
|
|
watch(
|
|
|
- isLoggedIn,
|
|
|
+ () => isLoginUser,
|
|
|
(newValue) => {
|
|
|
if (newValue) {
|
|
|
loadPriceConfig();
|
|
|
@@ -269,7 +384,7 @@ onMounted(async () => {
|
|
|
<!-- 个人信息区域 -->
|
|
|
<div class="flex items-center gap-3 mb-4">
|
|
|
<div
|
|
|
- v-if="isLoggedIn"
|
|
|
+ v-if="isLoginUser"
|
|
|
class="h-12 w-12 rounded-full bg-gradient-to-br from-emerald-400 to-emerald-600 grid place-items-center text-slate-900 font-semibold"
|
|
|
>
|
|
|
{{ userStore.userInfo?.name?.[0] || "用" }}
|
|
|
@@ -292,14 +407,14 @@ onMounted(async () => {
|
|
|
</div>
|
|
|
<div class="flex-1 min-w-0">
|
|
|
<p class="text-sm text-white/60">
|
|
|
- {{ isLoggedIn ? "欢迎回来" : "未登录" }}
|
|
|
+ {{ isLoginUser ? "欢迎回来" : "未登录" }}
|
|
|
</p>
|
|
|
<h2 class="text-base font-semibold text-white/90 truncate">
|
|
|
{{
|
|
|
- isLoggedIn ? userStore.userInfo?.name || "用户" : "点击登录账号"
|
|
|
+ isLoginUser ? userStore.userInfo?.name || "用户" : "点击登录账号"
|
|
|
}}
|
|
|
<span
|
|
|
- v-if="isVip"
|
|
|
+ v-if="isVipUser"
|
|
|
class="ml-2 px-2 py-0.5 rounded bg-yellow-400/90 text-xs text-yellow-900 font-semibold align-middle"
|
|
|
style="vertical-align: middle"
|
|
|
>
|
|
|
@@ -316,21 +431,19 @@ onMounted(async () => {
|
|
|
</div>
|
|
|
|
|
|
<!-- 按钮区域 -->
|
|
|
- <div v-if="isLoggedIn" class="flex gap-2">
|
|
|
+ <div v-if="isLoginUser" class="flex gap-2">
|
|
|
<button
|
|
|
- v-if="isGuest"
|
|
|
+ v-if="isGuestUser"
|
|
|
@click="showUpgradeDialog = true"
|
|
|
class="px-3 py-1.5 rounded-lg bg-blue-500 text-white text-sm font-medium hover:bg-blue-600 transition"
|
|
|
>
|
|
|
成为正式用户
|
|
|
</button>
|
|
|
<button
|
|
|
- @click="showMembershipDialog = true"
|
|
|
+ @click="showMembership"
|
|
|
class="px-3 py-1.5 rounded-lg bg-brand text-slate-900 text-sm font-medium"
|
|
|
- :disabled="isGuest"
|
|
|
- :class="{ 'opacity-50 cursor-not-allowed': isGuest }"
|
|
|
>
|
|
|
- {{ isVip ? "续订会员" : "开通会员" }}
|
|
|
+ {{ isVipUser ? "续订会员" : "开通会员" }}
|
|
|
</button>
|
|
|
</div>
|
|
|
<button
|
|
|
@@ -343,9 +456,48 @@ onMounted(async () => {
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
- v-if="isLoggedIn"
|
|
|
+ v-if="isLoginUser"
|
|
|
class="rounded-2xl overflow-hidden border border-white/10 divide-y divide-white/10"
|
|
|
>
|
|
|
+ <button
|
|
|
+ v-if="!isGuestUser"
|
|
|
+ @click="showEditUser"
|
|
|
+ class="row text-white/80"
|
|
|
+ >
|
|
|
+ <span>修改用户信息</span>
|
|
|
+ <svg
|
|
|
+ viewBox="0 0 24 24"
|
|
|
+ width="18"
|
|
|
+ height="18"
|
|
|
+ fill="none"
|
|
|
+ stroke="currentColor"
|
|
|
+ stroke-width="2"
|
|
|
+ >
|
|
|
+ <path
|
|
|
+ d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
|
|
|
+ />
|
|
|
+ <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ v-if="!isGuestUser"
|
|
|
+ @click="showResetPassword"
|
|
|
+ class="row text-white/80"
|
|
|
+ >
|
|
|
+ <span>重置密码</span>
|
|
|
+ <svg
|
|
|
+ viewBox="0 0 24 24"
|
|
|
+ width="18"
|
|
|
+ height="18"
|
|
|
+ fill="none"
|
|
|
+ stroke="currentColor"
|
|
|
+ stroke-width="2"
|
|
|
+ >
|
|
|
+ <path
|
|
|
+ d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"
|
|
|
+ />
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
<button @click="handleLogout" class="row text-red-300/90">
|
|
|
<span>退出登录</span>
|
|
|
<svg
|
|
|
@@ -571,10 +723,150 @@ onMounted(async () => {
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ <!-- 修改用户信息弹窗 -->
|
|
|
+ <div
|
|
|
+ v-if="showEditUserDialog"
|
|
|
+ class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
|
|
+ @click.self="showEditUserDialog = false"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ class="bg-surface border border-white/10 rounded-2xl p-6 w-full max-w-md mx-4"
|
|
|
+ >
|
|
|
+ <h3 class="text-xl font-semibold text-white/90 mb-4">修改用户信息</h3>
|
|
|
+
|
|
|
+ <form @submit.prevent="handleEditUser" class="space-y-4">
|
|
|
+ <div>
|
|
|
+ <label class="block text-sm font-medium text-white/70 mb-1">
|
|
|
+ 用户名 <span class="text-red-400">*</span>
|
|
|
+ </label>
|
|
|
+ <input
|
|
|
+ v-model="editUserForm.name"
|
|
|
+ type="text"
|
|
|
+ required
|
|
|
+ class="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white placeholder-white/40 focus:ring-2 focus:ring-brand focus:border-transparent"
|
|
|
+ placeholder="请输入用户名"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div>
|
|
|
+ <label class="block text-sm font-medium text-white/70 mb-1">
|
|
|
+ 邮箱
|
|
|
+ </label>
|
|
|
+ <input
|
|
|
+ v-model="editUserForm.email"
|
|
|
+ type="email"
|
|
|
+ class="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white placeholder-white/40 focus:ring-2 focus:ring-brand focus:border-transparent"
|
|
|
+ placeholder="请输入邮箱"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 错误信息显示 -->
|
|
|
+ <div
|
|
|
+ v-if="editUserError"
|
|
|
+ class="text-red-400 text-sm text-center py-2"
|
|
|
+ >
|
|
|
+ {{ editUserError }}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="flex gap-3 pt-4">
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ @click="showEditUserDialog = false"
|
|
|
+ class="flex-1 px-4 py-2 border border-white/20 text-white/70 rounded-lg hover:bg-white/5 transition"
|
|
|
+ >
|
|
|
+ 取消
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ type="submit"
|
|
|
+ :disabled="isEditUserLoading"
|
|
|
+ class="flex-1 px-4 py-2 bg-brand text-slate-900 rounded-lg hover:bg-brand/90 transition disabled:opacity-50"
|
|
|
+ >
|
|
|
+ {{ isEditUserLoading ? "处理中..." : "确认修改" }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 重置密码弹窗 -->
|
|
|
+ <div
|
|
|
+ v-if="showResetPasswordDialog"
|
|
|
+ class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
|
|
+ @click.self="showResetPasswordDialog = false"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ class="bg-surface border border-white/10 rounded-2xl p-6 w-full max-w-md mx-4"
|
|
|
+ >
|
|
|
+ <h3 class="text-xl font-semibold text-white/90 mb-4">重置密码</h3>
|
|
|
+
|
|
|
+ <div
|
|
|
+ class="bg-yellow-500/10 border border-yellow-500/20 rounded-lg p-4 mb-6"
|
|
|
+ >
|
|
|
+ <p class="text-sm text-yellow-200">
|
|
|
+ 重置密码后,您需要使用新密码重新登录
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <form @submit.prevent="handleResetPassword" class="space-y-4">
|
|
|
+ <div>
|
|
|
+ <label class="block text-sm font-medium text-white/70 mb-1">
|
|
|
+ 新密码 <span class="text-red-400">*</span>
|
|
|
+ </label>
|
|
|
+ <input
|
|
|
+ v-model="resetPasswordForm.password"
|
|
|
+ type="password"
|
|
|
+ required
|
|
|
+ class="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white placeholder-white/40 focus:ring-2 focus:ring-brand focus:border-transparent"
|
|
|
+ placeholder="请输入新密码"
|
|
|
+ />
|
|
|
+ <p class="mt-1 text-xs text-white/50">至少6位字符</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div>
|
|
|
+ <label class="block text-sm font-medium text-white/70 mb-1">
|
|
|
+ 确认密码 <span class="text-red-400">*</span>
|
|
|
+ </label>
|
|
|
+ <input
|
|
|
+ v-model="resetPasswordForm.confirmPassword"
|
|
|
+ type="password"
|
|
|
+ required
|
|
|
+ class="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white placeholder-white/40 focus:ring-2 focus:ring-brand focus:border-transparent"
|
|
|
+ placeholder="请再次输入新密码"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 错误信息显示 -->
|
|
|
+ <div
|
|
|
+ v-if="resetPasswordError"
|
|
|
+ class="text-red-400 text-sm text-center py-2"
|
|
|
+ >
|
|
|
+ {{ resetPasswordError }}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="flex gap-3 pt-4">
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ @click="showResetPasswordDialog = false"
|
|
|
+ class="flex-1 px-4 py-2 border border-white/20 text-white/70 rounded-lg hover:bg-white/5 transition"
|
|
|
+ >
|
|
|
+ 取消
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ type="submit"
|
|
|
+ :disabled="isResetPasswordLoading"
|
|
|
+ class="flex-1 px-4 py-2 bg-brand text-slate-900 rounded-lg hover:bg-brand/90 transition disabled:opacity-50"
|
|
|
+ >
|
|
|
+ {{ isResetPasswordLoading ? "处理中..." : "确认重置" }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<!-- 错误提示弹窗 -->
|
|
|
<div
|
|
|
v-if="showErrorDialog"
|
|
|
- class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
|
|
+ class="fixed inset-0 bg-black/50 flex items-center justify-center z-60"
|
|
|
@click.self="showErrorDialog = false"
|
|
|
>
|
|
|
<div
|
|
|
@@ -620,7 +912,7 @@ onMounted(async () => {
|
|
|
<!-- 成功提示弹窗 -->
|
|
|
<div
|
|
|
v-if="showSuccessDialog"
|
|
|
- class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
|
|
+ class="fixed inset-0 bg-black/50 flex items-center justify-center z-60"
|
|
|
@click.self="showSuccessDialog = false"
|
|
|
>
|
|
|
<div
|