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