关闭职位页空个人资料时强制上传一份经历,和添加时间处理工具

This commit is contained in:
xuxin
2026-05-22 09:59:34 +08:00
parent 1637ee5bbc
commit dd103f0794
6 changed files with 270 additions and 18 deletions
+20
View File
@@ -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<any, ApiResult<MemberStatus>>('/member/status')
}
/**
* 查询会员商品列表
* GET /member/product/list
@@ -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;
+52 -7
View File
@@ -66,17 +66,26 @@
<div class="settings-dialog__member-card">
<div class="settings-dialog__member-header">
<div class="settings-dialog__member-title-row">
<span class="settings-dialog__member-name">会员</span>
<span class="settings-dialog__member-badge">查看详情</span>
<span class="settings-dialog__member-name">正式会员</span>
<span
class="settings-dialog__member-badge"
:class="{ 'settings-dialog__member-badge--inactive': !memberStatus.isMember }"
>
{{ memberStatus.isMember ? '已开通' : '未开通' }}
</span>
</div>
<span class="settings-dialog__member-terms" @click="handleMemberTerms">会员条款</span>
</div>
<div class="settings-dialog__member-info-row">
<span class="settings-dialog__member-price">
¥19.99/
<!-- <span>将于2026年3月27日续费</span>-->
<!-- 已开通显示到期时间和剩余天数 -->
<span v-if="memberStatus.isMember" class="settings-dialog__member-price">
<span class="settings-dialog__member-expire-line">到期时间{{ memberExpireDateTime }}</span>
<span class="settings-dialog__member-remain-line">剩余 {{ memberRemainDays }} </span>
</span>
<!-- 未开通显示价格 -->
<span v-else class="settings-dialog__member-price">
¥19.99/
</span>
<!-- <button class="settings-dialog__member-manage-btn" @click="handleManageSubscription">管理我的订阅</button>-->
</div>
</div>
<div class="settings-dialog__member-issue">
@@ -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<MemberStatus>({
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()
}
})
+2 -2
View File
@@ -11,7 +11,7 @@
<!-- 邀请链接区域 -->
<div class="settings-invite-dialog__link-box">
<p class="settings-invite-dialog__link-text">来一起领取AI求职助手会员{{ inviteText }}</p>
<p class="settings-invite-dialog__link-text">来一起领取AI求职助手会员{{ inviteText }}点击链接进入活动</p>
</div>
<!-- 复制链接按钮 -->
@@ -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}`
})
/** 复制链接到剪贴板 */
+168
View File
@@ -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)
}
+8 -8
View File
@@ -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()