mirror of
https://github.com/woodchen-ink/Edgeone_CleanCache.git
synced 2025-07-18 14:01: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缓存清理工具
|
# 腾讯云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 接口,用于提交缓存清理任务。
|
||||||
- 支持多种缓存清理类型,如文件、目录、路径前缀等。
|
- 支持多种缓存清理类型,如文件、目录、路径前缀等。
|
||||||
- 可以通过配置文件指定腾讯云 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