左侧菜单,邀请,设置,和会员相关

This commit is contained in:
2026-07-07 23:40:20 +08:00
parent b740e0cd39
commit 851310a901
18 changed files with 2090 additions and 2169 deletions
+2
View File
@@ -34,6 +34,8 @@ declare module 'vue' {
ElRadio: typeof import('element-plus/es')['ElRadio'] ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElSelect: typeof import('element-plus/es')['ElSelect'] ElSelect: typeof import('element-plus/es')['ElSelect']
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem']
ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTooltip: typeof import('element-plus/es')['ElTooltip']
FullscreenLoading: typeof import('./src/components/FullscreenLoading.vue')['default'] FullscreenLoading: typeof import('./src/components/FullscreenLoading.vue')['default']
+4 -4
View File
@@ -21,8 +21,8 @@ export interface IndustryItem {
* GET /public/industries/tree * GET /public/industries/tree
* 返回一级二级嵌套的行业分类 * 返回一级二级嵌套的行业分类
*/ */
export function fetchIndustryTree() { export function fetchIndustryTree(params?: { includeEmpty?: boolean }) {
return request.get<any, ApiResult<IndustryItem[]>>('/public/industries/tree') return request.get<any, ApiResult<IndustryItem[]>>('/public/industries/tree', { params })
} }
// ==================== 岗位分类相关 ==================== // ==================== 岗位分类相关 ====================
@@ -55,8 +55,8 @@ export interface JobCategoryItem {
* GET /public/job-categories/tree * GET /public/job-categories/tree
* 返回一级二级三级嵌套的岗位分类 * 返回一级二级三级嵌套的岗位分类
*/ */
export function fetchJobCategoryTree() { export function fetchJobCategoryTree(params?: { includeEmpty?: boolean }) {
return request.get<any, ApiResult<JobCategoryItem[]>>('/public/job-categories/tree') return request.get<any, ApiResult<JobCategoryItem[]>>('/public/job-categories/tree', { params })
} }
// ==================== 地区分类相关 ==================== // ==================== 地区分类相关 ====================
+2
View File
@@ -36,6 +36,8 @@ export interface MemberProduct {
export interface MemberStatus { export interface MemberStatus {
/** 是否是会员 */ /** 是否是会员 */
isMember: boolean isMember: boolean
/** 会员类型 1=正式 2=试用 */
memberType?: number
/** 到期时间(毫秒时间戳) */ /** 到期时间(毫秒时间戳) */
expireTime?: number expireTime?: number
/** 首次开通时间(毫秒时间戳) */ /** 首次开通时间(毫秒时间戳) */
Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

@@ -8,7 +8,7 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
z-index: 2000; z-index: 2500;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
File diff suppressed because it is too large Load Diff
+212 -135
View File
@@ -16,7 +16,7 @@
.settings-dialog { .settings-dialog {
position: relative; position: relative;
width: 8.6rem; width: 6.4rem;
height: 6.2rem; height: 6.2rem;
background: $bg-white; background: $bg-white;
border-radius: 0.12rem; border-radius: 0.12rem;
@@ -60,35 +60,24 @@
&__nav-item { &__nav-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.08rem; padding: 0.1rem 0.16rem;
padding: 0.12rem 0.2rem; margin: 0.02rem 0.12rem;
font-size: 0.14rem; font-size: 0.14rem;
color: $text-dark; color: $text-dark;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
border-left: 0.03rem solid transparent; border-radius: 0.08rem;
font-weight: 500; font-weight: 500;
&:hover { &:hover {
background: $bg-white; background: #E2E4E9;
color: $accent;
} }
&--active { &--active {
background: $bg-white; background: #E2E4E9;
color: $accent;
border-left-color: $accent;
font-weight: 600;
} }
} }
&__nav-icon {
font-size: 0.16rem;
flex-shrink: 0;
width: 0.18rem;
text-align: center;
}
// 底部操作 // 底部操作
&__bottom-actions { &__bottom-actions {
margin-top: auto; margin-top: auto;
@@ -139,7 +128,10 @@
// ===== 账号与安全 ===== // ===== 账号与安全 =====
&__section { &__section {
margin-bottom: 0.28rem; margin-bottom: 0.16rem;
padding: 0.16rem 0.24rem;
background: #F9FAFB;
border-radius: 0.08rem;
} }
&__section-label { &__section-label {
@@ -156,7 +148,12 @@
} }
&__danger-section { &__danger-section {
margin-top: 0.32rem; padding: 0.16rem 0.24rem;
background: #F9FAFB;
border-radius: 0.08rem;
display: flex;
align-items: center;
justify-content: space-between;
} }
&__danger-title { &__danger-title {
@@ -179,161 +176,238 @@
} }
&__danger-btn { &__danger-btn {
background: $bg-main; border: none;
border: 1px solid $border-color; background: none;
border-radius: 0.06rem;
padding: 0.07rem 0.16rem; padding: 0.07rem 0.16rem;
font-size: 0.13rem; font-size: 0.13rem;
color: $text-dark; color: $danger;
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
transition: all 0.2s; transition: all 0.2s;
&:hover { &:hover {
border-color: $danger;
color: $danger;
} }
} }
// ===== 会员 ===== // ===== 会员卡片(新版) =====
&__member-card { &__mcard {
background: $bg-white; padding: 0.16rem 0.24rem;
border: 1px solid $border-color; border-radius: 0.12rem;
border-radius: 0.1rem;
padding: 0.2rem 0.24rem;
margin-bottom: 0.24rem;
}
&__member-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.1rem;
}
&__member-title-row {
display: flex;
align-items: center;
gap: 0.1rem;
}
&__member-name {
font-size: 0.16rem;
font-weight: 700;
color: $text-dark;
}
&__member-badge {
background: $accent;
color: $bg-white;
font-size: 0.11rem;
padding: 0.02rem 0.08rem;
border-radius: 0.1rem;
&--inactive {
background: #ccc;
color: $bg-white;
}
}
&__member-terms {
font-size: 0.12rem;
color: $accent;
cursor: pointer;
text-decoration: underline;
&:hover {
color: $accent-hover;
}
}
&__member-info-row {
display: flex;
align-items: center;
justify-content: space-between;
}
&__member-price {
font-size: 0.13rem;
color: #555;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.06rem; gap: 0.32rem;
span { &--formal {
margin-left: 0; background: linear-gradient(90deg, #52CAD1 0%, #52D1A7 100%);
color: $text-light; }
&--trial {
background: linear-gradient(90deg, #F59E0B 0%, #F5B70B 100%);
}
&--free {
background: #F3F4F6;
} }
} }
&__member-expire-line { &__mcard-top {
font-size: 0.13rem; display: flex;
color: #555; align-items: flex-start;
justify-content: space-between;
} }
&__member-remain-line { &__mcard-title {
font-size: 0.13rem; font-size: 0.18rem;
color: $accent; font-weight: 520;
font-weight: 600; color: #FFFFFF;
line-height: 0.24rem;
&--free {
color: #6A7282;
}
} }
&__member-manage-btn { &__mcard-remain {
background: $bg-main; display: flex;
border: 1px solid $border-color; align-items: flex-end;
border-radius: 0.06rem; gap: 0.04rem;
padding: 0.07rem 0.16rem; }
font-size: 0.13rem;
color: $text-dark; &__mcard-remain-label {
font-size: 0.14rem;
font-weight: 330;
color: #FFFFFF;
line-height: 0.19rem;
}
&__mcard-remain-days {
font-size: 0.18rem;
font-weight: 520;
color: #FFFFFF;
line-height: 0.24rem;
}
&__mcard-bottom {
display: flex;
align-items: flex-end;
justify-content: space-between;
}
&__mcard-expire {
font-size: 0.12rem;
font-weight: 330;
color: #FFFFFF;
line-height: 0.16rem;
&--free {
color: #6A7282;
}
}
&__mcard-actions {
display: flex;
align-items: flex-start;
gap: 0.08rem;
}
&__mcard-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 0.04rem 0.12rem;
font-size: 0.12rem;
font-weight: 330;
line-height: 0.16rem;
border-radius: 0.12rem;
cursor: pointer; cursor: pointer;
transition: all 0.2s; white-space: nowrap;
border: none;
transition: opacity 0.2s;
&:hover { &:hover {
border-color: $accent; opacity: 0.85;
color: $accent; }
&--outline {
background: transparent;
border: 1px solid #FFFFFF;
color: #FFFFFF;
}
&--solid-formal {
background: #FFFFFF;
color: #52CAD1;
}
&--solid-trial {
background: #FFFFFF;
color: #F59E0B;
}
&--solid-accent {
background: #52CAD1;
color: #FFFFFF;
} }
} }
&__member-issue { // ===== 问题反馈 =====
text-align: center; &__feedback-wrap {
padding: 0.2rem 0; background: #F9FAFB;
border-radius: 0.08rem;
padding: 0.16rem 0.24rem;
display: flex;
flex-direction: column;
gap: 0.24rem;
} }
&__member-issue-title { &__feedback-section {
font-size: 0.15rem; display: flex;
font-weight: 600; flex-direction: column;
color: $text-dark; gap: 0.12rem;
margin-bottom: 0.1rem;
} }
&__member-issue-desc { &__feedback-label {
font-size: 0.12rem; font-size: 0.14rem;
color: #666; font-weight: 520;
line-height: 1.8; color: #1A1A1A;
margin-bottom: 0.16rem; line-height: 0.19rem;
max-width: 5.0rem;
margin-left: auto;
margin-right: auto;
} }
&__member-issue-actions { &__feedback-options {
display: flex;
flex-wrap: wrap;
gap: 0.08rem;
}
&__feedback-option {
display: flex; display: flex;
justify-content: center; justify-content: center;
gap: 0.16rem; align-items: center;
} padding: 0.08rem 0.12rem;
font-size: 0.12rem;
&__member-issue-btn { font-weight: 330;
background: $bg-white; line-height: 0.16rem;
border: 1px solid $border-color; text-align: center;
border-radius: 0.06rem; color: $accent;
padding: 0.08rem 0.24rem; border: 1px solid $accent;
font-size: 0.13rem; border-radius: 0.08rem;
color: $text-dark;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
flex: 1 1 30%;
min-width: 0;
&:hover { &:hover {
background: rgba(82, 202, 209, 0.08);
}
&--active {
background: $accent;
color: #FFFFFF;
border-color: $accent; border-color: $accent;
color: $accent;
&:hover {
background: $accent;
}
}
}
&__feedback-textarea {
width: 100%;
min-height: 0.64rem;
padding: 0.08rem 0.12rem;
font-size: 0.12rem;
font-weight: 330;
line-height: 0.16rem;
color: #1A1A1A;
background: #FFFFFF;
border: none;
border-radius: 0.08rem;
resize: vertical;
outline: none;
box-sizing: border-box;
&::placeholder {
color: #99A1AF;
}
}
&__feedback-submit {
margin-top: 0.44rem;
margin-left: auto;
margin-right: auto;
display: block;
padding: 0.08rem 0.24rem;
background: $accent;
color: #FFFFFF;
font-size: 0.13rem;
font-weight: 500;
border: none;
border-radius: 0.08rem;
cursor: pointer;
transition: opacity 0.2s;
&:hover {
opacity: 0.85;
} }
} }
@@ -468,6 +542,9 @@
font-size: 0.13rem; font-size: 0.13rem;
color: #555; color: #555;
line-height: 1.8; line-height: 1.8;
padding: 0.16rem 0.24rem;
background: #F9FAFB;
border-radius: 0.08rem;
} }
&__privacy-section { &__privacy-section {
@@ -1,6 +1,6 @@
@use '../variables' as *; @use '../variables' as *;
// ==================== 邀请注册送会员弹窗(重设计版) ==================== // ==================== 邀请注册送会员弹窗 ====================
.settings-invite-overlay { .settings-invite-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
@@ -16,213 +16,219 @@
} }
.settings-invite-dialog { .settings-invite-dialog {
position: relative; box-sizing: border-box;
width: 4.2rem;
height: 5.0rem;
background: $bg-white;
border-radius: 0.26rem;
box-shadow: 0 0.2rem 0.44rem rgba(0, 0, 0, 0.14);
overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start;
padding: 0.24rem 0.32rem;
gap: 0.24rem;
width: 4.80rem;
background: linear-gradient(180deg, #EDF9FA 0%, #FFFFFF 20.63%);
border: 1px solid #F0F0F0;
box-shadow: 0 0 0.16rem rgba(0, 0, 0, 0.05);
border-radius: 0.12rem;
// 顶部渐变区域 // 顶部:标题行 + 关闭按钮
&__header { &__header {
width: 100%;
height: 1.04rem;
background: linear-gradient(90deg, #12C7BE 0%, #06B6D4 100%);
border-radius: 0.26rem;
display: flex; display: flex;
align-items: center; justify-content: space-between;
justify-content: center; align-items: flex-start;
flex-shrink: 0; width: 100%;
} }
// 皇冠图标 &__header-text {
&__crown { display: flex;
font-size: 0.32rem; flex-direction: column;
line-height: 0.39rem; gap: 0.04rem;
color: $bg-white; }
font-weight: 700;
&__title {
font-size: 0.18rem;
font-weight: 520;
line-height: 0.24rem;
color: $text-dark;
margin: 0;
}
&__subtitle {
font-size: 0.12rem;
font-weight: 330;
line-height: 0.16rem;
color: $text-middle;
margin: 0;
} }
// 关闭按钮 // 关闭按钮
&__close { &__close {
position: absolute; width: 0.24rem;
top: 0.24rem; height: 0.24rem;
right: 0.27rem; display: flex;
font-size: 0.22rem; align-items: center;
line-height: 0.27rem; justify-content: center;
color: #8EA0B8; font-size: 0.14rem;
color: $text-middle;
cursor: pointer; cursor: pointer;
transition: color 0.2s; transition: color 0.2s;
z-index: 1; flex-shrink: 0;
&:hover { &:hover {
color: $text-dark; color: $text-dark;
} }
} }
// 内容区域
&__body {
flex: 1;
padding: 0.22rem 0.28rem 0.2rem;
display: flex;
flex-direction: column;
}
// 标题
&__title {
font-size: 0.21rem;
font-weight: 700;
color: #132034;
text-align: center;
margin: 0 0 0.05rem 0;
line-height: 0.25rem;
}
// 副标题
&__subtitle {
font-size: 0.13rem;
font-weight: 400;
color: #64748B;
text-align: center;
margin: 0 0 0.2rem 0;
line-height: 0.16rem;
}
// 邀请链接区域 // 邀请链接区域
&__link-box { &__link-box {
box-sizing: border-box;
display: flex; display: flex;
align-items: center; align-items: center;
width: 3.64rem; justify-content: space-between;
height: 0.44rem; padding: 0.12rem 0.16rem;
background: #F3FFFD; width: 100%;
border-radius: 0.22rem; background: #F4FBFB;
padding: 0 0.06rem 0 0.16rem; border: 1px solid #AEE6EA;
margin: 0 auto 0.2rem; border-radius: 0.08rem;
box-sizing: border-box;
} }
&__link-text { &__link-text {
flex: 1; font-size: 0.14rem;
font-size: 0.11rem; font-weight: 330;
line-height: 0.13rem; line-height: 0.19rem;
color: #334155; color: $text-dark;
white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap;
} }
// 复制链接按钮
&__copy-btn { &__copy-btn {
width: 1.0rem; font-size: 0.14rem;
height: 0.32rem; font-weight: 450;
border: none; line-height: 0.19rem;
border-radius: 0.16rem; color: $accent;
background: #0F172A;
color: $bg-white;
font-size: 0.13rem;
font-weight: 600;
line-height: 0.16rem;
cursor: pointer; cursor: pointer;
flex-shrink: 0; flex-shrink: 0;
margin-left: 0.08rem;
transition: opacity 0.2s; transition: opacity 0.2s;
&:hover {
opacity: 0.7;
}
}
// 邀请步骤区域
&__steps {
display: flex;
flex-direction: column;
gap: 0.16rem;
width: 100%;
}
&__steps-title {
font-size: 0.14rem;
font-weight: 450;
line-height: 0.19rem;
color: $accent;
}
&__steps-list {
display: flex;
flex-direction: column;
gap: 0.16rem;
width: 100%;
}
// 步骤卡片
&__step-item {
box-sizing: border-box;
display: flex;
align-items: center;
padding: 0.12rem 0.16rem;
gap: 0.10rem;
width: 100%;
background: $bg-white;
border: 1px solid #F0F0F0;
border-radius: 0.08rem;
}
// 步骤数字圆圈
&__step-num {
display: flex;
justify-content: center;
align-items: center;
width: 0.20rem;
min-width: 0.20rem;
height: 0.20rem;
background: $bg-main;
border-radius: 0.10rem;
font-size: 0.14rem;
font-weight: 330;
line-height: 0.20rem;
text-align: center; text-align: center;
color: $text-middle;
flex-shrink: 0;
}
// 步骤文字
&__step-text {
font-size: 0.14rem;
font-weight: 330;
line-height: 0.19rem;
color: $text-dark;
}
// 底部:统计 + 按钮
&__footer {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
// 统计信息
&__stat {
display: flex;
flex-direction: column;
gap: 0.02rem;
}
&__stat-line {
font-size: 0.14rem;
font-weight: 330;
line-height: 0.19rem;
color: $text-middle;
em {
font-style: normal;
color: $accent;
font-weight: 450;
}
}
// 成为正式会员按钮
&__member-btn {
display: flex;
align-items: center;
padding: 0.08rem 0.12rem;
gap: 0.04rem;
background: $accent;
border-radius: 0.08rem;
border: none;
cursor: pointer;
transition: opacity 0.2s;
span {
font-size: 0.14rem;
font-weight: 520;
line-height: 0.19rem;
text-align: center;
color: #FFFFFF;
}
&:hover { &:hover {
opacity: 0.85; opacity: 0.85;
} }
} }
// 步骤卡片 &__member-btn-arrow {
&__step { font-size: 0.16rem;
display: flex;
align-items: center;
width: 3.64rem;
height: 0.46rem;
background: #F3FFFD;
border-radius: 0.14rem;
padding: 0 0.14rem;
margin: 0 auto 0.12rem;
box-sizing: border-box;
}
// 步骤数字圆圈
&__step-circle {
width: 0.26rem;
height: 0.26rem;
border-radius: 50%;
background: $bg-white;
border: 1px solid #BFEFE8;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.12rem;
font-weight: 700;
color: #12C7BE;
line-height: 0.15rem;
flex-shrink: 0;
box-sizing: border-box;
}
// 步骤文字
&__step-text {
margin-left: 0.14rem;
font-size: 0.13rem;
font-weight: 500;
color: #334155;
line-height: 0.16rem;
}
// 底部统计 + 按钮
&__footer {
display: flex;
align-items: center;
margin-top: auto;
padding-top: 0.06rem;
}
// 统计项
&__stat {
display: flex;
flex-direction: column;
margin-right: 0.2rem;
}
&__stat-label {
font-size: 0.13rem;
font-weight: 500;
color: #64748B;
line-height: 0.16rem;
}
&__stat-value {
font-size: 0.13rem;
font-weight: 500;
color: #64748B;
line-height: 0.16rem;
}
// 成为正式会员按钮
&__member-btn {
margin-left: auto;
width: 1.1rem;
height: 0.29rem;
background: #F3FFFD;
border: 1px solid #BFEFE8;
border-radius: 0.08rem;
font-size: 0.13rem;
font-weight: 400;
color: #000000;
line-height: 0.16rem;
cursor: pointer;
transition: all 0.2s;
text-align: center;
&:hover {
background: #e8faf8;
border-color: $accent;
}
} }
} }
+385 -34
View File
@@ -142,11 +142,21 @@
.side-nav__dialog-close { .side-nav__dialog-close {
cursor: pointer; cursor: pointer;
color: $text-light; display: flex;
font-size: 0.16rem; align-items: center;
justify-content: center;
width: 0.24rem;
height: 0.24rem;
border-radius: 4px;
transition: background 0.2s;
&:hover { &:hover {
color: $text-dark; background: rgba(0, 0, 0, 0.05);
}
svg {
width: 0.14rem;
height: 0.14rem;
} }
} }
@@ -158,75 +168,116 @@
// ==================== 消息通知弹窗(左右分栏) ==================== // ==================== 消息通知弹窗(左右分栏) ====================
.side-nav__message-dialog { .side-nav__message-dialog {
background: $bg-white; box-sizing: border-box;
border-radius: 0.12rem; display: flex;
flex-direction: column;
align-items: flex-start;
padding: 0.24rem 0.32rem;
gap: 0.24rem;
width: 8rem; width: 8rem;
height: 6rem; height: 6rem;
max-width: 90vw; max-width: 90vw;
max-height: 90vh; max-height: 90vh;
box-shadow: 0 0.04rem 0.2rem rgba(0, 0, 0, 0.15); background: linear-gradient(180deg, #EDF9FA 0%, #FFFFFF 12.08%);
display: flex; border: 1px solid #F0F0F0;
flex-direction: column; box-shadow: 0 0 0.16rem rgba(0, 0, 0, 0.05);
border-radius: 0.12rem;
} }
.side-nav__message-dialog-header { .side-nav__message-dialog-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0.16rem 0.2rem; width: 100%;
border-bottom: 1px solid $border-color;
font-size: 0.16rem;
font-weight: 600;
color: $text-dark;
flex-shrink: 0; flex-shrink: 0;
span:first-child {
font-weight: 520;
font-size: 0.18rem;
line-height: 0.24rem;
color: $text-dark;
}
} }
.side-nav__message-dialog-content { .side-nav__message-dialog-content {
display: flex; display: flex;
flex-direction: row;
align-items: flex-start;
gap: 0.16rem;
width: 100%;
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
background: $bg-white;
} }
// 左侧消息列表 // 左侧消息列表
.side-nav__message-list { .side-nav__message-list {
width: 2.1rem; display: flex;
border-right: 1px solid $border-color; flex-direction: column;
gap: 0.04rem;
width: 2.24rem;
height: 100%;
overflow-y: auto; overflow-y: auto;
flex-shrink: 0; flex-shrink: 0;
} }
.side-nav__message-list-item { .side-nav__message-list-item {
display: flex; display: flex;
align-items: center; flex-direction: column;
padding: 0.12rem 0.10rem; align-items: flex-start;
padding: 0.16rem 0.12rem;
border-radius: 0.08rem;
cursor: pointer; cursor: pointer;
border-bottom: 1px solid $border-color;
transition: background 0.2s; transition: background 0.2s;
&:hover { &:hover {
background: $bg-main; background: #F6F6F6;
} }
&--active { &--active {
background: $theme-color; background: #EDF9FA;
} }
} }
.side-nav__message-list-title { // 列表项内部行布局
flex: 1; .side-nav__message-list-item-row {
font-size: 0.13rem; display: flex;
color: $text-dark; flex-direction: row;
overflow: hidden; justify-content: space-between;
text-overflow: ellipsis; align-items: center;
white-space: nowrap; width: 100%;
}
.side-nav__message-list-item-left {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.08rem;
} }
.side-nav__message-unread-dot { .side-nav__message-unread-dot {
width: 0.08rem; width: 0.08rem;
height: 0.08rem; height: 0.08rem;
border-radius: 50%; border-radius: 50%;
background: $danger; background: $accent;
margin-left: 0.06rem; flex-shrink: 0;
}
.side-nav__message-list-title {
font-weight: 520;
font-size: 0.14rem;
line-height: 0.19rem;
color: $text-dark;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.side-nav__message-list-date {
font-weight: 330;
font-size: 0.12rem;
line-height: 0.16rem;
color: $text-light;
flex-shrink: 0; flex-shrink: 0;
} }
@@ -241,21 +292,45 @@
// 右侧消息详情 // 右侧消息详情
.side-nav__message-detail { .side-nav__message-detail {
flex: 1; flex: 1;
padding: 0.2rem; height: 100%;
overflow-y: auto; overflow-y: auto;
position: relative;
padding: 0.16rem 0.24rem;
}
.side-nav__message-detail-header {
display: flex;
justify-content: space-between;
align-items: center;
} }
.side-nav__message-detail-title { .side-nav__message-detail-title {
font-size: 0.16rem; font-weight: 630;
font-weight: 600; font-size: 0.22rem;
line-height: 0.29rem;
color: $text-dark; color: $text-dark;
margin-bottom: 0.16rem; }
.side-nav__message-detail-time {
font-weight: 330;
font-size: 0.12rem;
line-height: 0.16rem;
color: $text-middle;
flex-shrink: 0;
}
.side-nav__message-detail-divider {
width: 100%;
height: 1px;
background: #F0F0F0;
margin: 0.16rem 0;
} }
.side-nav__message-detail-content { .side-nav__message-detail-content {
font-weight: 330;
font-size: 0.14rem; font-size: 0.14rem;
line-height: 0.24rem;
color: $text-dark; color: $text-dark;
line-height: 1.7;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
} }
@@ -380,3 +455,279 @@
} }
} }
} }
// ==================== 邀请会员卡片 ====================
.side-nav__referral-card {
width: 100%;
background: linear-gradient(128.17deg, #52CAD1 0%, #82D79B 100%);
border-radius: 0.08rem;
padding: 0.16rem;
box-sizing: border-box;
margin-top: 0.12rem;
}
.side-nav__referral-title {
font-size: 0.16rem;
font-weight: 630;
color: #FFFFFF;
line-height: 0.21rem;
}
.side-nav__referral-subtitle {
font-size: 0.10rem;
font-weight: 330;
color: #EAFBFC;
line-height: 0.13rem;
margin-top: 0.04rem;
}
.side-nav__referral-panel {
background: #FFFFFF;
box-shadow: 0px 0.02rem 0.06rem rgba(26, 26, 26, 0.05);
border-radius: 0.06rem;
padding: 0.10rem 0.08rem;
margin-top: 0.10rem;
}
.side-nav__referral-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.side-nav__referral-label {
font-size: 0.10rem;
font-weight: 330;
color: #6B7280;
line-height: 0.13rem;
}
.side-nav__referral-value {
font-size: 0.10rem;
font-weight: 520;
color: #52CAD1;
line-height: 0.13rem;
}
.side-nav__referral-track {
width: 100%;
height: 0.02rem;
background: #D7F1F3;
border-radius: 9999px;
margin-top: 0.06rem;
overflow: hidden;
}
.side-nav__referral-fill {
height: 100%;
background: #52CAD1;
border-radius: 9999px;
}
.side-nav__referral-divider {
width: 100%;
border-top: 0.01rem solid #8C8C8C;
margin-top: 0.08rem;
}
.side-nav__referral-tip {
font-size: 0.10rem;
font-weight: 330;
color: #1A1A1A;
line-height: 0.13rem;
margin-top: 0.08rem;
strong {
font-weight: 630;
}
}
.side-nav__referral-action {
font-size: 0.11rem;
font-weight: 520;
color: #52CAD1;
line-height: 0.15rem;
text-align: center;
margin-top: 0.08rem;
cursor: pointer;
&:hover {
opacity: 0.8;
}
}
// ==================== 用户信息模块 ====================
.side-nav__user-info {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
background: #F3F4F6;
border-radius: 0.08rem;
padding: 0.12rem 0.16rem;
box-sizing: border-box;
margin-top: 0.12rem;
}
.side-nav__user-left {
display: flex;
flex-direction: column;
gap: 0.04rem;
}
.side-nav__user-phone {
font-size: 0.14rem;
font-weight: 330;
color: #1A1A1A;
line-height: 0.19rem;
}
.side-nav__user-member-tag {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.02rem 0.04rem;
border-radius: 0.04rem;
font-size: 0.10rem;
font-weight: 520;
line-height: 0.13rem;
width: fit-content;
// 正式会员:主题色字 + 浅绿底
&--formal {
color: #52CAD1;
background: #EDF9FA;
}
// 试用会员:橙色字 + 浅橙底
&--trial {
color: #F59E0B;
background: #FFFBF5;
}
// 免费用户:中灰字 + 灰底
&--free {
color: #6A7282;
background: #F3F4F6;
}
}
.side-nav__user-bell {
width: 0.20rem;
height: 0.20rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
position: relative;
svg {
width: 100%;
height: 100%;
}
&:hover {
opacity: 0.7;
}
}
// 铃铛右上角未读红点
.side-nav__user-bell-dot {
position: absolute;
top: -0.005rem;
right: 0.01rem;
width: 0.08rem;
height: 0.08rem;
background: #EF4444;
border-radius: 50%;
}
// ==================== 账户弹窗 ====================
.side-nav__user-info-wrapper {
position: relative;
margin-top: 0.12rem;
}
.side-nav__account-popup {
position: absolute;
bottom: calc(100% + 0.08rem);
left: 0;
width: 100%;
background: #FFFFFF;
border: 1px solid #F0F0F0;
box-shadow: 0px 0.02rem 0.08rem rgba(0, 0, 0, 0.04);
border-radius: 0.08rem;
overflow: hidden;
z-index: 100;
}
.side-nav__account-popup-header {
padding: 0.16rem;
background: linear-gradient(180deg, #EDF9FA 0%, #FFFFFF 38.18%);
border-radius: 0.08rem 0.08rem 0 0;
}
.side-nav__account-popup-header-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.side-nav__account-popup-header-left {
display: flex;
flex-direction: column;
gap: 0.04rem;
}
.side-nav__account-popup-member-type {
font-size: 0.14rem;
font-weight: 450;
line-height: 0.19rem;
// 正式会员 — 主题色
&--formal {
color: $accent;
}
// 试用会员 — 橙色
&--trial {
color: #F59E0B;
}
// 免费用户 — 中灰色
&--free {
color: $text-middle;
}
}
.side-nav__account-popup-expire {
font-size: 0.12rem;
font-weight: 330;
color: $text-light;
line-height: 0.16rem;
}
.side-nav__account-popup-detail {
font-size: 0.12rem;
font-weight: 330;
color: $text-light;
line-height: 0.16rem;
cursor: pointer;
&:hover {
color: $accent;
}
}
.side-nav__account-popup-item {
padding: 0.12rem 0.16rem;
font-size: 0.12rem;
font-weight: 330;
color: #000000;
line-height: 0.16rem;
cursor: pointer;
&:hover {
background: $bg-main;
}
}
+43 -2
View File
@@ -187,10 +187,28 @@
&__match-card { &__match-card {
flex-shrink: 0; flex-shrink: 0;
width: 2.06rem; width: 2.06rem;
background: #FBFDF7;
border: 1px solid #9FD051;
border-radius: 0.08rem; border-radius: 0.08rem;
padding: 0.16rem 0.24rem; padding: 0.16rem 0.24rem;
border: 1px solid #9FD051;
background: #FBFDF7;
}
// 高匹配度
&__match-card--high {
background: #FBFDF7;
border-color: #9FD051;
}
// 中匹配度
&__match-card--medium {
background: #FFFBF5;
border-color: #F59E0B;
}
// 低匹配度
&__match-card--low {
background: #FFF6F5;
border-color: #FE6D52;
} }
&__match-top { &__match-top {
@@ -214,6 +232,24 @@
color: #9FD051; color: #9FD051;
} }
// 高匹配度文字色
&__match-card--high &__match-score,
&__match-card--high &__match-label {
color: #9FD051;
}
// 中匹配度文字色
&__match-card--medium &__match-score,
&__match-card--medium &__match-label {
color: #F59E0B;
}
// 低匹配度文字色
&__match-card--low &__match-score,
&__match-card--low &__match-label {
color: #FE6D52;
}
&__match-list { &__match-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -392,3 +428,8 @@
margin: 0; margin: 0;
} }
} }
// ---- 骨架屏区域 ----
.job-detail__skeleton {
padding: 0.32rem;
}
+11 -10
View File
@@ -15,8 +15,6 @@
overflow: hidden; overflow: hidden;
background: $bg-main; background: $bg-main;
font-size: 0.14rem; font-size: 0.14rem;
display: flex;
flex-direction: column;
} }
// 页面标题 // 页面标题
@@ -156,9 +154,6 @@
font-size: 0.12rem; font-size: 0.12rem;
line-height: 0.16rem; line-height: 0.16rem;
color: $accent; color: $accent;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: text-top; vertical-align: text-top;
margin-left: 0.03rem; margin-left: 0.03rem;
} }
@@ -278,11 +273,11 @@
} }
} }
// 职位列表 // 职位列表(用 calc 减去上方头部+筛选栏大致高度,保证列表独立滚动)
&__list { &__list {
gap: 0; gap: 0;
flex: 1;
overflow-y: auto; overflow-y: auto;
height: calc(var(--app-height, 100vh) - 1.8rem);
padding-bottom: 0.2rem; padding-bottom: 0.2rem;
margin: 0 0.24rem; margin: 0 0.24rem;
@@ -307,7 +302,7 @@
&:hover { &:hover {
box-shadow: 0 0.04rem 0.16rem rgba(0, 0, 0, 0.06); box-shadow: 0 0.04rem 0.16rem rgba(0, 0, 0, 0.06);
background: #fbfbfb; //background: #fbfbfb;
} }
&--selected { &--selected {
@@ -581,7 +576,7 @@
} }
} }
// 不再显示 // 不再显示(默认隐藏,hover 卡片时显示)
&__hide-btn { &__hide-btn {
display: block; display: block;
margin-top: auto; margin-top: auto;
@@ -589,15 +584,21 @@
font-weight: 330; font-weight: 330;
color: #8C8C8C; color: #8C8C8C;
cursor: pointer; cursor: pointer;
transition: color 0.2s; transition: color 0.2s, opacity 0.2s;
line-height: 0.19rem; line-height: 0.19rem;
white-space: nowrap; white-space: nowrap;
opacity: 0;
&:hover { &:hover {
color: $text-dark; color: $text-dark;
} }
} }
// hover 卡片时显示"不再显示"按钮
&__job-card:hover &__hide-btn {
opacity: 1;
}
// 弹出菜单 // 弹出菜单
&__job-popup { &__job-popup {
position: absolute; position: absolute;
+290 -533
View File
@@ -5,362 +5,179 @@
<div v-if="modelValue" class="member-dialog-overlay" @click="$emit('update:modelValue', false)"> <div v-if="modelValue" class="member-dialog-overlay" @click="$emit('update:modelValue', false)">
<!-- 弹窗主体 阻止点击冒泡 --> <!-- 弹窗主体 阻止点击冒泡 -->
<div class="member-dialog" @click.stop> <div class="member-dialog" @click.stop>
<!-- 右上角关闭按钮 -->
<span class="member-dialog__close" @click="$emit('update:modelValue', false)"></span>
<!-- ==================== 会员介绍步骤一简介页 ==================== --> <!-- 顶部标题行 -->
<div v-if="currentView === 'intro1'" class="member-dialog__intro-step1"> <div class="member-dialog__header">
<h1 class="member-dialog__intro-title">Offer - AI 智能求职引擎</h1> <div class="member-dialog__header-left">
<p class="member-dialog__intro-subtitle">全自动海投简历精修模拟面试助你轻松拿高薪</p> <h2 class="member-dialog__title">会员中心</h2>
<button class="member-dialog__intro-btn" @click="currentView = 'intro2'"> <p class="member-dialog__subtitle">解锁更多求职功能让投递更高效</p>
<span class="member-dialog__intro-btn-icon"></span> </div>
升级 Pro 开启求职加速 <div class="member-dialog__header-right">
</button> <!-- 联系客服hover显示二维码 -->
<span class="member-dialog__contact-wrap">
<span class="member-dialog__contact">联系客服</span>
<div class="member-dialog__contact-qr">
<img src="@/assets/images/nav/kg-qcCode.png" alt="客服二维码" />
<div class="member-dialog__contact-qr-arrow"></div>
</div>
</span>
<span class="member-dialog__close" @click="$emit('update:modelValue', false)"></span>
</div>
</div> </div>
<!-- ==================== 会员介绍步骤二详细介绍页可滚动 ==================== --> <!-- 套餐选择区域 -->
<div v-else-if="currentView === 'intro2'" class="member-dialog__intro-step2"> <div class="member-dialog__plans">
<div class="member-dialog__step2-badge"> 求职加速会员</div> <div
<h1 class="member-dialog__step2-title">offer派 offer 就是快</h1> v-for="plan in plans"
<p class="member-dialog__step2-subtitle">每天不到 1 少投无效岗位把时间花在更可能拿面试的机会</p> :key="plan.key"
class="member-dialog__plan-card"
:class="{ 'member-dialog__plan-card--active': selectedPlan === plan.key }"
@click="selectedPlan = plan.key"
>
<!-- 推荐标签 -->
<div v-if="plan.recommend" class="member-dialog__plan-recommend"><svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_180_2594)">
<path d="M0.872709 4.61946C0.867714 4.56192 0.874732 4.50398 0.893317 4.44931C0.911903 4.39464 0.94165 4.34442 0.980672 4.30186C1.01969 4.25929 1.06714 4.2253 1.11999 4.20204C1.17285 4.17878 1.22996 4.16676 1.28771 4.16675H2.08333C2.19384 4.16675 2.29982 4.21065 2.37796 4.28879C2.4561 4.36693 2.5 4.47291 2.5 4.58341V8.54175C2.5 8.65225 2.4561 8.75823 2.37796 8.83637C2.29982 8.91451 2.19384 8.95841 2.08333 8.95841H1.63208C1.5278 8.95844 1.42729 8.91936 1.35042 8.84889C1.27355 8.77842 1.2259 8.68168 1.21688 8.57779L0.872709 4.61946ZM3.75 4.45321C3.75 4.27904 3.85833 4.1232 4.01563 4.04904C4.35917 3.88696 4.94438 3.56133 5.20833 3.12112C5.54854 2.55362 5.61271 1.52841 5.62313 1.29362C5.62458 1.26071 5.62375 1.22779 5.62813 1.19529C5.68458 0.788414 6.46979 1.26362 6.77083 1.76612C6.93438 2.03862 6.95521 2.39675 6.93813 2.67654C6.91958 2.97571 6.83188 3.26466 6.74583 3.55175L6.5625 4.16362H8.82438C8.88875 4.16362 8.95225 4.17853 9.0099 4.20719C9.06754 4.23585 9.11776 4.27748 9.15662 4.32881C9.19547 4.38014 9.22191 4.43977 9.23384 4.50303C9.24578 4.56629 9.2429 4.63145 9.22542 4.69341L8.10667 8.65508C8.08197 8.74245 8.02941 8.81937 7.95698 8.87413C7.88455 8.92888 7.79622 8.95848 7.70542 8.95841H4.16667C4.05616 8.95841 3.95018 8.91451 3.87204 8.83637C3.7939 8.75823 3.75 8.65225 3.75 8.54175V4.45321Z" fill="white" stroke="white" stroke-width="0.833333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_180_2594">
<rect width="10" height="10" fill="white"/>
</clipPath>
</defs>
</svg>
推荐</div>
<!-- 套餐名称 -->
<div class="member-dialog__plan-name">{{ plan.name }}</div>
<!-- 价格行 -->
<div class="member-dialog__plan-price">
<span class="member-dialog__plan-price-symbol">¥</span>
<span class="member-dialog__plan-price-num">{{ plan.priceInt }}</span>
<span v-if="plan.originalPrice" class="member-dialog__plan-original-price">¥{{ plan.originalPrice }}</span>
</div>
<!-- 每日价格 -->
<div class="member-dialog__plan-daily">{{ plan.dailyDesc }}</div>
</div>
</div>
<!-- 套餐卡片区域 --> <!-- 会员权益区域 -->
<div class="member-dialog__plans"> <div class="member-dialog__benefits">
<h3 class="member-dialog__benefits-title">会员权益</h3>
<div class="member-dialog__benefits-cards">
<div class="member-dialog__benefit-card">
<div class="member-dialog__benefit-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<rect width="24" height="24" rx="12" fill="#EDF9FA"/>
<path d="M7 8h7v2H7V8zm0 3h10v2H7v-2zm0 3h5v2H7v-2z" fill="#52CAD1"/>
</svg>
</div>
<div class="member-dialog__benefit-info">
<span class="member-dialog__benefit-name">AI优化简历</span>
<span class="member-dialog__benefit-desc">享受无限次数</span>
</div>
</div>
<div class="member-dialog__benefit-card">
<div class="member-dialog__benefit-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<rect width="24" height="24" rx="12" fill="#EDF9FA"/>
<path d="M12 7l1.5 3 3.5.5-2.5 2.5.5 3.5L12 15l-3 1.5.5-3.5L7 10.5l3.5-.5L12 7z" fill="#52CAD1"/>
</svg>
</div>
<div class="member-dialog__benefit-info">
<span class="member-dialog__benefit-name">AI+投递助手</span>
<span class="member-dialog__benefit-desc">会员专享</span>
</div>
</div>
</div>
</div>
<!-- 支付方式选择 -->
<div class="member-dialog__payment">
<span class="member-dialog__payment-label">选择支付方式</span>
<div class="member-dialog__payment-methods">
<div <div
v-for="plan in plans" class="member-dialog__payment-item"
:key="plan.key" :class="{ 'member-dialog__payment-item--wechat-active': selectedPayment === 'wechat' }"
class="member-dialog__plan-card" @click="selectedPayment = 'wechat'"
:class="{ 'member-dialog__plan-card--active': selectedPlan === plan.key }"
@click="selectedPlan = plan.key"
> >
<div v-if="plan.recommend" class="member-dialog__plan-recommend"> 推荐覆盖求职周期性价比最高</div> <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<div v-if="plan.tag" class="member-dialog__plan-tag">{{ plan.tag }}</div> <path d="M14 8c0 3.3-2.7 6-6 6s-6-2.7-6-6 2.7-6 6-6 6 2.7 6 6z" fill="#07C160"/>
<div class="member-dialog__plan-name">{{ plan.name }}</div> <path d="M5.5 6.5c.5-.3 1.2-.1 1.5.4l.5 1 1.5-2c.3-.5 1-.6 1.5-.3.5.3.6 1 .3 1.5l-2.5 3.5c-.2.3-.5.4-.8.4-.3 0-.6-.1-.8-.4L5.2 8c-.3-.5-.2-1.2.3-1.5z" fill="white"/>
<div class="member-dialog__plan-price"> </svg>
<span class="member-dialog__plan-price-symbol">¥</span> <span class="member-dialog__payment-text member-dialog__payment-text--wechat">微信支付</span>
<span class="member-dialog__plan-price-num">{{ plan.price }}</span>
<span class="member-dialog__plan-price-unit">/{{ plan.unit }}</span>
</div>
<div v-if="plan.perMonth" class="member-dialog__plan-per-month">折合仅 ¥{{ plan.perMonth }}/</div>
<button class="member-dialog__plan-btn" @click.stop="handleUpgrade(plan)">{{ plan.btnText }}</button>
</div> </div>
</div> <div
class="member-dialog__payment-item"
<!-- 提示信息 --> :class="{ 'member-dialog__payment-item--alipay-active': selectedPayment === 'alipay' }"
<div class="member-dialog__tip"> @click="selectedPayment = 'alipay'"
<span class="member-dialog__tip-icon"></span> >
<span class="member-dialog__tip-label">时间就是机会</span> <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<span class="member-dialog__tip-text">热门岗位通常在发布后 24-48 小时内收到大量申请使用 AI 加速工具越早投递越容易被 HR 看到!</span> <rect width="16" height="16" rx="3" fill="#1677FF"/>
</div> <text x="4" y="12" fill="white" font-size="10" font-weight="bold"></text>
</svg>
<!-- 底部内容左侧能力列表 + 右侧对比表格 --> <span class="member-dialog__payment-text member-dialog__payment-text--alipay">支付宝支付</span>
<div class="member-dialog__content-row">
<div class="member-dialog__abilities">
<h3 class="member-dialog__abilities-title">求职加速能力</h3>
<div v-for="ability in abilities" :key="ability.text" class="member-dialog__ability-item">
<span class="member-dialog__ability-icon" :style="{ color: ability.color }">{{ ability.icon }}</span>
<span class="member-dialog__ability-text">{{ ability.text }}</span>
</div>
</div>
<div class="member-dialog__compare">
<h3 class="member-dialog__compare-title">会员能力对比</h3>
<table class="member-dialog__compare-table">
<thead>
<tr>
<th>核心能力</th>
<th>免费版</th>
<th class="member-dialog__compare-th-pro"> 会员版</th>
</tr>
</thead>
<tbody>
<tr v-for="row in compareData" :key="row.name">
<td>{{ row.name }}</td>
<td class="member-dialog__compare-free">{{ row.free }}</td>
<td class="member-dialog__compare-pro"> {{ row.pro }}</td>
</tr>
</tbody>
</table>
<div class="member-dialog__compare-footer">
<span>🔒 支付安全保障</span>
<span> 支持随时取消</span>
</div>
</div>
</div>
<!-- 用户评价 -->
<div class="member-dialog__testimonial">
<div class="member-dialog__testimonial-avatar">
<img src="@/assets/images/home/alex-avatar.png" alt="用户头像" />
</div>
<div class="member-dialog__testimonial-content">
<div class="member-dialog__testimonial-stars"></div>
<p class="member-dialog__testimonial-text">"开通后我主要用 AI 简历优化和岗位匹配,投递效率明显提升,每次网申节省了十几分钟,终于知道该重点投哪些岗位了。"</p>
<div class="member-dialog__testimonial-author">
<span class="member-dialog__testimonial-name">张同学</span>
<span class="member-dialog__testimonial-desc">· 成功入职某大厂运营</span>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- ==================== 下单步骤一选择套餐 ==================== --> <!-- 购买按钮 -->
<div v-else-if="currentView === 'order1'" class="member-dialog__order"> <button
<!-- 顶部导航 --> class="member-dialog__buy-btn"
<div class="member-dialog__order-header"> :class="{ 'member-dialog__buy-btn--active': selectedPayment }"
<span class="melmber-dialog__order-back" @click="currentView = 'intro2'"> 返回会员介绍</span> @click="handleBuy"
<!-- 步骤条 二维码弹窗打开时显示第2步 --> >购买会员</button>
<div class="member-dialog__steps">
<div class="member-dialog__step" :class="showQrCode ? 'member-dialog__step--done' : 'member-dialog__step--active'"> </div>
<span class="member-dialog__step-num">{{ showQrCode ? '✓' : '1' }}</span> </div>
<span class="member-dialog__step-label">选择套餐</span>
</div> <!-- ==================== 扫码支付弹窗 ==================== -->
<div class="member-dialog__step-line" :class="{ 'member-dialog__step-line--done': showQrCode }"></div> <div v-if="showQrCode" class="member-dialog-overlay member-dialog-overlay--qrcode" @click="handleCloseQrCode">
<div class="member-dialog__step" :class="{ 'member-dialog__step--active': showQrCode }"> <div class="member-dialog member-dialog--qrcode" @click.stop>
<span class="member-dialog__step-num">2</span>
<span class="member-dialog__step-label">支付方式</span> <!-- 顶部标题行 -->
</div> <div class="member-dialog__header">
<div class="member-dialog__step-line"></div> <div class="member-dialog__header-left">
<div class="member-dialog__step"> <h2 class="member-dialog__title">扫码支付</h2>
<span class="member-dialog__step-num">3</span>
<span class="member-dialog__step-label">完成</span>
</div>
</div>
</div> </div>
<div class="member-dialog__header-right">
<!-- 主内容区域可滚动 --> <span class="member-dialog__close" @click="handleCloseQrCode"></span>
<div class="member-dialog__order-body">
<!-- 左侧内容 -->
<div class="member-dialog__order-left">
<h2 class="member-dialog__order-title">开启你的 AI 求职加速计划</h2>
<p class="member-dialog__order-desc">解锁简历优化岗位匹配投递追踪面试准备等能力让你的求职过程更高效更有方向</p>
<!-- 套餐选择卡片 -->
<div class="member-dialog__order-plans">
<div
v-for="plan in plans"
:key="plan.key"
class="member-dialog__order-plan-card"
:class="{ 'member-dialog__order-plan-card--active': selectedPlan === plan.key }"
@click="selectedPlan = plan.key"
>
<div v-if="plan.recommend" class="member-dialog__order-plan-badge"> 推荐</div>
<div class="member-dialog__order-plan-info">
<div class="dflex">
<div class="member-dialog__order-plan-name">{{ plan.name }}</div>
<!-- 选中圆圈 -->
<div class="member-dialog__order-plan-radio">
<div v-if="selectedPlan === plan.key" class="member-dialog__order-plan-radio-inner"></div>
</div>
</div>
<div class="member-dialog__order-plan-price">
<span class="member-dialog__order-plan-price-symbol">¥</span>
<span class="member-dialog__order-plan-price-num">{{ plan.price }}</span>
<span class="member-dialog__order-plan-price-unit">/{{ plan.unit }}</span>
</div>
<div class="member-dialog__order-plan-desc">{{ plan.orderDesc }}</div>
</div>
</div>
</div>
<!-- 支付方式 -->
<div class="member-dialog__order-payment">
<h3 class="member-dialog__order-payment-title">支付方式</h3>
<div class="member-dialog__order-payment-methods">
<div
v-for="method in paymentMethods"
:key="method.key"
class="member-dialog__order-payment-item"
:class="{ 'member-dialog__order-payment-item--active': selectedPayment === method.key }"
@click="selectedPayment = method.key"
>
<span class="member-dialog__order-payment-icon">{{ method.icon }}</span>
<span class="member-dialog__order-payment-name">{{ method.name }}</span>
</div>
</div>
<!-- 支付提示 -->
<div class="member-dialog__order-payment-hint">
点击右侧"立即开启求职加速"将弹出二维码完成支付
</div>
<div class="member-dialog__order-payment-safe">支付信息将通过安全通道加密处理请放心支付</div>
</div>
<!-- 底部保障标签 -->
<div class="member-dialog__order-guarantees">
<span class="member-dialog__order-guarantee-item">🔒 安全支付</span>
<span class="member-dialog__order-guarantee-item"> 立即解锁</span>
<span class="member-dialog__order-guarantee-item"> 支持取消续费</span>
<span class="member-dialog__order-guarantee-item">🎧 客服支持</span>
</div>
<!-- 常见问题 -->
<div class="member-dialog__order-faq">
<h3 class="member-dialog__order-faq-title">常见问题</h3>
<div
v-for="faq in faqList"
:key="faq.question"
class="member-dialog__order-faq-item"
@click="faq.open = !faq.open"
>
<div class="member-dialog__order-faq-question">
<span>{{ faq.question }}</span>
<span class="member-dialog__order-faq-arrow" :class="{ 'member-dialog__order-faq-arrow--open': faq.open }"></span>
</div>
<div v-if="faq.open" class="member-dialog__order-faq-answer">{{ faq.answer }}</div>
</div>
</div>
</div>
<!-- 右侧订单摘要 -->
<div class="member-dialog__order-right">
<div class="member-dialog__order-summary">
<h3 class="member-dialog__order-summary-title">订单摘要</h3>
<!-- 套餐信息 -->
<div class="member-dialog__order-summary-row">
<div>
<div class="member-dialog__order-summary-plan">{{ currentPlan.name }}</div>
<div class="member-dialog__order-summary-duration">{{ currentPlan.unit }}</div>
</div>
<span class="member-dialog__order-summary-price">¥{{ currentPlan.price }}</span>
</div>
<!-- 分割线 -->
<div class="member-dialog__order-summary-divider"></div>
<!-- 今日支付 -->
<div class="member-dialog__order-summary-row">
<span class="member-dialog__order-summary-label">今日支付</span>
<span class="member-dialog__order-summary-total">¥{{ currentPlan.price }}</span>
</div>
<!-- 协议勾选 -->
<div class="member-dialog__order-summary-agree">
<label class="member-dialog__order-checkbox">
<input type="checkbox" v-model="agreeProtocol" />
<span class="member-dialog__order-checkbox-mark"></span>
</label>
<span>我已阅读并同意 <a href="javascript:;" @click.prevent="openMemberAgreement">会员服务协议</a> </span>
</div>
<!-- 立即开启按钮 -->
<button
class="member-dialog__order-submit-btn"
:disabled="!agreeProtocol"
@click="handleShowQrCode"
>立即开启求职加速</button>
<div class="member-dialog__order-submit-tip">支付成功后立即解锁全部权益可随时取消续费</div>
<!-- 你将获得 -->
<div class="member-dialog__order-benefits">
<h4 class="member-dialog__order-benefits-title">你将获得</h4>
<div v-for="item in orderBenefits" :key="item" class="member-dialog__order-benefits-item">
<span class="member-dialog__order-benefits-check"></span>
<span>{{ item }}</span>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
<!-- ==================== 下单步骤二确认支付结果 ==================== --> <!-- 二维码区域 -->
<div v-else-if="currentView === 'order2'" class="member-dialog__order"> <div class="member-dialog__qrcode-content">
<div class="member-dialog__order-header"> <!-- 支付宝iframe 渲染支付表单 -->
<span class="member-dialog__order-back"></span> <div v-if="selectedPayment === 'alipay' && paymentFormHtml" class="member-dialog__qrcode-image">
<div class="member-dialog__steps"> <iframe
<div class="member-dialog__step member-dialog__step--done"> :srcdoc="paymentFormHtml"
<span class="member-dialog__step-num"></span> frameborder="0"
<span class="member-dialog__step-label">选择套餐</span> class="member-dialog__payment-iframe"
</div> scrolling="auto"
<div class="member-dialog__step-line member-dialog__step-line--done"></div> ></iframe>
<div class="member-dialog__step member-dialog__step--active">
<span class="member-dialog__step-num">2</span>
<span class="member-dialog__step-label">支付方式</span>
</div>
<div class="member-dialog__step-line"></div>
<div class="member-dialog__step">
<span class="member-dialog__step-num">3</span>
<span class="member-dialog__step-label">完成</span>
</div>
</div>
</div> </div>
<!-- 确认中内容 --> <!-- 微信使用 qrcode.js 渲染二维码 -->
<div class="member-dialog__order-confirming"> <div v-if="selectedPayment === 'wechat' && wechatCodeUrl" class="member-dialog__qrcode-image">
<div class="member-dialog__order-confirming-card"> <iframe
<!-- 加载动画 --> :srcdoc="wechatQrHtml"
<div class="member-dialog__order-loading-spinner"></div> frameborder="0"
<h2 class="member-dialog__order-confirming-title">正在确认支付结果</h2> class="member-dialog__wechat-qr-iframe"
<p class="member-dialog__order-confirming-desc">请稍候请勿关闭此页面</p> scrolling="no"
</div> ></iframe>
</div> </div>
<!-- 金额和提示 -->
<div class="member-dialog__qrcode-price-row">
<span class="member-dialog__qrcode-price">¥{{ currentPlan.priceInt }}</span>
<span class="member-dialog__qrcode-tip">扫码完成支付</span>
</div>
<!-- 协议提示 -->
<p class="member-dialog__qrcode-agreement">
开通即代表同意 <a href="javascript:;" @click.prevent="openMemberAgreement">会员服务协议</a>
</p>
</div> </div>
<!-- ==================== 下单步骤三支付完成 ==================== --> <!-- 已支付成功按钮 -->
<div v-else-if="currentView === 'order3'" class="member-dialog__order"> <button class="member-dialog__paid-btn" @click="handleConfirmPaid">已支付成功</button>
<div class="member-dialog__order-header">
<span class="member-dialog__order-back"></span>
<div class="member-dialog__steps">
<div class="member-dialog__step member-dialog__step--done">
<span class="member-dialog__step-num"></span>
<span class="member-dialog__step-label">选择套餐</span>
</div>
<div class="member-dialog__step-line member-dialog__step-line--done"></div>
<div class="member-dialog__step member-dialog__step--done">
<span class="member-dialog__step-num"></span>
<span class="member-dialog__step-label">支付方式</span>
</div>
<div class="member-dialog__step-line member-dialog__step-line--done"></div>
<div class="member-dialog__step member-dialog__step--active">
<span class="member-dialog__step-num">3</span>
<span class="member-dialog__step-label">完成</span>
</div>
</div>
</div>
<!-- 支付成功内容 -->
<div class="member-dialog__order-success">
<div class="member-dialog__order-success-card">
<!-- 成功图标 -->
<div class="member-dialog__order-success-icon"></div>
<h2 class="member-dialog__order-success-title">支付成功求职加速已开启</h2>
<p class="member-dialog__order-success-desc">你已成功解锁 AI 求职加速权益现在可以开始优化简历匹配岗位并准备面试</p>
<button class="member-dialog__order-success-btn" @click="handleGoResume">开始优化我的简历</button>
<button class="member-dialog__order-success-btn-secondary" @click="handleViewMemberBenefits">查看会员权益</button>
</div>
</div>
</div>
<!-- ==================== 付款弹窗 ==================== -->
<div v-if="showQrCode" class="member-dialog__qrcode-overlay" @click="handleCloseQrCode">
<div class="member-dialog__qrcode-modal" @click.stop>
<!-- 关闭按钮 -->
<span class="member-dialog__qrcode-close" @click="handleCloseQrCode"></span>
<!-- 支付方式标题 -->
<div class="member-dialog__qrcode-title">
<span class="member-dialog__qrcode-title-icon">{{ selectedPayment === 'wechat' ? '💬' : '🔷' }}</span>
<span>{{ selectedPayment === 'wechat' ? '微信支付' : '支付宝' }}</span>
</div>
<!-- 扫码提示 -->
<h3 class="member-dialog__qrcode-subtitle">{{ selectedPayment === 'alipay' ? '扫码完成支付' : '扫码完成支付' }}</h3>
<p class="member-dialog__qrcode-desc">{{ selectedPayment === 'alipay' ? '请在支付宝页面完成支付,完成后点击下方按钮确认。' : '请使用微信 App 扫描二维码完成支付,完成后此窗口会自动关闭。' }}</p>
<!-- 支付宝iframe 渲染支付表单 -->
<div v-if="selectedPayment === 'alipay' && paymentFormHtml" class="member-dialog__qrcode-image">
<iframe
:srcdoc="paymentFormHtml"
frameborder="0"
class="member-dialog__payment-iframe"
scrolling="auto"
></iframe>
</div>
<!-- 微信使用 qrcode.js 渲染二维码 -->
<div v-if="selectedPayment === 'wechat' && wechatCodeUrl" class="member-dialog__qrcode-image">
<iframe
:srcdoc="wechatQrHtml"
frameborder="0"
class="member-dialog__wechat-qr-iframe"
scrolling="no"
></iframe>
</div>
<!-- 金额 -->
<div class="member-dialog__qrcode-amount">¥{{ currentPlan.price }}</div>
</div>
</div>
</div> </div>
</div> </div>
@@ -370,9 +187,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, reactive, onMounted } from 'vue' import { ref, computed, watch, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useStore } from 'vuex' import { useStore } from 'vuex'
import { ElMessage } from 'element-plus'
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' import AgreementPreviewDialog from '@/components/tools/AgreementPreviewDialog.vue'
@@ -382,7 +199,6 @@ const props = defineProps<{ modelValue: boolean }>()
/** 组件 Emits — 通知父组件更新 modelValue */ /** 组件 Emits — 通知父组件更新 modelValue */
const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void }>() const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void }>()
const router = useRouter()
const store = useStore() const store = useStore()
// ==================== 类型定义 ==================== // ==================== 类型定义 ====================
@@ -391,46 +207,27 @@ const store = useStore()
interface PlanItem { interface PlanItem {
key: string key: string
name: string name: string
price: string /** 整数价格(元),用于显示 */
unit: string priceInt: string
tag?: string /** 划线价(元) */
originalPrice: string
/** 每日价格描述 */
dailyDesc: string
/** 是否推荐 */
recommend?: boolean recommend?: boolean
perMonth?: string
btnText: string
orderDesc: string
/** 后端原始数据,下单时使用 */ /** 后端原始数据,下单时使用 */
raw: MemberProduct raw: MemberProduct
} }
/** 支付方式类型 */
interface PaymentMethod {
key: string
name: string
icon: string
}
/** FAQ 项类型 */
interface FaqItem {
question: string
answer: string
open: boolean
}
// ==================== 状态 ==================== // ==================== 状态 ====================
/** 当前视图(intro1/intro2/order1/order2/order3 */ /** 当前选中的套餐 key */
const currentView = ref<'intro1' | 'intro2' | 'order1' | 'order2' | 'order3'>('intro2') const selectedPlan = ref('')
/** 当前选中的套餐 */ /** 当前选中的支付方式(wechat / alipay),默认不选 */
const selectedPlan = ref('quarterly') const selectedPayment = ref('')
/** 当前选中的支付方式 */ /** 是否显示扫码支付弹窗 */
const selectedPayment = ref('wechat')
/** 是否同意协议 */
const agreeProtocol = ref(false)
/** 是否显示二维码弹窗 */
const showQrCode = ref(false) const showQrCode = ref(false)
/** 支付宝表单 HTML(用于 iframe 渲染) */ /** 支付宝表单 HTML(用于 iframe 渲染) */
@@ -442,38 +239,29 @@ const wechatCodeUrl = ref('')
/** 微信二维码 iframe 的 srcdoc HTML */ /** 微信二维码 iframe 的 srcdoc HTML */
const wechatQrHtml = computed(() => { const wechatQrHtml = computed(() => {
if (!wechatCodeUrl.value) return '' if (!wechatCodeUrl.value) return ''
return `<!DOCTYPE html><html><head><meta charset="UTF-8"><script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"><\/script><style>body{display:flex;align-items:center;justify-content:center;margin:0;height:100%;}</style></head><body><div id="qrcode"></div><script>new QRCode(document.getElementById("qrcode"),{text:"${wechatCodeUrl.value}",width:200,height:200,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.H});<\/script></body></html>` return `<!DOCTYPE html><html><head><meta charset="UTF-8"><script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"><\/script><style>body{display:flex;align-items:center;justify-content:center;margin:0;height:100%;}</style></head><body><div id="qrcode"></div><script>new QRCode(document.getElementById("qrcode"),{text:"${wechatCodeUrl.value}",width:210,height:210,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.H});<\/script></body></html>`
}) })
/** 当前订单ID(用于后续轮询支付状态) */ /** 当前订单ID */
const orderId = ref('') const orderId = ref('')
/** 是否正在创建订单 */ /** 是否正在创建订单 */
const creatingOrder = ref(false) const creatingOrder = ref(false)
/** 协议预览弹窗显示状态 */
const showAgreementDialog = ref(false)
// ==================== 监听 ====================
/** 监听弹窗开关 — 打开时锁定背景滚动,关闭时恢复并重置状态 */ /** 监听弹窗开关 — 打开时锁定背景滚动,关闭时恢复并重置状态 */
watch(() => props.modelValue, (val) => { watch(() => props.modelValue, (val) => {
document.body.style.overflow = val ? 'hidden' : '' document.body.style.overflow = val ? 'hidden' : ''
if (!val) { if (!val) {
currentView.value = 'intro2'
showQrCode.value = false showQrCode.value = false
agreeProtocol.value = false selectedPayment.value = ''
paymentFormHtml.value = '' paymentFormHtml.value = ''
wechatCodeUrl.value = '' wechatCodeUrl.value = ''
orderId.value = '' orderId.value = ''
// 清除轮询计时器
if (pollTimer) {
clearInterval(pollTimer)
pollTimer = null
}
}
})
/** 监听二维码弹窗关闭 — 无论何种方式关闭都停止轮询 */
watch(showQrCode, (val) => {
if (!val && pollTimer) {
clearInterval(pollTimer)
pollTimer = null
} }
}) })
@@ -481,37 +269,54 @@ watch(showQrCode, (val) => {
/** 当前选中的套餐对象 */ /** 当前选中的套餐对象 */
const currentPlan = computed(() => { const currentPlan = computed(() => {
return plans.value.find(p => p.key === selectedPlan.value) || plans.value[0] return plans.value.find(p => p.key === selectedPlan.value) || plans.value[0] || { priceInt: '0', name: '', key: '', originalPrice: '', dailyDesc: '', raw: {} as MemberProduct }
}) })
// ==================== 常量数据 ==================== // ==================== 数据 ====================
/** 套餐列表(从接口获取) */ /** 套餐列表(从接口获取) */
const plans = ref<PlanItem[]>([]) const plans = ref<PlanItem[]>([])
/** 根据有效天数生成单位描述 */
function getDurationUnit(days: number): string {
if (days <= 7) return '周'
if (days <= 31) return '月'
if (days <= 93) return '3个月'
if (days <= 186) return '半年'
return '年'
}
/** 将后端商品数据转换为前端套餐项 */ /** 将后端商品数据转换为前端套餐项 */
function mapProductToPlan(product: MemberProduct): PlanItem { function mapProductToPlan(product: MemberProduct): PlanItem {
const priceYuan = (product.price / 100).toFixed(2) // 分转元,直接除以100不四舍五入
const monthlyPriceYuan = product.monthlyPrice ? (product.monthlyPrice / 100).toFixed(2) : undefined const priceYuan = product.price / 100
const originalPriceYuan = product.originalPrice ? product.originalPrice / 100 : 0
// 直接使用接口返回的 productName 作为套餐名称
const name = product.productName
// 根据天数计算每日价格描述
let dailyDesc = '轻量体验'
if (product.durationDays <= 7) {
dailyDesc = '轻量体验'
} else if (product.durationDays <= 31) {
// 月卡除以30天,四舍五入到小数点后2位
const dailyPrice = (priceYuan / 30).toFixed(2)
dailyDesc = `低至 ¥${dailyPrice} /天`
} else if (product.durationDays <= 93) {
// 季卡除以90天,四舍五入到小数点后2位
const dailyPrice = (priceYuan / 90).toFixed(2)
dailyDesc = `低至 ¥${dailyPrice} /天`
} else if (product.durationDays <= 186) {
const dailyPrice = (priceYuan / 180).toFixed(2)
dailyDesc = `低至 ¥${dailyPrice} /天`
} else {
const dailyPrice = (priceYuan / 365).toFixed(2)
dailyDesc = `低至 ¥${dailyPrice} /天`
}
// 价格显示:如果是整数就不显示小数,否则保留实际小数
const priceDisplay = Number.isInteger(priceYuan) ? String(priceYuan) : priceYuan.toFixed(2)
const originalDisplay = originalPriceYuan > priceYuan
? `¥${Number.isInteger(originalPriceYuan) ? originalPriceYuan : originalPriceYuan.toFixed(2)}`
: ''
return { return {
key: String(product.id), key: String(product.id),
name: product.productName, name,
price: priceYuan, priceInt: priceDisplay,
unit: getDurationUnit(product.durationDays), originalPrice: originalDisplay,
tag: product.tag || undefined, dailyDesc,
recommend: product.isFeatured === 1, recommend: product.isFeatured === 1,
perMonth: monthlyPriceYuan,
btnText: product.buyButtonText || '立即开通',
orderDesc: product.tag || product.productName,
raw: product, raw: product,
} }
} }
@@ -521,10 +326,9 @@ async function loadMemberProducts() {
try { try {
const res = await fetchMemberProductList() const res = await fetchMemberProductList()
if (res.data && res.data.length) { if (res.data && res.data.length) {
// 按 sortOrder 排序
const sorted = [...res.data].sort((a, b) => a.sortOrder - b.sortOrder) const sorted = [...res.data].sort((a, b) => a.sortOrder - b.sortOrder)
plans.value = sorted.map(mapProductToPlan) plans.value = sorted.map(mapProductToPlan)
// 默认选中主推套餐,若无则选第一个 // 默认选中推荐套餐(季卡),若无则选第一个
const featured = plans.value.find(p => p.recommend) const featured = plans.value.find(p => p.recommend)
selectedPlan.value = featured ? featured.key : plans.value[0].key selectedPlan.value = featured ? featured.key : plans.value[0].key
} }
@@ -538,84 +342,37 @@ onMounted(() => {
loadMemberProducts() loadMemberProducts()
}) })
/** 求职加速能力列表 */
const abilities = [
{ icon: '◉', color: '#4FC2C9', text: '个性化岗位匹配,量大极速又精准' },
{ icon: '◉', color: '#4FC2C9', text: '针对岗位 JD 自动优化简历,提高简历通过率' },
{ icon: '⚡', color: '#4FC2C9', text: '自动填写申请表,每次投递节省 10-15 分钟' },
{ icon: '↗', color: '#E85635', text: '内推码,提高简历被 HR 看到的概率' },
{ icon: '⏰', color: '#777777', text: '第一时间提醒高匹配岗位,抢占面试机会' },
]
/** 会员能力对比数据 */
const compareData = [
{ name: 'AI 岗位匹配', free: '免费 3 天', pro: '不限次数' },
{ name: '简历针对性优化', free: '免费 3 天', pro: '不限次数' },
{ name: '自动填写网申', free: '免费 3 天', pro: '不限次数' },
{ name: '内推码获取', free: '免费 3 天', pro: '不限次数' },
{ name: '高匹配岗位推送', free: '免费 3 天', pro: '不限次数' },
]
/** 支付方式列表 */
const paymentMethods: PaymentMethod[] = [
{ key: 'wechat', name: '微信支付', icon: '💬' },
{ key: 'alipay', name: '支付宝', icon: '🔷' },
]
/** 订单权益列表 */
const orderBenefits = [
'AI 简历深度优化',
'智能岗位匹配分析',
'求职信快速生成',
'面试准备指导',
'投递进度追踪',
'求职策略建议',
]
/** 常见问题列表 */
const faqList = reactive<FaqItem[]>([
{ question: '支付后如何开通?', answer: '支付成功后立即生效,无需等待,登录账号即可使用全部会员权益。', open: true },
{ question: '可以随时取消吗?', answer: '可以,您可以在设置中随时取消自动续费,当前周期内的权益仍然有效。', open: false },
{ question: '支持哪些支付方式?', answer: '目前支持微信支付和支付宝两种支付方式。', open: false },
{ question: '可以开发票吗?', answer: '支持开具电子发票,请在支付完成后联系客服申请。', open: false },
])
// ==================== 事件处理 ==================== // ==================== 事件处理 ====================
/** 从介绍页点击升级按钮 — 进入下单步骤一 */ /** 点击购买会员按钮 */
function handleUpgrade(plan: PlanItem) { async function handleBuy() {
selectedPlan.value = plan.key if (!selectedPayment.value) {
currentView.value = 'order1' ElMessage.warning('请选择付款方式')
} return
}
/** 点击立即开启求职加速 — 调用创建订单接口并显示支付弹窗 */ if (creatingOrder.value) return
async function handleShowQrCode() {
if (!agreeProtocol.value || creatingOrder.value) return
const plan = currentPlan.value const plan = currentPlan.value
if (!plan) return if (!plan || !plan.raw) return
creatingOrder.value = true creatingOrder.value = true
try { try {
// 支付渠道:1=微信 2=支付宝
const payChannel = selectedPayment.value === 'alipay' ? 2 : 1 const payChannel = selectedPayment.value === 'alipay' ? 2 : 1
const res = await createMemberOrder({ const res = await createMemberOrder({
productId: plan.key, // key 即为商品 id(字符串,避免大数精度丢失) productId: plan.key,
payChannel, payChannel,
}) })
if (res.data) { if (res.data) {
orderId.value = String(res.data.orderId) orderId.value = String(res.data.orderId)
if (selectedPayment.value === 'wechat') { if (selectedPayment.value === 'wechat') {
// 微信支付 — payData 为 code_url,用于生成二维码
wechatCodeUrl.value = res.data.payData wechatCodeUrl.value = res.data.payData
paymentFormHtml.value = '' paymentFormHtml.value = ''
} else { } else {
// 支付宝 — payData 为 HTML 表单
paymentFormHtml.value = res.data.payData paymentFormHtml.value = res.data.payData
wechatCodeUrl.value = '' wechatCodeUrl.value = ''
} }
showQrCode.value = true showQrCode.value = true
// 创建订单成功后立即开始轮询支付状态 // 【注释掉自动轮询,后续如需恢复取消注释即可】
confirmPayment() // confirmPaymentPolling()
} }
} catch (e) { } catch (e) {
console.error('创建订单失败', e) console.error('创建订单失败', e)
@@ -624,80 +381,80 @@ async function handleShowQrCode() {
} }
} }
/** 轮询计时器 */ // /** 轮询计时器 */
let pollTimer: ReturnType<typeof setInterval> | null = null // let pollTimer: ReturnType<typeof setInterval> | null = null
/** 轮询查询订单支付状态 */ // /**
async function confirmPayment() { // * 自动轮询查询订单支付状态(已关闭,保留逻辑防止后续要用)
const startTime = Date.now() // * 支付成功后自动关闭弹窗并刷新会员状态
/** 超时时间:5 分钟 */ // */
const timeout = 5 * 60 * 1000 // async function confirmPaymentPolling() {
/** 轮询间隔:500ms */ // const startTime = Date.now()
const interval = 500 // const timeout = 5 * 60 * 1000 // 5分钟超时
// const interval = 500
//
// if (pollTimer) {
// clearInterval(pollTimer)
// pollTimer = null
// }
//
// pollTimer = setInterval(async () => {
// try {
// const res = await fetchOrderDetail(orderId.value)
// const status = res.data?.status
//
// if (status === 1) {
// clearInterval(pollTimer!)
// pollTimer = null
// showQrCode.value = false
// ElMessage.success('开通成功')
// emit('update:modelValue', false)
// store.dispatch('loadMemberStatus')
// } else if (status === 2 || status === 3) {
// clearInterval(pollTimer!)
// pollTimer = null
// showQrCode.value = false
// ElMessage.error('支付未成功,请重新下单')
// } else if (Date.now() - startTime >= timeout) {
// clearInterval(pollTimer!)
// pollTimer = null
// showQrCode.value = false
// ElMessage.error('支付超时,请重新下单')
// }
// } catch (e) {
// console.error('查询订单状态失败', e)
// }
// }, interval)
// }
// 清除可能存在的旧计时器 /** 点击"已支付成功"按钮 — 手动查询一次订单状态 */
if (pollTimer) { async function handleConfirmPaid() {
clearInterval(pollTimer) if (!orderId.value) return
pollTimer = null try {
} const res = await fetchOrderDetail(orderId.value)
const status = res.data?.status
pollTimer = setInterval(async () => { if (status === 1) {
try { // 支付成功 — 关闭弹窗,刷新会员状态
const res = await fetchOrderDetail(orderId.value) showQrCode.value = false
const status = res.data?.status ElMessage.success('开通成功')
emit('update:modelValue', false)
if (status === 1) { // 刷新 vuex 中的会员状态
// 支付成功 — 关闭二维码弹窗,进入完成步骤 store.dispatch('loadMemberStatus')
clearInterval(pollTimer!) } else {
pollTimer = null // 未支付成功
showQrCode.value = false ElMessage.warning('未查询到支付订单,请再次确认')
currentView.value = 'order3'
} else if (status === 2 || status === 3) {
// 已退款或已关闭 — 提示并回到选择套餐步骤
clearInterval(pollTimer!)
pollTimer = null
showQrCode.value = false
ElMessage.error('支付未成功,请重新下单')
currentView.value = 'order1'
} else if (Date.now() - startTime >= timeout) {
// 超时 — 提示并回到选择套餐步骤
clearInterval(pollTimer!)
pollTimer = null
showQrCode.value = false
ElMessage.error('支付未成功,请重新下单')
currentView.value = 'order1'
}
// status === 0 继续等待
} catch (e) {
console.error('查询订单状态失败', e)
} }
}, interval) } catch (e) {
console.error('查询订单状态失败', e)
ElMessage.error('查询失败,请稍后重试')
}
} }
/** 支付成功后跳转简历页 */ /** 关闭扫码支付弹窗 */
function handleGoResume() {
router.push('/resume')
}
/** 关闭二维码弹窗 — 停止轮询 */
function handleCloseQrCode() { function handleCloseQrCode() {
showQrCode.value = false showQrCode.value = false
if (pollTimer) {
clearInterval(pollTimer)
pollTimer = null
}
} }
/** 查看会员权益 — 关闭会员弹窗,打开设置弹窗的会员 Tab */
function handleViewMemberBenefits() {
emit('update:modelValue', false)
store.commit('SET_SETTINGS_TAB', 'member')
store.commit('SET_SHOW_SETTINGS', true)
}
/** 协议预览弹窗显示状态 */
const showAgreementDialog = ref(false)
/** 打开会员服务协议 */ /** 打开会员服务协议 */
function openMemberAgreement() { function openMemberAgreement() {
showAgreementDialog.value = true showAgreementDialog.value = true
+137 -62
View File
@@ -19,26 +19,9 @@
:class="{ 'settings-dialog__nav-item--active': activeTab === tab.key }" :class="{ 'settings-dialog__nav-item--active': activeTab === tab.key }"
@click="activeTab = tab.key" @click="activeTab = tab.key"
> >
<span class="settings-dialog__nav-icon">{{ tab.icon }}</span>
<span>{{ tab.label }}</span> <span>{{ tab.label }}</span>
</div> </div>
</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> </div>
<!-- 右侧内容区 根据 activeTab 切换显示 --> <!-- 右侧内容区 根据 activeTab 切换显示 -->
@@ -51,60 +34,70 @@
<p class="settings-dialog__section-value">{{ userPhone }}</p> <p class="settings-dialog__section-value">{{ userPhone }}</p>
</div> </div>
<div class="settings-dialog__danger-section"> <div class="settings-dialog__danger-section">
<div class="settings-dialog__danger-title">注销我的账号</div> <div>
<div class="settings-dialog__danger-row"> <div class="settings-dialog__danger-title">注销我的账号</div>
<p class="settings-dialog__danger-desc">永久删除你的账号及所有相关数据</p> <div class="settings-dialog__danger-row">
<button class="settings-dialog__danger-btn" @click="handleDeleteAccount">注销账号</button> <p class="settings-dialog__danger-desc">永久删除你的账号及所有相关数据</p>
</div>
</div> </div>
<button class="settings-dialog__danger-btn" @click="handleDeleteAccount">注销账号</button>
</div> </div>
</template> </template>
<!-- Tab: 会员 显示当前会员信息订阅管理 --> <!-- Tab: 会员 根据会员类型显示不同卡片 -->
<template v-if="activeTab === 'member'"> <template v-if="activeTab === 'member'">
<h2 class="settings-dialog__content-title">会员</h2> <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 v-if="memberStatus.isMember && memberStatus.memberType === 1" class="settings-dialog__mcard settings-dialog__mcard--formal">
<div class="settings-dialog__member-title-row"> <div class="settings-dialog__mcard-top">
<span class="settings-dialog__member-name">正式会员</span> <span class="settings-dialog__mcard-title">正式会员</span>
<span <div class="settings-dialog__mcard-remain">
class="settings-dialog__member-badge" <span class="settings-dialog__mcard-remain-label">剩余</span>
:class="{ 'settings-dialog__member-badge--inactive': !memberStatus.isMember }" <span class="settings-dialog__mcard-remain-days">{{ memberRemainDays }}</span>
>
{{ memberStatus.isMember ? '已开通' : '未开通' }}
</span>
</div> </div>
<span class="settings-dialog__member-terms" @click="openAgreementDialog('ae8065i3')">会员条款</span>
</div> </div>
<div class="settings-dialog__member-info-row"> <div class="settings-dialog__mcard-bottom">
<!-- 已开通显示到期时间和剩余天数 --> <span class="settings-dialog__mcard-expire">有效期至{{ memberExpireDate }}</span>
<span v-if="memberStatus.isMember" class="settings-dialog__member-price"> <div class="settings-dialog__mcard-actions">
<span class="settings-dialog__member-expire-line">到期时间{{ memberExpireDateTime }}</span> <button class="settings-dialog__mcard-btn settings-dialog__mcard-btn--outline" @click="openAgreementDialog('ae8065i3')">查看权益</button>
<span class="settings-dialog__member-remain-line">剩余 {{ memberRemainDays }} </span> <button class="settings-dialog__mcard-btn settings-dialog__mcard-btn--solid-formal" @click="handleOpenMember">续费会员</button>
</span> </div>
<!-- 未开通显示价格 -->
<span v-else class="settings-dialog__member-price">
¥19.99/
</span>
</div> </div>
</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 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> </div>
</template> </template>
<!-- Tab: 岗位更新提醒 目标岗位即时提醒开关提醒频率 -->
<template v-if="activeTab === 'reminder'"> <template v-if="activeTab === 'reminder'">
<!-- <h2 class="settings-dialog__content-title">岗位更新提醒</h2>-->
<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">
<div class="settings-dialog__reminder-block-title-row"> <div class="settings-dialog__reminder-block-title-row">
@@ -167,6 +160,41 @@
<!-- </div>--> <!-- </div>-->
<!-- </div>--> <!-- </div>-->
</template> </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 渲染 --> <!-- 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>
@@ -201,6 +229,9 @@
<!-- 协议预览弹窗 --> <!-- 协议预览弹窗 -->
<AgreementPreviewDialog v-model="showAgreementDialog" :code="currentAgreementCode" /> <AgreementPreviewDialog v-model="showAgreementDialog" :code="currentAgreementCode" />
<!-- 会员购买弹窗 -->
<MemberDialog v-model="showMemberDialog" />
</Teleport> </Teleport>
</template> </template>
@@ -212,6 +243,7 @@ import { formatEmploymentType } from '@/stores/index'
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 { fetchAgreement } from '@/api/common'
import { userFeedback } from '@/api/setting'
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'
@@ -219,6 +251,7 @@ 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 MemberDialog from './MemberDialog.vue'
import AgreementPreviewDialog from '@/components/tools/AgreementPreviewDialog.vue' import AgreementPreviewDialog from '@/components/tools/AgreementPreviewDialog.vue'
// @ts-ignore // @ts-ignore
import markdownit from 'markdown-it' import markdownit from 'markdown-it'
@@ -238,9 +271,11 @@ const store = useStore()
/** 左侧导航 Tab 配置 */ /** 左侧导航 Tab 配置 */
const tabs = [ const tabs = [
{ key: 'account', label: '账号与安全', icon: '👤' }, { key: 'account', label: '账号与安全' },
{ key: 'member', label: '会员', icon: '🏅' }, { key: 'member', label: '会员' },
// { key: 'reminder', label: '目标岗位设置', icon: '🔔' }, { key: 'feedback', label: '问题反馈' },
// { key: 'reminder', label: '目标岗位设置' },
{ key: 'privacy', label: '用户隐私协议' },
] ]
/** 当前选中的 Tab */ /** 当前选中的 Tab */
@@ -281,6 +316,18 @@ const showDeleteAccount = ref(false)
/** 邀请注册弹窗显示状态 */ /** 邀请注册弹窗显示状态 */
const showInviteDialog = 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 showAgreementDialog = ref(false)
/** 当前预览的协议码 */ /** 当前预览的协议码 */
@@ -318,14 +365,16 @@ async function loadPrivacyAgreement() {
/** 会员状态数据 */ /** 会员状态数据 */
const memberStatus = reactive<MemberStatus>({ const memberStatus = reactive<MemberStatus>({
isMember: false, isMember: false,
memberType: undefined,
expireTime: undefined, expireTime: undefined,
createTime: undefined, createTime: undefined,
updateTime: undefined, updateTime: undefined,
}) })
/** 会员到期时间(格式化显示 */ /** 会员到期日期(格式yyyy-MM-dd */
const memberExpireDateTime = computed(() => { const memberExpireDate = computed(() => {
return timestampToLocalDateTime(memberStatus.expireTime, 'returnMinute') // timestampToLocalDateTime returnDay 返回 yyyy/MM/dd,替换为 yyyy-MM-dd
return timestampToLocalDateTime(memberStatus.expireTime, 'returnDay').replace(/\//g, '-')
}) })
/** 会员剩余天数 */ /** 会员剩余天数 */
@@ -339,6 +388,7 @@ const loadMemberStatus = async () => {
const res = await fetchMemberStatus() const res = await fetchMemberStatus()
if (res.data) { if (res.data) {
memberStatus.isMember = res.data.isMember ?? false memberStatus.isMember = res.data.isMember ?? false
memberStatus.memberType = res.data.memberType
memberStatus.expireTime = res.data.expireTime memberStatus.expireTime = res.data.expireTime
memberStatus.createTime = res.data.createTime memberStatus.createTime = res.data.createTime
memberStatus.updateTime = res.data.updateTime memberStatus.updateTime = res.data.updateTime
@@ -376,6 +426,31 @@ const handleEditTarget = () => {
showGoalDialog.value = true 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) => { watch(() => props.modelValue, (val) => {
if (val && store.state.isAuthenticated) { if (val && store.state.isAuthenticated) {
+41 -41
View File
@@ -3,55 +3,55 @@
<Teleport to="body"> <Teleport to="body">
<div v-if="modelValue" class="settings-invite-overlay" @click="$emit('update:modelValue', false)"> <div v-if="modelValue" class="settings-invite-overlay" @click="$emit('update:modelValue', false)">
<div class="settings-invite-dialog" @click.stop> <div class="settings-invite-dialog" @click.stop>
<!-- 顶部渐变区域 --> <!-- 顶部标题 + 关闭按钮 -->
<div class="settings-invite-dialog__header"> <div class="settings-invite-dialog__header">
<!-- 皇冠图标 --> <div class="settings-invite-dialog__header-text">
<span class="settings-invite-dialog__crown"></span> <h2 class="settings-invite-dialog__title">邀请好友免费获取 7 天会员</h2>
<p class="settings-invite-dialog__subtitle">好友通过你的链接注册并完成认证 3 天会员</p>
</div>
<!-- 关闭按钮 -->
<span class="settings-invite-dialog__close" @click="$emit('update:modelValue', false)"></span>
</div> </div>
<!-- 关闭按钮 --> <!-- 邀请链接区域 -->
<span class="settings-invite-dialog__close" @click="$emit('update:modelValue', false)"></span> <div class="settings-invite-dialog__link-box">
<span class="settings-invite-dialog__link-text">{{ inviteUrl }}</span>
<span class="settings-invite-dialog__copy-btn" @click="handleCopy">复制</span>
</div>
<!-- 内容区域 --> <!-- 邀请步骤区域 -->
<div class="settings-invite-dialog__body"> <div class="settings-invite-dialog__steps">
<!-- 标题 --> <span class="settings-invite-dialog__steps-title">邀请步骤</span>
<h2 class="settings-invite-dialog__title">邀请好友免费获取 7 天会员</h2> <div class="settings-invite-dialog__steps-list">
<!-- 副标题 --> <!-- 步骤1 -->
<p class="settings-invite-dialog__subtitle">好友通过你的链接注册并完成认证 3 天会员</p> <div class="settings-invite-dialog__step-item">
<span class="settings-invite-dialog__step-num">1</span>
<!-- 邀请链接区域 --> <span class="settings-invite-dialog__step-text">分享专属邀请链接给好友</span>
<div class="settings-invite-dialog__link-box">
<span class="settings-invite-dialog__link-text">{{ inviteUrl }}</span>
<button class="settings-invite-dialog__copy-btn" @click="handleCopy">复制链接</button>
</div>
<!-- 步骤说明 -->
<div class="settings-invite-dialog__step">
<div class="settings-invite-dialog__step-circle">1</div>
<span class="settings-invite-dialog__step-text">分享专属邀请链接给好友</span>
</div>
<div class="settings-invite-dialog__step">
<div class="settings-invite-dialog__step-circle">2</div>
<span class="settings-invite-dialog__step-text">好友完成注册并通过认证</span>
</div>
<div class="settings-invite-dialog__step">
<div class="settings-invite-dialog__step-circle">3</div>
<span class="settings-invite-dialog__step-text">获得 7 天会员奖励</span>
</div>
<!-- 底部统计 + 按钮 -->
<div class="settings-invite-dialog__footer">
<div class="settings-invite-dialog__stat">
<span class="settings-invite-dialog__stat-label">已邀请</span>
<span class="settings-invite-dialog__stat-value">{{ totalFriends }}</span>
</div> </div>
<div class="settings-invite-dialog__stat"> <!-- 步骤2 -->
<span class="settings-invite-dialog__stat-label">累计获得</span> <div class="settings-invite-dialog__step-item">
<span class="settings-invite-dialog__stat-value">{{ totalDays }}</span> <span class="settings-invite-dialog__step-num">2</span>
<span class="settings-invite-dialog__step-text">好友完成注册并通过认证</span>
</div>
<!-- 步骤3 -->
<div class="settings-invite-dialog__step-item">
<span class="settings-invite-dialog__step-num">3</span>
<span class="settings-invite-dialog__step-text">获得 7 天会员奖励</span>
</div> </div>
<button class="settings-invite-dialog__member-btn" @click="handleGoMember">成为正式会员</button>
</div> </div>
</div> </div>
<!-- 底部统计 + 按钮 -->
<div class="settings-invite-dialog__footer">
<div class="settings-invite-dialog__stat">
<span class="settings-invite-dialog__stat-line">已邀请<em>{{ totalFriends }}</em></span>
<span class="settings-invite-dialog__stat-line">累计获得<em>{{ totalDays }}</em></span>
</div>
<button class="settings-invite-dialog__member-btn" @click="handleGoMember">
<span>成为正式会员</span>
<span class="settings-invite-dialog__member-btn-arrow"></span>
</button>
</div>
</div> </div>
</div> </div>
</Teleport> </Teleport>
+243 -76
View File
@@ -24,72 +24,83 @@
</a> </a>
</nav> </nav>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_151_276)">
<path fill="currentColor" stroke="currentColor" stroke-width="0.2" d="M7.66664 2.16675C6.78388 2.16681 5.91411 2.37934 5.13081 2.7864C4.34751 3.19346 3.6737 3.78309 3.16631 4.50545C2.65891 5.22782 2.33285 6.0617 2.21567 6.93664C2.09849 7.81158 2.19362 8.70188 2.49305 9.5323C2.79247 10.3627 3.28738 11.1089 3.93596 11.7077C4.58454 12.3065 5.36773 12.7405 6.21936 12.9728C7.07099 13.2052 7.96603 13.2291 8.82886 13.0426C9.6917 12.8561 10.497 12.4647 11.1766 11.9014L12.962 13.6867C13.0568 13.7751 13.1821 13.8232 13.3117 13.8209C13.4412 13.8186 13.5648 13.7661 13.6564 13.6745C13.748 13.5829 13.8005 13.4593 13.8028 13.3298C13.805 13.2002 13.757 13.0749 13.6686 12.9801L11.8853 11.1961C12.5567 10.3936 12.9853 9.41624 13.1208 8.37872C13.2562 7.34121 13.0929 6.28658 12.65 5.33862C12.2071 4.39066 11.503 3.58871 10.6203 3.02688C9.73759 2.46506 8.71296 2.16667 7.66664 2.16675Z" />
</g>
<defs>
<clipPath id="clip0_151_276">
<rect width="16" height="16" fill="none"/>
</clipPath>
</defs>
</svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_151_281)">
<path fill="currentColor" stroke="currentColor" stroke-width="0.2" d="M10.5 8.66675C10.5 8.53414 10.4473 8.40696 10.3536 8.31319C10.2598 8.21943 10.1326 8.16675 10 8.16675H6C5.86739 8.16675 5.74022 8.21943 5.64645 8.31319C5.55268 8.40696 5.5 8.53414 5.5 8.66675C5.5 8.79936 5.55268 8.92653 5.64645 9.0203C5.74022 9.11407 5.86739 9.16675 6 9.16675H10C10.1326 9.16675 10.2598 9.11407 10.3536 9.0203C10.4473 8.92653 10.5 8.79936 10.5 8.66675ZM10.5 11.3334C10.5 11.2008 10.4473 11.0736 10.3536 10.9799C10.2598 10.8861 10.1326 10.8334 10 10.8334H6C5.86739 10.8334 5.74022 10.8861 5.64645 10.9799C5.55268 11.0736 5.5 11.2008 5.5 11.3334C5.5 11.466 5.55268 11.5932 5.64645 11.687C5.74022 11.7807 5.86739 11.8334 6 11.8334H10C10.1326 11.8334 10.2598 11.7807 10.3536 11.687C10.4473 11.5932 10.5 11.466 10.5 11.3334Z"/>
<path fill="currentColor" stroke="currentColor" stroke-width="0.1" fill-rule="evenodd" clip-rule="evenodd" d="M4.66671 1.5C4.18048 1.5 3.71416 1.69315 3.37034 2.03697C3.02653 2.38079 2.83337 2.8471 2.83337 3.33333V12.6667C2.83337 13.1529 3.02653 13.6192 3.37034 13.963C3.71416 14.3068 4.18048 14.5 4.66671 14.5H11.3334C11.8196 14.5 12.2859 14.3068 12.6297 13.963C12.9736 13.6192 13.1667 13.1529 13.1667 12.6667V5.312C13.1667 5.058 13.084 4.81133 12.9307 4.60867L10.932 1.96333C10.8233 1.81941 10.6827 1.70267 10.5212 1.62227C10.3597 1.54188 10.1818 1.50002 10.0014 1.5H4.66671ZM3.83337 3.33333C3.83337 2.87333 4.20671 2.5 4.66671 2.5H9.50004V5.43133C9.50004 5.70733 9.72404 5.93133 10 5.93133H12.1667V12.6667C12.1667 13.1267 11.7934 13.5 11.3334 13.5H4.66671C4.20671 13.5 3.83337 13.1267 3.83337 12.6667V3.33333Z" />
</g>
<defs>
<clipPath id="clip0_151_281">
<rect width="16" height="16" fill="currentColor"/>
</clipPath>
</defs>
</svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_151_287)">
<path fill="currentColor" stroke="currentColor" stroke-width="0.2" d="M13.8087 5.41874L13.6447 5.79607C13.6191 5.85751 13.5758 5.91 13.5204 5.94692C13.465 5.98384 13.4 6.00354 13.3334 6.00354C13.2668 6.00354 13.2017 5.98384 13.1463 5.94692C13.0909 5.91 13.0477 5.85751 13.022 5.79607L12.858 5.41874C12.5697 4.75122 12.0416 4.21602 11.378 3.91874L10.872 3.69274C10.8107 3.6645 10.7586 3.61925 10.7222 3.56236C10.6857 3.50546 10.6664 3.43931 10.6664 3.37174C10.6664 3.30417 10.6857 3.23801 10.7222 3.18112C10.7586 3.12422 10.8107 3.07897 10.872 3.05074L11.35 2.83807C12.0303 2.53232 12.5674 1.97732 12.8507 1.2874L13.0194 0.88007C13.0441 0.816972 13.0874 0.762802 13.1434 0.72462C13.1994 0.686438 13.2656 0.666016 13.3334 0.666016C13.4012 0.666016 13.4674 0.686438 13.5234 0.72462C13.5794 0.762802 13.6226 0.816972 13.6474 0.88007L13.816 1.28674C14.099 1.97679 14.6359 2.53202 15.316 2.83807L15.7947 3.0514C15.8559 3.07972 15.9077 3.12496 15.944 3.18179C15.9804 3.23861 15.9997 3.30463 15.9997 3.37207C15.9997 3.43951 15.9804 3.50553 15.944 3.56235C15.9077 3.61917 15.8559 3.66442 15.7947 3.69274L15.288 3.91807C14.6246 4.21565 14.0968 4.75109 13.8087 5.41874ZM6.66671 2.00007H9.33337V3.3334H6.66671C5.60584 3.3334 4.58843 3.75483 3.83828 4.50498C3.08813 5.25512 2.66671 6.27254 2.66671 7.3334C2.66671 9.74007 4.30804 11.3107 8.00004 12.9867V11.3334H9.33337C10.3942 11.3334 11.4117 10.912 12.1618 10.1618C12.9119 9.41168 13.3334 8.39427 13.3334 7.3334H14.6667C14.6667 8.74789 14.1048 10.1044 13.1046 11.1046C12.1044 12.1048 10.7479 12.6667 9.33337 12.6667V15.0001C6.00004 13.6667 1.33337 11.6667 1.33337 7.3334C1.33337 5.91892 1.89528 4.56236 2.89547 3.56217C3.89567 2.56197 5.25222 2.00007 6.66671 2.00007Z"/>
</g>
<defs>
<clipPath id="clip0_151_287">
<rect width="16" height="16" fill="currentColor"/>
</clipPath>
</defs>
</svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_151_292)">
<path fill="none" stroke="currentColor" stroke-width="0.2" d="M14.3333 5.29864V10.7013C14.3328 11.0668 14.2604 11.4286 14.1189 11.7656C13.9775 12.1027 13.7705 12.4083 13.51 12.6646C12.982 13.1846 12.2666 13.478 11.52 13.478H4.48063C3.7362 13.4788 3.02136 13.1866 2.49063 12.6646C1.96329 12.144 1.66663 11.438 1.66663 10.7013V5.29864C1.66704 4.93317 1.74003 4.57143 1.88137 4.23439C2.0227 3.89736 2.22957 3.59175 2.48996 3.33531C3.02069 2.8133 3.73554 2.52113 4.47996 2.52197H11.5193C12.2637 2.52113 12.9786 2.8133 13.5093 3.33531C13.7698 3.5917 13.9768 3.89728 14.1182 4.23432C14.2597 4.57135 14.3328 4.93313 14.3333 5.29864Z" stroke-linecap="round" stroke-linejoin="round"/>
<path fill="none" stroke="currentColor" stroke-width="0.2" d="M7.75534 10.6454C7.75534 10.1041 7.53934 9.5854 7.15401 9.20207C6.76801 8.81817 6.24574 8.60267 5.70134 8.60267C5.15694 8.60267 4.63467 8.81817 4.24867 9.20207C4.05815 9.39107 3.90693 9.6159 3.80372 9.86362C3.70052 10.1113 3.64737 10.377 3.64734 10.6454M9.71201 5.94607H12.108M9.71201 8.00007H10.7387M9.71201 10.0541H12.108M5.70134 8.0934C6.06451 8.0934 6.4128 7.94914 6.6696 7.69234C6.9264 7.43554 7.07067 7.08724 7.07067 6.72407C7.07067 6.3609 6.9264 6.01261 6.6696 5.75581C6.4128 5.49901 6.06451 5.35474 5.70134 5.35474C5.33817 5.35474 4.98987 5.49901 4.73307 5.75581C4.47627 6.01261 4.33201 6.3609 4.33201 6.72407C4.33201 7.08724 4.47627 7.43554 4.73307 7.69234C4.98987 7.94914 5.33817 8.0934 5.70134 8.0934Z" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_151_292">
<rect width="16" height="16" fill="currentColor"/>
</clipPath>
</defs>
</svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_151_299)">
<path fill="currentColor" stroke="currentColor" stroke-width="0.2" d="M6.07128 3.63192C6.46995 2.46525 8.08195 2.42992 8.55461 3.52592L8.59461 3.63259L9.13262 5.20592C9.25591 5.56674 9.45515 5.89693 9.7169 6.1742C9.97865 6.45148 10.2968 6.66939 10.6499 6.81325L10.7946 6.86725L12.3679 7.40459C13.5346 7.80325 13.5699 9.41525 12.4746 9.88792L12.3679 9.92792L10.7946 10.4659C10.4337 10.5891 10.1034 10.7883 9.82596 11.0501C9.54856 11.3118 9.33055 11.6301 9.18661 11.9833L9.13262 12.1273L8.59528 13.7013C8.19661 14.8679 6.58461 14.9033 6.11261 13.8079L6.07128 13.7013L5.53395 12.1279C5.41073 11.767 5.21153 11.4367 4.94978 11.1593C4.68802 10.8819 4.36981 10.6638 4.01661 10.5199L3.87261 10.4659L2.29928 9.92859C1.13195 9.52992 1.09661 7.91792 2.19261 7.44592L2.29928 7.40459L3.87261 6.86725C4.23344 6.74396 4.56362 6.54471 4.8409 6.28297C5.11817 6.02122 5.33609 5.70305 5.47995 5.34992L5.53395 5.20592L6.07128 3.63192ZM7.33328 4.06259L6.79595 5.63592C6.60821 6.18612 6.30279 6.68879 5.90097 7.10892C5.49915 7.52904 5.01058 7.85654 4.46928 8.06859L4.30261 8.12925L2.72928 8.66659L4.30261 9.20392C4.85282 9.39166 5.35548 9.69707 5.77561 10.0989C6.19574 10.5007 6.52323 10.9893 6.73528 11.5306L6.79595 11.6973L7.33328 13.2706L7.87061 11.6973C8.05835 11.147 8.36377 10.6444 8.76559 10.2243C9.16741 9.80413 9.65598 9.47664 10.1973 9.26459L10.3639 9.20459L11.9373 8.66659L10.3639 8.12925C9.81375 7.94151 9.31108 7.6361 8.89095 7.23428C8.47082 6.83245 8.14333 6.34388 7.93128 5.80259L7.87128 5.63592L7.33328 4.06259ZM12.6666 1.33325C12.7913 1.33325 12.9136 1.36824 13.0194 1.43423C13.1252 1.50023 13.2104 1.59459 13.2653 1.70659L13.2973 1.78459L13.5306 2.46859L14.2153 2.70192C14.3403 2.74438 14.4498 2.823 14.5301 2.9278C14.6104 3.0326 14.6577 3.15886 14.6661 3.2906C14.6746 3.42233 14.6437 3.55361 14.5774 3.66778C14.5112 3.78195 14.4125 3.87388 14.2939 3.93192L14.2153 3.96392L13.5313 4.19725L13.2979 4.88192C13.2554 5.00687 13.1767 5.11638 13.0719 5.19658C12.9671 5.27678 12.8408 5.32405 12.709 5.3324C12.5773 5.34075 12.4461 5.30981 12.3319 5.2435C12.2178 5.17719 12.1259 5.0785 12.0679 4.95992L12.0359 4.88192L11.8026 4.19792L11.1179 3.96459C10.993 3.92212 10.8834 3.84351 10.8031 3.73871C10.7229 3.63391 10.6755 3.50764 10.6671 3.37591C10.6587 3.24417 10.6895 3.1129 10.7558 2.99873C10.8221 2.88456 10.9207 2.79263 11.0393 2.73459L11.1179 2.70259L11.8019 2.46925L12.0353 1.78459C12.0802 1.65287 12.1653 1.53852 12.2785 1.45758C12.3917 1.37664 12.5274 1.33317 12.6666 1.33325Z"/>
</g>
<defs>
<clipPath id="clip0_151_299">
<rect width="16" height="16" fill="currentColor"/>
</clipPath>
</defs>
</svg>
<!-- 底部菜单 --> <!-- 底部菜单 -->
<div class="side-nav__footer"> <div class="side-nav__footer">
<div class="side-nav__item" v-for="item in footerMenus" :key="item.label" @click="item.action?.()"> <!-- <div class="side-nav__item" v-for="item in footerMenus" :key="item.label" @click="item.action?.()">
<span class="side-nav__item-label">{{ item.label }}</span> <span class="side-nav__item-label">{{ item.label }}</span>
<span v-if="item.badge" class="side-nav__badge">{{ item.badge }}</span> <span v-if="item.badge" class="side-nav__badge">{{ item.badge }}</span>
</div> -->
<!-- 邀请会员卡片 -->
<div class="side-nav__referral-card">
<!-- 顶部标题区域 -->
<div class="side-nav__referral-title">邀请好友送 7 天会员</div>
<div class="side-nav__referral-subtitle">好友加入后自动到账</div>
<!-- 白色面板 -->
<div class="side-nav__referral-panel">
<!-- 邀请进度行 -->
<div class="side-nav__referral-row">
<span class="side-nav__referral-label">邀请进度</span>
<span class="side-nav__referral-value">12/30</span>
</div>
<!-- 进度条 -->
<div class="side-nav__referral-track">
<div class="side-nav__referral-fill" style="width: 40%"></div>
</div>
<!-- 提示文字 -->
<div class="side-nav__referral-tip">再邀请 <strong>18</strong> 可获得年会员</div>
<!-- 分隔线 -->
<div class="side-nav__referral-divider"></div>
<!-- 去邀请按钮 -->
<div class="side-nav__referral-action" @click="showShareDialog = true">去邀请</div>
</div>
</div>
<!-- 用户信息模块 -->
<div class="side-nav__user-info-wrapper" v-if="store.state.isAuthenticated && store.state.userInfo">
<!-- 账户弹窗 -->
<div v-if="showAccountPopup" class="side-nav__account-popup">
<!-- 顶部渐变区域会员信息 -->
<div class="side-nav__account-popup-header">
<div class="side-nav__account-popup-header-row">
<div class="side-nav__account-popup-header-left">
<div class="side-nav__account-popup-member-type" :class="accountMemberColorClass">{{ memberTagText }}</div>
<div v-if="memberExpireText" class="side-nav__account-popup-expire">{{ memberExpireText }}</div>
</div>
<div class="side-nav__account-popup-detail" @click="handleAccountDetail">详情></div>
</div>
</div>
<!-- 设置按钮 -->
<div class="side-nav__account-popup-item" @click="handleAccountSettings">设置</div>
<!-- 退出登录按钮 -->
<div class="side-nav__account-popup-item" @click="handleAccountLogout">退出登录</div>
</div>
<!-- 用户信息卡片点击弹出/关闭账户弹窗 -->
<div class="side-nav__user-info" @click.stop="showAccountPopup = !showAccountPopup">
<div class="side-nav__user-left">
<!-- 手机号脱敏显示 -->
<div class="side-nav__user-phone">{{ maskedPhone }}</div>
<!-- 会员标签 -->
<div class="side-nav__user-member-tag" :class="memberTagClass">{{ memberTagText }}</div>
</div>
<!-- 右侧铃铛图标 -->
<div class="side-nav__user-bell" @click.stop="showMessageDialog = true">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_422_1692)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.83126 9.77736L6.60626 5.90736C6.76471 5.1241 7.18922 4.41976 7.80778 3.9138C8.42635 3.40785 9.20088 3.13143 10 3.13143C10.7991 3.13143 11.5737 3.40785 12.1922 3.9138C12.8108 4.41976 13.2353 5.1241 13.3938 5.90736L14.1688 9.77736C14.4013 10.9321 14.9687 11.9929 15.8 12.8274L16.0425 13.0686C16.1318 13.1581 16.1956 13.2697 16.2272 13.3921C16.2589 13.5145 16.2572 13.6431 16.2224 13.7646C16.1876 13.8862 16.1209 13.9962 16.0293 14.0832C15.9376 14.1703 15.8244 14.2313 15.7013 14.2599L14.675 14.4974C11.5989 15.2087 8.40109 15.2087 5.32501 14.4974L4.30001 14.2599C4.17678 14.2315 4.06339 14.1706 3.97161 14.0836C3.87983 13.9966 3.81301 13.8866 3.77806 13.7651C3.74312 13.6435 3.74133 13.5149 3.77288 13.3924C3.80444 13.2699 3.86818 13.1581 3.95751 13.0686L4.20001 12.8274C5.03317 11.9942 5.60081 10.9329 5.83126 9.77736ZM3.99376 9.40987L4.76751 5.53987C5.01082 4.33139 5.66493 3.24439 6.61874 2.46347C7.57256 1.68255 8.76728 1.25586 10 1.25586C11.2327 1.25586 12.4275 1.68255 13.3813 2.46347C14.3351 3.24439 14.9892 4.33139 15.2325 5.53987L16.0075 9.40987C16.165 10.2024 16.555 10.9299 17.1263 11.5011L17.3688 11.7436C17.6951 12.0696 17.9281 12.477 18.0437 12.9236C18.1592 13.3701 18.1531 13.8394 18.0259 14.2828C17.8987 14.7262 17.6551 15.1274 17.3203 15.4447C16.9856 15.7621 16.572 15.984 16.1225 16.0874L15.0963 16.3249C14.2188 16.5249 13.3354 16.674 12.4463 16.7724C12.3274 17.3329 12.0196 17.8356 11.5744 18.1963C11.1292 18.557 10.5736 18.7538 10.0006 18.7538C9.42765 18.7538 8.87205 18.557 8.42685 18.1963C7.98165 17.8356 7.67386 17.3329 7.55501 16.7724C6.66352 16.6757 5.77861 16.5259 4.90501 16.3236L3.87751 16.0861C3.4285 15.9822 3.01545 15.7601 2.68118 15.4428C2.34692 15.1255 2.10361 14.7246 1.97647 14.2816C1.84934 13.8386 1.84301 13.3697 1.95814 12.9234C2.07327 12.4772 2.30568 12.0698 2.63126 11.7436L2.87501 11.4999C3.44626 10.9286 3.83626 10.1999 3.99376 9.40862" fill="#6A7282"/>
</g>
<defs>
<clipPath id="clip0_422_1692">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>
<!-- 未读消息红点 -->
<span v-if="unreadCount > 0" class="side-nav__user-bell-dot"></span>
</div>
</div>
</div> </div>
</div> </div>
@@ -100,7 +111,12 @@
<!-- 弹窗头部 --> <!-- 弹窗头部 -->
<div class="side-nav__message-dialog-header"> <div class="side-nav__message-dialog-header">
<span>消息通知</span> <span>消息通知</span>
<span class="side-nav__dialog-close" @click="showMessageDialog = false"></span> <span class="side-nav__dialog-close" @click="showMessageDialog = false">
<!-- 关闭图标 SVG -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6l12 12" stroke="#6A7282" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
</div> </div>
<!-- 弹窗内容区左列表 + 右详情 --> <!-- 弹窗内容区左列表 + 右详情 -->
<div class="side-nav__message-dialog-content"> <div class="side-nav__message-dialog-content">
@@ -113,12 +129,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"
> >
<div class="dflex wp100 aliite-e fs14"> <!-- 列表项内容未读圆点 + 标题 + 日期 -->
<div class=""> <div class="side-nav__message-list-item-row">
<span class="side-nav__message-list-title">{{ msg.title }}</span> <div class="side-nav__message-list-item-left">
<span v-if="!msg.read" class="side-nav__message-unread-dot"></span> <span v-if="!msg.read" class="side-nav__message-unread-dot"></span>
<span class="side-nav__message-list-title">{{ msg.title }}</span>
</div> </div>
<div class="fs10 color-8 tar">{{timestampToLocalDateTime(msg.createTime, 'returnDay')}}</div> <span class="side-nav__message-list-date">{{ formatMonthDay(msg.createTime) }}</span>
</div> </div>
</div> </div>
<!-- 加载中提示 --> <!-- 加载中提示 -->
@@ -129,11 +146,15 @@
<!-- 右侧消息详情 --> <!-- 右侧消息详情 -->
<div class="side-nav__message-detail"> <div class="side-nav__message-detail">
<template v-if="currentMessage"> <template v-if="currentMessage">
<div class="side-nav__message-detail-title">{{ currentMessage.title }}</div> <!-- 详情头部标题 + 时间 -->
<div class="dflex aliite-e"> <div class="side-nav__message-detail-header">
<div class="side-nav__message-detail-content">{{ currentMessage.content }}</div> <div class="side-nav__message-detail-title">{{ currentMessage.title }}</div>
<div class="fs12 color-8 w140 tar">{{timestampToLocalDateTime(currentMessage.createTime, 'returnSecond')}}</div> <div class="side-nav__message-detail-time">{{ timestampToLocalDateTime(currentMessage.createTime, 'returnSecond') }}</div>
</div> </div>
<!-- 分隔线 -->
<div class="side-nav__message-detail-divider"></div>
<!-- 消息正文 -->
<div class="side-nav__message-detail-content">{{ currentMessage.content }}</div>
</template> </template>
<div v-else class="side-nav__message-detail-empty">请选择一条消息查看</div> <div v-else class="side-nav__message-detail-empty">请选择一条消息查看</div>
</div> </div>
@@ -182,6 +203,14 @@
</Teleport> </Teleport>
<!-- 设置弹窗 --> <!-- 设置弹窗 -->
<SettingsDialog v-model="showSettingsDialog" :initial-tab="store.state.settingsTab" /> <SettingsDialog v-model="showSettingsDialog" :initial-tab="store.state.settingsTab" />
<!-- 退出登录确认弹窗 -->
<el-dialog v-model="showLogoutConfirm" title="退出登录" width="3.6rem" style="line-height: 0.2rem" :close-on-click-modal="true" :append-to-body="true">
<p style="font-size: 0.14rem; color: #555; text-align: center;">确定要退出当前账号吗</p>
<template #footer>
<el-button @click="showLogoutConfirm = false">取消</el-button>
<el-button type="danger" @click="confirmLogout">确认退出</el-button>
</template>
</el-dialog>
<!-- 邀请注册送会员弹窗 --> <!-- 邀请注册送会员弹窗 -->
<SettingsInviteDialog v-model="showShareDialog" /> <SettingsInviteDialog v-model="showShareDialog" />
<!-- 会员权限拦截弹窗 --> <!-- 会员权限拦截弹窗 -->
@@ -192,15 +221,26 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch, onMounted } from 'vue' import { computed, ref, watch, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { useStore } from 'vuex' import { useStore } from 'vuex'
import SettingsDialog from '@/components/SettingsDialog.vue' import SettingsDialog from '@/components/SettingsDialog.vue'
import MemberAccessDialog from '@/components/MemberAccessDialog.vue' import MemberAccessDialog from '@/components/MemberAccessDialog.vue'
import MemberDialog from '@/components/MemberDialog.vue' import MemberDialog from '@/components/MemberDialog.vue'
import { checkLogin } from '@/api/auth' import { checkLogin } from '@/api/auth'
import { logout } from '@/api/auth'
import { fetchMessageList, fetchUnreadCount, markMessageRead } from '@/api/message' import { fetchMessageList, fetchUnreadCount, markMessageRead } from '@/api/message'
import { timestampToLocalDateTime } from '@/utils/time' import { timestampToLocalDateTime } from '@/utils/time'
/**
* 格式化时间戳为月/日显示(如 06/16)
* 利用 timestampToLocalDateTime 的 returnDay 精度,截取掉年份部分
*/
function formatMonthDay(timestamp: number | null | undefined): string {
const full = timestampToLocalDateTime(timestamp, 'returnDay') // 返回 "2026/06/16"
// 去掉年份的四位数字和第一个斜杠 → "06/16"
return full.replace(/^\d{4}\//, '')
}
import {userFeedback} from '@/api/setting' import {userFeedback} from '@/api/setting'
import type { MessageDto } from '@/api/message' import type { MessageDto } from '@/api/message'
import navJobsIcon from '@/assets/images/nav/nav-jobs-icon.png' import navJobsIcon from '@/assets/images/nav/nav-jobs-icon.png'
@@ -218,6 +258,17 @@ const route = useRoute()
const router = useRouter() const router = useRouter()
const store = useStore() const store = useStore()
/**
* 监听路由变化:已登录时刷新用户信息和会员状态到 vuex
* 这样每次页面切换都能拿到最新数据
*/
watch(() => route.path, () => {
if (store.state.isAuthenticated) {
store.dispatch('loadUserInfo')
store.dispatch('loadMemberStatus')
}
}, { immediate: true })
interface MenuItem { interface MenuItem {
name: string name: string
path: string path: string
@@ -346,10 +397,7 @@ async function loadUnreadCount() {
} }
} }
// 组件挂载时获取未读消息数量 // 组件挂载时获取未读消息数量(已移到下方 onMounted 统一处理)
onMounted(() => {
loadUnreadCount()
})
/** /**
* 加载消息列表(分页追加) * 加载消息列表(分页追加)
@@ -490,12 +538,125 @@ const settingsMenu = computed(() => {
}) })
const footerMenus = computed(() => [ const footerMenus = computed(() => [
{ iconImg: `<img src="${navShareIcon}" alt="" />`, label: '分享送会员', action: () => { showShareDialog.value = true } },
{ iconImg: `<img src="${navMessageIcon}" alt="" />`, label: '消息通知', badge: unreadCount.value > 0 ? 'NEW' : '', action: () => { showMessageDialog.value = true } }, { iconImg: `<img src="${navMessageIcon}" alt="" />`, label: '消息通知', badge: unreadCount.value > 0 ? 'NEW' : '', action: () => { showMessageDialog.value = true } },
{ iconImg: settingsMenu.value.iconImg, label: settingsMenu.value.label, action: () => { handleSettingsNav() } }, { iconImg: settingsMenu.value.iconImg, label: settingsMenu.value.label, action: () => { handleSettingsNav() } },
{ iconImg: `<img src="${navFeedbackIcon}" alt="" />`, label: '反馈', action: () => { showFeedbackDialog.value = true } }, { iconImg: `<img src="${navFeedbackIcon}" alt="" />`, label: '反馈', action: () => { showFeedbackDialog.value = true } },
]) ])
/**
* 手机号脱敏显示:134****1254 格式
*/
const maskedPhone = computed(() => {
const phone = store.state.userInfo?.mobileNumber || ''
if (phone.length >= 7) {
return phone.slice(0, 3) + '****' + phone.slice(-4)
}
return phone || '未绑定手机'
})
/**
* 会员标签文字:根据 isMember + memberType 判断
* memberType: 1=正式会员 2=试用会员, isMember=false → 免费用户
*/
const memberTagText = computed(() => {
const status = store.state.memberStatus
if (!status || !status.isMember) return '免费用户'
if (status.memberType === 1) return '正式会员'
if (status.memberType === 2) return '试用会员'
return '免费用户'
})
/**
* 会员标签样式 class
*/
const memberTagClass = computed(() => {
const status = store.state.memberStatus
if (!status || !status.isMember) return 'side-nav__user-member-tag--free'
if (status.memberType === 1) return 'side-nav__user-member-tag--formal'
if (status.memberType === 2) return 'side-nav__user-member-tag--trial'
return 'side-nav__user-member-tag--free'
})
/** 账户弹窗显示状态 */
const showAccountPopup = ref(false)
/** 退出登录确认弹窗 */
const showLogoutConfirm = ref(false)
/**
* 账户弹窗中会员类型字体颜色 class
*/
const accountMemberColorClass = computed(() => {
const status = store.state.memberStatus
if (!status || !status.isMember) return 'side-nav__account-popup-member-type--free'
if (status.memberType === 1) return 'side-nav__account-popup-member-type--formal'
if (status.memberType === 2) return 'side-nav__account-popup-member-type--trial'
return 'side-nav__account-popup-member-type--free'
})
/**
* 会员到期时间显示文字 — 使用项目时间工具 timestampToLocalDateTime
*/
const memberExpireText = computed(() => {
const status = store.state.memberStatus
if (!status || !status.isMember) return ''
if (status.expireTime) {
return timestampToLocalDateTime(status.expireTime, 'returnDay') + '到期'
}
return ''
})
/** 账户弹窗 — 点击"详情>"跳转会员页 */
function handleAccountDetail() {
showAccountPopup.value = false
showMemberDialog.value = true
}
/** 账户弹窗 — 点击"设置"打开设置弹窗 */
function handleAccountSettings() {
showAccountPopup.value = false
handleSettingsNav()
}
/** 账户弹窗 — 点击"退出登录"弹出确认弹窗 */
function handleAccountLogout() {
showAccountPopup.value = false
showLogoutConfirm.value = true
}
/** 确认退出登录 */
async function confirmLogout() {
showLogoutConfirm.value = false
try {
const res = await logout()
if (res.code === '0') {
ElMessage.success('已退出登录')
}
} catch {
// 即使接口失败也执行前端登出
}
store.dispatch('logout')
router.push('/')
}
/**
* 点击页面其他地方关闭账户弹窗
*/
function handleGlobalClick() {
if (showAccountPopup.value) {
showAccountPopup.value = false
}
}
onMounted(() => {
loadUnreadCount()
document.addEventListener('click', handleGlobalClick)
})
onUnmounted(() => {
document.removeEventListener('click', handleGlobalClick)
})
/** /**
* 设置弹窗:需要登录,通过 checkLogin 接口验证 * 设置弹窗:需要登录,通过 checkLogin 接口验证
*/ */
@@ -531,6 +692,12 @@ async function handleNav(item: MenuItem) {
return return
} }
// 外部链接 — routePath 是 http/https 开头,新标签页打开
if (/^https?:\/\//.test(item.path)) {
window.open(item.path, '_blank')
return
}
// Jobs 等静态页面 — 无需登录验证,直接跳转 // Jobs 等静态页面 — 无需登录验证,直接跳转
if (staticNames.includes(item.name)) { if (staticNames.includes(item.name)) {
router.push(item.path) router.push(item.path)
+30
View File
@@ -9,6 +9,8 @@ import { fetchJobIntention, saveJobIntention } from '@/api/jobs'
import type { JobIntention } from '@/api/jobs' import type { JobIntention } from '@/api/jobs'
import { fetchUserInfo } from '@/api/auth' import { fetchUserInfo } from '@/api/auth'
import type { UserInfo } from '@/api/auth' import type { UserInfo } from '@/api/auth'
import { fetchMemberStatus } from '@/api/member'
import type { MemberStatus } from '@/api/member'
/** 招聘分类选项:label → 接口参数 recruitCategory */ /** 招聘分类选项:label → 接口参数 recruitCategory */
export const JOB_TYPE_OPTIONS: { label: string; value: number }[] = [ export const JOB_TYPE_OPTIONS: { label: string; value: number }[] = [
@@ -46,6 +48,8 @@ export function formatEducation(edu: number | undefined | null): string {
/** 职位列表页缓存数据(从详情页返回时恢复用) */ /** 职位列表页缓存数据(从详情页返回时恢复用) */
export interface JobListCache { export interface JobListCache {
/** 缓存时所在的 Tabrecommend / collected / applied */
tab: string
/** 缓存的职位列表 */ /** 缓存的职位列表 */
list: any[] list: any[]
/** 当前已加载到的页码 */ /** 当前已加载到的页码 */
@@ -138,6 +142,12 @@ export interface RootState {
*/ */
userInfo: UserInfo | null userInfo: UserInfo | null
/**
* 会员状态信息 — 登录后从 /member/status 接口获取
* 多个页面可直接从 store 读取
*/
memberStatus: MemberStatus | null
/** /**
* 设置弹窗 — 控制显示/隐藏及初始 Tab * 设置弹窗 — 控制显示/隐藏及初始 Tab
*/ */
@@ -175,6 +185,7 @@ export default createStore<RootState>({
aiIndustryIds: [], aiIndustryIds: [],
}, },
userInfo: null, userInfo: null,
memberStatus: null,
showSettings: false, showSettings: false,
settingsTab: 'account', settingsTab: 'account',
inviteCode: '', inviteCode: '',
@@ -238,6 +249,9 @@ export default createStore<RootState>({
SET_USER_INFO(state, data: UserInfo | null) { SET_USER_INFO(state, data: UserInfo | null) {
state.userInfo = data state.userInfo = data
}, },
SET_MEMBER_STATUS(state, data: MemberStatus | null) {
state.memberStatus = data
},
SET_SHOW_SETTINGS(state, show: boolean) { SET_SHOW_SETTINGS(state, show: boolean) {
state.showSettings = show state.showSettings = show
}, },
@@ -311,6 +325,7 @@ export default createStore<RootState>({
commit('SET_SHOW_LOGIN', false) commit('SET_SHOW_LOGIN', false)
commit('SET_LOGIN_REDIRECT', '') commit('SET_LOGIN_REDIRECT', '')
commit('SET_USER_INFO', null) commit('SET_USER_INFO', null)
commit('SET_MEMBER_STATUS', null)
// 清除 Jobs 页面缓存数据 // 清除 Jobs 页面缓存数据
commit('SET_JOB_LIST_CACHE', null) commit('SET_JOB_LIST_CACHE', null)
commit('SET_JOB_INTENTION', { commit('SET_JOB_INTENTION', {
@@ -391,6 +406,21 @@ export default createStore<RootState>({
} }
}, },
/**
* 加载会员状态到 store
* 登录状态下调用,多个页面可直接读取 store.state.memberStatus
*/
async loadMemberStatus({ commit }) {
try {
const res = await fetchMemberStatus()
if (res.code === '0' && res.data) {
commit('SET_MEMBER_STATUS', res.data)
}
} catch (err) {
console.error('[store] 加载会员状态失败', err)
}
},
/** /**
* 保存求职意向:已登录时调接口保存并更新 store,未登录时仅更新 store * 保存求职意向:已登录时调接口保存并更新 store,未登录时仅更新 store
* @param data 求职意向数据 * @param data 求职意向数据
+53 -2
View File
@@ -18,6 +18,39 @@
<!-- 内容区域可滚动 --> <!-- 内容区域可滚动 -->
<div class="job-detail__body scrollbar-thin"> <div class="job-detail__body scrollbar-thin">
<!-- 加载中骨架屏 -->
<el-skeleton :loading="loading" animated :count="1">
<template #template>
<div class="job-detail__skeleton">
<!-- 模拟岗位标题行 -->
<el-skeleton-item variant="h1" style="width: 40%; height: 0.28rem; margin-bottom: 0.16rem;" />
<!-- 模拟标签行 -->
<div style="display: flex; gap: 0.1rem; margin-bottom: 0.2rem;">
<el-skeleton-item variant="text" style="width: 0.6rem; height: 0.2rem;" />
<el-skeleton-item variant="text" style="width: 0.8rem; height: 0.2rem;" />
<el-skeleton-item variant="text" style="width: 0.5rem; height: 0.2rem;" />
</div>
<!-- 模拟信息区 -->
<el-skeleton-item variant="text" style="width: 80%; height: 0.16rem; margin-bottom: 0.12rem;" />
<el-skeleton-item variant="text" style="width: 60%; height: 0.16rem; margin-bottom: 0.24rem;" />
<!-- 模拟描述区 -->
<el-skeleton-item variant="h3" style="width: 20%; height: 0.2rem; margin-bottom: 0.16rem;" />
<el-skeleton-item variant="text" style="width: 100%; height: 0.14rem; margin-bottom: 0.1rem;" />
<el-skeleton-item variant="text" style="width: 95%; height: 0.14rem; margin-bottom: 0.1rem;" />
<el-skeleton-item variant="text" style="width: 90%; height: 0.14rem; margin-bottom: 0.1rem;" />
<el-skeleton-item variant="text" style="width: 85%; height: 0.14rem; margin-bottom: 0.1rem;" />
<el-skeleton-item variant="text" style="width: 70%; height: 0.14rem; margin-bottom: 0.24rem;" />
<!-- 模拟公司信息区 -->
<el-skeleton-item variant="h3" style="width: 25%; height: 0.2rem; margin-bottom: 0.16rem;" />
<div style="display: flex; align-items: center; gap: 0.12rem; margin-bottom: 0.12rem;">
<el-skeleton-item variant="circle" style="width: 0.4rem; height: 0.4rem;" />
<el-skeleton-item variant="text" style="width: 1.2rem; height: 0.18rem;" />
</div>
<el-skeleton-item variant="text" style="width: 100%; height: 0.14rem; margin-bottom: 0.1rem;" />
<el-skeleton-item variant="text" style="width: 80%; height: 0.14rem;" />
</div>
</template>
<template #default>
<!-- 岗位基本信息区 --> <!-- 岗位基本信息区 -->
<div class="job-detail__header-section"> <div class="job-detail__header-section">
<!-- 左侧岗位信息 --> <!-- 左侧岗位信息 -->
@@ -49,10 +82,10 @@
</div> </div>
<!-- 右侧匹配度卡片 --> <!-- 右侧匹配度卡片 -->
<div class="job-detail__match-card"> <div class="job-detail__match-card" :class="matchCardClass">
<div class="job-detail__match-top"> <div class="job-detail__match-top">
<span class="job-detail__match-score">{{ job.matchScore }}%</span> <span class="job-detail__match-score">{{ job.matchScore }}%</span>
<span class="job-detail__match-label">高匹配度</span> <span class="job-detail__match-label">{{ matchLevelText }}</span>
</div> </div>
<div class="job-detail__match-list"> <div class="job-detail__match-list">
<div class="job-detail__match-row" v-for="m in matchItems" :key="m.label"> <div class="job-detail__match-row" v-for="m in matchItems" :key="m.label">
@@ -119,6 +152,8 @@
<p class="job-detail__company-desc">{{ job.companyInfo.description }}</p> <p class="job-detail__company-desc">{{ job.companyInfo.description }}</p>
</div> </div>
</div> </div>
</template>
</el-skeleton>
</div> </div>
</div> </div>
@@ -214,6 +249,22 @@ const matchItems = ref([
{ label: '过往经历', score: 0 }, { label: '过往经历', score: 0 },
]) ])
/** 匹配度卡片动态class:根据分数区间切换高/中/低样式 */
const matchCardClass = computed(() => {
const score = job.matchScore
if (score >= 80) return 'job-detail__match-card--high'
if (score >= 40) return 'job-detail__match-card--medium'
return 'job-detail__match-card--low'
})
/** 匹配度等级文字 */
const matchLevelText = computed(() => {
const score = job.matchScore
if (score >= 80) return '高匹配度'
if (score >= 40) return '中匹配度'
return '低匹配度'
})
/** 技能标签项(matched 表示用户是否具备该技能,可点击切换) */ /** 技能标签项(matched 表示用户是否具备该技能,可点击切换) */
interface SkillTag { interface SkillTag {
/** 技能名称 */ /** 技能名称 */
+105 -19
View File
@@ -18,7 +18,7 @@
<JobPageHeader v-model:activeTab="activeTab" /> <JobPageHeader v-model:activeTab="activeTab" />
<!-- 筛选条件 --> <!-- 筛选条件 -->
<div v-if="isRecommendTab" class="jobs-page__filters-bar"> <div v-if="isRecommendTab" class="jobs-page__filters-bar" :style="showAiRecommendTip?'':'margin-bottom:0.2rem'">
<div class="jobs-page__filters"> <div class="jobs-page__filters">
<div ref="filterGroupRef" class="jobs-page__filter-group"> <div ref="filterGroupRef" class="jobs-page__filter-group">
<!-- 筛选条件按钮列表行业单独用组件 --> <!-- 筛选条件按钮列表行业单独用组件 -->
@@ -114,7 +114,7 @@
<div class="jobs-page__ai-tip-content"> <div class="jobs-page__ai-tip-content">
<img class="jobs-page__ai-tip-logo" src="@/assets/images/logo-offerpai-2.png" alt="Nova" /> <img class="jobs-page__ai-tip-logo" src="@/assets/images/logo-offerpai-2.png" alt="Nova" />
<span class="jobs-page__ai-tip-text"> <span class="jobs-page__ai-tip-text">
Nova根据你的个人背景建议的行业{{ aiIndustryNames }}<template v-if="aiCategoryNames">岗位{{ aiCategoryNames }}</template> Nova根据你的个人背景建议的<template v-if="aiCategoryNames">岗位:{{ aiCategoryNames }}</template><template v-if="aiIndustryNames">行业{{ aiIndustryNames }}</template>
</span> </span>
</div> </div>
</div> </div>
@@ -302,8 +302,8 @@ import RegionSelector from '@/components/tools/RegionSelector.vue'
import { fetchJobList, fetchFavoriteList, toggleJobFavorite, removeJobFavorite, fetchFavoriteCount, fetchApplyList, fetchApplyCount, removeJobFromList } from '@/api/jobs' import { fetchJobList, fetchFavoriteList, toggleJobFavorite, removeJobFavorite, fetchFavoriteCount, fetchApplyList, fetchApplyCount, removeJobFromList } from '@/api/jobs'
import type { JobListItem, JobListParams, FavoriteListParams, ApplyListParams, ApplyCountData } from '@/api/jobs' import type { JobListItem, JobListParams, FavoriteListParams, ApplyListParams, ApplyCountData } from '@/api/jobs'
import { getTimeEvent, setTimeEvent, timestampToLocalDateTime } from '@/utils/time' import { getTimeEvent, setTimeEvent, timestampToLocalDateTime } from '@/utils/time'
import { resolveJobCategoryName } from '@/utils/jobCategory' import { fetchIndustryTree, fetchJobCategoryTree } from '@/api/common'
import { resolveIndustryName } from '@/utils/industry' import type { IndustryItem, JobCategoryItem } from '@/api/common'
// 2. 注意:这里的字符串不需要再手动转义双引号了 // 2. 注意:这里的字符串不需要再手动转义双引号了
@@ -516,27 +516,98 @@ const aiCategoryIds = computed<string[]>(
() => store.state.jobIntention.aiCategoryIds || [], () => store.state.jobIntention.aiCategoryIds || [],
) )
/** AI 推荐的行业名称(ID 转文字) */ /** ====== AI 推荐提示条专用:完整分类树(含空分类) ====== */
const fullIndustryTree = ref<IndustryItem[]>([])
const fullJobCategoryTree = ref<JobCategoryItem[]>([])
/** 加载完整分类树数据(includeEmpty=true),仅用于 AI 推荐提示条 ID 转名称 */
async function loadFullCategoryTrees() {
const [indRes, catRes] = await Promise.all([
fetchIndustryTree({ includeEmpty: true }),
fetchJobCategoryTree({ includeEmpty: true }),
])
if (indRes && indRes.code === '0' && indRes.data) {
fullIndustryTree.value = indRes.data
}
if (catRes && catRes.code === '0' && catRes.data) {
fullJobCategoryTree.value = catRes.data
}
}
/** 从完整行业树中根据 ID 查找名称 */
function resolveFullIndustryName(id: string): string {
for (const parent of fullIndustryTree.value) {
if (String(parent.id) === id) return parent.name
for (const child of parent.children) {
if (String(child.id) === id) return child.name
}
}
return ''
}
/** 从完整岗位树中根据 ID 查找名称 */
function resolveFullCategoryName(id: string): string {
for (const lv1 of fullJobCategoryTree.value) {
if (String(lv1.id) === id) return lv1.name
for (const lv2 of lv1.children) {
if (String(lv2.id) === id) return lv2.name
if (lv2.children) {
for (const lv3 of lv2.children) {
if (String(lv3.id) === id) return lv3.name
}
}
}
}
return ''
}
/** AI 推荐的行业名称(ID 转文字,基于完整行业树) */
const aiIndustryNames = computed<string>(() => { const aiIndustryNames = computed<string>(() => {
if (!aiIndustryIds.value.length) return '' if (!aiIndustryIds.value.length || !fullIndustryTree.value.length) return ''
return aiIndustryIds.value const idSet = new Set(aiIndustryIds.value.map(String))
.map(id => resolveIndustryName(Number(id))) const names: string[] = []
.filter(name => !!name) for (const id of aiIndustryIds.value) {
.join('、') // 如果该 ID 是一级父节点且子节点也在列表中,跳过父级避免重复
const parent = fullIndustryTree.value.find(p => String(p.id) === id)
if (parent && parent.children?.some(c => idSet.has(String(c.id)))) {
continue
}
const name = resolveFullIndustryName(id)
if (name) names.push(name)
}
return names.join('、')
}) })
/** AI 推荐的岗位名称(ID 转文字) */ /** AI 推荐的岗位名称(ID 转文字,基于完整岗位树 */
const aiCategoryNames = computed<string>(() => { const aiCategoryNames = computed<string>(() => {
if (!aiCategoryIds.value.length) return '' if (!aiCategoryIds.value.length || !fullJobCategoryTree.value.length) return ''
return aiCategoryIds.value const idSet = new Set(aiCategoryIds.value.map(String))
.map(id => resolveJobCategoryName(Number(id))) const names: string[] = []
.filter(name => !!name) for (const id of aiCategoryIds.value) {
.join('、') // 如果该 ID 是一级父节点且下级子节点也在列表中,跳过
const lv1 = fullJobCategoryTree.value.find(p => String(p.id) === id)
if (lv1 && lv1.children?.some(c => idSet.has(String(c.id)) || c.children?.some(leaf => idSet.has(String(leaf.id))))) {
continue
}
// 如果该 ID 是二级节点且三级子节点也在列表中,跳过
let isParentWithChild = false
for (const cat of fullJobCategoryTree.value) {
const lv2 = cat.children?.find(c => String(c.id) === id)
if (lv2 && lv2.children?.some(leaf => idSet.has(String(leaf.id)))) {
isParentWithChild = true
break
}
}
if (isParentWithChild) continue
const name = resolveFullCategoryName(id)
if (name) names.push(name)
}
return names.join('、')
}) })
/** 是否显示 AI 推荐提示条 */ /** 是否显示 AI 推荐提示条 */
const showAiRecommendTip = computed(() => { const showAiRecommendTip = computed(() => {
return isRecommendTab.value && (aiIndustryNames.value || aiCategoryNames.value) return isRecommendTab.value && (aiIndustryNames.value !== null || aiCategoryNames.value !== null)
}) })
/** 行业选择变更回调 */ /** 行业选择变更回调 */
@@ -699,9 +770,12 @@ onMounted(async () => {
// 窗口 resize 时更新尖角位置 // 窗口 resize 时更新尖角位置
window.addEventListener('resize', updateAiTipArrowPosition) window.addEventListener('resize', updateAiTipArrowPosition)
// 加载完整分类树(含空分类),用于 AI 推荐提示条 ID 转名称
loadFullCategoryTrees()
// 先判断是否有缓存(从详情页返回),在任何异步操作之前取出 // 先判断是否有缓存(从详情页返回),在任何异步操作之前取出
const cache = store.state.jobListCache const cache = store.state.jobListCache
const hasCache = !!(cache && cache.list.length > 0) const hasCache = !!(cache && cache.list.length > 0 && cache.tab === activeTab.value)
if (hasCache) { if (hasCache) {
// 立即恢复缓存数据,隐藏列表避免闪烁 // 立即恢复缓存数据,隐藏列表避免闪烁
@@ -710,7 +784,9 @@ onMounted(async () => {
pageNum.value = cache!.pageNum pageNum.value = cache!.pageNum
total.value = cache!.total total.value = cache!.total
savedScrollTop = cache!.scrollTop savedScrollTop = cache!.scrollTop
// 清除缓存,避免下次非详情页返回时误用 }
// 无论是否命中缓存,都清除,避免下次非详情页返回时误用
if (cache) {
store.commit('SET_JOB_LIST_CACHE', null) store.commit('SET_JOB_LIST_CACHE', null)
} }
@@ -1126,6 +1202,15 @@ watch(activeTab, (newTab, oldTab) => {
return return
} }
// 同步 tab 到 URL query,刷新浏览器时能恢复正确的 Tab
const query = { ...route.query }
if (newTab === 'recommend') {
delete query.tab
} else {
query.tab = newTab
}
router.replace({ query })
jobList.value = [] jobList.value = []
keyword.value = '' keyword.value = ''
pageNum.value = 1 pageNum.value = 1
@@ -1221,6 +1306,7 @@ function goToDetail(job: JobItem) {
} }
// 保存当前列表状态到 store,返回时恢复 // 保存当前列表状态到 store,返回时恢复
store.commit('SET_JOB_LIST_CACHE', { store.commit('SET_JOB_LIST_CACHE', {
tab: activeTab.value,
list: jobList.value, list: jobList.value,
pageNum: pageNum.value, pageNum: pageNum.value,
total: total.value, total: total.value,