Account.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. <script setup lang="ts">
  2. import { useUserStore } from "@/store/user";
  3. import { usePriceStore } from "@/store/price";
  4. import { computed, ref, onMounted, watch } from "vue";
  5. import { upgradeGuest, purchaseMember, userQueryOrder } from "@/services/api";
  6. import { vipLevelToText, VipLevel } from "@/types/vip";
  7. const userStore = useUserStore();
  8. const priceStore = usePriceStore();
  9. const isLoggedIn = computed(() => !!userStore.token);
  10. const isGuest = computed(() => {
  11. return userStore.userInfo?.vipLevel === "guest";
  12. });
  13. const isVip = computed(() => {
  14. const level = userStore.userInfo?.vipLevel;
  15. return level && level !== VipLevel.GUEST && level !== VipLevel.FREE;
  16. });
  17. // 是否显示VIP到期时间
  18. const shouldShowExpireTime = computed(() => {
  19. return (
  20. isVip.value &&
  21. userStore.userInfo?.vipLevel !== VipLevel.LIFETIME &&
  22. userStore.userInfo?.vipExpireTime
  23. );
  24. });
  25. const emit = defineEmits(["show-login"]);
  26. const showUpgradeDialog = ref(false);
  27. const showMembershipDialog = ref(false);
  28. const showPaymentWaitingDialog = ref(false);
  29. const showErrorDialog = ref(false);
  30. const showSuccessDialog = ref(false);
  31. const errorMessage = ref("");
  32. const successMessage = ref("");
  33. const currentOrderNo = ref("");
  34. const upgradeForm = ref({
  35. name: null,
  36. password: null,
  37. email: undefined,
  38. phone: undefined,
  39. });
  40. const isLoading = ref(false);
  41. const isPaymentLoading = ref(false);
  42. // 使用动态价格配置
  43. const membershipPlans = computed(() => priceStore.getMembershipPlans);
  44. const selectedPlan = ref("");
  45. const selectedPayment = ref("alipay");
  46. const handleLogout = () => {
  47. userStore.logout();
  48. };
  49. const showLoginDialog = () => {
  50. emit("show-login");
  51. };
  52. // 格式化VIP到期时间
  53. const formatVipExpireTime = (expireTime: string | null | undefined): string => {
  54. if (!expireTime) return "";
  55. try {
  56. const date = new Date(expireTime);
  57. const month = String(date.getMonth() + 1).padStart(2, "0");
  58. const day = String(date.getDate()).padStart(2, "0");
  59. const hours = String(date.getHours()).padStart(2, "0");
  60. const minutes = String(date.getMinutes()).padStart(2, "0");
  61. return `${month}-${day} ${hours}:${minutes}`;
  62. } catch (error) {
  63. console.error("格式化到期时间失败:", error);
  64. return "";
  65. }
  66. };
  67. const showError = (message: string) => {
  68. errorMessage.value = message;
  69. showErrorDialog.value = true;
  70. };
  71. const showSuccess = (message: string) => {
  72. successMessage.value = message;
  73. showSuccessDialog.value = true;
  74. };
  75. // Safari兼容性处理:打开支付页面
  76. const openPaymentPage = (url: string) => {
  77. // 检测是否为Safari浏览器
  78. const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
  79. if (isSafari) {
  80. // Safari浏览器:使用location.href跳转
  81. window.location.href = url;
  82. } else {
  83. // 其他浏览器:正常使用window.open
  84. window.open(url, "_blank");
  85. }
  86. };
  87. const handleMembershipPurchase = async () => {
  88. if (!selectedPlan.value) {
  89. alert("请选择会员套餐");
  90. return;
  91. }
  92. isPaymentLoading.value = true;
  93. try {
  94. const response = await purchaseMember(
  95. userStore.userInfo.id,
  96. selectedPlan.value
  97. );
  98. if (response.code === 1) {
  99. // 保存订单号
  100. currentOrderNo.value = response.out_trade_no;
  101. // 关闭购买弹窗
  102. showMembershipDialog.value = false;
  103. // Safari兼容性处理:使用多种方式打开支付页面
  104. openPaymentPage(response.code_url);
  105. // 显示支付等待弹窗
  106. showPaymentWaitingDialog.value = true;
  107. // 重置选择
  108. selectedPlan.value = "";
  109. } else {
  110. showError(`支付失败: ${response.msg || "未知错误"}`);
  111. }
  112. } catch (error) {
  113. console.error("购买会员失败", error);
  114. showError("购买失败,请重试");
  115. } finally {
  116. isPaymentLoading.value = false;
  117. }
  118. };
  119. const handleUpgrade = async () => {
  120. if (!upgradeForm.value.name || !upgradeForm.value.password) {
  121. alert("请填写用户名和密码");
  122. return;
  123. }
  124. isLoading.value = true;
  125. try {
  126. const response = await upgradeGuest(
  127. userStore.userInfo.id,
  128. upgradeForm.value.name,
  129. upgradeForm.value.password,
  130. upgradeForm.value.email || undefined,
  131. upgradeForm.value.phone || undefined
  132. );
  133. // 更新用户信息
  134. userStore.setUserInfo(response.user);
  135. showUpgradeDialog.value = false;
  136. // 重置表单
  137. upgradeForm.value = {
  138. name: null,
  139. password: null,
  140. email: undefined,
  141. phone: undefined,
  142. };
  143. alert("升级成功!");
  144. // 刷新页面以更新数据
  145. window.location.reload();
  146. } catch (error) {
  147. console.error("升级失败", error);
  148. alert("升级失败,请重试");
  149. } finally {
  150. isLoading.value = false;
  151. }
  152. };
  153. // 检查支付成功后的用户信息同步
  154. const checkPaymentSuccess = async () => {
  155. const urlParams = new URLSearchParams(window.location.search);
  156. const paymentSuccess = urlParams.get("pay");
  157. console.log("paymentSuccess", paymentSuccess);
  158. if (paymentSuccess === "true") {
  159. try {
  160. const response = await userQueryOrder();
  161. if (response.status === 1) {
  162. // 订单状态确认正确,同步用户信息
  163. await userStore.sync();
  164. showSuccess(`会员购买成功`);
  165. currentOrderNo.value = "";
  166. } else {
  167. // 订单状态异常,仍同步用户信息
  168. await userStore.sync();
  169. showError(`${response.msg}`);
  170. }
  171. // 清除URL参数
  172. const newUrl = window.location.pathname;
  173. window.history.replaceState({}, document.title, newUrl);
  174. } catch (error) {
  175. console.error("同步用户信息失败:", error);
  176. showError("会员购买失败");
  177. }
  178. }
  179. };
  180. // 查询订单状态
  181. const handleQueryOrder = async () => {
  182. if (!currentOrderNo.value) {
  183. showError("没有找到订单信息");
  184. return;
  185. }
  186. try {
  187. const response = await userQueryOrder(currentOrderNo.value);
  188. if (response.status === 1) {
  189. // 支付成功
  190. await userStore.sync();
  191. showPaymentWaitingDialog.value = false;
  192. showSuccess("会员购买成功");
  193. currentOrderNo.value = "";
  194. } else {
  195. showError(response.msg || "会员购买失败");
  196. }
  197. } catch (error) {
  198. console.error("查询订单状态失败:", error);
  199. showError("会员购买失败,请重试");
  200. }
  201. };
  202. // 加载价格配置的函数
  203. const loadPriceConfig = async () => {
  204. if (isLoggedIn.value && !priceStore.isPriceConfigLoaded) {
  205. try {
  206. await priceStore.fetchPriceConfig();
  207. } catch (error) {
  208. console.error("价格配置加载失败:", error);
  209. }
  210. }
  211. };
  212. // 监听登录状态变化
  213. watch(
  214. isLoggedIn,
  215. (newValue) => {
  216. if (newValue) {
  217. loadPriceConfig();
  218. }
  219. },
  220. { immediate: true }
  221. );
  222. onMounted(async () => {
  223. checkPaymentSuccess();
  224. // 初始加载价格配置
  225. await loadPriceConfig();
  226. });
  227. </script>
  228. <template>
  229. <section class="space-y-5">
  230. <div class="rounded-2xl bg-white/5 border border-white/10 p-4">
  231. <!-- 个人信息区域 -->
  232. <div class="flex items-center gap-3 mb-4">
  233. <div
  234. v-if="isLoggedIn"
  235. 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"
  236. >
  237. {{ userStore.userInfo?.name?.[0] || "用" }}
  238. </div>
  239. <div
  240. v-else
  241. class="h-12 w-12 rounded-full bg-white/10 grid place-items-center text-white/60"
  242. >
  243. <svg
  244. viewBox="0 0 24 24"
  245. width="24"
  246. height="24"
  247. fill="none"
  248. stroke="currentColor"
  249. stroke-width="2"
  250. >
  251. <path d="M20 21a8 8 0 1 0-16 0" />
  252. <circle cx="12" cy="7" r="4" />
  253. </svg>
  254. </div>
  255. <div class="flex-1 min-w-0">
  256. <p class="text-sm text-white/60">
  257. {{ isLoggedIn ? "欢迎回来" : "未登录" }}
  258. </p>
  259. <h2 class="text-base font-semibold text-white/90 truncate">
  260. {{
  261. isLoggedIn ? userStore.userInfo?.name || "用户" : "点击登录账号"
  262. }}
  263. <span
  264. v-if="isVip"
  265. class="ml-2 px-2 py-0.5 rounded bg-yellow-400/90 text-xs text-yellow-900 font-semibold align-middle"
  266. style="vertical-align: middle"
  267. >
  268. {{ vipLevelToText(userStore.userInfo?.vipLevel) }}
  269. </span>
  270. </h2>
  271. <!-- VIP到期时间显示 -->
  272. <p v-if="shouldShowExpireTime" class="text-xs text-white/60 mt-1">
  273. 到期时间:{{
  274. formatVipExpireTime(userStore.userInfo?.vipExpireTime)
  275. }}
  276. </p>
  277. </div>
  278. </div>
  279. <!-- 按钮区域 -->
  280. <div v-if="isLoggedIn" class="flex gap-2">
  281. <button
  282. v-if="isGuest"
  283. @click="showUpgradeDialog = true"
  284. class="px-3 py-1.5 rounded-lg bg-blue-500 text-white text-sm font-medium hover:bg-blue-600 transition"
  285. >
  286. 成为正式用户
  287. </button>
  288. <button
  289. @click="showMembershipDialog = true"
  290. class="px-3 py-1.5 rounded-lg bg-brand text-slate-900 text-sm font-medium"
  291. :disabled="isGuest"
  292. :class="{ 'opacity-50 cursor-not-allowed': isGuest }"
  293. >
  294. {{ isVip ? "续订会员" : "开通会员" }}
  295. </button>
  296. </div>
  297. <button
  298. v-else
  299. @click="showLoginDialog"
  300. class="px-3 py-1.5 rounded-lg bg-brand text-slate-900 text-sm font-medium"
  301. >
  302. 登录
  303. </button>
  304. </div>
  305. <div
  306. v-if="isLoggedIn"
  307. class="rounded-2xl overflow-hidden border border-white/10 divide-y divide-white/10"
  308. >
  309. <button @click="handleLogout" class="row text-red-300/90">
  310. <span>退出登录</span>
  311. <svg
  312. viewBox="0 0 24 24"
  313. width="18"
  314. height="18"
  315. fill="none"
  316. stroke="currentColor"
  317. stroke-width="2"
  318. >
  319. <path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
  320. <path d="m10 17 5-5-5-5" />
  321. <path d="M15 12H3" />
  322. </svg>
  323. </button>
  324. </div>
  325. <!-- 升级弹窗 -->
  326. <div
  327. v-if="showUpgradeDialog"
  328. class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
  329. @click.self="showUpgradeDialog = false"
  330. >
  331. <div
  332. class="bg-surface border border-white/10 rounded-2xl p-6 w-full max-w-md mx-4"
  333. >
  334. <h3 class="text-xl font-semibold text-white/90 mb-4">成为正式用户</h3>
  335. <form @submit.prevent="handleUpgrade" class="space-y-4">
  336. <div>
  337. <label class="block text-sm font-medium text-white/70 mb-1">
  338. 用户名 <span class="text-red-400">*</span>
  339. </label>
  340. <input
  341. v-model="upgradeForm.name"
  342. type="text"
  343. required
  344. 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"
  345. placeholder="请输入用户名"
  346. />
  347. </div>
  348. <div>
  349. <label class="block text-sm font-medium text-white/70 mb-1">
  350. 密码 <span class="text-red-400">*</span>
  351. </label>
  352. <input
  353. v-model="upgradeForm.password"
  354. type="password"
  355. required
  356. 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"
  357. placeholder="请输入密码"
  358. />
  359. </div>
  360. <div>
  361. <label class="block text-sm font-medium text-white/70 mb-1">
  362. 邮箱
  363. </label>
  364. <input
  365. v-model="upgradeForm.email"
  366. type="email"
  367. 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"
  368. placeholder="请输入邮箱(可选)"
  369. />
  370. </div>
  371. <div>
  372. <label class="block text-sm font-medium text-white/70 mb-1">
  373. 手机号
  374. </label>
  375. <input
  376. v-model="upgradeForm.phone"
  377. type="tel"
  378. 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"
  379. placeholder="请输入手机号(可选)"
  380. />
  381. </div>
  382. <div class="flex gap-3 pt-4">
  383. <button
  384. type="button"
  385. @click="showUpgradeDialog = false"
  386. class="flex-1 px-4 py-2 border border-white/20 text-white/70 rounded-lg hover:bg-white/5 transition"
  387. >
  388. 取消
  389. </button>
  390. <button
  391. type="submit"
  392. :disabled="isLoading"
  393. class="flex-1 px-4 py-2 bg-brand text-slate-900 rounded-lg hover:bg-brand/90 transition disabled:opacity-50"
  394. >
  395. {{ isLoading ? "处理中..." : "确认升级" }}
  396. </button>
  397. </div>
  398. </form>
  399. </div>
  400. </div>
  401. <!-- 会员购买弹窗 -->
  402. <div
  403. v-if="showMembershipDialog"
  404. class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
  405. @click.self="showMembershipDialog = false"
  406. >
  407. <div
  408. class="bg-surface border border-white/10 rounded-2xl p-6 w-full max-w-lg mx-4 max-h-[90vh] overflow-y-auto"
  409. >
  410. <h3 class="text-xl font-semibold text-white/90 mb-6 text-center">
  411. 开通会员
  412. </h3>
  413. <!-- 会员套餐选择 -->
  414. <div class="space-y-2 mb-4">
  415. <h4 class="text-sm font-medium text-white/70 mb-2">选择套餐</h4>
  416. <div class="grid grid-cols-1 gap-2">
  417. <label
  418. v-for="plan in membershipPlans"
  419. :key="plan.key"
  420. class="relative cursor-pointer"
  421. >
  422. <input
  423. v-model="selectedPlan"
  424. :value="plan.key"
  425. type="radio"
  426. name="membership-plan"
  427. class="sr-only"
  428. />
  429. <div
  430. class="border-2 rounded-lg p-2 transition-all"
  431. :class="
  432. selectedPlan === plan.key
  433. ? 'border-brand bg-brand/10'
  434. : 'border-white/20 hover:border-white/30'
  435. "
  436. >
  437. <div class="flex justify-between items-center">
  438. <div>
  439. <div class="font-medium text-white/90 text-sm">
  440. {{ plan.label }}
  441. </div>
  442. <div class="text-xs text-white/60">{{ plan.duration }}</div>
  443. </div>
  444. <div class="text-right">
  445. <div class="font-semibold text-white/90 text-sm">
  446. {{ plan.price }}
  447. </div>
  448. </div>
  449. </div>
  450. </div>
  451. </label>
  452. </div>
  453. </div>
  454. <!-- 支付方式选择 -->
  455. <div class="space-y-2 mb-4">
  456. <h4 class="text-sm font-medium text-white/70 mb-2">支付方式</h4>
  457. <div class="space-y-2">
  458. <label
  459. class="flex items-center p-2 border border-white/20 rounded-lg cursor-pointer hover:bg-white/5"
  460. >
  461. <input
  462. v-model="selectedPayment"
  463. value="alipay"
  464. type="radio"
  465. name="payment-method"
  466. class="sr-only"
  467. />
  468. <div class="flex items-center">
  469. <div
  470. class="w-8 h-8 bg-blue-500 rounded flex items-center justify-center mr-3"
  471. >
  472. <span class="text-white text-sm font-bold">支</span>
  473. </div>
  474. <span class="text-white/90">支付宝</span>
  475. </div>
  476. </label>
  477. </div>
  478. </div>
  479. <!-- 操作按钮 -->
  480. <div class="flex gap-3 pt-3 border-t border-white/10">
  481. <button
  482. type="button"
  483. @click="showMembershipDialog = false"
  484. class="flex-1 px-4 py-2 border border-white/20 text-white/70 rounded-lg hover:bg-white/5 transition"
  485. >
  486. 取消
  487. </button>
  488. <button
  489. @click="handleMembershipPurchase"
  490. :disabled="!selectedPlan || isPaymentLoading"
  491. 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"
  492. >
  493. {{ isPaymentLoading ? "处理中..." : "立即购买" }}
  494. </button>
  495. </div>
  496. </div>
  497. </div>
  498. <!-- 支付等待弹窗 -->
  499. <div
  500. v-if="showPaymentWaitingDialog"
  501. class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
  502. >
  503. <div
  504. class="bg-surface border border-white/10 rounded-2xl p-8 w-full max-w-sm mx-4 text-center"
  505. >
  506. <!-- 加载动画 -->
  507. <div class="mb-6">
  508. <div
  509. class="w-16 h-16 mx-auto border-4 border-white/20 border-t-brand rounded-full animate-spin"
  510. ></div>
  511. </div>
  512. <!-- 等待文字 -->
  513. <h3 class="text-lg font-semibold text-white/90 mb-2">
  514. 正在拉起支付页面,等待支付...
  515. </h3>
  516. <p class="text-sm text-white/60 mb-6">
  517. 请在支付页面完成支付<br />
  518. 支付完成后点击已完成支付
  519. </p>
  520. <!-- 操作按钮 -->
  521. <div class="flex gap-3">
  522. <button
  523. @click="showPaymentWaitingDialog = false"
  524. class="flex-1 px-4 py-2 border border-white/20 text-white/70 rounded-lg hover:bg-white/5 transition"
  525. >
  526. 取消支付
  527. </button>
  528. <button
  529. @click="handleQueryOrder"
  530. class="flex-1 px-4 py-2 bg-brand text-slate-900 rounded-lg hover:bg-brand/90 transition"
  531. >
  532. 已完成支付
  533. </button>
  534. </div>
  535. </div>
  536. </div>
  537. <!-- 错误提示弹窗 -->
  538. <div
  539. v-if="showErrorDialog"
  540. class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
  541. @click.self="showErrorDialog = false"
  542. >
  543. <div
  544. class="bg-surface border border-white/10 rounded-2xl p-6 w-full max-w-sm mx-4 text-center"
  545. >
  546. <!-- 错误图标 -->
  547. <div class="mb-4">
  548. <div
  549. class="w-12 h-12 mx-auto bg-red-500/20 rounded-full flex items-center justify-center"
  550. >
  551. <svg
  552. class="w-6 h-6 text-red-400"
  553. fill="none"
  554. stroke="currentColor"
  555. viewBox="0 0 24 24"
  556. >
  557. <path
  558. stroke-linecap="round"
  559. stroke-linejoin="round"
  560. stroke-width="2"
  561. 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"
  562. />
  563. </svg>
  564. </div>
  565. </div>
  566. <!-- 错误信息 -->
  567. <h3 class="text-lg font-semibold text-white/90 mb-2">操作失败</h3>
  568. <p class="text-sm text-white/70 mb-6">
  569. {{ errorMessage }}
  570. </p>
  571. <!-- 确认按钮 -->
  572. <button
  573. @click="showErrorDialog = false"
  574. class="w-full px-4 py-2 bg-brand text-slate-900 rounded-lg hover:bg-brand/90 transition"
  575. >
  576. 确定
  577. </button>
  578. </div>
  579. </div>
  580. <!-- 成功提示弹窗 -->
  581. <div
  582. v-if="showSuccessDialog"
  583. class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
  584. @click.self="showSuccessDialog = false"
  585. >
  586. <div
  587. class="bg-surface border border-white/10 rounded-2xl p-6 w-full max-w-sm mx-4 text-center"
  588. >
  589. <!-- 成功图标 -->
  590. <div class="mb-4">
  591. <div
  592. class="w-12 h-12 mx-auto bg-green-500/20 rounded-full flex items-center justify-center"
  593. >
  594. <svg
  595. class="w-6 h-6 text-green-400"
  596. fill="none"
  597. stroke="currentColor"
  598. viewBox="0 0 24 24"
  599. >
  600. <path
  601. stroke-linecap="round"
  602. stroke-linejoin="round"
  603. stroke-width="2"
  604. d="M5 13l4 4L19 7"
  605. />
  606. </svg>
  607. </div>
  608. </div>
  609. <!-- 成功信息 -->
  610. <h3 class="text-lg font-semibold text-white/90 mb-2">操作成功</h3>
  611. <p class="text-sm text-white/70 mb-6">
  612. {{ successMessage }}
  613. </p>
  614. <!-- 确认按钮 -->
  615. <button
  616. @click="showSuccessDialog = false"
  617. class="w-full px-4 py-2 bg-brand text-slate-900 rounded-lg hover:bg-brand/90 transition"
  618. >
  619. 确定
  620. </button>
  621. </div>
  622. </div>
  623. </section>
  624. </template>
  625. <style scoped>
  626. .row {
  627. @apply w-full flex items-center justify-between px-4 py-3 bg-white/5 text-white/80 hover:bg-white/10;
  628. }
  629. </style>