13 changed files with 5386 additions and 7 deletions
@ -0,0 +1,37 @@ |
|||||
|
import { defHttp } from '@/utils/http/axios'; |
||||
|
import { PaymentRequest, PaymentResponse } from './model'; |
||||
|
|
||||
|
/** |
||||
|
* 创建支付订单 |
||||
|
* @param data 支付请求数据 |
||||
|
* @returns |
||||
|
*/ |
||||
|
export function createPayment(data: PaymentRequest) { |
||||
|
return defHttp.postWithMsg<PaymentResponse>({ |
||||
|
url: '/contractreview/payment/create', |
||||
|
data |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 查询支付状态 |
||||
|
* @param orderNo 订单号 |
||||
|
* @returns |
||||
|
*/ |
||||
|
export function queryPaymentStatus(orderNo: string) { |
||||
|
return defHttp.get<any>({ |
||||
|
url: `/contractreview/payment/status/${orderNo}` |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 处理支付回调 |
||||
|
* @param data 回调数据 |
||||
|
* @returns |
||||
|
*/ |
||||
|
export function handlePaymentCallback(data: any) { |
||||
|
return defHttp.post<string>({ |
||||
|
url: '/contractreview/payment/callback', |
||||
|
data |
||||
|
}); |
||||
|
} |
@ -0,0 +1,71 @@ |
|||||
|
/** |
||||
|
* 支付请求参数 |
||||
|
*/ |
||||
|
export interface PaymentRequest { |
||||
|
/** 套餐类型 */ |
||||
|
planType: string; |
||||
|
/** 套餐名称 */ |
||||
|
planName: string; |
||||
|
/** 时长(月) */ |
||||
|
duration: number; |
||||
|
/** 金额 */ |
||||
|
amount: number; |
||||
|
/** 支付方式 */ |
||||
|
paymentMethod: string; |
||||
|
/** 用户ID */ |
||||
|
userId: number; |
||||
|
/** 客户端IP */ |
||||
|
clientIp: string; |
||||
|
/** 异步通知地址 */ |
||||
|
notifyUrl: string; |
||||
|
/** 同步回调地址 */ |
||||
|
returnUrl: string; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 支付响应结果 |
||||
|
*/ |
||||
|
export interface PaymentResponse { |
||||
|
/** 是否成功 */ |
||||
|
success: boolean; |
||||
|
/** 错误信息 */ |
||||
|
errorMsg?: string; |
||||
|
/** 订单号 */ |
||||
|
orderNo?: string; |
||||
|
/** 支付状态 */ |
||||
|
payStatus?: string; |
||||
|
/** 二维码链接 */ |
||||
|
qrCodeUrl?: string; |
||||
|
/** 支付链接 */ |
||||
|
payUrl?: string; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 支付状态查询响应 |
||||
|
*/ |
||||
|
export interface PaymentStatusResponse { |
||||
|
/** 是否成功 */ |
||||
|
success: boolean; |
||||
|
/** 错误信息 */ |
||||
|
errorMsg?: string; |
||||
|
/** 商户订单号 */ |
||||
|
mchOrderNo: string; |
||||
|
/** 支付状态文本 (INIT/ING/SUCCESS/FAIL/CANCEL/REFUND/CLOSED) */ |
||||
|
payStatus: string; |
||||
|
/** Jeepay原始状态码 (0-6) */ |
||||
|
jeepayState: number; |
||||
|
/** 支付金额 */ |
||||
|
amount?: number; |
||||
|
/** 支付时间 */ |
||||
|
payTime?: string; |
||||
|
/** Jeepay支付订单ID */ |
||||
|
payOrderId?: string; |
||||
|
/** 支付方式代码 */ |
||||
|
wayCode?: string; |
||||
|
/** 货币类型 */ |
||||
|
currency?: string; |
||||
|
/** 商品标题 */ |
||||
|
subject?: string; |
||||
|
/** 商品描述 */ |
||||
|
body?: string; |
||||
|
} |
@ -0,0 +1,322 @@ |
|||||
|
<template> |
||||
|
<div class="user-homepage"> |
||||
|
<!-- 导航栏 --> |
||||
|
<header class="navbar"> |
||||
|
<div class="nav-container"> |
||||
|
<div class="logo"> |
||||
|
<img src="/logo.png" alt="小研智审" class="logo-img" /> |
||||
|
<span class="logo-text">小研智审</span> |
||||
|
</div> |
||||
|
<nav class="nav-links"> |
||||
|
<a href="/user/login" class="admin-btn">立即体验</a> |
||||
|
</nav> |
||||
|
</div> |
||||
|
</header> |
||||
|
|
||||
|
<!-- 主banner区域 --> |
||||
|
<section class="hero-section"> |
||||
|
<div class="hero-container"> |
||||
|
<div class="hero-content"> |
||||
|
<h1 class="hero-title"> |
||||
|
小研智审<br /> |
||||
|
<span class="highlight">合同审核全能助手,AI赋能读改审</span> |
||||
|
</h1> |
||||
|
<p class="hero-subtitle"> |
||||
|
识别合同条款中的不平等内容,确保权利义务分配合理;<br> |
||||
|
依据现行法律法规、行业规范和监管要求,对合同条款进行全面审查,确保合同条款规定不违反法律法规;<br> |
||||
|
将合同条款与招标文件中的合同要求进行逐比对,防止关键条款偏离。 |
||||
|
</p> |
||||
|
<div class="hero-buttons"> |
||||
|
<button class="btn btn-primary" @click="startTrial">立即体验</button> |
||||
|
<button class="btn btn-secondary" @click="contactUs">联系我们</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="hero-visual"> |
||||
|
<div class="ai-card"> |
||||
|
<div class="card-header"> |
||||
|
<div class="card-icon">🤖</div> |
||||
|
<h3>AI智能分析</h3> |
||||
|
</div> |
||||
|
<div class="card-content"> |
||||
|
<div class="analysis-item"> |
||||
|
<span class="dot dot-success"></span> |
||||
|
<span>合同条款完整性检查</span> |
||||
|
</div> |
||||
|
<div class="analysis-item"> |
||||
|
<span class="dot dot-warning"></span> |
||||
|
<span>风险点识别与标注</span> |
||||
|
</div> |
||||
|
<div class="analysis-item"> |
||||
|
<span class="dot dot-info"></span> |
||||
|
<span>法律条文智能匹配</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts" setup> |
||||
|
import { onMounted } from 'vue'; |
||||
|
|
||||
|
const startTrial = () => { |
||||
|
// 可以跳转到试用页面或显示注册弹窗 |
||||
|
window.open('/user/login', '_blank'); |
||||
|
}; |
||||
|
|
||||
|
const contactUs = () => { |
||||
|
// 联系我们功能 |
||||
|
alert('联系我们:contact@markup.ai'); |
||||
|
}; |
||||
|
|
||||
|
onMounted(() => { |
||||
|
|
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<style lang="less" scoped> |
||||
|
* { |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
|
||||
|
.user-homepage { |
||||
|
min-height: 100vh; |
||||
|
background: #ffffff; |
||||
|
} |
||||
|
|
||||
|
/* 导航栏 */ |
||||
|
.navbar { |
||||
|
position: fixed; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
background: rgba(255, 255, 255, 0.95); |
||||
|
backdrop-filter: blur(10px); |
||||
|
border-bottom: 1px solid #eee; |
||||
|
z-index: 1000; |
||||
|
padding: 0.5rem 0; |
||||
|
} |
||||
|
|
||||
|
.nav-container { |
||||
|
max-width: 1200px; |
||||
|
margin: 0 auto; |
||||
|
padding: 0 2rem; |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.logo { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 0.5rem; |
||||
|
font-size: 1.5rem; |
||||
|
font-weight: bold; |
||||
|
color: #2c3e50; |
||||
|
} |
||||
|
|
||||
|
.logo-img { |
||||
|
width: 32px; |
||||
|
height: 32px; |
||||
|
} |
||||
|
|
||||
|
.nav-links { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 2rem; |
||||
|
|
||||
|
a { |
||||
|
text-decoration: none; |
||||
|
color: #2c3e50; |
||||
|
font-weight: 500; |
||||
|
transition: color 0.3s; |
||||
|
|
||||
|
&:hover { |
||||
|
color: #3498db; |
||||
|
} |
||||
|
|
||||
|
&.admin-btn { |
||||
|
background: #3498db; |
||||
|
color: white; |
||||
|
padding: 0.5rem 1rem; |
||||
|
border-radius: 6px; |
||||
|
|
||||
|
&:hover { |
||||
|
background: #2980b9; |
||||
|
color: white; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* Hero区域 */ |
||||
|
.hero-section { |
||||
|
padding: 8rem 0 4rem; |
||||
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); |
||||
|
min-height: 100vh; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.hero-container { |
||||
|
max-width: 1200px; |
||||
|
margin: 0 auto; |
||||
|
padding: 0 2rem; |
||||
|
display: grid; |
||||
|
grid-template-columns: 1fr 1fr; |
||||
|
gap: 4rem; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.hero-title { |
||||
|
font-size: 3rem; |
||||
|
font-weight: 700; |
||||
|
color: #2c3e50; |
||||
|
margin-bottom: 1.5rem; |
||||
|
line-height: 1.2; |
||||
|
|
||||
|
.highlight { |
||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||
|
-webkit-background-clip: text; |
||||
|
-webkit-text-fill-color: transparent; |
||||
|
background-clip: text; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.hero-subtitle { |
||||
|
font-size: 1.2rem; |
||||
|
color: #7f8c8d; |
||||
|
margin-bottom: 2rem; |
||||
|
line-height: 1.6; |
||||
|
} |
||||
|
|
||||
|
.hero-buttons { |
||||
|
display: flex; |
||||
|
gap: 1rem; |
||||
|
} |
||||
|
|
||||
|
.btn { |
||||
|
padding: 1rem 2rem; |
||||
|
border: none; |
||||
|
border-radius: 8px; |
||||
|
font-size: 1rem; |
||||
|
font-weight: 600; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s; |
||||
|
|
||||
|
&.btn-primary { |
||||
|
background: #3498db; |
||||
|
color: white; |
||||
|
|
||||
|
&:hover { |
||||
|
background: #2980b9; |
||||
|
transform: translateY(-2px); |
||||
|
box-shadow: 0 8px 25px rgba(52, 152, 219, 0.3); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.btn-secondary { |
||||
|
background: white; |
||||
|
color: #3498db; |
||||
|
border: 2px solid #3498db; |
||||
|
|
||||
|
&:hover { |
||||
|
background: #3498db; |
||||
|
color: white; |
||||
|
transform: translateY(-2px); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.hero-visual { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.ai-card { |
||||
|
background: white; |
||||
|
border-radius: 16px; |
||||
|
padding: 2rem; |
||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); |
||||
|
max-width: 350px; |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.card-header { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 1rem; |
||||
|
margin-bottom: 1.5rem; |
||||
|
|
||||
|
.card-icon { |
||||
|
font-size: 2rem; |
||||
|
} |
||||
|
|
||||
|
h3 { |
||||
|
color: #2c3e50; |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.analysis-item { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 0.75rem; |
||||
|
margin-bottom: 1rem; |
||||
|
font-size: 0.95rem; |
||||
|
color: #555; |
||||
|
} |
||||
|
|
||||
|
.dot { |
||||
|
width: 8px; |
||||
|
height: 8px; |
||||
|
border-radius: 50%; |
||||
|
|
||||
|
&.dot-success { |
||||
|
background: #27ae60; |
||||
|
} |
||||
|
|
||||
|
&.dot-warning { |
||||
|
background: #f39c12; |
||||
|
} |
||||
|
|
||||
|
&.dot-info { |
||||
|
background: #3498db; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.container { |
||||
|
max-width: 1200px; |
||||
|
margin: 0 auto; |
||||
|
padding: 0 2rem; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
/* 响应式设计 */ |
||||
|
@media (max-width: 768px) { |
||||
|
.nav-container { |
||||
|
padding: 0 1rem; |
||||
|
} |
||||
|
|
||||
|
.nav-links { |
||||
|
gap: 1rem; |
||||
|
|
||||
|
a { |
||||
|
font-size: 0.9rem; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.hero-container { |
||||
|
grid-template-columns: 1fr; |
||||
|
gap: 2rem; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.hero-title { |
||||
|
font-size: 2rem; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,752 @@ |
|||||
|
<template> |
||||
|
<div class="user-forgot-password-page"> |
||||
|
<!-- 背景装饰 --> |
||||
|
<div class="bg-decoration"> |
||||
|
<div class="decoration-circle circle-1"></div> |
||||
|
<div class="decoration-circle circle-2"></div> |
||||
|
<div class="decoration-circle circle-3"></div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="forgot-password-container"> |
||||
|
<!-- 左侧功能展示 --> |
||||
|
<div class="left-section"> |
||||
|
<div class="brand-info"> |
||||
|
<div class="logo-section"> |
||||
|
<img src="/logo.png" alt="小研智审" class="logo" /> |
||||
|
<h1 class="brand-name">小研智审</h1> |
||||
|
</div> |
||||
|
<p class="brand-desc">合同全能助手,AI赋能读改审</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="features-showcase"> |
||||
|
<div class="feature-card"> |
||||
|
<div class="feature-icon">📋</div> |
||||
|
<h3>合同审查</h3> |
||||
|
<p>智能识别合同条款中的不平等内容,确保权利义务分配合理</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="feature-card"> |
||||
|
<div class="feature-icon">⚖️</div> |
||||
|
<h3>法规检查</h3> |
||||
|
<p>依据现行法律法规、行业规范和监管要求进行全面审查</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="feature-card"> |
||||
|
<div class="feature-icon">🔍</div> |
||||
|
<h3>对比审查</h3> |
||||
|
<p>将合同条款与招标文件中的合同要求进行逐一比对</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="feature-card"> |
||||
|
<div class="feature-icon">📊</div> |
||||
|
<h3>结果生成</h3> |
||||
|
<p>生成详细的审查报告和改进建议</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 右侧忘记密码区域 --> |
||||
|
<div class="right-section"> |
||||
|
<div class="forgot-password-form-container"> |
||||
|
<!-- 标题 --> |
||||
|
<div class="form-header"> |
||||
|
<h2>忘记密码</h2> |
||||
|
<p>通过手机验证码重置您的密码</p> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 忘记密码表单 --> |
||||
|
<form @submit.prevent="handleResetPassword" class="forgot-password-form"> |
||||
|
<div class="form-group"> |
||||
|
<input |
||||
|
v-model="formData.username" |
||||
|
type="text" |
||||
|
placeholder="请输入用户名" |
||||
|
class="form-input" |
||||
|
required |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<input |
||||
|
v-model="formData.mobile" |
||||
|
type="tel" |
||||
|
placeholder="请输入手机号" |
||||
|
class="form-input" |
||||
|
required |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group sms-group"> |
||||
|
<input |
||||
|
v-model="formData.sms" |
||||
|
type="text" |
||||
|
placeholder="请输入短信验证码" |
||||
|
class="form-input sms-input" |
||||
|
required |
||||
|
/> |
||||
|
<button |
||||
|
type="button" |
||||
|
class="sms-btn" |
||||
|
@click="handleSendSmsCode" |
||||
|
:disabled="smsLoading || smsCountdown > 0" |
||||
|
> |
||||
|
{{ smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码' }} |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<div class="password-input-wrapper"> |
||||
|
<input |
||||
|
v-model="formData.password" |
||||
|
:type="passwordVisible ? 'text' : 'password'" |
||||
|
placeholder="请输入新密码" |
||||
|
class="form-input password-input" |
||||
|
required |
||||
|
/> |
||||
|
<button |
||||
|
type="button" |
||||
|
class="password-toggle-btn" |
||||
|
@click="togglePasswordVisibility" |
||||
|
> |
||||
|
<svg |
||||
|
v-if="!passwordVisible" |
||||
|
class="eye-icon" |
||||
|
viewBox="0 0 24 24" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
stroke-width="2" |
||||
|
> |
||||
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/> |
||||
|
<circle cx="12" cy="12" r="3"/> |
||||
|
</svg> |
||||
|
<svg |
||||
|
v-else |
||||
|
class="eye-icon" |
||||
|
viewBox="0 0 24 24" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
stroke-width="2" |
||||
|
> |
||||
|
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/> |
||||
|
<line x1="1" y1="1" x2="23" y2="23"/> |
||||
|
</svg> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<div class="password-input-wrapper"> |
||||
|
<input |
||||
|
v-model="formData.confirmPassword" |
||||
|
:type="confirmPasswordVisible ? 'text' : 'password'" |
||||
|
placeholder="请确认新密码" |
||||
|
class="form-input password-input" |
||||
|
required |
||||
|
/> |
||||
|
<button |
||||
|
type="button" |
||||
|
class="password-toggle-btn" |
||||
|
@click="toggleConfirmPasswordVisibility" |
||||
|
> |
||||
|
<svg |
||||
|
v-if="!confirmPasswordVisible" |
||||
|
class="eye-icon" |
||||
|
viewBox="0 0 24 24" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
stroke-width="2" |
||||
|
> |
||||
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/> |
||||
|
<circle cx="12" cy="12" r="3"/> |
||||
|
</svg> |
||||
|
<svg |
||||
|
v-else |
||||
|
class="eye-icon" |
||||
|
viewBox="0 0 24 24" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
stroke-width="2" |
||||
|
> |
||||
|
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/> |
||||
|
<line x1="1" y1="1" x2="23" y2="23"/> |
||||
|
</svg> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<button |
||||
|
type="submit" |
||||
|
class="reset-btn" |
||||
|
:disabled="loading" |
||||
|
:class="{ loading }" |
||||
|
> |
||||
|
<span v-if="loading" class="loading-spinner"></span> |
||||
|
{{ loading ? '重置中...' : '重置密码' }} |
||||
|
</button> |
||||
|
</form> |
||||
|
|
||||
|
<!-- 其他选项 --> |
||||
|
<div class="other-options"> |
||||
|
<p class="login-link"> |
||||
|
想起密码了?<a href="#" @click="handleBackToLogin($event)">立即登录</a> |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts" setup> |
||||
|
import { reactive, ref } from 'vue'; |
||||
|
import { useRouter } from 'vue-router'; |
||||
|
import { useMessage } from '@/hooks/web/useMessage'; |
||||
|
import { resetPasswordApi } from '@/api/auth'; |
||||
|
import { sendSmsCode } from '@/api/auth/captcha'; |
||||
|
|
||||
|
const router = useRouter(); |
||||
|
const { notification, createErrorModal } = useMessage(); |
||||
|
|
||||
|
// 表单数据 |
||||
|
const formData = reactive({ |
||||
|
username: '', |
||||
|
mobile: '', |
||||
|
sms: '', |
||||
|
password: '', |
||||
|
confirmPassword: '', |
||||
|
}); |
||||
|
|
||||
|
// 状态管理 |
||||
|
const loading = ref(false); |
||||
|
const smsLoading = ref(false); |
||||
|
const smsCountdown = ref(0); |
||||
|
|
||||
|
// 密码显示/隐藏状态 |
||||
|
const passwordVisible = ref(false); |
||||
|
const confirmPasswordVisible = ref(false); |
||||
|
|
||||
|
// 发送短信验证码 |
||||
|
const handleSendSmsCode = async () => { |
||||
|
// 验证手机号是否填写 |
||||
|
if (!formData.mobile) { |
||||
|
notification.error({ |
||||
|
message: '请先填写手机号码', |
||||
|
duration: 3, |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 验证手机号格式 |
||||
|
const mobileRegex = /^1[3-9]\d{9}$/; |
||||
|
if (!mobileRegex.test(formData.mobile)) { |
||||
|
notification.error({ |
||||
|
message: '请输入正确的手机号码', |
||||
|
duration: 3, |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
smsLoading.value = true; |
||||
|
await sendSmsCode(formData.mobile); |
||||
|
notification.success({ |
||||
|
message: '验证码发送成功', |
||||
|
duration: 3, |
||||
|
}); |
||||
|
|
||||
|
// 开始倒计时 |
||||
|
smsCountdown.value = 60; |
||||
|
const timer = setInterval(() => { |
||||
|
smsCountdown.value--; |
||||
|
if (smsCountdown.value <= 0) { |
||||
|
clearInterval(timer); |
||||
|
} |
||||
|
}, 1000); |
||||
|
} catch (error) { |
||||
|
notification.error({ |
||||
|
message: '验证码发送失败,请重试', |
||||
|
duration: 3, |
||||
|
}); |
||||
|
} finally { |
||||
|
smsLoading.value = false; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// 处理重置密码 |
||||
|
const handleResetPassword = async () => { |
||||
|
if (loading.value) return; |
||||
|
|
||||
|
// 验证密码确认 |
||||
|
if (formData.password !== formData.confirmPassword) { |
||||
|
notification.error({ |
||||
|
message: '两次输入的密码不一致', |
||||
|
duration: 3, |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 验证密码强度 |
||||
|
if (formData.password.length < 6) { |
||||
|
notification.error({ |
||||
|
message: '密码长度不能少于6位', |
||||
|
duration: 3, |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
loading.value = true; |
||||
|
|
||||
|
await resetPasswordApi({ |
||||
|
username: formData.username, |
||||
|
password: formData.password, |
||||
|
mobile: formData.mobile, |
||||
|
code: formData.sms, |
||||
|
uuid: formData.mobile, // 使用手机号作为uuid,用于短信验证码验证 |
||||
|
tenantId: '000000', // 使用默认租户 |
||||
|
}); |
||||
|
|
||||
|
notification.success({ |
||||
|
message: '密码重置成功!', |
||||
|
description: '您的密码已重置成功,请使用新密码登录系统', |
||||
|
duration: 5, |
||||
|
}); |
||||
|
|
||||
|
// 延迟跳转,让用户看到成功提示 |
||||
|
setTimeout(() => { |
||||
|
router.push('/user/login'); |
||||
|
}, 1000); |
||||
|
} catch (error: any) { |
||||
|
const content = error.message || '密码重置失败,请稍后重试'; |
||||
|
createErrorModal({ |
||||
|
title: '密码重置失败', |
||||
|
content, |
||||
|
}); |
||||
|
} finally { |
||||
|
loading.value = false; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// 切换密码显示/隐藏 |
||||
|
const togglePasswordVisibility = () => { |
||||
|
passwordVisible.value = !passwordVisible.value; |
||||
|
}; |
||||
|
|
||||
|
// 切换确认密码显示/隐藏 |
||||
|
const toggleConfirmPasswordVisibility = () => { |
||||
|
confirmPasswordVisible.value = !confirmPasswordVisible.value; |
||||
|
}; |
||||
|
|
||||
|
// 返回登录页面 |
||||
|
const handleBackToLogin = (event?: Event) => { |
||||
|
// 阻止默认的链接行为 |
||||
|
if (event) { |
||||
|
event.preventDefault(); |
||||
|
} |
||||
|
router.push('/user/login'); |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style lang="less" scoped> |
||||
|
* { |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
|
||||
|
.user-forgot-password-page { |
||||
|
min-height: 100vh; |
||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
position: relative; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
// 背景装饰 |
||||
|
.bg-decoration { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
pointer-events: none; |
||||
|
z-index: 1; |
||||
|
|
||||
|
.decoration-circle { |
||||
|
position: absolute; |
||||
|
border-radius: 50%; |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
|
||||
|
&.circle-1 { |
||||
|
width: 300px; |
||||
|
height: 300px; |
||||
|
top: -150px; |
||||
|
right: -150px; |
||||
|
} |
||||
|
|
||||
|
&.circle-2 { |
||||
|
width: 200px; |
||||
|
height: 200px; |
||||
|
bottom: -100px; |
||||
|
left: -100px; |
||||
|
} |
||||
|
|
||||
|
&.circle-3 { |
||||
|
width: 150px; |
||||
|
height: 150px; |
||||
|
top: 50%; |
||||
|
left: 20%; |
||||
|
transform: translateY(-50%); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.forgot-password-container { |
||||
|
display: grid; |
||||
|
grid-template-columns: 1fr 1fr; |
||||
|
max-width: 1200px; |
||||
|
width: 90%; |
||||
|
min-height: 600px; |
||||
|
background: white; |
||||
|
border-radius: 20px; |
||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); |
||||
|
overflow: hidden; |
||||
|
position: relative; |
||||
|
z-index: 2; |
||||
|
} |
||||
|
|
||||
|
// 左侧功能展示 |
||||
|
.left-section { |
||||
|
padding: 3rem; |
||||
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.brand-info { |
||||
|
text-align: center; |
||||
|
margin-bottom: 3rem; |
||||
|
|
||||
|
.logo-section { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
gap: 1rem; |
||||
|
margin-bottom: 1rem; |
||||
|
|
||||
|
.logo { |
||||
|
width: 48px; |
||||
|
height: 48px; |
||||
|
} |
||||
|
|
||||
|
.brand-name { |
||||
|
font-size: 2rem; |
||||
|
font-weight: 700; |
||||
|
color: #2c3e50; |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.brand-desc { |
||||
|
font-size: 1.1rem; |
||||
|
color: #7f8c8d; |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.features-showcase { |
||||
|
display: grid; |
||||
|
grid-template-columns: 1fr 1fr; |
||||
|
gap: 1.5rem; |
||||
|
} |
||||
|
|
||||
|
.feature-card { |
||||
|
padding: 1.5rem; |
||||
|
background: white; |
||||
|
border-radius: 12px; |
||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); |
||||
|
text-align: center; |
||||
|
transition: transform 0.3s, box-shadow 0.3s; |
||||
|
|
||||
|
&:hover { |
||||
|
transform: translateY(-2px); |
||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); |
||||
|
} |
||||
|
|
||||
|
.feature-icon { |
||||
|
font-size: 2rem; |
||||
|
margin-bottom: 0.5rem; |
||||
|
} |
||||
|
|
||||
|
h3 { |
||||
|
font-size: 1rem; |
||||
|
font-weight: 600; |
||||
|
color: #2c3e50; |
||||
|
margin: 0 0 0.5rem 0; |
||||
|
} |
||||
|
|
||||
|
p { |
||||
|
font-size: 0.85rem; |
||||
|
color: #7f8c8d; |
||||
|
line-height: 1.4; |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 右侧忘记密码区域 |
||||
|
.right-section { |
||||
|
padding: 2rem; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
position: relative; |
||||
|
} |
||||
|
|
||||
|
.forgot-password-form-container { |
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
max-width: 400px; |
||||
|
margin: 0 auto; |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.form-header { |
||||
|
text-align: center; |
||||
|
margin-bottom: 2rem; |
||||
|
|
||||
|
h2 { |
||||
|
font-size: 1.8rem; |
||||
|
font-weight: 600; |
||||
|
color: #2c3e50; |
||||
|
margin: 0 0 0.5rem 0; |
||||
|
} |
||||
|
|
||||
|
p { |
||||
|
color: #7f8c8d; |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 忘记密码表单 |
||||
|
.forgot-password-form { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 1.5rem; |
||||
|
} |
||||
|
|
||||
|
.form-group { |
||||
|
position: relative; |
||||
|
|
||||
|
&.sms-group { |
||||
|
display: flex; |
||||
|
gap: 1rem; |
||||
|
|
||||
|
.sms-input { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.sms-btn { |
||||
|
width: 120px; |
||||
|
padding: 12px 16px; |
||||
|
border: 1px solid #d9d9d9; |
||||
|
border-radius: 6px; |
||||
|
background: #fafafa; |
||||
|
color: #666; |
||||
|
font-size: 0.9rem; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s; |
||||
|
white-space: nowrap; |
||||
|
|
||||
|
&:hover:not(:disabled) { |
||||
|
border-color: #667eea; |
||||
|
color: #667eea; |
||||
|
} |
||||
|
|
||||
|
&:disabled { |
||||
|
opacity: 0.6; |
||||
|
cursor: not-allowed; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.form-input { |
||||
|
width: 100%; |
||||
|
padding: 12px 16px; |
||||
|
border: 1px solid #d9d9d9; |
||||
|
border-radius: 6px; |
||||
|
font-size: 1rem; |
||||
|
transition: border-color 0.3s, box-shadow 0.3s; |
||||
|
|
||||
|
&:focus { |
||||
|
outline: none; |
||||
|
border-color: #667eea; |
||||
|
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2); |
||||
|
} |
||||
|
|
||||
|
&::placeholder { |
||||
|
color: #bfbfbf; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.password-input-wrapper { |
||||
|
position: relative; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
|
||||
|
.password-input { |
||||
|
padding-right: 40px; // 为眼睛图标留出空间 |
||||
|
} |
||||
|
|
||||
|
.password-toggle-btn { |
||||
|
position: absolute; |
||||
|
right: 10px; |
||||
|
top: 50%; |
||||
|
transform: translateY(-50%); |
||||
|
background: none; |
||||
|
border: none; |
||||
|
padding: 5px; |
||||
|
cursor: pointer; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
border-radius: 4px; |
||||
|
transition: all 0.3s ease; |
||||
|
|
||||
|
.eye-icon { |
||||
|
width: 18px; |
||||
|
height: 18px; |
||||
|
color: #999; |
||||
|
transition: color 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
&:hover { |
||||
|
background-color: rgba(102, 126, 234, 0.1); |
||||
|
|
||||
|
.eye-icon { |
||||
|
color: #667eea; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&:active { |
||||
|
transform: translateY(-50%) scale(0.95); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.reset-btn { |
||||
|
padding: 12px 24px; |
||||
|
background: #667eea; |
||||
|
color: white; |
||||
|
border: none; |
||||
|
border-radius: 6px; |
||||
|
font-size: 1rem; |
||||
|
font-weight: 600; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
gap: 8px; |
||||
|
|
||||
|
&:hover:not(:disabled) { |
||||
|
background: #5a6fd8; |
||||
|
transform: translateY(-2px); |
||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); |
||||
|
} |
||||
|
|
||||
|
&:disabled { |
||||
|
opacity: 0.6; |
||||
|
cursor: not-allowed; |
||||
|
transform: none; |
||||
|
} |
||||
|
|
||||
|
&.loading { |
||||
|
pointer-events: none; |
||||
|
} |
||||
|
|
||||
|
.loading-spinner { |
||||
|
width: 16px; |
||||
|
height: 16px; |
||||
|
border: 2px solid transparent; |
||||
|
border-top: 2px solid currentColor; |
||||
|
border-radius: 50%; |
||||
|
animation: spin 1s linear infinite; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@keyframes spin { |
||||
|
to { |
||||
|
transform: rotate(360deg); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 其他选项 |
||||
|
.other-options { |
||||
|
margin-top: 2rem; |
||||
|
text-align: center; |
||||
|
|
||||
|
.login-link { |
||||
|
color: #666; |
||||
|
font-size: 0.9rem; |
||||
|
margin: 0; |
||||
|
|
||||
|
a { |
||||
|
color: #667eea; |
||||
|
text-decoration: none; |
||||
|
|
||||
|
&:hover { |
||||
|
text-decoration: underline; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 响应式设计 |
||||
|
@media (max-width: 768px) { |
||||
|
.forgot-password-container { |
||||
|
grid-template-columns: 1fr; |
||||
|
width: 95%; |
||||
|
min-height: auto; |
||||
|
} |
||||
|
|
||||
|
.left-section { |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
.right-section { |
||||
|
padding: 2rem 1.5rem; |
||||
|
} |
||||
|
|
||||
|
.features-showcase { |
||||
|
grid-template-columns: 1fr; |
||||
|
gap: 1rem; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media (max-width: 480px) { |
||||
|
.user-forgot-password-page { |
||||
|
padding: 1rem; |
||||
|
} |
||||
|
|
||||
|
.forgot-password-container { |
||||
|
width: 100%; |
||||
|
border-radius: 12px; |
||||
|
} |
||||
|
|
||||
|
.right-section { |
||||
|
padding: 1.5rem 1rem; |
||||
|
} |
||||
|
|
||||
|
.form-group.sms-group { |
||||
|
flex-direction: column; |
||||
|
gap: 0.5rem; |
||||
|
|
||||
|
.sms-btn { |
||||
|
width: 100%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,860 @@ |
|||||
|
<template> |
||||
|
<div class="user-register-page"> |
||||
|
<!-- 背景装饰 --> |
||||
|
<div class="bg-decoration"> |
||||
|
<div class="decoration-circle circle-1"></div> |
||||
|
<div class="decoration-circle circle-2"></div> |
||||
|
<div class="decoration-circle circle-3"></div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="register-container"> |
||||
|
<!-- 左侧功能展示 --> |
||||
|
<div class="left-section"> |
||||
|
<div class="brand-info"> |
||||
|
<div class="logo-section"> |
||||
|
<img src="/logo.png" alt="小研智审" class="logo" /> |
||||
|
<h1 class="brand-name">小研智审</h1> |
||||
|
</div> |
||||
|
<p class="brand-desc">合同全能助手,AI赋能读改审</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="features-showcase"> |
||||
|
<div class="feature-card"> |
||||
|
<div class="feature-icon">📋</div> |
||||
|
<h3>合同审查</h3> |
||||
|
<p>智能识别合同条款中的不平等内容,确保权利义务分配合理</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="feature-card"> |
||||
|
<div class="feature-icon">⚖️</div> |
||||
|
<h3>法规检查</h3> |
||||
|
<p>依据现行法律法规、行业规范和监管要求进行全面审查</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="feature-card"> |
||||
|
<div class="feature-icon">🔍</div> |
||||
|
<h3>对比审查</h3> |
||||
|
<p>将合同条款与招标文件中的合同要求进行逐一比对</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="feature-card"> |
||||
|
<div class="feature-icon">📊</div> |
||||
|
<h3>结果生成</h3> |
||||
|
<p>生成详细的审查报告和改进建议</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 右侧注册区域 --> |
||||
|
<div class="right-section"> |
||||
|
<div class="register-form-container"> |
||||
|
<!-- 标题 --> |
||||
|
<div class="form-header"> |
||||
|
<h2>用户注册</h2> |
||||
|
<p>创建您的账号,开始使用小研智审</p> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 注册表单 --> |
||||
|
<form @submit.prevent="handleRegister" class="register-form"> |
||||
|
<div class="form-group"> |
||||
|
<input |
||||
|
v-model="formData.username" |
||||
|
type="text" |
||||
|
placeholder="请输入用户名" |
||||
|
class="form-input" |
||||
|
required |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<input |
||||
|
v-model="formData.mobile" |
||||
|
type="tel" |
||||
|
placeholder="请输入手机号" |
||||
|
class="form-input" |
||||
|
required |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group sms-group"> |
||||
|
<input |
||||
|
v-model="formData.sms" |
||||
|
type="text" |
||||
|
placeholder="请输入短信验证码" |
||||
|
class="form-input sms-input" |
||||
|
required |
||||
|
/> |
||||
|
<button |
||||
|
type="button" |
||||
|
class="sms-btn" |
||||
|
@click="handleSendSmsCode" |
||||
|
:disabled="smsLoading || smsCountdown > 0" |
||||
|
> |
||||
|
{{ smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码' }} |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<div class="password-input-wrapper"> |
||||
|
<input |
||||
|
v-model="formData.password" |
||||
|
:type="passwordVisible ? 'text' : 'password'" |
||||
|
placeholder="请输入密码" |
||||
|
class="form-input password-input" |
||||
|
required |
||||
|
/> |
||||
|
<button |
||||
|
type="button" |
||||
|
class="password-toggle-btn" |
||||
|
@click="togglePasswordVisibility" |
||||
|
> |
||||
|
<svg |
||||
|
v-if="!passwordVisible" |
||||
|
class="eye-icon" |
||||
|
viewBox="0 0 24 24" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
stroke-width="2" |
||||
|
> |
||||
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/> |
||||
|
<circle cx="12" cy="12" r="3"/> |
||||
|
</svg> |
||||
|
<svg |
||||
|
v-else |
||||
|
class="eye-icon" |
||||
|
viewBox="0 0 24 24" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
stroke-width="2" |
||||
|
> |
||||
|
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/> |
||||
|
<line x1="1" y1="1" x2="23" y2="23"/> |
||||
|
</svg> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<div class="password-input-wrapper"> |
||||
|
<input |
||||
|
v-model="formData.confirmPassword" |
||||
|
:type="confirmPasswordVisible ? 'text' : 'password'" |
||||
|
placeholder="请确认密码" |
||||
|
class="form-input password-input" |
||||
|
required |
||||
|
/> |
||||
|
<button |
||||
|
type="button" |
||||
|
class="password-toggle-btn" |
||||
|
@click="toggleConfirmPasswordVisibility" |
||||
|
> |
||||
|
<svg |
||||
|
v-if="!confirmPasswordVisible" |
||||
|
class="eye-icon" |
||||
|
viewBox="0 0 24 24" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
stroke-width="2" |
||||
|
> |
||||
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/> |
||||
|
<circle cx="12" cy="12" r="3"/> |
||||
|
</svg> |
||||
|
<svg |
||||
|
v-else |
||||
|
class="eye-icon" |
||||
|
viewBox="0 0 24 24" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
stroke-width="2" |
||||
|
> |
||||
|
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/> |
||||
|
<line x1="1" y1="1" x2="23" y2="23"/> |
||||
|
</svg> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-options"> |
||||
|
<label class="policy-agreement"> |
||||
|
<input v-model="formData.policy" type="checkbox" required /> |
||||
|
<span class="checkmark"></span> |
||||
|
我已阅读并同意 |
||||
|
<a href="#" class="policy-link">《用户协议》</a> |
||||
|
和 |
||||
|
<a href="#" class="policy-link">《隐私政策》</a> |
||||
|
</label> |
||||
|
</div> |
||||
|
|
||||
|
<button |
||||
|
type="submit" |
||||
|
class="register-btn" |
||||
|
:disabled="loading" |
||||
|
:class="{ loading }" |
||||
|
> |
||||
|
<span v-if="loading" class="loading-spinner"></span> |
||||
|
{{ loading ? '注册中...' : '立即注册' }} |
||||
|
</button> |
||||
|
</form> |
||||
|
|
||||
|
<!-- 其他选项 --> |
||||
|
<div class="other-options"> |
||||
|
<p class="login-link"> |
||||
|
已有账号?<a href="#" @click="handleBackToLogin($event)">立即登录</a> |
||||
|
</p> |
||||
|
<p class="forgot-password-link"> |
||||
|
<a href="#" @click="handleForgotPassword($event)">忘记密码?</a> |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts" setup> |
||||
|
import { reactive, ref } from 'vue'; |
||||
|
import { useRouter } from 'vue-router'; |
||||
|
import { useMessage } from '@/hooks/web/useMessage'; |
||||
|
import { registerApi } from '@/api/auth'; |
||||
|
import { sendSmsCode } from '@/api/auth/captcha'; |
||||
|
|
||||
|
const router = useRouter(); |
||||
|
const { notification, createErrorModal } = useMessage(); |
||||
|
|
||||
|
// 表单数据 |
||||
|
const formData = reactive({ |
||||
|
username: '', |
||||
|
mobile: '', |
||||
|
sms: '', |
||||
|
password: '', |
||||
|
confirmPassword: '', |
||||
|
policy: false, |
||||
|
}); |
||||
|
|
||||
|
// 状态管理 |
||||
|
const loading = ref(false); |
||||
|
const smsLoading = ref(false); |
||||
|
const smsCountdown = ref(0); |
||||
|
|
||||
|
// 密码可见性状态 |
||||
|
const passwordVisible = ref(false); |
||||
|
const confirmPasswordVisible = ref(false); |
||||
|
|
||||
|
// 发送短信验证码 |
||||
|
const handleSendSmsCode = async () => { |
||||
|
// 验证手机号是否填写 |
||||
|
if (!formData.mobile) { |
||||
|
notification.error({ |
||||
|
message: '请先填写手机号码', |
||||
|
duration: 3, |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 验证手机号格式 |
||||
|
const mobileRegex = /^1[3-9]\d{9}$/; |
||||
|
if (!mobileRegex.test(formData.mobile)) { |
||||
|
notification.error({ |
||||
|
message: '请输入正确的手机号码', |
||||
|
duration: 3, |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
smsLoading.value = true; |
||||
|
await sendSmsCode(formData.mobile); |
||||
|
notification.success({ |
||||
|
message: '验证码发送成功', |
||||
|
duration: 3, |
||||
|
}); |
||||
|
|
||||
|
// 开始倒计时 |
||||
|
smsCountdown.value = 60; |
||||
|
const timer = setInterval(() => { |
||||
|
smsCountdown.value--; |
||||
|
if (smsCountdown.value <= 0) { |
||||
|
clearInterval(timer); |
||||
|
} |
||||
|
}, 1000); |
||||
|
} catch (error) { |
||||
|
notification.error({ |
||||
|
message: '验证码发送失败,请重试', |
||||
|
duration: 3, |
||||
|
}); |
||||
|
} finally { |
||||
|
smsLoading.value = false; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// 切换密码可见性 |
||||
|
const togglePasswordVisibility = () => { |
||||
|
passwordVisible.value = !passwordVisible.value; |
||||
|
}; |
||||
|
|
||||
|
// 切换确认密码可见性 |
||||
|
const toggleConfirmPasswordVisibility = () => { |
||||
|
confirmPasswordVisible.value = !confirmPasswordVisible.value; |
||||
|
}; |
||||
|
|
||||
|
// 处理注册 |
||||
|
const handleRegister = async () => { |
||||
|
if (loading.value) return; |
||||
|
|
||||
|
// 验证密码确认 |
||||
|
if (formData.password !== formData.confirmPassword) { |
||||
|
notification.error({ |
||||
|
message: '两次输入的密码不一致', |
||||
|
duration: 3, |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 验证密码强度 |
||||
|
if (formData.password.length < 6) { |
||||
|
notification.error({ |
||||
|
message: '密码长度不能少于6位', |
||||
|
duration: 3, |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 检查是否同意隐私政策 |
||||
|
if (!formData.policy) { |
||||
|
notification.error({ |
||||
|
message: '请先同意用户协议和隐私政策', |
||||
|
duration: 3, |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
loading.value = true; |
||||
|
|
||||
|
await registerApi({ |
||||
|
username: formData.username, |
||||
|
password: formData.password, |
||||
|
mobile: formData.mobile, |
||||
|
code: formData.sms, |
||||
|
uuid: formData.mobile, // 使用手机号作为uuid,用于短信验证码验证 |
||||
|
tenantId: '000000', // 使用默认租户 |
||||
|
userType: 'sys_user', |
||||
|
}); |
||||
|
|
||||
|
notification.success({ |
||||
|
message: '注册成功!', |
||||
|
description: '您的账号已创建成功,请使用新账号登录系统', |
||||
|
duration: 5, |
||||
|
}); |
||||
|
|
||||
|
// 延迟跳转,让用户看到成功提示 |
||||
|
setTimeout(() => { |
||||
|
router.push('/user/login'); |
||||
|
}, 1000); |
||||
|
} catch (error: any) { |
||||
|
const content = error.message || '注册失败,请稍后重试'; |
||||
|
createErrorModal({ |
||||
|
title: '注册失败', |
||||
|
content, |
||||
|
}); |
||||
|
} finally { |
||||
|
loading.value = false; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// 返回登录页面 |
||||
|
const handleBackToLogin = (event?: Event) => { |
||||
|
// 阻止默认的链接行为 |
||||
|
if (event) { |
||||
|
event.preventDefault(); |
||||
|
} |
||||
|
router.push('/user/login'); |
||||
|
}; |
||||
|
|
||||
|
// 处理忘记密码 |
||||
|
const handleForgotPassword = (event?: Event) => { |
||||
|
// 阻止默认的链接行为 |
||||
|
if (event) { |
||||
|
event.preventDefault(); |
||||
|
} |
||||
|
router.push('/user/forgot-password'); |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style lang="less" scoped> |
||||
|
* { |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
|
||||
|
.user-register-page { |
||||
|
min-height: 100vh; |
||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
position: relative; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
// 背景装饰 |
||||
|
.bg-decoration { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
pointer-events: none; |
||||
|
z-index: 1; |
||||
|
|
||||
|
.decoration-circle { |
||||
|
position: absolute; |
||||
|
border-radius: 50%; |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
|
||||
|
&.circle-1 { |
||||
|
width: 300px; |
||||
|
height: 300px; |
||||
|
top: -150px; |
||||
|
right: -150px; |
||||
|
} |
||||
|
|
||||
|
&.circle-2 { |
||||
|
width: 200px; |
||||
|
height: 200px; |
||||
|
bottom: -100px; |
||||
|
left: -100px; |
||||
|
} |
||||
|
|
||||
|
&.circle-3 { |
||||
|
width: 150px; |
||||
|
height: 150px; |
||||
|
top: 50%; |
||||
|
left: 20%; |
||||
|
transform: translateY(-50%); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.register-container { |
||||
|
display: grid; |
||||
|
grid-template-columns: 1fr 1fr; |
||||
|
max-width: 1200px; |
||||
|
width: 90%; |
||||
|
min-height: 600px; |
||||
|
background: white; |
||||
|
border-radius: 20px; |
||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); |
||||
|
overflow: hidden; |
||||
|
position: relative; |
||||
|
z-index: 2; |
||||
|
} |
||||
|
|
||||
|
// 左侧功能展示 |
||||
|
.left-section { |
||||
|
padding: 3rem; |
||||
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.brand-info { |
||||
|
text-align: center; |
||||
|
margin-bottom: 3rem; |
||||
|
|
||||
|
.logo-section { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
gap: 1rem; |
||||
|
margin-bottom: 1rem; |
||||
|
|
||||
|
.logo { |
||||
|
width: 48px; |
||||
|
height: 48px; |
||||
|
} |
||||
|
|
||||
|
.brand-name { |
||||
|
font-size: 2rem; |
||||
|
font-weight: 700; |
||||
|
color: #2c3e50; |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.brand-desc { |
||||
|
font-size: 1.1rem; |
||||
|
color: #7f8c8d; |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.features-showcase { |
||||
|
display: grid; |
||||
|
grid-template-columns: 1fr 1fr; |
||||
|
gap: 1.5rem; |
||||
|
} |
||||
|
|
||||
|
.feature-card { |
||||
|
padding: 1.5rem; |
||||
|
background: white; |
||||
|
border-radius: 12px; |
||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); |
||||
|
text-align: center; |
||||
|
transition: transform 0.3s, box-shadow 0.3s; |
||||
|
|
||||
|
&:hover { |
||||
|
transform: translateY(-2px); |
||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); |
||||
|
} |
||||
|
|
||||
|
.feature-icon { |
||||
|
font-size: 2rem; |
||||
|
margin-bottom: 0.5rem; |
||||
|
} |
||||
|
|
||||
|
h3 { |
||||
|
font-size: 1rem; |
||||
|
font-weight: 600; |
||||
|
color: #2c3e50; |
||||
|
margin: 0 0 0.5rem 0; |
||||
|
} |
||||
|
|
||||
|
p { |
||||
|
font-size: 0.85rem; |
||||
|
color: #7f8c8d; |
||||
|
line-height: 1.4; |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 右侧注册区域 |
||||
|
.right-section { |
||||
|
padding: 2rem; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
position: relative; |
||||
|
} |
||||
|
|
||||
|
.register-form-container { |
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
max-width: 400px; |
||||
|
margin: 0 auto; |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.form-header { |
||||
|
text-align: center; |
||||
|
margin-bottom: 2rem; |
||||
|
|
||||
|
h2 { |
||||
|
font-size: 1.8rem; |
||||
|
font-weight: 600; |
||||
|
color: #2c3e50; |
||||
|
margin: 0 0 0.5rem 0; |
||||
|
} |
||||
|
|
||||
|
p { |
||||
|
color: #7f8c8d; |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 注册表单 |
||||
|
.register-form { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 1.5rem; |
||||
|
} |
||||
|
|
||||
|
.form-group { |
||||
|
position: relative; |
||||
|
|
||||
|
&.sms-group { |
||||
|
display: flex; |
||||
|
gap: 1rem; |
||||
|
|
||||
|
.sms-input { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.sms-btn { |
||||
|
width: 120px; |
||||
|
padding: 12px 16px; |
||||
|
border: 1px solid #d9d9d9; |
||||
|
border-radius: 6px; |
||||
|
background: #fafafa; |
||||
|
color: #666; |
||||
|
font-size: 0.9rem; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s; |
||||
|
white-space: nowrap; |
||||
|
|
||||
|
&:hover:not(:disabled) { |
||||
|
border-color: #667eea; |
||||
|
color: #667eea; |
||||
|
} |
||||
|
|
||||
|
&:disabled { |
||||
|
opacity: 0.6; |
||||
|
cursor: not-allowed; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.form-input { |
||||
|
width: 100%; |
||||
|
padding: 12px 16px; |
||||
|
border: 1px solid #d9d9d9; |
||||
|
border-radius: 6px; |
||||
|
font-size: 1rem; |
||||
|
transition: border-color 0.3s, box-shadow 0.3s; |
||||
|
|
||||
|
&:focus { |
||||
|
outline: none; |
||||
|
border-color: #667eea; |
||||
|
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2); |
||||
|
} |
||||
|
|
||||
|
&::placeholder { |
||||
|
color: #bfbfbf; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.password-input-wrapper { |
||||
|
position: relative; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
|
||||
|
.password-input { |
||||
|
padding-right: 40px; // 为眼睛图标留出空间 |
||||
|
} |
||||
|
|
||||
|
.password-toggle-btn { |
||||
|
position: absolute; |
||||
|
right: 10px; |
||||
|
top: 50%; |
||||
|
transform: translateY(-50%); |
||||
|
background: none; |
||||
|
border: none; |
||||
|
cursor: pointer; |
||||
|
padding: 5px; |
||||
|
border-radius: 4px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
transition: all 0.3s ease; |
||||
|
|
||||
|
.eye-icon { |
||||
|
width: 18px; |
||||
|
height: 18px; |
||||
|
color: #999; |
||||
|
transition: color 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
&:hover { |
||||
|
background-color: rgba(102, 126, 234, 0.1); |
||||
|
|
||||
|
.eye-icon { |
||||
|
color: #667eea; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&:active { |
||||
|
transform: translateY(-50%) scale(0.95); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.form-options { |
||||
|
display: flex; |
||||
|
justify-content: flex-start; |
||||
|
align-items: flex-start; |
||||
|
} |
||||
|
|
||||
|
.policy-agreement { |
||||
|
display: flex; |
||||
|
align-items: flex-start; |
||||
|
gap: 8px; |
||||
|
cursor: pointer; |
||||
|
font-size: 0.9rem; |
||||
|
color: #666; |
||||
|
line-height: 1.4; |
||||
|
|
||||
|
input[type="checkbox"] { |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
.checkmark { |
||||
|
width: 16px; |
||||
|
height: 16px; |
||||
|
border: 2px solid #d9d9d9; |
||||
|
border-radius: 3px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
transition: all 0.3s; |
||||
|
flex-shrink: 0; |
||||
|
margin-top: 2px; |
||||
|
|
||||
|
&::after { |
||||
|
content: '✓'; |
||||
|
font-size: 10px; |
||||
|
color: white; |
||||
|
opacity: 0; |
||||
|
transition: opacity 0.3s; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
input[type="checkbox"]:checked + .checkmark { |
||||
|
background: #667eea; |
||||
|
border-color: #667eea; |
||||
|
|
||||
|
&::after { |
||||
|
opacity: 1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.policy-link { |
||||
|
color: #667eea; |
||||
|
text-decoration: none; |
||||
|
|
||||
|
&:hover { |
||||
|
text-decoration: underline; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.register-btn { |
||||
|
padding: 12px 24px; |
||||
|
background: #667eea; |
||||
|
color: white; |
||||
|
border: none; |
||||
|
border-radius: 6px; |
||||
|
font-size: 1rem; |
||||
|
font-weight: 600; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
gap: 8px; |
||||
|
|
||||
|
&:hover:not(:disabled) { |
||||
|
background: #5a6fd8; |
||||
|
transform: translateY(-2px); |
||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); |
||||
|
} |
||||
|
|
||||
|
&:disabled { |
||||
|
opacity: 0.6; |
||||
|
cursor: not-allowed; |
||||
|
transform: none; |
||||
|
} |
||||
|
|
||||
|
&.loading { |
||||
|
pointer-events: none; |
||||
|
} |
||||
|
|
||||
|
.loading-spinner { |
||||
|
width: 16px; |
||||
|
height: 16px; |
||||
|
border: 2px solid transparent; |
||||
|
border-top: 2px solid currentColor; |
||||
|
border-radius: 50%; |
||||
|
animation: spin 1s linear infinite; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@keyframes spin { |
||||
|
to { |
||||
|
transform: rotate(360deg); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 其他选项 |
||||
|
.other-options { |
||||
|
margin-top: 2rem; |
||||
|
text-align: center; |
||||
|
|
||||
|
.login-link { |
||||
|
color: #666; |
||||
|
font-size: 0.9rem; |
||||
|
margin: 0; |
||||
|
|
||||
|
a { |
||||
|
color: #667eea; |
||||
|
text-decoration: none; |
||||
|
|
||||
|
&:hover { |
||||
|
text-decoration: underline; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.forgot-password-link { |
||||
|
margin-top: 0.5rem; |
||||
|
text-align: center; |
||||
|
|
||||
|
a { |
||||
|
color: #667eea; |
||||
|
text-decoration: none; |
||||
|
font-size: 0.9rem; |
||||
|
|
||||
|
&:hover { |
||||
|
text-decoration: underline; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 响应式设计 |
||||
|
@media (max-width: 768px) { |
||||
|
.register-container { |
||||
|
grid-template-columns: 1fr; |
||||
|
width: 95%; |
||||
|
min-height: auto; |
||||
|
} |
||||
|
|
||||
|
.left-section { |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
.right-section { |
||||
|
padding: 2rem 1.5rem; |
||||
|
} |
||||
|
|
||||
|
.features-showcase { |
||||
|
grid-template-columns: 1fr; |
||||
|
gap: 1rem; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media (max-width: 480px) { |
||||
|
.user-register-page { |
||||
|
padding: 1rem; |
||||
|
} |
||||
|
|
||||
|
.register-container { |
||||
|
width: 100%; |
||||
|
border-radius: 12px; |
||||
|
} |
||||
|
|
||||
|
.right-section { |
||||
|
padding: 1.5rem 1rem; |
||||
|
} |
||||
|
|
||||
|
.form-group.sms-group { |
||||
|
flex-direction: column; |
||||
|
gap: 0.5rem; |
||||
|
|
||||
|
.sms-btn { |
||||
|
width: 100%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,116 @@ |
|||||
|
<template> |
||||
|
<div class="checklist-page"> |
||||
|
<div class="page-header"> |
||||
|
<h2 class="page-title">审查清单</h2> |
||||
|
</div> |
||||
|
<div class="checklist-content"> |
||||
|
<div class="empty-state"> |
||||
|
<div class="empty-icon">📋</div> |
||||
|
<p>功能开发中,敬请期待...</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
// 组件逻辑 |
||||
|
</script> |
||||
|
|
||||
|
<style lang="less" scoped> |
||||
|
.checklist-page { |
||||
|
background: white; |
||||
|
border-radius: 16px; |
||||
|
padding: 1.5rem; |
||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.08); |
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
|
||||
|
.page-header { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
margin-bottom: 1rem; |
||||
|
padding-bottom: 0.8rem; |
||||
|
border-bottom: 1px solid #eee; |
||||
|
|
||||
|
.page-title { |
||||
|
font-size: 1.2rem; |
||||
|
font-weight: 600; |
||||
|
color: #333; |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.checklist-content { |
||||
|
flex: 1; |
||||
|
min-height: 0; |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
padding: 2rem 0; |
||||
|
} |
||||
|
|
||||
|
.empty-state { |
||||
|
text-align: center; |
||||
|
|
||||
|
.empty-icon { |
||||
|
font-size: 4rem; |
||||
|
margin-bottom: 1rem; |
||||
|
opacity: 0.6; |
||||
|
} |
||||
|
|
||||
|
p { |
||||
|
font-size: 1.1rem; |
||||
|
color: #999; |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 响应式设计 |
||||
|
@media (max-width: 992px) { |
||||
|
.checklist-page { |
||||
|
padding: 1.5rem; |
||||
|
margin-bottom: 1.5rem; |
||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.08); |
||||
|
} |
||||
|
|
||||
|
.page-header { |
||||
|
margin-bottom: 1.5rem; |
||||
|
padding-bottom: 0.8rem; |
||||
|
} |
||||
|
|
||||
|
.page-title { |
||||
|
font-size: 1.1rem; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media (max-width: 768px) { |
||||
|
.checklist-page { |
||||
|
padding: 1.2rem; |
||||
|
margin-bottom: 1.5rem; |
||||
|
box-shadow: 0 4px 10px rgba(0,0,0,0.05); |
||||
|
} |
||||
|
|
||||
|
.page-header { |
||||
|
margin-bottom: 1.5rem; |
||||
|
padding-bottom: 0.8rem; |
||||
|
} |
||||
|
|
||||
|
.page-title { |
||||
|
font-size: 1rem; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media (max-width: 480px) { |
||||
|
.checklist-page { |
||||
|
padding: 1rem; |
||||
|
margin-bottom: 1.5rem; |
||||
|
} |
||||
|
|
||||
|
.checklist-page { |
||||
|
min-height: calc(100vh - 200px); |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,755 @@ |
|||||
|
<template> |
||||
|
<Modal |
||||
|
v-model:open="visible" |
||||
|
title="开通会员" |
||||
|
:width="900" |
||||
|
:footer="null" |
||||
|
@cancel="handleCancel" |
||||
|
class="membership-modal" |
||||
|
> |
||||
|
<div class="membership-content"> |
||||
|
<!-- 会员权益介绍 --> |
||||
|
<div class="membership-benefits"> |
||||
|
<div class="benefits-header"> |
||||
|
<CrownOutlined class="crown-icon" /> |
||||
|
<h3>会员专享权益</h3> |
||||
|
</div> |
||||
|
<div class="benefits-list"> |
||||
|
<div class="benefit-item"> |
||||
|
<CheckCircleOutlined class="benefit-icon" /> |
||||
|
<span>无限次合同审查</span> |
||||
|
</div> |
||||
|
<div class="benefit-item"> |
||||
|
<CheckCircleOutlined class="benefit-icon" /> |
||||
|
<span>优先处理队列</span> |
||||
|
</div> |
||||
|
|
||||
|
|
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 会员套餐选择 --> |
||||
|
<div class="membership-plans"> |
||||
|
<h4>选择套餐</h4> |
||||
|
<div class="plans-grid"> |
||||
|
<div |
||||
|
v-for="plan in membershipPlans" |
||||
|
:key="plan.id" |
||||
|
:class="['plan-card', { active: selectedPlan?.id === plan.id }]" |
||||
|
@click="selectPlan(plan)" |
||||
|
> |
||||
|
<div class="plan-header"> |
||||
|
<div class="plan-duration">{{ plan.duration }}</div> |
||||
|
<div class="plan-price"> |
||||
|
<span class="currency">¥</span> |
||||
|
<span class="amount">{{ plan.price }}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="plan-features"> |
||||
|
<div class="feature-item"> |
||||
|
<CheckOutlined class="feature-icon" /> |
||||
|
<span>{{ plan.duration }}会员服务</span> |
||||
|
</div> |
||||
|
<div class="feature-item"> |
||||
|
<CheckOutlined class="feature-icon" /> |
||||
|
<span>所有会员权益</span> |
||||
|
</div> |
||||
|
<div v-if="plan.discount" class="feature-item discount"> |
||||
|
<Tag color="red">{{ plan.discount }}</Tag> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="plan-action"> |
||||
|
<Button |
||||
|
:type="selectedPlan?.id === plan.id ? 'primary' : 'default'" |
||||
|
:ghost="selectedPlan?.id !== plan.id" |
||||
|
size="large" |
||||
|
block |
||||
|
> |
||||
|
{{ selectedPlan?.id === plan.id ? '已选择' : '选择套餐' }} |
||||
|
</Button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 支付方式选择 --> |
||||
|
<div class="payment-methods"> |
||||
|
<h4>支付方式</h4> |
||||
|
<Radio.Group v-model:value="selectedPayment" class="payment-options"> |
||||
|
<Radio value="wechat" class="payment-option"> |
||||
|
<div class="payment-item"> |
||||
|
<WechatOutlined class="payment-icon wechat" /> |
||||
|
<span>微信支付</span> |
||||
|
</div> |
||||
|
</Radio> |
||||
|
<Radio value="alipay" class="payment-option"> |
||||
|
<div class="payment-item"> |
||||
|
<AlipayCircleOutlined class="payment-icon alipay" /> |
||||
|
<span>支付宝</span> |
||||
|
</div> |
||||
|
</Radio> |
||||
|
</Radio.Group> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 订单信息 --> |
||||
|
<div v-if="selectedPlan" class="order-summary"> |
||||
|
<Divider /> |
||||
|
<div class="summary-item"> |
||||
|
<span>套餐:</span> |
||||
|
<span>{{ selectedPlan.duration }}</span> |
||||
|
</div> |
||||
|
<div class="summary-item"> |
||||
|
<span>价格:</span> |
||||
|
<span class="price">¥{{ selectedPlan.price }}</span> |
||||
|
</div> |
||||
|
<div v-if="selectedPlan.originalPrice" class="summary-item discount"> |
||||
|
<span>原价:</span> |
||||
|
<span class="original-price">¥{{ selectedPlan.originalPrice }}</span> |
||||
|
</div> |
||||
|
<div v-if="currentOrderNo" class="summary-item"> |
||||
|
<span>订单号:</span> |
||||
|
<span class="order-no">{{ currentOrderNo }}</span> |
||||
|
</div> |
||||
|
<div v-if="paymentStatus" class="summary-item"> |
||||
|
<span>支付状态:</span> |
||||
|
<span :class="['status', getStatusClass(paymentStatus)]">{{ getStatusText(paymentStatus) }}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 操作按钮 --> |
||||
|
<div class="modal-actions"> |
||||
|
<Button @click="handleCancel" size="large"> |
||||
|
取消 |
||||
|
</Button> |
||||
|
<Button |
||||
|
type="primary" |
||||
|
size="large" |
||||
|
:loading="loading" |
||||
|
:disabled="!selectedPlan || !selectedPayment" |
||||
|
@click="handlePurchase" |
||||
|
> |
||||
|
立即购买 ¥{{ selectedPlan?.price || 0 }} |
||||
|
</Button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</Modal> |
||||
|
|
||||
|
<!-- 二维码支付弹窗 --> |
||||
|
<PaymentQRCodeModal |
||||
|
v-model:visible="qrCodeVisible" |
||||
|
:qr-code-url="qrCodeUrl" |
||||
|
:payment-method="selectedPayment" |
||||
|
:order-no="currentOrderNo" |
||||
|
:amount="selectedPlan?.price || 0" |
||||
|
@payment-success="handlePaymentSuccess" |
||||
|
@payment-failed="handlePaymentFailed" |
||||
|
@cancel="handleQrCodeCancel" |
||||
|
/> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { ref, computed } from 'vue'; |
||||
|
import { |
||||
|
Modal, |
||||
|
Button, |
||||
|
Radio, |
||||
|
Tag, |
||||
|
Divider, |
||||
|
message |
||||
|
} from 'ant-design-vue'; |
||||
|
import { |
||||
|
CrownOutlined, |
||||
|
CheckCircleOutlined, |
||||
|
CheckOutlined, |
||||
|
WechatOutlined, |
||||
|
AlipayCircleOutlined, |
||||
|
LoadingOutlined |
||||
|
} from '@ant-design/icons-vue'; |
||||
|
import { createPayment, queryPaymentStatus } from '@/api/contractReview/Payment'; |
||||
|
import { useUserStore } from '@/store/modules/user'; |
||||
|
import PaymentQRCodeModal from './PaymentQRCodeModal.vue'; |
||||
|
|
||||
|
interface MembershipPlan { |
||||
|
id: string; |
||||
|
duration: string; |
||||
|
price: number; |
||||
|
originalPrice?: number; |
||||
|
discount?: string; |
||||
|
} |
||||
|
|
||||
|
const props = defineProps<{ |
||||
|
visible: boolean; |
||||
|
}>(); |
||||
|
|
||||
|
const emit = defineEmits<{ |
||||
|
'update:visible': [value: boolean]; |
||||
|
'purchase-success': [plan: MembershipPlan]; |
||||
|
}>(); |
||||
|
|
||||
|
// 状态管理 |
||||
|
const selectedPlan = ref<MembershipPlan | null>(null); |
||||
|
const selectedPayment = ref<string>(''); |
||||
|
const loading = ref(false); |
||||
|
const currentOrderNo = ref<string>(''); |
||||
|
const paymentStatus = ref<string>(''); |
||||
|
const qrCodeVisible = ref(false); |
||||
|
const qrCodeUrl = ref<string>(''); |
||||
|
|
||||
|
// 用户信息 |
||||
|
const userStore = useUserStore(); |
||||
|
|
||||
|
// 会员套餐数据 |
||||
|
const membershipPlans: MembershipPlan[] = [ |
||||
|
{ |
||||
|
id: '1month', |
||||
|
duration: '1个月', |
||||
|
price: 0.01, |
||||
|
originalPrice: 129, |
||||
|
discount: '限时优惠' |
||||
|
}, |
||||
|
{ |
||||
|
id: '3months', |
||||
|
duration: '季度', |
||||
|
price: 269, |
||||
|
originalPrice: 387, |
||||
|
discount: '省¥118' |
||||
|
}, |
||||
|
{ |
||||
|
id: '6months', |
||||
|
duration: '半年', |
||||
|
price: 499, |
||||
|
originalPrice: 774, |
||||
|
discount: '省¥275' |
||||
|
}, |
||||
|
{ |
||||
|
id: '12months', |
||||
|
duration: '12个月', |
||||
|
price: 899, |
||||
|
originalPrice: 1548, |
||||
|
discount: '省¥649' |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
// 计算属性 |
||||
|
const visible = computed({ |
||||
|
get: () => props.visible, |
||||
|
set: (value) => emit('update:visible', value) |
||||
|
}); |
||||
|
|
||||
|
// 方法 |
||||
|
function selectPlan(plan: MembershipPlan) { |
||||
|
selectedPlan.value = plan; |
||||
|
} |
||||
|
|
||||
|
function handleCancel() { |
||||
|
visible.value = false; |
||||
|
selectedPlan.value = null; |
||||
|
selectedPayment.value = ''; |
||||
|
currentOrderNo.value = ''; |
||||
|
paymentStatus.value = ''; |
||||
|
qrCodeVisible.value = false; |
||||
|
qrCodeUrl.value = ''; |
||||
|
} |
||||
|
|
||||
|
function handleQrCodeCancel() { |
||||
|
qrCodeVisible.value = false; |
||||
|
qrCodeUrl.value = ''; |
||||
|
} |
||||
|
|
||||
|
function handlePaymentSuccess() { |
||||
|
// 支付成功处理 |
||||
|
if (selectedPlan.value) { |
||||
|
emit('purchase-success', selectedPlan.value); |
||||
|
} |
||||
|
qrCodeVisible.value = false; |
||||
|
handleCancel(); |
||||
|
} |
||||
|
|
||||
|
function handlePaymentFailed() { |
||||
|
// 支付失败处理 |
||||
|
qrCodeVisible.value = false; |
||||
|
} |
||||
|
|
||||
|
async function handlePurchase() { |
||||
|
if (!selectedPlan.value || !selectedPayment.value) { |
||||
|
message.warning('请选择套餐和支付方式'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
loading.value = true; |
||||
|
|
||||
|
try { |
||||
|
// 构建支付请求 |
||||
|
const paymentRequest = { |
||||
|
planType: selectedPlan.value.id, |
||||
|
planName: selectedPlan.value.duration, |
||||
|
duration: getDurationInMonths(selectedPlan.value.duration), |
||||
|
amount: selectedPlan.value.price, |
||||
|
paymentMethod: selectedPayment.value, |
||||
|
userId: userStore.getUserInfo?.userId || 1, |
||||
|
clientIp: '', |
||||
|
notifyUrl: '', |
||||
|
returnUrl: '' |
||||
|
}; |
||||
|
|
||||
|
// 调用后端支付API |
||||
|
const result = await createPayment(paymentRequest); |
||||
|
|
||||
|
if (result.success) { |
||||
|
// 保存订单号用于状态查询 |
||||
|
currentOrderNo.value = result.orderNo || ''; |
||||
|
paymentStatus.value = result.payStatus || 'PENDING'; |
||||
|
|
||||
|
// 支付创建成功,显示二维码 |
||||
|
if (result.qrCodeUrl) { |
||||
|
qrCodeUrl.value = result.qrCodeUrl; |
||||
|
qrCodeVisible.value = true; |
||||
|
} |
||||
|
|
||||
|
message.success('支付订单创建成功,请扫码支付'); |
||||
|
} else { |
||||
|
message.error(result.errorMsg || '支付创建失败'); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('支付失败:', error); |
||||
|
message.error('购买失败,请重试'); |
||||
|
} finally { |
||||
|
loading.value = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 获取套餐时长(月数) |
||||
|
function getDurationInMonths(duration: string): number { |
||||
|
switch (duration) { |
||||
|
case '1个月': |
||||
|
return 1; |
||||
|
case '季度': |
||||
|
return 3; |
||||
|
case '半年': |
||||
|
return 6; |
||||
|
case '12个月': |
||||
|
return 12; |
||||
|
default: |
||||
|
return 1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 轮询逻辑已迁移到 PaymentQRCodeModal 组件中 |
||||
|
|
||||
|
// 获取状态文本 |
||||
|
function getStatusText(status: string): string { |
||||
|
const statusMap: Record<string, string> = { |
||||
|
'INIT': '订单生成', |
||||
|
'ING': '支付中', |
||||
|
'SUCCESS': '支付成功', |
||||
|
'FAIL': '支付失败', |
||||
|
'CANCEL': '已撤销', |
||||
|
'REFUND': '已退款', |
||||
|
'CLOSED': '已关闭', |
||||
|
'PENDING': '待支付' // 保留兼容性 |
||||
|
}; |
||||
|
return statusMap[status] || '未知状态'; |
||||
|
} |
||||
|
|
||||
|
// 获取状态样式类 |
||||
|
function getStatusClass(status: string): string { |
||||
|
const statusClassMap: Record<string, string> = { |
||||
|
'INIT': 'status-pending', // 订单生成 - 待支付状态 |
||||
|
'ING': 'status-pending', // 支付中 - 进行中状态 |
||||
|
'SUCCESS': 'status-success', // 支付成功 |
||||
|
'FAIL': 'status-failed', // 支付失败 |
||||
|
'CANCEL': 'status-failed', // 已撤销 - 失败状态 |
||||
|
'REFUND': 'status-closed', // 已退款 - 关闭状态 |
||||
|
'CLOSED': 'status-closed', // 已关闭 |
||||
|
'PENDING': 'status-pending' // 兼容性保留 - 待支付 |
||||
|
}; |
||||
|
return statusClassMap[status] || 'status-unknown'; |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="less" scoped> |
||||
|
.membership-modal { |
||||
|
:deep(.ant-modal-content) { |
||||
|
border-radius: 16px; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
:deep(.ant-modal-header) { |
||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||
|
color: white; |
||||
|
border-bottom: none; |
||||
|
padding: 24px 24px 20px; |
||||
|
|
||||
|
.ant-modal-title { |
||||
|
color: white; |
||||
|
font-size: 18px; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:deep(.ant-modal-close) { |
||||
|
color: white; |
||||
|
|
||||
|
&:hover { |
||||
|
color: rgba(255, 255, 255, 0.8); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:deep(.ant-modal-body) { |
||||
|
padding: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.membership-content { |
||||
|
padding: 24px; |
||||
|
} |
||||
|
|
||||
|
.membership-benefits { |
||||
|
margin-bottom: 32px; |
||||
|
padding: 20px; |
||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); |
||||
|
border-radius: 12px; |
||||
|
|
||||
|
.benefits-header { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
margin-bottom: 16px; |
||||
|
|
||||
|
.crown-icon { |
||||
|
font-size: 24px; |
||||
|
color: #ffd700; |
||||
|
margin-right: 12px; |
||||
|
} |
||||
|
|
||||
|
h3 { |
||||
|
margin: 0; |
||||
|
font-size: 18px; |
||||
|
font-weight: 600; |
||||
|
color: #333; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.benefits-list { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
||||
|
gap: 12px; |
||||
|
|
||||
|
.benefit-item { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
padding: 8px 0; |
||||
|
|
||||
|
.benefit-icon { |
||||
|
color: #52c41a; |
||||
|
margin-right: 8px; |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
|
||||
|
span { |
||||
|
color: #666; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.membership-plans { |
||||
|
margin-bottom: 32px; |
||||
|
|
||||
|
h4 { |
||||
|
margin-bottom: 16px; |
||||
|
font-size: 16px; |
||||
|
font-weight: 600; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.plans-grid { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(4, 1fr); |
||||
|
gap: 16px; |
||||
|
|
||||
|
.plan-card { |
||||
|
border: 2px solid #e8e8e8; |
||||
|
border-radius: 12px; |
||||
|
padding: 12px; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s ease; |
||||
|
background: white; |
||||
|
min-width: 0; |
||||
|
|
||||
|
&:hover { |
||||
|
border-color: #667eea; |
||||
|
transform: translateY(-2px); |
||||
|
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.15); |
||||
|
} |
||||
|
|
||||
|
&.active { |
||||
|
border-color: #1890ff; |
||||
|
background: linear-gradient(135deg, #f0f8ff 0%, #e6f7ff 100%); |
||||
|
color: #1890ff; |
||||
|
transform: translateY(-2px); |
||||
|
box-shadow: 0 8px 24px rgba(24, 144, 255, 0.15); |
||||
|
|
||||
|
.plan-price { |
||||
|
color: #1890ff; |
||||
|
|
||||
|
.currency { |
||||
|
color: #1890ff; |
||||
|
} |
||||
|
|
||||
|
.amount { |
||||
|
color: #1890ff; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.feature-item { |
||||
|
color: #333; |
||||
|
|
||||
|
.feature-icon { |
||||
|
color: #52c41a; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.plan-header { |
||||
|
text-align: center; |
||||
|
margin-bottom: 16px; |
||||
|
|
||||
|
.plan-duration { |
||||
|
font-size: 14px; |
||||
|
font-weight: 600; |
||||
|
margin-bottom: 6px; |
||||
|
} |
||||
|
|
||||
|
.plan-price { |
||||
|
.currency { |
||||
|
font-size: 12px; |
||||
|
color: #666; |
||||
|
} |
||||
|
|
||||
|
.amount { |
||||
|
font-size: 20px; |
||||
|
font-weight: 700; |
||||
|
color: #667eea; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.plan-features { |
||||
|
margin-bottom: 16px; |
||||
|
|
||||
|
.feature-item { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
margin-bottom: 6px; |
||||
|
font-size: 11px; |
||||
|
color: #666; |
||||
|
|
||||
|
.feature-icon { |
||||
|
margin-right: 4px; |
||||
|
color: #52c41a; |
||||
|
font-size: 10px; |
||||
|
} |
||||
|
|
||||
|
&.discount { |
||||
|
justify-content: center; |
||||
|
margin-top: 6px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.plan-action { |
||||
|
.ant-btn { |
||||
|
border-radius: 8px; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.payment-methods { |
||||
|
margin-bottom: 24px; |
||||
|
|
||||
|
h4 { |
||||
|
margin-bottom: 16px; |
||||
|
font-size: 16px; |
||||
|
font-weight: 600; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.payment-options { |
||||
|
display: flex; |
||||
|
gap: 16px; |
||||
|
|
||||
|
.payment-option { |
||||
|
flex: 1; |
||||
|
|
||||
|
.payment-item { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
padding: 12px 16px; |
||||
|
border: 2px solid #e8e8e8; |
||||
|
border-radius: 8px; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s ease; |
||||
|
|
||||
|
&:hover { |
||||
|
border-color: #667eea; |
||||
|
} |
||||
|
|
||||
|
.payment-icon { |
||||
|
font-size: 20px; |
||||
|
margin-right: 8px; |
||||
|
|
||||
|
&.wechat { |
||||
|
color: #07c160; |
||||
|
} |
||||
|
|
||||
|
&.alipay { |
||||
|
color: #1677ff; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
span { |
||||
|
font-size: 14px; |
||||
|
color: #333; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:deep(.ant-radio-wrapper) { |
||||
|
width: 100%; |
||||
|
margin-right: 0; |
||||
|
|
||||
|
.ant-radio { |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
&.ant-radio-wrapper-checked .payment-item { |
||||
|
border-color: #667eea; |
||||
|
background: #f0f2ff; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.order-summary { |
||||
|
margin-bottom: 24px; |
||||
|
|
||||
|
.summary-item { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
padding: 8px 0; |
||||
|
font-size: 14px; |
||||
|
|
||||
|
.price { |
||||
|
font-weight: 600; |
||||
|
color: #667eea; |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
|
||||
|
.order-no { |
||||
|
font-family: monospace; |
||||
|
color: #666; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
|
||||
|
.status { |
||||
|
font-weight: 500; |
||||
|
padding: 2px 8px; |
||||
|
border-radius: 4px; |
||||
|
font-size: 12px; |
||||
|
|
||||
|
&.status-pending { |
||||
|
background: #fff7e6; |
||||
|
color: #fa8c16; |
||||
|
} |
||||
|
|
||||
|
&.status-success { |
||||
|
background: #f6ffed; |
||||
|
color: #52c41a; |
||||
|
} |
||||
|
|
||||
|
&.status-failed { |
||||
|
background: #fff2f0; |
||||
|
color: #ff4d4f; |
||||
|
} |
||||
|
|
||||
|
&.status-closed { |
||||
|
background: #f5f5f5; |
||||
|
color: #999; |
||||
|
} |
||||
|
|
||||
|
&.status-unknown { |
||||
|
background: #f0f0f0; |
||||
|
color: #666; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.discount { |
||||
|
.original-price { |
||||
|
text-decoration: line-through; |
||||
|
color: #999; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.modal-actions { |
||||
|
display: flex; |
||||
|
gap: 12px; |
||||
|
justify-content: flex-end; |
||||
|
|
||||
|
.ant-btn { |
||||
|
min-width: 100px; |
||||
|
border-radius: 8px; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 响应式设计 |
||||
|
@media (max-width: 1000px) { |
||||
|
.plans-grid { |
||||
|
grid-template-columns: repeat(2, 1fr) !important; |
||||
|
gap: 12px !important; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media (max-width: 768px) { |
||||
|
.membership-content { |
||||
|
padding: 16px; |
||||
|
} |
||||
|
|
||||
|
.plans-grid { |
||||
|
grid-template-columns: repeat(2, 1fr) !important; |
||||
|
gap: 12px !important; |
||||
|
} |
||||
|
|
||||
|
.payment-options { |
||||
|
flex-direction: column; |
||||
|
gap: 12px; |
||||
|
} |
||||
|
|
||||
|
.modal-actions { |
||||
|
flex-direction: column; |
||||
|
|
||||
|
.ant-btn { |
||||
|
width: 100%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media (max-width: 480px) { |
||||
|
.plans-grid { |
||||
|
grid-template-columns: 1fr !important; |
||||
|
} |
||||
|
|
||||
|
.benefits-list { |
||||
|
grid-template-columns: 1fr !important; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 二维码弹窗样式已迁移到 PaymentQRCodeModal.vue 组件中 |
||||
|
</style> |
@ -0,0 +1,370 @@ |
|||||
|
<template> |
||||
|
<Modal |
||||
|
v-model:open="visible" |
||||
|
title="等待支付" |
||||
|
:width="400" |
||||
|
:footer="null" |
||||
|
:closable="true" |
||||
|
@cancel="handleCancel" |
||||
|
class="qr-code-modal" |
||||
|
> |
||||
|
<div class="qr-code-content"> |
||||
|
<div class="qr-code-wrapper"> |
||||
|
<img v-if="qrCodeUrl" :src="qrCodeUrl" alt="支付二维码" class="qr-code-image" /> |
||||
|
<div v-else class="qr-code-placeholder"> |
||||
|
<LoadingOutlined class="loading-icon" /> |
||||
|
<span>正在生成二维码...</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="payment-instruction"> |
||||
|
<WechatOutlined v-if="paymentMethod === 'wechat'" class="payment-icon wechat" /> |
||||
|
<AlipayCircleOutlined v-else class="payment-icon alipay" /> |
||||
|
<span>请使用{{ paymentMethod === 'wechat' ? '微信' : '支付宝' }}扫码支付</span> |
||||
|
</div> |
||||
|
<div class="order-info"> |
||||
|
<div class="order-item"> |
||||
|
<span>订单号:</span> |
||||
|
<span class="order-no">{{ orderNo }}</span> |
||||
|
</div> |
||||
|
<div class="order-item"> |
||||
|
<span>金额:</span> |
||||
|
<span class="amount">¥{{ amount }}</span> |
||||
|
</div> |
||||
|
<div v-if="paymentStatus" class="order-item"> |
||||
|
<span>支付状态:</span> |
||||
|
<span :class="['status', getStatusClass(paymentStatus)]">{{ getStatusText(paymentStatus) }}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</Modal> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { ref, computed, watch, onUnmounted } from 'vue'; |
||||
|
import { Modal, message } from 'ant-design-vue'; |
||||
|
import { WechatOutlined, AlipayCircleOutlined, LoadingOutlined } from '@ant-design/icons-vue'; |
||||
|
import { queryPaymentStatus } from '@/api/contractReview/Payment'; |
||||
|
|
||||
|
interface Props { |
||||
|
visible: boolean; |
||||
|
qrCodeUrl: string; |
||||
|
paymentMethod: string; |
||||
|
orderNo: string; |
||||
|
amount: number; |
||||
|
} |
||||
|
|
||||
|
interface Emits { |
||||
|
(e: 'update:visible', value: boolean): void; |
||||
|
(e: 'payment-success'): void; |
||||
|
(e: 'payment-failed'): void; |
||||
|
(e: 'cancel'): void; |
||||
|
} |
||||
|
|
||||
|
const props = defineProps<Props>(); |
||||
|
const emit = defineEmits<Emits>(); |
||||
|
|
||||
|
// 状态管理 |
||||
|
const paymentStatus = ref<string>(''); |
||||
|
|
||||
|
// 计算属性 |
||||
|
const visible = computed({ |
||||
|
get: () => props.visible, |
||||
|
set: (value) => emit('update:visible', value) |
||||
|
}); |
||||
|
|
||||
|
// 轮询相关 |
||||
|
let pollingTimer: NodeJS.Timeout | null = null; |
||||
|
|
||||
|
// 监听弹窗显示状态,控制轮询 |
||||
|
watch( |
||||
|
() => props.visible, |
||||
|
(newVisible) => { |
||||
|
if (newVisible && props.orderNo) { |
||||
|
// 弹窗打开时开始轮询 |
||||
|
startPaymentStatusPolling(); |
||||
|
} else { |
||||
|
// 弹窗关闭时停止轮询 |
||||
|
stopPaymentStatusPolling(); |
||||
|
} |
||||
|
}, |
||||
|
{ immediate: true } |
||||
|
); |
||||
|
|
||||
|
// 监听订单号变化,重新开始轮询 |
||||
|
watch( |
||||
|
() => props.orderNo, |
||||
|
(newOrderNo) => { |
||||
|
if (newOrderNo && props.visible) { |
||||
|
// 有新订单号且弹窗显示时,重新开始轮询 |
||||
|
startPaymentStatusPolling(); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
// 方法 |
||||
|
function handleCancel() { |
||||
|
stopPaymentStatusPolling(); |
||||
|
paymentStatus.value = ''; |
||||
|
emit('cancel'); |
||||
|
} |
||||
|
|
||||
|
// 开始支付状态轮询 |
||||
|
function startPaymentStatusPolling() { |
||||
|
if (!props.orderNo) return; |
||||
|
|
||||
|
// 清除之前的轮询 |
||||
|
stopPaymentStatusPolling(); |
||||
|
|
||||
|
console.log('开始轮询支付状态,订单号:', props.orderNo); |
||||
|
|
||||
|
// 立即执行一次查询 |
||||
|
checkPaymentStatus(); |
||||
|
|
||||
|
// 开始轮询,每3秒查询一次 |
||||
|
pollingTimer = setInterval(() => { |
||||
|
checkPaymentStatus(); |
||||
|
}, 3000); |
||||
|
} |
||||
|
|
||||
|
// 停止支付状态轮询 |
||||
|
function stopPaymentStatusPolling() { |
||||
|
if (pollingTimer) { |
||||
|
console.log('停止轮询支付状态'); |
||||
|
clearInterval(pollingTimer); |
||||
|
pollingTimer = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 检查支付状态 |
||||
|
async function checkPaymentStatus() { |
||||
|
if (!props.orderNo) return; |
||||
|
|
||||
|
try { |
||||
|
console.log('查询支付状态:', props.orderNo); |
||||
|
const result = await queryPaymentStatus(props.orderNo); |
||||
|
|
||||
|
if (result.success) { |
||||
|
paymentStatus.value = result.payStatus; |
||||
|
console.log('支付状态:', result.payStatus); |
||||
|
|
||||
|
if (result.payStatus === 'SUCCESS') { |
||||
|
// 支付成功 |
||||
|
message.success('支付成功!会员权益已生效'); |
||||
|
emit('payment-success'); |
||||
|
stopPaymentStatusPolling(); |
||||
|
} else if (['CLOSED', 'FAIL', 'CANCEL'].includes(result.payStatus)) { |
||||
|
// 支付失败、关闭或撤销 |
||||
|
message.error('支付失败或已关闭'); |
||||
|
emit('payment-failed'); |
||||
|
stopPaymentStatusPolling(); |
||||
|
} |
||||
|
} else { |
||||
|
console.error('查询支付状态失败:', result.errorMsg); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('查询支付状态异常:', error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 获取状态文本 |
||||
|
function getStatusText(status: string): string { |
||||
|
const statusMap: Record<string, string> = { |
||||
|
'INIT': '订单生成', |
||||
|
'ING': '支付中', |
||||
|
'SUCCESS': '支付成功', |
||||
|
'FAIL': '支付失败', |
||||
|
'CANCEL': '已撤销', |
||||
|
'REFUND': '已退款', |
||||
|
'CLOSED': '已关闭', |
||||
|
'PENDING': '待支付' // 保留兼容性 |
||||
|
}; |
||||
|
return statusMap[status] || '未知状态'; |
||||
|
} |
||||
|
|
||||
|
// 获取状态样式类 |
||||
|
function getStatusClass(status: string): string { |
||||
|
const statusClassMap: Record<string, string> = { |
||||
|
'INIT': 'status-pending', // 订单生成 - 待支付状态 |
||||
|
'ING': 'status-pending', // 支付中 - 进行中状态 |
||||
|
'SUCCESS': 'status-success', // 支付成功 |
||||
|
'FAIL': 'status-failed', // 支付失败 |
||||
|
'CANCEL': 'status-failed', // 已撤销 - 失败状态 |
||||
|
'REFUND': 'status-closed', // 已退款 - 关闭状态 |
||||
|
'CLOSED': 'status-closed', // 已关闭 |
||||
|
'PENDING': 'status-pending' // 兼容性保留 - 待支付 |
||||
|
}; |
||||
|
return statusClassMap[status] || 'status-unknown'; |
||||
|
} |
||||
|
|
||||
|
// 组件卸载时清理轮询 |
||||
|
onUnmounted(() => { |
||||
|
stopPaymentStatusPolling(); |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<style lang="less" scoped> |
||||
|
// 二维码弹窗样式 |
||||
|
.qr-code-modal { |
||||
|
:deep(.ant-modal-content) { |
||||
|
border-radius: 16px; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
:deep(.ant-modal-header) { |
||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||
|
color: white; |
||||
|
border-bottom: none; |
||||
|
padding: 20px 24px 16px; |
||||
|
|
||||
|
.ant-modal-title { |
||||
|
color: white; |
||||
|
font-size: 16px; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:deep(.ant-modal-close) { |
||||
|
color: white; |
||||
|
|
||||
|
&:hover { |
||||
|
color: rgba(255, 255, 255, 0.8); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:deep(.ant-modal-body) { |
||||
|
padding: 24px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.qr-code-content { |
||||
|
text-align: center; |
||||
|
|
||||
|
.qr-code-wrapper { |
||||
|
margin-bottom: 20px; |
||||
|
|
||||
|
.qr-code-image { |
||||
|
width: 200px; |
||||
|
height: 200px; |
||||
|
border: 1px solid #e8e8e8; |
||||
|
border-radius: 8px; |
||||
|
padding: 8px; |
||||
|
background: white; |
||||
|
} |
||||
|
|
||||
|
.qr-code-placeholder { |
||||
|
width: 200px; |
||||
|
height: 200px; |
||||
|
margin: 0 auto; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
border: 1px solid #e8e8e8; |
||||
|
border-radius: 8px; |
||||
|
background: #f8f9fa; |
||||
|
color: #666; |
||||
|
|
||||
|
.loading-icon { |
||||
|
font-size: 24px; |
||||
|
color: #667eea; |
||||
|
margin-bottom: 8px; |
||||
|
} |
||||
|
|
||||
|
span { |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.payment-instruction { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
margin-bottom: 16px; |
||||
|
padding: 12px; |
||||
|
background: #f8f9fa; |
||||
|
border-radius: 8px; |
||||
|
|
||||
|
.payment-icon { |
||||
|
font-size: 20px; |
||||
|
margin-right: 8px; |
||||
|
|
||||
|
&.wechat { |
||||
|
color: #07c160; |
||||
|
} |
||||
|
|
||||
|
&.alipay { |
||||
|
color: #1677ff; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
span { |
||||
|
font-size: 14px; |
||||
|
color: #333; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.order-info { |
||||
|
text-align: left; |
||||
|
background: #f8f9fa; |
||||
|
padding: 12px; |
||||
|
border-radius: 8px; |
||||
|
|
||||
|
.order-item { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
margin-bottom: 8px; |
||||
|
font-size: 14px; |
||||
|
|
||||
|
&:last-child { |
||||
|
margin-bottom: 0; |
||||
|
} |
||||
|
|
||||
|
.order-no { |
||||
|
font-family: monospace; |
||||
|
color: #666; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
|
||||
|
.amount { |
||||
|
font-weight: 600; |
||||
|
color: #667eea; |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
|
||||
|
.status { |
||||
|
font-weight: 500; |
||||
|
padding: 2px 8px; |
||||
|
border-radius: 4px; |
||||
|
font-size: 12px; |
||||
|
|
||||
|
&.status-pending { |
||||
|
background: #fff7e6; |
||||
|
color: #fa8c16; |
||||
|
} |
||||
|
|
||||
|
&.status-success { |
||||
|
background: #f6ffed; |
||||
|
color: #52c41a; |
||||
|
} |
||||
|
|
||||
|
&.status-failed { |
||||
|
background: #fff2f0; |
||||
|
color: #ff4d4f; |
||||
|
} |
||||
|
|
||||
|
&.status-closed { |
||||
|
background: #f5f5f5; |
||||
|
color: #999; |
||||
|
} |
||||
|
|
||||
|
&.status-unknown { |
||||
|
background: #f0f0f0; |
||||
|
color: #666; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,286 @@ |
|||||
|
<template> |
||||
|
<div class="records-page"> |
||||
|
<div class="page-header"> |
||||
|
<h5 class="page-title">审查记录</h5> |
||||
|
</div> |
||||
|
<div class="records-content"> |
||||
|
<BasicTable @register="registerTable" @expand="tableExpand"> |
||||
|
<template #bodyCell="{ column, record }"> |
||||
|
<template v-if="column.key === 'action'"> |
||||
|
<TableAction |
||||
|
stopButtonPropagation |
||||
|
:actions="[ |
||||
|
{ |
||||
|
label: record.childrenTasks.length > 1 ? '下载全部' : '下载', |
||||
|
icon: IconEnum.DOWNLOAD, |
||||
|
type: 'primary', |
||||
|
color: 'success', |
||||
|
ghost: true, |
||||
|
ifShow: () => { |
||||
|
return record.progress.includes('100%'); |
||||
|
}, |
||||
|
onClick: handleDownload.bind(null, record), |
||||
|
}, |
||||
|
]" |
||||
|
/> |
||||
|
</template> |
||||
|
</template> |
||||
|
<template #expandedRowRender> |
||||
|
<BasicTable @register="registerChildTable"> |
||||
|
<template #bodyCell="{ column, record }"> |
||||
|
<template v-if="column.key === 'action'"> |
||||
|
<TableAction |
||||
|
stopButtonPropagation |
||||
|
:actions="[ |
||||
|
{ |
||||
|
label: '详情', |
||||
|
icon: IconEnum.EDIT, |
||||
|
type: 'primary', |
||||
|
ghost: true, |
||||
|
ifShow: () => { |
||||
|
return record.progressStatus != 'PENDING' && |
||||
|
record.progressStatus != 'STARTED' && |
||||
|
record.progressStatus != 'REVOKED'; |
||||
|
}, |
||||
|
onClick: handleDetail.bind(null, record), |
||||
|
}, |
||||
|
{ |
||||
|
label: '下载', |
||||
|
icon: IconEnum.DOWNLOAD, |
||||
|
type: 'primary', |
||||
|
color: 'success', |
||||
|
ghost: true, |
||||
|
ifShow: () => { |
||||
|
return record.progressStatus != 'PENDING' && |
||||
|
record.progressStatus != 'STARTED' && |
||||
|
record.progressStatus != 'REVOKED'; |
||||
|
}, |
||||
|
onClick: handleDownload.bind(null, record), |
||||
|
}, |
||||
|
{ |
||||
|
label: '终止任务', |
||||
|
icon: IconEnum.DELETE, |
||||
|
type: 'primary', |
||||
|
danger: true, |
||||
|
ghost: true, |
||||
|
ifShow: () => { |
||||
|
return record.progressStatus == 'PENDING'; |
||||
|
}, |
||||
|
popConfirm: { |
||||
|
placement: 'left', |
||||
|
title: '是否终止当前任务?', |
||||
|
confirm: handleStop.bind(null, record), |
||||
|
}, |
||||
|
}, |
||||
|
]" |
||||
|
/> |
||||
|
</template> |
||||
|
</template> |
||||
|
</BasicTable> |
||||
|
</template> |
||||
|
</BasicTable> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { ref } from 'vue'; |
||||
|
import { BasicTable, useTable, TableAction } from '@/components/Table'; |
||||
|
import { IconEnum } from '@/enums/appEnum'; |
||||
|
import { DocumentTasksStop } from '@/api/documentReview/DocumentTasks'; |
||||
|
import { ContractualTasksList } from '@/api/contractReview/ContractualTasks'; |
||||
|
import { getDetailResultsByTaskId, ContractualTaskResultDownload } from '@/api/contractReview/ContractualTaskResults'; |
||||
|
import { ContractualTaskResultDetailVO } from '@/api/contractReview/ContractualTaskResults/model'; |
||||
|
import { formSchemas, columns, childColumns } from '@/views/contractReview/ContractualTasks/ContractualTasks.data'; |
||||
|
|
||||
|
// 定义事件 |
||||
|
const emit = defineEmits<{ |
||||
|
detail: [record: Recordable]; |
||||
|
resultDetailVisible: [visible: boolean]; |
||||
|
resultDetail: [detail: ContractualTaskResultDetailVO[]]; |
||||
|
taskInfo: [info: Recordable]; |
||||
|
}>(); |
||||
|
|
||||
|
// 状态管理 |
||||
|
const childTableData = ref([]); |
||||
|
|
||||
|
// 表格配置 |
||||
|
const [registerTable, { reload, multipleRemove, selected, getForm }] = useTable({ |
||||
|
rowSelection: { |
||||
|
type: 'checkbox', |
||||
|
}, |
||||
|
title: '合同任务列表', |
||||
|
api: ContractualTasksList, |
||||
|
showIndexColumn: false, |
||||
|
clickToRowSelect: false, |
||||
|
rowKey: 'id', |
||||
|
expandRowByClick: false, |
||||
|
useSearchForm: true, |
||||
|
formConfig: { |
||||
|
schemas: formSchemas, |
||||
|
baseColProps: { |
||||
|
xs: 24, |
||||
|
sm: 24, |
||||
|
md: 24, |
||||
|
lg: 6, |
||||
|
}, |
||||
|
}, |
||||
|
columns: columns, |
||||
|
actionColumn: { |
||||
|
width: 200, |
||||
|
title: '操作', |
||||
|
key: 'action', |
||||
|
fixed: 'right', |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const [registerChildTable, { setProps: setChildProps }] = useTable({ |
||||
|
size: 'small', |
||||
|
api: getchildTableData, |
||||
|
columns: childColumns, |
||||
|
rowKey: 'id', |
||||
|
useSearchForm: false, |
||||
|
showIndexColumn: false, |
||||
|
showTableSetting: false, |
||||
|
pagination: false, |
||||
|
maxHeight: 50, |
||||
|
actionColumn: { |
||||
|
width: 200, |
||||
|
title: '操作', |
||||
|
key: 'action', |
||||
|
fixed: 'right', |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
// 方法 |
||||
|
async function handleDetail(record: Recordable) { |
||||
|
try { |
||||
|
const detailRes = await getDetailResultsByTaskId(record.id); |
||||
|
if (detailRes && detailRes.length > 0) { |
||||
|
emit('resultDetail', detailRes); |
||||
|
emit('taskInfo', record); |
||||
|
emit('resultDetailVisible', true); |
||||
|
return; |
||||
|
} |
||||
|
} catch (detailEx) { |
||||
|
console.error('获取详细结果失败', detailEx); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function tableExpand(expanded, record) { |
||||
|
if (expanded) { |
||||
|
childTableData.value = record.childrenTasks; |
||||
|
console.log('expanded, record', expanded, record); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function getchildTableData() { |
||||
|
console.log('childTableData', childTableData.value); |
||||
|
const height = 50 * childTableData.value.length; |
||||
|
setChildProps({ maxHeight: height }); |
||||
|
return Promise.resolve(childTableData.value); |
||||
|
} |
||||
|
|
||||
|
async function handleStop(record: Recordable) { |
||||
|
await DocumentTasksStop(record.id); |
||||
|
await reload(); |
||||
|
} |
||||
|
|
||||
|
async function handleDownload(record: Recordable) { |
||||
|
if (record.childrenTasks?.length > 1) { |
||||
|
await ContractualTaskResultDownload(record.childrenTasks.map((item) => item.id)); |
||||
|
await reload(); |
||||
|
} else if (record.childrenTasks?.length == 1) { |
||||
|
await ContractualTaskResultDownload([record.childrenTasks[0].id]); |
||||
|
await reload(); |
||||
|
} else { |
||||
|
await ContractualTaskResultDownload([record.id]); |
||||
|
await reload(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 暴露方法给父组件 |
||||
|
defineExpose({ |
||||
|
reload, |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<style lang="less" scoped> |
||||
|
.records-page { |
||||
|
background: white; |
||||
|
border-radius: 16px; |
||||
|
padding: 1.5rem; |
||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.08); |
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
|
||||
|
.page-header { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
margin-bottom: 1rem; |
||||
|
padding-bottom: 0.8rem; |
||||
|
border-bottom: 1px solid #eee; |
||||
|
|
||||
|
.page-title { |
||||
|
font-size: 1.2rem; |
||||
|
font-weight: 600; |
||||
|
color: #333; |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.records-content { |
||||
|
flex: 1; |
||||
|
min-height: 0; |
||||
|
overflow-y: auto; |
||||
|
} |
||||
|
|
||||
|
// 响应式设计 |
||||
|
@media (max-width: 992px) { |
||||
|
.records-page { |
||||
|
padding: 1.5rem; |
||||
|
margin-bottom: 1.5rem; |
||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.08); |
||||
|
} |
||||
|
|
||||
|
.page-header { |
||||
|
margin-bottom: 1.5rem; |
||||
|
padding-bottom: 0.8rem; |
||||
|
} |
||||
|
|
||||
|
.page-title { |
||||
|
font-size: 1.1rem; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media (max-width: 768px) { |
||||
|
.records-page { |
||||
|
padding: 1.2rem; |
||||
|
margin-bottom: 1.5rem; |
||||
|
box-shadow: 0 4px 10px rgba(0,0,0,0.05); |
||||
|
} |
||||
|
|
||||
|
.page-header { |
||||
|
margin-bottom: 1.5rem; |
||||
|
padding-bottom: 0.8rem; |
||||
|
} |
||||
|
|
||||
|
.page-title { |
||||
|
font-size: 1rem; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media (max-width: 480px) { |
||||
|
.records-page { |
||||
|
padding: 1rem; |
||||
|
margin-bottom: 1.5rem; |
||||
|
} |
||||
|
|
||||
|
.records-page { |
||||
|
min-height: calc(100vh - 200px); |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,297 @@ |
|||||
|
<template> |
||||
|
<div class="review-page"> |
||||
|
<!-- 文件上传区域 --> |
||||
|
<div class="upload-section"> |
||||
|
<!-- 审查选项 --> |
||||
|
<div class="review-options"> |
||||
|
<div class="section-title">选择审查任务类型(可多选)</div> |
||||
|
<div class="option-buttons"> |
||||
|
<div |
||||
|
:class="['option-button', selectedOptions.includes('substantive') ? 'selected' : '']" |
||||
|
@click="toggleOption('substantive')" |
||||
|
> |
||||
|
<span class="option-icon">📄</span> |
||||
|
<div class="option-content"> |
||||
|
<div class="option-name">实质性审查</div> |
||||
|
<div class="option-desc">对合同条款的合理性、完整性进行深度分析</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div |
||||
|
:class="['option-button', 'disabled']" |
||||
|
> |
||||
|
<span class="option-icon">⚖️</span> |
||||
|
<div class="option-content"> |
||||
|
<div class="option-name">合规性审查</div> |
||||
|
<div class="option-desc">检查合同是否符合相关法律法规要求</div> |
||||
|
<div class="coming-soon">即将推出</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div |
||||
|
:class="['option-button', 'disabled']" |
||||
|
> |
||||
|
<span class="option-icon">🔄</span> |
||||
|
<div class="option-content"> |
||||
|
<div class="option-name">一致性审查</div> |
||||
|
<div class="option-desc">检查合同与投标文件或招标文件是否一致</div> |
||||
|
<div class="coming-soon">即将推出</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 文件上传组件 --> |
||||
|
<div class="file-upload-area"> |
||||
|
<InferenceReview :reviewTypes="selectedOptions" @success="handleReviewSuccess" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { ref } from 'vue'; |
||||
|
import { message } from 'ant-design-vue'; |
||||
|
import InferenceReview from '@/views/contractReview/ContractualTasks/components/InferenceReview.vue'; |
||||
|
|
||||
|
// 定义事件 |
||||
|
const emit = defineEmits<{ |
||||
|
reviewSuccess: [data: any]; |
||||
|
}>(); |
||||
|
|
||||
|
// 状态管理 |
||||
|
const selectedOptions = ref<string[]>(['substantive']); |
||||
|
|
||||
|
// 方法 |
||||
|
function toggleOption(option: string) { |
||||
|
// 只允许实质性审查 |
||||
|
if (option !== 'substantive') { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const index = selectedOptions.value.indexOf(option); |
||||
|
if (index > -1) { |
||||
|
selectedOptions.value.splice(index, 1); |
||||
|
} else { |
||||
|
selectedOptions.value.push(option); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function handleReviewSuccess(data: any) { |
||||
|
console.log('审查任务创建成功:', data); |
||||
|
message.success('合同审查任务已创建成功,正在后台处理'); |
||||
|
emit('reviewSuccess', data); |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="less" scoped> |
||||
|
.review-page { |
||||
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); |
||||
|
border-radius: 0; |
||||
|
padding: 1rem; |
||||
|
box-shadow: none; |
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.upload-section { |
||||
|
flex: 1; |
||||
|
min-height: 0; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
padding: 1rem; |
||||
|
justify-content: flex-start; |
||||
|
} |
||||
|
|
||||
|
// 区域标题 |
||||
|
.section-title { |
||||
|
text-align: center; |
||||
|
font-size: 1.3rem; |
||||
|
font-weight: 600; |
||||
|
color: #333; |
||||
|
margin-bottom: 1.5rem; |
||||
|
} |
||||
|
|
||||
|
// 审核选项 |
||||
|
.review-options { |
||||
|
margin-bottom: 1.5rem; |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
.option-buttons { |
||||
|
display: flex; |
||||
|
gap: 1.5rem; |
||||
|
justify-content: center; |
||||
|
flex-wrap: wrap; |
||||
|
} |
||||
|
|
||||
|
.option-button { |
||||
|
width: 280px; |
||||
|
min-height: 110px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
padding: 1.5rem; |
||||
|
background-color: #fff; |
||||
|
border: 2px solid #e8e8e8; |
||||
|
border-radius: 12px; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s ease; |
||||
|
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.05); |
||||
|
|
||||
|
&:hover { |
||||
|
border-color: #667eea; |
||||
|
box-shadow: 0 6px 18px rgba(102, 126, 234, 0.15); |
||||
|
transform: translateY(-2px); |
||||
|
} |
||||
|
|
||||
|
&.selected { |
||||
|
border-color: #667eea; |
||||
|
background: linear-gradient(135deg, #f6f9ff 0%, #e8f4ff 100%); |
||||
|
box-shadow: 0 6px 18px rgba(102, 126, 234, 0.25); |
||||
|
transform: translateY(-1px); |
||||
|
} |
||||
|
|
||||
|
&.disabled { |
||||
|
opacity: 0.5; |
||||
|
cursor: not-allowed; |
||||
|
background-color: #f5f5f5; |
||||
|
border-color: #d9d9d9; |
||||
|
|
||||
|
&:hover { |
||||
|
border-color: #d9d9d9; |
||||
|
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.05); |
||||
|
transform: none; |
||||
|
} |
||||
|
|
||||
|
.option-icon { |
||||
|
opacity: 0.6; |
||||
|
} |
||||
|
|
||||
|
.option-name { |
||||
|
color: #999; |
||||
|
} |
||||
|
|
||||
|
.option-desc { |
||||
|
color: #999; |
||||
|
} |
||||
|
|
||||
|
.coming-soon { |
||||
|
font-size: 0.75rem; |
||||
|
color: #ff6b6b; |
||||
|
font-weight: 500; |
||||
|
margin-top: 0.3rem; |
||||
|
background: rgba(255, 107, 107, 0.1); |
||||
|
padding: 0.2rem 0.5rem; |
||||
|
border-radius: 4px; |
||||
|
display: inline-block; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.option-icon { |
||||
|
font-size: 1.8rem; |
||||
|
margin-right: 1rem; |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
.option-content { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.option-name { |
||||
|
font-size: 1rem; |
||||
|
font-weight: 600; |
||||
|
color: #333; |
||||
|
margin-bottom: 0.4rem; |
||||
|
} |
||||
|
|
||||
|
.option-desc { |
||||
|
font-size: 0.85rem; |
||||
|
color: #666; |
||||
|
line-height: 1.4; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 文件上传区域 |
||||
|
.file-upload-area { |
||||
|
flex: 1; |
||||
|
min-height: 0; |
||||
|
width: 100%; |
||||
|
overflow: hidden; |
||||
|
background: white; |
||||
|
border-radius: 16px; |
||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.08); |
||||
|
margin-top: 3rem; |
||||
|
} |
||||
|
|
||||
|
// 响应式设计 |
||||
|
@media (max-width: 992px) { |
||||
|
.option-buttons { |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
gap: 1rem; |
||||
|
} |
||||
|
|
||||
|
.option-button { |
||||
|
width: 100%; |
||||
|
max-width: 350px; |
||||
|
min-height: 100px; |
||||
|
padding: 1.2rem; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media (max-width: 768px) { |
||||
|
.option-buttons { |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
gap: 0.8rem; |
||||
|
} |
||||
|
|
||||
|
.option-button { |
||||
|
width: 100%; |
||||
|
max-width: 300px; |
||||
|
padding: 0.8rem 1.5rem; |
||||
|
min-height: 90px; |
||||
|
|
||||
|
.option-icon { |
||||
|
font-size: 1.5rem; |
||||
|
margin-right: 0.8rem; |
||||
|
} |
||||
|
|
||||
|
.option-name { |
||||
|
font-size: 0.95rem; |
||||
|
} |
||||
|
|
||||
|
.option-desc { |
||||
|
font-size: 0.8rem; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media (max-width: 480px) { |
||||
|
.option-buttons { |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
gap: 0.8rem; |
||||
|
} |
||||
|
|
||||
|
.option-button { |
||||
|
width: 100%; |
||||
|
max-width: 300px; |
||||
|
padding: 1rem; |
||||
|
min-height: 90px; |
||||
|
|
||||
|
.option-icon { |
||||
|
font-size: 1.5rem; |
||||
|
margin-right: 0.8rem; |
||||
|
} |
||||
|
|
||||
|
.option-name { |
||||
|
font-size: 0.95rem; |
||||
|
} |
||||
|
|
||||
|
.option-desc { |
||||
|
font-size: 0.8rem; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,725 @@ |
|||||
|
<template> |
||||
|
<div class="user-home-layout"> |
||||
|
<!-- 左侧导航栏 --> |
||||
|
<div class="sidebar"> |
||||
|
<!-- 品牌区域 --> |
||||
|
<div class="brand-section"> |
||||
|
<div class="brand-logo"> |
||||
|
<img src="/logo.png" alt="小研智审" class="logo-img" /> |
||||
|
<span class="brand-text">小研智审</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 导航菜单 --> |
||||
|
<div class="nav-menu"> |
||||
|
<div class="menu-section"> |
||||
|
<div |
||||
|
:class="['menu-item', { active: activeTab === 'review' }]" |
||||
|
@click="setActiveTab('review')" |
||||
|
> |
||||
|
<span class="menu-icon">🏠</span> |
||||
|
<span class="menu-text">开始</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="menu-section"> |
||||
|
<div class="menu-title">合同审查</div> |
||||
|
<div |
||||
|
:class="['menu-item', 'sub-menu', { active: activeTab === 'records' }]" |
||||
|
@click="setActiveTab('records')" |
||||
|
> |
||||
|
<span class="menu-icon">📊</span> |
||||
|
<span class="menu-text">审查记录</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="menu-section"> |
||||
|
<div class="menu-title">合同配置</div> |
||||
|
<div |
||||
|
:class="['menu-item', 'sub-menu', { active: activeTab === 'checklist' }]" |
||||
|
@click="setActiveTab('checklist')" |
||||
|
> |
||||
|
<span class="menu-icon">✅</span> |
||||
|
<span class="menu-text">审查清单</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 底部用户区域 --> |
||||
|
<div class="user-section"> |
||||
|
<Dropdown :trigger="['click']" placement="topRight"> |
||||
|
<div class="user-info-card"> |
||||
|
<div class="user-avatar-section"> |
||||
|
<Avatar size="default" class="user-avatar-card"> |
||||
|
<template #icon> |
||||
|
<UserOutlined /> |
||||
|
</template> |
||||
|
</Avatar> |
||||
|
</div> |
||||
|
<div class="user-details"> |
||||
|
<div class="user-name-card"> |
||||
|
{{ userInfo.nickName || '超级管理员' }} |
||||
|
<Tag v-if="isVipMember" color="gold" class="vip-tag"> |
||||
|
<CrownOutlined /> |
||||
|
VIP |
||||
|
</Tag> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="user-menu-trigger"> |
||||
|
<DownOutlined class="dropdown-icon-card" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<template #overlay> |
||||
|
<Menu @click="handleMenuClick"> |
||||
|
<MenuItem key="profile"> |
||||
|
<UserOutlined /> |
||||
|
个人信息 |
||||
|
</MenuItem> |
||||
|
<MenuItem key="membership"> |
||||
|
<CrownOutlined /> |
||||
|
开通会员 |
||||
|
</MenuItem> |
||||
|
<MenuItem key="settings"> |
||||
|
<SettingOutlined /> |
||||
|
系统设置 |
||||
|
</MenuItem> |
||||
|
<MenuDivider /> |
||||
|
<MenuItem key="logout" class="logout-item"> |
||||
|
<LogoutOutlined /> |
||||
|
退出登录 |
||||
|
</MenuItem> |
||||
|
</Menu> |
||||
|
</template> |
||||
|
</Dropdown> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 右侧主内容区 --> |
||||
|
<div class="main-area"> |
||||
|
<!-- 主内容区 --> |
||||
|
<div class="content-area"> |
||||
|
<!-- 开始审核页面 --> |
||||
|
<ReviewPage |
||||
|
v-if="activeTab === 'review'" |
||||
|
@review-success="handleReviewSuccess" |
||||
|
/> |
||||
|
|
||||
|
<!-- 审查记录页面 --> |
||||
|
<RecordsPage |
||||
|
v-if="activeTab === 'records'" |
||||
|
ref="recordsPageRef" |
||||
|
@detail="handleDetail" |
||||
|
@result-detail-visible="resultDetailDrawerVisible = $event" |
||||
|
@result-detail="taskResultDetail = $event" |
||||
|
@task-info="currentTaskInfo = $event" |
||||
|
/> |
||||
|
|
||||
|
<!-- 审查清单页面 --> |
||||
|
<ChecklistPage v-if="activeTab === 'checklist'" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 弹窗组件 --> |
||||
|
<ContractualTasksModal @register="registerModal" @reload="() => recordsPageRef.value?.reload()" /> |
||||
|
<DocsDrawer @register="registerDrawer" /> |
||||
|
<ContractualResultDetailDrawer |
||||
|
:visible="resultDetailDrawerVisible" |
||||
|
:taskResultDetail="taskResultDetail" |
||||
|
:taskInfo="currentTaskInfo" |
||||
|
@update:visible="resultDetailDrawerVisible = $event" |
||||
|
@close="handleResultDetailDrawerClose" |
||||
|
/> |
||||
|
<MembershipModal |
||||
|
:visible="membershipModalVisible" |
||||
|
@update:visible="membershipModalVisible = $event" |
||||
|
@purchase-success="handleMembershipPurchaseSuccess" |
||||
|
/> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { ref, watch, computed } from 'vue'; |
||||
|
import { useRouter } from 'vue-router'; |
||||
|
import { message, Modal, Dropdown, Menu, MenuItem, MenuDivider, Avatar, Tag } from 'ant-design-vue'; |
||||
|
import { |
||||
|
UserOutlined, |
||||
|
DownOutlined, |
||||
|
SettingOutlined, |
||||
|
LogoutOutlined, |
||||
|
CrownOutlined |
||||
|
} from '@ant-design/icons-vue'; |
||||
|
import { BasicTable, useTable, TableAction } from '@/components/Table'; |
||||
|
import { useModal } from '@/components/Modal'; |
||||
|
import { useDrawer } from '@/components/Drawer'; |
||||
|
import { IconEnum } from '@/enums/appEnum'; |
||||
|
import { useUserStore } from '@/store/modules/user'; |
||||
|
|
||||
|
// 导入页面组件 |
||||
|
import ReviewPage from './components/ReviewPage.vue'; |
||||
|
import RecordsPage from './components/RecordsPage.vue'; |
||||
|
import ChecklistPage from './components/ChecklistPage.vue'; |
||||
|
|
||||
|
// 导入其他组件 |
||||
|
import ContractualTasksModal from '@/views/contractReview/ContractualTasks/ContractualTasksModal.vue'; |
||||
|
import DocsDrawer from '@/views/documentReview/DocumentTasks/DocsDrawer.vue'; |
||||
|
import ContractualResultDetailDrawer from '@/views/contractReview/ContractualTasks/ContractualResultDetailDrawer.vue'; |
||||
|
import MembershipModal from './components/MembershipModal.vue'; |
||||
|
|
||||
|
// 导入类型 |
||||
|
import { ContractualTaskResultDetailVO } from '@/api/contractReview/ContractualTaskResults/model'; |
||||
|
|
||||
|
defineOptions({ name: 'UserHome' }); |
||||
|
|
||||
|
const router = useRouter(); |
||||
|
const userStore = useUserStore(); |
||||
|
|
||||
|
// 状态管理 |
||||
|
const activeTab = ref<'review' | 'records' | 'checklist'>('review'); |
||||
|
const resultDetailDrawerVisible = ref(false); |
||||
|
const taskResultDetail = ref<ContractualTaskResultDetailVO[]>([]); |
||||
|
const currentTaskInfo = ref<Recordable>({}); |
||||
|
const membershipModalVisible = ref(false); |
||||
|
const isVipMember = ref(false); // 会员状态 |
||||
|
|
||||
|
// 用户信息 |
||||
|
const userInfo = computed(() => userStore.getUserInfo || {}); |
||||
|
|
||||
|
// 组件引用 |
||||
|
const recordsPageRef = ref(); |
||||
|
|
||||
|
// 钩子 |
||||
|
const [registerDrawer, { openDrawer }] = useDrawer(); |
||||
|
const [registerModal, { openModal }] = useModal(); |
||||
|
|
||||
|
// 方法 |
||||
|
function setActiveTab(tab: 'review' | 'records' | 'checklist') { |
||||
|
activeTab.value = tab; |
||||
|
if (tab === 'records' && recordsPageRef.value) { |
||||
|
recordsPageRef.value.reload(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 处理用户菜单点击 |
||||
|
function handleMenuClick({ key }: any) { |
||||
|
switch (key) { |
||||
|
case 'profile': |
||||
|
// 个人信息功能 |
||||
|
message.info('个人信息功能开发中'); |
||||
|
break; |
||||
|
case 'membership': |
||||
|
// 开通会员 |
||||
|
membershipModalVisible.value = true; |
||||
|
break; |
||||
|
case 'settings': |
||||
|
// 系统设置功能 |
||||
|
message.info('系统设置功能开发中'); |
||||
|
break; |
||||
|
case 'logout': |
||||
|
// 退出登录 |
||||
|
Modal.confirm({ |
||||
|
title: '确认退出', |
||||
|
content: '您确定要退出登录吗?', |
||||
|
okText: '确定', |
||||
|
cancelText: '取消', |
||||
|
onOk() { |
||||
|
handleLogout(); |
||||
|
}, |
||||
|
}); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 退出登录 |
||||
|
async function handleLogout() { |
||||
|
try { |
||||
|
await userStore.logout(); |
||||
|
message.success('退出登录成功'); |
||||
|
router.push('/user/login'); |
||||
|
} catch (error) { |
||||
|
console.error('退出登录失败:', error); |
||||
|
message.error('退出登录失败'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function handleReviewSuccess(data: any) { |
||||
|
console.log('审查任务创建成功:', data); |
||||
|
message.success('合同审查任务已创建成功,正在后台处理'); |
||||
|
|
||||
|
// 切换到审查记录页签并刷新 |
||||
|
setTimeout(() => { |
||||
|
setActiveTab('records'); |
||||
|
}, 1000); |
||||
|
} |
||||
|
|
||||
|
function handleDetail(record: Recordable) { |
||||
|
// 这个方法现在由RecordsPage组件处理 |
||||
|
console.log('handleDetail called with:', record); |
||||
|
} |
||||
|
|
||||
|
function handleResultDetailDrawerClose() { |
||||
|
resultDetailDrawerVisible.value = false; |
||||
|
} |
||||
|
|
||||
|
function handleMembershipPurchaseSuccess(plan: any) { |
||||
|
console.log('会员购买成功:', plan); |
||||
|
message.success(`恭喜!${plan.duration}会员开通成功`); |
||||
|
// 更新会员状态 |
||||
|
isVipMember.value = true; |
||||
|
// 这里可以调用API更新用户会员状态 |
||||
|
} |
||||
|
|
||||
|
// 监听路由变化,当有refresh参数时刷新表格 |
||||
|
watch( |
||||
|
() => router.currentRoute.value.query.refresh, |
||||
|
(newVal) => { |
||||
|
if (newVal && activeTab.value === 'records' && recordsPageRef.value) { |
||||
|
recordsPageRef.value.reload(); |
||||
|
router.replace({ |
||||
|
path: router.currentRoute.value.path, |
||||
|
query: { ...router.currentRoute.value.query, refresh: undefined } |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
</script> |
||||
|
|
||||
|
<style lang="less" scoped> |
||||
|
.user-home-layout { |
||||
|
display: flex; |
||||
|
min-height: 100vh; |
||||
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); |
||||
|
} |
||||
|
|
||||
|
// 左侧导航栏 |
||||
|
.sidebar { |
||||
|
width: 280px; |
||||
|
background: #fff; |
||||
|
padding: 2rem 1.5rem; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: space-between; |
||||
|
box-shadow: 4px 0 20px rgba(0, 0, 0, 0.08); |
||||
|
position: fixed; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
height: 100%; |
||||
|
z-index: 100; |
||||
|
} |
||||
|
|
||||
|
.brand-section { |
||||
|
text-align: center; |
||||
|
margin-bottom: 2.5rem; |
||||
|
|
||||
|
.brand-logo { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
gap: 0.8rem; |
||||
|
margin-bottom: 1rem; |
||||
|
|
||||
|
.logo-img { |
||||
|
width: 50px; |
||||
|
height: 50px; |
||||
|
border-radius: 10px; |
||||
|
box-shadow: 0 6px 20px rgba(0,0,0,0.15); |
||||
|
} |
||||
|
|
||||
|
.brand-text { |
||||
|
font-size: 1.8rem; |
||||
|
font-weight: 700; |
||||
|
color: #333; |
||||
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.3); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.nav-menu { |
||||
|
flex: 1; |
||||
|
overflow-y: auto; |
||||
|
padding-right: 10px; |
||||
|
|
||||
|
.menu-section { |
||||
|
margin-bottom: 1.5rem; |
||||
|
|
||||
|
&:last-child { |
||||
|
margin-bottom: 0; |
||||
|
} |
||||
|
|
||||
|
.menu-title { |
||||
|
font-size: 0.9rem; |
||||
|
color: #888; |
||||
|
margin-bottom: 0.5rem; |
||||
|
padding-left: 0.5rem; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.menu-item { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
padding: 0.8rem 1rem; |
||||
|
border-radius: 8px; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s ease; |
||||
|
color: #666; |
||||
|
font-weight: 500; |
||||
|
|
||||
|
&:hover { |
||||
|
background: #f8f9fa; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
&.active { |
||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||
|
color: white; |
||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); |
||||
|
transform: translateY(-2px); |
||||
|
} |
||||
|
|
||||
|
.menu-icon { |
||||
|
font-size: 1.2rem; |
||||
|
margin-right: 0.8rem; |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
.menu-text { |
||||
|
font-size: 0.95rem; |
||||
|
} |
||||
|
|
||||
|
&.sub-menu { |
||||
|
padding-left: 1.5rem; |
||||
|
margin-top: 0.5rem; |
||||
|
margin-bottom: 0.5rem; |
||||
|
border-left: 2px solid #e8e8e8; |
||||
|
|
||||
|
&:hover { |
||||
|
background: #f8f9fa; |
||||
|
border-left-color: #667eea; |
||||
|
} |
||||
|
|
||||
|
&.active { |
||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||
|
color: white; |
||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); |
||||
|
transform: translateY(-2px); |
||||
|
border-left-color: #667eea; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.user-section { |
||||
|
margin-top: 2rem; |
||||
|
|
||||
|
.user-info-card { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
padding: 1rem; |
||||
|
background: #f8f9fa; |
||||
|
border-radius: 12px; |
||||
|
border: 1px solid #e9ecef; |
||||
|
transition: all 0.3s ease; |
||||
|
cursor: pointer; |
||||
|
|
||||
|
&:hover { |
||||
|
background: #e9ecef; |
||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
||||
|
transform: translateY(-1px); |
||||
|
} |
||||
|
|
||||
|
.user-avatar-section { |
||||
|
margin-right: 0.8rem; |
||||
|
|
||||
|
.user-avatar-card { |
||||
|
background: #667eea; |
||||
|
color: white; |
||||
|
border: none; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.user-details { |
||||
|
flex: 1; |
||||
|
|
||||
|
.user-name-card { |
||||
|
font-size: 0.9rem; |
||||
|
font-weight: 600; |
||||
|
color: #333; |
||||
|
margin-bottom: 0.2rem; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 0.5rem; |
||||
|
|
||||
|
.vip-tag { |
||||
|
font-size: 0.7rem; |
||||
|
padding: 0.1rem 0.3rem; |
||||
|
border-radius: 4px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 0.2rem; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.user-role { |
||||
|
font-size: 0.75rem; |
||||
|
color: #666; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.user-menu-trigger { |
||||
|
padding: 0.3rem; |
||||
|
border-radius: 6px; |
||||
|
transition: all 0.3s ease; |
||||
|
|
||||
|
.dropdown-icon-card { |
||||
|
color: #666; |
||||
|
font-size: 0.8rem; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 右侧主内容区 |
||||
|
.main-area { |
||||
|
flex: 1; |
||||
|
margin-left: 280px; /* Match sidebar width */ |
||||
|
padding: 0; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
height: 100vh; |
||||
|
} |
||||
|
|
||||
|
.content-area { |
||||
|
flex: 1; |
||||
|
min-height: 0; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
padding: 1rem; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
// 用户菜单下拉项 |
||||
|
:deep(.ant-dropdown-menu) { |
||||
|
border-radius: 8px; |
||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); |
||||
|
padding: 0.5rem 0; |
||||
|
|
||||
|
.ant-dropdown-menu-item { |
||||
|
padding: 0.6rem 1rem; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 0.5rem; |
||||
|
|
||||
|
&:hover { |
||||
|
background: #f8f9fa; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.logout-item { |
||||
|
color: #ff4d4f; |
||||
|
border-top: 1px solid #f0f0f0; |
||||
|
margin-top: 0.25rem; |
||||
|
|
||||
|
&:hover { |
||||
|
background: #fff2f0; |
||||
|
color: #ff4d4f; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 响应式设计 |
||||
|
@media (max-width: 1200px) { |
||||
|
.sidebar { |
||||
|
width: 220px; |
||||
|
padding: 1.5rem 1rem; |
||||
|
} |
||||
|
|
||||
|
.brand-section .brand-text { |
||||
|
font-size: 1.5rem; |
||||
|
} |
||||
|
|
||||
|
.nav-menu .menu-item .menu-text { |
||||
|
font-size: 0.85rem; |
||||
|
} |
||||
|
|
||||
|
.main-area { |
||||
|
margin-left: 220px; |
||||
|
padding: 0; |
||||
|
} |
||||
|
|
||||
|
.content-area { |
||||
|
padding: 1rem; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
} |
||||
|
|
||||
|
@media (max-width: 992px) { |
||||
|
.user-home-layout { |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
|
||||
|
.sidebar { |
||||
|
width: 100%; |
||||
|
height: auto; |
||||
|
flex-direction: row; |
||||
|
justify-content: space-around; |
||||
|
padding: 1rem 0.5rem; |
||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); |
||||
|
} |
||||
|
|
||||
|
.brand-section { |
||||
|
flex: 1; |
||||
|
text-align: left; |
||||
|
margin-bottom: 0; |
||||
|
padding-left: 1rem; |
||||
|
} |
||||
|
|
||||
|
.nav-menu { |
||||
|
flex: 2; |
||||
|
padding: 0 1rem; |
||||
|
overflow-x: auto; |
||||
|
-webkit-overflow-scrolling: touch; |
||||
|
} |
||||
|
|
||||
|
.user-section { |
||||
|
margin-top: 0; |
||||
|
padding: 1rem 0.5rem; |
||||
|
border-top: 1px solid #eee; |
||||
|
} |
||||
|
|
||||
|
.main-area { |
||||
|
margin-left: 0; |
||||
|
padding: 0; |
||||
|
} |
||||
|
|
||||
|
.content-area { |
||||
|
padding: 1rem; |
||||
|
} |
||||
|
|
||||
|
.content-area { |
||||
|
flex-direction: row; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
} |
||||
|
|
||||
|
@media (max-width: 768px) { |
||||
|
.sidebar { |
||||
|
flex-direction: column; |
||||
|
padding: 0.8rem 0.5rem; |
||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05); |
||||
|
} |
||||
|
|
||||
|
.brand-section { |
||||
|
padding-left: 0; |
||||
|
margin-bottom: 1rem; |
||||
|
} |
||||
|
|
||||
|
.nav-menu { |
||||
|
padding: 0 0.5rem; |
||||
|
overflow-x: hidden; |
||||
|
} |
||||
|
|
||||
|
.nav-menu .menu-section { |
||||
|
margin-bottom: 1rem; |
||||
|
} |
||||
|
|
||||
|
.nav-menu .menu-title { |
||||
|
font-size: 0.8rem; |
||||
|
padding-left: 0; |
||||
|
} |
||||
|
|
||||
|
.nav-menu .menu-item .menu-text { |
||||
|
font-size: 0.8rem; |
||||
|
} |
||||
|
|
||||
|
.user-section { |
||||
|
padding: 0.8rem 0.5rem; |
||||
|
border-top: none; |
||||
|
} |
||||
|
|
||||
|
.user-info-card { |
||||
|
padding: 0.8rem; |
||||
|
|
||||
|
.user-name-card { |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
.user-role { |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
.dropdown-icon-card { |
||||
|
font-size: 0.7rem; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.main-area { |
||||
|
padding: 0; |
||||
|
} |
||||
|
|
||||
|
.content-area { |
||||
|
padding: 1rem; |
||||
|
} |
||||
|
|
||||
|
.content-area { |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
} |
||||
|
|
||||
|
@media (max-width: 480px) { |
||||
|
.sidebar { |
||||
|
padding: 0.6rem 0.4rem; |
||||
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.03); |
||||
|
} |
||||
|
|
||||
|
.brand-section .brand-text { |
||||
|
font-size: 1.3rem; |
||||
|
} |
||||
|
|
||||
|
.nav-menu .menu-item .menu-text { |
||||
|
font-size: 0.75rem; |
||||
|
} |
||||
|
|
||||
|
.user-section { |
||||
|
padding: 0.6rem 0.4rem; |
||||
|
} |
||||
|
|
||||
|
.user-info-card { |
||||
|
padding: 0.6rem; |
||||
|
border-radius: 16px; |
||||
|
|
||||
|
.user-name-card { |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
.user-role { |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
.dropdown-icon-card { |
||||
|
font-size: 0.6rem; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.main-area { |
||||
|
padding: 0; |
||||
|
} |
||||
|
|
||||
|
.content-area { |
||||
|
padding: 0.8rem; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
} |
||||
|
</style> |
@ -0,0 +1,728 @@ |
|||||
|
<template> |
||||
|
<div class="user-login-page"> |
||||
|
<!-- 背景装饰 --> |
||||
|
<div class="bg-decoration"> |
||||
|
<div class="decoration-circle circle-1"></div> |
||||
|
<div class="decoration-circle circle-2"></div> |
||||
|
<div class="decoration-circle circle-3"></div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="login-container"> |
||||
|
<!-- 左侧功能展示 --> |
||||
|
<div class="left-section"> |
||||
|
<div class="brand-info"> |
||||
|
<div class="logo-section"> |
||||
|
<img src="/logo.png" alt="小研智审" class="logo" /> |
||||
|
<h1 class="brand-name">小研智审</h1> |
||||
|
</div> |
||||
|
<p class="brand-desc">合同全能助手,AI赋能读改审</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="features-showcase"> |
||||
|
<div class="feature-card"> |
||||
|
<div class="feature-icon">📋</div> |
||||
|
<h3>合同审查</h3> |
||||
|
<p>智能识别合同条款中的不平等内容,确保权利义务分配合理</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="feature-card"> |
||||
|
<div class="feature-icon">⚖️</div> |
||||
|
<h3>法规检查</h3> |
||||
|
<p>依据现行法律法规、行业规范和监管要求进行全面审查</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="feature-card"> |
||||
|
<div class="feature-icon">🔍</div> |
||||
|
<h3>对比审查</h3> |
||||
|
<p>将合同条款与招标文件中的合同要求进行逐一比对</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="feature-card"> |
||||
|
<div class="feature-icon">📊</div> |
||||
|
<h3>结果生成</h3> |
||||
|
<p>生成详细的审查报告和改进建议</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 右侧登录区域 --> |
||||
|
<div class="right-section"> |
||||
|
<div class="login-form-container"> |
||||
|
<!-- 标题 --> |
||||
|
<div class="form-header"> |
||||
|
<h2>欢迎使用</h2> |
||||
|
<p>请输入您的账号信息</p> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 账号密码登录表单 --> |
||||
|
<form @submit.prevent="handleLogin" class="login-form"> |
||||
|
<div class="form-group"> |
||||
|
<input |
||||
|
v-model="formData.username" |
||||
|
type="text" |
||||
|
placeholder="请输入用户名/手机号" |
||||
|
class="form-input" |
||||
|
required |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<div class="password-input-wrapper"> |
||||
|
<input |
||||
|
v-model="formData.password" |
||||
|
:type="passwordVisible ? 'text' : 'password'" |
||||
|
placeholder="请输入密码" |
||||
|
class="form-input password-input" |
||||
|
required |
||||
|
/> |
||||
|
<button |
||||
|
type="button" |
||||
|
class="password-toggle-btn" |
||||
|
@click="togglePasswordVisibility" |
||||
|
> |
||||
|
<svg |
||||
|
v-if="!passwordVisible" |
||||
|
class="eye-icon" |
||||
|
viewBox="0 0 24 24" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
stroke-width="2" |
||||
|
> |
||||
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/> |
||||
|
<circle cx="12" cy="12" r="3"/> |
||||
|
</svg> |
||||
|
<svg |
||||
|
v-else |
||||
|
class="eye-icon" |
||||
|
viewBox="0 0 24 24" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
stroke-width="2" |
||||
|
> |
||||
|
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/> |
||||
|
<line x1="1" y1="1" x2="23" y2="23"/> |
||||
|
</svg> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group captcha-group" v-if="captcha.required"> |
||||
|
<input |
||||
|
v-model="formData.code" |
||||
|
type="text" |
||||
|
placeholder="请输入验证码" |
||||
|
class="form-input captcha-input" |
||||
|
required |
||||
|
/> |
||||
|
<div class="captcha-image" @click="refreshCaptcha"> |
||||
|
<img v-if="captcha.image" :src="captcha.image" alt="验证码" /> |
||||
|
<span v-else class="captcha-loading">加载中...</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-options"> |
||||
|
<label class="remember-me"> |
||||
|
<input v-model="rememberMe" type="checkbox" /> |
||||
|
<span class="checkmark"></span> |
||||
|
记住密码 |
||||
|
</label> |
||||
|
<a href="#" class="forgot-password" @click="handleForgotPassword($event)">忘记密码?</a> |
||||
|
</div> |
||||
|
|
||||
|
<button |
||||
|
type="submit" |
||||
|
class="login-btn" |
||||
|
:disabled="loading" |
||||
|
:class="{ loading }" |
||||
|
> |
||||
|
<span v-if="loading" class="loading-spinner"></span> |
||||
|
{{ loading ? '登录中...' : '登录' }} |
||||
|
</button> |
||||
|
</form> |
||||
|
|
||||
|
<!-- 其他登录选项 --> |
||||
|
<div class="other-options"> |
||||
|
<p class="register-link"> |
||||
|
还没有账号?<a href="#" @click="handleRegister($event)">立即注册</a> |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts" setup> |
||||
|
import { reactive, ref, onMounted } from 'vue'; |
||||
|
import { useRouter } from 'vue-router'; |
||||
|
import { useMessage } from '@/hooks/web/useMessage'; |
||||
|
import { loginApi } from '@/api/auth'; |
||||
|
import { captchaImage } from '@/api/auth/captcha'; |
||||
|
import { useUserStore } from '@/store/modules/user'; |
||||
|
|
||||
|
const router = useRouter(); |
||||
|
const { notification, createErrorModal } = useMessage(); |
||||
|
const userStore = useUserStore(); |
||||
|
|
||||
|
// 表单数据 |
||||
|
const formData = reactive({ |
||||
|
username: '', |
||||
|
password: '', |
||||
|
code: '', |
||||
|
uuid: '', |
||||
|
tenantId: '000000', // 默认租户ID |
||||
|
}); |
||||
|
|
||||
|
// 验证码信息 |
||||
|
const captcha = reactive({ |
||||
|
required: false, |
||||
|
image: '', |
||||
|
loading: false, |
||||
|
}); |
||||
|
|
||||
|
// 其他状态 |
||||
|
const loading = ref(false); |
||||
|
const rememberMe = ref(false); |
||||
|
const passwordVisible = ref(false); // 新增:控制密码显示/隐藏 |
||||
|
|
||||
|
// 刷新验证码 |
||||
|
const refreshCaptcha = async () => { |
||||
|
try { |
||||
|
captcha.loading = true; |
||||
|
const result = await captchaImage(); |
||||
|
captcha.required = result.captchaEnabled; |
||||
|
captcha.image = result.captchaEnabled ? `data:image/gif;base64,${result.img}` : ''; |
||||
|
formData.uuid = result.uuid; |
||||
|
formData.code = ''; |
||||
|
} catch (error) { |
||||
|
console.error('获取验证码失败:', error); |
||||
|
} finally { |
||||
|
captcha.loading = false; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// 切换密码显示/隐藏 |
||||
|
const togglePasswordVisibility = () => { |
||||
|
passwordVisible.value = !passwordVisible.value; |
||||
|
}; |
||||
|
|
||||
|
// 处理登录 |
||||
|
const handleLogin = async () => { |
||||
|
if (loading.value) return; |
||||
|
|
||||
|
try { |
||||
|
loading.value = true; |
||||
|
|
||||
|
const params = { |
||||
|
username: formData.username, |
||||
|
password: formData.password, |
||||
|
grantType: 'password' as const, |
||||
|
tenantId: formData.tenantId, |
||||
|
...(captcha.required && { code: formData.code, uuid: formData.uuid }), |
||||
|
}; |
||||
|
|
||||
|
const userInfo = await userStore.login({ |
||||
|
...params, |
||||
|
mode: 'none', |
||||
|
}); |
||||
|
|
||||
|
if (userInfo) { |
||||
|
notification.success({ |
||||
|
message: '登录成功', |
||||
|
description: `欢迎回来,${userInfo.nickName}!`, |
||||
|
duration: 3, |
||||
|
}); |
||||
|
// 登录成功后跳转到用户主页 |
||||
|
router.push('/user/home'); |
||||
|
} |
||||
|
} catch (error: any) { |
||||
|
const content = error.message || '登录失败,请稍后重试'; |
||||
|
createErrorModal({ |
||||
|
title: '登录失败', |
||||
|
content, |
||||
|
onOk() { |
||||
|
if (captcha.required && (content.includes('验证码') || content.includes('Captcha'))) { |
||||
|
formData.code = ''; |
||||
|
refreshCaptcha(); |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
} finally { |
||||
|
loading.value = false; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// 处理注册 |
||||
|
const handleRegister = (event?: Event) => { |
||||
|
// 阻止默认的链接行为 |
||||
|
if (event) { |
||||
|
event.preventDefault(); |
||||
|
} |
||||
|
// 跳转到注册页面 |
||||
|
router.push('/user/register'); |
||||
|
}; |
||||
|
|
||||
|
// 处理忘记密码 |
||||
|
const handleForgotPassword = (event?: Event) => { |
||||
|
// 阻止默认的链接行为 |
||||
|
if (event) { |
||||
|
event.preventDefault(); |
||||
|
} |
||||
|
// 跳转到忘记密码页面 |
||||
|
router.push('/user/forgot-password'); |
||||
|
}; |
||||
|
|
||||
|
// 初始化 |
||||
|
onMounted(async () => { |
||||
|
await refreshCaptcha(); |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<style lang="less" scoped> |
||||
|
* { |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
|
||||
|
.user-login-page { |
||||
|
min-height: 100vh; |
||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
position: relative; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
// 背景装饰 |
||||
|
.bg-decoration { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
pointer-events: none; |
||||
|
z-index: 1; |
||||
|
|
||||
|
.decoration-circle { |
||||
|
position: absolute; |
||||
|
border-radius: 50%; |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
|
||||
|
&.circle-1 { |
||||
|
width: 300px; |
||||
|
height: 300px; |
||||
|
top: -150px; |
||||
|
right: -150px; |
||||
|
} |
||||
|
|
||||
|
&.circle-2 { |
||||
|
width: 200px; |
||||
|
height: 200px; |
||||
|
bottom: -100px; |
||||
|
left: -100px; |
||||
|
} |
||||
|
|
||||
|
&.circle-3 { |
||||
|
width: 150px; |
||||
|
height: 150px; |
||||
|
top: 50%; |
||||
|
left: 20%; |
||||
|
transform: translateY(-50%); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.login-container { |
||||
|
display: grid; |
||||
|
grid-template-columns: 1fr 1fr; |
||||
|
max-width: 1200px; |
||||
|
width: 90%; |
||||
|
min-height: 600px; |
||||
|
background: white; |
||||
|
border-radius: 20px; |
||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); |
||||
|
overflow: hidden; |
||||
|
position: relative; |
||||
|
z-index: 2; |
||||
|
} |
||||
|
|
||||
|
// 左侧功能展示 |
||||
|
.left-section { |
||||
|
padding: 3rem; |
||||
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.brand-info { |
||||
|
text-align: center; |
||||
|
margin-bottom: 3rem; |
||||
|
|
||||
|
.logo-section { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
gap: 1rem; |
||||
|
margin-bottom: 1rem; |
||||
|
|
||||
|
.logo { |
||||
|
width: 48px; |
||||
|
height: 48px; |
||||
|
} |
||||
|
|
||||
|
.brand-name { |
||||
|
font-size: 2rem; |
||||
|
font-weight: 700; |
||||
|
color: #2c3e50; |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.brand-desc { |
||||
|
font-size: 1.1rem; |
||||
|
color: #7f8c8d; |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.features-showcase { |
||||
|
display: grid; |
||||
|
grid-template-columns: 1fr 1fr; |
||||
|
gap: 1.5rem; |
||||
|
} |
||||
|
|
||||
|
.feature-card { |
||||
|
padding: 1.5rem; |
||||
|
background: white; |
||||
|
border-radius: 12px; |
||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); |
||||
|
text-align: center; |
||||
|
transition: transform 0.3s, box-shadow 0.3s; |
||||
|
|
||||
|
&:hover { |
||||
|
transform: translateY(-2px); |
||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); |
||||
|
} |
||||
|
|
||||
|
.feature-icon { |
||||
|
font-size: 2rem; |
||||
|
margin-bottom: 0.5rem; |
||||
|
} |
||||
|
|
||||
|
h3 { |
||||
|
font-size: 1rem; |
||||
|
font-weight: 600; |
||||
|
color: #2c3e50; |
||||
|
margin: 0 0 0.5rem 0; |
||||
|
} |
||||
|
|
||||
|
p { |
||||
|
font-size: 0.85rem; |
||||
|
color: #7f8c8d; |
||||
|
line-height: 1.4; |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 右侧登录区域 |
||||
|
.right-section { |
||||
|
padding: 2rem; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
position: relative; |
||||
|
} |
||||
|
|
||||
|
.login-form-container { |
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
max-width: 400px; |
||||
|
margin: 0 auto; |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.form-header { |
||||
|
text-align: center; |
||||
|
margin-bottom: 2rem; |
||||
|
|
||||
|
h2 { |
||||
|
font-size: 1.8rem; |
||||
|
font-weight: 600; |
||||
|
color: #2c3e50; |
||||
|
margin: 0 0 0.5rem 0; |
||||
|
} |
||||
|
|
||||
|
p { |
||||
|
color: #7f8c8d; |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 账号登录表单 |
||||
|
.login-form { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 1.5rem; |
||||
|
} |
||||
|
|
||||
|
.form-group { |
||||
|
position: relative; |
||||
|
|
||||
|
&.captcha-group { |
||||
|
display: flex; |
||||
|
gap: 1rem; |
||||
|
|
||||
|
.captcha-input { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.captcha-image { |
||||
|
width: 120px; |
||||
|
height: 40px; |
||||
|
border: 1px solid #d9d9d9; |
||||
|
border-radius: 6px; |
||||
|
cursor: pointer; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
background: #fafafa; |
||||
|
|
||||
|
img { |
||||
|
max-width: 100%; |
||||
|
max-height: 100%; |
||||
|
} |
||||
|
|
||||
|
.captcha-loading { |
||||
|
font-size: 0.8rem; |
||||
|
color: #999; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.form-input { |
||||
|
width: 100%; |
||||
|
padding: 12px 16px; |
||||
|
border: 1px solid #d9d9d9; |
||||
|
border-radius: 6px; |
||||
|
font-size: 1rem; |
||||
|
transition: border-color 0.3s, box-shadow 0.3s; |
||||
|
|
||||
|
&:focus { |
||||
|
outline: none; |
||||
|
border-color: #667eea; |
||||
|
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2); |
||||
|
} |
||||
|
|
||||
|
&::placeholder { |
||||
|
color: #bfbfbf; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.password-input-wrapper { |
||||
|
position: relative; |
||||
|
.form-input { |
||||
|
padding-right: 40px; // 为眼睛图标留出空间 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.password-toggle-btn { |
||||
|
position: absolute; |
||||
|
top: 50%; |
||||
|
right: 10px; |
||||
|
transform: translateY(-50%); |
||||
|
background: none; |
||||
|
border: none; |
||||
|
cursor: pointer; |
||||
|
padding: 5px; |
||||
|
z-index: 10; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
border-radius: 4px; |
||||
|
transition: all 0.3s ease; |
||||
|
|
||||
|
.eye-icon { |
||||
|
width: 18px; |
||||
|
height: 18px; |
||||
|
color: #999; |
||||
|
transition: color 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
&:hover { |
||||
|
background-color: rgba(102, 126, 234, 0.1); |
||||
|
|
||||
|
.eye-icon { |
||||
|
color: #667eea; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&:active { |
||||
|
transform: translateY(-50%) scale(0.95); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.form-options { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.remember-me { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
cursor: pointer; |
||||
|
font-size: 0.9rem; |
||||
|
color: #666; |
||||
|
|
||||
|
input[type="checkbox"] { |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
.checkmark { |
||||
|
width: 16px; |
||||
|
height: 16px; |
||||
|
border: 2px solid #d9d9d9; |
||||
|
border-radius: 3px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
transition: all 0.3s; |
||||
|
|
||||
|
&::after { |
||||
|
content: '✓'; |
||||
|
font-size: 10px; |
||||
|
color: white; |
||||
|
opacity: 0; |
||||
|
transition: opacity 0.3s; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
input[type="checkbox"]:checked + .checkmark { |
||||
|
background: #667eea; |
||||
|
border-color: #667eea; |
||||
|
|
||||
|
&::after { |
||||
|
opacity: 1; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.forgot-password { |
||||
|
color: #667eea; |
||||
|
text-decoration: none; |
||||
|
font-size: 0.9rem; |
||||
|
|
||||
|
&:hover { |
||||
|
text-decoration: underline; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.login-btn { |
||||
|
padding: 12px 24px; |
||||
|
background: #667eea; |
||||
|
color: white; |
||||
|
border: none; |
||||
|
border-radius: 6px; |
||||
|
font-size: 1rem; |
||||
|
font-weight: 600; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
gap: 8px; |
||||
|
|
||||
|
&:hover:not(:disabled) { |
||||
|
background: #5a6fd8; |
||||
|
transform: translateY(-2px); |
||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); |
||||
|
} |
||||
|
|
||||
|
&:disabled { |
||||
|
opacity: 0.6; |
||||
|
cursor: not-allowed; |
||||
|
transform: none; |
||||
|
} |
||||
|
|
||||
|
&.loading { |
||||
|
pointer-events: none; |
||||
|
} |
||||
|
|
||||
|
.loading-spinner { |
||||
|
width: 16px; |
||||
|
height: 16px; |
||||
|
border: 2px solid transparent; |
||||
|
border-top: 2px solid currentColor; |
||||
|
border-radius: 50%; |
||||
|
animation: spin 1s linear infinite; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@keyframes spin { |
||||
|
to { |
||||
|
transform: rotate(360deg); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 其他选项 |
||||
|
.other-options { |
||||
|
margin-top: 2rem; |
||||
|
text-align: center; |
||||
|
|
||||
|
.register-link { |
||||
|
color: #666; |
||||
|
font-size: 0.9rem; |
||||
|
margin: 0; |
||||
|
|
||||
|
a { |
||||
|
color: #667eea; |
||||
|
text-decoration: none; |
||||
|
|
||||
|
&:hover { |
||||
|
text-decoration: underline; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 响应式设计 |
||||
|
@media (max-width: 768px) { |
||||
|
.login-container { |
||||
|
grid-template-columns: 1fr; |
||||
|
width: 95%; |
||||
|
min-height: auto; |
||||
|
} |
||||
|
|
||||
|
.left-section { |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
.right-section { |
||||
|
padding: 2rem 1.5rem; |
||||
|
} |
||||
|
|
||||
|
.features-showcase { |
||||
|
grid-template-columns: 1fr; |
||||
|
gap: 1rem; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media (max-width: 480px) { |
||||
|
.user-login-page { |
||||
|
padding: 1rem; |
||||
|
} |
||||
|
|
||||
|
.login-container { |
||||
|
width: 100%; |
||||
|
border-radius: 12px; |
||||
|
} |
||||
|
|
||||
|
.right-section { |
||||
|
padding: 1.5rem 1rem; |
||||
|
} |
||||
|
} |
||||
|
</style> |
Loading…
Reference in new issue