关闭职位页空个人资料时强制上传一份经历,和添加时间处理工具
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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}`
|
||||
})
|
||||
|
||||
/** 复制链接到剪贴板 */
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user