From dd103f0794781dfcd8ccca21af678ce1966ae256 Mon Sep 17 00:00:00 2001 From: xuxin <15279969124@163.com> Date: Fri, 22 May 2026 09:59:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=B3=E9=97=AD=E8=81=8C=E4=BD=8D=E9=A1=B5?= =?UTF-8?q?=E7=A9=BA=E4=B8=AA=E4=BA=BA=E8=B5=84=E6=96=99=E6=97=B6=E5=BC=BA?= =?UTF-8?q?=E5=88=B6=E4=B8=8A=E4=BC=A0=E4=B8=80=E4=BB=BD=E7=BB=8F=E5=8E=86?= =?UTF-8?q?=EF=BC=8C=E5=92=8C=E6=B7=BB=E5=8A=A0=E6=97=B6=E9=97=B4=E5=A4=84?= =?UTF-8?q?=E7=90=86=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/member.ts | 20 +++ .../styles/components/settings-dialog.scss | 21 ++- src/components/SettingsDialog.vue | 59 +++++- src/components/SettingsInviteDialog.vue | 4 +- src/utils/time.ts | 168 ++++++++++++++++++ src/views/Jobs.vue | 16 +- 6 files changed, 270 insertions(+), 18 deletions(-) create mode 100644 src/utils/time.ts diff --git a/src/api/member.ts b/src/api/member.ts index 87baaa0..5d5d65e 100644 --- a/src/api/member.ts +++ b/src/api/member.ts @@ -32,6 +32,26 @@ export interface MemberProduct { isDelete: number } +/** 会员状态返回类型 */ +export interface MemberStatus { + /** 是否是会员 */ + isMember: boolean + /** 到期时间(毫秒时间戳) */ + expireTime?: number + /** 首次开通时间(毫秒时间戳) */ + createTime?: number + /** 最近续费时间(毫秒时间戳) */ + updateTime?: number +} + +/** + * 查询会员状态 + * GET /member/status + */ +export function fetchMemberStatus() { + return request.get>('/member/status') +} + /** * 查询会员商品列表 * GET /member/product/list diff --git a/src/assets/styles/components/settings-dialog.scss b/src/assets/styles/components/settings-dialog.scss index 39edfa5..bd96542 100644 --- a/src/assets/styles/components/settings-dialog.scss +++ b/src/assets/styles/components/settings-dialog.scss @@ -229,6 +229,11 @@ font-size: 0.11rem; padding: 0.02rem 0.08rem; border-radius: 0.1rem; + + &--inactive { + background: #ccc; + color: $bg-white; + } } &__member-terms { @@ -251,13 +256,27 @@ &__member-price { font-size: 0.13rem; color: #555; + display: flex; + flex-direction: column; + gap: 0.06rem; span { - margin-left: 0.12rem; + margin-left: 0; color: $text-light; } } + &__member-expire-line { + font-size: 0.13rem; + color: #555; + } + + &__member-remain-line { + font-size: 0.13rem; + color: $accent; + font-weight: 600; + } + &__member-manage-btn { background: $bg-main; border: 1px solid $border-color; diff --git a/src/components/SettingsDialog.vue b/src/components/SettingsDialog.vue index 5e68ed6..c455d95 100644 --- a/src/components/SettingsDialog.vue +++ b/src/components/SettingsDialog.vue @@ -66,17 +66,26 @@
- 会员 - 查看详情 + 正式会员 + + {{ memberStatus.isMember ? '已开通' : '未开通' }} +
会员条款
- - ¥19.99/月 - + + + 到期时间:{{ memberExpireDateTime }} + 剩余 {{ memberRemainDays }} 天 + + + + ¥19.99/月 -
@@ -243,6 +252,8 @@ import { ref, reactive, watch, computed } from 'vue' import { useRouter } from 'vue-router' import { useStore } from 'vuex' import { logout } from '@/api/auth' +import { fetchMemberStatus, type MemberStatus } from '@/api/member' +import { timestampToLocalDateTime, timestampDiffDays } from '@/utils/time' import JobGoalDialog from './JobGoalDialog.vue' import { resolveRegionName } from '@/utils/region' import { resolveIndustryName } from '@/utils/industry' @@ -299,6 +310,39 @@ const showDeleteAccount = ref(false) /** 邀请注册弹窗显示状态 */ const showInviteDialog = ref(false) +/** 会员状态数据 */ +const memberStatus = reactive({ + isMember: false, + expireTime: undefined, + createTime: undefined, + updateTime: undefined, +}) + +/** 会员到期时间(格式化显示) */ +const memberExpireDateTime = computed(() => { + return timestampToLocalDateTime(memberStatus.expireTime, 'returnMinute') +}) + +/** 会员剩余天数 */ +const memberRemainDays = computed(() => { + return timestampDiffDays(memberStatus.expireTime) +}) + +/** 查询会员状态 */ +const loadMemberStatus = async () => { + try { + const res = await fetchMemberStatus() + if (res.data) { + memberStatus.isMember = res.data.isMember ?? false + memberStatus.expireTime = res.data.expireTime + memberStatus.createTime = res.data.createTime + memberStatus.updateTime = res.data.updateTime + } + } catch { + // 查询失败保持默认未开通状态 + } +} + /** 岗位名称列表 */ const intentionCategoryNames = computed(() => { const ids = store.state.jobIntention.categoryIds || [] @@ -327,11 +371,12 @@ const handleEditTarget = () => { showGoalDialog.value = true } -/** 弹窗打开时加载求职意向数据 */ +/** 弹窗打开时加载求职意向数据和会员状态 */ watch(() => props.modelValue, (val) => { if (val && store.state.isAuthenticated) { store.dispatch('loadCommonData') store.dispatch('loadJobIntention') + loadMemberStatus() } }) diff --git a/src/components/SettingsInviteDialog.vue b/src/components/SettingsInviteDialog.vue index 1083115..714a1b5 100644 --- a/src/components/SettingsInviteDialog.vue +++ b/src/components/SettingsInviteDialog.vue @@ -11,7 +11,7 @@ @@ -92,7 +92,7 @@ const inviteCode = computed(() => store.state.userInfo?.inviteCode || '') /** 邀请链接文案 */ const inviteText = computed(() => { const code = inviteCode.value - return `https://www.qiuzhizhushou.com/invite_code=${code},点击链接进入活动!` + return `https://www.offerpai.com.cn/invite_code=${code}` }) /** 复制链接到剪贴板 */ diff --git a/src/utils/time.ts b/src/utils/time.ts new file mode 100644 index 0000000..5006cce --- /dev/null +++ b/src/utils/time.ts @@ -0,0 +1,168 @@ +/** + * 时间处理工具 + * 提供毫秒时间戳转换、时间间隔计算方法、浏览器本地时间事件缓存记录工具 + */ + +/** + * 时间精度级别 + * returnYear — 只返回年 + * returnMonth — 返回到月 + * returnDay — 返回到天 + * returnHour — 返回到小时 + * returnMinute — 返回到分钟 + * returnSecond — 返回到秒 + */ +export type TimePrecision = 'returnYear' | 'returnMonth' | 'returnDay' | 'returnHour' | 'returnMinute' | 'returnSecond' + +/** + * 毫秒时间戳转 LocalDateTime 字符串 + * @param timestamp 毫秒时间戳 + * @param precision 返回精度,默认 returnSecond + * @returns 格式化的本地时间字符串 + */ +export function timestampToLocalDateTime(timestamp: number | null | undefined, precision: TimePrecision = 'returnSecond'): string { + if (timestamp == null) return '--' + const date = new Date(timestamp) + const y = date.getFullYear() + if (precision === 'returnYear') return `${y}` + const m = String(date.getMonth() + 1).padStart(2, '0') + if (precision === 'returnMonth') return `${y}-${m}` + const d = String(date.getDate()).padStart(2, '0') + if (precision === 'returnDay') return `${y}-${m}-${d}` + const h = String(date.getHours()).padStart(2, '0') + if (precision === 'returnHour') return `${y}-${m}-${d} ${h}` + const min = String(date.getMinutes()).padStart(2, '0') + if (precision === 'returnMinute') return `${y}-${m}-${d} ${h}:${min}` + const s = String(date.getSeconds()).padStart(2, '0') + return `${y}-${m}-${d} ${h}:${min}:${s}` +} + +/** + * 计算毫秒时间戳与当前时间的间隔天数 + * 如果目标时间在当前时间之前,返回负数 + * @param timestamp 毫秒时间戳 + * @returns 间隔天数(正数表示未来,负数表示过去) + */ +export function timestampDiffDays(timestamp: number | null | undefined): number { + if (timestamp == null) return 0 + const diffMs = timestamp - Date.now() + return Math.floor(diffMs / (1000 * 60 * 60 * 24)) +} + +/** + * 计算毫秒时间戳与当前时间的详细间隔 + * 返回如 "200天1小时5分30秒" 或 "-3天2小时10分15秒" 的格式 + * @param timestamp 毫秒时间戳 + * @returns 格式化的时间间隔字符串 + */ +export function timestampDiffDetailed(timestamp: number | null | undefined): string { + if (timestamp == null) return '--' + let diffMs = timestamp - Date.now() + // 判断是否为过去时间 + const prefix = diffMs < 0 ? '-' : '' + diffMs = Math.abs(diffMs) + + const days = Math.floor(diffMs / (1000 * 60 * 60 * 24)) + diffMs %= 1000 * 60 * 60 * 24 + const hours = Math.floor(diffMs / (1000 * 60 * 60)) + diffMs %= 1000 * 60 * 60 + const minutes = Math.floor(diffMs / (1000 * 60)) + diffMs %= 1000 * 60 + const seconds = Math.floor(diffMs / 1000) + + // 拼接结果,只显示有值的部分 + let result = '' + if (days > 0) result += `${days}天` + if (hours > 0) result += `${hours}小时` + if (minutes > 0) result += `${minutes}分` + if (seconds > 0 || result === '') result += `${seconds}秒` + + return `${prefix}${result}` +} + + + + +// ==================== 浏览器本地时间事件缓存记录工具 ==================== + +/** localStorage 存储的 key */ +const TIME_EVENT_CACHE_KEY = 'local_time_event_cache' + +/** + * 自定义事件名称 ID 注册表 + * 每次新增事件 ID 必须在此处登记,格式:事件ID — 事件描述说明 + * + * ┌──────────────────────────────────────────────────────────────┐ + * │ 事件名称ID │ 事件描述说明 │ + * ├──────────────────────────────────────────────────────────────┤ + * │ member_status_query │ 会员状态查询时间记录 │ + * │ │ │ + * │ │ │ + * │ │ │ + * └──────────────────────────────────────────────────────────────┘ + */ + +/** 本地时间事件缓存数据项 */ +export interface TimeEventRecord { + /** 用户ID(对应 vuex store 中 userInfo.id) */ + userId: number | string + /** 事件名称ID(需在上方注册表中登记) */ + eventId: string + /** 时间点(LocalDateTime 格式字符串,如 "2026-05-21 14:30:00") */ + time: string +} + +/** + * 从 localStorage 读取时间事件缓存数据 + * @returns 缓存的时间事件数组 + */ +function getTimeEventCache(): TimeEventRecord[] { + try { + const raw = localStorage.getItem(TIME_EVENT_CACHE_KEY) + if (!raw) return [] + return JSON.parse(raw) as TimeEventRecord[] + } catch { + return [] + } +} + +/** + * 将时间事件缓存数据保存到 localStorage + * @param data 时间事件数组 + */ +function setTimeEventCache(data: TimeEventRecord[]): void { + localStorage.setItem(TIME_EVENT_CACHE_KEY, JSON.stringify(data)) +} + +/** + * 查询本地时间事件缓存 + * 根据用户ID和事件名称ID查找对应的时间记录 + * @param eventId 事件名称ID + * @param userId 用户ID(从 vuex store.state.userInfo.id 获取) + * @returns 匹配的时间点字符串,未找到返回 null + */ +export function getTimeEvent(eventId: string, userId: number | string): string | null { + const cache = getTimeEventCache() + const record = cache.find(item => item.userId == userId && item.eventId === eventId) + return record ? record.time : null +} + +/** + * 保存/更新本地时间事件缓存 + * 同一个用户ID + 事件名称ID 只允许存在唯一一条记录 + * @param eventId 事件名称ID + * @param userId 用户ID(从 vuex store.state.userInfo.id 获取) + * @param time 要保存的时间点(LocalDateTime 格式字符串) + */ +export function setTimeEvent(eventId: string, userId: number | string, time: string): void { + const cache = getTimeEventCache() + const index = cache.findIndex(item => item.userId == userId && item.eventId === eventId) + if (index >= 0) { + // 已存在则更新时间点 + cache[index].time = time + } else { + // 不存在则新增一条记录 + cache.push({ userId, eventId, time }) + } + setTimeEventCache(cache) +} diff --git a/src/views/Jobs.vue b/src/views/Jobs.vue index 42f7e35..e0d88d3 100644 --- a/src/views/Jobs.vue +++ b/src/views/Jobs.vue @@ -553,14 +553,14 @@ onMounted(async () => { store.dispatch('loadUserInfo') // 检查个人资料是否存在,不存在则弹出欢迎弹窗 - try { - const profileRes = await fetchProfile() - if (profileRes.code === '0' && (!profileRes.data || profileRes.data === null)) { - showWelcomeDialog.value = true - } - } catch { - // 接口异常不阻塞页面加载 - } + // try { + // const profileRes = await fetchProfile() + // if (profileRes.code === '0' && (!profileRes.data || profileRes.data === null)) { + // showWelcomeDialog.value = true + // } + // } catch { + // // 接口异常不阻塞页面加载 + // } // 加载收藏统计(用于 Tab 标签显示) loadFavoriteCount()