510 lines
21 KiB
Vue
510 lines
21 KiB
Vue
<template>
|
||
<!-- 设置弹窗 — 通过 Teleport 挂载到 body,避免被父组件样式影响 -->
|
||
<Teleport to="body">
|
||
<!-- 遮罩层 — 点击关闭弹窗 -->
|
||
<div v-if="modelValue" class="settings-dialog-overlay" @click="$emit('update:modelValue', false)">
|
||
<!-- 弹窗主体 — 阻止点击冒泡到遮罩层 -->
|
||
<div class="settings-dialog" @click.stop>
|
||
<!-- 右上角关闭按钮 -->
|
||
<span class="settings-dialog__close" @click="$emit('update:modelValue', false)">✕</span>
|
||
|
||
<!-- 左侧导航栏 -->
|
||
<div class="settings-dialog__sidebar">
|
||
<!-- 主导航 Tab 列表 -->
|
||
<div class="settings-dialog__nav">
|
||
<div
|
||
v-for="tab in tabs"
|
||
:key="tab.key"
|
||
class="settings-dialog__nav-item"
|
||
:class="{ 'settings-dialog__nav-item--active': activeTab === tab.key }"
|
||
@click="activeTab = tab.key"
|
||
>
|
||
<span>{{ tab.label }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧内容区 — 根据 activeTab 切换显示 -->
|
||
<div class="settings-dialog__content">
|
||
<!-- Tab: 账号与安全 — 显示手机号、注销账号 -->
|
||
<template v-if="activeTab === 'account'">
|
||
<h2 class="settings-dialog__content-title">账号与安全</h2>
|
||
<div class="settings-dialog__section">
|
||
<div class="settings-dialog__section-label">手机号</div>
|
||
<p class="settings-dialog__section-value">{{ userPhone }}</p>
|
||
</div>
|
||
<div class="settings-dialog__danger-section">
|
||
<div>
|
||
<div class="settings-dialog__danger-title">注销我的账号</div>
|
||
<div class="settings-dialog__danger-row">
|
||
<p class="settings-dialog__danger-desc">永久删除你的账号及所有相关数据</p>
|
||
</div>
|
||
</div>
|
||
<button class="settings-dialog__danger-btn" @click="handleDeleteAccount">注销账号</button>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Tab: 会员 — 根据会员类型显示不同卡片 -->
|
||
<template v-if="activeTab === 'member'">
|
||
<h2 class="settings-dialog__content-title">会员</h2>
|
||
|
||
<!-- 正式会员卡片 -->
|
||
<div v-if="memberStatus.isMember && memberStatus.memberType === 1" class="settings-dialog__mcard settings-dialog__mcard--formal">
|
||
<div class="settings-dialog__mcard-top">
|
||
<span class="settings-dialog__mcard-title">正式会员</span>
|
||
<div class="settings-dialog__mcard-remain">
|
||
<span class="settings-dialog__mcard-remain-label">剩余</span>
|
||
<span class="settings-dialog__mcard-remain-days">{{ memberRemainDays }}天</span>
|
||
</div>
|
||
</div>
|
||
<div class="settings-dialog__mcard-bottom">
|
||
<span class="settings-dialog__mcard-expire">有效期至{{ memberExpireDate }}</span>
|
||
<div class="settings-dialog__mcard-actions">
|
||
<button class="settings-dialog__mcard-btn settings-dialog__mcard-btn--outline" @click="openAgreementDialog('ae8065i3')">查看权益</button>
|
||
<button class="settings-dialog__mcard-btn settings-dialog__mcard-btn--solid-formal" @click="handleOpenMember">续费会员</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 试用会员卡片 -->
|
||
<div v-else-if="memberStatus.isMember && memberStatus.memberType === 2" class="settings-dialog__mcard settings-dialog__mcard--trial">
|
||
<div class="settings-dialog__mcard-top">
|
||
<span class="settings-dialog__mcard-title">试用会员</span>
|
||
<div class="settings-dialog__mcard-remain">
|
||
<span class="settings-dialog__mcard-remain-label">剩余</span>
|
||
<span class="settings-dialog__mcard-remain-days">{{ memberRemainDays }}天</span>
|
||
</div>
|
||
</div>
|
||
<div class="settings-dialog__mcard-bottom">
|
||
<span class="settings-dialog__mcard-expire">有效期至{{ memberExpireDate }}</span>
|
||
<div class="settings-dialog__mcard-actions">
|
||
<button class="settings-dialog__mcard-btn settings-dialog__mcard-btn--outline" @click="openAgreementDialog('ae8065i3')">查看权益</button>
|
||
<button class="settings-dialog__mcard-btn settings-dialog__mcard-btn--solid-trial" @click="handleOpenMember">开通正式会员</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 免费用户卡片 -->
|
||
<div v-else class="settings-dialog__mcard settings-dialog__mcard--free">
|
||
<div class="settings-dialog__mcard-top">
|
||
<span class="settings-dialog__mcard-title settings-dialog__mcard-title--free">免费用户</span>
|
||
</div>
|
||
<div class="settings-dialog__mcard-bottom">
|
||
<span class="settings-dialog__mcard-expire settings-dialog__mcard-expire--free">仅享受免费功能</span>
|
||
<div class="settings-dialog__mcard-actions">
|
||
<button class="settings-dialog__mcard-btn settings-dialog__mcard-btn--solid-accent" @click="handleOpenMember">开通会员</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<template v-if="activeTab === 'reminder'">
|
||
<h2 class="settings-dialog__content-title">目标岗位设置</h2>
|
||
<div class="settings-dialog__reminder-block">
|
||
<div class="settings-dialog__reminder-block-title-row">
|
||
<span class="settings-dialog__reminder-block-title">目标岗位</span>
|
||
<button class="settings-dialog__reminder-edit-btn" @click="handleEditTarget">编辑</button>
|
||
</div>
|
||
<div class="settings-dialog__reminder-target">
|
||
<div class="settings-dialog__reminder-group" v-if="intentionCategoryNames.length">
|
||
<span class="settings-dialog__reminder-group-label">岗位</span>
|
||
<div class="settings-dialog__reminder-tags">
|
||
<span class="settings-dialog__reminder-tag" v-for="name in intentionCategoryNames" :key="name">{{ name }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="settings-dialog__reminder-group" v-if="intentionIndustryNames.length">
|
||
<span class="settings-dialog__reminder-group-label">行业</span>
|
||
<div class="settings-dialog__reminder-tags">
|
||
<span class="settings-dialog__reminder-tag" v-for="name in intentionIndustryNames" :key="name">{{ name }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="settings-dialog__reminder-group" v-if="intentionRegionNames.length">
|
||
<span class="settings-dialog__reminder-group-label">地区</span>
|
||
<div class="settings-dialog__reminder-tags">
|
||
<span class="settings-dialog__reminder-tag" v-for="name in intentionRegionNames" :key="name">{{ name }}</span>
|
||
</div>
|
||
</div>
|
||
<div v-if="intentionEmploymentLabel" class="settings-dialog__reminder-group">
|
||
<span class="settings-dialog__reminder-group-label">类型</span>
|
||
<div class="settings-dialog__reminder-tags">
|
||
<span class="settings-dialog__reminder-tag">{{ intentionEmploymentLabel }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- <div class="settings-dialog__reminder-block">-->
|
||
<!-- <div class="settings-dialog__reminder-block-title">即时岗位提醒</div>-->
|
||
<!-- <div class="settings-dialog__reminder-row">-->
|
||
<!-- <div class="settings-dialog__reminder-info">-->
|
||
<!-- <div class="settings-dialog__reminder-label">开启即时岗位更新提醒</div>-->
|
||
<!-- <div class="settings-dialog__reminder-desc">-->
|
||
<!-- 抢先申请 —— 在岗位发布后一小时内,即可收到为你量身定制的最新职位提醒-->
|
||
<!-- </div>-->
|
||
<!-- </div>-->
|
||
<!-- <el-switch v-model="reminders.instant" active-color="#4FC2C9" />-->
|
||
<!-- </div>-->
|
||
<!-- </div>-->
|
||
<!-- <div class="settings-dialog__reminder-block">-->
|
||
<!-- <div class="settings-dialog__reminder-block-title">岗位更新提醒频率</div>-->
|
||
<!-- <div class="settings-dialog__reminder-row">-->
|
||
<!-- <div class="settings-dialog__reminder-info">-->
|
||
<!-- <div class="settings-dialog__reminder-desc">-->
|
||
<!-- 会员用户每天可接收无限次岗位更新提醒,免费用户每天最多接收 1 次。-->
|
||
<!-- </div>-->
|
||
<!-- </div>-->
|
||
<!-- <el-select v-model="reminders.frequency" style="width: 1.2rem;">-->
|
||
<!-- <el-option label="1次/天" value="1" />-->
|
||
<!-- <el-option label="2次/天" value="2" />-->
|
||
<!-- <el-option label="5次/天" value="5" />-->
|
||
<!-- <el-option label="无限次" value="unlimited" />-->
|
||
<!-- </el-select>-->
|
||
<!-- </div>-->
|
||
<!-- </div>-->
|
||
</template>
|
||
|
||
<!-- Tab: 问题反馈 — 选择问题类型 + 填写细节 + 提交 -->
|
||
<template v-if="activeTab === 'feedback'">
|
||
<h2 class="settings-dialog__content-title">问题反馈</h2>
|
||
<div class="settings-dialog__feedback-wrap">
|
||
<!-- 问题类型选择 -->
|
||
<div class="settings-dialog__feedback-section">
|
||
<div class="settings-dialog__feedback-label">请选择你反馈的问题类型</div>
|
||
<div class="settings-dialog__feedback-options">
|
||
<div
|
||
v-for="(opt, index) in feedbackOptions"
|
||
:key="index"
|
||
class="settings-dialog__feedback-option"
|
||
:class="{ 'settings-dialog__feedback-option--active': feedbackType === opt }"
|
||
@click="feedbackType = opt; feedbackTypeIndex = index + 1"
|
||
>
|
||
{{ opt }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 问题细节 -->
|
||
<div class="settings-dialog__feedback-section">
|
||
<div class="settings-dialog__feedback-label">问题细节</div>
|
||
<textarea
|
||
v-model="feedbackDetail"
|
||
class="settings-dialog__feedback-textarea"
|
||
placeholder="请描述问题细节"
|
||
rows="4"
|
||
></textarea>
|
||
</div>
|
||
</div>
|
||
<!-- 提交按钮 -->
|
||
<button class="settings-dialog__feedback-submit" @click="handleFeedbackSubmit">确认提交</button>
|
||
</template>
|
||
|
||
<!-- Tab: 用户隐私协议 — 从接口获取并用 markdown-it 渲染 -->
|
||
<template v-if="activeTab === 'privacy'">
|
||
<h2 class="settings-dialog__content-title">用户隐私协议</h2>
|
||
<div class="settings-dialog__privacy-content">
|
||
<!-- 加载中 -->
|
||
<div v-if="privacyLoading" style="text-align: center; padding: 0.4rem 0; color: #999;">加载中...</div>
|
||
<!-- 渲染协议内容 -->
|
||
<div v-else class="settings-dialog__privacy-section markdown-body" v-html="privacyHtml"></div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 退出登录确认弹窗 — 放在 Teleport 内部,确保层级在 overlay 之上 -->
|
||
<el-dialog v-model="showLogout" title="退出登录" width="3.6rem" style="line-height: 0.2rem" :close-on-click-modal="true" :append-to-body="false" :z-index="2100">
|
||
<p style="font-size: 0.14rem; color: #555; text-align: center;">确定要退出当前账号吗?</p>
|
||
<template #footer>
|
||
<el-button @click="showLogout = false">取消</el-button>
|
||
<el-button type="danger" @click="handleLogout">确认退出</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 求职目标设置弹窗 -->
|
||
<JobGoalDialog v-model="showGoalDialog" />
|
||
|
||
<!-- 注销账号弹窗 -->
|
||
<SettingsDeleteAccountDialog v-model="showDeleteAccount" @deleted="onAccountDeleted" />
|
||
|
||
<!-- 邀请注册送会员弹窗 -->
|
||
<SettingsInviteDialog v-model="showInviteDialog" />
|
||
|
||
<!-- 协议预览弹窗 -->
|
||
<AgreementPreviewDialog v-model="showAgreementDialog" :code="currentAgreementCode" />
|
||
|
||
<!-- 会员购买弹窗 -->
|
||
<MemberDialog v-model="showMemberDialog" />
|
||
</Teleport>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, watch, computed } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { useStore } from 'vuex'
|
||
import { formatEmploymentType } from '@/stores/index'
|
||
import { logout } from '@/api/auth'
|
||
import { fetchMemberStatus, type MemberStatus } from '@/api/member'
|
||
import { fetchAgreement } from '@/api/common'
|
||
import { userFeedback } from '@/api/setting'
|
||
import { timestampToLocalDateTime, timestampDiffDays } from '@/utils/time'
|
||
import JobGoalDialog from './JobGoalDialog.vue'
|
||
import { resolveRegionName } from '@/utils/region'
|
||
import { resolveIndustryName } from '@/utils/industry'
|
||
import { resolveJobCategoryName } from '@/utils/jobCategory'
|
||
import SettingsDeleteAccountDialog from './SettingsDeleteAccountDialog.vue'
|
||
import SettingsInviteDialog from './SettingsInviteDialog.vue'
|
||
import MemberDialog from './MemberDialog.vue'
|
||
import AgreementPreviewDialog from '@/components/tools/AgreementPreviewDialog.vue'
|
||
// @ts-ignore
|
||
import markdownit from 'markdown-it'
|
||
|
||
/** markdown-it 实例 — 用于渲染协议内容 */
|
||
const md = markdownit({ html: false, breaks: true, linkify: true })
|
||
|
||
/** 组件 Props — 控制弹窗显示/隐藏,可指定初始 Tab */
|
||
const props = defineProps<{ modelValue: boolean; initialTab?: string }>()
|
||
|
||
/** 组件 Emits — 通知父组件更新 modelValue */
|
||
const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void }>()
|
||
|
||
/** 路由实例 — 退出登录后跳转首页 */
|
||
const router = useRouter()
|
||
const store = useStore()
|
||
|
||
/** 左侧导航 Tab 配置 */
|
||
const tabs = [
|
||
{ key: 'account', label: '账号与安全' },
|
||
{ key: 'member', label: '会员' },
|
||
{ key: 'feedback', label: '问题反馈' },
|
||
// { key: 'reminder', label: '目标岗位设置' },
|
||
{ key: 'privacy', label: '用户隐私协议' },
|
||
]
|
||
|
||
/** 当前选中的 Tab */
|
||
const activeTab = ref('account')
|
||
|
||
/** 用户手机号 — 从全局 store 读取 */
|
||
const userPhone = computed(() => store.state.userInfo?.mobileNumber || '')
|
||
|
||
/** 退出登录确认弹窗的显示状态 */
|
||
const showLogout = ref(false)
|
||
|
||
/** 监听弹窗开关 — 打开时锁定背景页面滚动,关闭时恢复 */
|
||
watch(() => props.modelValue, (val) => {
|
||
document.body.style.overflow = val ? 'hidden' : ''
|
||
if (val && props.initialTab) {
|
||
activeTab.value = props.initialTab
|
||
if (props.initialTab === 'privacy') loadPrivacyAgreement()
|
||
}
|
||
})
|
||
|
||
/** 监听 Tab 切换 — 切到隐私协议时加载内容 */
|
||
watch(activeTab, (tab) => {
|
||
if (tab === 'privacy') loadPrivacyAgreement()
|
||
})
|
||
|
||
/** 岗位更新提醒的配置项 */
|
||
const reminders = reactive({
|
||
instant: true, // 是否开启即时岗位提醒
|
||
frequency: 'unlimited', // 提醒频率:1/2/5/unlimited
|
||
})
|
||
|
||
/** 求职目标弹窗显示状态 */
|
||
const showGoalDialog = ref(false)
|
||
|
||
/** 注销账号弹窗显示状态 */
|
||
const showDeleteAccount = ref(false)
|
||
|
||
/** 邀请注册弹窗显示状态 */
|
||
const showInviteDialog = ref(false)
|
||
|
||
/** 会员购买弹窗显示状态 */
|
||
const showMemberDialog = ref(false)
|
||
|
||
/** 问题反馈 — 选项列表 */
|
||
const feedbackOptions = ['Bug反馈', '功能建议', '使用体验', '订阅及会员相关问题', '其他']
|
||
/** 问题反馈 — 选中的类型文字 */
|
||
const feedbackType = ref('')
|
||
/** 问题反馈 — 选中的类型索引(传给接口) */
|
||
const feedbackTypeIndex = ref(0)
|
||
/** 问题反馈 — 问题细节 */
|
||
const feedbackDetail = ref('')
|
||
|
||
/** 协议预览弹窗显示状态 */
|
||
const showAgreementDialog = ref(false)
|
||
/** 当前预览的协议码 */
|
||
const currentAgreementCode = ref('')
|
||
|
||
/** 打开协议预览弹窗 */
|
||
function openAgreementDialog(code: string) {
|
||
currentAgreementCode.value = code
|
||
showAgreementDialog.value = true
|
||
}
|
||
|
||
/** 隐私协议内容(Markdown 渲染后的 HTML) */
|
||
const privacyHtml = ref('')
|
||
/** 隐私协议加载状态 */
|
||
const privacyLoading = ref(false)
|
||
|
||
/** 加载隐私协议内容 */
|
||
async function loadPrivacyAgreement() {
|
||
if (privacyHtml.value) return // 已加载过不重复请求
|
||
privacyLoading.value = true
|
||
try {
|
||
const res = await fetchAgreement('hf8375i8')
|
||
if (res.data?.content) {
|
||
privacyHtml.value = md.render(res.data.content)
|
||
} else {
|
||
privacyHtml.value = '<p>暂无协议内容</p>'
|
||
}
|
||
} catch {
|
||
privacyHtml.value = '<p>加载失败,请稍后重试</p>'
|
||
} finally {
|
||
privacyLoading.value = false
|
||
}
|
||
}
|
||
|
||
/** 会员状态数据 */
|
||
const memberStatus = reactive<MemberStatus>({
|
||
isMember: false,
|
||
memberType: undefined,
|
||
expireTime: undefined,
|
||
createTime: undefined,
|
||
updateTime: undefined,
|
||
})
|
||
|
||
/** 会员到期日期(格式:yyyy-MM-dd) */
|
||
const memberExpireDate = computed(() => {
|
||
// timestampToLocalDateTime returnDay 返回 yyyy/MM/dd,替换为 yyyy-MM-dd
|
||
return timestampToLocalDateTime(memberStatus.expireTime, 'returnDay').replace(/\//g, '-')
|
||
})
|
||
|
||
/** 会员剩余天数 */
|
||
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.memberType = res.data.memberType
|
||
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 || []
|
||
return ids.map((id: number) => resolveJobCategoryName(id))
|
||
})
|
||
|
||
/** 行业名称列表 */
|
||
const intentionIndustryNames = computed(() => {
|
||
const ids = store.state.jobIntention.industryIds || []
|
||
return ids.map((id: number) => resolveIndustryName(id))
|
||
})
|
||
|
||
/** 地区名称列表 */
|
||
const intentionRegionNames = computed(() => {
|
||
const codes = store.state.jobIntention.regionCodes || []
|
||
return codes.map((code: string) => resolveRegionName(code))
|
||
})
|
||
|
||
/** 招聘分类标签 */
|
||
const intentionEmploymentLabel = computed(() => {
|
||
return formatEmploymentType(store.state.jobIntention.recruitCategory)
|
||
})
|
||
|
||
/** 编辑目标岗位 — 打开求职目标弹窗 */
|
||
const handleEditTarget = () => {
|
||
showGoalDialog.value = true
|
||
}
|
||
|
||
/** 打开会员购买弹窗 */
|
||
const handleOpenMember = () => {
|
||
showMemberDialog.value = true
|
||
}
|
||
|
||
/** 提交问题反馈 */
|
||
const handleFeedbackSubmit = async () => {
|
||
if (feedbackTypeIndex.value < 1 || feedbackDetail.value.trim() === '') {
|
||
ElMessage.warning('请选择问题并填写反馈内容')
|
||
return
|
||
}
|
||
try {
|
||
await userFeedback({
|
||
type: feedbackTypeIndex.value,
|
||
content: feedbackDetail.value,
|
||
})
|
||
ElMessage.success('提交成功')
|
||
feedbackType.value = ''
|
||
feedbackDetail.value = ''
|
||
feedbackTypeIndex.value = 0
|
||
} catch {
|
||
ElMessage.error('提交失败,请稍后重试')
|
||
}
|
||
}
|
||
|
||
/** 弹窗打开时加载求职意向数据和会员状态 */
|
||
watch(() => props.modelValue, (val) => {
|
||
if (val && store.state.isAuthenticated) {
|
||
store.dispatch('loadCommonData')
|
||
// 暂时注释掉loadJobIntention,现在设置弹窗里去掉了求职意向的设置功能,设置求职意向会导致Jobs.vue里岗位列表刷新
|
||
// store.dispatch('loadJobIntention')
|
||
loadMemberStatus()
|
||
}
|
||
})
|
||
|
||
/** 注销账号 — 打开注销账号弹窗 */
|
||
const handleDeleteAccount = () => {
|
||
showDeleteAccount.value = true
|
||
}
|
||
|
||
/** 注销完成回调 — 关闭设置弹窗 */
|
||
const onAccountDeleted = () => {
|
||
emit('update:modelValue', false)
|
||
}
|
||
|
||
/** 管理订阅 */
|
||
const handleManageSubscription = () => {
|
||
ElMessage.info('管理订阅功能开发中')
|
||
}
|
||
|
||
/** 查看会员条款 */
|
||
const handleMemberTerms = () => {
|
||
ElMessage.info('会员条款页面开发中')
|
||
}
|
||
|
||
/** 刷新订阅状态 */
|
||
const handleRefreshStatus = () => {
|
||
ElMessage.success('状态已更新')
|
||
}
|
||
|
||
/** 联系客服 */
|
||
const handleContactUs = () => {
|
||
ElMessage.info('联系客服功能开发中')
|
||
}
|
||
|
||
/** 退出登录 — 调用接口,后端会清除 Cookie,关闭弹窗,跳转 jobs 页 */
|
||
const handleLogout = async () => {
|
||
try {
|
||
const res = await logout()
|
||
if (res.code === '0') {
|
||
ElMessage.success('已退出登录')
|
||
}
|
||
} catch {
|
||
// 即使接口失败也关闭弹窗
|
||
}
|
||
store.commit('SET_AUTHENTICATED', false)
|
||
showLogout.value = false
|
||
emit('update:modelValue', false)
|
||
router.push('/')
|
||
}
|
||
</script>
|