Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
C
center-book
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
EzijingWeb
center-book
Commits
8d6dd989
提交
8d6dd989
authored
8月 26, 2026
作者:
王鹏飞
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
chore: update
上级
d8033dd5
显示空白字符变更
内嵌
并排
正在显示
102 个修改的文件
包含
619 行增加
和
1822 行删除
+619
-1822
admin-access.ts
apps/learning/src/api/admin-access.ts
+10
-0
errors.test.ts
apps/learning/src/api/errors.test.ts
+0
-11
errors.ts
apps/learning/src/api/errors.ts
+0
-58
http-auth.test.ts
apps/learning/src/api/http-auth.test.ts
+0
-24
http-auth.ts
apps/learning/src/api/http-auth.ts
+0
-43
types.ts
apps/learning/src/api/types.ts
+0
-6
unwrap.test.ts
apps/learning/src/api/unwrap.test.ts
+0
-41
unwrap.ts
apps/learning/src/api/unwrap.ts
+0
-19
MobileDownloadModal.css
apps/learning/src/components/MobileDownloadModal.css
+0
-0
MobileDownloadModal.tsx
apps/learning/src/components/MobileDownloadModal.tsx
+7
-5
LearningLayout.tsx
apps/learning/src/layouts/LearningLayout.tsx
+44
-10
AccountMenu.tsx
apps/learning/src/layouts/components/AccountMenu.tsx
+7
-7
WorkbenchEntry.tsx
apps/learning/src/layouts/components/WorkbenchEntry.tsx
+0
-22
main.tsx
apps/learning/src/main.tsx
+1
-12
admin-access.test.ts
apps/learning/src/modules/admin-access/admin-access.test.ts
+0
-10
admin-access.ts
apps/learning/src/modules/admin-access/admin-access.ts
+0
-18
api.ts
apps/learning/src/modules/admin-access/api.ts
+0
-32
query.ts
apps/learning/src/modules/admin-access/query.ts
+0
-19
api.ts
apps/learning/src/modules/auth/api.ts
+12
-17
types.ts
apps/learning/src/modules/auth/types.ts
+0
-17
LoginView.tsx
apps/learning/src/modules/auth/views/LoginView.tsx
+7
-8
api.ts
apps/learning/src/modules/book/api.ts
+1
-8
BookComments.tsx
apps/learning/src/modules/book/components/BookComments.tsx
+3
-1
BookDetailView.tsx
apps/learning/src/modules/book/views/BookDetailView.tsx
+62
-56
mobile-download.ts
apps/learning/src/modules/download/mobile-download.ts
+0
-13
api.ts
apps/learning/src/modules/help/api.ts
+4
-12
PageHero.tsx
apps/learning/src/modules/help/components/PageHero.tsx
+0
-0
Redbud.tsx
apps/learning/src/modules/help/components/Redbud.tsx
+15
-2
query.ts
apps/learning/src/modules/help/query.ts
+0
-0
routes.tsx
apps/learning/src/modules/help/routes.tsx
+0
-0
AboutView.tsx
apps/learning/src/modules/help/views/AboutView.tsx
+12
-3
ContactView.tsx
apps/learning/src/modules/help/views/ContactView.tsx
+7
-1
HelpDetailView.tsx
apps/learning/src/modules/help/views/HelpDetailView.tsx
+1
-18
HelpListView.tsx
apps/learning/src/modules/help/views/HelpListView.tsx
+1
-11
info-pages.css
apps/learning/src/modules/help/views/info-pages.css
+0
-0
api.ts
apps/learning/src/modules/library/api.ts
+1
-8
LibraryBookCard.tsx
...arning/src/modules/library/components/LibraryBookCard.tsx
+33
-11
LibraryView.css
apps/learning/src/modules/library/views/LibraryView.css
+1
-1
LibraryView.tsx
apps/learning/src/modules/library/views/LibraryView.tsx
+2
-29
api.ts
apps/learning/src/modules/my/api.ts
+4
-8
MyBooksView.tsx
apps/learning/src/modules/my/books/views/MyBooksView.tsx
+1
-10
MyBookshelfView.tsx
...arning/src/modules/my/bookshelf/views/MyBookshelfView.tsx
+0
-18
api.ts
apps/learning/src/modules/my/coupons/api.ts
+10
-44
CouponList.tsx
...learning/src/modules/my/coupons/components/CouponList.tsx
+5
-5
MyCouponsView.tsx
apps/learning/src/modules/my/coupons/views/MyCouponsView.tsx
+2
-11
api.ts
apps/learning/src/modules/my/discussions/api.ts
+5
-5
MyDiscussionsView.tsx
...ng/src/modules/my/discussions/views/MyDiscussionsView.tsx
+1
-20
api.ts
apps/learning/src/modules/my/favorites/api.ts
+6
-7
MyFavoritesView.tsx
...arning/src/modules/my/favorites/views/MyFavoritesView.tsx
+0
-18
api.ts
apps/learning/src/modules/my/feedback/api.ts
+3
-10
FeedbackView.tsx
apps/learning/src/modules/my/feedback/views/FeedbackView.tsx
+1
-10
api.ts
apps/learning/src/modules/my/history/api.ts
+5
-5
MyHistoryView.tsx
apps/learning/src/modules/my/history/views/MyHistoryView.tsx
+1
-10
api.ts
apps/learning/src/modules/my/messages/api.ts
+4
-8
MyMessagesView.tsx
...learning/src/modules/my/messages/views/MyMessagesView.tsx
+1
-13
api.ts
apps/learning/src/modules/my/notes/api.ts
+5
-5
MyNotesView.tsx
apps/learning/src/modules/my/notes/views/MyNotesView.tsx
+1
-21
api.ts
apps/learning/src/modules/my/orders/api.ts
+6
-7
OrderList.tsx
apps/learning/src/modules/my/orders/components/OrderList.tsx
+3
-8
MyOrdersView.tsx
apps/learning/src/modules/my/orders/views/MyOrdersView.tsx
+1
-20
OrderDetailView.tsx
.../learning/src/modules/my/orders/views/OrderDetailView.tsx
+36
-16
MyOverviewView.tsx
...learning/src/modules/my/overview/views/MyOverviewView.tsx
+3
-14
api.ts
apps/learning/src/modules/my/profile/api.ts
+3
-11
avatar-upload.ts
apps/learning/src/modules/my/profile/avatar-upload.ts
+2
-6
ProfileView.tsx
apps/learning/src/modules/my/profile/views/ProfileView.tsx
+3
-24
routes.tsx
apps/learning/src/modules/my/routes.tsx
+1
-1
api.ts
apps/learning/src/modules/my/security/api.ts
+6
-8
ChangePasswordView.tsx
...ning/src/modules/my/security/views/ChangePasswordView.tsx
+2
-11
ChangePhoneView.tsx
...earning/src/modules/my/security/views/ChangePhoneView.tsx
+1
-19
SecurityView.tsx
apps/learning/src/modules/my/security/views/SecurityView.tsx
+2
-21
api.ts
apps/learning/src/modules/my/wallet/api.ts
+3
-6
MyWalletView.tsx
apps/learning/src/modules/my/wallet/views/MyWalletView.tsx
+1
-10
api.ts
apps/learning/src/modules/my/wrong-questions/api.ts
+5
-5
MyWrongQuestionsView.tsx
...modules/my/wrong-questions/views/MyWrongQuestionsView.tsx
+1
-20
api.ts
apps/learning/src/modules/purchase/api.ts
+55
-0
routes.tsx
apps/learning/src/modules/purchase/routes.tsx
+14
-0
AlipayReturnView.tsx
.../learning/src/modules/purchase/views/AlipayReturnView.tsx
+69
-0
api.ts
apps/learning/src/modules/reader/api.ts
+11
-21
ReaderContent.tsx
.../learning/src/modules/reader/components/ReaderContent.tsx
+0
-15
ReaderInteractionLayer.tsx
.../src/modules/reader/components/ReaderInteractionLayer.tsx
+4
-11
ReaderSidePanel.tsx
...earning/src/modules/reader/components/ReaderSidePanel.tsx
+0
-31
routes.tsx
apps/learning/src/modules/reader/routes.tsx
+1
-1
ReaderView.tsx
apps/learning/src/modules/reader/views/ReaderView.tsx
+2
-26
api.ts
apps/learning/src/modules/search/api.ts
+0
-110
SearchBookCard.tsx
...learning/src/modules/search/components/SearchBookCard.tsx
+0
-71
SearchHistory.tsx
.../learning/src/modules/search/components/SearchHistory.tsx
+0
-36
query-state.ts
apps/learning/src/modules/search/query-state.ts
+0
-42
query.ts
apps/learning/src/modules/search/query.ts
+0
-21
routes.tsx
apps/learning/src/modules/search/routes.tsx
+0
-9
SearchView.css
apps/learning/src/modules/search/views/SearchView.css
+0
-137
SearchView.tsx
apps/learning/src/modules/search/views/SearchView.tsx
+0
-171
RequireSession.tsx
apps/learning/src/router/RequireSession.tsx
+1
-1
RootLayout.tsx
apps/learning/src/router/RootLayout.tsx
+0
-5
paths.ts
apps/learning/src/router/paths.ts
+0
-7
routes.test.tsx
apps/learning/src/router/routes.test.tsx
+1
-1
routes.tsx
apps/learning/src/router/routes.tsx
+7
-5
session.ts
apps/learning/src/stores/session.ts
+10
-19
device.ts
apps/learning/src/utils/device.ts
+0
-0
http.test.ts
apps/learning/src/utils/http.test.ts
+24
-45
http.ts
apps/learning/src/utils/http.ts
+40
-39
password.ts
apps/learning/src/utils/password.ts
+5
-0
vite.config.ts
apps/learning/vite.config.ts
+10
-10
没有找到文件。
apps/learning/src/api/admin-access.ts
0 → 100644
浏览文件 @
8d6dd989
import
{
post
}
from
'@/utils/http'
export
interface
AdminAccess
{
has_access
:
boolean
token
?:
string
}
export
async
function
requestAdminAccess
(
issue
=
false
)
{
return
post
<
AdminAccess
>
(
'/api/admin/user/memberSsoLogin'
,
{
issue
:
issue
?
1
:
0
})
}
apps/learning/src/api/errors.test.ts
deleted
100644 → 0
浏览文件 @
d8033dd5
import
{
ApiError
,
shouldRetryQuery
}
from
'./errors'
import
{
describe
,
expect
,
it
}
from
'vitest'
describe
(
'shouldRetryQuery'
,
()
=>
{
it
(
'retries only safe transient transport failures'
,
()
=>
{
expect
(
shouldRetryQuery
(
0
,
new
ApiError
({
kind
:
'transport'
,
transient
:
true
}))).
toBe
(
true
)
expect
(
shouldRetryQuery
(
2
,
new
ApiError
({
kind
:
'transport'
,
transient
:
true
}))).
toBe
(
false
)
expect
(
shouldRetryQuery
(
0
,
new
ApiError
({
kind
:
'auth'
}))).
toBe
(
false
)
expect
(
shouldRetryQuery
(
0
,
new
ApiError
({
kind
:
'business'
}))).
toBe
(
false
)
})
})
apps/learning/src/api/errors.ts
deleted
100644 → 0
浏览文件 @
d8033dd5
import
axios
from
'axios'
export
type
ApiErrorKind
=
'auth'
|
'business'
|
'cancelled'
|
'transport'
interface
ApiErrorOptions
{
kind
:
ApiErrorKind
message
?:
string
status
?:
number
transient
?:
boolean
cause
?:
unknown
}
export
class
ApiError
extends
Error
{
readonly
kind
:
ApiErrorKind
readonly
status
?:
number
readonly
transient
:
boolean
override
readonly
cause
?:
unknown
constructor
({
kind
,
message
,
status
,
transient
=
false
,
cause
}:
ApiErrorOptions
)
{
super
(
message
??
'请求失败'
)
this
.
name
=
'ApiError'
this
.
kind
=
kind
this
.
status
=
status
this
.
transient
=
transient
this
.
cause
=
cause
}
}
export
function
normalizeApiError
(
error
:
unknown
):
ApiError
{
if
(
error
instanceof
ApiError
)
return
error
if
(
axios
.
isCancel
(
error
)
||
(
axios
.
isAxiosError
(
error
)
&&
error
.
code
===
'ERR_CANCELED'
))
{
return
new
ApiError
({
kind
:
'cancelled'
,
message
:
'请求已取消'
,
cause
:
error
})
}
if
(
axios
.
isAxiosError
(
error
))
{
const
status
=
error
.
response
?.
status
if
(
status
===
401
||
status
===
403
)
{
return
new
ApiError
({
kind
:
'auth'
,
message
:
'会话已失效'
,
status
,
cause
:
error
})
}
return
new
ApiError
({
kind
:
'transport'
,
message
:
error
.
message
||
'网络请求失败'
,
status
,
transient
:
!
status
||
status
>=
500
,
cause
:
error
,
})
}
return
new
ApiError
({
kind
:
'transport'
,
message
:
'网络请求失败'
,
transient
:
true
,
cause
:
error
,
})
}
export
function
shouldRetryQuery
(
failureCount
:
number
,
error
:
unknown
)
{
const
normalized
=
normalizeApiError
(
error
)
return
normalized
.
kind
===
'transport'
&&
normalized
.
transient
&&
failureCount
<
2
}
apps/learning/src/api/http-auth.test.ts
deleted
100644 → 0
浏览文件 @
d8033dd5
import
{
describe
,
expect
,
it
}
from
'vitest'
import
{
applyRequestAuthentication
}
from
'./http-auth'
describe
(
'applyRequestAuthentication'
,
()
=>
{
it
(
'uses optional development bearer and signer providers without owning signing logic'
,
async
()
=>
{
const
request
=
await
applyRequestAuthentication
(
{
headers
:
{}
},
{
getBearerToken
:
()
=>
'member-session'
,
signRequest
:
()
=>
({
Sign
:
'delegated-signature'
,
appId
:
'dev-app'
}),
},
)
expect
(
request
.
headers
).
toMatchObject
({
Authorization
:
'member-session'
,
Sign
:
'delegated-signature'
,
appId
:
'dev-app'
,
})
})
it
(
'does not manufacture credentials when no providers are configured'
,
async
()
=>
{
expect
(
await
applyRequestAuthentication
({
headers
:
{}
},
{})).
toEqual
({
headers
:
{}
})
})
})
apps/learning/src/api/http-auth.ts
deleted
100644 → 0
浏览文件 @
d8033dd5
export
type
RequestHeaders
=
Record
<
string
,
string
>
export
interface
RequestAuthenticationInput
{
baseURL
?:
string
url
?:
string
method
?:
string
params
?:
unknown
data
?:
unknown
headers
:
RequestHeaders
}
export
interface
DevelopmentRequestAuthProviders
{
getBearerToken
?:
()
=>
string
|
undefined
|
Promise
<
string
|
undefined
>
signRequest
?:
(
request
:
RequestAuthenticationInput
)
=>
RequestHeaders
|
Promise
<
RequestHeaders
>
}
let
configuredProviders
:
DevelopmentRequestAuthProviders
=
{}
export
function
configureDevelopmentRequestAuth
(
providers
:
DevelopmentRequestAuthProviders
)
{
configuredProviders
=
providers
}
export
function
getDevelopmentRequestAuth
()
{
return
configuredProviders
}
export
async
function
applyRequestAuthentication
<
T
extends
Omit
<
RequestAuthenticationInput
,
'headers'
>
&
{
headers
?:
RequestHeaders
},
>
(
request
:
T
,
providers
:
DevelopmentRequestAuthProviders
,
):
Promise
<
T
&
{
headers
:
RequestHeaders
}
>
{
const
headers
=
{
...(
request
.
headers
??
{})
}
const
token
=
await
providers
.
getBearerToken
?.()
if
(
token
)
headers
.
Authorization
=
token
return
{
...
request
,
headers
:
providers
.
signRequest
?
{
...
headers
,
...(
await
providers
.
signRequest
({
...
request
,
headers
}))
}
:
headers
,
}
}
apps/learning/src/api/types.ts
deleted
100644 → 0
浏览文件 @
d8033dd5
export
interface
ApiEnvelope
<
T
>
{
code
:
number
message
?:
string
msg
?:
string
data
:
T
}
apps/learning/src/api/unwrap.test.ts
deleted
100644 → 0
浏览文件 @
d8033dd5
import
{
AxiosError
,
AxiosHeaders
,
type
AxiosResponse
}
from
'axios'
import
{
describe
,
expect
,
it
}
from
'vitest'
import
{
normalizeApiError
}
from
'./errors'
import
{
unwrapApiData
}
from
'./unwrap'
function
response
<
T
>
(
data
:
T
,
status
=
200
):
AxiosResponse
<
T
>
{
return
{
data
,
status
,
statusText
:
''
,
headers
:
new
AxiosHeaders
(),
config
:
{
headers
:
new
AxiosHeaders
()
},
}
}
describe
(
'unwrapApiData'
,
()
=>
{
it
(
'returns data from successful APP envelopes'
,
()
=>
{
expect
(
unwrapApiData
(
response
({
code
:
200
,
message
:
'ok'
,
data
:
{
id
:
1
}
}))).
toEqual
({
id
:
1
,
})
})
it
(
'normalizes APP business and HTTP auth failures'
,
()
=>
{
expect
(()
=>
unwrapApiData
(
response
({
code
:
422
,
msg
:
'不可购买'
,
data
:
null
}))).
toThrow
(
expect
.
objectContaining
({
kind
:
'business'
,
message
:
'不可购买'
}),
)
expect
(()
=>
unwrapApiData
(
response
({
code
:
200
,
data
:
null
},
401
))).
toThrow
(
expect
.
objectContaining
({
kind
:
'auth'
,
status
:
401
}),
)
})
it
(
'normalizes cancelled and transient transport errors'
,
()
=>
{
expect
(
normalizeApiError
(
new
AxiosError
(
'canceled'
,
'ERR_CANCELED'
))).
toMatchObject
({
kind
:
'cancelled'
,
})
expect
(
normalizeApiError
(
new
AxiosError
(
'network down'
,
'ERR_NETWORK'
))).
toMatchObject
({
kind
:
'transport'
,
transient
:
true
,
})
})
})
apps/learning/src/api/unwrap.ts
deleted
100644 → 0
浏览文件 @
d8033dd5
import
type
{
AxiosResponse
}
from
'axios'
import
{
ApiError
}
from
'./errors'
import
type
{
ApiEnvelope
}
from
'./types'
export
function
unwrapApiData
<
T
>
(
response
:
AxiosResponse
<
ApiEnvelope
<
T
>>
):
T
{
if
(
response
.
status
===
401
||
response
.
status
===
403
)
{
throw
new
ApiError
({
kind
:
'auth'
,
status
:
response
.
status
,
message
:
'会话已失效'
})
}
if
(
response
.
status
<
200
||
response
.
status
>=
300
)
{
throw
new
ApiError
({
kind
:
'transport'
,
status
:
response
.
status
,
message
:
'请求失败'
})
}
if
(
response
.
data
.
code
!==
200
)
{
throw
new
ApiError
({
kind
:
'business'
,
message
:
response
.
data
.
message
??
response
.
data
.
msg
??
'业务请求失败'
,
})
}
return
response
.
data
.
data
}
apps/learning/src/
modules/download/
components/MobileDownloadModal.css
→
apps/learning/src/components/MobileDownloadModal.css
浏览文件 @
8d6dd989
File moved
apps/learning/src/
modules/download/
components/MobileDownloadModal.tsx
→
apps/learning/src/components/MobileDownloadModal.tsx
浏览文件 @
8d6dd989
...
@@ -2,7 +2,6 @@ import { AndroidOutlined, AppleOutlined, CloseCircleFilled } from '@ant-design/i
...
@@ -2,7 +2,6 @@ import { AndroidOutlined, AppleOutlined, CloseCircleFilled } from '@ant-design/i
import
{
Button
,
Image
,
Modal
,
QRCode
,
Typography
}
from
'antd'
import
{
Button
,
Image
,
Modal
,
QRCode
,
Typography
}
from
'antd'
import
{
useState
}
from
'react'
import
{
useState
}
from
'react'
import
appDownloadQr
from
'@/assets/app-download-qr.png'
import
appDownloadQr
from
'@/assets/app-download-qr.png'
import
{
useMobileDownloadStore
}
from
'../mobile-download'
import
'./MobileDownloadModal.css'
import
'./MobileDownloadModal.css'
const
iosAppStoreUrl
=
const
iosAppStoreUrl
=
...
@@ -10,14 +9,17 @@ const iosAppStoreUrl =
...
@@ -10,14 +9,17 @@ const iosAppStoreUrl =
type
Platform
=
'ios'
|
'android'
type
Platform
=
'ios'
|
'android'
export
function
MobileDownloadModal
()
{
interface
MobileDownloadModalProps
{
const
open
=
useMobileDownloadStore
((
state
)
=>
state
.
open
)
open
:
boolean
const
hide
=
useMobileDownloadStore
((
state
)
=>
state
.
hide
)
onClose
:
()
=>
void
}
export
function
MobileDownloadModal
({
open
,
onClose
}:
MobileDownloadModalProps
)
{
const
[
hoveredPlatform
,
setHoveredPlatform
]
=
useState
<
Platform
|
null
>
(
null
)
const
[
hoveredPlatform
,
setHoveredPlatform
]
=
useState
<
Platform
|
null
>
(
null
)
const
close
=
()
=>
{
const
close
=
()
=>
{
setHoveredPlatform
(
null
)
setHoveredPlatform
(
null
)
hid
e
()
onClos
e
()
}
}
const
platformVisual
=
(
platform
:
Platform
)
=>
{
const
platformVisual
=
(
platform
:
Platform
)
=>
{
...
...
apps/learning/src/layouts/LearningLayout.tsx
浏览文件 @
8d6dd989
import
{
BellOutlined
}
from
'@ant-design/icons'
import
{
BellOutlined
}
from
'@ant-design/icons'
import
{
Badge
,
Button
,
Layout
}
from
'antd'
import
{
Badge
,
Button
,
Layout
}
from
'antd'
import
{
Link
,
Outlet
,
useLocation
,
useNavigate
}
from
'react-router'
import
{
Link
,
Outlet
,
useLocation
,
useNavigate
}
from
'react-router'
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
useMutation
,
useQuery
}
from
'@tanstack/react-query'
import
{
useState
}
from
'react'
import
'./LearningLayout.css'
import
'./LearningLayout.css'
import
{
getTopNavigationKey
,
navigationItems
}
from
'./navigation'
import
{
getTopNavigationKey
,
navigationItems
}
from
'./navigation'
import
{
useSessionStore
}
from
'@/
modules/auth
/session'
import
{
useSessionStore
}
from
'@/
stores
/session'
import
logo
from
'@/assets/logo.png'
import
logo
from
'@/assets/logo.png'
import
{
AccountMenu
}
from
'./components/AccountMenu'
import
{
AccountMenu
}
from
'./components/AccountMenu'
import
{
WorkbenchEntry
}
from
'./components/WorkbenchEntry
'
import
{
MobileDownloadModal
}
from
'@/components/MobileDownloadModal
'
import
{
MobileDownloadModal
}
from
'@/modules/download/components/MobileDownloadModal
'
import
{
requestAdminAccess
,
type
AdminAccess
}
from
'@/api/admin-access
'
import
{
useMobileDownloadStore
}
from
'@/modules/download/mobile-download
'
import
{
logout
}
from
'@/modules/auth/api
'
import
{
unreadMessageCountOptions
}
from
'@/modules/my/messages/query'
import
{
unreadMessageCountOptions
}
from
'@/modules/my/messages/query'
export
function
LearningLayout
()
{
export
function
LearningLayout
()
{
...
@@ -17,10 +18,34 @@ export function LearningLayout() {
...
@@ -17,10 +18,34 @@ export function LearningLayout() {
const
navigate
=
useNavigate
()
const
navigate
=
useNavigate
()
const
session
=
useSessionStore
((
state
)
=>
state
.
session
)
const
session
=
useSessionStore
((
state
)
=>
state
.
session
)
const
clearSession
=
useSessionStore
((
state
)
=>
state
.
clearSession
)
const
clearSession
=
useSessionStore
((
state
)
=>
state
.
clearSession
)
const
showMobileDownload
=
useMobileDownloadStore
((
state
)
=>
state
.
show
)
const
[
mobileDownloadOpen
,
setMobileDownloadOpen
]
=
useState
(
false
)
const
adminAccessQuery
=
useQuery
({
queryKey
:
[
'admin-access'
],
queryFn
:
()
=>
requestAdminAccess
(),
enabled
:
Boolean
(
session
),
staleTime
:
5
*
60
*
1000
,
})
const
issueAdminAccess
=
useMutation
({
mutationFn
:
()
=>
requestAdminAccess
(
true
)
})
const
unreadQuery
=
useQuery
({
...
unreadMessageCountOptions
(),
enabled
:
Boolean
(
session
)
})
const
unreadQuery
=
useQuery
({
...
unreadMessageCountOptions
(),
enabled
:
Boolean
(
session
)
})
const
unreadCount
=
unreadQuery
.
data
??
0
const
unreadCount
=
unreadQuery
.
data
??
0
const
logoutMember
=
async
()
=>
{
try
{
await
logout
()
}
catch
{
}
finally
{
clearSession
()
}
}
const
openAdmin
=
(
access
:
AdminAccess
)
=>
{
if
(
!
access
.
token
)
return
localStorage
.
setItem
(
'kiwi.token'
,
access
.
token
)
const
adminUrl
=
new
URL
(
import
.
meta
.
env
.
VITE_ADMIN_URL
)
adminUrl
.
hash
=
new
URLSearchParams
({
sso_token
:
access
.
token
}).
toString
()
window
.
open
(
adminUrl
.
toString
(),
'_blank'
)
}
return
(
return
(
<
Layout
className=
'learning-layout'
>
<
Layout
className=
'learning-layout'
>
<
Layout
.
Header
className=
'learning-layout__header'
>
<
Layout
.
Header
className=
'learning-layout__header'
>
...
@@ -40,7 +65,7 @@ export function LearningLayout() {
...
@@ -40,7 +65,7 @@ export function LearningLayout() {
key=
{
item
.
key
}
key=
{
item
.
key
}
type=
'button'
type=
'button'
className=
'learning-layout__nav-item'
className=
'learning-layout__nav-item'
onClick=
{
showMobileDownload
}
onClick=
{
()
=>
setMobileDownloadOpen
(
true
)
}
>
>
{
item
.
label
}
{
item
.
label
}
</
button
>
</
button
>
...
@@ -54,7 +79,16 @@ export function LearningLayout() {
...
@@ -54,7 +79,16 @@ export function LearningLayout() {
</
Link
>
</
Link
>
),
),
)
}
)
}
<
WorkbenchEntry
/>
{
session
&&
adminAccessQuery
.
data
?.
has_access
?
(
<
button
type=
'button'
className=
'learning-layout__nav-item'
disabled=
{
issueAdminAccess
.
isPending
}
onClick=
{
()
=>
issueAdminAccess
.
mutate
(
undefined
,
{
onSuccess
:
openAdmin
})
}
>
管理后台
</
button
>
)
:
null
}
</
nav
>
</
nav
>
<
div
className=
'learning-layout__session'
aria
-
label=
'用户会话区'
>
<
div
className=
'learning-layout__session'
aria
-
label=
'用户会话区'
>
{
session
?
(
{
session
?
(
...
@@ -68,7 +102,7 @@ export function LearningLayout() {
...
@@ -68,7 +102,7 @@ export function LearningLayout() {
<
BellOutlined
style=
{
{
fontSize
:
18
}
}
/>
<
BellOutlined
style=
{
{
fontSize
:
18
}
}
/>
</
Badge
>
</
Badge
>
</
Link
>
</
Link
>
<
AccountMenu
session=
{
session
}
onLogout=
{
clearSession
}
/>
<
AccountMenu
session=
{
session
}
onLogout=
{
logoutMember
}
/>
</>
</>
)
:
(
)
:
(
<
Button
<
Button
...
@@ -107,7 +141,7 @@ export function LearningLayout() {
...
@@ -107,7 +141,7 @@ export function LearningLayout() {
<
Link
to=
'/contact'
>
联系我们
</
Link
>
<
Link
to=
'/contact'
>
联系我们
</
Link
>
</
nav
>
</
nav
>
</
Layout
.
Footer
>
</
Layout
.
Footer
>
<
MobileDownloadModal
/>
<
MobileDownloadModal
open=
{
mobileDownloadOpen
}
onClose=
{
()
=>
setMobileDownloadOpen
(
false
)
}
/>
</
Layout
>
</
Layout
>
)
)
}
}
apps/learning/src/layouts/components/AccountMenu.tsx
浏览文件 @
8d6dd989
...
@@ -7,12 +7,12 @@ import {
...
@@ -7,12 +7,12 @@ import {
import
{
Avatar
,
Button
,
Divider
,
Popover
,
Typography
}
from
'antd'
import
{
Avatar
,
Button
,
Divider
,
Popover
,
Typography
}
from
'antd'
import
{
useState
}
from
'react'
import
{
useState
}
from
'react'
import
{
useNavigate
}
from
'react-router'
import
{
useNavigate
}
from
'react-router'
import
type
{
MemberSession
}
from
'@/
modules/auth/types
'
import
type
{
MemberSession
}
from
'@/
stores/session
'
import
'./AccountMenu.css'
import
'./AccountMenu.css'
interface
AccountMenuProps
{
interface
AccountMenuProps
{
session
:
MemberSession
session
:
MemberSession
onLogout
:
()
=>
void
onLogout
:
()
=>
Promise
<
void
>
}
}
function
avatarText
(
session
:
MemberSession
)
{
function
avatarText
(
session
:
MemberSession
)
{
...
@@ -30,9 +30,9 @@ export function AccountMenu({ session, onLogout }: AccountMenuProps) {
...
@@ -30,9 +30,9 @@ export function AccountMenu({ session, onLogout }: AccountMenuProps) {
navigate
(
path
)
navigate
(
path
)
}
}
const
logout
=
()
=>
{
const
logout
=
async
()
=>
{
setOpen
(
false
)
setOpen
(
false
)
onLogout
()
await
onLogout
()
navigate
(
'/library'
)
navigate
(
'/library'
)
}
}
...
@@ -43,7 +43,7 @@ export function AccountMenu({ session, onLogout }: AccountMenuProps) {
...
@@ -43,7 +43,7 @@ export function AccountMenu({ session, onLogout }: AccountMenuProps) {
className=
'account-menu__summary'
className=
'account-menu__summary'
onClick=
{
()
=>
navigateTo
(
'/my/profile'
)
}
onClick=
{
()
=>
navigateTo
(
'/my/profile'
)
}
>
>
<
Avatar
className=
'account-menu__avatar'
size=
{
48
}
src=
{
session
.
head_img
}
>
<
Avatar
className=
'account-menu__avatar'
size=
{
48
}
src=
{
session
.
head_img
||
undefined
}
>
{
avatarText
(
session
)
}
{
avatarText
(
session
)
}
</
Avatar
>
</
Avatar
>
<
span
className=
'account-menu__summary-copy'
>
<
span
className=
'account-menu__summary-copy'
>
...
@@ -75,7 +75,7 @@ export function AccountMenu({ session, onLogout }: AccountMenuProps) {
...
@@ -75,7 +75,7 @@ export function AccountMenu({ session, onLogout }: AccountMenuProps) {
<
Divider
/>
<
Divider
/>
<
div
className=
'account-menu__actions'
>
<
div
className=
'account-menu__actions'
>
<
Button
type=
'text'
icon=
{
<
LogoutOutlined
/>
}
onClick=
{
logout
}
>
<
Button
type=
'text'
icon=
{
<
LogoutOutlined
/>
}
onClick=
{
()
=>
void
logout
()
}
>
退出登录
退出登录
</
Button
>
</
Button
>
</
div
>
</
div
>
...
@@ -93,7 +93,7 @@ export function AccountMenu({ session, onLogout }: AccountMenuProps) {
...
@@ -93,7 +93,7 @@ export function AccountMenu({ session, onLogout }: AccountMenuProps) {
onOpenChange=
{
setOpen
}
onOpenChange=
{
setOpen
}
>
>
<
Button
type=
'text'
className=
'account-menu__trigger'
aria
-
label=
'打开账户菜单'
>
<
Button
type=
'text'
className=
'account-menu__trigger'
aria
-
label=
'打开账户菜单'
>
<
Avatar
className=
'account-menu__avatar'
size=
{
34
}
src=
{
session
.
head_img
}
>
<
Avatar
className=
'account-menu__avatar'
size=
{
34
}
src=
{
session
.
head_img
||
undefined
}
>
{
avatarText
(
session
)
}
{
avatarText
(
session
)
}
</
Avatar
>
</
Avatar
>
<
span
className=
'account-menu__trigger-copy'
>
<
span
className=
'account-menu__trigger-copy'
>
...
...
apps/learning/src/layouts/components/WorkbenchEntry.tsx
deleted
100644 → 0
浏览文件 @
d8033dd5
import
{
enterWorkbench
}
from
'@/modules/admin-access/admin-access'
import
{
useAdminAccess
,
useIssueAdminAccess
}
from
'@/modules/admin-access/query'
import
{
useSessionStore
}
from
'@/modules/auth/session'
export
function
WorkbenchEntry
()
{
const
session
=
useSessionStore
((
state
)
=>
state
.
session
)
const
accessQuery
=
useAdminAccess
(
Boolean
(
session
))
const
issueAccess
=
useIssueAdminAccess
()
if
(
!
session
||
!
accessQuery
.
data
?.
has_access
)
return
null
return
(
<
button
type=
'button'
className=
'learning-layout__nav-item'
disabled=
{
issueAccess
.
isPending
}
onClick=
{
()
=>
issueAccess
.
mutate
(
undefined
,
{
onSuccess
:
enterWorkbench
})
}
>
{
issueAccess
.
isPending
?
'返回管理后台'
:
'管理后台'
}
</
button
>
)
}
apps/learning/src/main.tsx
浏览文件 @
8d6dd989
...
@@ -8,22 +8,11 @@ import { RouterProvider } from 'react-router'
...
@@ -8,22 +8,11 @@ import { RouterProvider } from 'react-router'
import
{
NuqsAdapter
}
from
'nuqs/adapters/react-router/v7'
import
{
NuqsAdapter
}
from
'nuqs/adapters/react-router/v7'
import
{
router
}
from
'@/router/routes'
import
{
router
}
from
'@/router/routes'
import
{
LoadingFallback
}
from
'@/router/LoadingFallback'
import
{
LoadingFallback
}
from
'@/router/LoadingFallback'
import
{
shouldRetryQuery
}
from
'@/api/errors'
import
{
configureDevelopmentRequestAuth
}
from
'@/api/http-auth'
import
{
configureSessionExpiry
,
getMemberToken
}
from
'@/modules/auth/session'
import
'@/styles/global.css'
import
'@/styles/global.css'
configureDevelopmentRequestAuth
({
getBearerToken
:
getMemberToken
,
})
configureSessionExpiry
(()
=>
{
if
(
router
.
state
.
location
.
pathname
!==
'/login'
)
void
router
.
navigate
(
'/login'
,
{
replace
:
true
})
})
export
const
queryClient
=
new
QueryClient
({
export
const
queryClient
=
new
QueryClient
({
defaultOptions
:
{
defaultOptions
:
{
queries
:
{
staleTime
:
10
*
60
*
1000
,
retry
:
shouldRetryQuery
},
queries
:
{
staleTime
:
10
*
60
*
1000
,
retry
:
false
},
},
},
})
})
...
...
apps/learning/src/modules/admin-access/admin-access.test.ts
deleted
100644 → 0
浏览文件 @
d8033dd5
import
{
describe
,
expect
,
it
}
from
'vitest'
import
{
canOpenWorkbench
}
from
'./admin-access'
describe
(
'canOpenWorkbench'
,
()
=>
{
it
(
'only enables the workbench for an eligible matching admin account'
,
()
=>
{
expect
(
canOpenWorkbench
({
has_access
:
true
,
token
:
'admin-token'
})).
toBe
(
true
)
expect
(
canOpenWorkbench
({
has_access
:
false
})).
toBe
(
false
)
expect
(
canOpenWorkbench
({
has_access
:
true
})).
toBe
(
false
)
})
})
apps/learning/src/modules/admin-access/admin-access.ts
deleted
100644 → 0
浏览文件 @
d8033dd5
export
interface
AdminAccess
{
has_access
:
boolean
token
?:
string
}
export
function
canOpenWorkbench
(
access
:
AdminAccess
|
undefined
)
{
return
access
?.
has_access
===
true
&&
Boolean
(
access
.
token
)
}
export
function
enterWorkbench
(
access
:
AdminAccess
)
{
if
(
!
canOpenWorkbench
(
access
))
return
false
localStorage
.
setItem
(
'kiwi.token'
,
access
.
token
!
)
const
adminUrl
=
new
URL
(
import
.
meta
.
env
.
VITE_ADMIN_URL
)
adminUrl
.
hash
=
new
URLSearchParams
({
sso_token
:
access
.
token
!
}).
toString
()
window
.
open
(
adminUrl
.
toString
(),
'_blank'
)
return
true
}
apps/learning/src/modules/admin-access/api.ts
deleted
100644 → 0
浏览文件 @
d8033dd5
import
axios
from
'axios'
import
{
unwrapApiData
}
from
'@/api/unwrap'
import
type
{
ApiEnvelope
}
from
'@/api/types'
import
{
expireMemberSession
,
getMemberToken
}
from
'@/modules/auth/session'
import
type
{
AdminAccess
}
from
'./admin-access'
async
function
requestAdminAccess
(
issue
:
boolean
)
{
try
{
const
response
=
await
axios
.
post
<
ApiEnvelope
<
AdminAccess
>>
(
'/api/admin/user/memberSsoLogin'
,
{
issue
:
issue
?
1
:
0
},
{
withCredentials
:
true
,
headers
:
{
Authorization
:
getMemberToken
()
??
''
},
},
)
return
unwrapApiData
(
response
)
}
catch
(
error
)
{
if
(
axios
.
isAxiosError
(
error
)
&&
[
401
,
403
].
includes
(
error
.
response
?.
status
??
0
))
{
expireMemberSession
()
}
throw
error
}
}
export
function
getAdminAccess
()
{
return
requestAdminAccess
(
false
)
}
export
function
issueAdminAccess
()
{
return
requestAdminAccess
(
true
)
}
apps/learning/src/modules/admin-access/query.ts
deleted
100644 → 0
浏览文件 @
d8033dd5
import
{
useMutation
,
useQuery
}
from
'@tanstack/react-query'
import
{
getAdminAccess
,
issueAdminAccess
}
from
'./api'
export
const
adminAccessKeys
=
{
current
:
[
'admin-access'
]
as
const
,
}
export
function
useAdminAccess
(
enabled
:
boolean
)
{
return
useQuery
({
queryKey
:
adminAccessKeys
.
current
,
queryFn
:
getAdminAccess
,
enabled
,
staleTime
:
5
*
60
*
1000
,
})
}
export
function
useIssueAdminAccess
()
{
return
useMutation
({
mutationFn
:
issueAdminAccess
})
}
apps/learning/src/modules/auth/api.ts
浏览文件 @
8d6dd989
import
{
md5
}
from
'js-md5'
import
type
{
MemberSession
}
from
'@/stores/session'
import
{
unwrapApiData
}
from
'@/api/unwrap'
import
{
post
}
from
'@/utils/http'
import
type
{
ApiEnvelope
}
from
'@/api/types'
import
{
http
}
from
'@/utils/http'
import
type
{
LoginInput
,
MemberSession
}
from
'./types'
export
function
hashMemberPassword
(
password
:
string
)
{
interface
LoginInput
{
return
md5
(
md5
(
password
))
phone
:
string
type
:
'1'
|
'2'
password
?:
string
code
?:
string
uuid
?:
string
}
}
export
async
function
login
(
input
:
LoginInput
)
{
export
async
function
login
(
input
:
LoginInput
)
{
return
unwrapApiData
(
return
post
<
MemberSession
>
(
'/api/app/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
post
<
{
is_success
:
number
}
>
(
'/api/app/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
,
}),
})
)
}
}
export
async
function
logout
()
{
export
async
function
logout
()
{
return
unwrapApiData
(
return
post
(
'/api/app/v1/members/login/logout'
)
await
http
.
post
<
ApiEnvelope
<
{
is_success
:
number
}
>>
(
'/api/app/v1/members/login/logout'
,
{}),
)
}
}
apps/learning/src/modules/auth/types.ts
deleted
100644 → 0
浏览文件 @
d8033dd5
export
interface
MemberSession
{
members_id
:
number
name
?:
string
head_img
?:
string
phone
:
string
grade_name
?:
string
token
:
string
access_token
:
string
}
export
interface
LoginInput
{
phone
:
string
type
:
'1'
|
'2'
password
?:
string
code
?:
string
uuid
?:
string
}
apps/learning/src/modules/auth/views/LoginView.tsx
浏览文件 @
8d6dd989
import
{
useState
}
from
'react'
import
{
useState
}
from
'react'
import
{
Button
,
Card
,
Checkbox
,
Form
,
Input
,
Segmented
,
Space
,
Typography
,
message
}
from
'antd'
import
{
Button
,
Card
,
Checkbox
,
Form
,
Input
,
Segmented
,
Space
,
Typography
,
message
}
from
'antd'
import
{
useNavigate
}
from
'react-router'
import
{
useNavigate
}
from
'react-router'
import
{
login
,
hashMemberPassword
,
sendLoginCode
}
from
'../api'
import
{
login
,
sendLoginCode
}
from
'../api'
import
{
getDeviceUuid
}
from
'../device'
import
{
getDeviceUuid
}
from
'@/utils/device'
import
{
useSessionStore
}
from
'../session'
import
{
hashPassword
}
from
'@/utils/password'
import
{
useSessionStore
}
from
'@/stores/session'
import
'./LoginView.css'
import
'./LoginView.css'
type
LoginMode
=
'password'
|
'code'
type
LoginMode
=
'password'
|
'code'
...
@@ -33,8 +34,7 @@ export function LoginView() {
...
@@ -33,8 +34,7 @@ export function LoginView() {
try
{
try
{
await
sendLoginCode
(
phone
,
getDeviceUuid
())
await
sendLoginCode
(
phone
,
getDeviceUuid
())
message
.
success
(
'验证码已发送'
)
message
.
success
(
'验证码已发送'
)
}
catch
(
error
)
{
}
catch
{
message
.
error
(
error
instanceof
Error
?
error
.
message
:
'验证码发送失败'
)
}
finally
{
}
finally
{
setSending
(
false
)
setSending
(
false
)
}
}
...
@@ -46,15 +46,14 @@ export function LoginView() {
...
@@ -46,15 +46,14 @@ export function LoginView() {
const
session
=
await
login
({
const
session
=
await
login
({
phone
:
values
.
phone
,
phone
:
values
.
phone
,
type
:
mode
===
'password'
?
'1'
:
'2'
,
type
:
mode
===
'password'
?
'1'
:
'2'
,
password
:
mode
===
'password'
?
hash
Member
Password
(
values
.
password
??
''
)
:
undefined
,
password
:
mode
===
'password'
?
hashPassword
(
values
.
password
??
''
)
:
undefined
,
code
:
mode
===
'code'
?
values
.
code
:
undefined
,
code
:
mode
===
'code'
?
values
.
code
:
undefined
,
uuid
:
mode
===
'code'
?
getDeviceUuid
()
:
undefined
,
uuid
:
mode
===
'code'
?
getDeviceUuid
()
:
undefined
,
})
})
setSession
(
session
)
setSession
(
session
)
message
.
success
(
'登录成功'
)
message
.
success
(
'登录成功'
)
navigate
(
'/library'
,
{
replace
:
true
})
navigate
(
'/library'
,
{
replace
:
true
})
}
catch
(
error
)
{
}
catch
{
message
.
error
(
error
instanceof
Error
?
error
.
message
:
'登录失败'
)
}
finally
{
}
finally
{
setSubmitting
(
false
)
setSubmitting
(
false
)
}
}
...
...
apps/learning/src/modules/book/api.ts
浏览文件 @
8d6dd989
import
{
unwrapApiData
}
from
'@/api/unwrap'
import
{
post
}
from
'@/utils/http'
import
type
{
ApiEnvelope
}
from
'@/api/types'
import
{
http
}
from
'@/utils/http'
export
interface
BookDetail
{
export
interface
BookDetail
{
bookId
:
string
bookId
:
string
...
@@ -83,11 +81,6 @@ function list(value: unknown): unknown[] {
...
@@ -83,11 +81,6 @@ function list(value: unknown): unknown[] {
return
[]
return
[]
}
}
async
function
post
<
T
>
(
url
:
string
,
data
:
Record
<
string
,
unknown
>
):
Promise
<
T
>
{
const
response
=
await
http
.
post
<
ApiEnvelope
<
T
>>
(
url
,
data
)
return
unwrapApiData
(
response
)
}
function
toDetail
(
value
:
unknown
):
BookDetail
{
function
toDetail
(
value
:
unknown
):
BookDetail
{
const
data
=
record
(
value
)
const
data
=
record
(
value
)
return
{
return
{
...
...
apps/learning/src/modules/book/components/BookComments.tsx
浏览文件 @
8d6dd989
...
@@ -10,7 +10,9 @@ export function BookComments({ comments }: { comments: BookComment[] }) {
...
@@ -10,7 +10,9 @@ export function BookComments({ comments }: { comments: BookComment[] }) {
renderItem=
{
(
comment
)
=>
(
renderItem=
{
(
comment
)
=>
(
<
List
.
Item
>
<
List
.
Item
>
<
List
.
Item
.
Meta
<
List
.
Item
.
Meta
avatar=
{
<
Avatar
src=
{
comment
.
userImage
}
>
{
comment
.
user
.
slice
(
0
,
1
)
}
</
Avatar
>
}
avatar=
{
<
Avatar
src=
{
comment
.
userImage
||
undefined
}
>
{
comment
.
user
.
slice
(
0
,
1
)
}
</
Avatar
>
}
title=
{
title=
{
<>
<>
<
Typography
.
Text
>
{
comment
.
user
||
'匿名读者'
}
</
Typography
.
Text
>
{
' '
}
<
Typography
.
Text
>
{
comment
.
user
||
'匿名读者'
}
</
Typography
.
Text
>
{
' '
}
...
...
apps/learning/src/modules/book/views/BookDetailView.tsx
浏览文件 @
8d6dd989
import
{
useMutation
,
useQuery
,
useQueryClient
}
from
'@tanstack/react-query'
import
{
useMutation
,
useQuery
,
useQueryClient
}
from
'@tanstack/react-query'
import
{
import
{
Alert
,
Button
,
Button
,
Card
,
Card
,
Descriptions
,
Descriptions
,
...
@@ -17,15 +16,11 @@ import {
...
@@ -17,15 +16,11 @@ import {
Typography
,
Typography
,
message
,
message
,
}
from
'antd'
}
from
'antd'
import
{
useMemo
}
from
'react'
import
{
useMemo
,
useState
}
from
'react'
import
{
useNavigate
,
useParams
}
from
'react-router'
import
{
useNavigate
,
useParams
}
from
'react-router'
import
{
import
{
useSessionStore
}
from
'@/stores/session'
addBookToShelf
,
import
{
createBookAlipayOrder
,
getPayScene
}
from
'@/modules/purchase/api'
getBookStudyReport
,
import
{
getBookStudyReport
,
setBookCollection
,
type
BookChapter
,
type
BookDetail
}
from
'../api'
setBookCollection
,
type
BookChapter
,
type
BookDetail
,
}
from
'../api'
import
{
findFirstChapterId
}
from
'../chapter-navigation'
import
{
findFirstChapterId
}
from
'../chapter-navigation'
import
{
BookChapters
}
from
'../components/BookChapters'
import
{
BookChapters
}
from
'../components/BookChapters'
import
{
BookComments
}
from
'../components/BookComments'
import
{
BookComments
}
from
'../components/BookComments'
...
@@ -62,7 +57,9 @@ export function BookDetailView() {
...
@@ -62,7 +57,9 @@ export function BookDetailView() {
const
{
bookId
=
''
}
=
useParams
()
const
{
bookId
=
''
}
=
useParams
()
const
navigate
=
useNavigate
()
const
navigate
=
useNavigate
()
const
queryClient
=
useQueryClient
()
const
queryClient
=
useQueryClient
()
const
session
=
useSessionStore
((
state
)
=>
state
.
session
)
const
[
messageApi
,
contextHolder
]
=
message
.
useMessage
()
const
[
messageApi
,
contextHolder
]
=
message
.
useMessage
()
const
[
purchaseOpen
,
setPurchaseOpen
]
=
useState
(
false
)
const
detailQuery
=
useQuery
(
bookDetailOptions
(
bookId
))
const
detailQuery
=
useQuery
(
bookDetailOptions
(
bookId
))
const
chaptersQuery
=
useQuery
(
bookChaptersOptions
(
bookId
))
const
chaptersQuery
=
useQuery
(
bookChaptersOptions
(
bookId
))
const
commentsQuery
=
useQuery
(
bookCommentsOptions
(
bookId
))
const
commentsQuery
=
useQuery
(
bookCommentsOptions
(
bookId
))
...
@@ -83,21 +80,44 @@ export function BookDetailView() {
...
@@ -83,21 +80,44 @@ export function BookDetailView() {
)
)
},
},
})
})
const
shelfMutation
=
useMutation
({
const
reportMutation
=
useMutation
({
mutationFn
:
()
=>
getBookStudyReport
(
bookId
)
})
const
purchaseMutation
=
useMutation
({
mutationFn
:
async
()
=>
{
mutationFn
:
async
()
=>
{
if
(
!
(
await
addBookToShelf
(
bookId
)))
throw
new
Error
(
'加入购物车失败'
)
if
(
!
book
)
throw
new
Error
(
'图书信息尚未加载完成'
)
const
payment
=
await
createBookAlipayOrder
({
bookId
,
totalPrice
:
book
.
vipPrice
||
book
.
price
,
payScene
:
getPayScene
(),
})
if
(
payment
.
is_moneys
!==
0
&&
!
payment
.
pay_url
)
{
throw
new
Error
(
'未获取到支付宝支付地址'
)
}
return
payment
},
},
onSuccess
:
()
=>
{
onSuccess
:
(
payment
)
=>
{
messageApi
.
success
(
'已加入购物车'
)
setPurchaseOpen
(
false
)
if
(
payment
.
is_moneys
===
0
)
{
messageApi
.
success
(
'订单已完成'
)
void
queryClient
.
invalidateQueries
({
queryKey
:
bookKeys
.
detail
(
bookId
)
})
return
}
if
(
payment
.
pay_url
)
window
.
location
.
assign
(
payment
.
pay_url
)
},
},
})
})
const
reportMutation
=
useMutation
({
mutationFn
:
()
=>
getBookStudyReport
(
bookId
)
})
const
chapterAccess
=
useMemo
(
const
chapterAccess
=
useMemo
(
()
=>
(
chapter
:
BookChapter
)
=>
Boolean
(
book
?.
isFree
||
book
?.
isHave
||
chapter
.
isReading
),
()
=>
(
chapter
:
BookChapter
)
=>
Boolean
(
book
?.
isFree
||
book
?.
isHave
||
chapter
.
isReading
),
[
book
],
[
book
],
)
)
const
openReader
=
(
chapterId
:
string
)
=>
navigate
(
`/reader/
${
bookId
}
/
${
chapterId
}
`
)
const
openReader
=
(
chapterId
:
string
)
=>
navigate
(
`/reader/
${
bookId
}
/
${
chapterId
}
`
)
const
openPurchase
=
()
=>
{
if
(
!
session
)
{
messageApi
.
info
(
'请先登录'
)
navigate
(
'/login'
)
return
}
setPurchaseOpen
(
true
)
}
const
startReading
=
()
=>
{
const
startReading
=
()
=>
{
if
(
!
book
)
return
if
(
!
book
)
return
if
(
book
.
isFree
||
book
.
isHave
)
{
if
(
book
.
isFree
||
book
.
isHave
)
{
...
@@ -108,11 +128,11 @@ export function BookDetailView() {
...
@@ -108,11 +128,11 @@ export function BookDetailView() {
)
)
}
}
if
(
isTrialAvailable
(
book
))
return
openReader
(
book
.
readChapterId
)
if
(
isTrialAvailable
(
book
))
return
openReader
(
book
.
readChapterId
)
messageApi
.
info
(
'请使用 APP 购买'
)
openPurchase
(
)
}
}
const
selectChapter
=
(
chapter
:
BookChapter
)
=>
{
const
selectChapter
=
(
chapter
:
BookChapter
)
=>
{
if
(
chapterAccess
(
chapter
))
return
openReader
(
chapter
.
id
)
if
(
chapterAccess
(
chapter
))
return
openReader
(
chapter
.
id
)
messageApi
.
info
(
'请使用 APP 购买'
)
openPurchase
(
)
}
}
if
(
detailQuery
.
isLoading
)
{
if
(
detailQuery
.
isLoading
)
{
...
@@ -124,29 +144,18 @@ export function BookDetailView() {
...
@@ -124,29 +144,18 @@ export function BookDetailView() {
</
main
>
</
main
>
)
)
}
}
if
(
detailQuery
.
error
||
!
book
)
{
if
(
!
book
)
{
return
(
return
(
<
main
className=
'book-detail'
>
<
main
className=
'book-detail'
>
<
Typography
.
Title
level=
{
1
}
>
图书详情
</
Typography
.
Title
>
<
Typography
.
Title
level=
{
1
}
>
图书详情
</
Typography
.
Title
>
<
Typography
.
Text
>
图书编号:
{
bookId
}
</
Typography
.
Text
>
<
Typography
.
Text
>
图书编号:
{
bookId
}
</
Typography
.
Text
>
<
Alert
<
Empty
description=
'未找到图书'
/>
type=
'error'
showIcon
title=
'图书详情加载失败'
description=
{
(
detailQuery
.
error
as
Error
|
undefined
)?.
message
??
'未找到图书'
}
action=
{
<
Button
size=
'small'
onClick=
{
()
=>
void
detailQuery
.
refetch
()
}
>
重试
</
Button
>
}
/>
</
main
>
</
main
>
)
)
}
}
const
canRead
=
book
.
isFree
||
book
.
isHave
const
canRead
=
book
.
isFree
||
book
.
isHave
const
primaryAction
=
canRead
?
'阅读'
:
isTrialAvailable
(
book
)
?
'试读'
:
'购买'
const
primaryAction
=
canRead
?
'阅读'
:
isTrialAvailable
(
book
)
?
'试读'
:
'购买'
return
(
return
(
<
main
className=
'book-detail'
>
<
main
className=
'book-detail'
>
{
contextHolder
}
{
contextHolder
}
...
@@ -185,26 +194,6 @@ export function BookDetailView() {
...
@@ -185,26 +194,6 @@ export function BookDetailView() {
</
Flex
>
</
Flex
>
</
Card
>
</
Card
>
{
collectionMutation
.
error
||
shelfMutation
.
error
||
reportMutation
.
error
?
(
<
Alert
type=
'error'
showIcon
title=
'操作失败'
description=
{
(
collectionMutation
.
error
??
shelfMutation
.
error
??
(
reportMutation
.
error
as
Error
))
.
message
}
/>
)
:
null
}
{
chaptersQuery
.
error
?
(
<
Alert
type=
'warning'
showIcon
title=
'目录加载失败'
description=
{
(
chaptersQuery
.
error
as
Error
).
message
}
/>
)
:
null
}
<
Card
>
<
Card
>
<
Tabs
<
Tabs
items=
{
[
items=
{
[
...
@@ -265,13 +254,6 @@ export function BookDetailView() {
...
@@ -265,13 +254,6 @@ export function BookDetailView() {
label
:
`书评 (${book.commentNum || book.ratingCount})`
,
label
:
`书评 (${book.commentNum || book.ratingCount})`
,
children
:
commentsQuery
.
isLoading
?
(
children
:
commentsQuery
.
isLoading
?
(
<
Skeleton
active
paragraph=
{
{
rows
:
4
}
}
/>
<
Skeleton
active
paragraph=
{
{
rows
:
4
}
}
/>
)
:
commentsQuery
.
error
?
(
<
Alert
type=
'warning'
showIcon
title=
'书评加载失败'
description=
{
(
commentsQuery
.
error
as
Error
).
message
}
/>
)
:
(
)
:
(
<
BookComments
comments=
{
commentsQuery
.
data
??
[]
}
/>
<
BookComments
comments=
{
commentsQuery
.
data
??
[]
}
/>
),
),
...
@@ -303,11 +285,35 @@ export function BookDetailView() {
...
@@ -303,11 +285,35 @@ export function BookDetailView() {
学习报告
学习报告
</
Button
>
</
Button
>
)
:
null
}
)
:
null
}
{
isTrialAvailable
(
book
)
?
(
<>
<
Button
type=
'primary'
onClick=
{
startReading
}
>
<
Button
type=
'primary'
onClick=
{
startReading
}
>
{
primaryAction
}
{
primaryAction
}
</
Button
>
</
Button
>
<
Button
onClick=
{
openPurchase
}
loading=
{
purchaseMutation
.
isPending
}
>
购买
</
Button
>
</>
)
:
(
<
Button
type=
'primary'
onClick=
{
startReading
}
loading=
{
purchaseMutation
.
isPending
}
>
{
primaryAction
}
</
Button
>
)
}
</
Space
>
</
Space
>
</
Card
>
</
Card
>
<
Modal
title=
'确认购买'
open=
{
purchaseOpen
}
okText=
'支付宝支付'
cancelText=
'取消'
confirmLoading=
{
purchaseMutation
.
isPending
}
onOk=
{
()
=>
purchaseMutation
.
mutate
()
}
onCancel=
{
()
=>
setPurchaseOpen
(
false
)
}
>
<
Typography
.
Paragraph
>
{
book
.
bookName
}
</
Typography
.
Paragraph
>
<
Typography
.
Paragraph
>
支付金额:¥
{
book
.
vipPrice
||
book
.
price
}
</
Typography
.
Paragraph
>
<
Typography
.
Text
type=
'secondary'
>
支付完成后即可在浏览器或 APP 中阅读。
</
Typography
.
Text
>
</
Modal
>
<
Modal
<
Modal
title=
'学习报告'
title=
'学习报告'
open=
{
Boolean
(
reportMutation
.
data
)
}
open=
{
Boolean
(
reportMutation
.
data
)
}
...
...
apps/learning/src/modules/download/mobile-download.ts
deleted
100644 → 0
浏览文件 @
d8033dd5
import
{
create
}
from
'zustand'
interface
MobileDownloadState
{
open
:
boolean
show
:
()
=>
void
hide
:
()
=>
void
}
export
const
useMobileDownloadStore
=
create
<
MobileDownloadState
>
((
set
)
=>
({
open
:
false
,
show
:
()
=>
set
({
open
:
true
}),
hide
:
()
=>
set
({
open
:
false
}),
}))
apps/learning/src/modules/
my/
help/api.ts
→
apps/learning/src/modules/help/api.ts
浏览文件 @
8d6dd989
import
{
unwrapApiData
}
from
'@/api/unwrap'
import
{
post
}
from
'@/utils/http'
import
type
{
ApiEnvelope
}
from
'@/api/types'
import
{
http
}
from
'@/utils/http'
export
interface
HelpArticleSummary
{
export
interface
HelpArticleSummary
{
id
:
string
id
:
string
...
@@ -29,12 +27,10 @@ function text(value: unknown) {
...
@@ -29,12 +27,10 @@ function text(value: unknown) {
}
}
export
async
function
getHelpArticles
():
Promise
<
HelpArticleSummary
[]
>
{
export
async
function
getHelpArticles
():
Promise
<
HelpArticleSummary
[]
>
{
const
data
=
unwrapApiData
(
const
data
=
await
post
<
unknown
>
(
'/api/app/v1/common/Help/getList'
,
{
await
http
.
post
<
ApiEnvelope
<
unknown
>>
(
'/api/app/v1/common/Help/getList'
,
{
page
:
1
,
page
:
1
,
page_size
:
10
,
page_size
:
10
,
}),
})
)
return
list
(
data
)
return
list
(
data
)
.
map
((
value
)
=>
{
.
map
((
value
)
=>
{
const
item
=
record
(
value
)
const
item
=
record
(
value
)
...
@@ -44,11 +40,7 @@ export async function getHelpArticles(): Promise<HelpArticleSummary[]> {
...
@@ -44,11 +40,7 @@ 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
(
await
post
<
unknown
>
(
'/api/app/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
,
title
:
text
(
item
.
help_title
??
item
.
helpTitle
),
title
:
text
(
item
.
help_title
??
item
.
helpTitle
),
...
...
apps/learning/src/modules/
my/
help/components/PageHero.tsx
→
apps/learning/src/modules/help/components/PageHero.tsx
浏览文件 @
8d6dd989
File moved
apps/learning/src/modules/
my/
help/components/Redbud.tsx
→
apps/learning/src/modules/help/components/Redbud.tsx
浏览文件 @
8d6dd989
...
@@ -56,7 +56,15 @@ function FlowerShape({ offsetX, offsetY, petal }: FlowerShapeProps) {
...
@@ -56,7 +56,15 @@ function FlowerShape({ offsetX, offsetY, petal }: FlowerShapeProps) {
return
(
return
(
<
g
transform=
{
`translate(${offsetX} ${offsetY})`
}
>
<
g
transform=
{
`translate(${offsetX} ${offsetY})`
}
>
{
PETAL_ANGLES
.
map
((
angle
)
=>
(
{
PETAL_ANGLES
.
map
((
angle
)
=>
(
<
ellipse
key=
{
angle
}
cx=
'0'
cy=
'-6'
rx=
'4'
ry=
'5.6'
fill=
{
petal
}
transform=
{
`rotate(${angle})`
}
/>
<
ellipse
key=
{
angle
}
cx=
'0'
cy=
'-6'
rx=
'4'
ry=
'5.6'
fill=
{
petal
}
transform=
{
`rotate(${angle})`
}
/>
))
}
))
}
<
circle
r=
'3.2'
fill=
'#c2456a'
/>
<
circle
r=
'3.2'
fill=
'#c2456a'
/>
</
g
>
</
g
>
...
@@ -103,7 +111,12 @@ export function RedbudBranch({ className, style }: RedbudBranchProps) {
...
@@ -103,7 +111,12 @@ export function RedbudBranch({ className, style }: RedbudBranchProps) {
strokeWidth=
'3'
strokeWidth=
'3'
strokeLinecap=
'round'
strokeLinecap=
'round'
/>
/>
<
path
d=
'M96 60 C 88 44, 84 34, 90 22'
stroke=
'#ffc9d6'
strokeWidth=
'2.4'
strokeLinecap=
'round'
/>
<
path
d=
'M96 60 C 88 44, 84 34, 90 22'
stroke=
'#ffc9d6'
strokeWidth=
'2.4'
strokeLinecap=
'round'
/>
{
CLUSTERS
.
map
((
cluster
,
index
)
=>
(
{
CLUSTERS
.
map
((
cluster
,
index
)
=>
(
<
FlowerCluster
key=
{
index
}
x=
{
cluster
.
x
}
y=
{
cluster
.
y
}
scale=
{
cluster
.
scale
}
/>
<
FlowerCluster
key=
{
index
}
x=
{
cluster
.
x
}
y=
{
cluster
.
y
}
scale=
{
cluster
.
scale
}
/>
))
}
))
}
...
...
apps/learning/src/modules/
my/
help/query.ts
→
apps/learning/src/modules/help/query.ts
浏览文件 @
8d6dd989
File moved
apps/learning/src/modules/
my/
help/routes.tsx
→
apps/learning/src/modules/help/routes.tsx
浏览文件 @
8d6dd989
File moved
apps/learning/src/modules/
my/
help/views/AboutView.tsx
→
apps/learning/src/modules/help/views/AboutView.tsx
浏览文件 @
8d6dd989
...
@@ -95,7 +95,8 @@ export function AboutView() {
...
@@ -95,7 +95,8 @@ export function AboutView() {
<
section
className=
'info-section'
>
<
section
className=
'info-section'
>
<
h2
className=
'info-section__title'
>
平台能力
</
h2
>
<
h2
className=
'info-section__title'
>
平台能力
</
h2
>
<
p
className=
'info-section__lede'
>
<
p
className=
'info-section__lede'
>
平台打通「学校端 — 出版端 — 教学端 — 学生端」的完整流程,基于大数据、多模态的 AI 辅助教学,配套高品质专业出版服务。
平台打通「学校端 — 出版端 — 教学端 — 学生端」的完整流程,基于大数据、多模态的 AI
辅助教学,配套高品质专业出版服务。
</
p
>
</
p
>
<
div
className=
'info-highlights'
>
<
div
className=
'info-highlights'
>
{
highlights
.
map
((
item
)
=>
(
{
highlights
.
map
((
item
)
=>
(
...
@@ -135,10 +136,18 @@ export function AboutView() {
...
@@ -135,10 +136,18 @@ export function AboutView() {
<
div
className=
'info-meta__row'
>
<
div
className=
'info-meta__row'
>
<
dt
>
相关协议
</
dt
>
<
dt
>
相关协议
</
dt
>
<
dd
>
<
dd
>
<
a
href=
{
`${agreementBaseUrl}/ser_agreement.html?v=3`
}
target=
'_blank'
rel=
'noreferrer'
>
<
a
href=
{
`${agreementBaseUrl}/ser_agreement.html?v=3`
}
target=
'_blank'
rel=
'noreferrer'
>
《用户协议》
《用户协议》
</
a
>
</
a
>
<
a
href=
{
`${agreementBaseUrl}/pri_agreement.html?v=3`
}
target=
'_blank'
rel=
'noreferrer'
>
<
a
href=
{
`${agreementBaseUrl}/pri_agreement.html?v=3`
}
target=
'_blank'
rel=
'noreferrer'
>
《隐私政策》
《隐私政策》
</
a
>
</
a
>
</
dd
>
</
dd
>
...
...
apps/learning/src/modules/
my/
help/views/ContactView.tsx
→
apps/learning/src/modules/help/views/ContactView.tsx
浏览文件 @
8d6dd989
...
@@ -9,7 +9,13 @@ const contactChannels = [
...
@@ -9,7 +9,13 @@ const contactChannels = [
label
:
'公司地址'
,
label
:
'公司地址'
,
value
:
'北京市海淀区北四环西路52号方正国际大厦5层501'
,
value
:
'北京市海淀区北四环西路52号方正国际大厦5层501'
,
},
},
{
key
:
'phone'
,
icon
:
PhoneOutlined
,
label
:
'联系电话'
,
value
:
'010-62793909'
,
href
:
'tel:010-62793909'
},
{
key
:
'phone'
,
icon
:
PhoneOutlined
,
label
:
'联系电话'
,
value
:
'010-62793909'
,
href
:
'tel:010-62793909'
,
},
{
{
key
:
'email'
,
key
:
'email'
,
icon
:
MailOutlined
,
icon
:
MailOutlined
,
...
...
apps/learning/src/modules/
my/
help/views/HelpDetailView.tsx
→
apps/learning/src/modules/help/views/HelpDetailView.tsx
浏览文件 @
8d6dd989
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
Alert
,
Button
,
Card
,
Skeleton
}
from
'antd'
import
{
Card
,
Skeleton
}
from
'antd'
import
{
Link
,
useParams
}
from
'react-router'
import
{
Link
,
useParams
}
from
'react-router'
import
{
helpArticleOptions
}
from
'../query'
import
{
helpArticleOptions
}
from
'../query'
import
{
PageHero
}
from
'../components/PageHero'
import
{
PageHero
}
from
'../components/PageHero'
...
@@ -21,23 +21,6 @@ export function HelpDetailView() {
...
@@ -21,23 +21,6 @@ export function HelpDetailView() {
</
main
>
</
main
>
)
)
if
(
articleQuery
.
isError
)
return
(
<
main
className=
'info-page'
>
<
PageHero
title=
'帮助详情'
intro=
'文章加载失败,请稍后重试。'
/>
<
section
className=
'info-card'
>
<
Alert
style=
{
{
margin
:
'16px 0'
}
}
type=
'error'
showIcon
title=
'帮助详情加载失败'
description=
{
articleQuery
.
error
.
message
}
action=
{
<
Button
onClick=
{
()
=>
void
articleQuery
.
refetch
()
}
>
重试
</
Button
>
}
/>
</
section
>
</
main
>
)
const
article
=
articleQuery
.
data
const
article
=
articleQuery
.
data
if
(
!
article
)
return
null
if
(
!
article
)
return
null
return
(
return
(
...
...
apps/learning/src/modules/
my/
help/views/HelpListView.tsx
→
apps/learning/src/modules/help/views/HelpListView.tsx
浏览文件 @
8d6dd989
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
RightOutlined
}
from
'@ant-design/icons'
import
{
RightOutlined
}
from
'@ant-design/icons'
import
{
Alert
,
Button
,
Empty
,
Skeleton
}
from
'antd'
import
{
Empty
,
Skeleton
}
from
'antd'
import
{
Link
}
from
'react-router'
import
{
Link
}
from
'react-router'
import
{
helpArticlesOptions
}
from
'../query'
import
{
helpArticlesOptions
}
from
'../query'
import
{
PageHero
}
from
'../components/PageHero'
import
{
PageHero
}
from
'../components/PageHero'
...
@@ -17,16 +17,6 @@ export function HelpListView() {
...
@@ -17,16 +17,6 @@ export function HelpListView() {
{
articlesQuery
.
isPending
?
(
{
articlesQuery
.
isPending
?
(
<
Skeleton
active
paragraph=
{
{
rows
:
8
}
}
style=
{
{
padding
:
'16px 0'
}
}
/>
<
Skeleton
active
paragraph=
{
{
rows
:
8
}
}
style=
{
{
padding
:
'16px 0'
}
}
/>
)
:
null
}
)
:
null
}
{
articlesQuery
.
isError
?
(
<
Alert
style=
{
{
margin
:
'16px 0'
}
}
type=
'error'
showIcon
title=
'帮助内容加载失败'
description=
{
articlesQuery
.
error
.
message
}
action=
{
<
Button
onClick=
{
()
=>
void
articlesQuery
.
refetch
()
}
>
重试
</
Button
>
}
/>
)
:
null
}
{
articlesQuery
.
data
&&
articles
.
length
===
0
?
(
{
articlesQuery
.
data
&&
articles
.
length
===
0
?
(
<
Empty
description=
'暂无帮助内容'
style=
{
{
padding
:
'32px 0'
}
}
/>
<
Empty
description=
'暂无帮助内容'
style=
{
{
padding
:
'32px 0'
}
}
/>
)
:
null
}
)
:
null
}
...
...
apps/learning/src/modules/
my/
help/views/info-pages.css
→
apps/learning/src/modules/help/views/info-pages.css
浏览文件 @
8d6dd989
File moved
apps/learning/src/modules/library/api.ts
浏览文件 @
8d6dd989
import
{
unwrapApiData
}
from
'@/api/unwrap'
import
{
post
}
from
'@/utils/http'
import
type
{
ApiEnvelope
}
from
'@/api/types'
import
{
http
}
from
'@/utils/http'
import
type
{
LibrarySearchState
}
from
'./query-state'
import
type
{
LibrarySearchState
}
from
'./query-state'
export
interface
LibraryBrowseApiParams
{
export
interface
LibraryBrowseApiParams
{
...
@@ -97,11 +95,6 @@ function toFilter(value: unknown, idKey: 'category_id' | 'label_id'): LibraryFil
...
@@ -97,11 +95,6 @@ function toFilter(value: unknown, idKey: 'category_id' | 'label_id'): LibraryFil
return
{
id
:
asNumber
(
item
[
idKey
]),
name
:
asString
(
item
.
name
)
}
return
{
id
:
asNumber
(
item
[
idKey
]),
name
:
asString
(
item
.
name
)
}
}
}
async
function
post
<
T
>
(
url
:
string
,
data
:
Record
<
string
,
unknown
>
):
Promise
<
T
>
{
const
response
=
await
http
.
post
<
ApiEnvelope
<
T
>>
(
url
,
data
)
return
unwrapApiData
(
response
)
}
export
function
toLibraryApiParams
(
state
:
LibrarySearchState
):
LibraryApiParams
{
export
function
toLibraryApiParams
(
state
:
LibrarySearchState
):
LibraryApiParams
{
const
keyword
=
state
.
keyword
.
trim
()
const
keyword
=
state
.
keyword
.
trim
()
if
(
keyword
)
{
if
(
keyword
)
{
...
...
apps/learning/src/modules/library/components/LibraryBookCard.tsx
浏览文件 @
8d6dd989
import
{
HeartFilled
,
HeartOutlined
,
LoadingOutlined
}
from
'@ant-design/icons'
import
{
Button
,
Card
,
Image
,
Tag
,
Tooltip
,
Typography
}
from
'antd'
import
{
Button
,
Card
,
Image
,
Tag
,
Tooltip
,
Typography
}
from
'antd'
import
{
Link
}
from
'react-router'
import
{
useNavigate
}
from
'react-router'
import
defaultBookCover
from
'@/assets/default-book-cover.jpg'
import
defaultBookCover
from
'@/assets/default-book-cover.jpg'
import
type
{
LibraryBook
}
from
'../api'
import
type
{
LibraryBook
}
from
'../api'
...
@@ -21,12 +22,23 @@ export function LibraryBookCard({
...
@@ -21,12 +22,23 @@ export function LibraryBookCard({
isUpdatingCollection
,
isUpdatingCollection
,
onCollectionChange
,
onCollectionChange
,
}:
LibraryBookCardProps
)
{
}:
LibraryBookCardProps
)
{
const
navigate
=
useNavigate
()
const
favourite
=
book
.
is_collection
===
1
const
favourite
=
book
.
is_collection
===
1
const
openDetail
=
()
=>
navigate
(
`/book/
${
book
.
book_id
}
`
)
return
(
return
(
<
Card
className=
{
`library-book-card library-book-card--${view}`
}
hoverable
>
<
Card
<
Link
className=
{
`library-book-card library-book-card--${view}`
}
hoverable
role=
'link'
tabIndex=
{
0
}
onClick=
{
openDetail
}
onKeyDown=
{
(
event
)
=>
{
if
(
event
.
key
===
'Enter'
&&
event
.
currentTarget
===
event
.
target
)
openDetail
()
}
}
>
<
div
className=
'library-book-card__cover'
className=
'library-book-card__cover'
to=
{
`/book/${book.book_id}`
}
aria
-
label=
{
`查看《${book.book_name}》详情`
}
aria
-
label=
{
`查看《${book.book_name}》详情`
}
>
>
<
Image
<
Image
...
@@ -35,11 +47,11 @@ export function LibraryBookCard({
...
@@ -35,11 +47,11 @@ export function LibraryBookCard({
alt=
{
`${book.book_name}封面`
}
alt=
{
`${book.book_name}封面`
}
fallback=
{
defaultBookCover
}
fallback=
{
defaultBookCover
}
/>
/>
</
Link
>
</
div
>
<
div
className=
'library-book-card__content'
>
<
div
className=
'library-book-card__content'
>
<
div
>
<
div
>
<
Typography
.
Title
level=
{
4
}
ellipsis=
{
{
rows
:
2
}
}
>
<
Typography
.
Title
level=
{
4
}
ellipsis=
{
{
rows
:
2
}
}
>
<
Link
to=
{
`/book/${book.book_id}`
}
>
{
book
.
book_name
}
</
Link
>
{
book
.
book_name
}
</
Typography
.
Title
>
</
Typography
.
Title
>
<
Typography
.
Text
type=
'secondary'
>
{
book
.
authors
||
'作者未知'
}
</
Typography
.
Text
>
<
Typography
.
Text
type=
'secondary'
>
{
book
.
authors
||
'作者未知'
}
</
Typography
.
Text
>
</
div
>
</
div
>
...
@@ -60,11 +72,21 @@ export function LibraryBookCard({
...
@@ -60,11 +72,21 @@ export function LibraryBookCard({
aria
-
label=
{
favourite
?
`取消收藏《${book.book_name}》`
:
`收藏《${book.book_name}》`
}
aria
-
label=
{
favourite
?
`取消收藏《${book.book_name}》`
:
`收藏《${book.book_name}》`
}
type=
'text'
type=
'text'
danger=
{
favourite
}
danger=
{
favourite
}
loading=
{
isUpdatingCollection
}
disabled=
{
isUpdatingCollection
}
onClick=
{
()
=>
onCollectionChange
(
book
)
}
icon=
{
>
isUpdatingCollection
?
(
{
favourite
?
'♥'
:
'♡'
}
<
LoadingOutlined
spin
/>
</
Button
>
)
:
favourite
?
(
<
HeartFilled
/>
)
:
(
<
HeartOutlined
/>
)
}
onClick=
{
(
event
)
=>
{
event
.
stopPropagation
()
onCollectionChange
(
book
)
}
}
/>
</
Tooltip
>
</
Tooltip
>
</
div
>
</
div
>
</
div
>
</
div
>
...
...
apps/learning/src/modules/library/views/LibraryView.css
浏览文件 @
8d6dd989
...
@@ -152,7 +152,7 @@
...
@@ -152,7 +152,7 @@
box-shadow
:
0
12px
28px
rgb
(
31
41
55
/
12%
);
box-shadow
:
0
12px
28px
rgb
(
31
41
55
/
12%
);
}
}
.library-book-card.ant-card
:active
{
.library-book-card.ant-card
:active
:not
(
:has
(
.ant-btn
:active
))
{
transform
:
scale
(
0.985
);
transform
:
scale
(
0.985
);
transition-duration
:
100ms
;
transition-duration
:
100ms
;
}
}
...
...
apps/learning/src/modules/library/views/LibraryView.tsx
浏览文件 @
8d6dd989
import
{
useMutation
,
useQuery
,
useQueryClient
}
from
'@tanstack/react-query'
import
{
useMutation
,
useQuery
,
useQueryClient
}
from
'@tanstack/react-query'
import
{
Alert
,
Empty
,
Pagination
,
Skeleton
,
Space
}
from
'antd'
import
{
Empty
,
Pagination
,
Skeleton
,
Space
}
from
'antd'
import
{
useState
}
from
'react'
import
{
useState
}
from
'react'
import
{
setBookCollection
,
type
LibraryBook
}
from
'../api'
import
{
setBookCollection
,
type
LibraryBook
}
from
'../api'
import
{
LibraryBanner
}
from
'../components/LibraryBanner'
import
{
LibraryBanner
}
from
'../components/LibraryBanner'
...
@@ -45,21 +45,10 @@ export function LibraryView() {
...
@@ -45,21 +45,10 @@ export function LibraryView() {
onSettled
:
()
=>
setChangingBookId
(
undefined
),
onSettled
:
()
=>
setChangingBookId
(
undefined
),
})
})
const
filterError
=
categoriesQuery
.
error
??
labelsQuery
.
error
const
bookError
=
booksQuery
.
error
return
(
return
(
<
main
className=
'library-view'
>
<
main
className=
'library-view'
>
<
LibraryBanner
/>
<
LibraryBanner
/>
{
filterError
?
(
<
Alert
type=
'warning'
showIcon
title=
'筛选项加载失败'
description=
{
(
filterError
as
Error
).
message
}
/>
)
:
null
}
<
div
className=
'library-toolbar'
>
<
div
className=
'library-toolbar'
>
<
LibraryToolbar
<
LibraryToolbar
state=
{
state
}
state=
{
state
}
...
@@ -73,23 +62,7 @@ export function LibraryView() {
...
@@ -73,23 +62,7 @@ export function LibraryView() {
/>
/>
</
div
>
</
div
>
{
collectionMutation
.
error
?
(
{
booksQuery
.
isLoading
?
(
<
Alert
type=
'error'
showIcon
title=
'收藏操作失败'
description=
{
(
collectionMutation
.
error
as
Error
).
message
}
/>
)
:
null
}
{
bookError
?
(
<
Alert
type=
'error'
showIcon
title=
'图书加载失败'
description=
{
(
bookError
as
Error
).
message
}
action=
{
<
a
onClick=
{
()
=>
void
booksQuery
.
refetch
()
}
>
重试
</
a
>
}
/>
)
:
booksQuery
.
isLoading
?
(
<
Skeleton
active
paragraph=
{
{
rows
:
8
}
}
/>
<
Skeleton
active
paragraph=
{
{
rows
:
8
}
}
/>
)
:
booksQuery
.
data
?.
list
.
length
?
(
)
:
booksQuery
.
data
?.
list
.
length
?
(
<
div
className=
{
`library-books library-books--${state.view}`
}
>
<
div
className=
{
`library-books library-books--${state.view}`
}
>
...
...
apps/learning/src/modules/my/api.ts
浏览文件 @
8d6dd989
import
{
http
}
from
'@/utils/http'
import
{
post
}
from
'@/utils/http'
import
{
unwrapApiData
}
from
'@/api/unwrap'
export
interface
MyStatistics
{
export
interface
MyStatistics
{
name
?:
string
name
?:
string
...
@@ -85,8 +84,7 @@ function toMyBookListResult(data: unknown): MyBookListResult {
...
@@ -85,8 +84,7 @@ function toMyBookListResult(data: unknown): MyBookListResult {
}
}
export
async
function
getMyStatistics
():
Promise
<
MyStatistics
>
{
export
async
function
getMyStatistics
():
Promise
<
MyStatistics
>
{
const
response
=
await
http
.
post
(
'/api/app/v1/members/Information/getMyStatistics'
,
{})
const
data
=
asRecord
(
await
post
(
'/api/app/v1/members/Information/getMyStatistics'
))
const
data
=
asRecord
(
unwrapApiData
(
response
))
return
{
return
{
name
:
asOptionalText
(
data
.
name
),
name
:
asOptionalText
(
data
.
name
),
...
@@ -105,8 +103,7 @@ export async function getMyStatistics(): Promise<MyStatistics> {
...
@@ -105,8 +103,7 @@ export async function getMyStatistics(): Promise<MyStatistics> {
}
}
async
function
getMyBookList
(
path
:
string
,
params
:
MyListParams
):
Promise
<
MyBookListResult
>
{
async
function
getMyBookList
(
path
:
string
,
params
:
MyListParams
):
Promise
<
MyBookListResult
>
{
const
response
=
await
http
.
post
(
path
,
{
page
:
params
.
page
,
page_size
:
params
.
pageSize
})
return
toMyBookListResult
(
await
post
(
path
,
{
page
:
params
.
page
,
page_size
:
params
.
pageSize
}))
return
toMyBookListResult
(
unwrapApiData
(
response
))
}
}
export
function
getMyCourses
(
params
:
MyListParams
)
{
export
function
getMyCourses
(
params
:
MyListParams
)
{
...
@@ -118,7 +115,6 @@ export function getBookshelf(params: MyListParams) {
...
@@ -118,7 +115,6 @@ export function getBookshelf(params: MyListParams) {
}
}
export
async
function
deleteBookshelfItem
(
cartId
:
string
)
{
export
async
function
deleteBookshelfItem
(
cartId
:
string
)
{
const
response
=
await
http
.
post
(
'/api/app/v1/orders/Orders/delCart'
,
{
cart_id
:
cartId
})
const
data
=
await
post
<
ApiRecord
>
(
'/api/app/v1/orders/Orders/delCart'
,
{
cart_id
:
cartId
})
const
data
=
unwrapApiData
<
ApiRecord
>
(
response
)
return
data
.
is_success
===
1
||
data
.
is_success
===
'1'
||
data
.
is_success
===
true
return
data
.
is_success
===
1
||
data
.
is_success
===
'1'
||
data
.
is_success
===
true
}
}
apps/learning/src/modules/my/books/views/MyBooksView.tsx
浏览文件 @
8d6dd989
import
{
Alert
,
Button
,
Card
,
Empty
,
Image
,
Pagination
,
Skeleton
,
Tag
,
Typography
}
from
'antd'
import
{
Card
,
Empty
,
Image
,
Pagination
,
Skeleton
,
Tag
,
Typography
}
from
'antd'
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
useQuery
}
from
'@tanstack/react-query'
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'
...
@@ -23,15 +23,6 @@ export function MyBooksView() {
...
@@ -23,15 +23,6 @@ export function MyBooksView() {
</
div
>
</
div
>
{
coursesQuery
.
isPending
?
<
Skeleton
active
paragraph=
{
{
rows
:
12
}
}
/>
:
null
}
{
coursesQuery
.
isPending
?
<
Skeleton
active
paragraph=
{
{
rows
:
12
}
}
/>
:
null
}
{
coursesQuery
.
isError
?
(
<
Alert
showIcon
type=
'error'
title=
'书架加载失败'
description=
{
coursesQuery
.
error
.
message
}
action=
{
<
Button
onClick=
{
()
=>
void
coursesQuery
.
refetch
()
}
>
重试
</
Button
>
}
/>
)
:
null
}
{
coursesQuery
.
data
&&
coursesQuery
.
data
.
items
.
length
===
0
?
(
{
coursesQuery
.
data
&&
coursesQuery
.
data
.
items
.
length
===
0
?
(
<
Empty
description=
'暂无书籍'
/>
<
Empty
description=
'暂无书籍'
/>
)
:
null
}
)
:
null
}
...
...
apps/learning/src/modules/my/bookshelf/views/MyBookshelfView.tsx
浏览文件 @
8d6dd989
import
{
import
{
Alert
,
Button
,
Button
,
Card
,
Card
,
Checkbox
,
Checkbox
,
...
@@ -71,23 +70,6 @@ export function MyBookshelfView() {
...
@@ -71,23 +70,6 @@ export function MyBookshelfView() {
</
div
>
</
div
>
{
bookshelfQuery
.
isPending
?
<
Skeleton
active
paragraph=
{
{
rows
:
10
}
}
/>
:
null
}
{
bookshelfQuery
.
isPending
?
<
Skeleton
active
paragraph=
{
{
rows
:
10
}
}
/>
:
null
}
{
bookshelfQuery
.
isError
?
(
<
Alert
showIcon
type=
'error'
title=
'购物车加载失败'
description=
{
bookshelfQuery
.
error
.
message
}
action=
{
<
Button
onClick=
{
()
=>
void
bookshelfQuery
.
refetch
()
}
>
重试
</
Button
>
}
/>
)
:
null
}
{
deleteMutation
.
isError
?
(
<
Alert
showIcon
type=
'error'
title=
'删除失败'
description=
{
deleteMutation
.
error
.
message
}
/>
)
:
null
}
{
bookshelfQuery
.
data
&&
items
.
length
===
0
?
<
Empty
description=
'购物车暂无书籍'
/>
:
null
}
{
bookshelfQuery
.
data
&&
items
.
length
===
0
?
<
Empty
description=
'购物车暂无书籍'
/>
:
null
}
{
items
.
length
?
(
{
items
.
length
?
(
<>
<>
...
...
apps/learning/src/modules/my/coupons/api.ts
浏览文件 @
8d6dd989
import
{
unwrapApiData
}
from
'@/api/unwrap'
import
{
post
}
from
'@/utils/http'
import
{
http
}
from
'@/utils/http'
export
type
CouponStatus
=
0
|
1
export
type
CouponStatus
=
0
|
1
...
@@ -10,56 +9,23 @@ export interface CouponListParams {
...
@@ -10,56 +9,23 @@ export interface CouponListParams {
}
}
export
interface
CouponItem
{
export
interface
CouponItem
{
id
:
string
coupon_rec_id
:
number
name
:
string
coupon_name
:
string
threshold
:
string
norm_price
:
string
discount
:
string
reduced_price
:
string
startTime
:
string
end_time
:
string
endTime
:
string
use_status
:
number
useStatus
:
number
}
}
export
interface
CouponListResult
{
export
interface
CouponListResult
{
items
:
CouponItem
[]
list
:
CouponItem
[]
total
?:
number
total
?:
number
}
}
type
ApiRecord
=
Record
<
string
,
unknown
>
function
record
(
value
:
unknown
):
ApiRecord
{
return
value
!==
null
&&
typeof
value
===
'object'
&&
!
Array
.
isArray
(
value
)
?
(
value
as
ApiRecord
)
:
{}
}
function
text
(
value
:
unknown
)
{
return
value
===
null
||
value
===
undefined
?
''
:
String
(
value
)
}
function
toCoupon
(
value
:
unknown
):
CouponItem
{
const
data
=
record
(
value
)
return
{
id
:
text
(
data
.
coupon_rec_id
),
name
:
text
(
data
.
coupon_name
),
threshold
:
text
(
data
.
norm_price
),
discount
:
text
(
data
.
reduced_price
),
startTime
:
text
(
data
.
start_time
),
endTime
:
text
(
data
.
end_time
),
useStatus
:
Number
(
data
.
use_status
)
||
0
,
}
}
export
async
function
getCoupons
(
params
:
CouponListParams
):
Promise
<
CouponListResult
>
{
export
async
function
getCoupons
(
params
:
CouponListParams
):
Promise
<
CouponListResult
>
{
const
response
=
await
http
.
post
(
'/api/app/v1/coupon/Coupon/getCouponRecordListSAll'
,
{
return
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
===
0
?
2
:
1
,
})
})
const
data
=
unwrapApiData
<
unknown
>
(
response
)
const
payload
=
record
(
data
)
const
source
=
Array
.
isArray
(
payload
.
list
)
?
payload
.
list
:
[]
return
{
items
:
source
.
map
(
toCoupon
),
total
:
Number
.
isFinite
(
Number
(
payload
.
total
))
?
Number
(
payload
.
total
)
:
undefined
,
}
}
}
apps/learning/src/modules/my/coupons/components/CouponList.tsx
浏览文件 @
8d6dd989
...
@@ -6,14 +6,14 @@ export function CouponList({ items, status }: { items: CouponItem[]; status: num
...
@@ -6,14 +6,14 @@ export function CouponList({ items, status }: { items: CouponItem[]; status: num
<
Space
orientation=
'vertical'
size=
'middle'
style=
{
{
width
:
'100%'
}
}
>
<
Space
orientation=
'vertical'
size=
'middle'
style=
{
{
width
:
'100%'
}
}
>
{
items
.
map
((
coupon
)
=>
(
{
items
.
map
((
coupon
)
=>
(
<
Card
<
Card
key=
{
coupon
.
id
}
key=
{
coupon
.
coupon_rec_
id
}
size=
'small'
size=
'small'
style=
{
{
borderLeft
:
`4px solid ${status === 0 ? '#1677ff' : '#bfbfbf'}`
}
}
style=
{
{
borderLeft
:
`4px solid ${status === 0 ? '#1677ff' : '#bfbfbf'}`
}
}
>
>
<
Space
orientation=
'vertical'
size=
{
4
}
style=
{
{
width
:
'100%'
}
}
>
<
Space
orientation=
'vertical'
size=
{
4
}
style=
{
{
width
:
'100%'
}
}
>
<
Space
style=
{
{
justifyContent
:
'space-between'
,
width
:
'100%'
}
}
>
<
Space
style=
{
{
justifyContent
:
'space-between'
,
width
:
'100%'
}
}
>
<
Typography
.
Title
level=
{
4
}
style=
{
{
margin
:
0
}
}
>
<
Typography
.
Title
level=
{
4
}
style=
{
{
margin
:
0
}
}
>
{
coupon
.
name
||
'优惠券'
}
{
coupon
.
coupon_
name
||
'优惠券'
}
</
Typography
.
Title
>
</
Typography
.
Title
>
<
Tag
color=
{
status
===
0
?
'blue'
:
'default'
}
>
<
Tag
color=
{
status
===
0
?
'blue'
:
'default'
}
>
{
status
===
0
?
'可使用'
:
'已使用/已失效'
}
{
status
===
0
?
'可使用'
:
'已使用/已失效'
}
...
@@ -21,12 +21,12 @@ export function CouponList({ items, status }: { items: CouponItem[]; status: num
...
@@ -21,12 +21,12 @@ export function CouponList({ items, status }: { items: CouponItem[]; status: num
</
Space
>
</
Space
>
<
Typography
.
Text
>
<
Typography
.
Text
>
<
Typography
.
Text
strong
type=
'danger'
style=
{
{
fontSize
:
22
}
}
>
<
Typography
.
Text
strong
type=
'danger'
style=
{
{
fontSize
:
22
}
}
>
¥
{
coupon
.
discount
||
'0.00'
}
¥
{
coupon
.
reduced_price
||
'0.00'
}
</
Typography
.
Text
>
</
Typography
.
Text
>
满
{
coupon
.
threshold
||
'0'
}
元可用
满
{
coupon
.
norm_price
||
'0'
}
元可用
</
Typography
.
Text
>
</
Typography
.
Text
>
<
Typography
.
Text
type=
'secondary'
>
<
Typography
.
Text
type=
'secondary'
>
有效期
:
{
coupon
.
startTime
||
'—'
}
至
{
coupon
.
endT
ime
||
'—'
}
有效期
至:
{
coupon
.
end_t
ime
||
'—'
}
</
Typography
.
Text
>
</
Typography
.
Text
>
</
Space
>
</
Space
>
</
Card
>
</
Card
>
...
...
apps/learning/src/modules/my/coupons/views/MyCouponsView.tsx
浏览文件 @
8d6dd989
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
Alert
,
Button
,
Empty
,
Pagination
,
Segmented
,
Skeleton
,
Space
,
Typography
}
from
'antd'
import
{
Empty
,
Pagination
,
Segmented
,
Skeleton
,
Space
,
Typography
}
from
'antd'
import
type
{
CouponStatus
}
from
'../api'
import
type
{
CouponStatus
}
from
'../api'
import
{
CouponList
}
from
'../components/CouponList'
import
{
CouponList
}
from
'../components/CouponList'
import
{
couponsOptions
}
from
'../query'
import
{
couponsOptions
}
from
'../query'
...
@@ -12,7 +12,7 @@ function totalFor(page: number, pageSize: number, count: number, total?: number)
...
@@ -12,7 +12,7 @@ function totalFor(page: number, pageSize: number, count: number, total?: number)
export
function
MyCouponsView
()
{
export
function
MyCouponsView
()
{
const
queryState
=
useCouponsQueryState
()
const
queryState
=
useCouponsQueryState
()
const
couponsQuery
=
useQuery
(
couponsOptions
(
queryState
.
state
))
const
couponsQuery
=
useQuery
(
couponsOptions
(
queryState
.
state
))
const
items
=
couponsQuery
.
data
?.
items
??
[]
const
items
=
couponsQuery
.
data
?.
list
??
[]
return
(
return
(
<
main
>
<
main
>
...
@@ -33,15 +33,6 @@ export function MyCouponsView() {
...
@@ -33,15 +33,6 @@ export function MyCouponsView() {
onChange=
{
(
status
)
=>
void
queryState
.
setStatus
(
status
)
}
onChange=
{
(
status
)
=>
void
queryState
.
setStatus
(
status
)
}
/
>
/
>
{
couponsQuery
.
isPending
?
<
Skeleton
active
paragraph=
{
{
rows
:
8
}
}
/>
:
null
}
{
couponsQuery
.
isPending
?
<
Skeleton
active
paragraph=
{
{
rows
:
8
}
}
/>
:
null
}
{
couponsQuery
.
isError
?
(
<
Alert
type=
'error'
showIcon
title=
'优惠券加载失败'
description=
{
couponsQuery
.
error
.
message
}
action=
{
<
Button
onClick=
{
()
=>
void
couponsQuery
.
refetch
()
}
>
重试
</
Button
>
}
/>
)
:
null
}
{
couponsQuery
.
data
&&
items
.
length
===
0
?
<
Empty
description=
'暂无优惠券'
/>
:
null
}
{
couponsQuery
.
data
&&
items
.
length
===
0
?
<
Empty
description=
'暂无优惠券'
/>
:
null
}
{
items
.
length
?
(
{
items
.
length
?
(
<>
<>
...
...
apps/learning/src/modules/my/discussions/api.ts
浏览文件 @
8d6dd989
import
{
unwrapApiData
}
from
'@/api/unwrap'
import
{
post
}
from
'@/utils/http'
import
{
http
}
from
'@/utils/http'
import
type
{
MyListParams
}
from
'../api'
import
type
{
MyListParams
}
from
'../api'
export
interface
DiscussedBook
{
export
interface
DiscussedBook
{
...
@@ -28,11 +27,12 @@ function text(value: unknown) {
...
@@ -28,11 +27,12 @@ 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
(
'/api/app/v1/members/Information/myComment'
,
{
const
data
=
record
(
await
post
(
'/api/app/v1/members/Information/myComment'
,
{
page
:
params
.
page
,
page
:
params
.
page
,
page_size
:
params
.
pageSize
,
page_size
:
params
.
pageSize
,
})
}),
const
data
=
record
(
unwrapApiData
(
response
)
)
)
const
source
=
Array
.
isArray
(
data
.
list
)
?
data
.
list
:
[]
const
source
=
Array
.
isArray
(
data
.
list
)
?
data
.
list
:
[]
return
{
return
{
...
...
apps/learning/src/modules/my/discussions/views/MyDiscussionsView.tsx
浏览文件 @
8d6dd989
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
import
{
Empty
,
Image
,
List
,
Pagination
,
Statistic
,
Skeleton
,
Typography
}
from
'antd'
Alert
,
Button
,
Empty
,
Image
,
List
,
Pagination
,
Statistic
,
Skeleton
,
Typography
,
}
from
'antd'
import
{
Link
}
from
'react-router'
import
{
Link
}
from
'react-router'
import
{
useMyListQueryState
}
from
'../../query-state'
import
{
useMyListQueryState
}
from
'../../query-state'
import
{
discussedBooksOptions
}
from
'../query'
import
{
discussedBooksOptions
}
from
'../query'
...
@@ -28,15 +18,6 @@ export function MyDiscussionsView() {
...
@@ -28,15 +18,6 @@ export function MyDiscussionsView() {
<
Typography
.
Title
level=
{
1
}
>
我的评论
</
Typography
.
Title
>
<
Typography
.
Title
level=
{
1
}
>
我的评论
</
Typography
.
Title
>
<
Typography
.
Paragraph
type=
'secondary'
>
按图书汇总你发表主题和回复。
</
Typography
.
Paragraph
>
<
Typography
.
Paragraph
type=
'secondary'
>
按图书汇总你发表主题和回复。
</
Typography
.
Paragraph
>
{
discussionsQuery
.
isPending
?
<
Skeleton
active
paragraph=
{
{
rows
:
10
}
}
/>
:
null
}
{
discussionsQuery
.
isPending
?
<
Skeleton
active
paragraph=
{
{
rows
:
10
}
}
/>
:
null
}
{
discussionsQuery
.
isError
?
(
<
Alert
type=
'error'
showIcon
title=
'讨论加载失败'
description=
{
discussionsQuery
.
error
.
message
}
action=
{
<
Button
onClick=
{
()
=>
void
discussionsQuery
.
refetch
()
}
>
重试
</
Button
>
}
/>
)
:
null
}
{
discussionsQuery
.
data
&&
books
.
length
===
0
?
<
Empty
description=
'暂无讨论'
/>
:
null
}
{
discussionsQuery
.
data
&&
books
.
length
===
0
?
<
Empty
description=
'暂无讨论'
/>
:
null
}
{
books
.
length
?
(
{
books
.
length
?
(
<>
<>
...
...
apps/learning/src/modules/my/favorites/api.ts
浏览文件 @
8d6dd989
import
{
unwrapApiData
}
from
'@/api/unwrap'
import
{
post
}
from
'@/utils/http'
import
{
http
}
from
'@/utils/http'
import
type
{
MyListParams
}
from
'../api'
import
type
{
MyListParams
}
from
'../api'
export
interface
FavoriteBook
{
export
interface
FavoriteBook
{
...
@@ -34,11 +33,12 @@ function number(value: unknown) {
...
@@ -34,11 +33,12 @@ 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
(
'/api/app/v1/members/Information/myCollection'
,
{
const
data
=
record
(
await
post
(
'/api/app/v1/members/Information/myCollection'
,
{
page
:
params
.
page
,
page
:
params
.
page
,
page_size
:
params
.
pageSize
,
page_size
:
params
.
pageSize
,
})
}),
const
data
=
record
(
unwrapApiData
(
response
)
)
)
const
source
=
Array
.
isArray
(
data
.
list
)
?
data
.
list
:
[]
const
source
=
Array
.
isArray
(
data
.
list
)
?
data
.
list
:
[]
return
{
return
{
...
@@ -58,11 +58,10 @@ export async function getFavorites(params: MyListParams): Promise<FavoriteList>
...
@@ -58,11 +58,10 @@ 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
(
'/api/app/v1/book/Information/collection'
,
{
const
data
=
await
post
<
unknown
>
(
'/api/app/v1/book/Information/collection'
,
{
book_id
:
bookId
,
book_id
:
bookId
,
is_select
:
'0'
,
is_select
:
'0'
,
})
})
const
data
=
unwrapApiData
<
unknown
>
(
response
)
const
payload
=
record
(
data
)
const
payload
=
record
(
data
)
return
(
return
(
data
===
1
||
data
===
1
||
...
...
apps/learning/src/modules/my/favorites/views/MyFavoritesView.tsx
浏览文件 @
8d6dd989
import
{
useMutation
,
useQuery
,
useQueryClient
}
from
'@tanstack/react-query'
import
{
useMutation
,
useQuery
,
useQueryClient
}
from
'@tanstack/react-query'
import
{
import
{
Alert
,
Button
,
Button
,
Empty
,
Empty
,
Image
,
Image
,
...
@@ -36,23 +35,6 @@ export function MyFavoritesView() {
...
@@ -36,23 +35,6 @@ export function MyFavoritesView() {
<
main
>
<
main
>
<
Typography
.
Title
level=
{
1
}
>
我的收藏
</
Typography
.
Title
>
<
Typography
.
Title
level=
{
1
}
>
我的收藏
</
Typography
.
Title
>
{
favoritesQuery
.
isPending
?
<
Skeleton
active
paragraph=
{
{
rows
:
10
}
}
/>
:
null
}
{
favoritesQuery
.
isPending
?
<
Skeleton
active
paragraph=
{
{
rows
:
10
}
}
/>
:
null
}
{
favoritesQuery
.
isError
?
(
<
Alert
type=
'error'
showIcon
title=
'收藏加载失败'
description=
{
favoritesQuery
.
error
.
message
}
action=
{
<
Button
onClick=
{
()
=>
void
favoritesQuery
.
refetch
()
}
>
重试
</
Button
>
}
/>
)
:
null
}
{
removeMutation
.
isError
?
(
<
Alert
type=
'error'
showIcon
title=
'取消收藏失败'
description=
{
removeMutation
.
error
.
message
}
/>
)
:
null
}
{
favoritesQuery
.
data
&&
favorites
.
length
===
0
?
<
Empty
description=
'暂无收藏'
/>
:
null
}
{
favoritesQuery
.
data
&&
favorites
.
length
===
0
?
<
Empty
description=
'暂无收藏'
/>
:
null
}
{
favorites
.
length
?
(
{
favorites
.
length
?
(
<>
<>
...
...
apps/learning/src/modules/my/feedback/api.ts
浏览文件 @
8d6dd989
import
{
unwrapApiData
}
from
'@/api/unwrap'
import
{
post
}
from
'@/utils/http'
import
type
{
ApiEnvelope
}
from
'@/api/types'
import
{
http
}
from
'@/utils/http'
export
async
function
submitFeedback
(
proposal
:
string
)
{
export
async
function
submitFeedback
(
proposal
:
string
)
{
const
data
=
unwrapApiData
(
const
data
=
await
post
<
Record
<
string
,
unknown
>>
(
'/api/app/v1/members/Information/proposal'
,
{
await
http
.
post
<
ApiEnvelope
<
Record
<
string
,
unknown
>>>
(
'/api/app/v1/members/Information/proposal'
,
{
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
(
'反馈提交失败'
)
}
}
...
...
apps/learning/src/modules/my/feedback/views/FeedbackView.tsx
浏览文件 @
8d6dd989
import
{
useMutation
}
from
'@tanstack/react-query'
import
{
useMutation
}
from
'@tanstack/react-query'
import
{
Alert
,
Button
,
Card
,
Form
,
Input
,
Typography
,
message
}
from
'antd'
import
{
Button
,
Card
,
Form
,
Input
,
Typography
,
message
}
from
'antd'
import
{
useNavigate
}
from
'react-router'
import
{
useNavigate
}
from
'react-router'
import
{
submitFeedback
}
from
'../api'
import
{
submitFeedback
}
from
'../api'
...
@@ -21,15 +21,6 @@ export function FeedbackView() {
...
@@ -21,15 +21,6 @@ export function FeedbackView() {
return
(
return
(
<
main
>
<
main
>
<
Typography
.
Title
level=
{
1
}
>
意见反馈
</
Typography
.
Title
>
<
Typography
.
Title
level=
{
1
}
>
意见反馈
</
Typography
.
Title
>
{
mutation
.
isError
?
(
<
Alert
style=
{
{
marginBottom
:
16
}
}
type=
'error'
showIcon
title=
'反馈提交失败'
description=
{
mutation
.
error
.
message
}
/>
)
:
null
}
<
Card
style=
{
{
maxWidth
:
680
}
}
>
<
Card
style=
{
{
maxWidth
:
680
}
}
>
<
Form
<
Form
form=
{
form
}
form=
{
form
}
...
...
apps/learning/src/modules/my/history/api.ts
浏览文件 @
8d6dd989
import
{
unwrapApiData
}
from
'@/api/unwrap'
import
{
post
}
from
'@/utils/http'
import
{
http
}
from
'@/utils/http'
import
type
{
MyListParams
}
from
'../api'
import
type
{
MyListParams
}
from
'../api'
export
interface
ReadRecordBook
{
export
interface
ReadRecordBook
{
...
@@ -34,11 +33,12 @@ function list(value: unknown) {
...
@@ -34,11 +33,12 @@ 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
(
'/api/app/v1/members/Information/myReadRecord'
,
{
const
data
=
record
(
await
post
(
'/api/app/v1/members/Information/myReadRecord'
,
{
page
:
params
.
page
,
page
:
params
.
page
,
page_size
:
params
.
pageSize
,
page_size
:
params
.
pageSize
,
})
}),
const
data
=
record
(
unwrapApiData
(
response
)
)
)
return
list
(
data
.
list
).
map
((
group
)
=>
{
return
list
(
data
.
list
).
map
((
group
)
=>
{
const
item
=
record
(
group
)
const
item
=
record
(
group
)
...
...
apps/learning/src/modules/my/history/views/MyHistoryView.tsx
浏览文件 @
8d6dd989
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
Alert
,
Button
,
Empty
,
Image
,
List
,
Skeleton
,
Space
,
Tag
,
Typography
}
from
'antd'
import
{
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'
...
@@ -13,15 +13,6 @@ export function MyHistoryView() {
...
@@ -13,15 +13,6 @@ export function MyHistoryView() {
<
main
>
<
main
>
<
Typography
.
Title
level=
{
1
}
>
学习记录
</
Typography
.
Title
>
<
Typography
.
Title
level=
{
1
}
>
学习记录
</
Typography
.
Title
>
{
recordsQuery
.
isPending
?
<
Skeleton
active
paragraph=
{
{
rows
:
12
}
}
/>
:
null
}
{
recordsQuery
.
isPending
?
<
Skeleton
active
paragraph=
{
{
rows
:
12
}
}
/>
:
null
}
{
recordsQuery
.
isError
?
(
<
Alert
type=
'error'
showIcon
title=
'学习记录加载失败'
description=
{
recordsQuery
.
error
.
message
}
action=
{
<
Button
onClick=
{
()
=>
void
recordsQuery
.
refetch
()
}
>
重试
</
Button
>
}
/>
)
:
null
}
{
recordsQuery
.
data
&&
groups
.
length
===
0
?
<
Empty
description=
'暂无学习记录'
/>
:
null
}
{
recordsQuery
.
data
&&
groups
.
length
===
0
?
<
Empty
description=
'暂无学习记录'
/>
:
null
}
<
Space
orientation=
'vertical'
size=
{
20
}
style=
{
{
display
:
'flex'
}
}
>
<
Space
orientation=
'vertical'
size=
{
20
}
style=
{
{
display
:
'flex'
}
}
>
{
groups
.
map
((
group
)
=>
(
{
groups
.
map
((
group
)
=>
(
...
...
apps/learning/src/modules/my/messages/api.ts
浏览文件 @
8d6dd989
import
{
unwrapApiData
}
from
'@/api/unwrap'
import
{
post
}
from
'@/utils/http'
import
{
http
}
from
'@/utils/http'
export
interface
MessageItem
{
export
interface
MessageItem
{
id
:
string
id
:
string
...
@@ -62,11 +61,10 @@ function toMessage(value: unknown): MessageItem {
...
@@ -62,11 +61,10 @@ 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
(
'/api/app/v1/message/Message/getList'
,
{
const
data
=
await
post
<
unknown
>
(
'/api/app/v1/message/Message/getList'
,
{
page
:
params
.
page
,
page
:
params
.
page
,
page_size
:
params
.
pageSize
,
page_size
:
params
.
pageSize
,
})
})
const
data
=
unwrapApiData
<
unknown
>
(
response
)
const
payload
=
record
(
data
)
const
payload
=
record
(
data
)
return
{
return
{
items
:
list
(
data
).
map
(
toMessage
),
items
:
list
(
data
).
map
(
toMessage
),
...
@@ -75,12 +73,10 @@ export async function getMessages(params: MessageListParams): Promise<MessageLis
...
@@ -75,12 +73,10 @@ export async function getMessages(params: MessageListParams): Promise<MessageLis
}
}
export
async
function
getUnreadMessageCount
()
{
export
async
function
getUnreadMessageCount
()
{
const
response
=
await
http
.
post
(
'/api/app/v1/message/Message/getMessageNums'
,
{})
return
number
(
record
(
await
post
(
'/api/app/v1/message/Message/getMessageNums'
)).
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
(
'/api/app/v1/message/Message/editOneStatus'
,
{
id
})
const
data
=
record
(
await
post
(
'/api/app/v1/message/Message/editOneStatus'
,
{
id
}))
const
data
=
record
(
unwrapApiData
<
unknown
>
(
response
))
return
data
.
is_success
===
1
||
data
.
is_success
===
'1'
||
data
.
is_success
===
true
return
data
.
is_success
===
1
||
data
.
is_success
===
'1'
||
data
.
is_success
===
true
}
}
apps/learning/src/modules/my/messages/views/MyMessagesView.tsx
浏览文件 @
8d6dd989
import
{
useMutation
,
useQuery
,
useQueryClient
}
from
'@tanstack/react-query'
import
{
useMutation
,
useQuery
,
useQueryClient
}
from
'@tanstack/react-query'
import
{
Alert
,
Button
,
Empty
,
Pagination
,
Skeleton
,
Space
,
Typography
}
from
'antd'
import
{
Empty
,
Pagination
,
Skeleton
,
Space
,
Typography
}
from
'antd'
import
{
useNavigate
}
from
'react-router'
import
{
useNavigate
}
from
'react-router'
import
{
markMessageRead
,
type
MessageItem
}
from
'../api'
import
{
markMessageRead
,
type
MessageItem
}
from
'../api'
import
{
MessageList
}
from
'../components/MessageList'
import
{
MessageList
}
from
'../components/MessageList'
...
@@ -57,18 +57,6 @@ export function MyMessagesView() {
...
@@ -57,18 +57,6 @@ export function MyMessagesView() {
</
Typography
.
Text
>
</
Typography
.
Text
>
</
Space
>
</
Space
>
{
messagesQuery
.
isPending
?
<
Skeleton
active
paragraph=
{
{
rows
:
10
}
}
/>
:
null
}
{
messagesQuery
.
isPending
?
<
Skeleton
active
paragraph=
{
{
rows
:
10
}
}
/>
:
null
}
{
messagesQuery
.
isError
?
(
<
Alert
type=
'error'
showIcon
title=
'消息加载失败'
description=
{
messagesQuery
.
error
.
message
}
action=
{
<
Button
onClick=
{
()
=>
void
messagesQuery
.
refetch
()
}
>
重试
</
Button
>
}
/>
)
:
null
}
{
readMutation
.
isError
?
(
<
Alert
type=
'error'
showIcon
title=
'操作失败'
description=
{
readMutation
.
error
.
message
}
/>
)
:
null
}
{
messagesQuery
.
data
&&
items
.
length
===
0
?
<
Empty
description=
'暂无消息'
/>
:
null
}
{
messagesQuery
.
data
&&
items
.
length
===
0
?
<
Empty
description=
'暂无消息'
/>
:
null
}
{
items
.
length
?
(
{
items
.
length
?
(
<>
<>
...
...
apps/learning/src/modules/my/notes/api.ts
浏览文件 @
8d6dd989
import
{
unwrapApiData
}
from
'@/api/unwrap'
import
{
post
}
from
'@/utils/http'
import
{
http
}
from
'@/utils/http'
import
type
{
MyListParams
}
from
'../api'
import
type
{
MyListParams
}
from
'../api'
export
interface
NotedBook
{
export
interface
NotedBook
{
...
@@ -36,11 +35,12 @@ function number(value: unknown) {
...
@@ -36,11 +35,12 @@ 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
(
'/api/app/v1/members/Information/myNotes'
,
{
const
data
=
record
(
await
post
(
'/api/app/v1/members/Information/myNotes'
,
{
page
:
params
.
page
,
page
:
params
.
page
,
page_size
:
params
.
pageSize
,
page_size
:
params
.
pageSize
,
})
}),
const
data
=
record
(
unwrapApiData
(
response
)
)
)
const
source
=
Array
.
isArray
(
data
.
list
)
?
data
.
list
:
[]
const
source
=
Array
.
isArray
(
data
.
list
)
?
data
.
list
:
[]
return
{
return
{
...
...
apps/learning/src/modules/my/notes/views/MyNotesView.tsx
浏览文件 @
8d6dd989
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
import
{
Empty
,
Image
,
List
,
Pagination
,
Space
,
Statistic
,
Skeleton
,
Typography
}
from
'antd'
Alert
,
Button
,
Empty
,
Image
,
List
,
Pagination
,
Space
,
Statistic
,
Skeleton
,
Typography
,
}
from
'antd'
import
{
Link
}
from
'react-router'
import
{
Link
}
from
'react-router'
import
{
useMyListQueryState
}
from
'../../query-state'
import
{
useMyListQueryState
}
from
'../../query-state'
import
{
notedBooksOptions
}
from
'../query'
import
{
notedBooksOptions
}
from
'../query'
...
@@ -29,15 +18,6 @@ export function MyNotesView() {
...
@@ -29,15 +18,6 @@ export function MyNotesView() {
<
Typography
.
Title
level=
{
1
}
>
我的笔记
</
Typography
.
Title
>
<
Typography
.
Title
level=
{
1
}
>
我的笔记
</
Typography
.
Title
>
<
Typography
.
Paragraph
type=
'secondary'
>
按书籍汇总笔记、高亮和划线。
</
Typography
.
Paragraph
>
<
Typography
.
Paragraph
type=
'secondary'
>
按书籍汇总笔记、高亮和划线。
</
Typography
.
Paragraph
>
{
notesQuery
.
isPending
?
<
Skeleton
active
paragraph=
{
{
rows
:
10
}
}
/>
:
null
}
{
notesQuery
.
isPending
?
<
Skeleton
active
paragraph=
{
{
rows
:
10
}
}
/>
:
null
}
{
notesQuery
.
isError
?
(
<
Alert
type=
'error'
showIcon
title=
'笔记加载失败'
description=
{
notesQuery
.
error
.
message
}
action=
{
<
Button
onClick=
{
()
=>
void
notesQuery
.
refetch
()
}
>
重试
</
Button
>
}
/>
)
:
null
}
{
notesQuery
.
data
&&
books
.
length
===
0
?
<
Empty
description=
'暂无笔记'
/>
:
null
}
{
notesQuery
.
data
&&
books
.
length
===
0
?
<
Empty
description=
'暂无笔记'
/>
:
null
}
{
books
.
length
?
(
{
books
.
length
?
(
<>
<>
...
...
apps/learning/src/modules/my/orders/api.ts
浏览文件 @
8d6dd989
import
{
unwrapApiData
}
from
'@/api/unwrap'
import
{
post
}
from
'@/utils/http'
import
{
http
}
from
'@/utils/http'
export
type
OrderStatus
=
0
|
1
|
2
|
3
|
4
export
type
OrderStatus
=
0
|
1
|
2
|
3
|
4
...
@@ -115,8 +114,7 @@ export async function getOrders(params: OrderListParams): Promise<OrderListResul
...
@@ -115,8 +114,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
(
'/api/app/v1/orders/Orders/getOrdersList'
,
request
)
const
data
=
await
post
<
unknown
>
(
'/api/app/v1/orders/Orders/getOrdersList'
,
request
)
const
data
=
unwrapApiData
<
unknown
>
(
response
)
const
payload
=
record
(
data
)
const
payload
=
record
(
data
)
return
{
return
{
items
:
list
(
data
).
map
(
toOrder
),
items
:
list
(
data
).
map
(
toOrder
),
...
@@ -125,10 +123,11 @@ export async function getOrders(params: OrderListParams): Promise<OrderListResul
...
@@ -125,10 +123,11 @@ 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
(
'/api/app/v1/orders/Orders/getInfoOrdersnum'
,
{
const
data
=
record
(
await
post
(
'/api/app/v1/orders/Orders/getInfoOrdersnum'
,
{
ordersnum
:
orderNumber
,
ordersnum
:
orderNumber
,
})
}),
const
data
=
record
(
unwrapApiData
<
unknown
>
(
response
)
)
)
const
base
=
toOrder
(
data
)
const
base
=
toOrder
(
data
)
return
{
return
{
...
base
,
...
base
,
...
...
apps/learning/src/modules/my/orders/components/OrderList.tsx
浏览文件 @
8d6dd989
...
@@ -47,14 +47,9 @@ export function OrderList({
...
@@ -47,14 +47,9 @@ export function OrderList({
<
Space
wrap
size=
'middle'
style=
{
{
marginBottom
:
10
}
}
>
<
Space
wrap
size=
'middle'
style=
{
{
marginBottom
:
10
}
}
>
{
order
.
books
.
map
((
book
)
=>
(
{
order
.
books
.
map
((
book
)
=>
(
<
Space
key=
{
book
.
recordId
||
book
.
bookId
}
>
<
Space
key=
{
book
.
recordId
||
book
.
bookId
}
>
<
Image
{
book
.
image
?
(
width=
{
38
}
<
Image
width=
{
38
}
height=
{
52
}
src=
{
book
.
image
}
alt=
''
preview=
{
false
}
/>
height=
{
52
}
)
:
null
}
src=
{
book
.
image
}
alt=
''
preview=
{
false
}
fallback=
''
/>
<
Typography
.
Text
>
{
book
.
name
||
'图书'
}
</
Typography
.
Text
>
<
Typography
.
Text
>
{
book
.
name
||
'图书'
}
</
Typography
.
Text
>
</
Space
>
</
Space
>
))
}
))
}
...
...
apps/learning/src/modules/my/orders/views/MyOrdersView.tsx
浏览文件 @
8d6dd989
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
import
{
Empty
,
Input
,
Pagination
,
Segmented
,
Skeleton
,
Space
,
Typography
}
from
'antd'
Alert
,
Button
,
Empty
,
Input
,
Pagination
,
Segmented
,
Skeleton
,
Space
,
Typography
,
}
from
'antd'
import
{
useEffect
,
useState
}
from
'react'
import
{
useEffect
,
useState
}
from
'react'
import
{
useNavigate
}
from
'react-router'
import
{
useNavigate
}
from
'react-router'
import
type
{
OrderStatus
}
from
'../api'
import
type
{
OrderStatus
}
from
'../api'
...
@@ -62,15 +52,6 @@ export function MyOrdersView() {
...
@@ -62,15 +52,6 @@ export function MyOrdersView() {
onChange=
{
(
status
)
=>
void
queryState
.
setStatus
(
status
)
}
onChange=
{
(
status
)
=>
void
queryState
.
setStatus
(
status
)
}
/
>
/
>
{
ordersQuery
.
isPending
?
<
Skeleton
active
paragraph=
{
{
rows
:
10
}
}
/>
:
null
}
{
ordersQuery
.
isPending
?
<
Skeleton
active
paragraph=
{
{
rows
:
10
}
}
/>
:
null
}
{
ordersQuery
.
isError
?
(
<
Alert
type=
'error'
showIcon
title=
'订单加载失败'
description=
{
ordersQuery
.
error
.
message
}
action=
{
<
Button
onClick=
{
()
=>
void
ordersQuery
.
refetch
()
}
>
重试
</
Button
>
}
/>
)
:
null
}
{
ordersQuery
.
data
&&
items
.
length
===
0
?
<
Empty
description=
'暂无订单'
/>
:
null
}
{
ordersQuery
.
data
&&
items
.
length
===
0
?
<
Empty
description=
'暂无订单'
/>
:
null
}
{
items
.
length
?
(
{
items
.
length
?
(
<>
<>
...
...
apps/learning/src/modules/my/orders/views/OrderDetailView.tsx
浏览文件 @
8d6dd989
import
{
use
Query
}
from
'@tanstack/react-query'
import
{
use
Mutation
,
useQuery
,
useQueryClient
}
from
'@tanstack/react-query'
import
{
import
{
Alert
,
Alert
,
Button
,
Button
,
...
@@ -13,7 +13,8 @@ import {
...
@@ -13,7 +13,8 @@ import {
Typography
,
Typography
,
}
from
'antd'
}
from
'antd'
import
{
Link
,
useParams
}
from
'react-router'
import
{
Link
,
useParams
}
from
'react-router'
import
{
orderDetailOptions
}
from
'../query'
import
{
getPayScene
,
payOrderAgain
}
from
'@/modules/purchase/api'
import
{
orderDetailOptions
,
orderKeys
}
from
'../query'
const
statuses
:
Record
<
number
,
{
label
:
string
;
color
:
string
}
>
=
{
const
statuses
:
Record
<
number
,
{
label
:
string
;
color
:
string
}
>
=
{
1
:
{
label
:
'待支付'
,
color
:
'orange'
},
1
:
{
label
:
'待支付'
,
color
:
'orange'
},
...
@@ -24,8 +25,25 @@ const statuses: Record<number, { label: string; color: string }> = {
...
@@ -24,8 +25,25 @@ const statuses: Record<number, { label: string; color: string }> = {
export
function
OrderDetailView
()
{
export
function
OrderDetailView
()
{
const
{
orderNumber
=
''
}
=
useParams
()
const
{
orderNumber
=
''
}
=
useParams
()
const
queryClient
=
useQueryClient
()
const
orderQuery
=
useQuery
(
orderDetailOptions
(
orderNumber
))
const
orderQuery
=
useQuery
(
orderDetailOptions
(
orderNumber
))
const
order
=
orderQuery
.
data
const
order
=
orderQuery
.
data
const
payMutation
=
useMutation
({
mutationFn
:
async
()
=>
{
const
payment
=
await
payOrderAgain
(
orderNumber
,
getPayScene
())
if
(
payment
.
is_moneys
!==
0
&&
!
payment
.
pay_url
)
{
throw
new
Error
(
'未获取到支付宝支付地址'
)
}
return
payment
},
onSuccess
:
(
payment
)
=>
{
if
(
payment
.
is_moneys
===
0
)
{
void
queryClient
.
invalidateQueries
({
queryKey
:
orderKeys
.
detail
(
orderNumber
)
})
return
}
if
(
payment
.
pay_url
)
window
.
location
.
assign
(
payment
.
pay_url
)
},
})
if
(
orderQuery
.
isPending
)
if
(
orderQuery
.
isPending
)
return
(
return
(
...
@@ -33,18 +51,6 @@ export function OrderDetailView() {
...
@@ -33,18 +51,6 @@ export function OrderDetailView() {
<
Skeleton
active
paragraph=
{
{
rows
:
12
}
}
/>
<
Skeleton
active
paragraph=
{
{
rows
:
12
}
}
/>
</
main
>
</
main
>
)
)
if
(
orderQuery
.
isError
)
return
(
<
main
>
<
Alert
type=
'error'
showIcon
title=
'订单详情加载失败'
description=
{
orderQuery
.
error
.
message
}
action=
{
<
Button
onClick=
{
()
=>
void
orderQuery
.
refetch
()
}
>
重试
</
Button
>
}
/>
</
main
>
)
if
(
!
order
)
if
(
!
order
)
return
(
return
(
<
main
>
<
main
>
...
@@ -53,6 +59,7 @@ export function OrderDetailView() {
...
@@ -53,6 +59,7 @@ export function OrderDetailView() {
)
)
const
status
=
statuses
[
order
.
status
]
??
{
label
:
'订单状态未知'
,
color
:
'default'
}
const
status
=
statuses
[
order
.
status
]
??
{
label
:
'订单状态未知'
,
color
:
'default'
}
const
canPayInBrowser
=
order
.
status
===
1
&&
order
.
payType
===
2
return
(
return
(
<
main
>
<
main
>
<
Space
orientation=
'vertical'
size=
'middle'
style=
{
{
width
:
'100%'
}
}
>
<
Space
orientation=
'vertical'
size=
'middle'
style=
{
{
width
:
'100%'
}
}
>
...
@@ -66,8 +73,21 @@ export function OrderDetailView() {
...
@@ -66,8 +73,21 @@ export function OrderDetailView() {
<
Alert
<
Alert
type=
'info'
type=
'info'
showIcon
showIcon
title=
'待支付'
message=
'待支付'
description=
'请使用移动端完成支付。'
description=
{
canPayInBrowser
?
'可以直接使用支付宝网页完成支付。'
:
'请使用 APP 完成支付。'
}
action=
{
canPayInBrowser
?
(
<
Button
type=
'primary'
loading=
{
payMutation
.
isPending
}
onClick=
{
()
=>
payMutation
.
mutate
()
}
>
支付宝支付
</
Button
>
)
:
undefined
}
/>
/>
)
:
null
}
)
:
null
}
<
Card
title=
'订单信息'
>
<
Card
title=
'订单信息'
>
...
...
apps/learning/src/modules/my/overview/views/MyOverviewView.tsx
浏览文件 @
8d6dd989
import
{
A
lert
,
Avatar
,
Button
,
Card
,
Skeleton
,
Statistic
,
Typography
}
from
'antd'
import
{
A
vatar
,
Card
,
Skeleton
,
Statistic
,
Typography
}
from
'antd'
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
useNavigate
}
from
'react-router'
import
{
useNavigate
}
from
'react-router'
import
{
myStatisticsOptions
}
from
'../../query'
import
{
myStatisticsOptions
}
from
'../../query'
...
@@ -31,25 +31,14 @@ export function MyOverviewView() {
...
@@ -31,25 +31,14 @@ export function MyOverviewView() {
return
<
Skeleton
active
paragraph=
{
{
rows
:
10
}
}
/>
return
<
Skeleton
active
paragraph=
{
{
rows
:
10
}
}
/>
}
}
if
(
statisticsQuery
.
isError
)
{
return
(
<
Alert
showIcon
type=
'error'
title=
'个人数据加载失败'
description=
{
statisticsQuery
.
error
.
message
}
action=
{
<
Button
onClick=
{
()
=>
void
statisticsQuery
.
refetch
()
}
>
重试
</
Button
>
}
/>
)
}
const
statistics
=
statisticsQuery
.
data
const
statistics
=
statisticsQuery
.
data
if
(
!
statistics
)
return
null
return
(
return
(
<
main
className=
'my-overview'
>
<
main
className=
'my-overview'
>
<
section
aria
-
label=
'用户信息'
>
<
section
aria
-
label=
'用户信息'
>
<
div
className=
'my-overview__profile'
>
<
div
className=
'my-overview__profile'
>
<
Avatar
size=
{
48
}
src=
{
statistics
.
headImg
}
>
<
Avatar
size=
{
48
}
src=
{
statistics
.
headImg
||
undefined
}
>
{
statistics
.
name
?.
slice
(
0
,
1
)
}
{
statistics
.
name
?.
slice
(
0
,
1
)
}
</
Avatar
>
</
Avatar
>
<
div
className=
'my-overview__profile-copy'
>
<
div
className=
'my-overview__profile-copy'
>
...
...
apps/learning/src/modules/my/profile/api.ts
浏览文件 @
8d6dd989
import
{
unwrapApiData
}
from
'@/api/unwrap'
import
{
post
}
from
'@/utils/http'
import
type
{
ApiEnvelope
}
from
'@/api/types'
import
{
http
}
from
'@/utils/http'
export
interface
MemberProfile
{
export
interface
MemberProfile
{
name
:
string
name
:
string
...
@@ -31,11 +29,7 @@ function isSuccessful(data: ApiRecord) {
...
@@ -31,11 +29,7 @@ function isSuccessful(data: ApiRecord) {
}
}
export
async
function
getMemberProfile
():
Promise
<
MemberProfile
>
{
export
async
function
getMemberProfile
():
Promise
<
MemberProfile
>
{
const
data
=
record
(
const
data
=
record
(
await
post
(
'/api/app/v1/members/Information/getMyStatistics'
))
unwrapApiData
(
await
http
.
post
<
ApiEnvelope
<
unknown
>>
(
'/api/app/v1/members/Information/getMyStatistics'
,
{}),
),
)
return
{
return
{
name
:
text
(
data
.
name
),
name
:
text
(
data
.
name
),
phone
:
text
(
data
.
phone
),
phone
:
text
(
data
.
phone
),
...
@@ -52,13 +46,11 @@ export interface UpdateMemberProfileInput {
...
@@ -52,13 +46,11 @@ export interface UpdateMemberProfileInput {
export
async
function
updateMemberProfile
(
input
:
UpdateMemberProfileInput
)
{
export
async
function
updateMemberProfile
(
input
:
UpdateMemberProfileInput
)
{
const
data
=
record
(
const
data
=
record
(
unwrapApiData
(
await
post
(
'/api/app/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
}
:
{}),
}),
}),
),
)
)
if
(
!
isSuccessful
(
data
))
throw
new
Error
(
'个人资料保存失败'
)
if
(
!
isSuccessful
(
data
))
throw
new
Error
(
'个人资料保存失败'
)
}
}
apps/learning/src/modules/my/profile/avatar-upload.ts
浏览文件 @
8d6dd989
import
{
unwrapApiData
}
from
'@/api/unwrap'
import
{
post
}
from
'@/utils/http'
import
type
{
ApiEnvelope
}
from
'@/api/types'
import
{
http
}
from
'@/utils/http'
const
bucket
=
'zxts-user-file'
const
bucket
=
'zxts-user-file'
const
publicDomain
=
'https://zxts-user-file.zijingebook.com'
const
publicDomain
=
'https://zxts-user-file.zijingebook.com'
...
@@ -19,9 +17,7 @@ function createObjectKey(file: File) {
...
@@ -19,9 +17,7 @@ function createObjectKey(file: File) {
}
}
export
async
function
uploadAvatar
(
file
:
File
)
{
export
async
function
uploadAvatar
(
file
:
File
)
{
const
token
=
unwrapApiData
(
const
token
=
await
post
<
UploadToken
>
(
'/api/app/v1/common/Alioss/getUploadToken'
)
await
http
.
post
<
ApiEnvelope
<
UploadToken
>>
(
'/api/app/v1/common/Alioss/getUploadToken'
,
{}),
)
const
objectKey
=
createObjectKey
(
file
)
const
objectKey
=
createObjectKey
(
file
)
const
{
default
:
OSS
}
=
await
import
(
'ali-oss'
)
const
{
default
:
OSS
}
=
await
import
(
'ali-oss'
)
const
client
=
new
OSS
({
const
client
=
new
OSS
({
...
...
apps/learning/src/modules/my/profile/views/ProfileView.tsx
浏览文件 @
8d6dd989
...
@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react'
...
@@ -2,7 +2,6 @@ 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
{
UploadOutlined
}
from
'@ant-design/icons'
import
{
import
{
Alert
,
Avatar
,
Avatar
,
Button
,
Button
,
Card
,
Card
,
...
@@ -15,7 +14,7 @@ import {
...
@@ -15,7 +14,7 @@ import {
message
,
message
,
}
from
'antd'
}
from
'antd'
import
type
{
UploadProps
}
from
'antd'
import
type
{
UploadProps
}
from
'antd'
import
{
useSessionStore
}
from
'@/
modules/auth
/session'
import
{
useSessionStore
}
from
'@/
stores
/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
{
uploadAvatar
}
from
'../avatar-upload'
...
@@ -69,17 +68,7 @@ export function ProfileView() {
...
@@ -69,17 +68,7 @@ export function ProfileView() {
},
[
form
,
profileQuery
.
data
])
},
[
form
,
profileQuery
.
data
])
if
(
profileQuery
.
isPending
)
return
<
Skeleton
active
paragraph=
{
{
rows
:
8
}
}
/>
if
(
profileQuery
.
isPending
)
return
<
Skeleton
active
paragraph=
{
{
rows
:
8
}
}
/>
if
(
profileQuery
.
isError
)
{
if
(
!
profileQuery
.
data
)
return
null
return
(
<
Alert
type=
'error'
showIcon
title=
'个人资料加载失败'
description=
{
profileQuery
.
error
.
message
}
action=
{
<
Button
onClick=
{
()
=>
void
profileQuery
.
refetch
()
}
>
重试
</
Button
>
}
/>
)
}
const
avatarUploadProps
:
UploadProps
=
{
const
avatarUploadProps
:
UploadProps
=
{
accept
:
'image/jpeg,image/png,image/webp'
,
accept
:
'image/jpeg,image/png,image/webp'
,
...
@@ -104,7 +93,6 @@ export function ProfileView() {
...
@@ -104,7 +93,6 @@ export function ProfileView() {
message
.
success
(
'头像上传成功'
)
message
.
success
(
'头像上传成功'
)
}
catch
(
error
)
{
}
catch
(
error
)
{
options
.
onError
?.(
error
as
Error
)
options
.
onError
?.(
error
as
Error
)
message
.
error
(
error
instanceof
Error
?
error
.
message
:
'头像上传失败'
)
}
finally
{
}
finally
{
setUploading
(
false
)
setUploading
(
false
)
}
}
...
@@ -114,15 +102,6 @@ export function ProfileView() {
...
@@ -114,15 +102,6 @@ export function ProfileView() {
return
(
return
(
<
main
>
<
main
>
<
Typography
.
Title
level=
{
1
}
>
个人资料
</
Typography
.
Title
>
<
Typography
.
Title
level=
{
1
}
>
个人资料
</
Typography
.
Title
>
{
saveMutation
.
isError
?
(
<
Alert
style=
{
{
marginBottom
:
16
}
}
type=
'error'
showIcon
title=
'个人资料保存失败'
description=
{
saveMutation
.
error
.
message
}
/>
)
:
null
}
<
Card
style=
{
{
maxWidth
:
680
}
}
>
<
Card
style=
{
{
maxWidth
:
680
}
}
>
<
Form
<
Form
form=
{
form
}
form=
{
form
}
...
@@ -141,7 +120,7 @@ export function ProfileView() {
...
@@ -141,7 +120,7 @@ export function ProfileView() {
>
>
{
()
=>
(
{
()
=>
(
<
Form
.
Item
label=
'头像'
extra=
'支持 JPG、PNG、WebP,大小不超过 5MB。'
>
<
Form
.
Item
label=
'头像'
extra=
'支持 JPG、PNG、WebP,大小不超过 5MB。'
>
<
Avatar
size=
{
72
}
src=
{
form
.
getFieldValue
(
'headImg'
)
}
>
<
Avatar
size=
{
72
}
src=
{
form
.
getFieldValue
(
'headImg'
)
||
undefined
}
>
{
form
.
getFieldValue
(
'name'
)?.
slice
(
0
,
1
)
}
{
form
.
getFieldValue
(
'name'
)?.
slice
(
0
,
1
)
}
</
Avatar
>
</
Avatar
>
<
Upload
{
...
avatarUploadProps
}
>
<
Upload
{
...
avatarUploadProps
}
>
...
...
apps/learning/src/modules/my/routes.tsx
浏览文件 @
8d6dd989
import
type
{
RouteObject
}
from
'react-router'
import
type
{
RouteObject
}
from
'react-router'
import
{
RequireSession
}
from
'@/
modules/auth
/RequireSession'
import
{
RequireSession
}
from
'@/
router
/RequireSession'
import
{
lazyRoute
}
from
'@/router/lazy-route'
import
{
lazyRoute
}
from
'@/router/lazy-route'
import
{
routes
as
couponRoutes
}
from
'./coupons/routes'
import
{
routes
as
couponRoutes
}
from
'./coupons/routes'
import
{
routes
as
feedbackRoutes
}
from
'./feedback/routes'
import
{
routes
as
feedbackRoutes
}
from
'./feedback/routes'
...
...
apps/learning/src/modules/my/security/api.ts
浏览文件 @
8d6dd989
import
{
unwrapApiData
}
from
'@/api/unwrap'
import
{
getDeviceUuid
}
from
'@/utils/device'
import
type
{
ApiEnvelope
}
from
'@/api/types'
import
{
post
}
from
'@/utils/http'
import
{
getDeviceUuid
}
from
'@/modules/auth/device'
import
{
hashPassword
}
from
'@/utils/password'
import
{
hashMemberPassword
}
from
'@/modules/auth/api'
import
{
http
}
from
'@/utils/http'
type
ApiRecord
=
Record
<
string
,
unknown
>
type
ApiRecord
=
Record
<
string
,
unknown
>
...
@@ -21,7 +19,7 @@ async function successfulPost(
...
@@ -21,7 +19,7 @@ async function successfulPost(
payload
:
Record
<
string
,
unknown
>
,
payload
:
Record
<
string
,
unknown
>
,
failureMessage
:
string
,
failureMessage
:
string
,
)
{
)
{
const
data
=
record
(
unwrapApiData
(
await
http
.
post
<
ApiEnvelope
<
unknown
>>
(
path
,
payload
)
))
const
data
=
record
(
await
post
(
path
,
payload
))
if
(
!
isSuccessful
(
data
))
throw
new
Error
(
failureMessage
)
if
(
!
isSuccessful
(
data
))
throw
new
Error
(
failureMessage
)
}
}
...
@@ -29,8 +27,8 @@ export async function changeMemberPassword(password: string, repassword: string)
...
@@ -29,8 +27,8 @@ export async function changeMemberPassword(password: string, repassword: string)
await
successfulPost
(
await
successfulPost
(
'/api/app/v1/members/Information/changePasswd'
,
'/api/app/v1/members/Information/changePasswd'
,
{
{
password
:
hash
Member
Password
(
password
),
password
:
hashPassword
(
password
),
repassword
:
hash
Member
Password
(
repassword
),
repassword
:
hashPassword
(
repassword
),
},
},
'密码修改失败'
,
'密码修改失败'
,
)
)
...
...
apps/learning/src/modules/my/security/views/ChangePasswordView.tsx
浏览文件 @
8d6dd989
import
{
useMutation
}
from
'@tanstack/react-query'
import
{
useMutation
}
from
'@tanstack/react-query'
import
{
Alert
,
Button
,
Card
,
Form
,
Input
,
Typography
,
message
}
from
'antd'
import
{
Button
,
Card
,
Form
,
Input
,
Typography
,
message
}
from
'antd'
import
{
useNavigate
}
from
'react-router'
import
{
useNavigate
}
from
'react-router'
import
{
useSessionStore
}
from
'@/
modules/auth
/session'
import
{
useSessionStore
}
from
'@/
stores
/session'
import
{
changeMemberPassword
}
from
'../api'
import
{
changeMemberPassword
}
from
'../api'
interface
PasswordValues
{
interface
PasswordValues
{
...
@@ -28,15 +28,6 @@ export function ChangePasswordView() {
...
@@ -28,15 +28,6 @@ export function ChangePasswordView() {
return
(
return
(
<
main
>
<
main
>
<
Typography
.
Title
level=
{
1
}
>
修改密码
</
Typography
.
Title
>
<
Typography
.
Title
level=
{
1
}
>
修改密码
</
Typography
.
Title
>
{
mutation
.
isError
?
(
<
Alert
style=
{
{
marginBottom
:
16
}
}
type=
'error'
showIcon
title=
'密码修改失败'
description=
{
mutation
.
error
.
message
}
/>
)
:
null
}
<
Card
style=
{
{
maxWidth
:
520
}
}
>
<
Card
style=
{
{
maxWidth
:
520
}
}
>
<
Form
<
Form
form=
{
form
}
form=
{
form
}
...
...
apps/learning/src/modules/my/security/views/ChangePhoneView.tsx
浏览文件 @
8d6dd989
import
{
useEffect
,
useState
}
from
'react'
import
{
useEffect
,
useState
}
from
'react'
import
{
useMutation
,
useQueryClient
}
from
'@tanstack/react-query'
import
{
useMutation
,
useQueryClient
}
from
'@tanstack/react-query'
import
{
Alert
,
Button
,
Card
,
Form
,
Input
,
Space
,
Typography
,
message
}
from
'antd'
import
{
Button
,
Card
,
Form
,
Input
,
Space
,
Typography
,
message
}
from
'antd'
import
{
useNavigate
}
from
'react-router'
import
{
useNavigate
}
from
'react-router'
import
{
profileKeys
}
from
'../../profile/query'
import
{
profileKeys
}
from
'../../profile/query'
import
{
changeMemberPhone
,
sendChangePhoneCode
}
from
'../api'
import
{
changeMemberPhone
,
sendChangePhoneCode
}
from
'../api'
...
@@ -49,24 +49,6 @@ export function ChangePhoneView() {
...
@@ -49,24 +49,6 @@ export function ChangePhoneView() {
return
(
return
(
<
main
>
<
main
>
<
Typography
.
Title
level=
{
1
}
>
修改手机号
</
Typography
.
Title
>
<
Typography
.
Title
level=
{
1
}
>
修改手机号
</
Typography
.
Title
>
{
sendMutation
.
isError
?
(
<
Alert
style=
{
{
marginBottom
:
16
}
}
type=
'error'
showIcon
title=
'验证码发送失败'
description=
{
sendMutation
.
error
.
message
}
/>
)
:
null
}
{
changeMutation
.
isError
?
(
<
Alert
style=
{
{
marginBottom
:
16
}
}
type=
'error'
showIcon
title=
'手机号修改失败'
description=
{
changeMutation
.
error
.
message
}
/>
)
:
null
}
<
Card
style=
{
{
maxWidth
:
520
}
}
>
<
Card
style=
{
{
maxWidth
:
520
}
}
>
<
Form
<
Form
form=
{
form
}
form=
{
form
}
...
...
apps/learning/src/modules/my/security/views/SecurityView.tsx
浏览文件 @
8d6dd989
import
{
useState
}
from
'react'
import
{
useState
}
from
'react'
import
{
useMutation
,
useQuery
}
from
'@tanstack/react-query'
import
{
useMutation
,
useQuery
}
from
'@tanstack/react-query'
import
{
Alert
,
Button
,
Card
,
Descriptions
,
Modal
,
Space
,
Typography
,
message
}
from
'antd'
import
{
Button
,
Card
,
Descriptions
,
Modal
,
Space
,
Typography
,
message
}
from
'antd'
import
{
useNavigate
}
from
'react-router'
import
{
useNavigate
}
from
'react-router'
import
{
useSessionStore
}
from
'@/
modules/auth
/session'
import
{
useSessionStore
}
from
'@/
stores
/session'
import
{
getMemberProfile
}
from
'../../profile/api'
import
{
getMemberProfile
}
from
'../../profile/api'
import
{
profileKeys
}
from
'../../profile/query'
import
{
profileKeys
}
from
'../../profile/query'
import
{
deleteMemberAccount
}
from
'../api'
import
{
deleteMemberAccount
}
from
'../api'
...
@@ -24,25 +24,6 @@ export function SecurityView() {
...
@@ -24,25 +24,6 @@ export function SecurityView() {
return
(
return
(
<
main
>
<
main
>
<
Typography
.
Title
level=
{
1
}
>
账号安全
</
Typography
.
Title
>
<
Typography
.
Title
level=
{
1
}
>
账号安全
</
Typography
.
Title
>
{
profileQuery
.
isError
?
(
<
Alert
style=
{
{
marginBottom
:
16
}
}
type=
'error'
showIcon
title=
'账号信息加载失败'
description=
{
profileQuery
.
error
.
message
}
action=
{
<
Button
onClick=
{
()
=>
void
profileQuery
.
refetch
()
}
>
重试
</
Button
>
}
/>
)
:
null
}
{
deleteMutation
.
isError
?
(
<
Alert
style=
{
{
marginBottom
:
16
}
}
type=
'error'
showIcon
title=
'账号注销失败'
description=
{
deleteMutation
.
error
.
message
}
/>
)
:
null
}
<
Card
<
Card
style=
{
{
maxWidth
:
680
,
marginBottom
:
16
}
}
style=
{
{
maxWidth
:
680
,
marginBottom
:
16
}
}
loading=
{
profileQuery
.
isPending
}
loading=
{
profileQuery
.
isPending
}
...
...
apps/learning/src/modules/my/wallet/api.ts
浏览文件 @
8d6dd989
import
{
unwrapApiData
}
from
'@/api/unwrap'
import
{
post
}
from
'@/utils/http'
import
{
http
}
from
'@/utils/http'
export
type
WalletTab
=
'points'
|
'coins'
export
type
WalletTab
=
'points'
|
'coins'
...
@@ -52,11 +51,10 @@ function source(data: unknown) {
...
@@ -52,11 +51,10 @@ 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
(
'/api/app/v1/members/Information/getIntegralList'
,
{
const
data
=
await
post
<
unknown
>
(
'/api/app/v1/members/Information/getIntegralList'
,
{
page
:
params
.
page
,
page
:
params
.
page
,
page_size
:
params
.
pageSize
,
page_size
:
params
.
pageSize
,
})
})
const
data
=
unwrapApiData
<
unknown
>
(
response
)
const
payload
=
record
(
data
)
const
payload
=
record
(
data
)
return
{
return
{
items
:
source
(
data
).
map
(
toWalletRecord
),
items
:
source
(
data
).
map
(
toWalletRecord
),
...
@@ -65,11 +63,10 @@ export async function getPointRecords(params: WalletListParams): Promise<WalletL
...
@@ -65,11 +63,10 @@ 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
(
'/api/app/v1/members/Information/getBeanList'
,
{
const
data
=
await
post
<
unknown
>
(
'/api/app/v1/members/Information/getBeanList'
,
{
page
:
params
.
page
,
page
:
params
.
page
,
page_size
:
params
.
pageSize
,
page_size
:
params
.
pageSize
,
})
})
const
data
=
unwrapApiData
<
unknown
>
(
response
)
const
payload
=
record
(
data
)
const
payload
=
record
(
data
)
return
{
return
{
items
:
source
(
data
).
map
(
toWalletRecord
),
items
:
source
(
data
).
map
(
toWalletRecord
),
...
...
apps/learning/src/modules/my/wallet/views/MyWalletView.tsx
浏览文件 @
8d6dd989
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
Alert
,
Button
,
Empty
,
Pagination
,
Skeleton
,
Space
,
Tabs
,
Typography
}
from
'antd'
import
{
Empty
,
Pagination
,
Skeleton
,
Space
,
Tabs
,
Typography
}
from
'antd'
import
{
WalletRecordList
}
from
'../components/WalletRecordList'
import
{
WalletRecordList
}
from
'../components/WalletRecordList'
import
{
coinRecordsOptions
,
pointRecordsOptions
}
from
'../query'
import
{
coinRecordsOptions
,
pointRecordsOptions
}
from
'../query'
import
{
useWalletQueryState
}
from
'../query-state'
import
{
useWalletQueryState
}
from
'../query-state'
...
@@ -40,15 +40,6 @@ export function MyWalletView() {
...
@@ -40,15 +40,6 @@ export function MyWalletView() {
]
}
]
}
/>
/>
{
query
.
isPending
?
<
Skeleton
active
paragraph=
{
{
rows
:
10
}
}
/>
:
null
}
{
query
.
isPending
?
<
Skeleton
active
paragraph=
{
{
rows
:
10
}
}
/>
:
null
}
{
query
.
isError
?
(
<
Alert
type=
'error'
showIcon
title=
{
`${kind}流水加载失败`
}
description=
{
query
.
error
.
message
}
action=
{
<
Button
onClick=
{
()
=>
void
query
.
refetch
()
}
>
重试
</
Button
>
}
/>
)
:
null
}
{
query
.
data
&&
records
.
length
===
0
?
<
Empty
description=
{
`暂无${kind}流水`
}
/>
:
null
}
{
query
.
data
&&
records
.
length
===
0
?
<
Empty
description=
{
`暂无${kind}流水`
}
/>
:
null
}
{
records
.
length
?
(
{
records
.
length
?
(
<>
<>
...
...
apps/learning/src/modules/my/wrong-questions/api.ts
浏览文件 @
8d6dd989
import
{
unwrapApiData
}
from
'@/api/unwrap'
import
{
post
}
from
'@/utils/http'
import
{
http
}
from
'@/utils/http'
import
type
{
MyListParams
}
from
'../api'
import
type
{
MyListParams
}
from
'../api'
export
interface
WrongQuestionBook
{
export
interface
WrongQuestionBook
{
...
@@ -28,11 +27,12 @@ function text(value: unknown) {
...
@@ -28,11 +27,12 @@ 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
(
'/api/app/v1/members/Information/myWrongQuestion'
,
{
const
data
=
record
(
await
post
(
'/api/app/v1/members/Information/myWrongQuestion'
,
{
page
:
params
.
page
,
page
:
params
.
page
,
page_size
:
params
.
pageSize
,
page_size
:
params
.
pageSize
,
})
}),
const
data
=
record
(
unwrapApiData
(
response
)
)
)
const
source
=
Array
.
isArray
(
data
.
list
)
?
data
.
list
:
[]
const
source
=
Array
.
isArray
(
data
.
list
)
?
data
.
list
:
[]
return
{
return
{
...
...
apps/learning/src/modules/my/wrong-questions/views/MyWrongQuestionsView.tsx
浏览文件 @
8d6dd989
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
import
{
Empty
,
Image
,
List
,
Pagination
,
Statistic
,
Skeleton
,
Typography
}
from
'antd'
Alert
,
Button
,
Empty
,
Image
,
List
,
Pagination
,
Statistic
,
Skeleton
,
Typography
,
}
from
'antd'
import
{
Link
}
from
'react-router'
import
{
Link
}
from
'react-router'
import
{
useMyListQueryState
}
from
'../../query-state'
import
{
useMyListQueryState
}
from
'../../query-state'
import
{
wrongQuestionBooksOptions
}
from
'../query'
import
{
wrongQuestionBooksOptions
}
from
'../query'
...
@@ -28,15 +18,6 @@ export function MyWrongQuestionsView() {
...
@@ -28,15 +18,6 @@ export function MyWrongQuestionsView() {
<
Typography
.
Title
level=
{
1
}
>
错题本
</
Typography
.
Title
>
<
Typography
.
Title
level=
{
1
}
>
错题本
</
Typography
.
Title
>
<
Typography
.
Paragraph
type=
'secondary'
>
按图书汇总答错的题目。
</
Typography
.
Paragraph
>
<
Typography
.
Paragraph
type=
'secondary'
>
按图书汇总答错的题目。
</
Typography
.
Paragraph
>
{
wrongQuestionsQuery
.
isPending
?
<
Skeleton
active
paragraph=
{
{
rows
:
10
}
}
/>
:
null
}
{
wrongQuestionsQuery
.
isPending
?
<
Skeleton
active
paragraph=
{
{
rows
:
10
}
}
/>
:
null
}
{
wrongQuestionsQuery
.
isError
?
(
<
Alert
type=
'error'
showIcon
title=
'错题加载失败'
description=
{
wrongQuestionsQuery
.
error
.
message
}
action=
{
<
Button
onClick=
{
()
=>
void
wrongQuestionsQuery
.
refetch
()
}
>
重试
</
Button
>
}
/>
)
:
null
}
{
wrongQuestionsQuery
.
data
&&
books
.
length
===
0
?
<
Empty
description=
'暂无错题'
/>
:
null
}
{
wrongQuestionsQuery
.
data
&&
books
.
length
===
0
?
<
Empty
description=
'暂无错题'
/>
:
null
}
{
books
.
length
?
(
{
books
.
length
?
(
<>
<>
...
...
apps/learning/src/modules/purchase/api.ts
0 → 100644
浏览文件 @
8d6dd989
import
{
post
}
from
'@/utils/http'
export
type
PayScene
=
'web'
|
'wap'
export
interface
PaymentOrder
{
ordersnum
:
string
pay_url
?:
string
is_moneys
:
number
pay_type
?:
number
}
export
interface
PaymentStatus
{
ordersnum
:
string
pay_success
:
number
}
export
function
getPayScene
():
PayScene
{
return
typeof
window
!==
'undefined'
&&
window
.
matchMedia
(
'(max-width: 767px)'
).
matches
?
'wap'
:
'web'
}
function
getReturnUrl
()
{
return
typeof
window
===
'undefined'
?
''
:
`
${
window
.
location
.
origin
}
/payment/alipay/return`
}
export
async
function
createBookAlipayOrder
(
input
:
{
bookId
:
string
totalPrice
:
string
payScene
:
PayScene
})
{
return
post
<
PaymentOrder
>
(
'/api/app/v1/orders/Orders/createOrders'
,
{
book_ids_list
:
JSON
.
stringify
([{
book_id
:
input
.
bookId
,
price
:
input
.
totalPrice
,
cart_id
:
0
}]),
total_price
:
input
.
totalPrice
,
coupon_rec_id
:
0
,
integral
:
0
,
type
:
2
,
pay_scene
:
input
.
payScene
,
return_url
:
getReturnUrl
(),
})
}
export
async
function
payOrderAgain
(
orderNumber
:
string
,
payScene
:
PayScene
)
{
return
post
<
PaymentOrder
>
(
'/api/app/v1/orders/Orders/payAgain'
,
{
ordersNum
:
orderNumber
,
pay_scene
:
payScene
,
return_url
:
getReturnUrl
(),
})
}
export
async
function
getPaymentStatus
(
orderNumber
:
string
)
{
return
post
<
PaymentStatus
>
(
'/api/app/v1/orders/Orders/getOrdersStatus'
,
{
ordersnum
:
orderNumber
,
})
}
apps/learning/src/modules/purchase/routes.tsx
0 → 100644
浏览文件 @
8d6dd989
import
type
{
RouteObject
}
from
'react-router'
import
{
RequireSession
}
from
'@/router/RequireSession'
import
{
AlipayReturnView
}
from
'./views/AlipayReturnView'
export
const
routes
:
RouteObject
[]
=
[
{
path
:
'payment/alipay/return'
,
element
:
(
<
RequireSession
>
<
AlipayReturnView
/>
</
RequireSession
>
),
},
]
apps/learning/src/modules/purchase/views/AlipayReturnView.tsx
0 → 100644
浏览文件 @
8d6dd989
import
{
useQuery
,
useQueryClient
}
from
'@tanstack/react-query'
import
{
Button
,
Result
,
Skeleton
,
Space
}
from
'antd'
import
{
useEffect
,
useRef
}
from
'react'
import
{
useSearchParams
}
from
'react-router'
import
{
getPaymentStatus
}
from
'../api'
function
orderNumberFromSearch
(
searchParams
:
URLSearchParams
)
{
return
searchParams
.
get
(
'ordersnum'
)
||
searchParams
.
get
(
'out_trade_no'
)
||
''
}
export
function
AlipayReturnView
()
{
const
[
searchParams
]
=
useSearchParams
()
const
queryClient
=
useQueryClient
()
const
pollingStartedAt
=
useRef
(
Date
.
now
())
const
orderNumber
=
orderNumberFromSearch
(
searchParams
)
const
statusQuery
=
useQuery
({
queryKey
:
[
'purchase'
,
'payment-status'
,
orderNumber
],
queryFn
:
()
=>
getPaymentStatus
(
orderNumber
),
enabled
:
Boolean
(
orderNumber
),
refetchInterval
:
(
query
)
=>
query
.
state
.
data
?.
pay_success
===
1
||
Date
.
now
()
-
pollingStartedAt
.
current
>
60
_000
?
false
:
2000
,
refetchIntervalInBackground
:
true
,
})
useEffect
(()
=>
{
if
(
statusQuery
.
data
?.
pay_success
!==
1
)
return
void
queryClient
.
invalidateQueries
({
queryKey
:
[
'book'
]
})
void
queryClient
.
invalidateQueries
({
queryKey
:
[
'my'
]
})
},
[
queryClient
,
statusQuery
.
data
?.
pay_success
])
if
(
!
orderNumber
)
{
return
<
Result
status=
'warning'
title=
'缺少订单号'
subTitle=
'请从订单列表重新确认支付结果。'
/>
}
if
(
statusQuery
.
isPending
)
return
<
Skeleton
active
paragraph=
{
{
rows
:
6
}
}
/>
if
(
statusQuery
.
data
?.
pay_success
!==
1
)
{
return
(
<
Result
status=
'info'
title=
'支付结果确认中'
subTitle=
'支付宝已返回,订单状态还在同步,请稍候。'
extra=
{
<
Space
>
<
Button
onClick=
{
()
=>
void
statusQuery
.
refetch
()
}
>
重新确认
</
Button
>
<
Button
type=
'primary'
href=
{
`/my/orders/${orderNumber}`
}
>
查看订单
</
Button
>
</
Space
>
}
/>
)
}
return
(
<
Result
status=
'success'
title=
'支付成功'
subTitle=
'订单已完成,现在可以阅读已购买的图书。'
extra=
{
<
Button
type=
'primary'
href=
{
`/my/orders/${orderNumber}`
}
>
查看订单
</
Button
>
}
/>
)
}
apps/learning/src/modules/reader/api.ts
浏览文件 @
8d6dd989
import
{
ApiError
}
from
'@/api/errors'
import
{
post
}
from
'@/utils/http'
import
{
unwrapApiData
}
from
'@/api/unwrap'
import
type
{
ApiEnvelope
}
from
'@/api/types'
import
{
http
}
from
'@/utils/http'
export
interface
ReaderChapter
{
export
interface
ReaderChapter
{
id
:
string
id
:
string
...
@@ -214,11 +211,6 @@ export class ReaderChapterLockedError extends Error {
...
@@ -214,11 +211,6 @@ export class ReaderChapterLockedError extends Error {
}
}
}
}
async
function
post
<
T
>
(
url
:
string
,
data
:
Record
<
string
,
unknown
>
):
Promise
<
T
>
{
const
response
=
await
http
.
post
<
ApiEnvelope
<
T
>>
(
url
,
data
)
return
unwrapApiData
(
response
)
}
export
async
function
getReaderChapters
(
bookId
:
string
):
Promise
<
ReaderChapter
[]
>
{
export
async
function
getReaderChapters
(
bookId
:
string
):
Promise
<
ReaderChapter
[]
>
{
const
data
=
await
post
<
unknown
>
(
'/api/app/v1/book/Information/getBookChapter'
,
{
const
data
=
await
post
<
unknown
>
(
'/api/app/v1/book/Information/getBookChapter'
,
{
book_id
:
bookId
,
book_id
:
bookId
,
...
@@ -227,22 +219,20 @@ export async function getReaderChapters(bookId: string): Promise<ReaderChapter[]
...
@@ -227,22 +219,20 @@ export async function getReaderChapters(bookId: string): Promise<ReaderChapter[]
}
}
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
>>
(
let
body
:
Record
<
string
,
unknown
>
'/api/app/v1/book/Information/getSectionContent'
,
try
{
{
body
=
await
post
(
'/api/app/v1/book/Information/getSectionContent'
,
{
book_id
:
bookId
,
book_id
:
bookId
,
chapter_id
:
chapterId
,
chapter_id
:
chapterId
,
},
)
const
body
=
response
.
data
const
payload
=
record
(
body
.
data
)
if
(
body
.
code
===
3001
||
Number
(
payload
.
code
)
===
3001
)
throw
new
ReaderChapterLockedError
()
if
(
body
.
code
!==
200
)
{
throw
new
ApiError
({
kind
:
'business'
,
message
:
body
.
message
??
body
.
msg
??
'章节内容加载失败'
,
})
})
}
catch
(
error
)
{
if
(
error
instanceof
Error
&&
'code'
in
error
&&
error
.
code
===
3001
)
{
throw
new
ReaderChapterLockedError
()
}
throw
error
}
}
const
payload
=
record
(
body
)
if
(
Number
(
payload
.
code
)
===
3001
)
throw
new
ReaderChapterLockedError
()
return
{
return
{
content
:
string
(
payload
.
content
),
content
:
string
(
payload
.
content
),
previousChapter
:
toSectionLink
(
payload
.
up_chapter
),
previousChapter
:
toSectionLink
(
payload
.
up_chapter
),
...
...
apps/learning/src/modules/reader/components/ReaderContent.tsx
浏览文件 @
8d6dd989
...
@@ -29,7 +29,6 @@ interface ReaderContentProps {
...
@@ -29,7 +29,6 @@ interface ReaderContentProps {
error
:
Error
|
null
error
:
Error
|
null
restoreAnchor
:
string
restoreAnchor
:
string
onAnchorChange
:
(
anchor
:
string
,
progress
:
number
)
=>
void
onAnchorChange
:
(
anchor
:
string
,
progress
:
number
)
=>
void
onRetry
:
()
=>
void
hasPreviousChapter
:
boolean
hasPreviousChapter
:
boolean
hasNextChapter
:
boolean
hasNextChapter
:
boolean
onPreviousChapter
:
()
=>
void
onPreviousChapter
:
()
=>
void
...
@@ -203,7 +202,6 @@ export function ReaderContent({
...
@@ -203,7 +202,6 @@ export function ReaderContent({
error
,
error
,
restoreAnchor
,
restoreAnchor
,
onAnchorChange
,
onAnchorChange
,
onRetry
,
hasPreviousChapter
,
hasPreviousChapter
,
hasNextChapter
,
hasNextChapter
,
onPreviousChapter
,
onPreviousChapter
,
...
@@ -366,19 +364,6 @@ export function ReaderContent({
...
@@ -366,19 +364,6 @@ export function ReaderContent({
/>
/>
)
)
}
}
if
(
error
)
return
(
<
Result
status=
'error'
title=
'章节加载失败'
subTitle=
{
error
.
message
}
extra=
{
<
Button
type=
'primary'
onClick=
{
onRetry
}
>
重试
</
Button
>
}
/>
)
if
(
!
section
?.
content
)
if
(
!
section
?.
content
)
return
<
Empty
className=
'reader-content__state'
description=
'本章节暂无内容'
/>
return
<
Empty
className=
'reader-content__state'
description=
'本章节暂无内容'
/>
...
...
apps/learning/src/modules/reader/components/ReaderInteractionLayer.tsx
浏览文件 @
8d6dd989
import
{
CloseOutlined
}
from
'@ant-design/icons'
import
{
CloseOutlined
}
from
'@ant-design/icons'
import
{
useMutation
,
useQuery
}
from
'@tanstack/react-query'
import
{
useMutation
,
useQuery
}
from
'@tanstack/react-query'
import
{
import
{
Alert
,
Button
,
Button
,
Card
,
Card
,
Checkbox
,
Checkbox
,
...
@@ -231,7 +230,6 @@ export function ReaderInteractionLayer({
...
@@ -231,7 +230,6 @@ export function ReaderInteractionLayer({
message
.
success
(
'提交成功'
)
message
.
success
(
'提交成功'
)
await
practiceQuery
.
refetch
()
await
practiceQuery
.
refetch
()
},
},
onError
:
(
error
)
=>
message
.
warning
(
error
.
message
),
})
})
const
gallery
=
useMemo
(()
=>
{
const
gallery
=
useMemo
(()
=>
{
...
@@ -257,7 +255,6 @@ export function ReaderInteractionLayer({
...
@@ -257,7 +255,6 @@ export function ReaderInteractionLayer({
extra=
{
<
Button
type=
'text'
icon=
{
<
CloseOutlined
/>
}
onClick=
{
onClose
}
/>
}
extra=
{
<
Button
type=
'text'
icon=
{
<
CloseOutlined
/>
}
onClick=
{
onClose
}
/>
}
>
>
{
infoQuery
.
isLoading
?
<
Spin
size=
'small'
/>
:
null
}
{
infoQuery
.
isLoading
?
<
Spin
size=
'small'
/>
:
null
}
{
infoQuery
.
error
?
<
Alert
type=
'error'
showIcon
title=
{
infoQuery
.
error
.
message
}
/>
:
null
}
{
content
?
<
Html
>
{
content
}
</
Html
>
:
!
infoQuery
.
isLoading
?
<
Empty
/>
:
null
}
{
content
?
<
Html
>
{
content
}
</
Html
>
:
!
infoQuery
.
isLoading
?
<
Empty
/>
:
null
}
{
link
?
(
{
link
?
(
<
Typography
.
Link
onClick=
{
()
=>
openReaderLink
(
link
)
}
>
查看详情
</
Typography
.
Link
>
<
Typography
.
Link
onClick=
{
()
=>
openReaderLink
(
link
)
}
>
查看详情
</
Typography
.
Link
>
...
@@ -299,15 +296,13 @@ export function ReaderInteractionLayer({
...
@@ -299,15 +296,13 @@ export function ReaderInteractionLayer({
}
}
onCancel=
{
onClose
}
onCancel=
{
onClose
}
>
>
{
modalType
===
'image'
&&
interaction
?.
type
===
'image'
?
(
{
modalType
===
'image'
&&
interaction
?.
type
===
'image'
&&
interaction
.
src
?
(
<
Image
className=
'reader-preview-image'
src=
{
interaction
.
src
}
alt=
{
interaction
.
title
}
/>
<
Image
className=
'reader-preview-image'
src=
{
interaction
.
src
}
alt=
{
interaction
.
title
}
/>
)
:
null
}
)
:
null
}
{
modalType
===
'expand'
?
(
{
modalType
===
'expand'
?
(
infoQuery
.
isLoading
?
(
infoQuery
.
isLoading
?
(
<
Spin
/>
<
Spin
/>
)
:
infoQuery
.
error
?
(
<
Alert
type=
'error'
showIcon
title=
{
infoQuery
.
error
.
message
}
/>
)
:
infoQuery
.
data
?.
content
?
(
)
:
infoQuery
.
data
?.
content
?
(
<
div
<
div
className=
'reader-rich-dialog'
className=
'reader-rich-dialog'
...
@@ -333,12 +328,12 @@ export function ReaderInteractionLayer({
...
@@ -333,12 +328,12 @@ export function ReaderInteractionLayer({
{
modalType
===
'gallery'
?
(
{
modalType
===
'gallery'
?
(
infoQuery
.
isLoading
?
(
infoQuery
.
isLoading
?
(
<
Spin
/>
<
Spin
/>
)
:
infoQuery
.
error
?
(
<
Alert
type=
'error'
showIcon
title=
{
infoQuery
.
error
.
message
}
/>
)
:
gallery
.
length
?
(
)
:
gallery
.
length
?
(
<
Image
.
PreviewGroup
>
<
Image
.
PreviewGroup
>
<
div
className=
'reader-gallery'
>
<
div
className=
'reader-gallery'
>
{
gallery
.
map
((
item
)
=>
(
{
gallery
.
filter
((
item
)
=>
item
.
url
)
.
map
((
item
)
=>
(
<
figure
key=
{
`${item.url}-${item.title}`
}
>
<
figure
key=
{
`${item.url}-${item.title}`
}
>
<
Image
src=
{
item
.
url
}
alt=
{
item
.
title
}
/>
<
Image
src=
{
item
.
url
}
alt=
{
item
.
title
}
/>
<
figcaption
>
<
figcaption
>
...
@@ -357,8 +352,6 @@ export function ReaderInteractionLayer({
...
@@ -357,8 +352,6 @@ export function ReaderInteractionLayer({
{
modalType
===
'practice'
?
(
{
modalType
===
'practice'
?
(
practiceQuery
.
isLoading
?
(
practiceQuery
.
isLoading
?
(
<
Spin
/>
<
Spin
/>
)
:
practiceQuery
.
error
?
(
<
Alert
type=
'error'
showIcon
title=
{
practiceQuery
.
error
.
message
}
/>
)
:
practiceQuery
.
data
?.
questions
.
length
?
(
)
:
practiceQuery
.
data
?.
questions
.
length
?
(
<
Space
className=
'reader-practice'
orientation=
'vertical'
size=
'middle'
>
<
Space
className=
'reader-practice'
orientation=
'vertical'
size=
'middle'
>
{
practiceQuery
.
data
.
questions
.
map
((
question
,
index
)
=>
(
{
practiceQuery
.
data
.
questions
.
map
((
question
,
index
)
=>
(
...
...
apps/learning/src/modules/reader/components/ReaderSidePanel.tsx
浏览文件 @
8d6dd989
...
@@ -10,7 +10,6 @@ import {
...
@@ -10,7 +10,6 @@ import {
}
from
'@ant-design/icons'
}
from
'@ant-design/icons'
import
{
useMutation
,
useQuery
,
useQueryClient
}
from
'@tanstack/react-query'
import
{
useMutation
,
useQuery
,
useQueryClient
}
from
'@tanstack/react-query'
import
{
import
{
Alert
,
Avatar
,
Avatar
,
Button
,
Button
,
Empty
,
Empty
,
...
@@ -68,12 +67,6 @@ interface CreateNoteValues extends NoteFormValues {
...
@@ -68,12 +67,6 @@ interface CreateNoteValues extends NoteFormValues {
quote
?:
ReaderTextQuote
quote
?:
ReaderTextQuote
}
}
function
MutationError
({
error
}:
{
error
:
Error
|
null
})
{
return
error
?
(
<
Alert
type=
'error'
showIcon
title=
'操作未完成'
description=
{
error
.
message
}
/>
)
:
null
}
function
NoteList
({
function
NoteList
({
notes
,
notes
,
onEdit
,
onEdit
,
...
@@ -424,14 +417,6 @@ export function ReaderSidePanel({
...
@@ -424,14 +417,6 @@ export function ReaderSidePanel({
{
panel
===
'notes'
?
(
{
panel
===
'notes'
?
(
<>
<>
{
notesQuery
.
isPending
?
<
Skeleton
active
paragraph=
{
{
rows
:
7
}
}
/>
:
null
}
{
notesQuery
.
isPending
?
<
Skeleton
active
paragraph=
{
{
rows
:
7
}
}
/>
:
null
}
{
notesQuery
.
isError
?
(
<
Alert
type=
'error'
showIcon
title=
'笔记加载失败'
description=
{
notesQuery
.
error
.
message
}
/>
)
:
null
}
{
notesQuery
.
data
?
(
{
notesQuery
.
data
?
(
<
NoteList
<
NoteList
notes=
{
notesQuery
.
data
}
notes=
{
notesQuery
.
data
}
...
@@ -442,9 +427,6 @@ export function ReaderSidePanel({
...
@@ -442,9 +427,6 @@ export function ReaderSidePanel({
}
}
/>
/>
)
:
null
}
)
:
null
}
<
MutationError
error=
{
createNoteMutation
.
error
??
editNoteMutation
.
error
??
deleteNoteMutation
.
error
}
/>
</>
</>
)
:
(
)
:
(
<>
<>
...
@@ -464,14 +446,6 @@ export function ReaderSidePanel({
...
@@ -464,14 +446,6 @@ export function ReaderSidePanel({
</
Tooltip
>
</
Tooltip
>
</
div
>
</
div
>
{
commentsQuery
.
isPending
?
<
Skeleton
active
avatar
paragraph=
{
{
rows
:
8
}
}
/>
:
null
}
{
commentsQuery
.
isPending
?
<
Skeleton
active
avatar
paragraph=
{
{
rows
:
8
}
}
/>
:
null
}
{
commentsQuery
.
isError
?
(
<
Alert
type=
'error'
showIcon
title=
'讨论加载失败'
description=
{
commentsQuery
.
error
.
message
}
/>
)
:
null
}
{
commentsQuery
.
data
?.
length
===
0
?
(
{
commentsQuery
.
data
?.
length
===
0
?
(
<
Empty
image=
{
Empty
.
PRESENTED_IMAGE_SIMPLE
}
description=
'还没有章节讨论'
/>
<
Empty
image=
{
Empty
.
PRESENTED_IMAGE_SIMPLE
}
description=
'还没有章节讨论'
/>
)
:
null
}
)
:
null
}
...
@@ -487,11 +461,6 @@ export function ReaderSidePanel({
...
@@ -487,11 +461,6 @@ export function ReaderSidePanel({
}
}
/>
/>
))
}
))
}
<
MutationError
error=
{
addCommentMutation
.
error
??
praiseMutation
.
error
??
deleteCommentMutation
.
error
}
/>
</>
</>
)
}
)
}
</
div
>
</
div
>
...
...
apps/learning/src/modules/reader/routes.tsx
浏览文件 @
8d6dd989
import
type
{
RouteObject
}
from
'react-router'
import
type
{
RouteObject
}
from
'react-router'
import
{
Outlet
}
from
'react-router'
import
{
Outlet
}
from
'react-router'
import
{
RequireSession
}
from
'@/
modules/auth
/RequireSession'
import
{
RequireSession
}
from
'@/
router
/RequireSession'
import
{
lazyRoute
}
from
'@/router/lazy-route'
import
{
lazyRoute
}
from
'@/router/lazy-route'
export
const
routes
:
RouteObject
[]
=
[
export
const
routes
:
RouteObject
[]
=
[
...
...
apps/learning/src/modules/reader/views/ReaderView.tsx
浏览文件 @
8d6dd989
import
{
useMutation
,
useQuery
}
from
'@tanstack/react-query'
import
{
useMutation
,
useQuery
}
from
'@tanstack/react-query'
import
{
LeftOutlined
,
MenuFoldOutlined
,
MenuUnfoldOutlined
}
from
'@ant-design/icons'
import
{
LeftOutlined
,
MenuFoldOutlined
,
MenuUnfoldOutlined
}
from
'@ant-design/icons'
import
{
Alert
,
Button
,
Skeleton
,
Tooltip
,
Typography
}
from
'antd'
import
{
Button
,
Skeleton
,
Tooltip
,
Typography
}
from
'antd'
import
{
useCallback
,
useEffect
,
useMemo
,
useRef
,
useState
}
from
'react'
import
{
useCallback
,
useEffect
,
useMemo
,
useRef
,
useState
}
from
'react'
import
{
useNavigate
,
useParams
}
from
'react-router'
import
{
useNavigate
,
useParams
}
from
'react-router'
import
{
import
{
...
@@ -152,13 +152,7 @@ export function ReaderView() {
...
@@ -152,13 +152,7 @@ export function ReaderView() {
[
progressMutation
,
setAnchor
],
[
progressMutation
,
setAnchor
],
)
)
if
(
!
bookId
||
!
chapterId
)
{
if
(
!
bookId
||
!
chapterId
)
return
null
return
(
<
main
className=
'reader-view'
>
<
Alert
type=
'error'
showIcon
title=
'阅读地址无效'
description=
'缺少图书或章节编号。'
/>
</
main
>
)
}
return
(
return
(
<
main
className=
'reader-view'
>
<
main
className=
'reader-view'
>
...
@@ -201,14 +195,6 @@ export function ReaderView() {
...
@@ -201,14 +195,6 @@ export function ReaderView() {
{
!
readerState
.
catalogueCollapsed
?
(
{
!
readerState
.
catalogueCollapsed
?
(
<>
<>
{
chaptersQuery
.
isLoading
?
<
Skeleton
active
paragraph=
{
{
rows
:
10
}
}
/>
:
null
}
{
chaptersQuery
.
isLoading
?
<
Skeleton
active
paragraph=
{
{
rows
:
10
}
}
/>
:
null
}
{
chaptersQuery
.
error
?
(
<
Alert
type=
'warning'
showIcon
title=
'目录加载失败'
description=
{
chaptersQuery
.
error
.
message
}
/>
)
:
null
}
{
chaptersQuery
.
data
?
(
{
chaptersQuery
.
data
?
(
<
ReaderChapterTree
<
ReaderChapterTree
chapters=
{
chaptersQuery
.
data
}
chapters=
{
chaptersQuery
.
data
}
...
@@ -230,7 +216,6 @@ export function ReaderView() {
...
@@ -230,7 +216,6 @@ export function ReaderView() {
error=
{
sectionQuery
.
error
}
error=
{
sectionQuery
.
error
}
restoreAnchor=
{
readerState
.
anchor
}
restoreAnchor=
{
readerState
.
anchor
}
onAnchorChange=
{
handleAnchorChange
}
onAnchorChange=
{
handleAnchorChange
}
onRetry=
{
()
=>
void
sectionQuery
.
refetch
()
}
hasPreviousChapter=
{
Boolean
(
adjacentChapters
.
previous
)
}
hasPreviousChapter=
{
Boolean
(
adjacentChapters
.
previous
)
}
hasNextChapter=
{
Boolean
(
adjacentChapters
.
next
)
}
hasNextChapter=
{
Boolean
(
adjacentChapters
.
next
)
}
onPreviousChapter=
{
()
=>
changeChapter
(
adjacentChapters
.
previous
?.
id
??
''
)
}
onPreviousChapter=
{
()
=>
changeChapter
(
adjacentChapters
.
previous
?.
id
??
''
)
}
...
@@ -240,15 +225,6 @@ export function ReaderView() {
...
@@ -240,15 +225,6 @@ export function ReaderView() {
onTextQuoteAction=
{
handleTextQuoteAction
}
onTextQuoteAction=
{
handleTextQuoteAction
}
onAnnotationSelect=
{
handleAnnotationSelect
}
onAnnotationSelect=
{
handleAnnotationSelect
}
/>
/>
{
progressMutation
.
error
||
readRecordMutation
.
error
||
readTimeMutation
.
error
?
(
<
Alert
className=
'reader-view__report-error'
type=
'warning'
showIcon
title=
'阅读记录暂未同步'
description=
'不影响继续阅读,网络恢复后可再次滚动同步。'
/>
)
:
null
}
</
section
>
</
section
>
<
aside
className=
'reader-view__panel'
aria
-
label=
'笔记与讨论'
>
<
aside
className=
'reader-view__panel'
aria
-
label=
'笔记与讨论'
>
{
readerState
.
panelCollapsed
?
(
{
readerState
.
panelCollapsed
?
(
...
...
apps/learning/src/modules/search/api.ts
deleted
100644 → 0
浏览文件 @
d8033dd5
import
type
{
ApiEnvelope
}
from
'@/api/types'
import
{
unwrapApiData
}
from
'@/api/unwrap'
import
{
http
}
from
'@/utils/http'
import
type
{
SearchState
}
from
'./query-state'
export
interface
SearchBook
{
bookId
:
string
bookName
:
string
authors
:
string
img
:
string
introduction
:
string
readNum
:
number
isCollection
:
boolean
price
:
string
vipPrice
:
string
isFree
:
boolean
}
export
interface
SearchBooksResult
{
list
:
SearchBook
[]
total
:
number
}
type
UnknownRecord
=
Record
<
string
,
unknown
>
function
record
(
value
:
unknown
):
UnknownRecord
{
return
typeof
value
===
'object'
&&
value
!==
null
?
(
value
as
UnknownRecord
)
:
{}
}
function
string
(
value
:
unknown
):
string
{
return
value
===
null
||
value
===
undefined
?
''
:
String
(
value
)
}
function
number
(
value
:
unknown
):
number
{
const
parsed
=
Number
(
value
)
return
Number
.
isFinite
(
parsed
)
?
parsed
:
0
}
function
list
(
value
:
unknown
):
unknown
[]
{
if
(
Array
.
isArray
(
value
))
return
value
const
payload
=
record
(
value
)
for
(
const
key
of
[
'list'
,
'data'
,
'rows'
])
{
if
(
Array
.
isArray
(
payload
[
key
]))
return
payload
[
key
]
}
return
[]
}
function
total
(
value
:
unknown
,
fallback
:
number
):
number
{
const
payload
=
record
(
value
)
for
(
const
key
of
[
'total'
,
'count'
,
'total_count'
])
{
if
(
payload
[
key
]
!==
undefined
)
return
number
(
payload
[
key
])
}
return
fallback
}
function
toBook
(
value
:
unknown
):
SearchBook
{
const
item
=
record
(
value
)
return
{
bookId
:
string
(
item
.
book_id
),
bookName
:
string
(
item
.
book_name
),
authors
:
string
(
item
.
authors
),
img
:
string
(
item
.
img
),
introduction
:
string
(
item
.
introduction
),
readNum
:
number
(
item
.
read_num
),
isCollection
:
number
(
item
.
is_collection
)
===
1
,
price
:
string
(
item
.
price
),
vipPrice
:
string
(
item
.
vip_price
),
isFree
:
number
(
item
.
is_free
)
===
1
,
}
}
async
function
post
<
T
>
(
url
:
string
,
data
:
Record
<
string
,
unknown
>
):
Promise
<
T
>
{
const
response
=
await
http
.
post
<
ApiEnvelope
<
T
>>
(
url
,
data
)
return
unwrapApiData
(
response
)
}
export
async
function
getSearchHistory
():
Promise
<
string
[]
>
{
return
list
(
await
post
<
unknown
>
(
'/api/app/v1/book/Information/serachRecord'
,
{}))
.
map
((
item
)
=>
string
(
record
(
item
).
name
).
trim
())
.
filter
(
Boolean
)
}
export
async
function
clearSearchHistory
():
Promise
<
boolean
>
{
const
data
=
record
(
await
post
<
unknown
>
(
'/api/app/v1/book/Information/serachDel'
,
{}))
return
number
(
data
.
is_success
)
===
1
}
export
async
function
getSearchBooks
(
state
:
SearchState
):
Promise
<
SearchBooksResult
>
{
const
data
=
await
post
<
unknown
>
(
'/api/app/v1/book/Information/searchBook'
,
{
page
:
state
.
page
,
page_size
:
state
.
pageSize
,
keyword
:
state
.
q
,
})
const
books
=
list
(
data
).
map
(
toBook
)
const
fallbackTotal
=
books
.
length
===
state
.
pageSize
?
state
.
page
*
state
.
pageSize
+
1
:
(
state
.
page
-
1
)
*
state
.
pageSize
+
books
.
length
return
{
list
:
books
,
total
:
total
(
data
,
fallbackTotal
)
}
}
export
async
function
setSearchBookCollection
(
bookId
:
string
,
selected
:
boolean
):
Promise
<
boolean
>
{
const
data
=
record
(
await
post
<
unknown
>
(
'/api/app/v1/book/Information/collection'
,
{
book_id
:
bookId
,
is_select
:
selected
?
'1'
:
'0'
,
}),
)
return
number
(
data
.
is_success
)
===
1
}
apps/learning/src/modules/search/components/SearchBookCard.tsx
deleted
100644 → 0
浏览文件 @
d8033dd5
import
{
Button
,
Card
,
Image
,
Tag
,
Tooltip
,
Typography
}
from
'antd'
import
{
Link
}
from
'react-router'
import
defaultBookCover
from
'@/assets/default-book-cover.jpg'
import
type
{
SearchBook
}
from
'../api'
interface
SearchBookCardProps
{
book
:
SearchBook
view
:
'grid'
|
'list'
isUpdatingCollection
:
boolean
onCollectionChange
:
(
book
:
SearchBook
)
=>
void
}
function
priceLabel
(
book
:
SearchBook
)
{
if
(
book
.
isFree
)
return
'免费'
if
(
book
.
vipPrice
)
return
`¥
${
book
.
vipPrice
}
`
return
book
.
price
?
`¥
${
book
.
price
}
`
:
'收费'
}
export
function
SearchBookCard
({
book
,
view
,
isUpdatingCollection
,
onCollectionChange
,
}:
SearchBookCardProps
)
{
return
(
<
Card
className=
{
`search-book-card search-book-card--${view}`
}
hoverable
>
<
Link
className=
'search-book-card__cover'
to=
{
`/book/${book.bookId}`
}
aria
-
label=
{
`查看《${book.bookName}》详情`
}
>
<
Image
preview=
{
false
}
src=
{
book
.
img
||
defaultBookCover
}
alt=
{
`${book.bookName}封面`
}
fallback=
{
defaultBookCover
}
/>
</
Link
>
<
div
className=
'search-book-card__content'
>
<
div
>
<
Typography
.
Title
level=
{
4
}
ellipsis=
{
{
rows
:
2
}
}
>
<
Link
to=
{
`/book/${book.bookId}`
}
>
{
book
.
bookName
}
</
Link
>
</
Typography
.
Title
>
<
Typography
.
Text
type=
'secondary'
>
{
book
.
authors
||
'作者未知'
}
</
Typography
.
Text
>
</
div
>
{
view
===
'list'
&&
book
.
introduction
?
(
<
Typography
.
Paragraph
ellipsis=
{
{
rows
:
2
}
}
type=
'secondary'
>
{
book
.
introduction
}
</
Typography
.
Paragraph
>
)
:
null
}
<
div
className=
'search-book-card__meta'
>
<
Tag
color=
{
book
.
isFree
?
'green'
:
'magenta'
}
>
{
priceLabel
(
book
)
}
</
Tag
>
<
Typography
.
Text
type=
'secondary'
>
{
book
.
readNum
}
人读过
</
Typography
.
Text
>
<
Tooltip
title=
{
book
.
isCollection
?
'取消收藏'
:
'收藏'
}
>
<
Button
aria
-
label=
{
book
.
isCollection
?
`取消收藏《${book.bookName}》`
:
`收藏《${book.bookName}》`
}
type=
'text'
danger=
{
book
.
isCollection
}
loading=
{
isUpdatingCollection
}
onClick=
{
()
=>
onCollectionChange
(
book
)
}
>
{
book
.
isCollection
?
'♥'
:
'♡'
}
</
Button
>
</
Tooltip
>
</
div
>
</
div
>
</
Card
>
)
}
apps/learning/src/modules/search/components/SearchHistory.tsx
deleted
100644 → 0
浏览文件 @
d8033dd5
import
{
Button
,
Empty
,
Flex
,
Tag
,
Typography
}
from
'antd'
interface
SearchHistoryProps
{
items
:
string
[]
isClearing
:
boolean
onClear
:
()
=>
void
onSelect
:
(
keyword
:
string
)
=>
void
}
export
function
SearchHistory
({
items
,
isClearing
,
onClear
,
onSelect
}:
SearchHistoryProps
)
{
return
(
<
section
className=
'search-history'
aria
-
labelledby=
'search-history-title'
>
<
Flex
justify=
'space-between'
align=
'center'
gap=
{
16
}
>
<
Typography
.
Title
id=
'search-history-title'
level=
{
4
}
>
搜索历史
</
Typography
.
Title
>
{
items
.
length
?
(
<
Button
type=
'link'
danger
loading=
{
isClearing
}
onClick=
{
onClear
}
>
清空历史
</
Button
>
)
:
null
}
</
Flex
>
{
items
.
length
?
(
<
Flex
wrap
gap=
'small'
>
{
items
.
map
((
item
)
=>
(
<
Tag
.
CheckableTag
key=
{
item
}
checked=
{
false
}
onChange=
{
()
=>
onSelect
(
item
)
}
>
{
item
}
</
Tag
.
CheckableTag
>
))
}
</
Flex
>
)
:
(
<
Empty
image=
{
Empty
.
PRESENTED_IMAGE_SIMPLE
}
description=
'暂无搜索历史'
/>
)
}
</
section
>
)
}
apps/learning/src/modules/search/query-state.ts
deleted
100644 → 0
浏览文件 @
d8033dd5
import
{
createParser
,
parseAsString
,
parseAsStringLiteral
,
useQueryStates
}
from
'nuqs'
const
views
=
[
'grid'
,
'list'
]
as
const
function
parseAsMinimumInteger
(
minimum
:
number
)
{
return
createParser
({
parse
:
(
value
)
=>
{
const
parsed
=
Number
(
value
)
return
Number
.
isInteger
(
parsed
)
&&
parsed
>=
minimum
?
parsed
:
null
},
serialize
:
(
value
)
=>
String
(
value
),
})
}
export
const
searchParams
=
{
q
:
parseAsString
.
withDefault
(
''
),
page
:
parseAsMinimumInteger
(
1
).
withDefault
(
1
),
pageSize
:
parseAsMinimumInteger
(
1
).
withDefault
(
20
),
view
:
parseAsStringLiteral
(
views
).
withDefault
(
'list'
),
}
export
interface
SearchState
{
q
:
string
page
:
number
pageSize
:
number
view
:
(
typeof
views
)[
number
]
}
export
function
useSearchQueryState
()
{
const
[
state
,
setState
]
=
useQueryStates
(
searchParams
,
{
clearOnDefault
:
true
,
history
:
'replace'
,
})
return
{
state
,
search
:
(
q
:
string
)
=>
setState
({
q
,
page
:
1
},
{
history
:
'push'
}),
setView
:
(
view
:
SearchState
[
'view'
])
=>
setState
({
view
},
{
history
:
'replace'
}),
setPagination
:
(
page
:
number
,
pageSize
:
number
)
=>
setState
({
page
,
pageSize
},
{
history
:
'push'
}),
}
}
apps/learning/src/modules/search/query.ts
deleted
100644 → 0
浏览文件 @
d8033dd5
import
{
queryOptions
}
from
'@tanstack/react-query'
import
{
getSearchBooks
,
getSearchHistory
}
from
'./api'
import
type
{
SearchState
}
from
'./query-state'
export
const
searchKeys
=
{
all
:
[
'search'
]
as
const
,
history
:
()
=>
[
'search'
,
'history'
]
as
const
,
books
:
(
state
:
SearchState
)
=>
[
'search'
,
'books'
,
state
.
q
,
state
.
page
,
state
.
pageSize
]
as
const
,
}
export
function
searchHistoryOptions
()
{
return
queryOptions
({
queryKey
:
searchKeys
.
history
(),
queryFn
:
getSearchHistory
})
}
export
function
searchBooksOptions
(
state
:
SearchState
)
{
return
queryOptions
({
queryKey
:
searchKeys
.
books
(
state
),
queryFn
:
()
=>
getSearchBooks
(
state
),
enabled
:
Boolean
(
state
.
q
.
trim
()),
})
}
apps/learning/src/modules/search/routes.tsx
deleted
100644 → 0
浏览文件 @
d8033dd5
import
type
{
RouteObject
}
from
'react-router'
import
{
lazyRoute
}
from
'@/router/lazy-route'
export
const
routes
:
RouteObject
[]
=
[
{
path
:
'search'
,
lazy
:
lazyRoute
(()
=>
import
(
'./views/SearchView'
),
'SearchView'
),
},
]
apps/learning/src/modules/search/views/SearchView.css
deleted
100644 → 0
浏览文件 @
d8033dd5
.search-view
{
display
:
grid
;
width
:
100%
;
box-sizing
:
border-box
;
margin
:
0
auto
;
padding
:
0
;
gap
:
24px
;
}
.search-view
h1
{
margin-bottom
:
4px
;
}
.search-history
h4
,
.search-results
h3
{
margin
:
0
;
}
.search-history
.ant-empty
{
margin-block
:
28px
0
;
}
.search-results
{
display
:
grid
;
gap
:
20px
;
}
.search-results__toolbar
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
16px
;
}
.search-books
{
display
:
grid
;
gap
:
14px
;
}
.search-books--grid
{
grid-template-columns
:
repeat
(
auto-fill
,
minmax
(
280px
,
1
fr
));
}
.search-books--list
{
grid-template-columns
:
1
fr
;
}
.search-book-card
.ant-card-body
{
display
:
flex
;
height
:
100%
;
gap
:
14px
;
}
.search-book-card--grid
.ant-card-body
{
display
:
flex
;
padding
:
12px
;
}
.search-book-card.ant-card
{
border-color
:
transparent
;
box-shadow
:
0
1px
2px
rgb
(
31
41
55
/
4%
);
transition
:
transform
160ms
ease-out
,
box-shadow
160ms
ease-out
;
}
.search-book-card.ant-card
:hover
{
transform
:
translateY
(
-2px
);
box-shadow
:
0
12px
28px
rgb
(
31
41
55
/
12%
);
}
.search-book-card.ant-card
:active
{
transform
:
scale
(
0.985
);
}
.search-book-card__cover
{
display
:
block
;
flex
:
0
0
112px
;
width
:
112px
;
overflow
:
hidden
;
border-radius
:
4px
;
background
:
#f5f5f5
;
}
.search-book-card--grid
.search-book-card__cover
{
flex
:
0
0
92px
;
width
:
92px
;
height
:
124px
;
margin-bottom
:
0
;
}
.search-book-card__cover
.ant-image
,
.search-book-card__cover
img
{
width
:
100%
;
height
:
100%
;
object-fit
:
cover
;
}
.search-book-card__content
{
display
:
flex
;
min-width
:
0
;
flex
:
1
;
flex-direction
:
column
;
justify-content
:
space-between
;
gap
:
12px
;
}
.search-book-card__content
h4
,
.search-book-card__content
p
{
margin
:
0
;
}
.search-book-card__meta
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
}
.search-book-card__meta
.ant-btn
{
margin-left
:
auto
;
}
.search-view__pagination
{
justify-content
:
center
;
}
@media
(
max-width
:
640px
)
{
.search-view
{
padding
:
0
;
}
.search-results__toolbar
{
align-items
:
flex-start
;
flex-direction
:
column
;
}
.search-books--grid
{
grid-template-columns
:
1
fr
;
}
.search-book-card--list
.ant-card-body
{
gap
:
12px
;
}
.search-book-card__cover
{
flex-basis
:
88px
;
width
:
88px
;
}
}
apps/learning/src/modules/search/views/SearchView.tsx
deleted
100644 → 0
浏览文件 @
d8033dd5
import
{
useMutation
,
useQuery
,
useQueryClient
}
from
'@tanstack/react-query'
import
{
Alert
,
Empty
,
Input
,
Pagination
,
Segmented
,
Skeleton
,
Space
,
Typography
}
from
'antd'
import
{
useEffect
,
useState
}
from
'react'
import
{
clearSearchHistory
,
setSearchBookCollection
,
type
SearchBook
}
from
'../api'
import
{
SearchBookCard
}
from
'../components/SearchBookCard'
import
{
SearchHistory
}
from
'../components/SearchHistory'
import
{
searchBooksOptions
,
searchHistoryOptions
,
searchKeys
}
from
'../query'
import
{
useSearchQueryState
}
from
'../query-state'
import
'./SearchView.css'
export
function
SearchView
()
{
const
queryState
=
useSearchQueryState
()
const
{
state
}
=
queryState
const
[
draft
,
setDraft
]
=
useState
(
state
.
q
)
const
[
changingBookId
,
setChangingBookId
]
=
useState
<
string
>
()
const
queryClient
=
useQueryClient
()
const
historyQuery
=
useQuery
(
searchHistoryOptions
())
const
booksQuery
=
useQuery
(
searchBooksOptions
(
state
))
useEffect
(()
=>
setDraft
(
state
.
q
),
[
state
.
q
])
const
clearMutation
=
useMutation
({
mutationFn
:
clearSearchHistory
,
onSuccess
:
(
succeeded
)
=>
{
if
(
!
succeeded
)
throw
new
Error
(
'搜索历史清空失败'
)
queryClient
.
setQueryData
(
searchKeys
.
history
(),
[])
},
})
const
collectionMutation
=
useMutation
({
mutationFn
:
async
({
book
,
selected
}:
{
book
:
SearchBook
;
selected
:
boolean
})
=>
{
setChangingBookId
(
book
.
bookId
)
const
succeeded
=
await
setSearchBookCollection
(
book
.
bookId
,
selected
)
if
(
!
succeeded
)
throw
new
Error
(
'收藏状态更新失败'
)
return
{
book
,
selected
}
},
onSuccess
:
({
book
,
selected
})
=>
{
queryClient
.
setQueryData
(
searchKeys
.
books
(
state
),
(
previous
:
typeof
booksQuery
.
data
)
=>
previous
?
{
...
previous
,
list
:
previous
.
list
.
map
((
item
)
=>
item
.
bookId
===
book
.
bookId
?
{
...
item
,
isCollection
:
selected
}
:
item
,
),
}
:
previous
,
)
},
onSettled
:
()
=>
setChangingBookId
(
undefined
),
})
function
submit
(
keyword
=
draft
)
{
const
q
=
keyword
.
trim
()
if
(
!
q
)
return
setDraft
(
q
)
void
queryState
.
search
(
q
)
.
then
(()
=>
queryClient
.
invalidateQueries
({
queryKey
:
searchKeys
.
history
()
}))
}
return
(
<
main
className=
'search-view'
>
<
div
>
<
Typography
.
Title
level=
{
1
}
>
搜索图书
</
Typography
.
Title
>
<
Typography
.
Paragraph
type=
'secondary'
>
按书名或作者查找你想学习的内容。
</
Typography
.
Paragraph
>
</
div
>
<
Input
.
Search
size=
'large'
value=
{
draft
}
placeholder=
'请输入书籍名称或作者'
enterButton=
'搜索'
allowClear
onChange=
{
(
event
)
=>
setDraft
(
event
.
target
.
value
)
}
onSearch=
{
()
=>
submit
()
}
/>
{
clearMutation
.
error
?
(
<
Alert
type=
'error'
showIcon
title=
'清空搜索历史失败'
description=
{
(
clearMutation
.
error
as
Error
).
message
}
/>
)
:
null
}
{
collectionMutation
.
error
?
(
<
Alert
type=
'error'
showIcon
title=
'收藏操作失败'
description=
{
(
collectionMutation
.
error
as
Error
).
message
}
/>
)
:
null
}
{
!
state
.
q
.
trim
()
?
(
historyQuery
.
error
?
(
<
Alert
type=
'error'
showIcon
title=
'搜索历史加载失败'
description=
{
(
historyQuery
.
error
as
Error
).
message
}
/>
)
:
historyQuery
.
isLoading
?
(
<
Skeleton
active
paragraph=
{
{
rows
:
2
}
}
/>
)
:
(
<
SearchHistory
items=
{
historyQuery
.
data
??
[]
}
isClearing=
{
clearMutation
.
isPending
}
onClear=
{
()
=>
clearMutation
.
mutate
()
}
onSelect=
{
submit
}
/>
)
)
:
(
<
section
className=
'search-results'
aria
-
live=
'polite'
>
<
div
className=
'search-results__toolbar'
>
<
Typography
.
Title
level=
{
3
}
>
“
{
state
.
q
}
” 的搜索结果
</
Typography
.
Title
>
<
Segmented
<
SearchBook
['
isCollection
']
extends
boolean
?
'
grid
'
|
'
list
'
:
never
>
value=
{
state
.
view
}
options=
{
[
{
label
:
'列表'
,
value
:
'list'
},
{
label
:
'网格'
,
value
:
'grid'
},
]
}
onChange=
{
(
value
)
=>
void
queryState
.
setView
(
value
)
}
/
>
</
div
>
{
booksQuery
.
error
?
(
<
Alert
type=
'error'
showIcon
title=
'图书加载失败'
description=
{
(
booksQuery
.
error
as
Error
).
message
}
action=
{
<
a
onClick=
{
()
=>
void
booksQuery
.
refetch
()
}
>
重试
</
a
>
}
/>
)
:
booksQuery
.
isLoading
?
(
<
Skeleton
active
paragraph=
{
{
rows
:
8
}
}
/>
)
:
booksQuery
.
data
?.
list
.
length
?
(
<>
<
div
className=
{
`search-books search-books--${state.view}`
}
>
{
booksQuery
.
data
.
list
.
map
((
book
)
=>
(
<
SearchBookCard
key=
{
book
.
bookId
}
book=
{
book
}
view=
{
state
.
view
}
isUpdatingCollection=
{
changingBookId
===
book
.
bookId
}
onCollectionChange=
{
(
item
)
=>
collectionMutation
.
mutate
({
book
:
item
,
selected
:
!
item
.
isCollection
})
}
/>
))
}
</
div
>
<
Space
className=
'search-view__pagination'
>
<
Pagination
current=
{
state
.
page
}
pageSize=
{
state
.
pageSize
}
total=
{
booksQuery
.
data
.
total
}
hideOnSinglePage
showSizeChanger=
{
false
}
showTotal=
{
(
total
)
=>
`共 ${total} 本`
}
onChange=
{
(
page
,
pageSize
)
=>
void
queryState
.
setPagination
(
page
,
pageSize
)
}
/>
</
Space
>
</>
)
:
(
<
Empty
description=
'暂无图书,请检查输入内容'
/>
)
}
</
section
>
)
}
</
main
>
)
}
apps/learning/src/
modules/auth
/RequireSession.tsx
→
apps/learning/src/
router
/RequireSession.tsx
浏览文件 @
8d6dd989
import
type
{
ReactNode
}
from
'react'
import
type
{
ReactNode
}
from
'react'
import
{
Navigate
,
Outlet
}
from
'react-router'
import
{
Navigate
,
Outlet
}
from
'react-router'
import
{
useSessionStore
}
from
'
.
/session'
import
{
useSessionStore
}
from
'
@/stores
/session'
interface
RequireSessionProps
{
interface
RequireSessionProps
{
children
?:
ReactNode
children
?:
ReactNode
...
...
apps/learning/src/router/RootLayout.tsx
deleted
100644 → 0
浏览文件 @
d8033dd5
import
{
LearningLayout
}
from
'@/layouts/LearningLayout'
export
function
RootLayout
()
{
return
<
LearningLayout
/>
}
apps/learning/src/router/paths.ts
deleted
100644 → 0
浏览文件 @
d8033dd5
export
const
paths
=
{
login
:
'/login'
,
library
:
'/library'
,
my
:
'/my'
,
book
:
(
bookId
:
string
)
=>
`/book/
${
bookId
}
`
,
reader
:
(
bookId
:
string
,
chapterId
:
string
)
=>
`/reader/
${
bookId
}
/
${
chapterId
}
`
,
}
as
const
apps/learning/src/router/routes.test.tsx
浏览文件 @
8d6dd989
...
@@ -6,7 +6,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
...
@@ -6,7 +6,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import
{
NuqsTestingAdapter
}
from
'nuqs/adapters/testing'
import
{
NuqsTestingAdapter
}
from
'nuqs/adapters/testing'
import
{
afterEach
,
beforeEach
,
describe
,
expect
,
it
}
from
'vitest'
import
{
afterEach
,
beforeEach
,
describe
,
expect
,
it
}
from
'vitest'
import
{
appRoutes
}
from
'./routes'
import
{
appRoutes
}
from
'./routes'
import
{
useSessionStore
}
from
'@/
modules/auth
/session'
import
{
useSessionStore
}
from
'@/
stores
/session'
const
testSession
=
{
const
testSession
=
{
members_id
:
1
,
members_id
:
1
,
...
...
apps/learning/src/router/routes.tsx
浏览文件 @
8d6dd989
import
{
createBrowserRouter
,
Navigate
,
type
RouteObject
}
from
'react-router'
import
{
createBrowserRouter
,
Navigate
,
type
RouteObject
}
from
'react-router'
import
{
RootLayout
}
from
'./RootLayout'
import
{
LoadingFallback
}
from
'./LoadingFallback'
import
{
LoadingFallback
}
from
'./LoadingFallback'
import
{
lazyRoute
}
from
'./lazy-route'
import
{
lazyRoute
}
from
'./lazy-route'
import
{
LearningLayout
}
from
'@/layouts/LearningLayout'
import
{
routes
as
authRoutes
}
from
'@/modules/auth/routes'
import
{
routes
as
authRoutes
}
from
'@/modules/auth/routes'
import
{
routes
as
bookRoutes
}
from
'@/modules/book/routes'
import
{
routes
as
bookRoutes
}
from
'@/modules/book/routes'
import
{
routes
as
libraryRoutes
}
from
'@/modules/library/routes'
import
{
routes
as
libraryRoutes
}
from
'@/modules/library/routes'
import
{
routes
as
myRoutes
}
from
'@/modules/my/routes'
import
{
routes
as
myRoutes
}
from
'@/modules/my/routes'
import
{
routes
as
purchaseRoutes
}
from
'@/modules/purchase/routes'
import
{
routes
as
readerRoutes
}
from
'@/modules/reader/routes'
import
{
routes
as
readerRoutes
}
from
'@/modules/reader/routes'
import
{
routes
as
helpRoutes
}
from
'@/modules/
my/
help/routes'
import
{
routes
as
helpRoutes
}
from
'@/modules/help/routes'
export
const
appChildRoutes
:
RouteObject
[]
=
[
export
const
appChildRoutes
:
RouteObject
[]
=
[
{
index
:
true
,
element
:
<
Navigate
to=
'/library'
replace
/>
},
{
index
:
true
,
element
:
<
Navigate
to=
'/library'
replace
/>
},
{
{
path
:
'about'
,
path
:
'about'
,
lazy
:
lazyRoute
(()
=>
import
(
'@/modules/
my/
help/views/AboutView'
),
'AboutView'
),
lazy
:
lazyRoute
(()
=>
import
(
'@/modules/help/views/AboutView'
),
'AboutView'
),
},
},
{
{
path
:
'contact'
,
path
:
'contact'
,
lazy
:
lazyRoute
(()
=>
import
(
'@/modules/
my/
help/views/ContactView'
),
'ContactView'
),
lazy
:
lazyRoute
(()
=>
import
(
'@/modules/help/views/ContactView'
),
'ContactView'
),
},
},
...
helpRoutes
,
...
helpRoutes
,
...
libraryRoutes
,
...
libraryRoutes
,
...
bookRoutes
,
...
bookRoutes
,
...
myRoutes
,
...
myRoutes
,
...
purchaseRoutes
,
]
]
export
const
appRoutes
:
RouteObject
[]
=
[
export
const
appRoutes
:
RouteObject
[]
=
[
...
@@ -32,7 +34,7 @@ export const appRoutes: RouteObject[] = [
...
@@ -32,7 +34,7 @@ export const appRoutes: RouteObject[] = [
...
authRoutes
,
...
authRoutes
,
{
{
path
:
'/'
,
path
:
'/'
,
element
:
<
Root
Layout
/>,
element
:
<
Learning
Layout
/>,
children
:
appChildRoutes
,
children
:
appChildRoutes
,
},
},
...
readerRoutes
,
...
readerRoutes
,
...
...
apps/learning/src/
modules/auth
/session.ts
→
apps/learning/src/
stores
/session.ts
浏览文件 @
8d6dd989
import
{
create
}
from
'zustand'
import
{
create
}
from
'zustand'
import
type
{
MemberSession
}
from
'./types'
export
interface
MemberSession
{
members_id
:
number
name
?:
string
head_img
?:
string
phone
:
string
grade_name
?:
string
token
:
string
access_token
:
string
}
const
sessionKey
=
'ebook-learning-session'
const
sessionKey
=
'ebook-learning-session'
let
onSessionExpired
:
(()
=>
void
)
|
undefined
function
readSession
():
MemberSession
|
null
{
function
readSession
():
MemberSession
|
null
{
try
{
try
{
...
@@ -30,20 +38,3 @@ export const useSessionStore = create<SessionState>((set) => ({
...
@@ -30,20 +38,3 @@ export const useSessionStore = create<SessionState>((set) => ({
set
({
session
:
null
})
set
({
session
:
null
})
},
},
}))
}))
export
function
getMemberToken
()
{
return
useSessionStore
.
getState
().
session
?.
token
}
export
function
getMemberSession
()
{
return
useSessionStore
.
getState
().
session
}
export
function
configureSessionExpiry
(
handler
:
()
=>
void
)
{
onSessionExpired
=
handler
}
export
function
expireMemberSession
()
{
useSessionStore
.
getState
().
clearSession
()
onSessionExpired
?.()
}
apps/learning/src/
modules/auth
/device.ts
→
apps/learning/src/
utils
/device.ts
浏览文件 @
8d6dd989
File moved
apps/learning/src/utils/http.test.ts
浏览文件 @
8d6dd989
import
{
AxiosError
,
AxiosHeaders
,
type
AxiosResponse
}
from
'axios'
import
{
AxiosHeaders
,
type
AxiosResponse
,
type
InternalAxiosRequestConfig
}
from
'axios'
import
{
beforeEach
,
describe
,
expect
,
it
,
vi
}
from
'vitest'
import
{
describe
,
expect
,
it
}
from
'vitest'
import
{
useSessionStore
}
from
'@/stores/session'
import
{
http
}
from
'./http'
const
authMocks
=
vi
.
hoisted
(()
=>
({
describe
(
'HTTP client'
,
()
=>
{
expireMemberSession
:
vi
.
fn
(),
it
(
'adds the stored member token to requests'
,
async
()
=>
{
}))
useSessionStore
.
getState
().
setSession
({
members_id
:
1
,
phone
:
'13800000000'
,
token
:
'member-token'
,
access_token
:
'access-token'
,
})
const
requestHandler
=
http
.
interceptors
.
request
.
handlers
?.[
0
]?.
fulfilled
const
config
=
{
headers
:
new
AxiosHeaders
()
}
as
InternalAxiosRequestConfig
vi
.
mock
(
'@/modules/auth/session'
,
()
=>
({
expireMemberSession
:
authMocks
.
expireMemberSession
})
)
const
result
=
await
requestHandler
?.(
config
)
import
{
http
}
from
'./http'
expect
(
result
?.
headers
?.
get
(
'Authorization'
)).
toBe
(
'member-token'
)
})
function
unauthorizedError
()
{
it
(
'rejects business errors'
,
async
()
=>
{
const
config
=
{
headers
:
new
AxiosHeaders
()
}
const
response
=
{
const
response
=
{
data
:
{
code
:
401
,
message
:
'会话已失效'
},
data
:
{
code
:
422
,
message
:
'不可购买'
},
status
:
401
,
statusText
:
'Unauthorized'
,
headers
:
new
AxiosHeaders
(),
config
,
}
as
AxiosResponse
return
new
AxiosError
(
'Unauthorized'
,
'ERR_BAD_REQUEST'
,
config
,
undefined
,
response
)
}
function
unauthorizedResponse
()
{
const
config
=
{
headers
:
new
AxiosHeaders
()
}
return
{
data
:
{
code
:
401
,
message
:
'会话已失效'
},
status
:
200
,
status
:
200
,
statusText
:
'OK'
,
statusText
:
'OK'
,
headers
:
new
AxiosHeaders
(),
headers
:
new
AxiosHeaders
(),
config
,
config
:
{
headers
:
new
AxiosHeaders
()
}
,
}
as
AxiosResponse
}
as
AxiosResponse
}
const
responseHandler
=
http
.
interceptors
.
response
.
handlers
?.[
0
]?.
fulfilled
describe
(
'HTTP authentication expiry'
,
()
=>
{
await
expect
(
Promise
.
resolve
(
responseHandler
?.(
response
))).
rejects
.
toMatchObject
({
beforeEach
(()
=>
authMocks
.
expireMemberSession
.
mockClear
())
code
:
422
,
message
:
'不可购买'
,
it
(
'clears the session directly when the server returns an unauthorized HTTP status'
,
async
()
=>
{
const
rejectionHandler
=
http
.
interceptors
.
response
.
handlers
?.[
0
]?.
rejected
await
expect
(
rejectionHandler
?.(
unauthorizedError
())).
rejects
.
toMatchObject
({
kind
:
'auth'
,
status
:
401
,
})
expect
(
authMocks
.
expireMemberSession
).
toHaveBeenCalledOnce
()
})
})
it
(
'clears the session when the APP envelope returns an unauthorized code'
,
async
()
=>
{
const
fulfillmentHandler
=
http
.
interceptors
.
response
.
handlers
?.[
0
]?.
fulfilled
await
expect
(
Promise
.
resolve
(
fulfillmentHandler
?.(
unauthorizedResponse
())),
).
rejects
.
toMatchObject
({
kind
:
'auth'
,
status
:
401
})
expect
(
authMocks
.
expireMemberSession
).
toHaveBeenCalledOnce
()
})
})
})
})
apps/learning/src/utils/http.ts
浏览文件 @
8d6dd989
import
axios
from
'axios'
import
axios
from
'axios'
import
{
ApiError
,
normalizeApiError
}
from
'@/api/errors'
import
{
message
}
from
'antd'
import
{
applyRequestAuthentication
,
getDevelopmentRequestAuth
}
from
'@/api/http-auth'
import
{
useSessionStore
}
from
'@/stores/session'
import
{
expireMemberSession
}
from
'@/modules/auth/session'
interface
ApiEnvelope
<
T
>
{
code
:
number
message
?:
string
msg
?:
string
data
:
T
}
export
const
http
=
axios
.
create
({
export
const
http
=
axios
.
create
({
withCredentials
:
true
,
withCredentials
:
true
,
headers
:
{
'Content-Type'
:
'application/json'
,
'X-Skip-Sig'
:
'1'
},
headers
:
{
'Content-Type'
:
'application/json'
,
...(
import
.
meta
.
env
.
DEV
?
{
'X-Skip-Sig'
:
'1'
}
:
{}),
},
})
})
http
.
interceptors
.
request
.
use
(
async
(
config
)
=>
{
http
.
interceptors
.
request
.
use
((
config
)
=>
{
const
request
=
await
applyRequestAuthentication
(
const
token
=
useSessionStore
.
getState
().
session
?.
token
{
if
(
token
)
config
.
headers
.
set
(
'Authorization'
,
token
)
baseURL
:
config
.
baseURL
,
url
:
config
.
url
,
method
:
config
.
method
,
params
:
config
.
params
,
data
:
config
.
data
,
headers
:
(
config
.
headers
?.
toJSON
()
??
{})
as
Record
<
string
,
string
>
,
},
getDevelopmentRequestAuth
(),
)
for
(
const
[
key
,
value
]
of
Object
.
entries
(
request
.
headers
))
{
config
.
headers
.
set
(
key
,
value
)
}
return
config
return
config
})
})
http
.
interceptors
.
response
.
use
(
http
.
interceptors
.
response
.
use
(
(
response
)
=>
{
(
response
)
=>
{
if
(
isUnauthorized
(
response
.
status
,
response
.
data
))
{
const
{
data
}
=
response
expireMemberSession
()
if
(
data
?.
code
!==
200
)
{
return
Promise
.
reject
(
new
ApiError
({
kind
:
'auth'
,
status
:
unauthorizedStatus
(
response
.
status
,
response
.
data
)
}))
const
errorMessage
=
data
?.
message
||
data
?.
msg
||
'请求失败'
message
.
error
(
errorMessage
)
if
(
data
?.
code
===
401
||
data
?.
code
===
403
)
{
useSessionStore
.
getState
().
clearSession
()
if
(
location
.
pathname
!==
'/login'
)
location
.
href
=
'/login'
}
const
error
=
new
Error
(
errorMessage
)
as
Error
&
{
code
:
number
}
error
.
code
=
data
.
code
return
Promise
.
reject
(
error
)
}
}
return
response
return
response
},
},
(
error
)
=>
{
(
error
)
=>
{
if
(
axios
.
isAxiosError
(
error
)
&&
isUnauthorized
(
error
.
response
?.
status
,
error
.
response
?.
data
))
expireMemberSession
()
const
response
=
error
.
response
return
Promise
.
reject
(
normalizeApiError
(
error
))
const
data
=
response
?.
data
if
(
!
axios
.
isCancel
(
error
))
{
message
.
error
(
data
?.
message
||
data
?.
msg
||
error
.
message
||
'网络请求失败'
)
if
(
response
?.
status
===
401
||
response
?.
status
===
403
)
{
useSessionStore
.
getState
().
clearSession
()
if
(
location
.
pathname
!==
'/login'
)
location
.
href
=
'/login'
}
}
return
Promise
.
reject
(
error
)
},
},
)
)
function
unauthorizedStatus
(
status
:
number
|
undefined
,
data
:
unknown
)
{
export
async
function
post
<
T
>
(
url
:
string
,
data
?:
unknown
):
Promise
<
T
>
{
if
(
status
===
401
||
status
===
403
)
return
status
const
response
=
await
http
.
post
<
ApiEnvelope
<
T
>>
(
url
,
data
)
const
code
=
getResponseCode
(
data
)
return
response
.
data
.
data
return
code
===
401
||
code
===
403
?
code
:
undefined
}
function
isUnauthorized
(
status
:
number
|
undefined
,
data
:
unknown
)
{
return
unauthorizedStatus
(
status
,
data
)
!==
undefined
}
function
getResponseCode
(
data
:
unknown
)
{
if
(
typeof
data
!==
'object'
||
data
===
null
)
return
undefined
const
value
=
(
data
as
Record
<
string
,
unknown
>
).
code
const
code
=
Number
(
value
)
return
Number
.
isFinite
(
code
)
?
code
:
undefined
}
}
apps/learning/src/utils/password.ts
0 → 100644
浏览文件 @
8d6dd989
import
{
md5
}
from
'js-md5'
export
function
hashPassword
(
password
:
string
)
{
return
md5
(
md5
(
password
))
}
apps/learning/vite.config.ts
浏览文件 @
8d6dd989
...
@@ -13,17 +13,17 @@ export default defineConfig(({ mode }) => {
...
@@ -13,17 +13,17 @@ export default defineConfig(({ mode }) => {
port
:
5173
,
port
:
5173
,
strictPort
:
true
,
strictPort
:
true
,
proxy
:
{
proxy
:
{
'/api/app'
:
{
target
:
'http://127.0.0.1:7421'
,
changeOrigin
:
true
,
rewrite
:
(
path
)
=>
path
.
replace
(
/^
\/
api
\/
app/
,
''
),
},
'/api/admin'
:
{
target
:
'http://127.0.0.1:7419'
,
changeOrigin
:
true
,
rewrite
:
(
path
)
=>
path
.
replace
(
/^
\/
api
\/
admin/
,
''
),
},
'/api'
:
'https://ebook.ezijing.com'
,
'/api'
:
'https://ebook.ezijing.com'
,
// '/api/app': {
// target: 'http://127.0.0.1:7421',
// changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api\/app/, ''),
// },
// '/api/admin': {
// target: 'http://127.0.0.1:7419',
// changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api\/admin/, ''),
// },
},
},
},
},
build
:
{
build
:
{
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论