feat: 本次提交更新内容如下
恢复登录页
This commit is contained in:
@@ -1,52 +1,439 @@
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.login-content {
|
||||
padding: 40px 20px;
|
||||
background: var(--primary-gradient);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: calc(100vh - 60px);
|
||||
padding: 15px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// 背景装饰
|
||||
.bg-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.bg-circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
animation: float 6s ease-in-out infinite;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
top: -100px;
|
||||
right: -100px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
bottom: -75px;
|
||||
left: -75px;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
top: 50%;
|
||||
right: 10%;
|
||||
animation-delay: 4s;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0px) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20px) rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.login-container {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
background: #ffffff;
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 24px;
|
||||
padding: 24px 20px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 8px;
|
||||
.logo-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-size: 14px;
|
||||
.logo-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: var(--primary-gradient);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
box-shadow: 0 6px 12px var(--primary-shadow);
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
background: var(--primary-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
// 标签页样式
|
||||
.tab-container {
|
||||
display: flex;
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
padding: 3px;
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
border-radius: 7px;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
&.active {
|
||||
color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
background: white;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-indicator {
|
||||
display: none; // 隐藏分割线指示器
|
||||
}
|
||||
|
||||
// 表单样式
|
||||
.login-form {
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
:global(.adm-form) {
|
||||
--adm-font-size-main: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-button {
|
||||
margin-top: 20px;
|
||||
border-radius: 20px;
|
||||
.input-group {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #f8f9fa;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--primary-color);
|
||||
background: white;
|
||||
box-shadow: 0 0 0 3px var(--primary-shadow-light);
|
||||
}
|
||||
}
|
||||
|
||||
.input-prefix {
|
||||
padding: 0 12px;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
border-right: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.phone-input,
|
||||
.password-input,
|
||||
.code-input {
|
||||
flex: 1;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
padding: 12px 14px !important;
|
||||
font-size: 15px !important;
|
||||
color: #333 !important;
|
||||
|
||||
&::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.eye-icon {
|
||||
padding: 0 12px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.send-code-btn {
|
||||
padding: 6px 12px;
|
||||
margin-right: 6px;
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 3px 8px var(--primary-shadow);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: #e5e5e5;
|
||||
color: #999;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.agreement-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.agreement-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
|
||||
:global(.adm-checkbox) {
|
||||
margin-top: 0;
|
||||
flex-shrink: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.agreement-text {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.agreement-link {
|
||||
color: var(--primary-color);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
font-size: 11px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
height: 46px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
border-radius: 10px;
|
||||
background: var(--primary-gradient);
|
||||
border: none;
|
||||
box-shadow: 0 2px 8px var(--primary-shadow);
|
||||
height: 44px;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 6px 12px var(--primary-shadow);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 16px var(--primary-shadow-dark);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #e5e5e5;
|
||||
color: #999;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
margin: 24px 0;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: #e5e5e5;
|
||||
}
|
||||
|
||||
span {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 0 12px;
|
||||
color: #999;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.third-party-login {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.third-party-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f8f9fa;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.wechat-icon,
|
||||
.apple-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.wechat-icon {
|
||||
background: #07c160;
|
||||
box-shadow: 0 3px 8px rgba(7, 193, 96, 0.3);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(7, 193, 96, 0.4);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.apple-icon {
|
||||
background: #000;
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.3);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 480px) {
|
||||
.login-container {
|
||||
padding: 24px 20px;
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.third-party-login {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.third-party-item {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.wechat-icon,
|
||||
.apple-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,331 @@
|
||||
import React from "react";
|
||||
import { NavBar, Form, Input, Button, Toast } from "antd-mobile";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { Form, Input, Button, Toast, Tabs, Checkbox } from "antd-mobile";
|
||||
import {
|
||||
EyeInvisibleOutline,
|
||||
EyeOutline,
|
||||
UserOutline,
|
||||
} from "antd-mobile-icons";
|
||||
import { useUserStore } from "@/store/module/user";
|
||||
import { loginWithPassword, loginWithCode, sendVerificationCode } from "./api";
|
||||
import style from "./login.module.scss";
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const onFinish = (values: any) => {
|
||||
console.log("登录信息:", values);
|
||||
Toast.show({
|
||||
content: "登录功能待实现",
|
||||
position: "top",
|
||||
});
|
||||
const [form] = Form.useForm();
|
||||
const [activeTab, setActiveTab] = useState(1); // 1: 密码登录, 2: 验证码登录
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [agreeToTerms, setAgreeToTerms] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { login } = useUserStore();
|
||||
|
||||
// 倒计时效果
|
||||
useEffect(() => {
|
||||
if (countdown > 0) {
|
||||
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [countdown]);
|
||||
|
||||
// 检查URL是否为登录页面
|
||||
const isLoginPage = (url: string) => {
|
||||
try {
|
||||
const urlObj = new URL(url, window.location.origin);
|
||||
return urlObj.pathname === "/login" || urlObj.pathname.endsWith("/login");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 发送验证码
|
||||
const handleSendVerificationCode = async () => {
|
||||
const account = form.getFieldValue("account");
|
||||
|
||||
if (!account) {
|
||||
Toast.show({ content: "请输入手机号", position: "top" });
|
||||
return;
|
||||
}
|
||||
|
||||
// 手机号格式验证
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
if (!phoneRegex.test(account)) {
|
||||
Toast.show({ content: "请输入正确的11位手机号", position: "top" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
await sendVerificationCode({
|
||||
mobile: account,
|
||||
type: "login",
|
||||
});
|
||||
|
||||
Toast.show({ content: "验证码已发送", position: "top" });
|
||||
setCountdown(60);
|
||||
} catch (error) {
|
||||
// 错误已在request中处理,这里不需要额外处理
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 登录处理
|
||||
const handleLogin = async (values: any) => {
|
||||
if (!agreeToTerms) {
|
||||
Toast.show({ content: "请同意用户协议和隐私政策", position: "top" });
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// 添加typeId参数
|
||||
const loginParams = {
|
||||
...values,
|
||||
typeId: activeTab as number,
|
||||
};
|
||||
|
||||
let response;
|
||||
if (activeTab === 1) {
|
||||
response = await loginWithPassword(loginParams);
|
||||
} else {
|
||||
response = await loginWithCode(loginParams);
|
||||
}
|
||||
console.log(response, "response");
|
||||
|
||||
// 更新状态管理(token会自动存储到localStorage,用户信息存储在状态管理中)
|
||||
login(response.token, response.member);
|
||||
|
||||
Toast.show({ content: "登录成功", position: "top" });
|
||||
|
||||
// 跳转到首页或重定向URL
|
||||
const returnUrl = searchParams.get("returnUrl");
|
||||
if (returnUrl) {
|
||||
const decodedUrl = decodeURIComponent(returnUrl);
|
||||
if (isLoginPage(decodedUrl)) {
|
||||
navigate("/");
|
||||
} else {
|
||||
window.location.href = decodedUrl;
|
||||
}
|
||||
} else {
|
||||
navigate("/");
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 错误已在request中处理,这里不需要额外处理
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 第三方登录处理
|
||||
const handleWechatLogin = () => {
|
||||
Toast.show({ content: "微信登录功能开发中", position: "top" });
|
||||
};
|
||||
|
||||
const handleAppleLogin = () => {
|
||||
Toast.show({ content: "Apple登录功能开发中", position: "top" });
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className={style["login-page"]}>
|
||||
<NavBar back={null} style={{ background: "#fff" }}>
|
||||
<div className={style["nav-title"]}>登录</div>
|
||||
</NavBar>
|
||||
<div className={style["login-page"]}>
|
||||
{/* 背景装饰 */}
|
||||
<div className={style["bg-decoration"]}>
|
||||
<div className={style["bg-circle"]}></div>
|
||||
<div className={style["bg-circle"]}></div>
|
||||
<div className={style["bg-circle"]}></div>
|
||||
</div>
|
||||
|
||||
<div className={style["login-content"]}>
|
||||
<div className={style["login-header"]}>
|
||||
<h1 className={style["login-title"]}>存客宝</h1>
|
||||
<p className={style["login-subtitle"]}>欢迎回来,请登录您的账户</p>
|
||||
<div className={style["login-container"]}>
|
||||
{/* Logo和标题区域 */}
|
||||
<div className={style["login-header"]}>
|
||||
<div className={style["logo-section"]}>
|
||||
<div className={style["logo-icon"]}>
|
||||
<UserOutline />
|
||||
</div>
|
||||
<h1 className={style["app-name"]}>存客宝</h1>
|
||||
</div>
|
||||
<p className={style["subtitle"]}>登录您的账户继续使用</p>
|
||||
</div>
|
||||
|
||||
{/* 登录表单 */}
|
||||
<div className={style["form-container"]}>
|
||||
{/* 标签页切换 */}
|
||||
<div className={style["tab-container"]}>
|
||||
<div
|
||||
className={`${style["tab-item"]} ${
|
||||
activeTab === 1 ? style["active"] : ""
|
||||
}`}
|
||||
onClick={() => setActiveTab(1)}
|
||||
>
|
||||
密码登录
|
||||
</div>
|
||||
<div
|
||||
className={`${style["tab-item"]} ${
|
||||
activeTab === 2 ? style["active"] : ""
|
||||
}`}
|
||||
onClick={() => setActiveTab(2)}
|
||||
>
|
||||
验证码登录
|
||||
</div>
|
||||
<div
|
||||
className={`${style["tab-indicator"]} ${
|
||||
activeTab === 2 ? style["slide"] : ""
|
||||
}`}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={onFinish}
|
||||
className={style["login-form"]}
|
||||
onFinish={handleLogin}
|
||||
>
|
||||
{/* 手机号输入 */}
|
||||
<Form.Item
|
||||
label="用户名"
|
||||
name="username"
|
||||
rules={[{ required: true, message: "请输入用户名" }]}
|
||||
name="account"
|
||||
label="手机号"
|
||||
rules={[
|
||||
{ required: true, message: "请输入手机号" },
|
||||
{
|
||||
pattern: /^1[3-9]\d{9}$/,
|
||||
message: "请输入正确的11位手机号",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入用户名" />
|
||||
<div className={style["input-wrapper"]}>
|
||||
<span className={style["input-prefix"]}>+86</span>
|
||||
<Input
|
||||
placeholder="请输入手机号"
|
||||
clearable
|
||||
className={style["phone-input"]}
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="密码"
|
||||
name="password"
|
||||
rules={[{ required: true, message: "请输入密码" }]}
|
||||
>
|
||||
<Input type="password" placeholder="请输入密码" />
|
||||
</Form.Item>
|
||||
{/* 密码输入 */}
|
||||
{activeTab === 1 && (
|
||||
<Form.Item
|
||||
name="password"
|
||||
label="密码"
|
||||
rules={[{ required: true, message: "请输入密码" }]}
|
||||
>
|
||||
<div className={style["input-wrapper"]}>
|
||||
<Input
|
||||
placeholder="请输入密码"
|
||||
clearable
|
||||
type={showPassword ? "text" : "password"}
|
||||
className={style["password-input"]}
|
||||
/>
|
||||
<div
|
||||
className={style["eye-icon"]}
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? <EyeOutline /> : <EyeInvisibleOutline />}
|
||||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 验证码输入 */}
|
||||
{activeTab === 2 && (
|
||||
<Form.Item
|
||||
name="verificationCode"
|
||||
label="验证码"
|
||||
rules={[{ required: true, message: "请输入验证码" }]}
|
||||
>
|
||||
<div className={style["input-wrapper"]}>
|
||||
<Input
|
||||
placeholder="请输入验证码"
|
||||
clearable
|
||||
className={style["code-input"]}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={`${style["send-code-btn"]} ${
|
||||
countdown > 0 ? style["disabled"] : ""
|
||||
}`}
|
||||
onClick={handleSendVerificationCode}
|
||||
disabled={loading || countdown > 0}
|
||||
>
|
||||
{countdown > 0 ? `${countdown}s` : "获取验证码"}
|
||||
</button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 用户协议 */}
|
||||
<div className={style["agreement-section"]}>
|
||||
<Checkbox
|
||||
checked={agreeToTerms}
|
||||
onChange={setAgreeToTerms}
|
||||
className={style["agreement-checkbox"]}
|
||||
>
|
||||
<span className={style["agreement-text"]}>
|
||||
我已阅读并同意
|
||||
<span className={style["agreement-link"]}>
|
||||
《存客宝用户协议》
|
||||
</span>
|
||||
和
|
||||
<span className={style["agreement-link"]}>《隐私政策》</span>
|
||||
</span>
|
||||
</Checkbox>
|
||||
</div>
|
||||
|
||||
{/* 登录按钮 */}
|
||||
<Button
|
||||
block
|
||||
color="primary"
|
||||
size="large"
|
||||
type="submit"
|
||||
className={style["login-button"]}
|
||||
color="primary"
|
||||
loading={loading}
|
||||
size="large"
|
||||
className={style["login-btn"]}
|
||||
>
|
||||
登录
|
||||
{loading ? "登录中..." : "登录"}
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
{/* 分割线 */}
|
||||
<div className={style["divider"]}>
|
||||
<span>其他登录方式</span>
|
||||
</div>
|
||||
|
||||
{/* 第三方登录 */}
|
||||
<div className={style["third-party-login"]}>
|
||||
<div
|
||||
className={style["third-party-item"]}
|
||||
onClick={handleWechatLogin}
|
||||
>
|
||||
<div className={style["wechat-icon"]}>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
height="24"
|
||||
width="24"
|
||||
className={style["wechat-icon"]}
|
||||
>
|
||||
<path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.81-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.595-6.348zM5.959 5.48c.609 0 1.104.498 1.104 1.112 0 .612-.495 1.11-1.104 1.11-.612 0-1.108-.498-1.108-1.11 0-.614.496-1.112 1.108-1.112zm5.315 0c.61 0 1.107.498 1.107 1.112 0 .612-.497 1.11-1.107 1.11-.611 0-1.105-.498-1.105-1.11 0-.614.494-1.112 1.105-1.112z"></path>
|
||||
<path d="M23.002 15.816c0-3.309-3.136-6-7-6-3.863 0-7 2.691-7 6 0 3.31 3.137 6 7 6 .814 0 1.601-.099 2.338-.285a.7.7 0 0 1 .579.08l1.5.87a.267.267 0 0 0 .135.044c.13 0 .236-.108.236-.241 0-.06-.023-.118-.038-.17l-.309-1.167a.476.476 0 0 1 .172-.534c1.645-1.17 2.387-2.835 2.387-4.597zm-9.498-1.19c-.497 0-.9-.407-.9-.908a.905.905 0 0 1 .9-.91c.498 0 .9.408.9.91 0 .5-.402.908-.9.908zm4.998 0c-.497 0-.9-.407-.9-.908a.905.905 0 0 1 .9-.91c.498 0 .9.408.9.91 0 .5-.402.908-.9.908z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<span>微信</span>
|
||||
</div>
|
||||
<div
|
||||
className={style["third-party-item"]}
|
||||
onClick={handleAppleLogin}
|
||||
>
|
||||
<div className={style["apple-icon"]}>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span>Apple</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
27
nkebao/src/pages/scenarios/plan/list/api.ts
Normal file
27
nkebao/src/pages/scenarios/plan/list/api.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import request from "@/api/request";
|
||||
|
||||
// ==================== 计划相关接口 ====================
|
||||
// 获取计划列表
|
||||
export function getPlanList(params: any) {
|
||||
return request(`/v1/plan/list`, params, "GET");
|
||||
}
|
||||
|
||||
// 获取计划详情
|
||||
export function getPlanDetail(planId: string) {
|
||||
return request(`/api/scenarios/plans/${planId}`, {}, "GET");
|
||||
}
|
||||
|
||||
// 复制计划
|
||||
export function copyPlan(planId: string) {
|
||||
return request(`/api/scenarios/plans/${planId}/copy`, {}, "POST");
|
||||
}
|
||||
|
||||
// 删除计划
|
||||
export function deletePlan(planId: string) {
|
||||
return request(`/api/scenarios/plans/${planId}`, {}, "DELETE");
|
||||
}
|
||||
|
||||
// 获取小程序二维码
|
||||
export function getWxMinAppCode(planId: string) {
|
||||
return request(`/api/scenarios/plans/${planId}/qrcode`, {}, "GET");
|
||||
}
|
||||
@@ -30,60 +30,33 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.scenario-card {
|
||||
margin-bottom: 16px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.scenario-info {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.scenario-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.scenario-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.scenario-stats {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
|
||||
svg {
|
||||
font-size: 16px;
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
|
||||
.adm-input {
|
||||
flex: 1;
|
||||
padding-left: 40px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
@@ -92,32 +65,22 @@
|
||||
}
|
||||
|
||||
.plan-list {
|
||||
background: transparent;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.plan-item {
|
||||
background: white;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.plan-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.plan-header {
|
||||
@@ -138,15 +101,29 @@
|
||||
.plan-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
color: #666;
|
||||
|
||||
svg {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.plan-actions {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
@@ -216,6 +193,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.input-with-button {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.adm-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-dialog {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
Card,
|
||||
Tag,
|
||||
Space,
|
||||
Divider,
|
||||
} from "antd-mobile";
|
||||
import {
|
||||
PlusOutlined,
|
||||
@@ -20,12 +19,11 @@ import {
|
||||
CalendarOutlined,
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
SettingOutlined,
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
QrcodeOutlined,
|
||||
UpOutlined,
|
||||
EditOutlined,
|
||||
} from "@ant-design/icons";
|
||||
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
@@ -37,7 +35,7 @@ import {
|
||||
deletePlan,
|
||||
getWxMinAppCode,
|
||||
} from "./api";
|
||||
import style from "./ScenarioList.module.scss";
|
||||
import style from "./index.module.scss";
|
||||
|
||||
interface Task {
|
||||
id: string;
|
||||
@@ -48,6 +46,12 @@ interface Task {
|
||||
enabled: boolean;
|
||||
total_customers?: number;
|
||||
today_customers?: number;
|
||||
lastUpdated?: string;
|
||||
stats?: {
|
||||
devices?: number;
|
||||
acquired?: number;
|
||||
added?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface ScenarioData {
|
||||
@@ -131,7 +135,11 @@ const ScenarioList: React.FC = () => {
|
||||
|
||||
try {
|
||||
// 获取计划列表
|
||||
const response = await getPlanList(scenarioId, 1, 20);
|
||||
const response = await getPlanList({
|
||||
sceneId: scenarioId,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
});
|
||||
|
||||
// 设置计划列表
|
||||
if (response && response.data && response.data.list) {
|
||||
@@ -348,7 +356,11 @@ const ScenarioList: React.FC = () => {
|
||||
const handleRefresh = async () => {
|
||||
setLoadingTasks(true);
|
||||
try {
|
||||
const response = await getPlanList(scenarioId!, 1, 20);
|
||||
const response = await getPlanList({
|
||||
scenarioId: scenarioId!,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
});
|
||||
if (response && response.data && response.data.list) {
|
||||
setTasks(response.data.list);
|
||||
}
|
||||
@@ -406,28 +418,6 @@ const ScenarioList: React.FC = () => {
|
||||
footer={<MeauMobile />}
|
||||
>
|
||||
<div className={style["scenario-list-page"]}>
|
||||
{/* 场景信息卡片 */}
|
||||
{scenario && (
|
||||
<Card className={style["scenario-card"]}>
|
||||
<div className={style["scenario-info"]}>
|
||||
<div className={style["scenario-name"]}>{scenario.name}</div>
|
||||
<div className={style["scenario-desc"]}>
|
||||
{scenario.description}
|
||||
</div>
|
||||
<div className={style["scenario-stats"]}>
|
||||
<div className={style["stat-item"]}>
|
||||
<UserOutlined />
|
||||
<span>计划数: {scenario.totalPlans}</span>
|
||||
</div>
|
||||
<div className={style["stat-item"]}>
|
||||
<CalendarOutlined />
|
||||
<span>今日获客: {scenario.todayCustomers}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 搜索栏 */}
|
||||
<div className={style["search-bar"]}>
|
||||
<div className={style["search-input-wrapper"]}>
|
||||
@@ -450,7 +440,7 @@ const ScenarioList: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* 计划列表 */}
|
||||
<List className={style["plan-list"]}>
|
||||
<div className={style["plan-list"]}>
|
||||
{filteredTasks.length === 0 ? (
|
||||
<div className={style["empty-state"]}>
|
||||
<div className={style["empty-text"]}>
|
||||
@@ -466,73 +456,68 @@ const ScenarioList: React.FC = () => {
|
||||
</div>
|
||||
) : (
|
||||
filteredTasks.map((task) => (
|
||||
<List.Item
|
||||
key={task.id}
|
||||
className={style["plan-item"]}
|
||||
onClick={() => navigate(`/scenarios/edit/${task.id}`)}
|
||||
>
|
||||
<div className={style["plan-content"]}>
|
||||
<div className={style["plan-header"]}>
|
||||
<div className={style["plan-name"]}>{task.name}</div>
|
||||
<Tag color={getStatusColor(task.status)}>
|
||||
{getStatusText(task.status)}
|
||||
</Tag>
|
||||
<Card key={task.id} className={style["plan-item"]}>
|
||||
{/* 头部:标题和状态 */}
|
||||
<div className={style["plan-header"]}>
|
||||
<div className={style["plan-name"]}>{task.name}</div>
|
||||
<Tag color={getStatusColor(task.status)}>
|
||||
{getStatusText(task.status)}
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
{/* 中部:更新时间和统计 */}
|
||||
<div className={style["plan-meta"]}>
|
||||
<div className={style["meta-item"]}>
|
||||
<CalendarOutlined />
|
||||
<span>最后更新: {task.updated_at || task.created_at}</span>
|
||||
</div>
|
||||
<div className={style["plan-meta"]}>
|
||||
<span>创建时间: {task.created_at}</span>
|
||||
{task.total_customers !== undefined && (
|
||||
<span>总获客: {task.total_customers}</span>
|
||||
)}
|
||||
{task.today_customers !== undefined && (
|
||||
<span>今日获客: {task.today_customers}</span>
|
||||
)}
|
||||
<div className={style["meta-item"]}>
|
||||
<UserOutlined />
|
||||
<span>
|
||||
设备: {task.stats?.devices || 0} | 获客:{" "}
|
||||
{task.stats?.acquired || 0} | 添加:{" "}
|
||||
{task.stats?.added || 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底部:操作按钮 */}
|
||||
<div className={style["plan-actions"]}>
|
||||
<Space>
|
||||
<Button
|
||||
size="mini"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCopyPlan(task.id);
|
||||
}}
|
||||
onClick={() => navigate(`/scenarios/edit/${task.id}`)}
|
||||
>
|
||||
<EditOutlined />
|
||||
</Button>
|
||||
<Button
|
||||
size="mini"
|
||||
onClick={() => handleOpenApiSettings(task.id)}
|
||||
>
|
||||
<SettingOutlined />
|
||||
</Button>
|
||||
<Button size="mini" onClick={() => handleCopyPlan(task.id)}>
|
||||
<CopyOutlined />
|
||||
</Button>
|
||||
<Button
|
||||
size="mini"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleShowQrCode(task.id);
|
||||
}}
|
||||
>
|
||||
<QrcodeOutlined />
|
||||
</Button>
|
||||
<Button
|
||||
size="mini"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleOpenApiSettings(task.id);
|
||||
}}
|
||||
>
|
||||
<SettingOutlined />
|
||||
</Button>
|
||||
<Button
|
||||
size="mini"
|
||||
color="danger"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeletePlan(task.id);
|
||||
}}
|
||||
onClick={() => handleDeletePlan(task.id)}
|
||||
>
|
||||
<DeleteOutlined />
|
||||
</Button>
|
||||
<Button
|
||||
size="mini"
|
||||
onClick={() => handleShowQrCode(task.id)}
|
||||
>
|
||||
<QrcodeOutlined />
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</List.Item>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</List>
|
||||
</div>
|
||||
|
||||
{/* API设置弹窗 */}
|
||||
<Popup
|
||||
@@ -1,6 +1,6 @@
|
||||
import ScenariosList from "@/pages/scenarios/list";
|
||||
import NewPlan from "@/pages/scenarios/new/page";
|
||||
import ScenarioList from "@/pages/scenarios/ScenarioList";
|
||||
import NewPlan from "@/pages/scenarios/plan/new";
|
||||
import ListPlan from "@/pages/scenarios/plan/list";
|
||||
|
||||
const scenarioRoutes = [
|
||||
{
|
||||
@@ -25,7 +25,7 @@ const scenarioRoutes = [
|
||||
},
|
||||
{
|
||||
path: "/scenarios/list/:scenarioId/:scenarioName",
|
||||
element: <ScenarioList />,
|
||||
element: <ListPlan />,
|
||||
auth: true,
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user