Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
R
river-refunds
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Qian Xu
river-refunds
Commits
07f352eb
Commit
07f352eb
authored
Sep 08, 2026
by
Nicholas
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
增强用户体验
parent
422e6426
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
197 additions
and
27 deletions
+197
-27
manifest.json
manifest.json
+2
-2
App.jsx
src/App.jsx
+70
-16
styles.css
src/styles.css
+125
-9
No files found.
manifest.json
View file @
07f352eb
{
"name"
:
"Streams River Refunds UI"
,
"version"
:
"1.
7
"
,
"version"
:
"1.
9
"
,
"build"
:
"abc"
,
"description"
:
"river refunds"
,
"date"
:
"2026-0
8-19 00:04:11
"
,
"date"
:
"2026-0
9-08 15:14:19
"
,
"author"
:
"Nicholas"
,
"spa"
:
false
}
src/App.jsx
View file @
07f352eb
import
{
useEffect
,
use
Memo
,
useState
}
from
'react'
;
import
{
useEffect
,
use
Id
,
useMemo
,
useRef
,
useState
}
from
'react'
;
import
{
createDeviationService
,
fetchDeviationPageData
,
...
...
@@ -80,19 +80,36 @@ function buildPassengers(order, terms) {
},
[]);
}
function
PassengerAgreement
({
checkedIds
,
onToggle
,
passenger
})
{
function
PassengerAgreement
({
checkedIds
,
disabled
,
onToggle
,
passenger
,
showErrors
})
{
const
agreementId
=
useId
();
const
checked
=
checkedIds
.
includes
(
passenger
.
id
);
const
showError
=
showErrors
&&
!
checked
;
return
(
<
div
className=
"agreement-block"
>
<
label
className=
"agreement-check"
>
<
label
className=
{
`agreement-check${checked ? ' is-checked' : ''}${showError ? ' has-error' : ''}`
}
>
<
input
type=
"checkbox"
checked=
{
checked
}
disabled=
{
disabled
}
aria
-
invalid=
{
showError
||
undefined
}
aria
-
describedby=
{
showError
?
`${agreementId}-error`
:
undefined
}
onChange=
{
(
event
)
=>
onToggle
(
passenger
.
id
,
event
.
target
.
checked
)
}
/>
<
span
className=
"term-desc"
>
{
passenger
.
term
.
desc
}
</
span
>
<
span
className=
"agreement-copy"
>
<
span
className=
"agreement-prompt"
>
{
checked
?
'已确认本方案及条款'
:
'请勾选确认本方案及条款'
}
</
span
>
<
span
className=
"term-desc"
>
{
passenger
.
term
.
desc
}
</
span
>
</
span
>
</
label
>
{
showError
&&
(
<
p
className=
"agreement-error"
id=
{
`${agreementId}-error`
}
>
请先勾选确认此项。
</
p
>
)
}
<
p
className=
"term-heading"
>
补偿条款与细则:
</
p
>
<
div
className=
"term-content"
...
...
@@ -111,8 +128,10 @@ export default function App() {
const
[
code
,
setCode
]
=
useState
(
''
);
const
[
mobileToken
,
setMobileToken
]
=
useState
(
''
);
const
[
checkedIds
,
setCheckedIds
]
=
useState
([]);
const
[
showAgreementErrors
,
setShowAgreementErrors
]
=
useState
(
false
);
const
[
submitting
,
setSubmitting
]
=
useState
(
false
);
const
[
countdown
,
setCountdown
]
=
useState
(
0
);
const
agreementsRef
=
useRef
(
null
);
const
passengers
=
useMemo
(()
=>
buildPassengers
(
order
,
terms
),
[
order
,
terms
]);
// 开头文案(含河流名)从中台条款的 intro 字段取,同一订单各乘客一致,取第一位即可;没有则回退到写死文案。
...
...
@@ -201,6 +220,20 @@ export default function App() {
});
}
function
goToUncheckedAgreement
()
{
setShowAgreementErrors
(
true
);
const
checkbox
=
agreementsRef
.
current
?.
querySelector
(
'input[type="checkbox"]:not(:checked)'
);
if
(
!
checkbox
)
{
return
;
}
checkbox
.
focus
({
preventScroll
:
true
});
checkbox
.
closest
(
'.agreement-check'
)?.
scrollIntoView
({
behavior
:
window
.
matchMedia
(
'(prefers-reduced-motion: reduce)'
).
matches
?
'auto'
:
'smooth'
,
block
:
'center'
,
});
}
async
function
handleSendSmsCode
()
{
if
(
!
phone
)
{
window
.
alert
(
'请输入手机号码'
);
...
...
@@ -219,23 +252,22 @@ export default function App() {
}
async
function
handleSubmit
()
{
if
(
!
phone
)
{
window
.
alert
(
'请输入手机号码'
);
if
(
submitting
||
passengers
.
length
===
0
)
{
return
;
}
if
(
!
code
)
{
window
.
alert
(
'请输入验证码'
);
if
(
!
allChecked
)
{
goToUncheckedAgreement
(
);
return
;
}
if
(
checkedIds
.
length
<
1
)
{
window
.
alert
(
'请
选择方案
'
);
if
(
!
phone
)
{
window
.
alert
(
'请
输入手机号码
'
);
return
;
}
if
(
checkedIds
.
length
!==
passengers
.
length
)
{
window
.
alert
(
'请
勾选所有用户
'
);
if
(
!
code
)
{
window
.
alert
(
'请
输入验证码
'
);
return
;
}
...
...
@@ -322,13 +354,15 @@ export default function App() {
)
}
</
section
>
<
section
className=
"agreements-section"
>
<
section
className=
"agreements-section"
ref=
{
agreementsRef
}
>
{
passengers
.
map
((
passenger
)
=>
(
<
PassengerAgreement
checkedIds=
{
checkedIds
}
disabled=
{
submitting
}
key=
{
passenger
.
id
}
onToggle=
{
togglePassenger
}
passenger=
{
passenger
}
showErrors=
{
showAgreementErrors
}
/>
))
}
</
section
>
...
...
@@ -341,9 +375,23 @@ export default function App() {
</
section
>
<
section
className=
"form-section"
>
{
passengers
.
length
>
0
&&
(
<
div
className=
{
`agreement-progress${allChecked ? ' is-complete' : ''}`
}
>
<
div
role=
"status"
aria
-
live=
"polite"
aria
-
atomic=
"true"
>
<
strong
>
已确认
{
checkedIds
.
length
}
/
{
passengers
.
length
}
项
</
strong
>
<
p
>
{
allChecked
?
'全部方案已确认,请完成手机验证后提交。'
:
'请确认全部方案后提交。'
}
</
p
>
</
div
>
{
!
allChecked
&&
(
<
button
className=
"agreement-jump"
type=
"button"
onClick=
{
goToUncheckedAgreement
}
>
前往确认
</
button
>
)
}
</
div
>
)
}
<
input
type=
"text"
value=
{
phone
}
disabled=
{
submitting
}
placeholder=
"手机号码"
onChange=
{
(
event
)
=>
setPhone
(
event
.
target
.
value
)
}
/>
...
...
@@ -351,15 +399,21 @@ export default function App() {
<
input
type=
"text"
value=
{
code
}
disabled=
{
submitting
}
placeholder=
"验证码"
onChange=
{
(
event
)
=>
setCode
(
event
.
target
.
value
)
}
/>
<
button
type=
"button"
disabled=
{
countdown
>
0
}
onClick=
{
handleSendSmsCode
}
>
<
button
type=
"button"
disabled=
{
submitting
||
countdown
>
0
}
onClick=
{
handleSendSmsCode
}
>
{
countdown
>
0
?
`已发送(${countdown})`
:
'获取验证码'
}
</
button
>
</
div
>
<
button
className=
"submit-button"
type=
"button"
disabled=
{
!
allChecked
}
onClick=
{
handleSubmit
}
>
提交
<
button
className=
"submit-button"
type=
"button"
disabled=
{
submitting
||
passengers
.
length
===
0
}
onClick=
{
handleSubmit
}
>
{
submitting
?
'提交中…'
:
'提交'
}
</
button
>
</
section
>
...
...
src/styles.css
View file @
07f352eb
...
...
@@ -89,25 +89,92 @@ button:disabled {
}
.agreement-check
{
display
:
block
;
display
:
flex
;
align-items
:
flex-start
;
gap
:
12px
;
min-height
:
48px
;
margin
:
0
;
padding
:
0
;
padding
:
16px
;
background
:
#f4f6f9
;
border
:
2px
solid
#738199
;
border-radius
:
6px
;
cursor
:
pointer
;
scroll-margin-block
:
24px
;
}
.agreement-check.is-checked
{
background
:
#edf2f8
;
border-color
:
#0d1c34
;
}
.agreement-check.has-error
{
background
:
#fff4f1
;
border-color
:
#b42318
;
}
.agreement-check
input
{
width
:
13px
;
height
:
13px
;
margin
:
3px
6px
0
0
;
vertical-align
:
text-top
;
appearance
:
none
;
-webkit-appearance
:
none
;
display
:
grid
;
place-content
:
center
;
flex
:
0
0
26px
;
width
:
26px
;
height
:
26px
;
margin
:
0
;
background
:
#ffffff
;
border
:
2px
solid
#44536a
;
border-radius
:
4px
;
accent-color
:
#0d1c34
;
cursor
:
pointer
;
}
.agreement-check
input
:checked
{
background
:
#0d1c34
;
border-color
:
#0d1c34
;
}
.agreement-check
input
:checked::after
{
content
:
''
;
width
:
7px
;
height
:
13px
;
margin-top
:
-3px
;
border
:
solid
#ffffff
;
border-width
:
0
3px
3px
0
;
transform
:
rotate
(
45deg
);
}
.agreement-check
input
:focus-visible
,
.agreement-jump
:focus-visible
,
.submit-button
:focus-visible
{
outline
:
3px
solid
#3265a4
;
outline-offset
:
3px
;
}
.agreement-copy
{
min-width
:
0
;
overflow-wrap
:
anywhere
;
}
.agreement-prompt
{
display
:
block
;
color
:
#0d1c34
;
font-weight
:
700
;
line-height
:
26px
;
}
.term-desc
{
display
:
inline
;
display
:
block
;
margin-top
:
8px
;
}
.agreement-error
{
margin
:
8px
0
0
;
color
:
#b42318
;
font-weight
:
600
;
}
.term-heading
{
margin
:
0.25rem
1rem
1rem
;
margin
:
1rem
0
;
font-weight
:
700
;
}
...
...
@@ -127,6 +194,39 @@ button:disabled {
padding
:
3rem
0
;
}
.agreement-progress
{
display
:
flex
;
flex-wrap
:
wrap
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
8px
16px
;
padding
:
16px
;
color
:
#0d1c34
;
background
:
#f4f6f9
;
border
:
1px
solid
#738199
;
border-radius
:
6px
;
}
.agreement-progress.is-complete
{
border-color
:
#0d1c34
;
}
.agreement-progress
p
{
margin
:
4px
0
0
;
}
.agreement-jump
{
flex-shrink
:
0
;
min-height
:
44px
;
padding
:
8px
12px
;
color
:
#0d1c34
;
background
:
#ffffff
;
border
:
1px
solid
#738199
;
border-radius
:
4px
;
font-weight
:
600
;
cursor
:
pointer
;
}
.form-section
input
{
width
:
100%
;
height
:
40px
;
...
...
@@ -168,11 +268,12 @@ button:disabled {
.submit-button
{
width
:
100%
;
height
:
4
0
px
;
height
:
4
8
px
;
color
:
#ffffff
;
background
:
#0d1c34
;
border
:
0
;
border-radius
:
2px
;
cursor
:
pointer
;
}
.loading-mask
{
...
...
@@ -241,9 +342,24 @@ button:disabled {
width
:
100%
!important
;
}
.form-section
{
width
:
100%
;
}
.term-desc
,
.term-heading
,
.term-content
{
margin-left
:
0
;
}
}
@media
(
forced-colors
:
active
)
{
.agreement-check
input
{
appearance
:
auto
;
-webkit-appearance
:
auto
;
}
.agreement-check
input
:checked::after
{
content
:
none
;
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment