mirror of
https://github.com/woodchen-ink/Edgeone_CleanCache.git
synced 2025-07-18 05:51:57 +08:00
Compare commits
9 Commits
80b181e684
...
41a1168545
Author | SHA1 | Date | |
---|---|---|---|
|
41a1168545 | ||
|
fe0432dfca | ||
f2309e973c | |||
33f1abd6d8 | |||
65864d3506 | |||
a7b1a7b04c | |||
7ae6a8d466 | |||
983ccbdfcf | |||
eaeef66c19 |
503
.edgeone/functions/index.js
Normal file
503
.edgeone/functions/index.js
Normal file
@ -0,0 +1,503 @@
|
||||
|
||||
let global = globalThis;
|
||||
|
||||
class MessageChannel {
|
||||
constructor() {
|
||||
this.port1 = new MessagePort();
|
||||
this.port2 = new MessagePort();
|
||||
}
|
||||
}
|
||||
class MessagePort {
|
||||
constructor() {
|
||||
this.onmessage = null;
|
||||
}
|
||||
postMessage(data) {
|
||||
if (this.onmessage) {
|
||||
setTimeout(() => this.onmessage({ data }), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
global.MessageChannel = MessageChannel;
|
||||
|
||||
async function handleRequest(context){
|
||||
let routeParams = {};
|
||||
let pagesFunctionResponse = null;
|
||||
const request = context.request;
|
||||
const urlInfo = new URL(request.url);
|
||||
|
||||
if (urlInfo.pathname !== '/' && urlInfo.pathname.endsWith('/')) {
|
||||
urlInfo.pathname = urlInfo.pathname.slice(0, -1);
|
||||
}
|
||||
|
||||
let matchedFunc = false;
|
||||
|
||||
if('/api/clean-eo-cache' === urlInfo.pathname) {
|
||||
matchedFunc = true;
|
||||
(() => {
|
||||
// functions/api/clean-eo-cache.js
|
||||
async function qcloudV3Post(secretId, secretKey, service, bodyArray, headersArray) {
|
||||
const HTTPRequestMethod = "POST";
|
||||
const CanonicalURI = "/";
|
||||
const CanonicalQueryString = "";
|
||||
const sortHeadersArray = Object.keys(headersArray).sort().reduce((obj, key) => {
|
||||
obj[key] = headersArray[key];
|
||||
return obj;
|
||||
}, {});
|
||||
let SignedHeaders = "";
|
||||
let CanonicalHeaders = "";
|
||||
for (const key in sortHeadersArray) {
|
||||
SignedHeaders += key.toLowerCase() + ";";
|
||||
}
|
||||
SignedHeaders = SignedHeaders.slice(0, -1);
|
||||
for (const key in sortHeadersArray) {
|
||||
CanonicalHeaders += `${key.toLowerCase()}:${sortHeadersArray[key].toLowerCase()}
|
||||
`;
|
||||
}
|
||||
const HashedRequestPayload = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(JSON.stringify(bodyArray))
|
||||
).then((hash) => Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join(""));
|
||||
const CanonicalRequest = `${HTTPRequestMethod}
|
||||
${CanonicalURI}
|
||||
${CanonicalQueryString}
|
||||
${CanonicalHeaders}
|
||||
${SignedHeaders}
|
||||
${HashedRequestPayload}`;
|
||||
const RequestTimestamp = Math.floor(Date.now() / 1e3);
|
||||
const formattedDate = new Date(RequestTimestamp * 1e3).toISOString().split("T")[0];
|
||||
const Algorithm = "TC3-HMAC-SHA256";
|
||||
const CredentialScope = `${formattedDate}/${service}/tc3_request`;
|
||||
const HashedCanonicalRequest = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(CanonicalRequest)
|
||||
).then((hash) => Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join(""));
|
||||
const StringToSign = `${Algorithm}
|
||||
${RequestTimestamp}
|
||||
${CredentialScope}
|
||||
${HashedCanonicalRequest}`;
|
||||
async function hmac(key, string) {
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
typeof key === "string" ? new TextEncoder().encode(key) : key,
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
false,
|
||||
["sign"]
|
||||
);
|
||||
const signature = await crypto.subtle.sign(
|
||||
"HMAC",
|
||||
cryptoKey,
|
||||
new TextEncoder().encode(string)
|
||||
);
|
||||
return new Uint8Array(signature);
|
||||
}
|
||||
const SecretDate = await hmac("TC3" + secretKey, formattedDate);
|
||||
const SecretService = await hmac(SecretDate, service);
|
||||
const SecretSigning = await hmac(SecretService, "tc3_request");
|
||||
const Signature = Array.from(
|
||||
new Uint8Array(
|
||||
await crypto.subtle.sign(
|
||||
"HMAC",
|
||||
await crypto.subtle.importKey(
|
||||
"raw",
|
||||
SecretSigning,
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
false,
|
||||
["sign"]
|
||||
),
|
||||
new TextEncoder().encode(StringToSign)
|
||||
)
|
||||
)
|
||||
).map((b) => b.toString(16).padStart(2, "0")).join("");
|
||||
const Authorization = `${Algorithm} Credential=${secretId}/${CredentialScope}, SignedHeaders=${SignedHeaders}, Signature=${Signature}`;
|
||||
headersArray["X-TC-Timestamp"] = RequestTimestamp.toString();
|
||||
headersArray["Authorization"] = Authorization;
|
||||
return headersArray;
|
||||
}
|
||||
function onRequestOptions(context) {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type"
|
||||
}
|
||||
});
|
||||
}
|
||||
async function onRequestPost(context) {
|
||||
try {
|
||||
const data = await context.request.json();
|
||||
const { secretId, secretKey, zoneId, type, targets } = data;
|
||||
const service = "teo";
|
||||
const host = "teo.tencentcloudapi.com";
|
||||
const payload = {
|
||||
ZoneId: zoneId,
|
||||
Type: type,
|
||||
Targets: targets
|
||||
};
|
||||
const headersPending = {
|
||||
"Host": host,
|
||||
"Content-Type": "application/json",
|
||||
"X-TC-Action": "CreatePurgeTask",
|
||||
"X-TC-Version": "2022-09-01",
|
||||
"X-TC-Region": "ap-guangzhou"
|
||||
};
|
||||
const headers = await qcloudV3Post(secretId, secretKey, service, payload, headersPending);
|
||||
const response = await fetch(`https://${host}`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const result = await response.json();
|
||||
return new Response(JSON.stringify(result), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*"
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
status: 500,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
function onRequest(context) {
|
||||
return new Response(JSON.stringify({ error: "Only POST method is allowed" }), {
|
||||
status: 405,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pagesFunctionResponse = onRequest;
|
||||
})();
|
||||
}
|
||||
|
||||
if('/api/clean-eo-cache' === urlInfo.pathname && request.method === 'POST') {
|
||||
matchedFunc = true;
|
||||
(() => {
|
||||
// functions/api/clean-eo-cache.js
|
||||
async function qcloudV3Post(secretId, secretKey, service, bodyArray, headersArray) {
|
||||
const HTTPRequestMethod = "POST";
|
||||
const CanonicalURI = "/";
|
||||
const CanonicalQueryString = "";
|
||||
const sortHeadersArray = Object.keys(headersArray).sort().reduce((obj, key) => {
|
||||
obj[key] = headersArray[key];
|
||||
return obj;
|
||||
}, {});
|
||||
let SignedHeaders = "";
|
||||
let CanonicalHeaders = "";
|
||||
for (const key in sortHeadersArray) {
|
||||
SignedHeaders += key.toLowerCase() + ";";
|
||||
}
|
||||
SignedHeaders = SignedHeaders.slice(0, -1);
|
||||
for (const key in sortHeadersArray) {
|
||||
CanonicalHeaders += `${key.toLowerCase()}:${sortHeadersArray[key].toLowerCase()}
|
||||
`;
|
||||
}
|
||||
const HashedRequestPayload = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(JSON.stringify(bodyArray))
|
||||
).then((hash) => Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join(""));
|
||||
const CanonicalRequest = `${HTTPRequestMethod}
|
||||
${CanonicalURI}
|
||||
${CanonicalQueryString}
|
||||
${CanonicalHeaders}
|
||||
${SignedHeaders}
|
||||
${HashedRequestPayload}`;
|
||||
const RequestTimestamp = Math.floor(Date.now() / 1e3);
|
||||
const formattedDate = new Date(RequestTimestamp * 1e3).toISOString().split("T")[0];
|
||||
const Algorithm = "TC3-HMAC-SHA256";
|
||||
const CredentialScope = `${formattedDate}/${service}/tc3_request`;
|
||||
const HashedCanonicalRequest = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(CanonicalRequest)
|
||||
).then((hash) => Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join(""));
|
||||
const StringToSign = `${Algorithm}
|
||||
${RequestTimestamp}
|
||||
${CredentialScope}
|
||||
${HashedCanonicalRequest}`;
|
||||
async function hmac(key, string) {
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
typeof key === "string" ? new TextEncoder().encode(key) : key,
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
false,
|
||||
["sign"]
|
||||
);
|
||||
const signature = await crypto.subtle.sign(
|
||||
"HMAC",
|
||||
cryptoKey,
|
||||
new TextEncoder().encode(string)
|
||||
);
|
||||
return new Uint8Array(signature);
|
||||
}
|
||||
const SecretDate = await hmac("TC3" + secretKey, formattedDate);
|
||||
const SecretService = await hmac(SecretDate, service);
|
||||
const SecretSigning = await hmac(SecretService, "tc3_request");
|
||||
const Signature = Array.from(
|
||||
new Uint8Array(
|
||||
await crypto.subtle.sign(
|
||||
"HMAC",
|
||||
await crypto.subtle.importKey(
|
||||
"raw",
|
||||
SecretSigning,
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
false,
|
||||
["sign"]
|
||||
),
|
||||
new TextEncoder().encode(StringToSign)
|
||||
)
|
||||
)
|
||||
).map((b) => b.toString(16).padStart(2, "0")).join("");
|
||||
const Authorization = `${Algorithm} Credential=${secretId}/${CredentialScope}, SignedHeaders=${SignedHeaders}, Signature=${Signature}`;
|
||||
headersArray["X-TC-Timestamp"] = RequestTimestamp.toString();
|
||||
headersArray["Authorization"] = Authorization;
|
||||
return headersArray;
|
||||
}
|
||||
function onRequestOptions(context) {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type"
|
||||
}
|
||||
});
|
||||
}
|
||||
async function onRequestPost(context) {
|
||||
try {
|
||||
const data = await context.request.json();
|
||||
const { secretId, secretKey, zoneId, type, targets } = data;
|
||||
const service = "teo";
|
||||
const host = "teo.tencentcloudapi.com";
|
||||
const payload = {
|
||||
ZoneId: zoneId,
|
||||
Type: type,
|
||||
Targets: targets
|
||||
};
|
||||
const headersPending = {
|
||||
"Host": host,
|
||||
"Content-Type": "application/json",
|
||||
"X-TC-Action": "CreatePurgeTask",
|
||||
"X-TC-Version": "2022-09-01",
|
||||
"X-TC-Region": "ap-guangzhou"
|
||||
};
|
||||
const headers = await qcloudV3Post(secretId, secretKey, service, payload, headersPending);
|
||||
const response = await fetch(`https://${host}`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const result = await response.json();
|
||||
return new Response(JSON.stringify(result), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*"
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
status: 500,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
function onRequest(context) {
|
||||
return new Response(JSON.stringify({ error: "Only POST method is allowed" }), {
|
||||
status: 405,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pagesFunctionResponse = onRequestPost;
|
||||
})();
|
||||
}
|
||||
|
||||
if('/api/clean-eo-cache' === urlInfo.pathname && request.method === 'OPTIONS') {
|
||||
matchedFunc = true;
|
||||
(() => {
|
||||
// functions/api/clean-eo-cache.js
|
||||
async function qcloudV3Post(secretId, secretKey, service, bodyArray, headersArray) {
|
||||
const HTTPRequestMethod = "POST";
|
||||
const CanonicalURI = "/";
|
||||
const CanonicalQueryString = "";
|
||||
const sortHeadersArray = Object.keys(headersArray).sort().reduce((obj, key) => {
|
||||
obj[key] = headersArray[key];
|
||||
return obj;
|
||||
}, {});
|
||||
let SignedHeaders = "";
|
||||
let CanonicalHeaders = "";
|
||||
for (const key in sortHeadersArray) {
|
||||
SignedHeaders += key.toLowerCase() + ";";
|
||||
}
|
||||
SignedHeaders = SignedHeaders.slice(0, -1);
|
||||
for (const key in sortHeadersArray) {
|
||||
CanonicalHeaders += `${key.toLowerCase()}:${sortHeadersArray[key].toLowerCase()}
|
||||
`;
|
||||
}
|
||||
const HashedRequestPayload = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(JSON.stringify(bodyArray))
|
||||
).then((hash) => Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join(""));
|
||||
const CanonicalRequest = `${HTTPRequestMethod}
|
||||
${CanonicalURI}
|
||||
${CanonicalQueryString}
|
||||
${CanonicalHeaders}
|
||||
${SignedHeaders}
|
||||
${HashedRequestPayload}`;
|
||||
const RequestTimestamp = Math.floor(Date.now() / 1e3);
|
||||
const formattedDate = new Date(RequestTimestamp * 1e3).toISOString().split("T")[0];
|
||||
const Algorithm = "TC3-HMAC-SHA256";
|
||||
const CredentialScope = `${formattedDate}/${service}/tc3_request`;
|
||||
const HashedCanonicalRequest = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(CanonicalRequest)
|
||||
).then((hash) => Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join(""));
|
||||
const StringToSign = `${Algorithm}
|
||||
${RequestTimestamp}
|
||||
${CredentialScope}
|
||||
${HashedCanonicalRequest}`;
|
||||
async function hmac(key, string) {
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
typeof key === "string" ? new TextEncoder().encode(key) : key,
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
false,
|
||||
["sign"]
|
||||
);
|
||||
const signature = await crypto.subtle.sign(
|
||||
"HMAC",
|
||||
cryptoKey,
|
||||
new TextEncoder().encode(string)
|
||||
);
|
||||
return new Uint8Array(signature);
|
||||
}
|
||||
const SecretDate = await hmac("TC3" + secretKey, formattedDate);
|
||||
const SecretService = await hmac(SecretDate, service);
|
||||
const SecretSigning = await hmac(SecretService, "tc3_request");
|
||||
const Signature = Array.from(
|
||||
new Uint8Array(
|
||||
await crypto.subtle.sign(
|
||||
"HMAC",
|
||||
await crypto.subtle.importKey(
|
||||
"raw",
|
||||
SecretSigning,
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
false,
|
||||
["sign"]
|
||||
),
|
||||
new TextEncoder().encode(StringToSign)
|
||||
)
|
||||
)
|
||||
).map((b) => b.toString(16).padStart(2, "0")).join("");
|
||||
const Authorization = `${Algorithm} Credential=${secretId}/${CredentialScope}, SignedHeaders=${SignedHeaders}, Signature=${Signature}`;
|
||||
headersArray["X-TC-Timestamp"] = RequestTimestamp.toString();
|
||||
headersArray["Authorization"] = Authorization;
|
||||
return headersArray;
|
||||
}
|
||||
function onRequestOptions(context) {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type"
|
||||
}
|
||||
});
|
||||
}
|
||||
async function onRequestPost(context) {
|
||||
try {
|
||||
const data = await context.request.json();
|
||||
const { secretId, secretKey, zoneId, type, targets } = data;
|
||||
const service = "teo";
|
||||
const host = "teo.tencentcloudapi.com";
|
||||
const payload = {
|
||||
ZoneId: zoneId,
|
||||
Type: type,
|
||||
Targets: targets
|
||||
};
|
||||
const headersPending = {
|
||||
"Host": host,
|
||||
"Content-Type": "application/json",
|
||||
"X-TC-Action": "CreatePurgeTask",
|
||||
"X-TC-Version": "2022-09-01",
|
||||
"X-TC-Region": "ap-guangzhou"
|
||||
};
|
||||
const headers = await qcloudV3Post(secretId, secretKey, service, payload, headersPending);
|
||||
const response = await fetch(`https://${host}`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const result = await response.json();
|
||||
return new Response(JSON.stringify(result), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*"
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
status: 500,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
function onRequest(context) {
|
||||
return new Response(JSON.stringify({ error: "Only POST method is allowed" }), {
|
||||
status: 405,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pagesFunctionResponse = onRequestOptions;
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
const params = {};
|
||||
if (routeParams.id) {
|
||||
if (routeParams.mode === 1) {
|
||||
const value = urlInfo.pathname.match(routeParams.left);
|
||||
for (let i = 1; i < value.length; i++) {
|
||||
params[routeParams.id[i - 1]] = value[i];
|
||||
}
|
||||
} else {
|
||||
const value = urlInfo.pathname.replace(routeParams.left, '');
|
||||
const splitedValue = value.split('/');
|
||||
if (splitedValue.length === 1) {
|
||||
params[routeParams.id] = splitedValue[0];
|
||||
} else {
|
||||
params[routeParams.id] = splitedValue;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if(!matchedFunc){
|
||||
pagesFunctionResponse = function() {
|
||||
return new Response(null, {
|
||||
status: 404,
|
||||
headers: {
|
||||
"content-type": "text/html; charset=UTF-8",
|
||||
"x-edgefunctions-test": "Welcome to use Pages Functions.",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return pagesFunctionResponse({request, params, env: {} });
|
||||
}addEventListener('fetch',event=>{return event.respondWith(handleRequest({request:event.request,params: {}, env: {} }))});
|
29
.edgeone/meta.json
Normal file
29
.edgeone/meta.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"conf": {},
|
||||
"routes": [
|
||||
{
|
||||
"routePath": "/api/clean-eo-cache",
|
||||
"mountPath": "/api",
|
||||
"method": "",
|
||||
"module": [
|
||||
"api/clean-eo-cache.js:onRequest"
|
||||
]
|
||||
},
|
||||
{
|
||||
"routePath": "/api/clean-eo-cache",
|
||||
"mountPath": "/api",
|
||||
"method": "POST",
|
||||
"module": [
|
||||
"api/clean-eo-cache.js:onRequestPost"
|
||||
]
|
||||
},
|
||||
{
|
||||
"routePath": "/api/clean-eo-cache",
|
||||
"mountPath": "/api",
|
||||
"method": "OPTIONS",
|
||||
"module": [
|
||||
"api/clean-eo-cache.js:onRequestOptions"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
4
.edgeone/project.json
Normal file
4
.edgeone/project.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"Name": "eoccc",
|
||||
"ProjectId": "pages-zzbzb54ax2ni"
|
||||
}
|
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
BIN
app/favicon.ico
Normal file
BIN
app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
684
app/form.tsx
Normal file
684
app/form.tsx
Normal file
@ -0,0 +1,684 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Save, Trash, CheckCircle2, XCircle, Pencil } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
interface SavedConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
zoneId: string;
|
||||
}
|
||||
|
||||
interface EOApiResponse {
|
||||
Response: {
|
||||
RequestId: string;
|
||||
JobId: string;
|
||||
FailedList: string[];
|
||||
Error?: {
|
||||
Message: string;
|
||||
Code: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function CleanCacheForm() {
|
||||
const [secretId, setSecretId] = useState("");
|
||||
const [secretKey, setSecretKey] = useState("");
|
||||
const [zoneId, setZoneId] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [result, setResult] = useState<EOApiResponse | null>(null);
|
||||
const [savedConfigs, setSavedConfigs] = useState<SavedConfig[]>([]);
|
||||
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
|
||||
const [configName, setConfigName] = useState("");
|
||||
const [editingConfigId, setEditingConfigId] = useState<string | null>(null);
|
||||
const [urls, setUrls] = useState("");
|
||||
const [prefixes, setPrefixes] = useState("");
|
||||
const [hosts, setHosts] = useState("");
|
||||
const [tags, setTags] = useState("");
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
// 加载保存的配置列表
|
||||
const configs = localStorage.getItem("savedConfigs");
|
||||
if (configs) {
|
||||
setSavedConfigs(JSON.parse(configs));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const saveConfig = () => {
|
||||
if (!configName.trim()) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "错误",
|
||||
description: "请输入配置名称",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!secretId || !secretKey || !zoneId) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "错误",
|
||||
description: "请填写完整的配置信息!",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let updatedConfigs: SavedConfig[];
|
||||
|
||||
if (editingConfigId) {
|
||||
// 编辑现有配置
|
||||
updatedConfigs = savedConfigs.map(config => {
|
||||
if (config.id === editingConfigId) {
|
||||
return {
|
||||
...config,
|
||||
name: configName,
|
||||
secretId,
|
||||
secretKey,
|
||||
zoneId
|
||||
};
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
toast({
|
||||
title: "成功",
|
||||
description: "配置已更新",
|
||||
});
|
||||
} else {
|
||||
// 添加新配置
|
||||
const newConfig: SavedConfig = {
|
||||
id: Date.now().toString(),
|
||||
name: configName,
|
||||
secretId,
|
||||
secretKey,
|
||||
zoneId,
|
||||
};
|
||||
|
||||
updatedConfigs = [...savedConfigs, newConfig];
|
||||
|
||||
toast({
|
||||
title: "成功",
|
||||
description: "配置已保存",
|
||||
});
|
||||
}
|
||||
|
||||
setSavedConfigs(updatedConfigs);
|
||||
localStorage.setItem("savedConfigs", JSON.stringify(updatedConfigs));
|
||||
setSaveDialogOpen(false);
|
||||
setConfigName("");
|
||||
setEditingConfigId(null);
|
||||
};
|
||||
|
||||
const editConfig = (config: SavedConfig) => {
|
||||
setConfigName(config.name);
|
||||
setSecretId(config.secretId);
|
||||
setSecretKey(config.secretKey);
|
||||
setZoneId(config.zoneId);
|
||||
setEditingConfigId(config.id);
|
||||
setSaveDialogOpen(true);
|
||||
};
|
||||
|
||||
const loadConfig = (config: SavedConfig) => {
|
||||
setSecretId(config.secretId);
|
||||
setSecretKey(config.secretKey);
|
||||
setZoneId(config.zoneId);
|
||||
|
||||
toast({
|
||||
title: "成功",
|
||||
description: "配置已加载",
|
||||
});
|
||||
};
|
||||
|
||||
const deleteConfig = (id: string) => {
|
||||
const updatedConfigs = savedConfigs.filter(config => config.id !== id);
|
||||
setSavedConfigs(updatedConfigs);
|
||||
localStorage.setItem("savedConfigs", JSON.stringify(updatedConfigs));
|
||||
|
||||
toast({
|
||||
title: "成功",
|
||||
description: "配置已删除",
|
||||
});
|
||||
};
|
||||
|
||||
const purgeUrls = async () => {
|
||||
if (!urls.trim()) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "错误",
|
||||
description: "请输入需要刷新的URL",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await callApi("purge_url", urls.split("\n").map(t => t.trim()).filter(t => t));
|
||||
};
|
||||
|
||||
const purgePrefixes = async () => {
|
||||
if (!prefixes.trim()) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "错误",
|
||||
description: "请输入需要刷新的目录",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await callApi("purge_prefix", prefixes.split("\n").map(t => t.trim()).filter(t => t));
|
||||
};
|
||||
|
||||
const purgeHosts = async () => {
|
||||
if (!hosts.trim()) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "错误",
|
||||
description: "请输入需要刷新的主机",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await callApi("purge_host", hosts.split("\n").map(t => t.trim()).filter(t => t));
|
||||
};
|
||||
|
||||
const purgeTags = async () => {
|
||||
if (!tags.trim()) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "错误",
|
||||
description: "请输入需要刷新的缓存标签",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await callApi("purge_cache_tag", tags.split("\n").map(t => t.trim()).filter(t => t));
|
||||
};
|
||||
|
||||
const purgeAll = async () => {
|
||||
await callApi("purge_all", []);
|
||||
};
|
||||
|
||||
const callApi = async (type: string, targets: string[], method = "invalidate") => {
|
||||
if (!secretId || !secretKey || !zoneId) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "错误",
|
||||
description: "请填写完整的配置信息!",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch("/api/eo-cleancache", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
secretId,
|
||||
secretKey,
|
||||
zoneId,
|
||||
type,
|
||||
targets,
|
||||
method,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
setResult(result);
|
||||
setDialogOpen(true);
|
||||
|
||||
if (result.Response && !result.Response.Error) {
|
||||
toast({
|
||||
title: "成功",
|
||||
description: "操作成功!",
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "失败",
|
||||
description: `操作失败:${
|
||||
result.Response?.Error?.Message || "未知错误"
|
||||
}`,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "错误",
|
||||
description: `请求失败:${(error as Error).message}`,
|
||||
});
|
||||
setResult({
|
||||
Response: {
|
||||
RequestId: "",
|
||||
JobId: "",
|
||||
FailedList: [],
|
||||
Error: {
|
||||
Message: (error as Error).message,
|
||||
Code: "RequestError"
|
||||
}
|
||||
}
|
||||
});
|
||||
setDialogOpen(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-white/80 backdrop-blur-sm w-full max-w-4xl mx-auto">
|
||||
<CardHeader>
|
||||
<CardTitle>腾讯云EdgeOne缓存刷新工具</CardTitle>
|
||||
<CardDescription>
|
||||
数据保存在浏览器本地,不会上传到任何服务器。通用国内站和国际站。
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||
<div>
|
||||
<CardTitle className="text-base">已保存的配置</CardTitle>
|
||||
<CardDescription>点击配置名称可快速加载</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setConfigName("");
|
||||
setEditingConfigId(null);
|
||||
setSaveDialogOpen(true);
|
||||
}}
|
||||
className="h-8 w-full sm:w-auto"
|
||||
>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
保存当前配置
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{savedConfigs.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">暂无保存的配置</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
{savedConfigs.map((config) => (
|
||||
<div
|
||||
key={config.id}
|
||||
className="flex items-center justify-between p-2 rounded-lg hover:bg-accent group"
|
||||
>
|
||||
<button
|
||||
onClick={() => loadConfig(config)}
|
||||
className="flex-1 text-left text-sm font-medium truncate"
|
||||
>
|
||||
{config.name}
|
||||
</button>
|
||||
<div className="flex opacity-0 group-hover:opacity-100 ml-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
editConfig(config);
|
||||
}}
|
||||
className="h-6 w-6"
|
||||
>
|
||||
<Pencil className="w-4 h-4 text-muted-foreground" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteConfig(config.id);
|
||||
}}
|
||||
className="h-6 w-6"
|
||||
>
|
||||
<Trash className="w-4 h-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>SecretId</Label>
|
||||
<Input
|
||||
value={secretId}
|
||||
onChange={(e) => setSecretId(e.target.value)}
|
||||
placeholder="请输入 SecretId"
|
||||
/>
|
||||
<div className="text-sm text-muted-foreground space-y-1">
|
||||
<Link
|
||||
href="https://console.cloud.tencent.com/cam/capi"
|
||||
target="_blank"
|
||||
className="underline block"
|
||||
>
|
||||
国内站: https://console.cloud.tencent.com/cam/capi
|
||||
</Link>
|
||||
<Link
|
||||
href="https://console.intl.cloud.tencent.com/cam/capi"
|
||||
target="_blank"
|
||||
className="underline block"
|
||||
>
|
||||
国际站: https://console.intl.cloud.tencent.com/cam/capi
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>SecretKey</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={secretKey}
|
||||
onChange={(e) => setSecretKey(e.target.value)}
|
||||
placeholder="请输入 SecretKey"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>ZoneId</Label>
|
||||
<Input
|
||||
value={zoneId}
|
||||
onChange={(e) => setZoneId(e.target.value)}
|
||||
placeholder="请输入 ZoneId"
|
||||
/>
|
||||
<div className="text-sm text-muted-foreground space-y-1">
|
||||
<Link
|
||||
href="https://console.cloud.tencent.com/edgeone/zones"
|
||||
target="_blank"
|
||||
className="underline block"
|
||||
>
|
||||
国内站: https://console.cloud.tencent.com/edgeone/zones
|
||||
</Link>
|
||||
<Link
|
||||
href="https://console.intl.cloud.tencent.com/edgeone/zones"
|
||||
target="_blank"
|
||||
className="underline block"
|
||||
>
|
||||
国际站: https://console.intl.cloud.tencent.com/edgeone/zones
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="all" className="w-full">
|
||||
<div className="overflow-x-auto">
|
||||
<TabsList className="inline-flex w-max min-w-full sm:grid sm:grid-cols-5 h-auto p-1 gap-1">
|
||||
<TabsTrigger
|
||||
value="all"
|
||||
className="flex-shrink-0 text-xs sm:text-sm px-3 py-2 whitespace-nowrap data-[state=active]:bg-background data-[state=active]:text-foreground"
|
||||
>
|
||||
刷新全部
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="url"
|
||||
className="flex-shrink-0 text-xs sm:text-sm px-3 py-2 whitespace-nowrap data-[state=active]:bg-background data-[state=active]:text-foreground"
|
||||
>
|
||||
URL刷新
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="prefix"
|
||||
className="flex-shrink-0 text-xs sm:text-sm px-3 py-2 whitespace-nowrap data-[state=active]:bg-background data-[state=active]:text-foreground"
|
||||
>
|
||||
目录刷新
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="host"
|
||||
className="flex-shrink-0 text-xs sm:text-sm px-3 py-2 whitespace-nowrap data-[state=active]:bg-background data-[state=active]:text-foreground"
|
||||
>
|
||||
Host刷新
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="tag"
|
||||
className="flex-shrink-0 text-xs sm:text-sm px-3 py-2 whitespace-nowrap data-[state=active]:bg-background data-[state=active]:text-foreground"
|
||||
>
|
||||
Cache Tag
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="all">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>刷新全部缓存</CardTitle>
|
||||
<CardDescription>
|
||||
将清理该域名下的所有缓存文件
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button
|
||||
className="w-full"
|
||||
disabled={loading}
|
||||
onClick={purgeAll}
|
||||
>
|
||||
刷新所有缓存
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="url">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>按URL刷新</CardTitle>
|
||||
<CardDescription>
|
||||
输入需要刷新的URL地址,每行一个
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Textarea
|
||||
value={urls}
|
||||
onChange={(e) => setUrls(e.target.value)}
|
||||
placeholder="http://example.com/file1.jpg"
|
||||
className="min-h-[120px] sm:min-h-[150px]"
|
||||
/>
|
||||
<Button
|
||||
className="w-full"
|
||||
disabled={loading}
|
||||
onClick={purgeUrls}
|
||||
>
|
||||
刷新指定URL
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="prefix">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>按目录刷新</CardTitle>
|
||||
<CardDescription>
|
||||
输入需要刷新的目录,每行一个
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Textarea
|
||||
value={prefixes}
|
||||
onChange={(e) => setPrefixes(e.target.value)}
|
||||
placeholder="http://example.com/images/"
|
||||
className="min-h-[120px] sm:min-h-[150px]"
|
||||
/>
|
||||
<Button
|
||||
className="w-full"
|
||||
disabled={loading}
|
||||
onClick={purgePrefixes}
|
||||
>
|
||||
刷新指定目录
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="host">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>按主机刷新</CardTitle>
|
||||
<CardDescription>
|
||||
输入需要刷新的主机名,每行一个
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Textarea
|
||||
value={hosts}
|
||||
onChange={(e) => setHosts(e.target.value)}
|
||||
placeholder="www.example.com"
|
||||
className="min-h-[120px] sm:min-h-[150px]"
|
||||
/>
|
||||
<Button
|
||||
className="w-full"
|
||||
disabled={loading}
|
||||
onClick={purgeHosts}
|
||||
>
|
||||
刷新指定主机
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="tag">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>按Cache Tag刷新 (仅企业版)</CardTitle>
|
||||
<CardDescription>
|
||||
输入需要刷新的缓存标签,每行一个
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Textarea
|
||||
value={tags}
|
||||
onChange={(e) => setTags(e.target.value)}
|
||||
placeholder="tag1 tag2 tag3"
|
||||
className="min-h-[120px] sm:min-h-[150px]"
|
||||
/>
|
||||
<Button
|
||||
className="w-full"
|
||||
disabled={loading}
|
||||
onClick={purgeTags}
|
||||
>
|
||||
刷新指定标签
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent className="max-w-[90vw] sm:max-w-lg max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
{result?.Response && !result.Response.Error ? (
|
||||
<>
|
||||
<CheckCircle2 className="w-5 h-5 text-green-500" />
|
||||
<span>操作成功</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<XCircle className="w-5 h-5 text-red-500" />
|
||||
<span>操作失败</span>
|
||||
</>
|
||||
)}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
{result?.Response && !result.Response.Error ? (
|
||||
<div className="p-4 bg-green-50 text-green-700 rounded-lg">
|
||||
<p className="font-medium">缓存清理成功!</p>
|
||||
<div className="mt-2 space-y-1 text-sm">
|
||||
<p>任务ID: {result.Response.JobId}</p>
|
||||
<p>请求ID: {result.Response.RequestId}</p>
|
||||
{result.Response.FailedList?.length === 0 && (
|
||||
<p className="text-green-600">所有项目清理成功</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 bg-red-50 text-red-700 rounded-lg">
|
||||
<p className="font-medium">清理失败</p>
|
||||
<p className="text-sm mt-1">
|
||||
{result?.Response?.Error?.Message || "未知错误"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="bg-muted p-4 rounded-lg">
|
||||
<p className="text-sm text-muted-foreground mb-2">详细信息:</p>
|
||||
<pre className="text-xs overflow-x-auto whitespace-pre-wrap">
|
||||
{JSON.stringify(result, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={saveDialogOpen} onOpenChange={setSaveDialogOpen}>
|
||||
<DialogContent className="max-w-[90vw] sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingConfigId ? "编辑配置" : "保存配置"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>配置名称</Label>
|
||||
<Input
|
||||
value={configName}
|
||||
onChange={(e) => setConfigName(e.target.value)}
|
||||
placeholder="请输入配置名称"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>SecretId</Label>
|
||||
<Input
|
||||
value={secretId}
|
||||
onChange={(e) => setSecretId(e.target.value)}
|
||||
placeholder="请输入 SecretId"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>SecretKey</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={secretKey}
|
||||
onChange={(e) => setSecretKey(e.target.value)}
|
||||
placeholder="请输入 SecretKey"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>ZoneId</Label>
|
||||
<Input
|
||||
value={zoneId}
|
||||
onChange={(e) => setZoneId(e.target.value)}
|
||||
placeholder="请输入 ZoneId"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="flex-col sm:flex-row gap-2">
|
||||
<Button variant="outline" onClick={() => {
|
||||
setSaveDialogOpen(false);
|
||||
setEditingConfigId(null);
|
||||
}} className="w-full sm:w-auto">
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={saveConfig} className="w-full sm:w-auto">{editingConfigId ? "更新" : "保存"}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
122
app/globals.css
Normal file
122
app/globals.css
Normal file
@ -0,0 +1,122 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.129 0.042 264.695);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.129 0.042 264.695);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.129 0.042 264.695);
|
||||
--primary: oklch(0.208 0.042 265.755);
|
||||
--primary-foreground: oklch(0.984 0.003 247.858);
|
||||
--secondary: oklch(0.968 0.007 247.896);
|
||||
--secondary-foreground: oklch(0.208 0.042 265.755);
|
||||
--muted: oklch(0.968 0.007 247.896);
|
||||
--muted-foreground: oklch(0.554 0.046 257.417);
|
||||
--accent: oklch(0.968 0.007 247.896);
|
||||
--accent-foreground: oklch(0.208 0.042 265.755);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.929 0.013 255.508);
|
||||
--input: oklch(0.929 0.013 255.508);
|
||||
--ring: oklch(0.704 0.04 256.788);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.984 0.003 247.858);
|
||||
--sidebar-foreground: oklch(0.129 0.042 264.695);
|
||||
--sidebar-primary: oklch(0.208 0.042 265.755);
|
||||
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
|
||||
--sidebar-accent: oklch(0.968 0.007 247.896);
|
||||
--sidebar-accent-foreground: oklch(0.208 0.042 265.755);
|
||||
--sidebar-border: oklch(0.929 0.013 255.508);
|
||||
--sidebar-ring: oklch(0.704 0.04 256.788);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.129 0.042 264.695);
|
||||
--foreground: oklch(0.984 0.003 247.858);
|
||||
--card: oklch(0.208 0.042 265.755);
|
||||
--card-foreground: oklch(0.984 0.003 247.858);
|
||||
--popover: oklch(0.208 0.042 265.755);
|
||||
--popover-foreground: oklch(0.984 0.003 247.858);
|
||||
--primary: oklch(0.929 0.013 255.508);
|
||||
--primary-foreground: oklch(0.208 0.042 265.755);
|
||||
--secondary: oklch(0.279 0.041 260.031);
|
||||
--secondary-foreground: oklch(0.984 0.003 247.858);
|
||||
--muted: oklch(0.279 0.041 260.031);
|
||||
--muted-foreground: oklch(0.704 0.04 256.788);
|
||||
--accent: oklch(0.279 0.041 260.031);
|
||||
--accent-foreground: oklch(0.984 0.003 247.858);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.551 0.027 264.364);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.208 0.042 265.755);
|
||||
--sidebar-foreground: oklch(0.984 0.003 247.858);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
|
||||
--sidebar-accent: oklch(0.279 0.041 260.031);
|
||||
--sidebar-accent-foreground: oklch(0.984 0.003 247.858);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.551 0.027 264.364);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
38
app/layout.tsx
Normal file
38
app/layout.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
import { Footer } from "@/components/ui/footer";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "腾讯云EdgeOne缓存刷新工具",
|
||||
description: "单页面清理腾讯云Edgeone缓存,提供快速便捷的缓存刷新功能,支持URL、目录、Host、全部以及基于缓存标签的刷新操作。数据保存在浏览器本地,不会上传到任何服务器。",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const randomImage = `https://random-api.czl.net/pic/normal`;
|
||||
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body
|
||||
style={{
|
||||
backgroundImage: `url(${randomImage})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundAttachment: "fixed",
|
||||
backdropFilter: "blur(5px)"
|
||||
}}
|
||||
className="overflow-x-hidden"
|
||||
>
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<main className="flex-1 pb-20 px-4 py-8">
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
15
app/page.tsx
Normal file
15
app/page.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { CleanCacheForm } from "./form";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "腾讯云EdgeOne缓存刷新工具",
|
||||
description: "单页面清理腾讯云Edgeone缓存,提供快速便捷的缓存刷新功能,支持URL、目录、Host、全部以及基于缓存标签的刷新操作。数据保存在浏览器本地,不会上传到任何服务器。",
|
||||
};
|
||||
|
||||
export default function CleanCachePage() {
|
||||
return (
|
||||
<div className="w-full max-w-4xl mx-auto flex items-start justify-center">
|
||||
<CleanCacheForm />
|
||||
</div>
|
||||
);
|
||||
}
|
21
components.json
Normal file
21
components.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
59
components/ui/button.tsx
Normal file
59
components/ui/button.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
92
components/ui/card.tsx
Normal file
92
components/ui/card.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
143
components/ui/dialog.tsx
Normal file
143
components/ui/dialog.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
57
components/ui/footer.tsx
Normal file
57
components/ui/footer.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="mt-auto bg-black/20 backdrop-blur-sm border-t border-white/10">
|
||||
<div className="max-w-6xl mx-auto px-4 py-4">
|
||||
<div className="flex flex-wrap items-center justify-center gap-2 sm:gap-4 text-xs sm:text-sm text-white/80">
|
||||
<Link
|
||||
href="https://www.sunai.net"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-white transition-colors duration-200 flex items-center gap-1 whitespace-nowrap"
|
||||
>
|
||||
<span>🌟</span>
|
||||
<span>sunai论坛开发</span>
|
||||
</Link>
|
||||
|
||||
<span className="text-white/40 hidden sm:inline">|</span>
|
||||
|
||||
<Link
|
||||
href="https://github.com/woodchen-ink/Edgeone_CleanCache"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-white transition-colors duration-200 flex items-center gap-1 whitespace-nowrap"
|
||||
>
|
||||
<span>⭐</span>
|
||||
<span>GitHub</span>
|
||||
</Link>
|
||||
|
||||
<span className="text-white/40 hidden sm:inline">|</span>
|
||||
|
||||
<Link
|
||||
href="https://woodchen.ink"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-white transition-colors duration-200 flex items-center gap-1 whitespace-nowrap"
|
||||
>
|
||||
<span>📝</span>
|
||||
<span>个人博客</span>
|
||||
</Link>
|
||||
|
||||
<span className="text-white/40 hidden sm:inline">|</span>
|
||||
|
||||
<Link
|
||||
href="https://onepage.czl.net"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-white transition-colors duration-200 flex items-center gap-1 whitespace-nowrap"
|
||||
>
|
||||
<span>🔧</span>
|
||||
<span>其他工具</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
22
components/ui/input.tsx
Normal file
22
components/ui/input.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-gray-200 dark:border-gray-700 flex h-9 w-full min-w-0 rounded-md border bg-white dark:bg-gray-900 px-3 py-1 text-base shadow-xs transition-[color,box-shadow,border-color] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-blue-400 focus-visible:ring-blue-400/20 focus-visible:ring-[3px]",
|
||||
"hover:border-gray-300 dark:hover:border-gray-600",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
24
components/ui/label.tsx
Normal file
24
components/ui/label.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
25
components/ui/sonner.tsx
Normal file
25
components/ui/sonner.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner, ToasterProps } from "sonner"
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
66
components/ui/tabs.tsx
Normal file
66
components/ui/tabs.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn("flex-1 outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
18
components/ui/textarea.tsx
Normal file
18
components/ui/textarea.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
129
components/ui/toast.tsx
Normal file
129
components/ui/toast.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border bg-background text-foreground",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Toast.displayName = ToastPrimitives.Root.displayName
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||
className
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
))
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
}
|
189
components/ui/use-toast.ts
Normal file
189
components/ui/use-toast.ts
Normal file
@ -0,0 +1,189 @@
|
||||
import * as React from "react"
|
||||
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
|
||||
type ActionType = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
}
|
||||
|
||||
let count = 0
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_VALUE
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
}
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
|
||||
let memoryState: State = { toasts: [] }
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
16
eslint.config.mjs
Normal file
16
eslint.config.mjs
Normal file
@ -0,0 +1,16 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
162
functions/api/eo-cleancache.js
Normal file
162
functions/api/eo-cleancache.js
Normal file
@ -0,0 +1,162 @@
|
||||
// 腾讯云 V3 签名算法
|
||||
async function qcloudV3Post(secretId, secretKey, service, bodyArray, headersArray) {
|
||||
const HTTPRequestMethod = "POST";
|
||||
const CanonicalURI = "/";
|
||||
const CanonicalQueryString = "";
|
||||
|
||||
const sortHeadersArray = Object.keys(headersArray)
|
||||
.sort()
|
||||
.reduce((obj, key) => {
|
||||
obj[key] = headersArray[key];
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
let SignedHeaders = "";
|
||||
let CanonicalHeaders = "";
|
||||
|
||||
for (const key in sortHeadersArray) {
|
||||
SignedHeaders += key.toLowerCase() + ';';
|
||||
}
|
||||
SignedHeaders = SignedHeaders.slice(0, -1);
|
||||
|
||||
for (const key in sortHeadersArray) {
|
||||
CanonicalHeaders += `${key.toLowerCase()}:${sortHeadersArray[key].toLowerCase()}\n`;
|
||||
}
|
||||
|
||||
const HashedRequestPayload = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(JSON.stringify(bodyArray))
|
||||
).then(hash => Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join(''));
|
||||
|
||||
const CanonicalRequest =
|
||||
`${HTTPRequestMethod}\n${CanonicalURI}\n${CanonicalQueryString}\n${CanonicalHeaders}\n${SignedHeaders}\n${HashedRequestPayload}`;
|
||||
|
||||
const RequestTimestamp = Math.floor(Date.now() / 1000);
|
||||
const formattedDate = new Date(RequestTimestamp * 1000).toISOString().split('T')[0];
|
||||
const Algorithm = "TC3-HMAC-SHA256";
|
||||
const CredentialScope = `${formattedDate}/${service}/tc3_request`;
|
||||
|
||||
const HashedCanonicalRequest = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(CanonicalRequest)
|
||||
).then(hash => Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join(''));
|
||||
|
||||
const StringToSign =
|
||||
`${Algorithm}\n${RequestTimestamp}\n${CredentialScope}\n${HashedCanonicalRequest}`;
|
||||
|
||||
async function hmac(key, string) {
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
typeof key === 'string' ? new TextEncoder().encode(key) : key,
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['sign']
|
||||
);
|
||||
const signature = await crypto.subtle.sign(
|
||||
'HMAC',
|
||||
cryptoKey,
|
||||
new TextEncoder().encode(string)
|
||||
);
|
||||
return new Uint8Array(signature);
|
||||
}
|
||||
|
||||
const SecretDate = await hmac("TC3" + secretKey, formattedDate);
|
||||
const SecretService = await hmac(SecretDate, service);
|
||||
const SecretSigning = await hmac(SecretService, "tc3_request");
|
||||
|
||||
const Signature = Array.from(
|
||||
new Uint8Array(
|
||||
await crypto.subtle.sign(
|
||||
'HMAC',
|
||||
await crypto.subtle.importKey(
|
||||
'raw',
|
||||
SecretSigning,
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['sign']
|
||||
),
|
||||
new TextEncoder().encode(StringToSign)
|
||||
)
|
||||
)
|
||||
).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
const Authorization =
|
||||
`${Algorithm} Credential=${secretId}/${CredentialScope}, SignedHeaders=${SignedHeaders}, Signature=${Signature}`;
|
||||
|
||||
headersArray["X-TC-Timestamp"] = RequestTimestamp.toString();
|
||||
headersArray["Authorization"] = Authorization;
|
||||
|
||||
return headersArray;
|
||||
}
|
||||
|
||||
// 处理 OPTIONS 请求
|
||||
export function onRequestOptions() {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 处理 POST 请求
|
||||
export async function onRequestPost(context) {
|
||||
try {
|
||||
const data = await context.request.json();
|
||||
const { secretId, secretKey, zoneId, type, targets } = data;
|
||||
|
||||
const service = "teo";
|
||||
const host = "teo.tencentcloudapi.com";
|
||||
|
||||
const payload = {
|
||||
ZoneId: zoneId,
|
||||
Type: type,
|
||||
Targets: targets
|
||||
};
|
||||
|
||||
const headersPending = {
|
||||
'Host': host,
|
||||
'Content-Type': 'application/json',
|
||||
'X-TC-Action': 'CreatePurgeTask',
|
||||
'X-TC-Version': '2022-09-01',
|
||||
'X-TC-Region': 'ap-guangzhou',
|
||||
};
|
||||
|
||||
const headers = await qcloudV3Post(secretId, secretKey, service, payload, headersPending);
|
||||
|
||||
const response = await fetch(`https://${host}`, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
return new Response(JSON.stringify(result), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 处理其他 HTTP 方法
|
||||
export function onRequest() {
|
||||
return new Response(JSON.stringify({ error: 'Only POST method is allowed' }), {
|
||||
status: 405,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
}
|
||||
});
|
||||
}
|
201
index.html
201
index.html
@ -1,201 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>腾讯云EdgeOne缓存刷新工具</title>
|
||||
|
||||
<link rel="shortcut icon" href="https://i-aws.czl.net/r2/2023/06/20/649168ec9d6a8.ico">
|
||||
<link rel="stylesheet" href="https://i-aws.czl.net/cdnjs/ajax/libs/mdui/2.1.3/mdui.min.css" />
|
||||
<script src="https://i-aws.czl.net/cdnjs/ajax/libs/mdui/2.1.3/mdui.global.js"></script>
|
||||
<link rel="stylesheet" href="https://i-aws.czl.net/g-f/frame/czlfonts/slice/font-noimportant.css" media="all">
|
||||
<style>
|
||||
body {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.config-section,
|
||||
.operation-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
display: inline-block;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: 300px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
|
||||
#result {
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-left: 120px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>腾讯云EdgeOne缓存刷新工具</h1>
|
||||
|
||||
<div class="config-section">
|
||||
<h2>配置信息</h2>
|
||||
<div class="input-group">
|
||||
<mdui-text-field label="SecretId" type="text" id="secretId" ></mdui-text-field>
|
||||
<div class="help-text">从腾讯云API密钥管理页面获取: <a href="https://console.cloud.tencent.com/cam/capi" target="_blank">https://console.cloud.tencent.com/cam/capi</a></div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<mdui-text-field label="SecretKey" type="text" id="secretKey"></mdui-text-field>
|
||||
<div class="help-text">从腾讯云API密钥管理页面获取: <a href="https://console.cloud.tencent.com/cam/capi" target="_blank">https://console.cloud.tencent.com/cam/capi</a></div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<mdui-text-field label="ZoneId" type="text" id="zoneId"></mdui-text-field>
|
||||
<div class="help-text">从EdgeOne控制台站点信息中获取: <a href="https://console.cloud.tencent.com/edgeone/zones" target="_blank">https://console.cloud.tencent.com/edgeone/zones</a></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="operation-section">
|
||||
<h2>操作</h2>
|
||||
<div class="input-group">
|
||||
<mdui-text-field type="text" id="targets" label="目标地址/标签" placeholder="多个地址用逗号分隔">
|
||||
</div>
|
||||
<div>
|
||||
<h3>快速示例:</h3>
|
||||
<mdui-button variant="text" onclick="fillExample('url')">URL刷新示例</mdui-button>
|
||||
<mdui-button variant="text" onclick="fillExample('prefix')">目录刷新示例</mdui-button>
|
||||
<mdui-button variant="text" onclick="fillExample('host')">Host刷新示例</mdui-button>
|
||||
<mdui-button variant="text" onclick="fillExample('all')">全部刷新示例</mdui-button>
|
||||
<mdui-button variant="text" onclick="fillExample('tag')">Cache Tag示例</mdui-button>
|
||||
</div>
|
||||
<div style="margin-top: 10px;">
|
||||
<mdui-button variant="elevated" onclick="purgeUrl()">URL刷新</mdui-button>
|
||||
<mdui-button variant="elevated" onclick="purgePrefix()">目录刷新</mdui-button>
|
||||
<mdui-button variant="elevated" onclick="purgeHost()">Host刷新</mdui-button>
|
||||
<mdui-button variant="elevated" onclick="purgeAll()">刷新全部</mdui-button>
|
||||
<mdui-button variant="elevated" onclick="purgeCacheTag()">Cache Tag刷新</mdui-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="result">
|
||||
<h3>执行结果:</h3>
|
||||
<pre id="resultContent"></pre>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 页面加载时恢复保存的配置
|
||||
window.onload = function () {
|
||||
document.getElementById('secretId').value = localStorage.getItem('secretId') || '';
|
||||
document.getElementById('secretKey').value = localStorage.getItem('secretKey') || '';
|
||||
document.getElementById('zoneId').value = localStorage.getItem('zoneId') || '';
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
function saveConfig() {
|
||||
localStorage.setItem('secretId', document.getElementById('secretId').value);
|
||||
localStorage.setItem('secretKey', document.getElementById('secretKey').value);
|
||||
localStorage.setItem('zoneId', document.getElementById('zoneId').value);
|
||||
}
|
||||
|
||||
// 监听输入变化保存配置
|
||||
document.getElementById('secretId').addEventListener('change', saveConfig);
|
||||
document.getElementById('secretKey').addEventListener('change', saveConfig);
|
||||
document.getElementById('zoneId').addEventListener('change', saveConfig);
|
||||
|
||||
// 获取配置信息
|
||||
function getConfig() {
|
||||
return {
|
||||
secretId: document.getElementById('secretId').value,
|
||||
secretKey: document.getElementById('secretKey').value,
|
||||
zoneId: document.getElementById('zoneId').value,
|
||||
targets: document.getElementById('targets').value.split(',').map(t => t.trim()).filter(t => t)
|
||||
};
|
||||
}
|
||||
|
||||
// API调用函数
|
||||
async function callApi(type, method = 'invalidate') {
|
||||
const config = getConfig();
|
||||
if (!config.secretId || !config.secretKey || !config.zoneId) {
|
||||
alert('请填写完整的配置信息!');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
secretId: config.secretId,
|
||||
secretKey: config.secretKey,
|
||||
zoneId: config.zoneId,
|
||||
type: type,
|
||||
targets: config.targets,
|
||||
method: method
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('https://eo-cleancache.czl.net/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const result = await response.json();
|
||||
document.getElementById('resultContent').textContent = JSON.stringify(result, null, 2);
|
||||
} catch (error) {
|
||||
document.getElementById('resultContent').textContent = '错误:' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
function purgeUrl() {
|
||||
callApi('purge_url');
|
||||
}
|
||||
|
||||
function purgePrefix() {
|
||||
callApi('purge_prefix');
|
||||
}
|
||||
|
||||
function purgeHost() {
|
||||
callApi('purge_host');
|
||||
}
|
||||
|
||||
function purgeAll() {
|
||||
callApi('purge_all');
|
||||
}
|
||||
|
||||
function purgeCacheTag() {
|
||||
callApi('purge_cache_tag');
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function fillExample(type) {
|
||||
const examples = {
|
||||
'url': 'https://test.czl.net/123.txt',
|
||||
'prefix': 'https://test.czl.net/book/',
|
||||
'host': 'test.czl.net',
|
||||
'all': '',
|
||||
'tag': 'tag1'
|
||||
};
|
||||
document.getElementById('targets').value = examples[type];
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
6
lib/utils.ts
Normal file
6
lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
8
next.config.ts
Normal file
8
next.config.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
output: "export",
|
||||
};
|
||||
|
||||
export default nextConfig;
|
6909
package-lock.json
generated
Normal file
6909
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
package.json
Normal file
39
package.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "eoccc",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"@radix-ui/react-toast": "^1.2.14",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.525.0",
|
||||
"next": "15.3.5",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"sonner": "^2.0.6",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.5",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.3.5",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
5
postcss.config.mjs
Normal file
5
postcss.config.mjs
Normal file
@ -0,0 +1,5 @@
|
||||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
|
||||
export default config;
|
1
public/file.svg
Normal file
1
public/file.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 391 B |
1
public/globe.svg
Normal file
1
public/globe.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
After Width: | Height: | Size: 1.0 KiB |
1
public/next.svg
Normal file
1
public/next.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
After Width: | Height: | Size: 1.3 KiB |
1
public/vercel.svg
Normal file
1
public/vercel.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
After Width: | Height: | Size: 128 B |
1
public/window.svg
Normal file
1
public/window.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
After Width: | Height: | Size: 385 B |
12
readme.md
12
readme.md
@ -1,19 +1,23 @@
|
||||
# 腾讯云EdgeOne缓存清理工具
|
||||
|
||||
直接使用链接: https://onepage.czl.net/tools/eo_cleancache.html
|
||||
直接使用链接: https://edgeone-cleancache.czl.net/
|
||||
|
||||
## 介绍
|
||||
这是一个用于清理 EdgeOne 缓存的 Cloudflare Worker + 单html页面 项目。
|
||||
这是一个用于清理 EdgeOne 缓存的 Edgeone Pages 项目, 通过静态页面+边缘函数实现快捷清理edgeone缓存。
|
||||
|
||||
[](https://edgeone.ai/pages/new?repository-url=https://github.com/woodchen-ink/Edgeone_CleanCache)
|
||||
|
||||
## 功能
|
||||
|
||||
- 提供了一个 API 接口,用于提交缓存清理任务。
|
||||
- 支持多种缓存清理类型,如文件、目录、路径前缀等。
|
||||
- 可以通过配置文件指定腾讯云 API 的密钥和其他参数。
|
||||
- 保存多个配置到浏览器内存, 选择即用, 不需要反复填写.
|
||||
- 全部代码开源, 放心使用, 自部署方便.
|
||||
|
||||
## 使用方法和部署方法
|
||||
## 分步骤部署方法
|
||||
|
||||
看帖子: https://www.sunai.net/t/topic/181?u=wood
|
||||
看帖子: https://www.sunai.net/t/topic/181
|
||||
|
||||
## 注意事项
|
||||
|
||||
|
27
tsconfig.json
Normal file
27
tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user