Files
offerpai_web/src/components/SettingsDialog.vue
T

424 lines
27 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 class="settings-dialog__nav-icon">{{ tab.icon }}</span>
<span>{{ tab.label }}</span>
</div>
</div>
<!-- 底部操作区 隐私协议 + 退出登录 -->
<div class="settings-dialog__bottom-actions">
<!-- 用户隐私协议按钮 点击切换到隐私协议 tab -->
<button
class="settings-dialog__bottom-btn"
:class="{ 'settings-dialog__bottom-btn--active': activeTab === 'privacy' }"
@click="activeTab = 'privacy'"
>
用户隐私协议
</button>
<!-- 退出登录按钮 点击弹出确认弹窗 -->
<button class="settings-dialog__bottom-btn settings-dialog__bottom-btn--danger" @click="showLogout = true">
退出登录
</button>
</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 class="settings-dialog__danger-title">注销我的账号</div>
<div class="settings-dialog__danger-row">
<p class="settings-dialog__danger-desc">永久删除你的账号及所有相关数据</p>
<button class="settings-dialog__danger-btn" @click="handleDeleteAccount">注销账号</button>
</div>
</div>
</template>
<!-- Tab: 会员 显示当前会员信息订阅管理 -->
<template v-if="activeTab === 'member'">
<h2 class="settings-dialog__content-title">会员</h2>
<div class="settings-dialog__section-label" style="margin-bottom: 0.12rem;">当前会员</div>
<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"
: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 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>
</div>
</div>
<div class="settings-dialog__member-issue">
<!-- <div class="settings-dialog__member-issue-title">订阅状态异常</div>-->
<!-- <p class="settings-dialog__member-issue-desc">-->
<!-- 如果你已经和完成了付款或更改了订阅但是没有看到最新状态你可以尝试更新状态或联系我们获取帮助-->
<!-- </p>-->
<!-- <div class="settings-dialog__member-issue-actions">-->
<!-- <button class="settings-dialog__member-issue-btn" @click="handleRefreshStatus">更新状态</button>-->
<!-- <button class="settings-dialog__member-issue-btn" @click="handleContactUs">联系我们</button>-->
<!-- </div>-->
</div>
</template>
<!-- Tab: 岗位更新提醒 目标岗位即时提醒开关提醒频率 -->
<template v-if="activeTab === 'reminder'">
<!-- <h2 class="settings-dialog__content-title">岗位更新提醒</h2>-->
<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 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 === 'privacy'">
<h2 class="settings-dialog__content-title">用户隐私协议</h2>
<div class="settings-dialog__privacy-content">
<div class="settings-dialog__privacy-section">
<h4>引言</h4>
<p>欢迎使用 Offer派以下简称"本平台""我们"我们深知个人信息对您的重要性并会尽全力保护您的个人信息安全我们致力于维持您对我们的信任恪守以下原则保护您的个人信息权责一致原则目的明确原则选择同意原则最少够用原则确保安全原则主体参与原则公开透明原则等同时我们承诺将按照业界成熟的安全标准采取相应的安全保护措施来保护您的个人信息请您在使用本平台服务前仔细阅读并了解本隐私政策</p>
</div>
<div class="settings-dialog__privacy-section">
<h4>我们如何收集和使用您的个人信息</h4>
<p>个人信息是指以电子或者其他方式记录的能够单独或者与其他信息结合识别特定自然人身份或者反映特定自然人活动情况的各种信息我们仅会出于本政策所述的以下目的收集和使用您的个人信息</p>
<p>1. 注册与登录当您注册本平台账号时我们会收集您的手机号码用于创建账号和身份验证您也可以选择填写昵称头像等个人资料来完善您的账户信息手机号码属于敏感信息收集此类信息是为了满足相关法律法规的网络实名制要求如果您不提供手机号码将无法使用本平台的服务</p>
<p>2. 简历管理当您使用简历管理功能时我们会收集您主动填写的简历信息包括但不限于姓名性别出生日期教育经历工作经历项目经验技能特长求职意向期望薪资期望工作地点等这些信息将用于为您提供精准的岗位推荐服务您可以随时在个人中心修改或删除这些信息</p>
<p>3. 岗位推荐与搜索当您使用岗位搜索和推荐功能时我们会收集您的搜索关键词浏览记录收藏记录投递记录等行为数据以便为您提供更加精准和个性化的岗位推荐我们也会根据您的求职意向和简历信息通过算法模型为您匹配合适的职位</p>
<p>4. AI 助手服务当您使用 AI 助手功能时我们会收集您与 AI 的对话内容用于提供智能问答简历优化建议面试辅导等服务对话内容将被加密存储并仅用于改善服务质量我们不会将您的对话内容用于其他商业目的</p>
<p>5. 消息通知为了及时向您推送岗位更新申请状态变更等重要信息我们可能会收集您的设备标识符推送令牌等信息用于实现消息推送功能您可以在设置中随时关闭消息推送</p>
</div>
<div class="settings-dialog__privacy-section">
<h4>我们如何共享转让公开披露您的个人信息</h4>
<p>1. 共享我们不会与任何公司组织和个人共享您的个人信息但以下情况除外1在获取明确同意的情况下共享获得您的明确同意后我们会与其他方共享您的个人信息2我们可能会根据法律法规规定或按政府主管部门的强制性要求对外共享您的个人信息3与授权合作伙伴共享仅为实现本隐私政策中声明的目的我们的某些服务将由授权合作伙伴提供我们可能会与合作伙伴共享您的某些个人信息以提供更好的客户服务和用户体验我们仅会出于合法正当必要特定明确的目的共享您的个人信息并且只会共享提供服务所必要的个人信息</p>
<p>2. 转让我们不会将您的个人信息转让给任何公司组织和个人但以下情况除外1在获取明确同意的情况下转让获得您的明确同意后我们会向其他方转让您的个人信息2在涉及合并收购或破产清算时如涉及到个人信息转让我们会在要求新的持有您个人信息的公司组织继续受此隐私政策的约束否则我们将要求该公司组织重新向您征求授权同意</p>
<p>3. 公开披露我们仅会在以下情况下公开披露您的个人信息1获得您明确同意后2基于法律的披露在法律法律程序诉讼或政府主管部门强制性要求的情况下我们可能会公开披露您的个人信息</p>
</div>
<div class="settings-dialog__privacy-section">
<h4>我们如何保护您的个人信息</h4>
<p>1. 我们已使用符合业界标准的安全防护措施保护您提供的个人信息防止数据遭到未经授权的访问公开披露使用修改损坏或丢失我们会采取一切合理可行的措施保护您的个人信息例如在您的浏览器与服务之间交换数据时受 SSL 加密保护我们同时对网站提供 HTTPS 安全浏览方式我们会使用加密技术确保数据的保密性我们会使用受信赖的保护机制防止数据遭到恶意攻击我们会部署访问控制机制确保只有授权人员才可访问个人信息以及我们会举办安全和隐私保护培训课程加强员工对于保护个人信息重要性的认识</p>
<p>2. 我们会采取一切合理可行的措施确保未收集无关的个人信息我们只会在达成本政策所述目的所需的期限内保留您的个人信息除非需要延长保留期或受到法律的允许</p>
<p>3. 互联网并非绝对安全的环境而且电子邮件即时通讯及与其他用户的交流方式并未加密我们强烈建议您不要通过此类方式发送个人信息请使用复杂密码协助我们保证您的账号安全</p>
<p>4. 互联网环境并非百分之百安全我们将尽力确保或担保您发送给我们的任何信息的安全性如果我们的物理技术或管理防护设施遭到破坏导致信息被非授权访问公开披露篡改或毁坏导致您的合法权益受损我们将承担相应的法律责任</p>
<p>5. 在不幸发生个人信息安全事件后我们将按照法律法规的要求及时向您告知安全事件的基本情况和可能的影响我们已采取或将要采取的处置措施您可自主防范和降低风险的建议对您的补救措施等我们将及时将事件相关情况以邮件信函电话推送通知等方式告知您难以逐一告知个人信息主体时我们会采取合理有效的方式发布公告</p>
</div>
<div class="settings-dialog__privacy-section">
<h4>您的权利</h4>
<p>按照中国相关的法律法规标准以及其他国家地区的通行做法我们保障您对自己的个人信息行使以下权利</p>
<p>1. 访问您的个人信息您有权访问您的个人信息法律法规规定的例外情况除外如果您想行使数据访问权可以通过以下方式自行访问登录本平台进入"个人资料""简历管理"页面即可查看您的个人信息</p>
<p>2. 更正您的个人信息当您发现我们处理的关于您的个人信息有错误时您有权要求我们做出更正您可以通过上述访问方式提出更正申请</p>
<p>3. 删除您的个人信息在以下情形中您可以向我们提出删除个人信息的请求1如果我们处理个人信息的行为违反法律法规2如果我们收集使用您的个人信息却未征得您的同意3如果我们处理个人信息的行为违反了与您的约定4如果您不再使用我们的产品或服务或您注销了账号5如果我们不再为您提供产品或服务</p>
<p>4. 注销账户您随时可注销此前注册的账户您可以通过"设置 - 账号与安全 - 注销账号"进行操作在注销账户之后我们将停止为您提供产品或服务并依据您的要求删除您的个人信息法律法规另有规定的除外</p>
<p>5. 改变您授权同意的范围每个业务功能需要一些基本的个人信息才能得以完成对于额外收集的个人信息的收集和使用您可以随时给予或收回您的授权同意您可以通过关闭相应功能的方式来撤回授权当您收回同意后我们将不再处理相应的个人信息但您收回同意的决定不会影响此前基于您的授权而开展的个人信息处理</p>
</div>
<div class="settings-dialog__privacy-section">
<h4>我们如何处理未成年人的个人信息</h4>
<p>我们的产品和服务主要面向成年人如果没有父母或监护人的同意未成年人不应创建自己的用户账户如果我们发现在未事先获得可证实的父母或法定监护人同意的情况下收集了未成年人的个人信息则会设法尽快删除相关数据对于经父母或法定监护人同意而收集未成年人个人信息的情况我们只会在受到法律允许父母或监护人明确同意或者保护未成年人所必要的情况下使用或公开披露此信息</p>
</div>
<div class="settings-dialog__privacy-section">
<h4>本隐私政策如何更新</h4>
<p>我们可能适时会对本隐私政策进行调整或变更本隐私政策的任何更新将以标注更新时间的方式公布在本平台上除法律法规或监管规定另有强制性规定外经调整或变更的内容一经通知或公布后的7日后生效如您在隐私政策调整或变更后继续使用我们提供的任一服务或访问我们相关网站的我们相信这代表您已充分阅读理解并接受修改后的隐私政策并受其约束</p>
</div>
<div class="settings-dialog__privacy-section">
<h4>如何联系我们</h4>
<p>如果您对本隐私政策有任何疑问意见或建议可以通过以下方式与我们联系发送邮件至 privacy@offerpai.com或通过本平台内的"反馈"功能联系我们一般情况下我们将在15个工作日内回复如果您对我们的回复不满意特别是我们的个人信息处理行为损害了您的合法权益您还可以向网信部门电信主管部门公安部门等监管部门进行投诉或举报或通过向被告住所地有管辖权的法院提起诉讼来寻求解决方案</p>
<p>本隐私政策的最终解释权归本平台所有</p>
<p style="margin-top: 0.16rem; color: #999;">最后更新日期2026年3月1日</p>
</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" />
<!-- 邀请注册送会员弹窗 -->
<SettingsInviteDialog v-model="showInviteDialog" />
</Teleport>
</template>
<script setup lang="ts">
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'
import { resolveJobCategoryName } from '@/utils/jobCategory'
import SettingsDeleteAccountDialog from './SettingsDeleteAccountDialog.vue'
import SettingsInviteDialog from './SettingsInviteDialog.vue'
/** 组件 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: '账号与安全', icon: '👤' },
{ key: 'member', label: '会员', icon: '🏅' },
{ key: 'reminder', label: '目标岗位设置', icon: '🔔' },
]
/** 当前选中的 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
}
})
/** 岗位更新提醒的配置项 */
const reminders = reactive({
instant: true, // 是否开启即时岗位提醒
frequency: 'unlimited', // 提醒频率:1/2/5/unlimited
})
/** 求职目标弹窗显示状态 */
const showGoalDialog = ref(false)
/** 注销账号弹窗显示状态 */
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 || []
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 store.state.jobIntention.employmentType === 1 ? '实习' : '全职'
})
/** 编辑目标岗位 — 打开求职目标弹窗 */
const handleEditTarget = () => {
showGoalDialog.value = true
}
/** 弹窗打开时加载求职意向数据和会员状态 */
watch(() => props.modelValue, (val) => {
if (val && store.state.isAuthenticated) {
store.dispatch('loadCommonData')
store.dispatch('loadJobIntention')
loadMemberStatus()
}
})
/** 注销账号 — 打开注销账号弹窗 */
const handleDeleteAccount = () => {
showDeleteAccount.value = true
}
/** 管理订阅 */
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>