Files
offerpai_web/src/components/SettingsDialog.vue
T

510 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>