首页和设置会员页,购买页的协议

This commit is contained in:
xuxin
2026-05-28 21:22:30 +08:00
parent 0e6fde08c7
commit a1633978c5
14 changed files with 455 additions and 64 deletions
+1
View File
@@ -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']
+27
View File
@@ -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
View File
@@ -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;
}
}
}
}
+6 -1
View File
@@ -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;
}
}
} }
} }
+2 -2
View File
@@ -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;
+1
View File
@@ -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';
+13 -1
View File
@@ -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>
+56 -52
View File
@@ -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,
+8 -3
View File
@@ -52,8 +52,13 @@
:class="{ 'side-nav__message-list-item--active': selectedMessageIdx === idx }" :class="{ 'side-nav__message-list-item--active': selectedMessageIdx === idx }"
@click="selectedMessageIdx = idx" @click="selectedMessageIdx = idx"
> >
<span class="side-nav__message-list-title">{{ msg.title }}</span> <div class="dflex wp100 aliite-e fs14">
<span v-if="!msg.read" class="side-nav__message-unread-dot"></span> <div class="">
<span class="side-nav__message-list-title">{{ msg.title }}</span>
<span v-if="!msg.read" class="side-nav__message-unread-dot"></span>
</div>
<div class="fs10 color-8 tar">{{timestampToLocalDateTime(currentMessage.createTime, 'returnDay')}}</div>
</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
View File
@@ -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
View File
@@ -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>