feat: website revision
3
waf/.eslintrc.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
36
waf/.gitignore
vendored
Normal file
|
@ -0,0 +1,36 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
36
waf/README.md
Normal file
|
@ -0,0 +1,36 @@
|
|||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
21
waf/next.config.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
async rewrites() {
|
||||
return {
|
||||
fallback: [
|
||||
// These rewrites are checked after both pages/public files
|
||||
// and dynamic routes are checked
|
||||
{
|
||||
source: '/api/count',
|
||||
destination: 'https://waf-ce.chaitin.cn/api/count',
|
||||
},
|
||||
{
|
||||
source: '/api/:path*',
|
||||
destination: 'http://10.10.4.142:8080/api/:path*',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
3916
waf/package-lock.json
generated
Normal file
33
waf/package.json
Normal file
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "website2",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "11.11.1",
|
||||
"@emotion/styled": "11.11.0",
|
||||
"@mui/icons-material": "5.14.3",
|
||||
"@mui/lab": "5.0.0-alpha.138",
|
||||
"@mui/material": "5.14.3",
|
||||
"countup.js": "2.7.0",
|
||||
"next": "14.0.1",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"autoprefixer": "^10.0.1",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.0.1",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
6
waf/postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
BIN
waf/public/favicon.ico
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
waf/public/fonts/AlimamaShuHeiTi-Bold.otf
Normal file
1
waf/public/fonts/iconfont.js
Normal file
38
waf/public/images/ability/ability_CC.svg
Normal file
After Width: | Height: | Size: 105 KiB |
70
waf/public/images/ability/ability_HTTPS.svg
Normal file
After Width: | Height: | Size: 178 KiB |
69
waf/public/images/ability/ability_apisix.svg
Normal file
After Width: | Height: | Size: 236 KiB |
47
waf/public/images/ability/ability_asset.svg
Normal file
After Width: | Height: | Size: 2.4 MiB |
56
waf/public/images/ability/ability_cert.svg
Normal file
After Width: | Height: | Size: 138 KiB |
57
waf/public/images/ability/ability_maliciousip.svg
Normal file
After Width: | Height: | Size: 238 KiB |
59
waf/public/images/ability/ability_rivers.svg
Normal file
After Width: | Height: | Size: 425 KiB |
63
waf/public/images/ability/ability_verification.svg
Normal file
After Width: | Height: | Size: 101 KiB |
BIN
waf/public/images/banner-bg.png
Normal file
After Width: | Height: | Size: 1 MiB |
BIN
waf/public/images/community-banner.png
Normal file
After Width: | Height: | Size: 991 KiB |
BIN
waf/public/images/community-version.png
Normal file
After Width: | Height: | Size: 23 KiB |
7
waf/public/images/enterprise-bg.svg
Normal file
After Width: | Height: | Size: 7 MiB |
BIN
waf/public/images/enterprise-version.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
waf/public/images/feature1-bg.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
waf/public/images/feature1-icon.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
waf/public/images/feature1-left.png
Normal file
After Width: | Height: | Size: 158 KiB |
97
waf/public/images/feature1.svg
Normal file
|
@ -0,0 +1,97 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="606px" height="429px" viewBox="0 0 606 429" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>语义分析</title>
|
||||
<defs>
|
||||
<rect id="path-1" x="74" y="53" width="432" height="293" rx="12"></rect>
|
||||
<filter x="-16.2%" y="-17.1%" width="132.4%" height="147.8%" filterUnits="objectBoundingBox" id="filter-2">
|
||||
<feOffset dx="0" dy="20" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="20" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0.568627451 0 0 0 0 0.619607843 0 0 0 0 0.670588235 0 0 0 0.2 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
|
||||
</filter>
|
||||
<rect id="path-3" x="17" y="174" width="159" height="51" rx="25.5"></rect>
|
||||
<filter x="-44.0%" y="-98.0%" width="188.1%" height="374.5%" filterUnits="objectBoundingBox" id="filter-4">
|
||||
<feOffset dx="0" dy="20" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="20" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 1 0 0 0 0 0.352941176 0 0 0 0 0.368627451 0 0 0 0.2 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
|
||||
</filter>
|
||||
<rect id="path-5" x="175" y="56" width="40" height="40" rx="12"></rect>
|
||||
<filter x="-175.0%" y="-125.0%" width="450.0%" height="450.0%" filterUnits="objectBoundingBox" id="filter-6">
|
||||
<feOffset dx="0" dy="20" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="20" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feComposite in="shadowBlurOuter1" in2="SourceAlpha" operator="out" result="shadowBlurOuter1"></feComposite>
|
||||
<feColorMatrix values="0 0 0 0 0.568627451 0 0 0 0 0.619607843 0 0 0 0 0.670588235 0 0 0 0.2 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
|
||||
</filter>
|
||||
<rect id="path-7" x="329" y="255" width="214" height="114" rx="12"></rect>
|
||||
<filter x="-32.7%" y="-43.9%" width="165.4%" height="222.8%" filterUnits="objectBoundingBox" id="filter-8">
|
||||
<feOffset dx="0" dy="20" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="20" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0.568627451 0 0 0 0 0.619607843 0 0 0 0 0.670588235 0 0 0 0.2 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
|
||||
</filter>
|
||||
</defs>
|
||||
<g id="官网设计" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="白色版本3" transform="translate(-977.000000, -1250.000000)">
|
||||
<g id="语义分析" transform="translate(1000.000000, 1250.000000)">
|
||||
<rect id="矩形" fill-opacity="0" fill="#D8D8D8" x="0" y="0" width="560" height="400"></rect>
|
||||
<g id="矩形">
|
||||
<use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
|
||||
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-1"></use>
|
||||
</g>
|
||||
<rect id="矩形备份-39" fill="#F7F8FA" x="98" y="174" width="176" height="148" rx="3.5"></rect>
|
||||
<rect id="矩形备份-40" fill="#F7F8FA" x="306" y="174" width="176" height="148" rx="3.5"></rect>
|
||||
<path d="M86,53 L494,53 C500.627417,53 506,58.372583 506,65 L506,81 L506,81 L74,81 L74,65 C74,58.372583 79.372583,53 86,53 Z" id="矩形" fill="#111227"></path>
|
||||
<g id="矩形">
|
||||
<use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-3"></use>
|
||||
<use fill="#FF5A5E" fill-rule="evenodd" xlink:href="#path-3"></use>
|
||||
</g>
|
||||
<g id="矩形备份">
|
||||
<use fill="black" fill-opacity="1" filter="url(#filter-6)" xlink:href="#path-5"></use>
|
||||
<use fill-opacity="0.89767264" fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-5"></use>
|
||||
</g>
|
||||
<text id="0day-攻击" font-family="PingFangSC-Medium, PingFang SC" font-size="24" font-weight="400" line-spacing="36" fill="#FFFFFF">
|
||||
<tspan x="41" y="206">0day 攻击</tspan>
|
||||
</text>
|
||||
<circle id="椭圆形" fill-opacity="0.8" fill="#FF7777" cx="93" cy="67" r="3"></circle>
|
||||
<circle id="椭圆形备份" fill="#FFC641" cx="103" cy="67" r="3"></circle>
|
||||
<g id="矩形">
|
||||
<use fill="black" fill-opacity="1" filter="url(#filter-8)" xlink:href="#path-7"></use>
|
||||
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-7"></use>
|
||||
</g>
|
||||
<circle id="椭圆形备份-2" fill-opacity="0.8" fill="#52C41A" cx="113" cy="67" r="3"></circle>
|
||||
<rect id="矩形备份-27" fill="#0FC6C2" x="98" y="105" width="26" height="7" rx="3.5"></rect>
|
||||
<rect id="矩形备份-28" fill-opacity="0.6" fill="#0FC6C2" x="136" y="105" width="26" height="7" rx="3.5"></rect>
|
||||
<rect id="矩形备份-29" fill-opacity="0.2" fill="#0FC6C2" x="174" y="105" width="26" height="7" rx="3.5"></rect>
|
||||
<rect id="矩形备份-30" fill="#F7F8FA" x="98" y="128" width="176" height="7" rx="3.5"></rect>
|
||||
<rect id="矩形备份-33" fill="#F7F8FA" x="306" y="128" width="176" height="7" rx="3.5"></rect>
|
||||
<rect id="矩形备份-32" fill="#F7F8FA" x="98" y="151" width="176" height="7" rx="3.5"></rect>
|
||||
<rect id="矩形备份-38" fill="#F7F8FA" x="306" y="151" width="176" height="7" rx="3.5"></rect>
|
||||
<text id="多维-Web-应用防护" font-family="PingFangSC-Regular, PingFang SC" font-size="12" font-weight="normal" line-spacing="20" fill="#000000" fill-opacity="0.5">
|
||||
<tspan x="345" y="284">多维 Web 应用防护</tspan>
|
||||
</text>
|
||||
<text id="SQL-注入" font-family="PingFangSC-Regular, PingFang SC" font-size="10" font-weight="normal" line-spacing="10" fill="#52C41A">
|
||||
<tspan x="361" y="309">SQL 注入</tspan>
|
||||
</text>
|
||||
<text id="XSS" font-family="PingFangSC-Regular, PingFang SC" font-size="10" font-weight="normal" line-spacing="10" fill="#FFC641">
|
||||
<tspan x="361" y="331">XSS</tspan>
|
||||
</text>
|
||||
<text id="SSRF" font-family="PingFangSC-Regular, PingFang SC" font-size="10" font-weight="normal" line-spacing="10" fill="#FFC641">
|
||||
<tspan x="454" y="331">SSRF</tspan>
|
||||
</text>
|
||||
<text id="CSRF" font-family="PingFangSC-Regular, PingFang SC" font-size="10" font-weight="normal" line-spacing="10" fill="#52C41A">
|
||||
<tspan x="454" y="309">CSRF</tspan>
|
||||
</text>
|
||||
<text id="……" font-family="PingFangSC-Regular, PingFang SC" font-size="10" font-weight="normal" line-spacing="20" fill="#FFC641">
|
||||
<tspan x="360" y="344">……</tspan>
|
||||
</text>
|
||||
<circle id="椭圆形" stroke="#52C41A" stroke-width="2" cx="349" cy="303" r="3"></circle>
|
||||
<circle id="椭圆形备份-2885" stroke="#FFC641" stroke-width="2" cx="349" cy="325" r="3"></circle>
|
||||
<circle id="椭圆形备份-2887" stroke="#FFC641" stroke-width="2" cx="349" cy="346" r="3"></circle>
|
||||
<circle id="椭圆形备份-2886" stroke="#FFC641" stroke-width="2" cx="442" cy="325" r="3"></circle>
|
||||
<circle id="椭圆形备份-2884" stroke="#52C41A" stroke-width="2" cx="442" cy="303" r="3"></circle>
|
||||
<g id="算法" transform="translate(183.000000, 65.000000)" fill="#0FC6C2" fill-rule="nonzero">
|
||||
<path d="M20.1156247,7.15710835 C17.9350256,6.41506174 15.0525629,6.00787371 12.0013315,6.00787371 L11.8057741,6.00787371 C12.6121136,4.96311493 13.4425629,4.0603362 14.2515812,3.35043601 C14.9614814,2.72893848 15.6285197,2.28692516 16.1803666,2.07261567 C16.5661237,1.92259902 16.8822302,1.89313147 17.0483201,1.98689187 C17.2278043,2.09136775 17.3644266,2.41551085 17.4206828,2.87895513 C17.5010489,3.53259908 17.4206828,4.4193046 17.1876212,5.44263242 L19.068187,5.86857254 C19.3521471,4.61218314 19.4432286,3.52724134 19.333395,2.64321469 C19.2396346,1.87437939 18.9342436,0.848372695 18.0100339,0.315277835 C17.1233284,-0.196386076 15.5320804,-0.338366115 12.9791186,1.8984892 C11.7575545,2.96735779 10.5065228,4.42198347 9.34121497,6.11234958 C8.55630645,6.17664243 7.79818663,6.26772396 7.07221322,6.38559419 C6.94094866,5.93822312 6.83379391,5.5042464 6.75074899,5.08902176 C6.56590705,4.16213321 6.51768741,3.36383035 6.60876895,2.77983699 C6.6730618,2.37264895 6.80164749,2.08333114 6.96773735,1.98689187 C7.24098195,1.82883862 8.21341127,1.9413511 9.9278872,3.49777379 L11.2244596,2.0699368 C10.3002499,1.23145091 9.42693876,0.639420941 8.62595704,0.309920098 C7.92945119,0.0232811526 6.90880224,-0.207101551 6.00334463,0.315277835 C5.11663911,0.826941746 4.19778717,2.13690851 4.85946772,5.46406337 C4.94251265,5.88732462 5.0496674,6.32398021 5.17825309,6.77670901 C4.72552429,6.89190036 4.29422644,7.01780719 3.8870384,7.15710835 C0.675074899,8.25008676 0,9.6993547 0,10.7226825 C0,11.7754779 0.717936797,12.5469921 1.32336111,13.0077575 C2.01718809,13.5381735 2.97890193,14.0016177 4.18171396,14.3820171 L4.76570732,12.5443132 C2.52349426,11.834413 1.93146429,11.0414679 1.93146429,10.7226825 C1.93146429,10.5324828 2.11630623,10.2726326 2.43777047,10.0154612 C2.89853587,9.64577733 3.61647267,9.2894878 4.50853593,8.98409677 C4.91036623,8.84747447 5.33898521,8.72424651 5.78903514,8.6144129 C6.0488854,9.30288214 6.35159756,10.0074246 6.68913501,10.7200037 C6.21765413,11.7165428 5.81582383,12.7050453 5.49435959,13.650686 C4.93179718,15.3169422 4.63712162,16.8358608 4.64247936,18.0386728 C4.6478371,19.0807527 4.88893528,20.4576912 6.01138124,21.1059774 C6.40517493,21.3336812 6.8177207,21.4167262 7.219551,21.4167262 C7.74728812,21.4167262 8.24823656,21.2693884 8.64738799,21.1059774 C9.45372745,20.7737977 10.3323964,20.1737311 11.2592849,19.3272086 L9.96003362,17.9020505 C8.22948448,19.4799041 7.25169742,19.5950955 6.97577395,19.4343634 C6.78289541,19.3218509 6.57662252,18.8959108 6.57126479,18.0279573 C6.56590705,17.034097 6.82575731,15.7321669 7.32134801,14.2641469 C7.47136465,13.8194547 7.64013338,13.3667259 7.83033305,12.9059605 C7.86247947,12.9595378 7.89194703,13.0131152 7.92409345,13.0693715 C8.94474241,14.8347459 10.1073714,16.4795712 11.2887525,17.8216844 C12.4513815,19.1423667 13.6193682,20.1576579 14.664127,20.7550456 C15.2561569,21.0952619 16.0276711,21.4167262 16.8018641,21.4167262 C17.2090522,21.4167262 17.6189191,21.3283235 18.007355,21.1032985 C19.129801,20.4550123 19.3708992,19.075395 19.3762569,18.0333151 C19.3816146,16.830503 19.0869391,15.3115845 18.5243767,13.6426493 C18.2377377,12.7907691 17.8814482,11.9013847 17.4689024,11.0039637 L15.7142435,11.8156609 C16.1000006,12.6514679 16.4295014,13.4738806 16.6947094,14.2641469 C17.1903001,15.7321669 17.4501504,17.034097 17.4447926,18.0306362 C17.4421137,18.8985896 17.233162,19.3272086 17.0402835,19.4370422 C16.8474049,19.5495547 16.3732452,19.5147294 15.6204831,19.0834316 C14.7578874,18.5905197 13.7586694,17.7145297 12.7353415,16.5492218 C11.6370054,15.300869 10.5493847,13.7658773 9.59302862,12.1076576 C9.32514175,11.6442133 9.0733281,11.1834479 8.83758766,10.7200037 C9.07600697,10.2512016 9.33049949,9.7850785 9.59838636,9.32163422 C9.86359435,8.86086881 10.136839,8.41349775 10.4181202,7.97952103 C10.9378207,7.95273234 11.4655578,7.939338 12.0013315,7.939338 C14.84629,7.939338 17.5090855,8.31170074 19.4941271,8.98677564 C20.3888693,9.2894878 21.1041272,9.64577733 21.5648926,10.0181401 C21.8863568,10.2779903 22.0711988,10.5351617 22.0711988,10.7253614 C22.0711988,10.9316343 21.8595681,11.2102366 21.4898843,11.4915178 C20.9701838,11.8853115 20.169202,12.2576743 19.1753418,12.568423 L19.7512985,14.4088058 C20.9728626,14.0284064 21.9506497,13.5622833 22.6551921,13.0291884 C23.2686531,12.5657441 24,11.7888722 24,10.7253614 C24.0026631,9.6993547 23.3275882,8.25008676 20.1156247,7.15710835 L20.1156247,7.15710835 Z M7.92677232,8.35456264 C7.89194703,8.41349775 7.85980061,8.47243286 7.82497531,8.53136797 C7.78479228,8.4349287 7.74728812,8.33848943 7.70710509,8.24205016 C7.8089021,8.22597694 7.91337798,8.2125826 8.01785386,8.19918826 C7.9883863,8.24740789 7.95891874,8.30098527 7.92677232,8.35456264 L7.92677232,8.35456264 Z" id="形状"></path>
|
||||
<path d="M10.3002499,11.0736143 C10.3002499,12.4051628 11.3796833,13.4845961 12.7112317,13.4845961 C14.0427802,13.4845961 15.1222135,12.4051628 15.1222135,11.0736143 C15.1222135,9.74206584 14.0427802,8.66263253 12.7112317,8.66263253 C11.3796833,8.66263253 10.3002499,9.74206584 10.3002499,11.0736143 Z" id="路径"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 13 KiB |
BIN
waf/public/images/feature2-bg.png
Normal file
After Width: | Height: | Size: 1 MiB |
BIN
waf/public/images/feature2-icon.png
Normal file
After Width: | Height: | Size: 7.9 KiB |
BIN
waf/public/images/feature2-right.png
Normal file
After Width: | Height: | Size: 142 KiB |
68
waf/public/images/feature2.svg
Normal file
|
@ -0,0 +1,68 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="600px" height="443px" viewBox="0 0 600 443" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>化繁为简</title>
|
||||
<defs>
|
||||
<rect id="path-1" x="84" y="60" width="432" height="293" rx="12"></rect>
|
||||
<filter x="-16.2%" y="-17.1%" width="132.4%" height="147.8%" filterUnits="objectBoundingBox" id="filter-2">
|
||||
<feOffset dx="0" dy="20" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="20" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0.568627451 0 0 0 0 0.619607843 0 0 0 0 0.670588235 0 0 0 0.2 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
|
||||
</filter>
|
||||
<circle id="path-3" cx="473" cy="88" r="70"></circle>
|
||||
<filter x="-50.0%" y="-35.7%" width="200.0%" height="200.0%" filterUnits="objectBoundingBox" id="filter-4">
|
||||
<feOffset dx="0" dy="20" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="20" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0.266666667 0 0 0 0 0.843137255 0 0 0 0 0.71372549 0 0 0 0.5 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
|
||||
</filter>
|
||||
<circle id="path-5" cx="106" cy="331" r="50"></circle>
|
||||
<filter x="-70.0%" y="-50.0%" width="240.0%" height="240.0%" filterUnits="objectBoundingBox" id="filter-6">
|
||||
<feOffset dx="0" dy="20" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="20" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 1 0 0 0 0 0.776470588 0 0 0 0 0.254901961 0 0 0 0.5 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
|
||||
</filter>
|
||||
<circle id="path-7" cx="300.5" cy="213.5" r="67.5"></circle>
|
||||
<mask id="mask-8" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="135" height="135" fill="white">
|
||||
<use xlink:href="#path-7"></use>
|
||||
</mask>
|
||||
<linearGradient x1="15.9475528%" y1="15.5184638%" x2="60.8557404%" y2="61.8849334%" id="linearGradient-9">
|
||||
<stop stop-color="#FFFFFF" offset="0%"></stop>
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="官网设计" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="白色版本3" transform="translate(-276.000000, -1920.000000)">
|
||||
<g id="化繁为简" transform="translate(276.000000, 1922.000000)">
|
||||
<rect id="矩形" fill-opacity="0" fill="#D8D8D8" x="0" y="0" width="600" height="400"></rect>
|
||||
<g id="矩形">
|
||||
<use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
|
||||
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-1"></use>
|
||||
</g>
|
||||
<path d="M96,60 L504,60 C510.627417,60 516,65.372583 516,72 L516,88 L516,88 L84,88 L84,72 C84,65.372583 89.372583,60 96,60 Z" id="矩形" fill="#111227"></path>
|
||||
<circle id="椭圆形" fill-opacity="0.8" fill="#FF7777" cx="103" cy="74" r="3"></circle>
|
||||
<circle id="椭圆形备份" fill="#FFC641" cx="113" cy="74" r="3"></circle>
|
||||
<circle id="椭圆形备份-2" fill-opacity="0.8" fill="#52C41A" cx="123" cy="74" r="3"></circle>
|
||||
<g id="椭圆形">
|
||||
<use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-3"></use>
|
||||
<use fill="#44D7B6" fill-rule="evenodd" xlink:href="#path-3"></use>
|
||||
</g>
|
||||
<g id="椭圆形备份-21">
|
||||
<use fill="black" fill-opacity="1" filter="url(#filter-6)" xlink:href="#path-5"></use>
|
||||
<use fill="#FFC641" fill-rule="evenodd" xlink:href="#path-5"></use>
|
||||
</g>
|
||||
<use id="椭圆形" stroke="#0FC6C2" mask="url(#mask-8)" stroke-width="4" stroke-dasharray="5" xlink:href="#path-7"></use>
|
||||
<g id="安装-(1)" transform="translate(443.000000, 59.000000)" fill="#FFFFFF" fill-rule="nonzero">
|
||||
<path d="M51.057781,56.1094784 L15.1950051,56.1094784 C12.0502943,56.1094784 9.50111769,53.4949382 9.50111769,50.2703387 L9.50111769,20.9076002 C9.50111769,19.8399963 9.78799085,18.7941802 10.3254241,17.8790911 L13.8260029,11.9600627 C14.8572937,10.217036 16.6983657,9.15306336 18.6883213,9.14943206 L47.4773135,9.10947836 C49.4600065,9.10585639 51.3047098,10.1625664 52.3396319,11.8946992 L55.9128368,17.8681972 C56.461164,18.7869176 56.7516685,19.8436276 56.7516685,20.9184941 L56.7516685,50.2667074 C56.7516685,53.4949382 54.2024918,56.1094784 51.057781,56.1094784 L51.057781,56.1094784 Z" id="路径" fill-opacity="0.300808566"></path>
|
||||
<path d="M58.6857056,11.1076456 L54.4890691,4.26384245 C52.8761269,1.63534413 49.9601366,0 46.8761314,0 L46.8649305,0 L13.0379492,0.0448039487 C9.9427431,0.048537611 7.01928545,1.69508272 5.41381063,4.34598302 L1.29558101,11.1263139 C0.447823588,12.5231354 0,14.1258358 0,15.7597889 L0,49.4150217 C0,54.3397224 4.00621991,58.3459421 8.9309206,58.3459421 L51.0690343,58.3459421 C55.993735,58.3459421 59.9999775,54.3397224 59.9999775,49.4150217 L59.9999775,15.7747236 C60.0036884,14.1281785 59.5481816,12.5115027 58.6857056,11.1076456 Z M9.24081457,6.66832102 C10.0398183,5.34660454 11.4959467,4.52893248 13.0416829,4.52519881 L46.8686641,4.48039487 L46.8761314,4.48039487 C48.4144003,4.48039487 49.866795,5.29433327 50.6695324,6.60484876 L53.9663563,11.9775889 L6.01866393,11.9775889 L9.24081457,6.66832102 Z M51.072768,53.8692809 L8.93465426,53.8692809 C6.47790441,53.8692809 4.48412869,51.8717716 4.48412869,49.4187554 L4.48412869,16.4579838 L55.5232935,16.4579838 L55.5232935,49.4150217 C55.5232935,51.8717716 53.5257842,53.8692809 51.072768,53.8692809 Z" id="形状"></path>
|
||||
<path d="M39.7074997,35.6602095 L32.1916373,42.0858424 L32.2289739,22.8126772 C32.2307595,22.2182979 31.9959451,21.64764 31.5763546,21.2266485 C31.1567641,20.8056571 30.5868921,20.5687461 29.9925101,20.5687461 L29.9887765,20.5687461 C28.7529342,20.5687461 27.7523127,21.5693676 27.748579,22.8052099 L27.7112424,41.7012752 L21.069057,35.9962391 C20.1319078,35.189768 18.7168497,35.2980442 17.9103786,36.2351935 C17.1039076,37.1723427 17.2121838,38.5874008 18.149333,39.3938719 L28.7043966,48.4629378 C29.1225668,48.825103 29.6452795,49.0043188 30.1642586,49.0043188 C30.6832377,49.0043188 31.2022167,48.825103 31.6203869,48.4666715 L42.6160227,39.0653096 C43.5569056,38.2625722 43.6651818,36.8475141 42.8624444,35.9066312 C42.4774336,35.4545076 41.9281951,35.1743566 41.3361557,35.1281145 C40.7441162,35.0818723 40.1580406,35.2733479 39.7074997,35.6602095 L39.7074997,35.6602095 Z" id="路径"></path>
|
||||
</g>
|
||||
<g id="KHCFDC_点击" transform="translate(84.000000, 308.000000)">
|
||||
<path d="M15.8283717,12.0295625 L45.9022779,24.059125 C47.485115,24.6922598 48.4348173,26.5916644 47.8016825,28.1745016 C47.485115,29.1242039 46.8519802,29.7573388 45.9022779,30.0739062 L33.8727154,34.1892828 L29.7573388,45.9022779 C29.1242039,47.485115 27.5413667,48.4348173 25.6419621,47.8016825 C24.6922598,47.485115 24.059125,46.8519802 23.7425575,45.9022779 L11.712995,16.1449391 C11.0798602,14.5621019 12.0295625,12.6626973 13.6123996,12.0295625 C14.2455345,11.712995 15.1952368,11.712995 15.8283717,12.0295625 Z" id="形状" fill="#FFFFFF" fill-rule="nonzero"></path>
|
||||
<circle id="椭圆形" stroke="url(#linearGradient-9)" stroke-width="4" cx="15.5" cy="15.5" r="13.5"></circle>
|
||||
</g>
|
||||
<g id="安全盾牌" transform="translate(265.000000, 175.000000)" fill="#0FC6C2" fill-rule="nonzero">
|
||||
<path d="M33.0299631,0.492832555 C34.1251466,-0.164277518 35.6584034,-0.164277518 36.7535869,0.492832555 L36.7535869,0.492832555 L36.8290477,0.530049519 C37.7120774,0.962433222 44.0420345,3.99948035 51.1282254,7.53727687 L51.9184537,7.93231918 C52.1827416,8.06461559 52.4478286,8.19749201 52.7134763,8.33084155 L53.5118632,8.73220263 C54.5779867,9.26895817 55.6492688,9.81157339 56.7104578,10.3532073 L57.5042187,10.7591419 C61.1973806,12.6516469 64.7201915,14.5111808 67.4187235,16.0444376 C68.7329438,16.7015477 69.3900539,18.0157679 69.6090905,19.329988 C72.0184941,42.5478772 62.818953,57.4423721 54.9336322,65.7657664 C47.2277631,73.856929 39.1830214,77.5427484 37.0583424,78.4624694 L36.897171,78.5318678 C36.7003936,78.6161839 36.5767741,78.6678192 36.5345501,78.6889312 C36.0964768,78.9079679 35.4393667,78.9079679 35.0012933,78.9079679 L34.8276041,78.9075401 C34.4023649,78.9045454 33.8513508,78.8805883 33.4680365,78.6889312 C33.0299631,78.4698946 -4.20627438,63.7944363 0.393496125,19.329988 C0.612532884,18.0157679 1.26964296,16.7015477 2.5838631,16.0444376 C5.28239512,14.5111808 8.78803354,12.6516469 12.456467,10.7591418 L13.2448169,10.3532073 C13.5082742,10.2177988 13.7723273,10.082329 14.0367414,9.94690472 L14.8308318,9.54087553 C15.2281836,9.33803617 15.6258193,9.13553981 16.0229464,8.93374722 L16.8162877,8.53120729 C16.9483199,8.46430932 17.080237,8.39751627 17.2120094,8.33084151 L18.0006747,7.93231914 C18.6560588,7.60157812 19.3063612,7.27446215 19.9479131,6.9526414 L20.7133403,6.56910509 C20.8401332,6.50564162 20.9665174,6.44241668 21.0924635,6.37944363 L21.8426484,6.00473493 C27.7983156,3.03295726 32.6094127,0.703107794 33.0299631,0.492832555 Z M47.0483113,27.872419 L32.372853,43.2049873 L24.7065688,36.1958131 C22.9542753,34.6625563 20.1067983,34.6625563 18.5735415,36.4148499 C17.0402847,38.1671435 17.0402847,41.0146204 18.7925782,42.5478772 L29.7444127,52.4045283 C30.6205595,53.0616384 31.715743,53.4997118 32.5918897,53.4997118 C33.6870732,53.4997118 35.0012933,53.0616384 35.6584034,52.1854916 L53.1813387,33.5673729 C54.9336323,31.8150793 54.7145955,28.9676023 52.9623019,27.4343455 C51.4290451,25.9010887 48.5815681,26.1201254 47.0483113,27.872419 Z" id="形状结合"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 10 KiB |
BIN
waf/public/images/feature3-bg.png
Normal file
After Width: | Height: | Size: 2.6 MiB |
BIN
waf/public/images/feature3-icon.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
waf/public/images/feature3-left.png
Normal file
After Width: | Height: | Size: 172 KiB |
165
waf/public/images/feature3.svg
Normal file
After Width: | Height: | Size: 22 KiB |
60
waf/public/images/feedback.svg
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
waf/public/images/gif/starred.gif
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
waf/public/images/gif/waf-logo.gif
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
waf/public/images/home-banner.png
Normal file
After Width: | Height: | Size: 2 MiB |
BIN
waf/public/images/logo/aqiyi.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
waf/public/images/logo/bcm.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
waf/public/images/logo/bilibili.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
waf/public/images/logo/boc.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
waf/public/images/logo/csn.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
waf/public/images/logo/didi.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
waf/public/images/logo/douyin.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
waf/public/images/logo/huawei.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
waf/public/images/logo/shunfeng.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
waf/public/images/logo/thu.png
Normal file
After Width: | Height: | Size: 38 KiB |
9
waf/public/images/partner-bg.svg
Normal file
After Width: | Height: | Size: 2.1 MiB |
BIN
waf/public/images/professional-version.png
Normal file
After Width: | Height: | Size: 17 KiB |
39
waf/public/images/safeline.svg
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="298px" height="324px" viewBox="0 0 298 324" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>雷池logo</title>
|
||||
<defs>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1">
|
||||
<stop stop-color="#4B4B4B" offset="0%"></stop>
|
||||
<stop stop-color="#000000" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-2">
|
||||
<stop stop-color="#0FC6C2" stop-opacity="0.9" offset="0%"></stop>
|
||||
<stop stop-color="#0FC6C2" stop-opacity="0.7" offset="99.9426355%"></stop>
|
||||
</linearGradient>
|
||||
<path d="M110.049657,49.667649 C110.049657,49.667649 81.1358702,46.2263115 76.8,26.7636364 C72.4880848,46.2263115 43.5503431,49.667649 43.5503431,49.667649 C14.2053649,53.3001718 0,36.4567369 0,36.4567369 C13.941859,65.8036979 38.4,64.7712967 38.4,64.7712967 L115.2,64.7712967 C115.2,64.7712967 139.634186,65.8036979 153.6,36.4567369 C153.6,36.4567369 139.394635,53.3192904 110.049657,49.667649 Z" id="path-3"></path>
|
||||
<filter x="-16.9%" y="-57.9%" width="133.9%" height="236.8%" filterUnits="objectBoundingBox" id="filter-4">
|
||||
<feOffset dx="0" dy="4" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="8" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0.0346592498 0 0 0 0 0.410127944 0 0 0 0 0.401920978 0 0 0 1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
|
||||
</filter>
|
||||
</defs>
|
||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="雷池logo">
|
||||
<path d="M292.40836,59.04 C290.927217,51.9634286 285.002646,46.6971429 277.761503,46.368 C222.13636,44.8868571 176.385503,16.5805714 157.953503,3.08571429 C152.358074,-1.02857143 144.95236,-1.02857143 139.356931,3.08571429 C120.431217,16.5805714 75.1740742,44.8868571 19.5489314,46.368 C12.4723599,46.6971429 6.21864565,51.9634286 4.90207422,59.04 C-3.98478292,103.474286 -19.2899258,254.057143 148.902074,324 C316.60036,253.892571 300.966074,103.474286 292.40836,59.04 Z" id="路径" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
|
||||
<path d="M149,261.4 C205.553958,261.4 251.4,215.553958 251.4,159 C251.4,131.275004 240.381593,106.123494 222.484813,87.6855068 C209.900749,96.0964568 185.81512,106.024178 175.564259,100.853688 C166.334879,96.1984273 157.476591,88.4505652 148.989396,77.610101 C142.047769,88.5334102 134.670586,95.5517221 126.857848,98.6650367 C120.689419,101.123107 98.2592604,102.915695 75.4419467,87.761039 C57.5883513,106.192154 46.6,131.312844 46.6,159 C46.6,215.553958 92.4460416,261.4 149,261.4 Z" id="椭圆形备份-31" fill="url(#linearGradient-2)"></path>
|
||||
<g id="编组-5备份-5" transform="translate(91.771423, 102.101722)" fill="#FFFFFF">
|
||||
<polygon id="路径-130备份-29" transform="translate(57.217971, 95.920999) rotate(-180.000000) translate(-57.217971, -95.920999) " points="56.6651511 64.9496372 -7.57241738e-17 97.1108413 50.6084036 126.892361 68.8016729 117.264704 34.3433228 97.1108413 56.6651511 84.5503086 96.9001091 107.376711 96.9001091 114.88399 114.435942 125.435553 114.435942 97.1108413"></polygon>
|
||||
<polygon id="路径-130备份-30" transform="translate(57.217971, 30.971362) rotate(-360.000000) translate(-57.217971, -30.971362) " points="56.6651511 0 -7.57241738e-17 32.1612041 50.6084036 61.9427239 68.8016729 52.3150668 34.3433228 32.1612041 56.6651511 19.6006714 96.9001091 42.4270741 96.9001091 49.9343528 114.435942 60.4859155 114.435942 32.1612041"></polygon>
|
||||
</g>
|
||||
<g id="长亭logo备份-20" transform="translate(72.200000, 45.222222)" fill-rule="nonzero">
|
||||
<g id="编组-7">
|
||||
<path d="M96.7632666,18.0061837 C96.7632666,18.0061837 79.3862969,15.2966085 76.7907961,0 C74.1952953,15.2966085 56.8183256,18.0061837 56.8183256,18.0061837 C39.1836466,20.8694936 30.6424242,7.60987058 30.6424242,7.60987058 C39.0363842,30.6893013 53.7258141,29.862977 53.7258141,29.862977 L99.8741859,29.862977 C99.8741859,29.862977 114.563616,30.6700845 122.957576,7.60987058 C122.957576,7.60987058 114.416353,20.8694936 96.7816744,18.0061837 L96.7632666,18.0061837 Z" id="路径" fill="#0FC6C2"></path>
|
||||
<g id="路径">
|
||||
<use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-3"></use>
|
||||
<use fill="#0FC6C2" xlink:href="#path-3"></use>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.7 KiB |
BIN
waf/public/images/version-banner.png
Normal file
After Width: | Height: | Size: 1 MiB |
BIN
waf/public/images/wechat-230717.png
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
waf/public/images/wechat-230825.png
Normal file
After Width: | Height: | Size: 68 KiB |
22
waf/src/api/index.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
export {
|
||||
getSetupCount,
|
||||
getDiscussions,
|
||||
getIssues,
|
||||
};
|
||||
|
||||
const BASE_API = "/api";
|
||||
|
||||
const BASE_URL = "http://10.10.4.142:8080/api";
|
||||
|
||||
function getSetupCount() {
|
||||
return fetch(BASE_API + "/count").then((res) => res.json());
|
||||
}
|
||||
|
||||
function getDiscussions(query: string, isClient = true) {
|
||||
return fetch((isClient ? BASE_API : BASE_URL) + "/repos/discussions?q=" + query).then((res) => res.json());
|
||||
}
|
||||
|
||||
function getIssues(query: string, isClient = true) {
|
||||
return fetch((isClient ? BASE_API : BASE_URL) + "/repos/issues?q=" + query).then((res) => res.json());
|
||||
}
|
||||
|
47
waf/src/common/utils.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
const MARK = "__ct_react_root__";
|
||||
|
||||
type ContainerType = (Element | DocumentFragment) & {
|
||||
[MARK]?: any;
|
||||
};
|
||||
|
||||
export function render(node: React.ReactElement, container: ContainerType) {
|
||||
const root = createRoot(container[MARK] || container);
|
||||
|
||||
root.render(node);
|
||||
|
||||
container[MARK] = root;
|
||||
}
|
||||
|
||||
export async function unmount(container: ContainerType) {
|
||||
return Promise.resolve().then(() => {
|
||||
container[MARK]?.unmount();
|
||||
delete container[MARK];
|
||||
});
|
||||
}
|
||||
|
||||
export function sizeLength(l: number) {
|
||||
return l > 1024 * 2 ? Math.round(l / 1024) + "KB" : l + "B";
|
||||
}
|
||||
|
||||
export function sampleLength(s: string) {
|
||||
const l = new Blob([s]).size;
|
||||
return l > 1024 * 2 ? Math.round(l / 1024) + "KB" : l + "B";
|
||||
}
|
||||
|
||||
export function sampleSummary(s: string) {
|
||||
return s.split("\n").slice(0, 2).join(" ").slice(0, 60);
|
||||
}
|
||||
|
||||
export function formatNumberWithCommas(v: number) {
|
||||
return v.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
/* 将 时间戳 转换成 "Jul 23" 的格式 **/
|
||||
export function formatDate(time: number): string {
|
||||
const date = new Date(time * 1000);
|
||||
const month = date.toLocaleString('en-US', { month: 'short' });
|
||||
const day = date.getDate();
|
||||
return `${month} ${day}`;
|
||||
}
|
13
waf/src/components/CNZZ.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import Script from 'next/script'
|
||||
|
||||
export default function CNZZScript() {
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
id="cnzz-js"
|
||||
src="https://v1.cnzz.com/z_stat.php?id=1281262430&web_id=1281262430"
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
};
|
148
waf/src/components/Footer.tsx
Normal file
|
@ -0,0 +1,148 @@
|
|||
import React from 'react';
|
||||
import Image from 'next/image'
|
||||
import { Box, Grid, Typography, Stack, SxProps, Container, Link } from '@mui/material';
|
||||
import WafTitle from './home/WafTitle'
|
||||
|
||||
const LINKS = [
|
||||
{
|
||||
title: "资源",
|
||||
items: [
|
||||
{
|
||||
label: "技术文档",
|
||||
to: "https://waf-ce.chaitin.cn/docs",
|
||||
},
|
||||
{
|
||||
label: "教学视频",
|
||||
to: "https://www.bilibili.com/medialist/detail/ml2342694989",
|
||||
},
|
||||
{
|
||||
label: "学习资料",
|
||||
to: "https://waf-ce.chaitin.cn/docs/",
|
||||
},
|
||||
{
|
||||
label: "更新日志",
|
||||
to: "https://waf-ce.chaitin.cn/docs/about/changelog",
|
||||
},
|
||||
],
|
||||
xs: 6,
|
||||
},
|
||||
{
|
||||
title: "关于我们",
|
||||
items: [
|
||||
{
|
||||
label: "长亭科技",
|
||||
to: "https://www.chaitin.cn/zh/",
|
||||
},
|
||||
{
|
||||
label: "CT Stack 安全社区",
|
||||
to: "https://stack.chaitin.cn/",
|
||||
},
|
||||
],
|
||||
xs: 12,
|
||||
},
|
||||
];
|
||||
|
||||
export const items = [
|
||||
{ to: "/community", label: "社区" },
|
||||
{ to: "/version", label: "版本对比" },
|
||||
{ to: "", label: "用户协议" },
|
||||
];
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<Box
|
||||
component="footer"
|
||||
sx={{
|
||||
backgroundColor: '#121426',
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="lg">
|
||||
<Grid container justifyContent="space-around" columns={24} spacing={2} sx={{ py: 5, px: 2 }} mt={0}>
|
||||
<Grid item xs={24} md={10}>
|
||||
<Stack
|
||||
id="groupchat"
|
||||
spacing={4}
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<WafTitle title="雷池 SafeLine" sx={{ marginLeft: '16px' }} />
|
||||
<Box>
|
||||
{items.map((item, index) => (
|
||||
<Box key={index} component="span" mr={5}>
|
||||
{item.to ? (
|
||||
<Link sx={{ fontSize: '16px', fontWeight: 600 }} href={item.to} rel={item.label}>
|
||||
{item.label}
|
||||
</Link>
|
||||
) : (
|
||||
<Typography component="span" sx={{ fontSize: '16px', fontWeight: 600, color: "common.white" }}>
|
||||
{item.label}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Grid>
|
||||
{LINKS.map((link) => (
|
||||
<Grid item xs={12} md={5} my={{ xs: 4, md: 0 }} key={link.title}>
|
||||
<Stack
|
||||
id="groupchat"
|
||||
spacing={1}
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Title title={link.title} />
|
||||
<Grid container>
|
||||
{link.items.map((item, index) => (
|
||||
<Grid key={index} item xs={link.xs} md={12}>
|
||||
<Link sx={{ fontSize: '14px', opacity: 0.5, fontWeight: 400, lineHeight: "38px" }} href={item.to} target="_blank" rel={item.label}>
|
||||
{item.label}
|
||||
</Link>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Grid>
|
||||
))}
|
||||
<Grid item xs={24} md={4} display={'flex'} justifyContent={{ xs: "center", lg: "flex-end" }}>
|
||||
<Stack
|
||||
id="groupchat"
|
||||
spacing={2}
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Title title="微信交流群" />
|
||||
<Image
|
||||
src="/images/wechat-230825.png"
|
||||
alt="wechat"
|
||||
width={96}
|
||||
height={96}
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container sx={{ pb: 1.5 }}>
|
||||
<Typography
|
||||
variant="inherit"
|
||||
sx={{ fontSize: "12px", color: "rgba(255,255,255,0.26)", fontWeight: 400 }}
|
||||
>
|
||||
Copyright © 2023 北京长亭科技有限公司. All rights reserved.
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
interface TitleProps {
|
||||
title: string;
|
||||
sx?: SxProps;
|
||||
}
|
||||
|
||||
const Title: React.FC<TitleProps> = ({ title, sx }) => {
|
||||
return (
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ color: "common.white", marginLeft: "8px", ...sx }}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
);
|
||||
};
|
27
waf/src/components/Icon.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import React, { type FC, useEffect } from "react";
|
||||
|
||||
import { Box, type SxProps } from "@mui/material";
|
||||
|
||||
interface IconProps {
|
||||
type: string;
|
||||
sx?: SxProps;
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
const Icon: FC<IconProps> = ({ type, sx, ...restProps }) => {
|
||||
useEffect(() => {
|
||||
require("/public/fonts/iconfont");
|
||||
}, []);
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Box
|
||||
component="svg"
|
||||
sx={{ width: "1em", height: "1em", fill: "currentColor", ...sx }}
|
||||
aria-hidden="true"
|
||||
{...restProps}
|
||||
>
|
||||
<use xlinkHref={`#${type}`} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
export default Icon;
|
57
waf/src/components/Message/Alert.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import React, { type FC, useRef, useEffect } from 'react'
|
||||
|
||||
import { Alert as MAlert, type AlertColor } from '@mui/material'
|
||||
|
||||
interface AlertProps {
|
||||
duration?: number
|
||||
onClose?(key: React.Key): void
|
||||
noticeKey: React.Key
|
||||
content?: React.ReactNode
|
||||
severity: AlertColor
|
||||
}
|
||||
|
||||
const Alert: FC<AlertProps> = (props) => {
|
||||
const { duration, severity, content, noticeKey, onClose } = props
|
||||
const closeTimer = useRef<number | null>(null)
|
||||
|
||||
const startCloseTimer = () => {
|
||||
if (duration) {
|
||||
closeTimer.current = window.setTimeout(() => {
|
||||
close()
|
||||
}, duration * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
const clearCloseTimer = () => {
|
||||
if (closeTimer.current) {
|
||||
clearTimeout(closeTimer.current)
|
||||
closeTimer.current = null
|
||||
}
|
||||
}
|
||||
|
||||
const close = (e?: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
if (e) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
clearCloseTimer()
|
||||
if (onClose) {
|
||||
onClose(noticeKey)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
startCloseTimer()
|
||||
return () => {
|
||||
clearCloseTimer()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<MAlert severity={severity} sx={{ mb: '10px' }}>
|
||||
{content}
|
||||
</MAlert>
|
||||
)
|
||||
}
|
||||
|
||||
export default Alert
|
97
waf/src/components/Message/Message.tsx
Normal file
|
@ -0,0 +1,97 @@
|
|||
import React, { useState, forwardRef, useImperativeHandle } from "react";
|
||||
|
||||
import { Snackbar, Box, type AlertColor } from "@mui/material";
|
||||
|
||||
import { render, unmount } from "@/common/utils";
|
||||
|
||||
import Alert from "./Alert";
|
||||
|
||||
export interface Notice {
|
||||
key?: React.Key;
|
||||
content?: React.ReactNode;
|
||||
severity: AlertColor;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
interface MessageProps {}
|
||||
|
||||
let seed = 0;
|
||||
const now = Date.now();
|
||||
|
||||
function getUuid() {
|
||||
const id = seed;
|
||||
seed += 1;
|
||||
return `ctMessage_${now}_${id}`;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const Message = forwardRef<any, MessageProps>((props, ref) => {
|
||||
const [notices, setNotices] = useState<Notice[]>([]);
|
||||
const add = (notice: Notice) => {
|
||||
const key = notice.key ?? getUuid();
|
||||
setNotices((state) => {
|
||||
state.push({ ...notice, key });
|
||||
return [...state];
|
||||
});
|
||||
};
|
||||
|
||||
const remove = (key: React.Key) => {
|
||||
setNotices((state) => state.filter((s) => s.key !== key));
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
add,
|
||||
remove,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Snackbar open anchorOrigin={{ vertical: "top", horizontal: "center" }}>
|
||||
<Box>
|
||||
{notices.map((item) => {
|
||||
const alertProps = {
|
||||
...item,
|
||||
noticeKey: item.key!,
|
||||
onClose: (noticeKey: React.Key) => {
|
||||
remove(noticeKey);
|
||||
},
|
||||
};
|
||||
return <Alert {...alertProps} key={item.key} />;
|
||||
})}
|
||||
</Box>
|
||||
</Snackbar>
|
||||
);
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
Message.newInstance = (properties: MessageProps, callback) => {
|
||||
const { ...props } = properties || {};
|
||||
const div = document?.createElement("div");
|
||||
document.body.appendChild(div);
|
||||
let called = false;
|
||||
function ref(notification: any) {
|
||||
if (called) {
|
||||
return;
|
||||
}
|
||||
called = true;
|
||||
callback({
|
||||
notice(noticeProps: MessageProps) {
|
||||
notification.add(noticeProps);
|
||||
},
|
||||
removeNotice(key: React.Key) {
|
||||
notification.remove(key);
|
||||
},
|
||||
component: notification,
|
||||
destroy() {
|
||||
unmount(div);
|
||||
if (div.parentNode) {
|
||||
div.parentNode.removeChild(div);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
setTimeout(() => {
|
||||
render(<Message {...props} ref={ref} />, div);
|
||||
});
|
||||
};
|
||||
|
||||
export default Message;
|
37
waf/src/components/Message/index.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import type React from "react";
|
||||
|
||||
import { type AlertColor } from "@mui/material";
|
||||
|
||||
import Notification from "./Message";
|
||||
|
||||
type MessageStaticFunctions = Record<
|
||||
AlertColor,
|
||||
(context: React.ReactNode, duration?: number) => void
|
||||
>;
|
||||
|
||||
const Message = {} as MessageStaticFunctions;
|
||||
|
||||
let notification: any = null;
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
// @ts-ignore
|
||||
Notification.newInstance({}, (n: any) => {
|
||||
notification = n;
|
||||
});
|
||||
|
||||
const commonOpen =
|
||||
(type: AlertColor) =>
|
||||
(content: React.ReactNode, duration: number = 3) => {
|
||||
notification.notice({
|
||||
duration,
|
||||
severity: type,
|
||||
content,
|
||||
});
|
||||
};
|
||||
|
||||
(["success", "warning", "info", "error"] as const).forEach((type) => {
|
||||
Message[type] = commonOpen(type);
|
||||
});
|
||||
}
|
||||
|
||||
export default Message;
|
40
waf/src/components/Modal/ConfirmDialog.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import React, { type FC } from "react";
|
||||
|
||||
import ErrorIcon from "@mui/icons-material/Error";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
import Modal, { type ModalProps } from "./Modal";
|
||||
|
||||
export interface ConfirmDialogProps extends ModalProps {
|
||||
content?: React.ReactNode;
|
||||
width?: string;
|
||||
}
|
||||
|
||||
const ConfirmDialog: FC<ConfirmDialogProps> = (props) => {
|
||||
const { title = "提示", content, width = "480px", ...rest } = props;
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
lineHeight: "22px",
|
||||
color: "text.main",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<ErrorIcon sx={{ color: "#FFBF00", mr: "16px", fontSize: "24px" }} />
|
||||
{title}
|
||||
</Box>
|
||||
}
|
||||
closable={false}
|
||||
{...rest}
|
||||
sx={{ width }}
|
||||
>
|
||||
<Box sx={{ color: "text.main", pl: "40px" }}>{content}</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmDialog;
|
130
waf/src/components/Modal/Modal.tsx
Normal file
|
@ -0,0 +1,130 @@
|
|||
import React, { type FC, useState } from 'react'
|
||||
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
import { LoadingButton } from '@mui/lab'
|
||||
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton } from '@mui/material'
|
||||
|
||||
export interface ModalProps {
|
||||
open?: boolean
|
||||
title?: React.ReactNode
|
||||
children?: React.ReactNode
|
||||
footer?: false | React.ReactNode
|
||||
okText?: React.ReactNode
|
||||
cancelText?: React.ReactNode
|
||||
showCancel?: boolean
|
||||
okColor?: 'primary' | 'error'
|
||||
cancelColor?: 'primary' | 'error'
|
||||
closable?: boolean
|
||||
onOk?(): void
|
||||
onClose?(): void
|
||||
onCancel?(): void
|
||||
sx?: any
|
||||
}
|
||||
|
||||
const Modal: FC<ModalProps> = (props) => {
|
||||
const {
|
||||
open = false,
|
||||
title,
|
||||
children,
|
||||
footer,
|
||||
okText = '确认',
|
||||
okColor = 'primary',
|
||||
cancelColor = 'primary',
|
||||
showCancel = true,
|
||||
cancelText = '取消',
|
||||
onOk,
|
||||
onClose,
|
||||
onCancel,
|
||||
closable = true,
|
||||
sx = {},
|
||||
} = props
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const onConfirm = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
await onOk?.()
|
||||
} catch (error) {}
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose || onCancel}
|
||||
PaperProps={{
|
||||
elevation: 0,
|
||||
}}
|
||||
sx={{
|
||||
'.MuiDialog-paper': {
|
||||
borderRadius: '12px',
|
||||
...sx,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{(title || closable) && (
|
||||
<DialogTitle
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
fontSize: '16px',
|
||||
p: '32px',
|
||||
pb: '16px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
{closable && (
|
||||
<IconButton
|
||||
sx={{
|
||||
color: 'text.auxiliary',
|
||||
}}
|
||||
onClick={onClose || onCancel}
|
||||
>
|
||||
<CloseIcon sx={{ fontSize: '20px' }} />
|
||||
</IconButton>
|
||||
)}
|
||||
</DialogTitle>
|
||||
)}
|
||||
|
||||
<DialogContent sx={{ px: '32px' }}>{children}</DialogContent>
|
||||
{footer === false && null}
|
||||
{footer === undefined && (
|
||||
<DialogActions
|
||||
sx={{
|
||||
p: '0 32px 32px',
|
||||
'.MuiButtonBase-root': {
|
||||
px: '30px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{showCancel && (
|
||||
<Button color={cancelColor} onClick={onCancel}>
|
||||
{cancelText}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<LoadingButton loading={loading} variant='contained' color={okColor} onClick={onConfirm}>
|
||||
{okText}
|
||||
</LoadingButton>
|
||||
</DialogActions>
|
||||
)}
|
||||
{footer && (
|
||||
<DialogActions
|
||||
sx={{
|
||||
p: '4px 24px 24px',
|
||||
'.MuiButtonBase-root': {
|
||||
p: '8px 30px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{footer}
|
||||
</DialogActions>
|
||||
)}
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default Modal
|
32
waf/src/components/Modal/confrim.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { render as reactRender } from "@/common/utils";
|
||||
|
||||
import ConfirmDialog, { type ConfirmDialogProps } from "./ConfirmDialog";
|
||||
|
||||
export default function confirm(config: ConfirmDialogProps) {
|
||||
const container = document.createDocumentFragment();
|
||||
const { onCancel: propCancel, onOk: propOk } = config;
|
||||
const onCancel = async () => {
|
||||
await propCancel?.();
|
||||
close();
|
||||
};
|
||||
const onOk = async () => {
|
||||
await propOk?.();
|
||||
close();
|
||||
};
|
||||
let currentConfig = { ...config, open: true, onCancel, onOk } as any;
|
||||
function render(props: ConfirmDialogProps) {
|
||||
setTimeout(() => {
|
||||
reactRender(<ConfirmDialog {...props} />, container);
|
||||
});
|
||||
}
|
||||
|
||||
function close() {
|
||||
currentConfig = {
|
||||
...currentConfig,
|
||||
open: false,
|
||||
};
|
||||
render(currentConfig);
|
||||
}
|
||||
|
||||
render(currentConfig);
|
||||
}
|
12
waf/src/components/Modal/index.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { type ConfirmDialogProps } from './ConfirmDialog'
|
||||
import OriginModal from './Modal'
|
||||
import confirm from './confrim'
|
||||
|
||||
type ModalStaticFunctions = Record<'confirm', (config: ConfirmDialogProps) => void>
|
||||
type ModalType = typeof OriginModal
|
||||
|
||||
const Modal = OriginModal as ModalType & ModalStaticFunctions
|
||||
|
||||
Modal.confirm = confirm
|
||||
|
||||
export default Modal
|
148
waf/src/components/NavBar.tsx
Normal file
|
@ -0,0 +1,148 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { AppBar, Grid, Toolbar, Typography, Button, Box, Container, Link, Stack } from '@mui/material';
|
||||
import Image from 'next/image';
|
||||
import Icon from "@/components/Icon";
|
||||
import usePopupState, { bindPopover, bindHover } from '@/components/Popover/usePopupState'
|
||||
import HoverPopover from '@/components/Popover/HoverPopover'
|
||||
|
||||
const navs = [
|
||||
{ to: "https://waf-ce.chaitin.cn/docs", label: "帮助文档", target: "_blank" },
|
||||
{ to: "/community", label: "社区", target: "_self" },
|
||||
{ to: "/version", label: "版本对比", target: "_self" },
|
||||
];
|
||||
|
||||
export default function NavBar() {
|
||||
const [isSticky, setIsSticky] = useState(false);
|
||||
|
||||
const popoverState = usePopupState({
|
||||
popupId: "wechat-qrcode-popover"
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const scrollTop = window.pageYOffset;
|
||||
setIsSticky(scrollTop > 0);
|
||||
};
|
||||
handleScroll()
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AppBar position='fixed' color='transparent'
|
||||
sx={{
|
||||
boxShadow: 'none',
|
||||
...(isSticky ? { backdropFilter: 'blur(8px)', background: 'rgba(255,255,255,0.8)' } : {}),
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="lg" sx={{ mx: 0 }}>
|
||||
<Toolbar sx={{ backgroundColor: 'transparent', py: 1, ml: 2, px: { sm: 0 } }}>
|
||||
<Grid container justifyContent="space-around">
|
||||
<Grid item xs={12} md={6} display="flex">
|
||||
<Box display="flex" alignItems="center">
|
||||
<Link href="/">
|
||||
<Grid container flexDirection="row" display="flex" spacing={2} sx={{ marginTop: '0px' }}>
|
||||
<Image
|
||||
src="/images/safeline.svg"
|
||||
alt="Waf Logo"
|
||||
width={24}
|
||||
height={26}
|
||||
/>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
ml: 1,
|
||||
mr: 7,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: "common.black",
|
||||
fontFamily: "AlimamaShuHeiTi-Bold",
|
||||
}}
|
||||
>
|
||||
雷池 SafeLine
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Link>
|
||||
<Box display={{ xs: 'none', md: 'flex' }} alignItems="center">
|
||||
{navs.map((nav, index) => (
|
||||
<Box component="span" key={index} mr={3.5}>
|
||||
<Link key={index} href={nav.to} sx={{ color: "common.black" }} target={nav.target}>{nav.label}</Link>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={0} md={6} display={{ xs: 'none', md: 'flex' }} justifyContent={'flex-end'}>
|
||||
<Box sx={{ fontSize: "16px", display: "flex", alignItems: "center" }}>
|
||||
<Link
|
||||
href="https://github.com/chaitin/SafeLine"
|
||||
target="_blank"
|
||||
sx={{
|
||||
color: "common.black",
|
||||
display: "flex",
|
||||
"&:hover": {
|
||||
color: "primary.main",
|
||||
},
|
||||
}}
|
||||
mr={3.5}
|
||||
>
|
||||
<Icon type="icon-github-fill" sx={{ mr: 1 }} />
|
||||
GitHub
|
||||
</Link>
|
||||
<Box mr={3.5}>
|
||||
<Typography
|
||||
{...bindHover(popoverState as any)}
|
||||
variant='body1'
|
||||
sx={{
|
||||
color: popoverState.isOpen ? 'primary.main' : 'common.black',
|
||||
cursor: "pointer",
|
||||
'&:hover': {
|
||||
color: "primary.main",
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
transition: 'unset',
|
||||
}}
|
||||
>
|
||||
微信讨论组
|
||||
</Typography>
|
||||
<HoverPopover
|
||||
{...bindPopover(popoverState)}
|
||||
anchorOrigin={{
|
||||
vertical: 42,
|
||||
horizontal: 'left',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
marginThreshold={16}
|
||||
>
|
||||
<Image
|
||||
src="/images/wechat-230825.png"
|
||||
alt="wechat"
|
||||
width={259}
|
||||
height={259}
|
||||
/>
|
||||
</HoverPopover>
|
||||
</Box>
|
||||
<Link href="https://demo.waf-ce.chaitin.cn:9443/dashboard" sx={{ color: "common.black" }} mr={3.5} target="_blank">演示 demo</Link>
|
||||
<Button
|
||||
variant="contained"
|
||||
target="_blank"
|
||||
sx={{ width: { xs: "100%", sm: "auto" } }}
|
||||
href="https://waf-ce.chaitin.cn/docs/guide/install"
|
||||
>
|
||||
立即安装
|
||||
</Button>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Toolbar>
|
||||
</Container>
|
||||
</AppBar>
|
||||
);
|
||||
}
|
24
waf/src/components/Popover/HoverPopover.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import * as React from "react";
|
||||
import { Popover, type PopoverProps } from '@mui/material'
|
||||
|
||||
const HoverPopover: React.ComponentType<PopoverProps> = React.forwardRef(
|
||||
function HoverPopover(props: PopoverProps, ref): any {
|
||||
return (
|
||||
<Popover
|
||||
{...props}
|
||||
ref={ref}
|
||||
style={{ pointerEvents: 'none', ...props.style }}
|
||||
PaperProps={{
|
||||
...props.PaperProps,
|
||||
style: {
|
||||
pointerEvents: 'auto',
|
||||
...props.PaperProps?.style,
|
||||
boxShadow: "0px 25px 50px 0px rgba(145,158,171,0.2)"
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export default HoverPopover
|
22
waf/src/components/Popover/useEvent.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { useCallback, useRef, useLayoutEffect } from 'react'
|
||||
|
||||
export function useEvent<Fn extends (...args: any[]) => any>(handler: Fn): Fn {
|
||||
if (typeof window === 'undefined') {
|
||||
return handler
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
return useCallbackEvent(handler)
|
||||
}
|
||||
|
||||
export function useCallbackEvent<Fn extends (...args: any[]) => any>(handler: Fn): Fn {
|
||||
|
||||
const handlerRef = useRef<any>(null)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
handlerRef.current = handler
|
||||
})
|
||||
|
||||
return useCallback((...args: any[]): any => {
|
||||
handlerRef.current?.(...args)
|
||||
}, []) as Fn
|
||||
}
|
382
waf/src/components/Popover/usePopupState.ts
Normal file
|
@ -0,0 +1,382 @@
|
|||
|
||||
import {
|
||||
type SyntheticEvent,
|
||||
type MouseEvent,
|
||||
type FocusEvent,
|
||||
useCallback,
|
||||
useState,
|
||||
useRef,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import { type PopoverPosition, type PopoverReference } from '@mui/material'
|
||||
import { useEvent } from './useEvent'
|
||||
|
||||
type ControlAriaProps = {
|
||||
'aria-controls'?: string
|
||||
'aria-describedby'?: string
|
||||
'aria-haspopup'?: true
|
||||
}
|
||||
|
||||
export type PopupState = {
|
||||
open: (eventOrAnchorEl?: SyntheticEvent | Element | null) => void
|
||||
close: () => void
|
||||
toggle: (eventOrAnchorEl?: SyntheticEvent | Element | null) => void
|
||||
onBlur: (event: FocusEvent) => void
|
||||
onMouseLeave: (event: MouseEvent) => void
|
||||
setOpen: (
|
||||
open: boolean,
|
||||
eventOrAnchorEl?: SyntheticEvent | Element | null
|
||||
) => void
|
||||
isOpen: boolean
|
||||
anchorEl: Element | undefined
|
||||
anchorPosition: PopoverPosition | undefined
|
||||
setAnchorEl: (anchorEl: Element | null | undefined) => any
|
||||
setAnchorElUsed: boolean
|
||||
disableAutoFocus: boolean
|
||||
popupId: string | undefined
|
||||
_openEventType: string | null | undefined
|
||||
_childPopupState: PopupState | null | undefined
|
||||
_setChildPopupState: (popupState: PopupState | null | undefined) => void
|
||||
}
|
||||
|
||||
export type CoreState = {
|
||||
isOpen: boolean
|
||||
setAnchorElUsed: boolean
|
||||
anchorEl: Element | undefined
|
||||
anchorPosition: PopoverPosition | undefined
|
||||
hovered: boolean
|
||||
focused: boolean
|
||||
_openEventType: string | null | undefined
|
||||
_childPopupState: PopupState | null | undefined
|
||||
_deferNextOpen: boolean
|
||||
_deferNextClose: boolean
|
||||
}
|
||||
|
||||
export const initCoreState: CoreState = {
|
||||
isOpen: false,
|
||||
setAnchorElUsed: false,
|
||||
anchorEl: undefined,
|
||||
anchorPosition: undefined,
|
||||
hovered: false,
|
||||
focused: false,
|
||||
_openEventType: null,
|
||||
_childPopupState: null,
|
||||
_deferNextOpen: false,
|
||||
_deferNextClose: false,
|
||||
}
|
||||
|
||||
export function bindPopover({
|
||||
isOpen,
|
||||
anchorEl,
|
||||
anchorPosition,
|
||||
close,
|
||||
popupId,
|
||||
onMouseLeave,
|
||||
disableAutoFocus,
|
||||
_openEventType,
|
||||
}: PopupState): {
|
||||
id?: string
|
||||
anchorEl?: Element | null
|
||||
anchorPosition?: PopoverPosition
|
||||
anchorReference: PopoverReference
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onMouseLeave: (event: MouseEvent) => void
|
||||
disableAutoFocus?: boolean
|
||||
disableEnforceFocus?: boolean
|
||||
disableRestoreFocus?: boolean
|
||||
} {
|
||||
const usePopoverPosition = _openEventType === 'contextmenu'
|
||||
return {
|
||||
id: popupId,
|
||||
anchorEl,
|
||||
anchorPosition,
|
||||
anchorReference: usePopoverPosition ? 'anchorPosition' : 'anchorEl',
|
||||
open: isOpen,
|
||||
onClose: close,
|
||||
onMouseLeave,
|
||||
...(disableAutoFocus && {
|
||||
disableAutoFocus: true,
|
||||
disableEnforceFocus: true,
|
||||
disableRestoreFocus: true,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
function controlAriaProps({
|
||||
isOpen,
|
||||
popupId,
|
||||
}: PopupState): ControlAriaProps {
|
||||
return {
|
||||
'aria-haspopup': true,
|
||||
'aria-controls': isOpen && popupId != null ? popupId : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export function bindHover(popupState: PopupState): ControlAriaProps & {
|
||||
// onTouchStart: (event: TouchEvent) => any
|
||||
onMouseOver: (event: MouseEvent) => any
|
||||
onMouseLeave: (event: MouseEvent) => any
|
||||
} {
|
||||
const { open, onMouseLeave } = popupState
|
||||
return {
|
||||
...controlAriaProps(popupState),
|
||||
// onTouchStart: open,
|
||||
onMouseOver: open,
|
||||
onMouseLeave,
|
||||
}
|
||||
}
|
||||
|
||||
function getPopupElement(
|
||||
element: Element,
|
||||
{ popupId }: PopupState
|
||||
): Element | null | undefined {
|
||||
if (!popupId) return null
|
||||
const rootNode: any =
|
||||
typeof element.getRootNode === 'function' ? element.getRootNode() : document
|
||||
if (typeof rootNode.getElementById === 'function') {
|
||||
return rootNode.getElementById(popupId)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function isElementInPopup(element: Element, popupState: PopupState): boolean {
|
||||
const { anchorEl, _childPopupState } = popupState
|
||||
return (
|
||||
isAncestor(anchorEl, element) ||
|
||||
isAncestor(getPopupElement(element, popupState), element) ||
|
||||
(_childPopupState != null && isElementInPopup(element, _childPopupState))
|
||||
)
|
||||
}
|
||||
|
||||
function isAncestor(
|
||||
parent: Element | null | undefined,
|
||||
child: Element | null | undefined
|
||||
): boolean {
|
||||
if (!parent) return false
|
||||
while (child) {
|
||||
if (child === parent) return true
|
||||
child = child.parentElement
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export default function usePopupState({
|
||||
parentPopupState,
|
||||
popupId,
|
||||
disableAutoFocus,
|
||||
}: {
|
||||
parentPopupState?: PopupState | null | undefined
|
||||
popupId?: string
|
||||
disableAutoFocus?: boolean | null | undefined
|
||||
}): PopupState {
|
||||
const isMounted = useRef(true)
|
||||
|
||||
useEffect((): (() => void) => {
|
||||
isMounted.current = true
|
||||
return () => {
|
||||
isMounted.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
const [state, _setState] = useState(initCoreState)
|
||||
|
||||
const setState = useCallback(
|
||||
(state: CoreState | ((prevState: CoreState) => CoreState)) => {
|
||||
if (isMounted.current) _setState(state)
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const setAnchorEl = useCallback(
|
||||
(anchorEl: Element | null | undefined) =>
|
||||
setState((state) => ({
|
||||
...state,
|
||||
setAnchorElUsed: true,
|
||||
anchorEl: anchorEl ?? undefined,
|
||||
})),
|
||||
[],
|
||||
)
|
||||
|
||||
const toggle = useEvent(
|
||||
(eventOrAnchorEl?: SyntheticEvent | Element | null) => {
|
||||
if (state.isOpen) close(eventOrAnchorEl)
|
||||
else open(eventOrAnchorEl)
|
||||
return state
|
||||
}
|
||||
)
|
||||
|
||||
const open = useEvent((eventOrAnchorEl?: SyntheticEvent | Element | null) => {
|
||||
const event =
|
||||
eventOrAnchorEl instanceof Element ? undefined : eventOrAnchorEl
|
||||
const element =
|
||||
eventOrAnchorEl instanceof Element
|
||||
? eventOrAnchorEl
|
||||
: eventOrAnchorEl?.currentTarget instanceof Element
|
||||
? eventOrAnchorEl.currentTarget
|
||||
: undefined
|
||||
|
||||
// if (event?.type === 'touchstart') {
|
||||
// setState((state) => ({ ...state, _deferNextOpen: true }))
|
||||
// return
|
||||
// }
|
||||
|
||||
const clientX = (event as MouseEvent | undefined)?.clientX
|
||||
const clientY = (event as MouseEvent | undefined)?.clientY
|
||||
const anchorPosition =
|
||||
typeof clientX === 'number' && typeof clientY === 'number'
|
||||
? { left: clientX, top: clientY }
|
||||
: undefined
|
||||
|
||||
const doOpen = (state: CoreState): CoreState => {
|
||||
if (!eventOrAnchorEl && !state.setAnchorElUsed) {
|
||||
console.error(
|
||||
'missingEventOrAnchorEl',
|
||||
'eventOrAnchorEl should be defined if setAnchorEl is not used'
|
||||
)
|
||||
}
|
||||
|
||||
if (parentPopupState) {
|
||||
if (!parentPopupState.isOpen) return state
|
||||
setTimeout(() => parentPopupState._setChildPopupState(popupState))
|
||||
}
|
||||
|
||||
const newState: CoreState = {
|
||||
...state,
|
||||
isOpen: true,
|
||||
anchorPosition,
|
||||
hovered: event?.type === 'mouseover' || state.hovered,
|
||||
focused: event?.type === 'focus' || state.focused,
|
||||
_openEventType: event?.type,
|
||||
}
|
||||
|
||||
if (event?.currentTarget) {
|
||||
if (!state.setAnchorElUsed) {
|
||||
newState.anchorEl = event?.currentTarget as any
|
||||
}
|
||||
} else if (element) {
|
||||
newState.anchorEl = element
|
||||
}
|
||||
|
||||
return newState
|
||||
}
|
||||
|
||||
setState((state: CoreState): CoreState => {
|
||||
if (state._deferNextOpen) {
|
||||
setTimeout(() => setState(doOpen), 0)
|
||||
return { ...state, _deferNextOpen: false }
|
||||
} else {
|
||||
return doOpen(state)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
const doClose = (state: CoreState): CoreState => {
|
||||
const { _childPopupState } = state
|
||||
setTimeout(() => {
|
||||
_childPopupState?.close()
|
||||
parentPopupState?._setChildPopupState(null)
|
||||
})
|
||||
return { ...state, isOpen: false, hovered: false, focused: false }
|
||||
}
|
||||
|
||||
const close = useEvent(
|
||||
(eventOrAnchorEl?: SyntheticEvent | Element | null) => {
|
||||
const event =
|
||||
eventOrAnchorEl instanceof Element ? undefined : eventOrAnchorEl
|
||||
|
||||
// if (event?.type === 'touchstart') {
|
||||
// setState((state) => ({ ...state, _deferNextClose: true }))
|
||||
// return
|
||||
// }
|
||||
|
||||
setState((state: CoreState): CoreState => {
|
||||
if (state._deferNextClose) {
|
||||
setTimeout(() => setState(doClose), 0)
|
||||
return { ...state, _deferNextClose: false }
|
||||
} else {
|
||||
return doClose(state)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const setOpen = useCallback(
|
||||
(
|
||||
nextOpen: boolean,
|
||||
eventOrAnchorEl?: SyntheticEvent<any> | Element | null
|
||||
) => {
|
||||
if (nextOpen) {
|
||||
open(eventOrAnchorEl)
|
||||
} else {
|
||||
close(eventOrAnchorEl)
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const onMouseLeave = useEvent((event: MouseEvent) => {
|
||||
const { relatedTarget } = event
|
||||
setState((state: CoreState): CoreState => {
|
||||
if (
|
||||
state.hovered &&
|
||||
!(
|
||||
relatedTarget instanceof Element &&
|
||||
isElementInPopup(relatedTarget, popupState)
|
||||
)
|
||||
) {
|
||||
if (state.focused) {
|
||||
return { ...state, hovered: false }
|
||||
} else {
|
||||
return doClose(state)
|
||||
}
|
||||
}
|
||||
return state
|
||||
})
|
||||
})
|
||||
|
||||
const onBlur = useEvent((event: FocusEvent) => {
|
||||
if (!event) return
|
||||
const { relatedTarget } = event
|
||||
setState((state: CoreState): CoreState => {
|
||||
if (
|
||||
state.focused &&
|
||||
!(
|
||||
relatedTarget instanceof Element &&
|
||||
isElementInPopup(relatedTarget, popupState)
|
||||
)
|
||||
) {
|
||||
if (state.hovered) {
|
||||
return { ...state, focused: false }
|
||||
} else {
|
||||
return doClose(state)
|
||||
}
|
||||
}
|
||||
return state
|
||||
})
|
||||
})
|
||||
|
||||
const _setChildPopupState = useCallback(
|
||||
(_childPopupState: PopupState | null | undefined) =>
|
||||
setState((state) => ({ ...state, _childPopupState })),
|
||||
[]
|
||||
)
|
||||
|
||||
const popupState: PopupState = {
|
||||
...state,
|
||||
setAnchorEl,
|
||||
popupId,
|
||||
open,
|
||||
close,
|
||||
toggle,
|
||||
setOpen,
|
||||
onBlur,
|
||||
onMouseLeave,
|
||||
disableAutoFocus:
|
||||
disableAutoFocus ?? Boolean(state.hovered || state.focused),
|
||||
_setChildPopupState,
|
||||
}
|
||||
|
||||
return popupState
|
||||
}
|
378
waf/src/components/Theme.tsx
Normal file
|
@ -0,0 +1,378 @@
|
|||
import React, { useEffect, useMemo } from "react";
|
||||
import { zhCN } from "@mui/material/locale";
|
||||
import { createTheme } from "@mui/material/styles";
|
||||
|
||||
import { ThemeProvider as MUIThemeProvider } from "@mui/material";
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default ThemeProvider;
|
||||
|
||||
function ThemeProvider({ children }: Props) {
|
||||
const theme = useMemo(() => {
|
||||
return themes();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const bodyStyle = document.body.style;
|
||||
// @ts-ignore
|
||||
bodyStyle.backgroundColor = theme.palette.background.paper0;
|
||||
bodyStyle.color = theme.palette.text.primary;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [theme]);
|
||||
|
||||
return <MUIThemeProvider theme={theme}>{children}</MUIThemeProvider>;
|
||||
}
|
||||
|
||||
function themes() {
|
||||
// const color: Color = colors[mode] as Color;
|
||||
const themeOptions = {
|
||||
palette: themePalette(light),
|
||||
shadows: shadows(light),
|
||||
breakpoints: {
|
||||
values: {
|
||||
xs: 0,
|
||||
sm: 680,
|
||||
md: 900,
|
||||
lg: 1200,
|
||||
xl: 1536,
|
||||
},
|
||||
},
|
||||
mixins: {
|
||||
toolbar: {
|
||||
minHeight: "48px",
|
||||
padding: "16px",
|
||||
"@media (min-width: 600px)": {
|
||||
minHeight: "48px",
|
||||
},
|
||||
},
|
||||
},
|
||||
typography: {
|
||||
fontFamily: "inherit",
|
||||
body1: {
|
||||
fontSize: "16px",
|
||||
},
|
||||
body2: {
|
||||
fontSize: "14px",
|
||||
},
|
||||
subtitle1: {
|
||||
fontSize: "24px",
|
||||
},
|
||||
subtitle2: {
|
||||
fontSize: "12px",
|
||||
fontWeight: 400
|
||||
},
|
||||
h6: {
|
||||
fontSize: "16px",
|
||||
fontWeight: 600,
|
||||
},
|
||||
h5: {
|
||||
fontSize: "24px",
|
||||
fontWeight: 600,
|
||||
lineHeight: "32px",
|
||||
},
|
||||
h4: {
|
||||
fontSize: "28px",
|
||||
fontWeight: 600,
|
||||
},
|
||||
h3: {
|
||||
fontSize: "38px",
|
||||
fontWeight: 600,
|
||||
},
|
||||
h2: {
|
||||
fontSize: "48px",
|
||||
fontWeight: 600,
|
||||
},
|
||||
h1: {
|
||||
fontSize: "80px",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const themes = createTheme(themeOptions as any, zhCN);
|
||||
themes.components = componentStyleOverrides(light);
|
||||
|
||||
return themes;
|
||||
}
|
||||
|
||||
function themePalette(color: Color) {
|
||||
return {
|
||||
mode: "light",
|
||||
common: { black: "#000", white: "#fff" },
|
||||
primary: color.primary,
|
||||
secondary: color.secondary,
|
||||
info: color.info,
|
||||
success: color.success,
|
||||
warning: color.warning,
|
||||
error: color.error,
|
||||
neutral: color.neutral,
|
||||
divider: color.divider,
|
||||
text: color.text,
|
||||
background: color.background,
|
||||
shadowColor: color.shadowColor,
|
||||
charts: color.charts,
|
||||
action: {
|
||||
selectedOpacity: 0.1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function shadows(color: Color) {
|
||||
return [
|
||||
`0px 12px 24px -4px ${color.shadowColor},0px 0px 2px 0px ${color.shadowColor}`,
|
||||
`0px 12px 24px -4px ${color.shadowColor},0px 0px 2px 0px ${color.shadowColor}`,
|
||||
`0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.12),0px 1px 5px 0px rgba(0,0,0,0.12)`,
|
||||
`0px 3px 3px -2px rgba(0,0,0,0.2),0px 3px 4px 0px rgba(0,0,0,0.12),0px 1px 8px 0px rgba(0,0,0,0.12)`,
|
||||
`0px 2px 4px -1px rgba(0,0,0,0.2),0px 4px 5px 0px rgba(0,0,0,0.12),0px 1px 10px 0px rgba(0,0,0,0.12)`,
|
||||
`0px 3px 5px -1px rgba(0,0,0,0.2),0px 5px 8px 0px rgba(0,0,0,0.12),0px 1px 14px 0px rgba(0,0,0,0.12)`,
|
||||
`0px 3px 5px -1px rgba(0,0,0,0.2),0px 6px 10px 0px rgba(0,0,0,0.12),0px 1px 18px 0px rgba(0,0,0,0.12)`,
|
||||
`0px 4px 5px -2px rgba(0,0,0,0.2),0px 7px 10px 1px rgba(0,0,0,0.12),0px 2px 16px 1px rgba(0,0,0,0.12)`,
|
||||
`0px 5px 5px -3px rgba(0,0,0,0.2),0px 8px 10px 1px rgba(0,0,0,0.12),0px 3px 14px 2px rgba(0,0,0,0.12)`,
|
||||
`0px 5px 6px -3px rgba(0,0,0,0.2),0px 9px 12px 1px rgba(0,0,0,0.12),0px 3px 16px 2px rgba(0,0,0,0.12)`,
|
||||
`0px 6px 6px -3px rgba(0,0,0,0.2),0px 10px 14px 1px rgba(0,0,0,0.12),0px 4px 18px 3px rgba(0,0,0,0.12)`,
|
||||
`0px 6px 7px -4px rgba(0,0,0,0.2),0px 11px 15px 1px rgba(0,0,0,0.12),0px 4px 20px 3px rgba(0,0,0,0.12)`,
|
||||
`0px 7px 8px -4px rgba(0,0,0,0.2),0px 12px 17px 2px rgba(0,0,0,0.12),0px 5px 22px 4px rgba(0,0,0,0.12)`,
|
||||
`0px 7px 8px -4px rgba(0,0,0,0.2),0px 13px 19px 2px rgba(0,0,0,0.12),0px 5px 24px 4px rgba(0,0,0,0.12)`,
|
||||
`0px 7px 9px -4px rgba(0,0,0,0.2),0px 14px 21px 2px rgba(0,0,0,0.12),0px 5px 26px 4px rgba(0,0,0,0.12)`,
|
||||
`0px 8px 9px -5px rgba(0,0,0,0.2),0px 15px 22px 2px rgba(0,0,0,0.12),0px 6px 28px 5px rgba(0,0,0,0.12)`,
|
||||
`0px 8px 10px -5px rgba(0,0,0,0.2),0px 16px 24px 2px rgba(0,0,0,0.12),0px 6px 30px 5px rgba(0,0,0,0.12)`,
|
||||
`0px 8px 11px -5px rgba(0,0,0,0.2),0px 17px 26px 2px rgba(0,0,0,0.12),0px 6px 32px 5px rgba(0,0,0,0.12)`,
|
||||
`0px 9px 11px -5px rgba(0,0,0,0.2),0px 18px 28px 2px rgba(0,0,0,0.12),0px 7px 34px 6px rgba(0,0,0,0.12)`,
|
||||
`0px 9px 12px -6px rgba(0,0,0,0.2),0px 19px 29px 2px rgba(0,0,0,0.12),0px 7px 36px 6px rgba(0,0,0,0.12)`,
|
||||
`0px 10px 13px -6px rgba(0,0,0,0.2),0px 20px 31px 3px rgba(0,0,0,0.12),0px 8px 38px 7px rgba(0,0,0,0.12)`,
|
||||
`0px 10px 13px -6px rgba(0,0,0,0.2),0px 21px 33px 3px rgba(0,0,0,0.12),0px 8px 40px 7px rgba(0,0,0,0.12)`,
|
||||
`0px 10px 14px -6px rgba(0,0,0,0.2),0px 22px 35px 3px rgba(0,0,0,0.12),0px 8px 42px 7px rgba(0,0,0,0.12)`,
|
||||
`0px 11px 14px -7px rgba(0,0,0,0.2),0px 23px 36px 3px rgba(0,0,0,0.12),0px 9px 44px 8px rgba(0,0,0,0.12)`,
|
||||
`0px 11px 15px -7px rgba(0,0,0,0.2),0px 24px 38px 3px rgba(0,0,0,0.12),0px 9px 46px 8px rgba(0,0,0,0.12)`,
|
||||
];
|
||||
}
|
||||
|
||||
const light = {
|
||||
primary: {
|
||||
main: "#0FC6C2",
|
||||
lighter: "#B7EDEC",
|
||||
light: "#57D7D4",
|
||||
contrastText: "#fff",
|
||||
},
|
||||
secondary: {
|
||||
lighter: "#D6E4FF",
|
||||
light: "#84A9FF",
|
||||
main: "#3366FF",
|
||||
dark: "#1939B7",
|
||||
darker: "#091A7A",
|
||||
contrastText: "#fff",
|
||||
},
|
||||
info: {
|
||||
lighter: "#D0F2FF",
|
||||
light: "#74CAFF",
|
||||
main: "#1890FF",
|
||||
dark: "#0C53B7",
|
||||
darker: "#04297A",
|
||||
contrastText: "#fff",
|
||||
},
|
||||
success: {
|
||||
lighter: "#E9FCD4",
|
||||
light: "#AAF27F",
|
||||
main: "#0FC6C2",
|
||||
mainShadow: "#02BFA5",
|
||||
dark: "#229A16",
|
||||
darker: "#08660D",
|
||||
contrastText: "rgba(0,0,0,0.7)",
|
||||
},
|
||||
warning: {
|
||||
lighter: "#FFF7CD",
|
||||
light: "#FFE16A",
|
||||
main: "#FFBF00",
|
||||
dark: "#B78103",
|
||||
darker: "#7A4F01",
|
||||
contrastText: "rgba(0,0,0,0.7)",
|
||||
},
|
||||
neutral: {
|
||||
main: "#F4F6F8",
|
||||
contrastText: "rgba(0, 0, 0, 0.60)",
|
||||
},
|
||||
error: {
|
||||
lighter: "#FFE7D9",
|
||||
light: "#FFA48D",
|
||||
main: "#FF1844",
|
||||
dark: "#B72136",
|
||||
darker: "#7A0C2E",
|
||||
contrastText: "#fff",
|
||||
},
|
||||
divider: "#E3E8EF",
|
||||
text: {
|
||||
primary: "#000",
|
||||
secondary: "rgba(0,0,0,0.7)",
|
||||
auxiliary: "rgba(0,0,0,0.5)",
|
||||
slave: "rgba(0,0,0,0.05)",
|
||||
disabled: "rgba(0,0,0,0.15)",
|
||||
inversePrimary: "#fff",
|
||||
inverseAuxiliary: "rgba(255,255,255,0.5)",
|
||||
inverseDisabled: "rgba(255,255,255,0.15)",
|
||||
},
|
||||
background: {
|
||||
paper0: "#fff",
|
||||
paper: "#fff",
|
||||
paper2: "#f6f8fa",
|
||||
default: "#fff",
|
||||
chip: "#F4F6F8",
|
||||
circle: "#E6E8EC",
|
||||
},
|
||||
|
||||
shadowColor: "rgba(145,158,171,0.2)",
|
||||
table: {
|
||||
head: {
|
||||
backgroundColor: "#F4F6F8",
|
||||
color: "rgba(0,0,0,0.7)",
|
||||
},
|
||||
row: {
|
||||
hoverColor: "#F9FAFB",
|
||||
},
|
||||
cell: {
|
||||
borderColor: "#F3F4F5",
|
||||
},
|
||||
},
|
||||
charts: {
|
||||
color: ["#673AB7", "#02BFA5"],
|
||||
},
|
||||
};
|
||||
|
||||
type Color = typeof light;
|
||||
|
||||
function componentStyleOverrides(color: Color) {
|
||||
return {
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: ({ ownerState, theme }: any) => {
|
||||
return {
|
||||
boxShadow: "none",
|
||||
"&:hover": {
|
||||
boxShadow: "none",
|
||||
...(ownerState.color === "neutral" && {
|
||||
color: color.primary.main,
|
||||
fontWeight: 700,
|
||||
}),
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiFormControl: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
".MuiFormLabel-asterisk": {
|
||||
color: color.error.main,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableRow: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
"&:hover": {
|
||||
".MuiTableCell-root": {
|
||||
backgroundColor: color.table.row.hoverColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableBody: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
".MuiTableRow-root:hover": {
|
||||
".MuiTableCell-root": {
|
||||
backgroundColor: color.table.row.hoverColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
MuiTableCell: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
background: color.background.paper,
|
||||
lineHeight: 1.5,
|
||||
fontSize: "14px",
|
||||
paddingTop: "24px",
|
||||
paddingBottom: "24px",
|
||||
borderColor: color.table.cell.borderColor,
|
||||
paddingLeft: 0,
|
||||
"&:first-of-type": {
|
||||
paddingLeft: "32px",
|
||||
},
|
||||
},
|
||||
head: {
|
||||
backgroundColor: color.table.head.backgroundColor,
|
||||
color: color.table.head.color,
|
||||
fontSize: "12px",
|
||||
height: "24px",
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiMenu: {
|
||||
defaultProps: {
|
||||
elevation: 0,
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
defaultProps: {
|
||||
elevation: 1,
|
||||
},
|
||||
styleOverrides: {
|
||||
root: ({ ownerState }: any) => {
|
||||
return {
|
||||
...(ownerState.elevation === 0 && {
|
||||
backgroundColor: color.background.paper0,
|
||||
}),
|
||||
...(ownerState.elevation === 2 && {
|
||||
backgroundColor: color.background.paper2,
|
||||
}),
|
||||
backgroundImage: "none",
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiChip: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
// borderRadius: "4px",
|
||||
height: "auto",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiAppBar: {
|
||||
defaultProps: {
|
||||
elevation: 1,
|
||||
},
|
||||
},
|
||||
MuiOutlinedInput: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: color.primary.main,
|
||||
},
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: color.divider,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiLink: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
color: color.primary.contrastText,
|
||||
'&:hover': {
|
||||
color: color.primary.main,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
98
waf/src/components/Version/Consultation.tsx
Normal file
|
@ -0,0 +1,98 @@
|
|||
import React from 'react'
|
||||
import { useState, useEffect } from "react";
|
||||
import Message from "@/components/Message";
|
||||
import Modal from "@/components/Modal";
|
||||
import { Box, TextField, Typography, Button } from "@mui/material";
|
||||
|
||||
function Consultation() {
|
||||
const [text, setText] = useState("");
|
||||
const [wrongPhoneNumber, setWrongPhoneNumber] = useState(false);
|
||||
const [consultOpen, setConsultOpen] = useState(false);
|
||||
|
||||
const consultHandler = () => {
|
||||
const valid = /^1[3-9]\d{9}$/.test(text);
|
||||
setWrongPhoneNumber(!valid);
|
||||
if (!valid) {
|
||||
Message.error("手机号格式不正确");
|
||||
return;
|
||||
}
|
||||
fetch("https://leads.chaitin.net/api/trial", {
|
||||
// fetch('http://116.62.230.26:8999/api/trial', { // 测试用地址
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
phone: text,
|
||||
platform_source: "product-official-site",
|
||||
product_source: "safeline-ce",
|
||||
source_detail: "来自雷池社区版官网",
|
||||
}),
|
||||
})
|
||||
.then((d) => d.json())
|
||||
.then((d) => {
|
||||
if (d.code == 0) {
|
||||
Message.success("提交成功");
|
||||
} else {
|
||||
Message.error("提交失败");
|
||||
}
|
||||
setConsultOpen(false);
|
||||
});
|
||||
};
|
||||
|
||||
const textHandler = (v: string) => {
|
||||
setText(v);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!consultOpen) {
|
||||
setText("");
|
||||
}
|
||||
}, [consultOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{
|
||||
width: 146,
|
||||
mt: 4,
|
||||
mb: 4,
|
||||
boxShadow: "0px 15px 25px 0px rgba(15,198,194,0.3)",
|
||||
}}
|
||||
onClick={() => setConsultOpen(true)}
|
||||
>
|
||||
立即咨询
|
||||
</Button>
|
||||
<Modal
|
||||
open={consultOpen}
|
||||
onCancel={() => setConsultOpen(false)}
|
||||
title="咨询企业版"
|
||||
okText="提交"
|
||||
onOk={consultHandler}
|
||||
sx={{ width: 600 }}
|
||||
>
|
||||
<Box sx={{}}>
|
||||
<div style={{ margin: "10px 0 15px" }}>
|
||||
<TextField
|
||||
error={wrongPhoneNumber}
|
||||
fullWidth
|
||||
size="small"
|
||||
label="手机号"
|
||||
helperText={
|
||||
wrongPhoneNumber ? "手机号格式不正确" : ""
|
||||
}
|
||||
variant="outlined"
|
||||
value={text}
|
||||
onChange={(e) => textHandler(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Typography variant="body2">
|
||||
我们将在工作时间 2 小时内联系您,您的手机号不会用于其他目的
|
||||
</Typography>
|
||||
</Box>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Consultation;
|
379
waf/src/components/Version/FunctionTable.tsx
Normal file
|
@ -0,0 +1,379 @@
|
|||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Box,
|
||||
Collapse,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
alpha,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import React, { useState } from "react";
|
||||
import ExpandLess from "@mui/icons-material/ExpandLess";
|
||||
import ExpandMore from "@mui/icons-material/ExpandMore";
|
||||
import Icon from "@/components/Icon";
|
||||
|
||||
const Support = () => {
|
||||
return (
|
||||
<Icon type="icon-a-yiwanchengtianchong" color="#02BFA5" sx={{ margin: "auto" }} />
|
||||
);
|
||||
};
|
||||
const NotSupport = () => {
|
||||
return <Typography sx={{ margin: "auto" }} >-</Typography>;
|
||||
};
|
||||
|
||||
const Illustrate = ({ text }: { text: string }) => {
|
||||
return <Box color="#02BFA5">{text}</Box>;
|
||||
};
|
||||
|
||||
const versions = [
|
||||
{ title: '社区版', key: 'experience', width: '33%' },
|
||||
// { title: '专业版', key: 'major', width: '25%' },
|
||||
{ title: '企业版', key: 'basics', width: '33%' },
|
||||
]
|
||||
|
||||
const colors = ['light', 'main'] // 'lighter',
|
||||
|
||||
const FunctionTable = () => {
|
||||
const [open, setOpen] = useState<string[]>([
|
||||
"安全防护",
|
||||
"统计分析",
|
||||
"系统管理",
|
||||
"部署形态",
|
||||
"服务",
|
||||
]);
|
||||
|
||||
const cells = [
|
||||
{
|
||||
title: "安全防护",
|
||||
data: [
|
||||
{
|
||||
name: "智能语义分析检测",
|
||||
experience: <Support />, // 社区版
|
||||
major: <Support />, // 专业版
|
||||
basics: <Support />, // 企业版
|
||||
},
|
||||
{
|
||||
name: "简单规则特征库检测",
|
||||
experience: <Support />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
{
|
||||
name: "频率限制 / CC 攻击防护",
|
||||
experience: <Support />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
{
|
||||
name: "自定义黑白名单",
|
||||
experience: <Support />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
{
|
||||
name: "人机验证",
|
||||
experience: <Support />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
{
|
||||
name: "基础 API 采集",
|
||||
experience: <Support />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
{
|
||||
name: "威胁情报",
|
||||
experience: <Illustrate text="社区共享威胁 IP" />,
|
||||
major: <Support />,
|
||||
basics: <Illustrate text="高级威胁情报" />,
|
||||
},
|
||||
{
|
||||
name: "高级 Bot 防护、API 防护",
|
||||
experience: <NotSupport />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
{
|
||||
name: "网页防篡改",
|
||||
experience: <NotSupport />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
{
|
||||
name: "灵活可编程插件",
|
||||
experience: <NotSupport />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
{
|
||||
name: "精细化引擎调节",
|
||||
experience: <NotSupport />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "统计分析",
|
||||
data: [
|
||||
{
|
||||
name: "基础统计图表",
|
||||
tip: "",
|
||||
experience: <Support />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
{
|
||||
name: "高级统计分析与报告",
|
||||
experience: <NotSupport />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
{
|
||||
name: "实时可视化大屏",
|
||||
experience: <NotSupport />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "系统管理",
|
||||
data: [
|
||||
{
|
||||
name: "多设备集中管理",
|
||||
tip: "",
|
||||
experience: <Support />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
{
|
||||
name: "多租户管理",
|
||||
experience: <NotSupport />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
{
|
||||
name: "安全合规与审计",
|
||||
experience: <NotSupport />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "部署形态",
|
||||
data: [
|
||||
{
|
||||
name: "反向代理接入",
|
||||
experience: <Support />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
{
|
||||
name: "旁路、透明代理、路由等复杂接入方式",
|
||||
experience: <NotSupport />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
{
|
||||
name: "集群式可扩展部署",
|
||||
experience: <NotSupport />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
{
|
||||
name: "负载均衡",
|
||||
experience: <NotSupport />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
{
|
||||
name: "紧急模式 / Bypass",
|
||||
experience: <NotSupport />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
{
|
||||
name: "硬件形态交付",
|
||||
experience: <NotSupport />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
{
|
||||
name: "高可用 / HA",
|
||||
experience: <NotSupport />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "服务",
|
||||
data: [
|
||||
{
|
||||
name: "专业技术支持服务",
|
||||
experience: <NotSupport />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
{
|
||||
name: "漏洞应急服务",
|
||||
experience: <NotSupport />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
{
|
||||
name: "产品销售许可证",
|
||||
experience: <NotSupport />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
{
|
||||
name: "定制化",
|
||||
experience: <NotSupport />,
|
||||
major: <Support />,
|
||||
basics: <Support />,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const handleClick = (id: string) => {
|
||||
if (open?.includes(id)) {
|
||||
const udpateOpen = [...open];
|
||||
udpateOpen.splice(open?.indexOf(id), 1);
|
||||
setOpen([...udpateOpen]);
|
||||
} else {
|
||||
setOpen((open) => [...open, id]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer>
|
||||
<Table
|
||||
sx={{
|
||||
".MuiTableCell-root": {
|
||||
border: "none",
|
||||
backgroundColor: "transparent",
|
||||
pl: "8px",
|
||||
pr: "0px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TableHead sx={{ background: "transparent" }}>
|
||||
<TableRow sx={{ border: "0" }}>
|
||||
<TableCell sx={{ width: "33%" }} />
|
||||
{versions.map((item, index) => (
|
||||
<TableCell key={item.title} align="center" sx={{ width: item.width, fontSize: "16px" }}>
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
height: "40px",
|
||||
borderRadius: "4px",
|
||||
color: theme.palette.common.white,
|
||||
backgroundColor: theme.palette.primary[colors[index] as 'main' | 'light'],
|
||||
})}
|
||||
>
|
||||
{item.title}
|
||||
</Box>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{cells?.map((data) => (
|
||||
<React.Fragment key={`sub-table-${data.title}`}>
|
||||
<ListItem
|
||||
onClick={() => handleClick(data?.title)}
|
||||
sx={{
|
||||
backgroundColor: "#EFF1F8",
|
||||
borderRadius: "4px",
|
||||
cursor: "pointer",
|
||||
pl: 6,
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={data.title}
|
||||
sx={{
|
||||
".MuiTypography-root": { fontWeight: 600 },
|
||||
flex: "unset",
|
||||
}}
|
||||
/>
|
||||
<ListItemIcon sx={{ color: "common.black", marginLeft: 1 }}>
|
||||
{open?.includes(data?.title) ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItemIcon>
|
||||
</ListItem>
|
||||
<Collapse
|
||||
in={open?.includes(data?.title)}
|
||||
timeout="auto"
|
||||
unmountOnExit
|
||||
sx={{ width: "100%", mt: "16px !important" }}
|
||||
>
|
||||
<TableContainer>
|
||||
<Table
|
||||
sx={{
|
||||
".MuiTableRow-root": {
|
||||
"&:last-of-type": {
|
||||
".MuiTableCell-root": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
".MuiTableCell-root": {
|
||||
py: "12px !important",
|
||||
backgroundColor: "transparent !important",
|
||||
color: "#000",
|
||||
borderRight: "1px solid",
|
||||
borderColor: "rgba(0,0,0,.04)",
|
||||
"&:last-of-type": {
|
||||
borderRight: "none",
|
||||
},
|
||||
"&:first-of-type": {
|
||||
paddingLeft: 6,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TableBody
|
||||
sx={{
|
||||
backgroundColor: alpha("#EFF1F8", 0.2),
|
||||
}}
|
||||
>
|
||||
{data.data.map((item) => (
|
||||
<TableRow key={item.name}>
|
||||
<TableCell sx={{ width: "33%" }}>
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 400 }}>{item.name}</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
{versions.map((v, index) => (
|
||||
<TableCell key={index} sx={{ width: v.width }} align="center">
|
||||
{item[v.key as 'experience' | 'major' | 'basics']}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Collapse>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FunctionTable;
|
208
waf/src/components/Version/index.tsx
Normal file
|
@ -0,0 +1,208 @@
|
|||
import React from "react";
|
||||
import { alpha, Box, Button, Typography } from "@mui/material";
|
||||
import FunctionTable from "./FunctionTable";
|
||||
import Consultation from "./Consultation";
|
||||
|
||||
const VERSION_LIST = [
|
||||
{
|
||||
name: "社区版",
|
||||
name_bg: "/images/community-version.png",
|
||||
apply_desc: "适合小型个人网站或业余爱好项目",
|
||||
fee: "免费",
|
||||
fee_desc: "",
|
||||
desc: "无限站点,无限规则",
|
||||
operation: (
|
||||
<Button
|
||||
variant="contained"
|
||||
target="_blank"
|
||||
sx={{
|
||||
width: 146,
|
||||
my: 4,
|
||||
boxShadow: "0px 15px 25px 0px rgba(15,198,194,0.3)",
|
||||
}}
|
||||
href="https://waf-ce.chaitin.cn/docs/guide/install"
|
||||
>
|
||||
立即安装
|
||||
</Button>
|
||||
),
|
||||
functions: [
|
||||
"语义分析防护",
|
||||
"自定义规则",
|
||||
"访问频率限制",
|
||||
"人机验证",
|
||||
"社区 IP 情报",
|
||||
],
|
||||
},
|
||||
// {
|
||||
// name: "专业版",
|
||||
// name_bg: "/images/professional-version.png",
|
||||
// apply_desc: "适合专业网站与关键业务",
|
||||
// fee: "¥88",
|
||||
// fee_desc: "/月 按年订阅",
|
||||
// desc: "¥118 按月订阅",
|
||||
// operation: (
|
||||
// <Button
|
||||
// variant="contained"
|
||||
// target="_blank"
|
||||
// sx={{
|
||||
// width: 146,
|
||||
// my: 4,
|
||||
// boxShadow: "0px 15px 25px 0px rgba(15,198,194,0.3)",
|
||||
// }}
|
||||
// href="https://stack.chaitin.com/tool/detail?id=717"
|
||||
// >
|
||||
// 立即购买
|
||||
// </Button>
|
||||
// ),
|
||||
// functions: [
|
||||
// "所有社区版能力",
|
||||
// "自定义阻断页面",
|
||||
// "自定义相应动作",
|
||||
// "根据地区封禁",
|
||||
// "......",
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
name: "企业版",
|
||||
name_bg: "/images/enterprise-version.png",
|
||||
apply_desc: "适合小微到中大型企业",
|
||||
fee: "按需定制",
|
||||
fee_desc: "",
|
||||
desc: "",
|
||||
operation: (
|
||||
<Consultation />
|
||||
),
|
||||
functions: [
|
||||
"所有社区版能力",
|
||||
"高级统计分析与报告",
|
||||
"智能业务建模",
|
||||
"高级 Bot 防护、API 防护",
|
||||
"......",
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const Version = () => {
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
{VERSION_LIST.map((item, index) => (
|
||||
<Box
|
||||
key={item.name}
|
||||
sx={{
|
||||
width: { xs: "100%", sm: "42%" },
|
||||
flexShrink: 0,
|
||||
height: { xs: "auto" },
|
||||
borderRadius: "12px",
|
||||
mb: { xs: 2, sm: 2, md: 2 },
|
||||
mr: index < VERSION_LIST.length - 1 ? { xs: 0, sm: 2, md: 4 } : { xs: 0, sm: 0, md: 0 },
|
||||
position: "relative",
|
||||
bottom: 0,
|
||||
transition: "all 0.5s ease",
|
||||
"&:hover": {
|
||||
boxShadow: "0px 30px 40px 0px rgba(145,158,171,0.11)",
|
||||
bottom: 16,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
px: 3,
|
||||
py: 2,
|
||||
borderRadius: '12px 12px 0px 0px',
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "right bottom",
|
||||
textAlign: "center",
|
||||
backgroundImage: `url(${item.name_bg})`,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4" sx={{ fontWeight: 500, color: "common.white" }}>
|
||||
{item.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
py: 3,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
border: "1px solid #E3E8EF",
|
||||
borderRadius: '0px 0px 12px 12px ',
|
||||
borderTop: "none",
|
||||
}}
|
||||
>
|
||||
<Description content={item.apply_desc} />
|
||||
<Typography variant="h3" sx={{ mt: 3, mb: 1, lineHeight: "46px" }}>
|
||||
{item.fee}
|
||||
{item.fee_desc && (
|
||||
<Typography component="span" variant="subtitle2" sx={{ color: alpha("#000", 0.5), height: "20px" }}>
|
||||
{item.fee_desc}
|
||||
</Typography>
|
||||
)}
|
||||
</Typography>
|
||||
<Description content={item.desc} />
|
||||
<Box>{item.operation}</Box>
|
||||
<FunctionItems items={item.functions} />
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{ fontSize: "48px", fontWeight: 600, mt: "180px !important" }}
|
||||
>
|
||||
细节对比
|
||||
</Typography>
|
||||
<FunctionTable />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Version;
|
||||
|
||||
|
||||
const FunctionItems: React.FC<{ items: any[] }> = ({ items }) => {
|
||||
return (
|
||||
<Box>
|
||||
{items.map((f) => (
|
||||
<Box
|
||||
key={f}
|
||||
sx={{
|
||||
py: 2,
|
||||
position: "relative",
|
||||
pl: 2,
|
||||
"&:before": {
|
||||
content: "' '",
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: "22px",
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "success.main",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{f}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const Description: React.FC<{ content: string }> = ({ content }) => {
|
||||
return (
|
||||
<Typography variant="subtitle2" sx={{ color: alpha("#000", 0.5), height: "20px" }}>
|
||||
{content}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
|
264
waf/src/components/community/DiscussionList.tsx
Normal file
|
@ -0,0 +1,264 @@
|
|||
import React, { useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Table,
|
||||
TableRow,
|
||||
TableBody,
|
||||
TableContainer,
|
||||
Stack,
|
||||
Chip,
|
||||
Button,
|
||||
Typography,
|
||||
alpha,
|
||||
AvatarGroup,
|
||||
Avatar,
|
||||
Grid,
|
||||
Paper,
|
||||
IconButton,
|
||||
InputBase,
|
||||
SxProps,
|
||||
} from "@mui/material";
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import Icon from "@/components/Icon";
|
||||
import ArrowDropUpOutlinedIcon from '@mui/icons-material/ArrowDropUpOutlined';
|
||||
import { formatDate } from '@/common/utils'
|
||||
import { getDiscussions } from "@/api";
|
||||
|
||||
type User = {
|
||||
avatar_url: string
|
||||
login: string
|
||||
}
|
||||
|
||||
type Discussion = {
|
||||
id: string
|
||||
labels: { name: string, color: string }[]
|
||||
thumbs_up: number
|
||||
title: string
|
||||
url: string
|
||||
comment_count: number
|
||||
created_at: number
|
||||
author: User
|
||||
comment_users: User[]
|
||||
category: { name: string, emoji_html: string }
|
||||
is_answered: boolean
|
||||
upvote_count: number
|
||||
}
|
||||
|
||||
interface DiscussionListProps {
|
||||
value: Discussion[];
|
||||
}
|
||||
|
||||
export default function DiscussionList({ value }: DiscussionListProps) {
|
||||
const [discussionList, setDiscussionList] = useState<Array<Discussion>>(value || []);
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
|
||||
const handleSearch = async() => {
|
||||
const result = await getDiscussions(searchText)
|
||||
setDiscussionList(result || [])
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: any) => {
|
||||
if (event.key === 'Enter') {
|
||||
handleSearch();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
pb: 3,
|
||||
borderBottom: "1px solid #E3E8EF",
|
||||
}}
|
||||
>
|
||||
<Grid container>
|
||||
<Grid item xs={4} display="flex" alignItems="center">
|
||||
<Stack direction="row">
|
||||
<Typography variant="h6" sx={{ mr: 2 }}>Discussions</Typography>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={8}>
|
||||
<Stack direction="row" justifyContent={{ xs: "flex-start", md: "flex-end" }}>
|
||||
<Paper
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: 460,
|
||||
height: 36,
|
||||
boxShadow: "none",
|
||||
border: "1px solid #D9D9D9",
|
||||
}}
|
||||
>
|
||||
<IconButton sx={{ p: '10px' }} aria-label="menu" onClick={handleSearch}>
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
<InputBase
|
||||
sx={{ ml: 1, flex: 1 }}
|
||||
placeholder="搜索"
|
||||
inputProps={{ 'aria-label': 'search google maps' }}
|
||||
value={searchText}
|
||||
onChange={(e) =>setSearchText(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</Paper>
|
||||
<Button
|
||||
variant="contained"
|
||||
target="_blank"
|
||||
sx={{
|
||||
width: { xs: "102px" },
|
||||
ml: { xs: 2 },
|
||||
height: "36px",
|
||||
whiteSpace: 'nowrap',
|
||||
fontSize: { xs: '12px', md: '14px' }
|
||||
}}
|
||||
href="https://github.com/chaitin/SafeLine/discussions"
|
||||
>
|
||||
查看更多
|
||||
</Button>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
<TableContainer>
|
||||
<Table
|
||||
sx={{
|
||||
".MuiTableCell-root": {
|
||||
py: 2,
|
||||
color: "common.black",
|
||||
borderRight: "none",
|
||||
borderColor: "#E3E8EF",
|
||||
"&:first-of-type": {
|
||||
paddingLeft: "16px",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TableBody>
|
||||
{discussionList.map((discussion) => (
|
||||
<TableRow key={discussion.id}>
|
||||
<TableCell sx={{ width: "85%" }}>
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Box mr={2}>
|
||||
<Typography variant="subtitle2" sx={{ color: "error.main" }} display="flex" alignItems="center">
|
||||
<ArrowDropUpOutlinedIcon />
|
||||
{discussion.upvote_count}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
mr={3}
|
||||
sx={{
|
||||
width: "39px",
|
||||
height: "39px",
|
||||
background: "#F6F7F8",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
className="flex justify-center items-center"
|
||||
>
|
||||
<Box dangerouslySetInnerHTML={{ __html: discussion.category?.emoji_html }}></Box>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="h6">{discussion.title}</Typography>
|
||||
<Labels value={discussion.labels} />
|
||||
<Box>
|
||||
<Typography component="span" variant="subtitle2" sx={{ color: alpha('#000', 0.5) }}>
|
||||
{getDiscussionDesc(discussion)}
|
||||
</Typography>
|
||||
{isQA(discussion.category?.name) ? (
|
||||
<Typography
|
||||
component="span"
|
||||
variant="subtitle2"
|
||||
ml={1}
|
||||
sx={{ color: discussion.is_answered ? "primary.main" : alpha('#000', 0.5) }}
|
||||
>
|
||||
· {discussion.is_answered ? 'Answered' : 'Unanswered'}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell sx={{ width: "15%" }}>
|
||||
<Stack direction="row" alignItems="center">
|
||||
{renderCommentIcon(discussion)}
|
||||
<Typography component="span" variant="body1" sx={{ ml: 1 }} >
|
||||
{discussion.comment_count}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function isQA(name: string) {
|
||||
return name && name.includes('Q&A')
|
||||
}
|
||||
|
||||
function getDiscussionDesc(discussion: Discussion) {
|
||||
const { created_at, category, author } = discussion
|
||||
const time = formatDate(created_at)
|
||||
const action = isQA(category.name) ? 'asked' : 'started'
|
||||
const authorName = author?.login || ''
|
||||
return `${authorName} ${action} on ${time} in ${category.name}`
|
||||
}
|
||||
|
||||
const renderCommentIcon = ({ category, is_answered }: { category: { name: string }, is_answered: boolean }) => {
|
||||
if (isQA(category.name)) {
|
||||
if (is_answered) {
|
||||
return (<Icon type="icon-a-yiwanchengtianchong" color="#02BFA5" />)
|
||||
}
|
||||
return (<Icon type="icon-yiwancheng" color="common.black" />)
|
||||
}
|
||||
return (<Icon type="icon-pinglun" color="common.black" />)
|
||||
}
|
||||
|
||||
interface LabelsProps {
|
||||
value: { color: string, name: string }[];
|
||||
sx?: SxProps;
|
||||
}
|
||||
|
||||
export const Labels: React.FC<LabelsProps> = ({ value, sx }) => {
|
||||
if (!value || !value.length) return null
|
||||
|
||||
return (
|
||||
<Box sx={{ ...sx }}>
|
||||
{value.map((l, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
variant="filled"
|
||||
label={l.name}
|
||||
size="small"
|
||||
sx={{ backgroundColor: `#${l.color}`, color: "common.white", mr: 1, height: 16, fontSize: "12px" }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface AvatarsProps {
|
||||
value: User[];
|
||||
author: User,
|
||||
}
|
||||
|
||||
export const Avatars: React.FC<AvatarsProps> = ({ value }) => {
|
||||
const visibleAvatars = value.slice(0, 5)
|
||||
return (
|
||||
<AvatarGroup
|
||||
sx={{
|
||||
".MuiAvatar-root": {
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{visibleAvatars.map((avatar, index) => (
|
||||
<Avatar key={index} alt={avatar.login} src={avatar.avatar_url} />
|
||||
))}
|
||||
</AvatarGroup>
|
||||
);
|
||||
};
|
232
waf/src/components/community/IssueList.tsx
Normal file
|
@ -0,0 +1,232 @@
|
|||
import React, { useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
List,
|
||||
ListItem,
|
||||
Button,
|
||||
Stack,
|
||||
Typography,
|
||||
Grid,
|
||||
Paper,
|
||||
IconButton,
|
||||
InputBase,
|
||||
alpha,
|
||||
} from "@mui/material";
|
||||
import Icon from "@/components/Icon";
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import { Labels } from './DiscussionList'
|
||||
import { formatDate } from '@/common/utils'
|
||||
import { getIssues } from "@/api";
|
||||
|
||||
export default IssueList;
|
||||
|
||||
type Issue = {
|
||||
id: string
|
||||
labels: { name: string, color: string }[]
|
||||
thumbs_up: number
|
||||
title: string
|
||||
url: string
|
||||
comment_count: number
|
||||
created_at: number
|
||||
author: {
|
||||
avatar_url: string,
|
||||
login: string,
|
||||
}
|
||||
}
|
||||
interface IssueListProps {
|
||||
value: Issue[];
|
||||
}
|
||||
|
||||
const ROADMAP_TABS = [
|
||||
{ title: '正在考虑', key: 'inConsideration', color: '#FFBF00' },
|
||||
{ title: '进行中', key: 'inProgress', color: '#0FC6C2' },
|
||||
{ title: '最近完成', key: 'released', color: '#245CFF' },
|
||||
]
|
||||
|
||||
const isExistInLabels = (labels: Issue['labels'], label: string) => {
|
||||
return !!labels?.find((item: { name: string }) => item.name.includes(label))
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param issues
|
||||
* @returns
|
||||
* 正在考虑 = 带 enhancement,且没有 inprogress 和 released,且 open
|
||||
* 进行中 = 带 enhancement 和 inprogress,且 open
|
||||
* 最近完成 = 带 enhancement 和 released,且 open
|
||||
* 按点赞数量降序排序
|
||||
*/
|
||||
const handleSortIssues = (issues: Issue[]) => {
|
||||
const list = issues.filter((item: Issue) => isExistInLabels(item.labels, 'enhancement')).sort((item1, item2) => item2.thumbs_up - item1.thumbs_up)
|
||||
const inConsideration: Issue[] = []
|
||||
const inProgress: Issue[] = []
|
||||
const released: Issue[] = []
|
||||
list.forEach((item: Issue) => {
|
||||
const { labels } = item
|
||||
if (isExistInLabels(labels, 'inprogress')) {
|
||||
inProgress.push(item)
|
||||
} else if (isExistInLabels(labels, 'released')) {
|
||||
released.push(item)
|
||||
} else {
|
||||
inConsideration.push(item)
|
||||
}
|
||||
})
|
||||
return {
|
||||
inConsideration,
|
||||
inProgress,
|
||||
released,
|
||||
}
|
||||
}
|
||||
|
||||
function IssueList({ value }: IssueListProps) {
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [issues, setIssues] = useState<Record<string, Array<Issue>>>(handleSortIssues(value || []))
|
||||
|
||||
const handleSearch = async() => {
|
||||
const result = await getIssues(searchText)
|
||||
setIssues(handleSortIssues(result || []))
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: any) => {
|
||||
if (event.key === 'Enter') {
|
||||
handleSearch();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ pb: 3 }}>
|
||||
<Grid container>
|
||||
<Grid item xs={4} display="flex" alignItems="center">
|
||||
<Stack direction="row">
|
||||
<Typography variant="h6" sx={{ mr: 2 }}>Roadmap</Typography>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={8}>
|
||||
<Stack direction="row" justifyContent={{ xs: "flex-start", md: "flex-end" }}>
|
||||
<Paper
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: 460,
|
||||
height: 36,
|
||||
boxShadow: "none",
|
||||
border: "1px solid #D9D9D9",
|
||||
}}
|
||||
>
|
||||
<IconButton sx={{ p: '10px' }} aria-label="menu" onClick={handleSearch}>
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
<InputBase
|
||||
sx={{ ml: 1, flex: 1 }}
|
||||
placeholder="搜索"
|
||||
inputProps={{ 'aria-label': 'search google maps' }}
|
||||
value={searchText}
|
||||
onChange={(e) =>setSearchText(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</Paper>
|
||||
<Button
|
||||
variant="contained"
|
||||
target="_blank"
|
||||
sx={{
|
||||
width: { xs: "102px" },
|
||||
ml: { xs: 2 },
|
||||
height: "36px",
|
||||
whiteSpace: 'nowrap',
|
||||
fontSize: { xs: '12px', md: '14px' }
|
||||
}}
|
||||
href="https://github.com/chaitin/SafeLine/issues"
|
||||
>
|
||||
提交新反馈
|
||||
</Button>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
{ROADMAP_TABS.map((tab, index) => (
|
||||
<Box
|
||||
key={tab.key}
|
||||
sx={{
|
||||
width: { xs: "100%", sm: "32%" },
|
||||
height: { xs: "auto" },
|
||||
flexShrink: 0,
|
||||
borderRadius: "12px",
|
||||
mb: { xs: 3, md: 0 },
|
||||
backgroundColor: alpha(tab.color, 0.08),
|
||||
}}
|
||||
>
|
||||
<Box px={2} py={2}>
|
||||
<Typography variant="h6" color={tab.color}>{tab.title}</Typography>
|
||||
<List sx={{ py: 0, maxHeight: "830px", overflowY: "auto" }}>
|
||||
{issues[tab.key].map((issue) => (
|
||||
<ListItem key={issue.id} sx={{ pt: 2, pb: 0, px: 0 }}>
|
||||
<IssueItem issue={issue} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
interface IssueItemProps {
|
||||
issue: Issue;
|
||||
}
|
||||
|
||||
export const IssueItem: React.FC<IssueItemProps> = ({ issue }) => {
|
||||
if (!issue) return null
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: "common.white",
|
||||
borderRadius: "12px",
|
||||
px: "12px",
|
||||
py: "12px",
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">{issue.title}</Typography>
|
||||
<Labels value={issue.labels} />
|
||||
<Box mt={1}>
|
||||
<Typography component="span" variant="subtitle2" sx={{ color: alpha('#000', 0.5) }}>
|
||||
{getIssueDesc(issue)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack direction="row" alignItems="center" mt={1}>
|
||||
<Box mr={3}>
|
||||
<Icon type="icon-pinglun" color="common.black" sx={{ mr: 1 }} />
|
||||
<Typography component="span" variant="body1" sx={{}}>
|
||||
{issue.comment_count}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Icon type="icon-zan" color="warning.main" sx={{ mr: 1 }} />
|
||||
<Typography component="span" variant="body1" sx={{}}>
|
||||
{issue.thumbs_up}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
function getIssueDesc(issue: Issue) {
|
||||
const { created_at, author } = issue
|
||||
const time = formatDate(created_at)
|
||||
const authorName = author?.login || ''
|
||||
return `opened on ${time} by ${authorName}`
|
||||
}
|
164
waf/src/components/home/Abilities.tsx
Normal file
|
@ -0,0 +1,164 @@
|
|||
import React, { useState } from 'react'
|
||||
import { Grid, Box, Typography, Container, Link } from "@mui/material";
|
||||
import Image from 'next/image'
|
||||
import Icon from "@/components/Icon";
|
||||
|
||||
const ABILITY_LIST = [
|
||||
{
|
||||
title: "人机验证",
|
||||
href: "https://waf-ce.chaitin.cn/",
|
||||
img: "/images/ability/ability_verification.svg",
|
||||
},
|
||||
{
|
||||
title: "百川网站监控联动",
|
||||
href: "https://waf-ce.chaitin.cn/",
|
||||
img: "/images/ability/ability_rivers.svg",
|
||||
},
|
||||
{
|
||||
title: "APISIX 插件集成",
|
||||
href: "https://waf-ce.chaitin.cn/",
|
||||
img: "/images/ability/ability_apisix.svg",
|
||||
},
|
||||
{
|
||||
title: "长亭社区恶意 IP 情报",
|
||||
href: "https://waf-ce.chaitin.cn/",
|
||||
img: "/images/ability/ability_maliciousip.svg",
|
||||
},
|
||||
{
|
||||
title: "申请免费证书",
|
||||
href: "",
|
||||
img: "/images/ability/ability_cert.svg",
|
||||
},
|
||||
{
|
||||
title: "站点资源一览",
|
||||
href: "",
|
||||
img: "/images/ability/ability_asset.svg",
|
||||
},
|
||||
{
|
||||
title: "CC 攻击防护",
|
||||
href: "",
|
||||
img: "/images/ability/ability_CC.svg",
|
||||
},
|
||||
{
|
||||
title: "一键强制 HTTPS",
|
||||
href: "",
|
||||
img: "/images/ability/ability_HTTPS.svg",
|
||||
},
|
||||
];
|
||||
|
||||
const DEFAULT_URL = '/images/ability/ability_verification.svg';
|
||||
|
||||
const Abilities = () => {
|
||||
const [hoveredUrl, setHoveredUrl] = useState(DEFAULT_URL);
|
||||
|
||||
const handleIconHover = (url: string) => {
|
||||
setHoveredUrl(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
position={'relative'}
|
||||
sx={{
|
||||
background: "#111227",
|
||||
color: "common.white",
|
||||
pt: 18,
|
||||
pb: 27,
|
||||
px: 2,
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="lg">
|
||||
<Grid container alignItems="center">
|
||||
<Grid item xs={12} sm={12} md={6}>
|
||||
<Typography variant="h2" mb={4.5}>多维能力拓展</Typography>
|
||||
<Grid container spacing={2}>
|
||||
{ABILITY_LIST.map((ability) => (
|
||||
<AbilityItem key={ability.title} title={ability.title} img={ability.img} href={ability.href} handleIconHover={handleIconHover} />
|
||||
))}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={6}>
|
||||
<Box sx={{ width: { xs: "100%", sm: "100%" } }}>
|
||||
{ABILITY_LIST.map((ability) => (
|
||||
<Image
|
||||
key={ability.title}
|
||||
src={ability.img}
|
||||
alt={ability.title}
|
||||
layout="responsive"
|
||||
width={100}
|
||||
height={100}
|
||||
style={{ display: hoveredUrl === ability.img ? 'block' : 'none' }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Abilities;
|
||||
|
||||
interface ItemProps {
|
||||
title: string;
|
||||
href?: string;
|
||||
img?: string
|
||||
handleIconHover: Function
|
||||
}
|
||||
|
||||
const AbilityItem: React.FC<ItemProps> = ({ title, href, img, handleIconHover }) => {
|
||||
return (
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Box
|
||||
sx={{
|
||||
background: "#1B243E",
|
||||
boxShadow: "0px 4px 10px 0px rgba(3,13,23,0.6)",
|
||||
borderRadius: "12px",
|
||||
width: { xs: "100%", lg: "274px" },
|
||||
}}
|
||||
onMouseEnter={() => handleIconHover(img)}
|
||||
onMouseLeave={() => {}}
|
||||
onClick={() => handleIconHover(img)}
|
||||
>
|
||||
{href ? (
|
||||
<Link href={href} target="_blank" rel={title}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
px={3}
|
||||
py={3}
|
||||
sx={{
|
||||
fontSize: "20px",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
"&:hover": {
|
||||
backgroundColor: "primary.main",
|
||||
boxShadow: "0px 4px 10px 0px rgba(3,13,23,0.6)",
|
||||
borderRadius: "12px",
|
||||
color: "common.white",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
<Icon type="icon-youjiantouxian" color="common.white" />
|
||||
</Typography>
|
||||
</Link>
|
||||
) : (
|
||||
<Typography
|
||||
variant="h6"
|
||||
px={3}
|
||||
py={3}
|
||||
sx={{
|
||||
fontSize: "20px",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
);
|
||||
};
|
235
waf/src/components/home/Features.tsx
Normal file
|
@ -0,0 +1,235 @@
|
|||
import React from 'react'
|
||||
import { Grid, Box, Typography, List, ListItem, ListItemText, Button, alpha } from "@mui/material";
|
||||
import Image from 'next/image'
|
||||
|
||||
const FEATURE_LIST = [
|
||||
{
|
||||
title: "语义分析算法,有效保卫网站安全",
|
||||
desc: "首创语义分析算法,突破传统规则算法的极限,精准检测、低误报、难绕过",
|
||||
icon: "/images/feature1-icon.png",
|
||||
content: (
|
||||
<Grid container display={'flex'} justifyContent={'flex-end'} position={'relative'} sx={{ mb: 5 }}>
|
||||
<Grid item xs={0} md={2} sx={{ display: { xs: 'none', md: 'block' }, position: 'absolute', left: 0 }}>
|
||||
<Box>
|
||||
<Image
|
||||
src="/images/feature1-left.png"
|
||||
alt=""
|
||||
width={182}
|
||||
height={789}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4} ml={2} mt={14}>
|
||||
<List>
|
||||
<ListItem sx={{ mb: 1 }}>
|
||||
<ListItemText sx={{ textIndent: '-0.75rem' }} primary="· 国内首创、业内领先的智能语义分析算法" />
|
||||
</ListItem>
|
||||
<ListItem sx={{ mb: 1 }}>
|
||||
<ListItemText sx={{ textIndent: '-0.75rem' }} primary="· 基于代码的理解,防御 0day 攻击" />
|
||||
</ListItem>
|
||||
<ListItem sx={{ mb: 1 }}>
|
||||
<ListItemText sx={{ textIndent: '-0.75rem' }} primary="· 多维度 Web 应用防护" />
|
||||
</ListItem>
|
||||
</List>
|
||||
<Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
target="_blank"
|
||||
sx={{
|
||||
width: { xs: "100%", sm: "146px" },
|
||||
height: "50px",
|
||||
ml: { xs: 0, sm: 2 },
|
||||
mb: { xs: 2, sm: 0 },
|
||||
}}
|
||||
href="https://demo.waf-ce.chaitin.cn:9443/dashboard"
|
||||
>
|
||||
了解详情
|
||||
</Button>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6} mt={7}>
|
||||
<Box
|
||||
position={'relative'}
|
||||
>
|
||||
<Image
|
||||
src="/images/feature1-bg.png"
|
||||
alt=""
|
||||
layout="responsive"
|
||||
width={100}
|
||||
height={100}
|
||||
className='absolute left-1/2 top-0'
|
||||
/>
|
||||
<Image
|
||||
src="/images/feature1.svg"
|
||||
alt=""
|
||||
layout="responsive"
|
||||
width={100}
|
||||
height={100}
|
||||
className='relative'
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "化繁为简,智能安全",
|
||||
desc: "轻松上手,实现躺平式管理",
|
||||
icon: "/images/feature2-icon.png",
|
||||
content: (
|
||||
<Grid container position={'relative'} sx={{ mb: 5 }}>
|
||||
<Grid container>
|
||||
<Grid item xs={12} md={6} mt={7}>
|
||||
<Box
|
||||
position={'relative'}
|
||||
>
|
||||
<Image
|
||||
src="/images/feature2-bg.png"
|
||||
alt=""
|
||||
layout="responsive"
|
||||
width={100}
|
||||
height={100}
|
||||
className='absolute right-1/3'
|
||||
style={{ bottom: "10%" }}
|
||||
/>
|
||||
<Image
|
||||
src="/images/feature2.svg"
|
||||
alt=""
|
||||
layout="responsive"
|
||||
width={100}
|
||||
height={100}
|
||||
className='relative'
|
||||
style={{ right: "43px" }}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4} mt={14}>
|
||||
<List>
|
||||
<ListItem sx={{ mb: 1 }}>
|
||||
<ListItemText sx={{ textIndent: '-0.75rem' }} primary="· 一键安装,容器式管理,适配多种运行环境" />
|
||||
</ListItem>
|
||||
<ListItem sx={{ mb: 1 }}>
|
||||
<ListItemText sx={{ textIndent: '-0.75rem' }} primary="· 配置开箱即用,无需大量调整繁琐规则" />
|
||||
</ListItem>
|
||||
<ListItem sx={{ mb: 1 }}>
|
||||
<ListItemText sx={{ textIndent: '-0.75rem' }} primary="· 简洁操作,专为社区设计" />
|
||||
</ListItem>
|
||||
</List>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={0} md={2} sx={{ display: { xs: 'none', md: 'block' }, position: 'absolute', right: 0 }}>
|
||||
<Box>
|
||||
<Image
|
||||
src="/images/feature2-right.png"
|
||||
alt=""
|
||||
width={182}
|
||||
height={763}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "高性能、高并发、高可用性",
|
||||
desc: "无规则引擎,线性安全检测算法",
|
||||
icon: "/images/feature3-icon.png",
|
||||
content: (
|
||||
<Grid container display={'flex'} justifyContent={'flex-end'} position={'relative'} sx={{ mb: 10 }}>
|
||||
<Grid item xs={0} md={2} sx={{ display: { xs: 'none', md: 'block' }, position: 'absolute', left: 0 }}>
|
||||
<Box>
|
||||
<Image
|
||||
src="/images/feature3-left.png"
|
||||
alt=""
|
||||
width={182}
|
||||
height={649}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4} mt={14}>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemText sx={{ textIndent: '-0.75rem' }} primary="· 平均检测延迟 < 1 毫秒,单核轻松检测 2000+ TPS 并发" />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText sx={{ textIndent: '-0.75rem' }} primary="· 基于 Nginx 开发,完善的健康检查机制" />
|
||||
</ListItem>
|
||||
</List>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6} mt={7}>
|
||||
<Box
|
||||
position={'relative'}
|
||||
width={{ xs: "80%", md: "100%" }}
|
||||
>
|
||||
<Image
|
||||
src="/images/feature3-bg.png"
|
||||
alt=""
|
||||
width={1225}
|
||||
height={1310}
|
||||
className='absolute -left-1/2 -top-2/3'
|
||||
/>
|
||||
<Image
|
||||
src="/images/feature3.svg"
|
||||
alt=""
|
||||
layout="responsive"
|
||||
width={100}
|
||||
height={100}
|
||||
className='relative'
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const Features = () => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
color: "common.black",
|
||||
pb: 5,
|
||||
}}
|
||||
>
|
||||
<Grid container>
|
||||
{FEATURE_LIST.map((feature, index) => (
|
||||
<Grid item xs={12} key={feature.title}>
|
||||
<Box display={{ xs: "block", md: index % 2 == 1 ? 'flex' : ''}} alignItems={'flex-end'} flexDirection={index % 2 == 1 ? 'column' : 'row'}>
|
||||
<Box>
|
||||
<Image
|
||||
src={feature.icon}
|
||||
alt={feature.title}
|
||||
width={72}
|
||||
height={72}
|
||||
/>
|
||||
</Box>
|
||||
<Box ml={3}>
|
||||
<Typography variant="h4">
|
||||
<Box component="span" sx={{ mt: "5px" }}>
|
||||
{feature.title}
|
||||
</Box>
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ color: alpha("#000", 0.7), fontWeight: 100 }}
|
||||
mt={2}
|
||||
>
|
||||
<Box component="span">
|
||||
{feature.desc}
|
||||
</Box>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{ whiteSpace: "pre-line" }}
|
||||
>
|
||||
{feature.content}
|
||||
</Box>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Features;
|
124
waf/src/components/home/Partner.tsx
Normal file
|
@ -0,0 +1,124 @@
|
|||
import React from 'react'
|
||||
import { Box, Button, Container, Grid, Typography } from "@mui/material";
|
||||
import Image from 'next/image'
|
||||
import Icon from "@/components/Icon";
|
||||
|
||||
const PARTNER_LIST = [
|
||||
{
|
||||
title: "中国银行",
|
||||
icon: "boc.png",
|
||||
},
|
||||
{
|
||||
title: "交通银行",
|
||||
icon: "bcm.png",
|
||||
},
|
||||
{
|
||||
title: "滴滴出行",
|
||||
icon: "didi.png",
|
||||
},
|
||||
{
|
||||
title: "华为",
|
||||
icon: "huawei.png",
|
||||
},
|
||||
{
|
||||
title: "清华大学",
|
||||
icon: "thu.png",
|
||||
},
|
||||
{
|
||||
title: "bilibili",
|
||||
icon: "bilibili.png",
|
||||
},
|
||||
{
|
||||
title: "抖音",
|
||||
icon: "douyin.png",
|
||||
},
|
||||
{
|
||||
title: "爱奇艺",
|
||||
icon: "aqiyi.png",
|
||||
},
|
||||
{
|
||||
title: "顺丰快递",
|
||||
icon: "shunfeng.png",
|
||||
},
|
||||
{
|
||||
title: "中国南方航空",
|
||||
icon: "csn.png",
|
||||
},
|
||||
];
|
||||
|
||||
const ARTICLE_LIST = [
|
||||
"入围 Gartner 《Web 应用防火墙魔力象限》",
|
||||
"入围 Forrester 《NowTech:WebApplicationFirewalls,Q42019》"
|
||||
]
|
||||
|
||||
const Partner = () => {
|
||||
return (
|
||||
<Container>
|
||||
<Box
|
||||
sx={{
|
||||
px: 5,
|
||||
pt: 19,
|
||||
}}
|
||||
className="flex flex-col items-center"
|
||||
>
|
||||
<Typography variant="h2" mb={2}>
|
||||
优秀企业的信赖之选
|
||||
</Typography>
|
||||
{ARTICLE_LIST.map((item) => (
|
||||
<Typography
|
||||
key={item}
|
||||
variant='h5'
|
||||
sx={{ color: "#86909C", fontWeight: 500, wordBreak: "break-word" }}
|
||||
mt={2}
|
||||
>
|
||||
<Icon type="icon-jingxuan" className="mr-3 relative top-1" sx={{ fontSize: "1.3em" }} />
|
||||
{item}
|
||||
</Typography>
|
||||
))}
|
||||
<Grid container mt={4} spacing={3} display="flex" justifyContent="center">
|
||||
{PARTNER_LIST.map((item) => (
|
||||
<Grid item key={item.title} mb={1}>
|
||||
<Box
|
||||
sx={{
|
||||
width: { sx: "140px", md: "160px" },
|
||||
height: { sx: "auto", md: "95px" },
|
||||
backgroundColor: "common.white",
|
||||
boxShadow: "0px 8px 24px -4px rgba(145,158,171,0.1)",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
className="flex justify-center items-center"
|
||||
>
|
||||
<Box sx={{ width: "140px" }}>
|
||||
<Image
|
||||
src={`/images/logo/${item.icon}`}
|
||||
alt={item.title}
|
||||
layout="responsive"
|
||||
width={100}
|
||||
height={100}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
<Box mt={6}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
target="_blank"
|
||||
sx={{
|
||||
width: { xs: "100%", sm: "146px" },
|
||||
height: "50px",
|
||||
ml: { xs: 2, sm: 2 },
|
||||
mb: { xs: 2, sm: 0 },
|
||||
}}
|
||||
href="https://www.chaitin.cn/zh/"
|
||||
>
|
||||
了解详情
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Partner;
|
31
waf/src/components/home/WafTitle.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import React from "react";
|
||||
import Image from 'next/image'
|
||||
import { Typography, SxProps, Grid, Link } from "@mui/material";
|
||||
|
||||
interface TitleProps {
|
||||
title: string;
|
||||
sx?: SxProps;
|
||||
}
|
||||
|
||||
const Title: React.FC<TitleProps> = ({ title, sx }) => {
|
||||
return (
|
||||
<Link href="/">
|
||||
<Grid container flexDirection="row" display="flex" alignItems="center" sx={{ marginTop: 0 }}>
|
||||
<Image
|
||||
src="/images/safeline.svg"
|
||||
alt="Waf Logo"
|
||||
width={40}
|
||||
height={43}
|
||||
/>
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{ color: "common.white", fontFamily: "AlimamaShuHeiTi-Bold", ...sx }}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default Title;
|
26
waf/src/pages/_app.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import Head from 'next/head'
|
||||
import '/styles/globals.css'
|
||||
import NavBar from '@/components/NavBar'
|
||||
import Footer from '@/components/Footer'
|
||||
import CNZZScript from '@/components/CNZZ'
|
||||
import ThemeProvider from "@/components/Theme";
|
||||
|
||||
export default function MyApp({ Component, pageProps }: any) {
|
||||
return (
|
||||
<main className='overflow-x-hidden'>
|
||||
<Head>
|
||||
<title>长亭雷池 WAF 社区版</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="keywords" content="WAF,雷池,长亭,社区版,免费,开源,网站防护"></meta>
|
||||
<meta name="description" content="长亭雷池 WAF 社区版"></meta>
|
||||
</Head>
|
||||
<ThemeProvider>
|
||||
<NavBar />
|
||||
<Component {...pageProps} />
|
||||
<Footer />
|
||||
</ThemeProvider>
|
||||
<CNZZScript />
|
||||
</main>
|
||||
)
|
||||
}
|
138
waf/src/pages/community/index.tsx
Normal file
|
@ -0,0 +1,138 @@
|
|||
import React from "react";
|
||||
import { Box, Grid, Button, Typography, Container, Stack } from "@mui/material";
|
||||
import Image from 'next/image'
|
||||
import DiscussionList from '@/components/community/DiscussionList'
|
||||
import IssueList from '@/components/community/IssueList'
|
||||
import { getDiscussions, getIssues } from "@/api";
|
||||
|
||||
type CommunityPropsType = {
|
||||
discussions: any[];
|
||||
issues: any[];
|
||||
};
|
||||
|
||||
export async function getServerSideProps() {
|
||||
let discussions = []
|
||||
let issues = []
|
||||
let isClient = true
|
||||
if (typeof window === 'undefined') {
|
||||
isClient = false
|
||||
}
|
||||
try {
|
||||
discussions = await getDiscussions('', isClient);
|
||||
issues = await getIssues('', isClient);
|
||||
|
||||
} finally {
|
||||
return {
|
||||
props: {
|
||||
discussions: discussions || [],
|
||||
issues: issues || [],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Community({ discussions, issues }: CommunityPropsType) {
|
||||
return (
|
||||
<main title="社区 - 长亭雷池 WAF 社区版">
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "380px",
|
||||
backgroundImage: "url(/images/community-banner.png)",
|
||||
backgroundSize: "cover",
|
||||
position: 'relative',
|
||||
backgroundPosition: 'center center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}}
|
||||
>
|
||||
<Container>
|
||||
<Box pt={23}>
|
||||
<Typography variant="h2" sx={{ fontFamily: "AlimamaShuHeiTi-Bold" }}>通过社区获取更多帮助</Typography>
|
||||
<Typography variant="subtitle1" sx={{ opacity: 0.5 }} mt={1}>同样欢迎你的参与</Typography>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
<Container sx={{ pt: 6, mb: 18 }}>
|
||||
<DiscussionList value={discussions} />
|
||||
</Container>
|
||||
<Container>
|
||||
<IssueList value={issues} />
|
||||
</Container>
|
||||
<Container>
|
||||
<Box
|
||||
px={6}
|
||||
py={6}
|
||||
sx={{
|
||||
mt: 19,
|
||||
background: "#111227",
|
||||
borderRadius: "16px",
|
||||
}}
|
||||
className="flex flex-col justify-center"
|
||||
>
|
||||
<Grid container>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Stack sx={{ color: "common.white" }}>
|
||||
<Typography variant="h3" sx={{ fontSize: "36px" }}>绕过反馈</Typography>
|
||||
<Typography mt={1} variant="body1" sx={{ opacity: 0.5 }}>向 CT Stack 安全社区提交雷池 XSS、SQL 绕过,可获取积分奖励</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
target="_blank"
|
||||
sx={{
|
||||
width: { xs: "146px" },
|
||||
height: "50px",
|
||||
mt: 10,
|
||||
fontSize: "16px",
|
||||
backgroundColor: "common.white",
|
||||
}}
|
||||
href="https://stack.chaitin.com/security-challenge/safeline/index"
|
||||
>
|
||||
去提交
|
||||
</Button>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6} display="flex" justifyContent={{ sx: 'flex-start', md: 'flex-end' }} mt={{ xs: 2, md: 0 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: { xs: '100%', md: '367px' },
|
||||
height: { xs: 'auto', md: '206px' }
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src="/images/feedback.svg"
|
||||
alt=""
|
||||
layout="responsive"
|
||||
width={100}
|
||||
height={100}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Container>
|
||||
<Container>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundImage: "url(/images/partner-bg.svg)",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: 'center center',
|
||||
backgroundRepeat: 'no-repeat'
|
||||
}}
|
||||
>
|
||||
<Box textAlign="center" pt={13} pb={12}>
|
||||
<Image
|
||||
src="/images/wechat-230825.png"
|
||||
alt="wechat"
|
||||
width={300}
|
||||
height={300}
|
||||
/>
|
||||
<Typography variant="h4" mt={3}>微信讨论组</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default Community;
|
228
waf/src/pages/index.tsx
Normal file
|
@ -0,0 +1,228 @@
|
|||
import React, { useEffect, useRef } from "react";
|
||||
import { getSetupCount } from "@/api";
|
||||
import Features from "@/components/home/Features";
|
||||
import Abilities from "@/components/home/Abilities";
|
||||
import Partner from "@/components/home/Partner";
|
||||
import { Box, Grid, Button, Typography, Container, Stack } from "@mui/material";
|
||||
import Image from 'next/image'
|
||||
|
||||
const ARTICLES = [
|
||||
'《阮一峰·科技爱好者周刊》',
|
||||
'《Hello Github 月刊》',
|
||||
'《码农出击》',
|
||||
'《GitHub Daily》',
|
||||
'《Open Github 社区》',
|
||||
'《科技 lion》',
|
||||
]
|
||||
|
||||
const totalSx = {
|
||||
color: "primary.main",
|
||||
fontSize: { xs: "58px", md: "70px" },
|
||||
background: 'linear-gradient(90deg, #8FE5D7 0%, #0FC6C2 100%)',
|
||||
'-webkit-background-clip': 'text',
|
||||
'-webkit-text-fill-color': 'transparent',
|
||||
lineHeight: 1.25,
|
||||
fontFamily: "AlimamaShuHeiTi-Bold",
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const totalRef = useRef(null);
|
||||
|
||||
const initTotal = async (n: number) => {
|
||||
const countUpModule = await import("countup.js");
|
||||
const anim = new countUpModule.CountUp(totalRef.current!, Math.max(0, n), {
|
||||
duration: 2,
|
||||
});
|
||||
anim.start();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getSetupCount().then((d) => {
|
||||
initTotal(d.total);
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<main className="flex flex-col justify-between">
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "866px",
|
||||
backgroundImage: "url(/images/home-banner.png)",
|
||||
backgroundSize: "cover",
|
||||
position: 'relative',
|
||||
backgroundPosition: 'center center',
|
||||
backgroundRepeat: 'no-repeat'
|
||||
}}
|
||||
>
|
||||
<Box sx={{ position: 'absolute', bottom: 351, left: '50%', transform: 'translateX(-50%)' }}>
|
||||
<Box sx={{ width: "369px" }}>
|
||||
<Image
|
||||
src="/images/gif/waf-logo.gif"
|
||||
alt="WAf logo"
|
||||
layout="responsive"
|
||||
width={369}
|
||||
height={369}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ position: "relative", bottom: "360px", marginBottom: "-360px" }}>
|
||||
<Container>
|
||||
<Box
|
||||
sx={{ display: "flex", justifyContent: "center" }}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
target="_blank"
|
||||
sx={{
|
||||
width: { xs: "100%", sm: "188px" },
|
||||
height: "60px",
|
||||
ml: { xs: 0, sm: 0 },
|
||||
mb: { xs: 0, sm: 0 },
|
||||
fontSize: "20px",
|
||||
boxShadow: "0px 15px 25px 0px rgba(15,198,194,0.3)",
|
||||
}}
|
||||
href="https://waf-ce.chaitin.cn/docs/guide/install"
|
||||
>
|
||||
立即安装
|
||||
</Button>
|
||||
</Box>
|
||||
</Container>
|
||||
<Container>
|
||||
<Box mt={7.5}>
|
||||
<Grid container justifyContent="center">
|
||||
<Grid item xs={12} md={6}>
|
||||
<Stack spacing={2} alignItems="center">
|
||||
<Typography
|
||||
variant="h1"
|
||||
sx={{
|
||||
...totalSx,
|
||||
}}
|
||||
ref={totalRef}
|
||||
>
|
||||
-
|
||||
</Typography>
|
||||
<Typography variant="h5">
|
||||
装机量
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Stack direction="row" justifyContent="center">
|
||||
<Stack spacing={2} alignItems="center">
|
||||
<Typography
|
||||
variant="h1"
|
||||
sx={{
|
||||
...totalSx,
|
||||
}}
|
||||
>
|
||||
5.9k
|
||||
</Typography>
|
||||
<Typography variant="h5">
|
||||
GitHub Star
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Image
|
||||
src="/images/gif/starred.gif"
|
||||
alt="starred"
|
||||
width={50}
|
||||
height={49}
|
||||
style={{ marginTop: "16px" }}
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Container>
|
||||
<Container>
|
||||
<Box mt={7}>
|
||||
<Grid container spacing={2}>
|
||||
{ARTICLES.map((article, index) => (
|
||||
<Grid key={article} item xs={12} sm={4}>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
color: "#86909C",
|
||||
textAlign: { xs: 'center', md: index % 3 === 0 ? 'left' : 'right'},
|
||||
fontFamily: "AlimamaShuHeiTi-Bold",
|
||||
fontSize: "20px",
|
||||
}}
|
||||
>
|
||||
{article}
|
||||
</Typography>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
</Container>
|
||||
<Container sx={{ pb: 3, mb: 3, mt: 18 }}>
|
||||
<Features />
|
||||
</Container>
|
||||
<Abilities />
|
||||
<Box
|
||||
sx={{
|
||||
backgroundImage: "url(/images/partner-bg.svg)",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: 'center center',
|
||||
backgroundRepeat: 'no-repeat'
|
||||
}}
|
||||
>
|
||||
<Partner />
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
mt: 11,
|
||||
}}
|
||||
className="flex flex-col justify-center relative"
|
||||
>
|
||||
<Box>
|
||||
<Box className="relative top-1">
|
||||
<Image
|
||||
src="/images/enterprise-bg.svg"
|
||||
alt="雷池企业版"
|
||||
layout="responsive"
|
||||
width={100}
|
||||
height={100}
|
||||
/>
|
||||
</Box>
|
||||
<Container>
|
||||
<Stack className="absolute top-1/2" sx={{ transform: 'translateY(-40%)' }}>
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
fontWeight: 400,
|
||||
color: "common.white",
|
||||
fontSize: { xs: "16px", md: "28px" },
|
||||
fontFamily: "AlimamaShuHeiTi-Bold",
|
||||
letterSpacing: "3px",
|
||||
}}
|
||||
>欢迎使用雷池其他版本</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
sx={{
|
||||
width: { xs: "146px" },
|
||||
height: { xs:"38px", md: "50px" },
|
||||
mt: { xs: 1, md: 4 },
|
||||
backgroundColor: "common.white",
|
||||
fontSize: "16px",
|
||||
"&:hover": {
|
||||
color: "#0A8A87",
|
||||
backgroundColor: "common.white",
|
||||
},
|
||||
}}
|
||||
href="/version"
|
||||
>
|
||||
版本对比
|
||||
</Button>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</main>
|
||||
)
|
||||
}
|
33
waf/src/pages/version/index.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import React from "react";
|
||||
import { Box, Container, Stack, Typography } from "@mui/material";
|
||||
import Version from "@/components/Version";
|
||||
|
||||
export default function VersionView() {
|
||||
|
||||
return (
|
||||
<Box mb={26}>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "380px",
|
||||
backgroundImage: "url(/images/version-banner.png)",
|
||||
backgroundSize: "cover",
|
||||
position: 'relative',
|
||||
backgroundPosition: 'center center',
|
||||
backgroundRepeat: 'no-repeat'
|
||||
}}
|
||||
>
|
||||
<Container>
|
||||
<Box pt={23}>
|
||||
<Typography variant="h2" sx={{ fontFamily: "AlimamaShuHeiTi-Bold" }}>大小网站皆宜,免费即可开始</Typography>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
<Container>
|
||||
<Stack sx={{ pt: 20 }} spacing={3} alignItems="center">
|
||||
<Version />
|
||||
</Stack>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
74
waf/styles/globals.css
Normal file
|
@ -0,0 +1,74 @@
|
|||
/* @tailwind base; */
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
blockquote, q {
|
||||
quotes: none;
|
||||
}
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: '';
|
||||
content: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
:root {
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 214, 219, 220;
|
||||
--background-end-rgb: 255, 255, 255;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
}
|
||||
|
||||
a:-webkit-any-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'AlimamaShuHeiTi-Bold';
|
||||
src: url('/fonts/AlimamaShuHeiTi-Bold.otf') format('opentype');
|
||||
}
|
20
waf/tailwind.config.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import type { Config } from 'tailwindcss'
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
backgroundImage: {
|
||||
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
|
||||
'gradient-conic':
|
||||
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
export default config
|
27
waf/tsconfig.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"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": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
2929
waf/yarn.lock
Normal file
|
@ -92,6 +92,8 @@ docker compose up -d
|
|||
|
||||

|
||||
|
||||
<img src="/images/docs/guide_install/collie_apps.png" width="300px" />
|
||||
|
||||
参考视频教程 [用 “白嫖的云主机” 一键安装 “开源的 Web 防火墙”](https://www.bilibili.com/video/BV1sh4y1t7Pk/)
|
||||
|
||||
## 常见安装问题
|
||||
|
|