feat(frontend): 完成品牌 SVG 与后台 Phase 1 基础切片
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
import { useState } from 'react'
|
||||
import { Alert, App, Button, Form, Input, Typography } from 'antd'
|
||||
import { LockOutlined } from '@ant-design/icons'
|
||||
import { Navigate, useNavigate } from 'react-router-dom'
|
||||
|
||||
import { BrandLogo } from '@/components/BrandLogo'
|
||||
import { getAdminBasePath, getAdminLoginPath } from '@/shared/api/adminClient'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
|
||||
interface ChangePasswordForm {
|
||||
old_password: string
|
||||
new_password: string
|
||||
confirm_password: string
|
||||
}
|
||||
|
||||
export function AdminChangePasswordPage() {
|
||||
const { message } = App.useApp()
|
||||
const navigate = useNavigate()
|
||||
const admin = useAdminSessionStore(state => state.admin)
|
||||
const logout = useAdminSessionStore(state => state.logout)
|
||||
const changePassword = useAdminSessionStore(state => state.changePassword)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const basePath = getAdminBasePath()
|
||||
|
||||
if (!admin) return <Navigate to={getAdminLoginPath()} replace />
|
||||
if (!admin.password_must_change) return <Navigate to={basePath} replace />
|
||||
|
||||
const onFinish = async (values: ChangePasswordForm) => {
|
||||
setSubmitting(true)
|
||||
setError('')
|
||||
try {
|
||||
await changePassword(values.old_password, values.new_password)
|
||||
message.success('初始密码已更新')
|
||||
navigate(basePath, { replace: true })
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '密码没有更新,请检查原密码')
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="admin-login-page">
|
||||
<section className="admin-login-brand">
|
||||
<div className="admin-login-brand__inner">
|
||||
<BrandLogo height={56} variant="dark" />
|
||||
<div className="admin-login-brand__eyebrow">FIRST RUN · SECURITY</div>
|
||||
<Typography.Title className="brand-title admin-login-brand__title">
|
||||
先把钥匙
|
||||
<br />
|
||||
换掉。
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph className="admin-login-brand__copy">
|
||||
初始密码只用于第一次进入。修改完成后,这个后台会话才能打开订单、财务和分销操作。
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="admin-login-form-panel">
|
||||
<div className="admin-login-card">
|
||||
<div className="admin-login-card__head">
|
||||
<BrandLogo height={34} />
|
||||
<Typography.Title level={2} className="brand-title">
|
||||
修改初始密码
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
当前账号:{admin.nickname || admin.username}({admin.username})
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
{error ? <Alert type="error" showIcon message={error} className="login-alert" /> : null}
|
||||
|
||||
<Form<ChangePasswordForm>
|
||||
layout="vertical"
|
||||
size="large"
|
||||
requiredMark={false}
|
||||
onFinish={onFinish}
|
||||
>
|
||||
<Form.Item
|
||||
label="原密码"
|
||||
name="old_password"
|
||||
rules={[{ required: true, message: '请输入原密码' }]}
|
||||
>
|
||||
<Input.Password prefix={<LockOutlined />} placeholder="请输入原密码" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="新密码"
|
||||
name="new_password"
|
||||
rules={[
|
||||
{ required: true, message: '请输入新密码' },
|
||||
{ min: 8, max: 20, message: '密码长度需为 8–20 位' },
|
||||
]}
|
||||
>
|
||||
<Input.Password prefix={<LockOutlined />} placeholder="8–20 位新密码" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="确认新密码"
|
||||
name="confirm_password"
|
||||
dependencies={['new_password']}
|
||||
rules={[
|
||||
{ required: true, message: '请再次输入新密码' },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('new_password') === value) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return Promise.reject(new Error('两次输入的新密码不一致'))
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input.Password prefix={<LockOutlined />} placeholder="再次输入新密码" />
|
||||
</Form.Item>
|
||||
|
||||
<Button type="primary" htmlType="submit" block loading={submitting}>
|
||||
更新密码并进入后台
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
<div className="admin-login-card__foot">
|
||||
<span>改密后旧会话会按后端规则刷新</span>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={async () => {
|
||||
await logout()
|
||||
navigate(getAdminLoginPath(), { replace: true })
|
||||
}}
|
||||
>
|
||||
退出登录
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Empty,
|
||||
Row,
|
||||
Skeleton,
|
||||
Statistic,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
type TableColumnsType,
|
||||
} from 'antd'
|
||||
import {
|
||||
ArrowRightOutlined,
|
||||
ClockCircleOutlined,
|
||||
FileSearchOutlined,
|
||||
ShoppingCartOutlined,
|
||||
TeamOutlined,
|
||||
WalletOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import dayjs from 'dayjs'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import { fetchAdminDashboard } from '@/features/admin/api'
|
||||
import type { RecentOrder } from '@/features/admin/types'
|
||||
|
||||
const statusMap: Record<string, { label: string; color: string }> = {
|
||||
pending_payment: { label: '待支付', color: 'gold' },
|
||||
pending_handoff: { label: '待交接', color: 'blue' },
|
||||
renting: { label: '租赁中', color: 'cyan' },
|
||||
overdue: { label: '已逾期', color: 'red' },
|
||||
pending_checkout_confirm: { label: '待确认结账', color: 'orange' },
|
||||
pending_checkout_accept: { label: '待接受结账', color: 'orange' },
|
||||
checkout_disputing: { label: '结账争议', color: 'red' },
|
||||
completed: { label: '已完成', color: 'green' },
|
||||
cancelled: { label: '已取消', color: 'default' },
|
||||
closed: { label: '已关闭', color: 'default' },
|
||||
abnormal: { label: '异常', color: 'red' },
|
||||
}
|
||||
|
||||
function money(cents: number) {
|
||||
return `¥${(cents / 100).toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})}`
|
||||
}
|
||||
|
||||
const orderColumns: TableColumnsType<RecentOrder> = [
|
||||
{
|
||||
title: '订单号',
|
||||
dataIndex: 'order_no',
|
||||
width: 150,
|
||||
render: value => <span className="admin-mono">{value}</span>,
|
||||
},
|
||||
{
|
||||
title: '商品',
|
||||
dataIndex: 'title',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 110,
|
||||
render: value => {
|
||||
const item = statusMap[value] || { label: value, color: 'default' }
|
||||
return <Tag color={item.color}>{item.label}</Tag>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '租金',
|
||||
dataIndex: 'rent_amount_cent',
|
||||
width: 120,
|
||||
align: 'right',
|
||||
render: value => <span className="admin-money">{money(value)}</span>,
|
||||
},
|
||||
{
|
||||
title: '押金',
|
||||
dataIndex: 'deposit_amount_cent',
|
||||
width: 120,
|
||||
align: 'right',
|
||||
render: value => <span className="admin-money">{money(value)}</span>,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 170,
|
||||
render: value => dayjs(value).format('YYYY-MM-DD HH:mm'),
|
||||
},
|
||||
]
|
||||
|
||||
export function AdminDashboardPage() {
|
||||
const navigate = useNavigate()
|
||||
const { data, isPending, isError, refetch } = useQuery({
|
||||
queryKey: ['admin-dashboard'],
|
||||
queryFn: fetchAdminDashboard,
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message="仪表盘没有加载出来"
|
||||
description="后台会话可能已失效,或者仪表盘服务暂时不可用。"
|
||||
action={
|
||||
<Button size="small" onClick={() => void refetch()}>
|
||||
重试
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<div className="admin-page__head">
|
||||
<div>
|
||||
<Typography.Text className="admin-page__eyebrow">TODAY AT JIEYOU</Typography.Text>
|
||||
<Typography.Title level={2} className="brand-title">
|
||||
工作台
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
先看今天要处理的事,再看整盘账。数据更新于{' '}
|
||||
{data?.generated_at ? dayjs(data.generated_at).format('HH:mm:ss') : '加载中'}。
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Button type="primary" onClick={() => navigate('orders')}>
|
||||
去处理订单 <ArrowRightOutlined />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isPending ? (
|
||||
<Row gutter={[16, 16]}>
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<Col xs={24} sm={12} xl={6} key={index}>
|
||||
<Card className="admin-kpi-card">
|
||||
<Skeleton active paragraph={{ rows: 1 }} />
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
) : (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} xl={6}>
|
||||
<Card className="admin-kpi-card">
|
||||
<div className="admin-kpi-card__icon">
|
||||
<ShoppingCartOutlined />
|
||||
</div>
|
||||
<Statistic title="今日订单" value={data?.metrics.today_orders ?? 0} />
|
||||
<span className="admin-kpi-card__foot">
|
||||
总订单 {data?.metrics.total_orders ?? 0}
|
||||
</span>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} xl={6}>
|
||||
<Card className="admin-kpi-card">
|
||||
<div className="admin-kpi-card__icon">
|
||||
<TeamOutlined />
|
||||
</div>
|
||||
<Statistic title="用户总数" value={data?.metrics.total_users ?? 0} />
|
||||
<span className="admin-kpi-card__foot">
|
||||
已实名 {data?.metrics.verified_users ?? 0}
|
||||
</span>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} xl={6}>
|
||||
<Card className="admin-kpi-card">
|
||||
<div className="admin-kpi-card__icon">
|
||||
<FileSearchOutlined />
|
||||
</div>
|
||||
<Statistic title="在架商品" value={data?.metrics.published_listings ?? 0} />
|
||||
<span className="admin-kpi-card__foot">
|
||||
商品总数 {data?.metrics.total_listings ?? 0}
|
||||
</span>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} xl={6}>
|
||||
<Card className="admin-kpi-card admin-kpi-card--accent">
|
||||
<div className="admin-kpi-card__icon">
|
||||
<WalletOutlined />
|
||||
</div>
|
||||
<Statistic
|
||||
title="今日入账"
|
||||
value={money(data?.metrics.today_ledger_amount_cent ?? 0)}
|
||||
/>
|
||||
<span className="admin-kpi-card__foot">
|
||||
在租订单 {data?.metrics.renting_orders ?? 0}
|
||||
</span>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<div className="admin-section-title">
|
||||
<Typography.Title level={4}>待办</Typography.Title>
|
||||
<Typography.Text type="secondary">客服和运营每天先处理这四类</Typography.Text>
|
||||
</div>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
{[
|
||||
['商品待审核', data?.pending.listing_reviews ?? 0, '去审核', 'listings/review'],
|
||||
['待交接', data?.pending.pending_handoffs ?? 0, '去查看', 'orders'],
|
||||
['待确认归还', data?.pending.pending_return_confirms ?? 0, '去查看', 'orders'],
|
||||
['进行中申诉', data?.pending.disputes ?? 0, '去处理', 'disputes'],
|
||||
].map(([label, count, action, path]) => (
|
||||
<Col xs={24} sm={12} xl={6} key={String(label)}>
|
||||
<Card className="admin-pending-card">
|
||||
<div>
|
||||
<span className="admin-pending-card__label">{label}</span>
|
||||
<strong>{count}</strong>
|
||||
</div>
|
||||
<Button type="link" onClick={() => navigate(String(path))}>
|
||||
{action} <ArrowRightOutlined />
|
||||
</Button>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
<Card
|
||||
className="admin-table-card"
|
||||
title={
|
||||
<span>
|
||||
<ClockCircleOutlined /> 最近订单
|
||||
</span>
|
||||
}
|
||||
extra={
|
||||
<Button type="link" onClick={() => navigate('orders')}>
|
||||
查看全部
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Table<RecentOrder>
|
||||
rowKey="id"
|
||||
size="small"
|
||||
columns={orderColumns}
|
||||
dataSource={data?.recent_orders ?? []}
|
||||
pagination={false}
|
||||
locale={{
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="还没有订单" />,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useEffect } from 'react'
|
||||
import { Navigate, Outlet, useLocation } from 'react-router-dom'
|
||||
|
||||
import { PageLoading } from '@/components/PageLoading'
|
||||
import { getAdminBasePath, getAdminLoginPath } from '@/shared/api/adminClient'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
|
||||
export function AdminGuard() {
|
||||
const location = useLocation()
|
||||
const admin = useAdminSessionStore(state => state.admin)
|
||||
const loaded = useAdminSessionStore(state => state.loaded)
|
||||
const loading = useAdminSessionStore(state => state.loading)
|
||||
const loadMe = useAdminSessionStore(state => state.loadMe)
|
||||
|
||||
useEffect(() => {
|
||||
if (!loaded && !loading) {
|
||||
void loadMe()
|
||||
}
|
||||
}, [loaded, loading, loadMe])
|
||||
|
||||
if (!loaded || loading) {
|
||||
return <PageLoading tip="正在核对后台会话…" />
|
||||
}
|
||||
|
||||
if (!admin) {
|
||||
const redirect = encodeURIComponent(location.pathname + location.search)
|
||||
return <Navigate to={`${getAdminLoginPath()}?redirect=${redirect}`} replace />
|
||||
}
|
||||
|
||||
const changePasswordPath = `${getAdminBasePath()}/change-password`
|
||||
if (admin.password_must_change && location.pathname !== changePasswordPath) {
|
||||
return <Navigate to={changePasswordPath} replace />
|
||||
}
|
||||
|
||||
return <Outlet />
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Alert, App, Button, Form, Input, Space, Typography } from 'antd'
|
||||
import { LockOutlined, ReloadOutlined, SafetyCertificateOutlined, UserOutlined } from '@ant-design/icons'
|
||||
import { Navigate, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
|
||||
import { BrandLogo } from '@/components/BrandLogo'
|
||||
import { fetchAdminCaptcha } from '@/features/admin/api'
|
||||
import type { AdminCaptcha } from '@/features/admin/types'
|
||||
import { getAdminBasePath } from '@/shared/api/adminClient'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
|
||||
interface AdminLoginForm {
|
||||
username: string
|
||||
password: string
|
||||
captcha_code: string
|
||||
}
|
||||
|
||||
export function AdminLoginPage() {
|
||||
const { message } = App.useApp()
|
||||
const navigate = useNavigate()
|
||||
const [searchParams] = useSearchParams()
|
||||
const admin = useAdminSessionStore(state => state.admin)
|
||||
const loading = useAdminSessionStore(state => state.loading)
|
||||
const login = useAdminSessionStore(state => state.login)
|
||||
const [form] = Form.useForm<AdminLoginForm>()
|
||||
const [captcha, setCaptcha] = useState<AdminCaptcha | null>(null)
|
||||
const [captchaLoading, setCaptchaLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const basePath = getAdminBasePath()
|
||||
|
||||
const refreshCaptcha = async () => {
|
||||
setCaptchaLoading(true)
|
||||
try {
|
||||
setCaptcha(await fetchAdminCaptcha())
|
||||
form.setFieldValue('captcha_code', '')
|
||||
} catch {
|
||||
setError('图形验证码没有加载出来,请检查后端服务')
|
||||
} finally {
|
||||
setCaptchaLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void refreshCaptcha()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
if (admin) return <Navigate to={basePath} replace />
|
||||
|
||||
const onFinish = async (values: AdminLoginForm) => {
|
||||
if (!captcha) {
|
||||
setError('图形验证码还没有准备好,请刷新后重试')
|
||||
return
|
||||
}
|
||||
setError('')
|
||||
try {
|
||||
await login(values.username.trim(), values.password, captcha.captcha_id, values.captcha_code)
|
||||
message.success('已进入解忧商行后台')
|
||||
const current = useAdminSessionStore.getState().admin
|
||||
if (current?.password_must_change) {
|
||||
navigate(`${basePath}/change-password`, { replace: true })
|
||||
return
|
||||
}
|
||||
const redirect = searchParams.get('redirect')
|
||||
navigate(redirect && redirect.startsWith(basePath) ? redirect : basePath, { replace: true })
|
||||
} catch (err) {
|
||||
const text = err instanceof Error ? err.message : '登录没有完成,请核对账号、密码和验证码'
|
||||
setError(text)
|
||||
void refreshCaptcha()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="admin-login-page">
|
||||
<section className="admin-login-brand">
|
||||
<div className="admin-login-brand__inner">
|
||||
<BrandLogo height={56} variant="dark" />
|
||||
<div className="admin-login-brand__eyebrow">JIEYOU · OPERATIONS DESK</div>
|
||||
<Typography.Title className="brand-title admin-login-brand__title">
|
||||
同一本账,
|
||||
<br />
|
||||
另一面。
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph className="admin-login-brand__copy">
|
||||
订单、商品、资金、分销和客服会话都在这里。后台不是另一套系统,而是商行账本的操作台。
|
||||
</Typography.Paragraph>
|
||||
<div className="admin-login-brand__foot">
|
||||
<SafetyCertificateOutlined />
|
||||
独立认证 · 权限分级 · 高风险操作留痕
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="admin-login-form-panel">
|
||||
<div className="admin-login-card">
|
||||
<div className="admin-login-card__head">
|
||||
<BrandLogo height={34} />
|
||||
<Typography.Title level={2} className="brand-title">
|
||||
后台登录
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">使用管理员账号和图形验证码进入</Typography.Text>
|
||||
</div>
|
||||
|
||||
{error ? <Alert type="error" showIcon message={error} className="login-alert" /> : null}
|
||||
|
||||
<Form<AdminLoginForm>
|
||||
form={form}
|
||||
layout="vertical"
|
||||
size="large"
|
||||
requiredMark={false}
|
||||
onFinish={onFinish}
|
||||
>
|
||||
<Form.Item
|
||||
label="管理员账号"
|
||||
name="username"
|
||||
rules={[{ required: true, message: '请输入管理员账号' }]}
|
||||
>
|
||||
<Input prefix={<UserOutlined />} placeholder="请输入管理员账号" autoComplete="username" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="密码"
|
||||
name="password"
|
||||
rules={[{ required: true, message: '请输入密码' }]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
placeholder="请输入密码"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="图形验证码" required>
|
||||
<Space.Compact block>
|
||||
<Form.Item
|
||||
name="captcha_code"
|
||||
noStyle
|
||||
rules={[{ required: true, message: '请输入图形验证码' }]}
|
||||
>
|
||||
<Input placeholder="请输入验证码" maxLength={6} />
|
||||
</Form.Item>
|
||||
<Button
|
||||
className="admin-captcha-button"
|
||||
onClick={() => void refreshCaptcha()}
|
||||
loading={captchaLoading}
|
||||
icon={captcha ? undefined : <ReloadOutlined />}
|
||||
>
|
||||
{captcha ? <img src={captcha.image} alt="图形验证码" /> : '刷新'}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
|
||||
<Button type="primary" htmlType="submit" block loading={loading}>
|
||||
进入后台
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
{import.meta.env.DEV ? (
|
||||
<div className="login-dev-hint">开发管理员:admin / JieyouAdmin@2026</div>
|
||||
) : null}
|
||||
|
||||
<div className="admin-login-card__foot">
|
||||
<span>解忧商行 · 运营后台</span>
|
||||
<Button type="link" onClick={() => navigate('/')}>
|
||||
返回商行首页
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { adminClient } from '@/shared/api/adminClient'
|
||||
import type { ApiResponse } from '@/shared/api/types'
|
||||
import type {
|
||||
AdminCaptcha,
|
||||
AdminLoginData,
|
||||
AdminUser,
|
||||
DashboardData,
|
||||
} from '@/features/admin/types'
|
||||
|
||||
export async function fetchAdminCaptcha() {
|
||||
const { data } = await adminClient.get<ApiResponse<AdminCaptcha>>('/admin/auth/captcha')
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function loginAdmin(
|
||||
username: string,
|
||||
password: string,
|
||||
captchaId: string,
|
||||
captchaCode: string
|
||||
) {
|
||||
const { data } = await adminClient.post<ApiResponse<AdminLoginData>>('/admin/auth/login', {
|
||||
username,
|
||||
password,
|
||||
captcha_id: captchaId,
|
||||
captcha_code: captchaCode,
|
||||
})
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function fetchAdminMe() {
|
||||
const { data } = await adminClient.get<ApiResponse<AdminUser>>('/admin/me')
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function logoutAdmin() {
|
||||
await adminClient.post('/admin/auth/logout')
|
||||
}
|
||||
|
||||
export async function changeAdminPassword(oldPassword: string, newPassword: string) {
|
||||
await adminClient.put('/admin/admin-users/me/password', {
|
||||
old_password: oldPassword,
|
||||
new_password: newPassword,
|
||||
})
|
||||
}
|
||||
|
||||
export async function fetchAdminDashboard() {
|
||||
const { data } = await adminClient.get<ApiResponse<DashboardData>>('/admin/dashboard')
|
||||
return data.data
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
export interface AdminRole {
|
||||
id: number
|
||||
code: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface AdminUser {
|
||||
id: number
|
||||
username: string
|
||||
nickname: string
|
||||
status: string
|
||||
support_status: 'online' | 'offline' | 'busy'
|
||||
password_must_change: boolean
|
||||
roles: AdminRole[]
|
||||
permissions: string[]
|
||||
last_login_at?: string
|
||||
}
|
||||
|
||||
export interface AdminCaptcha {
|
||||
captcha_id: string
|
||||
image: string
|
||||
expires_in: number
|
||||
}
|
||||
|
||||
export interface AdminLoginData {
|
||||
admin: AdminUser
|
||||
}
|
||||
|
||||
export interface DashboardMetrics {
|
||||
total_users: number
|
||||
verified_users: number
|
||||
total_listings: number
|
||||
published_listings: number
|
||||
total_orders: number
|
||||
renting_orders: number
|
||||
today_orders: number
|
||||
today_ledger_amount_cent: number
|
||||
}
|
||||
|
||||
export interface DashboardPending {
|
||||
listing_reviews: number
|
||||
disputes: number
|
||||
pending_handoffs: number
|
||||
pending_return_confirms: number
|
||||
}
|
||||
|
||||
export interface RecentOrder {
|
||||
id: number
|
||||
order_no: string
|
||||
title: string
|
||||
renter_id: number
|
||||
owner_id: number
|
||||
status: string
|
||||
rent_amount_cent: number
|
||||
deposit_amount_cent: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface DashboardData {
|
||||
metrics: DashboardMetrics
|
||||
pending: DashboardPending
|
||||
recent_orders: RecentOrder[]
|
||||
generated_at: string
|
||||
}
|
||||
@@ -44,7 +44,7 @@ export function LoginPage() {
|
||||
<section className="login-brand-panel">
|
||||
<div className="login-brand-panel__inner">
|
||||
<div className="login-brand__logo">
|
||||
<BrandLogo height={58} />
|
||||
<BrandLogo height={58} variant="dark" />
|
||||
</div>
|
||||
<Typography.Title level={1} className="brand-title login-brand__title">
|
||||
租得明白
|
||||
|
||||
Reference in New Issue
Block a user