Compare commits

..

No commits in common. "0d48cc85918ba9a49210073cb7db16ea3ce2267c" and "864e482210ce59812a0d2dfd662b30cf11f4a039" have entirely different histories.

175 changed files with 3659 additions and 11603 deletions

View File

@ -9,7 +9,6 @@ import rehypeExternalLinks from "rehype-external-links";
import sitemap from "@astrojs/sitemap"; import sitemap from "@astrojs/sitemap";
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import swup from "@swup/astro"
import { SITE_URL } from "./src/consts"; import { SITE_URL } from "./src/consts";
import vercel from "@astrojs/vercel"; import vercel from "@astrojs/vercel";
@ -89,7 +88,6 @@ export default defineConfig({
}, },
gfm: true gfm: true
}), }),
swup(),
react(), react(),
sitemap({ sitemap({
filter: (page) => !page.includes("/api/"), filter: (page) => !page.includes("/api/"),

7099
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,24 +9,23 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"@astrojs/mdx": "^4.2.4", "@astrojs/mdx": "^4.2.2",
"@astrojs/node": "^9.2.0", "@astrojs/node": "^9.1.3",
"@astrojs/react": "^4.2.4", "@astrojs/react": "^4.2.2",
"@astrojs/sitemap": "^3.3.0", "@astrojs/sitemap": "^3.3.0",
"@astrojs/vercel": "^8.1.3", "@astrojs/vercel": "^8.1.3",
"@swup/astro": "^1.6.0", "@tailwindcss/vite": "^4.0.9",
"@tailwindcss/vite": "^4.1.4", "@types/react": "^19.0.10",
"@types/react": "^19.1.2", "@types/react-dom": "^19.0.4",
"@types/react-dom": "^19.1.2",
"@types/three": "^0.174.0", "@types/three": "^0.174.0",
"astro": "^5.7.4", "astro": "^5.5.5",
"cheerio": "^1.0.0", "cheerio": "^1.0.0-rc.12",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.0",
"octokit": "^3.2.1", "octokit": "^3.1.2",
"react": "^19.1.0", "react": "^19.0.0",
"react-dom": "^19.1.0", "react-dom": "^19.0.0",
"react-masonry-css": "^1.0.16", "react-masonry-css": "^1.0.16",
"tailwindcss": "^4.1.4", "tailwindcss": "^4.0.9",
"three": "^0.174.0" "three": "^0.174.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -0,0 +1,69 @@
---
// 面包屑导航组件
// 接收当前页面类型、路径段和标签过滤器作为参数
interface Breadcrumb {
name: string;
path: string;
}
export interface Props {
pageType: 'articles' | 'article' | 'timeline'; // 页面类型
pathSegments?: string[]; // 路径段数组
tagFilter?: string; // 标签过滤器
articleTitle?: string; // 文章标题(仅在文章详情页使用)
}
const { pageType, pathSegments = [], tagFilter = '', articleTitle = '' } = Astro.props;
// 将路径段转换为面包屑对象
const breadcrumbs: Breadcrumb[] = pathSegments
.filter(segment => segment.trim() !== '')
.map((segment, index, array) => {
const path = array.slice(0, index + 1).join('/');
return { name: segment, path };
});
---
<div class="flex items-center text-sm">
<!-- 文章列表链接 -->
<a href="/articles" class="text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd" />
</svg>
文章
</a>
{/* 标签过滤 */}
{tagFilter && (
<>
<span class="mx-2 text-secondary-300 dark:text-secondary-600">/</span>
<span class="text-secondary-600 dark:text-secondary-400 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M17.707 9.293a1 1 0 010 1.414l-7 7a1 1 0 01-1.414 0l-7-7A.997.997 0 012 10V5a3 3 0 013-3h5c.256 0 .512.098.707.293l7 7zM5 6a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
</svg>
{tagFilter}
</span>
</>
)}
{/* 目录路径 */}
{!tagFilter && breadcrumbs.map((crumb: Breadcrumb, index: number) => {
const crumbPath = breadcrumbs.slice(0, index + 1).map((b: Breadcrumb) => b.name).join('/');
return (
<span>
<span class="mx-2 text-secondary-300 dark:text-secondary-600">/</span>
<a href={`/articles?path=${encodeURIComponent(crumbPath)}`} class="text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400">{crumb.name}</a>
</span>
);
})}
{/* 文章标题 */}
{pageType === 'article' && articleTitle && (
<>
<span class="mx-2 text-secondary-300 dark:text-secondary-600">/</span>
<span class="text-secondary-600 dark:text-secondary-400 truncate max-w-[150px] sm:max-w-[300px]">{articleTitle}</span>
</>
)}
</div>

View File

@ -1,72 +0,0 @@
import React from 'react';
interface Breadcrumb {
name: string;
path: string;
}
export interface BreadcrumbProps {
pageType: 'articles' | 'article' | 'timeline'; // 页面类型
pathSegments?: string[]; // 路径段数组
tagFilter?: string; // 标签过滤器
articleTitle?: string; // 文章标题(仅在文章详情页使用)
}
export function Breadcrumb({
pageType,
pathSegments = [],
tagFilter = '',
articleTitle = ''
}: BreadcrumbProps) {
// 将路径段转换为面包屑对象
const breadcrumbs: Breadcrumb[] = pathSegments
.filter(segment => segment.trim() !== '')
.map((segment, index, array) => {
const path = array.slice(0, index + 1).join('/');
return { name: segment, path };
});
return (
<div className="flex items-center text-sm">
{/* 文章列表链接 */}
<a href="/articles" className="text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clipRule="evenodd" />
</svg>
</a>
{/* 标签过滤 */}
{tagFilter && (
<>
<span className="mx-2 text-secondary-300 dark:text-secondary-600">/</span>
<span className="text-secondary-600 dark:text-secondary-400 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M17.707 9.293a1 1 0 010 1.414l-7 7a1 1 0 01-1.414 0l-7-7A.997.997 0 012 10V5a3 3 0 013-3h5c.256 0 .512.098.707.293l7 7zM5 6a1 1 0 100-2 1 1 0 000 2z" clipRule="evenodd" />
</svg>
{tagFilter}
</span>
</>
)}
{/* 目录路径 */}
{!tagFilter && breadcrumbs.map((crumb: Breadcrumb, index: number) => {
const crumbPath = breadcrumbs.slice(0, index + 1).map((b: Breadcrumb) => b.name).join('/');
return (
<span key={`crumb-${index}`}>
<span className="mx-2 text-secondary-300 dark:text-secondary-600">/</span>
<a href={`/articles?path=${encodeURIComponent(crumbPath)}`} className="text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400">{crumb.name}</a>
</span>
);
})}
{/* 文章标题 */}
{pageType === 'article' && articleTitle && (
<>
<span className="mx-2 text-secondary-300 dark:text-secondary-600">/</span>
<span className="text-secondary-600 dark:text-secondary-400 truncate max-w-[150px] sm:max-w-[300px]">{articleTitle}</span>
</>
)}
</div>
);
}

View File

@ -1,101 +1,34 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect } from 'react';
interface CountdownProps { interface CountdownProps {
targetDate: string; // 目标日期,格式:'YYYY-MM-DD' targetDate: string; // 目标日期,格式:'YYYY-MM-DD'
className?: string; // 自定义类名
} }
interface TimeLeft { export const Countdown: React.FC<CountdownProps> = ({ targetDate }) => {
days: number; const [timeLeft, setTimeLeft] = useState({
hours: number;
minutes: number;
seconds: number;
expired: boolean;
}
export const Countdown: React.FC<CountdownProps> = ({ targetDate, className = '' }) => {
const [timeLeft, setTimeLeft] = useState<TimeLeft>({
days: 0, days: 0,
hours: 0, hours: 0,
minutes: 0, minutes: 0,
seconds: 0, seconds: 0
expired: false
}); });
const timerRef = useRef<number | null>(null);
useEffect(() => { useEffect(() => {
const calculateTimeLeft = () => { const timer = setInterval(() => {
try {
const now = new Date().getTime(); const now = new Date().getTime();
const target = new Date(targetDate).getTime(); const target = new Date(targetDate).getTime();
// 检查目标日期是否有效
if (isNaN(target)) {
console.error(`无效的目标日期: ${targetDate}`);
return {
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
expired: true
};
}
const difference = target - now; const difference = target - now;
const expired = difference <= 0;
if (expired) {
return {
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
expired: true
};
}
if (difference > 0) {
const days = Math.floor(difference / (1000 * 60 * 60 * 24)); const days = Math.floor(difference / (1000 * 60 * 60 * 24));
const hours = Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const hours = Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)); const minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((difference % (1000 * 60)) / 1000); const seconds = Math.floor((difference % (1000 * 60)) / 1000);
return { days, hours, minutes, seconds, expired: false }; setTimeLeft({ days, hours, minutes, seconds });
} catch (error) {
console.error('计算倒计时发生错误:', error);
return {
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
expired: true
};
}
};
// 立即计算一次时间
setTimeLeft(calculateTimeLeft());
// 设置定时器
timerRef.current = window.setInterval(() => {
const newTimeLeft = calculateTimeLeft();
setTimeLeft(newTimeLeft);
// 如果已经到期,清除计时器
if (newTimeLeft.expired) {
if (timerRef.current !== null) {
clearInterval(timerRef.current);
timerRef.current = null;
}
} }
}, 1000); }, 1000);
// 清理函数 return () => clearInterval(timer);
return () => {
if (timerRef.current !== null) {
clearInterval(timerRef.current);
timerRef.current = null;
}
};
}, [targetDate]); }, [targetDate]);
const TimeBox = ({ value, label }: { value: number; label: string }) => ( const TimeBox = ({ value, label }: { value: number; label: string }) => (
@ -103,20 +36,12 @@ export const Countdown: React.FC<CountdownProps> = ({ targetDate, className = ''
<div className="text-4xl font-light"> <div className="text-4xl font-light">
{value.toString().padStart(2, '0')} {value.toString().padStart(2, '0')}
</div> </div>
<div className="text-sm mt-1 text-gray-500 dark:text-gray-400">{label}</div> <div className="text-sm mt-1 text-gray-500">{label}</div>
</div> </div>
); );
if (timeLeft.expired) {
return ( return (
<div className={`text-center ${className}`}> <div className="flex items-center justify-center">
<div className="text-xl text-gray-500 dark:text-gray-400"></div>
</div>
);
}
return (
<div className={`flex items-center justify-center ${className}`}>
<TimeBox value={timeLeft.days} label="天" /> <TimeBox value={timeLeft.days} label="天" />
<TimeBox value={timeLeft.hours} label="时" /> <TimeBox value={timeLeft.hours} label="时" />
<TimeBox value={timeLeft.minutes} label="分" /> <TimeBox value={timeLeft.minutes} label="分" />

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback, useRef } from 'react'; import React, { useState, useEffect } from 'react';
import ReactMasonryCss from 'react-masonry-css'; import ReactMasonryCss from 'react-masonry-css';
interface DoubanItem { interface DoubanItem {
@ -20,138 +20,73 @@ interface Pagination {
interface DoubanCollectionProps { interface DoubanCollectionProps {
type: 'movie' | 'book'; type: 'movie' | 'book';
doubanId?: string; // 可选参数,使其与 MediaGrid 保持一致
className?: string; // 添加自定义类名
} }
const DoubanCollection: React.FC<DoubanCollectionProps> = ({ type, doubanId, className = '' }) => { const DoubanCollection: React.FC<DoubanCollectionProps> = ({ type }) => {
const [items, setItems] = useState<DoubanItem[]>([]); const [items, setItems] = useState<DoubanItem[]>([]);
const [pagination, setPagination] = useState<Pagination>({ current: 1, total: 1, hasNext: false, hasPrev: false }); const [pagination, setPagination] = useState<Pagination>({ current: 1, total: 1, hasNext: false, hasPrev: false });
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isPageChanging, setIsPageChanging] = useState(false); const [isPageChanging, setIsPageChanging] = useState(false);
// 使用 ref 避免竞态条件 const fetchData = async (start = 0) => {
const abortControllerRef = useRef<AbortController | null>(null);
const isMountedRef = useRef(true);
const fetchData = useCallback(async (start = 0) => {
// 如果已经有一个请求在进行中,取消它
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
// 创建新的 AbortController
abortControllerRef.current = new AbortController();
setLoading(true); setLoading(true);
setError(null);
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append('type', type); params.append('type', type);
params.append('start', start.toString()); params.append('start', start.toString());
if (doubanId) {
params.append('doubanId', doubanId);
}
const url = `/api/douban?${params.toString()}`; const url = `/api/douban?${params.toString()}`;
try { try {
const response = await fetch(url, { const response = await fetch(url);
signal: abortControllerRef.current.signal
});
// 如果组件已卸载,不继续处理
if (!isMountedRef.current) return;
if (!response.ok) { if (!response.ok) {
throw new Error(`获取数据失败:状态码 ${response.status}`); throw new Error('获取数据失败');
} }
const data = await response.json(); const data = await response.json();
setItems(data.items);
if (data.error) { setPagination(data.pagination);
throw new Error(data.error);
}
setItems(data.items || []);
setPagination(data.pagination || { current: 1, total: 1, hasNext: false, hasPrev: false });
} catch (err) { } catch (err) {
// 如果是取消请求的错误,不设置错误状态
if (err instanceof Error && err.name === 'AbortError') {
return;
}
// 如果组件已卸载,不设置状态
if (!isMountedRef.current) return;
console.error('获取豆瓣数据失败:', err);
setError(err instanceof Error ? err.message : '未知错误'); setError(err instanceof Error ? err.message : '未知错误');
setItems([]);
} finally { } finally {
// 如果组件已卸载,不设置状态
if (!isMountedRef.current) return;
setLoading(false); setLoading(false);
setIsPageChanging(false);
}
}, [type, doubanId]);
useEffect(() => {
// 组件挂载时设置标记
isMountedRef.current = true;
fetchData();
// 组件卸载时清理
return () => {
isMountedRef.current = false;
if (abortControllerRef.current) {
abortControllerRef.current.abort();
} }
}; };
}, [fetchData]);
const handlePageChange = useCallback((page: number) => { useEffect(() => {
if (isPageChanging) return; fetchData();
}, [type]);
setIsPageChanging(true); const handlePageChange = (page: number) => {
// 计算新页面的起始项
const start = (page - 1) * 15; const start = (page - 1) * 15;
// 更新分页状态 // 手动更新分页状态不等待API响应
setPagination(prev => ({ setPagination(prev => ({
...prev, ...prev,
current: page current: page
})); }));
// 清空当前项目,显示加载状态 // 重置当前状态,显示加载中
setItems([]); setItems([]);
setLoading(true); setLoading(true);
// 获取新页面的数据
fetchData(start); fetchData(start);
}, [fetchData, isPageChanging]); };
const renderStars = useCallback((rating: number) => { const renderStars = (rating: number) => {
return ( return (
<div className="flex"> <div className="flex">
{[1, 2, 3, 4, 5].map((star) => ( {[1, 2, 3, 4, 5].map((star) => (
<svg <svg
key={star} key={star}
className={`w-4 h-4 ${star <= rating ? 'text-accent-400' : 'text-secondary-300 dark:text-secondary-600'}`} className={`w-4 h-4 ${star <= rating ? 'text-accent-400' : 'text-secondary-300'}`}
fill="currentColor" fill="currentColor"
viewBox="0 0 20 20" viewBox="0 0 20 20"
aria-hidden="true"
> >
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" /> <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg> </svg>
))} ))}
</div> </div>
); );
}, []); };
const breakpointColumnsObj = { const breakpointColumnsObj = {
default: 3, default: 3,
@ -159,65 +94,17 @@ const DoubanCollection: React.FC<DoubanCollectionProps> = ({ type, doubanId, cla
700: 1 700: 1
}; };
// 加载中状态
if (loading && items.length === 0) { if (loading && items.length === 0) {
return ( return <div className="flex justify-center p-8">...</div>;
<div className={`douban-collection ${className}`}>
<h2 className="text-2xl font-bold mb-6 text-primary-700 dark:text-primary-400">
{type === 'movie' ? '观影记录' : '读书记录'}
</h2>
<div className="flex justify-center items-center p-8">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-current border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite]"></div>
<p className="ml-2 text-gray-600 dark:text-gray-400">...</p>
</div>
</div>
);
} }
// 错误状态
if (error) { if (error) {
return ( return <div className="text-red-500 p-4">: {error}</div>;
<div className={`douban-collection ${className}`}>
<h2 className="text-2xl font-bold mb-6 text-primary-700 dark:text-primary-400">
{type === 'movie' ? '观影记录' : '读书记录'}
</h2>
<div className="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-4 rounded-lg border border-red-200 dark:border-red-800">
<div className="flex items-center">
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<p>: {error}</p>
</div>
<button
onClick={() => fetchData()}
className="mt-3 px-4 py-2 bg-red-100 dark:bg-red-800/30 hover:bg-red-200 dark:hover:bg-red-800/50 text-red-700 dark:text-red-300 rounded"
>
</button>
</div>
</div>
);
}
// 数据为空状态
if (items.length === 0) {
return (
<div className={`douban-collection ${className}`}>
<h2 className="text-2xl font-bold mb-6 text-primary-700 dark:text-primary-400">
{type === 'movie' ? '观影记录' : '读书记录'}
</h2>
<div className="text-center p-8 text-gray-500 dark:text-gray-400">
{type === 'movie' ? '观影' : '读书'}
</div>
</div>
);
} }
return ( return (
<div className={`douban-collection ${className}`}> <div className="douban-collection">
<h2 className="text-2xl font-bold mb-6 text-primary-700 dark:text-primary-400"> <h2 className="text-2xl font-bold mb-6 text-primary-700">{type === 'movie' ? '观影记录' : '读书记录'}</h2>
{type === 'movie' ? '观影记录' : '读书记录'}
</h2>
<ReactMasonryCss <ReactMasonryCss
breakpointCols={breakpointColumnsObj} breakpointCols={breakpointColumnsObj}
@ -225,32 +112,23 @@ const DoubanCollection: React.FC<DoubanCollectionProps> = ({ type, doubanId, cla
columnClassName="pl-4 bg-clip-padding" columnClassName="pl-4 bg-clip-padding"
> >
{items.map((item, index) => ( {items.map((item, index) => (
<div <div key={index} className="mb-6 bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
key={`${item.title}-${index}`}
className="mb-6 bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg"
>
<a href={item.link} target="_blank" rel="noopener noreferrer" className="block"> <a href={item.link} target="_blank" rel="noopener noreferrer" className="block">
<div className="relative pb-[140%] overflow-hidden"> <div className="relative pb-[140%] overflow-hidden">
<img <img
src={item.imageUrl} src={item.imageUrl}
alt={item.title} alt={item.title}
className="absolute inset-0 w-full h-full object-cover hover:scale-105" className="absolute inset-0 w-full h-full object-cover transition-transform duration-300 hover:scale-105"
loading="lazy"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.onerror = null;
target.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYwIiBoZWlnaHQ9IjIyNCIgdmlld0JveD0iMCAwIDE2MCAyMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjE2MCIgaGVpZ2h0PSIyMjQiIGZpbGw9IiNmMWYxZjEiLz48dGV4dCB4PSI4MCIgeT0iMTEyIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM5OTk5OTkiPuWbuuWumuWbvueJh+acquivu+WPlzwvdGV4dD48L3N2Zz4=';
}}
/> />
</div> </div>
<div className="p-4"> <div className="p-4">
<h3 className="font-bold text-lg mb-1 line-clamp-1 text-primary-800 dark:text-primary-300">{item.title}</h3> <h3 className="font-bold text-lg mb-1 line-clamp-1 text-primary-800">{item.title}</h3>
{item.subtitle && <p className="text-secondary-600 dark:text-secondary-400 text-sm mb-2 line-clamp-1">{item.subtitle}</p>} {item.subtitle && <p className="text-secondary-600 text-sm mb-2 line-clamp-1">{item.subtitle}</p>}
<div className="flex justify-between items-center mb-2"> <div className="flex justify-between items-center mb-2">
{renderStars(item.rating)} {renderStars(item.rating)}
<span className="text-sm text-secondary-500 dark:text-secondary-400">{item.date}</span> <span className="text-sm text-secondary-500">{item.date}</span>
</div> </div>
<p className="text-secondary-700 dark:text-secondary-300 text-sm line-clamp-3">{item.intro}</p> <p className="text-secondary-700 text-sm line-clamp-3">{item.intro}</p>
</div> </div>
</a> </a>
</div> </div>
@ -261,45 +139,67 @@ const DoubanCollection: React.FC<DoubanCollectionProps> = ({ type, doubanId, cla
{pagination.total > 1 && ( {pagination.total > 1 && (
<div className="flex justify-center mt-8 space-x-2"> <div className="flex justify-center mt-8 space-x-2">
<button <button
onClick={() => handlePageChange(pagination.current - 1)} onClick={(e) => {
e.preventDefault();
if (isPageChanging) return;
const prevPage = pagination.current - 1;
if (prevPage > 0) {
setIsPageChanging(true);
const prevStart = (prevPage - 1) * 15;
// 直接调用fetchData
fetchData(prevStart);
// 手动更新分页状态
setPagination(prev => ({
...prev,
current: prevPage
}));
setTimeout(() => setIsPageChanging(false), 2000);
}
}}
disabled={!pagination.hasPrev || pagination.current <= 1 || isPageChanging} disabled={!pagination.hasPrev || pagination.current <= 1 || isPageChanging}
className={`px-4 py-2 rounded ${!pagination.hasPrev || pagination.current <= 1 || isPageChanging className={`px-4 py-2 rounded ${!pagination.hasPrev || pagination.current <= 1 || isPageChanging ? 'bg-secondary-200 text-secondary-500 cursor-not-allowed' : 'bg-primary-600 text-white hover:bg-primary-700'}`}
? 'bg-secondary-200 dark:bg-secondary-700 text-secondary-500 dark:text-secondary-500 cursor-not-allowed'
: 'bg-primary-600 text-white hover:bg-primary-700 dark:bg-primary-700 dark:hover:bg-primary-600'}`}
aria-label="上一页"
> >
{isPageChanging ? ( {isPageChanging ? '加载中...' : '上一页'}
<span className="flex items-center">
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</span>
) : '上一页'}
</button> </button>
<span className="px-4 py-2 bg-secondary-100 dark:bg-secondary-800 rounded"> <span className="px-4 py-2 bg-secondary-100 rounded">
{pagination.current} / {pagination.total} {pagination.current} / {pagination.total}
</span> </span>
<button <button
onClick={() => handlePageChange(pagination.current + 1)} onClick={(e) => {
e.preventDefault(); // 防止默认行为
if (isPageChanging) return;
// 明确记录当前操作
const nextPage = pagination.current + 1;
// 直接使用明确的页码而不是依赖state
if (pagination.current < pagination.total) {
setIsPageChanging(true);
const nextStart = (nextPage - 1) * 15; // 修正计算方式
// 直接调用fetchData而不是通过handlePageChange
fetchData(nextStart);
// 手动更新分页状态
setPagination(prev => ({
...prev,
current: nextPage
}));
setTimeout(() => setIsPageChanging(false), 2000);
}
}}
disabled={!pagination.hasNext || pagination.current >= pagination.total || isPageChanging} disabled={!pagination.hasNext || pagination.current >= pagination.total || isPageChanging}
className={`px-4 py-2 rounded ${!pagination.hasNext || pagination.current >= pagination.total || isPageChanging className={`px-4 py-2 rounded ${!pagination.hasNext || pagination.current >= pagination.total || isPageChanging ? 'bg-secondary-200 text-secondary-500 cursor-not-allowed' : 'bg-primary-600 text-white hover:bg-primary-700'}`}
? 'bg-secondary-200 dark:bg-secondary-700 text-secondary-500 dark:text-secondary-500 cursor-not-allowed'
: 'bg-primary-600 text-white hover:bg-primary-700 dark:bg-primary-700 dark:hover:bg-primary-600'}`}
aria-label="下一页"
> >
{isPageChanging ? ( {isPageChanging ? '加载中...' : '下一页'}
<span className="flex items-center">
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</span>
) : '下一页'}
</button> </button>
</div> </div>
)} )}

View File

@ -0,0 +1,58 @@
---
interface Props {
icp?: string;
psbIcp?: string;
psbIcpUrl?: string;
}
const {
icp = "",
psbIcp = "",
psbIcpUrl = "http://www.beian.gov.cn/portal/registerSystemInfo",
} = Astro.props;
const currentYear = new Date().getFullYear();
---
<footer class="w-full py-6 px-4 bg-gray-50 dark:bg-dark-bg border-t border-gray-200 dark:border-gray-800 mt-auto">
<div class="max-w-5xl mx-auto flex flex-col items-center justify-center space-y-4">
<div class="flex flex-wrap items-center justify-center gap-4 text-sm text-gray-600 dark:text-gray-400">
{icp && (
<a
href="https://beian.miit.gov.cn/"
target="_blank"
rel="noopener noreferrer"
class="hover:text-primary-600 dark:hover:text-primary-400 transition-colors"
>
{icp}
</a>
)}
{psbIcp && (
<a
href={psbIcpUrl}
target="_blank"
rel="noopener noreferrer"
class="flex items-center hover:text-primary-600 dark:hover:text-primary-400 transition-colors"
>
<img src="/images/national.png" alt="公安备案" class="h-4 mr-1" />
{psbIcp}
</a>
)}
</div>
<div class="text-sm text-gray-500 dark:text-gray-500 font-light flex items-center gap-2">
<a href="https://blog.lsy22.com" class="hover:text-primary-600 dark:hover:text-primary-400 transition-colors">© {currentYear} New Echoes. All rights reserved.</a>
<span>·</span>
<a
href="/sitemap-index.xml"
target="_blank"
rel="noopener noreferrer"
class="hover:text-primary-600 dark:hover:text-primary-400 transition-colors"
>
Sitemap
</a>
</div>
</div>
</footer>

View File

@ -1,65 +0,0 @@
import React from 'react';
interface FooterProps {
icp?: string;
psbIcp?: string;
psbIcpUrl?: string;
}
export function Footer({
icp = "",
psbIcp = "",
psbIcpUrl = "http://www.beian.gov.cn/portal/registerSystemInfo",
}: FooterProps) {
const currentYear = new Date().getFullYear();
return (
<footer className="w-full py-6 px-4 bg-gray-50 dark:bg-dark-bg border-t border-gray-200 dark:border-gray-800 mt-auto">
<div className="max-w-5xl mx-auto flex flex-col items-center justify-center space-y-4">
<div className="flex flex-wrap items-center justify-center gap-4 text-sm text-gray-600 dark:text-gray-400">
{icp && (
<a
href="https://beian.miit.gov.cn/"
target="_blank"
rel="noopener noreferrer"
className="hover:text-primary-600 dark:hover:text-primary-400"
aria-label="工信部备案信息"
>
{icp}
</a>
)}
{psbIcp && (
<a
href={psbIcpUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center hover:text-primary-600 dark:hover:text-primary-400"
aria-label="公安部备案信息"
>
<img src="/images/national.png" alt="公安备案" className="h-4 mr-1" width="14" height="16" loading="lazy" />
{psbIcp}
</a>
)}
</div>
<div className="text-sm text-gray-500 dark:text-gray-500 font-light flex flex-wrap items-center justify-center gap-2">
<a href="https://blog.lsy22.com" className="hover:text-primary-600 dark:hover:text-primary-400">
© {currentYear} New Echoes. All rights reserved.
</a>
<span aria-hidden="true" className="hidden sm:inline">·</span>
<a
href="/sitemap-index.xml"
target="_blank"
rel="noopener noreferrer"
className="hover:text-primary-600 dark:hover:text-primary-400"
aria-label="网站地图"
>
Sitemap
</a>
</div>
</div>
</footer>
);
}

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback, useRef } from 'react'; import React, { useState, useEffect } from 'react';
import ReactMasonryCss from 'react-masonry-css'; import ReactMasonryCss from 'react-masonry-css';
// Git 平台类型枚举 // Git 平台类型枚举
@ -59,7 +59,6 @@ interface GitProjectCollectionProps {
token?: string; token?: string;
perPage?: number; perPage?: number;
url?: string; url?: string;
className?: string; // 添加自定义类名
} }
const GitProjectCollection: React.FC<GitProjectCollectionProps> = ({ const GitProjectCollection: React.FC<GitProjectCollectionProps> = ({
@ -69,8 +68,7 @@ const GitProjectCollection: React.FC<GitProjectCollectionProps> = ({
title, title,
token, token,
perPage = DEFAULT_GIT_CONFIG.perPage, perPage = DEFAULT_GIT_CONFIG.perPage,
url, url
className = ''
}) => { }) => {
const [projects, setProjects] = useState<GitProject[]>([]); const [projects, setProjects] = useState<GitProject[]>([]);
const [pagination, setPagination] = useState<Pagination>({ current: 1, total: 1, hasNext: false, hasPrev: false }); const [pagination, setPagination] = useState<Pagination>({ current: 1, total: 1, hasNext: false, hasPrev: false });
@ -78,21 +76,8 @@ const GitProjectCollection: React.FC<GitProjectCollectionProps> = ({
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isPageChanging, setIsPageChanging] = useState(false); const [isPageChanging, setIsPageChanging] = useState(false);
// 使用 ref 跟踪组件挂载状态 const fetchData = async (page = 1) => {
const isMountedRef = useRef(true);
const abortControllerRef = useRef<AbortController | null>(null);
const fetchData = useCallback(async (page = 1) => {
// 取消可能存在的之前的请求
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
// 创建新的 AbortController
abortControllerRef.current = new AbortController();
setLoading(true); setLoading(true);
setError(null);
if (!platform || !Object.values(GitPlatform).includes(platform)) { if (!platform || !Object.values(GitPlatform).includes(platform)) {
setError('无效的平台参数'); setError('无效的平台参数');
@ -128,98 +113,65 @@ const GitProjectCollection: React.FC<GitProjectCollectionProps> = ({
method: 'GET', method: 'GET',
headers: { headers: {
'Accept': 'application/json' 'Accept': 'application/json'
}, }
signal: abortControllerRef.current.signal
}); });
// 如果组件已卸载,不继续更新状态
if (!isMountedRef.current) return;
if (!response.ok) { if (!response.ok) {
const errorData = await response.json(); const errorData = await response.json();
throw new Error(`请求失败: ${response.status} ${response.statusText}\n${JSON.stringify(errorData, null, 2)}`); throw new Error(`请求失败: ${response.status} ${response.statusText}\n${JSON.stringify(errorData, null, 2)}`);
} }
const data = await response.json(); const data = await response.json();
setProjects(data.projects);
// 如果组件已卸载,不继续更新状态 setPagination(data.pagination);
if (!isMountedRef.current) return;
setProjects(data.projects || []);
setPagination(data.pagination || { current: page, total: 1, hasNext: false, hasPrev: page > 1 });
} catch (err) { } catch (err) {
// 如果是取消的请求,不显示错误
if (err instanceof Error && err.name === 'AbortError') {
return;
}
// 如果组件已卸载,不继续更新状态
if (!isMountedRef.current) return;
console.error('请求错误:', err); console.error('请求错误:', err);
setError(err instanceof Error ? err.message : '未知错误'); setError(err instanceof Error ? err.message : '未知错误');
// 保持之前的项目列表,避免清空显示
if (projects.length === 0) {
setProjects([]);
}
} finally { } finally {
// 如果组件已卸载,不继续更新状态
if (!isMountedRef.current) return;
setLoading(false); setLoading(false);
setIsPageChanging(false);
}
}, [platform, username, organization, token, perPage, url, projects.length]);
useEffect(() => {
// 设置组件已挂载标志
isMountedRef.current = true;
fetchData(1);
// 清理函数
return () => {
isMountedRef.current = false;
if (abortControllerRef.current) {
abortControllerRef.current.abort();
} }
}; };
}, [fetchData]);
const handlePageChange = useCallback((page: number) => { useEffect(() => {
fetchData(1);
}, [platform, username, organization, token, perPage, url]);
const handlePageChange = (page: number) => {
if (isPageChanging) return; if (isPageChanging) return;
setIsPageChanging(true); setIsPageChanging(true);
// 更新分页状态 // 重置当前状态,显示加载中
setProjects([]);
setLoading(true);
// 手动更新分页状态
setPagination(prev => ({ setPagination(prev => ({
...prev, ...prev,
current: page current: page
})); }));
// 不清空当前项目列表,但显示加载状态
setLoading(true);
fetchData(page); fetchData(page);
}, [fetchData, isPageChanging]); setTimeout(() => setIsPageChanging(false), 2000);
};
const getPlatformIcon = (platform: GitPlatform) => { const getPlatformIcon = (platform: GitPlatform) => {
switch (platform) { switch (platform) {
case GitPlatform.GITHUB: case GitPlatform.GITHUB:
return ( return (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"> <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/> <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg> </svg>
); );
case GitPlatform.GITEA: case GitPlatform.GITEA:
return ( return (
<svg className="w-5 h-5" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"> <svg className="w-5 h-5" viewBox="0 0 16 16" fill="currentColor">
<path d="M8.948.291c-1.412.274-2.223 1.793-2.223 1.793S4.22 3.326 2.4 5.469c-1.82 2.142-1.415 5.481-1.415 5.481s1.094 3.61 5.061 3.61c3.967 0 5.681-1.853 5.681-1.853s1.225-1.087 1.225-3.718c0-2.632-1.946-3.598-1.946-3.598s.324-1.335-1.061-3.118C8.59.49 8.948.291 8.948.291zM8.13 2.577c.386 0 .699.313.699.699 0 .386-.313.699-.699.699-.386 0-.699-.313-.699-.699 0-.386.313-.699.699-.699zm-3.366.699c.386 0 .699.313.699.699 0 .386-.313.699-.699.699-.386 0-.699-.313-.699-.699 0-.386.313-.699.699-.699zm6.033 0c.386 0 .699.313.699.699 0 .386-.313.699-.699.699-.386 0-.699-.313-.699-.699 0-.386.313-.699.699-.699zm-4.764 2.1c.386 0 .699.313.699.699 0 .386-.313.699-.699.699-.386 0-.699-.313-.699-.699 0-.386.313-.699.699-.699zm3.366 0c.386 0 .699.313.699.699 0 .386-.313.699-.699.699-.386 0-.699-.313-.699-.699 0-.386.313-.699.699-.699zm-5.049 2.1c.386 0 .699.313.699.699 0 .386-.313.699-.699.699-.386 0-.699-.313-.699-.699 0-.386.313-.699.699-.699zm6.732 0c.386 0 .699.313.699.699 0 .386-.313.699-.699.699-.386 0-.699-.313-.699-.699 0-.386.313-.699.699-.699zm-3.366.699c.386 0 .699.313.699.699 0 .386-.313.699-.699.699-.386 0-.699-.313-.699-.699 0-.386.313-.699.699-.699zm-1.683 1.4c.386 0 .699.313.699.699 0 .386-.313.699-.699.699-.386 0-.699-.313-.699-.699 0-.386.313-.699.699-.699z"/> <path d="M8.948.291c-1.412.274-2.223 1.793-2.223 1.793S4.22 3.326 2.4 5.469c-1.82 2.142-1.415 5.481-1.415 5.481s1.094 3.61 5.061 3.61c3.967 0 5.681-1.853 5.681-1.853s1.225-1.087 1.225-3.718c0-2.632-1.946-3.598-1.946-3.598s.324-1.335-1.061-3.118C8.59.49 8.948.291 8.948.291zM8.13 2.577c.386 0 .699.313.699.699 0 .386-.313.699-.699.699-.386 0-.699-.313-.699-.699 0-.386.313-.699.699-.699zm-3.366.699c.386 0 .699.313.699.699 0 .386-.313.699-.699.699-.386 0-.699-.313-.699-.699 0-.386.313-.699.699-.699zm6.033 0c.386 0 .699.313.699.699 0 .386-.313.699-.699.699-.386 0-.699-.313-.699-.699 0-.386.313-.699.699-.699zm-4.764 2.1c.386 0 .699.313.699.699 0 .386-.313.699-.699.699-.386 0-.699-.313-.699-.699 0-.386.313-.699.699-.699zm3.366 0c.386 0 .699.313.699.699 0 .386-.313.699-.699.699-.386 0-.699-.313-.699-.699 0-.386.313-.699.699-.699zm-5.049 2.1c.386 0 .699.313.699.699 0 .386-.313.699-.699.699-.386 0-.699-.313-.699-.699 0-.386.313-.699.699-.699zm6.732 0c.386 0 .699.313.699.699 0 .386-.313.699-.699.699-.386 0-.699-.313-.699-.699 0-.386.313-.699.699-.699zm-3.366.699c.386 0 .699.313.699.699 0 .386-.313.699-.699.699-.386 0-.699-.313-.699-.699 0-.386.313-.699.699-.699zm-1.683 1.4c.386 0 .699.313.699.699 0 .386-.313.699-.699.699-.386 0-.699-.313-.699-.699 0-.386.313-.699.699-.699z"/>
</svg> </svg>
); );
case GitPlatform.GITEE: case GitPlatform.GITEE:
return ( return (
<svg className="w-5 h-5" viewBox="0 0 1024 1024" fill="currentColor" aria-hidden="true"> <svg className="w-5 h-5" viewBox="0 0 1024 1024" fill="currentColor">
<path d="M512 1024C229.222 1024 0 794.778 0 512S229.222 0 512 0s512 229.222 512 512-229.222 512-512 512z m259.149-568.883h-290.74a25.293 25.293 0 0 0-25.292 25.293l-0.026 63.206c0 13.952 11.315 25.293 25.267 25.293h177.024c13.978 0 25.293 11.315 25.293 25.267v12.646a75.853 75.853 0 0 1-75.853 75.853h-240.23a25.293 25.293 0 0 1-25.267-25.293V417.203a75.853 75.853 0 0 1 75.827-75.853h353.946a25.293 25.293 0 0 0 25.267-25.292l0.077-63.207a25.293 25.293 0 0 0-25.268-25.293H417.152a189.62 189.62 0 0 0-189.62 189.645V771.15c0 13.977 11.316 25.293 25.294 25.293h372.94a170.65 170.65 0 0 0 170.65-170.65V480.384a25.293 25.293 0 0 0-25.293-25.267z" /> <path d="M512 1024C229.222 1024 0 794.778 0 512S229.222 0 512 0s512 229.222 512 512-229.222 512-512 512z m259.149-568.883h-290.74a25.293 25.293 0 0 0-25.292 25.293l-0.026 63.206c0 13.952 11.315 25.293 25.267 25.293h177.024c13.978 0 25.293 11.315 25.293 25.267v12.646a75.853 75.853 0 0 1-75.853 75.853h-240.23a25.293 25.293 0 0 1-25.267-25.293V417.203a75.853 75.853 0 0 1 75.827-75.853h353.946a25.293 25.293 0 0 0 25.267-25.292l0.077-63.207a25.293 25.293 0 0 0-25.268-25.293H417.152a189.62 189.62 0 0 0-189.62 189.645V771.15c0 13.977 11.316 25.293 25.294 25.293h372.94a170.65 170.65 0 0 0 170.65-170.65V480.384a25.293 25.293 0 0 0-25.293-25.267z" />
</svg> </svg>
); );
@ -265,76 +217,35 @@ const GitProjectCollection: React.FC<GitProjectCollectionProps> = ({
// 自定义标题或使用默认标题 // 自定义标题或使用默认标题
const displayTitle = title || `${getPlatformName(platform)} 项目`; const displayTitle = title || `${getPlatformName(platform)} 项目`;
// 渲染加载状态 return (
const renderLoading = () => ( <div className="git-project-collection max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-center items-center p-8"> <h2 className="text-2xl font-bold mb-6 text-primary-700">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-current border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite]"></div> {displayTitle}
<p className="ml-2 text-gray-600 dark:text-gray-400">...</p> {username && <span className="ml-2 text-secondary-500">(@{username})</span>}
</div> {organization && <span className="ml-2 text-secondary-500">(: {organization})</span>}
); </h2>
// 渲染错误状态 {loading && projects.length === 0 ? (
const renderError = () => ( <div className="flex justify-center p-8">...</div>
<div className="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-4 rounded-lg border border-red-200 dark:border-red-800"> ) : error ? (
<div className="flex items-center"> <div className="text-red-500 p-4">: {error}</div>
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> ) : projects.length === 0 ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> <div className="text-secondary-500 p-4">
</svg>
<p>: {error}</p>
</div>
<button
onClick={() => fetchData(pagination.current)}
className="mt-3 px-4 py-2 bg-red-100 dark:bg-red-800/30 hover:bg-red-200 dark:hover:bg-red-800/50 text-red-700 dark:text-red-300 rounded"
>
</button>
</div>
);
// 渲染无数据状态
const renderEmpty = () => (
<div className="text-secondary-500 dark:text-secondary-400 p-4 text-center">
{platform === GitPlatform.GITEE ? {platform === GitPlatform.GITEE ?
"无法获取 Gitee 项目数据,可能需要配置访问令牌。" : "无法获取 Gitee 项目数据,可能需要配置访问令牌。" :
"没有找到项目数据。"} "没有找到项目数据。"}
</div> </div>
);
return (
<div className={`git-project-collection max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 ${className}`}>
<h2 className="text-2xl font-bold mb-6 text-primary-700 dark:text-primary-400">
{displayTitle}
{username && <span className="ml-2 text-secondary-500 dark:text-secondary-400">(@{username})</span>}
{organization && <span className="ml-2 text-secondary-500 dark:text-secondary-400">(: {organization})</span>}
</h2>
{/* 内容区域 */}
{loading && projects.length === 0 ? (
renderLoading()
) : error ? (
renderError()
) : projects.length === 0 ? (
renderEmpty()
) : ( ) : (
<>
{/* 仅显示加载中指示器,不隐藏项目 */}
{loading && projects.length > 0 && (
<div className="flex justify-center items-center py-2 mb-4">
<div className="inline-block h-5 w-5 animate-spin rounded-full border-2 border-solid border-current border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite]"></div>
<p className="ml-2 text-xs text-gray-500 dark:text-gray-400">...</p>
</div>
)}
<ReactMasonryCss <ReactMasonryCss
breakpointCols={breakpointColumnsObj} breakpointCols={breakpointColumnsObj}
className="flex -ml-4 w-auto" className="flex -ml-4 w-auto"
columnClassName="pl-4 bg-clip-padding" columnClassName="pl-4 bg-clip-padding"
> >
{projects.map((project, index) => ( {projects.map((project, index) => (
<div key={`${project.platform}-${project.owner}-${project.name}-${index}`} className="mb-4 overflow-hidden rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 shadow-lg"> <div key={`${project.platform}-${project.owner}-${project.name}-${index}`} className="mb-4 overflow-hidden rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 transition-all duration-300 shadow-lg">
<a href={project.url} target="_blank" rel="noopener noreferrer" className="block p-5"> <a href={project.url} target="_blank" rel="noopener noreferrer" className="block p-5">
<div className="flex items-start"> <div className="flex items-start">
<div className="w-10 h-10 flex-shrink-0 flex items-center justify-center rounded-lg bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 group-hover:bg-primary-200 dark:group-hover:bg-primary-800/50"> <div className="w-10 h-10 flex-shrink-0 flex items-center justify-center rounded-lg bg-primary-100 text-primary-600 group-hover:bg-primary-200 transition-colors">
{getPlatformIcon(project.platform as GitPlatform)} {getPlatformIcon(project.platform as GitPlatform)}
</div> </div>
<div className="ml-3 flex-1"> <div className="ml-3 flex-1">
@ -343,7 +254,6 @@ const GitProjectCollection: React.FC<GitProjectCollectionProps> = ({
src={project.avatarUrl} src={project.avatarUrl}
alt={`${project.owner}'s avatar`} alt={`${project.owner}'s avatar`}
className="w-5 h-5 rounded-full mr-2" className="w-5 h-5 rounded-full mr-2"
loading="lazy"
onError={(e) => { onError={(e) => {
const target = e.target as HTMLImageElement; const target = e.target as HTMLImageElement;
target.onerror = null; target.onerror = null;
@ -353,7 +263,7 @@ const GitProjectCollection: React.FC<GitProjectCollectionProps> = ({
<span className="text-sm text-gray-600 dark:text-gray-400 truncate">{project.owner}</span> <span className="text-sm text-gray-600 dark:text-gray-400 truncate">{project.owner}</span>
</div> </div>
<h3 className="font-bold text-base text-gray-800 dark:text-gray-100 group-hover:text-primary-700 dark:group-hover:text-primary-300 line-clamp-1 mt-2">{project.name}</h3> <h3 className="font-bold text-base text-gray-800 dark:text-gray-100 group-hover:text-primary-700 dark:group-hover:text-primary-300 transition-colors line-clamp-1 mt-2">{project.name}</h3>
<div className="h-12 mb-3"> <div className="h-12 mb-3">
{project.description ? ( {project.description ? (
@ -372,21 +282,21 @@ const GitProjectCollection: React.FC<GitProjectCollectionProps> = ({
)} )}
<div className="flex items-center"> <div className="flex items-center">
<svg className="w-4 h-4 mr-1.5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"> <svg className="w-4 h-4 mr-1.5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
</svg> </svg>
<span className="text-gray-600 dark:text-gray-400">{project.stars}</span> <span className="text-gray-600 dark:text-gray-400">{project.stars}</span>
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<svg className="w-4 h-4 mr-1.5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"> <svg className="w-4 h-4 mr-1.5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" />
</svg> </svg>
<span className="text-gray-600 dark:text-gray-400">{project.forks}</span> <span className="text-gray-600 dark:text-gray-400">{project.forks}</span>
</div> </div>
<div className="flex items-center ml-auto"> <div className="flex items-center ml-auto">
<svg className="w-4 h-4 mr-1.5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"> <svg className="w-4 h-4 mr-1.5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
<span className="text-gray-500 dark:text-gray-400">{new Date(project.updatedAt).toLocaleDateString('zh-CN')}</span> <span className="text-gray-500 dark:text-gray-400">{new Date(project.updatedAt).toLocaleDateString('zh-CN')}</span>
@ -398,7 +308,6 @@ const GitProjectCollection: React.FC<GitProjectCollectionProps> = ({
</div> </div>
))} ))}
</ReactMasonryCss> </ReactMasonryCss>
</>
)} )}
{pagination.total > 1 && ( {pagination.total > 1 && (
@ -406,43 +315,21 @@ const GitProjectCollection: React.FC<GitProjectCollectionProps> = ({
<button <button
onClick={() => handlePageChange(pagination.current - 1)} onClick={() => handlePageChange(pagination.current - 1)}
disabled={!pagination.hasPrev || pagination.current <= 1 || isPageChanging} disabled={!pagination.hasPrev || pagination.current <= 1 || isPageChanging}
className={`px-4 py-2 rounded ${!pagination.hasPrev || pagination.current <= 1 || isPageChanging className={`px-4 py-2 rounded ${!pagination.hasPrev || pagination.current <= 1 || isPageChanging ? 'bg-secondary-200 text-secondary-500 cursor-not-allowed' : 'bg-primary-600 text-white hover:bg-primary-700'}`}
? 'bg-secondary-200 dark:bg-secondary-700 text-secondary-500 dark:text-secondary-500 cursor-not-allowed'
: 'bg-primary-600 text-white hover:bg-primary-700 dark:bg-primary-700 dark:hover:bg-primary-600'}`}
aria-label="上一页"
> >
{isPageChanging ? ( {isPageChanging ? '加载中...' : '上一页'}
<span className="flex items-center">
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</span>
) : '上一页'}
</button> </button>
<span className="px-4 py-2 bg-secondary-100 dark:bg-secondary-800 rounded"> <span className="px-4 py-2 bg-secondary-100 rounded">
{pagination.current} / {pagination.total} {pagination.current} / {pagination.total}
</span> </span>
<button <button
onClick={() => handlePageChange(pagination.current + 1)} onClick={() => handlePageChange(pagination.current + 1)}
disabled={!pagination.hasNext || pagination.current >= pagination.total || isPageChanging} disabled={!pagination.hasNext || pagination.current >= pagination.total || isPageChanging}
className={`px-4 py-2 rounded ${!pagination.hasNext || pagination.current >= pagination.total || isPageChanging className={`px-4 py-2 rounded ${!pagination.hasNext || pagination.current >= pagination.total || isPageChanging ? 'bg-secondary-200 text-secondary-500 cursor-not-allowed' : 'bg-primary-600 text-white hover:bg-primary-700'}`}
? 'bg-secondary-200 dark:bg-secondary-700 text-secondary-500 dark:text-secondary-500 cursor-not-allowed'
: 'bg-primary-600 text-white hover:bg-primary-700 dark:bg-primary-700 dark:hover:bg-primary-600'}`}
aria-label="下一页"
> >
{isPageChanging ? ( {isPageChanging ? '加载中...' : '下一页'}
<span className="flex items-center">
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</span>
) : '下一页'}
</button> </button>
</div> </div>
)} )}

View File

@ -1,421 +0,0 @@
import { useEffect, useState, useRef } from 'react';
import { SITE_NAME, NAV_LINKS } from '@/consts.ts';
import { ThemeToggle } from './ThemeToggle';
import '@/styles/header.css';
// 文章对象类型定义
interface Article {
id: string;
title: string;
date: string | Date;
summary?: string;
tags?: string[];
image?: string;
content?: string;
}
export default function Header() {
// 状态定义
const [scrolled, setScrolled] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
const [articles, setArticles] = useState<Article[]>([]);
const [isArticlesLoaded, setIsArticlesLoaded] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [desktopSearchFocused, setDesktopSearchFocused] = useState(false);
// 获取当前路径 (使用window.location.pathname)
const [pathname, setPathname] = useState('/');
useEffect(() => {
setPathname(window.location.pathname);
}, []);
// 移除结尾的斜杠以统一路径格式
const normalizedPath = pathname?.endsWith('/') ? pathname.slice(0, -1) : pathname;
// 引用
const desktopSearchRef = useRef<HTMLInputElement>(null);
const desktopResultsRef = useRef<HTMLDivElement>(null);
const mobileSearchRef = useRef<HTMLInputElement>(null);
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// 处理滚动效果
useEffect(() => {
const scrollThreshold = 50;
function updateHeaderBackground() {
if (window.scrollY > scrollThreshold) {
setScrolled(true);
} else {
setScrolled(false);
}
}
// 初始检查
updateHeaderBackground();
// 添加滚动事件监听
window.addEventListener('scroll', updateHeaderBackground);
// 清理
return () => window.removeEventListener('scroll', updateHeaderBackground);
}, []);
// 搜索节流函数
function debounce<T extends (...args: any[]) => void>(func: T, wait: number): (...args: Parameters<T>) => void {
let timeout: ReturnType<typeof setTimeout> | undefined;
return function(this: any, ...args: Parameters<T>): void {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// 获取文章数据
async function fetchArticles() {
if (isArticlesLoaded && articles.length > 0) return;
try {
const response = await fetch('/api/search');
if (!response.ok) {
throw new Error('获取文章数据失败');
}
const data = await response.json();
setArticles(data);
setIsArticlesLoaded(true);
} catch (error) {
console.error('获取文章失败:', error);
}
}
// 高亮文本中的匹配部分
function highlightText(text: string, query: string): string {
if (!text || !query.trim()) return text;
// 转义正则表达式中的特殊字符
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escapedQuery})`, 'gi');
return text.replace(regex, '<mark class="bg-yellow-100 dark:bg-yellow-900/30 text-gray-900 dark:text-gray-100 px-0.5 rounded">$1</mark>');
}
// 搜索文章逻辑
const debouncedSearch = debounce((query: string) => {
setSearchQuery(query);
}, 300);
// 点击页面其他区域关闭搜索结果
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
desktopSearchRef.current &&
desktopResultsRef.current &&
!desktopSearchRef.current.contains(event.target as Node) &&
!desktopResultsRef.current.contains(event.target as Node)
) {
setDesktopSearchFocused(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
// 处理ESC键
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape') {
setDesktopSearchFocused(false);
setMobileSearchOpen(false);
}
}
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, []);
// 搜索结果处理
const getFilteredArticles = () => {
if (!searchQuery.trim()) return [];
const lowerQuery = searchQuery.toLowerCase();
return articles
.filter((article: Article) => {
const title = article.title.toLowerCase();
const tags = article.tags ? article.tags.map((tag: string) => tag.toLowerCase()) : [];
const summary = article.summary ? article.summary.toLowerCase() : '';
const content = article.content ? article.content.toLowerCase() : '';
return title.includes(lowerQuery) ||
tags.some((tag: string) => tag.includes(lowerQuery)) ||
summary.includes(lowerQuery) ||
content.includes(lowerQuery);
})
.sort((a: Article, b: Article) => {
// 标题匹配优先
const aTitle = a.title.toLowerCase();
const bTitle = b.title.toLowerCase();
if (aTitle.includes(lowerQuery) && !bTitle.includes(lowerQuery)) {
return -1;
}
if (!aTitle.includes(lowerQuery) && bTitle.includes(lowerQuery)) {
return 1;
}
// 内容匹配次之
const aContent = a.content ? a.content.toLowerCase() : '';
const bContent = b.content ? b.content.toLowerCase() : '';
if (aContent.includes(lowerQuery) && !bContent.includes(lowerQuery)) {
return -1;
}
if (!aContent.includes(lowerQuery) && bContent.includes(lowerQuery)) {
return 1;
}
// 日期排序
return new Date(b.date).getTime() - new Date(a.date).getTime();
})
.slice(0, 10); // 限制结果数量
};
// 生成搜索结果列表项
const renderSearchResults = () => {
const filteredArticles = getFilteredArticles();
if (filteredArticles.length === 0) {
return (
<div className="py-4 px-4 text-center text-gray-500 dark:text-gray-400">
</div>
);
}
return (
<ul className="py-2">
{filteredArticles.map(article => {
return (
<li key={article.id} className="group">
<a href={`/articles/${article.id}`} className="block px-4 py-3 hover:bg-gray-100 dark:hover:bg-gray-700/70">
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 group-hover:text-primary-600 dark:group-hover:text-primary-400"
dangerouslySetInnerHTML={{ __html: highlightText(article.title, searchQuery) }}
></h4>
{article.summary && (
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400 line-clamp-1"
dangerouslySetInnerHTML={{ __html: highlightText(article.summary, searchQuery) }}
></p>
)}
{article.tags && article.tags.length > 0 && (
<div className="mt-2 flex items-center flex-wrap gap-1">
{article.tags.slice(0, 3).map(tag => (
<span key={tag} className="inline-block text-2xs px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700/70 text-gray-600 dark:text-gray-400 rounded">
{tag}
</span>
))}
{article.tags.length > 3 && (
<span className="text-xs text-gray-400 dark:text-gray-500">
+{article.tags.length - 3}
</span>
)}
</div>
)}
</a>
</li>
);
})}
</ul>
);
};
// 准备样式类
const headerBgClasses = `absolute inset-0 bg-gray-50/95 dark:bg-dark-bg/95 ${
scrolled ? 'scrolled' : ''
}`;
return (
<header className="fixed w-full top-0 z-50" id="main-header">
<div className={headerBgClasses} id="header-bg"></div>
<nav className="relative">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
{/* Logo 部分 */}
<div className="flex items-center">
<a href="/" className="text-xl md:text-2xl font-bold tracking-tight bg-gradient-to-r from-primary-600 to-primary-400 bg-clip-text text-transparent hover:from-primary-500 hover:to-primary-300 dark:from-primary-400 dark:to-primary-200 dark:hover:from-primary-300 dark:hover:to-primary-100">
{SITE_NAME}
</a>
</div>
{/* 导航链接 */}
<div className="hidden md:flex md:items-center md:space-x-8">
{/* 桌面端搜索框 */}
<div className="relative">
<input
type="text"
id="desktop-search"
ref={desktopSearchRef}
className="w-48 pl-10 pr-4 py-1.5 rounded-full text-sm text-gray-700 dark:text-gray-200 placeholder-gray-500 dark:placeholder-gray-400 bg-gray-50/80 dark:bg-gray-800/60 border border-gray-200/60 dark:border-gray-700/40 focus:outline-none focus:ring-1 focus:ring-primary-400 dark:focus:ring-primary-500 focus:bg-white dark:focus:bg-gray-800 focus:border-primary-300 dark:focus:border-primary-600"
placeholder="搜索文章..."
onFocus={() => {
setDesktopSearchFocused(true);
fetchArticles();
}}
onChange={(e) => debouncedSearch(e.target.value)}
/>
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-4 w-4 text-gray-400 dark:text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
{/* 搜索结果容器 */}
<div
ref={desktopResultsRef}
className={`absolute top-full left-0 right-0 mt-2 max-h-80 overflow-y-auto rounded-lg bg-white/95 dark:bg-gray-800/95 shadow-md border border-gray-200/70 dark:border-gray-700/70 backdrop-blur-sm z-50 ${
desktopSearchFocused ? '' : 'hidden'
}`}
>
{renderSearchResults()}
</div>
</div>
{/* 导航链接 */}
{NAV_LINKS.map((link) => (
<a
key={link.href}
href={link.href}
className={`inline-flex items-center px-1 pt-1 text-sm font-medium ${
normalizedPath === (link.href === '/' ? '' : link.href)
? 'text-primary-600 dark:text-primary-400 border-b-2 border-primary-600 dark:border-primary-400'
: 'text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 hover:border-b-2 hover:border-primary-300 dark:hover:border-primary-700'
}`}
>
{link.text}
</a>
))}
<ThemeToggle />
</div>
{/* 移动端菜单按钮 */}
<div className="flex items-center md:hidden">
{/* 移动端搜索按钮 */}
<button
type="button"
className="inline-flex items-center justify-center p-2 rounded-md text-secondary-400 dark:text-secondary-500 hover:text-secondary-500 dark:hover:text-secondary-400 hover:bg-secondary-100 dark:hover:bg-dark-card focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500 mr-2"
aria-expanded={mobileSearchOpen}
aria-label="搜索"
onClick={() => {
setMobileSearchOpen(true);
setTimeout(() => {
mobileSearchRef.current?.focus();
}, 100);
fetchArticles();
}}
>
<span className="sr-only"></span>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
{/* 移动端菜单按钮 */}
<button
type="button"
className="inline-flex items-center justify-center p-2 rounded-md text-secondary-400 dark:text-secondary-500 hover:text-secondary-500 dark:hover:text-secondary-400 hover:bg-secondary-100 dark:hover:bg-dark-card focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500"
id="mobile-menu-button"
aria-expanded={mobileMenuOpen}
aria-label="打开菜单"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
<span className="sr-only"></span>
{mobileMenuOpen ? (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
) : (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
)}
</button>
</div>
</div>
</div>
{/* 移动端搜索面板 */}
{mobileSearchOpen && (
<div className="md:hidden fixed inset-x-0 top-16 p-4 bg-white dark:bg-gray-800 shadow-md z-50 border-t border-gray-200 dark:border-gray-700 show">
<div className="relative">
<input
type="text"
ref={mobileSearchRef}
className="w-full pl-10 pr-10 py-2 rounded-full text-sm text-gray-700 dark:text-gray-200 placeholder-gray-500 dark:placeholder-gray-400 bg-gray-50/80 dark:bg-gray-800/60 border border-gray-200/60 dark:border-gray-700/40 focus:outline-none focus:ring-1 focus:ring-primary-400 dark:focus:ring-primary-500 focus:bg-white dark:focus:bg-gray-800 focus:border-primary-300 dark:focus:border-primary-600"
placeholder="搜索文章..."
onChange={(e) => debouncedSearch(e.target.value)}
/>
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400 dark:text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<button
className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
aria-label="关闭搜索"
onClick={() => setMobileSearchOpen(false)}
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* 移动端搜索结果 */}
<div className={`mt-3 max-h-80 overflow-y-auto rounded-lg bg-white/95 dark:bg-gray-800/95 shadow-md border border-gray-200/70 dark:border-gray-700/70 backdrop-blur-sm ${searchQuery ? '' : 'hidden'}`}>
{renderSearchResults()}
</div>
</div>
)}
{/* 移动端菜单 */}
{mobileMenuOpen && (
<div className="md:hidden fixed inset-x-0 top-16 z-40" id="mobile-menu">
<div className="backdrop-blur-[6px] bg-white/80 dark:bg-gray-800/80 shadow-md">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-2">
<div className="grid gap-1">
{NAV_LINKS.map((link) => (
<a
key={link.href}
href={link.href}
className={`flex items-center px-3 py-3 rounded-lg text-base font-medium ${
normalizedPath === (link.href === '/' ? '' : link.href)
? 'text-white bg-primary-600 dark:bg-primary-500 shadow-sm'
: 'text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800/70'
}`}
>
{link.text}
</a>
))}
<div
className="mt-2 pt-3 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800/70 rounded-lg px-3 py-2"
onClick={(e) => {
const themeToggleButton = e.currentTarget.querySelector('[role="button"]');
const target = e.target as HTMLElement;
if (themeToggleButton instanceof HTMLElement && target !== themeToggleButton && !themeToggleButton.contains(target)) {
themeToggleButton.click();
}
}}
>
<span className="text-sm font-medium text-gray-600 dark:text-gray-300"></span>
<ThemeToggle />
</div>
</div>
</div>
</div>
</div>
)}
</nav>
</header>
);
}

View File

@ -1,7 +1,7 @@
--- ---
import "@/styles/global.css"; import "@/styles/global.css";
import Header from "@/components/Header.tsx"; import Header from "@/components/header.astro";
import {Footer} from "@/components/Footer.tsx"; import Footer from "@/components/Footer.astro";
import { ICP, PSB_ICP, PSB_ICP_URL, SITE_NAME, SITE_DESCRIPTION } from "@/consts"; import { ICP, PSB_ICP, PSB_ICP_URL, SITE_NAME, SITE_DESCRIPTION } from "@/consts";
// 定义Props接口 // 定义Props接口
@ -54,14 +54,9 @@ const { title = SITE_NAME, description = SITE_DESCRIPTION, date, author, tags, i
{tags && tags.map(tag => ( {tags && tags.map(tag => (
<meta property="article:tag" content={tag} /> <meta property="article:tag" content={tag} />
))} ))}
<script is:inline>
// 立即执行主题初始化,并防止变量重复声明
(function() {
// 检查变量是否已存在
if (typeof window.__themeInitDone === 'undefined') {
// 设置标志,表示初始化已完成
window.__themeInitDone = true;
<script is:inline>
// 立即执行主题初始化
const theme = (() => { const theme = (() => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) { if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
return localStorage.getItem('theme'); return localStorage.getItem('theme');
@ -72,12 +67,10 @@ const { title = SITE_NAME, description = SITE_DESCRIPTION, date, author, tags, i
return 'light'; return 'light';
})(); })();
document.documentElement.dataset.theme = theme; document.documentElement.dataset.theme = theme;
}
})();
</script> </script>
</head> </head>
<body class="m-0 w-full h-full bg-gray-50 dark:bg-dark-bg flex flex-col min-h-screen"> <body class="m-0 w-full h-full bg-gray-50 dark:bg-dark-bg transition-colors duration-300 flex flex-col min-h-screen">
<Header client:load/> <Header />
<main class="pt-16 flex-grow"> <main class="pt-16 flex-grow">
<slot /> <slot />
</main> </main>

View File

@ -0,0 +1,151 @@
---
interface Props {
type: 'movie' | 'book';
title: string;
doubanId: string;
}
const { type, title, doubanId } = Astro.props;
---
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h1 class="text-3xl font-bold mb-6">{title}</h1>
<div id="media-list" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
<!-- 内容将通过JS动态加载 -->
</div>
<div id="loading" class="text-center py-8">
<div class="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-current border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite]"></div>
<p class="mt-2 text-gray-600">加载更多...</p>
</div>
<div id="end-message" class="text-center py-8 hidden">
<p class="text-gray-600">已加载全部内容</p>
</div>
</div>
<script is:inline define:vars={{ type, doubanId }}>
let currentPage = 1;
let isLoading = false;
let hasMoreContent = true;
const itemsPerPage = 15; // 豆瓣每页显示的数量
async function fetchMedia(page = 1, append = false) {
if (isLoading || (!append && !hasMoreContent)) {
return;
}
isLoading = true;
showLoading(true);
const start = (page - 1) * itemsPerPage;
try {
const response = await fetch(`/api/douban?type=${type}&start=${start}&doubanId=${doubanId}`);
if (!response.ok) {
throw new Error(`获取${type === 'movie' ? '电影' : '图书'}数据失败`);
}
const data = await response.json();
renderMedia(data.items, append);
// 更新分页状态
currentPage = data.pagination.current;
hasMoreContent = data.pagination.hasNext;
if (!hasMoreContent) {
showEndMessage(true);
}
} catch (error) {
const mediaList = document.getElementById('media-list');
if (mediaList && !append) {
mediaList.innerHTML = '<div class="col-span-full text-center text-red-500">获取数据失败,请稍后再试</div>';
}
} finally {
isLoading = false;
showLoading(false);
}
}
function renderMedia(items, append = false) {
const mediaList = document.getElementById('media-list');
if (!mediaList) return;
if (!items || items.length === 0) {
if (!append) {
mediaList.innerHTML = `<div class="col-span-full text-center">暂无${type === 'movie' ? '电影' : '图书'}数据</div>`;
}
return;
}
const mediaHTML = items.map(item => `
<div class="bg-white rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-shadow duration-300">
<div class="relative pb-[150%] overflow-hidden">
<img src="${item.imageUrl}" alt="${item.title}" class="absolute top-0 left-0 w-full h-full object-cover transition-transform duration-300 hover:scale-105">
<div class="absolute bottom-0 left-0 right-0 p-3 bg-gradient-to-t from-black/80 to-transparent">
<h3 class="font-bold text-white text-sm line-clamp-2">
<a href="${item.link}" target="_blank" class="hover:text-blue-300 transition-colors">${item.title}</a>
</h3>
</div>
</div>
</div>
`).join('');
if (append) {
mediaList.innerHTML += mediaHTML;
} else {
mediaList.innerHTML = mediaHTML;
}
}
function showLoading(show) {
const loading = document.getElementById('loading');
if (loading) {
if (show) {
loading.classList.remove('hidden');
} else {
loading.classList.add('hidden');
}
}
}
function showEndMessage(show) {
const endMessage = document.getElementById('end-message');
if (endMessage) {
endMessage.classList.toggle('hidden', !show);
}
}
function setupInfiniteScroll() {
// 直接使用滚动事件
window.addEventListener('scroll', handleScroll);
// 初始检查一次,以防内容不足一屏
setTimeout(handleScroll, 500);
}
function handleScroll() {
if (isLoading || !hasMoreContent) {
return;
}
const scrollY = window.scrollY;
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
// 当滚动到距离底部300px时加载更多
if (scrollY + windowHeight >= documentHeight - 300) {
fetchMedia(currentPage + 1, true);
}
}
// 初始加载
document.addEventListener('DOMContentLoaded', () => {
fetchMedia(1, false).then(() => {
setupInfiniteScroll();
}).catch(err => {
// 错误已在fetchMedia中处理
});
});
</script>

View File

@ -1,359 +0,0 @@
import React, { useEffect, useRef, useState } from 'react';
interface MediaGridProps {
type: 'movie' | 'book';
title: string;
doubanId: string;
}
interface MediaItem {
title: string;
imageUrl: string;
link: string;
}
interface PaginationInfo {
current: number;
hasNext: boolean;
}
const MediaGrid: React.FC<MediaGridProps> = ({ type, title, doubanId }) => {
const [items, setItems] = useState<MediaItem[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [hasMoreContent, setHasMoreContent] = useState(true);
const [currentPage, setCurrentPage] = useState(1);
const [error, setError] = useState<string | null>(null);
const itemsPerPage = 15;
const mediaListRef = useRef<HTMLDivElement>(null);
const lastScrollTime = useRef(0);
// 使用ref来跟踪关键状态避免闭包问题
const stateRef = useRef({
isLoading: false,
hasMoreContent: true,
currentPage: 1,
error: null as string | null
});
// 封装fetch函数但不使用useCallback以避免依赖循环
const fetchMedia = async (page = 1, append = false) => {
// 使用ref中的最新状态
if (stateRef.current.isLoading ||
(!append && !stateRef.current.hasMoreContent) ||
(append && !stateRef.current.hasMoreContent)) {
return;
}
// 更新状态和ref
setIsLoading(true);
stateRef.current.isLoading = true;
// 只在首次加载时清除错误
if (!append) {
setError(null);
stateRef.current.error = null;
}
const start = (page - 1) * itemsPerPage;
try {
const response = await fetch(`/api/douban?type=${type}&start=${start}&doubanId=${doubanId}`);
if (!response.ok) {
// 解析响应内容,获取详细错误信息
let errorMessage = `获取${type === 'movie' ? '电影' : '图书'}数据失败`;
try {
const errorData = await response.json();
if (errorData && errorData.error) {
errorMessage = errorData.error;
if (errorData.message) {
errorMessage += `: ${errorData.message}`;
}
}
} catch (e) {
// 无法解析JSON使用默认错误信息
}
// 针对不同错误提供更友好的提示
if (response.status === 403) {
errorMessage = "豆瓣接口访问受限,可能是请求过于频繁,请稍后再试";
} else if (response.status === 404) {
// 对于404错误如果是追加模式说明已经到了最后一页设置hasMoreContent为false
if (append) {
setHasMoreContent(false);
stateRef.current.hasMoreContent = false;
setIsLoading(false);
stateRef.current.isLoading = false;
return; // 直接返回,不设置错误,不清空已有数据
} else {
errorMessage = "未找到相关内容请检查豆瓣ID是否正确";
}
}
// 设置错误状态和ref
setError(errorMessage);
stateRef.current.error = errorMessage;
// 只有非追加模式才清空数据
if (!append) {
setItems([]);
}
throw new Error(errorMessage);
}
const data = await response.json();
if (data.items.length === 0) {
// 如果返回的项目为空,则认为已经没有更多内容
setHasMoreContent(false);
stateRef.current.hasMoreContent = false;
if (!append) {
setItems([]);
}
} else {
if (append) {
setItems(prev => {
const newItems = [...prev, ...data.items];
return newItems;
});
} else {
setItems(data.items);
}
// 更新页码状态和ref
setCurrentPage(data.pagination.current);
stateRef.current.currentPage = data.pagination.current;
// 更新是否有更多内容的状态和ref
const newHasMoreContent = data.pagination.hasNext;
setHasMoreContent(newHasMoreContent);
stateRef.current.hasMoreContent = newHasMoreContent;
}
} catch (error) {
// 只有在非追加模式下才清空已加载的内容
if (!append) {
setItems([]);
}
} finally {
// 重置加载状态
setIsLoading(false);
stateRef.current.isLoading = false;
}
};
// 处理滚动事件
const handleScroll = () => {
// 获取关键滚动值
const scrollY = window.scrollY;
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
const scrollPosition = scrollY + windowHeight;
const threshold = documentHeight - 300;
// 限制滚动日志频率,每秒最多输出一次
const now = Date.now();
if (now - lastScrollTime.current < 1000) {
return;
}
lastScrollTime.current = now;
// 使用ref中的最新状态来检查
if (stateRef.current.isLoading || !stateRef.current.hasMoreContent || stateRef.current.error) {
return;
}
// 当滚动到距离底部300px时加载更多
if (scrollPosition >= threshold) {
fetchMedia(stateRef.current.currentPage + 1, true);
}
};
// 更新ref值以跟踪状态变化
useEffect(() => {
stateRef.current.isLoading = isLoading;
}, [isLoading]);
useEffect(() => {
stateRef.current.hasMoreContent = hasMoreContent;
}, [hasMoreContent]);
useEffect(() => {
stateRef.current.currentPage = currentPage;
}, [currentPage]);
useEffect(() => {
stateRef.current.error = error;
}, [error]);
// 组件初始化和依赖变化时重置
useEffect(() => {
// 重置状态
setCurrentPage(1);
stateRef.current.currentPage = 1;
setHasMoreContent(true);
stateRef.current.hasMoreContent = true;
setError(null);
stateRef.current.error = null;
setIsLoading(false);
stateRef.current.isLoading = false;
// 清空列表
setItems([]);
// 加载第一页数据
fetchMedia(1, false);
// 管理滚动事件
const scrollListener = handleScroll;
// 移除任何现有监听器
window.removeEventListener('scroll', scrollListener);
// 添加滚动事件监听器 - 使用passive: true可提高滚动性能
window.addEventListener('scroll', scrollListener, { passive: true });
// 创建一个IntersectionObserver作为备选检测方案
const observerOptions = {
root: null,
rootMargin: '300px',
threshold: 0.1
};
const intersectionObserver = new IntersectionObserver((entries) => {
const entry = entries[0];
if (entry.isIntersecting &&
!stateRef.current.isLoading &&
stateRef.current.hasMoreContent &&
!stateRef.current.error) {
fetchMedia(stateRef.current.currentPage + 1, true);
}
}, observerOptions);
// 添加检测底部的元素 - 放在grid容器的后面而不是内部
const footer = document.createElement('div');
footer.id = 'scroll-detector';
footer.style.width = '100%';
footer.style.height = '10px';
// 确保mediaListRef有父元素
if (mediaListRef.current && mediaListRef.current.parentElement) {
// 插入到grid后面而不是内部
mediaListRef.current.parentElement.insertBefore(footer, mediaListRef.current.nextSibling);
intersectionObserver.observe(footer);
}
// 初始检查一次,以防内容不足一屏
const timeoutId = setTimeout(() => {
if (stateRef.current.hasMoreContent && !stateRef.current.isLoading) {
scrollListener();
}
}, 500);
// 清理函数
return () => {
clearTimeout(timeoutId);
window.removeEventListener('scroll', scrollListener);
intersectionObserver.disconnect();
document.getElementById('scroll-detector')?.remove();
};
}, [type, doubanId]); // 只在关键属性变化时执行
// 错误提示组件
const ErrorMessage = () => {
if (!error) return null;
return (
<div className="col-span-full text-center bg-red-50 p-4 rounded-md">
<div className="flex flex-col items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-12 w-12 text-red-500 mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<h3 className="text-lg font-medium text-red-800">访</h3>
<p className="mt-1 text-sm text-red-700">{error}</p>
<button
onClick={() => {
// 重置错误和加载状态
setError(null);
stateRef.current.error = null;
// 允许再次加载
setHasMoreContent(true);
stateRef.current.hasMoreContent = true;
// 重新获取当前页
fetchMedia(currentPage, false);
}}
className="mt-4 px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
</button>
</div>
</div>
);
};
// 没有更多内容提示
const EndMessage = () => {
if (isLoading || !items.length || error) return null;
return (
<div className="text-center py-8">
<p className="text-gray-600"></p>
</div>
);
};
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h1 className="text-3xl font-bold mb-6">{title}</h1>
<div ref={mediaListRef} className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
{error && items.length === 0 ? (
<ErrorMessage />
) : items.length > 0 ? (
items.map((item, index) => (
<div key={`${item.title}-${index}`} className="bg-white rounded-lg overflow-hidden shadow-md hover:shadow-xl">
<div className="relative pb-[150%] overflow-hidden">
<img
src={item.imageUrl}
alt={item.title}
className="absolute top-0 left-0 w-full h-full object-cover hover:scale-105"
/>
<div className="absolute bottom-0 left-0 right-0 p-3 bg-gradient-to-t from-black/80 to-transparent">
<h3 className="font-bold text-white text-sm line-clamp-2">
<a href={item.link} target="_blank" rel="noopener noreferrer" className="hover:text-blue-300">
{item.title}
</a>
</h3>
</div>
</div>
</div>
))
) : !isLoading ? (
<div className="col-span-full text-center">{type === 'movie' ? '电影' : '图书'}</div>
) : null}
</div>
{error && items.length > 0 && (
<div className="mt-4">
<ErrorMessage />
</div>
)}
{isLoading && (
<div className="text-center py-8">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-current border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite]"></div>
<p className="mt-2 text-gray-600">...{currentPage}</p>
</div>
)}
{!hasMoreContent && items.length > 0 && !isLoading && (
<EndMessage />
)}
</div>
);
};
export default MediaGrid;

View File

@ -1,25 +1,17 @@
import { useEffect, useState, useCallback, useRef } from 'react'; import { useEffect, useState } from 'react';
export function ThemeToggle({ height = 16, width = 16, fill = "currentColor", className = "" }) { export function ThemeToggle({ height = 16, width = 16, fill = "currentColor" }) {
// 使null // 使null
const [theme, setTheme] = useState<string | null>(null); const [theme, setTheme] = useState(null);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [transitioning, setTransitioning] = useState(false);
const transitionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
//
const getSystemTheme = useCallback(() => {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}, []);
// //
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
// localStorage document.documentElement.dataset.theme // localStorage document.documentElement.dataset.theme
const savedTheme = localStorage.getItem('theme'); const savedTheme = localStorage.getItem('theme');
const rootTheme = document.documentElement.dataset.theme; const rootTheme = document.documentElement.dataset.theme;
const systemTheme = getSystemTheme(); const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
// 使 // 使
const initialTheme = savedTheme || rootTheme || systemTheme; const initialTheme = savedTheme || rootTheme || systemTheme;
@ -27,64 +19,34 @@ export function ThemeToggle({ height = 16, width = 16, fill = "currentColor", cl
// //
document.documentElement.dataset.theme = initialTheme; document.documentElement.dataset.theme = initialTheme;
}, []);
//
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleMediaChange = (e: MediaQueryListEvent) => {
//
if (!localStorage.getItem('theme')) {
const newTheme = e.matches ? 'dark' : 'light';
setTheme(newTheme);
document.documentElement.dataset.theme = newTheme;
}
};
mediaQuery.addEventListener('change', handleMediaChange);
return () => {
mediaQuery.removeEventListener('change', handleMediaChange);
//
if (transitionTimeoutRef.current) {
clearTimeout(transitionTimeoutRef.current);
transitionTimeoutRef.current = null;
}
};
}, [getSystemTheme]);
// DOM localStorage
useEffect(() => { useEffect(() => {
if (!mounted || theme === null) return; if (!mounted || theme === null) return;
// DOM localStorage
document.documentElement.dataset.theme = theme; document.documentElement.dataset.theme = theme;
// if (theme === getSystemTheme()) {
const isSystemTheme = theme === getSystemTheme();
if (isSystemTheme) {
localStorage.removeItem('theme'); localStorage.removeItem('theme');
} else { } else {
localStorage.setItem('theme', theme); localStorage.setItem('theme', theme);
} }
}, [theme, mounted, getSystemTheme]); }, [theme, mounted]);
const toggleTheme = useCallback(() => { function getSystemTheme() {
if (transitioning) return; // return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
setTransitioning(true); function toggleTheme() {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light'); setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
}
// 300ms
transitionTimeoutRef.current = setTimeout(() => {
setTransitioning(false);
}, 300);
}, [transitioning]);
// //
if (!mounted || theme === null) { if (!mounted || theme === null) {
return ( return (
<div <div
className={`inline-flex items-center justify-center h-8 w-8 cursor-pointer rounded-md hover:bg-gray-100 dark:hover:bg-gray-700/50 text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 ${className}`} className="inline-flex items-center justify-center h-8 w-8 cursor-pointer rounded-md transition-all duration-200 hover:bg-gray-100 dark:hover:bg-gray-700/50 text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 mt-1"
> >
<span className="sr-only">加载主题切换按钮...</span> <span className="sr-only">加载主题切换按钮...</span>
</div> </div>
@ -93,25 +55,17 @@ export function ThemeToggle({ height = 16, width = 16, fill = "currentColor", cl
return ( return (
<div <div
className={`inline-flex items-center justify-center h-8 w-8 cursor-pointer rounded-md hover:bg-gray-100 dark:hover:bg-gray-700/50 text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 ${transitioning ? 'pointer-events-none opacity-80' : ''} ${className}`} className="inline-flex items-center justify-center h-8 w-8 cursor-pointer rounded-md transition-all duration-200 hover:bg-gray-100 dark:hover:bg-gray-700/50 text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 mt-1"
onClick={toggleTheme} onClick={toggleTheme}
role="button" role="button"
tabIndex={0} aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleTheme();
}
}}
aria-label={`切换到${theme === 'dark' ? '浅色' : '深色'}模式`}
> >
{theme === 'dark' ? ( {theme === 'dark' ? (
<svg <svg
style={{ height: `${height}px`, width: `${width}px` }} style={{ height: `${height}px`, width: `${width}px` }}
fill={fill} fill={fill}
viewBox="0 0 16 16" viewBox="0 0 16 16"
className="hover:scale-110" className="transition-transform duration-200 hover:scale-110"
aria-hidden="true"
> >
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"/> <path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"/>
</svg> </svg>
@ -120,8 +74,7 @@ export function ThemeToggle({ height = 16, width = 16, fill = "currentColor", cl
style={{ height: `${height}px`, width: `${width}px` }} style={{ height: `${height}px`, width: `${width}px` }}
fill={fill} fill={fill}
viewBox="0 0 16 16" viewBox="0 0 16 16"
className="hover:scale-110" className="transition-transform duration-200 hover:scale-110"
aria-hidden="true"
> >
<path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"/> <path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"/>
</svg> </svg>

View File

@ -844,7 +844,7 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
/> />
{hoveredCountry && ( {hoveredCountry && (
<div className="absolute bottom-5 left-0 right-0 text-center z-10"> <div className="absolute bottom-5 left-0 right-0 text-center z-10">
<div className="inline-block bg-white/95 dark:bg-gray-800/95 px-6 py-3 rounded-xl shadow-lg backdrop-blur-sm border border-gray-200 dark:border-gray-700 hover:scale-105"> <div className="inline-block bg-white/95 dark:bg-gray-800/95 px-6 py-3 rounded-xl shadow-lg backdrop-blur-sm border border-gray-200 dark:border-gray-700 transition-all duration-300 hover:scale-105">
<p className="text-gray-800 dark:text-white font-medium text-lg flex items-center justify-center gap-2"> <p className="text-gray-800 dark:text-white font-medium text-lg flex items-center justify-center gap-2">
{hoveredCountry} {hoveredCountry}
{hoveredCountry && visitedPlaces.includes(hoveredCountry) ? ( {hoveredCountry && visitedPlaces.includes(hoveredCountry) ? (

546
src/components/header.astro Normal file
View File

@ -0,0 +1,546 @@
---
import { SITE_NAME, NAV_LINKS } from '@/consts.ts';
import { ThemeToggle } from './ThemeToggle';
// 获取当前路径
const currentPath = Astro.url.pathname;
// 移除结尾的斜杠以统一路径格式
const normalizedPath = currentPath.endsWith('/') ? currentPath.slice(0, -1) : currentPath;
// 定义导航链接
---
<header class="fixed w-full top-0 z-50 transition-all duration-300" id="main-header">
<div class="absolute inset-0 bg-gray-50/95 dark:bg-dark-bg/95 transition-all duration-300" id="header-bg"></div>
<nav class="relative">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<!-- Logo 部分 -->
<div class="flex items-center">
<a href="/" class="text-xl md:text-2xl font-bold tracking-tight transition-all duration-300 ease-in-out bg-gradient-to-r from-primary-600 to-primary-400 bg-clip-text text-transparent hover:from-primary-500 hover:to-primary-300 dark:from-primary-400 dark:to-primary-200 dark:hover:from-primary-300 dark:hover:to-primary-100">
{SITE_NAME}
</a>
</div>
<!-- 导航链接 -->
<div class="hidden md:flex md:items-center md:space-x-8">
<!-- 桌面端搜索框 -->
<div class="relative">
<input
type="text"
id="desktop-search"
class="w-48 pl-10 pr-4 py-1.5 rounded-full text-sm text-gray-700 dark:text-gray-200 placeholder-gray-500 dark:placeholder-gray-400 bg-gray-50/80 dark:bg-gray-800/60 border border-gray-200/60 dark:border-gray-700/40 focus:outline-none focus:ring-1 focus:ring-primary-400 dark:focus:ring-primary-500 focus:bg-white dark:focus:bg-gray-800 focus:border-primary-300 dark:focus:border-primary-600 transition-all duration-300"
placeholder="搜索文章..."
/>
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-4 w-4 text-gray-400 dark:text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<!-- 搜索结果容器(默认隐藏) -->
<div id="desktop-search-results" class="absolute top-full left-0 right-0 mt-2 max-h-80 overflow-y-auto rounded-lg bg-white/95 dark:bg-gray-800/95 shadow-md border border-gray-200/70 dark:border-gray-700/70 backdrop-blur-sm z-50 hidden">
<!-- 结果将通过JS动态填充 -->
<div class="p-4 text-center text-gray-500 dark:text-gray-400" id="desktop-search-message">
<p>输入关键词开始搜索</p>
</div>
<ul class="divide-y divide-gray-200/70 dark:divide-gray-700/70" id="desktop-search-list"></ul>
</div>
</div>
{NAV_LINKS.map(link => (
<a
href={link.href}
class:list={[
'inline-flex items-center px-1 pt-1 text-sm font-medium',
normalizedPath === (link.href === '/' ? '' : link.href)
? 'text-primary-600 dark:text-primary-400 border-b-2 border-primary-600 dark:border-primary-400'
: 'text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 hover:border-b-2 hover:border-primary-300 dark:hover:border-primary-700'
]}
>
{link.text}
</a>
))}
<ThemeToggle client:load />
</div>
<!-- 移动端菜单按钮 -->
<div class="flex items-center md:hidden">
<!-- 移动端搜索按钮 -->
<button
type="button"
id="mobile-search-button"
class="inline-flex items-center justify-center p-2 rounded-md text-secondary-400 dark:text-secondary-500 hover:text-secondary-500 dark:hover:text-secondary-400 hover:bg-secondary-100 dark:hover:bg-dark-card focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500 mr-2"
aria-expanded="false"
>
<span class="sr-only">搜索</span>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
<button
type="button"
class="inline-flex items-center justify-center p-2 rounded-md text-secondary-400 dark:text-secondary-500 hover:text-secondary-500 dark:hover:text-secondary-400 hover:bg-secondary-100 dark:hover:bg-dark-card focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500"
id="mobile-menu-button"
aria-expanded="false"
>
<span class="sr-only">打开菜单</span>
<svg class="h-6 w-6 block" id="menu-open-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
<svg class="h-6 w-6 hidden" id="menu-close-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
<!-- 移动端搜索面板 -->
<div id="mobile-search-panel" class="hidden md:hidden fixed inset-x-0 top-16 p-4 bg-white dark:bg-gray-800 shadow-md z-50 border-t border-gray-200 dark:border-gray-700">
<div class="relative">
<input
type="text"
id="mobile-search"
class="w-full pl-10 pr-10 py-2 rounded-full text-sm text-gray-700 dark:text-gray-200 placeholder-gray-500 dark:placeholder-gray-400 bg-gray-50/80 dark:bg-gray-800/60 border border-gray-200/60 dark:border-gray-700/40 focus:outline-none focus:ring-1 focus:ring-primary-400 dark:focus:ring-primary-500 focus:bg-white dark:focus:bg-gray-800 focus:border-primary-300 dark:focus:border-primary-600 transition-all duration-300"
placeholder="搜索文章..."
/>
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400 dark:text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<button
id="mobile-search-close"
class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- 移动端搜索结果 -->
<div id="mobile-search-results" class="mt-3 max-h-80 overflow-y-auto rounded-lg bg-white/95 dark:bg-gray-800/95 shadow-md border border-gray-200/70 dark:border-gray-700/70 backdrop-blur-sm hidden">
<!-- 结果将通过JS动态填充 -->
<div class="p-4 text-center text-gray-500 dark:text-gray-400" id="mobile-search-message">
<p>输入关键词开始搜索</p>
</div>
<ul class="divide-y divide-gray-200/70 dark:divide-gray-700/70" id="mobile-search-list"></ul>
</div>
</div>
<!-- 移动端菜单 -->
<div class="hidden md:hidden fixed inset-x-0 top-16 z-40" id="mobile-menu">
<div id="mobile-menu-bg">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-2">
<div class="grid gap-1">
{NAV_LINKS.map(link => (
<a
href={link.href}
class:list={[
'flex items-center px-3 py-3 rounded-lg text-base font-medium transition-all duration-200 ease-in-out',
normalizedPath === (link.href === '/' ? '' : link.href)
? 'text-white bg-primary-600 dark:bg-primary-500 shadow-sm'
: 'text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800/70'
]}
>
{link.text}
</a>
))}
<div class="mt-2 pt-3 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800/70 rounded-lg px-3 py-2 transition-colors duration-200" id="theme-toggle-container">
<span class="text-sm font-medium text-gray-600 dark:text-gray-300">切换主题</span>
<ThemeToggle client:load />
</div>
</div>
</div>
</div>
</div>
</nav>
</header>
<style>
#header-bg {
opacity: 1;
backdrop-filter: blur(0);
transition: all 0.5s ease;
}
#header-bg.scrolled {
backdrop-filter: blur(6px);
background: rgba(249, 250, 251, 0.8);
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.04),
0 2px 4px rgba(0, 0, 0, 0.04),
0 4px 8px rgba(0, 0, 0, 0.04);
}
:global([data-theme="dark"]) #header-bg.scrolled {
background: rgba(15, 23, 42, 0.8);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
}
/* 移动端菜单样式 */
#mobile-menu {
/* 移除过渡效果,确保菜单内容立即显示 */
opacity: 1;
transform: translateY(0);
max-height: calc(100vh - 4rem);
overflow-y: auto;
/* 确保子元素不受过渡效果影响 */
contain: layout;
}
/* 移动端菜单背景 */
#mobile-menu-bg {
/* 直接应用高斯模糊,不使用过渡效果 */
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
background: rgba(249, 250, 251, 0.8);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.04);
/* 确保高斯模糊立即应用 */
will-change: backdrop-filter;
}
:global([data-theme="dark"]) #mobile-menu-bg {
background: rgba(15, 23, 42, 0.8);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
}
/* 搜索面板动画 */
#mobile-search-panel.show {
animation: slide-down 0.2s ease-out forwards;
}
@keyframes slide-down {
0% {
opacity: 0;
transform: translateY(-10px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
</style>
<script>
const header = document.getElementById('header-bg');
const scrollThreshold = 50;
function updateHeaderBackground() {
if (window.scrollY > scrollThreshold) {
header?.classList.add('scrolled');
} else {
header?.classList.remove('scrolled');
}
}
// 初始检查
updateHeaderBackground();
// 添加滚动事件监听
window.addEventListener('scroll', updateHeaderBackground);
// 移动端菜单逻辑
const mobileMenuButton = document.getElementById('mobile-menu-button');
const mobileMenu = document.getElementById('mobile-menu');
const menuOpenIcon = document.getElementById('menu-open-icon');
const menuCloseIcon = document.getElementById('menu-close-icon');
if (mobileMenuButton && mobileMenu && menuOpenIcon && menuCloseIcon) {
mobileMenuButton.addEventListener('click', () => {
const expanded = mobileMenuButton.getAttribute('aria-expanded') === 'true';
// 切换菜单状态
mobileMenuButton.setAttribute('aria-expanded', (!expanded).toString());
if (expanded) {
// 直接隐藏菜单,不使用过渡效果
mobileMenu.classList.add('hidden');
} else {
// 直接显示菜单,不使用过渡效果
mobileMenu.classList.remove('hidden');
}
// 切换图标
menuOpenIcon.classList.toggle('hidden');
menuCloseIcon.classList.toggle('hidden');
});
}
// 移动端主题切换容器点击处理
document.addEventListener('DOMContentLoaded', () => {
const themeToggleContainer = document.getElementById('theme-toggle-container');
if (themeToggleContainer) {
themeToggleContainer.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
const themeToggleButton = themeToggleContainer.querySelector('[role="button"]');
// 如果点击的不是主题切换按钮本身,而是容器或文本
if (themeToggleButton instanceof HTMLElement && target !== themeToggleButton && !themeToggleButton.contains(target)) {
// 手动触发主题切换按钮的点击
themeToggleButton.click();
}
});
}
});
// 搜索功能逻辑
document.addEventListener('DOMContentLoaded', () => {
// 搜索节流函数
function debounce<T extends (...args: any[]) => void>(func: T, wait: number): (...args: Parameters<T>) => void {
let timeout: ReturnType<typeof setTimeout> | undefined;
return function(this: any, ...args: Parameters<T>): void {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// 获取DOM元素
const desktopSearch = document.getElementById('desktop-search');
const desktopResults = document.getElementById('desktop-search-results');
const desktopList = document.getElementById('desktop-search-list');
const desktopMessage = document.getElementById('desktop-search-message');
const mobileSearchButton = document.getElementById('mobile-search-button');
const mobileSearchPanel = document.getElementById('mobile-search-panel');
const mobileSearch = document.getElementById('mobile-search');
const mobileResults = document.getElementById('mobile-search-results');
const mobileList = document.getElementById('mobile-search-list');
const mobileMessage = document.getElementById('mobile-search-message');
const mobileSearchClose = document.getElementById('mobile-search-close');
// 文章对象的接口定义
interface Article {
id: string;
title: string;
date: string | Date;
summary?: string;
tags?: string[];
image?: string;
content?: string;
}
let articles: Article[] = [];
// 获取文章数据
async function fetchArticles() {
try {
const response = await fetch('/api/search');
if (!response.ok) {
throw new Error('获取文章数据失败');
}
articles = await response.json();
} catch (error) {
console.error('获取文章失败:', error);
}
}
// 高亮文本中的匹配部分
function highlightText(text: string, query: string): string {
if (!text || !query.trim()) return text;
// 转义正则表达式中的特殊字符
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escapedQuery})`, 'gi');
return text.replace(regex, '<mark class="bg-yellow-100 dark:bg-yellow-900/30 text-gray-900 dark:text-gray-100 px-0.5 rounded">$1</mark>');
}
// 搜索文章
function searchArticles(query: string, resultsList: HTMLElement, resultsMessage: HTMLElement) {
if (!query.trim()) {
resultsList.innerHTML = '';
resultsMessage.textContent = '输入关键词开始搜索';
resultsMessage.style.display = 'block';
return;
}
if (articles.length === 0) {
resultsMessage.textContent = '正在加载数据...';
resultsMessage.style.display = 'block';
return;
}
const lowerQuery = query.toLowerCase();
// 过滤并排序结果
const filteredArticles = articles
.filter((article: Article) => {
const title = article.title.toLowerCase();
const tags = article.tags ? article.tags.map((tag: string) => tag.toLowerCase()) : [];
const summary = article.summary ? article.summary.toLowerCase() : '';
const content = article.content ? article.content.toLowerCase() : '';
return title.includes(lowerQuery) ||
tags.some((tag: string) => tag.includes(lowerQuery)) ||
summary.includes(lowerQuery) ||
content.includes(lowerQuery);
})
.sort((a: Article, b: Article) => {
// 标题匹配优先
const aTitle = a.title.toLowerCase();
const bTitle = b.title.toLowerCase();
if (aTitle.includes(lowerQuery) && !bTitle.includes(lowerQuery)) {
return -1;
}
if (!aTitle.includes(lowerQuery) && bTitle.includes(lowerQuery)) {
return 1;
}
// 内容匹配次之
const aContent = a.content ? a.content.toLowerCase() : '';
const bContent = b.content ? b.content.toLowerCase() : '';
if (aContent.includes(lowerQuery) && !bContent.includes(lowerQuery)) {
return -1;
}
if (!aContent.includes(lowerQuery) && bContent.includes(lowerQuery)) {
return 1;
}
// 日期排序
return new Date(b.date).getTime() - new Date(a.date).getTime();
})
.slice(0, 10); // 限制结果数量
if (filteredArticles.length === 0) {
resultsList.innerHTML = '';
resultsMessage.textContent = '没有找到相关内容';
resultsMessage.style.display = 'block';
return;
}
// 显示结果
resultsMessage.style.display = 'none';
resultsList.innerHTML = filteredArticles.map((article: Article) => {
// 生成匹配的内容片段
let contentMatch = '';
if (article.content && article.content.toLowerCase().includes(lowerQuery)) {
// 找到匹配文本在内容中的位置
const matchIndex = article.content.toLowerCase().indexOf(lowerQuery);
// 计算片段的起始和结束位置
const startPos = Math.max(0, matchIndex - 50);
const endPos = Math.min(article.content.length, matchIndex + 100);
// 提取片段
let snippet = article.content.substring(startPos, endPos);
// 如果不是从文章开头开始,添加省略号
if (startPos > 0) {
snippet = '...' + snippet;
}
// 如果不是到文章结尾,添加省略号
if (endPos < article.content.length) {
snippet = snippet + '...';
}
// 高亮匹配的文本
snippet = highlightText(snippet, query);
contentMatch = `<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 line-clamp-2">${snippet}</p>`;
}
// 高亮标题和摘要中的匹配文本
const highlightedTitle = highlightText(article.title, query);
const highlightedSummary = article.summary ? highlightText(article.summary, query) : '';
return `
<li>
<a href="/articles/${article.id}" class="block px-4 py-3 hover:bg-gray-100 dark:hover:bg-gray-700/70 transition-colors duration-200">
<h3 class="text-sm font-medium text-gray-800 dark:text-gray-200 truncate">${highlightedTitle}</h3>
${article.summary ? `<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 truncate">${highlightedSummary}</p>` : ''}
${contentMatch}
${article.tags && article.tags.length > 0 ? `
<div class="flex flex-wrap gap-1 mt-1.5">
${article.tags.slice(0, 3).map((tag: string) => `
<span class="inline-block text-xs bg-primary-50/50 dark:bg-primary-900/20 text-primary-600 dark:text-primary-400 py-0.5 px-1.5 rounded-full">#${tag}</span>
`).join('')}
${article.tags.length > 3 ? `<span class="text-xs text-gray-400 dark:text-gray-500">+${article.tags.length - 3}</span>` : ''}
</div>
` : ''}
</a>
</li>
`}).join('');
}
// 节流搜索
const debouncedDesktopSearch = debounce((value: string) => {
if (desktopList && desktopMessage) {
searchArticles(value, desktopList as HTMLElement, desktopMessage as HTMLElement);
}
}, 300);
const debouncedMobileSearch = debounce((value: string) => {
if (mobileList && mobileMessage) {
searchArticles(value, mobileList as HTMLElement, mobileMessage as HTMLElement);
}
}, 300);
// 桌面端搜索逻辑
if (desktopSearch && desktopResults) {
desktopSearch.addEventListener('focus', () => {
desktopResults.classList.remove('hidden');
if (!articles.length) fetchArticles();
});
desktopSearch.addEventListener('input', (e: Event) => {
const target = e.target as HTMLInputElement;
if (target && target.value !== undefined) {
debouncedDesktopSearch(target.value);
}
});
// 点击外部关闭结果
document.addEventListener('click', (e: MouseEvent) => {
const target = e.target as Node;
if (desktopSearch && !desktopSearch.contains(target) && !desktopResults.contains(target)) {
desktopResults.classList.add('hidden');
}
});
// ESC键关闭结果
desktopSearch.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Escape') {
desktopResults.classList.add('hidden');
}
});
}
// 移动端搜索逻辑
if (mobileSearchButton && mobileSearchPanel) {
mobileSearchButton.addEventListener('click', () => {
mobileSearchPanel.classList.remove('hidden');
mobileSearchPanel.classList.add('show');
if (mobileSearch) mobileSearch.focus();
if (!articles.length) fetchArticles();
});
if (mobileSearchClose) {
mobileSearchClose.addEventListener('click', () => {
mobileSearchPanel.classList.add('hidden');
mobileSearchPanel.classList.remove('show');
});
}
if (mobileSearch && mobileResults) {
mobileSearch.addEventListener('input', (e: Event) => {
mobileResults.classList.remove('hidden');
const target = e.target as HTMLInputElement;
if (target && target.value !== undefined) {
debouncedMobileSearch(target.value);
}
});
// ESC键关闭搜索面板
mobileSearch.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Escape') {
mobileSearchPanel.classList.add('hidden');
mobileSearchPanel.classList.remove('show');
}
});
}
}
});
</script>

View File

@ -3,185 +3,170 @@ title: "常用软件"
date: 2023-04-28T20:56:00Z date: 2023-04-28T20:56:00Z
tags: [] tags: []
--- ---
### Windows 应用 ### Windows 应用
--- ---
- **开发软件:** * **开发软件:**
- [JetBrains 全家桶](https://www.jetbrains.com/zh-cn/products/) * [JetBrains全家桶](https://www.jetbrains.com/zh-cn/products/)
- [JetBrains 破解软件](https://linux.do/t/topic/115562) * [JetBrains破解软件](https://linux.do/t/topic/115562)
* **压缩软件:** [NanaZIP](https://github.com/M2Team/NanaZip) (应用商店可下载)
* **办公软件:**
- **压缩软件:** [NanaZIP](https://github.com/M2Team/NanaZip) (应用商店可下载) * [Office Tool Plus](https://otp.landian.vip/zh-cn/download.html) (office下载)
- **办公软件:** * [WAS](https://github.com/massgravel/Microsoft-Activation-Scripts) (windows/office 激活)
* [HEU_KMS_Activator](https://github.com/zbezj/HEU_KMS_Activator/releases) (windows/office 激活)
* **下载器:** [IDM](https://www.internetdownloadmanager.com/)
* **卸载工具:** [Geek](https://geekuninstaller.com/)
* **文件搜索工具:** [Everything](https://www.voidtools.com/downloads/)
* **电脑硬件检测:** [图吧工具箱](http://www.tbtool.cn/)
* **浏览器:**
- [Office Tool Plus](https://otp.landian.vip/zh-cn/download.html) (office 下载) * [chrome](https://www.google.com/chrome/)
- [WAS](https://github.com/massgravel/Microsoft-Activation-Scripts) (windows/office 激活) * [Arc](https://arc.net/)
- [HEU_KMS_Activator](https://github.com/zbezj/HEU_KMS_Activator/releases) (windows/office 激活) * [百分浏览器](https://www.centbrowser.cn/)
* **ssh工具**
- **下载器:** [IDM](https://www.internetdownloadmanager.com/) * [tabby](https://github.com/Eugeny/tabby/tree/master)
- **卸载工具:** [Geek](https://geekuninstaller.com/) * [FinalShell](http://www.hostbuf.com/)
- **文件搜索工具:** [Everything](https://www.voidtools.com/downloads/) * **adb工具** [Platform Tools](https://developer.android.com/tools/releases/platform-tools?hl=zh-cn)
- **电脑硬件检测:** [图吧工具箱](http://www.tbtool.cn/) * **科学上网工具:**
- **浏览器:**
- [chrome](https://www.google.com/chrome/) * [Clash for Windows](https://github.com/Z-Siqi/Clash-for-Windows_Chinese/releases)
- [Arc](https://arc.net/) * [v2rayN](https://github.com/2dust/v2rayN/releases/)
- [百分浏览器](https://www.centbrowser.cn/) * [v2ray-rules-dat](https://github.com/Loyalsoldier/v2ray-rules-dat)V2Ray 路由规则文件加强版
* **驱动管理:** [360驱动大师](http://dm.weishi.360.cn/home.html)
* **运行库安装:** [NET Framework](https://dotnet.microsoft.com/zh-cn/download/dotnet-framework)
* **任务栏透明工具:** [TranslucentTB](https://github.com/TranslucentTB/TranslucentTB#start-of-content) (应用商店可下载)
* **壁纸软件:** [Wallpaper Engine](https://store.steampowered.com/app/431960/Wallpaper_Engine/)
* **防火墙应用:**
- **ssh 工具:** * [360安全卫士极速版](https://weishi.360.cn/jisu/)
* [火绒](https://www.huorong.cn/)
- [tabby](https://github.com/Eugeny/tabby/tree/master)
- [FinalShell](http://www.hostbuf.com/)
- **adb 工具:** [Platform Tools](https://developer.android.com/tools/releases/platform-tools?hl=zh-cn)
- **科学上网工具:**
- [Clash for Windows](https://github.com/Z-Siqi/Clash-for-Windows_Chinese/releases)
- [v2rayN](https://github.com/2dust/v2rayN/releases/)
- [v2ray-rules-dat](https://github.com/Loyalsoldier/v2ray-rules-dat)V2Ray 路由规则文件加强版
- **驱动管理:** [360 驱动大师](http://dm.weishi.360.cn/home.html)
- **运行库安装:** [NET Framework](https://dotnet.microsoft.com/zh-cn/download/dotnet-framework)
- **任务栏透明工具:** [TranslucentTB](https://github.com/TranslucentTB/TranslucentTB#start-of-content) (应用商店可下载)
- **壁纸软件:** [Wallpaper Engine](https://store.steampowered.com/app/431960/Wallpaper_Engine/)
- **防火墙应用:**
- [360 安全卫士极速版](https://weishi.360.cn/jisu/)
- [火绒](https://www.huorong.cn/)
### Android 应用 ### Android 应用
--- ---
- **下载器:** 1DM+ * **下载器:** 1DM+
- **安装器:** R-安装组件 * **安装器:** R-安装组件
- **浏览器:** * **浏览器:**
- [雨见浏览器国际版](https://yjllq.com/) * [雨见浏览器国际版](https://yjllq.com/)
- [Alook](https://www.alookweb.com/) * [Alook](https://www.alookweb.com/)
* **文件管理器:** [MT管理器](https://mt2.cn/)
* **网络工具:**
- **文件管理器:** [MT 管理器](https://mt2.cn/) * Cellular Pro
- **网络工具:** * VPN热点
* **科学上网:**
- Cellular Pro * Surfboard
- VPN 热点 * [v2rayNG](https://github.com/2dust/v2rayNG/releases)
* **终端模拟器**:
- **科学上网:** * [Termux](https://termux.dev/)
* [ZeroTermux](https://github.com/hanxinhao000/ZeroTermux)
* **Git 存储库管理工具:** [GitNex](https://gitnex.com/)
* **视频播放器:**
- Surfboard * MX播放器专业版
- [v2rayNG](https://github.com/2dust/v2rayNG/releases) * XPlayer - 万能视频播放器
* **服务器连接:**
- **终端模拟器**: * [JuiceSSH](https://juicessh.com/)
* RD客户端 (连接Windows)
* [Termius](https://termius.com/)
* **根目录整理:** [存储空间隔离](https://sr.rikka.app/)
* **定位修改:** [Fake Location](https://github.com/Lerist/FakeLocation)
* **将操作系统映像写入USB驱动器** [EtchDroid](https://github.com/EtchDroid/EtchDroid)
* **adb工具**
- [Termux](https://termux.dev/) * [搞机助手](https://gjzsr.com/)
- [ZeroTermux](https://github.com/hanxinhao000/ZeroTermux) * 甲壳虫ADB助手
* Bugjaeger Premium
* [scene](https://github.com/kdrag0n/safetynet-fix/releases)
* **Xposed软件**
- **Git 存储库管理工具:** [GitNex](https://gitnex.com/) * **抖音增强:** [FreedomPlus](https://github.com/Xposed-Modules-Repo/io.github.fplus)
- **视频播放器:** * **bilibili增强** [哔哩漫游](https://github.com/yujincheng08/BiliRoaming)
* **微信增强:** [微x模块](https://github.com/Xposed-Modules-Repo/com.fkzhang.wechatxposed)
* **QQ增强:** [QAuxiliary](https://github.com/Xposed-Modules-Repo/io.github.qauxv)
* **识别短信验证码:** [XposedSmsCode](https://github.com/Xposed-Modules-Repo/com.github.tianma8023.xposed.smscode)
* **Magisk模块管理**
- MX 播放器专业版 * [LSPosed](https://github.com/LSPosed/LSPosed):已归档
- XPlayer - 万能视频播放器 * 关闭SELinux
* 停用HW叠加层模块
- **服务器连接:** * [Universal SafetyNet Fix](https://github.com/kdrag0n/safetynet-fix):绕过 Google 的 SafetyNet 认证
* [Shamiko](https://github.com/LSPosed/LSPosed.github.io/releases):隐藏更多 Magisk 痕迹
- [JuiceSSH](https://juicessh.com/)
- RD 客户端 (连接 Windows)
- [Termius](https://termius.com/)
- **根目录整理:** [存储空间隔离](https://sr.rikka.app/)
- **定位修改:** [Fake Location](https://github.com/Lerist/FakeLocation)
- **将操作系统映像写入 USB 驱动器:** [EtchDroid](https://github.com/EtchDroid/EtchDroid)
- **adb 工具:**
- [搞机助手](https://gjzsr.com/)
- 甲壳虫 ADB 助手
- Bugjaeger Premium
- [scene](https://github.com/kdrag0n/safetynet-fix/releases)
- **Xposed 软件**
- **抖音增强:** [FreedomPlus](https://github.com/Xposed-Modules-Repo/io.github.fplus)
- **bilibili 增强:** [哔哩漫游](https://github.com/yujincheng08/BiliRoaming)
- **微信增强:** [微 x 模块](https://github.com/Xposed-Modules-Repo/com.fkzhang.wechatxposed)
- **QQ 增强:** [QAuxiliary](https://github.com/Xposed-Modules-Repo/io.github.qauxv)
- **识别短信验证码:** [XposedSmsCode](https://github.com/Xposed-Modules-Repo/com.github.tianma8023.xposed.smscode)
- **Magisk 模块管理:**
- [LSPosed](https://github.com/LSPosed/LSPosed):已归档
- 关闭 SELinux
- 停用 HW 叠加层模块
- [Universal SafetyNet Fix](https://github.com/kdrag0n/safetynet-fix):绕过 Google 的 SafetyNet 认证
- [Shamiko](https://github.com/LSPosed/LSPosed.github.io/releases):隐藏更多 Magisk 痕迹
### 浏览器插件 ### 浏览器插件
--- ---
- [AdGuard](https://adguard.com/zh_cn/adguard-browser-extension/overview.html):广告拦截程序 * [AdGuard](https://adguard.com/zh_cn/adguard-browser-extension/overview.html):广告拦截程序
- [篡改猴](https://www.tampermonkey.net/index.php?browser=chrome&locale=zh):脚本管理器 * [篡改猴](https://www.tampermonkey.net/index.php?browser=chrome&locale=zh):脚本管理器
- [猫抓](https://o2bmm.gitbook.io/cat-catch):资源嗅探 * [猫抓](https://o2bmm.gitbook.io/cat-catch):资源嗅探
- [图片助手(ImageAssistant)](https://www.pullywood.com/ImageAssistant/):嗅探、分析网页图片、图片筛选 * [图片助手(ImageAssistant)](https://www.pullywood.com/ImageAssistant/):嗅探、分析网页图片、图片筛选
- [Circle 阅读助手](http://www.circlereader.com/):提取并排版网页内容 * [Circle 阅读助手](http://www.circlereader.com/):提取并排版网页内容
- [Global Speed](https://chrome.google.com/webstore/detail/global-speed/jpbjcnkcffbooppibceonlgknpkniiff?hl=zh-CN):速度控制 * [Global Speed](https://chrome.google.com/webstore/detail/global-speed/jpbjcnkcffbooppibceonlgknpkniiff?hl=zh-CN):速度控制
- [ColorZilla](https://www.colorzilla.com/zh-cn/):拾色器 * [ColorZilla](https://www.colorzilla.com/zh-cn/):拾色器
- [Selenium IDE](https://www.selenium.dev/zh-cn/documentation/ide/):记录和回放用户操作 * [Selenium IDE](https://www.selenium.dev/zh-cn/documentation/ide/):记录和回放用户操作
- [Floccus](https://floccus.org/download):同步书签和标签 * [Floccus](https://floccus.org/download):同步书签和标签
- [沉浸式翻译:](https://immersivetranslate.com/) 网页翻译 * [沉浸式翻译:](https://immersivetranslate.com/) 网页翻译
### Docker 应用 ### Docker 应用
--- ---
- [vaultwarden](https://github.com/dani-garcia/vaultwarden):密码管理器,[Bitwarden](https://bitwarden.com/)的第三方 Docker 项目。 * [vaultwarden](https://github.com/dani-garcia/vaultwarden):密码管理器,[Bitwarden](https://bitwarden.com/)的第三方 Docker 项目。
- [AList](https://alist.nn.ci/zh/):支持多种文件储存,支持 WebDAV * [AList](https://alist.nn.ci/zh/):支持多种文件储存,支持 WebDAV
- [思源笔记](https://b3log.org/siyuan/):支持 web * [思源笔记](https://b3log.org/siyuan/)支持web端
- [Gitea](https://about.gitea.com/):支持基于 Git 创建和管理存储库 * [Gitea](https://about.gitea.com/):支持基于 Git 创建和管理存储库
- [Nginx Proxy Manager](https://nginxproxymanager.com/)Nginx 代理管理器 * [Nginx Proxy Manager](https://nginxproxymanager.com/)Nginx 代理管理器
- [雷池](https://waf-ce.chaitin.cn/):基于 Nginx 开发的 Web 应用防火墙 * [雷池](https://waf-ce.chaitin.cn/)基于Nginx开发的Web 应用防火墙
### Linux 应用 ### Linux 应用
--- ---
- [OpenSSH](https://www.openssh.com/)SSH 连接工具 * [OpenSSH](https://www.openssh.com/)SSH连接工具
- `Wget` `curl`:从网络上获取或发送数据 * `Wget` `curl`:从网络上获取或发送数据
- [vim](https://www.vim.org/):文本编辑器 * [vim](https://www.vim.org/):文本编辑器
- [Git](https://git-scm.com/):分布式版本控制系统 * [Git](https://git-scm.com/):分布式版本控制系统
- [Zsh](https://www.zsh.org/):是一款功能强大、灵活可定制的 shell * [Zsh](https://www.zsh.org/)是一款功能强大、灵活可定制的shell
- [哪吒面板](https://nezha.wiki/):服务器监控与运维工具 * [哪吒面板](https://nezha.wiki/):服务器监控与运维工具
- [screenfetch](https://github.com/KittyKatt/screenFetch):屏幕截图 * [screenfetch](https://github.com/KittyKatt/screenFetch):屏幕截图
- [X-CMD](https://cn.x-cmd.com/):命令增强和扩展 * [X-CMD](https://cn.x-cmd.com/):命令增强和扩展
- 镜像仓库: * 镜像仓库:
- [中科大开源软件镜像站](https://mirrors.ustc.edu.cn/) * [中科大开源软件镜像站](https://mirrors.ustc.edu.cn/)
- [阿里巴巴开源镜像站](https://developer.aliyun.com/mirror/) * [阿里巴巴开源镜像站](https://developer.aliyun.com/mirror/)
- [网易开源镜像站](https://mirrors.163.com/) * [网易开源镜像站](https://mirrors.163.com/)
- [腾讯软件源](https://mirrors.cloud.tencent.com/) * [腾讯软件源](https://mirrors.cloud.tencent.com/)
- [华为开源镜像站](https://mirrors.huaweicloud.com/home) * [华为开源镜像站](https://mirrors.huaweicloud.com/home)
- [移动云开源镜像站](https://mirrors.cmecloud.cn/) * [移动云开源镜像站](https://mirrors.cmecloud.cn/)
- [清华大学开源软件镜像站](https://mirrors.tuna.tsinghua.edu.cn/) * [清华大学开源软件镜像站](https://mirrors.tuna.tsinghua.edu.cn/)
### 可被托管的应用 ### 可被托管的应用
--- ---
- [CF-Workers-docker.io](https://github.com/cmliu/CF-Workers-docker.io):使用 CF-workersDocker 仓库镜像代理工具 * [CF-Workers-docker.io](https://github.com/cmliu/CF-Workers-docker.io)使用CF-workersDocker仓库镜像代理工具
- [Cloudflare Proxy EX](https://github.com/1234567yang/cf-proxy-ex):使用 CF-workers 搭建代理 * [Cloudflare Proxy EX](https://github.com/1234567yang/cf-proxy-ex)使用CF-workers搭建代理
- [Vercel 部署 Hugo 站点](https://vercel.com/guides/deploying-hugo-with-vercel) * [Vercel 部署 Hugo 站点](https://vercel.com/guides/deploying-hugo-with-vercel)
### 网站 ### 网站
--- ---
- 盗版软件: * 盗版软件:
- [CyberMania](https://www.cybermania.ws/) * [CyberMania](https://www.cybermania.ws/)
- [果核剥壳](https://www.ghxi.com/) * [果核剥壳](https://www.ghxi.com/)
* 单价游戏:
- 单价游戏: * [土豆资源库](http://tdtd.chat/index)
* [3DMGAME](https://bbs.3dmgame.com/forum.php)
- [土豆资源库](http://tdtd.chat/index)
- [3DMGAME](https://bbs.3dmgame.com/forum.php)

View File

@ -22,5 +22,5 @@ tags: []
[VPN 热点](https://lsy22.lanzouj.com/iS9hw0hz5rfa?password=lsy22) [VPN 热点](https://lsy22.lanzouj.com/iS9hw0hz5rfa?password=lsy22)
1. 打开 _VPN 热点_ 给予 root 权限 1. 打开 *VPN 热点* 给予 root 权限
2. 将 _WLAN 热点_ 打开,打开后会多一个 _wlan1_,将 _wlan1_ 打开就可以实现 VPN 热点了 2. 将 *WLAN 热点* 打开,打开后会多一个 *wlan1*,将 *wlan1* 打开就可以实现 VPN 热点了

View File

@ -16,16 +16,16 @@ Eternalblue 通过 TCP 端口 445 和 139 来利用 SMBv1 和 NBT 中的远程
### 二. 复现环境 ### 二. 复现环境
- 虚拟环境搭建:`VMware Workstation 17 pro` * 虚拟环境搭建:`VMware Workstation 17 pro`
- 网络模式:`NAT` * 网络模式:`NAT`
- 攻击机:`kali Linux WSL` * 攻击机:`kali Linux WSL`
- 攻击机 IP`192.168.97.173` * 攻击机IP`192.168.97.173`
- 攻击工具:`nmap` `metasploit(MSF)` * 攻击工具:`nmap` `metasploit(MSF)`
- 靶机:`cn_windows_7_ultimate_with_sp1_x64_dvd_u_677408`**前提 win7 关闭防火墙** * 靶机:`cn_windows_7_ultimate_with_sp1_x64_dvd_u_677408`**前提win7关闭防火墙**
- 靶机 IP`192.168.97.128` * 靶机IP`192.168.97.128`
### 三. 启动 MSF ### 三. 启动 MSF
@ -49,39 +49,37 @@ Eternalblue 通过 TCP 端口 445 和 139 来利用 SMBv1 和 NBT 中的远程
### 四. 寻找主机 ### 四. 寻找主机
- **ipconfig** * **ipconfig**
使用`ipconfig`分别查看win7和kali中的ip地址 使用`ipconfig`分别查看win7和kali中的ip地址
* **nmap**
- **nmap**
```bash ```bash
nmap -T5 -sP 192.168.97.0/24 nmap -T5 -sP 192.168.97.0/24
``` ```
- **`-T5`**:使用速度模板**`T5`**,表示激进的扫描速度。 * **`-T5`**:使用速度模板**`T5`**,表示激进的扫描速度。
- **`-sP`**:执行 Ping 连接扫描。 * **`-sP`**:执行 Ping 连接扫描。
- **`192.168.97.0/24`**:扫描指定的 IP 地址范围。 * **`192.168.97.0/24`**:扫描指定的 IP 地址范围。
| IP地址 | 私有ip范围 | 子网掩码 | CIDR | | IP地址 | 私有ip范围 | 子网掩码 | CIDR |
| -------- | :----------------------------- | :------------ | :------------- | | --------- | :----------------------------- | :-------------- | :--------------- |
| A类地址 | 10.0.0.010.255.255.255 | 255.0.0.0 | 10.0.0.0/8 | | A类地址 | 10.0.0.010.255.255.255 | 255.0.0.0 | 10.0.0.0/8 |
| B类地址 | 172.16.0.0173.31.255.255 | 255.255.0.0 | 172.16.0.0/16 | | B类地址 | 172.16.0.0173.31.255.255 | 255.255.0.0 | 172.16.0.0/16 |
| C类地址 | 192.168.0.0192.168.255.255 | 255.255.255.0 | 192.168.0.0/24 | | C类地址 | 192.168.0.0192.168.255.255 | 255.255.255.0 | 192.168.0.0/24 |
### 五. 端口扫描 ### 五. 端口扫描
- **nmap** * **nmap**
```bash ```bash
nmap -T5 -sT 192.168.97.128 nmap -T5 -sT 192.168.97.128
``` ```
- **`-T5`**:使用速度模板**`T5`**,表示激进的扫描速度。 * **`-T5`**:使用速度模板**`T5`**,表示激进的扫描速度。
- **`-sT`**:执行 TCP 连接扫描。 * **`-sT`**:执行 TCP 连接扫描。
- **`192.168.97.128`**:扫描指定的 IP * **`192.168.97.128`**:扫描指定的 IP
* **MSF** 端口扫描
- **MSF** 端口扫描
1. 使用模块 1. 使用模块
@ -111,30 +109,27 @@ search ms17_010
1. `exploit/windows/smb/ms17_010_eternalblue` 1. `exploit/windows/smb/ms17_010_eternalblue`
- 这个模块利用了 MS17-010 漏洞,通过 EternalBlue 攻击载荷,远程执行代码。 * 这个模块利用了MS17-010漏洞通过EternalBlue攻击载荷远程执行代码。
- EternalBlue 利用 Windows 的 Server Message BlockSMB协议中的漏洞允许攻击者在目标机器上执行任意代码。 * EternalBlue利用Windows的Server Message BlockSMB协议中的漏洞允许攻击者在目标机器上执行任意代码。
- 攻击成功后,通常会在目标机器上生成一个 Meterpreter 会话,从而允许进一步的渗透测试操作。 * 攻击成功后通常会在目标机器上生成一个Meterpreter会话从而允许进一步的渗透测试操作。
2. `exploit/windows/smb/ms17_010_psexec` 2. `exploit/windows/smb/ms17_010_psexec`
- 这个模块结合 MS17-010 漏洞和 Psexec 技术,通过 SMB 协议在目标系统上执行命令。 * 这个模块结合MS17-010漏洞和Psexec技术通过SMB协议在目标系统上执行命令。
- 利用 MS17-010 漏洞进行初始攻击,然后使用 Psexec 进行进一步的远程命令执行。 * 利用MS17-010漏洞进行初始攻击然后使用Psexec进行进一步的远程命令执行。
- 适用于在利用 MS17-010 漏洞后希望使用 Psexec 执行进一步的命令和控制操作时。 * 适用于在利用MS17-010漏洞后希望使用Psexec执行进一步的命令和控制操作时。
3. `auxiliary/admin/smb/ms17_010_command` 3. `auxiliary/admin/smb/ms17_010_command`
- 这个辅助模块用于通过 MS17-010 漏洞在目标系统上执行指定的命令。 * 这个辅助模块用于通过MS17-010漏洞在目标系统上执行指定的命令。
- 不会生成一个持久的会话,而是直接执行特定的命令并返回结果。 * 不会生成一个持久的会话,而是直接执行特定的命令并返回结果。
- 适用于希望通过 MS17-010 漏洞在目标系统上执行单个命令的场景。 * 适用于希望通过MS17-010漏洞在目标系统上执行单个命令的场景。
4. `auxiliary/scanner/smb/smb_ms17_010` 4. `auxiliary/scanner/smb/smb_ms17_010`
- 这个辅助模块用于扫描目标系统是否存在 MS17-010 漏洞。 * 这个辅助模块用于扫描目标系统是否存在MS17-010漏洞。
- 不会进行实际的漏洞利用或攻击,而是仅检测目标系统是否易受 MS17-010 漏洞的影响。 * 不会进行实际的漏洞利用或攻击而是仅检测目标系统是否易受MS17-010漏洞的影响。
### 七. 漏洞检测 ### 七. 漏洞检测
- 使用探测模块 * 使用探测模块
1. 使用`Auxiliary`辅助探测模块 1. 使用`Auxiliary`辅助探测模块
@ -166,7 +161,7 @@ search ms17_010
run run
``` ```
- nmap * nmap
```bash ```bash
nmap --script smb-vuln-ms17-010 192.168.97.128 nmap --script smb-vuln-ms17-010 192.168.97.128
@ -375,19 +370,19 @@ timestomp 操作文件 MACE 属性
#### 基础使用 #### 基础使用
- 进入框架 * 进入框架
```bash ```bash
msfconsole msfconsole
``` ```
- 查找漏洞 * 查找漏洞
```bash ```bash
search 漏洞编号 search 漏洞编号
``` ```
- 使用模块 * 使用模块
```bash ```bash
run run
@ -421,39 +416,40 @@ exploit 漏洞利用模块路径(这里面有针对不同平台的 exploit)
##### Metasploit中的 Payload 模块主要有以下三种类型 ##### Metasploit中的 Payload 模块主要有以下三种类型
- Single * Single
> 是一种完全独立的Payload而且使用起来就像运行calc.exe一样简单例如添加一个系统用户或删除一份文件。由于Single Payload是完全独立的因此它们有可能会被类似netcat这样的非metasploit处理工具所捕捉到。 > 是一种完全独立的Payload而且使用起来就像运行calc.exe一样简单例如添加一个系统用户或删除一份文件。由于Single Payload是完全独立的因此它们有可能会被类似netcat这样的非metasploit处理工具所捕捉到。
>
* Stager
- Stager > 这种Payload 负责建立目标用户与攻击者之间的网络连接并下载额外的组件或应用程序。一种常见的Stager Payload就是reverse_tcp它可以让目标系统与攻击者建立一条 tcp 连接,让目标系统主动连接我们的端口(反向连接)。另一种常见的是bind_tcp它可以让目标系统开启一个tcp监听器而攻击者随时可以与目标系统进行通信(正向连接)。  
>
> 这种 Payload 负责建立目标用户与攻击者之间的网络连接,并下载额外的组件或应用程序。一种常见的 Stager Payload 就是 reverse_tcp它可以让目标系统与攻击者建立一条 tcp 连接,让目标系统主动连接我们的端口(反向连接)。另一种常见的是 bind_tcp它可以让目标系统开启一个 tcp 监听器,而攻击者随时可以与目标系统进行通信(正向连接)。 * Stage
- Stage
> 是Stager Payload下的一种Payload组件这种Payload可以提供更加高级的功能而且没有大小限制。 > 是Stager Payload下的一种Payload组件这种Payload可以提供更加高级的功能而且没有大小限制。
>
##### 几种常见的payload ##### 几种常见的payload
- 正向连接 * 正向连接
```bash ```bash
windows/meterpreter/bind_tcp windows/meterpreter/bind_tcp
``` ```
- 反向连接 * 反向连接
```bash ```bash
windows/meterpreter/reverse_tcp windows/meterpreter/reverse_tcp
``` ```
- 过监听 80 端口反向连接 * 过监听80端口反向连接
```bash ```bash
windows/meterpreter/reverse_http windows/meterpreter/reverse_http
``` ```
- 通过监听 443 端口反向连接 * 通过监听443端口反向连接
```bash ```bash
windows/meterpreter/reverse_https windows/meterpreter/reverse_https
@ -461,14 +457,15 @@ exploit 漏洞利用模块路径(这里面有针对不同平台的 exploit)
##### **使用场景** ##### **使用场景**
- 正向连接使用场景: * 正向连接使用场景:
> 我们的攻击机在内网环境被攻击机是外网环境由于被攻击机无法主动连接到我们的主机所以就必须我们主动连接被攻击机了。但是这里经常遇到的问题是被攻击机上开了防火墙只允许访问指定的端口比如被攻击机只对外开放了80端口。那么我们就只能设置正向连接80端口了这里很有可能失败因为80端口上的流量太多了。 > 我们的攻击机在内网环境被攻击机是外网环境由于被攻击机无法主动连接到我们的主机所以就必须我们主动连接被攻击机了。但是这里经常遇到的问题是被攻击机上开了防火墙只允许访问指定的端口比如被攻击机只对外开放了80端口。那么我们就只能设置正向连接80端口了这里很有可能失败因为80端口上的流量太多了。
>
- 反向连接使用场景: * 反向连接使用场景:
> 我们的主机和被攻击机都是在外网或者都是在内网,这样被攻击机就能主动连接到我们的主机了。如果是这样的情况,建议使用反向连接,因为反向连接的话,即使被攻击机开了防火墙也没事,防火墙只是阻止进入被攻击机的流量,而不会阻止被攻击机主动向外连接的流量。 > 我们的主机和被攻击机都是在外网或者都是在内网,这样被攻击机就能主动连接到我们的主机了。如果是这样的情况,建议使用反向连接,因为反向连接的话,即使被攻击机开了防火墙也没事,防火墙只是阻止进入被攻击机的流量,而不会阻止被攻击机主动向外连接的流量。
>
- 反向连接 80 和 443 端口使用场景: * 反向连接80和443端口使用场景:
> 被攻击机能主动连接到我们的主机还有就是被攻击机的防火墙设置的特别严格就连被攻击机访问外部网络的流量也进行了严格的限制只允许被攻击机的80端口或443端口与外部通信。 > 被攻击机能主动连接到我们的主机还有就是被攻击机的防火墙设置的特别严格就连被攻击机访问外部网络的流量也进行了严格的限制只允许被攻击机的80端口或443端口与外部通信。
>

View File

@ -98,11 +98,12 @@ tags: ["Docker-compose"]
可自行更改 可自行更改
- nginx 中的端口,默认为`9757` * nginx 中的端口,默认为`9757`
- MySQL 中的 root 的密码 和 需要创建的数据库名称,默认都为`typecho` * MySQL中的 root的密码 和 需要创建的数据库名称,默认都为`typecho`
```yaml ```yaml
services: # 定义多个服务 services: # 定义多个服务
nginx: # 服务名称 nginx: # 服务名称
image: nginx # 使用的镜像 image: nginx # 使用的镜像
ports: # 映射的端口 ports: # 映射的端口
@ -156,17 +157,17 @@ docker compose up -d
如果修改过`docker-compose.yml` 如果修改过`docker-compose.yml`
- 数据库地址: `mysql` * 数据库地址: `mysql`
```text ```text
因为docker内部网络可以用过容器名访问 因为docker内部网络可以用过容器名访问
``` ```
- 数据库用户名: `root` * 数据库用户名: `root`
- 数据库密码: `typecho` * 数据库密码: `typecho`
- 数据库名: `typecho` * 数据库名: `typecho`
- 启用数据库 SSL 服务端证书验证: 关闭 * 启用数据库 SSL 服务端证书验证: 关闭
- 其他默认或随意 * 其他默认或随意
## 问题 ## 问题

View File

@ -149,4 +149,3 @@ server {
proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Port $server_port;
} }
} }
```

View File

@ -8,8 +8,8 @@ tags: ["Docker-compose"]
### 替换说明 ### 替换说明
-`/var/www/siyuan/` 替换为你的实际物理路径。 *`/var/www/siyuan/` 替换为你的实际物理路径。
-`Password` 替换为你的访问密码。 *`Password` 替换为你的访问密码。
```yaml ```yaml
version: "3.9" version: "3.9"
@ -32,8 +32,8 @@ services:
### Nginx配置替换说明 ### Nginx配置替换说明
-`your_domain.com` 替换为你自己的域名。 *`your_domain.com` 替换为你自己的域名。
- 将 `path` 替换为你的 SSL 证书的实际路径。 * 将 `path` 替换为你的SSL证书的实际路径。
```nginx ```nginx
upstream siyuan { upstream siyuan {
@ -78,4 +78,3 @@ server {
proxy_set_header Connection 'Upgrade'; # 支持 WebSocket proxy_set_header Connection 'Upgrade'; # 支持 WebSocket
} }
} }
```

View File

@ -2,6 +2,7 @@
title: "密码管理器—Vaultwarden(bitwarden)" title: "密码管理器—Vaultwarden(bitwarden)"
date: 2023-05-18T21:47:00+00:00 date: 2023-05-18T21:47:00+00:00
tags: ["Docker-compose"] tags: ["Docker-compose"]
--- ---
## 1. 安装 Vaultwarden ## 1. 安装 Vaultwarden
@ -9,7 +10,7 @@ tags: ["Docker-compose"]
使用以下 `docker-compose.yml` 文件部署 Vaultwarden 使用以下 `docker-compose.yml` 文件部署 Vaultwarden
```yaml ```yaml
version: "3.8" version: '3.8'
services: services:
bitwarden: bitwarden:
image: vaultwarden/server:latest image: vaultwarden/server:latest

View File

@ -19,7 +19,7 @@ tags: ["Docker-compose", "WebDAV"]
运行以下Docker Compose文件进行Alist的安装 运行以下Docker Compose文件进行Alist的安装
```yaml ```yaml
version: "3.8" version: '3.8'
services: services:
alist: alist:
image: xhofe/alist:latest image: xhofe/alist:latest

View File

@ -42,19 +42,20 @@ sh -c "$(wget https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/ins
### 主题 ### 主题
- [powerlevel10k](https://github.com/romkatv/powerlevel10k) * [powerlevel10k](https://github.com/romkatv/powerlevel10k)
### 插件 ### 插件
- [zsh-autosuggestions](https://github.com/zsh-users/zsh-autosuggestions): 根据历史记录和完成情况在您输入时建议命令 * [zsh-autosuggestions](https://github.com/zsh-users/zsh-autosuggestions): 根据历史记录和完成情况在您输入时建议命令
- [zsh-syntax-highlighting](https://github.com/zsh-users/zsh-syntax-highlighting): 输入命令时提供语法高亮 * [zsh-syntax-highlighting](https://github.com/zsh-users/zsh-syntax-highlighting): 输入命令时提供语法高亮
#### oh-my-zsh 自带插件 #### oh-my-zsh 自带插件
直接按照上述方法在 `.zshrc` 配置的 `plugins` 中加入即可: 直接按照上述方法在 `.zshrc` 配置的 `plugins` 中加入即可:
- [command-not-found](https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/command-not-found): 在 `zsh` 找不到命令时提供建议的安装包 * [command-not-found](https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/command-not-found): 在 `zsh` 找不到命令时提供建议的安装包
- [extract](https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/extract): 使用 `x` 命令解压任何压缩文件 * [extract](https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/extract): 使用 `x` 命令解压任何压缩文件
- [pip](https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/pip): 为 `python` 包管理器 `pip` 提供补全 * [pip](https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/pip): 为 `python` 包管理器 `pip` 提供补全
- [docker](https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/docker): 为 `docker` 命令添加自动补全支持 * [docker](https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/docker): 为 `docker` 命令添加自动补全支持
- [docker-compose](https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/docker-compose): 为 `docker-compose` 命令添加自动补全支持 * [docker-compose](https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/docker-compose): 为 `docker-compose` 命令添加自动补全支持

View File

@ -52,11 +52,11 @@ tags: []
##### bypy 基本操作 ##### bypy 基本操作
- `bypy info`:查看空间使用信息。 * `bypy info`:查看空间使用信息。
- `bypy list`:查看目录信息。 * `bypy list`:查看目录信息。
- `bypy upload`:上传根目录所有文件。 * `bypy upload`:上传根目录所有文件。
- `bypy downdir`:把云盘上的内容同步到本地。 * `bypy downdir`:把云盘上的内容同步到本地。
- `bypy compare`:比较本地当前目录和云盘根目录。 * `bypy compare`:比较本地当前目录和云盘根目录。
## 安装阿里网盘备份工具 ## 安装阿里网盘备份工具

View File

@ -1,33 +0,0 @@
---
title: "3x-ui配置"
date: 2025-04-19T10:48:48+08:00
tags: []
---
3x-ui[https://github.com/MHSanaei/3x-ui](https://github.com/MHSanaei/3x-ui)
安全:`Reality`
Dest (Target)
```txt
apple.com:443
```
SNI
```txt
apple.com
```
公钥
```txt
jCK3ORKMwzmJsig7gMWOTGlaF2wvuVAcEr7jbB1bnwU
```
私钥
```txt
oN60djdg_ZRUiChp0Rj-emZ3VhM_x8rL93H4rhdq1Wc
```

View File

@ -0,0 +1,11 @@
---
title: "dns 解锁"
date: 2025-01-18 14:19:00Z
tags: []
---
## 1. 安装dnsmasq
```bash
yum install dnsmasq -y
```

View File

@ -63,7 +63,8 @@ ntpdate time.nist.gov
```json ```json
{ {
"path": "/随便", "path": "/随便",
"headers": { "headers":
{
"Host": "伪装的域名" "Host": "伪装的域名"
} }
} }
@ -91,7 +92,8 @@ ConnectionConfig:
DownlinkOnly: 4 # Time limit when the connection is closed after the uplink is closed, Second DownlinkOnly: 4 # Time limit when the connection is closed after the uplink is closed, Second
BufferSize: 64 # The internal cache size of each connection, kB BufferSize: 64 # The internal cache size of each connection, kB
Nodes: Nodes:
- PanelType: "NewV2board" ## 对接的面板类型: SSpanel, V2board, NewV2board, PMpanel, Proxypanel, V2RaySocks -
PanelType: "NewV2board" ## 对接的面板类型: SSpanel, V2board, NewV2board, PMpanel, Proxypanel, V2RaySocks
ApiConfig: ApiConfig:
ApiHost: "https://****.com" ## 面板域名地址,或自定义个专用后端对接不提供访问的域名 ApiHost: "https://****.com" ## 面板域名地址,或自定义个专用后端对接不提供访问的域名
ApiKey: "*****" ## 面板设置的通讯密钥 ApiKey: "*****" ## 面板设置的通讯密钥

View File

@ -25,9 +25,7 @@ tags: ["v2board"]
1. 复制一份创造成功的节点 1. 复制一份创造成功的节点
2. 修改复制节点: 2. 修改复制节点:
- 后台 > 节点管理 > 添加节点 - 后台 > 节点管理 > 添加节点
- 节点名称:随便填写 - 节点名称:随便填写
- 权限组:随便填写 - 权限组:随便填写
- 节点地址CloudFront分配的域名 - 节点地址CloudFront分配的域名

View File

@ -4,7 +4,8 @@ date: 2021-08-09T00:07:00+08:00
tags: [] tags: []
--- ---
## 1.同步时间 1.同步时间
------
CentOS 7 CentOS 7
@ -25,7 +26,8 @@ Debian 9 / Ubuntu 16
ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
ntpdate time.nist.gov ntpdate time.nist.gov
## 2.一键安装 2.一键安装
------
mkdir /home/mtproxy && cd /home/mtproxy mkdir /home/mtproxy && cd /home/mtproxy
curl -s -o mtproxy.sh https://raw.githubusercontent.com/ellermister/mtproxy/master/mtproxy.sh && chmod +x mtproxy.sh && bash mtproxy.sh curl -s -o mtproxy.sh https://raw.githubusercontent.com/ellermister/mtproxy/master/mtproxy.sh && chmod +x mtproxy.sh && bash mtproxy.sh

View File

@ -12,8 +12,7 @@ tags: []
## Google服务框架 ## Google服务框架
下载地址: 下载地址:`https://www.apkmirror.com/apk/google-inc/google-services-framework/`
[https://www.apkmirror.com/apk/google-inc/google-services-framework/](https://www.apkmirror.com/apk/google-inc/google-services-framework/)
首先点击上边的网站到Google服务框架程序的发布地址然后找到和自己手机的安卓版本相匹配的版本。 首先点击上边的网站到Google服务框架程序的发布地址然后找到和自己手机的安卓版本相匹配的版本。
选择noDPI的版本 选择noDPI的版本
@ -24,9 +23,7 @@ tags: []
## Google Play Service ## Google Play Service
下载地址: 下载地址:`https://www.apkmirror.com/apk/google-inc/google-play-services/`
[https://www.apkmirror.com/apk/google-inc/google-play-services/](https://www.apkmirror.com/apk/google-inc/google-play-services/)
首先点击上边的网站到Google Play Service的发布地址然后找到和自己手机的安卓版本相匹配的版本。 首先点击上边的网站到Google Play Service的发布地址然后找到和自己手机的安卓版本相匹配的版本。
那么就选择noDPI的版本 那么就选择noDPI的版本
@ -37,9 +34,7 @@ tags: []
## Google Play Store ## Google Play Store
下载地址: 下载地址:`https://www.apkmirror.com/apk/google-inc/google-play-store/`
[https://www.apkmirror.com/apk/google-inc/google-play-store/](https://www.apkmirror.com/apk/google-inc/google-play-store/)
首先点击上边的网站到Google Play Store的发布地址然后找到和自己手机的安卓版本相匹配的版本。 首先点击上边的网站到Google Play Store的发布地址然后找到和自己手机的安卓版本相匹配的版本。
选择noDPI的版本 选择noDPI的版本

View File

@ -3,7 +3,6 @@ title: "CDN配置"
date: 2023-12-25T12:07:21+08:00 date: 2023-12-25T12:07:21+08:00
tags: [] tags: []
--- ---
## 域名绑定 ## 域名绑定
1. 绑定到需要加速的服务器(源站) 1. 绑定到需要加速的服务器(源站)

View File

@ -3,14 +3,12 @@ title: "Cloudflare_自选IP"
date: 2024-06-18T16:16:35+08:00 date: 2024-06-18T16:16:35+08:00
tags: [] tags: []
--- ---
## Workers ## Workers
1. DNS解析 1. DNS解析
1. 删除现有Workers解析自定义域 1. 删除现有Workers解析自定义域
2. 将要用的域名解析到自选IP上注意**不要开启**代理(小云朵) 2. 将要用的域名解析到自选IP上注意**不要开启**代理(小云朵)
2. 自定义路由 2. 自定义路由
设置->触发器->路由 设置->触发器->路由

View File

@ -82,7 +82,7 @@ jobs:
- name: 安装 Python - name: 安装 Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: "3.x" python-version: '3.x'
- name: 安装依赖 - name: 安装依赖
run: pip install requests run: pip install requests
- name: 等待源站部署 - name: 等待源站部署

View File

@ -22,30 +22,30 @@ tags: []
```typescript ```typescript
// 网站基本信息 // 网站基本信息
export const SITE_URL = "https://your-domain.com"; export const SITE_URL = 'https://your-domain.com';
export const SITE_NAME = "你的网站名称"; export const SITE_NAME = "你的网站名称";
export const SITE_DESCRIPTION = "网站描述"; export const SITE_DESCRIPTION = "网站描述";
// 导航链接 // 导航链接
export const NAV_LINKS = [ export const NAV_LINKS = [
{ href: "/", text: "首页" }, { href: '/', text: '首页' },
{ href: "/articles", text: "文章" }, { href: '/articles', text: '文章' },
{ href: "/movies", text: "观影" }, { href: '/movies', text: '观影' },
{ href: "/books", text: "读书" }, { href: '/books', text: '读书' },
{ href: "/projects", text: "项目" }, { href: '/projects', text: '项目' },
{ href: "/other", text: "其他" }, { href: '/other', text: '其他' }
]; ];
// 备案信息(如果需要) // 备案信息(如果需要)
export const ICP = "你的ICP备案号"; export const ICP = '你的ICP备案号';
export const PSB_ICP = "你的公安备案号"; export const PSB_ICP = '你的公安备案号';
export const PSB_ICP_URL = "备案链接"; export const PSB_ICP_URL = '备案链接';
// 豆瓣配置 // 豆瓣配置
export const DOUBAN_ID = "你的豆瓣ID"; export const DOUBAN_ID = '你的豆瓣ID';
// 旅行足迹 // 旅行足迹
export const VISITED_PLACES = ["中国-北京", "中国-上海", "美国-纽约"]; export const VISITED_PLACES = ['中国-北京', '中国-上海', '美国-纽约'];
``` ```
## 文章写作 ## 文章写作
@ -100,7 +100,7 @@ tags: ["标签1", "标签2"]
export const ARTICLE_EXPIRY_CONFIG = { export const ARTICLE_EXPIRY_CONFIG = {
enabled: true, // 是否启用文章过期提醒 enabled: true, // 是否启用文章过期提醒
expiryDays: 365, // 文章过期天数 expiryDays: 365, // 文章过期天数
warningMessage: "这篇文章已经发布超过一年了,内容可能已经过时,请谨慎参考。", // 提醒消息 warningMessage: '这篇文章已经发布超过一年了,内容可能已经过时,请谨慎参考。' // 提醒消息
}; };
``` ```
@ -146,7 +146,7 @@ import { GitPlatform } from '@/components/GitProjectCollection';
`MediaGrid` 组件用于展示豆瓣的观影和读书记录。 `MediaGrid` 组件用于展示豆瓣的观影和读书记录。
基本用法 #### 基本用法
```astro ```astro
--- ---
@ -174,7 +174,7 @@ import MediaGrid from '@/components/MediaGrid.astro';
`WorldHeatmap` 组件用于展示你去过的地方,以热力图的形式在世界地图上显示。 `WorldHeatmap` 组件用于展示你去过的地方,以热力图的形式在世界地图上显示。
基本用法 #### 基本用法
`src/consts.ts` 中配置你去过的地方: `src/consts.ts` 中配置你去过的地方:
@ -182,13 +182,13 @@ import MediaGrid from '@/components/MediaGrid.astro';
// 配置你去过的地方 // 配置你去过的地方
export const VISITED_PLACES = [ export const VISITED_PLACES = [
// 国内地区格式:'中国-省份/城市' // 国内地区格式:'中国-省份/城市'
"中国-黑龙江", '中国-黑龙江',
"中国-北京", '中国-北京',
"中国-上海", '中国-上海',
// 国外地区直接使用国家名 // 国外地区直接使用国家名
"马来西亚", '马来西亚',
"泰国", '泰国',
"美国", '美国'
]; ];
``` ```
@ -214,6 +214,7 @@ import { VISITED_PLACES } from '@/consts';
</Layout> </Layout>
``` ```
## 主题切换 ## 主题切换
系统支持三种主题模式: 系统支持三种主题模式:
@ -244,6 +245,8 @@ import { VISITED_PLACES } from '@/consts';
```bash ```bash
npm install npm install
# 或者使用 pnpm
pnpm install
``` ```
3. 修改配置 3. 修改配置
@ -263,7 +266,6 @@ import { VISITED_PLACES } from '@/consts';
### 部署方式选择 ### 部署方式选择
1. **Vercel 部署(推荐)** 1. **Vercel 部署(推荐)**
- 支持所有功能 - 支持所有功能
- 自动部署和 HTTPS - 自动部署和 HTTPS
- 支持 API 路由和动态数据 - 支持 API 路由和动态数据
@ -315,17 +317,14 @@ npm run build
## 常见问题 ## 常见问题
1. **图片无法显示** 1. **图片无法显示**
- 检查图片路径是否正确 - 检查图片路径是否正确
- 确保图片已放入 `public` 目录 - 确保图片已放入 `public` 目录
2. **豆瓣数据无法获取** 2. **豆瓣数据无法获取**
- 确认豆瓣 ID 配置正确 - 确认豆瓣 ID 配置正确
- 检查豆瓣记录是否公开 - 检查豆瓣记录是否公开
3. **Git 项目无法显示** 3. **Git 项目无法显示**
- 验证用户名配置 - 验证用户名配置
- 确认 API 访问限制 - 确认 API 访问限制

View File

@ -4,6 +4,7 @@ date: 2025-01-15T00:34:11Z
tags: [] tags: []
--- ---
## 前端 ## 前端
## tailwind ## tailwind

View File

@ -230,4 +230,3 @@ for ((;;)); do
sleep $interval sleep $interval
clear clear
done done
```

View File

@ -6,10 +6,10 @@ tags: []
## 1. 环境准备 ## 1. 环境准备
- [宝塔面板](https://www.bt.cn/new/index.html) * [宝塔面板](https://www.bt.cn/new/index.html)
- PHP 7.4 * PHP 7.4
- MySQL 5.7 / 8 或 MariaDB 10 * MySQL 5.7 / 8 或 MariaDB 10
- Apache HTTP Web Server 或 Nginx * Apache HTTP Web Server 或 Nginx
## 2. 下载 ## 2. 下载

View File

@ -115,7 +115,9 @@ systemctl enable serverTZs.service
``` ```
> #赋权 > #赋权
> #拷贝进系统服务目录 #重新加载系统服务 #启动服务端并设置开机自启 > #拷贝进系统服务目录
> #重新加载系统服务
> #启动服务端并设置开机自启
### 在配置文件中增加服务器主机后重启 ### 在配置文件中增加服务器主机后重启
@ -196,7 +198,8 @@ systemctl enable serverTZc.service
``` ```
> #赋权 > #赋权
> #拷贝进系统服务目录 #重新加载系统服务 > #拷贝进系统服务目录
> #重新加载系统服务
> #启动服务端并设置开机自启 > #启动服务端并设置开机自启
在配置文件中增加服务器主机后重启 在配置文件中增加服务器主机后重启

View File

@ -13,7 +13,6 @@ tags: []
### 出现黄色更新弹窗 ### 出现黄色更新弹窗
1. 打开文件目录: 1. 打开文件目录:
- Mac 修改文件目录:`/Applications/Adobe Photoshop (Beta)/Adobe Photoshop (Beta).app/Contents/Required/UXP/com.adobe.photoshop.inAppMessaging/js/0.js` - Mac 修改文件目录:`/Applications/Adobe Photoshop (Beta)/Adobe Photoshop (Beta).app/Contents/Required/UXP/com.adobe.photoshop.inAppMessaging/js/0.js`
- Windows 目录(如果是 C 盘):`C:\Program Files\Adobe\Adobe Photoshop 2023\Required\UXP\com.adobe.photoshop.inAppMessaging\js\0.js` - Windows 目录(如果是 C 盘):`C:\Program Files\Adobe\Adobe Photoshop 2023\Required\UXP\com.adobe.photoshop.inAppMessaging\js\0.js`

View File

@ -54,8 +54,8 @@ tags: []
按 Win+R 打开运行,输入 `winver` 检查 Windows 版本要求: 按 Win+R 打开运行,输入 `winver` 检查 Windows 版本要求:
- 对于 x64 系统:版本 1903 或更高,内部版本 18362.1049 或更高。 * 对于 x64 系统:版本 1903 或更高,内部版本 18362.1049 或更高。
- 对于 ARM64 系统:版本 2004 或更高,内部版本 19041 或更高。 * 对于 ARM64 系统:版本 2004 或更高,内部版本 19041 或更高。
### 三、启用虚拟机功能 ### 三、启用虚拟机功能
@ -98,7 +98,7 @@ wsl --set-default-version 2
## 常见问题处理 ## 常见问题处理
- **关闭 WSL 自动挂载 Windows 分区** * **关闭 WSL 自动挂载 Windows 分区**
编辑 WSL 配置文件 `/etc/wsl.conf` 并添加内容以禁用自动挂载和 Windows 路径的添加。 编辑 WSL 配置文件 `/etc/wsl.conf` 并添加内容以禁用自动挂载和 Windows 路径的添加。
```bash ```bash
@ -111,18 +111,19 @@ wsl --set-default-version 2
enabled = false enabled = false
``` ```
- **解决无法定位 package screen 的问题** * **解决无法定位 package screen 的问题**
在 Linux 分发版中运行 `apt-get update` 来更新软件包列表。 在 Linux 分发版中运行 `apt-get update` 来更新软件包列表。
- **WSL 卸载** * **WSL 卸载**
查看已安装的 WSL 环境并卸载指定的 Linux 分发版。 查看已安装的 WSL 环境并卸载指定的 Linux 分发版。
```bash ```bash
wsl --unregister 指定的Linux分发版 wsl --unregister 指定的Linux分发版
``` ```
- **解决 WSLRegisterDistribution 错误** * **解决 WSLRegisterDistribution 错误**
在 PowerShell 中运行 在 PowerShell 中运行
```bash ```bash
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux
``` ```

View File

@ -1,69 +0,0 @@
---
title: "vscode配置"
date: 2025-04-19T11:10:57+08:00
tags: []
---
## 自动补全,语法检查
| 语言 | 插件 |
| -------- | ---------------------------------------------------------------------------------------------------------- |
| rust | [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) |
| tailwind | [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) |
| markdown | [markdownlint](https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint) |
| toml | [Even Better TOML](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml) |
## 格式化
### 格式化插件
| 插件 | 格式化言语 |
| ------------------------------------------------------------------------------------------------ | ------------------- |
| [prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) | js,md,css,html,yaml |
| [Black Formatter](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter) | python |
| [shell-format](https://marketplace.visualstudio.com/items?itemName=foxundermoon.shell-format) | shell |
### 保存自动格式化
`settings.json`增加以下代码
```txt
"editor.formatOnSave": true // 保存时自动规范代码
```
## 其他
| 名称 | 作用 |
| ----------------------------------------------------------------------------------------------------------------------------- | ---------------------- |
| [Atom Material Icons](https://marketplace.visualstudio.com/items?itemName=AtomMaterial.a-file-icon-vscode) | 美化图标 |
| [Atom One Dark Theme](https://marketplace.visualstudio.com/items?itemName=akamud.vscode-theme-onedark) | 美化主题 |
| [Live Preview](https://marketplace.visualstudio.com/items?itemName=ms-vscode.live-server) | 提供在线预览web项目 |
| [Markdown Preview Github Styling](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-preview-github-styles) | 提供markdown代码块样式 |
| [Chinese (Simplified)](https://marketplace.visualstudio.com/items?itemName=MS-CEINTL.vscode-language-pack-zh-hans) | 简体中文语言包 |
## 非html文件中启用tailwind高亮提示
### 安装高亮插件
- [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)
- [PostCSS Language Support](https://marketplace.visualstudio.com/items?itemName=csstools.postcss)
### 启动高亮配置
>以rust为例
修改`setting.json`文件
```json
// 配置识别的正则表达式
"tailwindCSS.experimental.classRegex": [
"class:\\s?\"(.*?)\"",
"class:\\s?format!\\((\"(.*?)\")\\)"
],
// 配置识别的语言
"tailwindCSS.includeLanguages": {
"rust": "html"
},
```

View File

@ -28,7 +28,7 @@ tags: []
&nbsp;&nbsp;国外的公交车不适合i人,我打算从布城从地铁到市中心需要先坐公交车到布城去差10秒就赶上了但是站台没人所以公交车司机没有停第二次在休息区等了半个小时司机又没停可能是司机没看到我吧第三次我站在公交车站台等车的位置等待可是他还是没停这次可能是没有给司机信号第四次等公交车快到的时候我死死的看着司机与他建立心灵链接但他还是不停浏览器查询原来要招手用打车软件看了一下两公里还是选择打车了 &nbsp;&nbsp;国外的公交车不适合i人,我打算从布城从地铁到市中心需要先坐公交车到布城去差10秒就赶上了但是站台没人所以公交车司机没有停第二次在休息区等了半个小时司机又没停可能是司机没看到我吧第三次我站在公交车站台等车的位置等待可是他还是没停这次可能是没有给司机信号第四次等公交车快到的时候我死死的看着司机与他建立心灵链接但他还是不停浏览器查询原来要招手用打车软件看了一下两公里还是选择打车了
&nbsp;&nbsp;在去酒店的路上看到了很多流浪汉,不过感觉他们的穿搭和我没有区别,一个包+拖鞋,历经大雨来到谷歌地图显示的位置,却找不到酒店,找了一个印度男人问路,他也找不到,他给酒店客服打电话后,告诉我不在这个区域,给我指路,往哪走再往哪走到一个塔下快到了问问别人,我一点没记住好在用高德地图重新导航,竟然没问题。 &nbsp;&nbsp;在去酒店的路上看到了很多流浪汉,不过感觉他们的穿搭和我没有区别,一个包+拖鞋,酒店的位置和平台给的地址不同还好遇到一个好心的印度男人,打电话和酒店交流,告诉我酒店在哪里
&nbsp;&nbsp;晚餐找了家本地人多的店,点了一个大虾饭,没想到是正宗印度菜,`米饭味道=70%八角+20%洗衣服+10%辣椒` &nbsp;&nbsp;晚餐找了家本地人多的店,点了一个大虾饭,没想到是正宗印度菜,`米饭味道=70%八角+20%洗衣服+10%辣椒`

View File

@ -12,7 +12,7 @@ tags: []
创建 创建
````sql ````
create funcition 函数名(@参数 类型) create funcition 函数名(@参数 类型)
returns 类型 returns 类型
as as
@ -28,7 +28,7 @@ end
#### 方案一(处理复杂逻辑,函数体除了sql查询之外还有其他逻辑代码) #### 方案一(处理复杂逻辑,函数体除了sql查询之外还有其他逻辑代码)
````sql ````
create function 函数名(@参数 类型) create function 函数名(@参数 类型)
returns @表名 table returns @表名 table
( (
@ -44,7 +44,7 @@ end
#### 方案二(只能return+sql查询结果) #### 方案二(只能return+sql查询结果)
````sql ````
create function 函数名(@参数 类型) create function 函数名(@参数 类型)
return table return table
as as

View File

@ -35,4 +35,5 @@ select @变量名=值
### go ### go
1.等待 go 语句之前的代码执行完之后才能执行后面的代码 2.批处理结束的一个标志 1.等待go语句之前的代码执行完之后才能执行后面的代码
2.批处理结束的一个标志

View File

@ -3,7 +3,6 @@ title: 触发器
date: 2024-06-06T23:51:43Z date: 2024-06-06T23:51:43Z
tags: [] tags: []
--- ---
## instead of(事前触发器) ## instead of(事前触发器)
## After(事后触发器) ## After(事后触发器)

View File

@ -4,11 +4,12 @@ date: 2024-06-06T23:51:36Z
tags: [] tags: []
--- ---
$n $n
> n代表数字,$1-$9代表第一到第九个参数,十以上需要用大括号把位置值包含类似 ${10} > n代表数字,$1-$9代表第一到第九个参数,十以上需要用大括号把位置值包含类似 ${10}
$\* $*
> 代表命令行中的所有参数(所有参数当作一个整体) > 代表命令行中的所有参数(所有参数当作一个整体)

View File

@ -9,7 +9,7 @@ tags: []
## 系统变量 ## 系统变量
|变量|作用| |变量|作用|
| --------------- | ------------------------------------ | | ------| --------------------------------------|
|`$USER`|当前用户的用户名| |`$USER`|当前用户的用户名|
|`$HOME`|当前用户的家目录| |`$HOME`|当前用户的家目录|
|`$PATH`|包含可执行文件的目录列表,用冒号分隔| |`$PATH`|包含可执行文件的目录列表,用冒号分隔|

View File

@ -4,10 +4,11 @@ date: 2024-06-06T23:51:45Z
tags: [] tags: []
--- ---
### 基本和高级条件表达式操作符 ### 基本和高级条件表达式操作符
|符号|作用| |符号|作用|
| -------- | ----------------------------------------------------------------------- | | ------| -------------------------------------------------------------------------|
|`{}`|代码块不创建新的子shell用于组织一系列的命令| |`{}`|代码块不创建新的子shell用于组织一系列的命令|
|`()`|创建一个子shell并在其中执行命令| |`()`|创建一个子shell并在其中执行命令|
|`[[]]`|执行高级条件表达式,支持模式匹配、正则表达式等| |`[[]]`|执行高级条件表达式,支持模式匹配、正则表达式等|
@ -20,7 +21,7 @@ tags: []
### 特殊 ### 特殊
|符号|作用| |符号|作用|
| ------ | -------------------- | | ------| ----------------------|
|`\|`|在正则中表示或| |`\|`|在正则中表示或|
|`!`|在引用中表示间距引用| |`!`|在引用中表示间距引用|
|`#`|在数组中表示长度| |`#`|在数组中表示长度|
@ -28,7 +29,7 @@ tags: []
### 条件操作符 ### 条件操作符
|操作符|描述| |操作符|描述|
| ------- | ----------------------------------------------------------- | | --------| ------------------------------------------------------------|
|`=`|字符串比较(相等)| |`=`|字符串比较(相等)|
|`!=`|字符串比较(不等)| |`!=`|字符串比较(不等)|
|`-lt`|数值比较(小于)| |`-lt`|数值比较(小于)|
@ -45,7 +46,7 @@ tags: []
### 逻辑操作符详解 ### 逻辑操作符详解
|操作符|描述| |操作符|描述|
| -------- | ------------- | | --------| ---------------|
|`&&`|逻辑与AND| |`&&`|逻辑与AND|
|`\|\|`|逻辑或OR| |`\|\|`|逻辑或OR|
|`!`|逻辑非NOT| |`!`|逻辑非NOT|
@ -53,7 +54,7 @@ tags: []
### 算术操作符详解 ### 算术操作符详解
|操作符|描述| |操作符|描述|
| ------ | ------ | | --------| --------|
|`+`|加法| |`+`|加法|
|`-`|减法| |`-`|减法|
|`*`|乘法| |`*`|乘法|
@ -64,7 +65,7 @@ tags: []
### 文件测试操作符 ### 文件测试操作符
|操作符|描述|示例| |操作符|描述|示例|
| ------ | -------------------- | -------------------------- | | --------| ----------------------| ------|
|`-f`|文件存在且为普通文件|`if [[ -f $file ]]`| |`-f`|文件存在且为普通文件|`if [[ -f $file ]]`|
|`-d`|目录存在|`if [[ -d $directory ]]`| |`-d`|目录存在|`if [[ -d $directory ]]`|
|`-e`|文件存在|`if [[ -e $filepath ]]`| |`-e`|文件存在|`if [[ -e $filepath ]]`|

View File

@ -41,7 +41,7 @@ case 关键字表示开始一个 case 语句块。
expression是需要匹配的表达式或变量。 expression是需要匹配的表达式或变量。
pattern1, pattern2, pattern3等是可能的匹配模式。 pattern1, pattern2, pattern3等是可能的匹配模式。
command1, command2, command3是与每个模式匹配时要执行的命令。 command1, command2, command3是与每个模式匹配时要执行的命令。
\*)是通配符,用于匹配所有不符合前面模式的情况。 *)是通配符,用于匹配所有不符合前面模式的情况。
default_command是当没有匹配模式时执行的命令。 default_command是当没有匹配模式时执行的命令。
;;用于标识每个模式下命令的结束。 ;;用于标识每个模式下命令的结束。

View File

@ -4,6 +4,7 @@ date: 2024-06-06T23:51:42Z
tags: [] tags: []
--- ---
## 将Shell变量输出为环境变量 ## 将Shell变量输出为环境变量
`export 变量名=变量值` `export 变量名=变量值`

View File

@ -4,6 +4,7 @@ date: 2024-06-06T23:51:42Z
tags: [] tags: []
--- ---
## 方法一:使用 function 关键字 ## 方法一:使用 function 关键字
```shell ```shell

View File

@ -4,6 +4,7 @@ date: 2024-06-06T23:51:38Z
tags: [] tags: []
--- ---
## 脚本要求 ## 脚本要求
脚本以`#!/bin/bash`开头 脚本以`#!/bin/bash`开头

View File

@ -4,6 +4,8 @@ date: 2024-06-06T23:51:10Z
tags: [] tags: []
--- ---
## 颜色名 ## 颜色名
## 光的三原色 ## 光的三原色

View File

@ -22,11 +22,9 @@ tags: []
1. 源程序(`.c`) 1. 源程序(`.c`)
编译 编译
2. 目标文件(`.obj`) 2. 目标文件(`.obj`)
链接 链接
3. 执行文件(`.exe`) 3. 执行文件(`.exe`)
执行 执行
@ -35,14 +33,14 @@ tags: []
#### 整数 #### 整数
- `0` 开头的是 8 进制 * `0`开头的是8进制
- `0x` 开头的是 16 进制 * `0x`开头的是16进制
#### 小数 #### 小数
- `0.7`=`.7` * `0.7`=`.7`
- `7.0`=`7.` * `7.0`=`7.`
- 科学计数`6E6` * 科学计数`6E6`
E的前后必须有数后面必须为整数 E的前后必须有数后面必须为整数
@ -58,15 +56,15 @@ a=97
##### 转义字符 ##### 转义字符
- 一般转义字符:`\n` `\t` * 一般转义字符:`\n` `\t`
- 八进制转义字符:`\0` 开头,`\0343` * 八进制转义字符:`\0`开头,`\0343`
- 十六进制转义字符:`\0x` 开头,`\0xaf` * 十六进制转义字符:`\0x`开头,`\0xaf`
### 注释 ### 注释
开头:/\* 开头:/*
结尾:\*/ 结尾:*/
### 三段论 ### 三段论
@ -76,24 +74,24 @@ a=97
#### 关键字 #### 关键字
- int * int
- float * float
- acse * acse
不能作为用户标识符 不能作为用户标识符
#### 预定义标识符 #### 预定义标识符
- printf * printf
- scanf * scanf
- define * define
可以作为用户标识符 可以作为用户标识符
#### 用户标识符 #### 用户标识符
- int a * int a
- int \_a * int _a
#### 规则 #### 规则

View File

@ -4,25 +4,27 @@ date: 2024-06-06T23:51:12Z
tags: [] tags: []
--- ---
## stdio.h ## stdio.h
- 读写文件 * 读写文件
`FILE *fopen(const char *filename, const char *mode)` `FILE *fopen(const char *filename, const char *mode)`
> - filename -- 字符串,表示要打开的文件名称 > * filename -- 字符串,表示要打开的文件名称
> - mode -- 字符串,表示文件的访问模式 > * mode -- 字符串,表示文件的访问模式
>
- 格式化输出字符串 * 格式化输出字符串
`snprintf ( char \* str, size_t size, const char \* format, ... )` `snprintf ( char \* str, size_t size, const char \* format, ... )`
> - **str** -- 目标字符串,用于存储格式化后的字符串的字符数组的指针。 > * **str** -- 目标字符串,用于存储格式化后的字符串的字符数组的指针。
> - **size** -- 字符数组的大小。 > * **size** -- 字符数组的大小。
> - **format** -- 格式化字符串。 > * **format** -- 格式化字符串。
> - **...** -- 可变参数,可变数量的参数根据 format 中的格式化指令进行格式化。 > * **...** -- 可变参数,可变数量的参数根据 format 中的格式化指令进行格式化。
>
- 清除缓存 * 清除缓存
```c ```c
fflush(stdin) fflush(stdin)
@ -30,42 +32,41 @@ tags: []
## stdlib.h ## stdlib.h
- 清屏命令 * 清屏命令
```c ```c
system("cls"); // windows清屏命令 system("cls"); // windows清屏命令
system("clear"); //Linux和macOS清屏命令 system("clear"); //Linux和macOS清屏命令
``` ```
- 手动管理内存 * 手动管理内存
- 分配所需的内存空间,并返回一个指向它的指针, 不会设置内存为零 * 分配所需的内存空间,并返回一个指向它的指针, 不会设置内存为零
`void *malloc(size_t size)` `void *malloc(size_t size)`
* 分配所需的内存空间,并返回一个指向它的指针, 会设置分配的内存为零
- 分配所需的内存空间,并返回一个指向它的指针, 会设置分配的内存为零
`void *calloc(size_t nitems, size_t size)` `void *calloc(size_t nitems, size_t size)`
- 尝试重新调整之前调用所分配的指针所指向的内存块的大小 * 尝试重新调整之前调用所分配的指针所指向的内存块的大小
`void *realloc(void *ptr, size_t size)` `void *realloc(void *ptr, size_t size)`
- 释放之前调用 函数 所分配的内存空间 * 释放之前调用 函数 所分配的内存空间
`void free(void *ptr)` `void free(void *ptr)`
- > - **ptr** -- 指针指向一个分配内存的内存块的指针。 * > * **ptr** -- 指针指向一个分配内存的内存块的指针。
> - **size** -- 内存块的大小,以字节为单位。 > * **size** -- 内存块的大小,以字节为单位。
> - **nitems** -- 要被分配的元素个数。 > * **nitems** -- 要被分配的元素个数。
>
- 排序 * 排序
```c ```c
void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*)) void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*))
``` ```
> - **base** -- 指向要排序的数组的第一个元素的指针。 > * **base** -- 指向要排序的数组的第一个元素的指针。
> - **nitems** -- 由 base 指向的数组中元素的个数。 > * **nitems** -- 由 base 指向的数组中元素的个数。
> - **size** -- 数组中每个元素的大小,以字节为单位。 > * **size** -- 数组中每个元素的大小,以字节为单位。
> - **compar** -- 用来比较两个元素的函数。 > * **compar** -- 用来比较两个元素的函数。
> - 该函数不返回任何值。 > * 该函数不返回任何值。
>
- 随机数 * 随机数
1. 设置随机数种子 1. 设置随机数种子
@ -74,7 +75,7 @@ tags: []
``` ```
> **seed** -- 这是一个整型值,用于伪随机数生成算法播种。 > **seed** -- 这是一个整型值,用于伪随机数生成算法播种。
>
2. 获取随机数 2. 获取随机数
```c ```c
@ -82,6 +83,7 @@ tags: []
``` ```
> 该函数返回一个范围在 0 到 RAND_MAX 之间的整数值。 > 该函数返回一个范围在 0 到 RAND_MAX 之间的整数值。
>
## string.h ## string.h
@ -91,20 +93,21 @@ tags: []
char *strncat(char *dest, const char *src, size_t n) char *strncat(char *dest, const char *src, size_t n)
``` ```
> - **dest** -- 指向目标数组,该数组包含了一个 C 字符串,且足够容纳追加后的字符串,包括额外的空字符。 > * **dest** -- 指向目标数组,该数组包含了一个 C 字符串,且足够容纳追加后的字符串,包括额外的空字符。
> - **src** -- 要追加的字符串。 > * **src** -- 要追加的字符串。
> - **n** -- 要追加的最大字符数。 > * **n** -- 要追加的最大字符数。
> - 该函数返回一个指向最终的目标字符串 dest 的指针。 > * 该函数返回一个指向最终的目标字符串 dest 的指针。
>
2. 字符串检索 2. 字符串检索
```c ```c
char *strstr(const char *haystack, const char *needle) char *strstr(const char *haystack, const char *needle)
``` ```
> - **haystack** -- 要被检索的 C 字符串。 > * **haystack** -- 要被检索的 C 字符串。
> - **needle** -- 在 haystack 字符串内要搜索的小字符串。 > * **needle** -- 在 haystack 字符串内要搜索的小字符串。
> - 该函数返回在 haystack 中第一次出现 needle 字符串的位置,如果未找到则返回 null。 > * 该函数返回在 haystack 中第一次出现 needle 字符串的位置,如果未找到则返回 null。
>
## time.h ## time.h
@ -114,8 +117,8 @@ sleep(1); // 暂停 1 秒
## conio.h ## conio.h
\_kbhit()//如果有按键按下,则\_kbhit()函数返回真 _kbhit()//如果有按键按下则_kbhit()函数返回真
\_getch();//使用\_getch()函数获取按下的键值 _getch();//使用_getch()函数获取按下的键值
## errno.h ## errno.h

View File

@ -8,17 +8,14 @@ tags: []
- **特点:** 堆区是动态分配的内存空间,用于存储程序运行时动态分配的数据。在堆区分配的内存需要手动释放,否则可能导致内存泄漏。 - **特点:** 堆区是动态分配的内存空间,用于存储程序运行时动态分配的数据。在堆区分配的内存需要手动释放,否则可能导致内存泄漏。
- **分配和释放:** 通过 `malloc`、`calloc`、`realloc` 等函数分配,通过 `free` 函数释放。 - **分配和释放:** 通过 `malloc`、`calloc`、`realloc` 等函数分配,通过 `free` 函数释放。
2. **栈区Stack** 2. **栈区Stack**
- **特点:** 栈区用于存储函数调用时的局部变量、函数参数和函数调用的返回地址等。它是一种后进先出LIFO的数据结构。 - **特点:** 栈区用于存储函数调用时的局部变量、函数参数和函数调用的返回地址等。它是一种后进先出LIFO的数据结构。
- **分配和释放:** 由编译器自动分配和释放,不需要手动管理。 - **分配和释放:** 由编译器自动分配和释放,不需要手动管理。
3. **静态区Static** 3. **静态区Static**
- **特点:** 静态区分为全局静态区和局部静态区。全局静态区用于存储全局变量和静态变量,而局部静态区用于存储在函数中定义的静态变量。 - **特点:** 静态区分为全局静态区和局部静态区。全局静态区用于存储全局变量和静态变量,而局部静态区用于存储在函数中定义的静态变量。
- **分配和释放:** 由编译器分配,程序运行期间一直存在。 - **分配和释放:** 由编译器分配,程序运行期间一直存在。
4. **代码区Code** 4. **代码区Code**
- **特点:** 代码区存储程序的执行代码。这是只读区域,存储了程序的二进制代码。 - **特点:** 代码区存储程序的执行代码。这是只读区域,存储了程序的二进制代码。
@ -39,7 +36,6 @@ tags: []
``` ```
这样定义的全局静态变量 `globalStaticVar` 具有文件作用域,即在整个源文件中可见,但是对其他源文件是不可见的。其他源文件中可以定义同名的全局变量,而不会产生冲突。 这样定义的全局静态变量 `globalStaticVar` 具有文件作用域,即在整个源文件中可见,但是对其他源文件是不可见的。其他源文件中可以定义同名的全局变量,而不会产生冲突。
2. **不带** **`static`** **关键字的全局静态变量:** 2. **不带** **`static`** **关键字的全局静态变量:**
```c ```c
@ -51,7 +47,6 @@ tags: []
``` ```
如果去掉 `static` 关键字,全局静态变量 `globalStaticVar` 将具有全局作用域,即在整个程序中可见,包括其他源文件。这可能导致命名冲突,因为其他源文件也可以定义同名的全局变量。 如果去掉 `static` 关键字,全局静态变量 `globalStaticVar` 将具有全局作用域,即在整个程序中可见,包括其他源文件。这可能导致命名冲突,因为其他源文件也可以定义同名的全局变量。
3. **带** **`static`** **关键字的局部静态变量:** 3. **带** **`static`** **关键字的局部静态变量:**
```c ```c
@ -65,7 +60,6 @@ tags: []
``` ```
这样定义的局部静态变量 `localStaticVar` 具有函数范围的作用域,即仅在定义它的函数 `exampleFunction` 中可见。该变量的生命周期贯穿整个程序的运行时间,而不是仅在 `exampleFunction` 被调用时存在。 这样定义的局部静态变量 `localStaticVar` 具有函数范围的作用域,即仅在定义它的函数 `exampleFunction` 中可见。该变量的生命周期贯穿整个程序的运行时间,而不是仅在 `exampleFunction` 被调用时存在。
4. **不带** **`static`** **关键字的局部静态变量:** 4. **不带** **`static`** **关键字的局部静态变量:**
```c ```c

View File

@ -4,5 +4,7 @@ date: 2024-06-06T23:51:47Z
tags: [] tags: []
--- ---
大端存储:把高位字节放在低字节 大端存储:把高位字节放在低字节
小端存储:把低位字节放在高字节 小端存储:把低位字节放在高字节

View File

@ -4,6 +4,7 @@ date: 2024-06-06T23:51:39Z
tags: [] tags: []
--- ---
1. while 1. while
```javascript ```javascript

View File

@ -4,6 +4,7 @@ date: 2024-06-06T23:51:37Z
tags: [] tags: []
--- ---
`&` 取地址调试符 `&` 取地址调试符
`%p` 以地址的格式打印数据使用 (需要将指针强制转换为 (void \*) `%p` 以地址的格式打印数据使用 (需要将指针强制转换为 (void \*)

View File

@ -4,6 +4,7 @@ date: 2024-06-06T23:51:47Z
tags: [] tags: []
--- ---
main写mian main写mian
函数后面加; 函数后面加;
switch 里面的case不加break switch 里面的case不加break

View File

@ -4,8 +4,10 @@ date: 2024-06-06T23:51:44Z
tags: [] tags: []
--- ---
|类型|符号|占位符|字节| |类型|符号|占位符|字节|
| :----------: | :-------: | :----: | :--: | | :------------: | :---------: | :--------------------------------: | :----: |
|字符|char|%c|1| |字符|char|%c|1|
|字符串型||%s|| |字符串型||%s||
|整形|int|%d|4| |整形|int|%d|4|
@ -18,7 +20,7 @@ tags: []
## 储存类 ## 储存类
|存储类|描述| |存储类|描述|
| :------: | :--------------------------------------------------: | | :--------: | :----------------------------------------------------: |
|register|用于定义存储在寄存器中而不是 RAM 中的局部变量| |register|用于定义存储在寄存器中而不是 RAM 中的局部变量|
|static|存储类指示编译器在程序的生命周期内保持局部变量的存在| |static|存储类指示编译器在程序的生命周期内保持局部变量的存在|
|extern|定义在其他文件中声明的全局变量或函数| |extern|定义在其他文件中声明的全局变量或函数|

View File

@ -4,6 +4,7 @@ date: 2024-06-06T23:51:38Z
tags: [] tags: []
--- ---
`scanf` 是针对标准输入的格式化语句 `scanf` 是针对标准输入的格式化语句
`printf`​是针对标准输出的格式化语句 `printf`​是针对标准输出的格式化语句
@ -38,13 +39,13 @@ tags: []
int fseek(FILE *stream, long int offset, int whence) int fseek(FILE *stream, long int offset, int whence)
``` ```
- 参数 * 参数
> stream -- 这是指向 FILE 对象的指针,该 FILE 对象标识了流。 > stream -- 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
> offset -- 这是相对 whence 的偏移量,以字节为单位。 > offset -- 这是相对 whence 的偏移量,以字节为单位。
> whence -- 这是表示开始添加偏移 offset 的位置。它一般指定为下列常量之一: > whence -- 这是表示开始添加偏移 offset 的位置。它一般指定为下列常量之一:
>
- 常量 描述 * 常量 描述
```c ```c
SEEK_SET 文件的开头 SEEK_SET 文件的开头

View File

@ -35,7 +35,7 @@ tags: []
> **member-list** 是标准的变量定义,比如 int i; 或者 float f;,或者其他有效的变量定义。 > **member-list** 是标准的变量定义,比如 int i; 或者 float f;,或者其他有效的变量定义。
> >
> **variable-list** 结构变量,定义在结构的末尾,最后一个分号之前,您可以指定一个或多个结构变量。 > **variable-list** 结构变量,定义在结构的末尾,最后一个分号之前,您可以指定一个或多个结构变量。
>
4. 共用体 4. 共用体
```c ```c

View File

@ -15,7 +15,6 @@ tags: []
2. 转换为汇编代码 2. 转换为汇编代码
`-S` `-S`
3. 将汇编代码转化为机器指令 3. 将汇编代码转化为机器指令
`-c` `-c`

View File

@ -18,7 +18,7 @@ tags: []
2. 三元运算 2. 三元运算
```javascript ```javascript
条件 ? true : false; 条件?true:false
``` ```
3. switch 3. switch
@ -26,14 +26,14 @@ tags: []
```javascript ```javascript
switch(data){ switch(data){
case value1: case value1:
code1; code1
break; break
case value2: case value2:
code2; code2
break; break
default: default:
coden; coden
break; break
} }
``` ```

View File

@ -4,6 +4,7 @@ date: 2024-06-06T23:51:44Z
tags: [] tags: []
--- ---
> 又称上下文管理器,在处理文件时,无论是否产生异常,都能保证with语句执行完毕后关闭已近打开的文件,这个过程是自动的,无需手动操作. > 又称上下文管理器,在处理文件时,无论是否产生异常,都能保证with语句执行完毕后关闭已近打开的文件,这个过程是自动的,无需手动操作.
语法结构 语法结构

View File

@ -24,7 +24,8 @@ tags: []
## 遍历 ## 遍历
```html ```html
for i in enumerate(数组): print(i) for i in enumerate(数组):
print(i)
``` ```
## 操作 ## 操作

View File

@ -27,7 +27,9 @@ tags: []
```html ```html
""" 需要注释的代码 """ """
需要注释的代码
"""
``` ```
## 导入模块 ## 导入模块

View File

@ -4,14 +4,13 @@ date: 2024-06-06T23:51:30Z
tags: [] tags: []
--- ---
## 创建方式 ## 创建方式
### 第一种使用{}直接创建字典 ### 第一种使用{}直接创建字典
`d={key1:value1,key2:value2....}` `d={key1:value1,key2:value2....}`
语法结构如下: 语法结构如下:
`dict(key1=value1,key2=value2....)` `dict(key1=value1,key2=value2....)`
### 第二种使用内置函数dict()创建字典 ### 第二种使用内置函数dict()创建字典
@ -44,21 +43,12 @@ d[key]或 d.get(key)
## 相关的操作方法 ## 相关的操作方法
获取所有的key数据 获取所有的key数据
`d.keys()` `d.keys()`
获取所有的value的数据 获取所有的value的数据
`d.values()` `d.values()`
key存在获取相应的value,同时删除key-value对,否则获取默认值 key存在获取相应的value,同时删除key-value对,否则获取默认值
`d.pop(key,default)` `d.pop(key,default)`
随机从字典种取出一个key-value对结果为元组类型,同时将该key-value从字典种删除 随机从字典种取出一个key-value对结果为元组类型,同时将该key-value从字典种删除
`d.popitem()` `d.popitem()`
清空字典中所有的key-value对 清空字典中所有的key-value对
`d.clear()` `d.clear()`

View File

@ -7,31 +7,18 @@ tags: []
## 常用方法 ## 常用方法
`str.lower()`:将str字符串全部转换成小写字母,结果为一个新的字符串 `str.lower()`:将str字符串全部转换成小写字母,结果为一个新的字符串
`str.upper()`:将字符串全部转为大写字母,结果为一个新的字符串 `str.upper()`:将字符串全部转为大写字母,结果为一个新的字符串
`str.split(sep=None)`:把str按照指定的分隔符sep进行分隔,结果为列表类型 `str.split(sep=None)`:把str按照指定的分隔符sep进行分隔,结果为列表类型
`str.count(sub)`:结果为sub这个字符串在str中出现的次数 `str.count(sub)`:结果为sub这个字符串在str中出现的次数
`find(sub)`:结果为sub这个字符串在str中是否存在,如果不存在结果为-1,如果存在,如果出现结果为sub首次出现的索引 `find(sub)`:结果为sub这个字符串在str中是否存在,如果不存在结果为-1,如果存在,如果出现结果为sub首次出现的索引
`str.index(sub)`:功能与find()相同,区别在于要查询的子串sub不存在时,程序报错 `str.index(sub)`:功能与find()相同,区别在于要查询的子串sub不存在时,程序报错
`str.startswith(s)`:查询字符串str是否以子字符串s开头 `str.startswith(s)`:查询字符串str是否以子字符串s开头
`str.endswith(s)`:查询字符串str是否以子字符串s结尾 `str.endswith(s)`:查询字符串str是否以子字符串s结尾
`str.replace(old,news,count)`:使用news替换字符串s中所有old字符串,结果是一个新的字符串 `str.replace(old,news,count)`:使用news替换字符串s中所有old字符串,结果是一个新的字符串
`str.center(width,filllchar)`:字符串str在指定的宽度范围内居中,可以使用fillchar进行填充 `str.center(width,filllchar)`:字符串str在指定的宽度范围内居中,可以使用fillchar进行填充
`str.join(iter)`:在iter中的每个元素后面都增加一个新的字符串str `str.join(iter)`:在iter中的每个元素后面都增加一个新的字符串str
`str.strip(chars)`从字符串中去掉左侧和右侧chars中列出的字符串 `str.strip(chars)`从字符串中去掉左侧和右侧chars中列出的字符串
`str.lstrip(chars)`:从字符串中去掉左侧chars、中列出的字符串 `str.lstrip(chars)`:从字符串中去掉左侧chars、中列出的字符串
`str.rstrip(chars)`:从字符串中去掉右侧chars、中列出的字符串 `str.rstrip(chars)`:从字符串中去掉右侧chars、中列出的字符串
## 格式化字符串的方法 ## 格式化字符串的方法
@ -97,10 +84,14 @@ python3.6 引入的格式化字符串的方式,比{}表明被替换的字符
1.使用`str.join()`方法进行拼接字符串 1.使用`str.join()`方法进行拼接字符串
例如 例如
`print("|".join([x,y,"ww"]))` 2.直接拼接 3.使用格式化字符串进行拼接 `print("|".join([x,y,"ww"]))`
2.直接拼接
3.使用格式化字符串进行拼接
### 字符串的去重 ### 字符串的去重
1.字符串的拼接以及not in 1.字符串的拼接以及not in
`for i in x: if i not in x_new: x_new += i print(x_new)` 2.索引+not in 3.通过列表集合+列表排序 `for i in x: if i not in x_new: x_new += i print(x_new)`
2.索引+not in
3.通过列表集合+列表排序
`x_new = set(x) lst = list(x_new) lst.sort(key=x.index) print(''.join(lst))` `x_new = set(x) lst = list(x_new) lst.sort(key=x.index) print(''.join(lst))`

View File

@ -6,34 +6,23 @@ tags: []
## 导入 ## 导入
```python `import 模块名称 [as 别名]`
import 模块名称 [as 别名] `from 模块名称 import 变量/函数/类/*`
```
```python
from 模块名称 import 变量/函数/类/*
```
## os模块 ## os模块
> 与操作系统和文件相关操作有关的模块 > 与操作系统和文件相关操作有关的模块
`getcwd()`:获取当前工作路径 `getcwd()`:获取当前工作路径
`listdir(path)`:获取path路径下的文件,如果没有指定path,则获取当前路径下的文件和目录信息 `listdir(path)`:获取path路径下的文件,如果没有指定path,则获取当前路径下的文件和目录信息
`mkdir(path)`:在指定路径下创建目录(文件夹) `mkdir(path)`:在指定路径下创建目录(文件夹)
`makedirs(path)`:创建多级目录 `makedirs(path)`:创建多级目录
### os.path模块 ### os.path模块
`abspath(path)`:获取目录或文件的绝对路径 `abspath(path)`:获取目录或文件的绝对路径
`exists(path)`:判断目录或文件在磁盘上是否存在,结果为bool类型,如果目录或文件在磁盘上存在,结果为True,否则为False `exists(path)`:判断目录或文件在磁盘上是否存在,结果为bool类型,如果目录或文件在磁盘上存在,结果为True,否则为False
`join(path.name)`:将目录与文件名进行拼接,相当于字符串`+`​的操作 `join(path.name)`:将目录与文件名进行拼接,相当于字符串`+`​的操作
`splitext()`:分别获取文件名和后缀 `splitext()`:分别获取文件名和后缀
## re模块 ## re模块
@ -45,17 +34,11 @@ from 模块名称 import 变量/函数/类/*
> 用于产生随机数的模块 > 用于产生随机数的模块
`seed(x)`:初始化给定的随机数种子,默认为当前系统时间 `seed(x)`:初始化给定的随机数种子,默认为当前系统时间
`random()`:产生一个[0.0,1.0]之间的随机小数 `random()`:产生一个[0.0,1.0]之间的随机小数
`randint(a,b)`生成一个[a,b]之间的整数 `randint(a,b)`生成一个[a,b]之间的整数
`randrange(m,n,k)`​生成一个[m,n)之间步长为k的随机整数 `randrange(m,n,k)`​生成一个[m,n)之间步长为k的随机整数
`uniform(a,b)`:生成一个[a,b]之间的随机小数 `uniform(a,b)`:生成一个[a,b]之间的随机小数
`choice(seq)`:从序列中随机选择一个数 `choice(seq)`:从序列中随机选择一个数
`shuffle(seq)`:将序列seq中元素随机排列,然后打乱后的序列 `shuffle(seq)`:将序列seq中元素随机排列,然后打乱后的序列
## json模块 ## json模块
@ -63,11 +46,8 @@ from 模块名称 import 变量/函数/类/*
> 用于对高维数据进行编码和解码的模块 > 用于对高维数据进行编码和解码的模块
`json.dumps(obj)`:将python数据类型转为json格式过程,编码过程 `json.dumps(obj)`:将python数据类型转为json格式过程,编码过程
`json.loads(s)`:将JSON格式字符串转为python数据类型,解码过程 `json.loads(s)`:将JSON格式字符串转为python数据类型,解码过程
`json.dump(obj,file)`:与dumps功能相同,将转换结果储存到文件file中 `json.dump(obj,file)`:与dumps功能相同,将转换结果储存到文件file中
`json.load(file)`:与loads功能相同,从文件a中读入数据 `json.load(file)`:与loads功能相同,从文件a中读入数据
## time模块 ## time模块
@ -75,15 +55,10 @@ from 模块名称 import 变量/函数/类/*
> 与时间相关的模块 > 与时间相关的模块
`time()`:获取当前时间戳 `time()`:获取当前时间戳
`localtime(sec)`:获取指定时间戳对应的本地时间的structural_time对象 `localtime(sec)`:获取指定时间戳对应的本地时间的structural_time对象
`ctime()`:获取当前时间戳对应的易读字符串 `ctime()`:获取当前时间戳对应的易读字符串
`strftime()`:格式化时间结果为字符串 `strftime()`:格式化时间结果为字符串
`strptime()`:提取字符串的时间,结果为字符串 `strptime()`:提取字符串的时间,结果为字符串
`sleep(sec)`:休眠sec秒 `sleep(sec)`:休眠sec秒
## datetime模块 ## datetime模块
@ -91,11 +66,7 @@ from 模块名称 import 变量/函数/类/*
> 与日期相关的模块,可以方便的显示日期并对日期进行运算 > 与日期相关的模块,可以方便的显示日期并对日期进行运算
`datetime`:表示日期时间类 `datetime`:表示日期时间类
`timedelta`:表示时间间隔的类 `timedelta`:表示时间间隔的类
`date`:表示日期的类 `date`:表示日期的类
`time`:表示时间的类 `time`:表示时间的类
`tzinfo`:时区相关的类 `tzinfo`:时区相关的类

View File

@ -22,7 +22,7 @@ raise [exception 类型(异常描述信息)]
``` ```
|异常名称|描述| |异常名称|描述|
| ------------------------- | -------------------------------------------------- | | ---------------------------| ----------------------------------------------------|
|BaseException|所有异常的基类| |BaseException|所有异常的基类|
|SystemExit|解释器请求退出| |SystemExit|解释器请求退出|
|KeyboardInterrupt|用户中断执行(通常是输入^C)| |KeyboardInterrupt|用户中断执行(通常是输入^C)|

View File

@ -4,10 +4,11 @@ date: 2024-06-06T23:51:27Z
tags: [] tags: []
--- ---
## 整数类型 ## 整数类型
|进制类型|引导符号| |进制类型|引导符号|
| -------- | -------- | | ----------| ----------|
|十进制|无| |十进制|无|
|二进制|0b或0B| |二进制|0b或0B|
|八进制|0o或0O| |八进制|0o或0O|
@ -46,7 +47,7 @@ False 表示整数 0
- False 或者是 None - False 或者是 None
- 数字中的0,包含0,0.0,虚数0 - 数字中的0,包含0,0.0,虚数0
- 空序列,包含空字符串,空元组,空列表,空字典,空合集 - 空序列,包含空字符串,空元组,空列表,空字典,空合集
- 自定义对象的实例,该对象**bool**()方法返回 False 或**len**()方法返回 False - 自定义对象的实例,该对象__bool__()方法返回False或__len__()方法返回False
## 数据类型的转换 ## 数据类型的转换

View File

@ -4,6 +4,7 @@ date: 2024-06-06T23:51:22Z
tags: [] tags: []
--- ---
`import numpy` `import numpy`
数组转numpy 数组转numpy
`对象名 = numpy.array(数组)` `对象名 = numpy.array(数组)`
@ -11,7 +12,7 @@ tags: []
1. 随机生成列表 1. 随机生成列表
|作用|语句| |作用|语句|
| :---------------------------------------------------------------------: | ------------------------------------------ | | :--------------------------------------------------------------------: | ------|
||`numpy.arange(起始,结束,步长)`| ||`numpy.arange(起始,结束,步长)`|
|全0|`numpy.zeros(shape)`| |全0|`numpy.zeros(shape)`|
|全1|`numpy.ones(shape)`| |全1|`numpy.ones(shape)`|
@ -26,7 +27,6 @@ tags: []
2. 设置数组 2. 设置数组
通过设置不同个数的元组,生成不同的 通过设置不同个数的元组,生成不同的
`numpy.reshape(shape)` `numpy.reshape(shape)`
1. 读取数据 1. 读取数据
`numpy.loadtxt(frame,dtype=numpy.float,delimiter=None,skiprows=0,usecols=None,unpack=False)` `numpy.loadtxt(frame,dtype=numpy.float,delimiter=None,skiprows=0,usecols=None,unpack=False)`
@ -40,7 +40,7 @@ tags: []
3. 取不同的行和列 3. 取不同的行和列
|作用|语句| |作用|语句|
| -------------- | ----------------------------------------------- | | ----------------| ------|
|取两个元素|`元素名[[number1,number2],[number1,number2]]`| |取两个元素|`元素名[[number1,number2],[number1,number2]]`|
|取单独的列|`元素名[:,number]`| |取单独的列|`元素名[:,number]`|
|取单独的行|`元素名[number,:]`| |取单独的行|`元素名[number,:]`|
@ -67,7 +67,7 @@ tags: []
8. 常用统计函数 8. 常用统计函数
|作用|语句|描述| |作用|语句|描述|
| -------- | ---------------------------------- | ------------------ | | ----------| ------| --------------------|
|求和|`元素名.sum(axis=None)`|| |求和|`元素名.sum(axis=None)`||
|均值|`元素名.mean(axis=None)`|受离群点影响比较大| |均值|`元素名.mean(axis=None)`|受离群点影响比较大|
|中值|`numpy.median(元素名.axis=None)`|| |中值|`numpy.median(元素名.axis=None)`||

View File

@ -15,15 +15,10 @@ tags: []
## 操作文件 ## 操作文件
`file.read(size)`:从文件中读写size个字符或字节,如果没有给定参数,则读取文件中的全部内容 `file.read(size)`:从文件中读写size个字符或字节,如果没有给定参数,则读取文件中的全部内容
`file.readline(size)`:读写文件中的一行数据,如果给定参数,则读取这一行中的size个字符或字节 `file.readline(size)`:读写文件中的一行数据,如果给定参数,则读取这一行中的size个字符或字节
`file.readlines()`从文件中读取所有内容,结果为列表类型 `file.readlines()`从文件中读取所有内容,结果为列表类型
`file.write(s)`:将字符串s写入文件 `file.write(s)`:将字符串s写入文件
`file.writelines(lst)`:将内容全部为字符串的列表写入文件 `file.writelines(lst)`:将内容全部为字符串的列表写入文件
`file.seek(offst)`:改变当前文件操作指针的位置,英文占一个字节,中文GBK占两个字节,utf-8编码占三个字节 `file.seek(offst)`:改变当前文件操作指针的位置,英文占一个字节,中文GBK占两个字节,utf-8编码占三个字节
## 关闭文件 ## 关闭文件

View File

@ -4,6 +4,7 @@ date: 2024-06-06T23:51:30Z
tags: [] tags: []
--- ---
## 请求 ## 请求
1 请求行 -> 请求方式 请求url地址 协议 1 请求行 -> 请求方式 请求url地址 协议

View File

@ -17,7 +17,6 @@ x = requests.get('https://www.runoob.com/')
print(x.text) print(x.text)
``` ```
```txt
apparent_encoding:编码方式 apparent_encoding:编码方式
close():关闭与服务器的连接 close():关闭与服务器的连接
content:返回响应的内容,以字节为单位 content:返回响应的内容,以字节为单位
@ -40,11 +39,9 @@ request:返回请求此响应的请求对象
status_code:返回 http 的状态码,比如 404 和 200200 是 OK404 是 Not Found status_code:返回 http 的状态码,比如 404 和 200200 是 OK404 是 Not Found
text:返回响应的内容unicode 类型数据 text:返回响应的内容unicode 类型数据
url:返回响应的 URL url:返回响应的 URL
````
## requests 方法 ## requests 方法
```txt
delete(url, args):发送 DELETE 请求到指定 url delete(url, args):发送 DELETE 请求到指定 url
get(url, params, args):发送 GET 请求到指定 url get(url, params, args):发送 GET 请求到指定 url
head(url, args):发送 HEAD 请求到指定 url head(url, args):发送 HEAD 请求到指定 url
@ -52,7 +49,6 @@ patch(url, data, args):发送 PATCH 请求到指定 url
post(url, data, json, args):发送 POST 请求到指定 url post(url, data, json, args):发送 POST 请求到指定 url
put(url, data, args):发送 PUT 请求到指定 url put(url, data, args):发送 PUT 请求到指定 url
request(method, url, args):向指定的 url 发送指定的请求方法 request(method, url, args):向指定的 url 发送指定的请求方法
```
- url 请求 url - url 请求 url
- data 参数为要发送到指定 url 的字典、元组列表、字节或文件对象 - data 参数为要发送到指定 url 的字典、元组列表、字节或文件对象

View File

@ -4,6 +4,7 @@ date: 2024-06-06T23:51:36Z
tags: [] tags: []
--- ---
## 创建工程 ## 创建工程
scrapy startproject (projectname) scrapy startproject (projectname)
@ -16,7 +17,7 @@ scrapy startproject (projectname)
## 编写对应的代码在爬虫文件中 ## 编写对应的代码在爬虫文件中
1. parse 中的 response 解析 1.将parse中的response解析
想要使用数据,必须使用extract()提取数据 想要使用数据,必须使用extract()提取数据
extract():返回列表 extract():返回列表
extract_first():返回一个数据 extract_first():返回一个数据

View File

@ -6,12 +6,10 @@ tags: []
## 爬虫源文件 ## 爬虫源文件
```txt
name:当前源文件的唯一标识 name:当前源文件的唯一标识
allowed_domains:允许请求的域名 allowed_domains:允许请求的域名
start_urls:起始的url列表,作用:列表中存储的url会被get发送 start_urls:起始的url列表,作用:列表中存储的url会被get发送
parse方法:解析服务器返回的响应对象的解析方法 parse方法:解析服务器返回的响应对象的解析方法
```
## settings ## settings

View File

@ -4,6 +4,7 @@ date: 2024-06-06T23:51:47Z
tags: [] tags: []
--- ---
```python ```python
from selenium import webdriver # 驱动 from selenium import webdriver # 驱动
from selenium.webdriver.common.by import By # 解析方式 from selenium.webdriver.common.by import By # 解析方式

View File

@ -4,6 +4,7 @@ date: 2024-06-06T23:51:26Z
tags: [] tags: []
--- ---
## 安装 ## 安装
`pip install 模块名称` `pip install 模块名称`
@ -55,7 +56,7 @@ Workbook():创建新的工作簿对象
1. 随机生成列表 1. 随机生成列表
|作用|语句| |作用|语句|
| :---------------------------------------------------------------------: | ------------------------------------------ | | :--------------------------------------------------------------------: | ------|
||`numpy.arange(起始,结束,步长)`| ||`numpy.arange(起始,结束,步长)`|
|全0|`numpy.zeros(shape)`| |全0|`numpy.zeros(shape)`|
|全1|`numpy.ones(shape)`| |全1|`numpy.ones(shape)`|
@ -83,7 +84,7 @@ Workbook():创建新的工作簿对象
4. 取不同的行和列 4. 取不同的行和列
|作用|语句| |作用|语句|
| -------------- | ----------------------------------------------- | | ----------------| ------|
|取两个元素|`元素名[[number1,number2],[number1,number2]]`| |取两个元素|`元素名[[number1,number2],[number1,number2]]`|
|取单独的列|`元素名[:,number]`| |取单独的列|`元素名[:,number]`|
|取单独的行|`元素名[number,:]`| |取单独的行|`元素名[number,:]`|
@ -109,7 +110,7 @@ Workbook():创建新的工作簿对象
9. 常用统计函数 9. 常用统计函数
|作用|语句|描述| |作用|语句|描述|
| -------- | ---------------------------------- | ------------------ | | ----------| ------| --------------------|
|求和|`元素名.sum(axis=None)`|| |求和|`元素名.sum(axis=None)`||
|均值|`元素名.mean(axis=None)`|受离群点影响比较大| |均值|`元素名.mean(axis=None)`|受离群点影响比较大|
|中值|`numpy.median(元素名.axis=None)`|| |中值|`numpy.median(元素名.axis=None)`||
@ -169,6 +170,7 @@ Workbook():创建新的工作簿对象
> >
> transparent:透明 > transparent:透明
> bbox_inches:是否紧凑布局 > bbox_inches:是否紧凑布局
>
## 图形表述 ## 图形表述
@ -182,3 +184,4 @@ Workbook():创建新的工作簿对象
> alpha:区间 > alpha:区间
> linestyle:样式 > linestyle:样式
>

View File

@ -7,4 +7,4 @@ tags: []
1.使用socket类创建一个套接字对象 1.使用socket类创建一个套接字对象
2.使用connect((host,port))设置连接的主机IP和主机设置的端口 2.使用connect((host,port))设置连接的主机IP和主机设置的端口
3.使用recv()/send()方法接收和发送数据 3.使用recv()/send()方法接收和发送数据
4. 使用 close()关闭套接字 3.使用close()关闭套接字

View File

@ -5,21 +5,12 @@ tags: []
--- ---
`bind(ip,port)`:绑定ip地址和端口 `bind(ip,port)`:绑定ip地址和端口
`listen(N)`:开始TCP监听,N表示操作系统挂起的最大连接数量,取值范围1-6之间,一般设置为5 `listen(N)`:开始TCP监听,N表示操作系统挂起的最大连接数量,取值范围1-6之间,一般设置为5
`accept()`:被动接收TCP客户端连接,阻塞式 `accept()`:被动接收TCP客户端连接,阻塞式
`connect((ip,port))`:主动初始化TCP服务器连接 `connect((ip,port))`:主动初始化TCP服务器连接
`recv(size)`:接收TCP数据,返回值为字符串类型,size表示要接收的最大数据量 `recv(size)`:接收TCP数据,返回值为字符串类型,size表示要接收的最大数据量
`send(str)`:发送TCP数据,返回值是要发送的字节数量 `send(str)`:发送TCP数据,返回值是要发送的字节数量
`sebdall(str)`:完整发送TCP数据,将str中的数据发送到连接的套接字,返回之前尝试发送所有数据,如果成功为None,失败抛出异常 `sebdall(str)`:完整发送TCP数据,将str中的数据发送到连接的套接字,返回之前尝试发送所有数据,如果成功为None,失败抛出异常
`recvfrom()`:接收UDP数据,返回值为一个元组(data,address),data表示接收的数据,address表示发送数据的套接字地址 `recvfrom()`:接收UDP数据,返回值为一个元组(data,address),data表示接收的数据,address表示发送数据的套接字地址
`sendto(data,(ip,port))`:发送UDP数据,返回值是发送的字节数 `sendto(data,(ip,port))`:发送UDP数据,返回值是发送的字节数
`close()`:关闭套接字 `close()`:关闭套接字

View File

@ -5,11 +5,7 @@ tags: []
--- ---
`/`表示层级关系,第一个是根目录 `/`表示层级关系,第一个是根目录
`text()`拿文本 `text()`拿文本
`//`后代 `//`后代
`*`:通配符 `*`:通配符
`@属性`:属性值 `@属性`:属性值

View File

@ -4,13 +4,14 @@ date: 2024-06-06T23:51:47Z
tags: [] tags: []
--- ---
| 描述 | 属性 | | 描述 | 属性 |
| ----------------------------------- | -------- | | ---------------------------------- | -------- |
| 当前进程的实例别名,默认为Process-N | `name` | | 当前进程的实例别名,默认为Process-N | `name` |
| 当前进程对象的PID值 | `pid` | | 当前进程对象的PID值 | `pid` |
| 描述 | 方法 | | 描述 | 方法 |
| ------------------------------------------------------------- | ----------------- | | --------------------------------------------------------- | ----------------- |
| 进程是否执行完,没执行完结果为True,否则为False | `is_alive()` | | 进程是否执行完,没执行完结果为True,否则为False | `is_alive()` |
| 等待结束或等待timeout秒 | `join(timeout)` | | 等待结束或等待timeout秒 | `join(timeout)` |
| 启动进程 | `start()` | | 启动进程 | `start()` |

View File

@ -10,11 +10,7 @@ tags: []
参数说明 参数说明
> group:表示分组,实际上不使用,值默认为None即可 > group:表示分组,实际上不使用,值默认为None即可
>
> target:表示子进程要执行的任务,支持函数名 > target:表示子进程要执行的任务,支持函数名
>
> name:表示子进程的名称 > name:表示子进程的名称
>
> args:表示调用函数的位置参数,以元组的形式进行传递 > args:表示调用函数的位置参数,以元组的形式进行传递
>
> kwargs:表示调用函数的关键字参数,以字典的形式进行传参 > kwargs:表示调用函数的关键字参数,以字典的形式进行传参

View File

@ -4,6 +4,7 @@ date: 2024-06-06T23:51:42Z
tags: [] tags: []
--- ---
> 队列是一种先进先出的数据结构 > 队列是一种先进先出的数据结构
## 语法 ## 语法
@ -13,7 +14,7 @@ tags: []
## 常用方法 ## 常用方法
| 功能 | 方法 | | 功能 | 方法 |
| --------------------------------------------------------- | ------------------------ | | ------------------------------------------------------- | ------------------------ |
| 获取当前队列包含的消息数量 | `qsize()` | | 获取当前队列包含的消息数量 | `qsize()` |
| 判断队列是否为空,为空结果为True,否则为False | `empty()` | | 判断队列是否为空,为空结果为True,否则为False | `empty()` |
| 判断队列是否满了,满结果为True,否则为False | `full()` | | 判断队列是否满了,满结果为True,否则为False | `full()` |

View File

@ -5,7 +5,7 @@ tags: []
--- ---
|名称|单词|作用| |名称|单词|作用|
| ---- | ---- | -------- | | ------| ------| ----------|
|and|与|从左到右| |and|与|从左到右|
|or|或|从左到右| |or|或|从左到右|
|not|非|从右到左| |not|非|从右到左|

View File

@ -43,15 +43,10 @@ A^B
## 相关操作 ## 相关操作
如果x不在集合s中,则将x添加到集合s 如果x不在集合s中,则将x添加到集合s
`s.add(x)` `s.add(x)`
如果x在集合中,将其删除,如何步骤集合中,程序报错 如果x在集合中,将其删除,如何步骤集合中,程序报错
`s.remove(x)` `s.remove(x)`
清楚集合所有元素 清楚集合所有元素
`s.clear()` `s.clear()`

View File

@ -1,378 +0,0 @@
---
title: rust
date: 2024-10-09T18:49:45Z
tags: []
---
### 变量
`let`
1. 支持类型推导,也可以显示指定变量类型
2. 变量名用蛇命名法`a_b`​,而枚举和结构体使用帕斯卡命名法`Ab`
3. 如果变量没有使用可以前置下划线,消除警告
4. 强制类型转换
let a = 3.1;let b = a as i32
5. 打印变量
```rust
printIn!("val{}",x);
printIn!("val{x}");
```
6. 变量默认不可变
7. 使用`mut`​关键字声明可变
```rust
let mut x=10;
```
8. 变量可以解构
### 常量
`const`
1. 必须指定类型和值
2. 常量值被直接嵌入生成的底层机器代码中
3. 常量名与静态变量名必须全部大写,单词之间加入下划线
4. 常量的作用域块级作用域,他们只再声明它们的作用域内可见
### 静态变量
`static`
1. static 变量是在运行时分配内存
2. 并不是不可修改,可以使用`unsafe`​修改
3. 静态变量的生命周期为整个程序的运行时间
### 解构
### 函数
1. 需要使用蛇形命名法
2. 当用`!`​做返回值,代表永不返回
### 所有权
1. 每一个值都被一个变量所拥有,该变量被称为值的所有权
2. 一个值只能被一个变量拥有
3. 当所有者(变量)离开作用域范围时会被丢弃
4. 有一个基本特征`copy`​,任何基本类型都具有该特征,在给其他值赋值后依旧可用
### 引用与解引用
* 引用
* 默认不可变引用
* 用`&`​引用,得到需要引用的地址
* 可变引用
1. 先声明可引用
2. 传参用`mut`​修饰,变成可引用类型
同一作用域,特定数据只能有一个可变引用
* 可变引用和不可变引用不能同时存在
* 引用作用域在最后一次引用的地方
* 解引用
* 用`*`​解开引用,得到需要引用的数据
### 复合类型
`unimplemented!()`​在复合类型中为了快速构建框架告诉编译器该函数未实现
### 切片
* 创建方法`[起始位置..结束位置]`
### 字符
* 字符Unicode编码
* 字符串UTF-8编码
* 追加字符`push`
* 追加字符串`push_str`
* 修改`insert`
* 替换
* 对原来进行修改`replace`
* 返回新字符串,可对自变量使用`replacen`
* 删除
* 删除最后一个字符`pop`
* 删除指定位置的字符`remove`
* 删除指定位置到结尾的字符`truncate`
* 清空`clear`
* 链接
* `+` `+=`
* `format!("{}{}",x,y)`
* 注意区分字符串和字变量
* 字符转义
`\`
* 保持字符串原样
```rust
r"<str>"
r#""<str>""# //如果包含双引号
r###""<str>""###//如果还有歧义可以一直加#
```
### 元组
* 长度固定
* 可以多种元素混合
* 可以使用解构或`.`
### 结构体
`struct`
* 可以通过关键词创建结构体
* 可以通过函数返回创建结构体
* 在使用`..<name>`​可以当前结构体没有显示声明的字段,全部从`<name>`​结构体中获得
* 元组结构体,结构体字段没有名称的结构体
* 单元结构体,只定义
* 打印结构体信息
```rust
#[derive(Debug)]
println!("{:?}", <struct variable>);
```
### 枚举
`enum` `enumeration`
* 也可以用结构体使用
* 枚举用`::`​访问
* 枚举能直接关联数据信息
```rust
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
```
* option枚举值用于处理空值
可以让编译器推断出整个变量是什么类型
Some(T):表示成员含有值
None:成员没有值
### 数组
元素必须相同类型
* 不可变数组
* 声明可以直接赋值
* 声明时可以定义类型和长度`let a:[i32;5]`
* 可变数组
`let arr:&[u16] = &[114,112];`
### 循环
loop是一个表达式
### 模式
特殊语法,它用于匹配类型中的结构和数据
* match
* if let
* while let
* let
* 忽略模式中
* 值`_`
* 使用下划线本身不会绑定值
* 省略模式`..`
* 匹配守卫
位于math分支后的额外if条件他能为分支提供一个额外的条件,因为他不是模式所以可以寻找外部的值,来使用
### 方法
1. 对象的属性和方法定义是分离的
2. 属性和方法名称相同即可
3. 生成对象的方法一般用`new`
4. `impl`​生成方法
5. `struct`​生成属性
6. self
* `self`​:表示该所有权
* `&self``self:&Self`​的简写,对该方法的不可变借用
* `&mut self`​:表示可借用
7. 允许方法名和结构体字段名相同
8. 可以多个`impl`​定义
9. 可以为枚举生成方法
### 闭包
本质上是一个匿名函数
```rust
|param1, param2,...| {
语句1;
语句2;
返回表达式
}
|param1| 返回表达式
```
1. 如果类型推断出一个类型,就不可改变了
2. 闭包可以捕获作用域终端值
3. 闭包特征:`T: Fn(u32) -> u32`
4. 三种fn特征
* `fnOnce`
所有的包自动实现这个特征
如果没有copy特征会拿走被捕获变量的所有权
如果想强制获取捕获变量的所有权,可以在参数列表前添加`move`​关键词
* `fnMut`
如果没有移出捕获所有权自动实现这个特征
可以以借用的方式捕获环境中的值,因此修改该值
如果想在闭包里面修改可变借用变量,需要把闭包声明为可变类型
* `fn`
不需要对捕获变量进行改变的闭包自动实现该特征
以不可变借用的方法捕获环境中的变量
### 迭代器
特征`IntoIterator`
* 惰性初始化
如果不适用不会产生任何的性能损耗
* IntoIterator特征
显示的转为迭代器
* `into_iter`​:夺走所有权
* `iter`​:借用
* `iter_mut`​:可变借用
* next方法
会改变迭代器当前的数据
对迭代器的遍历是消耗性的
for循环就是通过不断调用next方法
* 适配器
* 消费型适配器
该迭代器在内部调用了next方法会不断的消耗迭代器上的原素
* 迭代器适配器
会返回一个新的迭代器
迭代器适配器是惰性的所以需要消费者适配器来收尾
可以将闭包作为参数不仅实现了迭代器环境的处理,还可以捕获环境值
* collect
消费者迭代器
使用它可以将一个迭代器的元素收进指定元素中
### 类型转换
* as
* RryInto
需要导入`use std::convert::TryInto;`
* 点操作符
点操作符会发生很多类型转换,最外层的类型不匹配时,会自动引用,自动解引用,强类型转换直到找到需要的特征
* 强类型转换
### type别名
`type`
### Sized
每个可定大小都实现了该特征
### 动态大小类型DST
储存在堆上不能直接使用,只能通过引用或`Box`​来间接使用
* 切片
* str
是String和&str的底层数据
### 智能指针
使用智能指针可以将动态大小类型变成固定大小
* `Box<T>`
可以将数据存在堆上
Box::leak可以消费掉box并内存泄漏
* Derf特征
可以实现智能指针的自动解引用
三种Deref转换
* `T: Deref<Target=U>`​可以将&T转换为&U
* `T: DerefMut<Target=U>`​可以将&mut T转换为&mut U
* `T: Deref<Target=U>`​可以将&mut T转换为&U
* Drop特征
可以实现资源的回收
执行一些收尾工作
和Copy特征互斥
可以手动收回`Drop()`
* Rc与Arc
通过引用器计数
clone:可以实现浅拷贝,会拿走拷贝指针的所有权
Arc原子化的智能指针能够保证数据安全会增加数据损耗
* Cell和RefCell
提供了内部数据可修改性
cell只使用于copy类型没有性能损耗
RefCell 只是将借用规则从编译期推迟到程序运行期
* Weak
类似于Rc但是Weak不持有所有权仅仅保存一份指向数据引用
不计入所有权,不会阻止释放
存在返回Some不存在返回None
### 多线程

View File

@ -4,25 +4,25 @@ date: 2024-07-02T13:04:06Z
tags: [] tags: []
--- ---
### 常见命令 ### 常见命令
- 创建运行容器 * 创建运行容器
```docker ```docker
docker run [options] IMAGE [command] docker run [options] IMAGE [command]
``` ```
- command * command
- `-d`​:后台运行 * `-d`​:后台运行
- `--name [name]`​:容器名称 * `--name [name]`​:容器名称
- `-p [host port]:[container port]`​:映射端口 * `-p [host port]:[container port]`​:映射端口
- `-v [/host/data]:[/container/data]`​:绑定挂载一个数据卷,如果数据卷不存在会自动创建 * `-v [/host/data]:[/container/data]`​:绑定挂载一个数据卷,如果数据卷不存在会自动创建
- `-e [KEY]=[VALUE]`​:环境变量 * `-e [KEY]=[VALUE]`​:环境变量
- `--network [my-network]`​:指定连接到一个网络 * `--network [my-network]`​:指定连接到一个网络
- `[image]:[tag]`​:未指定版本时,默认是 latest,代表最新版本 * `[image]:[tag]`​:未指定版本时,默认是latest,代表最新版本
* 拉取镜像
- 拉取镜像
未指定版本时,默认是latest,代表最新版本 未指定版本时,默认是latest,代表最新版本
@ -30,7 +30,7 @@ tags: []
docker pull [image]:[tag] docker pull [image]:[tag]
``` ```
- 查看指定镜像 * 查看指定镜像
不写镜像名查看所有镜像 不写镜像名查看所有镜像
@ -38,117 +38,112 @@ tags: []
docker images [image] docker images [image]
``` ```
- 删除指定镜像 * 删除指定镜像
```docker ```docker
docker rmi [image] docker rmi [image]
``` ```
- 保存镜像 * 保存镜像
```docker ```docker
docker [options] sava [image:tag] docker [options] sava [image:tag]
``` ```
- options * options
- `-o [name]`​:文件保存文件名 * `-o [name]`​:文件保存文件名
* 加载镜像
- 加载镜像
```docker ```docker
docker load [OPTIONS] docker load [OPTIONS]
``` ```
- options * options
- `-i [name]`​:镜像文件名 * `-i [name]`​:镜像文件名
- `-q`​:不输入提示内容 * `-q`​:不输入提示内容
* 列出运行的容器
- 列出运行的容器
```docker ```docker
docker ps [options] docker ps [options]
``` ```
- options * options
- `-a`​:列出包括未运行的容器 * `-a`​:列出包括未运行的容器
- `-q`​:仅显示容器 ID * `-q`仅显示容器ID
* 停止容器
- 停止容器
```docker ```docker
docker stop [container] docker stop [container]
``` ```
- 启动容器 * 启动容器
```docker ```docker
docker start [container] docker start [container]
``` ```
- 删除容器 * 删除容器
```docker ```docker
docker rm [container] docker rm [container]
``` ```
- `-f`​:强制删除 * `-f`​:强制删除
* 查看容器详情
- 查看容器详情
```docker ```docker
docker inspect [container] docker inspect [container]
``` ```
- 查看容器日志 * 查看容器日志
```docker ```docker
docker logs [options] [container] docker logs [options] [container]
``` ```
- `-f`​:跟随日志输出(实时显示日志) * `-f`​:跟随日志输出(实时显示日志)
* 在运行的容器中执行命令
- 在运行的容器中执行命令
```docker ```docker
docker exec [OPTIONS] [container] [COMMAND] [bash] docker exec [OPTIONS] [container] [COMMAND] [bash]
``` ```
- `-i`​:保持标准输入打开,即使没有连接。 * `-i`​:保持标准输入打开,即使没有连接。
- `-t`​:分配一个伪终端 * `-t`​:分配一个伪终端
- `-u`​:以指定用户的身份运行命令 * `-u`​:以指定用户的身份运行命令
- `-d`​:在后台运行 * `-d`​:在后台运行
- `-e`​:设置环境变量 * `-e`​:设置环境变量
### 数据卷 ### 数据卷
- 创建数据卷 * 创建数据卷
```docker ```docker
docker volume create docker volume create
``` ```
- 查看所有数据卷 * 查看所有数据卷
```docker ```docker
docker volume ls docker volume ls
``` ```
- 删除指定数据卷 * 删除指定数据卷
```docker ```docker
docker volume rm [volume] docker volume rm [volume]
``` ```
- 查看某个数据卷的详细 * 查看某个数据卷的详细
```docker ```docker
docker volume inspect docker volume inspect
``` ```
- 清除数据卷 * 清除数据卷
```docker ```docker
docker volume prune docker volume prune
@ -158,25 +153,25 @@ tags: []
包含构建镜像需要执行的指令 包含构建镜像需要执行的指令
- 指定基础镜像 * 指定基础镜像
```docker ```docker
FROM [image:tag] FROM [image:tag]
``` ```
- 环境变量 * 环境变量
```docker ```docker
ENV [key] [value] ENV [key] [value]
``` ```
- 将本地文件拷贝到容器的指定目录 * 将本地文件拷贝到容器的指定目录
```docker ```docker
COPY [/host/file] [/container/file] COPY [/host/file] [/container/file]
``` ```
- 执行容器中的 shell 命令 * 执行容器中的shell命令
一般执行安装过程 一般执行安装过程
@ -184,13 +179,13 @@ tags: []
RUN [command] RUN [command]
``` ```
- 容器暴露的端口给连接的其他服务,但不会映射到宿主机 * 容器暴露的端口给连接的其他服务,但不会映射到宿主机
```docker ```docker
EXPOSE [port] EXPOSE [port]
``` ```
- 镜像中应用的启动命令 * 镜像中应用的启动命令
```docker ```docker
ENTRYPOINT [command] ENTRYPOINT [command]
@ -198,43 +193,43 @@ tags: []
### 网络 ### 网络
- 创建 * 创建
```docker ```docker
docker network create [NETWORK] docker network create [NETWORK]
``` ```
- 查看 * 查看
```docker ```docker
docker network ls docker network ls
``` ```
- 删除指定网络 * 删除指定网络
```docker ```docker
docker network rm [NETWORK] docker network rm [NETWORK]
``` ```
- 清楚未使用网络 * 清楚未使用网络
```docker ```docker
docker network prune docker network prune
``` ```
- 使指定容器加入指定网络 * 使指定容器加入指定网络
```docker ```docker
docker network connect [NETWORK] [CONTAINER] docker network connect [NETWORK] [CONTAINER]
``` ```
- 使指定容器离开指定网络 * 使指定容器离开指定网络
```docker ```docker
docker network disconnect [NETWORK] [CONTAINER] docker network disconnect [NETWORK] [CONTAINER]
``` ```
- 查看网络详细信息 * 查看网络详细信息
```docker ```docker
docker network inspect [NETWORK] docker network inspect [NETWORK]
@ -244,13 +239,13 @@ tags: []
#### 常见关键字 #### 常见关键字
- 指定 Docker Compose 文件的版本号 * 指定 Docker Compose 文件的版本号
```docker-compose ```docker-compose
version: "3.8" version: "3.8"
``` ```
- 定义各个服务,每个服务可以有多个配置项。 * 定义各个服务,每个服务可以有多个配置项。
```docker-compose ```docker-compose
services: services:
@ -260,61 +255,61 @@ tags: []
... ...
``` ```
- 定义 Docker 网络,用于连接各个服务 * 定义 Docker 网络,用于连接各个服务
```docker-compose ```docker-compose
networks: networks:
- [NETWORK]: - [NETWORK]:
``` ```
- 定义 Docker 卷,用于持久化数据或者与宿主机共享数据。 * 定义 Docker 卷,用于持久化数据或者与宿主机共享数据。
```docker-compose ```docker-compose
volumes: volumes:
- [/host/data]:[/container/data] - [/host/data]:[/container/data]
``` ```
- 指定使用的镜像名称。 * 指定使用的镜像名称。
```docker-compose ```docker-compose
[image]:[tag] [image]:[tag]
``` ```
- 指定构建 Docker 镜像时的 Dockerfile 路径。 * 指定构建 Docker 镜像时的 Dockerfile 路径。
```docker-compose ```docker-compose
build: [path] build: [path]
``` ```
- 将容器内部端口映射到宿主机,使外部可以访问容器服务。 * 将容器内部端口映射到宿主机,使外部可以访问容器服务。
```docker-compose ```docker-compose
ports: ports:
- "[host port]:[container port]" - "[host port]:[container port]"
``` ```
- 定义环境变量 * 定义环境变量
```docker-compose ```docker-compose
environment: environment:
- [KEY]=[VALUE] - [KEY]=[VALUE]
``` ```
- 指定容器启动时执行的命令 * 指定容器启动时执行的命令
```docker-compose ```docker-compose
command: command:
- "[command]" - "[command]"
``` ```
- 指定服务启动所依赖的其他服务,会等待依赖的服务启动完成后再启动 * 指定服务启动所依赖的其他服务,会等待依赖的服务启动完成后再启动
```docker-compose ```docker-compose
depends_on: depends_on:
- service - service
``` ```
- 定义容器退出时的重启策略 * 定义容器退出时的重启策略
```docker-compose ```docker-compose
restart [strategy] restart [strategy]
@ -323,14 +318,13 @@ tags: []
`no`​:不重启 `no`​:不重启
`always`​:总是重启 `always`​:总是重启
* 指定给容器的名称。它是一个唯一标识符
- 指定给容器的名称。它是一个唯一标识符
```docker-compose ```docker-compose
container_name: [my_container] container_name: [my_container]
``` ```
- 容器暴露的端口给连接的其他服务,但不会映射到宿主机 * 容器暴露的端口给连接的其他服务,但不会映射到宿主机
```docker-compose ```docker-compose
expose: expose:
@ -345,9 +339,8 @@ tags: []
docker-compose up docker-compose up
``` ```
- `-d`​:在后台启动服务。 * `-d`​:在后台启动服务。
- `--build`​:构建服务,即使镜像已存在。 * `--build`​:构建服务,即使镜像已存在。
2. **停止容器应用** 2. **停止容器应用**
```docker-compose ```docker-compose

View File

@ -110,105 +110,106 @@ git push github master
## 常用的 Git 命令 ## 常用的 Git 命令
- 推送 * 推送
```git ```git
git push <origin> <master> git push <origin> <master>
``` ```
- 强制将推送本地分支 * 强制将推送本地分支
```git ```git
git push -f <origin> <master> git push -f <origin> <master>
``` ```
- 拉取 * 拉取
```git ```git
git pull <origin> <master> git pull <origin> <master>
``` ```
- 强制将分支的最新内容拉取到本地的分支 * 强制将分支的最新内容拉取到本地的分支
```git ```git
git pull --force <origin> <master> git pull --force <origin> <master>
``` ```
- 将本地分支重置为远程分支的最新状态 * 将本地分支重置为远程分支的最新状态
```git ```git
git reset --hard <origin>/<master> git reset --hard <origin>/<master>
``` ```
- 克隆仓库 * 克隆仓库
```git ```git
git clone <url> git clone <url>
``` ```
- 添加所有更改到暂存区 * 添加所有更改到暂存区
```git ```git
git add . git add .
``` ```
- 撤销部分文件的暂存 * 撤销部分文件的暂存
```git ```git
git reset <file1> <file2> git reset <file1> <file2>
``` ```
- 将文件从缓存区中移除,但物理文件仍然存在 * 将文件从缓存区中移除,但物理文件仍然存在
```git ```git
git rm --cached <path> git rm --cached <path>
``` ```
- 查看暂存区的内容 * 查看暂存区的内容
```git ```git
git ls-files git ls-files
``` ```
- 提交已暂存的更改 * 提交已暂存的更改
```git ```git
git commit -m "Commit message" git commit -m "Commit message"
``` ```
- 查看分支 * 查看分支
```git ```git
git branch git branch
``` ```
- 创建并切换到新分支 * 创建并切换到新分支
```git ```git
git checkout -b <new_branch_name> git checkout -b <new_branch_name>
``` ```
- 删除本地分支 * 删除本地分支
```git ```git
git branch -d <branch_nam> git branch -d <branch_nam>
``` ```
- 添加远程仓库 * 添加远程仓库
```git ```git
git remote add <origin> <remote_repository_url> git remote add <origin> <remote_repository_url>
``` ```
- 移除与远程仓库的关联 * 移除与远程仓库的关联
```git ```git
git remote remove <origin> git remote remove <origin>
``` ```
- 版本回退 * 版本回退
> HEAD相当与当前、HEAD~1 退回上一个版本、HEAD~2 退回上两个版本,依次类推。 > HEAD相当与当前、HEAD~1 退回上一个版本、HEAD~2 退回上两个版本,依次类推。
>
```git ```git
git reset --hard HEAD~1 git reset --hard HEAD~1
@ -283,16 +284,11 @@ practice_test/
```markdown ```markdown
# 忽略 test.c 文件 # 忽略 test.c 文件
practice_code/test.c practice_code/test.c
# 忽略 practice_test/ 目录下的文件 # 忽略 practice_test/ 目录下的文件
practice_test/ practice_test/
# 忽略 所有test.c 文件 # 忽略 所有test.c 文件
**/test.c每次提交自动同步到代码托管服务平台
\*\*/test.c 每次提交自动同步到代码托管服务平台
``` ```
#### 2.将已被追踪的文件的更改加入到暂存区 #### 2.将已被追踪的文件的更改加入到暂存区

View File

@ -68,15 +68,13 @@ tags: []
## 分组 ## 分组
- 普通分组 * 普通分组
`()` `()`
* 非捕获分组
- 非捕获分组
`(?:<表达式>)` `(?:<表达式>)`
* 回溯分组
- 回溯分组
`\<number>` `\<number>`
@ -90,28 +88,30 @@ tags: []
- 正向 * 正向
`(?=<表达式>)` `(?=<表达式>)`
> 右边必须出现某个字符 > 右边必须出现某个字符
>
- 反向 * 反向
`(?!<表达式>)` `(?!<表达式>)`
> 右边不能出现某个字符 > 右边不能出现某个字符
>
### 后行断言 ### 后行断言
- 正向 * 正向
`(?<=<表达式>)` `(?<=<表达式>)`
> 左边必须出现某个字符 > 左边必须出现某个字符
>
- 反向 * 反向
`(?<!<表达式>)` `(?<!<表达式>)`
> 左边不能出现某个字符 > 左边不能出现某个字符
>

View File

@ -14,4 +14,7 @@ mov AX,BX
机器码是单片工作且能识别和运行的一类代码常见的格式有二进制BIN格式、十进制HEX格式等。 机器码是单片工作且能识别和运行的一类代码常见的格式有二进制BIN格式、十进制HEX格式等。
汇编语言三类组成 1.汇编指令(机器码的助记符(能够直接翻译为机器码)) 2.伪指令(由编译器执行(计算机不能直接识别,编译器可以识别,由编译器转化为想表达的意思) 3.其他符号(由编译器识别,如:+ - \* / 汇编语言三类组成
1.汇编指令(机器码的助记符(能够直接翻译为机器码))
2.伪指令(由编译器执行(计算机不能直接识别,编译器可以识别,由编译器转化为想表达的意思)
3.其他符号(由编译器识别,如:+ - * /

View File

@ -4,6 +4,7 @@ date: 2024-06-06T23:51:42Z
tags: [] tags: []
--- ---
预处理Preprocessing 预处理Preprocessing
任务:预处理阶段对源代码进行预处理,展开宏、处理条件编译等。 任务:预处理阶段对源代码进行预处理,展开宏、处理条件编译等。
工具预处理器如C语言中的cpp。 工具预处理器如C语言中的cpp。

Some files were not shown because too many files have changed in this diff Show More