index.vue 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. <template>
  2. <div class="header-view">
  3. <div class="page-header-view">
  4. <div class="header">
  5. <div class="left">
  6. <div class="logo" @click="goHome">
  7. <img src="@/assets/images/common/logo.svg" alt="Vitiens" class="logo-icon" />
  8. <img src="@/assets/images/common/Vitiens.svg" alt="Vitiens" class="logo-text" />
  9. </div>
  10. </div>
  11. <div class="header-right">
  12. <lang-popover ref="langPopoverRef" />
  13. <div class="avatar-dropdown-container" @click="onToggleAvatarDropdown">
  14. <img :src="avatarSrc" class="icon icon-avatar" />
  15. <img src="@/assets/images/common/icon_down.svg" class="icon icon-down" />
  16. <div v-if="isAvatarDropdownOpen" class="dropdown-container">
  17. <!-- 用户信息区 -->
  18. <div class="user-profile-section">
  19. <img :src="avatarSrc" class="profile-avatar" />
  20. <div class="profile-info">
  21. <div class="profile-name">{{ userInfo?.nickname || userInfo?.username || 'User' }}</div>
  22. <div class="profile-uid" @click="onCopy">
  23. <span>ID: {{ userInfo?.uid }}</span>
  24. <img src="@/assets/images/common/copy-icon.svg" class="copy-icon" />
  25. </div>
  26. </div>
  27. </div>
  28. <!-- 余额显示 -->
  29. <div class="balance-section">
  30. <div class="balance-label">{{ $t('finance.balance') }}</div>
  31. <div class="balance-value">{{ userInfo?.balance ?? '0.00' }}</div>
  32. </div>
  33. <!-- 基本信息 -->
  34. <div class="info-section">
  35. <div class="info-item">
  36. <span class="info-label">{{ $t('user.region') }}</span>
  37. <span class="info-value">{{ userInfo?.region || '--' }}</span>
  38. </div>
  39. <div class="info-item">
  40. <span class="info-label">{{ $t('user.age') }}</span>
  41. <span class="info-value">{{ userInfo?.age || '--' }}</span>
  42. </div>
  43. <div class="info-item">
  44. <span class="info-label">{{ $t('user.gender') }}</span>
  45. <span class="info-value">{{ genderText }}</span>
  46. </div>
  47. </div>
  48. <!-- 退出登录 -->
  49. <div class="logout-wrapper" @click="onLogout">
  50. <img src="@/assets/images/common/exit.svg" />
  51. <span>{{ $t("退出登录") }}</span>
  52. </div>
  53. </div>
  54. </div>
  55. </div>
  56. </div>
  57. </div>
  58. </div>
  59. </template>
  60. <script setup lang="ts">
  61. import LangPopover from "@/components/LangPopover/index.vue";
  62. import { computed, onMounted, onUnmounted, ref, watch } from "vue";
  63. import { useRouter } from "vue-router";
  64. import { useI18n } from "vue-i18n";
  65. import { copyText } from "@/utils/utils";
  66. import { useGlobalStore } from "@/store/modules/globalStore";
  67. import { requestGetUserInfo } from "@/api";
  68. import defaultAvatar from "@/assets/images/common/logo.svg";
  69. const { t } = useI18n();
  70. const router = useRouter();
  71. const userInfo = ref<UserInfo>();
  72. const avatarSrc = computed(() => userInfo.value?.avatar || defaultAvatar);
  73. const genderText = computed(() => {
  74. const g = userInfo.value?.gender;
  75. if (g === 1) return t('user.male');
  76. if (g === 2) return t('user.female');
  77. return '--';
  78. });
  79. const isAvatarDropdownOpen = ref(false);
  80. const langPopoverRef = ref<typeof LangPopover>(null);
  81. // 头像下拉框相关方法
  82. const onToggleAvatarDropdown = () => {
  83. isAvatarDropdownOpen.value = !isAvatarDropdownOpen.value;
  84. langPopoverRef.value?.close();
  85. };
  86. const onCopy = async () => copyText(userInfo.value?.uid);
  87. const onLogout = () => {
  88. isAvatarDropdownOpen.value = false;
  89. localStorage.removeItem("token");
  90. router.replace("/home");
  91. window.location.reload();
  92. };
  93. const goHome = () => {
  94. router.push('/');
  95. };
  96. // 点击外部区域关闭下拉框
  97. const handleClickOutside = (event: Event) => {
  98. const target = event.target as HTMLElement;
  99. if (!target.closest(".avatar-dropdown-container")) {
  100. isAvatarDropdownOpen.value = false;
  101. }
  102. };
  103. const getUserInfo = async () => {
  104. const res = await requestGetUserInfo();
  105. userInfo.value = res.data.user || res.data;
  106. };
  107. const globalStore = useGlobalStore();
  108. const isLoggedIn = computed(() => globalStore.isLoggedIn);
  109. watch(
  110. () => isLoggedIn.value,
  111. newVal => {
  112. if (newVal) {
  113. getUserInfo();
  114. }
  115. }
  116. );
  117. onMounted(() => {
  118. if (isLoggedIn.value) {
  119. getUserInfo();
  120. }
  121. });
  122. onMounted(() => {
  123. document.addEventListener("click", handleClickOutside);
  124. });
  125. onUnmounted(() => {
  126. document.removeEventListener("click", handleClickOutside);
  127. });
  128. </script>
  129. <style lang="scss" scoped>
  130. .header-view {
  131. max-width: 500px;
  132. margin: 0 auto;
  133. position: fixed;
  134. top: 0;
  135. left: 0;
  136. right: 0;
  137. z-index: 50;
  138. background-color: #161617;
  139. height: min(11.2vw, 53.76px);
  140. }
  141. .page-header-view {
  142. height: 100%;
  143. .header {
  144. height: 100%;
  145. padding: 0 min(4.267vw, 20.48px);
  146. display: flex;
  147. align-items: center;
  148. justify-content: space-between;
  149. .left {
  150. display: flex;
  151. align-items: center;
  152. height: 100%;
  153. .logo {
  154. display: flex;
  155. align-items: center;
  156. font-weight: 700;
  157. font-size: min(6.24vw, 29.9px);
  158. letter-spacing: 0.5px;
  159. cursor: pointer;
  160. .logo-icon {
  161. width: min(7vw, 32px);
  162. height: min(7vw, 32px);
  163. margin-right: min(1.5vw, 6px);
  164. }
  165. .logo-text {
  166. height: min(5vw, 24px);
  167. }
  168. }
  169. }
  170. .header-right {
  171. display: flex;
  172. align-items: center;
  173. height: 100%;
  174. }
  175. .icon-avatar {
  176. margin-left: 7px;
  177. width: min(8vw, 38.4px);
  178. height: min(8vw, 38.4px);
  179. border-radius: 50%;
  180. object-fit: cover;
  181. }
  182. .icon-down {
  183. margin-left: 7px;
  184. }
  185. }
  186. .avatar-dropdown-container {
  187. display: flex;
  188. align-items: center;
  189. position: relative;
  190. .dropdown-container {
  191. position: absolute;
  192. top: 150%;
  193. right: 0;
  194. width: 220px;
  195. background: #1e1e20;
  196. border-radius: 12px;
  197. display: flex;
  198. flex-direction: column;
  199. padding: 12px;
  200. gap: 8px;
  201. z-index: 50;
  202. box-shadow: 0px 2px 12px 0px rgba(0, 0, 0, 0.4);
  203. // 用户信息区
  204. .user-profile-section {
  205. display: flex;
  206. align-items: center;
  207. gap: 10px;
  208. padding-bottom: 10px;
  209. border-bottom: 1px solid #2a2a2c;
  210. .profile-avatar {
  211. width: 42px;
  212. height: 42px;
  213. border-radius: 50%;
  214. object-fit: cover;
  215. border: 2px solid #e8a820;
  216. flex-shrink: 0;
  217. }
  218. .profile-info {
  219. flex: 1;
  220. min-width: 0;
  221. .profile-name {
  222. font-size: 14px;
  223. font-weight: 600;
  224. color: #fff;
  225. white-space: nowrap;
  226. overflow: hidden;
  227. text-overflow: ellipsis;
  228. }
  229. .profile-uid {
  230. display: flex;
  231. align-items: center;
  232. gap: 4px;
  233. margin-top: 2px;
  234. cursor: pointer;
  235. span {
  236. font-size: 11px;
  237. color: #828694;
  238. }
  239. .copy-icon {
  240. width: 12px;
  241. height: 12px;
  242. opacity: 0.6;
  243. }
  244. }
  245. }
  246. }
  247. // 余额显示
  248. .balance-section {
  249. background: linear-gradient(135deg, #2a2520 0%, #1e1e20 100%);
  250. border: 1px solid #3a3020;
  251. border-radius: 8px;
  252. padding: 10px 12px;
  253. display: flex;
  254. justify-content: space-between;
  255. align-items: center;
  256. .balance-label {
  257. font-size: 12px;
  258. color: #828694;
  259. }
  260. .balance-value {
  261. font-size: 18px;
  262. font-weight: 700;
  263. color: #e8a820;
  264. }
  265. }
  266. // 基本信息
  267. .info-section {
  268. display: flex;
  269. flex-direction: column;
  270. gap: 6px;
  271. padding: 6px 0;
  272. border-bottom: 1px solid #2a2a2c;
  273. .info-item {
  274. display: flex;
  275. justify-content: space-between;
  276. align-items: center;
  277. padding: 2px 4px;
  278. .info-label {
  279. font-size: 12px;
  280. color: #828694;
  281. }
  282. .info-value {
  283. font-size: 12px;
  284. color: #d0d0d0;
  285. }
  286. }
  287. }
  288. // 退出登录
  289. .logout-wrapper {
  290. display: flex;
  291. align-items: center;
  292. padding: 8px 4px;
  293. gap: 6px;
  294. cursor: pointer;
  295. color: #828694;
  296. font-size: 13px;
  297. border-radius: 6px;
  298. transition: background 0.2s;
  299. &:hover {
  300. background: #232325;
  301. }
  302. img {
  303. width: 16px;
  304. height: 16px;
  305. }
  306. }
  307. }
  308. }
  309. }
  310. </style>