const SHEET_ID = "1sNlDhNaoVSUn8AXPA3FTzUfLAHN0MuLYHhHVST8biTg";
const DATA_SHEET_NAME = "partners";
const CODE_SHEET_NAME = "country_region_map";
const DATA_URL = `https://docs.google.com/spreadsheets/d/${SHEET_ID}/gviz/tq?sheet=${DATA_SHEET_NAME}&tqx=out:json`;
const CODE_URL = `https://docs.google.com/spreadsheets/d/${SHEET_ID}/gviz/tq?sheet=${CODE_SHEET_NAME}&tqx=out:json`;
let jsonData = [], filteredData = [], currentPage = 1, rowsPerPage = 15, countryCounts = {}, countryCodeMap = {};
Promise.all([
fetch(DATA_URL).then(res => res.text()),
fetch(CODE_URL).then(res => res.text())
])
.then(([dataText, codeText]) => {
const dataJson = JSON.parse(dataText.substring(47, dataText.length - 2));
const codeJson = JSON.parse(codeText.substring(47, codeText.length - 2));
codeJson.table.rows.forEach(row => {
const countryCn = row.c[2]?.v?.trim();
const isoCode = row.c[7]?.v?.trim();
if (countryCn && isoCode) {
countryCodeMap[countryCn] = isoCode;
}
});
jsonData = dataJson.table.rows.slice(1).map(row => {
const countryCn = row.c[2]?.v || "";
const code = countryCodeMap[countryCn] || "";
return {
RegionCn: row.c[0]?.v || "",
RegionEn: row.c[1]?.v || "",
CountryCn: countryCn,
CountryEn: row.c[3]?.v || "",
SchoolCn: row.c[4]?.v || "",
SchoolEn: row.c[5]?.v || "",
SchoolWeb: row.c[6]?.v || "",
CountryCode: code
};
}).filter(entry => entry.CountryCode);
jsonData.sort((a, b) => {
if (a.RegionEn !== b.RegionEn) return a.RegionEn.localeCompare(b.RegionEn);
if (a.CountryEn !== b.CountryEn) return a.CountryEn.localeCompare(b.CountryEn);
return a.SchoolEn.localeCompare(b.SchoolEn);
});
filteredData = [...jsonData];
calculateCountryCounts();
populateFilters();
displayResults();
initMap();
document.getElementById("search-btn")?.addEventListener("click", () => {
filterAndDisplay();
});
document.getElementById("region-filter")?.addEventListener("change", () => {
updateCountryOptions();
});
});
function calculateCountryCounts() {
countryCounts = {};
filteredData.forEach(entry => {
const countryId = entry.CountryCode;
countryCounts[countryId] = (countryCounts[countryId] || 0) + 1;
});
}
function populateFilters() {
const regionFilter = document.getElementById("region-filter");
const countryFilter = document.getElementById("country-filter");
regionFilter.innerHTML = '';
countryFilter.innerHTML = '';
const regionSet = new Set(jsonData.map(i => i.RegionCn));
regionSet.forEach(region => {
const opt = document.createElement("option");
opt.value = region;
opt.textContent = region;
regionFilter.appendChild(opt);
});
updateCountryOptions();
}
function updateCountryOptions() {
const region = document.getElementById("region-filter").value;
const countryFilter = document.getElementById("country-filter");
countryFilter.innerHTML = '';
const countrySet = new Set(jsonData
.filter(item => !region || item.RegionCn === region)
.map(i => i.CountryCn));
countrySet.forEach(c => {
const opt = document.createElement("option");
opt.value = c;
opt.textContent = c;
countryFilter.appendChild(opt);
});
}
function filterAndDisplay() {
const region = document.getElementById("region-filter").value;
const country = document.getElementById("country-filter").value;
const keyword = document.getElementById("keyword")?.value.trim().toLowerCase() || "";
filteredData = jsonData.filter(entry =>
(!region || entry.RegionCn === region) &&
(!country || entry.CountryCn === country) &&
(!keyword || Object.values(entry).some(val =>
typeof val === "string" && val.toLowerCase().includes(keyword)
))
);
currentPage = 1;
calculateCountryCounts();
displayResults();
}
function displayResults() {
const tbody = document.getElementById("table-body");
const totalCount = document.getElementById("total-count");
const totalPages = Math.ceil(filteredData.length / rowsPerPage);
tbody.innerHTML = "";
const pageData = filteredData.slice((currentPage - 1) * rowsPerPage, currentPage * rowsPerPage);
pageData.forEach(item => {
const row = document.createElement("tr");
row.innerHTML = `
${item.CountryCn} |
${item.SchoolCn} |
${item.SchoolEn} |
${item.RegionCn} |
请点我 |
`;
tbody.appendChild(row);
});
totalCount.textContent = `共 ${filteredData.length} 筆`;
updatePagination(totalPages);
}
function updatePagination(totalPages) {
if (totalPages < 1) return;
const pageNumbers = document.getElementById("page-numbers");
pageNumbers.innerHTML = "";
const maxPagesToShow = 5;
let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2));
let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1);
if (endPage - startPage + 1 < maxPagesToShow) {
startPage = Math.max(1, endPage - maxPagesToShow + 1);
}
const createBtn = (text, page, disabled = false, isActive = false) => {
const btn = document.createElement("button");
btn.textContent = text;
btn.classList.add("page-btn");
if (isActive) btn.classList.add("active");
if (disabled) btn.disabled = true;
btn.addEventListener("click", () => {
currentPage = page;
displayResults();
});
return btn;
};
pageNumbers.appendChild(createBtn("?", 1, currentPage === 1));
pageNumbers.appendChild(createBtn("?", currentPage - 1, currentPage === 1));
for (let i = startPage; i <= endPage; i++) {
pageNumbers.appendChild(createBtn(i, i, false, i === currentPage));
}
pageNumbers.appendChild(createBtn("?", currentPage + 1, currentPage === totalPages));
pageNumbers.appendChild(createBtn("?", totalPages, currentPage === totalPages));
}
function getColorByCount(value) {
if (value >= 20) return am5.color(0x6d9e6d); // 莫蘭迪深綠(調整後)
if (value >= 10) return am5.color(0x90b990); // 莫蘭迪中綠
if (value >= 5) return am5.color(0xb9d4b9); // 莫蘭迪淺綠
if (value > 0) return am5.color(0xd3e0d3); // 莫蘭迪淡綠
return am5.color(0xeceeea); // 非合作國家 - 最淺莫蘭迪灰
}
function initMap() {
const customLabelCoordinates = {
"HK": { lat: 22.3, lon: 114.2 }, "MO": { lat: 22.2, lon: 113.5 },
"SG": { lat: 1.3, lon: 103.8 }, "PH": { lat: 13.4, lon: 122.9 },
"KR": { lat: 36.5, lon: 127.8 }, "TH": { lat: 15.8, lon: 100.9 },
"VN": { lat: 14.1, lon: 108.3 }, "MY": { lat: 4.2, lon: 101.9 },
"BE": { lat: 50.5, lon: 4.5 }, "CZ": { lat: 49.8, lon: 15.5 },
"FR": { lat: 46.2, lon: 2.2 }, "DE": { lat: 51.2, lon: 10.4 },
"HU": { lat: 47.1, lon: 19.4 }, "IT": { lat: 41.9, lon: 12.6 },
"NL": { lat: 52.1, lon: 5.2 }, "PL": { lat: 52.1, lon: 19.3 },
"ES": { lat: 40.5, lon: -3.7 }, "SE": { lat: 60.1, lon: 18.6 },
"CH": { lat: 46.8, lon: 8.2 }, "GB": { lat: 55.3, lon: -3.4 },
"JP": { lat: 36.2, lon: 140.3 }, "CN": { lat: 35.9, lon: 104.2 },
"IN": { lat: 20.6, lon: 78.9 }, "ID": { lat: -0.7, lon: 113.9 },
"AU": { lat: -25.3, lon: 133.8 }, "NZ": { lat: -40.9, lon: 174.9 },
"US": { lat: 37.1, lon: -95.7 }, "CA": { lat: 56.1, lon: -106.3 },
"AT": { lat: 47.5, lon: 14.5 }, "FI": { lat: 64.0, lon: 26.0 },
"BR": { lat: -14.2, lon: -51.9 }, "RO": { lat: 45.9, lon: 24.9 }
};
let root = am5.Root.new("map-container");
root.setThemes([am5themes_Animated.new(root)]);
let chart = root.container.children.push(am5map.MapChart.new(root, {
panX: "translateX",
panY: "translateY",
projection: am5map.geoMercator()
}));
let polygonSeries = chart.series.push(am5map.MapPolygonSeries.new(root, {
geoJSON: am5geodata_worldLow,
exclude: ["AQ"]
}));
polygonSeries.mapPolygons.template.setAll({ interactive: true });
polygonSeries.mapPolygons.template.adapters.add("fill", (fill, target) => {
const data = target.dataItem?.dataContext;
return getColorByCount(data?.value || 0);
});
const allCountryIds = am5geodata_worldLow.features.map(f => f.id);
const dataForAllCountries = allCountryIds.map(id => ({
id,
value: countryCounts[id] || 0
}));
polygonSeries.data.setAll(dataForAllCountries);
let pointSeries = chart.series.push(am5map.MapPointSeries.new(root, {
latitudeField: "latitude",
longitudeField: "longitude"
}));
const labels = Object.entries(countryCounts).map(([code, count]) => {
let lat, lon;
const custom = customLabelCoordinates[code];
if (custom) {
lat = custom.lat;
lon = custom.lon;
} else {
const feature = am5geodata_worldLow.features.find(f => f.id === code);
const coordinates = am5map.getGeoCentroid(feature);
if (!Array.isArray(coordinates)) return null; // 防止錯誤
[lon, lat] = coordinates;
}
const countryName = Object.entries(countryCodeMap).find(([name, iso]) => iso === code)?.[0] || code;
return {
id: code,
latitude: lat,
longitude: lon,
value: count,
title: String(count),
CountryName: countryName
};
}).filter(item => item !== null);
pointSeries.bullets.push((root, dataItem) => {
const context = dataItem.dataContext || {};
const value = Number(context.value || 1);
const radius = 10 + Math.pow(value, 0.9) * 4.5;
const container = am5.Container.new(root, {
centerX: am5.p50,
centerY: am5.p50,
width: radius * 2,
height: radius * 2
});
const circle = am5.Circle.new(root, {
radius: 20,
fill: am5.color(0x4E79A7),
fillOpacity: 0.85,
tooltipText: "{CountryName}: ",
populateText: true,
shadowColor: am5.color(0x000000),
shadowBlur: 10,
shadowOffsetX: 0,
shadowOffsetY: 4,
shadowOpacity: 0.25
});
const label = am5.Label.new(root, {
text: "",
populateText: true,
centerX: am5.p50,
centerY: am5.p50,
fontSize: Math.max(12, radius * 0.5),
fontWeight: "normal",
fill: am5.color(0xffffff)
});
label.adapters.add("fontSize", (fontSize, target) => {
const context = target.dataItem?.dataContext;
const value = parseInt(context?.title || context?.value || "0");
if (!isNaN(value)) {
if (value > 19) {
circle.set("radius", 28);
circle.set("fill", am5.color(0xf16f28));
return 24;
} else if (value < 6) {
circle.set("radius", 14);
circle.set("fill", am5.color(0xffd060));
return 14;
} else {
circle.set("radius", 20);
circle.set("fill", am5.color(0xf19e28));
return 18;
}
}
return 18;
});
container.children.push(circle);
container.children.push(label);
return am5.Bullet.new(root, { sprite: container });
});
pointSeries.data.setAll(labels);
chart.seriesContainer.toFront();
}
document.addEventListener("keydown", function(event) {
if (event.key === "F12" ||
(event.ctrlKey && event.shiftKey && (event.key === "I" || event.key === "J")) ||
(event.ctrlKey && event.key === "U")) {
event.preventDefault();
}
});
// **禁用右鍵**
document.addEventListener("contextmenu", function(event) {
event.preventDefault();
});