提交 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_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' ...@@ -2,12 +2,12 @@ import axios from '@/utils/axios'
// 获取用户 // 获取用户
export function getUser() { export function getUser() {
return axios.post('/api/user/user/getUserInfo') return axios.post('/api/admin/user/user/getUserInfo')
} }
// 获取菜单 // 获取菜单
export async function getMenuList(params) { 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 { return {
data: response.data, data: response.data,
operation_permissions_list: response.operation_permissions_list ?? [], operation_permissions_list: response.operation_permissions_list ?? [],
...@@ -16,15 +16,15 @@ export async function getMenuList(params) { ...@@ -16,15 +16,15 @@ export async function getMenuList(params) {
// 检查当前是否存在可用的 SSO 登录态;未登录时后端返回 { run: "login" }。 // 检查当前是否存在可用的 SSO 登录态;未登录时后端返回 { run: "login" }。
export async function checkSSOLogin() { export async function checkSSOLogin() {
return (await axios.post('/api/user/checkSsoLogin')).data return (await axios.post('/api/admin/user/checkSsoLogin')).data
} }
// 获取STSToken // 获取STSToken
export function getSTSToken(data) { export function getSTSToken(data) {
return axios.post('/api/common/OssUpload/getUploadToken', data) return axios.post('/api/admin/common/OssUpload/getUploadToken', data)
} }
// 退出登录 // 退出登录
export function logout() { 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' ...@@ -2,20 +2,20 @@ import axios from '@/utils/axios'
// 获取未读消息条数 // 获取未读消息条数
export function getMessageCount(data) { 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) { 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) { export function setMessageRead(data) {
return axios.post('/api/message/message/setOneRead', data) return axios.post('/api/admin/message/message/setOneRead', data)
} }
// 设置全部已读 // 设置全部已读
export function setMessageAllRead() { 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' ...@@ -2,5 +2,5 @@ import axios from '@/utils/axios'
// 获取教师列表 // 获取教师列表
export function getTeacherList(data) { 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 ...@@ -50,14 +50,14 @@ export function AppList<Row extends object, Filters extends object = Record<stri
} }
return ( return (
<div className="app-list"> <div className='app-list'>
{filters ? ( {filters ? (
<section className="app-list__filters" aria-label="筛选条件"> <section className='app-list__filters' aria-label='筛选条件'>
<div className="app-list__filter-main"> <div className='app-list__filter-main'>
<div className="app-list__filter-items"> <div className='app-list__filter-items'>
{filters.items.map((item) => ( {filters.items.map((item) => (
<div className="app-list__filter-item" key={String(item.name)}> <div className='app-list__filter-item' key={String(item.name)}>
<span className="app-list__filter-label">{item.label}</span> <span className='app-list__filter-label'>{item.label}</span>
{item.render({ {item.render({
value: filters.value[item.name], value: filters.value[item.name],
onChange: (value) => updateFilter(item, value), onChange: (value) => updateFilter(item, value),
...@@ -65,8 +65,8 @@ export function AppList<Row extends object, Filters extends object = Record<stri ...@@ -65,8 +65,8 @@ export function AppList<Row extends object, Filters extends object = Record<stri
</div> </div>
))} ))}
</div> </div>
<Space wrap className="app-list__filter-actions"> <Space wrap className='app-list__filter-actions'>
<Button type="primary" icon={<SearchOutlined />} onClick={filters.onSearch}> <Button type='primary' icon={<SearchOutlined />} onClick={filters.onSearch}>
搜索 搜索
</Button> </Button>
<Button icon={<ReloadOutlined />} onClick={filters.onReset}> <Button icon={<ReloadOutlined />} onClick={filters.onReset}>
...@@ -74,21 +74,21 @@ export function AppList<Row extends object, Filters extends object = Record<stri ...@@ -74,21 +74,21 @@ export function AppList<Row extends object, Filters extends object = Record<stri
</Button> </Button>
</Space> </Space>
</div> </div>
{toolbar ? <div className="app-list__toolbar">{toolbar}</div> : null} {toolbar ? <div className='app-list__toolbar'>{toolbar}</div> : null}
</section> </section>
) : null} ) : null}
{!filters && toolbar ? <div className="app-list__toolbar">{toolbar}</div> : null} {!filters && toolbar ? <div className='app-list__toolbar'>{toolbar}</div> : null}
{hasRefreshError ? ( {hasRefreshError ? (
<Alert <Alert
className="app-list__error" className='app-list__error'
type="error" type='error'
showIcon showIcon
message={data.error?.message} message={data.error?.message}
action={ action={
onRetry ? ( onRetry ? (
<Button size="small" danger onClick={onRetry}> <Button size='small' danger onClick={onRetry}>
重试 重试
</Button> </Button>
) : null ) : null
...@@ -97,10 +97,10 @@ export function AppList<Row extends object, Filters extends object = Record<stri ...@@ -97,10 +97,10 @@ export function AppList<Row extends object, Filters extends object = Record<stri
) : null} ) : null}
{hasBlockingError ? ( {hasBlockingError ? (
<div className="app-list__blocking-error" role="alert"> <div className='app-list__blocking-error' role='alert'>
<Empty description={data.error?.message}> <Empty description={data.error?.message}>
{onRetry ? ( {onRetry ? (
<Button type="primary" danger onClick={onRetry}> <Button type='primary' danger onClick={onRetry}>
重试 重试
</Button> </Button>
) : null} ) : null}
...@@ -115,7 +115,7 @@ export function AppList<Row extends object, Filters extends object = Record<stri ...@@ -115,7 +115,7 @@ export function AppList<Row extends object, Filters extends object = Record<stri
loading={data.loading} loading={data.loading}
pagination={tablePagination} pagination={tablePagination}
rowSelection={selection} rowSelection={selection}
locale={{ ...tableProps?.locale, emptyText: <Empty description="暂无数据" /> }} locale={{ ...tableProps?.locale, emptyText: <Empty description='暂无数据' /> }}
/> />
)} )}
</div> </div>
......
...@@ -2,15 +2,15 @@ import axios from '@/utils/axios' ...@@ -2,15 +2,15 @@ import axios from '@/utils/axios'
// 百度聊天接口 // 百度聊天接口
export function baiduAIChat(data) { 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) { export function addType(data) {
return axios.post('/api/type/autoAdd', data) return axios.post('/api/admin/type/autoAdd', data)
} }
// 创建试题 // 创建试题
export function addQuestion(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' ...@@ -2,15 +2,15 @@ import axios from '@/utils/axios'
// 查询我的设计 // 查询我的设计
export function getDesignList(data) { 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) { 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) { 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) => { ...@@ -109,7 +109,6 @@ const renderExpandRead = (elem, children, editor) => {
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
fontSize: '18px', fontSize: '18px',
lineHeight: '24px', lineHeight: '24px',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all', wordBreak: 'break-all',
wordWrap: 'break-word', wordWrap: 'break-word',
fontWeight: 'bold', fontWeight: 'bold',
......
...@@ -37,8 +37,13 @@ const InsertTopic = (props, ref) => { ...@@ -37,8 +37,13 @@ const InsertTopic = (props, ref) => {
return groups return groups
}, [insertList]) }, [insertList])
const { radio: radioList, checkbox: checkboxList, equals: equalsList, press: pressList, textarea: textareaList } = const {
questionGroups radio: radioList,
checkbox: checkboxList,
equals: equalsList,
press: pressList,
textarea: textareaList,
} = questionGroups
// 删除 // 删除
const delConfirm = (record) => { const delConfirm = (record) => {
......
...@@ -6,8 +6,8 @@ const post = async (path, params, notify = false) => { ...@@ -6,8 +6,8 @@ const post = async (path, params, notify = false) => {
return response.data return response.data
} }
export const getList = (params) => post('/question/getList', params) export const getList = (params) => post('/api/admin/question/getList', params)
export const questionAdd = (params) => post('/question/add', params, true) export const questionAdd = (params) => post('/api/admin/question/add', params, true)
export const questionAddType = (params) => post('/type/add', params) export const questionAddType = (params) => post('/api/admin/type/add', params)
export const typeAllList = (params) => post('/type/getList', params) export const typeAllList = (params) => post('/api/admin/type/getList', params)
export const bookNameList = (params) => post('/book/teacher/getListAll', params) export const bookNameList = (params) => post('/api/admin/book/teacher/getListAll', params)
...@@ -2,17 +2,21 @@ import axios from '@/utils/axios' ...@@ -2,17 +2,21 @@ import axios from '@/utils/axios'
const post = async (path, params) => (await axios.post(path, params)).data const post = async (path, params) => (await axios.post(path, params)).data
export const getTopicList = (params) => post('/book/question/getList', params) export const getTopicList = (params) => post('/api/admin/book/question/getList', params)
export const getTopicTypeList = (params) => post('/book/question/type/getList', params) export const getTopicTypeList = (params) => post('/api/admin/book/question/type/getList', params)
export const addChapterTopic = (params) => post('/book/chapter/question/add', params) export const addChapterTopic = (params) => post('/api/admin/book/chapter/question/add', params)
export const getChapterTopic = (params) => post('/book/chapter/question/getQuestionList', params) export const getChapterTopic = (params) =>
export const delChapterTopic = (params) => post('/book/chapter/question/removeQuestion', params) post('/api/admin/book/chapter/question/getQuestionList', params)
export const addChapterTooltip = (params) => post('/book/bubble/add', params) export const delChapterTopic = (params) =>
export const delChapterTooltip = (params) => post('/book/bubble/removeBubble', params) post('/api/admin/book/chapter/question/removeQuestion', params)
export const addExpandRead = (params) => post('/book/extend/add', params) export const addChapterTooltip = (params) => post('/api/admin/book/bubble/add', params)
export const delExpandRead = (params) => post('/book/extend/removeExtend', params) export const delChapterTooltip = (params) => post('/api/admin/book/bubble/removeBubble', params)
export const expandReadInfo = (params) => post('/book/extend/info', params) export const addExpandRead = (params) => post('/api/admin/book/extend/add', params)
export const addGallery = (params) => post('/book/gallery/add', params) export const delExpandRead = (params) => post('/api/admin/book/extend/removeExtend', params)
export const delGallery = (params) => post('/book/gallery/removeGallery', params) export const expandReadInfo = (params) => post('/api/admin/book/extend/info', params)
export const getRecordInfo = (params) => post('/book/teacher/section/getRecordInfo', params) export const addGallery = (params) => post('/api/admin/book/gallery/add', params)
export const getRecordList = (params) => post('/book/teacher/section/getRecordList', 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 = ({ ...@@ -16,9 +16,7 @@ export const EditorSidebar = ({
{[ {[
{ key: 'text', title: '文本设置' }, { key: 'text', title: '文本设置' },
{ key: 'style', title: '样式模版' }, { key: 'style', title: '样式模版' },
...(reviewCommentPanel ...(reviewCommentPanel ? [{ key: 'comment', title: '评论' }] : []),
? [{ key: 'comment', title: '评论' }]
: []),
].map((item) => ( ].map((item) => (
<div <div
className={`tabs-item ${item.key === tabKey ? 'active' : ''}`} className={`tabs-item ${item.key === tabKey ? 'active' : ''}`}
......
...@@ -176,9 +176,14 @@ export default function AppLayout() { ...@@ -176,9 +176,14 @@ export default function AppLayout() {
const pageTitle = headerOverride?.title || getCurrentMenuTitle(menuList, pathname) const pageTitle = headerOverride?.title || getCurrentMenuTitle(menuList, pathname)
const getSSOToken = async () => { const getSSOToken = async () => {
const data = await checkSSOLogin() if (localStorage.getItem('kiwi.token')) return
if (data && data.token) { try {
localStorage.setItem('kiwi.token', data.token) const data = await checkSSOLogin()
if (data?.token) {
localStorage.setItem('kiwi.token', data.token)
}
} catch {
// SSO 不可用时仍继续由后续业务接口校验当前登录态。
} }
} }
// 获取菜单 // 获取菜单
......
...@@ -61,28 +61,25 @@ export function usePagedList<Row, Filters, Query>({ ...@@ -61,28 +61,25 @@ export function usePagedList<Row, Filters, Query>({
requestRef.current = request requestRef.current = request
}, [request]) }, [request])
const load = useCallback( const load = useCallback(async (query: Query) => {
async (query: Query) => { const requestId = ++requestIdRef.current
const requestId = ++requestIdRef.current lastQueryRef.current = query
lastQueryRef.current = query setState((current) => ({ ...current, loading: true, error: null }))
setState((current) => ({ ...current, loading: true, error: null }))
try {
try { const result = await requestRef.current(query)
const result = await requestRef.current(query) if (requestId !== requestIdRef.current) return
if (requestId !== requestIdRef.current) return setState((current) => ({ ...current, rows: result.rows, total: result.total }))
setState((current) => ({ ...current, rows: result.rows, total: result.total })) } catch (reason) {
} catch (reason) { if (requestId === requestIdRef.current) {
if (requestId === requestIdRef.current) { setState((current) => ({ ...current, error: toError(reason) }))
setState((current) => ({ ...current, error: toError(reason) }))
}
} finally {
if (requestId === requestIdRef.current) {
setState((current) => ({ ...current, loading: false }))
}
} }
}, } finally {
[], if (requestId === requestIdRef.current) {
) setState((current) => ({ ...current, loading: false }))
}
}
}, [])
const loadFor = useCallback( const loadFor = useCallback(
(page: number, pageSize: number, filters: Filters) => (page: number, pageSize: number, filters: Filters) =>
...@@ -155,11 +152,7 @@ export function usePagedList<Row, Filters, Query>({ ...@@ -155,11 +152,7 @@ export function usePagedList<Row, Filters, Query>({
const changePageSize = useCallback( const changePageSize = useCallback(
(pageSize: number) => { (pageSize: number) => {
setState((current) => ({ ...current, page: initialPage, pageSize })) setState((current) => ({ ...current, page: initialPage, pageSize }))
void loadFor( void loadFor(initialPage, pageSize, useAppliedFilters ? state.appliedFilters : state.filters)
initialPage,
pageSize,
useAppliedFilters ? state.appliedFilters : state.filters,
)
}, },
[initialPage, loadFor, state.appliedFilters, state.filters, useAppliedFilters], [initialPage, loadFor, state.appliedFilters, state.filters, useAppliedFilters],
) )
......
...@@ -38,36 +38,39 @@ async function mutate(url: string, params: ApiParams): Promise<boolean> { ...@@ -38,36 +38,39 @@ async function mutate(url: string, params: ApiParams): Promise<boolean> {
} }
export async function getAdminList(params: AdminQuery): Promise<AdminListResult> { 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 }>(
...params, '/api/admin/user/user/getList',
}) {
...params,
},
)
return { rows: data.list ?? [], total: data.total ?? 0 } return { rows: data.list ?? [], total: data.total ?? 0 }
} }
export function getRoleOptions() { 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) { 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) { export function addAdmin(params: ApiParams) {
return mutate('/user/user/add', params) return mutate('/api/admin/user/user/add', params)
} }
export function editAdmin(params: ApiParams) { export function editAdmin(params: ApiParams) {
return mutate('/user/user/edit', params) return mutate('/api/admin/user/user/edit', params)
} }
export function deleteAdmin(id: number) { 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') { 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) { 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) { ...@@ -37,44 +37,49 @@ async function mutate(url: string, params: ApiParams) {
} }
export async function getRoleList(params: RoleQuery): Promise<RoleListResult> { 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 }>(
...params, '/api/admin/user/role/getList',
}) {
...params,
},
)
return { rows: data.list ?? [], total: data.total ?? 0 } return { rows: data.list ?? [], total: data.total ?? 0 }
} }
export function getRoleInfo(id: number) { 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) { export function addRole(params: ApiParams) {
return mutate('/user/role/add', params) return mutate('/api/admin/user/role/add', params)
} }
export function editRole(params: ApiParams) { export function editRole(params: ApiParams) {
return mutate('/user/role/edit', params) return mutate('/api/admin/user/role/edit', params)
} }
export function deleteRole(id: number) { 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') { 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) { 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>) { 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() { export function getAllPermissions() {
return getPayload<PermissionRecord[]>('/system/power/getListAll', {}) return getPayload<PermissionRecord[]>('/api/admin/system/power/getListAll', {})
} }
export function getTeacherRoles() { 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' import axios, { notifySuccess } from '@/utils/axios'
export const getUserInfo = async (params) => 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) => { 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) notifySuccess(response.message)
return response.data return response.data
} }
...@@ -13,7 +13,7 @@ export type AdsenseListResult = { ...@@ -13,7 +13,7 @@ export type AdsenseListResult = {
} }
export async function getPositionList(query: Record<string, unknown>): Promise<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 } const data = response.data as { list?: AdsenseRow[]; total?: number }
return { return {
...@@ -29,7 +29,7 @@ export async function changePositionStatus({ ...@@ -29,7 +29,7 @@ export async function changePositionStatus({
id: number id: number
status: '0' | '1' status: '0' | '1'
}): Promise<boolean> { }): 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) notifySuccess(response.message)
return true return true
} }
...@@ -37,33 +37,36 @@ async function mutate(url: string, params: ApiParams) { ...@@ -37,33 +37,36 @@ async function mutate(url: string, params: ApiParams) {
} }
export async function getAdvertList(query: AdvertQuery): Promise<AdvertListResult> { 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 }>(
...query, '/api/admin/ad/ad/getList',
}) {
...query,
},
)
return { rows: data.list ?? [], total: data.total ?? 0 } return { rows: data.list ?? [], total: data.total ?? 0 }
} }
export function getAdvertInfo(id: number) { 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) { export function addAdvert(params: ApiParams) {
return mutate('/ad/ad/add', params) return mutate('/api/admin/ad/ad/add', params)
} }
export function editAdvert(params: ApiParams) { export function editAdvert(params: ApiParams) {
return mutate('/ad/ad/edit', params) return mutate('/api/admin/ad/ad/edit', params)
} }
export function deleteAdvert(id: number) { 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') { 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[]> { 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[] return (result.list ?? []) as AdvertPosition[]
} }
...@@ -4,7 +4,9 @@ const LOGIN_SUCCESS_MESSAGE = 'OK' ...@@ -4,7 +4,9 @@ const LOGIN_SUCCESS_MESSAGE = 'OK'
export const login = async (params) => { export const login = async (params) => {
try { 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 ?? {} const data = response.data ?? {}
if (response.code === 200 && response.message === LOGIN_SUCCESS_MESSAGE && data.token) { if (response.code === 200 && response.message === LOGIN_SUCCESS_MESSAGE && data.token) {
......
...@@ -142,8 +142,8 @@ const Login = () => { ...@@ -142,8 +142,8 @@ const Login = () => {
Icon22, Icon22,
Icon23, Icon23,
], ],
initUrl: '/user/login/initData', initUrl: '/api/admin/user/login/initData',
verifyUrl: '/user/login/checkVerify', verifyUrl: '/api/admin/user/login/checkVerify',
getSuccessState: function (data) { getSuccessState: function (data) {
console.log('验证成功', data) console.log('验证成功', data)
setDisLf(data.disLf) setDisLf(data.disLf)
......
...@@ -5,7 +5,8 @@ async function post(path: string, data: Record<string, unknown>) { ...@@ -5,7 +5,8 @@ async function post(path: string, data: Record<string, unknown>) {
return { data: response.data, msg: response.message || '' } return { data: response.data, msg: response.message || '' }
} }
export const getAuditList = async (query: AuditQuery) => 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) => export const getExamineList = async (id: string | number) =>
(await post('/book/teacher/getExamineList', { id })).data (await post('/api/admin/book/teacher/getExamineList', { id })).data
export const addExamine = (data: Record<string, unknown>) => post('/book/examine/add', 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) ...@@ -5,11 +5,15 @@ async function post(path: string, data: Record<string, unknown>, notify = false)
if (notify) notifySuccess(response.message) if (notify) notifySuccess(response.message)
return response.data return response.data
} }
export const getClassifyList = (query: ClassifyQuery) => post('/book/category/getList', query) export const getClassifyList = (query: ClassifyQuery) =>
export const addCategory = (data: Record<string, unknown>) => post('/book/category/add', data, true) post('/api/admin/book/category/getList', query)
export const getCategory = (id: string | number) => post('/book/category/getInfoById', { id }) 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>) => export const editCategory = (data: Record<string, unknown>) =>
post('/book/category/edit', data, true) post('/api/admin/book/category/edit', data, true)
export const deleteCategory = (id: string | number) => post('/book/category/del', { id }, true) export const deleteCategory = (id: string | number) =>
post('/api/admin/book/category/del', { id }, true)
export const changeCategorySort = (data: Record<string, unknown>) => 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) ...@@ -6,11 +6,12 @@ async function post(path: string, data: Record<string, unknown>, notify = false)
return response.data return response.data
} }
export const getDiscussionList = (q: DiscussionQuery) => 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) => export const deleteComment = (id: any, book_id: any) =>
post('/comment/comment/delComment', { id, book_id }, true) post('/api/admin/comment/comment/delComment', { id, book_id }, true)
export const changeCommentStatus = (data: any) => post('/comment/comment/changeStatus', data, true) export const changeCommentStatus = (data: any) =>
export const getBooks = () => post('/book/book/getAllList', {}) post('/api/admin/comment/comment/changeStatus', data, true)
export const getDiscussionSystem = () => post('/system/config/getSystem', { type: 3 }) 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) => 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) ...@@ -5,9 +5,11 @@ async function post(path: string, data: Record<string, unknown>, notify = false)
if (notify) notifySuccess(response.message) if (notify) notifySuccess(response.message)
return response.data return response.data
} }
export const getLabelList = (query: LabelQuery) => post('/book/label/getList', query) export const getLabelList = (query: LabelQuery) => post('/api/admin/book/label/getList', query)
export const getAllBooks = () => post('/book/label/getAllBooks', {}) export const getAllBooks = () => post('/api/admin/book/label/getAllBooks', {})
export const getLabel = (id: string | number) => post('/book/label/getInfoById', { id }) export const getLabel = (id: string | number) => post('/api/admin/book/label/getInfoById', { id })
export const addLabel = (data: Record<string, unknown>) => post('/book/label/add', data, true) export const addLabel = (data: Record<string, unknown>) =>
export const editLabel = (data: Record<string, unknown>) => post('/book/label/edit', data, true) post('/api/admin/book/label/add', data, true)
export const deleteLabel = (id: string | number) => post('/book/label/del', { id }, 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( ...@@ -46,39 +46,39 @@ export async function getManagementList(
query: ManagementQuery, query: ManagementQuery,
): Promise<ManagementListResult | false> { ): Promise<ManagementListResult | false> {
const data = await getPayload<{ list?: ManagementRow[]; total?: number }>( const data = await getPayload<{ list?: ManagementRow[]; total?: number }>(
'/book/teacher/getList', '/api/admin/book/teacher/getList',
{ ...query }, { ...query },
) )
return { rows: data.list ?? [], total: data.total ?? 0 } return { rows: data.list ?? [], total: data.total ?? 0 }
} }
export function deleteManagementBook(id: number) { export function deleteManagementBook(id: number) {
return mutate('/book/teacher/del', { id }) return mutate('/api/admin/book/teacher/del', { id })
} }
// 编辑页仍沿用旧接口封装的返回值形状;保留在管理模块中,避免页面反向依赖旧目录。 // 编辑页仍沿用旧接口封装的返回值形状;保留在管理模块中,避免页面反向依赖旧目录。
export function getBook(params: ApiParams) { 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) { 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) { 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) { 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) { 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) { 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' }, headers: { 'Content-Type': 'multipart/form-data' },
}) })
notifySuccess(response.message) notifySuccess(response.message)
......
...@@ -9,47 +9,47 @@ const mutate = async (path, params, notify = false) => { ...@@ -9,47 +9,47 @@ const mutate = async (path, params, notify = false) => {
// 获取书籍详情 // 获取书籍详情
export function getBook(data) { 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) { 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) { 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) { 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) { 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) { 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) { 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) { 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) { 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) { ...@@ -62,26 +62,30 @@ export function bindTag(data) {
return axios.post('/api/resource/v1/course/course/tag-add-book', data) return axios.post('/api/resource/v1/course/course/tag-add-book', data)
} }
export const getUserInfo = (params) => getData('/user/user/getUserInfo', params) export const getUserInfo = (params) => getData('/api/admin/user/user/getUserInfo', params)
export const getPowerByRoleId = (params) => getData('/user/role/getPowerByRoleId', params) export const getPowerByRoleId = (params) => getData('/api/admin/user/role/getPowerByRoleId', params)
export const getAllList = async (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 } return { code: response.code, data: response.data }
} }
export const chapterAdd = (params) => getData('/book/teacher/chapter/add', params) export const chapterAdd = (params) => getData('/api/admin/book/teacher/chapter/add', params)
export const getInfoById = (params) => getData('/book/teacher/chapter/getInfoById', params) export const getInfoById = (params) =>
export const chapterEdit = (params) => mutate('/book/teacher/chapter/edit', params, true) getData('/api/admin/book/teacher/chapter/getInfoById', params)
export const chapterDel = (params) => mutate('/book/teacher/chapter/del', params, true) export const chapterEdit = (params) => mutate('/api/admin/book/teacher/chapter/edit', params, true)
export const dragOrder = (params) => mutate('/book/teacher/chapter/dragOrder', 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) => export const getInfoByChapterId = (params) =>
getData('/book/teacher/section/getInfoByChapterId', params) getData('/api/admin/book/teacher/section/getInfoByChapterId', params)
export const sectionAdd = (params) => mutate('/book/teacher/section/add', params, true) export const sectionAdd = (params) => mutate('/api/admin/book/teacher/section/add', params, true)
export const sectionEdit = (params) => mutate('/book/teacher/section/edit', params) export const sectionEdit = (params) => mutate('/api/admin/book/teacher/section/edit', params)
export const getQuestionList = (params) => getData('/book/chapter/question/getQuestionList', params) export const getQuestionList = (params) =>
export const questionDel = (params) => getData('/book/chapter/question/del', params) getData('/api/admin/book/chapter/question/getQuestionList', params)
export const questionAdd = (params) => getData('/book/chapter/question/add', params) export const questionDel = (params) => getData('/api/admin/book/chapter/question/del', params)
export const release = (params) => mutate('/book/teacher/release', params) export const questionAdd = (params) => getData('/api/admin/book/chapter/question/add', params)
export const templateGetList = (params) => mutate('/book/template/getList', params) export const release = (params) => mutate('/api/admin/book/teacher/release', params)
export const templateImport = (params) => mutate('/book/template/import', params) export const templateGetList = (params) => mutate('/api/admin/book/template/getList', params)
export const teacherImport = (params) => mutate('/book/teacher/import', params) export const templateImport = (params) => mutate('/api/admin/book/template/import', params)
export const getRecordList = (params) => getData('/book/teacher/section/getRecordList', 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 @@ ...@@ -82,7 +82,6 @@
.ant-spin-container { .ant-spin-container {
height: 100%; height: 100%;
} }
} }
} }
.draggable-tree { .draggable-tree {
......
...@@ -25,7 +25,7 @@ export default function ExportBookModal({ open, onCancel, onConfirm }: ExportBoo ...@@ -25,7 +25,7 @@ export default function ExportBookModal({ open, onCancel, onConfirm }: ExportBoo
okText='确认' okText='确认'
> >
<p style={{ textAlign: 'center', padding: '30px 0 18px', fontSize: 16 }}> <p style={{ textAlign: 'center', padding: '30px 0 18px', fontSize: 16 }}>
只能导出文本部分,画廊、音频、视频不支持 将导出书籍正文、图片、画廊、扩展阅读、练习题、链接、气泡提示、视频和音频
</p> </p>
</Modal> </Modal>
) )
......
...@@ -39,9 +39,7 @@ const BookAddEdit = () => { ...@@ -39,9 +39,7 @@ const BookAddEdit = () => {
const editors = values.editors?.map((item) => item.value) || [] const editors = values.editors?.map((item) => item.value) || []
const params = { ...values, img, editors } const params = { ...values, img, editors }
if (editors.length === 0) delete params.editors if (editors.length === 0) delete params.editors
const res = isEdit const res = isEdit ? await updateBook({ id: bookId, ...params }) : await addBook(params)
? await updateBook({ id: bookId, ...params })
: await addBook(params)
message.success(res.message) message.success(res.message)
navigate(-1) 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' ...@@ -5,6 +5,7 @@ import { useState } from 'react'
import { useNavigate } from 'react-router' import { useNavigate } from 'react-router'
import { AppList, type AppListFilterItem } from '@/components/AppList' import { AppList, type AppListFilterItem } from '@/components/AppList'
import { expandReadInfo, getChapterTopic } from '@/components/editor/wangeditor-customer/public-api'
import { exportHtmlToWord } from '@/utils/exportWordHtmlDocx' import { exportHtmlToWord } from '@/utils/exportWordHtmlDocx'
import { usePermission } from '@/hooks/usePermission' import { usePermission } from '@/hooks/usePermission'
import { useUserStore } from '@/stores/userStore' import { useUserStore } from '@/stores/userStore'
...@@ -19,6 +20,7 @@ import { ...@@ -19,6 +20,7 @@ import {
} from '../api' } from '../api'
import DeleteBookModal from '../components/DeleteBookModal' import DeleteBookModal from '../components/DeleteBookModal'
import ExportBookModal from '../components/ExportBookModal' import ExportBookModal from '../components/ExportBookModal'
import { normalizeBookExportHtml } from '../export/normalizeBookExportHtml'
import ImportBookDrawer from '../components/ImportBookDrawer' import ImportBookDrawer from '../components/ImportBookDrawer'
import ReleaseBookDrawer, { type ReleaseBookFormValues } from '../components/ReleaseBookDrawer' import ReleaseBookDrawer, { type ReleaseBookFormValues } from '../components/ReleaseBookDrawer'
import type { ManagementFilters } from '../query' import type { ManagementFilters } from '../query'
...@@ -26,15 +28,6 @@ import { useManagementList } from '../useManagementList' ...@@ -26,15 +28,6 @@ import { useManagementList } from '../useManagementList'
const { RangePicker } = DatePicker const { RangePicker } = DatePicker
const IMAGE_FALLBACK = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=' 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() { export default function ManagementListView() {
const list = useManagementList() const list = useManagementList()
const navigate = useNavigate() const navigate = useNavigate()
...@@ -61,7 +54,12 @@ export default function ManagementListView() { ...@@ -61,7 +54,12 @@ export default function ManagementListView() {
try { try {
const result = await exportManagementBook(selectedId) const result = await exportManagementBook(selectedId)
if (!result.name) return 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) { } catch (error) {
console.error('导出Word失败', error) console.error('导出Word失败', error)
} finally { } finally {
...@@ -152,9 +150,7 @@ export default function ManagementListView() { ...@@ -152,9 +150,7 @@ export default function ManagementListView() {
</Button> </Button>
) : null} ) : null}
{checkPermission('/books/management/getInfoById') ? ( {checkPermission('/books/management/getInfoById') ? (
<Button <Button onClick={() => navigate(`/books/management/chapter?book_id=${record.id}`)}>
onClick={() => navigate(`/books/management/chapter?book_id=${record.id}`)}
>
编辑 编辑
</Button> </Button>
) : null} ) : null}
......
...@@ -60,18 +60,21 @@ async function getPayload<T>(url: string, params?: ApiParams): Promise<T> { ...@@ -60,18 +60,21 @@ async function getPayload<T>(url: string, params?: ApiParams): Promise<T> {
} }
export async function getOrders(query: OrdersQuery): Promise<OrdersListResult> { 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 }>(
...query, '/api/admin/order/getOrderList',
}) {
...query,
},
)
return { rows: data.list ?? [], total: data.total ?? 0 } return { rows: data.list ?? [], total: data.total ?? 0 }
} }
export function getSalesStatistics() { export function getSalesStatistics() {
return getPayload<SalesStatisticsResult>('/order/salesStatistics') return getPayload<SalesStatisticsResult>('/api/admin/order/salesStatistics')
} }
export async function refundOrder(id: number, refund_reason?: string) { 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) notifySuccess(response.message)
return true return true
} }
...@@ -7,22 +7,26 @@ async function post(path: string, data: Record<string, unknown> = {}, notify = f ...@@ -7,22 +7,26 @@ async function post(path: string, data: Record<string, unknown> = {}, notify = f
return response.data return response.data
} }
export const getQuestionList = (query: QuestionBankQuery) => post('/book/question/getList', query) export const getQuestionList = (query: QuestionBankQuery) =>
export const getQuestion = (id: string | number) => post('/book/question/getInfoById', { id }) post('/api/admin/book/question/getList', query)
export const addQuestion = (data: Record<string, unknown>) => post('/book/question/add', data, true) 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>) => export const editQuestion = (data: Record<string, unknown>) =>
post('/book/question/edit', data, true) post('/api/admin/book/question/edit', data, true)
export const deleteQuestion = (id: string | number) => post('/book/question/del', { id }, true) export const deleteQuestion = (id: string | number) =>
post('/api/admin/book/question/del', { id }, true)
export const addQuestionType = (typename: string) => export const addQuestionType = (typename: string) =>
post('/book/question/type/add', { typename }, true) post('/api/admin/book/question/type/add', { typename }, true)
export const getQuestionTypes = () => export const getQuestionTypes = () =>
post('/book/question/type/getList', { page: 1, page_size: 999 }) post('/api/admin/book/question/type/getList', { page: 1, page_size: 999 })
export const getBookNames = () => post('book/teacher/getListAll') export const getBookNames = () => post('/api/admin/book/teacher/getListAll')
export const getAliOssToken = () => post('/common/OssUpload/getUploadToken') export const getAliOssToken = () => post('/api/admin/common/OssUpload/getUploadToken')
export async function importQuestions(file: File) { export async function importQuestions(file: File) {
const form = new FormData() const form = new FormData()
form.append('file', file) 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' }, headers: { 'Content-Type': 'multipart/form-data' },
}) })
return { msg: response.message, data: response.data } return { msg: response.message, data: response.data }
......
...@@ -10,11 +10,7 @@ async function requestQuestionBankList(query: ReturnType<typeof buildQuestionBan ...@@ -10,11 +10,7 @@ async function requestQuestionBankList(query: ReturnType<typeof buildQuestionBan
} }
export function useQuestionBankList() { export function useQuestionBankList() {
const list = usePagedList< const list = usePagedList<any, QuestionBankFilters, ReturnType<typeof buildQuestionBankQuery>>({
any,
QuestionBankFilters,
ReturnType<typeof buildQuestionBankQuery>
>({
initialFilters: {}, initialFilters: {},
buildQuery: ({ page, pageSize, filters }) => buildQuestionBankQuery(page, pageSize, filters), buildQuery: ({ page, pageSize, filters }) => buildQuestionBankQuery(page, pageSize, filters),
request: requestQuestionBankList, request: requestQuestionBankList,
......
...@@ -280,7 +280,7 @@ export default function QuestionBankView() { ...@@ -280,7 +280,7 @@ export default function QuestionBankView() {
<Button <Button
type='primary' type='primary'
ghost ghost
onClick={() => downloadFile(`temp/${importResult.data?.name}`)} onClick={() => downloadFile(`/api/admin/temp/${importResult.data?.name}`)}
> >
下载错误文件 下载错误文件
</Button> </Button>
......
...@@ -8,31 +8,31 @@ async function post<T>(path: string, data: unknown): Promise<T> { ...@@ -8,31 +8,31 @@ async function post<T>(path: string, data: unknown): Promise<T> {
} }
export const getReviewComments = (bookId: number, chapterId?: number) => 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, book_id: bookId,
...(chapterId ? { chapter_id: chapterId } : {}), ...(chapterId ? { chapter_id: chapterId } : {}),
}) })
export const getCurrentReviewComments = (bookId: number, chapterId?: number) => 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, book_id: bookId,
...(chapterId ? { chapter_id: chapterId } : {}), ...(chapterId ? { chapter_id: chapterId } : {}),
}) })
export const addReviewComment = (data: AddReviewCommentPayload) => 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) => 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) => 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) => 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) => 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) => 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) ...@@ -5,26 +5,29 @@ async function post(path: string, data: Record<string, unknown>, notify = false)
if (notify) notifySuccess(response.message) if (notify) notifySuccess(response.message)
return response.data 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) => 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) => 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 getListAll = (data: Record<string, unknown>) =>
export const getInfoById = (data: Record<string, unknown>) => post('/book/book/getInfoById', data) 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>) => 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>) => export const publisherAdd = (data: Record<string, unknown>) =>
post('/book/publisher/add', data, true) post('/api/admin/book/publisher/add', data, true)
export const editBook = (data: Record<string, unknown>) => post('/book/book/edit', data, true) export const editBook = (data: Record<string, unknown>) =>
post('/api/admin/book/book/edit', data, true)
export const getCommentInfo = (data: Record<string, unknown>) => 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>) => { 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 return true
} }
export const commentReply = async (data: Record<string, unknown>) => { 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 return true
} }
...@@ -150,7 +150,7 @@ const DiscussDetail = () => { ...@@ -150,7 +150,7 @@ const DiscussDetail = () => {
} else if (type === 'text') { } else if (type === 'text') {
return <span key={index}>{value}</span> return <span key={index}>{value}</span>
} else if (type === 'image') { } else if (type === 'image') {
return <img height={100} src={value}></img> return <img key={index} height={100} src={value}></img>
} }
})} })}
</div> </div>
......
...@@ -98,16 +98,15 @@ export default function SaleListView() { ...@@ -98,16 +98,15 @@ export default function SaleListView() {
title: '操作', title: '操作',
render: (_, row) => ( render: (_, row) => (
<Space> <Space>
{Boolean(row.status) && {Boolean(row.status) && checkPermission('/books/sale/getCommentList') && (
checkPermission('/books/sale/getCommentList') && ( <Button
<Button onClick={() =>
onClick={() => navigate(`discussP?book_id=${row.id}&book_name=${encodeURIComponent(row.name)}`)
navigate(`discussP?book_id=${row.id}&book_name=${encodeURIComponent(row.name)}`) }
} >
> 讨论
讨论 </Button>
</Button> )}
)}
{!row.status && checkPermission('/books/sale/getInfoById') && ( {!row.status && checkPermission('/books/sale/getInfoById') && (
<Button onClick={() => navigate(`detail?id=${row.id}`)}>编辑</Button> <Button onClick={() => navigate(`detail?id=${row.id}`)}>编辑</Button>
)} )}
......
...@@ -39,32 +39,32 @@ async function mutate(url: string, params: ApiParams) { ...@@ -39,32 +39,32 @@ async function mutate(url: string, params: ApiParams) {
export async function getMemberLevelList(params: MemberLevelQuery): Promise<MemberLevelListResult> { export async function getMemberLevelList(params: MemberLevelQuery): Promise<MemberLevelListResult> {
const data = await getPayload<{ list?: MemberLevelRow[]; total?: number }>( const data = await getPayload<{ list?: MemberLevelRow[]; total?: number }>(
'/members/grade/getList', '/api/admin/members/grade/getList',
{ ...params }, { ...params },
) )
return { rows: data.list ?? [], total: data.total ?? 0 } return { rows: data.list ?? [], total: data.total ?? 0 }
} }
export function getMemberLevelInfo(id: number) { 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) { 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) { 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) { export function deleteMemberLevel(id: number) {
return mutate('/members/grade/del', { id }) return mutate('/api/admin/members/grade/del', { id })
} }
export function getMemberLevelSystem() { 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) { 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) { ...@@ -11,14 +11,12 @@ function isCleared(previous: MemberLevelFilters, next: MemberLevelFilters) {
} }
export function useMemberLevelList() { export function useMemberLevelList() {
return usePagedList< return usePagedList<MemberLevelRow, MemberLevelFilters, ReturnType<typeof buildMemberLevelQuery>>(
MemberLevelRow, {
MemberLevelFilters, initialFilters: {},
ReturnType<typeof buildMemberLevelQuery> buildQuery: buildMemberLevelQuery,
>({ request: getMemberLevelList,
initialFilters: {}, shouldReloadOnFilterChange: isCleared,
buildQuery: buildMemberLevelQuery, },
request: getMemberLevelList, )
shouldReloadOnFilterChange: isCleared,
})
} }
...@@ -7,12 +7,16 @@ async function post(path: string, data: Record<string, unknown>, notify = false) ...@@ -7,12 +7,16 @@ async function post(path: string, data: Record<string, unknown>, notify = false)
return response.data return response.data
} }
export const getMemberList = (query: MemberQuery) => post('/members/members/getList', query) export const getMemberList = (query: MemberQuery) =>
export const getMemberInfo = (id: string | number) => post('/members/members/getInfoById', { id }) post('/api/admin/members/members/getList', query)
export const addMember = (data: Record<string, unknown>) => post('/members/members/add', data, true) 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>) => export const editMember = (data: Record<string, unknown>) =>
post('/members/members/edit', data, true) post('/api/admin/members/members/edit', data, true)
export const deleteMember = (id: string | number) => post('/members/members/del', { id }, true) export const deleteMember = (id: string | number) =>
post('/api/admin/members/members/del', { id }, true)
export type MemberRecordQuery = { export type MemberRecordQuery = {
id: string | number id: string | number
...@@ -20,8 +24,9 @@ export type MemberRecordQuery = { ...@@ -20,8 +24,9 @@ export type MemberRecordQuery = {
page_size: number 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) => export const getIntegralRecords = (query: MemberRecordQuery) =>
post('/members/members/getIntegralRecordList', query) post('/api/admin/members/members/getIntegralRecordList', query)
export const getCouponRecords = (query: MemberRecordQuery) => 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) => { ...@@ -7,8 +7,8 @@ const saveData = async (path, params) => {
return response.data return response.data
} }
export const getList = (params) => getData('/system/version/getList', params) export const getList = (params) => getData('/api/admin/system/version/getList', params)
export const addVersion = (params) => saveData('/system/version/add', params) export const addVersion = (params) => saveData('/api/admin/system/version/add', params)
export const infoVersion = (params) => getData('/system/version/getInfoById', params) export const infoVersion = (params) => getData('/api/admin/system/version/getInfoById', params)
export const editVersion = (params) => saveData('/system/version/edit', params) export const editVersion = (params) => saveData('/api/admin/system/version/edit', params)
export const delVersion = (params) => saveData('/system/version/del', params) export const delVersion = (params) => saveData('/api/admin/system/version/del', params)
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
Drawer, Drawer,
Form, Form,
Modal, Modal,
Row,
Upload, Upload,
Spin, Spin,
} from 'antd' } from 'antd'
...@@ -132,7 +133,7 @@ const AppVersion = () => { ...@@ -132,7 +133,7 @@ const AppVersion = () => {
// 删除 // 删除
const handleDel = async () => { const handleDel = async () => {
const bool = await delVersion({ id: delid }) const bool = await delVersion({ id: delid })
bool && refresh() if (bool) refresh()
setDelModal(false) setDelModal(false)
} }
......
import axios, { notifySuccess } from '@/utils/axios' import axios, { notifySuccess } from '@/utils/axios'
import endpoints from './endpoints.js'
const getData = async (path, params) => (await axios.post(path, params)).data const getData = async (path, params) => (await axios.post(path, params)).data
const mutate = async (path, params, notify = false) => { const mutate = async (path, params, notify = false) => {
...@@ -8,9 +7,9 @@ const mutate = async (path, params, notify = false) => { ...@@ -8,9 +7,9 @@ const mutate = async (path, params, notify = false) => {
return true return true
} }
export const getAppList = (params) => getData(endpoints.getAppList, params) export const getAppList = (params) => getData('/api/admin/system/app/getList', params)
export const addApp = (params) => mutate(endpoints.addApp, params, true) export const addApp = (params) => mutate('/api/admin/system/app/add', params, true)
export const getAppInfo = (params) => getData(endpoints.getAppInfo, params) export const getAppInfo = (params) => getData('/api/admin/system/app/getInfoById', params)
export const editApp = (params) => getData(endpoints.editApp, params) export const editApp = (params) => getData('/api/admin/system/app/edit', params)
export const changeStatus = (params) => mutate(endpoints.changeStatus, params) export const changeStatus = (params) => mutate('/api/admin/system/app/changeStatus', params)
export const delApp = (params) => mutate(endpoints.delApp, params, true) 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 ...@@ -31,7 +31,7 @@ export type CouponListRequest = (params: CouponQuery) => Promise<CouponListResul
export async function getCouponList(params: CouponQuery): Promise<CouponListResult> { export async function getCouponList(params: CouponQuery): Promise<CouponListResult> {
const response = (await axios.post( const response = (await axios.post(
'/api/system/coupon/getList', '/api/admin/system/coupon/getList',
params, params,
)) as unknown as CouponListEnvelope )) as unknown as CouponListEnvelope
const { list = [], total = 0 } = response.data ?? {} const { list = [], total = 0 } = response.data ?? {}
...@@ -40,23 +40,23 @@ export async function getCouponList(params: CouponQuery): Promise<CouponListResu ...@@ -40,23 +40,23 @@ export async function getCouponList(params: CouponQuery): Promise<CouponListResu
} }
export function addCoupon(data: object) { 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) { 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) { 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) { 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) { 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) { async function legacyPost(path: string, data: object) {
...@@ -65,12 +65,12 @@ async function legacyPost(path: string, data: object) { ...@@ -65,12 +65,12 @@ async function legacyPost(path: string, data: object) {
} }
export async function getCouponConfig() { 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 } return result.payload as { coupon_switch?: string }
} }
export async function setCouponConfig(data: { coupon_switch: '0' | '1' }) { 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) message.success(result.message)
return true return true
} }
...@@ -27,7 +27,7 @@ export type FeedbackListRequest = (params: FeedbackQuery) => Promise<FeedbackLis ...@@ -27,7 +27,7 @@ export type FeedbackListRequest = (params: FeedbackQuery) => Promise<FeedbackLis
// 保留旧 axios 客户端和业务 envelope,await 后才做模块内归一化。 // 保留旧 axios 客户端和业务 envelope,await 后才做模块内归一化。
export async function getFeedbackList(params: FeedbackQuery): Promise<FeedbackListResult> { export async function getFeedbackList(params: FeedbackQuery): Promise<FeedbackListResult> {
const response = (await axios.post( const response = (await axios.post(
'/api/system/proposal/getList', '/api/admin/system/proposal/getList',
params, params,
)) as unknown as FeedbackListEnvelope )) as unknown as FeedbackListEnvelope
const { list = [], total = 0 } = response.data ?? {} const { list = [], total = 0 } = response.data ?? {}
......
...@@ -2,35 +2,35 @@ import axios from '@/utils/axios' ...@@ -2,35 +2,35 @@ import axios from '@/utils/axios'
import { message } from 'antd' import { message } from 'antd'
// 获取列表 // 获取列表
export const getList = async (obj) => { 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 return res.data
} }
// 添加帮助 // 添加帮助
export const addHelp = async (obj) => { 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) message.success(res.message)
return true return true
} }
// 获取帮助详情 // 获取帮助详情
export const HelpInfo = async (obj) => { 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 return res.data
} }
// 修改 // 修改
export const editHelp = async (obj) => { 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) message.success(res.message)
return res.data return res.data
} }
// 修改 // 修改
export const editPreview = async (obj) => { 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 return res.data
} }
// 删除 // 删除
export const delHelp = async (obj) => { 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) message.success(res.message)
return true return true
} }
...@@ -7,12 +7,12 @@ export interface PersonalizedSettings { ...@@ -7,12 +7,12 @@ export interface PersonalizedSettings {
} }
export async function getPersonalizedSettings(): Promise<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 ?? {} return response.data ?? {}
} }
export async function savePersonalizedSettings(values: PersonalizedSettings) { 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) notifySuccess(response.message)
return true return true
} }
...@@ -30,23 +30,23 @@ interface RechargeResult { ...@@ -30,23 +30,23 @@ interface RechargeResult {
} }
export async function getPointSettings(): Promise<PointSettings> { 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 ?? {} return response.data ?? {}
} }
export async function setPointSwitch(integralSwitch: '0' | '1') { 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 return true
} }
export async function savePointSettings(values: PointSettingsFormValues) { 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) notifySuccess(response.message)
return true return true
} }
export async function rechargePoints(values: RechargeFormValues) { 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 data = response.data as RechargeResult
const success = data?.suc ?? {} const success = data?.suc ?? {}
const failure = data?.err ?? {} const failure = data?.err ?? {}
......
...@@ -7,12 +7,13 @@ const mutate = async (path, params, notify = false) => { ...@@ -7,12 +7,13 @@ const mutate = async (path, params, notify = false) => {
return true return true
} }
export const getList = (params) => getData('/system/sensitive/words/getList', params) export const getList = (params) => getData('/api/admin/system/sensitive/words/getList', params)
export const addWords = (params) => mutate('/system/sensitive/words/add', params, true) export const addWords = (params) => mutate('/api/admin/system/sensitive/words/add', params, true)
export const editWords = (params) => mutate('/system/sensitive/words/edit', params, true) export const editWords = (params) => mutate('/api/admin/system/sensitive/words/edit', params, true)
export const delWords = (params) => mutate('/system/sensitive/words/del', params) export const delWords = (params) => mutate('/api/admin/system/sensitive/words/del', params)
export const infoWords = (params) => getData('/system/sensitive/words/getInfoById', params) export const infoWords = (params) =>
getData('/api/admin/system/sensitive/words/getInfoById', params)
export const importWords = async (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 } return { msg: response.message, err: response.data?.err }
} }
...@@ -165,9 +165,7 @@ export default function SensitiveListView() { ...@@ -165,9 +165,7 @@ export default function SensitiveListView() {
<Button <Button
type='primary' type='primary'
ghost ghost
onClick={() => onClick={() => window.open('/api/admin/敏感词导入失败.xlsx')}
window.open(`${import.meta.env.VITE_API_URL}/敏感词导入失败.xlsx`)
}
> >
下载导入失败的数据 下载导入失败的数据
</Button> </Button>
...@@ -184,7 +182,7 @@ export default function SensitiveListView() { ...@@ -184,7 +182,7 @@ export default function SensitiveListView() {
<Button <Button
type='primary' type='primary'
ghost ghost
onClick={() => window.open(`${import.meta.env.VITE_API_URL}/敏感词导入示例.xlsx`)} onClick={() => window.open('/api/admin/敏感词导入示例.xlsx')}
> >
下载模板 下载模板
</Button> </Button>
......
...@@ -11,20 +11,20 @@ async function request( ...@@ -11,20 +11,20 @@ async function request(
} }
export const getTeacherList = (payload: Record<string, unknown>) => 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>) => 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>) => 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>) => 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>) => 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>) => 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>) => 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>) => 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>) => 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() { ...@@ -19,23 +19,18 @@ export function useTeacherList() {
const { fetchPage, page, pageSize } = list const { fetchPage, page, pageSize } = list
const changePage = useCallback( const changePage = useCallback(
(nextPage: number, filters: Record<string, unknown>) => (nextPage: number, filters: Record<string, unknown>) => fetchPage(nextPage, pageSize, filters),
fetchPage(nextPage, pageSize, filters),
[fetchPage, pageSize], [fetchPage, pageSize],
) )
const changePageSize = useCallback( const changePageSize = useCallback(
(nextPageSize: number, filters: Record<string, unknown>) => (nextPageSize: number, filters: Record<string, unknown>) => fetchPage(1, nextPageSize, filters),
fetchPage(1, nextPageSize, filters),
[fetchPage], [fetchPage],
) )
const init = useCallback( const init = useCallback(
( (filters: Record<string, unknown> = {}, nextPage = page, nextPageSize = pageSize) =>
filters: Record<string, unknown> = {}, fetchPage(nextPage, nextPageSize, filters),
nextPage = page,
nextPageSize = pageSize,
) => fetchPage(nextPage, nextPageSize, filters),
[fetchPage, page, pageSize], [fetchPage, page, pageSize],
) )
......
...@@ -42,8 +42,7 @@ export const useUserStore = create<UserState>()( ...@@ -42,8 +42,7 @@ export const useUserStore = create<UserState>()(
setUserInfo: (userInfo) => set({ userInfo }), setUserInfo: (userInfo) => set({ userInfo }),
setToken: (token) => set({ token }), setToken: (token) => set({ token }),
setMenuListRouter: (menuListRouter) => set({ menuListRouter }), setMenuListRouter: (menuListRouter) => set({ menuListRouter }),
setOperationPermissionsList: (operationPermissionsList) => setOperationPermissionsList: (operationPermissionsList) => set({ operationPermissionsList }),
set({ operationPermissionsList }),
setTreeChapter: (treeChapter) => set({ treeChapter: { ...treeChapter } }), 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, { ...@@ -6,7 +6,7 @@ import axios, {
import md5 from 'js-md5' import md5 from 'js-md5'
import { message, Modal, notification } from 'antd' 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> { export interface ApiResponse<T = unknown> {
code: number code: number
...@@ -76,14 +76,14 @@ function getSignData(data: unknown): Record<string, unknown> { ...@@ -76,14 +76,14 @@ function getSignData(data: unknown): Record<string, unknown> {
return data && typeof data === 'object' ? (data as Record<string, unknown>) : {} return data && typeof data === 'object' ? (data as Record<string, unknown>) : {}
} }
const httpRequest: any = axios.create({ const httpRequest: any = axios.create({ withCredentials: true })
baseURL: (import.meta as ImportMeta & { env: { VITE_API_PREFIX?: string } }).env.VITE_API_PREFIX,
withCredentials: true,
})
httpRequest.interceptors.request.use( httpRequest.interceptors.request.use(
(config: InternalAxiosRequestConfig) => { (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 token = getToken()
const appId = 'TzEU5jPk2tu80266' const appId = 'TzEU5jPk2tu80266'
...@@ -96,7 +96,7 @@ httpRequest.interceptors.request.use( ...@@ -96,7 +96,7 @@ httpRequest.interceptors.request.use(
appSecret, appSecret,
timestamp, timestamp,
token, token,
url: config.url, url: signaturePath,
} }
const signStr = Object.entries(signData) const signStr = Object.entries(signData)
.sort(([keyA], [keyB]) => alphabeticalSort(keyA, keyB)) .sort(([keyA], [keyB]) => alphabeticalSort(keyA, keyB))
......
...@@ -4,7 +4,7 @@ import { message } from 'antd' ...@@ -4,7 +4,7 @@ import { message } from 'antd'
let RotateVerify // 声明 RotateVerify 变量 let RotateVerify // 声明 RotateVerify 变量
;(function (undefined) { ;(function () {
'use strict' 'use strict'
//判断是否为 数组 //判断是否为 数组
......
...@@ -233,8 +233,8 @@ export const clacTopicText = (type, radio, checkbox, judge, fill, textarea) => { ...@@ -233,8 +233,8 @@ export const clacTopicText = (type, radio, checkbox, judge, fill, textarea) => {
} }
} }
export const downloadFile = (res = '') => { export const downloadFile = (url = '') => {
window.open(import.meta.env.VITE_API_URL + '/' + res) window.open(url)
} }
// 延时函数 // 延时函数
export const sleep = (time) => new Promise((resolve) => setTimeout(resolve, time)) export const sleep = (time) => new Promise((resolve) => setTimeout(resolve, time))
...@@ -8,7 +8,9 @@ import { saveAs } from 'file-saver' ...@@ -8,7 +8,9 @@ import { saveAs } from 'file-saver'
async function imgToBase64(src) { async function imgToBase64(src) {
// 方式1:fetch // 方式1:fetch
try { try {
const res = await fetch(src) // OSS 刚更新 CORS 规则时,浏览器可能复用规则生效前的无 CORS 缓存响应。
// 导出只在用户主动触发时执行,强制重新校验可确保读取当前响应头。
const res = await fetch(src, { cache: 'reload' })
if (res.ok) { if (res.ok) {
const blob = await res.blob() const blob = await res.blob()
return await blobToDataUri(blob) return await blobToDataUri(blob)
......
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } from 'vite' import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import mkcert from 'vite-plugin-mkcert' import mkcert from 'vite-plugin-mkcert'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig(({ mode }) => { export default defineConfig(() => {
const env = loadEnv(mode, process.cwd(), '')
const adminApiTarget = env.VITE_ADMIN_API_TARGET || 'http://127.0.0.1:7419'
return { return {
base: '/admin/', base: '/admin/',
plugins: [react(), mkcert()], plugins: [react(), mkcert()],
...@@ -16,10 +13,10 @@ export default defineConfig(({ mode }) => { ...@@ -16,10 +13,10 @@ export default defineConfig(({ mode }) => {
open: '/admin/', open: '/admin/',
host: 'dev.ezijing.com', host: 'dev.ezijing.com',
port: 5174, port: 5174,
strictPort: true, strictPort: true,
proxy: { proxy: {
'/api/admin': { '/api/admin': {
target: adminApiTarget, target: 'http://127.0.0.1:7419',
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
rewrite: path => path.replace(/^\/api\/admin/, ''), rewrite: path => path.replace(/^\/api\/admin/, ''),
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
"dependencies": { "dependencies": {
"@ant-design/icons": "^6.3.2", "@ant-design/icons": "^6.3.2",
"@tanstack/react-query": "^5.76.2", "@tanstack/react-query": "^5.76.2",
"ali-oss": "^6.20.0",
"antd": "^6.5.1", "antd": "^6.5.1",
"axios": "^1.8.4", "axios": "^1.8.4",
"js-md5": "^0.8.3", "js-md5": "^0.8.3",
......
...@@ -10,13 +10,13 @@ export function hashMemberPassword(password: string) { ...@@ -10,13 +10,13 @@ export function hashMemberPassword(password: string) {
export async function login(input: LoginInput) { export async function login(input: LoginInput) {
return unwrapApiData( 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) { export async function sendLoginCode(phone: string, uuid: string) {
return unwrapApiData( 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, phone,
types: 'login', types: 'login',
uuid, uuid,
...@@ -26,6 +26,6 @@ export async function sendLoginCode(phone: string, uuid: string) { ...@@ -26,6 +26,6 @@ export async function sendLoginCode(phone: string, uuid: string) {
export async function logout() { export async function logout() {
return unwrapApiData( 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 { ...@@ -142,19 +142,19 @@ function toComment(value: unknown): BookComment {
export async function getBookDetail(bookId: string): Promise<BookDetail> { export async function getBookDetail(bookId: string): Promise<BookDetail> {
return toDetail( 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[]> { export async function getBookChapters(bookId: string): Promise<BookChapter[]> {
return list(await post<unknown>('/v1/book/Information/getBookChapter', { book_id: bookId })).map( return list(
toChapter, await post<unknown>('/api/app/v1/book/Information/getBookChapter', { book_id: bookId }),
) ).map(toChapter)
} }
export async function getBookComments(bookId: string): Promise<BookComment[]> { export async function getBookComments(bookId: string): Promise<BookComment[]> {
return list( return list(
await post<unknown>('/v1/book/Information/getBookCommentList', { await post<unknown>('/api/app/v1/book/Information/getBookCommentList', {
book_id: bookId, book_id: bookId,
page: 1, page: 1,
page_size: 3, page_size: 3,
...@@ -164,7 +164,7 @@ export async function getBookComments(bookId: string): Promise<BookComment[]> { ...@@ -164,7 +164,7 @@ export async function getBookComments(bookId: string): Promise<BookComment[]> {
export async function setBookCollection(bookId: string, selected: boolean): Promise<boolean> { export async function setBookCollection(bookId: string, selected: boolean): Promise<boolean> {
const data = record( const data = record(
await post<unknown>('/v1/book/Information/collection', { await post<unknown>('/api/app/v1/book/Information/collection', {
book_id: bookId, book_id: bookId,
is_select: selected ? '1' : '0', is_select: selected ? '1' : '0',
}), }),
...@@ -173,13 +173,13 @@ export async function setBookCollection(bookId: string, selected: boolean): Prom ...@@ -173,13 +173,13 @@ export async function setBookCollection(bookId: string, selected: boolean): Prom
} }
export async function addBookToShelf(bookId: string): Promise<boolean> { 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 return number(data.is_success) === 1
} }
export async function getBookStudyReport(bookId: string): Promise<StudyReport> { export async function getBookStudyReport(bookId: string): Promise<StudyReport> {
const data = record( 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 { return {
progress: string(data.progress), progress: string(data.progress),
......
...@@ -124,24 +124,24 @@ export function toLibraryApiParams(state: LibrarySearchState): LibraryApiParams ...@@ -124,24 +124,24 @@ export function toLibraryApiParams(state: LibrarySearchState): LibraryApiParams
} }
export async function getLibraryCategories(): Promise<LibraryFilter[]> { 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')) return listFromPayload(data).map((item) => toFilter(item, 'category_id'))
} }
export async function getLibraryLabels(): Promise<LibraryFilter[]> { 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')) return listFromPayload(data).map((item) => toFilter(item, 'label_id'))
} }
export async function getLibraryBooks(params: LibraryApiParams): Promise<LibraryListResult> { export async function getLibraryBooks(params: LibraryApiParams): Promise<LibraryListResult> {
const data = const data =
'keyword' in params 'keyword' in params
? await post<unknown>('/v1/book/Information/searchBook', { ? await post<unknown>('/api/app/v1/book/Information/searchBook', {
keyword: params.keyword, keyword: params.keyword,
page: params.page, page: params.page,
page_size: params.page_size, 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, category_id: params.category_id,
label_id: params.label_id, label_id: params.label_id,
...(params.is_free === undefined ? {} : { is_free: params.is_free }), ...(params.is_free === undefined ? {} : { is_free: params.is_free }),
...@@ -161,7 +161,7 @@ export async function getLibraryBooks(params: LibraryApiParams): Promise<Library ...@@ -161,7 +161,7 @@ export async function getLibraryBooks(params: LibraryApiParams): Promise<Library
} }
export async function setBookCollection(bookId: string, isSelected: boolean): Promise<boolean> { 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, book_id: bookId,
is_select: isSelected ? '1' : '0', is_select: isSelected ? '1' : '0',
}) })
......
...@@ -85,7 +85,7 @@ function toMyBookListResult(data: unknown): MyBookListResult { ...@@ -85,7 +85,7 @@ function toMyBookListResult(data: unknown): MyBookListResult {
} }
export async function getMyStatistics(): Promise<MyStatistics> { 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)) const data = asRecord(unwrapApiData(response))
return { return {
...@@ -110,15 +110,15 @@ async function getMyBookList(path: string, params: MyListParams): Promise<MyBook ...@@ -110,15 +110,15 @@ async function getMyBookList(path: string, params: MyListParams): Promise<MyBook
} }
export function getMyCourses(params: MyListParams) { 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) { 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) { 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) const data = unwrapApiData<ApiRecord>(response)
return data.is_success === 1 || data.is_success === '1' || data.is_success === true return data.is_success === 1 || data.is_success === '1' || data.is_success === true
} }
...@@ -13,7 +13,7 @@ import { ...@@ -13,7 +13,7 @@ import {
Typography, Typography,
} from 'antd' } from 'antd'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useMemo, useState } from 'react' import { useState } from 'react'
import { Link } from 'react-router' import { Link } from 'react-router'
import defaultBookCover from '@/assets/default-book-cover.jpg' import defaultBookCover from '@/assets/default-book-cover.jpg'
import { deleteBookshelfItem } from '../../api' import { deleteBookshelfItem } from '../../api'
...@@ -33,11 +33,9 @@ export function MyBookshelfView() { ...@@ -33,11 +33,9 @@ export function MyBookshelfView() {
const bookshelfQuery = useQuery(bookshelfOptions(state)) const bookshelfQuery = useQuery(bookshelfOptions(state))
const [selectedCartIds, setSelectedCartIds] = useState<Set<string>>(new Set()) const [selectedCartIds, setSelectedCartIds] = useState<Set<string>>(new Set())
const items = bookshelfQuery.data?.items ?? [] const items = bookshelfQuery.data?.items ?? []
const selectableCartIds = useMemo( const selectableCartIds = items
() => .filter((book) => book.status === 1 && book.cartId)
items.filter((book) => book.status === 1 && book.cartId).map((book) => String(book.cartId)), .map((book) => String(book.cartId))
[items],
)
const deleteMutation = useMutation({ const deleteMutation = useMutation({
mutationFn: async (cartIds: string[]) => { mutationFn: async (cartIds: string[]) => {
......
...@@ -50,7 +50,7 @@ function toCoupon(value: unknown): CouponItem { ...@@ -50,7 +50,7 @@ function toCoupon(value: unknown): CouponItem {
} }
export async function getCoupons(params: CouponListParams): Promise<CouponListResult> { 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: params.page,
page_size: params.pageSize, page_size: params.pageSize,
type: params.status, type: params.status,
......
...@@ -28,7 +28,7 @@ function text(value: unknown) { ...@@ -28,7 +28,7 @@ function text(value: unknown) {
} }
export async function getDiscussedBooks(params: MyListParams): Promise<DiscussedBookList> { 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: params.page,
page_size: params.pageSize, page_size: params.pageSize,
}) })
......
...@@ -6,7 +6,6 @@ import { ...@@ -6,7 +6,6 @@ import {
Image, Image,
List, List,
Pagination, Pagination,
Space,
Statistic, Statistic,
Skeleton, Skeleton,
Typography, Typography,
......
...@@ -34,7 +34,7 @@ function number(value: unknown) { ...@@ -34,7 +34,7 @@ function number(value: unknown) {
} }
export async function getFavorites(params: MyListParams): Promise<FavoriteList> { 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: params.page,
page_size: params.pageSize, page_size: params.pageSize,
}) })
...@@ -58,7 +58,7 @@ export async function getFavorites(params: MyListParams): Promise<FavoriteList> ...@@ -58,7 +58,7 @@ export async function getFavorites(params: MyListParams): Promise<FavoriteList>
} }
export async function removeFavorite(bookId: string) { 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, book_id: bookId,
is_select: '0', is_select: '0',
}) })
......
...@@ -4,9 +4,12 @@ import { http } from '@/utils/http' ...@@ -4,9 +4,12 @@ import { http } from '@/utils/http'
export async function submitFeedback(proposal: string) { export async function submitFeedback(proposal: string) {
const data = unwrapApiData( const data = unwrapApiData(
await http.post<ApiEnvelope<Record<string, unknown>>>('/v1/members/Information/proposal', { await http.post<ApiEnvelope<Record<string, unknown>>>(
proposal, '/api/app/v1/members/Information/proposal',
}), {
proposal,
},
),
) )
if (data.is_success !== 1 && data.is_success !== '1' && data.is_success !== true) { if (data.is_success !== 1 && data.is_success !== '1' && data.is_success !== true) {
throw new Error('反馈提交失败') throw new Error('反馈提交失败')
......
...@@ -30,7 +30,10 @@ function text(value: unknown) { ...@@ -30,7 +30,10 @@ function text(value: unknown) {
export async function getHelpArticles(): Promise<HelpArticleSummary[]> { export async function getHelpArticles(): Promise<HelpArticleSummary[]> {
const data = unwrapApiData( 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) return list(data)
.map((value) => { .map((value) => {
...@@ -42,7 +45,9 @@ export async function getHelpArticles(): Promise<HelpArticleSummary[]> { ...@@ -42,7 +45,9 @@ export async function getHelpArticles(): Promise<HelpArticleSummary[]> {
export async function getHelpArticle(id: string): Promise<HelpArticle> { export async function getHelpArticle(id: string): Promise<HelpArticle> {
const item = record( 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 { return {
id: text(item.id) || id, id: text(item.id) || id,
......
...@@ -34,7 +34,7 @@ function list(value: unknown) { ...@@ -34,7 +34,7 @@ function list(value: unknown) {
} }
export async function getReadRecords(params: MyListParams): Promise<ReadRecordGroup[]> { 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: params.page,
page_size: params.pageSize, page_size: params.pageSize,
}) })
......
import { useQuery } from '@tanstack/react-query' 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 { Link } from 'react-router'
import { useMyListQueryState } from '../../query-state' import { useMyListQueryState } from '../../query-state'
import { readRecordsOptions } from '../query' import { readRecordsOptions } from '../query'
......
...@@ -62,7 +62,7 @@ function toMessage(value: unknown): MessageItem { ...@@ -62,7 +62,7 @@ function toMessage(value: unknown): MessageItem {
} }
export async function getMessages(params: MessageListParams): Promise<MessageListResult> { 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: params.page,
page_size: params.pageSize, page_size: params.pageSize,
}) })
...@@ -75,12 +75,12 @@ export async function getMessages(params: MessageListParams): Promise<MessageLis ...@@ -75,12 +75,12 @@ export async function getMessages(params: MessageListParams): Promise<MessageLis
} }
export async function getUnreadMessageCount() { 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) return number(record(unwrapApiData<unknown>(response)).num)
} }
export async function markMessageRead(id: string) { 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)) const data = record(unwrapApiData<unknown>(response))
return data.is_success === 1 || data.is_success === '1' || data.is_success === true return data.is_success === 1 || data.is_success === '1' || data.is_success === true
} }
...@@ -36,7 +36,7 @@ function number(value: unknown) { ...@@ -36,7 +36,7 @@ function number(value: unknown) {
} }
export async function getNotedBooks(params: MyListParams): Promise<NotedBookList> { 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: params.page,
page_size: params.pageSize, page_size: params.pageSize,
}) })
......
...@@ -115,7 +115,7 @@ export async function getOrders(params: OrderListParams): Promise<OrderListResul ...@@ -115,7 +115,7 @@ export async function getOrders(params: OrderListParams): Promise<OrderListResul
status: params.status, status: params.status,
} }
if (params.keyword.trim()) request.keys = params.keyword.trim() 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 data = unwrapApiData<unknown>(response)
const payload = record(data) const payload = record(data)
return { return {
...@@ -125,7 +125,7 @@ export async function getOrders(params: OrderListParams): Promise<OrderListResul ...@@ -125,7 +125,7 @@ export async function getOrders(params: OrderListParams): Promise<OrderListResul
} }
export async function getOrderDetail(orderNumber: string): Promise<OrderDetail> { 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, ordersnum: orderNumber,
}) })
const data = record(unwrapApiData<unknown>(response)) const data = record(unwrapApiData<unknown>(response))
......
...@@ -33,7 +33,7 @@ function isSuccessful(data: ApiRecord) { ...@@ -33,7 +33,7 @@ function isSuccessful(data: ApiRecord) {
export async function getMemberProfile(): Promise<MemberProfile> { export async function getMemberProfile(): Promise<MemberProfile> {
const data = record( const data = record(
unwrapApiData( unwrapApiData(
await http.post<ApiEnvelope<unknown>>('/v1/members/Information/getMyStatistics', {}), await http.post<ApiEnvelope<unknown>>('/api/app/v1/members/Information/getMyStatistics', {}),
), ),
) )
return { return {
...@@ -53,7 +53,7 @@ export interface UpdateMemberProfileInput { ...@@ -53,7 +53,7 @@ export interface UpdateMemberProfileInput {
export async function updateMemberProfile(input: UpdateMemberProfileInput) { export async function updateMemberProfile(input: UpdateMemberProfileInput) {
const data = record( const data = record(
unwrapApiData( unwrapApiData(
await http.post<ApiEnvelope<unknown>>('/v1/members/Information/editInformation', { await http.post<ApiEnvelope<unknown>>('/api/app/v1/members/Information/editInformation', {
name: input.name, name: input.name,
sex: input.sex, sex: input.sex,
...(input.headImg ? { head_img: input.headImg } : {}), ...(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 { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { UploadOutlined } from '@ant-design/icons'
import { import {
Alert, Alert,
Avatar, Avatar,
...@@ -10,10 +11,14 @@ import { ...@@ -10,10 +11,14 @@ import {
Radio, Radio,
Skeleton, Skeleton,
Typography, Typography,
Upload,
message, message,
} from 'antd' } from 'antd'
import type { UploadProps } from 'antd'
import { useSessionStore } from '@/modules/auth/session'
import { myKeys } from '../../query' import { myKeys } from '../../query'
import { updateMemberProfile, type UpdateMemberProfileInput } from '../api' import { updateMemberProfile, type UpdateMemberProfileInput } from '../api'
import { uploadAvatar } from '../avatar-upload'
import { memberProfileOptions, profileKeys } from '../query' import { memberProfileOptions, profileKeys } from '../query'
interface ProfileFormValues { interface ProfileFormValues {
...@@ -30,12 +35,22 @@ const sexOptions = [ ...@@ -30,12 +35,22 @@ const sexOptions = [
export function ProfileView() { export function ProfileView() {
const [form] = Form.useForm<ProfileFormValues>() 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 queryClient = useQueryClient()
const profileQuery = useQuery(memberProfileOptions()) const profileQuery = useQuery(memberProfileOptions())
const saveMutation = useMutation({ const saveMutation = useMutation({
mutationFn: (values: ProfileFormValues) => mutationFn: (values: ProfileFormValues) =>
updateMemberProfile(values satisfies UpdateMemberProfileInput), updateMemberProfile(values satisfies UpdateMemberProfileInput),
onSuccess: async () => { onSuccess: async (_, values) => {
if (session) {
setSession({
...session,
name: values.name,
head_img: values.headImg,
})
}
await Promise.all([ await Promise.all([
queryClient.invalidateQueries({ queryKey: profileKeys.all }), queryClient.invalidateQueries({ queryKey: profileKeys.all }),
queryClient.invalidateQueries({ queryKey: myKeys.statistics() }), queryClient.invalidateQueries({ queryKey: myKeys.statistics() }),
...@@ -66,6 +81,36 @@ export function ProfileView() { ...@@ -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 ( return (
<main> <main>
<Typography.Title level={1}>个人资料</Typography.Title> <Typography.Title level={1}>个人资料</Typography.Title>
...@@ -85,6 +130,9 @@ export function ProfileView() { ...@@ -85,6 +130,9 @@ export function ProfileView() {
requiredMark='optional' requiredMark='optional'
onFinish={(values) => saveMutation.mutate(values)} onFinish={(values) => saveMutation.mutate(values)}
> >
<Form.Item name='headImg' hidden>
<Input />
</Form.Item>
<Form.Item <Form.Item
noStyle noStyle
shouldUpdate={(previous, current) => shouldUpdate={(previous, current) =>
...@@ -92,21 +140,18 @@ export function ProfileView() { ...@@ -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')}> <Avatar size={72} src={form.getFieldValue('headImg')}>
{form.getFieldValue('name')?.slice(0, 1)} {form.getFieldValue('name')?.slice(0, 1)}
</Avatar> </Avatar>
<Upload {...avatarUploadProps}>
<Button icon={<UploadOutlined />} loading={uploading} style={{ marginLeft: 12 }}>
上传头像
</Button>
</Upload>
</Form.Item> </Form.Item>
)} )}
</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 <Form.Item
label='昵称' label='昵称'
name='name' name='name'
......
...@@ -27,7 +27,7 @@ async function successfulPost( ...@@ -27,7 +27,7 @@ async function successfulPost(
export async function changeMemberPassword(password: string, repassword: string) { export async function changeMemberPassword(password: string, repassword: string) {
await successfulPost( await successfulPost(
'/v1/members/Information/changePasswd', '/api/app/v1/members/Information/changePasswd',
{ {
password: hashMemberPassword(password), password: hashMemberPassword(password),
repassword: hashMemberPassword(repassword), repassword: hashMemberPassword(repassword),
...@@ -38,7 +38,7 @@ export async function changeMemberPassword(password: string, repassword: string) ...@@ -38,7 +38,7 @@ export async function changeMemberPassword(password: string, repassword: string)
export async function sendChangePhoneCode(phone: string) { export async function sendChangePhoneCode(phone: string) {
await successfulPost( await successfulPost(
'/v1/members/login/sendCode', '/api/app/v1/members/login/sendCode',
{ {
phone, phone,
types: 'login', types: 'login',
...@@ -49,9 +49,13 @@ export async function sendChangePhoneCode(phone: string) { ...@@ -49,9 +49,13 @@ export async function sendChangePhoneCode(phone: string) {
} }
export async function changeMemberPhone(phone: string, code: 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() { 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) { ...@@ -52,7 +52,7 @@ function source(data: unknown) {
} }
export async function getPointRecords(params: WalletListParams): Promise<WalletListResult> { 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: params.page,
page_size: params.pageSize, page_size: params.pageSize,
}) })
...@@ -65,7 +65,7 @@ export async function getPointRecords(params: WalletListParams): Promise<WalletL ...@@ -65,7 +65,7 @@ export async function getPointRecords(params: WalletListParams): Promise<WalletL
} }
export async function getCoinRecords(params: WalletListParams): Promise<WalletListResult> { 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: params.page,
page_size: params.pageSize, page_size: params.pageSize,
}) })
......
...@@ -28,7 +28,7 @@ function text(value: unknown) { ...@@ -28,7 +28,7 @@ function text(value: unknown) {
} }
export async function getWrongQuestionBooks(params: MyListParams): Promise<WrongQuestionBookList> { 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: params.page,
page_size: params.pageSize, page_size: params.pageSize,
}) })
......
...@@ -6,7 +6,6 @@ import { ...@@ -6,7 +6,6 @@ import {
Image, Image,
List, List,
Pagination, Pagination,
Space,
Statistic, Statistic,
Skeleton, Skeleton,
Typography, Typography,
......
...@@ -157,15 +157,20 @@ async function post<T>(url: string, data: Record<string, unknown>): Promise<T> { ...@@ -157,15 +157,20 @@ async function post<T>(url: string, data: Record<string, unknown>): Promise<T> {
} }
export async function getReaderChapters(bookId: string): Promise<ReaderChapter[]> { 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) return list(data).map(toChapter)
} }
export async function getSectionContent(bookId: string, chapterId: string): Promise<ReaderSection> { 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>>(
book_id: bookId, '/api/app/v1/book/Information/getSectionContent',
chapter_id: chapterId, {
}) book_id: bookId,
chapter_id: chapterId,
},
)
const body = response.data const body = response.data
const payload = record(body.data) const payload = record(body.data)
if (body.code === 3001 || Number(payload.code) === 3001) throw new ReaderChapterLockedError() if (body.code === 3001 || Number(payload.code) === 3001) throw new ReaderChapterLockedError()
...@@ -183,7 +188,7 @@ export async function getSectionContent(bookId: string, chapterId: string): Prom ...@@ -183,7 +188,7 @@ export async function getSectionContent(bookId: string, chapterId: string): Prom
} }
export function addProgress(bookId: string, chapterId: string, progress: number) { 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, book_id: bookId,
chapter_id: chapterId, chapter_id: chapterId,
progress, progress,
...@@ -191,7 +196,7 @@ export function addProgress(bookId: string, chapterId: string, progress: number) ...@@ -191,7 +196,7 @@ export function addProgress(bookId: string, chapterId: string, progress: number)
} }
export function addReadRecord(bookId: string, chapterId: string, positioning: 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, book_id: bookId,
chapter_id: chapterId, chapter_id: chapterId,
positioning, positioning,
...@@ -199,14 +204,14 @@ export function addReadRecord(bookId: string, chapterId: string, positioning: nu ...@@ -199,14 +204,14 @@ export function addReadRecord(bookId: string, chapterId: string, positioning: nu
} }
export function addReadTime(bookId: string, readTypes: 'open' | 'close') { 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, book_id: bookId,
read_types: readTypes, read_types: readTypes,
}) })
} }
export function getChapterNotesList(bookId: string, chapterId: string) { 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: 1,
page_size: 100, page_size: 100,
book_id: bookId, book_id: bookId,
...@@ -226,7 +231,7 @@ export function addNotes( ...@@ -226,7 +231,7 @@ export function addNotes(
color?: string color?: string
}, },
) { ) {
return post<unknown>('/v1/book/Information/addNotes', { return post<unknown>('/api/app/v1/book/Information/addNotes', {
book_id: bookId, book_id: bookId,
chapter_id: chapterId, chapter_id: chapterId,
types: String(values.type ?? 3), types: String(values.type ?? 3),
...@@ -243,7 +248,7 @@ export function editNotes( ...@@ -243,7 +248,7 @@ export function editNotes(
noteId: string, noteId: string,
values: { content: string; noteContent: string; positioning: string; isOpen: boolean }, 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, notes_id: noteId,
book_id: bookId, book_id: bookId,
content: values.content, content: values.content,
...@@ -255,11 +260,14 @@ export function editNotes( ...@@ -255,11 +260,14 @@ export function editNotes(
} }
export function delNotes(bookId: string, noteId: string) { 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) { 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: 1,
page_size: 100, page_size: 100,
book_id: bookId, book_id: bookId,
...@@ -268,7 +276,7 @@ export function getChapterCommentList(bookId: string, chapterId: string) { ...@@ -268,7 +276,7 @@ export function getChapterCommentList(bookId: string, chapterId: string) {
} }
export function addComment(bookId: string, chapterId: string, content: 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, book_id: bookId,
chapter_id: chapterId, chapter_id: chapterId,
comment_id: '0', comment_id: '0',
...@@ -279,7 +287,7 @@ export function addComment(bookId: string, chapterId: string, content: string) { ...@@ -279,7 +287,7 @@ export function addComment(bookId: string, chapterId: string, content: string) {
} }
export function clickPraise(bookId: string, commentId: string, type: 0 | 1) { 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, book_id: bookId,
comment_id: commentId, comment_id: commentId,
type, type,
...@@ -287,7 +295,7 @@ export function clickPraise(bookId: string, commentId: string, type: 0 | 1) { ...@@ -287,7 +295,7 @@ export function clickPraise(bookId: string, commentId: string, type: 0 | 1) {
} }
export function delComment(bookId: string, commentId: string) { 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, book_id: bookId,
comment_id: commentId, comment_id: commentId,
}) })
......
...@@ -2,7 +2,7 @@ import { useMutation, useQuery } from '@tanstack/react-query' ...@@ -2,7 +2,7 @@ import { useMutation, useQuery } from '@tanstack/react-query'
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons' import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'
import { Alert, Button, Skeleton, Tooltip, Typography } from 'antd' import { Alert, Button, Skeleton, Tooltip, Typography } from 'antd'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router' import { useNavigate, useParams } from 'react-router'
import { import {
addProgress, addProgress,
addReadRecord, addReadRecord,
...@@ -31,7 +31,6 @@ function findChapter(chapters: ReaderChapter[], chapterId: string): ReaderChapte ...@@ -31,7 +31,6 @@ function findChapter(chapters: ReaderChapter[], chapterId: string): ReaderChapte
export function ReaderView() { export function ReaderView() {
const { bookId = '', chapterId = '' } = useParams() const { bookId = '', chapterId = '' } = useParams()
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation()
const { const {
state: readerState, state: readerState,
setAnchor, setAnchor,
......
...@@ -75,18 +75,18 @@ async function post<T>(url: string, data: Record<string, unknown>): Promise<T> { ...@@ -75,18 +75,18 @@ async function post<T>(url: string, data: Record<string, unknown>): Promise<T> {
} }
export async function getSearchHistory(): Promise<string[]> { 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()) .map((item) => string(record(item).name).trim())
.filter(Boolean) .filter(Boolean)
} }
export async function clearSearchHistory(): Promise<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 return number(data.is_success) === 1
} }
export async function getSearchBooks(state: SearchState): Promise<SearchBooksResult> { 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: state.page,
page_size: state.pageSize, page_size: state.pageSize,
keyword: state.q, keyword: state.q,
...@@ -101,7 +101,7 @@ export async function getSearchBooks(state: SearchState): Promise<SearchBooksRes ...@@ -101,7 +101,7 @@ export async function getSearchBooks(state: SearchState): Promise<SearchBooksRes
export async function setSearchBookCollection(bookId: string, selected: boolean): Promise<boolean> { export async function setSearchBookCollection(bookId: string, selected: boolean): Promise<boolean> {
const data = record( const data = record(
await post<unknown>('/v1/book/Information/collection', { await post<unknown>('/api/app/v1/book/Information/collection', {
book_id: bookId, book_id: bookId,
is_select: selected ? '1' : '0', 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 ...@@ -4,7 +4,6 @@ import { applyRequestAuthentication, getDevelopmentRequestAuth } from '@/api/htt
import { expireMemberSession } from '@/modules/auth/session' import { expireMemberSession } from '@/modules/auth/session'
export const http = axios.create({ export const http = axios.create({
baseURL: '/api/web',
withCredentials: true, withCredentials: true,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
......
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'
import { loadEnv } from 'vite'
import { defineConfig } from 'vitest/config' import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import mkcert from 'vite-plugin-mkcert' import mkcert from 'vite-plugin-mkcert'
export default defineConfig(({ mode }) => { 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 { return {
base: '/', base: '/',
plugins: [react(), ...(mode === 'test' ? [] : [mkcert()])], plugins: [react(), ...(mode === 'test' ? [] : [mkcert()])],
...@@ -24,13 +19,13 @@ export default defineConfig(({ mode }) => { ...@@ -24,13 +19,13 @@ export default defineConfig(({ mode }) => {
secure: false, secure: false,
ws: true, ws: true,
}, },
'/api/web': { '/api/app': {
target: appApiTarget, target: 'http://127.0.0.1:7421',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/web/, ''), rewrite: (path) => path.replace(/^\/api\/app/, ''),
}, },
'/api/admin': { '/api/admin': {
target: adminApiTarget, target: 'http://127.0.0.1:7419',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/admin/, ''), 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: ...@@ -155,6 +155,9 @@ importers:
'@tanstack/react-query': '@tanstack/react-query':
specifier: ^5.76.2 specifier: ^5.76.2
version: 5.101.2(react@19.2.7) version: 5.101.2(react@19.2.7)
ali-oss:
specifier: ^6.20.0
version: 6.23.0
antd: antd:
specifier: ^6.5.1 specifier: ^6.5.1
version: 6.5.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) version: 6.5.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
......
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论