ssr render
Gitea Actions Demo / build_and_push (push) Successful in 57s
Details
Gitea Actions Demo / build_and_push (push) Successful in 57s
Details
This commit is contained in:
parent
87b6f3cca2
commit
84cd573695
|
|
@ -2,10 +2,10 @@
|
||||||
|
|
||||||
## Task State
|
## Task State
|
||||||
|
|
||||||
- task_id: TASK-F14
|
- task_id: TASK-SSR-DETAIL
|
||||||
- status: success
|
- status: success
|
||||||
- parent_task: TASK-F12, TASK-F13
|
- parent_task: TASK-SSR-2
|
||||||
- summary: **Карта тренировки: одиночные метки фото не пропадают при сбросе участка (v-memo + стабильные :settings), метки центрированы на GPS-точках, клик по треку — поиск ближайшей точки (радиус 40 м).**
|
- summary: **SSR /public/workouts/:id — HTML-контент переписан под DOM-структуру WorkoutItem.vue (вёрстка + фото + OG:image + noscript).**
|
||||||
- next task: — (no active tasks)
|
- next task: — (no active tasks)
|
||||||
|
|
||||||
### TASK-F12/F13/F14 — фиксы карты тренировки (WorkoutItem.vue)
|
### TASK-F12/F13/F14 — фиксы карты тренировки (WorkoutItem.vue)
|
||||||
|
|
|
||||||
140
server/index.ts
140
server/index.ts
|
|
@ -20,7 +20,11 @@ function getAssetTagsSafe(): string {
|
||||||
|
|
||||||
function escapeHtml(text: string | null | undefined): string {
|
function escapeHtml(text: string | null | undefined): string {
|
||||||
if (!text) return "";
|
if (!text) return "";
|
||||||
return text.replace(/</g, "<").replace(/>/g, ">");
|
return text
|
||||||
|
.replace(/&/g, String.fromCharCode(38) + "amp;")
|
||||||
|
.replace(/</g, String.fromCharCode(38) + "lt;")
|
||||||
|
.replace(/>/g, String.fromCharCode(38) + "gt;")
|
||||||
|
.replace(/"/g, String.fromCharCode(38) + "quot;");
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(iso: string): string {
|
function formatDate(iso: string): string {
|
||||||
|
|
@ -183,21 +187,126 @@ app.get("/public/workouts/:id", async (req: Request, res: Response) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Build params section ---
|
||||||
|
const params: string[] = [];
|
||||||
|
|
||||||
|
if (w.workouted_at) {
|
||||||
|
params.push(
|
||||||
|
` <div class="workout-item-params"><div class="workout-item-params-name">Дата:</div><div class="workout-item-params-value">${formatDate(
|
||||||
|
w.workouted_at,
|
||||||
|
)}</div></div>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (w.distantion) {
|
||||||
|
params.push(
|
||||||
|
` <div class="workout-item-params"><div class="workout-item-params-name">Расстояние:</div><div class="workout-item-params-value">${formatDistance(
|
||||||
|
w.distantion,
|
||||||
|
)} км</div></div>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (w.heart_rate) {
|
||||||
|
params.push(
|
||||||
|
` <div class="workout-item-params"><div class="workout-item-params-name">Средний пульс:</div><div class="workout-item-params-value">${Math.floor(
|
||||||
|
w.heart_rate,
|
||||||
|
)} уд./ мин.</div></div>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (w.max_heart_rate) {
|
||||||
|
params.push(
|
||||||
|
` <div class="workout-item-params"><div class="workout-item-params-name">Максимальный пульс:</div><div class="workout-item-params-value">${Math.floor(
|
||||||
|
w.max_heart_rate,
|
||||||
|
)} уд./ мин.</div></div>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (w.speed) {
|
||||||
|
params.push(
|
||||||
|
` <div class="workout-item-params"><div class="workout-item-params-name">Средняя скорость:</div><div class="workout-item-params-value">${formatSpeed(
|
||||||
|
w.speed,
|
||||||
|
)} км / ч</div></div>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (w.max_speed) {
|
||||||
|
params.push(
|
||||||
|
` <div class="workout-item-params"><div class="workout-item-params-name">Максимальная скорость:</div><div class="workout-item-params-value">${formatSpeed(
|
||||||
|
w.max_speed,
|
||||||
|
)} км / ч</div></div>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (w.temperature) {
|
||||||
|
params.push(
|
||||||
|
` <div class="workout-item-params"><div class="workout-item-params-name">Температура:</div><div class="workout-item-params-value">${w.temperature} °C</div></div>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (w.power) {
|
||||||
|
params.push(
|
||||||
|
` <div class="workout-item-params"><div class="workout-item-params-name">Средняя мощность:</div><div class="workout-item-params-value">${Math.floor(
|
||||||
|
w.power,
|
||||||
|
)} Вт</div></div>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (w.max_power) {
|
||||||
|
params.push(
|
||||||
|
` <div class="workout-item-params"><div class="workout-item-params-name">Максимальная мощность:</div><div class="workout-item-params-value">${Math.floor(
|
||||||
|
w.max_power,
|
||||||
|
)} Вт</div></div>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (w.cadence) {
|
||||||
|
params.push(
|
||||||
|
` <div class="workout-item-params"><div class="workout-item-params-name">Каденс:</div><div class="workout-item-params-value">${w.cadence} об/мин</div></div>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (w.duraion_sec) {
|
||||||
|
params.push(
|
||||||
|
` <div class="workout-item-params"><div class="workout-item-params-name">Длительность:</div><div class="workout-item-params-value">${formatDuration(
|
||||||
|
w.duraion_sec,
|
||||||
|
)}</div></div>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
w.external_links &&
|
||||||
|
w.external_links.values &&
|
||||||
|
w.external_links.values.length > 0
|
||||||
|
) {
|
||||||
|
const dzenUrl = w.external_links.values[0].value;
|
||||||
|
params.push(
|
||||||
|
` <div class="workout-item-params"><div class="workout-item-params-name">Ссылки на описание:</div><div><a href="${escapeHtml(
|
||||||
|
dzenUrl,
|
||||||
|
)}" target="_blank">Дзен</a></div></div>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Build photos section ---
|
||||||
|
let photosHtml = "";
|
||||||
|
if (w.photos && w.photos.length > 0) {
|
||||||
|
const photoItems = w.photos
|
||||||
|
.map(
|
||||||
|
(p: { id: string; url: string }) =>
|
||||||
|
` <div class="workout-photo"><img src="${escapeHtml(
|
||||||
|
p.url,
|
||||||
|
)}" alt="${escapeHtml(p.id)}" /></div>`,
|
||||||
|
)
|
||||||
|
.join("\n");
|
||||||
|
photosHtml = `
|
||||||
|
<div id="workout-photos">
|
||||||
|
<div class="workout-photos-grid">
|
||||||
|
${photoItems}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
const content = `
|
const content = `
|
||||||
<h1>${escapeHtml(w.name)}</h1>
|
<div id="workout-container">
|
||||||
<p>${escapeHtml(w.description)}</p>
|
<div id="workout-map" style="height:350px;background:#e9ecef;display:flex;align-items:center;justify-content:center;color:#999;font-family:sans-serif;">Карта (загрузится после инициализации JS)</div>
|
||||||
<ul class="workout-metrics">
|
<div id="workout-short-data">
|
||||||
<li>Дата: ${formatDate(w.workouted_at)}</li>
|
<div class="workout-item-editable-title"><h3>${escapeHtml(
|
||||||
<li>Расстояние: ${formatDistance(w.distantion)} км</li>
|
w.name,
|
||||||
<li>Средняя скорость: ${formatSpeed(w.speed)} км/ч</li>
|
)}</h3></div>
|
||||||
<li>Макс. скорость: ${formatSpeed(w.max_speed)} км/ч</li>
|
${params.join("\n")}
|
||||||
<li>Средний пульс: ${w.heart_rate} уд/мин</li>
|
</div>
|
||||||
<li>Макс. пульс: ${w.max_heart_rate} уд/мин</li>
|
</div>${photosHtml}
|
||||||
<li>Средняя мощность: ${w.power} Вт</li>
|
<div id="workout-charts" style="margin-top:20px;padding:20px;background:#f8f9fa;border-radius:6px;color:#999;text-align:center;font-family:sans-serif;">Графики (загрузятся после инициализации JS)</div>
|
||||||
<li>Макс. мощность: ${w.max_power} Вт</li>
|
<noscript><p>Для полноценного просмотра тренировки (карта, графики) включите JavaScript.</p></noscript>`;
|
||||||
<li>Каденс: ${w.cadence} об/мин</li>
|
|
||||||
<li>Длительность: ${formatDuration(w.duraion_sec)}</li>
|
|
||||||
</ul>`;
|
|
||||||
|
|
||||||
const desc = w.description
|
const desc = w.description
|
||||||
? w.description.slice(0, 150) + (w.description.length > 150 ? "…" : "")
|
? w.description.slice(0, 150) + (w.description.length > 150 ? "…" : "")
|
||||||
|
|
@ -210,6 +319,7 @@ app.get("/public/workouts/:id", async (req: Request, res: Response) => {
|
||||||
title: `${w.name} — Cycle Rider`,
|
title: `${w.name} — Cycle Rider`,
|
||||||
description: `${desc} ${metrics}`,
|
description: `${desc} ${metrics}`,
|
||||||
canonicalUrl: `${BASE_URL}/public/workouts/${w.id}`,
|
canonicalUrl: `${BASE_URL}/public/workouts/${w.id}`,
|
||||||
|
ogImage: w.attachment?.url,
|
||||||
jsonLd: {
|
jsonLd: {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "SportsActivity",
|
"@type": "SportsActivity",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue