344 lines
14 KiB
TypeScript
344 lines
14 KiB
TypeScript
import { Suspense, lazy, useEffect } from 'react'
|
||
import { Spin } from 'antd'
|
||
import {
|
||
BrowserRouter,
|
||
HashRouter,
|
||
Navigate,
|
||
Outlet,
|
||
Route,
|
||
Routes,
|
||
useLocation,
|
||
} from 'react-router'
|
||
|
||
import { getAdminRole, hasAdminSession } from '@/utils/admin-auth'
|
||
import { isClaimPath, isH5ClaimPath } from '@/utils/claim-path'
|
||
import { isCollectMaterialLinkPath } from '@/utils/collect-link'
|
||
import { isRedeemPath } from '@/utils/redeem'
|
||
import { isWorkerSite, WORKER_SITE_ORIGIN } from '@/utils/site'
|
||
import { hasWorkerSession } from '@/utils/worker-auth'
|
||
import { resolveLegacyWorkerPath } from '@/utils/worker-routes'
|
||
import AdminLayout from '@/layouts/AdminLayout'
|
||
import WorkerLayout from '@/layouts/WorkerLayout'
|
||
|
||
const AdminDashboardPage = lazy(() => import('@/pages/admin/AdminDashboardPage'))
|
||
const AdminFinancePage = lazy(() => import('@/pages/admin/AdminFinancePage'))
|
||
const AdminFinanceLedgerPage = lazy(() => import('@/pages/admin/AdminFinanceLedgerPage'))
|
||
const AdminSalesProfitPage = lazy(() => import('@/pages/admin/AdminSalesProfitPage'))
|
||
const AdminCostCenterPage = lazy(() => import('@/pages/admin/AdminCostCenterPage'))
|
||
const AdminFulfillmentChannelPage = lazy(() => import('@/pages/admin/AdminFulfillmentChannelPage'))
|
||
const AdminFundPage = lazy(() => import('@/pages/admin/AdminFundPage'))
|
||
const AdminAuditLogsPage = lazy(() => import('@/pages/admin/AdminAuditLogsPage'))
|
||
const AdminKuaishouIndustryPage = lazy(() => import('@/pages/admin/AdminKuaishouIndustryPage'))
|
||
const AdminDouyinMashidaCertificatePage = lazy(
|
||
() => import('@/pages/admin/AdminDouyinMashidaCertificatePage'),
|
||
)
|
||
const AdminLoginPage = lazy(() => import('@/pages/admin/AdminLoginPage'))
|
||
const AdminOrderDetailPage = lazy(() => import('@/pages/admin/AdminOrderDetailPage'))
|
||
const AdminOrdersPage = lazy(() => import('@/pages/admin/AdminOrdersPage'))
|
||
const AdminPlatformFulfillmentPage = lazy(
|
||
() => import('@/pages/admin/platform/AdminPlatformFulfillmentPage'),
|
||
)
|
||
const AdminPlatformShopsPage = lazy(() => import('@/pages/admin/platform/AdminPlatformShopsPage'))
|
||
const AdminTaskDetailPage = lazy(() => import('@/pages/admin/AdminTaskDetailPage'))
|
||
const AdminTasksPage = lazy(() => import('@/pages/admin/AdminTasksPage'))
|
||
const AdminUsersPage = lazy(() => import('@/pages/admin/AdminUsersPage'))
|
||
const AdminDevMockPage = lazy(() => import('@/pages/admin/AdminDevMockPage'))
|
||
const AdminWorkerPlatformPage = lazy(() => import('@/pages/admin/AdminWorkerPlatformPage'))
|
||
const ClaimPage = lazy(() => import('@/pages/claim/ClaimPage'))
|
||
const H5ClaimEntryPage = lazy(() => import('@/pages/claim/H5ClaimEntryPage'))
|
||
const CollectPage = lazy(() => import('@/pages/collect/CollectPage'))
|
||
const CollectMaterialLinkEntryPage = lazy(
|
||
() => import('@/pages/collect/CollectMaterialLinkEntryPage'),
|
||
)
|
||
const CollectMaterialLinkPage = lazy(() => import('@/pages/collect/CollectMaterialLinkPage'))
|
||
const NotFoundPage = lazy(() => import('@/pages/NotFoundPage'))
|
||
const RedeemPage = lazy(() => import('@/pages/redeem/RedeemPage'))
|
||
const WorkerHallPage = lazy(() => import('@/pages/worker/WorkerHallPage'))
|
||
const WorkerLoginPage = lazy(() => import('@/pages/worker/WorkerLoginPage'))
|
||
const WorkerOrdersPage = lazy(() => import('@/pages/worker/WorkerOrdersPage'))
|
||
const WorkerAfterSalesPage = lazy(() => import('@/pages/worker/WorkerAfterSalesPage'))
|
||
const WorkerConsignmentPage = lazy(() => import('@/pages/worker/WorkerConsignmentPage'))
|
||
const WorkerProfilePage = lazy(() => import('@/pages/worker/WorkerProfilePage'))
|
||
|
||
function RequireAdmin() {
|
||
const location = useLocation()
|
||
|
||
if (!hasAdminSession()) {
|
||
return <Navigate to="/admin/login" replace state={{ from: location }} />
|
||
}
|
||
|
||
return <Outlet />
|
||
}
|
||
|
||
function RequireRole({ roles }: { roles: Array<'admin' | 'operator' | 'support' | 'finance'> }) {
|
||
if (!roles.includes(getAdminRole())) {
|
||
return (
|
||
<Navigate to={getAdminRole() === 'finance' ? '/admin/finance' : '/admin/dashboard'} replace />
|
||
)
|
||
}
|
||
|
||
return <Outlet />
|
||
}
|
||
|
||
function LoginRoute() {
|
||
if (hasAdminSession()) {
|
||
return <Navigate to="/admin/dashboard" replace />
|
||
}
|
||
|
||
return <AdminLoginPage />
|
||
}
|
||
|
||
function RequireWorker() {
|
||
const location = useLocation()
|
||
|
||
if (!hasWorkerSession()) {
|
||
return <Navigate to="/" replace state={{ from: location }} />
|
||
}
|
||
|
||
return <Outlet />
|
||
}
|
||
|
||
export default function App() {
|
||
const legacyWorkerPath = resolveLegacyWorkerPath(window.location.hash)
|
||
|
||
if (legacyWorkerPath) {
|
||
return <WorkerSiteRedirect path={legacyWorkerPath} />
|
||
}
|
||
|
||
// 路径式领取链接(`https://<域名>/claim/<token>`,不带 `#`)复用主站现成的领取页:
|
||
// 买家输 UID → 出二维码 → 扫码绑定 → 比对 UID → 发货完成。
|
||
// 必须早于站点判断,用独立的 BrowserRouter 分支承载,主站 HashRouter 不受影响。
|
||
// 注意:主站打手领取页是 HashRouter 里的 `#/claim/:token`,它的 pathname 是 `/`,
|
||
// 不会命中这里(判定见 utils/claim-path.isClaimPath),两个入口互不影响。
|
||
if (isClaimPath(window.location.pathname)) {
|
||
return <ClaimApplication />
|
||
}
|
||
|
||
// H5 领取入口(`https://<域名>/h5` 及其子路径,不带 `#`)。
|
||
// 判定是**命名空间级**:`/h5` 本身(只有前缀、没有订单号)也算命中。
|
||
// 否则 `https://<域名>/h5` 会落回主站 HashRouter,被后台路由接管成
|
||
// `https://<域名>/h5/#/admin/dashboard` 后台登录页(买家点领取链接看到后台登录,历史缺陷)。
|
||
if (isH5ClaimPath(window.location.pathname)) {
|
||
return <H5ClaimApplication />
|
||
}
|
||
|
||
// 固定兑换链接是路径式(`https://<域名>/redeem`,不带 `#`),必须早于站点判断,
|
||
// 用独立的 BrowserRouter 分支承载,主站 HashRouter 不受影响。
|
||
if (isRedeemPath(window.location.pathname)) {
|
||
return <RedeemApplication />
|
||
}
|
||
|
||
// 补资料页(`/supplement`):路径式公开页,先于站点判断,
|
||
// 保证在后台域名和打手端域名下都能打开。带订单号直接进补资料页,不带订单号进输入页。
|
||
if (isCollectMaterialLinkPath(window.location.pathname)) {
|
||
return <CollectMaterialLinkApplication />
|
||
}
|
||
|
||
if (isWorkerSite()) {
|
||
return <WorkerApplication />
|
||
}
|
||
|
||
return <AdminApplication />
|
||
}
|
||
|
||
function AdminApplication() {
|
||
return (
|
||
<HashRouter>
|
||
<Suspense fallback={<RouteLoading />}>
|
||
<Routes>
|
||
<Route path="/" element={<Navigate to="/admin/dashboard" replace />} />
|
||
<Route path="/admin/login" element={<LoginRoute />} />
|
||
<Route element={<RequireAdmin />}>
|
||
<Route path="/admin" element={<AdminLayout />}>
|
||
<Route index element={<Navigate to="/admin/dashboard" replace />} />
|
||
<Route element={<RequireRole roles={['admin', 'operator', 'support']} />}>
|
||
<Route path="dashboard" element={<AdminDashboardPage />} />
|
||
<Route path="tasks" element={<AdminTasksPage />} />
|
||
<Route path="tasks/:taskId" element={<AdminTaskDetailPage />} />
|
||
</Route>
|
||
<Route element={<RequireRole roles={['admin', 'operator', 'support', 'finance']} />}>
|
||
<Route path="orders" element={<AdminOrdersPage />} />
|
||
<Route path="orders/:orderId" element={<AdminOrderDetailPage />} />
|
||
</Route>
|
||
<Route element={<RequireRole roles={['admin', 'operator', 'finance']} />}>
|
||
<Route path="finance" element={<AdminFinancePage />} />
|
||
<Route path="finance/ledger" element={<AdminFinanceLedgerPage />} />
|
||
<Route path="sales-profit" element={<AdminSalesProfitPage />} />
|
||
<Route path="cost-center" element={<AdminCostCenterPage />} />
|
||
<Route path="fulfillment-channel" element={<AdminFulfillmentChannelPage />} />
|
||
</Route>
|
||
<Route element={<RequireRole roles={['admin', 'operator', 'support', 'finance']} />}>
|
||
<Route path="fund" element={<AdminFundPage />} />
|
||
</Route>
|
||
<Route path="worker-platform" element={<AdminWorkerPlatformPage />} />
|
||
{/* 旧「配置 → 对接客服」地址已并入接单平台 tab,书签继续可用。 */}
|
||
<Route
|
||
path="consignment-contacts"
|
||
element={<Navigate to="/admin/worker-platform?tab=consignment-contacts" replace />}
|
||
/>
|
||
<Route element={<RequireRole roles={['admin', 'operator', 'support']} />}>
|
||
<Route path="kuaishou-industry" element={<AdminKuaishouIndustryPage />} />
|
||
<Route
|
||
path="douyin-mashida-certificates"
|
||
element={<AdminDouyinMashidaCertificatePage />}
|
||
/>
|
||
<Route path="dev-mock" element={<AdminDevMockPage />} />
|
||
</Route>
|
||
<Route element={<RequireRole roles={['admin']} />}>
|
||
<Route path="users" element={<AdminUsersPage />} />
|
||
<Route path="platform-shops" element={<AdminPlatformShopsPage />} />
|
||
<Route path="platform-fulfillment" element={<AdminPlatformFulfillmentPage />} />
|
||
<Route path="audit-logs" element={<AdminAuditLogsPage />} />
|
||
</Route>
|
||
</Route>
|
||
</Route>
|
||
<Route path="/claim/:token" element={<ClaimPage />} />
|
||
<Route path="/collect" element={<CollectPage />} />
|
||
<Route path="*" element={<NotFoundPage />} />
|
||
</Routes>
|
||
</Suspense>
|
||
</HashRouter>
|
||
)
|
||
}
|
||
|
||
function RedeemApplication() {
|
||
return (
|
||
<BrowserRouter>
|
||
<Suspense fallback={<RouteLoading />}>
|
||
<Routes>
|
||
<Route path="/redeem" element={<RedeemPage />} />
|
||
<Route path="*" element={<Navigate to="/redeem" replace />} />
|
||
</Routes>
|
||
</Suspense>
|
||
</BrowserRouter>
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 路径式领取页(`/claim/<token>`)的独立路由树。
|
||
*
|
||
* 复用主站 HashRouter 里的**同一个** `ClaimPage`:它只依赖 `useParams` 取 token,
|
||
* 内部不 navigate、也不读 hash,所以换成 BrowserRouter 承载不需要复制页面。
|
||
*
|
||
* `isClaimPath` 是命名空间判定(`/claim` 及子路径都命中),所以必须保留兜底路由:
|
||
* 只有前缀(`/claim`)或多段路径(`/claim/a/b`)时 `ClaimPage` 取不到 token,
|
||
* 由页面自己展示「链接无效」,既不会路由空屏,也不会漏回后台路由。
|
||
*/
|
||
function ClaimApplication() {
|
||
useClearStrayHash()
|
||
|
||
return (
|
||
<BrowserRouter>
|
||
<Suspense fallback={<RouteLoading />}>
|
||
<Routes>
|
||
<Route path="/claim/:token" element={<ClaimPage />} />
|
||
<Route path="*" element={<ClaimPage />} />
|
||
</Routes>
|
||
</Suspense>
|
||
</BrowserRouter>
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 补资料页(`/supplement`)的独立路由树。
|
||
*
|
||
* - `/supplement`:输入平台订单号;
|
||
* - `/supplement/<订单号>`:直接打开对应订单的补资料页;
|
||
* - 其余子路径:交给补资料页展示「订单号无效」。
|
||
*
|
||
* 路径式公开页,不带 hash;不依赖后台会话。
|
||
*/
|
||
function CollectMaterialLinkApplication() {
|
||
useClearStrayHash()
|
||
|
||
return (
|
||
<BrowserRouter>
|
||
<Suspense fallback={<RouteLoading />}>
|
||
<Routes>
|
||
<Route path="/supplement" element={<CollectMaterialLinkEntryPage />} />
|
||
<Route path="/supplement/:code" element={<CollectMaterialLinkPage />} />
|
||
<Route path="*" element={<CollectMaterialLinkPage />} />
|
||
</Routes>
|
||
</Suspense>
|
||
</BrowserRouter>
|
||
)
|
||
}
|
||
|
||
/**
|
||
* H5 领取入口(`/h5` 命名空间)的独立路由树。
|
||
*
|
||
* - `/h5`:只有前缀、没有订单号 → 入口页(买家补订单号 / 粘贴完整链接);
|
||
* - `/h5/<订单号>`:与分享链接完全相同的领取流程;
|
||
* - 其余子路径(如 `/h5/a/b`):没有订单号可领,回入口页重新输入,
|
||
* 避免"前缀下面任何一条路径"漏回后台路由。
|
||
*
|
||
* 全程路径式(不带 `#`):后台 HashRouter 永远拿不到 `/h5` 这几个路径。
|
||
*/
|
||
function H5ClaimApplication() {
|
||
useClearStrayHash()
|
||
|
||
return (
|
||
<BrowserRouter>
|
||
<Suspense fallback={<RouteLoading />}>
|
||
<Routes>
|
||
<Route path="/h5" element={<H5ClaimEntryPage />} />
|
||
<Route path="/h5/:orderNo" element={<ClaimPage />} />
|
||
<Route path="*" element={<Navigate to="/h5" replace />} />
|
||
</Routes>
|
||
</Suspense>
|
||
</BrowserRouter>
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 公开领取页是**路径式**的、从不使用 hash 路由,但历史链接可能带着后台残留的 hash
|
||
* (例如 `https://<域名>/h5#/admin/dashboard`)。
|
||
* 挂载时把 hash 抹掉,避免买家在地址栏看到、并转发一条"看着像后台地址"的领取链接。
|
||
*/
|
||
function useClearStrayHash() {
|
||
useEffect(() => {
|
||
if (!window.location.hash) return
|
||
|
||
const { pathname, search } = window.location
|
||
window.history.replaceState(window.history.state, '', `${pathname}${search}`)
|
||
}, [])
|
||
}
|
||
|
||
function WorkerApplication() {
|
||
return (
|
||
<BrowserRouter>
|
||
<Suspense fallback={<RouteLoading />}>
|
||
<Routes>
|
||
<Route path="/" element={<WorkerLoginPage />} />
|
||
<Route element={<RequireWorker />}>
|
||
<Route element={<WorkerLayout />}>
|
||
<Route path="hall" element={<WorkerHallPage />} />
|
||
<Route path="orders" element={<WorkerOrdersPage />} />
|
||
<Route path="orders/after-sales" element={<WorkerAfterSalesPage />} />
|
||
<Route path="consignment" element={<WorkerConsignmentPage />} />
|
||
<Route path="profile" element={<WorkerProfilePage />} />
|
||
</Route>
|
||
</Route>
|
||
<Route path="*" element={<Navigate to="/" replace />} />
|
||
</Routes>
|
||
</Suspense>
|
||
</BrowserRouter>
|
||
)
|
||
}
|
||
|
||
function WorkerSiteRedirect({ path }: { path: string }) {
|
||
useEffect(() => {
|
||
window.location.replace(`${WORKER_SITE_ORIGIN}${path}`)
|
||
}, [path])
|
||
|
||
return <RouteLoading />
|
||
}
|
||
|
||
function RouteLoading() {
|
||
return (
|
||
<div className="route-loading">
|
||
<Spin />
|
||
</div>
|
||
)
|
||
}
|