@astgov/theme 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # @astgov/theme
2
+
3
+ 老派机构风 Astro 主题包。看起来像 2005 的政府网站,写起来像 2026 的 Astro 项目。
4
+
5
+ ## 快速开始
6
+
7
+ ```bash
8
+ # 1. 复制示例网站
9
+ cp -r node_modules/@astgov/theme/example-site/* .
10
+
11
+ # 2. 安装依赖
12
+ npm install
13
+
14
+ # 3. 启动
15
+ npm run dev
16
+
17
+ # 4. 构建
18
+ npm run build
19
+ ```
20
+
21
+ ## 文档
22
+
23
+ - [快速开始](./docs/getting-started.md)
24
+ - [配置详解](./docs/configuration.md)
25
+ - [页面 API](./docs/pages-api.md)
26
+ - [部署指南](./docs/deployment.md)
27
+
28
+ ## License
29
+
30
+ MIT
package/lib/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ export { default as BaseLayout } from '../src/layouts/BaseLayout.astro';
2
+ export { default as BlogLayout } from '../src/layouts/BlogLayout.astro';
3
+ export { default as Header } from '../src/components/Header.astro';
4
+ export { default as Nav } from '../src/components/Nav.astro';
5
+ export { default as Footer } from '../src/components/Footer.astro';
6
+ export { default as Banner } from '../src/components/Banner.astro';
7
+ export { default as CategorySection } from '../src/components/CategorySection.astro';
8
+ export { default as QuickLinks } from '../src/components/QuickLinks.astro';
9
+ export { default as FriendLinks } from '../src/components/FriendLinks.astro';
10
+ export { default as Headline } from '../src/components/Headline.astro';
11
+ export { default as Countdown } from '../src/components/Countdown.astro';
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@astgov/theme",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "2005-style university website theme for Astro — 看起来像 2005,写起来像 2026",
6
+ "exports": {
7
+ ".": "./lib/index.ts",
8
+ "./layouts/*": "./src/layouts/*.astro",
9
+ "./components/*": "./src/components/*.astro",
10
+ "./styles/*": "./src/styles/*.css"
11
+ },
12
+ "files": [
13
+ "lib",
14
+ "src",
15
+ "README.md"
16
+ ],
17
+ "peerDependencies": {
18
+ "astro": "^4.0.0"
19
+ },
20
+ "keywords": ["astro", "theme", "university", "government", "retro", "china"],
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/your-org/astgov-theme"
25
+ }
26
+ }
@@ -0,0 +1,41 @@
1
+ ---
2
+ interface Props { images: { src: string; alt: string; link?: string }[] }
3
+ const { images = [] } = Astro.props;
4
+ ---
5
+
6
+ {images.length > 0 && (
7
+ <div class="banner" id="banner">
8
+ <div class="banner-slides" id="bannerSlides">
9
+ {images.map((img, i) => (
10
+ <div class="banner-slide" data-index={i}>
11
+ {img.link ? <a href={img.link}><img src={img.src} alt={img.alt} /></a> : <img src={img.src} alt={img.alt} />}
12
+ </div>
13
+ ))}
14
+ </div>
15
+ {images.length > 1 && (<>
16
+ <button class="banner-btn banner-prev" id="bannerPrev" aria-label="上一张">‹</button>
17
+ <button class="banner-btn banner-next" id="bannerNext" aria-label="下一张">›</button>
18
+ <div class="banner-dots" id="bannerDots">{images.map((_, i) => (<span class="banner-dot" data-index={i}></span>))}</div>
19
+ </>)}
20
+ </div>
21
+ )}
22
+
23
+ <script>
24
+ (function(){var b=document.getElementById('banner');if(!b)return;var s=document.getElementById('bannerSlides'),sl=s?.querySelectorAll('.banner-slide'),dt=document.querySelectorAll('.banner-dot'),p=document.getElementById('bannerPrev'),n=document.getElementById('bannerNext');if(!sl||sl.length<2)return;var c=0,iv;function go(i){sl.forEach(function(e,j){e.style.display=j===i?'block':'none'});dt.forEach(function(e,j){e.className=j===i?'banner-dot active':'banner-dot'});c=i}function nx(){go((c+1)%sl.length)}function pr(){go((c-1+sl.length)%sl.length)}function st(){cl();iv=window.setInterval(nx,4000)}function cl(){if(iv){clearInterval(iv);iv=0}}go(0);st();p?.addEventListener('click',function(e){e.preventDefault();pr();st()});n?.addEventListener('click',function(e){e.preventDefault();nx();st()});dt.forEach(function(e){e.addEventListener('click',function(){go(parseInt(this.getAttribute('data-index')||'0',10));st()})});b.addEventListener('mouseenter',cl);b.addEventListener('mouseleave',st)})();
25
+ </script>
26
+
27
+ <style>
28
+ .banner { position: relative; width: 100%; overflow: hidden; background: var(--color-primary, #003366); }
29
+ .banner-slides { position: relative; }
30
+ .banner-slide { display: none; }
31
+ .banner-slide:first-child { display: block; }
32
+ .banner-slide img { display: block; width: 100%; height: 360px; object-fit: cover; }
33
+ .banner-btn { position: absolute; top: 50%; transform: translateY(-50%); z-index: 10; background: rgba(0,0,0,.35); color: #FFF; border: none; font-size: 36px; line-height: 1; padding: 8px 14px; cursor: pointer; font-family: Arial,sans-serif; transition: background .2s; border-radius: 0; }
34
+ .banner-btn:hover { background: rgba(0,0,0,.6); }
35
+ .banner-prev { left: 10px; }
36
+ .banner-next { right: 10px; }
37
+ .banner-dots { position: absolute; bottom: 12px; left: 0; right: 0; text-align: center; z-index: 10; }
38
+ .banner-dot { display: inline-block; width: 12px; height: 12px; margin: 0 5px; background: rgba(255,255,255,.5); border: 2px solid #FFF; cursor: pointer; transition: background .2s; }
39
+ .banner-dot.active { background: #FFF; }
40
+ @media (max-width: 767px) { .banner-slide img { height: 200px; } .banner-btn { font-size: 24px; padding: 4px 10px; } .banner-dot { width: 10px; height: 10px; } }
41
+ </style>
@@ -0,0 +1,53 @@
1
+ ---
2
+ interface Post {
3
+ slug: string;
4
+ data: { title: string; date: string; tags?: string[] };
5
+ }
6
+
7
+ interface Props {
8
+ title?: string;
9
+ posts?: Post[];
10
+ viewAllLink?: string;
11
+ }
12
+
13
+ const { title, posts = [], viewAllLink } = Astro.props;
14
+ ---
15
+
16
+ <div class="module">
17
+ <div class="list-header">
18
+ <h3 class="module-title">{title}</h3>
19
+ {viewAllLink && <a href={viewAllLink} class="more-link">更多 →</a>}
20
+ </div>
21
+ <div class="list-body">
22
+ {posts.map(post => {
23
+ const d = post.data.date;
24
+ return (
25
+ <div class="list-item">
26
+ <a href={`/${post.slug}`} class="list-link">
27
+ <span class="list-date-block">
28
+ <span class="list-day">{d.slice(8, 10)}</span>
29
+ <span class="list-month">{d.slice(5, 7)}月</span>
30
+ </span>
31
+ <span class="list-text">{post.data.title}</span>
32
+ </a>
33
+ </div>
34
+ );
35
+ })}
36
+ </div>
37
+ </div>
38
+
39
+ <style>
40
+ .list-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; padding-bottom: 5px; border-bottom: 2px solid var(--color-primary, #003366); }
41
+ .module-title { font-size: 15px; font-weight: bold; color: var(--color-primary, #003366); margin: 0; }
42
+ .more-link { font-size: 12px; color: var(--color-link, #00F); text-decoration: none; }
43
+ .list-body { list-style: none; padding: 0; margin: 0; }
44
+ .list-item { border-bottom: 1px dashed var(--color-divider, #EEE); }
45
+ .list-item:last-child { border-bottom: none; }
46
+ .list-link { display: flex; align-items: center; gap: 10px; padding: 5px 0; text-decoration: none; color: var(--color-text, #333); }
47
+ .list-link:hover { text-decoration: none; }
48
+ .list-date-block { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 40px; flex-shrink: 0; line-height: 1.2; }
49
+ .list-day { font-size: 17px; font-weight: bold; font-family: Arial,sans-serif; color: var(--color-secondary, #C00); }
50
+ .list-month { font-size: 10px; font-family: Arial,sans-serif; color: #888; }
51
+ .list-text { font-size: 13px; font-family: "宋体","SimSun",serif; color: var(--color-link, #00F); line-height: 1.4; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
52
+ .list-link:hover .list-text { color: var(--color-secondary, #C00); text-decoration: underline; }
53
+ </style>
@@ -0,0 +1,31 @@
1
+ ---
2
+ interface Props { title?: string; targetDate?: string; buttonLink?: string; buttonText?: string; }
3
+ const { title = '距报名截止还有', targetDate = '', buttonLink = '/signup', buttonText = '立即报名 →' } = Astro.props;
4
+ ---
5
+
6
+ {targetDate && (
7
+ <div class="countdown-module">
8
+ <div class="countdown-title">{title}</div>
9
+ <div class="countdown-display" id="countdownDisplay" data-target={targetDate}>
10
+ <span class="countdown-num" id="cdDays">00</span><span class="countdown-label">天</span>
11
+ <span class="countdown-num" id="cdHours">00</span><span class="countdown-label">时</span>
12
+ <span class="countdown-num" id="cdMinutes">00</span><span class="countdown-label">分</span>
13
+ <span class="countdown-num" id="cdSeconds">00</span><span class="countdown-label">秒</span>
14
+ </div>
15
+ <a href={buttonLink} class="countdown-btn">{buttonText}</a>
16
+ </div>
17
+ )}
18
+
19
+ <script>
20
+ (function(){var e=document.getElementById('countdownDisplay');if(!e)return;var t=e.getAttribute('data-target')||'';if(!t)return;var d=document.getElementById('cdDays'),h=document.getElementById('cdHours'),m=document.getElementById('cdMinutes'),s=document.getElementById('cdSeconds');function p(n){return String(n).padStart(2,'0')}function u(){var n=new Date,o=new Date(t).getTime()-n.getTime();if(o<=0){d&&(d.textContent='00');h&&(h.textContent='00');m&&(m.textContent='00');s&&(s.textContent='00');return}var l=Math.floor(o/1e3),r=Math.floor(l/86400),g=Math.floor(l%86400/3600),e=Math.floor(l%3600/60),i=l%60;d&&(d.textContent=p(r));h&&(h.textContent=p(g));m&&(m.textContent=p(e));s&&(s.textContent=p(i))}u();setInterval(u,1e3)})();
21
+ </script>
22
+
23
+ <style>
24
+ .countdown-module { text-align: center; padding: 8px; }
25
+ .countdown-title { font-size: 15px; font-weight: bold; color: var(--color-secondary, #C00); font-family: "黑体","SimHei",sans-serif; margin-bottom: 8px; }
26
+ .countdown-display { margin-bottom: 10px; }
27
+ .countdown-num { display: inline-block; font-size: 24px; font-weight: bold; font-family: Arial,sans-serif; color: var(--color-primary, #036); background: #F0F0F0; padding: 3px 6px; margin: 0 2px; border: 1px solid var(--color-border, #CCC); min-width: 38px; text-align: center; }
28
+ .countdown-label { font-size: 13px; color: #666; font-family: "宋体","SimSun",serif; margin: 0 2px; }
29
+ .countdown-btn { display: inline-block; background: var(--color-secondary, #C00); color: #FFF; padding: 6px 24px; font-size: 14px; font-weight: bold; font-family: "黑体","SimHei",sans-serif; text-decoration: none; }
30
+ .countdown-btn:hover { background: #A00; text-decoration: none; color: #FFF; }
31
+ </style>
@@ -0,0 +1,47 @@
1
+ ---
2
+ interface Props {
3
+ organization?: string;
4
+ contacts?: { label: string; value: string }[];
5
+ records?: { label: string; value: string; link?: string }[];
6
+ policeRecord?: string;
7
+ links?: { text: string; url: string }[];
8
+ extraLinks?: { text: string; url: string }[];
9
+ qrCodes?: { src: string; alt: string }[];
10
+ footerLogo?: string;
11
+ }
12
+
13
+ const { organization = '', contacts = [], records = [], policeRecord, links = [], extraLinks = [], qrCodes = [] } = Astro.props;
14
+ ---
15
+
16
+ <footer class="footer">
17
+ <div class="container">
18
+ <div class="footer-bar">
19
+ <span class="footer-org">{organization}</span>
20
+ {records.length > 0 && records.map((r, i) => (<><span class="footer-sep">|</span><span>{r.link ? <a href={r.link} target="_blank" rel="noopener">{r.value}</a> : r.value}</span></>))}
21
+ {policeRecord && <><span class="footer-sep">|</span><a href="http://www.beian.gov.cn" target="_blank" rel="noopener">{policeRecord}</a></>}
22
+ </div>
23
+ {extraLinks.length > 0 && (
24
+ <div class="footer-extra">{extraLinks.map((link, i) => (<span>{i > 0 && <span class="footer-sep">|</span>}<a href={link.url}>{link.text}</a></span>))}</div>
25
+ )}
26
+ {qrCodes.length > 0 && (
27
+ <div class="footer-qr">{qrCodes.map(qr => (<div class="qr-item"><img src={qr.src} alt={qr.alt} class="qr-img" /><span class="qr-alt">{qr.alt}</span></div>))}</div>
28
+ )}
29
+ <div class="footer-copyright">版权所有 &copy; {new Date().getFullYear()} {organization}</div>
30
+ </div>
31
+ </footer>
32
+
33
+ <style>
34
+ .footer { background: var(--color-primary, #003366); color: #EEE; padding: 10px 0; font-size: 12px; line-height: 1.6; font-family: "宋体","SimSun",serif; }
35
+ .footer a { color: #E8F0FF; text-decoration: none; }
36
+ .footer a:hover { color: #FFF; text-decoration: underline; }
37
+ .footer-bar { text-align: center; margin-bottom: 4px; }
38
+ .footer-org { color: #FFF; font-weight: bold; }
39
+ .footer-sep { margin: 0 6px; color: #B0C8E0; }
40
+ .footer-extra { text-align: center; margin-bottom: 6px; font-size: 12px; }
41
+ .footer-qr { display: flex; gap: 8px; justify-content: center; margin-bottom: 6px; }
42
+ .qr-item { text-align: center; }
43
+ .qr-img { width: 50px; height: 50px; display: block; border: 1px solid #FFF; margin-bottom: 2px; }
44
+ .qr-alt { font-size: 10px; color: #E8F0FF; display: block; }
45
+ .footer-copyright { text-align: center; font-size: 11px; color: #D0E0F0; }
46
+ @media (max-width: 767px) { .footer-bar { display: flex; flex-direction: column; gap: 2px; } .footer-sep { display: none; } .footer-qr { flex-wrap: wrap; } }
47
+ </style>
@@ -0,0 +1,25 @@
1
+ ---
2
+ interface Props { title?: string; links?: { text: string; url: string; target?: string }[] }
3
+ const { title = '友情链接', links = [] } = Astro.props;
4
+ ---
5
+
6
+ {links.length > 0 && (
7
+ <div class="module">
8
+ <h3 class="module-title">{title}</h3>
9
+ <div class="fl-links">
10
+ {links.map(link => (
11
+ <a href={link.url} target={link.target || '_blank'} rel="noopener" class="fl-item">{link.text}</a>
12
+ ))}
13
+ </div>
14
+ </div>
15
+ )}
16
+
17
+ <style>
18
+ .module-title { font-size: 15px; font-weight: bold; color: var(--color-primary, #036); border-left: 3px solid var(--color-secondary, #C00); padding-left: 8px; margin-bottom: 10px; line-height: 1.4; }
19
+ .fl-links { display: flex; flex-wrap: wrap; }
20
+ .fl-item { display: inline-block; padding: 5px 16px; color: var(--color-link, #00F); font-size: 13px; font-family: "宋体","SimSun",serif; text-decoration: none; border-right: 1px solid var(--color-border, #CCC); line-height: 1.6; }
21
+ .fl-item:first-child { padding-left: 0; }
22
+ .fl-item:last-child { border-right: none; }
23
+ .fl-item:hover { color: var(--color-secondary, #C00); text-decoration: underline; }
24
+ @media (max-width: 767px) { .fl-item { padding: 5px 10px; font-size: 12px; } }
25
+ </style>
@@ -0,0 +1,93 @@
1
+ ---
2
+ interface Props {
3
+ siteName?: string;
4
+ siteSubtitle?: string;
5
+ siteLogo?: string;
6
+ topBar?: {
7
+ enabled?: boolean;
8
+ quickLinks?: { text: string; url: string; target?: string }[];
9
+ rightLinks?: { text: string; url: string; target?: string }[];
10
+ searchEnabled?: boolean;
11
+ searchPlaceholder?: string;
12
+ };
13
+ }
14
+
15
+ const { siteName, siteSubtitle, siteLogo, topBar = {} } = Astro.props;
16
+ const { enabled = true, quickLinks = [], rightLinks = [], searchEnabled = false, searchPlaceholder = '请输入关键字' } = topBar;
17
+ ---
18
+
19
+ <header class="top-header">
20
+ {enabled && (
21
+ <div class="toolbar">
22
+ <div class="container toolbar-inner">
23
+ <div class="toolbar-left">
24
+ {quickLinks.map(link => (
25
+ <a href={link.url} target={link.target || '_self'} class="toolbar-link">{link.text}</a>
26
+ ))}
27
+ </div>
28
+ <div class="toolbar-right">
29
+ <span class="font-size-group">
30
+ <a href="#" class="font-btn font-small" data-size="small" title="小字号">小</a>
31
+ <span class="font-sep">|</span>
32
+ <a href="#" class="font-btn font-medium" data-size="medium" title="中字号">中</a>
33
+ <span class="font-sep">|</span>
34
+ <a href="#" class="font-btn font-large" data-size="large" title="大字号">大</a>
35
+ </span>
36
+ {searchEnabled && (
37
+ <form class="search-form" action="/search" method="get">
38
+ <input type="text" class="search-input" placeholder={searchPlaceholder} name="keyword" />
39
+ <button type="submit" class="search-btn">搜索</button>
40
+ </form>
41
+ )}
42
+ {rightLinks.map(link => (
43
+ <a href={link.url} target={link.target || '_self'} class="toolbar-link toolbar-link-right">{link.text}</a>
44
+ ))}
45
+ </div>
46
+ </div>
47
+ </div>
48
+ )}
49
+ <div class="header-main">
50
+ <div class="container header-main-inner">
51
+ {siteLogo && <a href="/" class="logo-link"><img src={siteLogo} alt={siteName} class="logo-img" /></a>}
52
+ <div class="site-title-group">
53
+ <h1 class="site-name"><a href="/">{siteName}</a></h1>
54
+ {siteSubtitle && <p class="site-subtitle">{siteSubtitle}</p>}
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </header>
59
+
60
+ <script>
61
+ (function(){var b=document.querySelectorAll('.font-btn'),d=document.documentElement;b.forEach(function(e){e.addEventListener('click',function(f){f.preventDefault();b.forEach(function(g){g.classList.remove('active')});e.classList.add('active');d.setAttribute('data-font-size',e.getAttribute('data-size')||'medium')})})})();
62
+ </script>
63
+
64
+ <style>
65
+ .toolbar { background: var(--color-primary, #003366); font-size: 12px; line-height: 1; border-bottom: 1px solid rgba(255,255,255,.1); }
66
+ .toolbar-inner { display: flex; justify-content: space-between; align-items: center; height: 34px; }
67
+ .toolbar-left, .toolbar-right { display: flex; align-items: center; gap: 0; }
68
+ .toolbar-link { color: #F0F0F0; text-decoration: none; padding: 0 10px; border-right: 1px solid rgba(255,255,255,.15); font-family: "宋体", "SimSun", serif; white-space: nowrap; }
69
+ .toolbar-link:first-child { padding-left: 0; }
70
+ .toolbar-link:last-child { border-right: none; }
71
+ .toolbar-link:hover { color: #FFF; text-decoration: underline; }
72
+ .toolbar-link-right { border-right: none; border-left: 1px solid rgba(255,255,255,.15); }
73
+ .toolbar-link-right:first-child { border-left: none; }
74
+ .font-size-group { display: flex; align-items: center; margin-right: 10px; white-space: nowrap; }
75
+ .font-btn { color: #E8F0FF; text-decoration: none; padding: 0 3px; font-family: "宋体", "SimSun", serif; font-size: 12px; }
76
+ .font-btn:hover { color: #FFF; text-decoration: underline; }
77
+ .font-btn.active { color: #FFF; font-weight: bold; }
78
+ .font-sep { color: #8AC; margin: 0 2px; }
79
+ .search-form { display: flex; align-items: center; margin: 0 10px; }
80
+ .search-input { height: 22px; padding: 0 6px; border: 1px solid #557799; background: #FFF; font-size: 12px; font-family: "宋体", "SimSun", serif; width: 140px; outline: none; border-radius: 0; }
81
+ .search-input:focus { border-color: #9BD; }
82
+ .search-btn { height: 22px; padding: 0 8px; background: var(--color-secondary, #CC0000); color: #FFF; border: none; font-size: 11px; font-family: "宋体", "SimSun", serif; cursor: pointer; border-radius: 0; }
83
+ .search-btn:hover { background: #900; }
84
+ .header-main { background: #FFF; padding: 10px 0; border-bottom: 2px solid var(--color-primary, #003366); }
85
+ .header-main-inner { display: flex; align-items: center; gap: 16px; }
86
+ .logo-img { max-height: 64px; width: auto; display: block; }
87
+ .site-title-group { flex: 1; }
88
+ .site-name { font-size: 24px; font-weight: bold; line-height: 1.3; margin: 0; }
89
+ .site-name a { color: var(--color-primary, #003366); text-decoration: none; font-family: "黑体", "SimHei", sans-serif; }
90
+ .site-name a:hover { color: var(--color-secondary, #CC0000); }
91
+ .site-subtitle { font-size: 13px; color: #888; margin-top: 2px; font-family: "宋体", "SimSun", serif; }
92
+ @media (max-width: 767px) { .toolbar { display: none; } .header-main { padding: 8px 0; } .site-name { font-size: 18px; } .logo-img { max-height: 44px; } .search-form { display: none; } }
93
+ </style>
@@ -0,0 +1,37 @@
1
+ ---
2
+ interface Props {
3
+ cards?: { src: string; alt: string; title: string; date: string; link?: string }[];
4
+ }
5
+ const { cards = [] } = Astro.props;
6
+ ---
7
+
8
+ {cards.length > 0 && (
9
+ <section class="headline-section"><div class="container">
10
+ <div class="headline-grid">
11
+ {cards.map(card => (
12
+ <article class="headline-card">
13
+ <a href={card.link || '#'}>
14
+ <div class="card-img"><img src={card.src} alt={card.alt} loading="lazy" /></div>
15
+ <div class="card-body"><h3 class="card-title">{card.title}</h3><time class="card-date">{card.date}</time></div>
16
+ </a>
17
+ </article>
18
+ ))}
19
+ </div>
20
+ </div></section>
21
+ )}
22
+
23
+ <style>
24
+ .headline-section { padding: 10px 0 5px 0; }
25
+ .headline-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 10px; }
26
+ .headline-card { background: #FFF; border: 1px solid var(--color-border, #CCC); transition: border-color .15s; }
27
+ .headline-card:hover { border-color: var(--color-secondary, #C00); }
28
+ .headline-card a { text-decoration: none; color: inherit; display: block; }
29
+ .card-img { width: 100%; height: 150px; overflow: hidden; border-bottom: 1px solid var(--color-divider, #EEE); }
30
+ .card-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
31
+ .card-body { padding: 8px 10px 10px; }
32
+ .card-title { font-size: 13px; font-weight: bold; font-family: "黑体","SimHei",sans-serif; color: var(--color-text, #333); line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin: 0 0 4px; }
33
+ .headline-card:hover .card-title { color: var(--color-secondary, #C00); }
34
+ .card-date { font-size: 12px; color: #999; font-family: Arial,sans-serif; }
35
+ @media (max-width: 768px) { .headline-grid { grid-template-columns: 1fr; } .card-img { height: 180px; } }
36
+ @media (min-width:769px) and (max-width:1199px) { .headline-grid { grid-template-columns: repeat(2,1fr); } }
37
+ </style>
@@ -0,0 +1,52 @@
1
+ ---
2
+ interface NavItem {
3
+ text: string;
4
+ url: string;
5
+ target?: string;
6
+ highlight?: boolean;
7
+ dropdown?: { text: string; url: string; target?: string }[];
8
+ }
9
+
10
+ interface Props { items?: NavItem[] }
11
+ const { items = [] } = Astro.props;
12
+ ---
13
+
14
+ <nav class="nav">
15
+ <div class="container nav-inner">
16
+ {items.map(item => (
17
+ <div class:list={{ 'nav-item': true, 'has-dropdown': item.dropdown && item.dropdown.length > 0 }}>
18
+ <a
19
+ href={item.dropdown ? 'javascript:void(0)' : item.url}
20
+ class:list={{ 'nav-link': true, highlight: item.highlight }}
21
+ >
22
+ {item.text}
23
+ {item.dropdown && item.dropdown.length > 0 && <span class="nav-arrow">▼</span>}
24
+ </a>
25
+ {item.dropdown && item.dropdown.length > 0 && (
26
+ <div class="dropdown-menu">
27
+ {item.dropdown.map(sub => (
28
+ <a href={sub.url} target={sub.target || '_self'} class="dropdown-item">{sub.text}</a>
29
+ ))}
30
+ </div>
31
+ )}
32
+ </div>
33
+ ))}
34
+ </div>
35
+ </nav>
36
+
37
+ <style>
38
+ .nav { background: var(--color-secondary, #CC0000); padding: 0; position: relative; z-index: 100; }
39
+ .nav-inner { display: flex; flex-wrap: nowrap; position: relative; }
40
+ .nav-item { position: relative; flex: 0 0 auto; }
41
+ .nav-link { display: block; padding: 8px 18px; color: #FFF; font-size: 15px; font-weight: bold; text-decoration: none; font-family: "黑体","SimHei",sans-serif; white-space: nowrap; border-right: 1px solid rgba(255,255,255,.15); transition: background .15s; line-height: 1.4; }
42
+ .nav-link:hover { background: rgba(0,0,0,.2); text-decoration: none; color: #FFF; }
43
+ .nav-link.highlight { background: #FF6600; }
44
+ .nav-link.highlight:hover { background: #E05500; }
45
+ .nav-arrow { font-size: 10px; margin-left: 4px; vertical-align: middle; }
46
+ .dropdown-menu { display: none; position: absolute; top: 100%; left: 0; min-width: 140px; background: #FFF; border: 1px solid var(--color-border, #CCC); border-top: 2px solid var(--color-secondary, #C00); z-index: 200; padding: 0; }
47
+ .nav-item:hover .dropdown-menu { display: block; }
48
+ .dropdown-item { display: block; padding: 8px 16px; color: #333; font-size: 13px; font-family: "宋体","SimSun",serif; text-decoration: none; border-bottom: 1px solid #EEE; }
49
+ .dropdown-item:last-child { border-bottom: none; }
50
+ .dropdown-item:hover { background: #F5F5F5; color: var(--color-primary, #036); text-decoration: none; }
51
+ @media (max-width: 767px) { .nav-inner { flex-wrap: wrap; } .nav-link { padding: 7px 12px; font-size: 13px; border-right: none; border-bottom: 1px solid rgba(255,255,255,.1); } .nav-item { width: 100%; } .dropdown-menu { position: static; width: 100%; } }
52
+ </style>
@@ -0,0 +1,40 @@
1
+ ---
2
+ interface Props {
3
+ links?: { text: string; url: string; target?: string }[];
4
+ }
5
+ const { links = [] } = Astro.props;
6
+ ---
7
+
8
+ {links.length > 0 && (
9
+ <section class="ql-section"><div class="container">
10
+ <div class="ql-bar">
11
+ <div class="ql-label">快速通道</div>
12
+ <div class="ql-list" id="qlList">
13
+ {links.map(link => (
14
+ <a href={link.url} target={link.target || '_self'} class="ql-item">
15
+ <span class="ql-text">{link.text}</span>
16
+ </a>
17
+ ))}
18
+ </div>
19
+ {links.length > 4 && <button class="ql-toggle" id="qlToggle">展开更多</button>}
20
+ </div>
21
+ </div></section>
22
+ )}
23
+
24
+ <script>
25
+ (function(){var t=document.getElementById('qlToggle'),l=document.getElementById('qlList');if(!t||!l)return;t.addEventListener('click',function(){l.classList.toggle('expanded');t.textContent=l.classList.contains('expanded')?'收起':'展开更多'})})();
26
+ </script>
27
+
28
+ <style>
29
+ .ql-section { padding: 0 0 8px 0; }
30
+ .ql-bar { display: flex; align-items: stretch; border: 1px solid var(--color-border, #CCC); background: #FFF; }
31
+ .ql-label { background: var(--color-primary, #036); color: #FFF; font-size: 14px; font-weight: bold; font-family: "黑体","SimHei",sans-serif; padding: 8px 14px; display: flex; align-items: center; white-space: nowrap; flex-shrink: 0; }
32
+ .ql-list { display: flex; flex-wrap: wrap; align-items: center; flex: 1; overflow: hidden; max-height: 80px; transition: max-height .25s; }
33
+ .ql-list.expanded { max-height: 200px; }
34
+ .ql-item { display: flex; align-items: center; gap: 4px; padding: 6px 14px; text-decoration: none; color: var(--color-text, #333); font-size: 13px; font-family: "宋体","SimSun",serif; border-right: 1px solid var(--color-divider, #EEE); transition: background .15s; }
35
+ .ql-item:hover { background: #F5F5F5; color: var(--color-primary, #036); text-decoration: none; }
36
+ .ql-text { line-height: 1.3; }
37
+ .ql-toggle { background: var(--color-bg, #F5F5F5); border: none; border-left: 1px solid var(--color-border, #CCC); padding: 6px 12px; font-size: 12px; color: var(--color-link, #00F); cursor: pointer; font-family: "宋体","SimSun",serif; white-space: nowrap; flex-shrink: 0; }
38
+ .ql-toggle:hover { color: var(--color-secondary, #C00); text-decoration: underline; }
39
+ @media (max-width: 767px) { .ql-bar { flex-wrap: wrap; } .ql-label { width: 100%; justify-content: center; } .ql-item { padding: 5px 10px; font-size: 12px; } }
40
+ </style>
@@ -0,0 +1,133 @@
1
+ ---
2
+ import Header from '../components/Header.astro';
3
+ import Nav from '../components/Nav.astro';
4
+ import Footer from '../components/Footer.astro';
5
+ import '../styles/global.css';
6
+
7
+ interface TopBarLink {
8
+ text: string;
9
+ url: string;
10
+ target?: string;
11
+ }
12
+
13
+ interface NavItem {
14
+ text: string;
15
+ url: string;
16
+ target?: string;
17
+ highlight?: boolean;
18
+ dropdown?: { text: string; url: string; target?: string }[];
19
+ }
20
+
21
+ interface FooterConfig {
22
+ organization: string;
23
+ contacts?: { label: string; value: string }[];
24
+ records?: { label: string; value: string; link?: string }[];
25
+ policeRecord?: string;
26
+ links?: { text: string; url: string }[];
27
+ extraLinks?: { text: string; url: string }[];
28
+ qrCodes?: { src: string; alt: string }[];
29
+ footerLogo?: string;
30
+ }
31
+
32
+ interface Props {
33
+ title?: string;
34
+ description?: string;
35
+ keywords?: string;
36
+ siteName?: string;
37
+ siteSubtitle?: string;
38
+ siteLogo?: string;
39
+ siteIcon?: string;
40
+ topBar?: {
41
+ enabled?: boolean;
42
+ quickLinks?: TopBarLink[];
43
+ rightLinks?: TopBarLink[];
44
+ searchEnabled?: boolean;
45
+ searchPlaceholder?: string;
46
+ };
47
+ navItems?: NavItem[];
48
+ footer?: FooterConfig;
49
+ colors?: {
50
+ primary?: string;
51
+ primaryLight?: string;
52
+ secondary?: string;
53
+ accent?: string;
54
+ background?: string;
55
+ text?: string;
56
+ link?: string;
57
+ linkVisited?: string;
58
+ border?: string;
59
+ divider?: string;
60
+ fontFamily?: string;
61
+ };
62
+ defaultFontSize?: string;
63
+ }
64
+
65
+ const {
66
+ title = 'AstGov',
67
+ description = '',
68
+ keywords = '',
69
+ siteName,
70
+ siteSubtitle,
71
+ siteLogo,
72
+ siteIcon = '/favicon.svg',
73
+ topBar = {},
74
+ navItems = [],
75
+ footer = { organization: '' },
76
+ colors = {},
77
+ defaultFontSize = 'medium',
78
+ } = Astro.props;
79
+
80
+ const fontSizeAttr = defaultFontSize;
81
+ const cssVars = `
82
+ :root {
83
+ --color-primary: ${colors.primary || '#003366'};
84
+ --color-primary-light: ${colors.primaryLight || colors.primary || '#003366'};
85
+ --color-secondary: ${colors.secondary || '#CC0000'};
86
+ --color-accent: ${colors.accent || colors.secondary || '#CC0000'};
87
+ --color-bg: ${colors.background || '#F5F5F5'};
88
+ --color-text: ${colors.text || '#333333'};
89
+ --color-link: ${colors.link || '#0000FF'};
90
+ --color-link-visited: ${colors.linkVisited || '#800080'};
91
+ --color-border: ${colors.border || '#CCCCCC'};
92
+ --color-divider: ${colors.divider || '#EEEEEE'};
93
+ --font-body: ${colors.fontFamily || '"宋体", "SimSun", serif'};
94
+ --font-heading: "黑体", "SimHei", sans-serif;
95
+ }
96
+ `;
97
+ ---
98
+
99
+ <!DOCTYPE html>
100
+ <html lang="zh-CN" data-font-size={fontSizeAttr}>
101
+ <head>
102
+ <meta charset="UTF-8" />
103
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
104
+ <title>{title ? `${title} - ${siteName || ''}`.replace(/ - $/, '') : siteName || 'AstGov'}</title>
105
+ <meta name="description" content={description} />
106
+ <meta name="keywords" content={keywords} />
107
+ <link rel="icon" href={siteIcon} type="image/svg+xml" />
108
+ <link rel="stylesheet" href="/styles/global.css" />
109
+ <style is:global set:html={cssVars}></style>
110
+ </head>
111
+ <body>
112
+ <Header
113
+ siteName={siteName}
114
+ siteSubtitle={siteSubtitle}
115
+ siteLogo={siteLogo}
116
+ topBar={topBar}
117
+ />
118
+ <Nav items={navItems} />
119
+ <main>
120
+ <slot />
121
+ </main>
122
+ <Footer
123
+ organization={footer.organization}
124
+ contacts={footer.contacts}
125
+ records={footer.records}
126
+ policeRecord={footer.policeRecord}
127
+ links={footer.links}
128
+ extraLinks={footer.extraLinks}
129
+ qrCodes={footer.qrCodes}
130
+ footerLogo={footer.footerLogo}
131
+ />
132
+ </body>
133
+ </html>
@@ -0,0 +1,38 @@
1
+ ---
2
+ import BaseLayout from './BaseLayout.astro';
3
+
4
+ interface Props {
5
+ title?: string;
6
+ description?: string;
7
+ keywords?: string;
8
+ siteName?: string;
9
+ siteSubtitle?: string;
10
+ siteLogo?: string;
11
+ siteIcon?: string;
12
+ topBar?: any;
13
+ navItems?: any[];
14
+ footer?: any;
15
+ colors?: any;
16
+ defaultFontSize?: string;
17
+ }
18
+
19
+ const props = Astro.props;
20
+ ---
21
+
22
+ <BaseLayout {...props}>
23
+ <section class="page-section">
24
+ <div class="container">
25
+ <article class="article">
26
+ <slot />
27
+ </article>
28
+ </div>
29
+ </section>
30
+ </BaseLayout>
31
+
32
+ <style>
33
+ .page-section { padding: 15px 0; min-height: 400px; }
34
+ .article { background: #FFFFFF; border: 1px solid var(--color-border, #CCCCCC); padding: 30px 35px; }
35
+ @media (max-width: 767px) {
36
+ .article { padding: 15px 18px; }
37
+ }
38
+ </style>
@@ -0,0 +1,26 @@
1
+ /* ============================================================
2
+ AstGov 主题包 — 全局样式
3
+ 2000 年代老派政府网站风格
4
+ ============================================================ */
5
+ *,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
6
+ html{font-size:14px;scroll-behavior:smooth}
7
+ body{font-family:var(--font-body, "宋体", "SimSun", serif);font-size:14px;line-height:1.8;color:var(--color-text, #333);background-color:var(--color-bg, #F5F5F5);min-height:100vh}
8
+ img{max-width:100%;height:auto;border:none}
9
+ a{color:var(--color-link, #00F);text-decoration:none}
10
+ a:visited{color:var(--color-link-visited, #808)}
11
+ a:hover{color:var(--color-secondary, #C00);text-decoration:underline}
12
+ ul,ol{list-style:none;padding:0;margin:0}
13
+ .container{max-width:1200px;margin:0 auto;padding:0 15px}
14
+ h1,h2,h3,h4,h5,h6{font-family:var(--font-heading, "黑体", "SimHei", sans-serif);line-height:1.5;color:var(--color-text, #333)}
15
+ h1{font-size:24px}h2{font-size:20px}h3{font-size:16px}h4{font-size:14px}
16
+ .module-title{font-size:15px;font-weight:bold;color:var(--color-primary, #036);border-left:3px solid var(--color-secondary, #C00);padding-left:8px;margin-bottom:10px;line-height:1.4}
17
+ .module-title .more-link{float:right;font-size:12px;font-weight:normal;color:var(--color-link, #00F);padding-top:1px}
18
+ .module{background:#FFF;border:1px solid var(--color-border, #CCC);padding:10px;margin-bottom:10px}
19
+ .two-col{display:flex;gap:10px}
20
+ .two-col .col-main{flex:0 0 70%;max-width:70%}
21
+ .two-col .col-side{flex:0 0 calc(30% - 10px);max-width:calc(30% - 10px)}
22
+ html[data-font-size="small"]{font-size:12px}html[data-font-size="small"] body{font-size:12px}html[data-font-size="small"] h1{font-size:20px}html[data-font-size="small"] h2{font-size:17px}html[data-font-size="small"] h3{font-size:14px}html[data-font-size="small"] .module-title{font-size:13px}
23
+ html[data-font-size="large"]{font-size:16px}html[data-font-size="large"] body{font-size:16px}html[data-font-size="large"] h1{font-size:28px}html[data-font-size="large"] h2{font-size:23px}html[data-font-size="large"] h3{font-size:18px}html[data-font-size="large"] .module-title{font-size:17px}
24
+ .card-date,.footer,.toolbar{font-size:12px!important}
25
+ @media(max-width:1199px){.two-col{flex-direction:column}.two-col .col-main,.two-col .col-side{flex:0 0 100%;max-width:100%}}
26
+ @media(max-width:767px){.container{padding:0 10px}.module{padding:8px}}