添加密码登录&调整接口数据展示&配置底部导航跳转

This commit is contained in:
Eamon-meng
2025-07-01 11:32:37 +08:00
parent b096e20fcd
commit bae4ee9b92
9 changed files with 175 additions and 92 deletions

View File

@@ -16,12 +16,13 @@ export async function login(props: {
username: string
password: string
remember: boolean
mode: 'phone_code' | 'password'
}): Promise<ApiResponse> {
// 尝试登录
const result = await callByDevice<TokenResp>('/api/auth/token', {
...props,
grant_type: 'password',
login_type: 'phone_code',
login_type: props.mode,
})
if (!result.success) {

View File

@@ -4,32 +4,32 @@ import {ApiResponse, ExtraResp} from '@/lib/api'
import {callByUser} from './base'
import {listAnnouncements} from './announcement'
type listAccountReq = {
type statisticsResourceUsageReq = {
resource_no?: string
create_after?: Date
create_before?: Date
}
type listAccountResp = {
type statisticsResourceUsageResp = {
date: string
count: number
}[]
export async function listAccount(props: listAccountReq) {
return await callByUser<listAccountResp>('/api/resource/statistics/usage', props)
export async function statisticsResourceUsage(props: statisticsResourceUsageReq) {
return await callByUser<statisticsResourceUsageResp>('/api/resource/statistics/usage', props)
}
export async function statisticsResourceFree() {
return await callByUser<{
long: {
ResourceCount: number
ResourceDailyFreeSum: number
ResourceQuotaSum: number
resource_count: number
resource_daily_free_sum: number
resource_quota_sum: number
}
short: {
ResourceCount: number
ResourceDailyFreeSum: number
ResourceQuotaSum: number
resource_count: number
resource_daily_free_sum: number
resource_quota_sum: number
}
}>('/api/resource/statistics/free')
}
@@ -37,7 +37,7 @@ export async function statisticsResourceFree() {
type listInitializationResp = {
anno: ExtraResp<typeof listAnnouncements>
free: ExtraResp<typeof statisticsResourceFree>
usage: ExtraResp<typeof listAccount>
usage: ExtraResp<typeof statisticsResourceUsage>
}
export async function listInitialization(): Promise<ApiResponse<listInitializationResp>> {
const free = await statisticsResourceFree()
@@ -59,7 +59,7 @@ export async function listInitialization(): Promise<ApiResponse<listInitializati
message: '公告数据获取失败',
}
}
const usage = await listAccount({
const usage = await statisticsResourceUsage({
create_after: new Date(),
create_before: new Date(),
})

View File

@@ -1,5 +1,5 @@
'use client'
import {useState, useCallback, useRef, useContext} from 'react'
import {useState, useCallback, useRef} from 'react'
import {Input} from '@/components/ui/input'
import {Button} from '@/components/ui/button'
import {Checkbox} from '@/components/ui/checkbox'
@@ -32,24 +32,31 @@ import Link from 'next/link'
export type LoginPageProps = {}
// 定义表单验证模式
const formSchema = zod.object({
const smsSchema = zod.object({
username: zod.string().min(11, '请输入正确的手机号码').max(11, '请输入正确的手机号码'),
password: zod.string().min(1, '请输入验证码'),
remember: zod.boolean().default(false),
})
type FormValues = zod.infer<typeof formSchema>
const pwdSchema = zod.object({
username: zod.string().min(11, '请输入正确的手机号码').max(11, '请输入正确的手机号码'),
password: zod.string().min(6, '请输入至少6位密码'),
remember: zod.boolean().default(false),
})
type FormValues = zod.infer<typeof smsSchema>
export default function LoginPage(props: LoginPageProps) {
const router = useRouter()
const [submitting, setSubmitting] = useState(false)
const [countdown, setCountdown] = useState(0)
const [showCaptcha, setShowCaptcha] = useState(false)
const [loginMode, setLoginMode] = useState<'sms' | 'password'>('sms')
const [showPwd, setShowPwd] = useState(false)
const timerRef = useRef<NodeJS.Timeout>(undefined)
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
resolver: zodResolver(loginMode === 'sms' ? smsSchema : pwdSchema),
defaultValues: {
username: '',
password: '',
@@ -103,7 +110,6 @@ export default function LoginPage(props: LoginPageProps) {
toast.error(resp.message)
return true
}
setShowCaptcha(false)
waiting = parseInt(resp.message)
console.log(resp.message)
@@ -132,12 +138,30 @@ export default function LoginPage(props: LoginPageProps) {
return prev - 1
})
}, 1000)
return false
}, [username])
// 处理表单提交
const onSubmit = async (values: FormValues) => {
// 密码登录时增加更严格的校验
if (loginMode === 'password') {
const pwd = values.password || ''
// 至少6位包含字母和数字
if (pwd.length < 6) {
form.setError('password', {
type: 'manual',
message: '密码长度至少6位',
})
return
}
if (!/[A-Za-z]/.test(pwd) || !/[0-9]/.test(pwd)) {
form.setError('password', {
type: 'manual',
message: '密码需包含字母和数字',
})
return
}
}
try {
setSubmitting(true)
@@ -153,7 +177,7 @@ export default function LoginPage(props: LoginPageProps) {
if (!values.password) {
form.setError('password', {
type: 'manual',
message: '请输入验证码',
message: loginMode === 'sms' ? '请输入验证码' : '请输入密码',
})
return
}
@@ -163,11 +187,12 @@ export default function LoginPage(props: LoginPageProps) {
username: values.username,
password: values.password,
remember: values.remember,
mode: loginMode === 'sms' ? 'phone_code' : 'password', // 后端区分登录方式
})
// 登录失败
if (!result.success) {
throw new Error(result.message || '请检查手机号码和验证码是否正确')
throw new Error(result.message || '请检查账号和密码/验证码是否正确')
}
// 登录成功
@@ -193,7 +218,6 @@ export default function LoginPage(props: LoginPageProps) {
const params = useSearchParams()
const redirect = params.get('redirect')
const refreshProfile = useProfileStore(store => store.refreshProfile)
// ======================
@@ -207,7 +231,6 @@ export default function LoginPage(props: LoginPageProps) {
`flex justify-center xl:justify-end items-center`,
)}>
<Image src={bg} alt="背景图" fill priority className="absolute -z-20 object-cover"/>
<Link href="/">
<Image src={logo} alt="logo" priority height={64} className="absolute top-8 left-8 -z-10"/>
</Link>
@@ -217,8 +240,32 @@ export default function LoginPage(props: LoginPageProps) {
<CardHeader className="text-center">
<CardTitle className="text-2xl">/</CardTitle>
</CardHeader>
<CardContent className="px-8">
{/* 登录方式切换 */}
<Card className="mb-6">
<CardContent className="flex justify-center gap-2">
<Button
type="button"
theme={loginMode === 'sms' ? 'gradient' : 'outline'}
onClick={() => {
setLoginMode('sms')
form.reset({username: form.getValues('username'), password: '', remember: false})
}}
>
</Button>
<Button
type="button"
theme={loginMode === 'password' ? 'gradient' : 'outline'}
onClick={() => {
setLoginMode('password')
form.reset({username: form.getValues('username'), password: '', remember: false})
}}
>
</Button>
</CardContent>
</Card>
<Form<FormValues> className="space-y-6" onSubmit={onSubmit} form={form}>
<FormField name="username" label="手机号码">
{({id, field}) => (
@@ -231,29 +278,65 @@ export default function LoginPage(props: LoginPageProps) {
/>
)}
</FormField>
<FormField name="password" label="验证码">
{({id, field}) => (
<div className="flex space-x-4">
<Input
{...field}
id={id}
className="h-12"
placeholder="请输入验证码"
/>
<Button
type="button"
theme="outline"
className="whitespace-nowrap h-12"
onClick={checkUsername}
disabled={countdown > 0}
>
{countdown > 0 ? `${countdown}秒后重发` : '获取验证码'}
</Button>
</div>
)}
<FormField name="password" label={loginMode === 'sms' ? '验证码' : '密码'}>
{({id, field}) =>
loginMode === 'sms' ? (
<div className="flex space-x-4">
<Input
{...field}
id={id}
className="h-12"
placeholder="请输入验证码"
/>
<Button
type="button"
theme="outline"
className="whitespace-nowrap h-12"
onClick={checkUsername}
disabled={countdown > 0}
>
{countdown > 0 ? `${countdown}秒后重发` : '获取验证码'}
</Button>
</div>
) : (
<div className="relative">
<Input
{...field}
id={id}
type={showPwd ? 'text' : 'password'}
className="h-12 pr-10"
placeholder="至少6位密码需包含字母和数字"
autoComplete="current-password"
minLength={6}
/>
<button
type="button"
tabIndex={-1}
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
onClick={() => setShowPwd(v => !v)}
aria-label={showPwd ? '隐藏密码' : '显示密码'}
>
{showPwd ? (
// 眼睛开 icon
<svg width="20" height="20" fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" strokeWidth="1.5" d="M1.667 10S4.167 4.167 10 4.167 18.333 10 18.333 10 15.833 15.833 10 15.833 1.667 10 1.667 10Z"/>
<circle cx="10" cy="10" r="2.5" stroke="currentColor" strokeWidth="1.5"/>
</svg>
) : (
// 眼睛关 icon
<>
<svg width="20" height="20" fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" strokeWidth="1.5" d="M1.667 10S4.167 4.167 10 4.167c1.57 0 2.97.33 4.13.87M18.333 10s-2.5 5.833-8.333 5.833c-1.57 0-2.97-.33-4.13-.87"/>
<circle cx="10" cy="10" r="2.5" stroke="currentColor" strokeWidth="1.5"/>
<path stroke="currentColor" strokeWidth="1.5" d="M3 3l14 14"/>
</svg>
</>
)}
</button>
</div>
)
}
</FormField>
<FormField name="remember">
{({id, field}) => (
<div className="flex flex-row items-start space-x-2 space-y-0">
@@ -268,7 +351,6 @@ export default function LoginPage(props: LoginPageProps) {
</div>
)}
</FormField>
<div className="flex flex-col gap-3">
<Button
className="w-full h-12 text-lg"
@@ -276,9 +358,8 @@ export default function LoginPage(props: LoginPageProps) {
theme="gradient"
disabled={submitting}
>
{submitting ? '登录中...' : '注册 / 登录'}
{submitting ? '登录中...' : (loginMode === 'sms' ? '注册 / 登录' : '密码登录')}
</Button>
<p className="text-xs text-center text-gray-500">
<a href="#" className="text-blue-600 hover:text-blue-500"></a>
@@ -289,14 +370,14 @@ export default function LoginPage(props: LoginPageProps) {
</Form>
</CardContent>
</Card>
{/* 图形验证码弹窗 */}
<Captcha
showCaptcha={showCaptcha}
setShowCaptcha={setShowCaptcha}
handleSendCode={sendCode}
/>
{loginMode === 'sms' && (
<Captcha
showCaptcha={showCaptcha}
setShowCaptcha={setShowCaptcha}
handleSendCode={sendCode}
/>
)}
</main>
)
}

View File

@@ -50,7 +50,7 @@ export default function Footer(props: FooterProps) {
<SiteNavList
title="使用案例"
items={[
{name: `数据抓取`, href: `#`},
{name: `数据抓取`, href: `/data-capture`},
{name: `媒体矩阵`, href: `#`},
{name: `广告验证`, href: `#`},
{name: `价格监控`, href: `#`},

View File

@@ -12,7 +12,7 @@ import zod from 'zod'
import {merge} from '@/lib/utils'
import {Button} from '@/components/ui/button'
import {useState} from 'react'
import {listAccount} from '@/actions/dashboard'
import {statisticsResourceUsage} from '@/actions/dashboard'
import {ExtraResp} from '@/lib/api'
import {toast} from 'sonner'
import {addDays, format} from 'date-fns'
@@ -20,19 +20,13 @@ import {Label} from '@/components/ui/label'
import {ChartConfig, ChartContainer} from '@/components/ui/chart'
import {CartesianGrid, XAxis, YAxis, Tooltip, Area, AreaChart, Legend} from 'recharts'
type ChartDataItem = {
date: string
count: number
count2?: number
}
type ChartsProps = {
initialData?: ExtraResp<typeof listAccount>
initialData?: ExtraResp<typeof statisticsResourceUsage>
}
export default function Charts({initialData}: ChartsProps) {
// const [submittedData, setSubmittedData] = useState<ExtraReq<typeof listAccount>>()
const [submittedData, setSubmittedData] = useState<ExtraResp<typeof listAccount>>(initialData || [])
const [submittedData, setSubmittedData] = useState<ExtraResp<typeof statisticsResourceUsage>>(initialData || [])
const formSchema = zod.object({
resource_no: zod.string().optional(),
create_after: zod.date().optional(),
@@ -58,7 +52,7 @@ export default function Charts({initialData}: ChartsProps) {
create_before: value.create_before ?? today,
}
const resp = await listAccount(res)
const resp = await statisticsResourceUsage(res)
if (!resp.success) {
toast.error('接口请求失败:' + resp.message)
return
@@ -149,7 +143,7 @@ const config = {
} satisfies ChartConfig
type DashboardChartProps = {
data: ExtraResp<typeof listAccount>
data: ExtraResp<typeof statisticsResourceUsage>
}
function DashboardChart(props: DashboardChartProps) {

View File

@@ -9,7 +9,7 @@ import Charts from './_client/charts'
import UserCenter from './_client/userCenter'
import soon from './_assets/coming-soon.svg'
import mask from './_assets/Mask group.webp'
import {Button} from '@/components/ui/button'
import {ExtraResp} from '@/lib/api'
export type DashboardPageProps = {}
@@ -43,12 +43,7 @@ export default async function DashboardPage(props: DashboardPageProps) {
{/* 磁贴集 */}
{initData && (
<Pins
short_term={String(initData.free.short.ResourceCount)}
short_term_monthly={String(initData.free.short.ResourceQuotaSum)}
long_term={String(initData.free.long.ResourceCount)}
long_term_monthly={String(initData.free.long.ResourceDailyFreeSum)}
/>
<Pins {...initData.free}/>
)}
{/* 图表 */}
@@ -69,13 +64,9 @@ export default async function DashboardPage(props: DashboardPageProps) {
</Page>
)
}
type DashboardChartProps = {
short_term: string
short_term_monthly: string
long_term: string
long_term_monthly: string
}
function Pins(props: DashboardChartProps) {
type DashboardPinsProps = ExtraResp<typeof listInitialization>['free']
function Pins(props: DashboardPinsProps) {
return (
<div className="flex md:row-start-2 md:col-start-1 md:col-span-3 gap-4 max-md:flex-col">
{/* 短效 */}
@@ -91,7 +82,7 @@ function Pins(props: DashboardChartProps) {
<h4></h4>
<p className="flex flex-col items-end">
<span className="text-sm text-weak"></span>
<span className="text-sm">{props.short_term}</span>
<span className="text-sm">{props.short.resource_daily_free_sum}</span>
</p>
</div>
<div className="border-b"></div>
@@ -99,7 +90,7 @@ function Pins(props: DashboardChartProps) {
<h4 className="text-balance"></h4>
<p className="flex flex-col items-end">
<span className="text-sm text-weak"></span>
<span className="text-sm">{props.short_term_monthly}</span>
<span className="text-sm">{props.short.resource_quota_sum}</span>
</p>
</div>
</CardContent>
@@ -118,7 +109,7 @@ function Pins(props: DashboardChartProps) {
<h4 className="text-balance"></h4>
<p className="flex flex-col items-end">
<span className="text-sm text-weak" ></span>
<span className="text-sm">{props.long_term}</span>
<span className="text-sm">{props.long.resource_daily_free_sum}</span>
</p>
</div>
<div className="border-b"></div>
@@ -126,7 +117,7 @@ function Pins(props: DashboardChartProps) {
<h4 className="text-balance"></h4>
<p className="flex flex-col items-end">
<span className="text-sm text-weak"></span>
<span className="text-sm">{props.long_term_monthly}</span>
<span className="text-sm">{props.long.resource_quota_sum}</span>
</p>
</div>
</CardContent>

View File

@@ -324,7 +324,16 @@ function PasswordForm(props: {
type Schema = z.infer<typeof schema>
const form = useForm<Schema>({
resolver: zodResolver(schema),
resolver: zodResolver(
schema.refine(
data =>
/^(?=.*[a-z])(?=.*[A-Z]).{6,}$/.test(data.password),
{
message: '密码需包含大小写字母且不少于6位',
path: ['password'],
},
),
),
defaultValues: {
phone: '',
captcha: '',
@@ -333,6 +342,7 @@ function PasswordForm(props: {
confirm_password: '',
},
})
const router = useRouter()
const handler = form.handleSubmit(async (value) => {
try {
const resp = await updatePassword({
@@ -344,8 +354,10 @@ function PasswordForm(props: {
throw new Error(resp.message)
}
toast.success(`保存成功`)
toast.success(`保存成功,请重新登录`)
setOpen(false)
// 立即跳转到登录页
router.replace('/login')
}
catch (e) {
console.error(e)

View File

@@ -230,14 +230,14 @@ export default function WhitelistPage(props: WhitelistPageProps) {
<Plus/>
</Button>
<Button
{/* <Button
theme="fail"
className="ml-2"
disabled={selection.size === 0 || wait}
onClick={() => confirmRemove()}>
<Trash2/>
删除选中
</Button>
</Button> */}
</section>
{/* 数据表 */}

View File

@@ -276,7 +276,11 @@ const FormFields = memo(() => {
{...field}
id={id}
type="number"
onChange={e => field.onChange(Number(e.target.value))}
min={1}
onChange={(e) => {
const value = Math.max(1, Number(e.target.value))
field.onChange(value)
}}
className="h-10"
placeholder="输入提取数量"
/>