| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687 |
- <script setup lang="ts">
- 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 { 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;
- });
- // 是否显示VIP到期时间
- const shouldShowExpireTime = computed(() => {
- return (
- isVip.value &&
- userStore.userInfo?.vipLevel !== VipLevel.LIFETIME &&
- userStore.userInfo?.vipExpireTime
- );
- });
- const emit = defineEmits(["show-login"]);
- const showUpgradeDialog = ref(false);
- const showMembershipDialog = ref(false);
- const showPaymentWaitingDialog = ref(false);
- const showErrorDialog = ref(false);
- const showSuccessDialog = ref(false);
- const errorMessage = ref("");
- const successMessage = ref("");
- const currentOrderNo = ref("");
- const upgradeForm = ref({
- name: null,
- password: null,
- email: undefined,
- phone: undefined,
- });
- const isLoading = ref(false);
- const isPaymentLoading = ref(false);
- // 使用动态价格配置
- const membershipPlans = computed(() => priceStore.getMembershipPlans);
- const selectedPlan = ref("");
- const selectedPayment = ref("alipay");
- const handleLogout = () => {
- userStore.logout();
- };
- const showLoginDialog = () => {
- emit("show-login");
- };
- // 格式化VIP到期时间
- const formatVipExpireTime = (expireTime: string | null | undefined): string => {
- if (!expireTime) return "";
- try {
- const date = new Date(expireTime);
- const month = String(date.getMonth() + 1).padStart(2, "0");
- const day = String(date.getDate()).padStart(2, "0");
- const hours = String(date.getHours()).padStart(2, "0");
- const minutes = String(date.getMinutes()).padStart(2, "0");
- return `${month}-${day} ${hours}:${minutes}`;
- } catch (error) {
- console.error("格式化到期时间失败:", error);
- return "";
- }
- };
- const showError = (message: string) => {
- errorMessage.value = message;
- showErrorDialog.value = true;
- };
- const showSuccess = (message: string) => {
- successMessage.value = message;
- showSuccessDialog.value = true;
- };
- // Safari兼容性处理:打开支付页面
- const openPaymentPage = (url: string) => {
- // 检测是否为Safari浏览器
- const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
- if (isSafari) {
- // Safari浏览器:使用location.href跳转
- window.location.href = url;
- } else {
- // 其他浏览器:正常使用window.open
- window.open(url, "_blank");
- }
- };
- const handleMembershipPurchase = async () => {
- if (!selectedPlan.value) {
- alert("请选择会员套餐");
- return;
- }
- isPaymentLoading.value = true;
- try {
- const response = await purchaseMember(
- userStore.userInfo.id,
- selectedPlan.value
- );
- if (response.code === 1) {
- // 保存订单号
- currentOrderNo.value = response.out_trade_no;
- // 关闭购买弹窗
- showMembershipDialog.value = false;
- // Safari兼容性处理:使用多种方式打开支付页面
- openPaymentPage(response.code_url);
- // 显示支付等待弹窗
- showPaymentWaitingDialog.value = true;
- // 重置选择
- selectedPlan.value = "";
- } else {
- showError(`支付失败: ${response.msg || "未知错误"}`);
- }
- } catch (error) {
- console.error("购买会员失败", error);
- showError("购买失败,请重试");
- } finally {
- isPaymentLoading.value = false;
- }
- };
- const handleUpgrade = async () => {
- if (!upgradeForm.value.name || !upgradeForm.value.password) {
- alert("请填写用户名和密码");
- return;
- }
- isLoading.value = true;
- try {
- const response = await upgradeGuest(
- userStore.userInfo.id,
- upgradeForm.value.name,
- upgradeForm.value.password,
- upgradeForm.value.email || undefined,
- upgradeForm.value.phone || undefined
- );
- // 更新用户信息
- userStore.setUserInfo(response.user);
- showUpgradeDialog.value = false;
- // 重置表单
- upgradeForm.value = {
- name: null,
- password: null,
- email: undefined,
- phone: undefined,
- };
- alert("升级成功!");
- // 刷新页面以更新数据
- window.location.reload();
- } catch (error) {
- console.error("升级失败", error);
- alert("升级失败,请重试");
- } finally {
- isLoading.value = false;
- }
- };
- // 检查支付成功后的用户信息同步
- const checkPaymentSuccess = async () => {
- const urlParams = new URLSearchParams(window.location.search);
- const paymentSuccess = urlParams.get("pay");
- console.log("paymentSuccess", paymentSuccess);
- if (paymentSuccess === "true") {
- try {
- const response = await userQueryOrder();
- if (response.status === 1) {
- // 订单状态确认正确,同步用户信息
- await userStore.sync();
- showSuccess(`会员购买成功`);
- currentOrderNo.value = "";
- } else {
- // 订单状态异常,仍同步用户信息
- await userStore.sync();
- showError(`${response.msg}`);
- }
- // 清除URL参数
- const newUrl = window.location.pathname;
- window.history.replaceState({}, document.title, newUrl);
- } catch (error) {
- console.error("同步用户信息失败:", error);
- showError("会员购买失败");
- }
- }
- };
- // 查询订单状态
- const handleQueryOrder = async () => {
- if (!currentOrderNo.value) {
- showError("没有找到订单信息");
- return;
- }
- try {
- const response = await userQueryOrder(currentOrderNo.value);
- if (response.status === 1) {
- // 支付成功
- await userStore.sync();
- showPaymentWaitingDialog.value = false;
- showSuccess("会员购买成功");
- currentOrderNo.value = "";
- } else {
- showError(response.msg || "会员购买失败");
- }
- } catch (error) {
- console.error("查询订单状态失败:", error);
- showError("会员购买失败,请重试");
- }
- };
- // 加载价格配置的函数
- const loadPriceConfig = async () => {
- if (isLoggedIn.value && !priceStore.isPriceConfigLoaded) {
- try {
- await priceStore.fetchPriceConfig();
- } catch (error) {
- console.error("价格配置加载失败:", error);
- }
- }
- };
- // 监听登录状态变化
- watch(
- isLoggedIn,
- (newValue) => {
- if (newValue) {
- loadPriceConfig();
- }
- },
- { immediate: true }
- );
- onMounted(async () => {
- checkPaymentSuccess();
- // 初始加载价格配置
- await loadPriceConfig();
- });
- </script>
- <template>
- <section class="space-y-5">
- <div class="rounded-2xl bg-white/5 border border-white/10 p-4">
- <!-- 个人信息区域 -->
- <div class="flex items-center gap-3 mb-4">
- <div
- v-if="isLoggedIn"
- 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] || "用" }}
- </div>
- <div
- v-else
- class="h-12 w-12 rounded-full bg-white/10 grid place-items-center text-white/60"
- >
- <svg
- viewBox="0 0 24 24"
- width="24"
- height="24"
- fill="none"
- stroke="currentColor"
- stroke-width="2"
- >
- <path d="M20 21a8 8 0 1 0-16 0" />
- <circle cx="12" cy="7" r="4" />
- </svg>
- </div>
- <div class="flex-1 min-w-0">
- <p class="text-sm text-white/60">
- {{ isLoggedIn ? "欢迎回来" : "未登录" }}
- </p>
- <h2 class="text-base font-semibold text-white/90 truncate">
- {{
- isLoggedIn ? userStore.userInfo?.name || "用户" : "点击登录账号"
- }}
- <span
- v-if="isVip"
- 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"
- >
- {{ vipLevelToText(userStore.userInfo?.vipLevel) }}
- </span>
- </h2>
- <!-- VIP到期时间显示 -->
- <p v-if="shouldShowExpireTime" class="text-xs text-white/60 mt-1">
- 到期时间:{{
- formatVipExpireTime(userStore.userInfo?.vipExpireTime)
- }}
- </p>
- </div>
- </div>
- <!-- 按钮区域 -->
- <div v-if="isLoggedIn" class="flex gap-2">
- <button
- v-if="isGuest"
- @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"
- 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 ? "续订会员" : "开通会员" }}
- </button>
- </div>
- <button
- v-else
- @click="showLoginDialog"
- class="px-3 py-1.5 rounded-lg bg-brand text-slate-900 text-sm font-medium"
- >
- 登录
- </button>
- </div>
- <div
- v-if="isLoggedIn"
- class="rounded-2xl overflow-hidden border border-white/10 divide-y divide-white/10"
- >
- <button @click="handleLogout" class="row text-red-300/90">
- <span>退出登录</span>
- <svg
- viewBox="0 0 24 24"
- width="18"
- height="18"
- fill="none"
- stroke="currentColor"
- stroke-width="2"
- >
- <path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
- <path d="m10 17 5-5-5-5" />
- <path d="M15 12H3" />
- </svg>
- </button>
- </div>
- <!-- 升级弹窗 -->
- <div
- v-if="showUpgradeDialog"
- class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
- @click.self="showUpgradeDialog = 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="handleUpgrade" 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="upgradeForm.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">
- 密码 <span class="text-red-400">*</span>
- </label>
- <input
- v-model="upgradeForm.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="请输入密码"
- />
- </div>
- <div>
- <label class="block text-sm font-medium text-white/70 mb-1">
- 邮箱
- </label>
- <input
- v-model="upgradeForm.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>
- <label class="block text-sm font-medium text-white/70 mb-1">
- 手机号
- </label>
- <input
- v-model="upgradeForm.phone"
- type="tel"
- 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 class="flex gap-3 pt-4">
- <button
- type="button"
- @click="showUpgradeDialog = 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="isLoading"
- class="flex-1 px-4 py-2 bg-brand text-slate-900 rounded-lg hover:bg-brand/90 transition disabled:opacity-50"
- >
- {{ isLoading ? "处理中..." : "确认升级" }}
- </button>
- </div>
- </form>
- </div>
- </div>
- <!-- 会员购买弹窗 -->
- <div
- v-if="showMembershipDialog"
- class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
- @click.self="showMembershipDialog = false"
- >
- <div
- class="bg-surface border border-white/10 rounded-2xl p-6 w-full max-w-lg mx-4 max-h-[90vh] overflow-y-auto"
- >
- <h3 class="text-xl font-semibold text-white/90 mb-6 text-center">
- 开通会员
- </h3>
- <!-- 会员套餐选择 -->
- <div class="space-y-2 mb-4">
- <h4 class="text-sm font-medium text-white/70 mb-2">选择套餐</h4>
- <div class="grid grid-cols-1 gap-2">
- <label
- v-for="plan in membershipPlans"
- :key="plan.key"
- class="relative cursor-pointer"
- >
- <input
- v-model="selectedPlan"
- :value="plan.key"
- type="radio"
- name="membership-plan"
- class="sr-only"
- />
- <div
- class="border-2 rounded-lg p-2 transition-all"
- :class="
- selectedPlan === plan.key
- ? 'border-brand bg-brand/10'
- : 'border-white/20 hover:border-white/30'
- "
- >
- <div class="flex justify-between items-center">
- <div>
- <div class="font-medium text-white/90 text-sm">
- {{ plan.label }}
- </div>
- <div class="text-xs text-white/60">{{ plan.duration }}</div>
- </div>
- <div class="text-right">
- <div class="font-semibold text-white/90 text-sm">
- {{ plan.price }}
- </div>
- </div>
- </div>
- </div>
- </label>
- </div>
- </div>
- <!-- 支付方式选择 -->
- <div class="space-y-2 mb-4">
- <h4 class="text-sm font-medium text-white/70 mb-2">支付方式</h4>
- <div class="space-y-2">
- <label
- class="flex items-center p-2 border border-white/20 rounded-lg cursor-pointer hover:bg-white/5"
- >
- <input
- v-model="selectedPayment"
- value="alipay"
- type="radio"
- name="payment-method"
- class="sr-only"
- />
- <div class="flex items-center">
- <div
- class="w-8 h-8 bg-blue-500 rounded flex items-center justify-center mr-3"
- >
- <span class="text-white text-sm font-bold">支</span>
- </div>
- <span class="text-white/90">支付宝</span>
- </div>
- </label>
- </div>
- </div>
- <!-- 操作按钮 -->
- <div class="flex gap-3 pt-3 border-t border-white/10">
- <button
- type="button"
- @click="showMembershipDialog = false"
- class="flex-1 px-4 py-2 border border-white/20 text-white/70 rounded-lg hover:bg-white/5 transition"
- >
- 取消
- </button>
- <button
- @click="handleMembershipPurchase"
- :disabled="!selectedPlan || isPaymentLoading"
- class="flex-1 px-4 py-2 bg-brand text-slate-900 rounded-lg hover:bg-brand/90 transition disabled:opacity-50 disabled:cursor-not-allowed"
- >
- {{ isPaymentLoading ? "处理中..." : "立即购买" }}
- </button>
- </div>
- </div>
- </div>
- <!-- 支付等待弹窗 -->
- <div
- v-if="showPaymentWaitingDialog"
- class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
- >
- <div
- class="bg-surface border border-white/10 rounded-2xl p-8 w-full max-w-sm mx-4 text-center"
- >
- <!-- 加载动画 -->
- <div class="mb-6">
- <div
- class="w-16 h-16 mx-auto border-4 border-white/20 border-t-brand rounded-full animate-spin"
- ></div>
- </div>
- <!-- 等待文字 -->
- <h3 class="text-lg font-semibold text-white/90 mb-2">
- 正在拉起支付页面,等待支付...
- </h3>
- <p class="text-sm text-white/60 mb-6">
- 请在支付页面完成支付<br />
- 支付完成后点击已完成支付
- </p>
- <!-- 操作按钮 -->
- <div class="flex gap-3">
- <button
- @click="showPaymentWaitingDialog = false"
- class="flex-1 px-4 py-2 border border-white/20 text-white/70 rounded-lg hover:bg-white/5 transition"
- >
- 取消支付
- </button>
- <button
- @click="handleQueryOrder"
- class="flex-1 px-4 py-2 bg-brand text-slate-900 rounded-lg hover:bg-brand/90 transition"
- >
- 已完成支付
- </button>
- </div>
- </div>
- </div>
- <!-- 错误提示弹窗 -->
- <div
- v-if="showErrorDialog"
- class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
- @click.self="showErrorDialog = false"
- >
- <div
- class="bg-surface border border-white/10 rounded-2xl p-6 w-full max-w-sm mx-4 text-center"
- >
- <!-- 错误图标 -->
- <div class="mb-4">
- <div
- class="w-12 h-12 mx-auto bg-red-500/20 rounded-full flex items-center justify-center"
- >
- <svg
- class="w-6 h-6 text-red-400"
- fill="none"
- stroke="currentColor"
- viewBox="0 0 24 24"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- stroke-width="2"
- d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
- />
- </svg>
- </div>
- </div>
- <!-- 错误信息 -->
- <h3 class="text-lg font-semibold text-white/90 mb-2">操作失败</h3>
- <p class="text-sm text-white/70 mb-6">
- {{ errorMessage }}
- </p>
- <!-- 确认按钮 -->
- <button
- @click="showErrorDialog = false"
- class="w-full px-4 py-2 bg-brand text-slate-900 rounded-lg hover:bg-brand/90 transition"
- >
- 确定
- </button>
- </div>
- </div>
- <!-- 成功提示弹窗 -->
- <div
- v-if="showSuccessDialog"
- class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
- @click.self="showSuccessDialog = false"
- >
- <div
- class="bg-surface border border-white/10 rounded-2xl p-6 w-full max-w-sm mx-4 text-center"
- >
- <!-- 成功图标 -->
- <div class="mb-4">
- <div
- class="w-12 h-12 mx-auto bg-green-500/20 rounded-full flex items-center justify-center"
- >
- <svg
- class="w-6 h-6 text-green-400"
- fill="none"
- stroke="currentColor"
- viewBox="0 0 24 24"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- stroke-width="2"
- d="M5 13l4 4L19 7"
- />
- </svg>
- </div>
- </div>
- <!-- 成功信息 -->
- <h3 class="text-lg font-semibold text-white/90 mb-2">操作成功</h3>
- <p class="text-sm text-white/70 mb-6">
- {{ successMessage }}
- </p>
- <!-- 确认按钮 -->
- <button
- @click="showSuccessDialog = false"
- class="w-full px-4 py-2 bg-brand text-slate-900 rounded-lg hover:bg-brand/90 transition"
- >
- 确定
- </button>
- </div>
- </div>
- </section>
- </template>
- <style scoped>
- .row {
- @apply w-full flex items-center justify-between px-4 py-3 bg-white/5 text-white/80 hover:bg-white/10;
- }
- </style>
|