├── src
├── assets
│ ├── font
│ │ ├── iconfont.ttf
│ │ ├── iconfont.woff
│ │ ├── iconfont.woff2
│ │ └── iconfont.css
│ ├── img
│ │ ├── loading
│ │ │ └── 3.gif
│ │ ├── logo
│ │ │ └── favicon.ico
│ │ ├── wallpaper
│ │ │ ├── 2.jpeg
│ │ │ └── khadmv.webp
│ │ └── error
│ │ │ └── image-error.png
│ └── css
│ │ ├── tailwind.css
│ │ ├── content.scss
│ │ ├── normalize.css
│ │ └── base.css
├── utils
│ └── index.ts
├── vite-env.d.ts
├── App.vue
├── config
│ ├── index.ts
│ └── data.json
├── store
│ ├── index.ts
│ └── admin.ts
├── main.ts
├── router
│ └── index.ts
├── components
│ ├── index
│ │ ├── Background.vue
│ │ ├── Footer.vue
│ │ ├── LeftDrawer.vue
│ │ ├── Clock.vue
│ │ ├── Sidebar.vue
│ │ ├── Anchor.vue
│ │ ├── Head.vue
│ │ ├── Search.vue
│ │ └── Site.vue
│ └── admin
│ │ └── LoginDialog.vue
└── views
│ ├── AdminCallback.vue
│ ├── Index
│ └── index.vue
│ └── AdminDashboard.vue
├── postcss.config.js
├── vercel.json
├── tsconfig.node.json
├── .gitignore
├── api
├── tsconfig.json
├── auth
│ ├── callback.ts
│ └── github.ts
├── github
│ ├── get-file.ts
│ └── update-file.ts
└── favicon.ts
├── index.html
├── package.json
├── tsconfig.app.json
├── tailwind.utils.js
├── tsconfig.json
├── env.example
├── vite-plugin-favicon-api.ts
├── README.md
├── vite.config.ts
└── tailwind.config.js
/src/assets/font/iconfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xia-66/nav/HEAD/src/assets/font/iconfont.ttf
--------------------------------------------------------------------------------
/src/assets/img/loading/3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xia-66/nav/HEAD/src/assets/img/loading/3.gif
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | // 打开页面
2 | export function openUrl(url: string) {
3 | window.open(url)
4 | }
5 |
--------------------------------------------------------------------------------
/src/assets/font/iconfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xia-66/nav/HEAD/src/assets/font/iconfont.woff
--------------------------------------------------------------------------------
/src/assets/font/iconfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xia-66/nav/HEAD/src/assets/font/iconfont.woff2
--------------------------------------------------------------------------------
/src/assets/img/logo/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xia-66/nav/HEAD/src/assets/img/logo/favicon.ico
--------------------------------------------------------------------------------
/src/assets/img/wallpaper/2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xia-66/nav/HEAD/src/assets/img/wallpaper/2.jpeg
--------------------------------------------------------------------------------
/src/assets/img/error/image-error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xia-66/nav/HEAD/src/assets/img/error/image-error.png
--------------------------------------------------------------------------------
/src/assets/img/wallpaper/khadmv.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xia-66/nav/HEAD/src/assets/img/wallpaper/khadmv.webp
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/src/assets/css/tailwind.css:
--------------------------------------------------------------------------------
1 | /* 取消浏览器样式 */
2 | /* @tailwind base; */
3 | @tailwind base;
4 | @tailwind components;
5 | @tailwind utilities;
6 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare module '*.vue' {
3 | import { DefineComponent } from "vue"
4 | const component: DefineComponent<{}, {}, any>
5 | export default component
6 | }
7 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "buildCommand": "npm run build",
3 | "outputDirectory": "dist",
4 | "framework": "vite",
5 | "rewrites": [
6 | {
7 | "source": "/api/(.*)",
8 | "destination": "/api/$1"
9 | },
10 | {
11 | "source": "/(.*)",
12 | "destination": "/index.html"
13 | }
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
5 | "skipLibCheck": true,
6 | "module": "ESNext",
7 | "moduleResolution": "bundler",
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "noEmit": true
11 | },
12 | "include": ["vite.config.ts", "vite-plugin-*.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | .env
26 | .env.local
27 | .env.development
28 | .env.production
--------------------------------------------------------------------------------
/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "ESNext",
5 | "moduleResolution": "bundler",
6 | "lib": ["ES2022"],
7 | "esModuleInterop": true,
8 | "skipLibCheck": true,
9 | "strict": true,
10 | "resolveJsonModule": true,
11 | "isolatedModules": true,
12 | "noEmit": true
13 | },
14 | "include": ["*.ts"],
15 | "exclude": ["node_modules"]
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 黑羽导航
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/config/index.ts:
--------------------------------------------------------------------------------
1 | import * as pkg from '../../package.json'
2 |
3 | // ==================== 网站基本信息 ====================
4 | // 网站名称
5 | export const SITE_NAME = '黑羽导航'
6 |
7 | // 备案号
8 | export const ICP_NUMBER = ''
9 |
10 | // ==================== 版本信息 ====================
11 | // 项目版本号
12 | export const RELEASE = `v${pkg.version}`
13 |
14 | // ==================== API 配置 ====================
15 | // 获取网站Favicon接口
16 | // 开发环境通过 Vite 插件模拟,生产环境使用 Vercel Serverless Function
17 | export const Favicon = '/api/favicon?url='
18 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 |
3 | export const useMainStore = defineStore('mainStore', {
4 | state: () => {
5 | return {
6 | isShowDrawer: false,
7 | site: [],
8 | menu: [
9 | {
10 | index: 1,
11 | name: '首页',
12 | iconClass: 'iconfont icon-md-home'
13 | },
14 | {
15 | index: 2,
16 | name: '更新日志',
17 | iconClass: 'iconfont icon-iconset0278'
18 | },
19 | {
20 | index: 3,
21 | name: '关于我们',
22 | iconClass: 'iconfont icon-iconset0156'
23 | }
24 | ]
25 | }
26 | },
27 | getters: {},
28 | actions: {},
29 |
30 | persist: true
31 | })
32 |
--------------------------------------------------------------------------------
/api/auth/callback.ts:
--------------------------------------------------------------------------------
1 | import { VercelRequest, VercelResponse } from '@vercel/node';
2 |
3 | /**
4 | * GitHub OAuth 回调处理
5 | * 将 GitHub 的回调重定向到前端的 hash 路由
6 | */
7 | export default async function handler(req: VercelRequest, res: VercelResponse) {
8 | const { code, error, error_description } = req.query;
9 |
10 | // 如果有错误,重定向到首页并提示错误
11 | if (error) {
12 | const errorMsg = error_description || error || '授权失败';
13 | return res.redirect(`/#/index?error=${encodeURIComponent(errorMsg as string)}`);
14 | }
15 |
16 | // 如果没有 code,返回错误
17 | if (!code) {
18 | return res.redirect('/#/index?error=缺少授权码');
19 | }
20 |
21 | // 重定向到前端的 callback 页面(hash 路由)
22 | return res.redirect(`/#/admin/callback?code=${code}`);
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/src/assets/css/content.scss:
--------------------------------------------------------------------------------
1 | body {
2 | font-size: 14px;
3 | color: var(--gray-600);
4 | }
5 |
6 | a {
7 | text-decoration: none;
8 | }
9 |
10 | li {
11 | list-style-type: none;
12 | }
13 |
14 | // 鼠标小手
15 | .pointer {
16 | cursor: pointer;
17 | }
18 |
19 | // 文字溢出处理
20 | .text {
21 | white-space: nowrap;
22 | overflow: hidden;
23 | text-overflow: ellipsis;
24 | word-break: break-all;
25 | }
26 |
27 | // 右键菜单选择颜色
28 | .hs-right-menu-shadow {
29 | box-shadow: 0 0px 0px 2px var(--red-400), 0px 0px 0px 4px var(--blue-400) !important;
30 | }
31 |
32 | // 继承主题样式
33 | .inherit-theme {
34 | .inherit-text {
35 | color: inherit !important;
36 | }
37 | .inherit-bg {
38 | background-color: inherit !important;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import App from './App.vue'
3 | import ElementPlus from 'element-plus'
4 | import 'element-plus/dist/index.css'
5 | import '@/assets/css/tailwind.css'
6 | import '@/assets/css/normalize.css'
7 | import '@/assets/font/iconfont.css'
8 | import '@/assets/css/base.css'
9 | import '@/assets/css/content.scss'
10 | import router from "./router";
11 |
12 | import { createPinia } from "pinia";
13 | import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
14 |
15 | const pinia = createPinia();
16 | pinia.use(piniaPluginPersistedstate)
17 | createApp(App).use(router).use(ElementPlus).use(pinia).mount('#app')
18 | // router.beforeEach((to, from, next) => {
19 | // if (to.meta.title) {
20 | // document.title = to.meta.title;
21 | // }
22 | // next();
23 | // });
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "heiyu-nav",
3 | "private": true,
4 | "version": "2.2.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite --mode dev --open",
8 | "build": "vite build --mode prod",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "element-plus": "^2.7.6",
13 | "pinia": "^2.1.7",
14 | "pinia-plugin-persistedstate": "^3.2.1",
15 | "vue": "^3.4.29",
16 | "vue-router": "^4.4.0"
17 | },
18 | "devDependencies": {
19 | "@types/node": "^20.14.9",
20 | "@vercel/node": "^5.5.3",
21 | "@vitejs/plugin-vue": "^5.0.5",
22 | "autoprefixer": "^10.4.19",
23 | "postcss": "^8.4.39",
24 | "sass": "^1.86.3",
25 | "tailwindcss": "^3.4.4",
26 | "typescript": "^5.2.2",
27 | "vite": "^5.3.1",
28 | "vue-tsc": "^2.0.21"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
5 | "target": "ES2020",
6 | "useDefineForClassFields": true,
7 | "module": "ESNext",
8 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
9 | "skipLibCheck": true,
10 |
11 | /* Bundler mode */
12 | "moduleResolution": "bundler",
13 | "allowImportingTsExtensions": true,
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "moduleDetection": "force",
17 | "noEmit": true,
18 | "jsx": "preserve",
19 |
20 | /* Linting */
21 | "strict": true,
22 | "noUnusedLocals": true,
23 | "noUnusedParameters": true,
24 | "noFallthroughCasesInSwitch": true
25 | },
26 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
27 | }
28 |
--------------------------------------------------------------------------------
/tailwind.utils.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | /**
4 | * 注入精细化变量:
5 | * top-px-98
6 | * -top-px-98
7 | * p-px-50
8 | * -p-px-50
9 | * m-px-50
10 | * -m-px-50
11 | * w-px-50
12 | * -w-px-50
13 | * h-px-50
14 | * -h-px-50
15 | * @returns Object
16 | */
17 |
18 | /**
19 | * // 构建px的尺寸
20 | * @param {Number} range 生成范围
21 | * @param {String} tag 正数 | 负数
22 | * @returns
23 | */
24 | function createPX(range = 400, tag) {
25 | let fullPxs = {};
26 | new Array(range).fill(undefined).forEach((item, index) => {
27 | if (index % 2 === 0 || index % 5 === 0) {
28 | if (tag === 'negative') {
29 | // 负数
30 | fullPxs[`-px-${index}`] = `-${index}px`;
31 | } else {
32 | // 正数
33 | fullPxs[`px-${index}`] = `${index}px`;
34 | }
35 | }
36 | });
37 | return fullPxs;
38 | }
39 |
40 | module.exports = { createPX };
41 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "skipLibCheck": true,
8 |
9 | /* 解决找不到vue模块的问题 */
10 | "moduleResolution": "node",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "allowSyntheticDefaultImports": true,
14 | "isolatedModules": true,
15 | "noEmit": true,
16 | "jsx": "preserve",
17 |
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true,
22 |
23 | /* 使用@映射至src目录 */
24 | "paths": {
25 | "@/*": ["./src/*"]
26 | }
27 | },
28 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "src/utils/index.ts"],
29 | "references": [{ "path": "./tsconfig.node.json" }]
30 | }
31 |
--------------------------------------------------------------------------------
/env.example:
--------------------------------------------------------------------------------
1 | # GitHub OAuth 配置
2 | # 在 https://github.com/settings/developers 创建 OAuth App 获取
3 | GITHUB_CLIENT_ID=your_github_client_id_here
4 | GITHUB_CLIENT_SECRET=your_github_client_secret_here
5 |
6 | # GitHub 仓库配置
7 | # 格式:owner/repo(例如:xia-66/nav)
8 | GITHUB_REPO_OWNER=xia-66
9 | GITHUB_REPO_NAME=nav
10 |
11 | # 允许访问后台的 GitHub 用户名(多个用户用逗号分隔)
12 | GITHUB_ALLOWED_USERS=xia-66
13 |
14 | # 说明:
15 | # 1. VITE_GITHUB_CLIENT_ID - GitHub OAuth App 的 Client ID(前端使用)
16 | # 2. GITHUB_CLIENT_SECRET - GitHub OAuth App 的 Client Secret(后端使用,不要泄露)
17 | # 3. GITHUB_REPO_OWNER - 仓库所有者(你的 GitHub 用户名)
18 | # 4. GITHUB_REPO_NAME - 仓库名称
19 | # 5. GITHUB_ALLOWED_USERS - 允许登录后台的用户名
20 | #
21 | # OAuth App 配置:
22 | # - Application name: 任意名称(例如:黑羽导航管理后台)
23 | # - Homepage URL: https://yourdomain.com
24 | # - Authorization callback URL: https://yourdomain.com/api/auth/callback
25 | #
26 | # 需要的权限(Scope):
27 | # - repo(用于读写仓库文件)
28 |
29 |
--------------------------------------------------------------------------------
/src/router/index.ts:
--------------------------------------------------------------------------------
1 | import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
2 | import { SITE_NAME } from '@/config'
3 |
4 | const routes: Array = [
5 | {
6 | path: '/',
7 | redirect: '/index'
8 | },
9 | {
10 | path: '/index',
11 | name: 'Index',
12 | component: () => import('@/views/Index/index.vue'),
13 | meta: { title: SITE_NAME }
14 | },
15 | {
16 | path: '/admin/callback',
17 | name: 'AdminCallback',
18 | component: () => import('@/views/AdminCallback.vue'),
19 | meta: { title: '授权回调 - ' + SITE_NAME }
20 | },
21 | {
22 | path: '/admin/dashboard',
23 | name: 'AdminDashboard',
24 | component: () => import('@/views/AdminDashboard.vue'),
25 | meta: { title: '后台管理 - ' + SITE_NAME }
26 | }
27 | ]
28 |
29 | const router = createRouter({
30 | history: createWebHashHistory(),
31 | routes
32 | })
33 |
34 | export default router
35 |
--------------------------------------------------------------------------------
/src/components/index/Background.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
51 |
--------------------------------------------------------------------------------
/src/views/AdminCallback.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
40 |
41 |
46 |
47 |
--------------------------------------------------------------------------------
/src/components/index/Footer.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
26 |
27 |
48 |
--------------------------------------------------------------------------------
/api/github/get-file.ts:
--------------------------------------------------------------------------------
1 | import { VercelRequest, VercelResponse } from '@vercel/node';
2 |
3 | interface GitHubFileResponse {
4 | content: string;
5 | sha: string;
6 | }
7 |
8 | const REPO_OWNER = process.env.GITHUB_REPO_OWNER || 'xia-66';
9 | const REPO_NAME = process.env.GITHUB_REPO_NAME || 'nav';
10 | const FILE_PATH = 'src/config/data.json';
11 |
12 | export default async function handler(req: VercelRequest, res: VercelResponse) {
13 | const { token } = req.query;
14 |
15 | if (!token || typeof token !== 'string') {
16 | return res.status(401).json({ error: '未授权' });
17 | }
18 |
19 | try {
20 | // 获取文件内容和 SHA
21 | const response = await fetch(
22 | `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/contents/${FILE_PATH}`,
23 | {
24 | headers: {
25 | 'Authorization': `token ${token}`,
26 | 'Accept': 'application/vnd.github.v3+json',
27 | },
28 | }
29 | );
30 |
31 | if (!response.ok) {
32 | throw new Error('获取文件失败');
33 | }
34 |
35 | const data = await response.json() as GitHubFileResponse;
36 |
37 | // 解码 Base64 内容
38 | const content = Buffer.from(data.content, 'base64').toString('utf-8');
39 |
40 | return res.status(200).json({
41 | content: JSON.parse(content),
42 | sha: data.sha,
43 | });
44 | } catch (error) {
45 | console.error('Get File Error:', error);
46 | return res.status(500).json({ error: '获取文件失败' });
47 | }
48 | }
49 |
50 |
--------------------------------------------------------------------------------
/src/components/index/LeftDrawer.vue:
--------------------------------------------------------------------------------
1 |
2 |
4 | {{ SITE_NAME }}
5 |
6 | {{ category.name }}
8 |
9 |
10 |
11 |
38 |
--------------------------------------------------------------------------------
/src/components/index/Clock.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ hours }}
5 |
时
6 |
{{ minutes }}
7 |
分
8 |
{{ seconds }}
9 |
秒
10 |
11 |
12 |
13 |
14 |
34 |
35 |
--------------------------------------------------------------------------------
/src/views/Index/index.vue:
--------------------------------------------------------------------------------
1 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
75 |
--------------------------------------------------------------------------------
/src/components/index/Sidebar.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
60 |
61 |
85 |
--------------------------------------------------------------------------------
/src/components/index/Anchor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | -
7 |
{{ category.name }}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
45 |
46 |
83 |
--------------------------------------------------------------------------------
/api/auth/github.ts:
--------------------------------------------------------------------------------
1 | import { VercelRequest, VercelResponse } from '@vercel/node';
2 |
3 | interface GitHubTokenResponse {
4 | access_token?: string;
5 | error?: string;
6 | error_description?: string;
7 | }
8 |
9 | interface GitHubUser {
10 | login: string;
11 | name: string;
12 | avatar_url: string;
13 | }
14 |
15 | const ALLOWED_USERS = (process.env.GITHUB_ALLOWED_USERS || 'xia-66').split(',').map(u => u.trim());
16 |
17 | export default async function handler(req: VercelRequest, res: VercelResponse) {
18 | const { code } = req.query;
19 |
20 | if (!code) {
21 | return res.status(400).json({ error: '缺少授权码' });
22 | }
23 |
24 | const clientId = process.env.GITHUB_CLIENT_ID;
25 | const clientSecret = process.env.GITHUB_CLIENT_SECRET;
26 |
27 | if (!clientId || !clientSecret) {
28 | return res.status(500).json({ error: '服务器配置错误' });
29 | }
30 |
31 | try {
32 | // 1. 使用 code 换取 access_token
33 | const tokenResponse = await fetch('https://github.com/login/oauth/access_token', {
34 | method: 'POST',
35 | headers: {
36 | 'Content-Type': 'application/json',
37 | 'Accept': 'application/json',
38 | },
39 | body: JSON.stringify({
40 | client_id: clientId,
41 | client_secret: clientSecret,
42 | code: code,
43 | }),
44 | });
45 |
46 | const tokenData = await tokenResponse.json() as GitHubTokenResponse;
47 |
48 | if (tokenData.error) {
49 | return res.status(400).json({ error: tokenData.error_description || '授权失败' });
50 | }
51 |
52 | const accessToken = tokenData.access_token;
53 |
54 | // 2. 获取用户信息
55 | const userResponse = await fetch('https://api.github.com/user', {
56 | headers: {
57 | 'Authorization': `token ${accessToken}`,
58 | 'Accept': 'application/vnd.github.v3+json',
59 | },
60 | });
61 |
62 | const userData = await userResponse.json() as GitHubUser;
63 |
64 | // 3. 验证用户是否在允许列表中
65 | if (!ALLOWED_USERS.includes(userData.login)) {
66 | return res.status(403).json({ error: `无权限访问,仅允许以下用户: ${ALLOWED_USERS.join(', ')}` });
67 | }
68 |
69 | // 4. 返回 token 和用户信息
70 | return res.status(200).json({
71 | token: accessToken,
72 | user: {
73 | login: userData.login,
74 | name: userData.name,
75 | avatar_url: userData.avatar_url,
76 | },
77 | });
78 | } catch (error) {
79 | console.error('GitHub OAuth Error:', error);
80 | return res.status(500).json({ error: '服务器内部错误' });
81 | }
82 | }
83 |
84 |
--------------------------------------------------------------------------------
/api/github/update-file.ts:
--------------------------------------------------------------------------------
1 | import { VercelRequest, VercelResponse } from '@vercel/node';
2 |
3 | interface GitHubUser {
4 | login: string;
5 | }
6 |
7 | interface GitHubErrorResponse {
8 | message?: string;
9 | }
10 |
11 | interface GitHubUpdateResponse {
12 | commit: {
13 | sha: string;
14 | url: string;
15 | };
16 | }
17 |
18 | const REPO_OWNER = process.env.GITHUB_REPO_OWNER || 'xia-66';
19 | const REPO_NAME = process.env.GITHUB_REPO_NAME || 'nav';
20 | const FILE_PATH = 'src/config/data.json';
21 | const ALLOWED_USERS = (process.env.GITHUB_ALLOWED_USERS || 'xia-66').split(',').map(u => u.trim());
22 |
23 | export default async function handler(req: VercelRequest, res: VercelResponse) {
24 | if (req.method !== 'POST') {
25 | return res.status(405).json({ error: '方法不允许' });
26 | }
27 |
28 | const { token, content, sha, message } = req.body;
29 |
30 | if (!token || !content || !sha) {
31 | return res.status(400).json({ error: '缺少必要参数' });
32 | }
33 |
34 | try {
35 | // 验证用户身份
36 | const userResponse = await fetch('https://api.github.com/user', {
37 | headers: {
38 | 'Authorization': `token ${token}`,
39 | 'Accept': 'application/vnd.github.v3+json',
40 | },
41 | });
42 |
43 | const userData = await userResponse.json() as GitHubUser;
44 |
45 | if (!ALLOWED_USERS.includes(userData.login)) {
46 | return res.status(403).json({ error: `无权限修改,仅允许以下用户: ${ALLOWED_USERS.join(', ')}` });
47 | }
48 |
49 | // 更新文件
50 | const updateResponse = await fetch(
51 | `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/contents/${FILE_PATH}`,
52 | {
53 | method: 'PUT',
54 | headers: {
55 | 'Authorization': `token ${token}`,
56 | 'Accept': 'application/vnd.github.v3+json',
57 | 'Content-Type': 'application/json',
58 | },
59 | body: JSON.stringify({
60 | message: message || '更新导航数据',
61 | content: Buffer.from(JSON.stringify(content, null, 2)).toString('base64'),
62 | sha: sha,
63 | }),
64 | }
65 | );
66 |
67 | if (!updateResponse.ok) {
68 | const errorData = await updateResponse.json() as GitHubErrorResponse;
69 | throw new Error(errorData.message || '更新文件失败');
70 | }
71 |
72 | const result = await updateResponse.json() as GitHubUpdateResponse;
73 |
74 | return res.status(200).json({
75 | success: true,
76 | commit: result.commit,
77 | });
78 | } catch (error: any) {
79 | console.error('Update File Error:', error);
80 | return res.status(500).json({ error: error.message || '更新文件失败' });
81 | }
82 | }
83 |
84 |
--------------------------------------------------------------------------------
/src/assets/css/normalize.css:
--------------------------------------------------------------------------------
1 | article,
2 | aside,
3 | details,
4 | figcaption,
5 | figure,
6 | footer,
7 | header,
8 | hgroup,
9 | main,
10 | nav,
11 | section,
12 | summary {
13 | display: block;
14 | }
15 |
16 | audio,
17 | canvas,
18 | video {
19 | display: inline-block;
20 | }
21 |
22 | audio:not([controls]) {
23 | display: none;
24 | height: 0;
25 | }
26 |
27 | [hidden],
28 | template {
29 | display: none;
30 | }
31 |
32 | html {
33 | font-family: sans-serif; /* 1 */
34 | -ms-text-size-adjust: 100%; /* 2 */
35 | -webkit-text-size-adjust: 100%; /* 2 */
36 | }
37 |
38 | body {
39 | margin: 0;
40 | }
41 |
42 | a {
43 | background: transparent;
44 | }
45 |
46 | a:focus {
47 | outline: none;
48 | }
49 |
50 | a:active,
51 | a:hover {
52 | outline: 0;
53 | }
54 |
55 | h1 {
56 | font-size: 2em;
57 | margin: 0.67em 0;
58 | }
59 |
60 | abbr[title] {
61 | border-bottom: 1px dotted;
62 | }
63 |
64 | b,
65 | strong {
66 | font-weight: bold;
67 | }
68 |
69 | dfn {
70 | font-style: italic;
71 | }
72 |
73 | hr {
74 | -moz-box-sizing: content-box;
75 | box-sizing: content-box;
76 | height: 0;
77 | }
78 |
79 | mark {
80 | background: #ff0;
81 | color: #000;
82 | }
83 |
84 | code,
85 | kbd,
86 | pre,
87 | samp {
88 | font-family: monospace, serif;
89 | font-size: 1em;
90 | }
91 |
92 | pre {
93 | white-space: pre-wrap;
94 | }
95 |
96 | q {
97 | quotes: "\201C""\201D""\2018""\2019";
98 | }
99 |
100 | small {
101 | font-size: 80%;
102 | }
103 |
104 | sub,
105 | sup {
106 | font-size: 75%;
107 | line-height: 0;
108 | position: relative;
109 | vertical-align: baseline;
110 | }
111 |
112 | sup {
113 | top: -0.5em;
114 | }
115 |
116 | sub {
117 | bottom: -0.25em;
118 | }
119 |
120 | img {
121 | border: 0;
122 | }
123 |
124 | svg:not(:root) {
125 | overflow: hidden;
126 | }
127 |
128 | figure {
129 | margin: 0;
130 | }
131 |
132 | fieldset {
133 | border: 1px solid #c0c0c0;
134 | margin: 0 2px;
135 | padding: 0.35em 0.625em 0.75em;
136 | }
137 |
138 | legend {
139 | border: 0; /* 1 */
140 | padding: 0; /* 2 */
141 | }
142 |
143 | button,
144 | input,
145 | select,
146 | textarea {
147 | font-family: inherit; /* 1 */
148 | font-size: 100%; /* 2 */
149 | margin: 0; /* 3 */
150 | }
151 |
152 | button,
153 | input {
154 | line-height: normal;
155 | }
156 |
157 | button,
158 | select {
159 | text-transform: none;
160 | }
161 |
162 | button,
163 | html input[type="button"], /* 1 */
164 | input[type="reset"],
165 | input[type="submit"] {
166 | -webkit-appearance: button; /* 2 */
167 | cursor: pointer; /* 3 */
168 | }
169 |
170 | button[disabled],
171 | html input[disabled] {
172 | cursor: default;
173 | }
174 |
175 | input[type="checkbox"],
176 | input[type="radio"] {
177 | box-sizing: border-box; /* 1 */
178 | padding: 0; /* 2 */
179 | }
180 |
181 | input[type="search"] {
182 | -webkit-appearance: textfield; /* 1 */
183 | -moz-box-sizing: content-box;
184 | -webkit-box-sizing: content-box; /* 2 */
185 | box-sizing: content-box;
186 | }
187 |
188 | input[type="search"]::-webkit-search-cancel-button,
189 | input[type="search"]::-webkit-search-decoration {
190 | -webkit-appearance: none;
191 | }
192 |
193 | button::-moz-focus-inner,
194 | input::-moz-focus-inner {
195 | border: 0;
196 | padding: 0;
197 | }
198 |
199 | textarea {
200 | overflow: auto; /* 1 */
201 | vertical-align: top; /* 2 */
202 | }
203 |
204 | table {
205 | border-collapse: collapse;
206 | border-spacing: 0;
207 | }
208 |
--------------------------------------------------------------------------------
/src/store/admin.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia';
2 | import { ref } from 'vue';
3 |
4 | interface User {
5 | login: string;
6 | name: string;
7 | avatar_url: string;
8 | }
9 |
10 | export const useAdminStore = defineStore('admin', () => {
11 | const token = ref(localStorage.getItem('github_token'));
12 | const user = ref(
13 | localStorage.getItem('github_user')
14 | ? JSON.parse(localStorage.getItem('github_user')!)
15 | : null
16 | );
17 | const isAuthenticated = ref(!!token.value);
18 |
19 | // 设置认证信息
20 | const setAuth = (newToken: string, newUser: User) => {
21 | token.value = newToken;
22 | user.value = newUser;
23 | isAuthenticated.value = true;
24 | localStorage.setItem('github_token', newToken);
25 | localStorage.setItem('github_user', JSON.stringify(newUser));
26 | };
27 |
28 | // 清除认证信息
29 | const clearAuth = () => {
30 | token.value = null;
31 | user.value = null;
32 | isAuthenticated.value = false;
33 | localStorage.removeItem('github_token');
34 | localStorage.removeItem('github_user');
35 | };
36 |
37 | // GitHub OAuth 登录
38 | const loginWithGitHub = () => {
39 | const clientId = import.meta.env.VITE_GITHUB_CLIENT_ID;
40 | // 注意:这里使用 /api/auth/callback 作为回调地址(没有 #)
41 | // 服务端会重定向到 /#/admin/callback
42 | const redirectUri = `${window.location.origin}/api/auth/callback`;
43 | const scope = 'repo';
44 |
45 | window.location.href = `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}`;
46 | };
47 |
48 | // 处理 OAuth 回调
49 | const handleCallback = async (code: string) => {
50 | try {
51 | const response = await fetch(`/api/auth/github?code=${code}`);
52 | const data = await response.json();
53 |
54 | if (!response.ok) {
55 | throw new Error(data.error || '登录失败');
56 | }
57 |
58 | setAuth(data.token, data.user);
59 | return { success: true };
60 | } catch (error: any) {
61 | return { success: false, error: error.message };
62 | }
63 | };
64 |
65 | // 获取文件内容
66 | const getFileContent = async () => {
67 | if (!token.value) {
68 | throw new Error('未授权');
69 | }
70 |
71 | try {
72 | const response = await fetch(`/api/github/get-file?token=${token.value}`);
73 | const data = await response.json();
74 |
75 | if (!response.ok) {
76 | throw new Error(data.error || '获取文件失败');
77 | }
78 |
79 | return data;
80 | } catch (error: any) {
81 | throw new Error(error.message || '获取文件失败');
82 | }
83 | };
84 |
85 | // 更新文件内容
86 | const updateFileContent = async (content: any, sha: string, message?: string) => {
87 | if (!token.value) {
88 | throw new Error('未授权');
89 | }
90 |
91 | try {
92 | const response = await fetch('/api/github/update-file', {
93 | method: 'POST',
94 | headers: {
95 | 'Content-Type': 'application/json',
96 | },
97 | body: JSON.stringify({
98 | token: token.value,
99 | content,
100 | sha,
101 | message,
102 | }),
103 | });
104 |
105 | const data = await response.json();
106 |
107 | if (!response.ok) {
108 | throw new Error(data.error || '更新文件失败');
109 | }
110 |
111 | return data;
112 | } catch (error: any) {
113 | throw new Error(error.message || '更新文件失败');
114 | }
115 | };
116 |
117 | return {
118 | token,
119 | user,
120 | isAuthenticated,
121 | setAuth,
122 | clearAuth,
123 | loginWithGitHub,
124 | handleCallback,
125 | getFileContent,
126 | updateFileContent,
127 | };
128 | });
129 |
130 |
--------------------------------------------------------------------------------
/vite-plugin-favicon-api.ts:
--------------------------------------------------------------------------------
1 | import type { Plugin } from 'vite'
2 |
3 | /**
4 | * Vite 插件 - 开发环境 Favicon API 模拟
5 | * 在开发环境中提供 /api/favicon 路由
6 | */
7 | export default function faviconApiPlugin(): Plugin {
8 | return {
9 | name: 'vite-plugin-favicon-api',
10 | configureServer(server) {
11 | server.middlewares.use(async (req, res, next) => {
12 | // 只处理 /api/favicon 请求
13 | if (!req.url?.startsWith('/api/favicon')) {
14 | return next()
15 | }
16 |
17 | // 解析查询参数
18 | const url = new URL(req.url, `http://${req.headers.host}`)
19 | const targetUrl = url.searchParams.get('url')
20 |
21 | if (!targetUrl) {
22 | res.statusCode = 400
23 | res.setHeader('Content-Type', 'application/json')
24 | res.end(JSON.stringify({ error: '缺少 URL 参数' }))
25 | return
26 | }
27 |
28 | try {
29 | // 解析目标 URL
30 | let parsedUrl: URL
31 | try {
32 | parsedUrl = new URL(targetUrl.startsWith('http') ? targetUrl : `https://${targetUrl}`)
33 | } catch {
34 | res.statusCode = 400
35 | res.setHeader('Content-Type', 'application/json')
36 | res.end(JSON.stringify({ error: 'URL 格式错误' }))
37 | return
38 | }
39 |
40 | const hostname = parsedUrl.hostname
41 |
42 | // 尝试获取 favicon
43 | const services = [
44 | `https://www.google.com/s2/favicons?domain=${hostname}&sz=128`,
45 | `https://icons.duckduckgo.com/ip3/${hostname}.ico`,
46 | `https://favicon.im/${hostname}?larger=true`,
47 | `${parsedUrl.origin}/favicon.ico`,
48 | ]
49 |
50 | for (const serviceUrl of services) {
51 | try {
52 | const controller = new AbortController()
53 | const timeoutId = setTimeout(() => controller.abort(), 5000)
54 |
55 | const response = await fetch(serviceUrl, {
56 | headers: {
57 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
58 | },
59 | signal: controller.signal,
60 | })
61 |
62 | clearTimeout(timeoutId)
63 |
64 | if (response.ok) {
65 | const buffer = await response.arrayBuffer()
66 | const contentType = response.headers.get('content-type') || 'image/x-icon'
67 |
68 | res.statusCode = 200
69 | res.setHeader('Content-Type', contentType)
70 | res.setHeader('Cache-Control', 'public, max-age=86400')
71 | res.end(Buffer.from(buffer))
72 | return
73 | }
74 | } catch (error) {
75 | // 继续尝试下一个服务
76 | continue
77 | }
78 | }
79 |
80 | // 所有服务都失败,返回默认图标
81 | const defaultSvg = ``
85 |
86 | res.statusCode = 200
87 | res.setHeader('Content-Type', 'image/svg+xml')
88 | res.setHeader('Cache-Control', 'public, max-age=3600')
89 | res.end(defaultSvg)
90 | } catch (error) {
91 | console.error('Favicon API 错误:', error)
92 |
93 | const errorSvg = ``
97 |
98 | res.statusCode = 200
99 | res.setHeader('Content-Type', 'image/svg+xml')
100 | res.end(errorSvg)
101 | }
102 | })
103 | },
104 | }
105 | }
106 |
107 |
--------------------------------------------------------------------------------
/src/components/index/Head.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
14 |
18 |
19 |
20 |
21 |
22 |
72 |
--------------------------------------------------------------------------------
/api/favicon.ts:
--------------------------------------------------------------------------------
1 | import type { VercelRequest, VercelResponse } from '@vercel/node'
2 |
3 | /**
4 | * Vercel Serverless Function - 获取网站 Favicon
5 | * 使用方法: /api/favicon?url=https://example.com
6 | */
7 | export default async function handler(req: VercelRequest, res: VercelResponse) {
8 | // 允许跨域
9 | res.setHeader('Access-Control-Allow-Origin', '*')
10 | res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS')
11 | res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
12 |
13 | // 处理 OPTIONS 请求
14 | if (req.method === 'OPTIONS') {
15 | return res.status(200).end()
16 | }
17 |
18 | // 只允许 GET 请求
19 | if (req.method !== 'GET') {
20 | return res.status(405).json({ error: '方法不允许' })
21 | }
22 |
23 | const { url } = req.query
24 |
25 | // 验证 URL 参数
26 | if (!url || typeof url !== 'string') {
27 | return res.status(400).json({ error: '缺少 URL 参数' })
28 | }
29 |
30 | try {
31 | // 解析 URL,处理各种格式
32 | let targetUrl: URL
33 | try {
34 | // 如果 URL 没有协议,添加 https://
35 | const urlToProcess = url.startsWith('http') ? url : `https://${url}`
36 | targetUrl = new URL(urlToProcess)
37 | } catch (urlError) {
38 | return res.status(400).json({ error: 'URL 格式错误' })
39 | }
40 |
41 | const hostname = targetUrl.hostname
42 |
43 | // 首先尝试通过 Google 的服务获取(最可靠)
44 | const googleFaviconUrl = `https://www.google.com/s2/favicons?domain=${hostname}&sz=128`
45 |
46 | try {
47 | const controller = new AbortController()
48 | const timeoutId = setTimeout(() => controller.abort(), 5000)
49 |
50 | const response = await fetch(googleFaviconUrl, {
51 | headers: {
52 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
53 | },
54 | signal: controller.signal
55 | })
56 |
57 | clearTimeout(timeoutId)
58 |
59 | if (response.ok) {
60 | const buffer = await response.arrayBuffer()
61 | const contentType = response.headers.get('content-type') || 'image/x-icon'
62 |
63 | // 设置缓存(24小时)
64 | res.setHeader('Cache-Control', 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=604800')
65 | res.setHeader('Content-Type', contentType)
66 |
67 | return res.status(200).send(Buffer.from(buffer))
68 | }
69 | } catch (googleError) {
70 | console.error('Google favicon 服务失败:', googleError instanceof Error ? googleError.message : String(googleError))
71 | }
72 |
73 | // 如果 Google 服务失败,尝试其他服务
74 | const fallbackServices = [
75 | `https://icons.duckduckgo.com/ip3/${hostname}.ico`,
76 | `https://favicon.im/${hostname}?larger=true`,
77 | ]
78 |
79 | for (const serviceUrl of fallbackServices) {
80 | try {
81 | const controller = new AbortController()
82 | const timeoutId = setTimeout(() => controller.abort(), 3000)
83 |
84 | const response = await fetch(serviceUrl, {
85 | headers: {
86 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
87 | },
88 | signal: controller.signal
89 | })
90 |
91 | clearTimeout(timeoutId)
92 |
93 | if (response.ok) {
94 | const buffer = await response.arrayBuffer()
95 | const contentType = response.headers.get('content-type') || 'image/x-icon'
96 |
97 | res.setHeader('Cache-Control', 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=604800')
98 | res.setHeader('Content-Type', contentType)
99 |
100 | return res.status(200).send(Buffer.from(buffer))
101 | }
102 | } catch (error) {
103 | console.error(`服务 ${serviceUrl} 失败:`, error instanceof Error ? error.message : String(error))
104 | continue
105 | }
106 | }
107 |
108 | // 尝试直接从网站获取
109 | const directUrls = [
110 | `${targetUrl.origin}/favicon.ico`,
111 | `${targetUrl.origin}/favicon.png`,
112 | ]
113 |
114 | for (const directUrl of directUrls) {
115 | try {
116 | const controller = new AbortController()
117 | const timeoutId = setTimeout(() => controller.abort(), 2000)
118 |
119 | const response = await fetch(directUrl, {
120 | headers: {
121 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
122 | },
123 | signal: controller.signal
124 | })
125 |
126 | clearTimeout(timeoutId)
127 |
128 | if (response.ok) {
129 | const buffer = await response.arrayBuffer()
130 | const contentType = response.headers.get('content-type') || 'image/x-icon'
131 |
132 | res.setHeader('Cache-Control', 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=604800')
133 | res.setHeader('Content-Type', contentType)
134 |
135 | return res.status(200).send(Buffer.from(buffer))
136 | }
137 | } catch (error) {
138 | continue
139 | }
140 | }
141 |
142 | // 所有尝试都失败,返回一个简单的 SVG 占位图标
143 | const defaultSvg = ``
147 |
148 | res.setHeader('Content-Type', 'image/svg+xml')
149 | res.setHeader('Cache-Control', 'public, max-age=3600')
150 | return res.status(200).send(defaultSvg)
151 |
152 | } catch (error) {
153 | console.error('获取 favicon 错误:', error)
154 |
155 | // 返回错误占位图标
156 | const errorSvg = ``
160 |
161 | res.setHeader('Content-Type', 'image/svg+xml')
162 | return res.status(200).send(errorSvg)
163 | }
164 | }
165 |
166 |
--------------------------------------------------------------------------------
/src/assets/css/base.css:
--------------------------------------------------------------------------------
1 | * {
2 | padding: 0;
3 | margin: 0;
4 | }
5 |
6 | html {
7 | /* 缅怀黑白 */
8 | /* filter: grayscale(100%); */
9 | }
10 |
11 | body {
12 | /* element色卡 */
13 | --white: #ffffff;
14 | --black: #000000;
15 | --blue: #6e8cd7;
16 | --pink: #e83e8c;
17 | --red: #dc3545;
18 | --orange: #fd7e14;
19 | --yellow: #ffc107;
20 | --teal: #33b86c;
21 | --gray: #eef0f4;
22 |
23 | --primary: #007bff;
24 | --secondary: #6c757d;
25 | --success: #28a745;
26 | --info: #17a2b8;
27 | --warning: #ffc107;
28 | --danger: #dc3545;
29 |
30 | /* --ui-theme: rgba(255, 0, 0, 0.8); */
31 | --ui-theme: #409eff;
32 | }
33 |
34 | /* 明亮模式 */
35 | :root[theme-mode='light'] {
36 | --gray-o1: rgba(255, 255, 255, 0.1);
37 | --gray-o2: rgba(255, 255, 255, 0.2);
38 | --gray-o3: rgba(255, 255, 255, 0.3);
39 | --gray-o4: rgba(255, 255, 255, 0.4);
40 | --gray-o5: rgba(255, 255, 255, 0.5);
41 | --gray-o6: rgba(255, 255, 255, 0.6);
42 | --gray-o7: rgba(255, 255, 255, 0.7);
43 | --gray-o8: rgba(255, 255, 255, 0.8);
44 | --gray-o9: rgba(255, 255, 255, 0.9);
45 |
46 | --gray-0: #ffffff;
47 | --gray-50: #f9fafb;
48 | --gray-100: #f3f4f6;
49 | --gray-200: #e5e7eb;
50 | --gray-300: #d1d5db;
51 | --gray-400: #9ca3af;
52 | --gray-500: #6b7280;
53 | --gray-600: #4b5563;
54 | --gray-700: #374151;
55 | --gray-800: #1f2937;
56 | --gray-900: #111827;
57 | --gray-1000: #000000;
58 |
59 | --red-50: #fef2f2;
60 | --red-100: #fee2e2;
61 | --red-200: #fecaca;
62 | --red-300: #fca5a5;
63 | --red-400: #f87171;
64 | --red-500: #ef4444;
65 | --red-600: #dc2626;
66 | --red-700: #b91c1c;
67 | --red-800: #991b1b;
68 | --red-900: #7f1d1d;
69 |
70 | --green-50: #ecfdf5;
71 | --green-100: #d1fae5;
72 | --green-200: #a7f3d0;
73 | --green-300: #6ee7b7;
74 | --green-400: #34d399;
75 | --green-500: #10b981;
76 | --green-600: #059669;
77 | --green-700: #047857;
78 | --green-800: #065f46;
79 | --green-900: #064e3b;
80 |
81 | --yellow-50: #fffbeb;
82 | --yellow-100: #fef3c7;
83 | --yellow-200: #fde68a;
84 | --yellow-300: #fcd34d;
85 | --yellow-400: #fbbf24;
86 | --yellow-500: #f59e0b;
87 | --yellow-600: #d97706;
88 | --yellow-700: #b45309;
89 | --yellow-800: #92400e;
90 | --yellow-900: #78350f;
91 |
92 | --blue-50: #eff6ff;
93 | --blue-100: #dbeafe;
94 | --blue-200: #bfdbfe;
95 | --blue-300: #93c5fd;
96 | --blue-400: #60a5fa;
97 | --blue-500: #3b82f6;
98 | --blue-600: #2563eb;
99 | --blue-700: #1d4ed8;
100 | --blue-800: #1e40af;
101 | --blue-900: #1e3a8a;
102 |
103 | --indigo-50: #eef2ff;
104 | --indigo-100: #e0e7ff;
105 | --indigo-200: #c7d2fe;
106 | --indigo-300: #a5b4fc;
107 | --indigo-400: #818cf8;
108 | --indigo-500: #6366f1;
109 | --indigo-600: #4f46e5;
110 | --indigo-700: #4338ca;
111 | --indigo-800: #3730a3;
112 | --indigo-900: #312e81;
113 |
114 | --purple-50: #f5f3ff;
115 | --purple-100: #ede9fe;
116 | --purple-200: #ddd6fe;
117 | --purple-300: #c4b5fd;
118 | --purple-400: #a78bfa;
119 | --purple-500: #8b5cf6;
120 | --purple-600: #7c3aed;
121 | --purple-700: #6d28d9;
122 | --purple-800: #5b21b6;
123 | --purple-900: #4c1d95;
124 |
125 | --pink-50: #fdf2f8;
126 | --pink-100: #fce7f3;
127 | --pink-200: #fbcfe8;
128 | --pink-300: #f9a8d4;
129 | --pink-400: #f472b6;
130 | --pink-500: #ec4899;
131 | --pink-600: #db2777;
132 | --pink-700: #be185d;
133 | --pink-800: #9d174d;
134 | --pink-900: #831843;
135 | }
136 |
137 | /* 暗夜模式 */
138 | :root[theme-mode='dark'] {
139 | --gray-o9: rgba(255, 255, 255, 0.1);
140 | --gray-o8: rgba(255, 255, 255, 0.2);
141 | --gray-o7: rgba(255, 255, 255, 0.3);
142 | --gray-o6: rgba(255, 255, 255, 0.4);
143 | --gray-o5: rgba(255, 255, 255, 0.5);
144 | --gray-o4: rgba(255, 255, 255, 0.6);
145 | --gray-o3: rgba(255, 255, 255, 0.7);
146 | --gray-o2: rgba(255, 255, 255, 0.8);
147 | --gray-o1: rgba(255, 255, 255, 0.9);
148 |
149 | --gray-1000: #ffffff;
150 | --gray-900: #f9fafb;
151 | --gray-800: #f3f4f6;
152 | --gray-700: #e5e7eb;
153 | --gray-600: #d1d5db;
154 | --gray-500: #9ca3af;
155 | --gray-400: #6b7280;
156 | --gray-300: #4b5563;
157 | --gray-200: #374151;
158 | --gray-100: #1f2937;
159 | --gray-50: #111827;
160 | --gray-0: #000000;
161 |
162 | --red-900: #fef2f2;
163 | --red-800: #fee2e2;
164 | --red-700: #fecaca;
165 | --red-600: #fca5a5;
166 | --red-500: #f87171;
167 | --red-400: #ef4444;
168 | --red-300: #dc2626;
169 | --red-200: #b91c1c;
170 | --red-100: #991b1b;
171 | --red-50: #7f1d1d;
172 |
173 | --green-900: #ecfdf5;
174 | --green-800: #d1fae5;
175 | --green-700: #a7f3d0;
176 | --green-600: #6ee7b7;
177 | --green-500: #34d399;
178 | --green-400: #10b981;
179 | --green-300: #059669;
180 | --green-200: #047857;
181 | --green-100: #065f46;
182 | --green-50: #064e3b;
183 |
184 | --yellow-900: #fffbeb;
185 | --yellow-800: #fef3c7;
186 | --yellow-700: #fde68a;
187 | --yellow-600: #fcd34d;
188 | --yellow-500: #fbbf24;
189 | --yellow-400: #f59e0b;
190 | --yellow-300: #d97706;
191 | --yellow-200: #b45309;
192 | --yellow-100: #92400e;
193 | --yellow-50: #78350f;
194 |
195 | --blue-900: #eff6ff;
196 | --blue-800: #dbeafe;
197 | --blue-700: #bfdbfe;
198 | --blue-600: #93c5fd;
199 | --blue-500: #60a5fa;
200 | --blue-400: #3b82f6;
201 | --blue-300: #2563eb;
202 | --blue-200: #1d4ed8;
203 | --blue-100: #1e40af;
204 | --blue-50: #1e3a8a;
205 |
206 | --indigo-900: #eef2ff;
207 | --indigo-800: #e0e7ff;
208 | --indigo-700: #c7d2fe;
209 | --indigo-600: #a5b4fc;
210 | --indigo-500: #818cf8;
211 | --indigo-400: #6366f1;
212 | --indigo-300: #4f46e5;
213 | --indigo-200: #4338ca;
214 | --indigo-100: #3730a3;
215 | --indigo-50: #312e81;
216 |
217 | --purple-900: #f5f3ff;
218 | --purple-800: #ede9fe;
219 | --purple-700: #ddd6fe;
220 | --purple-600: #c4b5fd;
221 | --purple-500: #a78bfa;
222 | --purple-400: #8b5cf6;
223 | --purple-300: #7c3aed;
224 | --purple-200: #6d28d9;
225 | --purple-100: #5b21b6;
226 | --purple-50: #4c1d95;
227 |
228 | --pink-900: #fdf2f8;
229 | --pink-800: #fce7f3;
230 | --pink-700: #fbcfe8;
231 | --pink-600: #f9a8d4;
232 | --pink-500: #f472b6;
233 | --pink-400: #ec4899;
234 | --pink-300: #db2777;
235 | --pink-200: #be185d;
236 | --pink-100: #9d174d;
237 | --pink-50: #831843;
238 | }
239 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 黑羽导航 v2.2.0
2 |
3 | > 一个简洁、美观、易用的个人导航网站
4 |
5 | [](https://github.com/xia-66/nav)
6 | [](LICENSE)
7 | [](https://vuejs.org/)
8 | [](https://vitejs.dev/)
9 |
10 | ## 🚀 一键部署到 Vercel
11 |
12 | 点击下方按钮即可一键部署到 Vercel,几分钟内拥有你的专属导航网站:
13 |
14 | [](https://vercel.com/new/clone?repository-url=https://github.com/xia-66/nav)
15 |
16 | ### 部署步骤
17 |
18 | 1. **Fork 本仓库** - 点击右上角 Fork 按钮,将项目复制到你的 GitHub 账号
19 | 2. **修改配置** - 根据你的需求修改以下配置文件:
20 | - `src/config/data.json` - 网站数据(分类和网址)
21 | - `src/config/index.ts` - 网站基本信息(标题、描述、备案号等)
22 | 3. **一键部署** - 点击上方 Deploy 按钮,选择你 Fork 的仓库
23 | 4. **完成** - 等待几分钟,即可获得你的专属导航网站
24 |
25 | ## ✨ 特性
26 |
27 | - 🎨 **美观界面** - 现代化设计,支持响应式布局
28 | - 🚀 **快速搜索** - 本地搜索,即时响应
29 | - 📱 **移动优先** - 完美支持移动端和桌面端
30 | - 🎯 **分类管理** - 9 大分类,79 个精选网站
31 | - 🔍 **智能搜索** - 支持按名称和描述搜索
32 | - 🔐 **后台管理** - GitHub OAuth 登录,在线编辑数据
33 | - ⚡ **一键部署** - 支持 Vercel 一键部署
34 | - 💾 **GitHub 同步** - 数据自动同步到 GitHub 仓库
35 | - 🌐 **全球 CDN** - 支持 Vercel 等平台部署
36 |
37 | ## 📦 技术栈
38 |
39 | - **前端框架**: Vue 3 + TypeScript
40 | - **构建工具**: Vite 5
41 | - **UI 框架**: Element Plus
42 | - **状态管理**: Pinia
43 | - **路由管理**: Vue Router
44 | - **CSS 框架**: TailwindCSS + Sass
45 | - **图标字体**: 自定义 iconfont
46 |
47 | ## 🚀 快速开始
48 |
49 | ### 环境要求
50 |
51 | - Node.js >= 16
52 | - npm >= 8
53 |
54 | ### 安装依赖
55 |
56 | ```bash
57 | npm install
58 | ```
59 |
60 | ### 开发模式
61 |
62 | ```bash
63 | npm run dev
64 | ```
65 |
66 | 访问 http://localhost:8080
67 |
68 | ### 构建生产版本
69 |
70 | ```bash
71 | npm run build
72 | ```
73 |
74 | ### 预览构建结果
75 |
76 | ```bash
77 | npm run preview
78 | ```
79 |
80 | ## 📂 项目结构
81 |
82 | ```
83 | heiyu-nav/
84 | ├── src/
85 | │ ├── assets/ # 静态资源
86 | │ │ ├── css/ # 样式文件
87 | │ │ ├── font/ # 字体图标
88 | │ │ └── img/ # 图片资源
89 | │ ├── components/ # 组件
90 | │ │ ├── index/ # 前台组件
91 | │ │ └── admin/ # 后台组件
92 | │ ├── views/ # 视图页面
93 | │ │ ├── Index/ # 前台页面
94 | │ │ ├── AdminCallback.vue # OAuth 回调页面
95 | │ │ └── AdminDashboard.vue # 后台管理页面
96 | │ ├── router/ # 路由配置
97 | │ ├── store/ # 状态管理
98 | │ │ ├── index.ts # 主 store
99 | │ │ └── admin.ts # 后台 store
100 | │ ├── utils/ # 工具函数
101 | │ ├── config/ # ⭐ 配置文件(需要修改)
102 | │ │ ├── data.json # ⭐ 网站数据
103 | │ │ └── index.ts # ⭐ 网站信息
104 | │ ├── App.vue # 根组件
105 | │ └── main.ts # 入口文件
106 | ├── api/ # Serverless API
107 | │ ├── auth/ # OAuth 认证
108 | │ │ ├── github.ts # GitHub OAuth 处理
109 | │ │ └── callback.ts # OAuth 回调处理
110 | │ ├── github/ # GitHub API
111 | │ │ ├── get-file.ts # 获取文件
112 | │ │ └── update-file.ts # 更新文件
113 | │ └── favicon.ts # 网站图标获取
114 | ├── public/ # 公共资源
115 | ├── index.html # HTML 模板
116 | ├── vite.config.ts # Vite 配置
117 | ├── vercel.json # Vercel 配置
118 | ├── tsconfig.json # TypeScript 配置
119 | ├── tailwind.config.js # TailwindCSS 配置
120 | └── package.json # 项目配置
121 | ```
122 |
123 |
124 | ## 📝 配置说明
125 |
126 | ### 必改配置项
127 |
128 | #### 1. 网站数据 (`src/config/data.json`)
129 |
130 | 包含所有网站分类和链接:
131 |
132 | ```json
133 | {
134 | "categories": [
135 | {
136 | "id": 1,
137 | "name": "分类名称"
138 | }
139 | ],
140 | "items": [
141 | {
142 | "id": 1,
143 | "name": "网站名称",
144 | "url": "https://example.com",
145 | "description": "网站描述",
146 | "categoryId": 1
147 | }
148 | ]
149 | }
150 | ```
151 |
152 | #### 2. 网站信息 (`src/config/index.ts`)
153 |
154 | 配置项说明:
155 |
156 | ```typescript
157 | // 网站名称
158 | export const SITE_NAME = '黑羽导航'
159 |
160 | // 备案号(如果没有可以留空或删除)
161 | export const ICP_NUMBER = '豫ICP备2023030596号'
162 |
163 | // 网站图标API(无需修改)
164 | export const Favicon = '/api/favicon?url='
165 | ```
166 |
167 | ### 可选配置
168 |
169 | - **自定义域名**:在 Vercel Dashboard 中配置
170 | - **环境变量**:在 Vercel 项目设置中添加
171 | - **构建配置**:`vercel.json` 已预配置,一般无需修改
172 |
173 | ## 🔐 后台管理系统
174 |
175 | 本项目内置了后台管理系统,支持通过 Web 界面管理导航数据。
176 |
177 | ### 快速配置
178 |
179 | 1. **创建 GitHub OAuth App** - 参考 [ADMIN_CONFIG.md](./ADMIN_CONFIG.md)
180 | 2. **配置环境变量**:
181 |
182 | 在 Vercel Dashboard → Settings → Environment Variables 添加:
183 |
184 | ```bash
185 | # 必需的环境变量
186 | GITHUB_CLIENT_ID=你的_client_id # 后端 API 使用
187 | GITHUB_CLIENT_SECRET=你的_client_secret # 后端 API 使用
188 | VITE_GITHUB_CLIENT_ID=你的_client_id # 前端使用(与上面相同)
189 |
190 | # 可选的环境变量(有默认值)
191 | GITHUB_REPO_OWNER=你的_github_用户名 # 默认: xia-66
192 | GITHUB_REPO_NAME=nav # 默认: nav
193 | GITHUB_ALLOWED_USERS=你的_github_用户名 # 默认: xia-66
194 | ```
195 |
196 | 3. **访问后台** - `https://你的域名.com/#/admin/login`
197 |
198 | 详细配置请参考:[后台管理配置指南](./ADMIN_CONFIG.md)
199 |
200 | ## 📊 网站分类
201 |
202 | | 分类 | 数量 | 说明 |
203 | | -------- | ---- | --------------------------- |
204 | | 常用推荐 | 12 | AI 工具、视频网站、云服务等 |
205 | | 实用工具 | 17 | 截图、文件传输、激活工具等 |
206 | | 日漫专区 | 4 | 动漫资源和在线观看 |
207 | | 在线翻译 | 2 | 多语种翻译工具 |
208 | | 站长工具 | 11 | 测速、检测、域名工具等 |
209 | | 清理工具 | 2 | 磁盘清理和卸载工具 |
210 | | 我的收藏 | 15 | VPS、代理、OCR 等工具 |
211 | | 国外服务 | 11 | AI 助手、GitHub、YouTube 等 |
212 | | 开发工具 | 5 | API、图标、模板等 |
213 |
214 | ## 🎨 功能特性
215 |
216 | ### 主要功能
217 |
218 | **前台功能**
219 | - ✅ 网站展示 - 按分类展示所有导航网站
220 | - ✅ 实时搜索 - 支持搜索网站名称和描述
221 | - ✅ 锚点导航 - 快速跳转到指定分类
222 | - ✅ 懒加载 - 网站图标按需加载
223 | - ✅ 响应式 - 完美支持 PC 和移动端
224 | - ✅ 侧边栏 - 移动端友好的抽屉菜单
225 | - ✅ 时钟显示 - 实时显示当前时间
226 | - ✅ 背景效果 - 精美的背景图片
227 |
228 | **后台管理**
229 | - ✅ GitHub OAuth - 安全的第三方登录
230 | - ✅ 在线编辑 - 可视化编辑导航数据
231 | - ✅ 实时预览 - 修改后即时查看效果
232 | - ✅ 自动同步 - 一键提交到 GitHub 仓库
233 | - ✅ 权限控制 - 仅允许授权用户访问
234 |
235 | ### 交互体验
236 |
237 | - 流畅的动画效果
238 | - 直观的操作界面
239 | - 快速的搜索响应
240 | - 优雅的加载状态
241 |
242 | ## 📖 版本历史
243 |
244 | ### v2.2.0 (2024-11-27)
245 |
246 | - 🎨 优化后台登录对话框组件,简化样式
247 | - ✅ 修复 Vercel 部署环境变量配置问题
248 | - ✅ 替换用户头像为 `el-avatar` 组件
249 | - ✅ 移除登录加载页面,改为静默处理
250 | - ✅ 修复图标字体问题(icon-lock → icon-md-lock)
251 | - ✅ 移除按钮悬浮样式
252 | - ✅ 优化弹窗关闭逻辑
253 | - 📝 完善环境变量配置文档
254 |
255 | ### v2.1.0 (2024-10-31)
256 |
257 | - 🚀 优化部署流程,支持 Vercel 一键部署
258 | - ✅ 简化配置文件管理
259 | - ✅ 更新文档说明
260 | - ✅ 优化项目结构
261 |
262 | ### v2.0.0 (2024-10-25)
263 |
264 | - 🎉 重大更新:纯前端化架构
265 | - ✅ 删除所有后台代码和依赖
266 | - ✅ 使用本地 JSON 数据
267 | - ✅ 优化所有网站描述
268 | - ✅ 删除无用代码和文件
269 | - ✅ 精简项目结构
270 |
271 | ## 🤝 贡献
272 |
273 | 欢迎贡献!你可以:
274 |
275 | - 提交 Issue 报告问题
276 | - 发起 Pull Request 改进代码
277 | - 分享你的网站资源
278 | - 完善文档说明
279 |
280 | ## 📄 开源协议
281 |
282 | 本项目采用 MIT 协议开源。
283 |
284 | ## 🙏 鸣谢
285 |
286 | - [Vue.js](https://vuejs.org/) - 渐进式 JavaScript 框架
287 | - [Vite](https://vitejs.dev/) - 下一代前端构建工具
288 | - [Element Plus](https://element-plus.org/) - Vue 3 组件库
289 | - [TailwindCSS](https://tailwindcss.com/) - CSS 框架
290 | - [Vercel](https://vercel.com/) - 部署平台
291 |
292 | ## 📮 联系方式
293 |
294 | - 项目主页: [GitHub](https://github.com/xia-66/nav)
295 | - 问题反馈: [Issues](https://github.com/xia-66/nav/issues)
296 |
297 | ---
298 |
299 | ⭐ 如果这个项目对你有帮助,请给个 Star 支持一下!
300 |
301 | **黑羽导航 - 你的个人网站导航助手** 🎉
302 |
--------------------------------------------------------------------------------
/src/components/index/Search.vue:
--------------------------------------------------------------------------------
1 |
2 |
33 |
34 |
35 |
154 |
155 |
334 |
--------------------------------------------------------------------------------
/src/components/index/Site.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
32 |
33 |
34 |
35 |
36 |
137 |
138 |
286 |
--------------------------------------------------------------------------------
/src/components/admin/LoginDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
22 |
23 |
24 |
25 |
26 |
27 | {{ feature }}
28 |
29 |
30 |
31 |
37 |
40 | 使用 GitHub 登录
41 |
42 |
43 |
44 |
45 | 登录即表示同意授权访问您的 GitHub 账号
46 |
47 |
48 |
49 |
50 |
51 |
82 |
83 |
352 |
353 |
--------------------------------------------------------------------------------
/src/assets/font/iconfont.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "iconfont"; /* Project id 2940247 */
3 | src: url('iconfont.woff2?t=1711903975415') format('woff2'),
4 | url('iconfont.woff?t=1711903975415') format('woff'),
5 | url('iconfont.ttf?t=1711903975415') format('truetype');
6 | }
7 |
8 | .iconfont {
9 | font-family: "iconfont" !important;
10 | font-size: 20px;
11 | font-style: normal;
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | }
15 |
16 | .icon-SEVENZ:before {
17 | content: "\e613";
18 | }
19 |
20 | .icon-BAT:before {
21 | content: "\e615";
22 | }
23 |
24 | .icon-CONF:before {
25 | content: "\e61a";
26 | }
27 |
28 | .icon-EOT:before {
29 | content: "\e61b";
30 | }
31 |
32 | .icon-DOCX:before {
33 | content: "\e61e";
34 | }
35 |
36 | .icon-DOC:before {
37 | content: "\e61f";
38 | }
39 |
40 | .icon-ICO:before {
41 | content: "\e623";
42 | }
43 |
44 | .icon-JAR:before {
45 | content: "\e624";
46 | }
47 |
48 | .icon-JAVA:before {
49 | content: "\e625";
50 | }
51 |
52 | .icon-JPEG:before {
53 | content: "\e626";
54 | }
55 |
56 | .icon-JPG:before {
57 | content: "\e627";
58 | }
59 |
60 | .icon-JS:before {
61 | content: "\e628";
62 | }
63 |
64 | .icon-MD:before {
65 | content: "\e629";
66 | }
67 |
68 | .icon-MP3:before {
69 | content: "\e62a";
70 | }
71 |
72 | .icon-MP4:before {
73 | content: "\e62b";
74 | }
75 |
76 | .icon-PNG:before {
77 | content: "\e62e";
78 | }
79 |
80 | .icon-PPT:before {
81 | content: "\e630";
82 | }
83 |
84 | .icon-PSD:before {
85 | content: "\e631";
86 | }
87 |
88 | .icon-RAR:before {
89 | content: "\e633";
90 | }
91 |
92 | .icon-SH:before {
93 | content: "\e634";
94 | }
95 |
96 | .icon-SVG:before {
97 | content: "\e635";
98 | }
99 |
100 | .icon-TEXT:before {
101 | content: "\e636";
102 | }
103 |
104 | .icon-XLSX:before {
105 | content: "\e638";
106 | }
107 |
108 | .icon-XML:before {
109 | content: "\e639";
110 | }
111 |
112 | .icon-ZIP:before {
113 | content: "\e63a";
114 | }
115 |
116 | .icon-BIN:before {
117 | content: "\e63b";
118 | }
119 |
120 | .icon-github:before {
121 | content: "\e66f";
122 | }
123 |
124 | .icon-rcd-image-error-f:before {
125 | content: "\e618";
126 | }
127 |
128 | .icon-xitongpeizhi:before {
129 | content: "\e686";
130 | }
131 |
132 | .icon-chengxvpeizhi:before {
133 | content: "\e619";
134 | }
135 |
136 | .icon-json:before {
137 | content: "\e7bd";
138 | }
139 |
140 | .icon-daima:before {
141 | content: "\e66a";
142 | }
143 |
144 | .icon-fenlei:before {
145 | content: "\e643";
146 | }
147 |
148 | .icon-chubufenlei:before {
149 | content: "\e663";
150 | }
151 |
152 | .icon-rizhifuwu:before {
153 | content: "\e669";
154 | }
155 |
156 | .icon-wenzi:before {
157 | content: "\e883";
158 | }
159 |
160 | .icon-bianji:before {
161 | content: "\e621";
162 | }
163 |
164 | .icon-bianjibiaoge:before {
165 | content: "\eabc";
166 | }
167 |
168 | .icon-xinzeng:before {
169 | content: "\e632";
170 | }
171 |
172 | .icon-CSS:before {
173 | content: "\e614";
174 | }
175 |
176 | .icon-HTML:before {
177 | content: "\e617";
178 | }
179 |
180 | .icon-qitawenjian:before {
181 | content: "\e683";
182 | }
183 |
184 | .icon-PDF:before {
185 | content: "\e61d";
186 | }
187 |
188 | .icon-tupian:before {
189 | content: "\e63d";
190 | }
191 |
192 | .icon-badge-fill:before {
193 | content: "\e9fe";
194 | }
195 |
196 | .icon-badge-line:before {
197 | content: "\ea0c";
198 | }
199 |
200 | .icon-a-boxminus-line:before {
201 | content: "\ea22";
202 | }
203 |
204 | .icon-bug-fill:before {
205 | content: "\ea69";
206 | }
207 |
208 | .icon-a-documentationupload-line:before {
209 | content: "\eb69";
210 | }
211 |
212 | .icon-a-downloadto-fill:before {
213 | content: "\eb70";
214 | }
215 |
216 | .icon-exit-line:before {
217 | content: "\eb8d";
218 | }
219 |
220 | .icon-girl-line:before {
221 | content: "\ec38";
222 | }
223 |
224 | .icon-interactive-fill:before {
225 | content: "\eca7";
226 | }
227 |
228 | .icon-man-line:before {
229 | content: "\ed11";
230 | }
231 |
232 | .icon-a-lefttoo-line:before {
233 | content: "\ed17";
234 | }
235 |
236 | .icon-pen-fill:before {
237 | content: "\eda5";
238 | }
239 |
240 | .icon-remind-line:before {
241 | content: "\ee11";
242 | }
243 |
244 | .icon-a-rightlinefill-line:before {
245 | content: "\ee19";
246 | }
247 |
248 | .icon-robot-line:before {
249 | content: "\ee35";
250 | }
251 |
252 | .icon-rocket-fill:before {
253 | content: "\ee3a";
254 | }
255 |
256 | .icon-a-smartrobot-fill:before {
257 | content: "\ef09";
258 | }
259 |
260 | .icon-star-fill:before {
261 | content: "\ef0b";
262 | }
263 |
264 | .icon-star-line:before {
265 | content: "\ef14";
266 | }
267 |
268 | .icon-think-fill:before {
269 | content: "\ef91";
270 | }
271 |
272 | .icon-trophy-fill:before {
273 | content: "\efa6";
274 | }
275 |
276 | .icon-a-unfoldcross-line:before {
277 | content: "\efb5";
278 | }
279 |
280 | .icon-bianji-yonghu:before {
281 | content: "\efc6";
282 | }
283 |
284 | .icon-tianjia-yonghu:before {
285 | content: "\efc7";
286 | }
287 |
288 | .icon-a-leftsmallline-line:before {
289 | content: "\f010";
290 | }
291 |
292 | .icon-a-rightsmallline-line:before {
293 | content: "\f011";
294 | }
295 |
296 | .icon-a-upsmall-line:before {
297 | content: "\f012";
298 | }
299 |
300 | .icon-a-undersmall-line:before {
301 | content: "\f013";
302 | }
303 |
304 | .icon-tag:before {
305 | content: "\e61c";
306 | }
307 |
308 | .icon-tianjia:before {
309 | content: "\e65b";
310 | }
311 |
312 | .icon-paixu:before {
313 | content: "\e7e9";
314 | }
315 |
316 | .icon-tuichu:before {
317 | content: "\e62c";
318 | }
319 |
320 | .icon-md-menu:before {
321 | content: "\e9b8";
322 | }
323 |
324 | .icon-md-reorder:before {
325 | content: "\e9e8";
326 | }
327 |
328 | .icon-md-rocket:before {
329 | content: "\e9f8";
330 | }
331 |
332 | .icon-md-search:before {
333 | content: "\e9fd";
334 | }
335 |
336 | .icon-md-notifications:before {
337 | content: "\e9c1";
338 | }
339 |
340 | .icon-md-notifications-outline:before {
341 | content: "\e9c6";
342 | }
343 |
344 | .icon-md-photos:before {
345 | content: "\e9d0";
346 | }
347 |
348 | .icon-md-pin:before {
349 | content: "\e9d4";
350 | }
351 |
352 | .icon-md-qr-scanner:before {
353 | content: "\e9e4";
354 | }
355 |
356 | .icon-md-planet:before {
357 | content: "\e9e6";
358 | }
359 |
360 | .icon-md-school:before {
361 | content: "\e9fb";
362 | }
363 |
364 | .icon-md-settings:before {
365 | content: "\ea01";
366 | }
367 |
368 | .icon-md-stats:before {
369 | content: "\ea0a";
370 | }
371 |
372 | .icon-md-sync:before {
373 | content: "\ea11";
374 | }
375 |
376 | .icon-md-trash:before {
377 | content: "\ea1e";
378 | }
379 |
380 | .icon-weixin:before {
381 | content: "\e694";
382 | }
383 |
384 | .icon-md-arrow-dropdown:before {
385 | content: "\e907";
386 | }
387 |
388 | .icon-md-arrow-dropleft:before {
389 | content: "\e90b";
390 | }
391 |
392 | .icon-md-arrow-dropup:before {
393 | content: "\e90d";
394 | }
395 |
396 | .icon-md-arrow-dropright:before {
397 | content: "\e90f";
398 | }
399 |
400 | .icon-md-arrow-round-back:before {
401 | content: "\e911";
402 | }
403 |
404 | .icon-md-arrow-round-down:before {
405 | content: "\e912";
406 | }
407 |
408 | .icon-md-arrow-round-up:before {
409 | content: "\e913";
410 | }
411 |
412 | .icon-md-arrow-round-forward:before {
413 | content: "\e914";
414 | }
415 |
416 | .icon-md-attach:before {
417 | content: "\e916";
418 | }
419 |
420 | .icon-md-barcode:before {
421 | content: "\e918";
422 | }
423 |
424 | .icon-md-at:before {
425 | content: "\e91c";
426 | }
427 |
428 | .icon-md-checkmark:before {
429 | content: "\e942";
430 | }
431 |
432 | .icon-md-checkmark-circle:before {
433 | content: "\e943";
434 | }
435 |
436 | .icon-md-clipboard:before {
437 | content: "\e945";
438 | }
439 |
440 | .icon-md-close:before {
441 | content: "\e946";
442 | }
443 |
444 | .icon-md-close-circle:before {
445 | content: "\e948";
446 | }
447 |
448 | .icon-md-close-circle-outline:before {
449 | content: "\e94a";
450 | }
451 |
452 | .icon-md-contact:before {
453 | content: "\e959";
454 | }
455 |
456 | .icon-md-cut:before {
457 | content: "\e964";
458 | }
459 |
460 | .icon-md-cube:before {
461 | content: "\e965";
462 | }
463 |
464 | .icon-md-eye:before {
465 | content: "\e96c";
466 | }
467 |
468 | .icon-md-eye-off:before {
469 | content: "\e96e";
470 | }
471 |
472 | .icon-md-happy:before {
473 | content: "\e989";
474 | }
475 |
476 | .icon-md-heart:before {
477 | content: "\e98e";
478 | }
479 |
480 | .icon-md-heart-empty:before {
481 | content: "\e991";
482 | }
483 |
484 | .icon-md-help-circle:before {
485 | content: "\e994";
486 | }
487 |
488 | .icon-md-help-circle-outline:before {
489 | content: "\e996";
490 | }
491 |
492 | .icon-md-home:before {
493 | content: "\e998";
494 | }
495 |
496 | .icon-md-image:before {
497 | content: "\e99a";
498 | }
499 |
500 | .icon-md-information-circle:before {
501 | content: "\e99e";
502 | }
503 |
504 | .icon-md-laptop:before {
505 | content: "\e9a3";
506 | }
507 |
508 | .icon-md-link:before {
509 | content: "\e9a5";
510 | }
511 |
512 | .icon-md-lock:before {
513 | content: "\e9a7";
514 | }
515 |
516 | .icon-md-moon:before {
517 | content: "\e9ba";
518 | }
519 |
520 | .icon-iconset0133:before {
521 | content: "\e620";
522 | }
523 |
524 | .icon-iconset0148:before {
525 | content: "\e62f";
526 | }
527 |
528 | .icon-iconset0156:before {
529 | content: "\e637";
530 | }
531 |
532 | .icon-iconset0278:before {
533 | content: "\e6b1";
534 | }
535 |
536 | .icon-iconset0352:before {
537 | content: "\e6fb";
538 | }
539 |
540 | .icon-iconset0448:before {
541 | content: "\e747";
542 | }
543 |
544 | .icon-iconset0455:before {
545 | content: "\e74e";
546 | }
547 |
548 | .icon-chongwugou:before {
549 | content: "\e77d";
550 | }
551 |
552 | .icon-chongwugouliang:before {
553 | content: "\e77e";
554 | }
555 |
556 | .icon-tuwenxiangqing:before {
557 | content: "\e600";
558 | }
559 |
560 | .icon-xuexi:before {
561 | content: "\e7f1";
562 | }
563 |
564 | .icon-translate:before {
565 | content: "\eaf0";
566 | }
567 |
568 | .icon-chrome:before {
569 | content: "\e77f";
570 | }
571 |
572 | .icon-baidu:before {
573 | content: "\e780";
574 | }
575 |
576 | .icon-yingyong:before {
577 | content: "\e781";
578 | }
579 |
580 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, loadEnv } from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 | import { resolve } from 'path'
4 | import faviconApiPlugin from './vite-plugin-favicon-api'
5 | import pkg from './package.json'
6 |
7 | // https://vitejs.dev/config/
8 | export default defineConfig(({ mode }) => {
9 | // 加载环境变量
10 | const env = loadEnv(mode, process.cwd(), '')
11 |
12 | return {
13 | plugins: [
14 | vue(),
15 | faviconApiPlugin(),
16 | // 自定义插件处理所有 API 路由(模拟 Vercel Serverless)
17 | {
18 | name: 'api-handler',
19 | configureServer(server) {
20 | server.middlewares.use(async (req, res, next) => {
21 | // 处理 /api/auth/callback 路由
22 | if (req.url?.startsWith('/api/auth/callback')) {
23 | const url = new URL(req.url, `http://${req.headers.host}`);
24 | const code = url.searchParams.get('code');
25 | const error = url.searchParams.get('error');
26 | const errorDescription = url.searchParams.get('error_description');
27 |
28 | if (error) {
29 | const errorMsg = errorDescription || error || '授权失败';
30 | res.writeHead(302, { Location: `/#/index?error=${encodeURIComponent(errorMsg)}` });
31 | res.end();
32 | return;
33 | }
34 |
35 | if (!code) {
36 | res.writeHead(302, { Location: '/#/index?error=缺少授权码' });
37 | res.end();
38 | return;
39 | }
40 |
41 | // 重定向到前端的 callback 页面(hash 路由)
42 | res.writeHead(302, { Location: `/#/admin/callback?code=${code}` });
43 | res.end();
44 | return;
45 | }
46 |
47 | // 处理 /api/auth/github 路由
48 | if (req.url?.startsWith('/api/auth/github')) {
49 | const url = new URL(req.url, `http://${req.headers.host}`);
50 | const code = url.searchParams.get('code');
51 |
52 | res.setHeader('Content-Type', 'application/json');
53 |
54 | if (!code) {
55 | res.statusCode = 400;
56 | res.end(JSON.stringify({ error: '缺少授权码' }));
57 | return;
58 | }
59 |
60 | const clientId = env.VITE_GITHUB_CLIENT_ID;
61 | const clientSecret = env.GITHUB_CLIENT_SECRET;
62 |
63 | if (!clientId || !clientSecret) {
64 | res.statusCode = 500;
65 | res.end(JSON.stringify({ error: '服务器配置错误:请检查 .env 文件中的 GITHUB_CLIENT_ID 和 GITHUB_CLIENT_SECRET' }));
66 | return;
67 | }
68 |
69 | try {
70 | // 1. 使用 code 换取 access_token
71 | const tokenResponse = await fetch('https://github.com/login/oauth/access_token', {
72 | method: 'POST',
73 | headers: {
74 | 'Content-Type': 'application/json',
75 | 'Accept': 'application/json',
76 | },
77 | body: JSON.stringify({
78 | client_id: clientId,
79 | client_secret: clientSecret,
80 | code: code,
81 | }),
82 | });
83 |
84 | const tokenData = await tokenResponse.json();
85 |
86 | if (tokenData.error) {
87 | res.statusCode = 400;
88 | res.end(JSON.stringify({ error: tokenData.error_description || '授权失败' }));
89 | return;
90 | }
91 |
92 | const accessToken = tokenData.access_token;
93 |
94 | // 2. 获取用户信息
95 | const userResponse = await fetch('https://api.github.com/user', {
96 | headers: {
97 | 'Authorization': `token ${accessToken}`,
98 | 'Accept': 'application/vnd.github.v3+json',
99 | },
100 | });
101 |
102 | const userData = await userResponse.json();
103 |
104 | // 3. 验证用户是否为 xia-66
105 | if (userData.login !== 'xia-66') {
106 | res.statusCode = 403;
107 | res.end(JSON.stringify({ error: '无权限访问,仅允许 xia-66 用户' }));
108 | return;
109 | }
110 |
111 | // 4. 返回 token 和用户信息
112 | res.statusCode = 200;
113 | res.end(JSON.stringify({
114 | token: accessToken,
115 | user: {
116 | login: userData.login,
117 | name: userData.name,
118 | avatar_url: userData.avatar_url,
119 | },
120 | }));
121 | return;
122 | } catch (error) {
123 | console.error('GitHub OAuth Error:', error);
124 | res.statusCode = 500;
125 | res.end(JSON.stringify({ error: '服务器内部错误' }));
126 | return;
127 | }
128 | }
129 |
130 | // 处理 /api/github/get-file 路由
131 | if (req.url?.startsWith('/api/github/get-file')) {
132 | const url = new URL(req.url, `http://${req.headers.host}`);
133 | const token = url.searchParams.get('token');
134 |
135 | res.setHeader('Content-Type', 'application/json');
136 |
137 | if (!token) {
138 | res.statusCode = 401;
139 | res.end(JSON.stringify({ error: '未授权' }));
140 | return;
141 | }
142 |
143 | const repoOwner = env.GITHUB_REPO_OWNER || 'xia-66';
144 | const repoName = env.GITHUB_REPO_NAME || 'nav';
145 |
146 | try {
147 | const response = await fetch(
148 | `https://api.github.com/repos/${repoOwner}/${repoName}/contents/src/config/data.json`,
149 | {
150 | headers: {
151 | 'Authorization': `token ${token}`,
152 | 'Accept': 'application/vnd.github.v3+json',
153 | },
154 | }
155 | );
156 |
157 | if (!response.ok) {
158 | res.statusCode = response.status;
159 | res.end(JSON.stringify({ error: '获取文件失败' }));
160 | return;
161 | }
162 |
163 | const data = await response.json();
164 | const content = Buffer.from(data.content, 'base64').toString('utf-8');
165 |
166 | res.statusCode = 200;
167 | res.end(JSON.stringify({
168 | content: JSON.parse(content),
169 | sha: data.sha,
170 | }));
171 | return;
172 | } catch (error) {
173 | console.error('Get File Error:', error);
174 | res.statusCode = 500;
175 | res.end(JSON.stringify({ error: '获取文件失败' }));
176 | return;
177 | }
178 | }
179 |
180 | // 处理 /api/github/update-file 路由
181 | if (req.url?.startsWith('/api/github/update-file') && req.method === 'POST') {
182 | let body = '';
183 | req.on('data', chunk => {
184 | body += chunk.toString();
185 | });
186 |
187 | req.on('end', async () => {
188 | try {
189 | const { token, content, sha, message } = JSON.parse(body);
190 |
191 | res.setHeader('Content-Type', 'application/json');
192 |
193 | if (!token || !content || !sha) {
194 | res.statusCode = 400;
195 | res.end(JSON.stringify({ error: '缺少必要参数' }));
196 | return;
197 | }
198 |
199 | const allowedUsers = (env.GITHUB_ALLOWED_USERS || 'xia-66').split(',').map(u => u.trim());
200 |
201 | // 验证用户身份
202 | const userResponse = await fetch('https://api.github.com/user', {
203 | headers: {
204 | 'Authorization': `token ${token}`,
205 | 'Accept': 'application/vnd.github.v3+json',
206 | },
207 | });
208 |
209 | const userData = await userResponse.json();
210 |
211 | if (!allowedUsers.includes(userData.login)) {
212 | res.statusCode = 403;
213 | res.end(JSON.stringify({ error: `无权限修改,仅允许以下用户: ${allowedUsers.join(', ')}` }));
214 | return;
215 | }
216 |
217 | const repoOwner = env.GITHUB_REPO_OWNER || 'xia-66';
218 | const repoName = env.GITHUB_REPO_NAME || 'nav';
219 |
220 | // 更新文件
221 | const updateResponse = await fetch(
222 | `https://api.github.com/repos/${repoOwner}/${repoName}/contents/src/config/data.json`,
223 | {
224 | method: 'PUT',
225 | headers: {
226 | 'Authorization': `token ${token}`,
227 | 'Accept': 'application/vnd.github.v3+json',
228 | 'Content-Type': 'application/json',
229 | },
230 | body: JSON.stringify({
231 | message: message || '更新导航数据',
232 | content: Buffer.from(JSON.stringify(content, null, 2)).toString('base64'),
233 | sha: sha,
234 | }),
235 | }
236 | );
237 |
238 | if (!updateResponse.ok) {
239 | const errorData = await updateResponse.json();
240 | res.statusCode = updateResponse.status;
241 | res.end(JSON.stringify({ error: errorData.message || '更新文件失败' }));
242 | return;
243 | }
244 |
245 | const result = await updateResponse.json();
246 |
247 | res.statusCode = 200;
248 | res.end(JSON.stringify({
249 | success: true,
250 | commit: result.commit,
251 | }));
252 | return;
253 | } catch (error) {
254 | console.error('Update File Error:', error);
255 | res.statusCode = 500;
256 | res.end(JSON.stringify({ error: '更新文件失败' }));
257 | return;
258 | }
259 | });
260 | return;
261 | }
262 |
263 | next();
264 | });
265 | }
266 | }
267 | ],
268 | define: {
269 | __APP_VERSION__: JSON.stringify(pkg.version),
270 | },
271 | // 配置根路径
272 | resolve: {
273 | // ↓路径别名,主要是这部分
274 | alias: {
275 | '@': resolve(__dirname, './src')
276 | }
277 | },
278 | css: {
279 | preprocessorOptions: {
280 | // 使用新的API方式
281 | scss: {
282 | api: 'modern-compiler'
283 | }
284 | // 如果需要全局引入变量或mixin
285 | // additionalData: `@import "@/styles/variables.scss";`
286 | }
287 | },
288 | server: {
289 | // 配置host,局域网可访问
290 | host: '0.0.0.0',
291 | port: 8080
292 | }
293 | }})
294 |
--------------------------------------------------------------------------------
/src/config/data.json:
--------------------------------------------------------------------------------
1 | {
2 | "categories": [
3 | {
4 | "id": 1,
5 | "name": "常用推荐"
6 | },
7 | {
8 | "id": 2,
9 | "name": "实用工具"
10 | },
11 | {
12 | "id": 3,
13 | "name": "日漫专区"
14 | },
15 | {
16 | "id": 5,
17 | "name": "站长工具"
18 | },
19 | {
20 | "id": 8,
21 | "name": "清理工具"
22 | },
23 | {
24 | "id": 10,
25 | "name": "我的收藏"
26 | },
27 | {
28 | "id": 11,
29 | "name": "国外服务"
30 | },
31 | {
32 | "id": 12,
33 | "name": "开发工具"
34 | }
35 | ],
36 | "items": [
37 | {
38 | "id": 1,
39 | "name": "Kimi",
40 | "url": "https://kimi.moonshot.cn/",
41 | "description": "月之暗面AI助手",
42 | "categoryId": 1
43 | },
44 | {
45 | "id": 2,
46 | "name": "Gmail",
47 | "url": "https://www.google.com.hk/intl/zh-HK_hk/gmail/about/",
48 | "description": "谷歌免费邮箱服务",
49 | "categoryId": 11
50 | },
51 | {
52 | "id": 4,
53 | "name": "游戏下载",
54 | "url": "https://www.gamer520.com/",
55 | "description": "游戏资源下载站",
56 | "categoryId": 1
57 | },
58 | {
59 | "id": 8,
60 | "name": "ChatGPT",
61 | "url": "https://chat.openai.com/",
62 | "description": "OpenAI聊天机器人",
63 | "categoryId": 11
64 | },
65 | {
66 | "id": 9,
67 | "name": "QQ邮箱",
68 | "url": "https://mail.qq.com/",
69 | "description": "腾讯免费邮箱服务",
70 | "categoryId": 1
71 | },
72 | {
73 | "id": 10,
74 | "name": "农大Gitlab",
75 | "url": "https://git.henau.edu.cn/",
76 | "description": "河南农业大学代码托管平台",
77 | "categoryId": 1
78 | },
79 | {
80 | "id": 12,
81 | "name": "哔哩哔哩",
82 | "url": "https://www.bilibili.com/",
83 | "description": "年轻人的视频社区",
84 | "categoryId": 1
85 | },
86 | {
87 | "id": 14,
88 | "name": "致美化",
89 | "url": "https://zhutix.com/",
90 | "description": "Windows美化主题资源",
91 | "categoryId": 2
92 | },
93 | {
94 | "id": 17,
95 | "name": "性格测试",
96 | "url": "https://www.16personalities.com/ch",
97 | "description": "MBTI性格测试",
98 | "categoryId": 2
99 | },
100 | {
101 | "id": 18,
102 | "name": "一键激活",
103 | "url": "https://kms.cx/",
104 | "description": "Windows系统激活工具",
105 | "categoryId": 2
106 | },
107 | {
108 | "id": 19,
109 | "name": "PPT超级市场",
110 | "url": "https://www.pptsupermarket.com/",
111 | "description": "PPT模板免费下载",
112 | "categoryId": 2
113 | },
114 | {
115 | "id": 22,
116 | "name": "uTools",
117 | "url": "https://u.tools/",
118 | "description": "新一代效率工具平台",
119 | "categoryId": 2
120 | },
121 | {
122 | "id": 23,
123 | "name": "Real-ESRGAN-GUI",
124 | "url": "https://github.com/tsukumijima/Real-ESRGAN-GUI",
125 | "description": "AI图片超分辨率工具",
126 | "categoryId": 10
127 | },
128 | {
129 | "id": 25,
130 | "name": "GitHub Desktop",
131 | "url": "https://desktop.github.com/",
132 | "description": "GitHub官方桌面客户端",
133 | "categoryId": 10
134 | },
135 | {
136 | "id": 26,
137 | "name": "文件转换器",
138 | "url": "https://convertio.co/zh/",
139 | "description": "在线文件格式转换工具",
140 | "categoryId": 2
141 | },
142 | {
143 | "id": 27,
144 | "name": "草料二维码",
145 | "url": "https://cli.im/",
146 | "description": "二维码生成与解码",
147 | "categoryId": 2
148 | },
149 | {
150 | "id": 28,
151 | "name": "Adobe全家桶",
152 | "url": "https://www.superso.top/Adobe/",
153 | "description": "Adobe软件下载",
154 | "categoryId": 2
155 | },
156 | {
157 | "id": 29,
158 | "name": "LocalSend",
159 | "url": "https://localsend.org/#/",
160 | "description": "跨平台局域网文件传输",
161 | "categoryId": 2
162 | },
163 | {
164 | "id": 30,
165 | "name": "Greasy Fork",
166 | "url": "https://greasyfork.org/zh-CN",
167 | "description": "用户脚本分享平台",
168 | "categoryId": 2
169 | },
170 | {
171 | "id": 32,
172 | "name": "Anime Garden",
173 | "url": "https://garden.onekuma.cn/",
174 | "description": "动漫花园资源站",
175 | "categoryId": 3
176 | },
177 | {
178 | "id": 35,
179 | "name": "风车动漫",
180 | "url": "https://www.dm302.com/",
181 | "description": "免费动漫在线观看",
182 | "categoryId": 3
183 | },
184 | {
185 | "id": 36,
186 | "name": "Omofun",
187 | "url": "https://omofun.link/",
188 | "description": "动漫追番网站",
189 | "categoryId": 3
190 | },
191 | {
192 | "id": 37,
193 | "name": "沪江小D",
194 | "url": "https://dict.hjenglish.com/",
195 | "description": "多语种在线词典",
196 | "categoryId": 2
197 | },
198 | {
199 | "id": 38,
200 | "name": "有道翻译",
201 | "url": "https://fanyi.youdao.com/index.html#/",
202 | "description": "网易有道在线翻译",
203 | "categoryId": 2
204 | },
205 | {
206 | "id": 40,
207 | "name": "获取Favicon图标",
208 | "url": "https://favicon.vwood.xyz/",
209 | "description": "网站图标提取工具",
210 | "categoryId": 12
211 | },
212 | {
213 | "id": 42,
214 | "name": "Web API",
215 | "url": "https://api.vvhan.com/",
216 | "description": "免费API接口服务",
217 | "categoryId": 12
218 | },
219 | {
220 | "id": 44,
221 | "name": "帧率测试",
222 | "url": "https://www.testufo.com/",
223 | "description": "显示器刷新率测试",
224 | "categoryId": 5
225 | },
226 | {
227 | "id": 45,
228 | "name": "CDN加速",
229 | "url": "https://www.bootcdn.cn/",
230 | "description": "前端开源库CDN加速",
231 | "categoryId": 5
232 | },
233 | {
234 | "id": 47,
235 | "name": "测速网站",
236 | "url": "https://test.ustc.edu.cn/",
237 | "description": "中科大网络测速",
238 | "categoryId": 5
239 | },
240 | {
241 | "id": 48,
242 | "name": "IPv6测试",
243 | "url": "https://www.test-ipv6.com/index.html.zh_CN",
244 | "description": "IPv6连通性测试",
245 | "categoryId": 5
246 | },
247 | {
248 | "id": 49,
249 | "name": "宝塔面板",
250 | "url": "https://www.bt.cn/new/index.html?btwaf=35186210",
251 | "description": "Linux服务器管理面板",
252 | "categoryId": 5
253 | },
254 | {
255 | "id": 50,
256 | "name": "IT Tools",
257 | "url": "https://it-tools.tech/",
258 | "description": "程序员工具集合",
259 | "categoryId": 12
260 | },
261 | {
262 | "id": 51,
263 | "name": "检测IP端口",
264 | "url": "https://coding.tools/cn/port-checker",
265 | "description": "在线端口检测工具",
266 | "categoryId": 5
267 | },
268 | {
269 | "id": 52,
270 | "name": "IPChecking",
271 | "url": "https://ipcheck.ing/",
272 | "description": "IP地址信息查询",
273 | "categoryId": 5
274 | },
275 | {
276 | "id": 54,
277 | "name": "Vercel",
278 | "url": "https://vercel.com/",
279 | "description": "前端应用部署平台",
280 | "categoryId": 11
281 | },
282 | {
283 | "id": 63,
284 | "name": "SpaceSniffer(磁盘空间分析工具)",
285 | "url": "https://www.spacesniffer.com.cn",
286 | "description": "磁盘空间可视化分析",
287 | "categoryId": 8
288 | },
289 | {
290 | "id": 64,
291 | "name": "Geek",
292 | "url": "https://geekuninstaller.com/",
293 | "description": "极客卸载工具",
294 | "categoryId": 8
295 | },
296 | {
297 | "id": 88,
298 | "name": "V2EX",
299 | "url": "https://global.v2ex.co/",
300 | "description": "创意工作者社区",
301 | "categoryId": 1
302 | },
303 | {
304 | "id": 89,
305 | "name": "LINUX DO",
306 | "url": "https://linux.do/",
307 | "description": "Linux技术社区",
308 | "categoryId": 1
309 | },
310 | {
311 | "id": 92,
312 | "name": "sparkle",
313 | "url": "https://github.com/xishang0128/sparkle",
314 | "description": "Clash代理客户端",
315 | "categoryId": 10
316 | },
317 | {
318 | "id": 93,
319 | "name": "异次元发卡",
320 | "url": "https://github.com/lizhipay/acg-faka",
321 | "description": "开源自动发卡系统",
322 | "categoryId": 10
323 | },
324 | {
325 | "id": 98,
326 | "name": "GitHub",
327 | "url": "https://github.com/",
328 | "description": "全球最大开源社区",
329 | "categoryId": 11
330 | },
331 | {
332 | "id": 101,
333 | "name": "腾讯元宝",
334 | "url": "https://yuanbao.tencent.com/chat",
335 | "description": "腾讯AI智能助手",
336 | "categoryId": 1
337 | },
338 | {
339 | "id": 104,
340 | "name": "VUE后台模板",
341 | "url": "http://vue.easydo.work/",
342 | "description": "Vue后台管理系统模板",
343 | "categoryId": 12
344 | },
345 | {
346 | "id": 107,
347 | "name": "小码短链接",
348 | "url": "https://xiaomark.com/",
349 | "description": "短网址生成工具",
350 | "categoryId": 2
351 | },
352 | {
353 | "id": 109,
354 | "name": "FOFA",
355 | "url": "https://fofa.info/",
356 | "description": "网络资产搜索引擎",
357 | "categoryId": 12
358 | },
359 | {
360 | "id": 110,
361 | "name": "Ping",
362 | "url": "https://www.itdog.cn/ping/",
363 | "description": "全国多地Ping测试",
364 | "categoryId": 5
365 | },
366 | {
367 | "id": 113,
368 | "name": "IP纯净度",
369 | "url": "https://ping0.cc/",
370 | "description": "IP信誉检测工具",
371 | "categoryId": 5
372 | },
373 | {
374 | "id": 119,
375 | "name": "网络面板",
376 | "url": "https://traffic.heiyu.fun",
377 | "description": "流量消耗测试工具",
378 | "categoryId": 5
379 | },
380 | {
381 | "id": 121,
382 | "name": "宝可梦",
383 | "url": "https://web2.52pokemon.cc/dashboard",
384 | "description": "代理服务订阅",
385 | "categoryId": 10
386 | },
387 | {
388 | "id": 122,
389 | "name": "临时邮箱",
390 | "url": "https://mail.heiyu.fun",
391 | "description": "临时邮箱服务",
392 | "categoryId": 2
393 | },
394 | {
395 | "id": 123,
396 | "name": "宝塔纯净版",
397 | "url": "https://www.hostcli.com/",
398 | "description": "宝塔面板纯净版",
399 | "categoryId": 10
400 | },
401 | {
402 | "id": 132,
403 | "name": "icon",
404 | "url": "https://www.flaticon.com/",
405 | "description": "免费图标资源库",
406 | "categoryId": 12
407 | },
408 | {
409 | "id": 134,
410 | "name": "随机身份信息",
411 | "url": "https://id.dcode.top/",
412 | "description": "随机身份信息生成",
413 | "categoryId": 12
414 | },
415 | {
416 | "id": 136,
417 | "name": "Claude",
418 | "url": "https://claude.ai/login",
419 | "description": "Anthropic AI助手",
420 | "categoryId": 11
421 | },
422 | {
423 | "id": 137,
424 | "name": "Grok",
425 | "url": "https://grok.com/",
426 | "description": "xAI聊天机器人",
427 | "categoryId": 11
428 | },
429 | {
430 | "id": 138,
431 | "name": "Gemini",
432 | "url": "https://deepmind.google/technologies/gemini/",
433 | "description": "Google AI助手",
434 | "categoryId": 11
435 | },
436 | {
437 | "id": 141,
438 | "name": "FIclash",
439 | "url": "https://github.com/chen08209/FlClash",
440 | "description": "Flutter Clash客户端",
441 | "categoryId": 10
442 | },
443 | {
444 | "id": 143,
445 | "name": "Cloudflare",
446 | "url": "https://www.cloudflare-cn.com/enterprise/",
447 | "description": "CDN与网络安全服务",
448 | "categoryId": 11
449 | },
450 | {
451 | "id": 144,
452 | "name": "YouTube",
453 | "url": "https://www.youtube.com/",
454 | "description": "全球视频分享平台",
455 | "categoryId": 11
456 | },
457 | {
458 | "id": 146,
459 | "name": "CSDN解锁",
460 | "url": "https://csdn.zeroai.chat/",
461 | "description": "CSDN内容免费查看",
462 | "categoryId": 2
463 | },
464 | {
465 | "id": 148,
466 | "name": "lanerc",
467 | "url": "https://lanerc.app/",
468 | "description": "动漫追番应用",
469 | "categoryId": 3
470 | },
471 | {
472 | "id": 149,
473 | "name": "spaceship",
474 | "url": "https://www.spaceship.com/",
475 | "description": "域名注册服务商",
476 | "categoryId": 11
477 | },
478 | {
479 | "id": 150,
480 | "name": "whois",
481 | "url": "https://whois.heiyu.fun/",
482 | "description": "域名信息查询",
483 | "categoryId": 5
484 | },
485 | {
486 | "id": 152,
487 | "name": "NodeSeek",
488 | "url": "https://www.nodeseek.com/",
489 | "description": "VPS交流社区",
490 | "categoryId": 1
491 | },
492 | {
493 | "id": 156,
494 | "name": "文叔叔",
495 | "url": "https://www.wenshushu.cn/",
496 | "description": "大文件传输工具",
497 | "categoryId": 2
498 | },
499 | {
500 | "id": 159,
501 | "name": "v2rayNG",
502 | "url": "https://github.com/2dust/v2rayNG/releases",
503 | "description": "Android代理客户端",
504 | "categoryId": 10
505 | },
506 | {
507 | "id": 160,
508 | "name": "华为云",
509 | "url": "https://www.huaweicloud.com/intl/zh-cn/",
510 | "description": "华为云服务平台",
511 | "categoryId": 1
512 | },
513 | {
514 | "id": 161,
515 | "name": "Everything",
516 | "url": "https://www.voidtools.com/zh-cn/support/everything/",
517 | "description": "文件快速搜索工具",
518 | "categoryId": 2
519 | },
520 | {
521 | "id": 162,
522 | "name": "PixPin",
523 | "url": "https://pixpin.cn/",
524 | "description": "功能强大的截图工具",
525 | "categoryId": 2
526 | },
527 | {
528 | "id": 163,
529 | "name": "Cherry Studio",
530 | "url": "https://www.cherry-ai.com/",
531 | "description": "多AI模型桌面客户端",
532 | "categoryId": 10
533 | },
534 | {
535 | "id": 164,
536 | "name": "Umi-OCR",
537 | "url": "https://github.com/hiroi-sora/Umi-OCR",
538 | "description": "离线OCR文字识别",
539 | "categoryId": 10
540 | }
541 | ]
542 | }
--------------------------------------------------------------------------------
/src/views/AdminDashboard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | {{ getItemCountByCategory(row.id) }}
65 |
66 |
67 |
68 |
69 |
75 | 编辑
76 |
77 |
83 | 删除
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 | {{ getCategoryName(row.categoryId) }}
139 |
140 |
141 |
142 |
143 |
149 | 编辑
150 |
151 |
157 | 删除
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
174 |
175 |
176 |
181 |
182 |
183 |
184 |
185 |
186 |
187 | 取消
188 | 确定
189 |
190 |
191 |
192 |
193 |
199 |
200 |
201 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
220 |
221 |
222 |
223 |
229 |
230 |
231 |
232 |
233 | 取消
234 | 确定
235 |
236 |
237 |
238 |
239 |
240 |
547 |
548 |
998 |
999 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const colors = require('tailwindcss/colors')
2 | // 导入tailwindcss辅助方法
3 | const { createPX } = require('./tailwind.utils.js')
4 |
5 | module.exports = {
6 | content: ['./src/**/*.html', './src/**/*.vue', './src/**/*.js'],
7 | presets: [],
8 | // 主题触发方式
9 | darkMode: 'class', // 'media' or 'class'
10 | theme: {
11 | // 屏幕断点
12 | screens: {
13 | xl: { min: '1280px' },
14 | lg: { max: '1280px' },
15 | md: { max: '1024px' },
16 | sm: { max: '640px' }
17 | },
18 | // 内置颜色
19 | colors: {
20 | // 自定义
21 | 'gray-0': '#ffffff',
22 | 'gray-1000': '#000000',
23 | 'gray-o5': 'rgba(255, 255, 255, 0.5)',
24 | 'gray-o3': 'rgba(255, 255, 255, 0.3)',
25 | 'gray-o7': 'rgba(255, 255, 255, 0.7)',
26 | // 系统自带
27 | transparent: 'transparent',
28 | current: 'currentColor',
29 | black: colors.black,
30 | white: colors.white,
31 | gray: colors.gray,
32 | red: colors.red,
33 | yellow: colors.amber,
34 | green: colors.emerald,
35 | blue: colors.blue,
36 | indigo: colors.indigo,
37 | purple: colors.violet,
38 | pink: colors.pink
39 | },
40 | // 间隔
41 | spacing: {
42 | px: '1px',
43 | 0: '0px',
44 | 0.5: '0.125rem',
45 | 1: '0.25rem',
46 | 1.5: '0.375rem',
47 | 2: '0.5rem',
48 | 2.5: '0.625rem',
49 | 3: '0.75rem',
50 | 3.5: '0.875rem',
51 | 4: '1rem',
52 | 5: '1.25rem',
53 | 6: '1.5rem',
54 | 7: '1.75rem',
55 | 8: '2rem',
56 | 9: '2.25rem',
57 | 10: '2.5rem',
58 | 11: '2.75rem',
59 | 12: '3rem',
60 | 14: '3.5rem',
61 | 16: '4rem',
62 | 20: '5rem',
63 | 24: '6rem',
64 | 28: '7rem',
65 | 32: '8rem',
66 | 36: '9rem',
67 | 40: '10rem',
68 | 44: '11rem',
69 | 48: '12rem',
70 | 52: '13rem',
71 | 56: '14rem',
72 | 60: '15rem',
73 | 64: '16rem',
74 | 72: '18rem',
75 | 80: '20rem',
76 | 96: '24rem',
77 | ...createPX(800, 'positive'),
78 | ...createPX(200, 'negative')
79 | },
80 | // 默认动画
81 | animation: {
82 | none: 'none',
83 | spin: 'spin 1s linear infinite',
84 | ping: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
85 | pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
86 | bounce: 'bounce 1s infinite'
87 | },
88 | backdropBlur: theme => theme('blur'),
89 | backdropBrightness: theme => theme('brightness'),
90 | backdropContrast: theme => theme('contrast'),
91 | backdropGrayscale: theme => theme('grayscale'),
92 | backdropHueRotate: theme => theme('hueRotate'),
93 | backdropInvert: theme => theme('invert'),
94 | backdropOpacity: theme => theme('opacity'),
95 | backdropSaturate: theme => theme('saturate'),
96 | backdropSepia: theme => theme('sepia'),
97 | backgroundColor: theme => theme('colors'),
98 | // 默认渐变
99 | backgroundImage: {
100 | none: 'none',
101 | 'gradient-to-t': 'linear-gradient(to top, var(--tw-gradient-stops))',
102 | 'gradient-to-tr': 'linear-gradient(to top right, var(--tw-gradient-stops))',
103 | 'gradient-to-r': 'linear-gradient(to right, var(--tw-gradient-stops))',
104 | 'gradient-to-br': 'linear-gradient(to bottom right, var(--tw-gradient-stops))',
105 | 'gradient-to-b': 'linear-gradient(to bottom, var(--tw-gradient-stops))',
106 | 'gradient-to-bl': 'linear-gradient(to bottom left, var(--tw-gradient-stops))',
107 | 'gradient-to-l': 'linear-gradient(to left, var(--tw-gradient-stops))',
108 | 'gradient-to-tl': 'linear-gradient(to top left, var(--tw-gradient-stops))'
109 | },
110 | // 背景透明度
111 | backgroundOpacity: theme => theme('opacity'),
112 | // 背景定位
113 | backgroundPosition: {
114 | bottom: 'bottom',
115 | center: 'center',
116 | left: 'left',
117 | 'left-bottom': 'left bottom',
118 | 'left-top': 'left top',
119 | right: 'right',
120 | 'right-bottom': 'right bottom',
121 | 'right-top': 'right top',
122 | top: 'top'
123 | },
124 | // 背景填充方式
125 | backgroundSize: {
126 | auto: 'auto',
127 | cover: 'cover',
128 | contain: 'contain'
129 | },
130 | // 背景模糊度
131 | blur: {
132 | 0: '0',
133 | none: '0',
134 | sm: '4px',
135 | DEFAULT: '8px',
136 | md: '12px',
137 | lg: '16px',
138 | xl: '24px',
139 | '2xl': '40px',
140 | '3xl': '64px'
141 | },
142 | // 亮度过滤器
143 | brightness: {
144 | 0: '0',
145 | 50: '.5',
146 | 75: '.75',
147 | 90: '.9',
148 | 95: '.95',
149 | 100: '1',
150 | 105: '1.05',
151 | 110: '1.1',
152 | 125: '1.25',
153 | 150: '1.5',
154 | 200: '2'
155 | },
156 | // 边框颜色
157 | borderColor: theme => ({
158 | ...theme('colors'),
159 | DEFAULT: theme('colors.gray.200', 'currentColor')
160 | }),
161 | // 边框透明度
162 | borderOpacity: theme => theme('opacity'),
163 | // 边框圆角
164 | borderRadius: {
165 | none: '0px',
166 | sm: '0.125rem',
167 | DEFAULT: '0.25rem',
168 | md: '0.375rem',
169 | lg: '0.5rem',
170 | xl: '0.75rem',
171 | '2xl': '1rem',
172 | '3xl': '1.5rem',
173 | full: '9999px'
174 | },
175 | // 边框宽度
176 | borderWidth: {
177 | DEFAULT: '1px',
178 | 0: '0px',
179 | 2: '2px',
180 | 4: '4px',
181 | 8: '8px'
182 | },
183 | // 盒子阴影
184 | boxShadow: {
185 | sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
186 | DEFAULT: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
187 | md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
188 | lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
189 | xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
190 | '2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
191 | inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)',
192 | none: 'none'
193 | },
194 | // 光标的颜色
195 | caretColor: theme => theme('colors'),
196 | // 对比度过滤器
197 | contrast: {
198 | 0: '0',
199 | 50: '.5',
200 | 75: '.75',
201 | 100: '1',
202 | 125: '1.25',
203 | 150: '1.5',
204 | 200: '2'
205 | },
206 | container: {},
207 | content: {
208 | none: 'none'
209 | },
210 | // 鼠标图标
211 | cursor: {
212 | auto: 'auto',
213 | default: 'default',
214 | pointer: 'pointer',
215 | wait: 'wait',
216 | text: 'text',
217 | move: 'move',
218 | help: 'help',
219 | 'not-allowed': 'not-allowed'
220 | },
221 | divideColor: theme => theme('borderColor'),
222 | divideOpacity: theme => theme('borderOpacity'),
223 | divideWidth: theme => theme('borderWidth'),
224 | dropShadow: {
225 | sm: '0 1px 1px rgba(0,0,0,0.05)',
226 | DEFAULT: ['0 1px 2px rgba(0, 0, 0, 0.1)', '0 1px 1px rgba(0, 0, 0, 0.06)'],
227 | md: ['0 4px 3px rgba(0, 0, 0, 0.07)', '0 2px 2px rgba(0, 0, 0, 0.06)'],
228 | lg: ['0 10px 8px rgba(0, 0, 0, 0.04)', '0 4px 3px rgba(0, 0, 0, 0.1)'],
229 | xl: ['0 20px 13px rgba(0, 0, 0, 0.03)', '0 8px 5px rgba(0, 0, 0, 0.08)'],
230 | '2xl': '0 25px 25px rgba(0, 0, 0, 0.15)',
231 | none: '0 0 #0000'
232 | },
233 | fill: { current: 'currentColor' },
234 | grayscale: {
235 | 0: '0',
236 | DEFAULT: '100%'
237 | },
238 | hueRotate: {
239 | '-180': '-180deg',
240 | '-90': '-90deg',
241 | '-60': '-60deg',
242 | '-30': '-30deg',
243 | '-15': '-15deg',
244 | 0: '0deg',
245 | 15: '15deg',
246 | 30: '30deg',
247 | 60: '60deg',
248 | 90: '90deg',
249 | 180: '180deg'
250 | },
251 | invert: {
252 | 0: '0',
253 | DEFAULT: '100%'
254 | },
255 | flex: {
256 | 1: '1 1 0%',
257 | auto: '1 1 auto',
258 | initial: '0 1 auto',
259 | none: 'none'
260 | },
261 | flexGrow: {
262 | 0: '0',
263 | DEFAULT: '1'
264 | },
265 | flexShrink: {
266 | 0: '0',
267 | DEFAULT: '1'
268 | },
269 | // 字体
270 | fontFamily: {
271 | sans: ['ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', '"Helvetica Neue"', 'Arial', '"Noto Sans"', 'sans-serif', '"Apple Color Emoji"', '"Segoe UI Emoji"', '"Segoe UI Symbol"', '"Noto Color Emoji"'],
272 | serif: ['ui-serif', 'Georgia', 'Cambria', '"Times New Roman"', 'Times', 'serif'],
273 | mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', '"Liberation Mono"', '"Courier New"', 'monospace']
274 | },
275 | // 文字大小
276 | fontSize: {
277 | xs: ['0.75rem', { lineHeight: '1rem' }],
278 | sm: ['0.875rem', { lineHeight: '1.25rem' }],
279 | base: ['1rem', { lineHeight: '1.5rem' }],
280 | lg: ['1.125rem', { lineHeight: '1.75rem' }],
281 | xl: ['1.25rem', { lineHeight: '1.75rem' }],
282 | '2xl': ['1.5rem', { lineHeight: '2rem' }],
283 | '3xl': ['1.875rem', { lineHeight: '2.25rem' }],
284 | '4xl': ['2.25rem', { lineHeight: '2.5rem' }],
285 | '5xl': ['3rem', { lineHeight: '1' }],
286 | '6xl': ['3.75rem', { lineHeight: '1' }],
287 | '7xl': ['4.5rem', { lineHeight: '1' }],
288 | '8xl': ['6rem', { lineHeight: '1' }],
289 | '9xl': ['8rem', { lineHeight: '1' }],
290 | // 文字大小配合px值
291 | '10px': '10px',
292 | '12px': '12px',
293 | '14px': '14px',
294 | '16px': '16px',
295 | '18px': '18px',
296 | '20px': '20px',
297 | '22px': '22px',
298 | '24px': '24px',
299 | '26px': '26px',
300 | '28px': '28px',
301 | '30px': '30px',
302 | '32px': '32px'
303 | },
304 | // 字体粗细
305 | fontWeight: {
306 | thin: '100',
307 | extralight: '200',
308 | light: '300',
309 | normal: '400',
310 | medium: '500',
311 | semibold: '600',
312 | bold: '700',
313 | extrabold: '800',
314 | black: '900'
315 | },
316 | gap: theme => theme('spacing'),
317 | gradientColorStops: theme => theme('colors'),
318 | gridAutoColumns: {
319 | auto: 'auto',
320 | min: 'min-content',
321 | max: 'max-content',
322 | fr: 'minmax(0, 1fr)'
323 | },
324 | gridAutoRows: {
325 | auto: 'auto',
326 | min: 'min-content',
327 | max: 'max-content',
328 | fr: 'minmax(0, 1fr)'
329 | },
330 | gridColumn: {
331 | auto: 'auto',
332 | 'span-1': 'span 1 / span 1',
333 | 'span-2': 'span 2 / span 2',
334 | 'span-3': 'span 3 / span 3',
335 | 'span-4': 'span 4 / span 4',
336 | 'span-5': 'span 5 / span 5',
337 | 'span-6': 'span 6 / span 6',
338 | 'span-7': 'span 7 / span 7',
339 | 'span-8': 'span 8 / span 8',
340 | 'span-9': 'span 9 / span 9',
341 | 'span-10': 'span 10 / span 10',
342 | 'span-11': 'span 11 / span 11',
343 | 'span-12': 'span 12 / span 12',
344 | 'span-full': '1 / -1'
345 | },
346 | gridColumnEnd: {
347 | auto: 'auto',
348 | 1: '1',
349 | 2: '2',
350 | 3: '3',
351 | 4: '4',
352 | 5: '5',
353 | 6: '6',
354 | 7: '7',
355 | 8: '8',
356 | 9: '9',
357 | 10: '10',
358 | 11: '11',
359 | 12: '12',
360 | 13: '13'
361 | },
362 | gridColumnStart: {
363 | auto: 'auto',
364 | 1: '1',
365 | 2: '2',
366 | 3: '3',
367 | 4: '4',
368 | 5: '5',
369 | 6: '6',
370 | 7: '7',
371 | 8: '8',
372 | 9: '9',
373 | 10: '10',
374 | 11: '11',
375 | 12: '12',
376 | 13: '13'
377 | },
378 | gridRow: {
379 | auto: 'auto',
380 | 'span-1': 'span 1 / span 1',
381 | 'span-2': 'span 2 / span 2',
382 | 'span-3': 'span 3 / span 3',
383 | 'span-4': 'span 4 / span 4',
384 | 'span-5': 'span 5 / span 5',
385 | 'span-6': 'span 6 / span 6',
386 | 'span-full': '1 / -1'
387 | },
388 | gridRowStart: {
389 | auto: 'auto',
390 | 1: '1',
391 | 2: '2',
392 | 3: '3',
393 | 4: '4',
394 | 5: '5',
395 | 6: '6',
396 | 7: '7'
397 | },
398 | gridRowEnd: {
399 | auto: 'auto',
400 | 1: '1',
401 | 2: '2',
402 | 3: '3',
403 | 4: '4',
404 | 5: '5',
405 | 6: '6',
406 | 7: '7'
407 | },
408 | gridTemplateColumns: {
409 | none: 'none',
410 | 1: 'repeat(1, minmax(0, 1fr))',
411 | 2: 'repeat(2, minmax(0, 1fr))',
412 | 3: 'repeat(3, minmax(0, 1fr))',
413 | 4: 'repeat(4, minmax(0, 1fr))',
414 | 5: 'repeat(5, minmax(0, 1fr))',
415 | 6: 'repeat(6, minmax(0, 1fr))',
416 | 7: 'repeat(7, minmax(0, 1fr))',
417 | 8: 'repeat(8, minmax(0, 1fr))',
418 | 9: 'repeat(9, minmax(0, 1fr))',
419 | 10: 'repeat(10, minmax(0, 1fr))',
420 | 11: 'repeat(11, minmax(0, 1fr))',
421 | 12: 'repeat(12, minmax(0, 1fr))'
422 | },
423 | gridTemplateRows: {
424 | none: 'none',
425 | 1: 'repeat(1, minmax(0, 1fr))',
426 | 2: 'repeat(2, minmax(0, 1fr))',
427 | 3: 'repeat(3, minmax(0, 1fr))',
428 | 4: 'repeat(4, minmax(0, 1fr))',
429 | 5: 'repeat(5, minmax(0, 1fr))',
430 | 6: 'repeat(6, minmax(0, 1fr))'
431 | },
432 | // 宽度定义
433 | width: theme => ({
434 | auto: 'auto',
435 | ...theme('spacing'),
436 | '1/2': '50%',
437 | '1/3': '33.333333%',
438 | '2/3': '66.666667%',
439 | '1/4': '25%',
440 | '2/4': '50%',
441 | '3/4': '75%',
442 | '1/5': '20%',
443 | '2/5': '40%',
444 | '3/5': '60%',
445 | '4/5': '80%',
446 | '1/6': '16.666667%',
447 | '2/6': '33.333333%',
448 | '3/6': '50%',
449 | '4/6': '66.666667%',
450 | '5/6': '83.333333%',
451 | '1/12': '8.333333%',
452 | '2/12': '16.666667%',
453 | '3/12': '25%',
454 | '4/12': '33.333333%',
455 | '5/12': '41.666667%',
456 | '6/12': '50%',
457 | '7/12': '58.333333%',
458 | '8/12': '66.666667%',
459 | '9/12': '75%',
460 | '10/12': '83.333333%',
461 | '11/12': '91.666667%',
462 | full: '100%',
463 | screen: '100vw',
464 | min: 'min-content',
465 | max: 'max-content'
466 | }),
467 | // 高度配置
468 | height: theme => ({
469 | auto: 'auto',
470 | ...theme('spacing'),
471 | '1/2': '50%',
472 | '1/3': '33.333333%',
473 | '2/3': '66.666667%',
474 | '1/4': '25%',
475 | '2/4': '50%',
476 | '3/4': '75%',
477 | '1/5': '20%',
478 | '2/5': '40%',
479 | '3/5': '60%',
480 | '4/5': '80%',
481 | '1/6': '16.666667%',
482 | '2/6': '33.333333%',
483 | '3/6': '50%',
484 | '4/6': '66.666667%',
485 | '5/6': '83.333333%',
486 | full: '100%',
487 | screen: '100vh'
488 | }),
489 | inset: (theme, { negative }) => ({
490 | auto: 'auto',
491 | full: '100%',
492 | '1/2': '50%',
493 | '1/3': '33.333333%',
494 | '2/3': '66.666667%',
495 | '1/4': '25%',
496 | '2/4': '50%',
497 | '3/4': '75%',
498 | '-1/2': '-50%',
499 | '-1/3': '-33.333333%',
500 | '-2/3': '-66.666667%',
501 | '-1/4': '-25%',
502 | '-2/4': '-50%',
503 | '-3/4': '-75%',
504 | '-full': '-100%',
505 | ...theme('spacing'),
506 | ...negative(theme('spacing'))
507 | }),
508 | keyframes: {
509 | spin: {
510 | to: {
511 | transform: 'rotate(360deg)'
512 | }
513 | },
514 | ping: {
515 | '75%, 100%': {
516 | transform: 'scale(2)',
517 | opacity: '0'
518 | }
519 | },
520 | pulse: {
521 | '50%': {
522 | opacity: '.5'
523 | }
524 | },
525 | bounce: {
526 | '0%, 100%': {
527 | transform: 'translateY(-25%)',
528 | animationTimingFunction: 'cubic-bezier(0.8,0,1,1)'
529 | },
530 | '50%': {
531 | transform: 'none',
532 | animationTimingFunction: 'cubic-bezier(0,0,0.2,1)'
533 | }
534 | }
535 | },
536 | letterSpacing: {
537 | tighter: '-0.05em',
538 | tight: '-0.025em',
539 | normal: '0em',
540 | wide: '0.025em',
541 | wider: '0.05em',
542 | widest: '0.1em'
543 | },
544 | lineHeight: {
545 | none: '1',
546 | tight: '1.25',
547 | snug: '1.375',
548 | normal: '1.5',
549 | relaxed: '1.625',
550 | loose: '2',
551 | 3: '.75rem',
552 | 4: '1rem',
553 | 5: '1.25rem',
554 | 6: '1.5rem',
555 | 7: '1.75rem',
556 | 8: '2rem',
557 | 9: '2.25rem',
558 | 10: '2.5rem'
559 | },
560 | listStyleType: {
561 | none: 'none',
562 | disc: 'disc',
563 | decimal: 'decimal'
564 | },
565 | margin: (theme, { negative }) => ({
566 | auto: 'auto',
567 | ...theme('spacing'),
568 | ...negative(theme('spacing'))
569 | }),
570 | maxHeight: theme => ({
571 | ...theme('spacing'),
572 | full: '100%',
573 | screen: '100vh'
574 | }),
575 | maxWidth: (theme, { breakpoints }) => ({
576 | none: 'none',
577 | 0: '0rem',
578 | xs: '20rem',
579 | sm: '24rem',
580 | md: '28rem',
581 | lg: '32rem',
582 | xl: '36rem',
583 | '2xl': '42rem',
584 | '3xl': '48rem',
585 | '4xl': '56rem',
586 | '5xl': '64rem',
587 | '6xl': '72rem',
588 | '7xl': '80rem',
589 | full: '100%',
590 | min: 'min-content',
591 | max: 'max-content',
592 | prose: '65ch',
593 | ...breakpoints(theme('screens'))
594 | }),
595 | minHeight: {
596 | 0: '0px',
597 | full: '100%',
598 | screen: '100vh'
599 | },
600 | minWidth: {
601 | 0: '0px',
602 | full: '100%',
603 | min: 'min-content',
604 | max: 'max-content'
605 | },
606 | objectPosition: {
607 | bottom: 'bottom',
608 | center: 'center',
609 | left: 'left',
610 | 'left-bottom': 'left bottom',
611 | 'left-top': 'left top',
612 | right: 'right',
613 | 'right-bottom': 'right bottom',
614 | 'right-top': 'right top',
615 | top: 'top'
616 | },
617 | opacity: {
618 | 0: '0',
619 | 5: '0.05',
620 | 10: '0.1',
621 | 20: '0.2',
622 | 25: '0.25',
623 | 30: '0.3',
624 | 40: '0.4',
625 | 50: '0.5',
626 | 60: '0.6',
627 | 70: '0.7',
628 | 75: '0.75',
629 | 80: '0.8',
630 | 90: '0.9',
631 | 95: '0.95',
632 | 100: '1'
633 | },
634 | order: {
635 | first: '-9999',
636 | last: '9999',
637 | none: '0',
638 | 1: '1',
639 | 2: '2',
640 | 3: '3',
641 | 4: '4',
642 | 5: '5',
643 | 6: '6',
644 | 7: '7',
645 | 8: '8',
646 | 9: '9',
647 | 10: '10',
648 | 11: '11',
649 | 12: '12'
650 | },
651 | outline: {
652 | none: ['2px solid transparent', '2px'],
653 | white: ['2px dotted white', '2px'],
654 | black: ['2px dotted black', '2px']
655 | },
656 | padding: theme => theme('spacing'),
657 | placeholderColor: theme => theme('colors'),
658 | placeholderOpacity: theme => theme('opacity'),
659 | ringColor: theme => ({
660 | DEFAULT: theme('colors.blue.500', '#3b82f6'),
661 | ...theme('colors')
662 | }),
663 | ringOffsetColor: theme => theme('colors'),
664 | ringOffsetWidth: {
665 | 0: '0px',
666 | 1: '1px',
667 | 2: '2px',
668 | 4: '4px',
669 | 8: '8px'
670 | },
671 | ringOpacity: theme => ({
672 | DEFAULT: '0.5',
673 | ...theme('opacity')
674 | }),
675 | ringWidth: {
676 | DEFAULT: '3px',
677 | 0: '0px',
678 | 1: '1px',
679 | 2: '2px',
680 | 4: '4px',
681 | 8: '8px'
682 | },
683 | rotate: {
684 | '-180': '-180deg',
685 | '-90': '-90deg',
686 | '-45': '-45deg',
687 | '-12': '-12deg',
688 | '-6': '-6deg',
689 | '-3': '-3deg',
690 | '-2': '-2deg',
691 | '-1': '-1deg',
692 | 0: '0deg',
693 | 1: '1deg',
694 | 2: '2deg',
695 | 3: '3deg',
696 | 6: '6deg',
697 | 12: '12deg',
698 | 45: '45deg',
699 | 90: '90deg',
700 | 180: '180deg'
701 | },
702 | saturate: {
703 | 0: '0',
704 | 50: '.5',
705 | 100: '1',
706 | 150: '1.5',
707 | 200: '2'
708 | },
709 | scale: {
710 | 0: '0',
711 | 50: '.5',
712 | 75: '.75',
713 | 90: '.9',
714 | 95: '.95',
715 | 100: '1',
716 | 105: '1.05',
717 | 110: '1.1',
718 | 125: '1.25',
719 | 150: '1.5'
720 | },
721 | sepia: {
722 | 0: '0',
723 | DEFAULT: '100%'
724 | },
725 | skew: {
726 | '-12': '-12deg',
727 | '-6': '-6deg',
728 | '-3': '-3deg',
729 | '-2': '-2deg',
730 | '-1': '-1deg',
731 | 0: '0deg',
732 | 1: '1deg',
733 | 2: '2deg',
734 | 3: '3deg',
735 | 6: '6deg',
736 | 12: '12deg'
737 | },
738 | space: (theme, { negative }) => ({
739 | ...theme('spacing'),
740 | ...negative(theme('spacing'))
741 | }),
742 | stroke: {
743 | current: 'currentColor'
744 | },
745 | strokeWidth: {
746 | 0: '0',
747 | 1: '1',
748 | 2: '2'
749 | },
750 | textColor: theme => theme('colors'),
751 | textOpacity: theme => theme('opacity'),
752 | transformOrigin: {
753 | center: 'center',
754 | top: 'top',
755 | 'top-right': 'top right',
756 | right: 'right',
757 | 'bottom-right': 'bottom right',
758 | bottom: 'bottom',
759 | 'bottom-left': 'bottom left',
760 | left: 'left',
761 | 'top-left': 'top left'
762 | },
763 | transitionDelay: {
764 | 75: '75ms',
765 | 100: '100ms',
766 | 150: '150ms',
767 | 200: '200ms',
768 | 300: '300ms',
769 | 500: '500ms',
770 | 700: '700ms',
771 | 1000: '1000ms'
772 | },
773 | transitionDuration: {
774 | DEFAULT: '150ms',
775 | 75: '75ms',
776 | 100: '100ms',
777 | 150: '150ms',
778 | 200: '200ms',
779 | 300: '300ms',
780 | 500: '500ms',
781 | 700: '700ms',
782 | 1000: '1000ms'
783 | },
784 | transitionProperty: {
785 | none: 'none',
786 | all: 'all',
787 | DEFAULT: 'background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter',
788 | colors: 'background-color, border-color, color, fill, stroke',
789 | opacity: 'opacity',
790 | shadow: 'box-shadow',
791 | transform: 'transform'
792 | },
793 | transitionTimingFunction: {
794 | DEFAULT: 'cubic-bezier(0.4, 0, 0.2, 1)',
795 | linear: 'linear',
796 | in: 'cubic-bezier(0.4, 0, 1, 1)',
797 | out: 'cubic-bezier(0, 0, 0.2, 1)',
798 | 'in-out': 'cubic-bezier(0.4, 0, 0.2, 1)'
799 | },
800 | translate: (theme, { negative }) => ({
801 | ...theme('spacing'),
802 | ...negative(theme('spacing')),
803 | '1/2': '50%',
804 | '1/3': '33.333333%',
805 | '2/3': '66.666667%',
806 | '1/4': '25%',
807 | '2/4': '50%',
808 | '3/4': '75%',
809 | full: '100%',
810 | '-1/2': '-50%',
811 | '-1/3': '-33.333333%',
812 | '-2/3': '-66.666667%',
813 | '-1/4': '-25%',
814 | '-2/4': '-50%',
815 | '-3/4': '-75%',
816 | '-full': '-100%'
817 | }),
818 | // 层级
819 | zIndex: {
820 | auto: 'auto',
821 | 0: '0',
822 | 10: '10',
823 | 20: '20',
824 | 30: '30',
825 | 40: '40',
826 | 50: '50'
827 | }
828 | },
829 |
830 | // 变体顺序
831 | variantOrder: ['first', 'last', 'odd', 'even', 'visited', 'checked', 'empty', 'read-only', 'group-hover', 'group-focus', 'focus-within', 'hover', 'focus', 'focus-visible', 'active', 'disabled'],
832 | // 变体触发条件
833 | variants: {
834 | accessibility: ['responsive', 'focus-within', 'focus'],
835 | alignContent: ['responsive'],
836 | alignItems: ['responsive'],
837 | alignSelf: ['responsive'],
838 | animation: ['responsive'],
839 | appearance: ['responsive'],
840 | backdropBlur: ['responsive'],
841 | backdropBrightness: ['responsive'],
842 | backdropContrast: ['responsive'],
843 | backdropFilter: ['responsive'],
844 | backdropGrayscale: ['responsive'],
845 | backdropHueRotate: ['responsive'],
846 | backdropInvert: ['responsive'],
847 | backdropOpacity: ['responsive'],
848 | backdropSaturate: ['responsive'],
849 | backdropSepia: ['responsive'],
850 | backgroundAttachment: ['responsive'],
851 | backgroundBlendMode: ['responsive'],
852 | backgroundClip: ['responsive'],
853 | backgroundColor: ['responsive', 'dark', 'group-hover', 'focus-within', 'hover', 'focus'],
854 | backgroundImage: ['responsive'],
855 | backgroundOpacity: ['responsive', 'dark', 'group-hover', 'focus-within', 'hover', 'focus'],
856 | backgroundPosition: ['responsive'],
857 | backgroundRepeat: ['responsive'],
858 | backgroundSize: ['responsive'],
859 | backgroundOrigin: ['responsive'],
860 | blur: ['responsive'],
861 | borderCollapse: ['responsive'],
862 | borderColor: ['responsive', 'dark', 'group-hover', 'focus-within', 'hover', 'focus'],
863 | borderOpacity: ['responsive', 'dark', 'group-hover', 'focus-within', 'hover', 'focus'],
864 | borderRadius: ['responsive'],
865 | borderStyle: ['responsive'],
866 | borderWidth: ['responsive'],
867 | boxDecorationBreak: ['responsive'],
868 | boxShadow: ['responsive', 'group-hover', 'focus-within', 'hover', 'focus'],
869 | boxSizing: ['responsive'],
870 | brightness: ['responsive'],
871 | clear: ['responsive'],
872 | container: ['responsive'],
873 | contrast: ['responsive'],
874 | cursor: ['responsive'],
875 | display: ['responsive'],
876 | divideColor: ['responsive', 'dark'],
877 | divideOpacity: ['responsive', 'dark'],
878 | divideStyle: ['responsive'],
879 | divideWidth: ['responsive'],
880 | dropShadow: ['responsive'],
881 | fill: ['responsive'],
882 | filter: ['responsive'],
883 | flex: ['responsive'],
884 | flexDirection: ['responsive'],
885 | flexGrow: ['responsive'],
886 | flexShrink: ['responsive'],
887 | flexWrap: ['responsive'],
888 | float: ['responsive'],
889 | fontFamily: ['responsive'],
890 | fontSize: ['responsive'],
891 | fontSmoothing: ['responsive'],
892 | fontStyle: ['responsive'],
893 | fontVariantNumeric: ['responsive'],
894 | fontWeight: ['responsive'],
895 | gap: ['responsive'],
896 | gradientColorStops: ['responsive', 'dark', 'hover', 'focus'],
897 | grayscale: ['responsive'],
898 | gridAutoColumns: ['responsive'],
899 | gridAutoFlow: ['responsive'],
900 | gridAutoRows: ['responsive'],
901 | gridColumn: ['responsive'],
902 | gridColumnEnd: ['responsive'],
903 | gridColumnStart: ['responsive'],
904 | gridRow: ['responsive'],
905 | gridRowEnd: ['responsive'],
906 | gridRowStart: ['responsive'],
907 | gridTemplateColumns: ['responsive'],
908 | gridTemplateRows: ['responsive'],
909 | height: ['responsive'],
910 | hueRotate: ['responsive'],
911 | inset: ['responsive'],
912 | invert: ['responsive'],
913 | isolation: ['responsive'],
914 | justifyContent: ['responsive'],
915 | justifyItems: ['responsive'],
916 | justifySelf: ['responsive'],
917 | letterSpacing: ['responsive'],
918 | lineHeight: ['responsive'],
919 | listStylePosition: ['responsive'],
920 | listStyleType: ['responsive'],
921 | margin: ['responsive', 'first', 'last', 'hover'],
922 | maxHeight: ['responsive'],
923 | maxWidth: ['responsive'],
924 | minHeight: ['responsive'],
925 | minWidth: ['responsive'],
926 | mixBlendMode: ['responsive'],
927 | objectFit: ['responsive'],
928 | objectPosition: ['responsive'],
929 | opacity: ['responsive', 'group-hover', 'focus-within', 'hover', 'focus'],
930 | order: ['responsive'],
931 | outline: ['responsive', 'focus-within', 'focus'],
932 | overflow: ['responsive'],
933 | overscrollBehavior: ['responsive'],
934 | padding: ['responsive'],
935 | placeContent: ['responsive'],
936 | placeItems: ['responsive'],
937 | placeSelf: ['responsive'],
938 | placeholderColor: ['responsive', 'dark', 'focus'],
939 | placeholderOpacity: ['responsive', 'dark', 'focus'],
940 | pointerEvents: ['responsive'],
941 | position: ['responsive'],
942 | resize: ['responsive'],
943 | ringColor: ['responsive', 'dark', 'focus-within', 'focus'],
944 | ringOffsetColor: ['responsive', 'dark', 'focus-within', 'focus'],
945 | ringOffsetWidth: ['responsive', 'focus-within', 'focus'],
946 | ringOpacity: ['responsive', 'dark', 'focus-within', 'focus'],
947 | ringWidth: ['responsive', 'focus-within', 'focus'],
948 | rotate: ['responsive', 'hover', 'focus'],
949 | saturate: ['responsive'],
950 | scale: ['responsive', 'hover', 'focus'],
951 | sepia: ['responsive'],
952 | skew: ['responsive', 'hover', 'focus'],
953 | space: ['responsive'],
954 | stroke: ['responsive'],
955 | strokeWidth: ['responsive'],
956 | tableLayout: ['responsive'],
957 | textAlign: ['responsive'],
958 | textColor: ['responsive', 'dark', 'group-hover', 'focus-within', 'hover', 'focus'],
959 | textDecoration: ['responsive', 'group-hover', 'focus-within', 'hover', 'focus'],
960 | textOpacity: ['responsive', 'dark', 'group-hover', 'focus-within', 'hover', 'focus'],
961 | textOverflow: ['responsive'],
962 | textTransform: ['responsive'],
963 | transform: ['responsive'],
964 | transformOrigin: ['responsive'],
965 | transitionDelay: ['responsive'],
966 | transitionDuration: ['responsive'],
967 | transitionProperty: ['responsive'],
968 | transitionTimingFunction: ['responsive'],
969 | translate: ['responsive', 'hover', 'focus'],
970 | userSelect: ['responsive'],
971 | verticalAlign: ['responsive'],
972 | visibility: ['responsive'],
973 | whitespace: ['responsive'],
974 | width: ['responsive'],
975 | wordBreak: ['responsive'],
976 | zIndex: ['responsive', 'focus-within', 'focus']
977 | },
978 | plugins: []
979 | }
980 |
--------------------------------------------------------------------------------