Add persistent anonymous usage logging

This commit is contained in:
wuyanwanwu
2026-08-14 01:05:50 +08:00
parent b890ce4035
commit 5a312f5039
18 changed files with 169 additions and 48 deletions
+1
View File
@@ -40,3 +40,4 @@ yarn-error.log*
/outputs/ /outputs/
/work/ /work/
/mard-source.html /mard-source.html
/data/
+2 -2
View File
@@ -76,7 +76,7 @@ input[type="range"] { accent-color: var(--green); }
.zoom-control button { width: 36px; height: 36px; border: 1px solid var(--line); background: white; cursor: pointer; font-size: 20px; } .zoom-control button { width: 36px; height: 36px; border: 1px solid var(--line); background: white; cursor: pointer; font-size: 20px; }
.zoom-control input { width: 95px; } .zoom-control input { width: 95px; }
.zoom-control output { width: 38px; color: var(--muted); font-size: 11px; text-align: right; } .zoom-control output { width: 38px; color: var(--muted); font-size: 11px; text-align: right; }
.canvas-viewport { position: relative; width: 100%; height: clamp(520px, 68vh, 760px); min-height: 0; flex: 0 0 clamp(520px, 68vh, 760px); contain: size layout; overflow: hidden; } .canvas-viewport { position: relative; width: min(100%, 82vh, 1000px); aspect-ratio: 1 / 1; height: auto; min-height: 0; flex: 0 0 auto; align-self: center; contain: size layout; overflow: hidden; }
.canvas-stage { position: absolute; inset: 0; width: auto; height: auto; overflow: scroll; display: block; padding: 36px; cursor: grab; touch-action: none; overscroll-behavior: contain; user-select: none; scrollbar-gutter: stable; } .canvas-stage { position: absolute; inset: 0; width: auto; height: auto; overflow: scroll; display: block; padding: 36px; cursor: grab; touch-action: none; overscroll-behavior: contain; user-select: none; scrollbar-gutter: stable; }
.canvas-stage.is-dragging { cursor: grabbing; } .canvas-stage.is-dragging { cursor: grabbing; }
.canvas-wrap { width: max-content; line-height: 0; position: relative; margin: auto; border: 1px solid var(--ink); box-shadow: 10px 10px 0 rgba(32,39,36,.13); background: white; } .canvas-wrap { width: max-content; line-height: 0; position: relative; margin: auto; border: 1px solid var(--ink); box-shadow: 10px 10px 0 rgba(32,39,36,.13); background: white; }
@@ -161,7 +161,7 @@ input[type="range"] { accent-color: var(--green); }
.pattern-toolbar { align-items: flex-start; flex-direction: column; padding: 20px; } .pattern-toolbar { align-items: flex-start; flex-direction: column; padding: 20px; }
.zoom-control { width: 100%; } .zoom-control { width: 100%; }
.zoom-control input { flex: 1; } .zoom-control input { flex: 1; }
.canvas-viewport { height: max(360px, min(68vh, 620px)); min-height: 0; flex-basis: max(360px, min(68vh, 620px)); } .canvas-viewport { width: min(100%, 82vh); height: auto; min-height: 0; aspect-ratio: 1 / 1; flex-basis: auto; }
.canvas-stage { display: block; padding: 24px; } .canvas-stage { display: block; padding: 24px; }
.canvas-wrap { margin: auto; } .canvas-wrap { margin: auto; }
.pattern-footer { align-items: flex-start; flex-wrap: wrap; padding: 14px 18px; } .pattern-footer { align-items: flex-start; flex-wrap: wrap; padding: 14px 18px; }
+91 -28
View File
@@ -28,8 +28,17 @@ type ColorCluster = {
type SamplingStrategy = "smooth" | "dominant"; type SamplingStrategy = "smooth" | "dominant";
function logUsage(event: Record<string, unknown>) {
void fetch("/api/usage", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(event),
keepalive: true,
}).catch(() => undefined);
}
type Pixel = { type Pixel = {
color: BeadColor; color: BeadColor | null;
}; };
type HoveredPixel = { type HoveredPixel = {
@@ -47,9 +56,11 @@ type SavedPattern = {
width: number; width: number;
height: number; height: number;
colorLimit: number; colorLimit: number;
codes: string[]; codes: Array<string | null>;
}; };
const TRANSPARENT_ALPHA_THRESHOLD = 128;
const MARD_SERIES_NAMES: Record<string, string> = { const MARD_SERIES_NAMES: Record<string, string> = {
A: "黄橙系", B: "绿色系", C: "蓝青系", D: "紫蓝系", E: "粉红系", A: "黄橙系", B: "绿色系", C: "蓝青系", D: "紫蓝系", E: "粉红系",
F: "红色系", G: "肤棕系", H: "黑白灰系", M: "莫兰迪系", F: "红色系", G: "肤棕系", H: "黑白灰系", M: "莫兰迪系",
@@ -102,9 +113,13 @@ function nearestColor(lab: Oklab, palette: BeadColor[]) {
} }
function mergePerceptualColors(data: Uint8ClampedArray) { function mergePerceptualColors(data: Uint8ClampedArray) {
const pixelKeys: number[] = []; const pixelKeys: Array<number | null> = [];
const histogram = new Map<number, { count: number; red: number; green: number; blue: number }>(); const histogram = new Map<number, { count: number; red: number; green: number; blue: number }>();
for (let index = 0; index < data.length; index += 4) { for (let index = 0; index < data.length; index += 4) {
if (data[index + 3] < TRANSPARENT_ALPHA_THRESHOLD) {
pixelKeys.push(null);
continue;
}
const red = data[index]; const red = data[index];
const green = data[index + 1]; const green = data[index + 1];
const blue = data[index + 2]; const blue = data[index + 2];
@@ -187,7 +202,7 @@ function chooseDistinctMardColors(clusters: ColorCluster[], maximum: number) {
separated.push(candidate.color); separated.push(candidate.color);
} }
} }
return separated.slice(0, Math.max(1, Math.min(maximum, separated.length))); return separated.slice(0, Math.max(0, Math.min(maximum, separated.length)));
} }
function sampleDominantRegions( function sampleDominantRegions(
@@ -204,8 +219,7 @@ function sampleDominantRegions(
sample.width = width * scale; sample.width = width * scale;
sample.height = height * scale; sample.height = height * scale;
const ctx = sample.getContext("2d", { willReadFrequently: true })!; const ctx = sample.getContext("2d", { willReadFrequently: true })!;
ctx.fillStyle = "#f7f5ed"; ctx.clearRect(0, 0, sample.width, sample.height);
ctx.fillRect(0, 0, sample.width, sample.height);
drawFittedImage(ctx, image, sample.width, sample.height, fitMode, cropZoom, cropX, cropY); drawFittedImage(ctx, image, sample.width, sample.height, fitMode, cropZoom, cropX, cropY);
const source = ctx.getImageData(0, 0, sample.width, sample.height).data; const source = ctx.getImageData(0, 0, sample.width, sample.height).data;
const result = new Uint8ClampedArray(width * height * 4); const result = new Uint8ClampedArray(width * height * 4);
@@ -215,9 +229,14 @@ function sampleDominantRegions(
for (let row = 0; row < height; row++) { for (let row = 0; row < height; row++) {
for (let column = 0; column < width; column++) { for (let column = 0; column < width; column++) {
const groups: Array<{ count: number; lab: Oklab; samples: Array<{ rgb: [number, number, number]; lab: Oklab }> }> = []; const groups: Array<{ count: number; lab: Oklab; samples: Array<{ rgb: [number, number, number]; lab: Oklab }> }> = [];
let transparentCount = 0;
for (let offsetY = 0; offsetY < scale; offsetY++) { for (let offsetY = 0; offsetY < scale; offsetY++) {
for (let offsetX = 0; offsetX < scale; offsetX++) { for (let offsetX = 0; offsetX < scale; offsetX++) {
const sourceIndex = ((row * scale + offsetY) * sample.width + column * scale + offsetX) * 4; const sourceIndex = ((row * scale + offsetY) * sample.width + column * scale + offsetX) * 4;
if (source[sourceIndex + 3] < TRANSPARENT_ALPHA_THRESHOLD) {
transparentCount += 1;
continue;
}
const rgb: [number, number, number] = [source[sourceIndex], source[sourceIndex + 1], source[sourceIndex + 2]]; const rgb: [number, number, number] = [source[sourceIndex], source[sourceIndex + 1], source[sourceIndex + 2]];
const lab = rgbToOklab(rgb); const lab = rgbToOklab(rgb);
let closest: (typeof groups)[number] | undefined; let closest: (typeof groups)[number] | undefined;
@@ -239,23 +258,28 @@ function sampleDominantRegions(
} }
} }
} }
const targetIndex = (row * width + column) * 4;
if (transparentCount >= scale * scale / 2 || groups.length === 0) {
result[targetIndex + 3] = 0;
confidence[row * width + column] = transparentCount / (scale * scale);
continue;
}
const dominant = groups.sort((a, b) => b.count - a.count)[0]; const dominant = groups.sort((a, b) => b.count - a.count)[0];
const representative = dominant.samples.reduce((best, current) => const representative = dominant.samples.reduce((best, current) =>
oklabDistance(current.lab, dominant.lab) < oklabDistance(best.lab, dominant.lab) ? current : best, oklabDistance(current.lab, dominant.lab) < oklabDistance(best.lab, dominant.lab) ? current : best,
); );
const targetIndex = (row * width + column) * 4;
result[targetIndex] = representative.rgb[0]; result[targetIndex] = representative.rgb[0];
result[targetIndex + 1] = representative.rgb[1]; result[targetIndex + 1] = representative.rgb[1];
result[targetIndex + 2] = representative.rgb[2]; result[targetIndex + 2] = representative.rgb[2];
result[targetIndex + 3] = 255; result[targetIndex + 3] = 255;
confidence[row * width + column] = dominant.count / (scale * scale); confidence[row * width + column] = dominant.count / Math.max(1, scale * scale - transparentCount);
} }
} }
return { data: result, confidence }; return { data: result, confidence };
} }
function cleanLowConfidenceIsolatedColors( function cleanLowConfidenceIsolatedColors(
colors: BeadColor[], colors: Array<BeadColor | null>,
confidence: Float32Array, confidence: Float32Array,
width: number, width: number,
height: number, height: number,
@@ -267,6 +291,7 @@ function cleanLowConfidenceIsolatedColors(
for (let column = 0; column < width; column++) { for (let column = 0; column < width; column++) {
const index = row * width + column; const index = row * width + column;
const current = colors[index]; const current = colors[index];
if (!current) continue;
if (confidence[index] >= 0.625) continue; if (confidence[index] >= 0.625) continue;
const chroma = Math.hypot(current.lab[1], current.lab[2]); const chroma = Math.hypot(current.lab[1], current.lab[2]);
const protectedDetail = current.lab[0] < 0.32 || current.lab[0] > 0.96 || chroma > 0.14; const protectedDetail = current.lab[0] < 0.32 || current.lab[0] > 0.96 || chroma > 0.14;
@@ -283,6 +308,7 @@ function cleanLowConfidenceIsolatedColors(
if (neighborRow < 0 || neighborColumn < 0 || neighborRow >= height || neighborColumn >= width) continue; if (neighborRow < 0 || neighborColumn < 0 || neighborRow >= height || neighborColumn >= width) continue;
const neighborIndex = neighborRow * width + neighborColumn; const neighborIndex = neighborRow * width + neighborColumn;
const neighbor = colors[neighborIndex]; const neighbor = colors[neighborIndex];
if (!neighbor) continue;
if (neighbor.code === current.code) sameColorNeighbors += 1; if (neighbor.code === current.code) sameColorNeighbors += 1;
const entry = neighborCounts.get(neighbor.code) ?? { color: neighbor, count: 0, confidentCount: 0 }; const entry = neighborCounts.get(neighbor.code) ?? { color: neighbor, count: 0, confidentCount: 0 };
entry.count += 1; entry.count += 1;
@@ -370,11 +396,11 @@ function makeDemoImage() {
export default function Home() { export default function Home() {
const [sourceUrl, setSourceUrl] = useState(""); const [sourceUrl, setSourceUrl] = useState("");
const [sourceName, setSourceName] = useState("示例:田野小屋"); const [sourceName, setSourceName] = useState("示例:田野小屋");
const [requestedWidth, setRequestedWidth] = useState(32); const [requestedWidth, setRequestedWidth] = useState(100);
const [requestedHeight, setRequestedHeight] = useState(32); const [requestedHeight, setRequestedHeight] = useState(100);
const [gridWidth, setGridWidth] = useState(32); const [gridWidth, setGridWidth] = useState(100);
const [gridHeight, setGridHeight] = useState(32); const [gridHeight, setGridHeight] = useState(100);
const [colorLimit, setColorLimit] = useState(18); const [colorLimit, setColorLimit] = useState(64);
const [fitMode, setFitMode] = useState<"cover" | "contain">("cover"); const [fitMode, setFitMode] = useState<"cover" | "contain">("cover");
const [samplingStrategy, setSamplingStrategy] = useState<SamplingStrategy>("smooth"); const [samplingStrategy, setSamplingStrategy] = useState<SamplingStrategy>("smooth");
const [cropZoom, setCropZoom] = useState(1); const [cropZoom, setCropZoom] = useState(1);
@@ -383,7 +409,7 @@ export default function Home() {
const [pixels, setPixels] = useState<Pixel[]>([]); const [pixels, setPixels] = useState<Pixel[]>([]);
const [selectedCodes, setSelectedCodes] = useState<Set<string>>(new Set()); const [selectedCodes, setSelectedCodes] = useState<Set<string>>(new Set());
const [onlySelected, setOnlySelected] = useState(false); const [onlySelected, setOnlySelected] = useState(false);
const [zoom, setZoom] = useState(24); const [zoom, setZoom] = useState(8);
const [showGrid, setShowGrid] = useState(true); const [showGrid, setShowGrid] = useState(true);
const [showCodes, setShowCodes] = useState(true); const [showCodes, setShowCodes] = useState(true);
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
@@ -409,6 +435,7 @@ export default function Home() {
const suppressCanvasClickRef = useRef(false); const suppressCanvasClickRef = useRef(false);
useEffect(() => { useEffect(() => {
logUsage({ event: "page_view" });
const saved = localStorage.getItem("bead-pattern-history"); const saved = localStorage.getItem("bead-pattern-history");
if (!saved) return; if (!saved) return;
queueMicrotask(() => { queueMicrotask(() => {
@@ -419,6 +446,7 @@ export default function Home() {
const convertImage = ( const convertImage = (
image = sourceImageRef.current, image = sourceImageRef.current,
crop = { zoom: cropZoom, x: cropX, y: cropY }, crop = { zoom: cropZoom, x: cropX, y: cropY },
recordUsage = true,
) => { ) => {
if (!image) return; if (!image) return;
const width = Math.max(8, Math.min(256, requestedWidth)); const width = Math.max(8, Math.min(256, requestedWidth));
@@ -434,15 +462,14 @@ export default function Home() {
sample.width = width; sample.width = width;
sample.height = height; sample.height = height;
const ctx = sample.getContext("2d", { willReadFrequently: true })!; const ctx = sample.getContext("2d", { willReadFrequently: true })!;
ctx.fillStyle = "#f7f5ed"; ctx.clearRect(0, 0, width, height);
ctx.fillRect(0, 0, width, height);
drawFittedImage(ctx, image, width, height, fitMode, crop.zoom, crop.x, crop.y); drawFittedImage(ctx, image, width, height, fitMode, crop.zoom, crop.x, crop.y);
data = ctx.getImageData(0, 0, width, height).data; data = ctx.getImageData(0, 0, width, height).data;
} }
const { pixelKeys, clusters, binClusters } = mergePerceptualColors(data); const { pixelKeys, clusters, binClusters } = mergePerceptualColors(data);
const limitedPalette = chooseDistinctMardColors(clusters, Math.max(2, Math.min(colorLimit, PALETTE.length))); const limitedPalette = chooseDistinctMardColors(clusters, Math.max(2, Math.min(colorLimit, PALETTE.length)));
const clusterMatches = clusters.map((cluster) => nearestColor(cluster.lab, limitedPalette)); const clusterMatches = clusters.map((cluster) => nearestColor(cluster.lab, limitedPalette));
let matchedColors = pixelKeys.map((key) => clusterMatches[binClusters.get(key) ?? 0]); let matchedColors: Array<BeadColor | null> = pixelKeys.map((key) => key === null ? null : clusterMatches[binClusters.get(key) ?? 0]);
let cleanedCount = 0; let cleanedCount = 0;
if (dominantConfidence) { if (dominantConfidence) {
const cleaned = cleanLowConfidenceIsolatedColors(matchedColors, dominantConfidence, width, height); const cleaned = cleanLowConfidenceIsolatedColors(matchedColors, dominantConfidence, width, height);
@@ -457,8 +484,21 @@ export default function Home() {
setPixels(converted); setPixels(converted);
setSelectedCodes(new Set()); setSelectedCodes(new Set());
const strategyName = samplingStrategy === "dominant" ? `区域主色 · 清理 ${cleanedCount} 个低可信孤立格` : "平滑取色"; const strategyName = samplingStrategy === "dominant" ? `区域主色 · 清理 ${cleanedCount} 个低可信孤立格` : "平滑取色";
const actualColorCount = new Set(matchedColors.map((color) => color.code)).size; const actualColorCount = new Set(matchedColors.filter((color): color is BeadColor => color !== null).map((color) => color.code)).size;
setStatus(`已转换为 ${width} × ${height} · ${strategyName} · ${actualColorCount} 种差异色`); const actualBeadCount = matchedColors.filter((color) => color !== null).length;
if (recordUsage) {
logUsage({
event: "conversion",
width,
height,
sampling_strategy: samplingStrategy,
color_limit: colorLimit,
actual_colors: actualColorCount,
bead_count: actualBeadCount,
transparent_cells: width * height - actualBeadCount,
});
}
setStatus(`已转换为 ${width} × ${height} · ${actualBeadCount} 颗拼豆 · ${strategyName} · ${actualColorCount} 种差异色`);
}; };
useEffect(() => { useEffect(() => {
@@ -467,7 +507,7 @@ export default function Home() {
image.onload = () => { image.onload = () => {
sourceImageRef.current = image; sourceImageRef.current = image;
setSourceUrl(demo); setSourceUrl(demo);
convertImage(image); convertImage(image, undefined, false);
}; };
image.src = demo; image.src = demo;
// This intentionally runs once to create the initial example. // This intentionally runs once to create the initial example.
@@ -493,12 +533,14 @@ export default function Home() {
const usedColors = useMemo(() => { const usedColors = useMemo(() => {
const counts = new Map<string, number>(); const counts = new Map<string, number>();
pixels.forEach(({ color }) => counts.set(color.code, (counts.get(color.code) ?? 0) + 1)); pixels.forEach(({ color }) => { if (color) counts.set(color.code, (counts.get(color.code) ?? 0) + 1); });
return PALETTE.filter((color) => counts.has(color.code)) return PALETTE.filter((color) => counts.has(color.code))
.map((color) => ({ ...color, count: counts.get(color.code)! })) .map((color) => ({ ...color, count: counts.get(color.code)! }))
.sort((a, b) => b.count - a.count); .sort((a, b) => b.count - a.count);
}, [pixels]); }, [pixels]);
const beadCount = useMemo(() => pixels.reduce((total, pixel) => total + (pixel.color ? 1 : 0), 0), [pixels]);
const filteredColors = useMemo(() => { const filteredColors = useMemo(() => {
const key = query.trim().toLowerCase(); const key = query.trim().toLowerCase();
return usedColors.filter((color) => !key || color.code.toLowerCase().includes(key) || color.name.includes(key) || color.hex.toLowerCase().includes(key)); return usedColors.filter((color) => !key || color.code.toLowerCase().includes(key) || color.name.includes(key) || color.hex.toLowerCase().includes(key));
@@ -541,6 +583,15 @@ export default function Home() {
pixels.forEach(({ color }, index) => { pixels.forEach(({ color }, index) => {
const x = ruler + (index % gridWidth) * cell; const x = ruler + (index % gridWidth) * cell;
const y = ruler + Math.floor(index / gridWidth) * cell; const y = ruler + Math.floor(index / gridWidth) * cell;
if (!color) {
ctx.clearRect(x, y, cell, cell);
if (showGrid) {
ctx.strokeStyle = "rgba(32,38,36,.12)";
ctx.lineWidth = 0.6;
ctx.strokeRect(x + 0.3, y + 0.3, cell - 0.6, cell - 0.6);
}
return;
}
const hasSelection = selectedCodes.size > 0; const hasSelection = selectedCodes.size > 0;
const isSelected = selectedCodes.has(color.code); const isSelected = selectedCodes.has(color.code);
if (onlySelected && hasSelection && !isSelected) { if (onlySelected && hasSelection && !isSelected) {
@@ -565,6 +616,18 @@ export default function Home() {
ctx.fillText(color.code, x + cell / 2, y + cell / 2); ctx.fillText(color.code, x + cell / 2, y + cell / 2);
} }
}); });
if (showGrid) {
ctx.strokeStyle = "rgba(32,38,36,.48)";
ctx.lineWidth = 1.5;
for (let column = 5; column < gridWidth; column += 5) {
const x = ruler + column * cell;
ctx.beginPath(); ctx.moveTo(x, ruler); ctx.lineTo(x, ruler + gridHeight * cell); ctx.stroke();
}
for (let row = 5; row < gridHeight; row += 5) {
const y = ruler + row * cell;
ctx.beginPath(); ctx.moveTo(ruler, y); ctx.lineTo(ruler + gridWidth * cell, y); ctx.stroke();
}
}
}, [pixels, gridWidth, gridHeight, zoom, selectedCodes, onlySelected, showGrid, showCodes]); }, [pixels, gridWidth, gridHeight, zoom, selectedCodes, onlySelected, showGrid, showCodes]);
const handleUpload = (event: ChangeEvent<HTMLInputElement>) => { const handleUpload = (event: ChangeEvent<HTMLInputElement>) => {
@@ -686,7 +749,7 @@ export default function Home() {
const x = Math.floor((event.clientX - rect.left - ruler) / zoom); const x = Math.floor((event.clientX - rect.left - ruler) / zoom);
const y = Math.floor((event.clientY - rect.top - ruler) / zoom); const y = Math.floor((event.clientY - rect.top - ruler) / zoom);
const pixel = pixels[y * gridWidth + x]; const pixel = pixels[y * gridWidth + x];
if (x >= 0 && y >= 0 && x < gridWidth && y < gridHeight && pixel) toggleColor(pixel.color.code); if (x >= 0 && y >= 0 && x < gridWidth && y < gridHeight && pixel?.color) toggleColor(pixel.color.code);
}; };
const handleCanvasMove = (event: React.MouseEvent<HTMLCanvasElement>) => { const handleCanvasMove = (event: React.MouseEvent<HTMLCanvasElement>) => {
@@ -701,7 +764,7 @@ export default function Home() {
const column = Math.floor((event.clientX - rect.left - ruler) / zoom); const column = Math.floor((event.clientX - rect.left - ruler) / zoom);
const row = Math.floor((event.clientY - rect.top - ruler) / zoom); const row = Math.floor((event.clientY - rect.top - ruler) / zoom);
const pixel = pixels[row * gridWidth + column]; const pixel = pixels[row * gridWidth + column];
if (column < 0 || row < 0 || column >= gridWidth || row >= gridHeight || !pixel) { if (column < 0 || row < 0 || column >= gridWidth || row >= gridHeight || !pixel?.color) {
setHoveredPixel(null); setHoveredPixel(null);
return; return;
} }
@@ -722,7 +785,7 @@ export default function Home() {
width: gridWidth, width: gridWidth,
height: gridHeight, height: gridHeight,
colorLimit, colorLimit,
codes: pixels.map((pixel) => pixel.color.code), codes: pixels.map((pixel) => pixel.color?.code ?? null),
}; };
persistHistory([entry, ...history].slice(0, 30)); persistHistory([entry, ...history].slice(0, 30));
setStatus("图纸已保存到本机历史记录"); setStatus("图纸已保存到本机历史记录");
@@ -730,7 +793,7 @@ export default function Home() {
const restorePattern = (entry: SavedPattern) => { const restorePattern = (entry: SavedPattern) => {
const colorMap = new Map(PALETTE.map((color) => [color.code, color])); const colorMap = new Map(PALETTE.map((color) => [color.code, color]));
setPixels(entry.codes.map((code) => ({ color: colorMap.get(code) ?? PALETTE[0] }))); setPixels(entry.codes.map((code) => ({ color: code === null ? null : colorMap.get(code) ?? PALETTE[0] })));
setGridWidth(entry.width); setGridWidth(entry.width);
setGridHeight(entry.height); setGridHeight(entry.height);
setRequestedWidth(entry.width); setRequestedWidth(entry.width);
@@ -868,7 +931,7 @@ export default function Home() {
</section> </section>
<aside className="color-panel"> <aside className="color-panel">
<div className="panel-heading"><span>03</span><div><h2></h2><p>{usedColors.length} · {pixels.length} </p></div></div> <div className="panel-heading"><span>03</span><div><h2></h2><p>{usedColors.length} · {beadCount} </p></div></div>
<div className="search-box"><span></span><input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="输入 A6、MARD A 或 #FEAC4C" /></div> <div className="search-box"><span></span><input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="输入 A6、MARD A 或 #FEAC4C" /></div>
<div className="selection-tools"> <div className="selection-tools">
<label className="switch-row"><input type="checkbox" checked={onlySelected} onChange={(e) => setOnlySelected(e.target.checked)} /><span className="switch" /><span></span></label> <label className="switch-row"><input type="checkbox" checked={onlySelected} onChange={(e) => setOnlySelected(e.target.checked)} /><span className="switch" /><span></span></label>
@@ -894,7 +957,7 @@ export default function Home() {
</div> </div>
{filteredHistory.length ? <div className="history-grid">{filteredHistory.map((entry) => { {filteredHistory.length ? <div className="history-grid">{filteredHistory.map((entry) => {
const colorCounts = new Map<string, number>(); const colorCounts = new Map<string, number>();
entry.codes.forEach((code) => colorCounts.set(code, (colorCounts.get(code) ?? 0) + 1)); entry.codes.forEach((code) => { if (code) colorCounts.set(code, (colorCounts.get(code) ?? 0) + 1); });
const topCodes = [...colorCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6); const topCodes = [...colorCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6);
return <article className="history-card" key={entry.id}> return <article className="history-card" key={entry.id}>
<div className="history-swatches">{topCodes.map(([code, count]) => <i key={code} style={{ backgroundColor: PALETTE.find((color) => color.code === code)?.hex, flexGrow: count }} />)}</div> <div className="history-swatches">{topCodes.map(([code, count]) => <i key={code} style={{ backgroundColor: PALETTE.find((color) => color.code === code)?.hex, flexGrow: count }} />)}</div>
+56 -1
View File
@@ -1,10 +1,14 @@
import { createReadStream, existsSync, statSync } from "node:fs"; import { appendFile, createReadStream, existsSync, mkdirSync, statSync } from "node:fs";
import { createHash } from "node:crypto";
import { createServer } from "node:http"; import { createServer } from "node:http";
import { extname, join, normalize } from "node:path"; import { extname, join, normalize } from "node:path";
const packagedRoot = join(process.cwd(), "public"); const packagedRoot = join(process.cwd(), "public");
const root = existsSync(join(packagedRoot, "index.html")) ? packagedRoot : join(process.cwd(), "out"); const root = existsSync(join(packagedRoot, "index.html")) ? packagedRoot : join(process.cwd(), "out");
const port = Number(process.env.APP_PORT || 3200); const port = Number(process.env.APP_PORT || 3200);
const dataDirectory = process.env.DATA_DIR || join(process.cwd(), "data");
const usageLogPath = join(dataDirectory, "usage.jsonl");
mkdirSync(dataDirectory, { recursive: true });
const mimeTypes = { const mimeTypes = {
".css": "text/css; charset=utf-8", ".css": "text/css; charset=utf-8",
".html": "text/html; charset=utf-8", ".html": "text/html; charset=utf-8",
@@ -17,8 +21,59 @@ const mimeTypes = {
".webp": "image/webp", ".webp": "image/webp",
}; };
function sendJson(response, status, value) {
response.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
response.end(JSON.stringify(value));
}
function writeUsageLog(request, response) {
let body = "";
request.setEncoding("utf8");
request.on("data", (chunk) => {
body += chunk;
if (body.length > 16_384) request.destroy();
});
request.on("end", async () => {
try {
const input = JSON.parse(body || "{}");
if (!['page_view', 'conversion'].includes(input.event)) {
sendJson(response, 400, { error: "Invalid event" });
return;
}
const entry = {
timestamp: new Date().toISOString(),
event: input.event,
client_hash: request.headers["x-forwarded-for"] || request.socket.remoteAddress
? createHash("sha256").update(String(request.headers["x-forwarded-for"] || request.socket.remoteAddress)).digest("hex").slice(0, 16)
: undefined,
...(input.event === "conversion" ? {
width: Math.max(8, Math.min(256, Number(input.width) || 0)),
height: Math.max(8, Math.min(256, Number(input.height) || 0)),
sampling_strategy: input.sampling_strategy === "dominant" ? "dominant" : "smooth",
color_limit: Math.max(2, Math.min(64, Number(input.color_limit) || 0)),
actual_colors: Math.max(0, Math.min(221, Number(input.actual_colors) || 0)),
bead_count: Math.max(0, Math.min(65_536, Number(input.bead_count) || 0)),
transparent_cells: Math.max(0, Math.min(65_536, Number(input.transparent_cells) || 0)),
} : {}),
};
await appendFile(usageLogPath, `${JSON.stringify(entry)}\n`, "utf8");
sendJson(response, 202, { ok: true });
} catch {
sendJson(response, 400, { error: "Invalid request" });
}
});
}
createServer((request, response) => { createServer((request, response) => {
const pathname = decodeURIComponent(new URL(request.url || "/", "http://localhost").pathname); const pathname = decodeURIComponent(new URL(request.url || "/", "http://localhost").pathname);
if (pathname === "/api/usage" && request.method === "POST") {
writeUsageLog(request, response);
return;
}
if (pathname.startsWith("/api/")) {
sendJson(response, 404, { error: "Not found" });
return;
}
const relativePath = normalize(pathname).replace(/^([/\\])+/, ""); const relativePath = normalize(pathname).replace(/^([/\\])+/, "");
let filePath = join(root, relativePath || "index.html"); let filePath = join(root, relativePath || "index.html");
+2
View File
@@ -4,3 +4,5 @@ services:
restart: unless-stopped restart: unless-stopped
ports: ports:
- "${APP_PORT:-3200}:3200" - "${APP_PORT:-3200}:3200"
volumes:
- ./data:/app/data
+3 -3
View File
@@ -11,7 +11,7 @@
"name": "rolldown-runtime" "name": "rolldown-runtime"
}, },
"app/page.tsx": { "app/page.tsx": {
"file": "_next/static/chunks/page-DuaIrYp1.js", "file": "_next/static/chunks/page-OnKjbyiO.js",
"name": "page", "name": "page",
"src": "app/page.tsx", "src": "app/page.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -21,7 +21,7 @@
] ]
}, },
"node_modules/vinext/dist/shims/layout-segment-context.js": { "node_modules/vinext/dist/shims/layout-segment-context.js": {
"file": "_next/static/chunks/layout-segment-context-D3q3zeiS.js", "file": "_next/static/chunks/layout-segment-context-a2puOOKn.js",
"name": "layout-segment-context", "name": "layout-segment-context",
"src": "node_modules/vinext/dist/shims/layout-segment-context.js", "src": "node_modules/vinext/dist/shims/layout-segment-context.js",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -32,7 +32,7 @@
] ]
}, },
"virtual:vinext-app-browser-entry": { "virtual:vinext-app-browser-entry": {
"file": "_next/static/chunks/index-CYDm4YY8.js", "file": "_next/static/chunks/index-DJm5cxJ6.js",
"name": "index", "name": "index",
"src": "virtual:vinext-app-browser-entry", "src": "virtual:vinext-app-browser-entry",
"isEntry": true, "isEntry": true,
+2 -2
View File
@@ -1,2 +1,2 @@
<!DOCTYPE html><html lang="zh-CN"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/css/index.JNuJjVb4.css" data-rsc-css-href="/_next/static/css/index.JNuJjVb4.css" data-precedence="vite-rsc/importer-resources"/><link rel="modulepreload" fetchPriority="low" href="/_next/static/chunks/index-CYDm4YY8.js"/><script src="/_next/static/chunks/rolldown-runtime-C60lm6uB.js" type="module" async=""></script><script src="/_next/static/chunks/framework-BgSIrAUN.js" type="module" async=""></script><meta name="robots" content="noindex"/><title>豆格工坊|图片转拼豆图纸</title><meta name="description" content="在浏览器中把图片转换为拼豆像素图纸,按色号、名称或像素格筛选颜色。"/><title>404: This page could not be found.</title><script>Object.assign(((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}),{params:{},nav:{"pathname":"/__vinext_nonexistent_for_404__","searchParams":[]}})</script><link rel="modulepreload" href="/_next/static/chunks/index-CYDm4YY8.js" /> <!DOCTYPE html><html lang="zh-CN"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/css/index.B96d0yxr.css" data-rsc-css-href="/_next/static/css/index.B96d0yxr.css" data-precedence="vite-rsc/importer-resources"/><link rel="modulepreload" fetchPriority="low" href="/_next/static/chunks/index-DJm5cxJ6.js"/><script src="/_next/static/chunks/rolldown-runtime-C60lm6uB.js" type="module" async=""></script><script src="/_next/static/chunks/framework-BgSIrAUN.js" type="module" async=""></script><meta name="robots" content="noindex"/><title>豆格工坊|图片转拼豆图纸</title><meta name="description" content="在浏览器中把图片转换为拼豆像素图纸,按色号、名称或像素格筛选颜色。"/><title>404: This page could not be found.</title><script>Object.assign(((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}),{params:{},nav:{"pathname":"/__vinext_nonexistent_for_404__","searchParams":[]}})</script><link rel="modulepreload" href="/_next/static/chunks/index-DJm5cxJ6.js" />
</head><body><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><script type="module" src="/_next/static/chunks/index-CYDm4YY8.js" id="_R_" async=""></script><script>((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}).rsc.push("1:\"$Sreact.fragment\"\n")</script><script>((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}).rsc.push(":HL[\"/_next/static/css/index.JNuJjVb4.css\",\"style\" ]\n")</script><script>((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}).rsc.push("0:{\"__route\":\"route:/__vinext_nonexistent_for_404__\",\"__interceptionContext\":null,\"__layoutIds\":[],\"__rootLayout\":null,\"route:/__vinext_nonexistent_for_404__\":[[[\"$\",\"link\",\"css:/_next/static/css/index.JNuJjVb4.css\",{\"rel\":\"stylesheet\",\"precedence\":\"vite-rsc/importer-resources\",\"href\":\"/_next/static/css/index.JNuJjVb4.css\",\"data-rsc-css-href\":\"/_next/static/css/index.JNuJjVb4.css\"}],\"$undefined\"],[\"$\",\"html\",null,{\"lang\":\"zh-CN\",\"children\":[\"$\",\"body\",null,{\"children\":[[\"$\",\"meta\",\"charset\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"robots\",{\"name\":\"robots\",\"content\":\"noindex\"}],[\"$\",\"$1\",\"metadata\",{\"children\":[[\"$\",\"title\",\"0\",{\"children\":\"豆格工坊|图片转拼豆图纸\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"在浏览器中把图片转换为拼豆像素图纸,按色号、名称或像素格筛选颜色。\"}]]}],[\"$\",\"$1\",\"viewport\",{\"children\":[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]}],[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]]]}]}]]}\n")</script><script>((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}).done=true</script></body></html> </head><body><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><script type="module" src="/_next/static/chunks/index-DJm5cxJ6.js" id="_R_" async=""></script><script>((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}).rsc.push("1:\"$Sreact.fragment\"\n")</script><script>((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}).rsc.push(":HL[\"/_next/static/css/index.B96d0yxr.css\",\"style\" ]\n")</script><script>((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}).rsc.push("0:{\"__route\":\"route:/__vinext_nonexistent_for_404__\",\"__interceptionContext\":null,\"__layoutIds\":[],\"__rootLayout\":null,\"route:/__vinext_nonexistent_for_404__\":[[[\"$\",\"link\",\"css:/_next/static/css/index.B96d0yxr.css\",{\"rel\":\"stylesheet\",\"precedence\":\"vite-rsc/importer-resources\",\"href\":\"/_next/static/css/index.B96d0yxr.css\",\"data-rsc-css-href\":\"/_next/static/css/index.B96d0yxr.css\"}],\"$undefined\"],[\"$\",\"html\",null,{\"lang\":\"zh-CN\",\"children\":[\"$\",\"body\",null,{\"children\":[[\"$\",\"meta\",\"charset\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"robots\",{\"name\":\"robots\",\"content\":\"noindex\"}],[\"$\",\"$1\",\"metadata\",{\"children\":[[\"$\",\"title\",\"0\",{\"children\":\"豆格工坊|图片转拼豆图纸\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"在浏览器中把图片转换为拼豆像素图纸,按色号、名称或像素格筛选颜色。\"}]]}],[\"$\",\"$1\",\"viewport\",{\"children\":[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]}],[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]]]}]}]]}\n")</script><script>((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}).done=true</script></body></html>
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{r as e}from"./rolldown-runtime-C60lm6uB.js";import{r as t}from"./framework-BgSIrAUN.js";import{t as n}from"./index-CYDm4YY8.js";var r=e(t(),1),i=new Map;function a(e,t){return e?{...e,...t}:t}function o({providerId:e,segmentMap:t,children:o}){let s=(0,r.useRef)(null),c=n(),l=a(s.current??(e?i.get(e)??null:null),t);return(0,r.useEffect)(()=>{s.current=l,e&&i.set(e,l)},[l,e]),c?(0,r.createElement)(c.Provider,{value:l},o):o}export{o as LayoutSegmentProvider,a as mergeLayoutSegmentMap}; import{r as e}from"./rolldown-runtime-C60lm6uB.js";import{r as t}from"./framework-BgSIrAUN.js";import{t as n}from"./index-DJm5cxJ6.js";var r=e(t(),1),i=new Map;function a(e,t){return e?{...e,...t}:t}function o({providerId:e,segmentMap:t,children:o}){let s=(0,r.useRef)(null),c=n(),l=a(s.current??(e?i.get(e)??null:null),t);return(0,r.useEffect)(()=>{s.current=l,e&&i.set(e,l)},[l,e]),c?(0,r.createElement)(c.Provider,{value:l},o):o}export{o as LayoutSegmentProvider,a as mergeLayoutSegmentMap};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -6,8 +6,8 @@
7:I["8c0f216c4604",[],"Slot",1] 7:I["8c0f216c4604",[],"Slot",1]
8:I["9276801271d6",[],"AppRouterScrollTarget",1] 8:I["9276801271d6",[],"AppRouterScrollTarget",1]
9:I["593f344dc510",[],"RedirectBoundary",1] 9:I["593f344dc510",[],"RedirectBoundary",1]
:HL["/_next/static/css/index.JNuJjVb4.css","style" ] :HL["/_next/static/css/index.B96d0yxr.css","style" ]
0:{"__route":"route:/","__interceptionContext":null,"__layoutIds":["layout:/"],"__rootLayout":"/","__sourcePage":"/page","page:/":"$L1","layout:/":[[[["$","link","css:/_next/static/css/index.JNuJjVb4.css",{"rel":"stylesheet","precedence":"vite-rsc/importer-resources","href":"/_next/static/css/index.JNuJjVb4.css","data-rsc-css-href":"/_next/static/css/index.JNuJjVb4.css"}],"$undefined"],["$","html",null,{"lang":"zh-CN","children":["$","body",null,{"children":["$","$L2",null,{}]}]}]],null],"route:/":[[["$","meta",null,{"charSet":"utf-8"}],[["$","title","0",{"children":"豆格工坊|图片转拼豆图纸"}],["$","meta","1",{"name":"description","content":"在浏览器中把图片转换为拼豆像素图纸,按色号、名称或像素格筛选颜色。"}]],[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]],["$","$L3",null,{"fallback":"$4","children":["$","$L5",null,{"fallback":"$4","children":["$","$L6",null,{"providerId":"layout:/","segmentMap":{"children":[]},"children":["$","$L7",null,{"id":"layout:/","parallelSlots":"$undefined","children":["$","$L8",null,{"children":["$","$L9",null,{"children":[["$","$L6",null,{"providerId":"page:/","segmentMap":{"children":["__PAGE__"]},"children":["$","$L7",null,{"id":"page:/"}]}],null]}]}]}]}]}]}],null,null],"__layoutFlags":{"layout:/":"s"},"__artifactCompatibility":{"schemaVersion":1,"graphVersion":"app-route-graph:1177fd80f83fa7b0","deploymentVersion":"c795b2fa-f236-43d0-8c60-3bd049510d5e","appElementsSchemaVersion":1,"rscPayloadSchemaVersion":1,"rootBoundaryId":"/","renderEpoch":null},"__renderObservation":{"schemaVersion":1,"output":{"kind":"app-rsc","mountedSlotsFingerprint":null,"renderEpoch":null,"rootBoundaryId":"/","routeId":"route:/"},"completeness":"partial","boundaryOutcome":{"kind":"unknown"},"requestApis":[{"kind":"connection","status":"unknown"},{"kind":"cookies","status":"unknown"},{"kind":"draftMode","status":"unknown"},{"kind":"headers","status":"unknown"},{"kind":"params","status":"unknown"},{"kind":"searchParams","status":"unknown"}],"dynamicFetches":[],"cacheTags":["/","_N_T_/","_N_T_/index","_N_T_/layout","_N_T_/page"],"pathTags":["/"],"cacheability":"unknown","downgrade":{"target":"freshRender","reasons":[{"code":"CP_DOWNGRADE_CACHEABILITY_UNKNOWN","target":"freshRender"},{"code":"CP_DOWNGRADE_INCOMPLETE_OBSERVATION","completeness":"partial","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"connection","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"cookies","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"draftMode","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"headers","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"params","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"searchParams","target":"freshRender"}],"fallback":{"kind":"breakerFallback","code":"CP_PRIVATE_DYNAMIC_DOWNGRADE","mode":"renderFresh","scope":"affectedOutput","fields":{"reasonCodes":["CP_DOWNGRADE_CACHEABILITY_UNKNOWN","CP_DOWNGRADE_INCOMPLETE_OBSERVATION","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API"],"target":"freshRender"}},"isPublicCacheCandidate":false}}} 0:{"__route":"route:/","__interceptionContext":null,"__layoutIds":["layout:/"],"__rootLayout":"/","__sourcePage":"/page","page:/":"$L1","layout:/":[[[["$","link","css:/_next/static/css/index.B96d0yxr.css",{"rel":"stylesheet","precedence":"vite-rsc/importer-resources","href":"/_next/static/css/index.B96d0yxr.css","data-rsc-css-href":"/_next/static/css/index.B96d0yxr.css"}],"$undefined"],["$","html",null,{"lang":"zh-CN","children":["$","body",null,{"children":["$","$L2",null,{}]}]}]],null],"route:/":[[["$","meta",null,{"charSet":"utf-8"}],[["$","title","0",{"children":"豆格工坊|图片转拼豆图纸"}],["$","meta","1",{"name":"description","content":"在浏览器中把图片转换为拼豆像素图纸,按色号、名称或像素格筛选颜色。"}]],[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]],["$","$L3",null,{"fallback":"$4","children":["$","$L5",null,{"fallback":"$4","children":["$","$L6",null,{"providerId":"layout:/","segmentMap":{"children":[]},"children":["$","$L7",null,{"id":"layout:/","parallelSlots":"$undefined","children":["$","$L8",null,{"children":["$","$L9",null,{"children":[["$","$L6",null,{"providerId":"page:/","segmentMap":{"children":["__PAGE__"]},"children":["$","$L7",null,{"id":"page:/"}]}],null]}]}]}]}]}]}],null,null],"__layoutFlags":{"layout:/":"s"},"__artifactCompatibility":{"schemaVersion":1,"graphVersion":"app-route-graph:1177fd80f83fa7b0","deploymentVersion":"b9f856ce-42d7-4216-b786-6bd488655129","appElementsSchemaVersion":1,"rscPayloadSchemaVersion":1,"rootBoundaryId":"/","renderEpoch":null},"__renderObservation":{"schemaVersion":1,"output":{"kind":"app-rsc","mountedSlotsFingerprint":null,"renderEpoch":null,"rootBoundaryId":"/","routeId":"route:/"},"completeness":"partial","boundaryOutcome":{"kind":"unknown"},"requestApis":[{"kind":"connection","status":"unknown"},{"kind":"cookies","status":"unknown"},{"kind":"draftMode","status":"unknown"},{"kind":"headers","status":"unknown"},{"kind":"params","status":"unknown"},{"kind":"searchParams","status":"unknown"}],"dynamicFetches":[],"cacheTags":["/","_N_T_/","_N_T_/index","_N_T_/layout","_N_T_/page"],"pathTags":["/"],"cacheability":"unknown","downgrade":{"target":"freshRender","reasons":[{"code":"CP_DOWNGRADE_CACHEABILITY_UNKNOWN","target":"freshRender"},{"code":"CP_DOWNGRADE_INCOMPLETE_OBSERVATION","completeness":"partial","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"connection","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"cookies","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"draftMode","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"headers","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"params","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"searchParams","target":"freshRender"}],"fallback":{"kind":"breakerFallback","code":"CP_PRIVATE_DYNAMIC_DOWNGRADE","mode":"renderFresh","scope":"affectedOutput","fields":{"reasonCodes":["CP_DOWNGRADE_CACHEABILITY_UNKNOWN","CP_DOWNGRADE_INCOMPLETE_OBSERVATION","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API"],"target":"freshRender"}},"isPublicCacheCandidate":false}}}
a:I["6efdf509a785",[],"default",1] a:I["6efdf509a785",[],"default",1]
1:["$","$La",null,{"params":"$@b","searchParams":"$@c"}] 1:["$","$La",null,{"params":"$@b","searchParams":"$@c"}]
b:{} b:{}
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"appBrowserEntry": "_next/static/chunks/index-CYDm4YY8.js" "appBrowserEntry": "_next/static/chunks/index-DJm5cxJ6.js"
} }