提交 2be389ac authored 作者: 王鹏飞's avatar 王鹏飞

chore: update

上级 2d0f2327
VITE_API_URL=https://zijingebook.ezijing.com/api/admin
VITE_API_PREFIX=/api/admin
VITE_ADMIN_API_TARGET=http://127.0.0.1:7419
VITE_API_WEBSOCKET_URL=wss://zijingebook.ezijing.com/ws
VITE_API_URL=https://zijingebook.ezijing.com/api/admin
VITE_API_PREFIX=/api/admin
VITE_ADMIN_API_TARGET=http://127.0.0.1:7419
VITE_API_WEBSOCKET_URL=wss://zijingebook.ezijing.com/ws
......@@ -2,12 +2,12 @@ import axios from '@/utils/axios'
// 获取用户
export function getUser() {
return axios.post('/api/user/user/getUserInfo')
return axios.post('/api/admin/user/user/getUserInfo')
}
// 获取菜单
export async function getMenuList(params) {
const response = await axios.post('/api/system/power/getMenuList', params)
const response = await axios.post('/api/admin/system/power/getMenuList', params)
return {
data: response.data,
operation_permissions_list: response.operation_permissions_list ?? [],
......@@ -16,15 +16,15 @@ export async function getMenuList(params) {
// 检查当前是否存在可用的 SSO 登录态;未登录时后端返回 { run: "login" }。
export async function checkSSOLogin() {
return (await axios.post('/api/user/checkSsoLogin')).data
return (await axios.post('/api/admin/user/checkSsoLogin')).data
}
// 获取STSToken
export function getSTSToken(data) {
return axios.post('/api/common/OssUpload/getUploadToken', data)
return axios.post('/api/admin/common/OssUpload/getUploadToken', data)
}
// 退出登录
export function logout() {
return axios.post('/api/login/out')
return axios.post('/api/admin/login/out')
}
......@@ -2,20 +2,20 @@ import axios from '@/utils/axios'
// 获取未读消息条数
export function getMessageCount(data) {
return axios.post('/api/message/message/getMessageCount', data)
return axios.post('/api/admin/message/message/getMessageCount', data)
}
// 获取消息列表
export function getMessageList(data) {
return axios.post('/api/message/message/getMessageList', data)
return axios.post('/api/admin/message/message/getMessageList', data)
}
// 设置部分已读
export function setMessageRead(data) {
return axios.post('/api/message/message/setOneRead', data)
return axios.post('/api/admin/message/message/setOneRead', data)
}
// 设置全部已读
export function setMessageAllRead() {
return axios.post('/api/message/message/setRead')
return axios.post('/api/admin/message/message/setRead')
}
......@@ -2,5 +2,5 @@ import axios from '@/utils/axios'
// 获取教师列表
export function getTeacherList(data) {
return axios.post('/api/user/teacher/getList', data)
return axios.post('/api/admin/user/teacher/getList', data)
}
......@@ -50,14 +50,14 @@ export function AppList<Row extends object, Filters extends object = Record<stri
}
return (
<div className="app-list">
<div className='app-list'>
{filters ? (
<section className="app-list__filters" aria-label="筛选条件">
<div className="app-list__filter-main">
<div className="app-list__filter-items">
<section className='app-list__filters' aria-label='筛选条件'>
<div className='app-list__filter-main'>
<div className='app-list__filter-items'>
{filters.items.map((item) => (
<div className="app-list__filter-item" key={String(item.name)}>
<span className="app-list__filter-label">{item.label}</span>
<div className='app-list__filter-item' key={String(item.name)}>
<span className='app-list__filter-label'>{item.label}</span>
{item.render({
value: filters.value[item.name],
onChange: (value) => updateFilter(item, value),
......@@ -65,8 +65,8 @@ export function AppList<Row extends object, Filters extends object = Record<stri
</div>
))}
</div>
<Space wrap className="app-list__filter-actions">
<Button type="primary" icon={<SearchOutlined />} onClick={filters.onSearch}>
<Space wrap className='app-list__filter-actions'>
<Button type='primary' icon={<SearchOutlined />} onClick={filters.onSearch}>
搜索
</Button>
<Button icon={<ReloadOutlined />} onClick={filters.onReset}>
......@@ -74,21 +74,21 @@ export function AppList<Row extends object, Filters extends object = Record<stri
</Button>
</Space>
</div>
{toolbar ? <div className="app-list__toolbar">{toolbar}</div> : null}
{toolbar ? <div className='app-list__toolbar'>{toolbar}</div> : null}
</section>
) : null}
{!filters && toolbar ? <div className="app-list__toolbar">{toolbar}</div> : null}
{!filters && toolbar ? <div className='app-list__toolbar'>{toolbar}</div> : null}
{hasRefreshError ? (
<Alert
className="app-list__error"
type="error"
className='app-list__error'
type='error'
showIcon
message={data.error?.message}
action={
onRetry ? (
<Button size="small" danger onClick={onRetry}>
<Button size='small' danger onClick={onRetry}>
重试
</Button>
) : null
......@@ -97,10 +97,10 @@ export function AppList<Row extends object, Filters extends object = Record<stri
) : null}
{hasBlockingError ? (
<div className="app-list__blocking-error" role="alert">
<div className='app-list__blocking-error' role='alert'>
<Empty description={data.error?.message}>
{onRetry ? (
<Button type="primary" danger onClick={onRetry}>
<Button type='primary' danger onClick={onRetry}>
重试
</Button>
) : null}
......@@ -115,7 +115,7 @@ export function AppList<Row extends object, Filters extends object = Record<stri
loading={data.loading}
pagination={tablePagination}
rowSelection={selection}
locale={{ ...tableProps?.locale, emptyText: <Empty description="暂无数据" /> }}
locale={{ ...tableProps?.locale, emptyText: <Empty description='暂无数据' /> }}
/>
)}
</div>
......
......@@ -2,15 +2,15 @@ import axios from '@/utils/axios'
// 百度聊天接口
export function baiduAIChat(data) {
return axios.post('/api/ai/baidubce/chat', data)
return axios.post('/api/admin/ai/baidubce/chat', data)
}
// 题目分类自动添加(名称存在则返回数据,不存在则创建)
export function addType(data) {
return axios.post('/api/type/autoAdd', data)
return axios.post('/api/admin/type/autoAdd', data)
}
// 创建试题
export function addQuestion(data) {
return axios.post('/api/question/add', data)
return axios.post('/api/admin/question/add', data)
}
......@@ -2,15 +2,15 @@ import axios from '@/utils/axios'
// 查询我的设计
export function getDesignList(data) {
return axios.post('/api/ai/chuangKit/designs', data)
return axios.post('/api/admin/ai/chuangKit/designs', data)
}
// 下载图片
export function getSourceImage(data) {
return axios.post('/api/ai/chuangKit/getSourceImage', data)
return axios.post('/api/admin/ai/chuangKit/getSourceImage', data)
}
// 下载图片
export function getDesignDownloadList(data) {
return axios.post('/api/ai/chuangKit/getDesignDownloadList', data)
return axios.post('/api/admin/ai/chuangKit/getDesignDownloadList', data)
}
......@@ -109,7 +109,6 @@ const renderExpandRead = (elem, children, editor) => {
whiteSpace: 'pre-wrap',
fontSize: '18px',
lineHeight: '24px',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
wordWrap: 'break-word',
fontWeight: 'bold',
......
......@@ -37,8 +37,13 @@ const InsertTopic = (props, ref) => {
return groups
}, [insertList])
const { radio: radioList, checkbox: checkboxList, equals: equalsList, press: pressList, textarea: textareaList } =
questionGroups
const {
radio: radioList,
checkbox: checkboxList,
equals: equalsList,
press: pressList,
textarea: textareaList,
} = questionGroups
// 删除
const delConfirm = (record) => {
......
......@@ -6,8 +6,8 @@ const post = async (path, params, notify = false) => {
return response.data
}
export const getList = (params) => post('/question/getList', params)
export const questionAdd = (params) => post('/question/add', params, true)
export const questionAddType = (params) => post('/type/add', params)
export const typeAllList = (params) => post('/type/getList', params)
export const bookNameList = (params) => post('/book/teacher/getListAll', params)
export const getList = (params) => post('/api/admin/question/getList', params)
export const questionAdd = (params) => post('/api/admin/question/add', params, true)
export const questionAddType = (params) => post('/api/admin/type/add', params)
export const typeAllList = (params) => post('/api/admin/type/getList', params)
export const bookNameList = (params) => post('/api/admin/book/teacher/getListAll', params)
......@@ -2,17 +2,21 @@ import axios from '@/utils/axios'
const post = async (path, params) => (await axios.post(path, params)).data
export const getTopicList = (params) => post('/book/question/getList', params)
export const getTopicTypeList = (params) => post('/book/question/type/getList', params)
export const addChapterTopic = (params) => post('/book/chapter/question/add', params)
export const getChapterTopic = (params) => post('/book/chapter/question/getQuestionList', params)
export const delChapterTopic = (params) => post('/book/chapter/question/removeQuestion', params)
export const addChapterTooltip = (params) => post('/book/bubble/add', params)
export const delChapterTooltip = (params) => post('/book/bubble/removeBubble', params)
export const addExpandRead = (params) => post('/book/extend/add', params)
export const delExpandRead = (params) => post('/book/extend/removeExtend', params)
export const expandReadInfo = (params) => post('/book/extend/info', params)
export const addGallery = (params) => post('/book/gallery/add', params)
export const delGallery = (params) => post('/book/gallery/removeGallery', params)
export const getRecordInfo = (params) => post('/book/teacher/section/getRecordInfo', params)
export const getRecordList = (params) => post('/book/teacher/section/getRecordList', params)
export const getTopicList = (params) => post('/api/admin/book/question/getList', params)
export const getTopicTypeList = (params) => post('/api/admin/book/question/type/getList', params)
export const addChapterTopic = (params) => post('/api/admin/book/chapter/question/add', params)
export const getChapterTopic = (params) =>
post('/api/admin/book/chapter/question/getQuestionList', params)
export const delChapterTopic = (params) =>
post('/api/admin/book/chapter/question/removeQuestion', params)
export const addChapterTooltip = (params) => post('/api/admin/book/bubble/add', params)
export const delChapterTooltip = (params) => post('/api/admin/book/bubble/removeBubble', params)
export const addExpandRead = (params) => post('/api/admin/book/extend/add', params)
export const delExpandRead = (params) => post('/api/admin/book/extend/removeExtend', params)
export const expandReadInfo = (params) => post('/api/admin/book/extend/info', params)
export const addGallery = (params) => post('/api/admin/book/gallery/add', params)
export const delGallery = (params) => post('/api/admin/book/gallery/removeGallery', params)
export const getRecordInfo = (params) =>
post('/api/admin/book/teacher/section/getRecordInfo', params)
export const getRecordList = (params) =>
post('/api/admin/book/teacher/section/getRecordList', params)
......@@ -16,9 +16,7 @@ export const EditorSidebar = ({
{[
{ key: 'text', title: '文本设置' },
{ key: 'style', title: '样式模版' },
...(reviewCommentPanel
? [{ key: 'comment', title: '评论' }]
: []),
...(reviewCommentPanel ? [{ key: 'comment', title: '评论' }] : []),
].map((item) => (
<div
className={`tabs-item ${item.key === tabKey ? 'active' : ''}`}
......
......@@ -176,10 +176,15 @@ export default function AppLayout() {
const pageTitle = headerOverride?.title || getCurrentMenuTitle(menuList, pathname)
const getSSOToken = async () => {
if (localStorage.getItem('kiwi.token')) return
try {
const data = await checkSSOLogin()
if (data && data.token) {
if (data?.token) {
localStorage.setItem('kiwi.token', data.token)
}
} catch {
// SSO 不可用时仍继续由后续业务接口校验当前登录态。
}
}
// 获取菜单
const getMenu = async () => {
......
......@@ -61,8 +61,7 @@ export function usePagedList<Row, Filters, Query>({
requestRef.current = request
}, [request])
const load = useCallback(
async (query: Query) => {
const load = useCallback(async (query: Query) => {
const requestId = ++requestIdRef.current
lastQueryRef.current = query
setState((current) => ({ ...current, loading: true, error: null }))
......@@ -80,9 +79,7 @@ export function usePagedList<Row, Filters, Query>({
setState((current) => ({ ...current, loading: false }))
}
}
},
[],
)
}, [])
const loadFor = useCallback(
(page: number, pageSize: number, filters: Filters) =>
......@@ -155,11 +152,7 @@ export function usePagedList<Row, Filters, Query>({
const changePageSize = useCallback(
(pageSize: number) => {
setState((current) => ({ ...current, page: initialPage, pageSize }))
void loadFor(
initialPage,
pageSize,
useAppliedFilters ? state.appliedFilters : state.filters,
)
void loadFor(initialPage, pageSize, useAppliedFilters ? state.appliedFilters : state.filters)
},
[initialPage, loadFor, state.appliedFilters, state.filters, useAppliedFilters],
)
......
......@@ -38,36 +38,39 @@ async function mutate(url: string, params: ApiParams): Promise<boolean> {
}
export async function getAdminList(params: AdminQuery): Promise<AdminListResult> {
const data = await getPayload<{ list?: AdminRow[]; total?: number }>('/user/user/getList', {
const data = await getPayload<{ list?: AdminRow[]; total?: number }>(
'/api/admin/user/user/getList',
{
...params,
})
},
)
return { rows: data.list ?? [], total: data.total ?? 0 }
}
export function getRoleOptions() {
return getPayload<RoleOption[]>('/user/role/getAllList', { type: 1 })
return getPayload<RoleOption[]>('/api/admin/user/role/getAllList', { type: 1 })
}
export function getAdminInfo(id: number) {
return getPayload<AdminRow>('/user/user/getInfoById', { id })
return getPayload<AdminRow>('/api/admin/user/user/getInfoById', { id })
}
export function addAdmin(params: ApiParams) {
return mutate('/user/user/add', params)
return mutate('/api/admin/user/user/add', params)
}
export function editAdmin(params: ApiParams) {
return mutate('/user/user/edit', params)
return mutate('/api/admin/user/user/edit', params)
}
export function deleteAdmin(id: number) {
return mutate('/user/user/del', { id })
return mutate('/api/admin/user/user/del', { id })
}
export function changeAdminStatus(id: number, status: '0' | '1') {
return mutate('/user/user/changeStatus', { status, id })
return mutate('/api/admin/user/user/changeStatus', { status, id })
}
export function changeAdminPassword(params: ApiParams) {
return mutate('/user/user/editPass', params)
return mutate('/api/admin/user/user/editPass', params)
}
......@@ -37,44 +37,49 @@ async function mutate(url: string, params: ApiParams) {
}
export async function getRoleList(params: RoleQuery): Promise<RoleListResult> {
const data = await getPayload<{ list?: RoleRow[]; total?: number }>('/user/role/getList', {
const data = await getPayload<{ list?: RoleRow[]; total?: number }>(
'/api/admin/user/role/getList',
{
...params,
})
},
)
return { rows: data.list ?? [], total: data.total ?? 0 }
}
export function getRoleInfo(id: number) {
return getPayload<RoleRow>('/user/role/getInfoById', { id })
return getPayload<RoleRow>('/api/admin/user/role/getInfoById', { id })
}
export function addRole(params: ApiParams) {
return mutate('/user/role/add', params)
return mutate('/api/admin/user/role/add', params)
}
export function editRole(params: ApiParams) {
return mutate('/user/role/edit', params)
return mutate('/api/admin/user/role/edit', params)
}
export function deleteRole(id: number) {
return mutate('/user/role/del', { id })
return mutate('/api/admin/user/role/del', { id })
}
export function changeRoleStatus(id: number, status: '1' | '2') {
return mutate('/user/role/changeStatus', { status, id })
return mutate('/api/admin/user/role/changeStatus', { status, id })
}
export function getRolePermissionIds(roleId: number) {
return getPayload<Array<string | number>>('/user/role/getPowerByRoleId', { role_id: roleId })
return getPayload<Array<string | number>>('/api/admin/user/role/getPowerByRoleId', {
role_id: roleId,
})
}
export function setRolePermissions(roleId: number, powerList: Array<string | number>) {
return mutate('/user/role/setRolePower', { role_id: roleId, power_list: powerList })
return mutate('/api/admin/user/role/setRolePower', { role_id: roleId, power_list: powerList })
}
export function getAllPermissions() {
return getPayload<PermissionRecord[]>('/system/power/getListAll', {})
return getPayload<PermissionRecord[]>('/api/admin/system/power/getListAll', {})
}
export function getTeacherRoles() {
return getPayload<RoleRow[]>('/user/role/getAllList', { type: 2 })
return getPayload<RoleRow[]>('/api/admin/user/role/getAllList', { type: 2 })
}
import axios, { notifySuccess } from '@/utils/axios'
export const getUserInfo = async (params) =>
(await axios.post('/user/user/getUserInfo', params)).data
(await axios.post('/api/admin/user/user/getUserInfo', params)).data
export const changePass = async (params) => (await axios.post('/user/user/changePass', params)).data
export const changePass = async (params) =>
(await axios.post('/api/admin/user/user/changePass', params)).data
export const changeInfo = async (params) => {
const response = await axios.post('/user/user/editUserInfo', params)
const response = await axios.post('/api/admin/user/user/editUserInfo', params)
notifySuccess(response.message)
return response.data
}
......@@ -13,7 +13,7 @@ export type AdsenseListResult = {
}
export async function getPositionList(query: Record<string, unknown>): Promise<AdsenseListResult> {
const response = await axios.post('/ad/position/getList', query)
const response = await axios.post('/api/admin/ad/position/getList', query)
const data = response.data as { list?: AdsenseRow[]; total?: number }
return {
......@@ -29,7 +29,7 @@ export async function changePositionStatus({
id: number
status: '0' | '1'
}): Promise<boolean> {
const response = await axios.post('/ad/position/changeStatus', { id, status })
const response = await axios.post('/api/admin/ad/position/changeStatus', { id, status })
notifySuccess(response.message)
return true
}
......@@ -37,33 +37,36 @@ async function mutate(url: string, params: ApiParams) {
}
export async function getAdvertList(query: AdvertQuery): Promise<AdvertListResult> {
const data = await getPayload<{ list?: AdvertRow[]; total?: number }>('/ad/ad/getList', {
const data = await getPayload<{ list?: AdvertRow[]; total?: number }>(
'/api/admin/ad/ad/getList',
{
...query,
})
},
)
return { rows: data.list ?? [], total: data.total ?? 0 }
}
export function getAdvertInfo(id: number) {
return getPayload<AdvertRow>('/ad/ad/getInfoById', { id })
return getPayload<AdvertRow>('/api/admin/ad/ad/getInfoById', { id })
}
export function addAdvert(params: ApiParams) {
return mutate('/ad/ad/add', params)
return mutate('/api/admin/ad/ad/add', params)
}
export function editAdvert(params: ApiParams) {
return mutate('/ad/ad/edit', params)
return mutate('/api/admin/ad/ad/edit', params)
}
export function deleteAdvert(id: number) {
return mutate('/ad/ad/del', { id })
return mutate('/api/admin/ad/ad/del', { id })
}
export function changeAdvertStatus(id: number, status: '0' | '1') {
return mutate('/ad/ad/changeStatus', { id, status })
return mutate('/api/admin/ad/ad/changeStatus', { id, status })
}
export async function getAdvertPositions(): Promise<AdvertPosition[]> {
const result = await getPayload<{ list?: AdvertPosition[] }>('/ad/position/getList', {})
const result = await getPayload<{ list?: AdvertPosition[] }>('/api/admin/ad/position/getList', {})
return (result.list ?? []) as AdvertPosition[]
}
......@@ -4,7 +4,9 @@ const LOGIN_SUCCESS_MESSAGE = 'OK'
export const login = async (params) => {
try {
const response = await axios.post('/user/login', params, { resolveBusinessError: true })
const response = await axios.post('/api/admin/user/login', params, {
resolveBusinessError: true,
})
const data = response.data ?? {}
if (response.code === 200 && response.message === LOGIN_SUCCESS_MESSAGE && data.token) {
......
......@@ -142,8 +142,8 @@ const Login = () => {
Icon22,
Icon23,
],
initUrl: '/user/login/initData',
verifyUrl: '/user/login/checkVerify',
initUrl: '/api/admin/user/login/initData',
verifyUrl: '/api/admin/user/login/checkVerify',
getSuccessState: function (data) {
console.log('验证成功', data)
setDisLf(data.disLf)
......
......@@ -5,7 +5,8 @@ async function post(path: string, data: Record<string, unknown>) {
return { data: response.data, msg: response.message || '' }
}
export const getAuditList = async (query: AuditQuery) =>
(await post('/book/teacher/getList/examine', query)).data
(await post('/api/admin/book/teacher/getList/examine', query)).data
export const getExamineList = async (id: string | number) =>
(await post('/book/teacher/getExamineList', { id })).data
export const addExamine = (data: Record<string, unknown>) => post('/book/examine/add', data)
(await post('/api/admin/book/teacher/getExamineList', { id })).data
export const addExamine = (data: Record<string, unknown>) =>
post('/api/admin/book/examine/add', data)
......@@ -5,11 +5,15 @@ async function post(path: string, data: Record<string, unknown>, notify = false)
if (notify) notifySuccess(response.message)
return response.data
}
export const getClassifyList = (query: ClassifyQuery) => post('/book/category/getList', query)
export const addCategory = (data: Record<string, unknown>) => post('/book/category/add', data, true)
export const getCategory = (id: string | number) => post('/book/category/getInfoById', { id })
export const getClassifyList = (query: ClassifyQuery) =>
post('/api/admin/book/category/getList', query)
export const addCategory = (data: Record<string, unknown>) =>
post('/api/admin/book/category/add', data, true)
export const getCategory = (id: string | number) =>
post('/api/admin/book/category/getInfoById', { id })
export const editCategory = (data: Record<string, unknown>) =>
post('/book/category/edit', data, true)
export const deleteCategory = (id: string | number) => post('/book/category/del', { id }, true)
post('/api/admin/book/category/edit', data, true)
export const deleteCategory = (id: string | number) =>
post('/api/admin/book/category/del', { id }, true)
export const changeCategorySort = (data: Record<string, unknown>) =>
post('/book/category/changeSort', data, true)
post('/api/admin/book/category/changeSort', data, true)
......@@ -6,11 +6,12 @@ async function post(path: string, data: Record<string, unknown>, notify = false)
return response.data
}
export const getDiscussionList = (q: DiscussionQuery) =>
post('/comment/comment/getAllCommentList', q)
post('/api/admin/comment/comment/getAllCommentList', q)
export const deleteComment = (id: any, book_id: any) =>
post('/comment/comment/delComment', { id, book_id }, true)
export const changeCommentStatus = (data: any) => post('/comment/comment/changeStatus', data, true)
export const getBooks = () => post('/book/book/getAllList', {})
export const getDiscussionSystem = () => post('/system/config/getSystem', { type: 3 })
post('/api/admin/comment/comment/delComment', { id, book_id }, true)
export const changeCommentStatus = (data: any) =>
post('/api/admin/comment/comment/changeStatus', data, true)
export const getBooks = () => post('/api/admin/book/book/getAllList', {})
export const getDiscussionSystem = () => post('/api/admin/system/config/getSystem', { type: 3 })
export const editDiscussionSystem = (set_value: string) =>
post('/system/config/systemEdit', { type: 3, set_value }, true)
post('/api/admin/system/config/systemEdit', { type: 3, set_value }, true)
......@@ -5,9 +5,11 @@ async function post(path: string, data: Record<string, unknown>, notify = false)
if (notify) notifySuccess(response.message)
return response.data
}
export const getLabelList = (query: LabelQuery) => post('/book/label/getList', query)
export const getAllBooks = () => post('/book/label/getAllBooks', {})
export const getLabel = (id: string | number) => post('/book/label/getInfoById', { id })
export const addLabel = (data: Record<string, unknown>) => post('/book/label/add', data, true)
export const editLabel = (data: Record<string, unknown>) => post('/book/label/edit', data, true)
export const deleteLabel = (id: string | number) => post('/book/label/del', { id }, true)
export const getLabelList = (query: LabelQuery) => post('/api/admin/book/label/getList', query)
export const getAllBooks = () => post('/api/admin/book/label/getAllBooks', {})
export const getLabel = (id: string | number) => post('/api/admin/book/label/getInfoById', { id })
export const addLabel = (data: Record<string, unknown>) =>
post('/api/admin/book/label/add', data, true)
export const editLabel = (data: Record<string, unknown>) =>
post('/api/admin/book/label/edit', data, true)
export const deleteLabel = (id: string | number) => post('/api/admin/book/label/del', { id }, true)
......@@ -46,39 +46,39 @@ export async function getManagementList(
query: ManagementQuery,
): Promise<ManagementListResult | false> {
const data = await getPayload<{ list?: ManagementRow[]; total?: number }>(
'/book/teacher/getList',
'/api/admin/book/teacher/getList',
{ ...query },
)
return { rows: data.list ?? [], total: data.total ?? 0 }
}
export function deleteManagementBook(id: number) {
return mutate('/book/teacher/del', { id })
return mutate('/api/admin/book/teacher/del', { id })
}
// 编辑页仍沿用旧接口封装的返回值形状;保留在管理模块中,避免页面反向依赖旧目录。
export function getBook(params: ApiParams) {
return axios.post('/api/book/teacher/getInfoById', params)
return axios.post('/api/admin/book/teacher/getInfoById', params)
}
export function addBook(params: ApiParams) {
return axios.post('/api/book/teacher/add', params)
return axios.post('/api/admin/book/teacher/add', params)
}
export function updateBook(params: ApiParams) {
return axios.post('/api/book/teacher/edit', params)
return axios.post('/api/admin/book/teacher/edit', params)
}
export function exportManagementBook(id: number) {
return getPayload<ExportBookResult>('/book/teacher/export', { id })
return getPayload<ExportBookResult>('/api/admin/book/teacher/export', { id })
}
export function releaseManagementBook(id: number, releaseMess?: string) {
return mutate('/book/teacher/release', { release_mess: releaseMess, id })
return mutate('/api/admin/book/teacher/release', { release_mess: releaseMess, id })
}
export async function importManagementBook(values: ImportBookValues) {
const response = await axios.post('/book/teacher/import', values, {
const response = await axios.post('/api/admin/book/teacher/import', values, {
headers: { 'Content-Type': 'multipart/form-data' },
})
notifySuccess(response.message)
......
......@@ -9,47 +9,47 @@ const mutate = async (path, params, notify = false) => {
// 获取书籍详情
export function getBook(data) {
return axios.post('/api/book/teacher/getInfoById', data)
return axios.post('/api/admin/book/teacher/getInfoById', data)
}
// 获取章节列表
export function getChapterList(data) {
return axios.post('/api/book/teacher/chapter/getAllList', data)
return axios.post('/api/admin/book/teacher/chapter/getAllList', data)
}
// 获取章节详情
export function getChapter(data) {
return axios.post('/api/book/teacher/chapter/getInfoById', data)
return axios.post('/api/admin/book/teacher/chapter/getInfoById', data)
}
// 添加章节
export function addChapter(data) {
return axios.post('/api/book/teacher/chapter/add', data)
return axios.post('/api/admin/book/teacher/chapter/add', data)
}
// 修改章节
export function updateChapter(data) {
return axios.post('/api/book/teacher/chapter/edit', data)
return axios.post('/api/admin/book/teacher/chapter/edit', data)
}
// 删除章节
export function delChapter(data) {
return axios.post('/api/book/teacher/chapter/del', data)
return axios.post('/api/admin/book/teacher/chapter/del', data)
}
// 章节排序
export function sortChapter(data) {
return axios.post('/api/book/teacher/chapter/dragOrder', data)
return axios.post('/api/admin/book/teacher/chapter/dragOrder', data)
}
// 获取章节编辑者信息
export function getChapterEditors(data) {
return axios.post('/api/book/teacher/chapter/getEditors', data)
return axios.post('/api/admin/book/teacher/chapter/getEditors', data)
}
// 保存章节编辑者
export function updateChapterEditors(data) {
return axios.post('/api/book/teacher/chapter/saveEditors', data)
return axios.post('/api/admin/book/teacher/chapter/saveEditors', data)
}
// 知识图谱列表
......@@ -62,26 +62,30 @@ export function bindTag(data) {
return axios.post('/api/resource/v1/course/course/tag-add-book', data)
}
export const getUserInfo = (params) => getData('/user/user/getUserInfo', params)
export const getPowerByRoleId = (params) => getData('/user/role/getPowerByRoleId', params)
export const getUserInfo = (params) => getData('/api/admin/user/user/getUserInfo', params)
export const getPowerByRoleId = (params) => getData('/api/admin/user/role/getPowerByRoleId', params)
export const getAllList = async (params) => {
const response = await axios.post('/book/teacher/chapter/getAllList', params)
const response = await axios.post('/api/admin/book/teacher/chapter/getAllList', params)
return { code: response.code, data: response.data }
}
export const chapterAdd = (params) => getData('/book/teacher/chapter/add', params)
export const getInfoById = (params) => getData('/book/teacher/chapter/getInfoById', params)
export const chapterEdit = (params) => mutate('/book/teacher/chapter/edit', params, true)
export const chapterDel = (params) => mutate('/book/teacher/chapter/del', params, true)
export const dragOrder = (params) => mutate('/book/teacher/chapter/dragOrder', params, true)
export const chapterAdd = (params) => getData('/api/admin/book/teacher/chapter/add', params)
export const getInfoById = (params) =>
getData('/api/admin/book/teacher/chapter/getInfoById', params)
export const chapterEdit = (params) => mutate('/api/admin/book/teacher/chapter/edit', params, true)
export const chapterDel = (params) => mutate('/api/admin/book/teacher/chapter/del', params, true)
export const dragOrder = (params) =>
mutate('/api/admin/book/teacher/chapter/dragOrder', params, true)
export const getInfoByChapterId = (params) =>
getData('/book/teacher/section/getInfoByChapterId', params)
export const sectionAdd = (params) => mutate('/book/teacher/section/add', params, true)
export const sectionEdit = (params) => mutate('/book/teacher/section/edit', params)
export const getQuestionList = (params) => getData('/book/chapter/question/getQuestionList', params)
export const questionDel = (params) => getData('/book/chapter/question/del', params)
export const questionAdd = (params) => getData('/book/chapter/question/add', params)
export const release = (params) => mutate('/book/teacher/release', params)
export const templateGetList = (params) => mutate('/book/template/getList', params)
export const templateImport = (params) => mutate('/book/template/import', params)
export const teacherImport = (params) => mutate('/book/teacher/import', params)
export const getRecordList = (params) => getData('/book/teacher/section/getRecordList', params)
getData('/api/admin/book/teacher/section/getInfoByChapterId', params)
export const sectionAdd = (params) => mutate('/api/admin/book/teacher/section/add', params, true)
export const sectionEdit = (params) => mutate('/api/admin/book/teacher/section/edit', params)
export const getQuestionList = (params) =>
getData('/api/admin/book/chapter/question/getQuestionList', params)
export const questionDel = (params) => getData('/api/admin/book/chapter/question/del', params)
export const questionAdd = (params) => getData('/api/admin/book/chapter/question/add', params)
export const release = (params) => mutate('/api/admin/book/teacher/release', params)
export const templateGetList = (params) => mutate('/api/admin/book/template/getList', params)
export const templateImport = (params) => mutate('/api/admin/book/template/import', params)
export const teacherImport = (params) => mutate('/api/admin/book/teacher/import', params)
export const getRecordList = (params) =>
getData('/api/admin/book/teacher/section/getRecordList', params)
......@@ -82,7 +82,6 @@
.ant-spin-container {
height: 100%;
}
}
}
.draggable-tree {
......
......@@ -25,7 +25,7 @@ export default function ExportBookModal({ open, onCancel, onConfirm }: ExportBoo
okText='确认'
>
<p style={{ textAlign: 'center', padding: '30px 0 18px', fontSize: 16 }}>
只能导出文本部分,画廊、音频、视频不支持
将导出书籍正文、图片、画廊、扩展阅读、练习题、链接、气泡提示、视频和音频
</p>
</Modal>
)
......
......@@ -39,9 +39,7 @@ const BookAddEdit = () => {
const editors = values.editors?.map((item) => item.value) || []
const params = { ...values, img, editors }
if (editors.length === 0) delete params.editors
const res = isEdit
? await updateBook({ id: bookId, ...params })
: await addBook(params)
const res = isEdit ? await updateBook({ id: bookId, ...params }) : await addBook(params)
message.success(res.message)
navigate(-1)
}
......
import { describe, expect, it } from 'vitest'
import { normalizeBookExportHtml } from './normalizeBookExportHtml'
describe('normalizeBookExportHtml', () => {
it('turns editor-only blocks into static Word content', async () => {
const html = `
<div data-export-chapter-id="8">
<div class="chapter-gallery-container" data-galleryList="${encodeURIComponent(JSON.stringify([{ url: 'https://cdn.example.com/a.png', title: '图片说明' }]))}" data-title="画廊标题"></div>
<div class="chapter-expand" data-w-e-type="chapterExpandRead" data-random="expand-1" data-name="扩展阅读"></div>
<div class="chapter-practice" data-w-e-type="chapterPractice" data-practiceNum="practice-1"></div>
<span class="chapter-item-link" data-w-e-type="chapterLink" data-title="资料链接" data-linktype="1" data-link="https://example.com"></span>
<span class="chapter-item-tooltip" data-w-e-type="chapterTooltip" data-title="术语" data-content="术语解释"></span>
<div class="chapter-video-container" data-w-e-type="chapterVideo" data-title="课程视频" data-videoUrl="https://example.com/video.mp4"></div>
</div>
`
const result = await normalizeBookExportHtml(html, {
bookId: 7,
fetchExpandRead: async (params) => {
expect(params).toEqual({ book_id: 7, chapter_id: 8, position: 'expand-1' })
return { content: '<p>扩展正文</p>' }
},
fetchPracticeQuestions: async (params) => {
expect(params).toEqual({ book_id: 7, chapter_id: 8, position: 'practice-1' })
return [
{
titles: '单选题题干',
question_style: 1,
question_list: JSON.stringify([{ option: '选项 A' }, { option: '选项 B' }]),
answers: 'A',
analysis: '题目解析',
},
]
},
})
expect(result).toContain('画廊标题')
expect(result).toContain('https://cdn.example.com/a.png')
expect(result).toContain('图片说明')
expect(result).toContain('扩展正文')
expect(result).toContain('单选题题干')
expect(result).toContain('选项 A')
expect(result).toContain('答案:A')
expect(result).toContain('资料链接')
expect(result).toContain('术语解释')
expect(result).toContain('课程视频')
expect(result).toContain('https://example.com/video.mp4')
expect(result).not.toContain('chapter-practice')
expect(result).not.toContain('<video')
})
})
export interface ExportPracticeQuestion {
titles?: string
question_style?: number | string
question_list?: string | unknown[]
answers?: string
analysis?: string
}
interface ExportParams {
book_id: number
chapter_id: number
position: string
}
interface NormalizeBookExportOptions {
bookId: number
fetchExpandRead: (params: ExportParams) => Promise<{ content?: string } | null | undefined>
fetchPracticeQuestions: (params: ExportParams) => Promise<ExportPracticeQuestion[]>
}
function getChapterId(element: Element): number | null {
const container = element.closest('[data-export-chapter-id]')
const chapterId = Number(container?.getAttribute('data-export-chapter-id'))
return Number.isInteger(chapterId) && chapterId > 0 ? chapterId : null
}
function createTextElement<K extends keyof HTMLElementTagNameMap>(
tagName: K,
text: string,
className?: string,
): HTMLElementTagNameMap[K] {
const element = document.createElement(tagName)
element.textContent = text
if (className) element.className = className
return element
}
function parseGalleryList(value: string | null): Array<{ url?: string; title?: string }> {
if (!value) return []
try {
return JSON.parse(decodeURIComponent(value))
} catch {
try {
return JSON.parse(value)
} catch {
return []
}
}
}
function replaceGallery(element: Element) {
const section = document.createElement('section')
section.className = 'word-export-gallery'
const title = element.getAttribute('data-title')?.trim()
if (title) section.append(createTextElement('h4', title))
parseGalleryList(element.getAttribute('data-galleryList')).forEach((item) => {
if (!item.url) return
const figure = document.createElement('figure')
const image = document.createElement('img')
image.src = item.url
image.alt = item.title ?? ''
figure.append(image)
if (item.title) figure.append(createTextElement('figcaption', item.title))
section.append(figure)
})
element.replaceWith(section)
}
function replaceLink(element: Element) {
const title = element.getAttribute('data-title') || element.textContent?.trim() || '链接'
const link = element.getAttribute('data-link')
if (link) {
const anchor = document.createElement('a')
anchor.href = link
anchor.textContent = title
element.replaceWith(anchor)
return
}
element.replaceWith(createTextElement('span', title))
}
function replaceTooltip(element: Element) {
const title = element.getAttribute('data-title') || element.textContent?.trim() || '提示'
const content = element.getAttribute('data-content') || ''
const note = createTextElement('span', `${title}(提示:${content})`, 'word-export-tooltip')
element.replaceWith(note)
}
function replaceMedia(element: Element, type: '视频' | '音频', urlAttribute: string) {
const title = element.getAttribute('data-title') || `${type}内容`
const url =
element.getAttribute(urlAttribute) ||
element.querySelector(type === '视频' ? 'video' : 'audio')?.getAttribute('src') ||
''
const wrapper = document.createElement('p')
wrapper.className = 'word-export-media'
wrapper.append(`${type}:${title}`)
if (url) {
const anchor = document.createElement('a')
anchor.href = url
anchor.textContent = '(点击播放)'
wrapper.append(anchor)
}
element.replaceWith(wrapper)
}
function parseQuestionList(questionList: ExportPracticeQuestion['question_list']): unknown[] {
if (Array.isArray(questionList)) return questionList
if (typeof questionList !== 'string') return []
try {
const value = JSON.parse(questionList)
return Array.isArray(value) ? value : []
} catch {
return []
}
}
function formatOption(option: unknown): string {
if (typeof option === 'string') return option
if (option && typeof option === 'object') {
const value = option as { option?: string; title?: string; content?: string; label?: string }
return value.option ?? value.title ?? value.content ?? value.label ?? ''
}
return ''
}
function createHtmlParagraph(prefix: string, html: string) {
const paragraph = document.createElement('p')
paragraph.append(prefix)
const content = new DOMParser().parseFromString(html, 'text/html')
while (content.body.firstChild) paragraph.append(content.body.firstChild)
return paragraph
}
function createPracticeSection(title: string, questions: ExportPracticeQuestion[]) {
const section = document.createElement('section')
section.className = 'word-export-practice'
section.append(createTextElement('h4', title || '练习题'))
questions.forEach((question, index) => {
section.append(createHtmlParagraph(`${index + 1}. `, question.titles ?? ''))
parseQuestionList(question.question_list).forEach((option, optionIndex) => {
const text = formatOption(option)
if (text)
section.append(createHtmlParagraph(`${String.fromCharCode(65 + optionIndex)}. `, text))
})
if (question.answers) section.append(createTextElement('p', `答案:${question.answers}`))
if (question.analysis) section.append(createTextElement('p', `解析:${question.analysis}`))
})
return section
}
async function replaceExpandRead(element: Element, options: NormalizeBookExportOptions) {
const chapterId = getChapterId(element)
const title =
element.getAttribute('data-title') || element.getAttribute('data-name') || '扩展阅读'
const random = element.getAttribute('data-random') || ''
const section = document.createElement('section')
section.className = 'word-export-expand'
section.append(createTextElement('h4', title))
if (chapterId && random) {
const result = await options.fetchExpandRead({
book_id: options.bookId,
chapter_id: chapterId,
position: random,
})
if (result?.content) {
const content = new DOMParser().parseFromString(result.content, 'text/html')
while (content.body.firstChild) section.append(content.body.firstChild)
}
}
element.replaceWith(section)
}
async function replacePractice(element: Element, options: NormalizeBookExportOptions) {
const chapterId = getChapterId(element)
const practiceNum = element.getAttribute('data-practiceNum') || ''
const title = element.getAttribute('data-title') || '练习题'
const questions =
chapterId && practiceNum
? await options.fetchPracticeQuestions({
book_id: options.bookId,
chapter_id: chapterId,
position: practiceNum,
})
: []
element.replaceWith(createPracticeSection(title, questions))
}
/** Convert WangEditor custom blocks into static HTML that Word can render. */
export async function normalizeBookExportHtml(
htmlString: string,
options: NormalizeBookExportOptions,
): Promise<string> {
const doc = new DOMParser().parseFromString(htmlString, 'text/html')
doc.body.querySelectorAll('.chapter-gallery-container').forEach(replaceGallery)
doc.body.querySelectorAll('[data-w-e-type="chapterLink"]').forEach(replaceLink)
doc.body.querySelectorAll('[data-w-e-type="chapterTooltip"]').forEach(replaceTooltip)
doc.body
.querySelectorAll(
'[data-w-e-type="chapterVideo"], [data-w-e-type="video"], .chapter-video-container',
)
.forEach((element) => replaceMedia(element, '视频', 'data-videoUrl'))
doc.body
.querySelectorAll('[data-w-e-type="chapterAudio"], .chapter-audio-container')
.forEach((element) => replaceMedia(element, '音频', 'data-audioUrl'))
await Promise.all(
Array.from(
doc.body.querySelectorAll('[data-w-e-type="chapterExpandRead"], .chapter-expand'),
).map((element) => replaceExpandRead(element, options)),
)
await Promise.all(
Array.from(
doc.body.querySelectorAll('[data-w-e-type="chapterPractice"], .chapter-practice'),
).map((element) => replacePractice(element, options)),
)
return doc.body.innerHTML
}
......@@ -5,6 +5,7 @@ import { useState } from 'react'
import { useNavigate } from 'react-router'
import { AppList, type AppListFilterItem } from '@/components/AppList'
import { expandReadInfo, getChapterTopic } from '@/components/editor/wangeditor-customer/public-api'
import { exportHtmlToWord } from '@/utils/exportWordHtmlDocx'
import { usePermission } from '@/hooks/usePermission'
import { useUserStore } from '@/stores/userStore'
......@@ -19,6 +20,7 @@ import {
} from '../api'
import DeleteBookModal from '../components/DeleteBookModal'
import ExportBookModal from '../components/ExportBookModal'
import { normalizeBookExportHtml } from '../export/normalizeBookExportHtml'
import ImportBookDrawer from '../components/ImportBookDrawer'
import ReleaseBookDrawer, { type ReleaseBookFormValues } from '../components/ReleaseBookDrawer'
import type { ManagementFilters } from '../query'
......@@ -26,15 +28,6 @@ import { useManagementList } from '../useManagementList'
const { RangePicker } = DatePicker
const IMAGE_FALLBACK = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='
const EXPORT_REMOVE_SELECTORS = [
'.chapter-gallery-container',
'.chapter-expand',
'.chapter-practice',
'.chapter-item-link',
'.chapter-item-tooltip',
"div[data-w-e-type='video']",
]
export default function ManagementListView() {
const list = useManagementList()
const navigate = useNavigate()
......@@ -61,7 +54,12 @@ export default function ManagementListView() {
try {
const result = await exportManagementBook(selectedId)
if (!result.name) return
await exportHtmlToWord(result.name, result.content ?? '', EXPORT_REMOVE_SELECTORS)
const content = await normalizeBookExportHtml(result.content ?? '', {
bookId: selectedId,
fetchExpandRead: expandReadInfo,
fetchPracticeQuestions: getChapterTopic,
})
await exportHtmlToWord(result.name, content)
} catch (error) {
console.error('导出Word失败', error)
} finally {
......@@ -152,9 +150,7 @@ export default function ManagementListView() {
</Button>
) : null}
{checkPermission('/books/management/getInfoById') ? (
<Button
onClick={() => navigate(`/books/management/chapter?book_id=${record.id}`)}
>
<Button onClick={() => navigate(`/books/management/chapter?book_id=${record.id}`)}>
编辑
</Button>
) : null}
......
......@@ -60,18 +60,21 @@ async function getPayload<T>(url: string, params?: ApiParams): Promise<T> {
}
export async function getOrders(query: OrdersQuery): Promise<OrdersListResult> {
const data = await getPayload<{ list?: OrderRow[]; total?: number }>('/order/getOrderList', {
const data = await getPayload<{ list?: OrderRow[]; total?: number }>(
'/api/admin/order/getOrderList',
{
...query,
})
},
)
return { rows: data.list ?? [], total: data.total ?? 0 }
}
export function getSalesStatistics() {
return getPayload<SalesStatisticsResult>('/order/salesStatistics')
return getPayload<SalesStatisticsResult>('/api/admin/order/salesStatistics')
}
export async function refundOrder(id: number, refund_reason?: string) {
const response = await axios.post('/order/refund', { id, refund_reason })
const response = await axios.post('/api/admin/order/refund', { id, refund_reason })
notifySuccess(response.message)
return true
}
......@@ -7,22 +7,26 @@ async function post(path: string, data: Record<string, unknown> = {}, notify = f
return response.data
}
export const getQuestionList = (query: QuestionBankQuery) => post('/book/question/getList', query)
export const getQuestion = (id: string | number) => post('/book/question/getInfoById', { id })
export const addQuestion = (data: Record<string, unknown>) => post('/book/question/add', data, true)
export const getQuestionList = (query: QuestionBankQuery) =>
post('/api/admin/book/question/getList', query)
export const getQuestion = (id: string | number) =>
post('/api/admin/book/question/getInfoById', { id })
export const addQuestion = (data: Record<string, unknown>) =>
post('/api/admin/book/question/add', data, true)
export const editQuestion = (data: Record<string, unknown>) =>
post('/book/question/edit', data, true)
export const deleteQuestion = (id: string | number) => post('/book/question/del', { id }, true)
post('/api/admin/book/question/edit', data, true)
export const deleteQuestion = (id: string | number) =>
post('/api/admin/book/question/del', { id }, true)
export const addQuestionType = (typename: string) =>
post('/book/question/type/add', { typename }, true)
post('/api/admin/book/question/type/add', { typename }, true)
export const getQuestionTypes = () =>
post('/book/question/type/getList', { page: 1, page_size: 999 })
export const getBookNames = () => post('book/teacher/getListAll')
export const getAliOssToken = () => post('/common/OssUpload/getUploadToken')
post('/api/admin/book/question/type/getList', { page: 1, page_size: 999 })
export const getBookNames = () => post('/api/admin/book/teacher/getListAll')
export const getAliOssToken = () => post('/api/admin/common/OssUpload/getUploadToken')
export async function importQuestions(file: File) {
const form = new FormData()
form.append('file', file)
const response = await axios.post('/book/question/import', form, {
const response = await axios.post('/api/admin/book/question/import', form, {
headers: { 'Content-Type': 'multipart/form-data' },
})
return { msg: response.message, data: response.data }
......
......@@ -10,11 +10,7 @@ async function requestQuestionBankList(query: ReturnType<typeof buildQuestionBan
}
export function useQuestionBankList() {
const list = usePagedList<
any,
QuestionBankFilters,
ReturnType<typeof buildQuestionBankQuery>
>({
const list = usePagedList<any, QuestionBankFilters, ReturnType<typeof buildQuestionBankQuery>>({
initialFilters: {},
buildQuery: ({ page, pageSize, filters }) => buildQuestionBankQuery(page, pageSize, filters),
request: requestQuestionBankList,
......
......@@ -280,7 +280,7 @@ export default function QuestionBankView() {
<Button
type='primary'
ghost
onClick={() => downloadFile(`temp/${importResult.data?.name}`)}
onClick={() => downloadFile(`/api/admin/temp/${importResult.data?.name}`)}
>
下载错误文件
</Button>
......
......@@ -8,31 +8,31 @@ async function post<T>(path: string, data: unknown): Promise<T> {
}
export const getReviewComments = (bookId: number, chapterId?: number) =>
post<ReviewComment[]>('/book/teacher/review-comment/getList', {
post<ReviewComment[]>('/api/admin/book/teacher/review-comment/getList', {
book_id: bookId,
...(chapterId ? { chapter_id: chapterId } : {}),
})
export const getCurrentReviewComments = (bookId: number, chapterId?: number) =>
post<ReviewComment[]>('/book/teacher/review-comment/current', {
post<ReviewComment[]>('/api/admin/book/teacher/review-comment/current', {
book_id: bookId,
...(chapterId ? { chapter_id: chapterId } : {}),
})
export const addReviewComment = (data: AddReviewCommentPayload) =>
post<{ id: number }>('/book/teacher/review-comment/add', data)
post<{ id: number }>('/api/admin/book/teacher/review-comment/add', data)
export const deleteReviewComment = (id: number) =>
post<void>('/book/teacher/review-comment/del', { id })
post<void>('/api/admin/book/teacher/review-comment/del', { id })
export const resolveReviewComment = (id: number) =>
post<void>('/book/teacher/review-comment/resolve', { id })
post<void>('/api/admin/book/teacher/review-comment/resolve', { id })
export const reopenReviewComment = (id: number) =>
post<void>('/book/teacher/review-comment/reopen', { id })
post<void>('/api/admin/book/teacher/review-comment/reopen', { id })
export const replyReviewComment = (data: ReplyReviewCommentPayload) =>
post<{ id: number }>('/book/teacher/review-comment/reply', data)
post<{ id: number }>('/api/admin/book/teacher/review-comment/reply', data)
export const deleteReviewCommentReply = (id: number) =>
post<void>('/book/teacher/review-comment/reply/del', { id })
post<void>('/api/admin/book/teacher/review-comment/reply/del', { id })
......@@ -5,26 +5,29 @@ async function post(path: string, data: Record<string, unknown>, notify = false)
if (notify) notifySuccess(response.message)
return response.data
}
export const getSaleList = (query: SaleQuery) => post('/book/book/getList', query)
export const getSaleList = (query: SaleQuery) => post('/api/admin/book/book/getList', query)
export const changeSaleStatus = (id: string | number, status: string) =>
post('/book/book/changeStatus', { id, status }, true)
post('/api/admin/book/book/changeStatus', { id, status }, true)
export const getSaleDiscussionList = (query: SaleDiscussionQuery) =>
post('/comment/comment/getCommentList', query)
post('/api/admin/comment/comment/getCommentList', query)
export const getListAll = (data: Record<string, unknown>) => post('/book/chapter/getListAll', data)
export const getInfoById = (data: Record<string, unknown>) => post('/book/book/getInfoById', data)
export const getListAll = (data: Record<string, unknown>) =>
post('/api/admin/book/chapter/getListAll', data)
export const getInfoById = (data: Record<string, unknown>) =>
post('/api/admin/book/book/getInfoById', data)
export const publisherList = (data: Record<string, unknown>) =>
post('/book/publisher/getListAll', data)
post('/api/admin/book/publisher/getListAll', data)
export const publisherAdd = (data: Record<string, unknown>) =>
post('/book/publisher/add', data, true)
export const editBook = (data: Record<string, unknown>) => post('/book/book/edit', data, true)
post('/api/admin/book/publisher/add', data, true)
export const editBook = (data: Record<string, unknown>) =>
post('/api/admin/book/book/edit', data, true)
export const getCommentInfo = (data: Record<string, unknown>) =>
post('/comment/comment/getCommentInfo', data)
post('/api/admin/comment/comment/getCommentInfo', data)
export const commentDel = async (data: Record<string, unknown>) => {
await post('/comment/comment/del', data, true)
await post('/api/admin/comment/comment/del', data, true)
return true
}
export const commentReply = async (data: Record<string, unknown>) => {
await post('/comment/comment/reply', data, true)
await post('/api/admin/comment/comment/reply', data, true)
return true
}
......@@ -150,7 +150,7 @@ const DiscussDetail = () => {
} else if (type === 'text') {
return <span key={index}>{value}</span>
} else if (type === 'image') {
return <img height={100} src={value}></img>
return <img key={index} height={100} src={value}></img>
}
})}
</div>
......
......@@ -98,8 +98,7 @@ export default function SaleListView() {
title: '操作',
render: (_, row) => (
<Space>
{Boolean(row.status) &&
checkPermission('/books/sale/getCommentList') && (
{Boolean(row.status) && checkPermission('/books/sale/getCommentList') && (
<Button
onClick={() =>
navigate(`discussP?book_id=${row.id}&book_name=${encodeURIComponent(row.name)}`)
......
......@@ -39,32 +39,32 @@ async function mutate(url: string, params: ApiParams) {
export async function getMemberLevelList(params: MemberLevelQuery): Promise<MemberLevelListResult> {
const data = await getPayload<{ list?: MemberLevelRow[]; total?: number }>(
'/members/grade/getList',
'/api/admin/members/grade/getList',
{ ...params },
)
return { rows: data.list ?? [], total: data.total ?? 0 }
}
export function getMemberLevelInfo(id: number) {
return getPayload<MemberLevelRow>('/members/grade/getInfoById', { id })
return getPayload<MemberLevelRow>('/api/admin/members/grade/getInfoById', { id })
}
export function addMemberLevel(values: MemberLevelFormValues) {
return mutate('/members/grade/add', { ...values })
return mutate('/api/admin/members/grade/add', { ...values })
}
export function editMemberLevel(id: number, values: MemberLevelFormValues) {
return mutate('/members/grade/edit', { ...values, id })
return mutate('/api/admin/members/grade/edit', { ...values, id })
}
export function deleteMemberLevel(id: number) {
return mutate('/members/grade/del', { id })
return mutate('/api/admin/members/grade/del', { id })
}
export function getMemberLevelSystem() {
return getPayload<MemberLevelSystemConfig>('/system/config/getSystem', { type: 2 })
return getPayload<MemberLevelSystemConfig>('/api/admin/system/config/getSystem', { type: 2 })
}
export function editMemberLevelSystem(setValue: string | number | undefined) {
return mutate('/system/config/systemEdit', { type: 2, set_value: setValue })
return mutate('/api/admin/system/config/systemEdit', { type: 2, set_value: setValue })
}
......@@ -11,14 +11,12 @@ function isCleared(previous: MemberLevelFilters, next: MemberLevelFilters) {
}
export function useMemberLevelList() {
return usePagedList<
MemberLevelRow,
MemberLevelFilters,
ReturnType<typeof buildMemberLevelQuery>
>({
return usePagedList<MemberLevelRow, MemberLevelFilters, ReturnType<typeof buildMemberLevelQuery>>(
{
initialFilters: {},
buildQuery: buildMemberLevelQuery,
request: getMemberLevelList,
shouldReloadOnFilterChange: isCleared,
})
},
)
}
......@@ -7,12 +7,16 @@ async function post(path: string, data: Record<string, unknown>, notify = false)
return response.data
}
export const getMemberList = (query: MemberQuery) => post('/members/members/getList', query)
export const getMemberInfo = (id: string | number) => post('/members/members/getInfoById', { id })
export const addMember = (data: Record<string, unknown>) => post('/members/members/add', data, true)
export const getMemberList = (query: MemberQuery) =>
post('/api/admin/members/members/getList', query)
export const getMemberInfo = (id: string | number) =>
post('/api/admin/members/members/getInfoById', { id })
export const addMember = (data: Record<string, unknown>) =>
post('/api/admin/members/members/add', data, true)
export const editMember = (data: Record<string, unknown>) =>
post('/members/members/edit', data, true)
export const deleteMember = (id: string | number) => post('/members/members/del', { id }, true)
post('/api/admin/members/members/edit', data, true)
export const deleteMember = (id: string | number) =>
post('/api/admin/members/members/del', { id }, true)
export type MemberRecordQuery = {
id: string | number
......@@ -20,8 +24,9 @@ export type MemberRecordQuery = {
page_size: number
}
export const getOrders = (query: MemberRecordQuery) => post('/members/members/getOrdersList', query)
export const getOrders = (query: MemberRecordQuery) =>
post('/api/admin/members/members/getOrdersList', query)
export const getIntegralRecords = (query: MemberRecordQuery) =>
post('/members/members/getIntegralRecordList', query)
post('/api/admin/members/members/getIntegralRecordList', query)
export const getCouponRecords = (query: MemberRecordQuery) =>
post('/members/members/getCouponList', query)
post('/api/admin/members/members/getCouponList', query)
......@@ -7,8 +7,8 @@ const saveData = async (path, params) => {
return response.data
}
export const getList = (params) => getData('/system/version/getList', params)
export const addVersion = (params) => saveData('/system/version/add', params)
export const infoVersion = (params) => getData('/system/version/getInfoById', params)
export const editVersion = (params) => saveData('/system/version/edit', params)
export const delVersion = (params) => saveData('/system/version/del', params)
export const getList = (params) => getData('/api/admin/system/version/getList', params)
export const addVersion = (params) => saveData('/api/admin/system/version/add', params)
export const infoVersion = (params) => getData('/api/admin/system/version/getInfoById', params)
export const editVersion = (params) => saveData('/api/admin/system/version/edit', params)
export const delVersion = (params) => saveData('/api/admin/system/version/del', params)
......@@ -9,6 +9,7 @@ import {
Drawer,
Form,
Modal,
Row,
Upload,
Spin,
} from 'antd'
......@@ -132,7 +133,7 @@ const AppVersion = () => {
// 删除
const handleDel = async () => {
const bool = await delVersion({ id: delid })
bool && refresh()
if (bool) refresh()
setDelModal(false)
}
......
import axios, { notifySuccess } from '@/utils/axios'
import endpoints from './endpoints.js'
const getData = async (path, params) => (await axios.post(path, params)).data
const mutate = async (path, params, notify = false) => {
......@@ -8,9 +7,9 @@ const mutate = async (path, params, notify = false) => {
return true
}
export const getAppList = (params) => getData(endpoints.getAppList, params)
export const addApp = (params) => mutate(endpoints.addApp, params, true)
export const getAppInfo = (params) => getData(endpoints.getAppInfo, params)
export const editApp = (params) => getData(endpoints.editApp, params)
export const changeStatus = (params) => mutate(endpoints.changeStatus, params)
export const delApp = (params) => mutate(endpoints.delApp, params, true)
export const getAppList = (params) => getData('/api/admin/system/app/getList', params)
export const addApp = (params) => mutate('/api/admin/system/app/add', params, true)
export const getAppInfo = (params) => getData('/api/admin/system/app/getInfoById', params)
export const editApp = (params) => getData('/api/admin/system/app/edit', params)
export const changeStatus = (params) => mutate('/api/admin/system/app/changeStatus', params)
export const delApp = (params) => mutate('/api/admin/system/app/del', params, true)
const api = {
getAppList: '/system/app/getList', // 应用管理列表
addApp: '/system/app/add', // 添加应用
getAppInfo: '/system/app/getInfoById', // 获取应用详情
editApp: '/system/app/edit', // 应用编辑
changeStatus: '/system/app/changeStatus', // 启用/停用
delApp: '/system/app/del', // 删除应用
}
export default api
......@@ -31,7 +31,7 @@ export type CouponListRequest = (params: CouponQuery) => Promise<CouponListResul
export async function getCouponList(params: CouponQuery): Promise<CouponListResult> {
const response = (await axios.post(
'/api/system/coupon/getList',
'/api/admin/system/coupon/getList',
params,
)) as unknown as CouponListEnvelope
const { list = [], total = 0 } = response.data ?? {}
......@@ -40,23 +40,23 @@ export async function getCouponList(params: CouponQuery): Promise<CouponListResu
}
export function addCoupon(data: object) {
return axios.post('/api/system/coupon/add', data)
return axios.post('/api/admin/system/coupon/add', data)
}
export function editCoupon(data: object) {
return axios.post('/api/system/coupon/edit', data)
return axios.post('/api/admin/system/coupon/edit', data)
}
export function deleteCoupon(data: object) {
return axios.post('/api/system/coupon/del', data)
return axios.post('/api/admin/system/coupon/del', data)
}
export function getCouponDetail(data: object) {
return axios.post('/api/system/coupon/getInfoById', data)
return axios.post('/api/admin/system/coupon/getInfoById', data)
}
export function addCouponRecord(data: object) {
return axios.post('/api/system/coupon/record/add', data)
return axios.post('/api/admin/system/coupon/record/add', data)
}
async function legacyPost(path: string, data: object) {
......@@ -65,12 +65,12 @@ async function legacyPost(path: string, data: object) {
}
export async function getCouponConfig() {
const result = await legacyPost('/system/config/getCoupon', {})
const result = await legacyPost('/api/admin/system/config/getCoupon', {})
return result.payload as { coupon_switch?: string }
}
export async function setCouponConfig(data: { coupon_switch: '0' | '1' }) {
const result = await legacyPost('/system/config/couponEdit', data)
const result = await legacyPost('/api/admin/system/config/couponEdit', data)
message.success(result.message)
return true
}
......@@ -27,7 +27,7 @@ export type FeedbackListRequest = (params: FeedbackQuery) => Promise<FeedbackLis
// 保留旧 axios 客户端和业务 envelope,await 后才做模块内归一化。
export async function getFeedbackList(params: FeedbackQuery): Promise<FeedbackListResult> {
const response = (await axios.post(
'/api/system/proposal/getList',
'/api/admin/system/proposal/getList',
params,
)) as unknown as FeedbackListEnvelope
const { list = [], total = 0 } = response.data ?? {}
......
......@@ -2,35 +2,35 @@ import axios from '@/utils/axios'
import { message } from 'antd'
// 获取列表
export const getList = async (obj) => {
const res = await axios.post('/system/app/help/getList', obj)
const res = await axios.post('/api/admin/system/app/help/getList', obj)
return res.data
}
// 添加帮助
export const addHelp = async (obj) => {
const res = await axios.post('/system/app/help/add', obj)
const res = await axios.post('/api/admin/system/app/help/add', obj)
message.success(res.message)
return true
}
// 获取帮助详情
export const HelpInfo = async (obj) => {
const res = await axios.post('/system/app/help/getInfoById', obj)
const res = await axios.post('/api/admin/system/app/help/getInfoById', obj)
return res.data
}
// 修改
export const editHelp = async (obj) => {
const res = await axios.post('/system/app/help/edit', obj)
const res = await axios.post('/api/admin/system/app/help/edit', obj)
message.success(res.message)
return res.data
}
// 修改
export const editPreview = async (obj) => {
const res = await axios.post('/system/app/help/edit', obj)
const res = await axios.post('/api/admin/system/app/help/edit', obj)
return res.data
}
// 删除
export const delHelp = async (obj) => {
const res = await axios.post('/system/app/help/del', obj)
const res = await axios.post('/api/admin/system/app/help/del', obj)
message.success(res.message)
return true
}
......@@ -7,12 +7,12 @@ export interface PersonalizedSettings {
}
export async function getPersonalizedSettings(): Promise<PersonalizedSettings> {
const response = await axios.post('/system/config/getSystem')
const response = await axios.post('/api/admin/system/config/getSystem')
return response.data ?? {}
}
export async function savePersonalizedSettings(values: PersonalizedSettings) {
const response = await axios.post('/system/config/systemEdit', values)
const response = await axios.post('/api/admin/system/config/systemEdit', values)
notifySuccess(response.message)
return true
}
......@@ -30,23 +30,23 @@ interface RechargeResult {
}
export async function getPointSettings(): Promise<PointSettings> {
const response = await axios.post('/system/config/getIntegral')
const response = await axios.post('/api/admin/system/config/getIntegral')
return response.data ?? {}
}
export async function setPointSwitch(integralSwitch: '0' | '1') {
await axios.post('/system/config/integralSet', { integral_switch: integralSwitch })
await axios.post('/api/admin/system/config/integralSet', { integral_switch: integralSwitch })
return true
}
export async function savePointSettings(values: PointSettingsFormValues) {
const response = await axios.post('/system/config/integralEdit', values)
const response = await axios.post('/api/admin/system/config/integralEdit', values)
notifySuccess(response.message)
return true
}
export async function rechargePoints(values: RechargeFormValues) {
const response = await axios.post('/members/integral/add', values)
const response = await axios.post('/api/admin/members/integral/add', values)
const data = response.data as RechargeResult
const success = data?.suc ?? {}
const failure = data?.err ?? {}
......
......@@ -7,12 +7,13 @@ const mutate = async (path, params, notify = false) => {
return true
}
export const getList = (params) => getData('/system/sensitive/words/getList', params)
export const addWords = (params) => mutate('/system/sensitive/words/add', params, true)
export const editWords = (params) => mutate('/system/sensitive/words/edit', params, true)
export const delWords = (params) => mutate('/system/sensitive/words/del', params)
export const infoWords = (params) => getData('/system/sensitive/words/getInfoById', params)
export const getList = (params) => getData('/api/admin/system/sensitive/words/getList', params)
export const addWords = (params) => mutate('/api/admin/system/sensitive/words/add', params, true)
export const editWords = (params) => mutate('/api/admin/system/sensitive/words/edit', params, true)
export const delWords = (params) => mutate('/api/admin/system/sensitive/words/del', params)
export const infoWords = (params) =>
getData('/api/admin/system/sensitive/words/getInfoById', params)
export const importWords = async (params) => {
const response = await axios.post('/system/sensitive/words/import', params)
const response = await axios.post('/api/admin/system/sensitive/words/import', params)
return { msg: response.message, err: response.data?.err }
}
......@@ -165,9 +165,7 @@ export default function SensitiveListView() {
<Button
type='primary'
ghost
onClick={() =>
window.open(`${import.meta.env.VITE_API_URL}/敏感词导入失败.xlsx`)
}
onClick={() => window.open('/api/admin/敏感词导入失败.xlsx')}
>
下载导入失败的数据
</Button>
......@@ -184,7 +182,7 @@ export default function SensitiveListView() {
<Button
type='primary'
ghost
onClick={() => window.open(`${import.meta.env.VITE_API_URL}/敏感词导入示例.xlsx`)}
onClick={() => window.open('/api/admin/敏感词导入示例.xlsx')}
>
下载模板
</Button>
......
......@@ -11,20 +11,20 @@ async function request(
}
export const getTeacherList = (payload: Record<string, unknown>) =>
request('/user/teacher/getList', payload, { returnData: true })
request('/api/admin/user/teacher/getList', payload, { returnData: true })
export const addTeacher = (payload: Record<string, unknown>) =>
request('/user/teacher/add', payload, { success: true })
request('/api/admin/user/teacher/add', payload, { success: true })
export const getTeacherInfo = (payload: Record<string, unknown>) =>
request('/user/teacher/getInfoById', payload, { returnData: true })
request('/api/admin/user/teacher/getInfoById', payload, { returnData: true })
export const editTeacher = (payload: Record<string, unknown>) =>
request('/user/teacher/edit', payload, { success: true })
request('/api/admin/user/teacher/edit', payload, { success: true })
export const deleteTeacher = (payload: Record<string, unknown>) =>
request('/user/teacher/del', payload, { success: true })
request('/api/admin/user/teacher/del', payload, { success: true })
export const changeTeacherStatus = (payload: Record<string, unknown>) =>
request('/user/teacher/changeStatus', payload, { success: true })
request('/api/admin/user/teacher/changeStatus', payload, { success: true })
export const changeTeacherPassword = (payload: Record<string, unknown>) =>
request('/user/teacher/editPass', payload, { success: true })
request('/api/admin/user/teacher/editPass', payload, { success: true })
export const getTeacherSystem = (payload: Record<string, unknown>) =>
request('/system/config/getSystem', payload, { returnData: true })
request('/api/admin/system/config/getSystem', payload, { returnData: true })
export const updateTeacherSystem = (payload: Record<string, unknown>) =>
request('/system/config/systemEdit', payload, { success: true })
request('/api/admin/system/config/systemEdit', payload, { success: true })
......@@ -19,23 +19,18 @@ export function useTeacherList() {
const { fetchPage, page, pageSize } = list
const changePage = useCallback(
(nextPage: number, filters: Record<string, unknown>) =>
fetchPage(nextPage, pageSize, filters),
(nextPage: number, filters: Record<string, unknown>) => fetchPage(nextPage, pageSize, filters),
[fetchPage, pageSize],
)
const changePageSize = useCallback(
(nextPageSize: number, filters: Record<string, unknown>) =>
fetchPage(1, nextPageSize, filters),
(nextPageSize: number, filters: Record<string, unknown>) => fetchPage(1, nextPageSize, filters),
[fetchPage],
)
const init = useCallback(
(
filters: Record<string, unknown> = {},
nextPage = page,
nextPageSize = pageSize,
) => fetchPage(nextPage, nextPageSize, filters),
(filters: Record<string, unknown> = {}, nextPage = page, nextPageSize = pageSize) =>
fetchPage(nextPage, nextPageSize, filters),
[fetchPage, page, pageSize],
)
......
......@@ -42,8 +42,7 @@ export const useUserStore = create<UserState>()(
setUserInfo: (userInfo) => set({ userInfo }),
setToken: (token) => set({ token }),
setMenuListRouter: (menuListRouter) => set({ menuListRouter }),
setOperationPermissionsList: (operationPermissionsList) =>
set({ operationPermissionsList }),
setOperationPermissionsList: (operationPermissionsList) => set({ operationPermissionsList }),
setTreeChapter: (treeChapter) => set({ treeChapter: { ...treeChapter } }),
}),
{
......
export const ADMIN_BASE_PATH = '/admin'
export const ADMIN_LOGIN_PATH = `${ADMIN_BASE_PATH}/login`
export function normalizeAdminApiPath(url = '') {
if (url === '/api/admin') return '/'
if (url.startsWith('/api/admin/')) return url.slice('/api/admin'.length)
if (url === '/api') return '/'
if (url.startsWith('/api/')) return url.slice('/api'.length)
return url
}
export function stripAdminGatewayPrefix(url = '') {
return url.replace('/api/admin', '')
}
import test from 'node:test'
import assert from 'node:assert/strict'
import { ADMIN_LOGIN_PATH, normalizeAdminApiPath, stripAdminGatewayPrefix } from './adminPaths.js'
test('admin login stays under the admin base path', () => {
assert.equal(ADMIN_LOGIN_PATH, '/admin/login')
})
test('legacy /api requests are normalized for the admin gateway', () => {
assert.equal(normalizeAdminApiPath('/api/admin/user/login'), '/user/login')
assert.equal(normalizeAdminApiPath('/api/user/login'), '/user/login')
assert.equal(normalizeAdminApiPath('/user/login'), '/user/login')
})
test('gateway prefix is removed before backend signature calculation', () => {
assert.equal(
stripAdminGatewayPrefix('https://zijingebook.ezijing.com/api/admin/user/login'),
'https://zijingebook.ezijing.com/user/login',
)
})
......@@ -6,7 +6,7 @@ import axios, {
import md5 from 'js-md5'
import { message, Modal, notification } from 'antd'
import { ADMIN_LOGIN_PATH, normalizeAdminApiPath } from '@/utils/adminPaths.js'
const ADMIN_LOGIN_PATH = '/admin/login'
export interface ApiResponse<T = unknown> {
code: number
......@@ -76,14 +76,14 @@ function getSignData(data: unknown): Record<string, unknown> {
return data && typeof data === 'object' ? (data as Record<string, unknown>) : {}
}
const httpRequest: any = axios.create({
baseURL: (import.meta as ImportMeta & { env: { VITE_API_PREFIX?: string } }).env.VITE_API_PREFIX,
withCredentials: true,
})
const httpRequest: any = axios.create({ withCredentials: true })
httpRequest.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
config.url = normalizeAdminApiPath(config.url)
const requestPath = config.url ?? ''
const signaturePath = requestPath.startsWith('/api/admin')
? requestPath.slice('/api/admin'.length) || '/'
: requestPath
const token = getToken()
const appId = 'TzEU5jPk2tu80266'
......@@ -96,7 +96,7 @@ httpRequest.interceptors.request.use(
appSecret,
timestamp,
token,
url: config.url,
url: signaturePath,
}
const signStr = Object.entries(signData)
.sort(([keyA], [keyB]) => alphabeticalSort(keyA, keyB))
......
......@@ -4,7 +4,7 @@ import { message } from 'antd'
let RotateVerify // 声明 RotateVerify 变量
;(function (undefined) {
;(function () {
'use strict'
//判断是否为 数组
......
......@@ -233,8 +233,8 @@ export const clacTopicText = (type, radio, checkbox, judge, fill, textarea) => {
}
}
export const downloadFile = (res = '') => {
window.open(import.meta.env.VITE_API_URL + '/' + res)
export const downloadFile = (url = '') => {
window.open(url)
}
// 延时函数
export const sleep = (time) => new Promise((resolve) => setTimeout(resolve, time))
......@@ -8,7 +8,9 @@ import { saveAs } from 'file-saver'
async function imgToBase64(src) {
// 方式1:fetch
try {
const res = await fetch(src)
// OSS 刚更新 CORS 规则时,浏览器可能复用规则生效前的无 CORS 缓存响应。
// 导出只在用户主动触发时执行,强制重新校验可确保读取当前响应头。
const res = await fetch(src, { cache: 'reload' })
if (res.ok) {
const blob = await res.blob()
return await blobToDataUri(blob)
......
import { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } from 'vite'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import mkcert from 'vite-plugin-mkcert'
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
const adminApiTarget = env.VITE_ADMIN_API_TARGET || 'http://127.0.0.1:7419'
export default defineConfig(() => {
return {
base: '/admin/',
plugins: [react(), mkcert()],
......@@ -19,7 +16,7 @@ export default defineConfig(({ mode }) => {
strictPort: true,
proxy: {
'/api/admin': {
target: adminApiTarget,
target: 'http://127.0.0.1:7419',
changeOrigin: true,
secure: false,
rewrite: path => path.replace(/^\/api\/admin/, ''),
......
......@@ -14,6 +14,7 @@
"dependencies": {
"@ant-design/icons": "^6.3.2",
"@tanstack/react-query": "^5.76.2",
"ali-oss": "^6.20.0",
"antd": "^6.5.1",
"axios": "^1.8.4",
"js-md5": "^0.8.3",
......
......@@ -10,13 +10,13 @@ export function hashMemberPassword(password: string) {
export async function login(input: LoginInput) {
return unwrapApiData(
await http.post<ApiEnvelope<MemberSession>>('/v1/members/login/login', input),
await http.post<ApiEnvelope<MemberSession>>('/api/app/v1/members/login/login', input),
)
}
export async function sendLoginCode(phone: string, uuid: string) {
return unwrapApiData(
await http.post<ApiEnvelope<{ is_success: number }>>('/v1/members/login/sendCode', {
await http.post<ApiEnvelope<{ is_success: number }>>('/api/app/v1/members/login/sendCode', {
phone,
types: 'login',
uuid,
......@@ -26,6 +26,6 @@ export async function sendLoginCode(phone: string, uuid: string) {
export async function logout() {
return unwrapApiData(
await http.post<ApiEnvelope<{ is_success: number }>>('/v1/members/login/logout', {}),
await http.post<ApiEnvelope<{ is_success: number }>>('/api/app/v1/members/login/logout', {}),
)
}
......@@ -142,19 +142,19 @@ function toComment(value: unknown): BookComment {
export async function getBookDetail(bookId: string): Promise<BookDetail> {
return toDetail(
await post<unknown>('/v1/book/Information/getBookInformation', { book_id: bookId }),
await post<unknown>('/api/app/v1/book/Information/getBookInformation', { book_id: bookId }),
)
}
export async function getBookChapters(bookId: string): Promise<BookChapter[]> {
return list(await post<unknown>('/v1/book/Information/getBookChapter', { book_id: bookId })).map(
toChapter,
)
return list(
await post<unknown>('/api/app/v1/book/Information/getBookChapter', { book_id: bookId }),
).map(toChapter)
}
export async function getBookComments(bookId: string): Promise<BookComment[]> {
return list(
await post<unknown>('/v1/book/Information/getBookCommentList', {
await post<unknown>('/api/app/v1/book/Information/getBookCommentList', {
book_id: bookId,
page: 1,
page_size: 3,
......@@ -164,7 +164,7 @@ export async function getBookComments(bookId: string): Promise<BookComment[]> {
export async function setBookCollection(bookId: string, selected: boolean): Promise<boolean> {
const data = record(
await post<unknown>('/v1/book/Information/collection', {
await post<unknown>('/api/app/v1/book/Information/collection', {
book_id: bookId,
is_select: selected ? '1' : '0',
}),
......@@ -173,13 +173,13 @@ export async function setBookCollection(bookId: string, selected: boolean): Prom
}
export async function addBookToShelf(bookId: string): Promise<boolean> {
const data = record(await post<unknown>('/v1/orders/Orders/addCart', { book_id: bookId }))
const data = record(await post<unknown>('/api/app/v1/orders/Orders/addCart', { book_id: bookId }))
return number(data.is_success) === 1
}
export async function getBookStudyReport(bookId: string): Promise<StudyReport> {
const data = record(
await post<unknown>('/v1/members/Information/myStudyReport', { book_id: bookId }),
await post<unknown>('/api/app/v1/members/Information/myStudyReport', { book_id: bookId }),
)
return {
progress: string(data.progress),
......
......@@ -124,24 +124,24 @@ export function toLibraryApiParams(state: LibrarySearchState): LibraryApiParams
}
export async function getLibraryCategories(): Promise<LibraryFilter[]> {
const data = await post<unknown>('/v1/book/category/getCategoryListAll', {})
const data = await post<unknown>('/api/app/v1/book/category/getCategoryListAll', {})
return listFromPayload(data).map((item) => toFilter(item, 'category_id'))
}
export async function getLibraryLabels(): Promise<LibraryFilter[]> {
const data = await post<unknown>('/v1/book/category/getLabelListAll', {})
const data = await post<unknown>('/api/app/v1/book/category/getLabelListAll', {})
return listFromPayload(data).map((item) => toFilter(item, 'label_id'))
}
export async function getLibraryBooks(params: LibraryApiParams): Promise<LibraryListResult> {
const data =
'keyword' in params
? await post<unknown>('/v1/book/Information/searchBook', {
? await post<unknown>('/api/app/v1/book/Information/searchBook', {
keyword: params.keyword,
page: params.page,
page_size: params.page_size,
})
: await post<unknown>('/v1/book/category/getBookList', {
: await post<unknown>('/api/app/v1/book/category/getBookList', {
category_id: params.category_id,
label_id: params.label_id,
...(params.is_free === undefined ? {} : { is_free: params.is_free }),
......@@ -161,7 +161,7 @@ export async function getLibraryBooks(params: LibraryApiParams): Promise<Library
}
export async function setBookCollection(bookId: string, isSelected: boolean): Promise<boolean> {
const data = await post<unknown>('/v1/book/Information/collection', {
const data = await post<unknown>('/api/app/v1/book/Information/collection', {
book_id: bookId,
is_select: isSelected ? '1' : '0',
})
......
......@@ -85,7 +85,7 @@ function toMyBookListResult(data: unknown): MyBookListResult {
}
export async function getMyStatistics(): Promise<MyStatistics> {
const response = await http.post('/v1/members/Information/getMyStatistics', {})
const response = await http.post('/api/app/v1/members/Information/getMyStatistics', {})
const data = asRecord(unwrapApiData(response))
return {
......@@ -110,15 +110,15 @@ async function getMyBookList(path: string, params: MyListParams): Promise<MyBook
}
export function getMyCourses(params: MyListParams) {
return getMyBookList('/v1/members/Information/myCourse', params)
return getMyBookList('/api/app/v1/members/Information/myCourse', params)
}
export function getBookshelf(params: MyListParams) {
return getMyBookList('/v1/orders/Orders/getCartListAll', params)
return getMyBookList('/api/app/v1/orders/Orders/getCartListAll', params)
}
export async function deleteBookshelfItem(cartId: string) {
const response = await http.post('/v1/orders/Orders/delCart', { cart_id: cartId })
const response = await http.post('/api/app/v1/orders/Orders/delCart', { cart_id: cartId })
const data = unwrapApiData<ApiRecord>(response)
return data.is_success === 1 || data.is_success === '1' || data.is_success === true
}
......@@ -13,7 +13,7 @@ import {
Typography,
} from 'antd'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useMemo, useState } from 'react'
import { useState } from 'react'
import { Link } from 'react-router'
import defaultBookCover from '@/assets/default-book-cover.jpg'
import { deleteBookshelfItem } from '../../api'
......@@ -33,11 +33,9 @@ export function MyBookshelfView() {
const bookshelfQuery = useQuery(bookshelfOptions(state))
const [selectedCartIds, setSelectedCartIds] = useState<Set<string>>(new Set())
const items = bookshelfQuery.data?.items ?? []
const selectableCartIds = useMemo(
() =>
items.filter((book) => book.status === 1 && book.cartId).map((book) => String(book.cartId)),
[items],
)
const selectableCartIds = items
.filter((book) => book.status === 1 && book.cartId)
.map((book) => String(book.cartId))
const deleteMutation = useMutation({
mutationFn: async (cartIds: string[]) => {
......
......@@ -50,7 +50,7 @@ function toCoupon(value: unknown): CouponItem {
}
export async function getCoupons(params: CouponListParams): Promise<CouponListResult> {
const response = await http.post('/v1/coupon/Coupon/getCouponRecordListSAll', {
const response = await http.post('/api/app/v1/coupon/Coupon/getCouponRecordListSAll', {
page: params.page,
page_size: params.pageSize,
type: params.status,
......
......@@ -28,7 +28,7 @@ function text(value: unknown) {
}
export async function getDiscussedBooks(params: MyListParams): Promise<DiscussedBookList> {
const response = await http.post('/v1/members/Information/myComment', {
const response = await http.post('/api/app/v1/members/Information/myComment', {
page: params.page,
page_size: params.pageSize,
})
......
......@@ -6,7 +6,6 @@ import {
Image,
List,
Pagination,
Space,
Statistic,
Skeleton,
Typography,
......
......@@ -34,7 +34,7 @@ function number(value: unknown) {
}
export async function getFavorites(params: MyListParams): Promise<FavoriteList> {
const response = await http.post('/v1/members/Information/myCollection', {
const response = await http.post('/api/app/v1/members/Information/myCollection', {
page: params.page,
page_size: params.pageSize,
})
......@@ -58,7 +58,7 @@ export async function getFavorites(params: MyListParams): Promise<FavoriteList>
}
export async function removeFavorite(bookId: string) {
const response = await http.post('/v1/book/Information/collection', {
const response = await http.post('/api/app/v1/book/Information/collection', {
book_id: bookId,
is_select: '0',
})
......
......@@ -4,9 +4,12 @@ import { http } from '@/utils/http'
export async function submitFeedback(proposal: string) {
const data = unwrapApiData(
await http.post<ApiEnvelope<Record<string, unknown>>>('/v1/members/Information/proposal', {
await http.post<ApiEnvelope<Record<string, unknown>>>(
'/api/app/v1/members/Information/proposal',
{
proposal,
}),
},
),
)
if (data.is_success !== 1 && data.is_success !== '1' && data.is_success !== true) {
throw new Error('反馈提交失败')
......
......@@ -30,7 +30,10 @@ function text(value: unknown) {
export async function getHelpArticles(): Promise<HelpArticleSummary[]> {
const data = unwrapApiData(
await http.post<ApiEnvelope<unknown>>('/v1/common/Help/getList', { page: 1, page_size: 10 }),
await http.post<ApiEnvelope<unknown>>('/api/app/v1/common/Help/getList', {
page: 1,
page_size: 10,
}),
)
return list(data)
.map((value) => {
......@@ -42,7 +45,9 @@ export async function getHelpArticles(): Promise<HelpArticleSummary[]> {
export async function getHelpArticle(id: string): Promise<HelpArticle> {
const item = record(
unwrapApiData(await http.post<ApiEnvelope<unknown>>('/v1/common/Help/getInfoById', { id })),
unwrapApiData(
await http.post<ApiEnvelope<unknown>>('/api/app/v1/common/Help/getInfoById', { id }),
),
)
return {
id: text(item.id) || id,
......
......@@ -34,7 +34,7 @@ function list(value: unknown) {
}
export async function getReadRecords(params: MyListParams): Promise<ReadRecordGroup[]> {
const response = await http.post('/v1/members/Information/myReadRecord', {
const response = await http.post('/api/app/v1/members/Information/myReadRecord', {
page: params.page,
page_size: params.pageSize,
})
......
import { useQuery } from '@tanstack/react-query'
import { Alert, Button, Card, Empty, Image, List, Skeleton, Space, Tag, Typography } from 'antd'
import { Alert, Button, Empty, Image, List, Skeleton, Space, Tag, Typography } from 'antd'
import { Link } from 'react-router'
import { useMyListQueryState } from '../../query-state'
import { readRecordsOptions } from '../query'
......
......@@ -62,7 +62,7 @@ function toMessage(value: unknown): MessageItem {
}
export async function getMessages(params: MessageListParams): Promise<MessageListResult> {
const response = await http.post('/v1/message/Message/getList', {
const response = await http.post('/api/app/v1/message/Message/getList', {
page: params.page,
page_size: params.pageSize,
})
......@@ -75,12 +75,12 @@ export async function getMessages(params: MessageListParams): Promise<MessageLis
}
export async function getUnreadMessageCount() {
const response = await http.post('/v1/message/Message/getMessageNums', {})
const response = await http.post('/api/app/v1/message/Message/getMessageNums', {})
return number(record(unwrapApiData<unknown>(response)).num)
}
export async function markMessageRead(id: string) {
const response = await http.post('/v1/message/Message/editOneStatus', { id })
const response = await http.post('/api/app/v1/message/Message/editOneStatus', { id })
const data = record(unwrapApiData<unknown>(response))
return data.is_success === 1 || data.is_success === '1' || data.is_success === true
}
......@@ -36,7 +36,7 @@ function number(value: unknown) {
}
export async function getNotedBooks(params: MyListParams): Promise<NotedBookList> {
const response = await http.post('/v1/members/Information/myNotes', {
const response = await http.post('/api/app/v1/members/Information/myNotes', {
page: params.page,
page_size: params.pageSize,
})
......
......@@ -115,7 +115,7 @@ export async function getOrders(params: OrderListParams): Promise<OrderListResul
status: params.status,
}
if (params.keyword.trim()) request.keys = params.keyword.trim()
const response = await http.post('/v1/orders/Orders/getOrdersList', request)
const response = await http.post('/api/app/v1/orders/Orders/getOrdersList', request)
const data = unwrapApiData<unknown>(response)
const payload = record(data)
return {
......@@ -125,7 +125,7 @@ export async function getOrders(params: OrderListParams): Promise<OrderListResul
}
export async function getOrderDetail(orderNumber: string): Promise<OrderDetail> {
const response = await http.post('/v1/orders/Orders/getInfoOrdersnum', {
const response = await http.post('/api/app/v1/orders/Orders/getInfoOrdersnum', {
ordersnum: orderNumber,
})
const data = record(unwrapApiData<unknown>(response))
......
......@@ -33,7 +33,7 @@ function isSuccessful(data: ApiRecord) {
export async function getMemberProfile(): Promise<MemberProfile> {
const data = record(
unwrapApiData(
await http.post<ApiEnvelope<unknown>>('/v1/members/Information/getMyStatistics', {}),
await http.post<ApiEnvelope<unknown>>('/api/app/v1/members/Information/getMyStatistics', {}),
),
)
return {
......@@ -53,7 +53,7 @@ export interface UpdateMemberProfileInput {
export async function updateMemberProfile(input: UpdateMemberProfileInput) {
const data = record(
unwrapApiData(
await http.post<ApiEnvelope<unknown>>('/v1/members/Information/editInformation', {
await http.post<ApiEnvelope<unknown>>('/api/app/v1/members/Information/editInformation', {
name: input.name,
sex: input.sex,
...(input.headImg ? { head_img: input.headImg } : {}),
......
import { unwrapApiData } from '@/api/unwrap'
import type { ApiEnvelope } from '@/api/types'
import { http } from '@/utils/http'
const bucket = 'zxts-user-file'
const publicDomain = 'https://zxts-user-file.zijingebook.com'
interface UploadToken {
AccessKeyId: string
AccessKeySecret: string
SecurityToken: string
}
function createObjectKey(file: File) {
const extension = file.name.split('.').pop()?.toLowerCase()
const suffix = extension ? `.${extension}` : ''
const date = new Date().toISOString().slice(0, 10).replaceAll('-', '/')
return `avatar/${date}/${crypto.randomUUID().replaceAll('-', '')}${suffix}`
}
export async function uploadAvatar(file: File) {
const token = unwrapApiData(
await http.post<ApiEnvelope<UploadToken>>('/api/app/v1/common/Alioss/getUploadToken', {}),
)
const objectKey = createObjectKey(file)
const { default: OSS } = await import('ali-oss')
const client = new OSS({
region: 'oss-cn-beijing',
bucket,
accessKeyId: token.AccessKeyId,
accessKeySecret: token.AccessKeySecret,
stsToken: token.SecurityToken,
})
await client.put(objectKey, file)
return `${publicDomain}/${objectKey}`
}
import { useEffect } from 'react'
import { useEffect, useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { UploadOutlined } from '@ant-design/icons'
import {
Alert,
Avatar,
......@@ -10,10 +11,14 @@ import {
Radio,
Skeleton,
Typography,
Upload,
message,
} from 'antd'
import type { UploadProps } from 'antd'
import { useSessionStore } from '@/modules/auth/session'
import { myKeys } from '../../query'
import { updateMemberProfile, type UpdateMemberProfileInput } from '../api'
import { uploadAvatar } from '../avatar-upload'
import { memberProfileOptions, profileKeys } from '../query'
interface ProfileFormValues {
......@@ -30,12 +35,22 @@ const sexOptions = [
export function ProfileView() {
const [form] = Form.useForm<ProfileFormValues>()
const [uploading, setUploading] = useState(false)
const session = useSessionStore((state) => state.session)
const setSession = useSessionStore((state) => state.setSession)
const queryClient = useQueryClient()
const profileQuery = useQuery(memberProfileOptions())
const saveMutation = useMutation({
mutationFn: (values: ProfileFormValues) =>
updateMemberProfile(values satisfies UpdateMemberProfileInput),
onSuccess: async () => {
onSuccess: async (_, values) => {
if (session) {
setSession({
...session,
name: values.name,
head_img: values.headImg,
})
}
await Promise.all([
queryClient.invalidateQueries({ queryKey: profileKeys.all }),
queryClient.invalidateQueries({ queryKey: myKeys.statistics() }),
......@@ -66,6 +81,36 @@ export function ProfileView() {
)
}
const avatarUploadProps: UploadProps = {
accept: 'image/jpeg,image/png,image/webp',
showUploadList: false,
beforeUpload: (file) => {
if (!['image/jpeg', 'image/png', 'image/webp'].includes(file.type)) {
message.error('头像仅支持 JPG、PNG、WebP 格式')
return Upload.LIST_IGNORE
}
if (file.size > 5 * 1024 * 1024) {
message.error('头像大小不能超过 5MB')
return Upload.LIST_IGNORE
}
return true
},
customRequest: async (options) => {
setUploading(true)
try {
const url = await uploadAvatar(options.file as File)
form.setFieldValue('headImg', url)
options.onSuccess?.({ url })
message.success('头像上传成功')
} catch (error) {
options.onError?.(error as Error)
message.error(error instanceof Error ? error.message : '头像上传失败')
} finally {
setUploading(false)
}
},
}
return (
<main>
<Typography.Title level={1}>个人资料</Typography.Title>
......@@ -85,6 +130,9 @@ export function ProfileView() {
requiredMark='optional'
onFinish={(values) => saveMutation.mutate(values)}
>
<Form.Item name='headImg' hidden>
<Input />
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(previous, current) =>
......@@ -92,21 +140,18 @@ export function ProfileView() {
}
>
{() => (
<Form.Item label='头像预览'>
<Form.Item label='头像' extra='支持 JPG、PNG、WebP,大小不超过 5MB。'>
<Avatar size={72} src={form.getFieldValue('headImg')}>
{form.getFieldValue('name')?.slice(0, 1)}
</Avatar>
<Upload {...avatarUploadProps}>
<Button icon={<UploadOutlined />} loading={uploading} style={{ marginLeft: 12 }}>
上传头像
</Button>
</Upload>
</Form.Item>
)}
</Form.Item>
<Form.Item
label='头像 URL'
name='headImg'
rules={[{ type: 'url', message: '请输入有效的头像 URL' }]}
extra='暂不支持上传,可填写可访问的图片地址。'
>
<Input placeholder='https://example.com/avatar.png' autoComplete='url' />
</Form.Item>
<Form.Item
label='昵称'
name='name'
......
......@@ -27,7 +27,7 @@ async function successfulPost(
export async function changeMemberPassword(password: string, repassword: string) {
await successfulPost(
'/v1/members/Information/changePasswd',
'/api/app/v1/members/Information/changePasswd',
{
password: hashMemberPassword(password),
repassword: hashMemberPassword(repassword),
......@@ -38,7 +38,7 @@ export async function changeMemberPassword(password: string, repassword: string)
export async function sendChangePhoneCode(phone: string) {
await successfulPost(
'/v1/members/login/sendCode',
'/api/app/v1/members/login/sendCode',
{
phone,
types: 'login',
......@@ -49,9 +49,13 @@ export async function sendChangePhoneCode(phone: string) {
}
export async function changeMemberPhone(phone: string, code: string) {
await successfulPost('/v1/members/Information/editPhone', { phone, code }, '手机号修改失败')
await successfulPost(
'/api/app/v1/members/Information/editPhone',
{ phone, code },
'手机号修改失败',
)
}
export async function deleteMemberAccount() {
await successfulPost('/v1/members/login/del', {}, '账号注销失败')
await successfulPost('/api/app/v1/members/login/del', {}, '账号注销失败')
}
......@@ -52,7 +52,7 @@ function source(data: unknown) {
}
export async function getPointRecords(params: WalletListParams): Promise<WalletListResult> {
const response = await http.post('/v1/members/Information/getIntegralList', {
const response = await http.post('/api/app/v1/members/Information/getIntegralList', {
page: params.page,
page_size: params.pageSize,
})
......@@ -65,7 +65,7 @@ export async function getPointRecords(params: WalletListParams): Promise<WalletL
}
export async function getCoinRecords(params: WalletListParams): Promise<WalletListResult> {
const response = await http.post('/v1/members/Information/getBeanList', {
const response = await http.post('/api/app/v1/members/Information/getBeanList', {
page: params.page,
page_size: params.pageSize,
})
......
......@@ -28,7 +28,7 @@ function text(value: unknown) {
}
export async function getWrongQuestionBooks(params: MyListParams): Promise<WrongQuestionBookList> {
const response = await http.post('/v1/members/Information/myWrongQuestion', {
const response = await http.post('/api/app/v1/members/Information/myWrongQuestion', {
page: params.page,
page_size: params.pageSize,
})
......
......@@ -6,7 +6,6 @@ import {
Image,
List,
Pagination,
Space,
Statistic,
Skeleton,
Typography,
......
......@@ -157,15 +157,20 @@ async function post<T>(url: string, data: Record<string, unknown>): Promise<T> {
}
export async function getReaderChapters(bookId: string): Promise<ReaderChapter[]> {
const data = await post<unknown>('/v1/book/Information/getBookChapter', { book_id: bookId })
const data = await post<unknown>('/api/app/v1/book/Information/getBookChapter', {
book_id: bookId,
})
return list(data).map(toChapter)
}
export async function getSectionContent(bookId: string, chapterId: string): Promise<ReaderSection> {
const response = await http.post<ApiEnvelope<unknown>>('/v1/book/Information/getSectionContent', {
const response = await http.post<ApiEnvelope<unknown>>(
'/api/app/v1/book/Information/getSectionContent',
{
book_id: bookId,
chapter_id: chapterId,
})
},
)
const body = response.data
const payload = record(body.data)
if (body.code === 3001 || Number(payload.code) === 3001) throw new ReaderChapterLockedError()
......@@ -183,7 +188,7 @@ export async function getSectionContent(bookId: string, chapterId: string): Prom
}
export function addProgress(bookId: string, chapterId: string, progress: number) {
return post<unknown>('/v1/book/Information/addProgress', {
return post<unknown>('/api/app/v1/book/Information/addProgress', {
book_id: bookId,
chapter_id: chapterId,
progress,
......@@ -191,7 +196,7 @@ export function addProgress(bookId: string, chapterId: string, progress: number)
}
export function addReadRecord(bookId: string, chapterId: string, positioning: number) {
return post<unknown>('/v1/book/Information/addReadRecord', {
return post<unknown>('/api/app/v1/book/Information/addReadRecord', {
book_id: bookId,
chapter_id: chapterId,
positioning,
......@@ -199,14 +204,14 @@ export function addReadRecord(bookId: string, chapterId: string, positioning: nu
}
export function addReadTime(bookId: string, readTypes: 'open' | 'close') {
return post<unknown>('/v1/book/Information/addReadTime', {
return post<unknown>('/api/app/v1/book/Information/addReadTime', {
book_id: bookId,
read_types: readTypes,
})
}
export function getChapterNotesList(bookId: string, chapterId: string) {
return post<unknown>('/v1/book/Information/getChapterNotesList', {
return post<unknown>('/api/app/v1/book/Information/getChapterNotesList', {
page: 1,
page_size: 100,
book_id: bookId,
......@@ -226,7 +231,7 @@ export function addNotes(
color?: string
},
) {
return post<unknown>('/v1/book/Information/addNotes', {
return post<unknown>('/api/app/v1/book/Information/addNotes', {
book_id: bookId,
chapter_id: chapterId,
types: String(values.type ?? 3),
......@@ -243,7 +248,7 @@ export function editNotes(
noteId: string,
values: { content: string; noteContent: string; positioning: string; isOpen: boolean },
) {
return post<unknown>('/v1/book/Information/editNotes', {
return post<unknown>('/api/app/v1/book/Information/editNotes', {
notes_id: noteId,
book_id: bookId,
content: values.content,
......@@ -255,11 +260,14 @@ export function editNotes(
}
export function delNotes(bookId: string, noteId: string) {
return post<unknown>('/v1/book/Information/delNotes', { book_id: bookId, notes_id: noteId })
return post<unknown>('/api/app/v1/book/Information/delNotes', {
book_id: bookId,
notes_id: noteId,
})
}
export function getChapterCommentList(bookId: string, chapterId: string) {
return post<unknown>('/v1/book/Information/getChapterCommentList', {
return post<unknown>('/api/app/v1/book/Information/getChapterCommentList', {
page: 1,
page_size: 100,
book_id: bookId,
......@@ -268,7 +276,7 @@ export function getChapterCommentList(bookId: string, chapterId: string) {
}
export function addComment(bookId: string, chapterId: string, content: string) {
return post<unknown>('/v1/book/Information/addComment', {
return post<unknown>('/api/app/v1/book/Information/addComment', {
book_id: bookId,
chapter_id: chapterId,
comment_id: '0',
......@@ -279,7 +287,7 @@ export function addComment(bookId: string, chapterId: string, content: string) {
}
export function clickPraise(bookId: string, commentId: string, type: 0 | 1) {
return post<unknown>('/v1/book/Information/clickPraise', {
return post<unknown>('/api/app/v1/book/Information/clickPraise', {
book_id: bookId,
comment_id: commentId,
type,
......@@ -287,7 +295,7 @@ export function clickPraise(bookId: string, commentId: string, type: 0 | 1) {
}
export function delComment(bookId: string, commentId: string) {
return post<unknown>('/v1/book/Information/delComment', {
return post<unknown>('/api/app/v1/book/Information/delComment', {
book_id: bookId,
comment_id: commentId,
})
......
......@@ -2,7 +2,7 @@ import { useMutation, useQuery } from '@tanstack/react-query'
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'
import { Alert, Button, Skeleton, Tooltip, Typography } from 'antd'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router'
import { useNavigate, useParams } from 'react-router'
import {
addProgress,
addReadRecord,
......@@ -31,7 +31,6 @@ function findChapter(chapters: ReaderChapter[], chapterId: string): ReaderChapte
export function ReaderView() {
const { bookId = '', chapterId = '' } = useParams()
const navigate = useNavigate()
const location = useLocation()
const {
state: readerState,
setAnchor,
......
......@@ -75,18 +75,18 @@ async function post<T>(url: string, data: Record<string, unknown>): Promise<T> {
}
export async function getSearchHistory(): Promise<string[]> {
return list(await post<unknown>('/v1/book/Information/serachRecord', {}))
return list(await post<unknown>('/api/app/v1/book/Information/serachRecord', {}))
.map((item) => string(record(item).name).trim())
.filter(Boolean)
}
export async function clearSearchHistory(): Promise<boolean> {
const data = record(await post<unknown>('/v1/book/Information/serachDel', {}))
const data = record(await post<unknown>('/api/app/v1/book/Information/serachDel', {}))
return number(data.is_success) === 1
}
export async function getSearchBooks(state: SearchState): Promise<SearchBooksResult> {
const data = await post<unknown>('/v1/book/Information/searchBook', {
const data = await post<unknown>('/api/app/v1/book/Information/searchBook', {
page: state.page,
page_size: state.pageSize,
keyword: state.q,
......@@ -101,7 +101,7 @@ export async function getSearchBooks(state: SearchState): Promise<SearchBooksRes
export async function setSearchBookCollection(bookId: string, selected: boolean): Promise<boolean> {
const data = record(
await post<unknown>('/v1/book/Information/collection', {
await post<unknown>('/api/app/v1/book/Information/collection', {
book_id: bookId,
is_select: selected ? '1' : '0',
}),
......
declare module 'ali-oss' {
interface OSSOptions {
region: string
bucket: string
accessKeyId: string
accessKeySecret: string
stsToken?: string
}
export default class OSS {
constructor(options: OSSOptions)
put(name: string, file: Blob): Promise<unknown>
}
}
......@@ -4,7 +4,6 @@ import { applyRequestAuthentication, getDevelopmentRequestAuth } from '@/api/htt
import { expireMemberSession } from '@/modules/auth/session'
export const http = axios.create({
baseURL: '/api/web',
withCredentials: true,
headers: {
'Content-Type': 'application/json',
......
import { fileURLToPath, URL } from 'node:url'
import { loadEnv } from 'vite'
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import mkcert from 'vite-plugin-mkcert'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
const appApiTarget = env.VITE_APP_API_TARGET || 'http://127.0.0.1:7421'
const adminApiTarget = env.VITE_ADMIN_API_TARGET || 'http://127.0.0.1:7419'
return {
base: '/',
plugins: [react(), ...(mode === 'test' ? [] : [mkcert()])],
......@@ -24,13 +19,13 @@ export default defineConfig(({ mode }) => {
secure: false,
ws: true,
},
'/api/web': {
target: appApiTarget,
'/api/app': {
target: 'http://127.0.0.1:7421',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/web/, ''),
rewrite: (path) => path.replace(/^\/api\/app/, ''),
},
'/api/admin': {
target: adminApiTarget,
target: 'http://127.0.0.1:7419',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/admin/, ''),
},
......
# Book Full Content Export Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Export every authoring-content block in a book as readable Word-compatible HTML.
**Architecture:** The backend retains the complete ordered chapter tree and includes every node's body before its children. A focused frontend normalizer replaces editor-only custom nodes with static HTML, loads practice questions through the existing API, then passes the result to the existing image-embedding DOCX exporter.
**Tech Stack:** PHP/ThinkPHP, React, DOMParser, html-docx-js-typescript, Vitest.
## Global Constraints
- Export authoring content only; exclude comments and audit records.
- Use static links for audio and video, not browser media players in Word.
- Do not create commits.
---
### Task 1: Preserve all chapter bodies
**Files:**
- Modify: `admin/com-ebook-pc-api/app/event/book/TeacherBookEvent.php`
- [x] Change the export tree traversal so it appends a node's content in a chapter-id container immediately after its heading, before recursing into children.
- [x] Return a base export name without a misleading `.pdf` extension.
- [x] Validate PHP syntax with `php -l`.
### Task 2: Normalize editor-only blocks for Word
**Files:**
- Create: `admin/center-book/apps/admin/src/modules/books/management/export/normalizeBookExportHtml.ts`
- Create: `admin/center-book/apps/admin/src/modules/books/management/export/normalizeBookExportHtml.test.ts`
- [x] Write failing tests proving gallery images/captions, fetched expansion text, links, tooltips, media links, and a fetched practice question become static HTML.
- [x] Implement a DOM-based normalizer that converts each custom node while preserving ordinary body HTML and loads dynamic extension/practice payloads from injected request functions.
- [x] Run the focused Vitest test and confirm it passes.
### Task 3: Connect book export UI
**Files:**
- Modify: `admin/center-book/apps/admin/src/modules/books/management/views/ManagementListView.tsx`
- Modify: `admin/center-book/apps/admin/src/modules/books/management/components/ExportBookModal.tsx`
- [x] Normalize the server HTML before calling `exportHtmlToWord`, loading practice data via the existing request API.
- [x] Remove the obsolete custom-block deletion selector list.
- [x] Replace the modal's unsupported-content notice with an accurate export description.
- [x] Build the admin app with `pnpm --filter @ebook/admin build`.
# 书籍完整内容导出设计
## 目标
将书籍导出为可阅读、可编辑的 Word 文档,保留章与小节的正文,以及画廊、扩展阅读、练习题、自定义链接卡片、气泡提示、视频和音频。
## 约束
- Word 不可靠支持网页视频、音频播放器;媒体导出为标题和可点击地址。
- 图片继续转为内嵌 Data URI,确保离线打开文档也可显示。
- 评论、审核记录不属于书籍正文,不在本次导出范围。
## 方案
后端按目录树输出每个节点的标题与自身内容,再递归输出子节点,并使用章节内容容器保留自定义节点所属的章节。前端将编辑器自定义节点转换为 Word 兼容的静态 HTML;练习题和扩展阅读分别通过现有章节题目、扩展阅读接口,按节点位置和所属章节加载完整内容。
文件名由后端返回无扩展名的基础名称,下载端统一添加 `.docx`。
......@@ -155,6 +155,9 @@ importers:
'@tanstack/react-query':
specifier: ^5.76.2
version: 5.101.2(react@19.2.7)
ali-oss:
specifier: ^6.20.0
version: 6.23.0
antd:
specifier: ^6.5.1
version: 6.5.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
......
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论