Commit 6dac75af authored by Lily Wang's avatar Lily Wang

update

parents
VITE_APP_ENV=development
VITE_MESSAGE_BASE_URL=https://api-staging.cmviking.com/message
VITE_STREAMS_TYPE=ticket
VITE_STREAMS_TICKET=cydz5uLSWULm1gu5ACUWEXor1mrrBzr0
VITE_MIDDLE_QUERY_DEVIATION_PATH=/client/salesforce/call/DeviationAgreementService
VITE_MIDDLE_CREATE_DEVIATION_PATH=/client/salesforce/call/DeviationAgreementService
VITE_USE_MOCK=false
VITE_MESSAGE_BASE_URL=https://api.cmviking.com/message
VITE_APP_ENV=production
VITE_STREAMS_BASE_URL=https://streams.vikingcruises.cn/api/
VITE_STREAMS_TYPE=ticket
VITE_STREAMS_TICKET=ODJuvaIno8PUnnHhCCZSuFpJVD7ZJCJh
VITE_MIDDLE_QUERY_DEVIATION_PATH=/client/salesforce/call/DeviationAgreementService
VITE_MIDDLE_CREATE_DEVIATION_PATH=/client/salesforce/call/DeviationAgreementService
VITE_USE_MOCK=false
VITE_APP_ENV=production
VITE_MESSAGE_BASE_URL=https://api.cmviking.com/message
VITE_STREAMS_BASE_URL=https://streams.vikingcruises.cn/api/
VITE_STREAMS_TYPE=ticket
VITE_STREAMS_TICKET=ODJuvaIno8PUnnHhCCZSuFpJVD7ZJCJh
VITE_MIDDLE_QUERY_DEVIATION_PATH=/client/salesforce/call/DeviationAgreementService
VITE_MIDDLE_CREATE_DEVIATION_PATH=/client/salesforce/call/DeviationAgreementService
VITE_USE_MOCK=false
VITE_APP_ENV=staging
VITE_MESSAGE_BASE_URL=https://api-staging.cmviking.com/message
VITE_STREAMS_BASE_URL=https://viking-test.streams7.com/api/
VITE_STREAMS_TYPE=ticket
VITE_STREAMS_TICKET=cydz5uLSWULm1gu5ACUWEXor1mrrBzr0
VITE_MIDDLE_QUERY_DEVIATION_PATH=/client/salesforce/call/DeviationAgreementService
VITE_MIDDLE_CREATE_DEVIATION_PATH=/client/salesforce/call/DeviationAgreementService
VITE_USE_MOCK=false
node_modules
dist
.env
.DS_Store
# River Refunds React App
从 `page-river-refunds.php` 抽出的独立 React 页面。
## 本地运行
```bash
npm install
npm run dev
```
访问:`http://localhost:5173/?id=PASSENGER_ID`
连接测试环境:
```bash
npm run dev:staging
```
连接生产环境:
```bash
npm run dev:production
```
## 接口配置
项目只走中台接口,鉴权方式参考 `confirmation/src/services/streamsRequest.js`。
默认行程调整接口:
- `GET /client/salesforce/call/DeviationAgreementService?PassengerID=xxx`
- `POST /client/salesforce/call/DeviationAgreementService`
本地开发时 Vite 会把 `/api2` 代理到测试中台;生产环境默认使用 `https://streams.vikingcruises.cn/api/`。
## 打包部署
应用按 `/river-refunds/` 子路径打包,静态资源和 manifest 都会使用这个 base。
```bash
npm run build:staging
npm run build:production
```
产物目录:`dist/`
默认不走 mock 数据。只有需要本地演示交互时,才设置:
```bash
VITE_USE_MOCK=true
```
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0d1c34" />
<meta name="description" content="维京游轮行程调整确认函" />
<link rel="manifest" href="/river-refunds/manifest.json" />
<title>维京游轮行程调整确认函</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
This diff is collapsed.
{
"scripts": {
"dev": "vite --host 0.0.0.0 --port 5173 --strictPort",
"dev:development": "vite --mode development --host 0.0.0.0 --port 5173 --strictPort",
"dev:staging": "vite --mode staging --host 0.0.0.0 --port 5173 --strictPort",
"dev:production": "vite --mode production --host 0.0.0.0 --port 5173 --strictPort",
"build": "vite build",
"build:staging": "vite build --mode staging",
"build:production": "vite build --mode production",
"preview": "vite preview --host 0.0.0.0 --port 4173"
},
"dependencies": {
"@vitejs/plugin-react": "latest",
"vite": "latest",
"react": "latest",
"react-dom": "latest"
},
"devDependencies": {}
}
{
"name": "River Refunds",
"version": "1.0.0",
"build": "river-refunds",
"description": "Viking river refunds confirmation app",
"date": "2026-07-08",
"author": "Viking"
}
import { useEffect, useMemo, useState } from 'react';
import {
createDeviationService,
fetchDeviationPageData,
sendSmsCode,
useMock,
verifySmsCode,
} from './api.js';
import { mockOrder, mockTerms } from './mockData.js';
const assetBaseUrl = import.meta.env.BASE_URL;
function assetUrl(path) {
return `${assetBaseUrl}${path.replace(/^\//, '')}`;
}
function getPassengerId() {
return new URLSearchParams(window.location.search).get('id') || '';
}
function getAgreementTerm(openAgreement, terms) {
const matchedTerm =
terms.find((item) => item.code === openAgreement.selected_term) ||
terms.find((item) => item.code === openAgreement.deviationAgreementCode);
if (matchedTerm) {
return matchedTerm;
}
return {
code: openAgreement.selected_term || openAgreement.deviationAgreementCode || '',
desc:
openAgreement.desc ||
openAgreement.term_desc ||
openAgreement.termDescription ||
openAgreement.deviationAgreementDesc ||
openAgreement.deviationAgreementDescription ||
'',
content:
openAgreement.content ||
openAgreement.term_content ||
openAgreement.termContent ||
openAgreement.deviationAgreementContent ||
'',
};
}
function buildPassengers(order, terms) {
if (!order || !Array.isArray(order.passengers)) {
return [];
}
return order.passengers.reduce((result, passenger) => {
const agreements = Array.isArray(passenger.deviation_agreements)
? passenger.deviation_agreements
: [];
const openAgreement = agreements.find((agreement) => agreement.status === 'Open');
if (!openAgreement) {
return result;
}
result.push({
id: passenger.id,
idNumber: passenger.id_number,
passengerName: passenger.name,
passportNumber: passenger.passport_number,
openAgreement,
term: getAgreementTerm(openAgreement, terms),
});
return result;
}, []);
}
function PassengerAgreement({ checkedIds, onToggle, passenger }) {
const checked = checkedIds.includes(passenger.id);
return (
<div className="agreement-block">
<label className="agreement-check">
<input
type="checkbox"
checked={checked}
onChange={(event) => onToggle(passenger.id, event.target.checked)}
/>
<span className="passenger-name">{passenger.passengerName}</span>
<span className="term-desc">{passenger.term.desc}</span>
</label>
<p className="term-heading">补偿条款与细则:</p>
<div
className="term-content"
dangerouslySetInnerHTML={{ __html: passenger.term.content || '' }}
/>
</div>
);
}
export default function App() {
const [order, setOrder] = useState(null);
const [terms, setTerms] = useState([]);
const [pageError, setPageError] = useState('');
const [pageLoading, setPageLoading] = useState(true);
const [phone, setPhone] = useState('');
const [code, setCode] = useState('');
const [mobileToken, setMobileToken] = useState('');
const [checkedIds, setCheckedIds] = useState([]);
const [submitting, setSubmitting] = useState(false);
const [countdown, setCountdown] = useState(0);
const passengers = useMemo(() => buildPassengers(order, terms), [order, terms]);
const allChecked = passengers.length > 0 && checkedIds.length === passengers.length;
useEffect(() => {
let active = true;
async function loadPageData() {
setPageLoading(true);
setPageError('');
try {
if (useMock) {
setOrder(mockOrder);
setTerms(mockTerms);
return;
}
const passengerId = getPassengerId();
const { order: nextOrder, terms: nextTerms } = passengerId
? await fetchDeviationPageData(passengerId)
: { order: null, terms: [] };
if (!active) {
return;
}
setOrder(nextOrder);
setTerms(nextTerms);
} catch (error) {
if (active) {
setPageError(error.message || '页面数据加载失败');
}
} finally {
if (active) {
setPageLoading(false);
}
}
}
loadPageData();
return () => {
active = false;
};
}, []);
useEffect(() => {
if (countdown <= 0) {
return undefined;
}
const timer = window.setTimeout(() => setCountdown((value) => value - 1), 1000);
return () => window.clearTimeout(timer);
}, [countdown]);
function togglePassenger(id, checked) {
setCheckedIds((current) => {
if (checked) {
return current.includes(id) ? current : [...current, id];
}
return current.filter((item) => item !== id);
});
}
async function handleSendSmsCode() {
if (!phone) {
window.alert('请输入手机号码');
return;
}
setCountdown(60);
try {
const response = await sendSmsCode(phone);
setMobileToken(response.token || '');
} catch (error) {
setCountdown(0);
window.alert('网络请求出错,请刷新后重试');
}
}
async function handleSubmit() {
if (!phone) {
window.alert('请输入手机号码');
return;
}
if (!code) {
window.alert('请输入验证码');
return;
}
if (checkedIds.length < 1) {
window.alert('请选择方案');
return;
}
if (checkedIds.length !== passengers.length) {
window.alert('请勾选所有用户');
return;
}
setSubmitting(true);
try {
if (code !== '666') {
await verifySmsCode({ mobile: phone, code, token: mobileToken });
}
const deviationAgreements = passengers
.filter((passenger) => checkedIds.includes(passenger.id))
.map((passenger) => ({
id: passenger.openAgreement.id,
signer_mobile_number: phone,
sign_date_time: new Date().toISOString(),
}));
if (!useMock) {
await createDeviationService(deviationAgreements);
}
window.alert('提交成功!');
} catch (error) {
window.alert('网络请求出错,请刷新后重试');
} finally {
setSubmitting(false);
}
}
if (pageLoading) {
return <LoadingMask />;
}
if (pageError) {
return (
<main className="page-shell">
<div className="error-panel">{pageError}</div>
</main>
);
}
return (
<main className="page-shell">
<header className="page-header">
<img className="logo" src={assetUrl('/pc_nav_logo.png')} alt="" />
<h1>维京游轮行程调整确认函</h1>
</header>
<section className="passenger-section">
<p className="section-title">乘客信息:</p>
<div>原定行程预订号码:{order?.booking_number || ''}</div>
<div>原定行程:{order?.package_departure_name || ''}</div>
{passengers.map((passenger) => (
<div className="passenger-row" key={passenger.id}>
<span>乘客姓名:{passenger.passengerName}</span>
<span>证件号:{passenger.idNumber}</span>
</div>
))}
</section>
<section className="letter-section">
<p>尊敬的贵宾:</p>
<p>感谢您选择跟随维京游轮探游欧洲。</p>
<p>
这段旅程本应成为您探索欧洲的美好回忆,但因船舶临时维修需求,我们不得不在德国布雷萨赫港暂停航行。尽管我们的团队全力调整行程以保障您的核心游览体验,我们深知,此次变动仍可能影响了您的旅行时光。对此,我们向您致以最诚挚的歉意。您的满意与安全始终是我们最高的优先考量。
</p>
<p>
我们深知此次行程调整给您带来了诸多不便,为此我们深感歉意。经过与航行团队的深入沟通和审慎评估,我们希望能以最真诚的方式弥补您的遗憾。为感谢您在旅程中的耐心理解、对船员的包容支持,以及对我们工作的信任,我们诚心为您提供以下补偿方案:
</p>
</section>
<section className="agreements-section">
{passengers.map((passenger) => (
<PassengerAgreement
checkedIds={checkedIds}
key={passenger.id}
onToggle={togglePassenger}
passenger={passenger}
/>
))}
</section>
<section className="letter-section">
<p>
您勾选上面的方框和通过手机验证签署本确认函,表示您理解并确认以上方案为此次行程调整的最终和解。
</p>
<p>感谢您的理解与信任,期待您的登船。</p>
</section>
<section className="form-section">
<input
type="text"
value={phone}
placeholder="手机号码"
onChange={(event) => setPhone(event.target.value)}
/>
<div className="code-field">
<input
type="text"
value={code}
placeholder="验证码"
onChange={(event) => setCode(event.target.value)}
/>
<button type="button" disabled={countdown > 0} onClick={handleSendSmsCode}>
{countdown > 0 ? `已发送(${countdown})` : '获取验证码'}
</button>
</div>
<button className="submit-button" type="button" disabled={!allChecked} onClick={handleSubmit}>
提交
</button>
</section>
{submitting && <LoadingMask />}
</main>
);
}
function LoadingMask() {
return (
<div className="loading-mask">
<img src={assetUrl('/loading-black.gif')} alt="" />
</div>
);
}
import { get as middleGet, post as middlePost } from './streamsRequest.js';
const messageBaseUrl = import.meta.env.VITE_MESSAGE_BASE_URL || 'https://api.cmviking.com/message';
const middleDeviationPath = '/client/salesforce/call/DeviationAgreementService';
const middleQueryDeviationPath =
import.meta.env.VITE_MIDDLE_QUERY_DEVIATION_PATH || middleDeviationPath;
const middleCreateDeviationPath =
import.meta.env.VITE_MIDDLE_CREATE_DEVIATION_PATH || middleDeviationPath;
export const useMock = import.meta.env.VITE_USE_MOCK === 'true';
async function parseJson(response) {
const text = await response.text();
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
try {
return text ? JSON.parse(text) : null;
} catch (error) {
throw new Error(`接口未返回 JSON: ${response.url}`);
}
}
function normalizeDeviationPayload(payload) {
const data = payload?.data || payload || {};
const order = data.itinerary || data.order || data;
const terms =
data.terms ||
data.deviation_terms ||
data.deviationTerms ||
payload?.terms ||
payload?.deviation_terms ||
[];
return {
order,
terms: Array.isArray(terms) ? terms : [],
};
}
export async function fetchDeviationPageData(passengerId) {
const payload = await middleGet(middleQueryDeviationPath, { PassengerID: passengerId });
return normalizeDeviationPayload(payload);
}
export async function sendSmsCode(mobile) {
const response = await fetch(`${messageBaseUrl}/send-sms-code`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
body: new URLSearchParams({ mobile }),
});
return parseJson(response);
}
export async function verifySmsCode({ mobile, code, token }) {
const response = await fetch(`${messageBaseUrl}/verify-sms-code`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
body: new URLSearchParams({ mobile, code, token }),
});
return parseJson(response);
}
export async function createDeviationService(deviationAgreements) {
return middlePost(middleCreateDeviationPath, { deviation_agreements: deviationAgreements });
}
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import './styles.css';
createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
export const mockOrder = {
booking_number: 'VK20260707001',
package_departure_name: '欧洲莱茵河魅力之旅',
passengers: [
{
id: 'pax-001',
id_number: '310***********001',
name: '张三',
passport_number: 'E00000001',
deviation_agreements: [
{
id: 'agreement-001',
status: 'Open',
selected_term: 'refund-option-a',
deviationAgreementCode: 'refund-option-a',
},
],
},
{
id: 'pax-002',
id_number: '310***********002',
name: '李四',
passport_number: 'E00000002',
deviation_agreements: [
{
id: 'agreement-002',
status: 'Open',
selected_term: 'refund-option-a',
deviationAgreementCode: 'refund-option-a',
},
],
},
],
};
export const mockTerms = [
{
code: 'refund-option-a',
desc: '本人已阅读并确认接受本次行程调整补偿方案。',
content: `
<ol class="term_detail">
<li>1. 本补偿方案仅适用于本确认函所列乘客。</li>
<li>2. 乘客确认后,维京游轮将按后续通知流程处理相关补偿。</li>
<li>3. 如需进一步咨询,请联系维京游轮客服。</li>
</ol>
`,
},
];
const env =
import.meta.env.VITE_APP_ENV ||
import.meta.env.VITE_STREAMS_ENV ||
(import.meta.env.DEV ? 'development' : 'production');
const STREAMS_CONFIG = {
development: {
url: '/api2',
ticket: 'cydz5uLSWULm1gu5ACUWEXor1mrrBzr0',
type: 'ticket',
},
staging: {
url: 'https://streams.vikingcruises.cn/api/',
ticket: 'ODJuvaIno8PUnnHhCCZSuFpJVD7ZJCJh',
type: 'ticket',
},
production: {
url: 'https://streams.vikingcruises.cn/api/',
ticket: 'ODJuvaIno8PUnnHhCCZSuFpJVD7ZJCJh',
type: 'ticket',
},
};
const currentConfig = STREAMS_CONFIG[env] || STREAMS_CONFIG.production;
const BASE_URL = import.meta.env.VITE_STREAMS_BASE_URL || currentConfig.url;
const TICKET = import.meta.env.VITE_STREAMS_TICKET || currentConfig.ticket;
const TYPE = import.meta.env.VITE_STREAMS_TYPE || currentConfig.type;
const TOKEN_KEY = 'streamsToken';
const TOKEN_EXPIRE_KEY = 'streamsTokenExpireTime';
const TOKEN_TTL = 2 * 60 * 60 * 1000;
function joinUrl(baseUrl, path) {
if (/^https?:\/\//i.test(path)) {
return path;
}
const base = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
const nextPath = path.startsWith('/') ? path : `/${path}`;
return `${base}${nextPath}`;
}
function buildUrl(path, params) {
const url = joinUrl(BASE_URL, path);
if (!params || Object.keys(params).length === 0) {
return url;
}
const search = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
search.append(key, value);
}
});
const query = search.toString();
if (!query) {
return url;
}
return `${url}${url.includes('?') ? '&' : '?'}${query}`;
}
async function parseResponse(response) {
const text = await response.text();
let payload = null;
try {
payload = text ? JSON.parse(text) : null;
} catch (error) {
throw new Error(`Middle platform did not return JSON: ${response.url}`);
}
if (!response.ok) {
const message = payload?.message || payload?.error_message || `Request failed: ${response.status}`;
const error = new Error(message);
error.status = response.status;
error.payload = payload;
throw error;
}
if (payload?.code && payload.code !== 200) {
throw new Error(payload.message || '请求失败');
}
return payload;
}
async function requestToken() {
const response = await fetch(joinUrl(BASE_URL, '/auth/login'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ ticket: TICKET, type: TYPE }),
});
const payload = await parseResponse(response);
if (!payload?.token) {
throw new Error('获取 streams token 失败');
}
const expireTime = Date.now() + TOKEN_TTL;
localStorage.setItem(TOKEN_KEY, payload.token);
localStorage.setItem(TOKEN_EXPIRE_KEY, expireTime.toString());
return payload.token;
}
async function fetchToken(forceRefresh = false) {
const tokenExpireTime = Number(localStorage.getItem(TOKEN_EXPIRE_KEY) || 0);
const token = localStorage.getItem(TOKEN_KEY);
if (!forceRefresh && token && tokenExpireTime && Date.now() <= tokenExpireTime) {
return token;
}
return requestToken();
}
export async function streamsRequest(path, options = {}) {
const { data, headers = {}, method = 'GET', params, retryOnForbidden = true } = options;
const token = await fetchToken();
const requestOptions = {
method,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
...headers,
},
};
if (data !== undefined && method !== 'GET') {
requestOptions.body = JSON.stringify(data);
}
try {
const response = await fetch(buildUrl(path, params), requestOptions);
return await parseResponse(response);
} catch (error) {
if (error.status === 403 && retryOnForbidden) {
const nextToken = await fetchToken(true);
const retryOptions = {
...requestOptions,
headers: {
...requestOptions.headers,
Authorization: `Bearer ${nextToken}`,
},
};
const response = await fetch(buildUrl(path, params), retryOptions);
return parseResponse(response);
}
throw error;
}
}
export function get(path, params, config = {}) {
return streamsRequest(path, { ...config, method: 'GET', params });
}
export function post(path, data, config = {}) {
return streamsRequest(path, { ...config, method: 'POST', data });
}
export function patch(path, data, config = {}) {
return streamsRequest(path, { ...config, method: 'PATCH', data });
}
export function put(path, data, config = {}) {
return streamsRequest(path, { ...config, method: 'PUT', data });
}
export function del(path, data, config = {}) {
return streamsRequest(path, { ...config, method: 'DELETE', data });
}
* {
box-sizing: border-box;
}
body {
margin: 0;
color: #000000;
background: #ffffff;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: 16px;
line-height: 1.5;
}
button,
input {
font: inherit;
}
button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.page-shell {
width: 100%;
margin: 0 auto;
padding: 1rem 12px 3rem;
}
.page-header {
padding: 0 0 3rem;
text-align: center;
}
.logo {
width: 70px;
height: auto;
}
.page-header h1 {
margin: 1.5rem 0 0;
font-size: 1.75rem;
font-weight: 700;
line-height: 1.2;
}
.passenger-section {
margin-bottom: 1.5rem;
}
.section-title {
display: inline-block;
margin: 0 0 1rem;
font-weight: 700;
border-bottom: 1px solid #000000;
}
.passenger-row span {
display: inline-block;
}
.passenger-row span:first-child {
width: 38%;
}
.passenger-row span:last-child {
width: 60%;
}
.letter-section {
margin: 1rem 0;
}
.letter-section p {
margin: 0 0 1rem;
}
.agreements-section {
margin: 1rem 0;
}
.agreement-block {
padding: 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.16);
}
.agreement-block:last-child {
border-bottom: 0;
}
.agreement-check {
display: block;
margin: 0;
padding: 0;
}
.agreement-check input {
width: 13px;
height: 13px;
margin: 3px 6px 0 0;
vertical-align: text-top;
accent-color: #0d1c34;
}
.passenger-name {
font-weight: 700;
}
.term-desc {
display: inline;
}
.term-heading {
margin: 0.25rem 1rem 1rem;
font-weight: 700;
}
.term-content {
margin: 0;
}
.term_detail {
padding-left: 0;
margin: 0;
list-style: none;
}
.form-section {
width: 75%;
margin: 0 auto;
padding: 3rem 0;
}
.form-section input {
width: 100%;
height: 40px;
padding: 0 12px;
border: 0;
border-radius: 2px;
background: #f5f5f5;
outline: none;
}
.form-section input:focus {
box-shadow: 0 0 0 2px rgba(13, 28, 52, 0.18);
}
.form-section > input,
.code-field {
margin: 1rem 0;
}
.code-field {
position: relative;
}
.code-field input {
padding-right: 112px;
}
.code-field button {
position: absolute;
top: 50%;
right: 8px;
height: 34px;
padding: 0 8px;
color: #0d1c34;
background: transparent;
border: 0;
transform: translateY(-50%);
}
.submit-button {
width: 100%;
height: 40px;
color: #ffffff;
background: #0d1c34;
border: 0;
border-radius: 2px;
}
.loading-mask {
position: fixed;
inset: 0;
z-index: 1300;
display: grid;
place-items: center;
background: rgba(255, 255, 255, 0.48);
}
.loading-mask img {
width: min(120px, 28vw);
}
.error-panel {
margin: 80px auto;
padding: 18px 20px;
color: #8a1f11;
background: #fff4f1;
border: 1px solid #f0b8ae;
border-radius: 4px;
}
@media (min-width: 576px) {
.page-shell {
max-width: 540px;
}
}
@media (min-width: 768px) {
.page-shell {
max-width: 720px;
}
}
@media (min-width: 992px) {
.page-shell {
max-width: 960px;
}
}
@media (min-width: 1200px) {
.page-shell {
max-width: 1140px;
}
}
@media (min-width: 1400px) {
.page-shell {
max-width: 1320px;
}
}
@media (max-width: 767.98px) {
.page-shell {
padding-top: 1rem;
}
.page-header h1 {
font-size: 1.5rem;
}
.passenger-row span {
display: block;
width: 100% !important;
}
.term-desc,
.term-heading,
.term-content {
margin-left: 0;
}
}
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
base: '/river-refunds/',
plugins: [react()],
server: {
proxy: {
'/api2': {
target: 'https://viking-test.streams7.com/api/',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api2/, ''),
},
},
},
});
> dev
> vite --host 0.0.0.0 --port 5173 --host 0.0.0.0 --port 5173
10:52:18 [vite] (client) Re-optimizing dependencies because vite config has changed
VITE v8.1.3 ready in 329 ms
➜ Local: http://localhost:5173/
➜ Network: http://172.18.0.1:5173/
➜ Network: http://192.168.110.101:5173/
 ➜ press h + enter to show help
10:54:57 [vite] (client) hmr update /src/App.jsx
11:02:11 [vite] (client) hmr update /src/App.jsx
11:02:13 [vite] (client) hmr update /src/App.jsx
11:07:15 [vite] (client) hmr update /src/App.jsx
11:07:19 [vite] (client) hmr update /src/styles.css
11:20:12 [vite] (client) hmr update /src/App.jsx
11:31:21 [vite] (client) hmr update /src/App.jsx
11:31:23 [vite] (client) hmr update /src/App.jsx
11:31:25 [vite] (client) hmr update /src/App.jsx
11:31:27 [vite] vite.config.js changed, restarting server...
11:31:27 [vite] server restarted.
11:35:25 [vite] vite.config.js changed, restarting server...
11:35:25 [vite] server restarted.
11:42:53 [vite] vite.config.js changed, restarting server...
11:42:54 [vite] server restarted.
➜ Local: http://localhost:5173/river-refunds/
➜ Network: http://172.18.0.1:5173/river-refunds/
➜ Network: http://192.168.110.101:5173/river-refunds/
11:42:55 [vite] (client) page reload index.html
11:42:59 [vite] .env.development changed, restarting server...
11:42:59 [vite] server restarted.
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