首页和设置会员页,购买页的协议
This commit is contained in:
Vendored
+1
@@ -21,6 +21,7 @@ declare module 'vue' {
|
|||||||
AgentSettingsPanel: typeof import('./src/components/AgentSettingsPanel.vue')['default']
|
AgentSettingsPanel: typeof import('./src/components/AgentSettingsPanel.vue')['default']
|
||||||
AgentSetupWizard: typeof import('./src/components/AgentSetupWizard.vue')['default']
|
AgentSetupWizard: typeof import('./src/components/AgentSetupWizard.vue')['default']
|
||||||
AgentTaskListDropdown: typeof import('./src/components/AgentTaskListDropdown.vue')['default']
|
AgentTaskListDropdown: typeof import('./src/components/AgentTaskListDropdown.vue')['default']
|
||||||
|
AgreementPreviewDialog: typeof import('./src/components/tools/AgreementPreviewDialog.vue')['default']
|
||||||
AiChat: typeof import('./src/components/AiChat.vue')['default']
|
AiChat: typeof import('./src/components/AiChat.vue')['default']
|
||||||
AiThinkingIndicator: typeof import('./src/components/tools/AiThinkingIndicator.vue')['default']
|
AiThinkingIndicator: typeof import('./src/components/tools/AiThinkingIndicator.vue')['default']
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
|
|||||||
@@ -118,3 +118,30 @@ export function uploadFileToOss(file: File, pathEnum: OssPathEnum = 'ResumeFile'
|
|||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 协议查询相关 ====================
|
||||||
|
|
||||||
|
/** 协议数据结构 */
|
||||||
|
export interface AgreementDto {
|
||||||
|
/** 协议表ID */
|
||||||
|
id?: number
|
||||||
|
/** 协议码,唯一标识一类协议 */
|
||||||
|
agreementCode?: string
|
||||||
|
/** 修改版本 */
|
||||||
|
version?: number
|
||||||
|
/** 协议名称 */
|
||||||
|
agreementName?: string
|
||||||
|
/** 协议内容(富文本/Markdown) */
|
||||||
|
content?: string
|
||||||
|
/** 状态 1=启用 0=禁用 */
|
||||||
|
status?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据协议码查询协议内容
|
||||||
|
* GET /public/agreement?code=xxx
|
||||||
|
* @param code 协议码
|
||||||
|
*/
|
||||||
|
export function fetchAgreement(code: string) {
|
||||||
|
return request.get<any, ApiResult<AgreementDto>>('/public/agreement', { params: { code } })
|
||||||
|
}
|
||||||
|
|||||||
+2
-2
@@ -16,8 +16,8 @@ export interface MessageDto {
|
|||||||
bizId: number
|
bizId: number
|
||||||
/** 是否已读 */
|
/** 是否已读 */
|
||||||
read: boolean
|
read: boolean
|
||||||
/** 创建时间 */
|
/** 创建时间(毫秒时间戳) */
|
||||||
createTime: { seconds: number; nanos: number }
|
createTime: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 分页响应结构 */
|
/** 分页响应结构 */
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
@use '../variables' as *;
|
||||||
|
|
||||||
|
// 协议预览弹窗样式
|
||||||
|
.agreement-preview-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 2000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-preview-dialog {
|
||||||
|
width: 7rem;
|
||||||
|
max-width: 90vw;
|
||||||
|
height: 80vh;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 0.12rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
// 顶部标题栏
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.16rem 0.24rem;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 0.18rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-dark;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__close {
|
||||||
|
font-size: 0.18rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.04rem;
|
||||||
|
transition: color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内容区域
|
||||||
|
&__body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.24rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.4rem 0;
|
||||||
|
color: #999;
|
||||||
|
font-size: 0.14rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Markdown 渲染内容样式
|
||||||
|
&__content {
|
||||||
|
font-size: 0.13rem;
|
||||||
|
color: #555;
|
||||||
|
line-height: 1.8;
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
color: $text-dark;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0.14rem 0 0.08rem;
|
||||||
|
}
|
||||||
|
h1 { font-size: 0.2rem; }
|
||||||
|
h2 { font-size: 0.17rem; }
|
||||||
|
h3 { font-size: 0.15rem; }
|
||||||
|
h4 { font-size: 0.14rem; }
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 0.08rem;
|
||||||
|
text-align: justify;
|
||||||
|
&:last-child { margin-bottom: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
ul, ol {
|
||||||
|
padding-left: 0.2rem;
|
||||||
|
margin: 0.06rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 0.04rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #2563eb;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-left: 3px solid #cbd5e1;
|
||||||
|
padding-left: 0.1rem;
|
||||||
|
margin: 0.08rem 0;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0.1rem 0;
|
||||||
|
font-size: 0.12rem;
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
padding: 0.06rem 0.1rem;
|
||||||
|
text-align: left;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: #f8fafc;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:nth-child(even) {
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
margin: 0.1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: #f1f5f9;
|
||||||
|
padding: 0.01rem 0.04rem;
|
||||||
|
border-radius: 0.03rem;
|
||||||
|
font-size: 0.12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: #1e293b;
|
||||||
|
color: #e2e8f0;
|
||||||
|
padding: 0.1rem 0.12rem;
|
||||||
|
border-radius: 0.06rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 0.08rem 0;
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -163,14 +163,19 @@
|
|||||||
|
|
||||||
th, td {
|
th, td {
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
padding: 0.04rem 0.08rem;
|
padding: 0.06rem 0.1rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tr:nth-child(even) {
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
|
|||||||
@@ -487,5 +487,66 @@
|
|||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
text-align: justify;
|
text-align: justify;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Markdown 渲染额外样式
|
||||||
|
h1, h2, h3, h5, h6 {
|
||||||
|
color: $text-dark;
|
||||||
|
margin: 0.14rem 0 0.08rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
h1 { font-size: 0.2rem; }
|
||||||
|
h2 { font-size: 0.17rem; }
|
||||||
|
h3 { font-size: 0.15rem; }
|
||||||
|
|
||||||
|
ul, ol {
|
||||||
|
padding-left: 0.2rem;
|
||||||
|
margin: 0.06rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 0.04rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #2563eb;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-left: 3px solid #cbd5e1;
|
||||||
|
padding-left: 0.1rem;
|
||||||
|
margin: 0.08rem 0;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0.1rem 0;
|
||||||
|
font-size: 0.12rem;
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
padding: 0.06rem 0.1rem;
|
||||||
|
text-align: left;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: #f8fafc;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:nth-child(even) {
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,7 +184,7 @@
|
|||||||
|
|
||||||
// 左侧消息列表
|
// 左侧消息列表
|
||||||
.side-nav__message-list {
|
.side-nav__message-list {
|
||||||
width: 2rem;
|
width: 2.1rem;
|
||||||
border-right: 1px solid $border-color;
|
border-right: 1px solid $border-color;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -193,7 +193,7 @@
|
|||||||
.side-nav__message-list-item {
|
.side-nav__message-list-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.12rem 0.14rem;
|
padding: 0.12rem 0.10rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-bottom: 1px solid $border-color;
|
border-bottom: 1px solid $border-color;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
@use './components/profile-welcome-dialog.scss';
|
@use './components/profile-welcome-dialog.scss';
|
||||||
@use './components/settings-invite-dialog.scss';
|
@use './components/settings-invite-dialog.scss';
|
||||||
@use './components/resume-upload-dialog.scss';
|
@use './components/resume-upload-dialog.scss';
|
||||||
|
@use './components/agreement-preview-dialog.scss';
|
||||||
|
|
||||||
// 全局样式(优先级最高)
|
// 全局样式(优先级最高)
|
||||||
@use './auto.scss';
|
@use './auto.scss';
|
||||||
|
|||||||
@@ -237,7 +237,7 @@
|
|||||||
<input type="checkbox" v-model="agreeProtocol" />
|
<input type="checkbox" v-model="agreeProtocol" />
|
||||||
<span class="member-dialog__order-checkbox-mark"></span>
|
<span class="member-dialog__order-checkbox-mark"></span>
|
||||||
</label>
|
</label>
|
||||||
<span>我已阅读并同意 <a href="javascript:;">《会员服务协议》</a> <a href="javascript:;">《自动续费协议》</a></span>
|
<span>我已阅读并同意 <a href="javascript:;" @click.prevent="openMemberAgreement">《会员服务协议》</a> </span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 立即开启按钮 -->
|
<!-- 立即开启按钮 -->
|
||||||
<button
|
<button
|
||||||
@@ -355,6 +355,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 协议预览弹窗 -->
|
||||||
|
<AgreementPreviewDialog v-model="showAgreementDialog" code="ae8065i3" />
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -363,6 +366,7 @@ import { ref, computed, watch, reactive, onMounted } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useStore } from 'vuex'
|
import { useStore } from 'vuex'
|
||||||
import { fetchMemberProductList, createMemberOrder, fetchOrderDetail, type MemberProduct } from '@/api/member'
|
import { fetchMemberProductList, createMemberOrder, fetchOrderDetail, type MemberProduct } from '@/api/member'
|
||||||
|
import AgreementPreviewDialog from '@/components/tools/AgreementPreviewDialog.vue'
|
||||||
|
|
||||||
/** 组件 Props — 控制弹窗显示/隐藏 */
|
/** 组件 Props — 控制弹窗显示/隐藏 */
|
||||||
const props = defineProps<{ modelValue: boolean }>()
|
const props = defineProps<{ modelValue: boolean }>()
|
||||||
@@ -664,4 +668,12 @@ function handleViewMemberBenefits() {
|
|||||||
store.commit('SET_SETTINGS_TAB', 'member')
|
store.commit('SET_SETTINGS_TAB', 'member')
|
||||||
store.commit('SET_SHOW_SETTINGS', true)
|
store.commit('SET_SHOW_SETTINGS', true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 协议预览弹窗显示状态 */
|
||||||
|
const showAgreementDialog = ref(false)
|
||||||
|
|
||||||
|
/** 打开会员服务协议 */
|
||||||
|
function openMemberAgreement() {
|
||||||
|
showAgreementDialog.value = true
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
{{ memberStatus.isMember ? '已开通' : '未开通' }}
|
{{ memberStatus.isMember ? '已开通' : '未开通' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="settings-dialog__member-terms" @click="handleMemberTerms">会员条款</span>
|
<span class="settings-dialog__member-terms" @click="openAgreementDialog('ae8065i3')">会员条款</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-dialog__member-info-row">
|
<div class="settings-dialog__member-info-row">
|
||||||
<!-- 已开通:显示到期时间和剩余天数 -->
|
<!-- 已开通:显示到期时间和剩余天数 -->
|
||||||
@@ -167,60 +167,14 @@
|
|||||||
<!-- </div>-->
|
<!-- </div>-->
|
||||||
<!-- </div>-->
|
<!-- </div>-->
|
||||||
</template>
|
</template>
|
||||||
<!-- Tab: 用户隐私协议 — 长文本,可滚动查看 -->
|
<!-- Tab: 用户隐私协议 — 从接口获取并用 markdown-it 渲染 -->
|
||||||
<template v-if="activeTab === 'privacy'">
|
<template v-if="activeTab === 'privacy'">
|
||||||
<h2 class="settings-dialog__content-title">用户隐私协议</h2>
|
<h2 class="settings-dialog__content-title">用户隐私协议</h2>
|
||||||
<div class="settings-dialog__privacy-content">
|
<div class="settings-dialog__privacy-content">
|
||||||
<div class="settings-dialog__privacy-section">
|
<!-- 加载中 -->
|
||||||
<h4>一、引言</h4>
|
<div v-if="privacyLoading" style="text-align: center; padding: 0.4rem 0; color: #999;">加载中...</div>
|
||||||
<p>欢迎使用 Offer派(以下简称"本平台"或"我们")。我们深知个人信息对您的重要性,并会尽全力保护您的个人信息安全。我们致力于维持您对我们的信任,恪守以下原则保护您的个人信息:权责一致原则、目的明确原则、选择同意原则、最少够用原则、确保安全原则、主体参与原则、公开透明原则等。同时,我们承诺将按照业界成熟的安全标准,采取相应的安全保护措施来保护您的个人信息。请您在使用本平台服务前,仔细阅读并了解本隐私政策。</p>
|
<!-- 渲染协议内容 -->
|
||||||
</div>
|
<div v-else class="settings-dialog__privacy-section markdown-body" v-html="privacyHtml"></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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -244,6 +198,9 @@
|
|||||||
|
|
||||||
<!-- 邀请注册送会员弹窗 -->
|
<!-- 邀请注册送会员弹窗 -->
|
||||||
<SettingsInviteDialog v-model="showInviteDialog" />
|
<SettingsInviteDialog v-model="showInviteDialog" />
|
||||||
|
|
||||||
|
<!-- 协议预览弹窗 -->
|
||||||
|
<AgreementPreviewDialog v-model="showAgreementDialog" :code="currentAgreementCode" />
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -253,6 +210,7 @@ import { useRouter } from 'vue-router'
|
|||||||
import { useStore } from 'vuex'
|
import { useStore } from 'vuex'
|
||||||
import { logout } from '@/api/auth'
|
import { logout } from '@/api/auth'
|
||||||
import { fetchMemberStatus, type MemberStatus } from '@/api/member'
|
import { fetchMemberStatus, type MemberStatus } from '@/api/member'
|
||||||
|
import { fetchAgreement } from '@/api/common'
|
||||||
import { timestampToLocalDateTime, timestampDiffDays } from '@/utils/time'
|
import { timestampToLocalDateTime, timestampDiffDays } from '@/utils/time'
|
||||||
import JobGoalDialog from './JobGoalDialog.vue'
|
import JobGoalDialog from './JobGoalDialog.vue'
|
||||||
import { resolveRegionName } from '@/utils/region'
|
import { resolveRegionName } from '@/utils/region'
|
||||||
@@ -260,6 +218,12 @@ import { resolveIndustryName } from '@/utils/industry'
|
|||||||
import { resolveJobCategoryName } from '@/utils/jobCategory'
|
import { resolveJobCategoryName } from '@/utils/jobCategory'
|
||||||
import SettingsDeleteAccountDialog from './SettingsDeleteAccountDialog.vue'
|
import SettingsDeleteAccountDialog from './SettingsDeleteAccountDialog.vue'
|
||||||
import SettingsInviteDialog from './SettingsInviteDialog.vue'
|
import SettingsInviteDialog from './SettingsInviteDialog.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 */
|
/** 组件 Props — 控制弹窗显示/隐藏,可指定初始 Tab */
|
||||||
const props = defineProps<{ modelValue: boolean; initialTab?: string }>()
|
const props = defineProps<{ modelValue: boolean; initialTab?: string }>()
|
||||||
@@ -292,9 +256,15 @@ watch(() => props.modelValue, (val) => {
|
|||||||
document.body.style.overflow = val ? 'hidden' : ''
|
document.body.style.overflow = val ? 'hidden' : ''
|
||||||
if (val && props.initialTab) {
|
if (val && props.initialTab) {
|
||||||
activeTab.value = props.initialTab
|
activeTab.value = props.initialTab
|
||||||
|
if (props.initialTab === 'privacy') loadPrivacyAgreement()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 监听 Tab 切换 — 切到隐私协议时加载内容 */
|
||||||
|
watch(activeTab, (tab) => {
|
||||||
|
if (tab === 'privacy') loadPrivacyAgreement()
|
||||||
|
})
|
||||||
|
|
||||||
/** 岗位更新提醒的配置项 */
|
/** 岗位更新提醒的配置项 */
|
||||||
const reminders = reactive({
|
const reminders = reactive({
|
||||||
instant: true, // 是否开启即时岗位提醒
|
instant: true, // 是否开启即时岗位提醒
|
||||||
@@ -310,6 +280,40 @@ const showDeleteAccount = ref(false)
|
|||||||
/** 邀请注册弹窗显示状态 */
|
/** 邀请注册弹窗显示状态 */
|
||||||
const showInviteDialog = ref(false)
|
const showInviteDialog = ref(false)
|
||||||
|
|
||||||
|
/** 协议预览弹窗显示状态 */
|
||||||
|
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>({
|
const memberStatus = reactive<MemberStatus>({
|
||||||
isMember: false,
|
isMember: false,
|
||||||
|
|||||||
@@ -52,9 +52,14 @@
|
|||||||
:class="{ 'side-nav__message-list-item--active': selectedMessageIdx === idx }"
|
:class="{ 'side-nav__message-list-item--active': selectedMessageIdx === idx }"
|
||||||
@click="selectedMessageIdx = idx"
|
@click="selectedMessageIdx = idx"
|
||||||
>
|
>
|
||||||
|
<div class="dflex wp100 aliite-e fs14">
|
||||||
|
<div class="">
|
||||||
<span class="side-nav__message-list-title">{{ msg.title }}</span>
|
<span class="side-nav__message-list-title">{{ msg.title }}</span>
|
||||||
<span v-if="!msg.read" class="side-nav__message-unread-dot"></span>
|
<span v-if="!msg.read" class="side-nav__message-unread-dot"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="fs10 color-8 tar">{{timestampToLocalDateTime(currentMessage.createTime, 'returnDay')}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- 加载中提示 -->
|
<!-- 加载中提示 -->
|
||||||
<div v-if="messageLoading" class="side-nav__message-list-loading">加载中...</div>
|
<div v-if="messageLoading" class="side-nav__message-list-loading">加载中...</div>
|
||||||
<!-- 无消息提示 -->
|
<!-- 无消息提示 -->
|
||||||
@@ -66,7 +71,7 @@
|
|||||||
<div class="side-nav__message-detail-title">{{ currentMessage.title }}</div>
|
<div class="side-nav__message-detail-title">{{ currentMessage.title }}</div>
|
||||||
<div class="dflex aliite-e">
|
<div class="dflex aliite-e">
|
||||||
<div class="side-nav__message-detail-content">{{ currentMessage.content }}</div>
|
<div class="side-nav__message-detail-content">{{ currentMessage.content }}</div>
|
||||||
<div class="fs12 color-8 w200 tar">{{timestampToLocalDateTime(currentMessage.createTime, 'returnSecond')}}</div>
|
<div class="fs12 color-8 w140 tar">{{timestampToLocalDateTime(currentMessage.createTime, 'returnSecond')}}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="side-nav__message-detail-empty">请选择一条消息查看</div>
|
<div v-else class="side-nav__message-detail-empty">请选择一条消息查看</div>
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 协议预览弹窗 — 通过 Teleport 挂载到 body -->
|
||||||
|
<Teleport to="body">
|
||||||
|
<!-- 遮罩层 -->
|
||||||
|
<div v-if="modelValue" class="agreement-preview-overlay" @click="$emit('update:modelValue', false)">
|
||||||
|
<!-- 弹窗主体 -->
|
||||||
|
<div class="agreement-preview-dialog" @click.stop>
|
||||||
|
<!-- 顶部标题栏 -->
|
||||||
|
<div class="agreement-preview-dialog__header">
|
||||||
|
<h2 class="agreement-preview-dialog__title">{{ agreementName || '协议内容' }}</h2>
|
||||||
|
<span class="agreement-preview-dialog__close" @click="$emit('update:modelValue', false)">✕</span>
|
||||||
|
</div>
|
||||||
|
<!-- 内容区域 — 可滚动 -->
|
||||||
|
<div class="agreement-preview-dialog__body">
|
||||||
|
<!-- 加载中 -->
|
||||||
|
<div v-if="loading" class="agreement-preview-dialog__loading">加载中...</div>
|
||||||
|
<!-- 渲染协议 Markdown 内容 -->
|
||||||
|
<div v-else class="agreement-preview-dialog__content" v-html="contentHtml"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { fetchAgreement } from '@/api/common'
|
||||||
|
// @ts-ignore
|
||||||
|
import markdownit from 'markdown-it'
|
||||||
|
|
||||||
|
/** markdown-it 实例 */
|
||||||
|
const md = markdownit({ html: false, breaks: true, linkify: true })
|
||||||
|
|
||||||
|
/** 组件属性 */
|
||||||
|
const props = defineProps<{
|
||||||
|
/** 控制弹窗显示/隐藏 */
|
||||||
|
modelValue: boolean
|
||||||
|
/** 协议码 */
|
||||||
|
code: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
/** 组件事件 */
|
||||||
|
defineEmits<{ (e: 'update:modelValue', value: boolean): void }>()
|
||||||
|
|
||||||
|
/** 协议名称 */
|
||||||
|
const agreementName = ref('')
|
||||||
|
/** 渲染后的 HTML 内容 */
|
||||||
|
const contentHtml = ref('')
|
||||||
|
/** 加载状态 */
|
||||||
|
const loading = ref(false)
|
||||||
|
/** 是否已加载过(避免重复请求) */
|
||||||
|
const loaded = ref(false)
|
||||||
|
|
||||||
|
/** 加载协议内容 */
|
||||||
|
async function loadAgreement() {
|
||||||
|
if (loaded.value) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await fetchAgreement(props.code)
|
||||||
|
if (res.data?.content) {
|
||||||
|
agreementName.value = res.data.agreementName || '协议内容'
|
||||||
|
contentHtml.value = md.render(res.data.content)
|
||||||
|
loaded.value = true
|
||||||
|
} else {
|
||||||
|
contentHtml.value = '<p>暂无协议内容</p>'
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
contentHtml.value = '<p>加载失败,请稍后重试</p>'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 监听弹窗打开 — 触发加载 */
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
if (val) {
|
||||||
|
loadAgreement()
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 监听 code 变化 — 重置已加载状态 */
|
||||||
|
watch(() => props.code, () => {
|
||||||
|
loaded.value = false
|
||||||
|
contentHtml.value = ''
|
||||||
|
agreementName.value = ''
|
||||||
|
})
|
||||||
|
</script>
|
||||||
+1
-1
@@ -28,7 +28,7 @@ export function timestampToLocalDateTime(timestamp: number | null | undefined, p
|
|||||||
const m = String(date.getMonth() + 1).padStart(2, '0')
|
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
if (precision === 'returnMonth') return `${y}-${m}`
|
if (precision === 'returnMonth') return `${y}-${m}`
|
||||||
const d = String(date.getDate()).padStart(2, '0')
|
const d = String(date.getDate()).padStart(2, '0')
|
||||||
if (precision === 'returnDay') return `${y}-${m}-${d}`
|
if (precision === 'returnDay') return `${y}/${m}/${d}`
|
||||||
const h = String(date.getHours()).padStart(2, '0')
|
const h = String(date.getHours()).padStart(2, '0')
|
||||||
if (precision === 'returnHour') return `${y}-${m}-${d} ${h}`
|
if (precision === 'returnHour') return `${y}-${m}-${d} ${h}`
|
||||||
const min = String(date.getMinutes()).padStart(2, '0')
|
const min = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
|||||||
+17
-2
@@ -431,8 +431,8 @@
|
|||||||
<div class="home-footer__col">
|
<div class="home-footer__col">
|
||||||
<h5>其他信息</h5>
|
<h5>其他信息</h5>
|
||||||
<ul>
|
<ul>
|
||||||
<li>隐私协议</li>
|
<li @click="openAgreement('hf8375i8')" style="cursor: pointer;">隐私协议</li>
|
||||||
<li>服务条款</li>
|
<li @click="openAgreement('ae8065i3')" style="cursor: pointer;">会员服务协议</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -440,6 +440,9 @@
|
|||||||
<p>©2016-2026 - 广州油梨信息科技有限公司 版权所有</p>
|
<p>©2016-2026 - 广州油梨信息科技有限公司 版权所有</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<!-- 隐私协议预览弹窗 -->
|
||||||
|
<AgreementPreviewDialog v-model="showPrivacyDialog" :code="agreementCode" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -451,6 +454,7 @@ import avatarImg from '@/assets/images/home/avatar-temporary.png'
|
|||||||
import IndustrySelector from '@/components/tools/IndustrySelector.vue'
|
import IndustrySelector from '@/components/tools/IndustrySelector.vue'
|
||||||
import RegionSelector from '@/components/tools/RegionSelector.vue'
|
import RegionSelector from '@/components/tools/RegionSelector.vue'
|
||||||
import JobCategorySelector from '@/components/tools/JobCategorySelector.vue'
|
import JobCategorySelector from '@/components/tools/JobCategorySelector.vue'
|
||||||
|
import AgreementPreviewDialog from '@/components/tools/AgreementPreviewDialog.vue'
|
||||||
|
|
||||||
/** 路由实例 */
|
/** 路由实例 */
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -570,4 +574,15 @@ onMounted(() => {
|
|||||||
function handleLoginClick() {
|
function handleLoginClick() {
|
||||||
store.dispatch('openLogin', '/jobs')
|
store.dispatch('openLogin', '/jobs')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 隐私协议弹窗显示状态 */
|
||||||
|
const showPrivacyDialog = ref(false)
|
||||||
|
/** 当前要预览的协议码 */
|
||||||
|
const agreementCode = ref('')
|
||||||
|
|
||||||
|
/** 打开协议预览弹窗 */
|
||||||
|
function openAgreement(code: string) {
|
||||||
|
agreementCode.value = code
|
||||||
|
showPrivacyDialog.value = true
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user