From 688b74a658a217a0e4dac67bf95a859ebb793e07 Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Tue, 6 Oct 2026 14:24:46 +0900 Subject: ParksTable: 公園一覧に設備フィルタ・並べ替え・カード表示を追加 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 設備(トイレ・水飲み場・遊具・ボール遊び)で絞り込めるようにする。 公園名・面積の見出しで並べ替え、0件時は案内を表示する。 スマホはカード表示にし、面積に小・中・大の目安を付ける。 設備はアイコンバッジで示し、地図リンクは住所ではなく公園名 (+小平市)で検索するようにする。 updates.json を再生成(公園一覧を追加、上限40件)。 --- src/components/ParksTable.astro | 479 ++++++++++++++++++++++++++++---- src/content/docs/kodaira-data/parks.mdx | 14 +- src/data/updates.json | 14 +- 3 files changed, 434 insertions(+), 73 deletions(-) diff --git a/src/components/ParksTable.astro b/src/components/ParksTable.astro index 268bdfe..8d7f648 100644 --- a/src/components/ParksTable.astro +++ b/src/components/ParksTable.astro @@ -1,7 +1,8 @@ --- // 小平市の公園一覧(/kodaira-data/parks/)を表示する。 // データは src/data/kodaira-parks.json(321件、市の公開資料から作成)。 -// 町名セレクトとキーワード入力で絞り込む(絞り込みは利用者の端末で実行)。 +// DOM を抑えるため、設備は1セルにまとめ、アイコンは CSS の擬似要素で描く。 +// 絞り込み・並べ替え・カード表示は利用者の端末で実行する。 import parksData from "@/data/kodaira-parks.json"; type Park = Record; @@ -9,42 +10,110 @@ const parks = parksData as Park[]; const EMPTY = "—"; -const columns: { key: string; label: string; numeric?: boolean; wide?: boolean }[] = [ - { key: "公園名", label: "公園名" }, - { key: "住所", label: "住所" }, - { key: "面積㎡", label: "面積(㎡)", numeric: true }, - { key: "500㎡以上", label: "500㎡以上" }, - { key: "種別", label: "種別" }, - { key: "南西部指定管理", label: "南西部指定管理" }, - { key: "トイレ", label: "トイレ", wide: true }, - { key: "水飲み場", label: "水飲み場" }, - { key: "ベンチ・椅子", label: "ベンチ・椅子", wide: true }, - { key: "テーブル", label: "テーブル", wide: true }, - { key: "防災用具", label: "防災用具", wide: true }, - { key: "ボール遊び", label: "ボール遊び", wide: true }, - { key: "遊具", label: "遊具", wide: true }, - { key: "備考", label: "備考", wide: true }, -]; +// 種別の表示(色付きの丸+区分名。正式名は title / aria-label で補う)。 +const TYPE_LABEL: Record = { + 街区: "街区公園", + 近隣: "近隣公園", + 運動: "運動公園", +}; +const TYPE_CLASS: Record = { + 街区: "parks-type--block", + 近隣: "parks-type--neighbor", + 運動: "parks-type--sports", +}; + +// 設備バッジ(アイコンは CSS の ::before で描く)。 +type Badge = { kind: string; label: string; title: string }; +function facilityBadges(p: Park): Badge[] { + const badges: Badge[] = []; + if (p["トイレ"]) { + badges.push({ kind: "toilet", label: "トイレ", title: `トイレ: ${p["トイレ"]}` }); + } + if (p["水飲み場"]) { + badges.push({ kind: "water", label: "水飲み場", title: `水飲み場: ${p["水飲み場"]}` }); + } + if (p["遊具"]) { + badges.push({ kind: "toy", label: "遊具", title: `遊具: ${p["遊具"]}` }); + } + const ball = p["ボール遊び"]; + if (ball && ball !== "禁止") { + const label = ball.includes("一部") ? "ボール遊び一部可" : "ボール遊び可"; + badges.push({ kind: "ball", label, title: `ボール遊び: ${ball}` }); + } + return badges; +} -function display(key: string, value: string): string { - if (value === "" || value == null) return EMPTY; - if (key === "面積㎡") { - const n = Number(value); - if (Number.isFinite(n)) return n.toLocaleString("ja-JP"); +// 設備フィルタ用のトークン(data-fac)。 +function facTokens(p: Park): string { + const tokens: string[] = []; + if (p["トイレ"]) tokens.push("toilet"); + if (p["水飲み場"]) tokens.push("water"); + if (p["遊具"]) tokens.push("toy"); + const ball = p["ボール遊び"]; + if (ball && ball !== "禁止") tokens.push("ball"); + return tokens.join(" "); +} + +// 設備セルの補足テキスト(バッジで示した項目の詳細と、その他の項目)。 +const DETAIL_FIELDS: { key: string; label: string }[] = [ + { key: "ベンチ・椅子", label: "ベンチ・椅子" }, + { key: "テーブル", label: "テーブル" }, + { key: "防災用具", label: "防災用具" }, + { key: "遊具", label: "遊具" }, + { key: "備考", label: "備考" }, +]; +function detailText(p: Park): string { + const parts: string[] = []; + if (p["南西部指定管理"]) parts.push("南西部指定管理"); + for (const { key, label } of DETAIL_FIELDS) { + const value = p[key]; + if (!value) continue; + parts.push(key === "備考" ? value : `${label}: ${value}`); } - return value; + const ball = p["ボール遊び"]; + if (ball && ball !== "禁止" && ball !== "可") parts.push(`ボール遊び: ${ball}`); + return parts.join(" / "); } -// 検索対象(公園名・住所・町名・種別・ボール遊び・遊具・備考)。 +// 面積(数値) +function areaNumber(value: string): number { + const n = Number(value); + return Number.isFinite(n) ? n : 0; +} +function areaText(value: string): string { + const n = areaNumber(value); + return n ? n.toLocaleString("ja-JP") : value; +} +// 面積の目安(小・中・大)。しきい値はページの注記に合わせる。 +function sizeLabel(area: number): string { + if (area < 1000) return "小"; + if (area < 10000) return "中"; + return "大"; +} +function sizeTitle(area: number): string { + if (area < 1000) return "目安: 1,000㎡未満(小さめ)"; + if (area < 10000) return "目安: 1,000〜10,000㎡(ふつう)"; + return "目安: 10,000㎡以上(広め)"; +} + +// 住所ではなく公園名で検索する(地番は地図側で正しく解決されないため)。 +// 「小平市」を付けないと別の自治体の同名公園に飛ぶので必ず付ける。 +function mapsUrl(name: string): string { + const query = encodeURIComponent(`小平市${name}`); + return `https://www.google.com/maps/search/?api=1&query=${query}`; +} + +// 検索対象(公園名・住所・町名・種別・設備・備考)。 function searchIndex(p: Park): string { return [ p["公園名"], p["住所"], p["町名"], p["種別"], + p["トイレ"], + p["水飲み場"], p["ボール遊び"], - p["遊具"], - p["備考"], + ...DETAIL_FIELDS.map((f) => p[f.key]), ] .join(" ") .toLowerCase(); @@ -71,32 +140,85 @@ const total = parks.length; キーワード +
+ 設備で絞る + + + + +
{total}件 +

設備の情報は市の南西部地域の公園のみです。設備で絞ると、この情報がある公園だけが対象になります。

+
- {columns.map((col) => ( - - ))} + + + + + {parks.map((park) => ( - - {columns.map((col) => ( - - ))} + + + + + + ))} @@ -108,9 +230,9 @@ const total = parks.length; .parks-controls { display: flex; flex-wrap: wrap; - gap: 0.75rem 1rem; + gap: 0.6rem 1rem; align-items: center; - margin: 1rem 0; + margin: 1rem 0 0.4rem; } .parks-controls label { display: inline-flex; @@ -119,7 +241,7 @@ const total = parks.length; font-size: 0.9rem; } .parks-controls select, - .parks-controls input { + .parks-controls input[type="search"] { padding: 0.3rem 0.5rem; border: 1px solid var(--sl-color-hairline, #ccc); border-radius: 0.3rem; @@ -127,10 +249,41 @@ const total = parks.length; color: var(--sl-color-text, inherit); font: inherit; } + .parks-facilities { + display: flex; + flex-wrap: wrap; + gap: 0.3rem 0.8rem; + align-items: center; + margin: 0; + padding: 0.2rem 0.6rem; + border: 1px solid var(--sl-color-hairline, #ccc); + border-radius: 0.3rem; + } + .parks-facilities legend { + padding: 0 0.2rem; + font-size: 0.8em; + color: inherit; + opacity: 0.7; + } + .parks-facilities label { + font-size: 0.85rem; + } .parks-count { margin-left: auto; font-size: 0.9rem; - color: var(--sl-color-gray-3, #666); + opacity: 0.75; + } + .parks-hint, + .parks-none { + margin: 0 0 0.6rem; + font-size: 0.8rem; + opacity: 0.75; + } + .parks-none { + padding: 0.6rem; + border: 1px dashed var(--sl-color-hairline, #ccc); + border-radius: 0.3rem; + opacity: 1; } .parks-scroll { overflow-x: auto; @@ -166,19 +319,177 @@ const total = parks.length; .parks tbody tr:nth-child(even) td { background: color-mix(in srgb, var(--sl-color-text, #333) 5%, transparent); } + .parks tr.is-hidden { + display: none; + } + .parks-sort { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0; + border: none; + background: none; + color: inherit; + font: inherit; + cursor: pointer; + } + .parks-sort:hover .parks-sort-mark { + color: var(--sl-color-text-accent, #4c8); + } + .parks-sort-mark { + display: inline-block; + min-width: 1em; + text-align: center; + } + .parks .is-name { + font-weight: 600; + } + .parks .is-address { + font-size: 0.8em; + white-space: normal; + min-width: 9rem; + } + .parks .is-address a { + color: var(--sl-color-text-accent, #357); + } + .parks .is-address a::before { + content: "📍"; + margin-right: 0.2em; + } .parks .is-num { text-align: right; font-variant-numeric: tabular-nums; + white-space: nowrap; } - .parks .is-wide { - white-space: normal; - min-width: 11rem; - max-width: 20rem; + .parks .is-num::after { + content: attr(data-size); + display: inline-block; + margin-left: 0.4em; + padding: 0 0.45em; + border-radius: 0.7em; + font-size: 0.75em; + line-height: 1.7; + vertical-align: middle; + background: color-mix(in srgb, currentColor 14%, transparent); + } + .parks .is-num[data-size="中"]::after { + background: color-mix(in srgb, #2e9e4f 22%, transparent); + } + .parks .is-num[data-size="大"]::after { + background: color-mix(in srgb, #2f7fe0 24%, transparent); } - .parks .is-empty { - color: var(--sl-color-gray-4, #aaa); + .parks .is-type { text-align: center; } + .parks-type { + display: inline-flex; + align-items: center; + gap: 0.3em; + font-size: 0.9em; + } + .parks-type::before { + content: ""; + width: 0.7em; + height: 0.7em; + border-radius: 50%; + background: var(--type-color, #888); + flex: none; + } + .parks-type--block { + --type-color: #2e9e4f; + } + .parks-type--neighbor { + --type-color: #2f7fe0; + } + .parks-type--sports { + --type-color: #e8862a; + } + .parks .is-facility { + white-space: normal; + min-width: 12rem; + max-width: 26rem; + } + .parks .fac-badge { + display: inline-flex; + align-items: center; + gap: 0.25em; + margin: 0 0.3em 0.2em 0; + padding: 0 0.5em; + border-radius: 1em; + font-size: 0.8em; + line-height: 1.8; + white-space: nowrap; + background: color-mix(in srgb, currentColor 10%, transparent); + } + .parks .fac-toilet::before { + content: "🚻"; + } + .parks .fac-water::before { + content: "🚰"; + } + .parks .fac-toy::before { + content: "🎠"; + } + .parks .fac-ball::before { + content: "⚽"; + } + .parks .fac-detail { + display: block; + margin-top: 0.25em; + font-size: 0.8em; + opacity: 0.85; + } + + /* スマートフォンでは1件ずつカードで表示する。 */ + @media (max-width: 640px) { + .parks-scroll { + overflow-x: visible; + border: none; + border-radius: 0; + } + .parks table, + .parks tbody, + .parks tr, + .parks td { + display: block; + width: auto; + } + .parks thead { + display: none; + } + .parks tbody tr { + margin-bottom: 0.6rem; + padding: 0.4rem 0.6rem; + border: 1px solid var(--sl-color-hairline, #ddd); + border-radius: 0.4rem; + } + .parks tbody tr:nth-child(even) td { + background: transparent; + } + .parks td { + border: none; + padding: 0.1rem 0; + white-space: normal; + max-width: none; + } + .parks td::before { + content: attr(data-label) ":"; + display: inline-block; + min-width: 4.5em; + font-size: 0.85em; + opacity: 0.7; + } + .parks td.is-name { + font-size: 1rem; + } + .parks td.is-name::before { + content: none; + } + .parks td.is-num, + .parks td.is-type { + text-align: left; + } + } diff --git a/src/content/docs/kodaira-data/parks.mdx b/src/content/docs/kodaira-data/parks.mdx index 6cf9576..6887653 100644 --- a/src/content/docs/kodaira-data/parks.mdx +++ b/src/content/docs/kodaira-data/parks.mdx @@ -1,11 +1,11 @@ --- title: 小平市の公園一覧 -description: 小平市内の公園321件を、町名やキーワードで検索できる一覧にまとめました。 +description: 小平市内の公園321件を、町名・キーワード・設備で検索できる一覧にまとめました。 --- import ParksTable from '@/components/ParksTable.astro'; -小平市内の公園の一覧です。町名で絞り込んだり、公園名や住所、遊具などのキーワードで検索したりできます。スマートフォンでは表を横にスクロールできます。 +小平市内の公園の一覧です。町名や設備で絞り込んだり、公園名や住所、遊具などのキーワードで検索したりできます。公園名と面積の見出しを押すと、並べ替えられます。住所を押すと、公園名で地図を検索します。スマートフォンでは1件ずつカードで表示します。 @@ -13,12 +13,14 @@ import ParksTable from '@/components/ParksTable.astro'; 出典は、小平市が公開している次の資料です。 -- 公園名・住所・面積・500㎡以上・ボール遊び:「500㎡以上・未満の小平市立公園一覧(住所順)」(令和6年1月1日現在) +- 公園名・住所・面積・ボール遊び:「500㎡以上・未満の小平市立公園一覧(住所順)」(令和6年1月1日現在) - 種別・南西部指定管理:「別紙7 公園一覧」(令和6年度 指定管理者公募) - トイレ・水飲み場・ベンチ・テーブル・防災用具・遊具:「参考資料7 公園施設一覧(市南西部地域)」(令和6年度 指定管理者公募、公園台帳より) :::note -- 施設の情報(トイレ・水飲み場・ベンチ・テーブル・防災用具・遊具)は、市の南西部地域の公園のみです。全市分は公開されていません。 -- 施設の情報は「竣工時のデータが基」との注記があります。現在と異なる場合があります。 -- 空欄は「—」で示しています。出典に記載がないという意味であり、施設がないという意味ではありません。 +- 「種別」は、市の資料にある公園の区分です。街区は街区公園、近隣は近隣公園、運動は運動公園を指します。 +- 面積の「小・中・大」は目安です。小は1000㎡未満、中は1000〜10000㎡、大は10000㎡以上としています。 +- 「種別」と設備(トイレ・水飲み場・遊具など)の情報は、市の南西部地域の公園のみです。全市分は公開されていません。設備の絞り込みは、この情報がある公園だけが対象です。 +- 設備の情報は「竣工時のデータが基」との注記があります。現在と異なる場合があります。 +- 空欄は「—」で示しています。出典に記載がないという意味であり、設備がないという意味ではありません。 ::: diff --git a/src/data/updates.json b/src/data/updates.json index fc72608..2eabc28 100644 --- a/src/data/updates.json +++ b/src/data/updates.json @@ -1,4 +1,11 @@ [ + { + "title": "小平市の公園一覧", + "url": "/kodaira-data/parks/", + "group": "", + "date": "2026-10-06", + "kind": "new" + }, { "title": "(1)カーボンニュートラルの虚実を、まず科学的に捉えよ", "url": "/ippan-situmon/r2d/3gatu/1-carbon-neutral-giman/", @@ -271,12 +278,5 @@ "group": "令和8年9月定例会", "date": "2026-09-15", "kind": "new" - }, - { - "title": "(3)いじめ対策委員会記録がほぼ皆無だった件に絡む矛盾について", - "url": "/ippan-situmon/r8d/9gatu/3-ijime-taisaku-kiroku-mujun/", - "group": "令和8年9月定例会", - "date": "2026-09-15", - "kind": "new" } ] -- cgit v1.3.1
{col.label} + + 住所 + + 種別施設・備考
- {display(col.key, park[col.key])} -
{park["公園名"]} + {park["住所"]} + + {areaText(park["面積㎡"])}㎡ + + {park["種別"] ? ( + + {park["種別"]} + + ) : ( + EMPTY + )} + + {facilityBadges(park).map((b) => ( + + {b.label} + + ))} + {detailText(park) ? {detailText(park)} : null} + {facilityBadges(park).length === 0 && !detailText(park) ? EMPTY : null} +