提交 48bd74bd authored 作者: 王鹏飞's avatar 王鹏飞

refactor(admin): migrate advertisements adsense module

上级 195f403c
# Task 6 — advertisements/adsense
## Delivered
- Added the `advertisements/adsense` module with request normalization, controlled pagination state, AppList view, and a lazy nested route.
- Preserved the legacy list request (`page`/`page_size`, form-encoded POST), the three original columns, controlled status Switch, exact change-status permission, and refresh of the current page after a successful mutation.
- The mutation sends status as the required string (`"1"`/`"0"`). Failed mutations leave the displayed server row unchanged.
- Removed `pages/advertisement/adsense/index.jsx` after routing had switched to the new module.
## Compatibility exception
`pages/advertisement/adsense/request.js` remains intentionally: the un-migrated `pages/advertisement/advert/index.jsx` imports its `getPositionList` helper. Removing it breaks the unrelated advert page and the admin build. The new AdSense module does not import this compatibility file.
## Verification
- Focused state regression test: 2 passed (`useAdsenseList.test.tsx`) using an ephemeral Vitest config because the tracked admin Vitest config only includes `src/test/**` and therefore excludes module-local tests.
- `pnpm build`: passed for learning and admin.
- `pnpm test`: admin's 11 tests passed; the overall workspace command failed in pre-existing learning tests (`src/modules/my/routes.test.tsx`, `src/api/http-auth.test.ts`) unrelated to this migration.
- `git diff --check`: passed.
import { message } from 'antd'
import qs from 'qs'
import axios from '@/utils/request'
export type AdsenseRow = {
id: number
position_name: string
pic_size: string
status: 0 | 1
}
export type AdsenseListResult = {
rows: AdsenseRow[]
total: number
}
type ApiEnvelope<T> = {
code?: number
data?: T
message?: string
}
function getMessage(envelope: ApiEnvelope<unknown>) {
return envelope.message || '数据获取失败!'
}
export async function getPositionList(query: Record<string, unknown>): Promise<AdsenseListResult> {
const response = await axios.post('/ad/position/getList', qs.stringify(query))
const envelope = response.data as ApiEnvelope<{ list?: AdsenseRow[]; total?: number }>
if (envelope.code !== 200) {
const error = new Error(getMessage(envelope))
message.error(error.message)
throw error
}
return {
rows: envelope.data?.list ?? [],
total: envelope.data?.total ?? 0,
}
}
export async function changePositionStatus({
id,
status,
}: {
id: number
status: '0' | '1'
}): Promise<boolean> {
try {
const response = await axios.post('/ad/position/changeStatus', qs.stringify({ id, status }))
const envelope = response.data as ApiEnvelope<unknown>
if (envelope.code !== 200) {
message.error(getMessage(envelope))
return false
}
message.success(getMessage(envelope))
return true
} catch {
return false
}
}
export type AdsenseQueryState = {
page: number
pageSize: number
}
export function buildAdsenseQuery({ page, pageSize }: AdsenseQueryState) {
return {
page,
page_size: pageSize,
}
}
import { lazy } from 'react'
import { Navigate, useRoutes } from 'react-router'
const AdsenseListView = lazy(() => import('./views/AdsenseListView'))
export default function AdsenseRoutes() {
return useRoutes([
{ index: true, Component: AdsenseListView },
{ path: '*', element: <Navigate to="/advertisement/adsense" /> },
])
}
import { act, renderHook, waitFor } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { useAdsenseList } from './useAdsenseList'
describe('useAdsenseList', () => {
it('sends string status and refreshes the current page after a successful status change', async () => {
const request = vi.fn().mockResolvedValue({ rows: [{ id: 9, status: 0 }], total: 1 })
const changeStatusRequest = vi.fn().mockResolvedValue(true)
const { result } = renderHook(() => useAdsenseList({ request, changeStatusRequest }))
await waitFor(() => expect(request).toHaveBeenCalledTimes(1))
act(() => result.current.changePage(3))
await waitFor(() => expect(request).toHaveBeenCalledTimes(2))
await act(async () => {
await result.current.changeStatus(9, true)
})
await waitFor(() => expect(request).toHaveBeenCalledTimes(3))
expect(changeStatusRequest).toHaveBeenCalledWith({ id: 9, status: '1' })
expect(request).toHaveBeenLastCalledWith({ page: 3, page_size: 10 })
})
it('keeps the server row state and does not refresh when the status mutation fails', async () => {
const rows = [{ id: 9, status: 0 }]
const request = vi.fn().mockResolvedValue({ rows, total: 1 })
const changeStatusRequest = vi.fn().mockResolvedValue(false)
const { result } = renderHook(() => useAdsenseList({ request, changeStatusRequest }))
await waitFor(() => expect(result.current.loading).toBe(false))
await act(async () => {
await result.current.changeStatus(9, true)
})
expect(request).toHaveBeenCalledTimes(1)
expect(result.current.rows).toEqual(rows)
})
})
import { useCallback, useEffect, useRef, useState } from 'react'
import {
changePositionStatus,
getPositionList,
type AdsenseListResult,
type AdsenseRow,
} from './api'
import { buildAdsenseQuery } from './query'
type AdsenseRequest = (query: Record<string, unknown>) => Promise<AdsenseListResult>
type ChangeStatusRequest = (params: { id: number; status: '0' | '1' }) => Promise<boolean>
export function useAdsenseList({
request = getPositionList,
changeStatusRequest = changePositionStatus,
}: {
request?: AdsenseRequest
changeStatusRequest?: ChangeStatusRequest
} = {}) {
const [rows, setRows] = useState<AdsenseRow[]>([])
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
const requestId = useRef(0)
const lastQuery = useRef(buildAdsenseQuery({ page: 1, pageSize: 10 }))
const load = useCallback(
async (nextPage: number, nextPageSize: number) => {
const query = buildAdsenseQuery({ page: nextPage, pageSize: nextPageSize })
lastQuery.current = query
const id = ++requestId.current
setLoading(true)
setError(null)
try {
const result = await request(query)
if (id === requestId.current) {
setRows(result.rows)
setTotal(result.total)
}
} catch (reason) {
if (id === requestId.current) {
setError(reason instanceof Error ? reason : new Error('数据获取失败!'))
}
} finally {
if (id === requestId.current) {
setLoading(false)
}
}
},
[request],
)
useEffect(() => {
void load(1, 10)
}, [load])
const changePage = useCallback(
(nextPage: number) => {
setPage(nextPage)
void load(nextPage, pageSize)
},
[load, pageSize],
)
const changePageSize = useCallback(
(nextPageSize: number) => {
setPage(1)
setPageSize(nextPageSize)
void load(1, nextPageSize)
},
[load],
)
const refresh = useCallback(() => {
void load(page, pageSize)
}, [load, page, pageSize])
const retry = useCallback(() => {
void load(lastQuery.current.page, lastQuery.current.page_size)
}, [load])
const changeStatus = useCallback(
async (id: number, checked: boolean) => {
const changed = await changeStatusRequest({ id, status: checked ? '1' : '0' })
if (changed) {
await load(page, pageSize)
}
},
[changeStatusRequest, load, page, pageSize],
)
return {
rows,
total,
page,
pageSize,
loading,
error,
changePage,
changePageSize,
refresh,
retry,
changeStatus,
}
}
import { Switch } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { useMemo } from 'react'
import { useSelector } from 'react-redux'
import { AppList } from '@/shared/components/AppList'
import type { AdsenseRow } from '../api'
import { useAdsenseList } from '../useAdsenseList'
export default function AdsenseListView() {
const { operationPermissionsList = [] } = useSelector(
(state: { user: { operationPermissionsList?: string[] } }) => state.user,
)
const canChangeStatus = operationPermissionsList.includes(
'/advertisement/adsense/changeStatus',
)
const {
rows,
total,
page,
pageSize,
loading,
error,
changePage,
changePageSize,
changeStatus,
retry,
} = useAdsenseList()
const columns = useMemo<ColumnsType<AdsenseRow>>(
() => [
{
title: '广告位位置',
key: 'position_name',
dataIndex: 'position_name',
},
{
title: '广告位大小',
key: 'pic_size',
dataIndex: 'pic_size',
},
{
title: '状态',
key: 'status',
dataIndex: 'status',
render: (_, record) => (
<Switch
checked={Boolean(record.status)}
disabled={!canChangeStatus}
onChange={(checked) => void changeStatus(record.id, checked)}
/>
),
},
],
[canChangeStatus, changeStatus],
)
return (
<div className="classify">
<AppList
rowKey="id"
columns={columns}
data={{ rows, total, loading, error }}
pagination={{
page,
pageSize,
pageSizeOptions: [10, 20, 30, 40],
onChange: changePage,
onPageSizeChange: changePageSize,
}}
onRetry={retry}
/>
</div>
)
}
...@@ -4,7 +4,7 @@ import ModuleBoundary from "@/app/ModuleBoundary"; ...@@ -4,7 +4,7 @@ import ModuleBoundary from "@/app/ModuleBoundary";
const AdvertisementsLayout = lazy(() => import("@/pages/advertisement/index")); const AdvertisementsLayout = lazy(() => import("@/pages/advertisement/index"));
const Advert = lazy(() => import("@/pages/advertisement/advert/index")); const Advert = lazy(() => import("@/pages/advertisement/advert/index"));
const Adsense = lazy(() => import("@/pages/advertisement/adsense/index")); const AdsenseRoutes = lazy(() => import("./adsense/routes"));
export default function AdvertisementsRoutes() { export default function AdvertisementsRoutes() {
const routes = useRoutes([ const routes = useRoutes([
...@@ -13,7 +13,7 @@ export default function AdvertisementsRoutes() { ...@@ -13,7 +13,7 @@ export default function AdvertisementsRoutes() {
children: [ children: [
{ index: true, element: <Navigate to="/advertisement/advert" /> }, { index: true, element: <Navigate to="/advertisement/advert" /> },
{ path: "advert", Component: Advert }, { path: "advert", Component: Advert },
{ path: "adsense", Component: Adsense }, { path: "adsense/*", Component: AdsenseRoutes },
], ],
}, },
{ path: "*", element: <Navigate to="/" /> }, { path: "*", element: <Navigate to="/" /> },
......
import React, { useEffect, useState } from "react";
import { Table, Row, DatePicker, Form, Modal, Switch } from "antd";
const { RangePicker } = DatePicker;
import PaginationCom from "@/common/Pagination";
import { getPositionList, changeStatus } from "./request";
import TableCom from "@/common/TableCom/index";
import { useSelector } from "react-redux";
const Adsense = () => {
const [data, setData] = useState([]);
const [page_size, setpage_size] = useState(10);
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [isEdit, setIsEdit] = useState(false);
const [delMoadal, setDelModal] = useState(false);
const [form] = Form.useForm();
const [swichDisabled, setSwichDisabled] = useState(false);
// 获取操作权限
const { operationPermissionsList } = useSelector((state) => state.user);
const handleStatus = async (status, id) => {
const bool = await changeStatus({ id, status: status ? "1" : "0" });
bool && init();
};
const onClose = () => {
setIsEdit(false);
};
const submitForm = (obj) => {
console.log(obj);
};
const delSuccess = () => {
setDelModal(false);
};
const init = async (obj = {}) => {
setLoading(true);
const { total, list } = await getPositionList({ page, page_size, ...obj });
setTotal(total);
setData(list);
setLoading(false);
};
useEffect(() => {
setSwichDisabled(!operationPermissionsList.includes("/advertisement/adsense/changeStatus"));
});
const columns = [
{
title: "广告位位置",
key: "position_name",
dataIndex: "position_name",
},
{
title: "广告位大小",
key: "pic_size",
dataIndex: "pic_size",
},
{
title: "状态",
key: "status",
dataIndex: "status",
render(_, { status, id }) {
return (
<Switch
defaultChecked={status}
onChange={(ev) => handleStatus(ev, id)}
disabled={swichDisabled}
/>
);
},
},
];
return (
<div className="classify">
{TableCom({ columns, data, loading })}
<Modal
mask={false}
centered
open={delMoadal}
onOk={delSuccess}
onCancel={() => setDelModal(false)}
footer={(_, { OkBtn, CancelBtn }) => {
return (
<Row justify={"center"} className="delModal">
<Space>
<CancelBtn />
<OkBtn />
</Space>
</Row>
);
}}
okText="确认"
>
<p style={{ textAlign: "center", padding: "50px 0 30px", fontSize: 16 }}>
分类将永久删除,是否继续?
</p>
</Modal>
<br />
<PaginationCom
total={total}
page_size={page_size}
setpage_size={setpage_size}
page={page}
setPage={setPage}
init={init}
/>
</div>
);
};
export default Adsense;
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论