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
422e6426
Commit
422e6426
authored
Sep 01, 2026
by
Nicholas
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
配置化增强
parent
5347a1b7
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
135 additions
and
27 deletions
+135
-27
manifest.json
manifest.json
+2
-2
App.jsx
src/App.jsx
+41
-12
api.js
src/api.js
+1
-0
mockData.js
src/mockData.js
+82
-0
styles.css
src/styles.css
+9
-13
No files found.
manifest.json
View file @
422e6426
{
{
"name"
:
"Streams River Refunds UI"
,
"name"
:
"Streams River Refunds UI"
,
"version"
:
"1.
1
"
,
"version"
:
"1.
7
"
,
"build"
:
"abc"
,
"build"
:
"abc"
,
"description"
:
"river refunds"
,
"description"
:
"river refunds"
,
"date"
:
"2026-0
7-15 18:34:09
"
,
"date"
:
"2026-0
8-19 00:04:11
"
,
"author"
:
"Nicholas"
,
"author"
:
"Nicholas"
,
"spa"
:
false
"spa"
:
false
}
}
src/App.jsx
View file @
422e6426
...
@@ -7,7 +7,7 @@ import {
...
@@ -7,7 +7,7 @@ import {
useMock
,
useMock
,
verifySmsCode
,
verifySmsCode
,
}
from
'./api.js'
;
}
from
'./api.js'
;
import
{
mockOrder
,
mockTerms
}
from
'./mockData.js'
;
import
{
mockOrder
,
mock
Scenarios
,
mock
Terms
}
from
'./mockData.js'
;
const
assetBaseUrl
=
import
.
meta
.
env
.
BASE_URL
;
const
assetBaseUrl
=
import
.
meta
.
env
.
BASE_URL
;
...
@@ -19,6 +19,11 @@ function getPassengerId() {
...
@@ -19,6 +19,11 @@ function getPassengerId() {
return
new
URLSearchParams
(
window
.
location
.
search
).
get
(
'id'
)
||
''
;
return
new
URLSearchParams
(
window
.
location
.
search
).
get
(
'id'
)
||
''
;
}
}
// 预览用:URL 带 test_id 时走本地 mock,不接 SF
function
getTestId
()
{
return
new
URLSearchParams
(
window
.
location
.
search
).
get
(
'test_id'
)
||
''
;
}
function
getAgreementTerm
(
openAgreement
,
terms
)
{
function
getAgreementTerm
(
openAgreement
,
terms
)
{
const
matchedTerm
=
const
matchedTerm
=
terms
.
find
((
item
)
=>
item
.
code
===
openAgreement
.
selected_term
)
||
terms
.
find
((
item
)
=>
item
.
code
===
openAgreement
.
selected_term
)
||
...
@@ -37,6 +42,7 @@ function getAgreementTerm(openAgreement, terms) {
...
@@ -37,6 +42,7 @@ function getAgreementTerm(openAgreement, terms) {
openAgreement
.
deviationAgreementDesc
||
openAgreement
.
deviationAgreementDesc
||
openAgreement
.
deviationAgreementDescription
||
openAgreement
.
deviationAgreementDescription
||
''
,
''
,
intro
:
''
,
content
:
content
:
openAgreement
.
content
||
openAgreement
.
content
||
openAgreement
.
term_content
||
openAgreement
.
term_content
||
...
@@ -85,7 +91,6 @@ function PassengerAgreement({ checkedIds, onToggle, passenger }) {
...
@@ -85,7 +91,6 @@ function PassengerAgreement({ checkedIds, onToggle, passenger }) {
checked=
{
checked
}
checked=
{
checked
}
onChange=
{
(
event
)
=>
onToggle
(
passenger
.
id
,
event
.
target
.
checked
)
}
onChange=
{
(
event
)
=>
onToggle
(
passenger
.
id
,
event
.
target
.
checked
)
}
/>
/>
<
span
className=
"passenger-name"
>
{
passenger
.
passengerName
}
</
span
>
<
span
className=
"term-desc"
>
{
passenger
.
term
.
desc
}
</
span
>
<
span
className=
"term-desc"
>
{
passenger
.
term
.
desc
}
</
span
>
</
label
>
</
label
>
<
p
className=
"term-heading"
>
补偿条款与细则:
</
p
>
<
p
className=
"term-heading"
>
补偿条款与细则:
</
p
>
...
@@ -110,6 +115,8 @@ export default function App() {
...
@@ -110,6 +115,8 @@ export default function App() {
const
[
countdown
,
setCountdown
]
=
useState
(
0
);
const
[
countdown
,
setCountdown
]
=
useState
(
0
);
const
passengers
=
useMemo
(()
=>
buildPassengers
(
order
,
terms
),
[
order
,
terms
]);
const
passengers
=
useMemo
(()
=>
buildPassengers
(
order
,
terms
),
[
order
,
terms
]);
// 开头文案(含河流名)从中台条款的 intro 字段取,同一订单各乘客一致,取第一位即可;没有则回退到写死文案。
const
letterIntro
=
passengers
.
find
((
passenger
)
=>
passenger
.
term
?.
intro
)?.
term
.
intro
||
''
;
const
allChecked
=
passengers
.
length
>
0
&&
checkedIds
.
length
===
passengers
.
length
;
const
allChecked
=
passengers
.
length
>
0
&&
checkedIds
.
length
===
passengers
.
length
;
useEffect
(()
=>
{
useEffect
(()
=>
{
...
@@ -120,6 +127,19 @@ export default function App() {
...
@@ -120,6 +127,19 @@ export default function App() {
setPageError
(
''
);
setPageError
(
''
);
try
{
try
{
// 预览用 mock:仅当 URL 带 test_id 时生效,真实 SF 流程不受影响
const
testId
=
getTestId
();
if
(
testId
)
{
const
scenario
=
mockScenarios
[
testId
];
if
(
scenario
)
{
setOrder
(
scenario
.
order
);
setTerms
(
scenario
.
terms
);
}
else
{
setPageError
(
`未找到 test_id=
${
testId
}
对应的 mock 数据`
);
}
return
;
}
if
(
useMock
)
{
if
(
useMock
)
{
setOrder
(
mockOrder
);
setOrder
(
mockOrder
);
setTerms
(
mockTerms
);
setTerms
(
mockTerms
);
...
@@ -267,7 +287,7 @@ export default function App() {
...
@@ -267,7 +287,7 @@ export default function App() {
<
main
className=
"page-shell"
>
<
main
className=
"page-shell"
>
<
header
className=
"page-header"
>
<
header
className=
"page-header"
>
<
img
className=
"logo"
src=
{
assetUrl
(
'/pc_nav_logo.png'
)
}
alt=
""
/>
<
img
className=
"logo"
src=
{
assetUrl
(
'/pc_nav_logo.png'
)
}
alt=
""
/>
<
h1
>
维京游轮
行程调整
确认函
</
h1
>
<
h1
>
维京游轮
低水位补偿方案
确认函
</
h1
>
</
header
>
</
header
>
<
section
className=
"passenger-section"
>
<
section
className=
"passenger-section"
>
...
@@ -283,14 +303,23 @@ export default function App() {
...
@@ -283,14 +303,23 @@ export default function App() {
</
section
>
</
section
>
<
section
className=
"letter-section"
>
<
section
className=
"letter-section"
>
<
p
>
尊敬的贵宾:
</
p
>
{
letterIntro
?
(
<
p
>
感谢您选择跟随维京游轮探游欧洲。
</
p
>
<
div
dangerouslySetInnerHTML=
{
{
__html
:
letterIntro
}
}
/>
<
p
>
)
:
(
这段旅程本应成为您探索欧洲的美好回忆,但因船舶临时维修需求,我们不得不在德国布雷萨赫港暂停航行。尽管我们的团队全力调整行程以保障您的核心游览体验,我们深知,此次变动仍可能影响了您的旅行时光。对此,我们向您致以最诚挚的歉意。您的满意与安全始终是我们最高的优先考量。
<>
</
p
>
<
p
>
尊敬的宾客:
</
p
>
<
p
>
<
p
>
感谢您选择跟随维京游轮畅游欧洲!
</
p
>
我们深知此次行程调整给您带来了诸多不便,为此我们深感歉意。经过与航行团队的深入沟通和审慎评估,我们希望能以最真诚的方式弥补您的遗憾。为感谢您在旅程中的耐心理解、对船员的包容支持,以及对我们工作的信任,我们诚心为您提供以下补偿方案:
<
p
>
</
p
>
受近期欧洲持续高温及降雨不均影响,多瑙河局部流域水位处于低位运行,出于对航行安全的绝对考量,我们不得不对将本次航程的航行计划及岸上安排进行临时调整。
</
p
>
<
p
>
对于由此给您带来的不便与遗憾,我们致以最诚挚的歉意。
</
p
>
<
p
>
维京游轮秉持一贯高品质的贴心服务,考虑到此次行程调整对您造成的不便,经双方沟通,您最终同意选择我们提供的以下补偿方案:
</
p
>
</>
)
}
</
section
>
</
section
>
<
section
className=
"agreements-section"
>
<
section
className=
"agreements-section"
>
...
@@ -308,7 +337,7 @@ export default function App() {
...
@@ -308,7 +337,7 @@ export default function App() {
<
p
>
<
p
>
您勾选上面的方框和通过手机验证签署本确认函,表示您理解并确认以上方案为此次行程调整的最终和解。
您勾选上面的方框和通过手机验证签署本确认函,表示您理解并确认以上方案为此次行程调整的最终和解。
</
p
>
</
p
>
<
p
>
感谢您的理解与信任,期待您
的登船。
</
p
>
<
p
>
感谢您的理解与信任,期待您
下一次登船!
</
p
>
</
section
>
</
section
>
<
section
className=
"form-section"
>
<
section
className=
"form-section"
>
...
...
src/api.js
View file @
422e6426
...
@@ -57,6 +57,7 @@ function normalizeTermRecord(record) {
...
@@ -57,6 +57,7 @@ function normalizeTermRecord(record) {
return
{
return
{
code
:
data
.
code
||
record
?.
code
||
record
?.
slug
||
data
.
slug
||
''
,
code
:
data
.
code
||
record
?.
code
||
record
?.
slug
||
data
.
slug
||
''
,
desc
:
data
.
desc
||
record
?.
desc
||
record
?.
description
||
data
.
description
||
''
,
desc
:
data
.
desc
||
record
?.
desc
||
record
?.
description
||
data
.
description
||
''
,
intro
:
data
.
intro
||
''
,
content
:
content
:
data
.
content
||
data
.
content
||
record
?.
content
||
record
?.
content
||
...
...
src/mockData.js
View file @
422e6426
...
@@ -46,3 +46,85 @@ export const mockTerms = [
...
@@ -46,3 +46,85 @@ export const mockTerms = [
`
,
`
,
},
},
];
];
// ===== 预览用 mock(仅当 URL 带 test_id 时生效,不影响真实 SF 流程)=====
const
MOCK_FCC_CONTENT
=
'<p>维京游轮未来航次代金券(以下简称"代金券")使用范围如下:</p><ul class="term_detail"><li>1. 代金券有效期两年(至12月31日2028年),并可在有效期内用于 www.vikingcruises.cn 维京游轮官网公布在售的任意航次,航次及预订条款详情请见官网,并以官网公布价格为准。</li><li>2. 代金券可在有效期内用于预订维京游轮航次船票费用,或用于升级房型。</li><li>3. 此代金券每人在单个航次预订中最多可使用一张。</li><li>4. 代金券可与当季优惠及维京客专属优惠进行组合叠加使用。</li><li>5. 代金券无法用于抵扣附加升级游览项目、机票、高铁票及船上消费等其他费用。</li><li>6. 代金券不能兑换现金。</li><li>7. 代金券将在您签署本《方案确认函》后,五至七个工作日内自动发放至船票订单主要联系人的"维京好友"微信小程序账户中。</li></ul>'
;
const
MOCK_CASH_CONTENT
=
'<p>温馨提示:</p><ul class="term_detail"><li>1. 我们将在您签署本《方案确认函》之日起两周内完成退款,款项到账日期取决于收款方的银行处理流程,预计需要额外五个工作日(以上退款周期仅供参考,以实际情况为准)。</li><li>2. 以上费用的退款将按原付款路径返回至付款人账户中。</li></ul>'
;
function
mockIntro
(
river
)
{
return
(
'<p>尊敬的宾客:</p>'
+
'<p>感谢您选择跟随维京游轮畅游欧洲!</p>'
+
`<p>受近期欧洲持续高温及降雨不均影响,
${
river
}
局部流域水位处于低位运行,出于对航行安全的绝对考量,我们不得不对将本次航程的航行计划及岸上安排进行临时调整。</p>`
+
'<p>对于由此给您带来的不便与遗憾,我们致以最诚挚的歉意。</p>'
+
'<p>维京游轮秉持一贯高品质的贴心服务,考虑到此次行程调整对您造成的不便,经双方沟通,您最终同意选择我们提供的以下补偿方案:</p>'
);
}
function
buildMockScenario
({
prefix
,
booking
,
itinerary
,
river
,
days
})
{
const
fccCode
=
`
${
prefix
}
FCC`
;
const
cashCode
=
`
${
prefix
}
Cash refund`
;
const
intro
=
mockIntro
(
river
);
return
{
order
:
{
booking_number
:
booking
,
package_departure_name
:
itinerary
,
passengers
:
[
{
id
:
'mock-pax-fcc'
,
id_number
:
'310***********888'
,
name
:
'测试乘客一'
,
passport_number
:
'E00000888'
,
deviation_agreements
:
[
{
id
:
'mock-agreement-fcc'
,
status
:
'Open'
,
selected_term
:
fccCode
,
deviationAgreementCode
:
fccCode
},
],
},
{
id
:
'mock-pax-cash'
,
id_number
:
'310***********999'
,
name
:
'测试乘客二'
,
passport_number
:
'E00000999'
,
deviation_agreements
:
[
{
id
:
'mock-agreement-cash'
,
status
:
'Open'
,
selected_term
:
cashCode
,
deviationAgreementCode
:
cashCode
},
],
},
],
},
terms
:
[
{
code
:
fccCode
,
desc
:
`未来航次代金券:等值于
${
days
}
天受影响天数之实付船票费用的75%的代金券补偿。`
,
intro
,
content
:
MOCK_FCC_CONTENT
,
},
{
code
:
cashCode
,
desc
:
`现金:等值于
${
days
}
天受影响天数之实付船票费用的35%的现金补偿。`
,
intro
,
content
:
MOCK_CASH_CONTENT
,
},
],
};
}
// key = test_id(取今天 code 去掉 FCC/Cash 后缀的前缀)
export
const
mockScenarios
=
{
'2026-07-30-RHINE_2026_11A_AMSBAS'
:
buildMockScenario
({
prefix
:
'2026-07-30-RHINE_2026_11A_AMSBAS-'
,
booking
:
'VCC-RHINE-TEST'
,
itinerary
:
'2026年7月30日出发的11日莱茵河魅力之旅(阿姆斯特丹 – 巴塞尔)'
,
river
:
'莱茵河'
,
days
:
3
,
}),
'2026-08-03-DANUBE_2026_11A_BUDDEG'
:
buildMockScenario
({
prefix
:
'2026-08-03-DANUBE_2026_11A_BUDDEG-'
,
booking
:
'VCC-DANUBE-TEST'
,
itinerary
:
'2026年8月3日出发的11日多瑙河浪漫之旅(布达佩斯 – 代根多夫)'
,
river
:
'多瑙河'
,
days
:
6
,
}),
};
src/styles.css
View file @
422e6426
...
@@ -55,16 +55,12 @@ button:disabled {
...
@@ -55,16 +55,12 @@ button:disabled {
border-bottom
:
1px
solid
#000000
;
border-bottom
:
1px
solid
#000000
;
}
}
.passenger-row
span
{
.passenger-row
{
display
:
inline-block
;
margin-bottom
:
0.5rem
;
}
.passenger-row
span
:first-child
{
width
:
38%
;
}
}
.passenger-row
span
:last-child
{
.passenger-row
span
{
width
:
60%
;
display
:
block
;
}
}
.letter-section
{
.letter-section
{
...
@@ -80,10 +76,14 @@ button:disabled {
...
@@ -80,10 +76,14 @@ button:disabled {
}
}
.agreement-block
{
.agreement-block
{
padding
:
0
;
padding
:
1.5rem
0
;
border-bottom
:
1px
solid
rgba
(
0
,
0
,
0
,
0.16
);
border-bottom
:
1px
solid
rgba
(
0
,
0
,
0
,
0.16
);
}
}
.agreement-block
:first-child
{
padding-top
:
0
;
}
.agreement-block
:last-child
{
.agreement-block
:last-child
{
border-bottom
:
0
;
border-bottom
:
0
;
}
}
...
@@ -102,10 +102,6 @@ button:disabled {
...
@@ -102,10 +102,6 @@ button:disabled {
accent-color
:
#0d1c34
;
accent-color
:
#0d1c34
;
}
}
.passenger-name
{
font-weight
:
700
;
}
.term-desc
{
.term-desc
{
display
:
inline
;
display
:
inline
;
}
}
...
...
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