Commit 422e6426 authored by Nicholas's avatar Nicholas

配置化增强

parent 5347a1b7
{ {
"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-07-15 18:34:09", "date": "2026-08-19 00:04:11",
"author": "Nicholas", "author": "Nicholas",
"spa": false "spa": false
} }
...@@ -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, mockScenarios, mockTerms } 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">
......
...@@ -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 ||
......
...@@ -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,
}),
};
...@@ -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;
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment