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

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 username: string
password: string password: string
remember: boolean remember: boolean
mode: 'phone_code' | 'password'
}): Promise<ApiResponse> { }): Promise<ApiResponse> {
// 尝试登录 // 尝试登录
const result = await callByDevice<TokenResp>('/api/auth/token', { const result = await callByDevice<TokenResp>('/api/auth/token', {
...props, ...props,
grant_type: 'password', grant_type: 'password',
login_type: 'phone_code', login_type: props.mode,
}) })
if (!result.success) { if (!result.success) {

View File

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

View File

@@ -1,5 +1,5 @@
'use client' 'use client'
import {useState, useCallback, useRef, useContext} from 'react' import {useState, useCallback, useRef} from 'react'
import {Input} from '@/components/ui/input' import {Input} from '@/components/ui/input'
import {Button} from '@/components/ui/button' import {Button} from '@/components/ui/button'
import {Checkbox} from '@/components/ui/checkbox' import {Checkbox} from '@/components/ui/checkbox'
@@ -32,24 +32,31 @@ import Link from 'next/link'
export type LoginPageProps = {} export type LoginPageProps = {}
// 定义表单验证模式 const smsSchema = zod.object({
const formSchema = zod.object({
username: zod.string().min(11, '请输入正确的手机号码').max(11, '请输入正确的手机号码'), username: zod.string().min(11, '请输入正确的手机号码').max(11, '请输入正确的手机号码'),
password: zod.string().min(1, '请输入验证码'), password: zod.string().min(1, '请输入验证码'),
remember: zod.boolean().default(false), 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) { export default function LoginPage(props: LoginPageProps) {
const router = useRouter() const router = useRouter()
const [submitting, setSubmitting] = useState(false) const [submitting, setSubmitting] = useState(false)
const [countdown, setCountdown] = useState(0) const [countdown, setCountdown] = useState(0)
const [showCaptcha, setShowCaptcha] = useState(false) const [showCaptcha, setShowCaptcha] = useState(false)
const [loginMode, setLoginMode] = useState<'sms' | 'password'>('sms')
const [showPwd, setShowPwd] = useState(false)
const timerRef = useRef<NodeJS.Timeout>(undefined) const timerRef = useRef<NodeJS.Timeout>(undefined)
const form = useForm<FormValues>({ const form = useForm<FormValues>({
resolver: zodResolver(formSchema), resolver: zodResolver(loginMode === 'sms' ? smsSchema : pwdSchema),
defaultValues: { defaultValues: {
username: '', username: '',
password: '', password: '',
@@ -103,7 +110,6 @@ export default function LoginPage(props: LoginPageProps) {
toast.error(resp.message) toast.error(resp.message)
return true return true
} }
setShowCaptcha(false) setShowCaptcha(false)
waiting = parseInt(resp.message) waiting = parseInt(resp.message)
console.log(resp.message) console.log(resp.message)
@@ -132,12 +138,30 @@ export default function LoginPage(props: LoginPageProps) {
return prev - 1 return prev - 1
}) })
}, 1000) }, 1000)
return false return false
}, [username]) }, [username])
// 处理表单提交 // 处理表单提交
const onSubmit = async (values: FormValues) => { 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 { try {
setSubmitting(true) setSubmitting(true)
@@ -153,7 +177,7 @@ export default function LoginPage(props: LoginPageProps) {
if (!values.password) { if (!values.password) {
form.setError('password', { form.setError('password', {
type: 'manual', type: 'manual',
message: '请输入验证码', message: loginMode === 'sms' ? '请输入验证码' : '请输入密码',
}) })
return return
} }
@@ -163,11 +187,12 @@ export default function LoginPage(props: LoginPageProps) {
username: values.username, username: values.username,
password: values.password, password: values.password,
remember: values.remember, remember: values.remember,
mode: loginMode === 'sms' ? 'phone_code' : 'password', // 后端区分登录方式
}) })
// 登录失败 // 登录失败
if (!result.success) { 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 params = useSearchParams()
const redirect = params.get('redirect') const redirect = params.get('redirect')
const refreshProfile = useProfileStore(store => store.refreshProfile) const refreshProfile = useProfileStore(store => store.refreshProfile)
// ====================== // ======================
@@ -207,7 +231,6 @@ export default function LoginPage(props: LoginPageProps) {
`flex justify-center xl:justify-end items-center`, `flex justify-center xl:justify-end items-center`,
)}> )}>
<Image src={bg} alt="背景图" fill priority className="absolute -z-20 object-cover"/> <Image src={bg} alt="背景图" fill priority className="absolute -z-20 object-cover"/>
<Link href="/"> <Link href="/">
<Image src={logo} alt="logo" priority height={64} className="absolute top-8 left-8 -z-10"/> <Image src={logo} alt="logo" priority height={64} className="absolute top-8 left-8 -z-10"/>
</Link> </Link>
@@ -217,8 +240,32 @@ export default function LoginPage(props: LoginPageProps) {
<CardHeader className="text-center"> <CardHeader className="text-center">
<CardTitle className="text-2xl">/</CardTitle> <CardTitle className="text-2xl">/</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="px-8"> <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}> <Form<FormValues> className="space-y-6" onSubmit={onSubmit} form={form}>
<FormField name="username" label="手机号码"> <FormField name="username" label="手机号码">
{({id, field}) => ( {({id, field}) => (
@@ -231,9 +278,9 @@ export default function LoginPage(props: LoginPageProps) {
/> />
)} )}
</FormField> </FormField>
<FormField name="password" label={loginMode === 'sms' ? '验证码' : '密码'}>
<FormField name="password" label="验证码"> {({id, field}) =>
{({id, field}) => ( loginMode === 'sms' ? (
<div className="flex space-x-4"> <div className="flex space-x-4">
<Input <Input
{...field} {...field}
@@ -251,9 +298,45 @@ export default function LoginPage(props: LoginPageProps) {
{countdown > 0 ? `${countdown}秒后重发` : '获取验证码'} {countdown > 0 ? `${countdown}秒后重发` : '获取验证码'}
</Button> </Button>
</div> </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>
<FormField name="remember"> <FormField name="remember">
{({id, field}) => ( {({id, field}) => (
<div className="flex flex-row items-start space-x-2 space-y-0"> <div className="flex flex-row items-start space-x-2 space-y-0">
@@ -268,7 +351,6 @@ export default function LoginPage(props: LoginPageProps) {
</div> </div>
)} )}
</FormField> </FormField>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<Button <Button
className="w-full h-12 text-lg" className="w-full h-12 text-lg"
@@ -276,9 +358,8 @@ export default function LoginPage(props: LoginPageProps) {
theme="gradient" theme="gradient"
disabled={submitting} disabled={submitting}
> >
{submitting ? '登录中...' : '注册 / 登录'} {submitting ? '登录中...' : (loginMode === 'sms' ? '注册 / 登录' : '密码登录')}
</Button> </Button>
<p className="text-xs text-center text-gray-500"> <p className="text-xs text-center text-gray-500">
<a href="#" className="text-blue-600 hover:text-blue-500"></a> <a href="#" className="text-blue-600 hover:text-blue-500"></a>
@@ -289,14 +370,14 @@ export default function LoginPage(props: LoginPageProps) {
</Form> </Form>
</CardContent> </CardContent>
</Card> </Card>
{/* 图形验证码弹窗 */} {/* 图形验证码弹窗 */}
{loginMode === 'sms' && (
<Captcha <Captcha
showCaptcha={showCaptcha} showCaptcha={showCaptcha}
setShowCaptcha={setShowCaptcha} setShowCaptcha={setShowCaptcha}
handleSendCode={sendCode} handleSendCode={sendCode}
/> />
)}
</main> </main>
) )
} }

View File

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

View File

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

View File

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

View File

@@ -324,7 +324,16 @@ function PasswordForm(props: {
type Schema = z.infer<typeof schema> type Schema = z.infer<typeof schema>
const form = useForm<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: { defaultValues: {
phone: '', phone: '',
captcha: '', captcha: '',
@@ -333,6 +342,7 @@ function PasswordForm(props: {
confirm_password: '', confirm_password: '',
}, },
}) })
const router = useRouter()
const handler = form.handleSubmit(async (value) => { const handler = form.handleSubmit(async (value) => {
try { try {
const resp = await updatePassword({ const resp = await updatePassword({
@@ -344,8 +354,10 @@ function PasswordForm(props: {
throw new Error(resp.message) throw new Error(resp.message)
} }
toast.success(`保存成功`) toast.success(`保存成功,请重新登录`)
setOpen(false) setOpen(false)
// 立即跳转到登录页
router.replace('/login')
} }
catch (e) { catch (e) {
console.error(e) console.error(e)

View File

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

View File

@@ -276,7 +276,11 @@ const FormFields = memo(() => {
{...field} {...field}
id={id} id={id}
type="number" 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" className="h-10"
placeholder="输入提取数量" placeholder="输入提取数量"
/> />