提交 89d5a908 authored 作者: 王鹏飞's avatar 王鹏飞

feat: 优化上传逻辑,添加成功提示信息,调整 API 调用方式

上级 a46dc4da
......@@ -71,13 +71,20 @@ describe('OSS upload progress', () => {
client.generateObjectUrl.mockReturnValue('https://example.com/video.mp4')
const onProgress = vi.fn()
await expect(
uploadFile({ name: 'video.mp4' }, { onProgress, parallel: 3, retries: 3 }),
).resolves.toBe('https://example.com/video.mp4')
await expect(uploadFile({ name: 'video.mp4' }, { onProgress })).resolves.toBe(
'https://example.com/video.mp4',
)
expect(client.multipartUpload).toHaveBeenCalledTimes(2)
expect(client.multipartUpload.mock.calls[0][2]).toMatchObject({ parallel: 3 })
expect(client.multipartUpload.mock.calls[1][2]).toMatchObject({ checkpoint, parallel: 3 })
expect(client.multipartUpload.mock.calls[0][2]).toMatchObject({
partSize: 4 * 1024 * 1024,
parallel: 3,
})
expect(client.multipartUpload.mock.calls[1][2]).toMatchObject({
checkpoint,
partSize: 4 * 1024 * 1024,
parallel: 3,
})
expect(onProgress.mock.calls.map(([percent]) => percent)).toEqual([50, 100])
})
})
......@@ -122,13 +122,14 @@ const ExpandModal = (props, ref) => {
SlateTransforms.insertNodes(editor, nextNode, { at: existingPath })
}
const data = await addExpandRead({
await addExpandRead({
book_id: bookId,
chapter_id: chapterId,
position: nowRandom,
type: flex,
...other,
})
message.success('扩展阅读更新成功')
setNowRandom(null)
setExpandVisible(false)
setLoadLoading(false)
......@@ -136,13 +137,14 @@ const ExpandModal = (props, ref) => {
}
const random = Math.random().toString(10).substring(2, 10)
const data = await addExpandRead({
await addExpandRead({
book_id: bookId,
chapter_id: chapterId,
position: random,
type: flex,
...other,
})
message.success('扩展阅读添加成功')
editor.restoreSelection()
if (parseInt(values.flex) === 1) {
......
import { useState, useEffect, forwardRef } from 'react'
import { Divider, Input, Space, Button, Form, ColorPicker, Tabs, Select, Row, Col } from 'antd'
import {
Divider,
Input,
Space,
Button,
Form,
ColorPicker,
Tabs,
Select,
Row,
Col,
message,
} from 'antd'
import '../../index.less'
import { addGallery } from '../../shared/request'
import { SlateTransforms } from '@wangeditor/editor'
......@@ -230,6 +242,7 @@ const GalleryModal = (props, ref) => {
content: JSON.stringify(gallery),
position: nowRandom,
})
message.success('画廊更新成功')
if (flex === 2) {
const aPath = findNodeWithParent(
......@@ -260,6 +273,7 @@ const GalleryModal = (props, ref) => {
content: JSON.stringify(gallery),
position: random,
})
message.success('画廊添加成功')
editor.restoreSelection()
if (parseInt(flex) !== 2) {
removeSelectedEmptyParagraph(editor, SlateTransforms)
......
import { DomEditor, SlateTransforms } from '@wangeditor/editor'
import { h } from 'snabbdom'
import { message } from 'antd'
import { openEditorDialog } from '../../core/editor-dialog'
import iconClose from '@/assets/images/icon_chapter_close.png'
import { delChapterTopic } from '../../shared/request'
......@@ -127,6 +128,7 @@ const renderPractice = (elem, children, editor) => {
book_id: bookId,
chapter_id: chapterId,
})
message.success('题目删除成功')
try {
const path = findNodeWithParent(
......
......@@ -142,6 +142,7 @@ const TooltipModal = (props, ref) => {
chapter_id: chapterId,
position: nowRandom,
})
message.success('气泡删除成功')
if (data) {
editor.restoreSelection()
const nodeEntries = SlateEditor.nodes(editor, {
......
......@@ -50,8 +50,6 @@ const VideoModal = ({ open, onClose, payload = {}, runtime }) => {
setUploadProgress(0)
try {
const url = await uploadFile(file, {
parallel: 3,
retries: 3,
onProgress: (percent) => {
setUploadProgress(percent)
onProgress?.({ percent })
......
......@@ -12,14 +12,18 @@ function DebounceSelect({ fetchOptions, debounceTimeout = 800, ...props }) {
const fetchId = fetchRef.current
setOptions([])
setFetching(true)
fetchOptions(value).then((newOptions) => {
if (fetchId !== fetchRef.current) {
// for fetch callback order
return
}
setOptions(newOptions)
setFetching(false)
})
fetchOptions(value)
.then((newOptions) => {
if (fetchId !== fetchRef.current) {
// for fetch callback order
return
}
setOptions(newOptions)
})
.catch(() => undefined)
.finally(() => {
if (fetchId === fetchRef.current) setFetching(false)
})
}
return debounce(loadOptions, debounceTimeout)
}, [fetchOptions, debounceTimeout])
......@@ -29,6 +33,12 @@ function DebounceSelect({ fetchOptions, debounceTimeout = 800, ...props }) {
showSearch={{ filterOption: false, onSearch: debounceFetcher }}
notFoundContent={fetching ? <Spin size='small' /> : null}
{...props}
onOpenChange={(open) => {
if (open) {
debounceFetcher('')
debounceFetcher.flush()
}
}}
options={options}
/>
)
......
......@@ -3,8 +3,11 @@ import axios, { notifySuccess } from '@/utils/axios'
export const getUserInfo = async (params) =>
(await axios.post('/api/admin/user/user/getUserInfo', params)).data
export const changePass = async (params) =>
(await axios.post('/api/admin/user/user/changePass', params)).data
export const changePass = async (params) => {
const response = await axios.post('/api/admin/user/user/changePass', params)
notifySuccess(response.message)
return response.data
}
export const changeInfo = async (params) => {
const response = await axios.post('/api/admin/user/user/editUserInfo', params)
......
......@@ -71,13 +71,13 @@ export const getAllList = async (params) => {
export const chapterAdd = (params) => getData('/api/admin/book/teacher/chapter/add', params)
export const getInfoById = (params) =>
getData('/api/admin/book/teacher/chapter/getInfoById', params)
export const chapterEdit = (params) => mutate('/api/admin/book/teacher/chapter/edit', params, true)
export const chapterEdit = (params) => mutate('/api/admin/book/teacher/chapter/edit', params)
export const chapterDel = (params) => mutate('/api/admin/book/teacher/chapter/del', params, true)
export const dragOrder = (params) =>
mutate('/api/admin/book/teacher/chapter/dragOrder', params, true)
export const getInfoByChapterId = (params) =>
getData('/api/admin/book/teacher/section/getInfoByChapterId', params)
export const sectionAdd = (params) => mutate('/api/admin/book/teacher/section/add', params, true)
export const sectionAdd = (params) => mutate('/api/admin/book/teacher/section/add', params)
export const sectionEdit = (params) => mutate('/api/admin/book/teacher/section/edit', params)
export const getQuestionList = (params) =>
getData('/api/admin/book/chapter/question/getQuestionList', params)
......
import { useState, useEffect, useRef } from 'react'
import { Form, Button, Space, Input } from 'antd'
import { Form, Button, Space, Input, message } from 'antd'
import { useUserStore } from '@/stores/userStore'
import { chapterEdit, chapterAdd, sectionAdd } from '../api'
......@@ -57,6 +57,7 @@ const EditChapterTitle = (props) => {
if (parentId) {
await sectionAdd({ book_id: bookId, chapter_id: data.id, content: '', word_count: 0 })
}
message.success(parentId ? '章节创建成功' : '章节更新成功')
setEditKey(false)
setEditValue(null)
await getChapterTreeList()
......
import { useState, useEffect, useRef, useCallback } from 'react'
import { Button, Row, Col, Descriptions, Tree, Dropdown, Modal, Spin, App, Tooltip } from 'antd'
import {
Button,
Row,
Col,
Descriptions,
Tree,
Dropdown,
Modal,
Spin,
App,
Tooltip,
Empty,
} from 'antd'
import {
EllipsisOutlined,
DiffOutlined,
......@@ -134,6 +146,18 @@ const Examine = () => {
}
if (data.length === 0) {
chapterRequestRef.current += 1
setGData([])
setExpandedKeys([])
setCheckedKeys([])
setChapterId(0)
setNowTitle('')
setHtml('')
setContentId(false)
setLoadedChapterId(0)
setChapterLoadFailed(false)
contentMd5Ref.current = md5('')
setTreeChapter({ saveBookId: id, saveChapterId: null })
setLoading(false)
return
}
......@@ -538,6 +562,15 @@ const Examine = () => {
<div className='book-content-editor' style={{ flex: 1, minWidth: 0 }}>
<div className='editor-right'>
<Spin spinning={loading}>
{!loading && gData.length === 0 && (
<Empty className='chapter-empty' description='暂无章节'>
{isAdminOrCreator && (
<Button type='primary' onClick={addChapterParent}>
新增第一个章节
</Button>
)}
</Empty>
)}
{editorReady && (
<WangEditorCustomer
key={chapterId}
......
......@@ -92,6 +92,13 @@
gap: 12px;
color: #6f7886;
}
.chapter-empty {
display: flex;
height: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
}
}
.draggable-tree {
flex: 1;
......
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import type { ComponentProps, ReactNode } from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import Examine from './index'
const mocks = vi.hoisted(() => ({
chapterDel: vi.fn(),
confirm: vi.fn(),
getAllList: vi.fn(),
getInfoByChapterId: vi.fn(),
sectionEdit: vi.fn(),
......@@ -16,7 +18,7 @@ vi.mock('antd', () => {
const PassThrough = ({ children }: { children?: ReactNode }) => <>{children}</>
return {
App: { useApp: () => ({ modal: { confirm: vi.fn() } }) },
App: { useApp: () => ({ modal: { confirm: mocks.confirm } }) },
Button: ({ children, onClick, ...props }: ComponentProps<'button'>) => (
<button onClick={onClick} {...props}>
{children}
......@@ -30,7 +32,34 @@ vi.mock('antd', () => {
))}
</>
),
Dropdown: PassThrough,
Dropdown: ({
children,
menu,
}: {
children?: ReactNode
menu?: {
items?: Array<{ key: string; label: string }>
onClick: (event: { key: string; domEvent: { stopPropagation: () => void } }) => void
}
}) => (
<>
{children}
{menu?.items?.map((item: { key: string; label: string }) => (
<button
key={item.key}
onClick={() => menu.onClick({ key: item.key, domEvent: { stopPropagation: vi.fn() } })}
>
{item.label}
</button>
))}
</>
),
Empty: ({ children, description }: { children?: ReactNode; description?: ReactNode }) => (
<div>
{description}
{children}
</div>
),
Modal: ({ children, open }: { children?: ReactNode; open?: boolean }) =>
open ? <>{children}</> : null,
Row: PassThrough,
......@@ -39,17 +68,22 @@ vi.mock('antd', () => {
Tree: ({
disabled,
onSelect,
titleRender,
treeData = [],
}: {
disabled?: boolean
onSelect: (keys: number[], info: { node: { key: number; title: string } }) => void
titleRender?: (node: { key: number; title: string }) => ReactNode
treeData?: Array<{ key: number; title: string }>
}) => (
<div>
{treeData.map((node) => (
<button key={node.key} disabled={disabled} onClick={() => onSelect([node.key], { node })}>
{node.title}
</button>
<div key={node.key}>
<button disabled={disabled} onClick={() => onSelect([node.key], { node })}>
{node.title}
</button>
{titleRender?.(node)}
</div>
))}
</div>
),
......@@ -115,7 +149,7 @@ vi.mock('@/utils/common', () => ({
}))
vi.mock('./api', () => ({
chapterDel: vi.fn(),
chapterDel: mocks.chapterDel,
dragOrder: vi.fn(),
getAllList: mocks.getAllList,
getInfoByChapterId: mocks.getInfoByChapterId,
......@@ -135,8 +169,12 @@ const deferred = <T,>() => {
}
describe('chapter editor lifecycle', () => {
afterEach(cleanup)
beforeEach(() => {
vi.clearAllMocks()
mocks.chapterDel.mockResolvedValue(true)
mocks.confirm.mockImplementation(({ onOk }) => onOk())
mocks.getAllList.mockResolvedValue({
code: 200,
data: [
......@@ -188,4 +226,28 @@ describe('chapter editor lifecycle', () => {
expect(secondEditor).toHaveAttribute('data-chapter-id', '2')
expect(secondEditor).toHaveAttribute('data-html', '<p>章节二内容</p>')
})
it('clears the last chapter and shows an empty state after deletion', async () => {
mocks.getAllList
.mockResolvedValueOnce({ code: 200, data: [{ id: 1, name: '章节一' }] })
.mockResolvedValueOnce({ code: 200, data: [] })
mocks.getInfoByChapterId.mockResolvedValue({
id: 101,
content: '<p>章节一内容</p>',
selected_editor_ids: [1],
})
render(<Examine />)
expect(await screen.findByTestId('chapter-editor')).toBeInTheDocument()
fireEvent.click(await screen.findByRole('button', { name: '删除' }))
expect(await screen.findByText('暂无章节')).toBeInTheDocument()
expect(screen.queryByTestId('chapter-editor')).not.toBeInTheDocument()
expect(screen.getByRole('button', { name: '新增第一个章节' })).toBeInTheDocument()
expect(mocks.setTreeChapter).toHaveBeenLastCalledWith({
saveBookId: 10,
saveChapterId: null,
})
})
})
......@@ -10,6 +10,6 @@ const mutate = async (path, params, notify = false) => {
export const getAppList = (params) => getData('/api/admin/system/app/getList', params)
export const addApp = (params) => mutate('/api/admin/system/app/add', params, true)
export const getAppInfo = (params) => getData('/api/admin/system/app/getInfoById', params)
export const editApp = (params) => getData('/api/admin/system/app/edit', params)
export const changeStatus = (params) => mutate('/api/admin/system/app/changeStatus', params)
export const editApp = (params) => mutate('/api/admin/system/app/edit', params, true)
export const changeStatus = (params) => mutate('/api/admin/system/app/changeStatus', params, true)
export const delApp = (params) => mutate('/api/admin/system/app/del', params, true)
import { message } from 'antd'
import axios from '@/utils/axios'
import axios, { notifySuccess } from '@/utils/axios'
import type { CouponQuery } from './query'
......@@ -47,8 +47,10 @@ export function editCoupon(data: object) {
return axios.post('/api/admin/system/coupon/edit', data)
}
export function deleteCoupon(data: object) {
return axios.post('/api/admin/system/coupon/del', data)
export async function deleteCoupon(data: object) {
const response = await axios.post('/api/admin/system/coupon/del', data)
notifySuccess(response.message)
return response
}
export function getCouponDetail(data: object) {
......
......@@ -145,12 +145,10 @@ const HelpAddEdit = () => {
// };
try {
// message.success(`视频已上传:${uploadProgress}% , 玩命加载中~~`);
message.success(`视频已上传:99% , 正玩命加载中~~`)
const url = await uploadFile(file, { filenamePrefix: 'help' })
// insertFn(result.res.requestUrls[0], '题库视频');
insertFn(url, '题库视频')
message.success('视频上传成功')
setLoading(false)
} catch (error) {
console.error('上传视频失败:', error)
......
......@@ -35,7 +35,10 @@ export async function getPointSettings(): Promise<PointSettings> {
}
export async function setPointSwitch(integralSwitch: '0' | '1') {
await axios.post('/api/admin/system/config/integralSet', { integral_switch: integralSwitch })
const response = await axios.post('/api/admin/system/config/integralSet', {
integral_switch: integralSwitch,
})
notifySuccess(response.message)
return true
}
......
......@@ -143,7 +143,8 @@ httpRequest.interceptors.response.use(
console.error(`${status}: ${data?.message}`)
}
} else {
console.log(error)
notification.error({ message: '网络请求失败,请稍后重试' })
console.error(error)
}
return Promise.reject(error.response || error)
},
......
......@@ -58,14 +58,19 @@ export async function uploadFile(file, options = {}) {
const objectName = buildObjectName(file, options)
if (options.onProgress) {
const retries = options.retries ?? 0
const {
partSize = 4 * 1024 * 1024,
parallel = 3,
retries = 3,
} = options
let checkpoint
for (let attempt = 0; attempt <= retries; attempt += 1) {
try {
const result = await oss.multipartUpload(objectName, file, {
checkpoint,
parallel: options.parallel,
partSize,
parallel,
progress: (percentage, nextCheckpoint) => {
checkpoint = nextCheckpoint || checkpoint
options.onProgress(Math.round(percentage * 100))
......
......@@ -15,13 +15,13 @@ export default defineConfig(() => {
port: 5174,
strictPort: true,
proxy: {
// '/api/admin': {
// target: 'http://127.0.0.1:7419',
// changeOrigin: true,
// secure: false,
// rewrite: (path) => path.replace(/^\/api\/admin/, ''),
// },
'/api': 'https://zijingebook.ezijing.com',
'/api/admin': {
target: 'http://127.0.0.1:7419',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/api\/admin/, ''),
},
// '/api': 'https://zijingebook.ezijing.com',
},
},
build: {
......
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论