diff --git a/nkebao/src/pages/login/login.module.scss b/nkebao/src/pages/login/login.module.scss index 21ebe95ab..a13baa521 100644 --- a/nkebao/src/pages/login/login.module.scss +++ b/nkebao/src/pages/login/login.module.scss @@ -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; + } } diff --git a/nkebao/src/pages/login/login.tsx b/nkebao/src/pages/login/login.tsx index c30137295..f53ccc6d0 100644 --- a/nkebao/src/pages/login/login.tsx +++ b/nkebao/src/pages/login/login.tsx @@ -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 ( - -
- -
登录
-
+
+ {/* 背景装饰 */} +
+
+
+
+
-
-
-

存客宝

-

欢迎回来,请登录您的账户

+
+ {/* Logo和标题区域 */} +
+
+
+ +
+

存客宝

+
+

登录您的账户继续使用

+
+ + {/* 登录表单 */} +
+ {/* 标签页切换 */} +
+
setActiveTab(1)} + > + 密码登录 +
+
setActiveTab(2)} + > + 验证码登录 +
+
+ {/* 手机号输入 */} - +
+ +86 + +
- - - + {/* 密码输入 */} + {activeTab === 1 && ( + +
+ +
setShowPassword(!showPassword)} + > + {showPassword ? : } +
+
+
+ )} + {/* 验证码输入 */} + {activeTab === 2 && ( + +
+ + +
+
+ )} + + {/* 用户协议 */} +
+ + + 我已阅读并同意 + + 《存客宝用户协议》 + + 和 + 《隐私政策》 + + +
+ + {/* 登录按钮 */}
+ + {/* 分割线 */} +
+ 其他登录方式 +
+ + {/* 第三方登录 */} +
+
+
+ + + + +
+ 微信 +
+
+
+ + + +
+ Apple +
+
- +
); }; diff --git a/nkebao/src/pages/scenarios/plan/list/api.ts b/nkebao/src/pages/scenarios/plan/list/api.ts new file mode 100644 index 000000000..b2106181f --- /dev/null +++ b/nkebao/src/pages/scenarios/plan/list/api.ts @@ -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"); +} diff --git a/nkebao/src/pages/scenarios/ScenarioList.module.scss b/nkebao/src/pages/scenarios/plan/list/index.module.scss similarity index 76% rename from nkebao/src/pages/scenarios/ScenarioList.module.scss rename to nkebao/src/pages/scenarios/plan/list/index.module.scss index 67292ac1d..b3696f7b9 100644 --- a/nkebao/src/pages/scenarios/ScenarioList.module.scss +++ b/nkebao/src/pages/scenarios/plan/list/index.module.scss @@ -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; diff --git a/nkebao/src/pages/scenarios/ScenarioList.tsx b/nkebao/src/pages/scenarios/plan/list/index.tsx similarity index 85% rename from nkebao/src/pages/scenarios/ScenarioList.tsx rename to nkebao/src/pages/scenarios/plan/list/index.tsx index 30e3710b8..5d02c3591 100644 --- a/nkebao/src/pages/scenarios/ScenarioList.tsx +++ b/nkebao/src/pages/scenarios/plan/list/index.tsx @@ -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={} >
- {/* 场景信息卡片 */} - {scenario && ( - -
-
{scenario.name}
-
- {scenario.description} -
-
-
- - 计划数: {scenario.totalPlans} -
-
- - 今日获客: {scenario.todayCustomers} -
-
-
-
- )} - {/* 搜索栏 */}
@@ -450,7 +440,7 @@ const ScenarioList: React.FC = () => {
{/* 计划列表 */} - +
{filteredTasks.length === 0 ? (
@@ -466,73 +456,68 @@ const ScenarioList: React.FC = () => {
) : ( filteredTasks.map((task) => ( - navigate(`/scenarios/edit/${task.id}`)} - > -
-
-
{task.name}
- - {getStatusText(task.status)} - + + {/* 头部:标题和状态 */} +
+
{task.name}
+ + {getStatusText(task.status)} + +
+ + {/* 中部:更新时间和统计 */} +
+
+ + 最后更新: {task.updated_at || task.created_at}
-
- 创建时间: {task.created_at} - {task.total_customers !== undefined && ( - 总获客: {task.total_customers} - )} - {task.today_customers !== undefined && ( - 今日获客: {task.today_customers} - )} +
+ + + 设备: {task.stats?.devices || 0} | 获客:{" "} + {task.stats?.acquired || 0} | 添加:{" "} + {task.stats?.added || 0} +
+ + {/* 底部:操作按钮 */}
+ + - - +
- + )) )} - +
{/* API设置弹窗 */} , + element: , auth: true, }, ];