支付宝下单和其他细节优化
This commit is contained in:
Vendored
+3
@@ -47,14 +47,17 @@ declare module 'vue' {
|
|||||||
MemberDialog: typeof import('./src/components/MemberDialog.vue')['default']
|
MemberDialog: typeof import('./src/components/MemberDialog.vue')['default']
|
||||||
ProfileEditDrawer: typeof import('./src/components/ProfileEditDrawer.vue')['default']
|
ProfileEditDrawer: typeof import('./src/components/ProfileEditDrawer.vue')['default']
|
||||||
ProfilePageContent: typeof import('./src/components/ProfilePageContent.vue')['default']
|
ProfilePageContent: typeof import('./src/components/ProfilePageContent.vue')['default']
|
||||||
|
ProfileWelcomeDialog: typeof import('./src/components/ProfileWelcomeDialog.vue')['default']
|
||||||
RegionSelector: typeof import('./src/components/tools/RegionSelector.vue')['default']
|
RegionSelector: typeof import('./src/components/tools/RegionSelector.vue')['default']
|
||||||
ResumeAnalysisReportDrawer: typeof import('./src/components/ResumeAnalysisReportDrawer.vue')['default']
|
ResumeAnalysisReportDrawer: typeof import('./src/components/ResumeAnalysisReportDrawer.vue')['default']
|
||||||
ResumeEditNameDialog: typeof import('./src/components/ResumeEditNameDialog.vue')['default']
|
ResumeEditNameDialog: typeof import('./src/components/ResumeEditNameDialog.vue')['default']
|
||||||
|
ResumeExportDialog: typeof import('./src/components/ResumeExportDialog.vue')['default']
|
||||||
ResumeIssueFixDrawer: typeof import('./src/components/ResumeIssueFixDrawer.vue')['default']
|
ResumeIssueFixDrawer: typeof import('./src/components/ResumeIssueFixDrawer.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
SettingsDeleteAccountDialog: typeof import('./src/components/SettingsDeleteAccountDialog.vue')['default']
|
SettingsDeleteAccountDialog: typeof import('./src/components/SettingsDeleteAccountDialog.vue')['default']
|
||||||
SettingsDialog: typeof import('./src/components/SettingsDialog.vue')['default']
|
SettingsDialog: typeof import('./src/components/SettingsDialog.vue')['default']
|
||||||
|
SettingsInviteDialog: typeof import('./src/components/SettingsInviteDialog.vue')['default']
|
||||||
SideNav: typeof import('./src/components/SideNav.vue')['default']
|
SideNav: typeof import('./src/components/SideNav.vue')['default']
|
||||||
}
|
}
|
||||||
export interface GlobalDirectives {
|
export interface GlobalDirectives {
|
||||||
|
|||||||
@@ -57,3 +57,45 @@ export function checkLogin() {
|
|||||||
export function logout() {
|
export function logout() {
|
||||||
return request.post<any, ApiResult>('/public/logout')
|
return request.post<any, ApiResult>('/public/logout')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注销账号
|
||||||
|
* POST /user/manage/cancel
|
||||||
|
* Cookie 自动携带 Token
|
||||||
|
*/
|
||||||
|
export function cancelAccount() {
|
||||||
|
return request.post<any, ApiResult>('/user/manage/cancel')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 用户个人信息 DTO */
|
||||||
|
export interface UserInfo {
|
||||||
|
/** 用户ID */
|
||||||
|
id?: number
|
||||||
|
/** 手机号 */
|
||||||
|
mobileNumber?: string
|
||||||
|
/** 邮箱 */
|
||||||
|
email?: string
|
||||||
|
/** 昵称 */
|
||||||
|
nick?: string
|
||||||
|
/** 真实姓名 */
|
||||||
|
realName?: string
|
||||||
|
/** 头像 */
|
||||||
|
picture?: string
|
||||||
|
/** 生日 */
|
||||||
|
birthday?: { seconds?: number; nanos?: number }
|
||||||
|
/** 性别 1男 2女 */
|
||||||
|
sex?: number
|
||||||
|
/** 邀请码 */
|
||||||
|
inviteCode?: string
|
||||||
|
/** 注册时间 */
|
||||||
|
createTime?: { seconds?: number; nanos?: number }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询个人信息
|
||||||
|
* GET /user/manage/info
|
||||||
|
* Cookie 自动携带 Token
|
||||||
|
*/
|
||||||
|
export function fetchUserInfo() {
|
||||||
|
return request.get<any, ApiResult<UserInfo>>('/user/manage/info')
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
import type { ApiResult } from '@/api/auth'
|
||||||
|
|
||||||
|
/** 会员商品项类型(后端返回) */
|
||||||
|
export interface MemberProduct {
|
||||||
|
id: number
|
||||||
|
/** 商品名称 */
|
||||||
|
productName: string
|
||||||
|
/** 标签,如"限时优惠"、"最划算" */
|
||||||
|
tag: string
|
||||||
|
/** 主推标识 0=否 1=是 */
|
||||||
|
isFeatured: number
|
||||||
|
/** 购买按钮文字 */
|
||||||
|
buyButtonText: string
|
||||||
|
/** 实付价格(分) */
|
||||||
|
price: number
|
||||||
|
/** 划线价(分) */
|
||||||
|
originalPrice: number
|
||||||
|
/** 折算月价(分) */
|
||||||
|
monthlyPrice: number
|
||||||
|
/** 有效天数 */
|
||||||
|
durationDays: number
|
||||||
|
/** 排序,越小越靠前 */
|
||||||
|
sortOrder: number
|
||||||
|
/** 状态 0=下架 1=上架 */
|
||||||
|
status: number
|
||||||
|
/** 创建时间 */
|
||||||
|
createTime: { seconds: number; nanos: number }
|
||||||
|
/** 更新时间 */
|
||||||
|
updateTime: { seconds: number; nanos: number }
|
||||||
|
/** 逻辑删除 0=正常 非0=已删除 */
|
||||||
|
isDelete: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询会员商品列表
|
||||||
|
* GET /member/product/list
|
||||||
|
*/
|
||||||
|
export function fetchMemberProductList() {
|
||||||
|
return request.get<any, ApiResult<MemberProduct[]>>('/member/product/list')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建订单请求参数 */
|
||||||
|
export interface CreateOrderParams {
|
||||||
|
/** 商品ID */
|
||||||
|
productId: string
|
||||||
|
/** 支付渠道 1=微信 2=支付宝 */
|
||||||
|
payChannel: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建订单返回数据 */
|
||||||
|
export interface CreateOrderResult {
|
||||||
|
/** 订单ID,用于轮询状态 */
|
||||||
|
orderId: string
|
||||||
|
/** 支付渠道 */
|
||||||
|
payChannel: number
|
||||||
|
/** 支付数据(支付宝表单HTML / 微信二维码链接) */
|
||||||
|
payData: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建会员订单
|
||||||
|
* POST /member/product/createOrder
|
||||||
|
*/
|
||||||
|
export function createMemberOrder(data: CreateOrderParams) {
|
||||||
|
return request.post<any, ApiResult<CreateOrderResult>>('/member/product/createOrder', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 订单详情返回数据 */
|
||||||
|
export interface OrderDetailResult {
|
||||||
|
/** 订单ID */
|
||||||
|
orderId: string
|
||||||
|
/** 订单编号 */
|
||||||
|
orderNo: string
|
||||||
|
/** 商品名称 */
|
||||||
|
productName: string
|
||||||
|
/** 实付金额(分) */
|
||||||
|
payAmount: number
|
||||||
|
/** 订单状态 0=待支付 1=已支付 2=已退款 3=已关闭 */
|
||||||
|
status: number
|
||||||
|
/** 支付渠道 1=微信 2=支付宝 */
|
||||||
|
payChannel: number
|
||||||
|
/** 支付时间 */
|
||||||
|
payTime: { seconds: number; nanos: number }
|
||||||
|
/** 下单时间 */
|
||||||
|
createTime: { seconds: number; nanos: number }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询订单详情
|
||||||
|
* GET /member/product/orderDetail
|
||||||
|
*/
|
||||||
|
export function fetchOrderDetail(orderId: string) {
|
||||||
|
return request.get<any, ApiResult<OrderDetailResult>>('/member/product/orderDetail', {
|
||||||
|
params: { orderId },
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -75,6 +75,17 @@ export function fetchProfile() {
|
|||||||
return request.get<any, ApiResult<ProfileData>>('/user/profile')
|
return request.get<any, ApiResult<ProfileData>>('/user/profile')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据简历ID同步更新个人资料
|
||||||
|
* POST /user/profile/syncFromResume?resumeId=xxx
|
||||||
|
* Cookie 自动携带 Token
|
||||||
|
*/
|
||||||
|
export function syncProfileFromResume(resumeId: string) {
|
||||||
|
return request.post<any, ApiResult>('/user/profile/syncFromResume', null, {
|
||||||
|
params: { resumeId },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 教育经历相关 ====================
|
// ==================== 教育经历相关 ====================
|
||||||
|
|
||||||
/** 描述段落 */
|
/** 描述段落 */
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
@@ -200,7 +200,7 @@
|
|||||||
// 双击提示文字
|
// 双击提示文字
|
||||||
&__hint {
|
&__hint {
|
||||||
font-size: 0.11rem;
|
font-size: 0.11rem;
|
||||||
color: $text-light;
|
color: #666;
|
||||||
margin-bottom: 0.06rem;
|
margin-bottom: 0.06rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,7 +206,7 @@
|
|||||||
// 双击提示文字
|
// 双击提示文字
|
||||||
&__hint {
|
&__hint {
|
||||||
font-size: 0.11rem;
|
font-size: 0.11rem;
|
||||||
color: $text-light;
|
color: #666;
|
||||||
margin-bottom: 0.06rem;
|
margin-bottom: 0.06rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,18 +10,20 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-dialog {
|
.member-dialog {
|
||||||
background: $bg-white;
|
background: $bg-white;
|
||||||
border-radius: 0.2rem;
|
border-radius: 0.2rem;
|
||||||
width: 8.4rem;
|
width: 8.8rem;
|
||||||
height: 80vh;
|
height: 80vh;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 0 0.1rem 0.4rem rgba(0, 0, 0, 0.15);
|
box-shadow: 0 0.1rem 0.4rem rgba(0, 0, 0, 0.15);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
font-size: 0.14rem;
|
||||||
|
|
||||||
// 关闭按钮
|
// 关闭按钮
|
||||||
&__close {
|
&__close {
|
||||||
@@ -520,12 +522,11 @@
|
|||||||
&__order-body {
|
&__order-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: hidden;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__order-left {
|
&__order-left {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
|
||||||
padding: 0.3rem;
|
padding: 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,7 +534,6 @@
|
|||||||
width: 2.8rem;
|
width: 2.8rem;
|
||||||
padding: 0.3rem 0.24rem;
|
padding: 0.3rem 0.24rem;
|
||||||
background: $bg-main;
|
background: $bg-main;
|
||||||
overflow-y: auto;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1116,7 +1116,9 @@
|
|||||||
padding: 0.3rem 0.4rem;
|
padding: 0.3rem 0.4rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
min-width: 3.2rem;
|
width: 5rem;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
box-shadow: 0 0.1rem 0.4rem rgba(0, 0, 0, 0.15);
|
box-shadow: 0 0.1rem 0.4rem rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1163,21 +1165,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__qrcode-image {
|
&__qrcode-image {
|
||||||
width: 1.8rem;
|
width: 2.1rem;
|
||||||
height: 1.8rem;
|
height: 2.1rem;
|
||||||
margin: 0 auto 0.16rem;
|
margin: 0 auto 0.16rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__payment-iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.08rem;
|
||||||
|
}
|
||||||
|
|
||||||
&__qrcode-placeholder {
|
&__qrcode-placeholder {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: $bg-main;
|
background: $bg-main;
|
||||||
border: 1px dashed $border-color;
|
border: 1px dashed $border-color;
|
||||||
border-radius: 0.08rem;
|
border-radius: 0.08rem;
|
||||||
// TODO: 替换为真实二维码
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__qrcode-amount {
|
&__qrcode-amount {
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
@use '../variables' as *;
|
||||||
|
|
||||||
|
// ==================== 欢迎使用弹窗 ====================
|
||||||
|
.profile-welcome-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: $overlay-bg;
|
||||||
|
z-index: 2200;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-welcome-dialog {
|
||||||
|
width: 5.6rem;
|
||||||
|
background: $bg-white;
|
||||||
|
border-radius: 0.12rem;
|
||||||
|
padding: 0.36rem 0.4rem;
|
||||||
|
box-shadow: 0 0.04rem 0.2rem rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
&__title {
|
||||||
|
font-size: 0.22rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-dark;
|
||||||
|
margin: 0 0 0.24rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单区域
|
||||||
|
&__form {
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标签
|
||||||
|
&__label {
|
||||||
|
font-size: 0.14rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-dark;
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传区域
|
||||||
|
&__upload-area {
|
||||||
|
background: $bg-main;
|
||||||
|
border-radius: 0.08rem;
|
||||||
|
padding: 0.32rem 0.2rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
margin-bottom: 0.14rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: darken(#F3F4F5, 3%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传图标
|
||||||
|
&__upload-icon {
|
||||||
|
width: 0.44rem;
|
||||||
|
height: 0.44rem;
|
||||||
|
background: $text-dark;
|
||||||
|
border-radius: 0.06rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: $bg-white;
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
|
||||||
|
&--done {
|
||||||
|
background: $accent;
|
||||||
|
font-size: 0.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传提示文字
|
||||||
|
&__upload-tip {
|
||||||
|
font-size: 0.12rem;
|
||||||
|
color: $text-middle;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐私说明
|
||||||
|
&__privacy {
|
||||||
|
font-size: 0.11rem;
|
||||||
|
color: $text-middle;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始匹配按钮
|
||||||
|
&__start-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 0.46rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.06rem;
|
||||||
|
background: $text-dark;
|
||||||
|
color: $bg-white;
|
||||||
|
font-size: 0.15rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全屏加载遮罩 — z-index 需高于弹窗 overlay(2200)
|
||||||
|
.profile-welcome-loading {
|
||||||
|
z-index: 2300 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传中弹窗层级降低,让 loading 遮罩盖在上面
|
||||||
|
.profile-welcome-overlay--behind {
|
||||||
|
z-index: 2000 !important;
|
||||||
|
}
|
||||||
@@ -317,7 +317,82 @@
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 步骤2:安全验证 =====
|
// ===== 步骤2:二次确认 =====
|
||||||
|
&__confirm-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.5rem 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__confirm-icon {
|
||||||
|
width: 0.56rem;
|
||||||
|
height: 0.56rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba($danger, 0.1);
|
||||||
|
color: $danger;
|
||||||
|
font-size: 0.24rem;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__confirm-title {
|
||||||
|
font-size: 0.22rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-dark;
|
||||||
|
margin: 0 0 0.12rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__confirm-desc {
|
||||||
|
font-size: 0.13rem;
|
||||||
|
color: $text-middle;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0 0 0.28rem 0;
|
||||||
|
max-width: 3.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__confirm-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.2rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 4.0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__confirm-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 0.44rem;
|
||||||
|
border-radius: 0.06rem;
|
||||||
|
font-size: 0.15rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&--cancel {
|
||||||
|
background: #EDF1F7;
|
||||||
|
border: none;
|
||||||
|
color: $text-dark;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $text-middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--danger {
|
||||||
|
background: #DC2626;
|
||||||
|
border: none;
|
||||||
|
color: $bg-white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: darken(#DC2626, 8%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 步骤2(备用):安全验证 =====
|
||||||
&__verify {
|
&__verify {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -424,25 +499,60 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== 步骤3:完成 =====
|
// ===== 步骤3:完成 =====
|
||||||
&__done {
|
&__done-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0.6rem 0;
|
background: $bg-white;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
border-radius: 0.12rem;
|
||||||
|
padding: 0.6rem 0.8rem;
|
||||||
|
margin-top: 0.4rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__done-icon {
|
&__done-icon {
|
||||||
width: 0.6rem;
|
width: 0.56rem;
|
||||||
height: 0.6rem;
|
height: 0.56rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: $accent;
|
background: rgba($accent, 0.15);
|
||||||
color: $bg-white;
|
color: $accent;
|
||||||
font-size: 0.28rem;
|
font-size: 0.26rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 0.2rem;
|
margin-bottom: 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__done-title {
|
||||||
|
font-size: 0.22rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-dark;
|
||||||
|
margin: 0 0 0.12rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__done-desc {
|
||||||
|
font-size: 0.13rem;
|
||||||
|
color: $text-middle;
|
||||||
|
margin: 0 0 0.28rem 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__done-btn {
|
||||||
|
width: 2.6rem;
|
||||||
|
height: 0.44rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.12rem;
|
||||||
|
background: $accent;
|
||||||
|
color: $bg-white;
|
||||||
|
font-size: 0.15rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $accent-hover;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
@use '../variables' as *;
|
||||||
|
|
||||||
|
// ==================== 邀请注册送会员弹窗 ====================
|
||||||
|
.settings-invite-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: $overlay-bg;
|
||||||
|
z-index: 2200;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.14rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-invite-dialog {
|
||||||
|
position: relative;
|
||||||
|
width: 4.2rem;
|
||||||
|
max-height: 85vh;
|
||||||
|
background: $bg-white;
|
||||||
|
border-radius: 0.12rem;
|
||||||
|
padding: 0.32rem 0.28rem;
|
||||||
|
box-shadow: 0 0.04rem 0.2rem rgba(0, 0, 0, 0.15);
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
// 关闭按钮
|
||||||
|
&__close {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.14rem;
|
||||||
|
right: 0.18rem;
|
||||||
|
font-size: 0.18rem;
|
||||||
|
color: $text-light;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $text-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
&__title {
|
||||||
|
font-size: 0.18rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-dark;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 0 0.2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 邀请链接区域
|
||||||
|
&__link-box {
|
||||||
|
background: $bg-main;
|
||||||
|
border-radius: 0.08rem;
|
||||||
|
padding: 0.14rem 0.16rem;
|
||||||
|
margin-bottom: 0.14rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__link-text {
|
||||||
|
font-size: 0.12rem;
|
||||||
|
color: $text-dark;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin: 0;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制链接按钮
|
||||||
|
&__copy-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 0.4rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
background: $text-dark;
|
||||||
|
color: $bg-white;
|
||||||
|
font-size: 0.14rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 0.18rem;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 步骤提示
|
||||||
|
&__steps-tip {
|
||||||
|
font-size: 0.12rem;
|
||||||
|
color: $text-middle;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 0 0.14rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 步骤卡片
|
||||||
|
&__step {
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
border-radius: 0.08rem;
|
||||||
|
padding: 0.12rem 0.16rem;
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
background: #F2F4F7;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__step-title {
|
||||||
|
font-size: 0.13rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-dark;
|
||||||
|
margin-bottom: 0.04rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__step-desc {
|
||||||
|
font-size: 0.11rem;
|
||||||
|
color: $text-middle;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
&__stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.12rem;
|
||||||
|
margin-top: 0.16rem;
|
||||||
|
margin-bottom: 0.16rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stat-item {
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
border-radius: 0.08rem;
|
||||||
|
padding: 0.12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stat-label {
|
||||||
|
font-size: 0.11rem;
|
||||||
|
color: $text-middle;
|
||||||
|
margin-bottom: 0.06rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stat-value {
|
||||||
|
font-size: 0.16rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 活动规则
|
||||||
|
&__rules {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
border-radius: 0.08rem;
|
||||||
|
padding: 0.12rem 0.16rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $bg-main;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__rules-label {
|
||||||
|
font-size: 0.13rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__rules-arrow {
|
||||||
|
width: 0.14rem;
|
||||||
|
height: 0.14rem;
|
||||||
|
color: $text-middle;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
|
||||||
|
&--open {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__rules-content {
|
||||||
|
margin-top: 0.1rem;
|
||||||
|
padding: 0.12rem 0.16rem;
|
||||||
|
background: $bg-main;
|
||||||
|
border-radius: 0.06rem;
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 0.11rem;
|
||||||
|
color: $text-middle;
|
||||||
|
line-height: 1.8;
|
||||||
|
margin: 0 0 0.04rem 0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,8 @@
|
|||||||
@use './components/agent-apply-progress.scss';
|
@use './components/agent-apply-progress.scss';
|
||||||
@use './components/ai-thinking-indicator.scss';
|
@use './components/ai-thinking-indicator.scss';
|
||||||
@use './components/settings-delete-account-dialog.scss';
|
@use './components/settings-delete-account-dialog.scss';
|
||||||
|
@use './components/profile-welcome-dialog.scss';
|
||||||
|
@use './components/settings-invite-dialog.scss';
|
||||||
|
|
||||||
// 全局样式(优先级最高)
|
// 全局样式(优先级最高)
|
||||||
@use './auto.scss';
|
@use './auto.scss';
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
background: $bg-main;
|
background: $bg-main;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
font-size: 0.14rem;
|
||||||
|
|
||||||
// 手动包裹的两层 div,需要撑满剩余高度并允许内部滚动
|
// 手动包裹的两层 div,需要撑满剩余高度并允许内部滚动
|
||||||
> .bg-white {
|
> .bg-white {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
background: $bg-main;
|
background: $bg-main;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
font-size: 0.14rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 页面标题
|
// 页面标题
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
background: $bg-main;
|
background: $bg-main;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
font-size: 0.14rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 页面标题区
|
// 页面标题区
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
background: $bg-main;
|
background: $bg-main;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
font-size: 0.14rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__header {
|
&__header {
|
||||||
@@ -160,6 +161,14 @@
|
|||||||
height: 0.16rem;
|
height: 0.16rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 空状态提示
|
||||||
|
&__empty {
|
||||||
|
padding: 0.6rem 0.2rem;
|
||||||
|
text-align: center;
|
||||||
|
color: $text-light;
|
||||||
|
font-size: 0.14rem;
|
||||||
|
}
|
||||||
|
|
||||||
// 弹出菜单
|
// 弹出菜单
|
||||||
&__popup {
|
&__popup {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -48,10 +48,10 @@ $btn-dark: #4FC2C9;
|
|||||||
$btn-dark-hover: #42A8B3;
|
$btn-dark-hover: #42A8B3;
|
||||||
|
|
||||||
// 次要深色按钮背景(除非特意指定使用否则不用这个按钮色)
|
// 次要深色按钮背景(除非特意指定使用否则不用这个按钮色)
|
||||||
$btn-dark: #1A1A2E;
|
//$btn-dark: #1A1A2E;
|
||||||
|
|
||||||
// 次要深色按钮悬停态(除非特意指定使用否则不用这个按钮色)
|
// 次要深色按钮悬停态(除非特意指定使用否则不用这个按钮色)
|
||||||
$btn-dark-hover: #2E3142;
|
//$btn-dark-hover: #2E3142;
|
||||||
|
|
||||||
// 渐变色背景
|
// 渐变色背景
|
||||||
$gradient-bg: linear-gradient(to right, #4FC2C9, #42A8B3);
|
$gradient-bg: linear-gradient(to right, #4FC2C9, #42A8B3);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<!-- AI 欢迎消息 -->
|
<!-- AI 欢迎消息 -->
|
||||||
<div class="ai-chat__msg ai-chat__msg--ai">
|
<div class="ai-chat__msg ai-chat__msg--ai">
|
||||||
<div class="ai-chat__msg-bubble">
|
<div class="ai-chat__msg-bubble">
|
||||||
<div class="ai-chat__msg-title">欢迎回来,李华!</div>
|
<div class="ai-chat__msg-title">欢迎回来,{{nickName}}!</div>
|
||||||
<div class="ai-chat__msg-text">很高兴再次见到你,让我们继续您通往理想工作的旅程吧。</div>
|
<div class="ai-chat__msg-text">很高兴再次见到你,让我们继续您通往理想工作的旅程吧。</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,6 +92,10 @@ const props = defineProps<{
|
|||||||
const currentRoute = useRoute()
|
const currentRoute = useRoute()
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
|
|
||||||
|
/** 用户昵称 — 从全局 store 读取 */
|
||||||
|
const nickName = computed(() => store.state.userInfo?.nick || '')
|
||||||
|
|
||||||
// ==================== 状态 ====================
|
// ==================== 状态 ====================
|
||||||
|
|
||||||
/** 会员购买弹窗的显示状态 */
|
/** 会员购买弹窗的显示状态 */
|
||||||
|
|||||||
+200
-46
@@ -108,14 +108,14 @@
|
|||||||
<!-- 顶部导航 -->
|
<!-- 顶部导航 -->
|
||||||
<div class="member-dialog__order-header">
|
<div class="member-dialog__order-header">
|
||||||
<span class="member-dialog__order-back" @click="currentView = 'intro2'">‹ 返回会员介绍</span>
|
<span class="member-dialog__order-back" @click="currentView = 'intro2'">‹ 返回会员介绍</span>
|
||||||
<!-- 步骤条 -->
|
<!-- 步骤条 — 二维码弹窗打开时显示第2步 -->
|
||||||
<div class="member-dialog__steps">
|
<div class="member-dialog__steps">
|
||||||
<div class="member-dialog__step member-dialog__step--active">
|
<div class="member-dialog__step" :class="showQrCode ? 'member-dialog__step--done' : 'member-dialog__step--active'">
|
||||||
<span class="member-dialog__step-num">1</span>
|
<span class="member-dialog__step-num">{{ showQrCode ? '✓' : '1' }}</span>
|
||||||
<span class="member-dialog__step-label">选择套餐</span>
|
<span class="member-dialog__step-label">选择套餐</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="member-dialog__step-line"></div>
|
<div class="member-dialog__step-line" :class="{ 'member-dialog__step-line--done': showQrCode }"></div>
|
||||||
<div class="member-dialog__step">
|
<div class="member-dialog__step" :class="{ 'member-dialog__step--active': showQrCode }">
|
||||||
<span class="member-dialog__step-num">2</span>
|
<span class="member-dialog__step-num">2</span>
|
||||||
<span class="member-dialog__step-label">支付方式</span>
|
<span class="member-dialog__step-label">支付方式</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -144,12 +144,15 @@
|
|||||||
@click="selectedPlan = plan.key"
|
@click="selectedPlan = plan.key"
|
||||||
>
|
>
|
||||||
<div v-if="plan.recommend" class="member-dialog__order-plan-badge">★ 推荐</div>
|
<div v-if="plan.recommend" class="member-dialog__order-plan-badge">★ 推荐</div>
|
||||||
<!-- 选中圆圈 -->
|
|
||||||
<div class="member-dialog__order-plan-radio">
|
|
||||||
<div v-if="selectedPlan === plan.key" class="member-dialog__order-plan-radio-inner"></div>
|
|
||||||
</div>
|
|
||||||
<div class="member-dialog__order-plan-info">
|
<div class="member-dialog__order-plan-info">
|
||||||
<div class="member-dialog__order-plan-name">{{ plan.name }}</div>
|
<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">
|
<div class="member-dialog__order-plan-price">
|
||||||
<span class="member-dialog__order-plan-price-symbol">¥</span>
|
<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-num">{{ plan.price }}</span>
|
||||||
@@ -157,6 +160,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="member-dialog__order-plan-desc">{{ plan.orderDesc }}</div>
|
<div class="member-dialog__order-plan-desc">{{ plan.orderDesc }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -317,33 +321,36 @@
|
|||||||
<h2 class="member-dialog__order-success-title">支付成功,求职加速已开启</h2>
|
<h2 class="member-dialog__order-success-title">支付成功,求职加速已开启</h2>
|
||||||
<p class="member-dialog__order-success-desc">你已成功解锁 AI 求职加速权益,现在可以开始优化简历、匹配岗位并准备面试。</p>
|
<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" @click="handleGoResume">开始优化我的简历</button>
|
||||||
<button class="member-dialog__order-success-btn-secondary" @click="$emit('update:modelValue', false)">查看会员权益</button>
|
<button class="member-dialog__order-success-btn-secondary" @click="handleViewMemberBenefits">查看会员权益</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ==================== 付款二维码遮罩层 ==================== -->
|
<!-- ==================== 付款弹窗 ==================== -->
|
||||||
<div v-if="showQrCode" class="member-dialog__qrcode-overlay" @click="showQrCode = false">
|
<div v-if="showQrCode" class="member-dialog__qrcode-overlay" @click="handleCloseQrCode">
|
||||||
<div class="member-dialog__qrcode-modal" @click.stop>
|
<div class="member-dialog__qrcode-modal" @click.stop>
|
||||||
<!-- 关闭按钮 -->
|
<!-- 关闭按钮 -->
|
||||||
<span class="member-dialog__qrcode-close" @click="showQrCode = false">✕</span>
|
<span class="member-dialog__qrcode-close" @click="handleCloseQrCode">✕</span>
|
||||||
<!-- 支付方式标题 -->
|
<!-- 支付方式标题 -->
|
||||||
<div class="member-dialog__qrcode-title">
|
<div class="member-dialog__qrcode-title">
|
||||||
<span class="member-dialog__qrcode-title-icon">{{ selectedPayment === 'wechat' ? '💬' : '🔷' }}</span>
|
<span class="member-dialog__qrcode-title-icon">{{ selectedPayment === 'wechat' ? '💬' : '🔷' }}</span>
|
||||||
<span>{{ selectedPayment === 'wechat' ? '微信支付' : '支付宝' }}</span>
|
<span>{{ selectedPayment === 'wechat' ? '微信支付' : '支付宝' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 扫码提示 -->
|
<!-- 扫码提示 -->
|
||||||
<h3 class="member-dialog__qrcode-subtitle">扫码完成支付</h3>
|
<h3 class="member-dialog__qrcode-subtitle">{{ selectedPayment === 'alipay' ? '扫码完成支付' : '扫码完成支付' }}</h3>
|
||||||
<p class="member-dialog__qrcode-desc">请使用{{ selectedPayment === 'wechat' ? '微信' : '支付宝' }} App 扫描二维码完成支付,完成后此窗口会自动关闭。</p>
|
<p class="member-dialog__qrcode-desc">{{ selectedPayment === 'alipay' ? '请在支付宝页面完成支付,完成后点击下方按钮确认。' : '请使用微信 App 扫描二维码完成支付,完成后此窗口会自动关闭。' }}</p>
|
||||||
<!-- 二维码占位 -->
|
<!-- 支付宝:iframe 渲染支付表单 -->
|
||||||
<div class="member-dialog__qrcode-image">
|
<div class="member-dialog__qrcode-image">
|
||||||
<!-- TODO: 替换为真实二维码图片 -->
|
<iframe
|
||||||
<div class="member-dialog__qrcode-placeholder"></div>
|
v-if="paymentFormHtml"
|
||||||
|
:srcdoc="paymentFormHtml"
|
||||||
|
frameborder="0"
|
||||||
|
class="member-dialog__payment-iframe"
|
||||||
|
scrolling="auto"
|
||||||
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
<!-- 金额 -->
|
<!-- 金额 -->
|
||||||
<div class="member-dialog__qrcode-amount">¥{{ currentPlan.price }}</div>
|
<div class="member-dialog__qrcode-amount">¥{{ currentPlan.price }}</div>
|
||||||
<!-- 我已完成支付按钮 -->
|
|
||||||
<button class="member-dialog__qrcode-confirm-btn" @click="handlePaymentDone">我已完成支付</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -352,20 +359,23 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, reactive } from 'vue'
|
import { ref, computed, watch, reactive, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import { fetchMemberProductList, createMemberOrder, fetchOrderDetail, type MemberProduct } from '@/api/member'
|
||||||
|
|
||||||
/** 组件 Props — 控制弹窗显示/隐藏 */
|
/** 组件 Props — 控制弹窗显示/隐藏 */
|
||||||
const props = defineProps<{ modelValue: boolean }>()
|
const props = defineProps<{ modelValue: boolean }>()
|
||||||
|
|
||||||
/** 组件 Emits — 通知父组件更新 modelValue */
|
/** 组件 Emits — 通知父组件更新 modelValue */
|
||||||
defineEmits<{ (e: 'update:modelValue', value: boolean): void }>()
|
const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void }>()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
// ==================== 类型定义 ====================
|
// ==================== 类型定义 ====================
|
||||||
|
|
||||||
/** 套餐项类型 */
|
/** 套餐项类型(包含后端完整数据 + 前端展示字段) */
|
||||||
interface PlanItem {
|
interface PlanItem {
|
||||||
key: string
|
key: string
|
||||||
name: string
|
name: string
|
||||||
@@ -376,6 +386,8 @@ interface PlanItem {
|
|||||||
perMonth?: string
|
perMonth?: string
|
||||||
btnText: string
|
btnText: string
|
||||||
orderDesc: string
|
orderDesc: string
|
||||||
|
/** 后端原始数据,下单时使用 */
|
||||||
|
raw: MemberProduct
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 支付方式类型 */
|
/** 支付方式类型 */
|
||||||
@@ -409,6 +421,15 @@ const agreeProtocol = ref(false)
|
|||||||
/** 是否显示二维码弹窗 */
|
/** 是否显示二维码弹窗 */
|
||||||
const showQrCode = ref(false)
|
const showQrCode = ref(false)
|
||||||
|
|
||||||
|
/** 支付宝表单 HTML(用于 iframe 渲染) */
|
||||||
|
const paymentFormHtml = ref('')
|
||||||
|
|
||||||
|
/** 当前订单ID(用于后续轮询支付状态) */
|
||||||
|
const orderId = ref('')
|
||||||
|
|
||||||
|
/** 是否正在创建订单 */
|
||||||
|
const creatingOrder = ref(false)
|
||||||
|
|
||||||
/** 监听弹窗开关 — 打开时锁定背景滚动,关闭时恢复并重置状态 */
|
/** 监听弹窗开关 — 打开时锁定背景滚动,关闭时恢复并重置状态 */
|
||||||
watch(() => props.modelValue, (val) => {
|
watch(() => props.modelValue, (val) => {
|
||||||
document.body.style.overflow = val ? 'hidden' : ''
|
document.body.style.overflow = val ? 'hidden' : ''
|
||||||
@@ -416,6 +437,21 @@ watch(() => props.modelValue, (val) => {
|
|||||||
currentView.value = 'intro2'
|
currentView.value = 'intro2'
|
||||||
showQrCode.value = false
|
showQrCode.value = false
|
||||||
agreeProtocol.value = false
|
agreeProtocol.value = false
|
||||||
|
paymentFormHtml.value = ''
|
||||||
|
orderId.value = ''
|
||||||
|
// 清除轮询计时器
|
||||||
|
if (pollTimer) {
|
||||||
|
clearInterval(pollTimer)
|
||||||
|
pollTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 监听二维码弹窗关闭 — 无论何种方式关闭都停止轮询 */
|
||||||
|
watch(showQrCode, (val) => {
|
||||||
|
if (!val && pollTimer) {
|
||||||
|
clearInterval(pollTimer)
|
||||||
|
pollTimer = null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -423,17 +459,62 @@ watch(() => props.modelValue, (val) => {
|
|||||||
|
|
||||||
/** 当前选中的套餐对象 */
|
/** 当前选中的套餐对象 */
|
||||||
const currentPlan = computed(() => {
|
const currentPlan = computed(() => {
|
||||||
return plans.find(p => p.key === selectedPlan.value) || plans[1]
|
return plans.value.find(p => p.key === selectedPlan.value) || plans.value[0]
|
||||||
})
|
})
|
||||||
|
|
||||||
// ==================== 常量数据 ====================
|
// ==================== 常量数据 ====================
|
||||||
|
|
||||||
/** 套餐列表 */
|
/** 套餐列表(从接口获取) */
|
||||||
const plans: PlanItem[] = [
|
const plans = ref<PlanItem[]>([])
|
||||||
{ key: 'weekly', name: '周会员', price: '17.99', unit: '周', tag: '临时体验 / 急用一次', btnText: '立即体验', orderDesc: '适合临时体验,急用一次的求职者' },
|
|
||||||
{ key: 'quarterly', name: '季度会员', price: '49.99', unit: '3个月', recommend: true, perMonth: '16.66', btnText: '开启完整求职冲刺', orderDesc: '推荐正在集中投递、准备面试的求职者,性价比最高' },
|
/** 根据有效天数生成单位描述 */
|
||||||
{ key: 'monthly', name: '月度会员', price: '19.99', unit: '月', tag: '标准求职', btnText: '开始求职加速', orderDesc: '标准求职周期,持续使用 AI 求职加速能力' },
|
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 {
|
||||||
|
const priceYuan = (product.price / 100).toFixed(2)
|
||||||
|
const monthlyPriceYuan = product.monthlyPrice ? (product.monthlyPrice / 100).toFixed(2) : undefined
|
||||||
|
return {
|
||||||
|
key: String(product.id),
|
||||||
|
name: product.productName,
|
||||||
|
price: priceYuan,
|
||||||
|
unit: getDurationUnit(product.durationDays),
|
||||||
|
tag: product.tag || undefined,
|
||||||
|
recommend: product.isFeatured === 1,
|
||||||
|
perMonth: monthlyPriceYuan,
|
||||||
|
btnText: product.buyButtonText || '立即开通',
|
||||||
|
orderDesc: product.tag || product.productName,
|
||||||
|
raw: product,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载会员商品列表 */
|
||||||
|
async function loadMemberProducts() {
|
||||||
|
try {
|
||||||
|
const res = await fetchMemberProductList()
|
||||||
|
if (res.data && res.data.length) {
|
||||||
|
// 按 sortOrder 排序
|
||||||
|
const sorted = [...res.data].sort((a, b) => a.sortOrder - b.sortOrder)
|
||||||
|
plans.value = sorted.map(mapProductToPlan)
|
||||||
|
// 默认选中主推套餐,若无则选第一个
|
||||||
|
const featured = plans.value.find(p => p.recommend)
|
||||||
|
selectedPlan.value = featured ? featured.key : plans.value[0].key
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载会员商品列表失败', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 组件挂载时加载商品列表 */
|
||||||
|
onMounted(() => {
|
||||||
|
loadMemberProducts()
|
||||||
|
})
|
||||||
|
|
||||||
/** 求职加速能力列表 */
|
/** 求职加速能力列表 */
|
||||||
const abilities = [
|
const abilities = [
|
||||||
@@ -485,29 +566,102 @@ function handleUpgrade(plan: PlanItem) {
|
|||||||
currentView.value = 'order1'
|
currentView.value = 'order1'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 点击立即开启求职加速 — 显示二维码弹窗 */
|
/** 点击立即开启求职加速 — 调用创建订单接口并显示支付弹窗 */
|
||||||
function handleShowQrCode() {
|
async function handleShowQrCode() {
|
||||||
if (!agreeProtocol.value) return
|
if (!agreeProtocol.value || creatingOrder.value) return
|
||||||
showQrCode.value = true
|
const plan = currentPlan.value
|
||||||
|
if (!plan) return
|
||||||
|
|
||||||
|
creatingOrder.value = true
|
||||||
|
try {
|
||||||
|
// 支付渠道:1=微信 2=支付宝
|
||||||
|
const payChannel = selectedPayment.value === 'alipay' ? 2 : 1
|
||||||
|
const res = await createMemberOrder({
|
||||||
|
productId: plan.key, // key 即为商品 id(字符串,避免大数精度丢失)
|
||||||
|
payChannel,
|
||||||
|
})
|
||||||
|
if (res.data) {
|
||||||
|
orderId.value = String(res.data.orderId)
|
||||||
|
paymentFormHtml.value = res.data.payData
|
||||||
|
showQrCode.value = true
|
||||||
|
// 创建订单成功后立即开始轮询支付状态
|
||||||
|
confirmPayment()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('创建订单失败', e)
|
||||||
|
} finally {
|
||||||
|
creatingOrder.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 点击我已完成支付 — 进入确认支付结果步骤 */
|
/** 轮询计时器 */
|
||||||
function handlePaymentDone() {
|
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||||
showQrCode.value = false
|
|
||||||
currentView.value = 'order2'
|
|
||||||
// 模拟接口确认支付结果,成功后进入步骤三
|
|
||||||
confirmPayment()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 模拟确认支付结果接口 */
|
/** 轮询查询订单支付状态 */
|
||||||
async function confirmPayment() {
|
async function confirmPayment() {
|
||||||
// TODO: 替换为真实支付确认接口
|
const startTime = Date.now()
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
/** 超时时间:5 分钟 */
|
||||||
currentView.value = 'order3'
|
const timeout = 5 * 60 * 1000
|
||||||
|
/** 轮询间隔:500ms */
|
||||||
|
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
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 支付成功后跳转简历页 */
|
/** 支付成功后跳转简历页 */
|
||||||
function handleGoResume() {
|
function handleGoResume() {
|
||||||
router.push('/resume')
|
router.push('/resume')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 关闭二维码弹窗 — 停止轮询 */
|
||||||
|
function handleCloseQrCode() {
|
||||||
|
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)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 欢迎使用弹窗 — 首次登录无个人资料时弹出,引导上传简历 -->
|
||||||
|
<Teleport to="body">
|
||||||
|
<div v-if="dialogVisible" class="profile-welcome-overlay" :class="{ 'profile-welcome-overlay--behind': uploading }" @click.stop>
|
||||||
|
<div class="profile-welcome-dialog" @click.stop>
|
||||||
|
<!-- 标题 -->
|
||||||
|
<h2 class="profile-welcome-dialog__title">欢迎使用Offer派</h2>
|
||||||
|
|
||||||
|
<!-- 简历上传区域 -->
|
||||||
|
<div class="profile-welcome-dialog__form">
|
||||||
|
<div class="profile-welcome-dialog__label">*简历</div>
|
||||||
|
<div class="profile-welcome-dialog__upload-area" @click="handleUploadClick">
|
||||||
|
<!-- 未上传状态 -->
|
||||||
|
<template v-if="!uploadedFileName">
|
||||||
|
<div class="profile-welcome-dialog__upload-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" width="28" height="28">
|
||||||
|
<path d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
<path d="M12 14V4m0 0l-4 4m4-4l4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p class="profile-welcome-dialog__upload-tip">支持上传PDF、WORD格式,文件大小不超过10M</p>
|
||||||
|
</template>
|
||||||
|
<!-- 已上传状态 -->
|
||||||
|
<template v-else>
|
||||||
|
<div class="profile-welcome-dialog__upload-icon profile-welcome-dialog__upload-icon--done">✓</div>
|
||||||
|
<p class="profile-welcome-dialog__upload-tip">{{ uploadedFileName }}</p>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 隐私说明 -->
|
||||||
|
<p class="profile-welcome-dialog__privacy">
|
||||||
|
您的个人资料仅用于职位匹配、简历优化及网申投递。未经您的明确许可,我们绝不会将信息泄露给第三方或招聘人员。您可以随时更新或删除您的相关信息。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 开始匹配按钮 -->
|
||||||
|
<button
|
||||||
|
class="profile-welcome-dialog__start-btn"
|
||||||
|
:disabled="!uploadedFileName"
|
||||||
|
@click="handleStart"
|
||||||
|
>
|
||||||
|
开始匹配
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { uploadResume } from '@/utils/aiRequest'
|
||||||
|
import { syncProfileFromResume } from '@/api/profile'
|
||||||
|
import { ElLoading, ElMessage } from 'element-plus'
|
||||||
|
import 'element-plus/es/components/loading/style/css'
|
||||||
|
|
||||||
|
/** 组件 Props */
|
||||||
|
const props = defineProps<{ modelValue: boolean }>()
|
||||||
|
|
||||||
|
/** 组件 Emits */
|
||||||
|
const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void }>()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
/** 已上传的文件名 */
|
||||||
|
const uploadedFileName = ref('')
|
||||||
|
|
||||||
|
/** 内部控制弹窗显隐(用于上传时临时隐藏) */
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
|
||||||
|
/** 上传中状态 — 降低弹窗层级 */
|
||||||
|
const uploading = ref(false)
|
||||||
|
|
||||||
|
/** 上传后获得的简历 ID */
|
||||||
|
const resumeId = ref('')
|
||||||
|
|
||||||
|
/** 弹窗打开时重置状态 */
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
if (val) {
|
||||||
|
uploadedFileName.value = ''
|
||||||
|
resumeId.value = ''
|
||||||
|
dialogVisible.value = true
|
||||||
|
} else {
|
||||||
|
dialogVisible.value = false
|
||||||
|
}
|
||||||
|
document.body.style.overflow = val ? 'hidden' : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 点击上传区域 — 弹出文件选择 */
|
||||||
|
function handleUploadClick() {
|
||||||
|
const input = document.createElement('input')
|
||||||
|
input.type = 'file'
|
||||||
|
input.accept = '.pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||||
|
|
||||||
|
input.onchange = async () => {
|
||||||
|
const file = input.files?.[0]
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
// 文件大小校验(10MB)
|
||||||
|
if (file.size > 10 * 1024 * 1024) {
|
||||||
|
ElMessage.error('文件大小不能超过10M')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记上传中,降低弹窗层级让 loading 显示在上面
|
||||||
|
uploading.value = true
|
||||||
|
|
||||||
|
// 全屏加载
|
||||||
|
const loading = ElLoading.service({
|
||||||
|
lock: true,
|
||||||
|
text: '简历解析中,请耐心等待…',
|
||||||
|
background: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
customClass: 'profile-welcome-loading',
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 上传简历文件
|
||||||
|
const res = await uploadResume(file)
|
||||||
|
if (res.code === 0 && res.data?.resumeId) {
|
||||||
|
resumeId.value = String(res.data.resumeId)
|
||||||
|
uploadedFileName.value = file.name
|
||||||
|
|
||||||
|
// 继续调用同步个人资料接口,加载特效延续
|
||||||
|
loading.setText('正在同步个人资料…')
|
||||||
|
await syncProfileFromResume(resumeId.value)
|
||||||
|
loading.close()
|
||||||
|
uploading.value = false
|
||||||
|
ElMessage.success('简历上传并同步成功')
|
||||||
|
} else {
|
||||||
|
loading.close()
|
||||||
|
uploading.value = false
|
||||||
|
ElMessage.error(res.msg || '上传失败')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
loading.close()
|
||||||
|
uploading.value = false
|
||||||
|
ElMessage.error('上传失败,请稍后重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 点击开始匹配按钮 */
|
||||||
|
function handleStart() {
|
||||||
|
emit('update:modelValue', false)
|
||||||
|
// 如果当前在 profile 页面就跳转 profile,其余跳转 jobs
|
||||||
|
if (route.path === '/profile') {
|
||||||
|
// 已经在 profile 页面,刷新页面数据
|
||||||
|
router.go(0)
|
||||||
|
} else {
|
||||||
|
router.push('/jobs')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 导出简历格式选择弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
title="导出简历"
|
||||||
|
width="3.6rem"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
class="resume-export-dialog"
|
||||||
|
>
|
||||||
|
<!-- 格式选择 -->
|
||||||
|
<el-radio-group v-model="exportFormat" class="resume-export-dialog__radio-group">
|
||||||
|
<el-radio value="pdf">PDF 简历</el-radio>
|
||||||
|
<el-radio value="word">Word 简历</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="exporting" @click="doExport">下载</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 隐藏的简历模板,用于导出时渲染DOM -->
|
||||||
|
<div v-if="exportTemplateData" style="position:absolute;left:-9999px;top:0;">
|
||||||
|
<JobResumeTemplate ref="exportTemplateRef" :resume-data="exportTemplateData" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, nextTick } from 'vue'
|
||||||
|
import JobResumeTemplate from '@/components/JobResumeTemplate.vue'
|
||||||
|
import type { ResumeTemplateData } from '@/components/JobResumeTemplate.vue'
|
||||||
|
import { exportResumePdf, exportResumeWord, loadResumeTemplateData } from '@/utils/resumeExport'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
// ==================== Props & Emits ====================
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
/** 控制弹窗显示/隐藏(v-model) */
|
||||||
|
modelValue: boolean
|
||||||
|
/** 要导出的简历 ID */
|
||||||
|
resumeId: string
|
||||||
|
/** 导出文件名(不含扩展名) */
|
||||||
|
resumeName?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// ==================== 弹窗显隐双向绑定 ====================
|
||||||
|
|
||||||
|
/** 弹窗可见状态 */
|
||||||
|
const visible = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val: boolean) => emit('update:modelValue', val),
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 导出相关状态 ====================
|
||||||
|
|
||||||
|
/** 导出格式:pdf 或 word */
|
||||||
|
const exportFormat = ref<'pdf' | 'word'>('pdf')
|
||||||
|
|
||||||
|
/** 导出中状态 */
|
||||||
|
const exporting = ref(false)
|
||||||
|
|
||||||
|
/** 导出用的简历模板数据 */
|
||||||
|
const exportTemplateData = ref<ResumeTemplateData | null>(null)
|
||||||
|
|
||||||
|
/** 导出用的简历模板组件引用 */
|
||||||
|
const exportTemplateRef = ref<InstanceType<typeof JobResumeTemplate> | null>(null)
|
||||||
|
|
||||||
|
// ==================== 导出逻辑 ====================
|
||||||
|
|
||||||
|
/** 执行导出下载 */
|
||||||
|
async function doExport() {
|
||||||
|
exporting.value = true
|
||||||
|
try {
|
||||||
|
// 1. 加载简历完整数据
|
||||||
|
const data = await loadResumeTemplateData(props.resumeId)
|
||||||
|
if (!data) {
|
||||||
|
ElMessage.error('获取简历数据失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 设置模板数据,等待DOM渲染
|
||||||
|
exportTemplateData.value = data
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
// 3. 获取渲染后的DOM
|
||||||
|
const element = exportTemplateRef.value?.resumeRef
|
||||||
|
if (!element) {
|
||||||
|
ElMessage.error('简历模板渲染失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = props.resumeName || '简历'
|
||||||
|
|
||||||
|
if (exportFormat.value === 'pdf') {
|
||||||
|
await exportResumePdf(element, fileName)
|
||||||
|
} else {
|
||||||
|
exportResumeWord(element, fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.success('导出成功')
|
||||||
|
visible.value = false
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[导出简历] 失败', err)
|
||||||
|
ElMessage.error('导出失败,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
exporting.value = false
|
||||||
|
// 清理隐藏模板数据
|
||||||
|
exportTemplateData.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -98,8 +98,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 步骤2:安全验证 -->
|
<!-- 步骤2:二次确认 -->
|
||||||
<template v-if="currentStep === 2">
|
<template v-if="currentStep === 2">
|
||||||
|
<div class="delete-account-dialog__confirm-card">
|
||||||
|
<div class="delete-account-dialog__confirm-icon">!</div>
|
||||||
|
<h2 class="delete-account-dialog__confirm-title">确认注销账号?</h2>
|
||||||
|
<p class="delete-account-dialog__confirm-desc">注销完成后不能撤回,账号权益将清零,已开通会员权益不退款。</p>
|
||||||
|
<div class="delete-account-dialog__confirm-actions">
|
||||||
|
<button class="delete-account-dialog__confirm-btn delete-account-dialog__confirm-btn--cancel" @click="currentStep = 1">再想想</button>
|
||||||
|
<button class="delete-account-dialog__confirm-btn delete-account-dialog__confirm-btn--danger" @click="handleConfirmDelete">确认注销</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 步骤2(备用):安全验证 — 手机验证码,暂时注释 -->
|
||||||
|
<!-- <template v-if="currentStep === 2">
|
||||||
<h2 class="delete-account-dialog__title">安全验证</h2>
|
<h2 class="delete-account-dialog__title">安全验证</h2>
|
||||||
<p class="delete-account-dialog__subtitle">为保护账号安全,请完成身份验证后继续注销流程。</p>
|
<p class="delete-account-dialog__subtitle">为保护账号安全,请完成身份验证后继续注销流程。</p>
|
||||||
<div class="delete-account-dialog__verify">
|
<div class="delete-account-dialog__verify">
|
||||||
@@ -116,15 +129,15 @@
|
|||||||
<p class="delete-account-dialog__verify-tip">完成验证后进入最终确认。</p>
|
<p class="delete-account-dialog__verify-tip">完成验证后进入最终确认。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template> -->
|
||||||
|
|
||||||
<!-- 步骤3:完成 -->
|
<!-- 步骤3:完成 -->
|
||||||
<template v-if="currentStep === 3">
|
<template v-if="currentStep === 3">
|
||||||
<div class="delete-account-dialog__done">
|
<div class="delete-account-dialog__done-card">
|
||||||
<div class="delete-account-dialog__done-icon">✓</div>
|
<div class="delete-account-dialog__done-icon">✓</div>
|
||||||
<h2 class="delete-account-dialog__title">账号注销申请已提交</h2>
|
<h2 class="delete-account-dialog__done-title">注销申请已提交</h2>
|
||||||
<p class="delete-account-dialog__subtitle">你的账号将在 7 个工作日内完成注销处理。在此期间如需撤回,请联系客服。</p>
|
<p class="delete-account-dialog__done-desc">我们将按注销流程处理你的账号。处理期间,请勿再次购买会员权益。</p>
|
||||||
<button class="delete-account-dialog__btn delete-account-dialog__btn--primary" @click="handleFinish">我知道了</button>
|
<button class="delete-account-dialog__done-btn" @click="handleFinish">我知道了</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -136,6 +149,7 @@
|
|||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useStore } from 'vuex'
|
import { useStore } from 'vuex'
|
||||||
|
import { cancelAccount } from '@/api/auth'
|
||||||
|
|
||||||
/** 组件 Props */
|
/** 组件 Props */
|
||||||
const props = defineProps<{ modelValue: boolean }>()
|
const props = defineProps<{ modelValue: boolean }>()
|
||||||
@@ -196,10 +210,19 @@ const sendCode = () => {
|
|||||||
ElMessage.success('验证码已发送')
|
ElMessage.success('验证码已发送')
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 确认注销 */
|
/** 确认注销 — 调用后端注销接口 */
|
||||||
const handleConfirmDelete = () => {
|
const handleConfirmDelete = async () => {
|
||||||
// TODO: 调用注销接口
|
try {
|
||||||
currentStep.value = 3
|
await cancelAccount()
|
||||||
|
// 注销成功,清空登录状态
|
||||||
|
store.commit('SET_AUTHENTICATED', false)
|
||||||
|
// 清空浏览器缓存数据(聊天记录、session 等)
|
||||||
|
localStorage.clear()
|
||||||
|
sessionStorage.clear()
|
||||||
|
currentStep.value = 3
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('注销请求失败,请稍后重试')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 完成 — 关闭弹窗并退出登录 */
|
/** 完成 — 关闭弹窗并退出登录 */
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
<h2 class="settings-dialog__content-title">账号与安全</h2>
|
<h2 class="settings-dialog__content-title">账号与安全</h2>
|
||||||
<div class="settings-dialog__section">
|
<div class="settings-dialog__section">
|
||||||
<div class="settings-dialog__section-label">手机号</div>
|
<div class="settings-dialog__section-label">手机号</div>
|
||||||
<p class="settings-dialog__section-value">130****2222</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 class="settings-dialog__danger-title">注销我的账号</div>
|
||||||
@@ -80,14 +80,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-dialog__member-issue">
|
<div class="settings-dialog__member-issue">
|
||||||
<div class="settings-dialog__member-issue-title">订阅状态异常?</div>
|
<!-- <div class="settings-dialog__member-issue-title">订阅状态异常?</div>-->
|
||||||
<p class="settings-dialog__member-issue-desc">
|
<!-- <p class="settings-dialog__member-issue-desc">-->
|
||||||
如果你已经和完成了付款或更改了订阅但是没有看到最新状态,你可以尝试更新状态或联系我们获取帮助。
|
<!-- 如果你已经和完成了付款或更改了订阅但是没有看到最新状态,你可以尝试更新状态或联系我们获取帮助。-->
|
||||||
</p>
|
<!-- </p>-->
|
||||||
<div class="settings-dialog__member-issue-actions">
|
<!-- <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="handleRefreshStatus">更新状态</button>-->
|
||||||
<button class="settings-dialog__member-issue-btn" @click="handleContactUs">联系我们</button>
|
<!-- <button class="settings-dialog__member-issue-btn" @click="handleContactUs">联系我们</button>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -127,34 +129,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-dialog__reminder-block">
|
<!-- <div class="settings-dialog__reminder-block">-->
|
||||||
<div class="settings-dialog__reminder-block-title">即时岗位提醒</div>
|
<!-- <div class="settings-dialog__reminder-block-title">即时岗位提醒</div>-->
|
||||||
<div class="settings-dialog__reminder-row">
|
<!-- <div class="settings-dialog__reminder-row">-->
|
||||||
<div class="settings-dialog__reminder-info">
|
<!-- <div class="settings-dialog__reminder-info">-->
|
||||||
<div class="settings-dialog__reminder-label">开启即时岗位更新提醒</div>
|
<!-- <div class="settings-dialog__reminder-label">开启即时岗位更新提醒</div>-->
|
||||||
<div class="settings-dialog__reminder-desc">
|
<!-- <div class="settings-dialog__reminder-desc">-->
|
||||||
抢先申请 —— 在岗位发布后一小时内,即可收到为你量身定制的最新职位提醒
|
<!-- 抢先申请 —— 在岗位发布后一小时内,即可收到为你量身定制的最新职位提醒-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
<el-switch v-model="reminders.instant" active-color="#4FC2C9" />
|
<!-- <el-switch v-model="reminders.instant" active-color="#4FC2C9" />-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
<div class="settings-dialog__reminder-block">
|
<!-- <div class="settings-dialog__reminder-block">-->
|
||||||
<div class="settings-dialog__reminder-block-title">岗位更新提醒频率</div>
|
<!-- <div class="settings-dialog__reminder-block-title">岗位更新提醒频率</div>-->
|
||||||
<div class="settings-dialog__reminder-row">
|
<!-- <div class="settings-dialog__reminder-row">-->
|
||||||
<div class="settings-dialog__reminder-info">
|
<!-- <div class="settings-dialog__reminder-info">-->
|
||||||
<div class="settings-dialog__reminder-desc">
|
<!-- <div class="settings-dialog__reminder-desc">-->
|
||||||
会员用户每天可接收无限次岗位更新提醒,免费用户每天最多接收 1 次。
|
<!-- 会员用户每天可接收无限次岗位更新提醒,免费用户每天最多接收 1 次。-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
<el-select v-model="reminders.frequency" style="width: 1.2rem;">
|
<!-- <el-select v-model="reminders.frequency" style="width: 1.2rem;">-->
|
||||||
<el-option label="1次/天" value="1" />
|
<!-- <el-option label="1次/天" value="1" />-->
|
||||||
<el-option label="2次/天" value="2" />
|
<!-- <el-option label="2次/天" value="2" />-->
|
||||||
<el-option label="5次/天" value="5" />
|
<!-- <el-option label="5次/天" value="5" />-->
|
||||||
<el-option label="无限次" value="unlimited" />
|
<!-- <el-option label="无限次" value="unlimited" />-->
|
||||||
</el-select>
|
<!-- </el-select>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</template>
|
</template>
|
||||||
<!-- Tab: 用户隐私协议 — 长文本,可滚动查看 -->
|
<!-- Tab: 用户隐私协议 — 长文本,可滚动查看 -->
|
||||||
<template v-if="activeTab === 'privacy'">
|
<template v-if="activeTab === 'privacy'">
|
||||||
@@ -230,6 +232,9 @@
|
|||||||
|
|
||||||
<!-- 注销账号弹窗 -->
|
<!-- 注销账号弹窗 -->
|
||||||
<SettingsDeleteAccountDialog v-model="showDeleteAccount" />
|
<SettingsDeleteAccountDialog v-model="showDeleteAccount" />
|
||||||
|
|
||||||
|
<!-- 邀请注册送会员弹窗 -->
|
||||||
|
<SettingsInviteDialog v-model="showInviteDialog" />
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -243,9 +248,10 @@ import { resolveRegionName } from '@/utils/region'
|
|||||||
import { resolveIndustryName } from '@/utils/industry'
|
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'
|
||||||
|
|
||||||
/** 组件 Props — 控制弹窗显示/隐藏 */
|
/** 组件 Props — 控制弹窗显示/隐藏,可指定初始 Tab */
|
||||||
const props = defineProps<{ modelValue: boolean }>()
|
const props = defineProps<{ modelValue: boolean; initialTab?: string }>()
|
||||||
|
|
||||||
/** 组件 Emits — 通知父组件更新 modelValue */
|
/** 组件 Emits — 通知父组件更新 modelValue */
|
||||||
const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void }>()
|
const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void }>()
|
||||||
@@ -258,18 +264,24 @@ const store = useStore()
|
|||||||
const tabs = [
|
const tabs = [
|
||||||
{ key: 'account', label: '账号与安全', icon: '👤' },
|
{ key: 'account', label: '账号与安全', icon: '👤' },
|
||||||
{ key: 'member', label: '会员', icon: '🏅' },
|
{ key: 'member', label: '会员', icon: '🏅' },
|
||||||
{ key: 'reminder', label: '岗位更新提醒', icon: '🔔' },
|
{ key: 'reminder', label: '目标岗位设置', icon: '🔔' },
|
||||||
]
|
]
|
||||||
|
|
||||||
/** 当前选中的 Tab */
|
/** 当前选中的 Tab */
|
||||||
const activeTab = ref('account')
|
const activeTab = ref('account')
|
||||||
|
|
||||||
|
/** 用户手机号 — 从全局 store 读取 */
|
||||||
|
const userPhone = computed(() => store.state.userInfo?.mobileNumber || '')
|
||||||
|
|
||||||
/** 退出登录确认弹窗的显示状态 */
|
/** 退出登录确认弹窗的显示状态 */
|
||||||
const showLogout = ref(false)
|
const showLogout = ref(false)
|
||||||
|
|
||||||
/** 监听弹窗开关 — 打开时锁定背景页面滚动,关闭时恢复 */
|
/** 监听弹窗开关 — 打开时锁定背景页面滚动,关闭时恢复 */
|
||||||
watch(() => props.modelValue, (val) => {
|
watch(() => props.modelValue, (val) => {
|
||||||
document.body.style.overflow = val ? 'hidden' : ''
|
document.body.style.overflow = val ? 'hidden' : ''
|
||||||
|
if (val && props.initialTab) {
|
||||||
|
activeTab.value = props.initialTab
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 岗位更新提醒的配置项 */
|
/** 岗位更新提醒的配置项 */
|
||||||
@@ -284,6 +296,9 @@ const showGoalDialog = ref(false)
|
|||||||
/** 注销账号弹窗显示状态 */
|
/** 注销账号弹窗显示状态 */
|
||||||
const showDeleteAccount = ref(false)
|
const showDeleteAccount = ref(false)
|
||||||
|
|
||||||
|
/** 邀请注册弹窗显示状态 */
|
||||||
|
const showInviteDialog = ref(false)
|
||||||
|
|
||||||
/** 岗位名称列表 */
|
/** 岗位名称列表 */
|
||||||
const intentionCategoryNames = computed(() => {
|
const intentionCategoryNames = computed(() => {
|
||||||
const ids = store.state.jobIntention.categoryIds || []
|
const ids = store.state.jobIntention.categoryIds || []
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 邀请注册送会员弹窗 -->
|
||||||
|
<Teleport to="body">
|
||||||
|
<div v-if="modelValue" class="settings-invite-overlay" @click="$emit('update:modelValue', false)">
|
||||||
|
<div class="settings-invite-dialog" @click.stop>
|
||||||
|
<!-- 关闭按钮 -->
|
||||||
|
<span class="settings-invite-dialog__close" @click="$emit('update:modelValue', false)">✕</span>
|
||||||
|
|
||||||
|
<!-- 标题 -->
|
||||||
|
<h2 class="settings-invite-dialog__title">邀请好友免费获取7天会员权益</h2>
|
||||||
|
|
||||||
|
<!-- 邀请链接区域 -->
|
||||||
|
<div class="settings-invite-dialog__link-box">
|
||||||
|
<p class="settings-invite-dialog__link-text">来一起领取AI求职助手会员!{{ inviteText }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 复制链接按钮 -->
|
||||||
|
<button class="settings-invite-dialog__copy-btn" @click="handleCopy">复制链接</button>
|
||||||
|
|
||||||
|
<!-- 步骤说明 -->
|
||||||
|
<p class="settings-invite-dialog__steps-tip">仅需 3 步可免费获取会员权益</p>
|
||||||
|
|
||||||
|
<div class="settings-invite-dialog__step">
|
||||||
|
<div class="settings-invite-dialog__step-title">第1步:分享专属邀请链接</div>
|
||||||
|
<p class="settings-invite-dialog__step-desc">点击"复制链接",并发送给好友</p>
|
||||||
|
</div>
|
||||||
|
<div class="settings-invite-dialog__step">
|
||||||
|
<div class="settings-invite-dialog__step-title">第2步:好友完成新用户首次体验</div>
|
||||||
|
<p class="settings-invite-dialog__step-desc">链接 24 小时内有效,好友在时效内体验AI求职助手</p>
|
||||||
|
</div>
|
||||||
|
<div class="settings-invite-dialog__step">
|
||||||
|
<div class="settings-invite-dialog__step-title">第3步:分享人获得7天会员权益</div>
|
||||||
|
<p class="settings-invite-dialog__step-desc">免费会员权益一年内有效,AI助力加速上岸</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计数据 -->
|
||||||
|
<div class="settings-invite-dialog__stats">
|
||||||
|
<div class="settings-invite-dialog__stat-item">
|
||||||
|
<div class="settings-invite-dialog__stat-label">累计获得会员天数</div>
|
||||||
|
<div class="settings-invite-dialog__stat-value">{{ totalDays }}天</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-invite-dialog__stat-item">
|
||||||
|
<div class="settings-invite-dialog__stat-label">累计邀请好友</div>
|
||||||
|
<div class="settings-invite-dialog__stat-value">{{ totalFriends }}位</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 活动规则折叠 -->
|
||||||
|
<div class="settings-invite-dialog__rules" @click="showRules = !showRules">
|
||||||
|
<span class="settings-invite-dialog__rules-label">活动规则</span>
|
||||||
|
<svg class="settings-invite-dialog__rules-arrow" :class="{ 'settings-invite-dialog__rules-arrow--open': showRules }" viewBox="0 0 12 12" fill="none">
|
||||||
|
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div v-if="showRules" class="settings-invite-dialog__rules-content">
|
||||||
|
<p>1. 每成功邀请一位新用户注册并完成首次体验,邀请人可获得 7 天免费会员权益。</p>
|
||||||
|
<p>2. 邀请链接有效期为 24 小时,过期需重新生成。</p>
|
||||||
|
<p>3. 累计获得的会员天数一年内有效,超过一年未使用的天数将自动失效。</p>
|
||||||
|
<p>4. 同一被邀请人仅计算一次,重复邀请不累计奖励。</p>
|
||||||
|
<p>5. 本活动最终解释权归平台所有。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
/** 组件 Props */
|
||||||
|
defineProps<{ modelValue: boolean }>()
|
||||||
|
|
||||||
|
/** 组件 Emits */
|
||||||
|
defineEmits<{ (e: 'update:modelValue', value: boolean): void }>()
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
/** 活动规则展开状态 */
|
||||||
|
const showRules = ref(false)
|
||||||
|
|
||||||
|
/** 累计获得会员天数(暂用模拟数据) */
|
||||||
|
const totalDays = ref(1)
|
||||||
|
|
||||||
|
/** 累计邀请好友数(暂用模拟数据) */
|
||||||
|
const totalFriends = ref(1)
|
||||||
|
|
||||||
|
/** 用户邀请码 — 从全局 store 读取 */
|
||||||
|
const inviteCode = computed(() => store.state.userInfo?.inviteCode || '')
|
||||||
|
|
||||||
|
/** 邀请链接文案 */
|
||||||
|
const inviteText = computed(() => {
|
||||||
|
const code = inviteCode.value
|
||||||
|
return `https://www.qiuzhizhushou.com/invite_code=${code},点击链接进入活动!`
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 复制链接到剪贴板 */
|
||||||
|
async function handleCopy() {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(inviteText.value)
|
||||||
|
ElMessage.success('链接已复制')
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('复制失败,请手动复制')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -112,7 +112,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<!-- 设置弹窗 -->
|
<!-- 设置弹窗 -->
|
||||||
<SettingsDialog v-model="showSettingsDialog" />
|
<SettingsDialog v-model="showSettingsDialog" :initial-tab="store.state.settingsTab" />
|
||||||
|
<!-- 邀请注册送会员弹窗 -->
|
||||||
|
<SettingsInviteDialog v-model="showShareDialog" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -132,6 +134,7 @@ import navAgentIcon from '@/assets/images/nav/nav-agent-icon.png'
|
|||||||
import navMessageIcon from '@/assets/images/nav/nav-message-icon.png'
|
import navMessageIcon from '@/assets/images/nav/nav-message-icon.png'
|
||||||
import navSettingIcon from '@/assets/images/nav/nav-setting-icon.png'
|
import navSettingIcon from '@/assets/images/nav/nav-setting-icon.png'
|
||||||
import navFeedbackIcon from '@/assets/images/nav/nav-feedback-icon.png'
|
import navFeedbackIcon from '@/assets/images/nav/nav-feedback-icon.png'
|
||||||
|
import navShareIcon from '@/assets/images/nav/nav-share-icon.png'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -190,13 +193,18 @@ const mainMenus = computed<MenuItem[]>(() => {
|
|||||||
return [...staticMenus, ...dynamicMenuItems.value]
|
return [...staticMenus, ...dynamicMenuItems.value]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const showShareDialog = ref(false)
|
||||||
const showMessageDialog = ref(false)
|
const showMessageDialog = ref(false)
|
||||||
const showFeedbackDialog = ref(false)
|
const showFeedbackDialog = ref(false)
|
||||||
const showSettingsDialog = ref(false)
|
const showSettingsDialog = computed({
|
||||||
|
get: () => store.state.showSettings,
|
||||||
|
set: (val: boolean) => store.commit('SET_SHOW_SETTINGS', val),
|
||||||
|
})
|
||||||
const feedbackType = ref('')
|
const feedbackType = ref('')
|
||||||
const feedbackTypeIndex = ref(0)
|
const feedbackTypeIndex = ref(0)
|
||||||
const feedbackDetail = ref('')
|
const feedbackDetail = ref('')
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import SettingsInviteDialog from "@/components/SettingsInviteDialog.vue";
|
||||||
|
|
||||||
// ==================== 站内信相关 ====================
|
// ==================== 站内信相关 ====================
|
||||||
/** 未读消息数量 */
|
/** 未读消息数量 */
|
||||||
@@ -374,6 +382,7 @@ const settingsMenu = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const footerMenus = computed(() => [
|
const footerMenus = computed(() => [
|
||||||
|
{ iconImg: navShareIcon, label: '分享送会员', action: () => { showShareDialog.value = true } },
|
||||||
{ iconImg: navMessageIcon, label: '消息通知', badge: unreadCount.value > 0 ? 'NEW' : '', action: () => { showMessageDialog.value = true } },
|
{ iconImg: navMessageIcon, 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: navFeedbackIcon, label: '反馈', action: () => { showFeedbackDialog.value = true } },
|
{ iconImg: navFeedbackIcon, label: '反馈', action: () => { showFeedbackDialog.value = true } },
|
||||||
@@ -387,6 +396,7 @@ async function handleSettingsNav() {
|
|||||||
const res = await checkLogin()
|
const res = await checkLogin()
|
||||||
if (res.code === '0' && res.data === true) {
|
if (res.code === '0' && res.data === true) {
|
||||||
store.commit('SET_AUTHENTICATED', true)
|
store.commit('SET_AUTHENTICATED', true)
|
||||||
|
store.commit('SET_SETTINGS_TAB', 'account')
|
||||||
showSettingsDialog.value = true
|
showSettingsDialog.value = true
|
||||||
} else {
|
} else {
|
||||||
store.commit('SET_AUTHENTICATED', false)
|
store.commit('SET_AUTHENTICATED', false)
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 提示:双击选中一级(仅 allowParentSelect 开启时显示) -->
|
<!-- 提示:双击选中一级(仅 allowParentSelect 开启时显示) -->
|
||||||
<div v-if="searchText.length < 2 && allowParentSelect" class="industry-selector__hint">双击可选中一级行业分类</div>
|
<div v-if="searchText.length < 2 && allowParentSelect" class="industry-selector__hint">(双击可选中一级行业分类)</div>
|
||||||
|
|
||||||
<!-- 分栏联动选择区(搜索关键词不足 2 字符时显示) -->
|
<!-- 分栏联动选择区(搜索关键词不足 2 字符时显示) -->
|
||||||
<div v-if="searchText.length < 2" class="industry-selector__columns">
|
<div v-if="searchText.length < 2" class="industry-selector__columns">
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 提示:双击选中一二级(仅 allowParentSelect 开启时显示) -->
|
<!-- 提示:双击选中一二级(仅 allowParentSelect 开启时显示) -->
|
||||||
<div v-if="searchText.length < 2 && allowParentSelect" class="job-category-selector__hint">双击可选中一级/二级岗位分类</div>
|
<div v-if="searchText.length < 2 && allowParentSelect" class="job-category-selector__hint">(双击可选中一级/二级岗位分类)</div>
|
||||||
|
|
||||||
<!-- 三栏联动选择区(搜索关键词不足 2 字符时显示) -->
|
<!-- 三栏联动选择区(搜索关键词不足 2 字符时显示) -->
|
||||||
<div v-if="searchText.length < 2" class="job-category-selector__columns">
|
<div v-if="searchText.length < 2" class="job-category-selector__columns">
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { fetchIndustryTree, fetchJobCategoryTree, fetchRegionTree } from '@/api/
|
|||||||
import type { IndustryItem, JobCategoryItem, RegionItem } from '@/api/common'
|
import type { IndustryItem, JobCategoryItem, RegionItem } from '@/api/common'
|
||||||
import { fetchJobIntention, saveJobIntention } from '@/api/jobs'
|
import { fetchJobIntention, saveJobIntention } from '@/api/jobs'
|
||||||
import type { JobIntention } from '@/api/jobs'
|
import type { JobIntention } from '@/api/jobs'
|
||||||
|
import { fetchUserInfo } from '@/api/auth'
|
||||||
|
import type { UserInfo } from '@/api/auth'
|
||||||
|
|
||||||
/** 职位列表页缓存数据(从详情页返回时恢复用) */
|
/** 职位列表页缓存数据(从详情页返回时恢复用) */
|
||||||
export interface JobListCache {
|
export interface JobListCache {
|
||||||
@@ -79,6 +81,18 @@ export interface RootState {
|
|||||||
* 由 loadJobIntention action 从接口加载,saveJobIntention action 保存后更新
|
* 由 loadJobIntention action 从接口加载,saveJobIntention action 保存后更新
|
||||||
*/
|
*/
|
||||||
jobIntention: JobIntention
|
jobIntention: JobIntention
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户个人信息 — 登录后从 /user/manage/info 接口获取
|
||||||
|
* 多个页面可直接从 store 读取
|
||||||
|
*/
|
||||||
|
userInfo: UserInfo | null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置弹窗 — 控制显示/隐藏及初始 Tab
|
||||||
|
*/
|
||||||
|
showSettings: boolean
|
||||||
|
settingsTab: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default createStore<RootState>({
|
export default createStore<RootState>({
|
||||||
@@ -100,6 +114,9 @@ export default createStore<RootState>({
|
|||||||
industryIds: [],
|
industryIds: [],
|
||||||
employmentType: 0,
|
employmentType: 0,
|
||||||
},
|
},
|
||||||
|
userInfo: null,
|
||||||
|
showSettings: false,
|
||||||
|
settingsTab: 'account',
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
getAppName: (state) => state.appName,
|
getAppName: (state) => state.appName,
|
||||||
@@ -151,6 +168,15 @@ export default createStore<RootState>({
|
|||||||
employmentType: data.employmentType ?? 0,
|
employmentType: data.employmentType ?? 0,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
SET_USER_INFO(state, data: UserInfo | null) {
|
||||||
|
state.userInfo = data
|
||||||
|
},
|
||||||
|
SET_SHOW_SETTINGS(state, show: boolean) {
|
||||||
|
state.showSettings = show
|
||||||
|
},
|
||||||
|
SET_SETTINGS_TAB(state, tab: string) {
|
||||||
|
state.settingsTab = tab
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
updateAppName({ commit }, name: string) {
|
updateAppName({ commit }, name: string) {
|
||||||
@@ -201,6 +227,7 @@ export default createStore<RootState>({
|
|||||||
commit('SET_AUTHENTICATED', false)
|
commit('SET_AUTHENTICATED', false)
|
||||||
commit('SET_SHOW_LOGIN', false)
|
commit('SET_SHOW_LOGIN', false)
|
||||||
commit('SET_LOGIN_REDIRECT', '')
|
commit('SET_LOGIN_REDIRECT', '')
|
||||||
|
commit('SET_USER_INFO', null)
|
||||||
// 清除 Jobs 页面缓存数据
|
// 清除 Jobs 页面缓存数据
|
||||||
commit('SET_JOB_LIST_CACHE', null)
|
commit('SET_JOB_LIST_CACHE', null)
|
||||||
commit('SET_JOB_INTENTION', {
|
commit('SET_JOB_INTENTION', {
|
||||||
@@ -263,6 +290,21 @@ export default createStore<RootState>({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载用户个人信息到 store
|
||||||
|
* 登录状态下调用,多个页面可直接读取 store.state.userInfo
|
||||||
|
*/
|
||||||
|
async loadUserInfo({ commit }) {
|
||||||
|
try {
|
||||||
|
const res = await fetchUserInfo()
|
||||||
|
if (res.code === '0' && res.data) {
|
||||||
|
commit('SET_USER_INFO', res.data)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[store] 加载用户信息失败', err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存求职意向:已登录时调接口保存并更新 store,未登录时仅更新 store
|
* 保存求职意向:已登录时调接口保存并更新 store,未登录时仅更新 store
|
||||||
* @param data 求职意向数据
|
* @param data 求职意向数据
|
||||||
|
|||||||
+63
-2
@@ -2,6 +2,18 @@
|
|||||||
<div class="jobs-page dflex">
|
<div class="jobs-page dflex">
|
||||||
<SideNav />
|
<SideNav />
|
||||||
<div class="jobs-page__content">
|
<div class="jobs-page__content">
|
||||||
|
|
||||||
|
<div v-if="false" class="fs18 p20 bgeee">
|
||||||
|
支付
|
||||||
|
<iframe
|
||||||
|
v-if="paymentFormHtml"
|
||||||
|
:srcdoc="paymentFormHtml"
|
||||||
|
frameborder="0"
|
||||||
|
style="width: 100%; height: 600px;"
|
||||||
|
scrolling="auto"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 页面标题 + Tab 切换 -->
|
<!-- 页面标题 + Tab 切换 -->
|
||||||
<JobPageHeader v-model:activeTab="activeTab" :favoriteCount="favoriteTotal" :applyCount="applyTotal" />
|
<JobPageHeader v-model:activeTab="activeTab" :favoriteCount="favoriteTotal" :applyCount="applyTotal" />
|
||||||
<!-- 收藏统计栏 -->
|
<!-- 收藏统计栏 -->
|
||||||
@@ -43,7 +55,7 @@
|
|||||||
:categoryIds="selectedCategoryIds"
|
:categoryIds="selectedCategoryIds"
|
||||||
:maxSelect="3"
|
:maxSelect="3"
|
||||||
:level="3"
|
:level="3"
|
||||||
:allowParentSelect="false"
|
:allowParentSelect="true"
|
||||||
@update:categoryIds="onCategoryChange"
|
@update:categoryIds="onCategoryChange"
|
||||||
/>
|
/>
|
||||||
<!-- 行业筛选:使用行业选择组件 -->
|
<!-- 行业筛选:使用行业选择组件 -->
|
||||||
@@ -52,7 +64,7 @@
|
|||||||
:industryIds="selectedIndustryIds"
|
:industryIds="selectedIndustryIds"
|
||||||
:maxSelect="3"
|
:maxSelect="3"
|
||||||
:level="2"
|
:level="2"
|
||||||
:allowParentSelect="false"
|
:allowParentSelect="true"
|
||||||
@update:industryIds="onIndustryChange"
|
@update:industryIds="onIndustryChange"
|
||||||
/>
|
/>
|
||||||
<!-- 其他筛选条件 -->
|
<!-- 其他筛选条件 -->
|
||||||
@@ -246,6 +258,9 @@
|
|||||||
|
|
||||||
<!-- 职位问题反馈弹窗 -->
|
<!-- 职位问题反馈弹窗 -->
|
||||||
<JobFeedbackDialog ref="feedbackDialogRef" v-model="showFeedbackDialog" :job-id="feedbackJobId" />
|
<JobFeedbackDialog ref="feedbackDialogRef" v-model="showFeedbackDialog" :job-id="feedbackJobId" />
|
||||||
|
|
||||||
|
<!-- 欢迎上传简历弹窗 -->
|
||||||
|
<ProfileWelcomeDialog v-model="showWelcomeDialog" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -261,9 +276,21 @@ import JobFeedbackDialog from '@/components/JobFeedbackDialog.vue'
|
|||||||
import IndustrySelector from '@/components/tools/IndustrySelector.vue'
|
import IndustrySelector from '@/components/tools/IndustrySelector.vue'
|
||||||
import JobCategorySelector from '@/components/tools/JobCategorySelector.vue'
|
import JobCategorySelector from '@/components/tools/JobCategorySelector.vue'
|
||||||
import RegionSelector from '@/components/tools/RegionSelector.vue'
|
import RegionSelector from '@/components/tools/RegionSelector.vue'
|
||||||
|
import ProfileWelcomeDialog from '@/components/ProfileWelcomeDialog.vue'
|
||||||
|
import { fetchProfile } from '@/api/profile'
|
||||||
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'
|
||||||
|
|
||||||
|
|
||||||
|
// 2. 注意:这里的字符串不需要再手动转义双引号了
|
||||||
|
const paymentFormHtml = ref(`
|
||||||
|
<form name="punchout_form" method="post" action="https://openapi-sandbox.dl.alipaydev.com/gateway.do?charset=UTF-8&method=alipay.trade.page.pay&sign=o8MQ7cEAh4yrf6ri5iT9gXbtR%2BTncX36p9JNZf8nmXxEp1WKO6dF1HDC6DPF8nxJbus6OG9E5UPSoiHgqrEiu3aeOs4LMcYzXXZP0UQzG%2F6pXyZrvlL70G8uwpnV6MgQojzhKheG0vYIJgSPaGsHKjwcp%2BT8yOZg3eEDIgFYEBf2a3c7kANc7%2FuOOGCSD9Ip9zqwX9LQ3b433NPH3AbGQkFSLsdGOTY62I7yI8hEF7EM7nNm%2BYryieqju21yN76JrV4k%2BgIGHn83I5oT3y7zurzdTqHLD4UawpPWdJ5B7p6wnJ4cN4uzO8JgRpNqogYqqKBPcHIlB9o4uax3bcRqhg%3D%3D¬ify_url=http%3A%2F%2F39.108.134.218%3A10106%2Fapi%2F%2Fpublic%2FalipayNotify%2Fpay&version=1.0&app_id=2018062160374941&sign_type=RSA2×tamp=2026-05-20+15%3A29%3A21&alipay_sdk=alipay-sdk-java-4.40.791.ALL&format=json">
|
||||||
|
<input type="hidden" name="biz_content" value='{"out_trade_no":"OP2057000709638877186","product_code":"FAST_INSTANT_TRADE_PAY","qr_pay_mode":"4","qrcode_width":200,"subject":"周会员","total_amount":"0.10"}'>
|
||||||
|
<input type="submit" value="立即支付" style="display:none">
|
||||||
|
</form>
|
||||||
|
<script>document.forms[0].submit();<\/script>
|
||||||
|
`)
|
||||||
|
|
||||||
// ==================== 路由相关 ====================
|
// ==================== 路由相关 ====================
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -273,6 +300,9 @@ const store = useStore()
|
|||||||
/** 登录状态 — 从 store 读取 */
|
/** 登录状态 — 从 store 读取 */
|
||||||
const isAuthenticated = computed(() => store.state.isAuthenticated)
|
const isAuthenticated = computed(() => store.state.isAuthenticated)
|
||||||
|
|
||||||
|
/** 用户昵称 — 从全局 store 读取 */
|
||||||
|
const nickName = computed(() => store.state.userInfo?.nick || '')
|
||||||
|
|
||||||
// ==================== 页面状态 ====================
|
// ==================== 页面状态 ====================
|
||||||
|
|
||||||
/** 当前激活的 Tab,从 URL query 参数读取,默认"推荐" */
|
/** 当前激活的 Tab,从 URL query 参数读取,默认"推荐" */
|
||||||
@@ -302,6 +332,9 @@ const feedbackJobId = ref<string | null>(null)
|
|||||||
/** 当前问助手的岗位 ID(传给 AiChat 组件) */
|
/** 当前问助手的岗位 ID(传给 AiChat 组件) */
|
||||||
const currentAskJobId = ref<string>('')
|
const currentAskJobId = ref<string>('')
|
||||||
|
|
||||||
|
/** 欢迎弹窗的显示状态 */
|
||||||
|
const showWelcomeDialog = ref(false)
|
||||||
|
|
||||||
/** 点击"问助手"按钮,传入岗位 ID 给 AiChat */
|
/** 点击"问助手"按钮,传入岗位 ID 给 AiChat */
|
||||||
function askAssistant(job: JobItem) {
|
function askAssistant(job: JobItem) {
|
||||||
currentAskJobId.value = job.id
|
currentAskJobId.value = job.id
|
||||||
@@ -516,6 +549,19 @@ onMounted(async () => {
|
|||||||
|
|
||||||
// 仅登录状态下才加载需要鉴权的数据
|
// 仅登录状态下才加载需要鉴权的数据
|
||||||
if (isAuthenticated.value) {
|
if (isAuthenticated.value) {
|
||||||
|
// 加载用户个人信息到全局 store
|
||||||
|
store.dispatch('loadUserInfo')
|
||||||
|
|
||||||
|
// 检查个人资料是否存在,不存在则弹出欢迎弹窗
|
||||||
|
try {
|
||||||
|
const profileRes = await fetchProfile()
|
||||||
|
if (profileRes.code === '0' && (!profileRes.data || profileRes.data === null)) {
|
||||||
|
showWelcomeDialog.value = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 接口异常不阻塞页面加载
|
||||||
|
}
|
||||||
|
|
||||||
// 加载收藏统计(用于 Tab 标签显示)
|
// 加载收藏统计(用于 Tab 标签显示)
|
||||||
loadFavoriteCount()
|
loadFavoriteCount()
|
||||||
|
|
||||||
@@ -861,6 +907,21 @@ watch(
|
|||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 监听登录状态变化 — 登录成功后检查个人资料是否存在
|
||||||
|
watch(isAuthenticated, async (newVal, oldVal) => {
|
||||||
|
if (newVal && !oldVal) {
|
||||||
|
// 从未登录变为已登录,检查个人资料
|
||||||
|
try {
|
||||||
|
const profileRes = await fetchProfile()
|
||||||
|
if (profileRes.code === '0' && (!profileRes.data || profileRes.data === null)) {
|
||||||
|
showWelcomeDialog.value = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 接口异常不阻塞
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 监听 Tab 切换,重置列表状态并加载对应数据
|
// 监听 Tab 切换,重置列表状态并加载对应数据
|
||||||
watch(activeTab, (newTab, oldTab) => {
|
watch(activeTab, (newTab, oldTab) => {
|
||||||
if (initializing) return
|
if (initializing) return
|
||||||
|
|||||||
+45
-24
@@ -9,6 +9,9 @@
|
|||||||
:initial-data="editInitialData"
|
:initial-data="editInitialData"
|
||||||
@save="handleSaveEdit"
|
@save="handleSaveEdit"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 欢迎上传简历弹窗 -->
|
||||||
|
<ProfileWelcomeDialog v-model="showWelcomeDialog" />
|
||||||
|
|
||||||
<!-- 页面标题 -->
|
<!-- 页面标题 -->
|
||||||
<div class="profile-page__header">
|
<div class="profile-page__header">
|
||||||
@@ -51,12 +54,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, watch, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useStore } from 'vuex'
|
import { useStore } from 'vuex'
|
||||||
import SideNav from '@/components/SideNav.vue'
|
import SideNav from '@/components/SideNav.vue'
|
||||||
import ProfileEditDrawer from '@/components/ProfileEditDrawer.vue'
|
import ProfileEditDrawer from '@/components/ProfileEditDrawer.vue'
|
||||||
import ProfilePageContent from '@/components/ProfilePageContent.vue'
|
import ProfilePageContent from '@/components/ProfilePageContent.vue'
|
||||||
|
import ProfileWelcomeDialog from '@/components/ProfileWelcomeDialog.vue'
|
||||||
import { saveProfile, fetchProfile, fetchEducation, saveEducation, fetchWork, saveWork, fetchInternship, saveInternship, fetchProject, saveProject, fetchCompetition, saveCompetition } from '@/api/profile'
|
import { saveProfile, fetchProfile, fetchEducation, saveEducation, fetchWork, saveWork, fetchInternship, saveInternship, fetchProject, saveProject, fetchCompetition, saveCompetition } from '@/api/profile'
|
||||||
import type { SaveEducationItem, SaveWorkItem, SaveProjectItem, SaveCompetitionItem } from '@/api/profile'
|
import type { SaveEducationItem, SaveWorkItem, SaveProjectItem, SaveCompetitionItem } from '@/api/profile'
|
||||||
|
|
||||||
@@ -68,8 +72,26 @@ onMounted(async () => {
|
|||||||
if (!store.state.regions.length) {
|
if (!store.state.regions.length) {
|
||||||
store.dispatch('loadCommonData')
|
store.dispatch('loadCommonData')
|
||||||
}
|
}
|
||||||
// 请求个人资料主表数据,填充非数组字段
|
// 请求个人资料主表数据,判断是否需要弹出欢迎弹窗
|
||||||
await loadProfile()
|
const profileRes = await fetchProfile()
|
||||||
|
if (profileRes.code === '0' && (!profileRes.data || profileRes.data === null)) {
|
||||||
|
// 无个人资料数据,弹出欢迎上传弹窗
|
||||||
|
showWelcomeDialog.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 有数据,正常填充
|
||||||
|
if (profileRes.code === '0' && profileRes.data) {
|
||||||
|
const d = profileRes.data
|
||||||
|
profile.value.name = d.name || ''
|
||||||
|
profile.value.phone = d.mobileNumber || ''
|
||||||
|
profile.value.email = d.email || ''
|
||||||
|
profile.value.idNumber = d.idCard || ''
|
||||||
|
profile.value.regionCode = d.regionCode || ''
|
||||||
|
profile.value.wechat = d.wechatNumber || ''
|
||||||
|
profile.value.skills = d.skills || []
|
||||||
|
profile.value.certificates = d.certificates || []
|
||||||
|
profile.value.portfolioUrl = d.portfolioUrl || ''
|
||||||
|
}
|
||||||
// 请求教育经历数据
|
// 请求教育经历数据
|
||||||
await loadEducation()
|
await loadEducation()
|
||||||
// 请求工作经历数据
|
// 请求工作经历数据
|
||||||
@@ -82,27 +104,6 @@ onMounted(async () => {
|
|||||||
await loadCompetition()
|
await loadCompetition()
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 加载个人资料主表数据 */
|
|
||||||
async function loadProfile() {
|
|
||||||
try {
|
|
||||||
const res = await fetchProfile()
|
|
||||||
if (res.code === '0' && res.data) {
|
|
||||||
const d = res.data
|
|
||||||
profile.value.name = d.name || ''
|
|
||||||
profile.value.phone = d.mobileNumber || ''
|
|
||||||
profile.value.email = d.email || ''
|
|
||||||
profile.value.idNumber = d.idCard || ''
|
|
||||||
profile.value.regionCode = d.regionCode || ''
|
|
||||||
profile.value.wechat = d.wechatNumber || ''
|
|
||||||
profile.value.skills = d.skills || []
|
|
||||||
profile.value.certificates = d.certificates || []
|
|
||||||
profile.value.portfolioUrl = d.portfolioUrl || ''
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
console.error('[Profile] 加载个人资料失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 加载教育经历数据 */
|
/** 加载教育经历数据 */
|
||||||
async function loadEducation() {
|
async function loadEducation() {
|
||||||
try {
|
try {
|
||||||
@@ -213,6 +214,26 @@ async function loadCompetition() {
|
|||||||
/** 编辑抽屉的显示状态 */
|
/** 编辑抽屉的显示状态 */
|
||||||
const showEditDrawer = ref(false)
|
const showEditDrawer = ref(false)
|
||||||
|
|
||||||
|
/** 欢迎弹窗的显示状态 */
|
||||||
|
const showWelcomeDialog = ref(false)
|
||||||
|
|
||||||
|
/** 登录状态 */
|
||||||
|
const isAuthenticated = computed(() => store.state.isAuthenticated)
|
||||||
|
|
||||||
|
/** 监听登录状态变化 — 登录成功后检查个人资料是否存在 */
|
||||||
|
watch(isAuthenticated, async (newVal, oldVal) => {
|
||||||
|
if (newVal && !oldVal) {
|
||||||
|
try {
|
||||||
|
const profileRes = await fetchProfile()
|
||||||
|
if (profileRes.code === '0' && (!profileRes.data || profileRes.data === null)) {
|
||||||
|
showWelcomeDialog.value = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 接口异常不阻塞
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
/** 当前编辑的模块名称 */
|
/** 当前编辑的模块名称 */
|
||||||
const editModule = ref('info')
|
const editModule = ref('info')
|
||||||
|
|
||||||
|
|||||||
+12
-77
@@ -15,7 +15,11 @@
|
|||||||
|
|
||||||
<!-- 简历表格 -->
|
<!-- 简历表格 -->
|
||||||
<div class="resume-page__table-wrap">
|
<div class="resume-page__table-wrap">
|
||||||
<table class="resume-page__table">
|
<!-- 空状态提示 -->
|
||||||
|
<div v-if="resumeList.length === 0" class="resume-page__empty">
|
||||||
|
<p>暂无简历,请点击上方按钮上传简历</p>
|
||||||
|
</div>
|
||||||
|
<table v-else class="resume-page__table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="resume-page__th">简历</th>
|
<th class="resume-page__th">简历</th>
|
||||||
@@ -82,40 +86,21 @@
|
|||||||
@saved="loadResumeList"
|
@saved="loadResumeList"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 导出简历格式选择弹窗 -->
|
<!-- 导出简历弹窗组件 -->
|
||||||
<el-dialog
|
<ResumeExportDialog
|
||||||
v-model="exportDialogVisible"
|
v-model="exportDialogVisible"
|
||||||
title="导出简历"
|
:resume-id="exportResumeId"
|
||||||
width="3.6rem"
|
:resume-name="exportResumeName"
|
||||||
:close-on-click-modal="false"
|
/>
|
||||||
class="resume-export-dialog"
|
|
||||||
>
|
|
||||||
<!-- 格式选择 -->
|
|
||||||
<el-radio-group v-model="exportFormat" class="resume-export-dialog__radio-group">
|
|
||||||
<el-radio value="pdf">PDF 简历</el-radio>
|
|
||||||
<el-radio value="word">Word 简历</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="exportDialogVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="exporting" @click="doExport">下载</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- 隐藏的简历模板,用于导出时渲染DOM -->
|
|
||||||
<div v-if="exportTemplateData" style="position:absolute;left:-9999px;top:0;">
|
|
||||||
<JobResumeTemplate ref="exportTemplateRef" :resume-data="exportTemplateData" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, nextTick } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import SideNav from '@/components/SideNav.vue'
|
import SideNav from '@/components/SideNav.vue'
|
||||||
import ResumeEditNameDialog from '@/components/ResumeEditNameDialog.vue'
|
import ResumeEditNameDialog from '@/components/ResumeEditNameDialog.vue'
|
||||||
import JobResumeTemplate from '@/components/JobResumeTemplate.vue'
|
import ResumeExportDialog from '@/components/ResumeExportDialog.vue'
|
||||||
import type { ResumeTemplateData } from '@/components/JobResumeTemplate.vue'
|
|
||||||
import { exportResumePdf, exportResumeWord, loadResumeTemplateData } from '@/utils/resumeExport'
|
|
||||||
import { uploadResume } from '@/utils/aiRequest'
|
import { uploadResume } from '@/utils/aiRequest'
|
||||||
import {
|
import {
|
||||||
fetchResumeList, deleteResume, type ResumeListItem,
|
fetchResumeList, deleteResume, type ResumeListItem,
|
||||||
@@ -228,60 +213,10 @@ const editTargetPosition = ref('')
|
|||||||
|
|
||||||
/** 导出弹窗是否可见 */
|
/** 导出弹窗是否可见 */
|
||||||
const exportDialogVisible = ref(false)
|
const exportDialogVisible = ref(false)
|
||||||
/** 导出格式:pdf 或 word */
|
|
||||||
const exportFormat = ref<'pdf' | 'word'>('pdf')
|
|
||||||
/** 导出中状态 */
|
|
||||||
const exporting = ref(false)
|
|
||||||
/** 当前导出的简历 ID */
|
/** 当前导出的简历 ID */
|
||||||
const exportResumeId = ref('')
|
const exportResumeId = ref('')
|
||||||
/** 当前导出的简历名称(用于文件名) */
|
/** 当前导出的简历名称(用于文件名) */
|
||||||
const exportResumeName = ref('')
|
const exportResumeName = ref('')
|
||||||
/** 导出用的简历模板数据 */
|
|
||||||
const exportTemplateData = ref<ResumeTemplateData | null>(null)
|
|
||||||
/** 导出用的简历模板组件引用 */
|
|
||||||
const exportTemplateRef = ref<InstanceType<typeof JobResumeTemplate> | null>(null)
|
|
||||||
|
|
||||||
/** 执行导出下载 */
|
|
||||||
async function doExport() {
|
|
||||||
exporting.value = true
|
|
||||||
try {
|
|
||||||
// 1. 加载简历完整数据
|
|
||||||
const data = await loadResumeTemplateData(exportResumeId.value)
|
|
||||||
if (!data) {
|
|
||||||
ElMessage.error('获取简历数据失败')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 设置模板数据,等待DOM渲染
|
|
||||||
exportTemplateData.value = data
|
|
||||||
await nextTick()
|
|
||||||
|
|
||||||
// 3. 获取渲染后的DOM
|
|
||||||
const element = exportTemplateRef.value?.resumeRef
|
|
||||||
if (!element) {
|
|
||||||
ElMessage.error('简历模板渲染失败')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileName = exportResumeName.value || '简历'
|
|
||||||
|
|
||||||
if (exportFormat.value === 'pdf') {
|
|
||||||
await exportResumePdf(element, fileName)
|
|
||||||
} else {
|
|
||||||
exportResumeWord(element, fileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
ElMessage.success('导出成功')
|
|
||||||
exportDialogVisible.value = false
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[导出简历] 失败', err)
|
|
||||||
ElMessage.error('导出失败,请稍后重试')
|
|
||||||
} finally {
|
|
||||||
exporting.value = false
|
|
||||||
// 清理隐藏模板数据
|
|
||||||
exportTemplateData.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 弹出菜单操作项 */
|
/** 弹出菜单操作项 */
|
||||||
const popupActions = ['设为默认简历', '编辑名称岗位', '导出简历', '删除']
|
const popupActions = ['设为默认简历', '编辑名称岗位', '导出简历', '删除']
|
||||||
|
|||||||
@@ -29,6 +29,22 @@
|
|||||||
@submit="handleFixSubmit"
|
@submit="handleFixSubmit"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 导出简历弹窗组件 -->
|
||||||
|
<ResumeExportDialog
|
||||||
|
v-model="exportDialogVisible"
|
||||||
|
:resume-id="resumeId"
|
||||||
|
:resume-name="resumeMain.resumeName || '简历'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 编辑简历名称弹窗 -->
|
||||||
|
<ResumeEditNameDialog
|
||||||
|
v-model="editNameVisible"
|
||||||
|
:resume-id="resumeId"
|
||||||
|
:resume-name="resumeMain.resumeName || ''"
|
||||||
|
:target-position="resumeMain.targetPosition || ''"
|
||||||
|
@saved="loadResumeDetail"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 顶部标题 -->
|
<!-- 顶部标题 -->
|
||||||
<h2 class="resume-detail__page-title">我的简历</h2>
|
<h2 class="resume-detail__page-title">我的简历</h2>
|
||||||
|
|
||||||
@@ -43,7 +59,6 @@
|
|||||||
<span class="resume-detail__tab-name">{{ resumeMain.resumeName || '未命名简历' }}</span>
|
<span class="resume-detail__tab-name">{{ resumeMain.resumeName || '未命名简历' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="resume-detail__toolbar-right">
|
<div class="resume-detail__toolbar-right">
|
||||||
<button class="resume-detail__tool-btn" @click="handleFeedback">问题反馈</button>
|
|
||||||
<button class="resume-detail__tool-btn" @click="handleEdit">
|
<button class="resume-detail__tool-btn" @click="handleEdit">
|
||||||
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__tool-icon">
|
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__tool-icon">
|
||||||
<path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
|
<path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
|
||||||
@@ -407,6 +422,8 @@ import SideNav from '@/components/SideNav.vue'
|
|||||||
import ProfileEditDrawer from '@/components/ProfileEditDrawer.vue'
|
import ProfileEditDrawer from '@/components/ProfileEditDrawer.vue'
|
||||||
import ResumeAnalysisReportDrawer from '@/components/ResumeAnalysisReportDrawer.vue'
|
import ResumeAnalysisReportDrawer from '@/components/ResumeAnalysisReportDrawer.vue'
|
||||||
import ResumeIssueFixDrawer from '@/components/ResumeIssueFixDrawer.vue'
|
import ResumeIssueFixDrawer from '@/components/ResumeIssueFixDrawer.vue'
|
||||||
|
import ResumeExportDialog from '@/components/ResumeExportDialog.vue'
|
||||||
|
import ResumeEditNameDialog from '@/components/ResumeEditNameDialog.vue'
|
||||||
import {
|
import {
|
||||||
fetchResumeMain,
|
fetchResumeMain,
|
||||||
fetchResumeEducation,
|
fetchResumeEducation,
|
||||||
@@ -675,14 +692,23 @@ function goBack() {
|
|||||||
router.push('/resume')
|
router.push('/resume')
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 问题反馈 */
|
|
||||||
function handleFeedback() { console.log('问题反馈') }
|
|
||||||
|
|
||||||
/** 编辑简历信息 */
|
|
||||||
function handleEdit() { console.log('编辑简历信息') }
|
|
||||||
|
|
||||||
/** 导出简历 */
|
/** 编辑简历名称弹窗显示状态 */
|
||||||
function handleExport() { console.log('导出') }
|
const editNameVisible = ref(false)
|
||||||
|
|
||||||
|
/** 编辑简历信息 — 打开编辑名称弹窗 */
|
||||||
|
function handleEdit() {
|
||||||
|
editNameVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出简历弹窗显示状态 */
|
||||||
|
const exportDialogVisible = ref(false)
|
||||||
|
|
||||||
|
/** 导出简历 — 打开导出弹窗 */
|
||||||
|
function handleExport() {
|
||||||
|
exportDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
/** 删除简历 — 二次确认后调用删除接口,成功后返回简历列表 */
|
/** 删除简历 — 二次确认后调用删除接口,成功后返回简历列表 */
|
||||||
async function handleDelete() {
|
async function handleDelete() {
|
||||||
|
|||||||
+2
-1
@@ -37,7 +37,8 @@ export default defineConfig({
|
|||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://127.0.0.1:8080',
|
// target: 'http://192.168.31.133:8080',
|
||||||
|
target: 'http://127.0.0.1:8080',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
'/ai-api': {
|
'/ai-api': {
|
||||||
|
|||||||
Reference in New Issue
Block a user