@astgov/theme 1.0.0 → 1.0.1

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 CHANGED
@@ -1,30 +1,37 @@
1
- # @astgov/theme
1
+ # AstGov 主题包
2
2
 
3
- 老派机构风 Astro 主题包。看起来像 2005 的政府网站,写起来像 2026 的 Astro 项目。
3
+ 老派机构风 Astro 主题包。看起来像 2005 的政府网站,写起来像 2026 的 Astro
4
4
 
5
- ## 快速开始
5
+ 基于 **Astro 6.x**,使用 Content Layer API、`astro:assets` 图片优化、CSS 自定义属性主题系统。
6
+
7
+ 完整文档在 `docs/` 目录下:
8
+
9
+ - [快速开始](./docs/getting-started.md) — 5 分钟跑起来
10
+ - [目录结构](./docs/directory-structure.md) — 文件说明
11
+ - [配置详解](./docs/configuration.md) — 所有可配置项
12
+ - [文章与页面管理](./docs/content-management.md) — 写文章、建页面
13
+ - [样式定制](./docs/styling.md) — 改样式、换字体
14
+ - [部署指南](./docs/deployment.md) — 发布上线
15
+ - [常见问题](./docs/faq.md) — 排查问题
16
+
17
+ ## 快速上手
6
18
 
7
19
  ```bash
8
- # 1. 复制示例网站
9
- cp -r node_modules/@astgov/theme/example-site/* .
20
+ # 1. 克隆示例网站
21
+ git clone https://github.com/OrO-c/astgov-demo-site my-site
22
+ cd my-site
10
23
 
11
- # 2. 安装依赖
24
+ # 2. 安装
12
25
  npm install
13
26
 
14
27
  # 3. 启动
15
28
  npm run dev
16
-
17
- # 4. 构建
18
- npm run build
19
29
  ```
20
30
 
21
- ## 文档
31
+ **要求:Node.js 22.12.0 或更高版本。**
22
32
 
23
- - [快速开始](./docs/getting-started.md)
24
- - [配置详解](./docs/configuration.md)
25
- - [页面 API](./docs/pages-api.md)
26
- - [部署指南](./docs/deployment.md)
33
+ 浏览器打开 `http://localhost:4321` 即可看到效果。
27
34
 
28
- ## License
35
+ ## 许可证
29
36
 
30
37
  MIT
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@astgov/theme",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
5
  "description": "2005-style university website theme for Astro — 看起来像 2005,写起来像 2026",
6
+ "peerDependencies": {
7
+ "astro": "^6.4.8"
8
+ },
6
9
  "exports": {
7
10
  ".": "./lib/index.ts",
8
11
  "./layouts/*": "./src/layouts/*.astro",
@@ -14,13 +17,22 @@
14
17
  "src",
15
18
  "README.md"
16
19
  ],
17
- "peerDependencies": {
18
- "astro": "^4.0.0"
20
+ "scripts": {
21
+ "dev": "astro dev",
22
+ "build": "astro build",
23
+ "preview": "astro preview"
19
24
  },
20
- "keywords": ["astro", "theme", "university", "government", "retro", "china"],
25
+ "keywords": [
26
+ "astro",
27
+ "theme",
28
+ "university",
29
+ "government",
30
+ "retro",
31
+ "china"
32
+ ],
21
33
  "license": "MIT",
22
34
  "repository": {
23
35
  "type": "git",
24
- "url": "https://github.com/your-org/astgov-theme"
36
+ "url": "https://github.com/OrO-c/AstGov"
25
37
  }
26
38
  }
@@ -1,6 +1,7 @@
1
1
  ---
2
- interface Props { images: { src: string; alt: string; link?: string }[] }
3
- const { images = [] } = Astro.props;
2
+ import { Image } from 'astro:assets';
3
+ interface Props { images: { src: string; alt: string; link?: string }[]; height?: number; autoplayInterval?: number; prevText?: string; nextText?: string }
4
+ const { images = [], height = 360, autoplayInterval = 4000, prevText = '‹', nextText = '›' } = Astro.props;
4
5
  ---
5
6
 
6
7
  {images.length > 0 && (
@@ -8,20 +9,20 @@ const { images = [] } = Astro.props;
8
9
  <div class="banner-slides" id="bannerSlides">
9
10
  {images.map((img, i) => (
10
11
  <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
+ {img.link ? <a href={img.link}><Image src={img.src} alt={img.alt} /></a> : <Image src={img.src} alt={img.alt} />}
12
13
  </div>
13
14
  ))}
14
15
  </div>
15
16
  {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>
17
+ <button class="banner-btn banner-prev" id="bannerPrev" aria-label="上一张">{prevText}</button>
18
+ <button class="banner-btn banner-next" id="bannerNext" aria-label="下一张">{nextText}</button>
18
19
  <div class="banner-dots" id="bannerDots">{images.map((_, i) => (<span class="banner-dot" data-index={i}></span>))}</div>
19
20
  </>)}
20
21
  </div>
21
22
  )}
22
23
 
23
24
  <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
+ (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,{autoplayInterval})}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
26
  </script>
26
27
 
27
28
  <style>
@@ -29,7 +30,7 @@ const { images = [] } = Astro.props;
29
30
  .banner-slides { position: relative; }
30
31
  .banner-slide { display: none; }
31
32
  .banner-slide:first-child { display: block; }
32
- .banner-slide img { display: block; width: 100%; height: 360px; object-fit: cover; }
33
+ .banner-slide img { display: block; width: 100%; height: {height}px; object-fit: cover; }
33
34
  .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
35
  .banner-btn:hover { background: rgba(0,0,0,.6); }
35
36
  .banner-prev { left: 10px; }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  interface Post {
3
- slug: string;
3
+ id: string;
4
4
  data: { title: string; date: string; tags?: string[] };
5
5
  }
6
6
 
@@ -8,25 +8,33 @@ interface Props {
8
8
  title?: string;
9
9
  posts?: Post[];
10
10
  viewAllLink?: string;
11
+ moreText?: string;
12
+ dateFormat?: string;
11
13
  }
12
14
 
13
- const { title, posts = [], viewAllLink } = Astro.props;
15
+ const { title, posts = [], viewAllLink, moreText = '更多 →', dateFormat = '{month}月' } = Astro.props;
16
+ // 从 id 中提取链接路径(去掉 "pages/" 前缀、保留其余路径段)
17
+ function entryLink(id: string): string {
18
+ const parts = id.split('/');
19
+ // id 格式如 "pages/news/001",去掉第一段得到 "news/001"
20
+ return parts.slice(1).join('/');
21
+ }
14
22
  ---
15
23
 
16
24
  <div class="module">
17
25
  <div class="list-header">
18
26
  <h3 class="module-title">{title}</h3>
19
- {viewAllLink && <a href={viewAllLink} class="more-link">更多 →</a>}
27
+ {viewAllLink && <a href={viewAllLink} class="more-link">{moreText}</a>}
20
28
  </div>
21
29
  <div class="list-body">
22
30
  {posts.map(post => {
23
31
  const d = post.data.date;
24
32
  return (
25
33
  <div class="list-item">
26
- <a href={`/${post.slug}`} class="list-link">
34
+ <a href={`/${entryLink(post.id)}`} class="list-link">
27
35
  <span class="list-date-block">
28
36
  <span class="list-day">{d.slice(8, 10)}</span>
29
- <span class="list-month">{d.slice(5, 7)}月</span>
37
+ <span class="list-month">{dateFormat.replace('{month}', d.slice(5, 7)).replace('{day}', d.slice(8, 10)).replace('{year}', d.slice(0, 4))}</span>
30
38
  </span>
31
39
  <span class="list-text">{post.data.title}</span>
32
40
  </a>
@@ -1,16 +1,17 @@
1
1
  ---
2
- interface Props { title?: string; targetDate?: string; buttonLink?: string; buttonText?: string; }
3
- const { title = '距报名截止还有', targetDate = '', buttonLink = '/signup', buttonText = '立即报名 →' } = Astro.props;
2
+ interface Props { title?: string; targetDate?: string; buttonLink?: string; buttonText?: string; labels?: { days?: string; hours?: string; minutes?: string; seconds?: string } }
3
+ const { title = '距报名截止还有', targetDate = '', buttonLink = '/signup', buttonText = '立即报名 →', labels } = Astro.props;
4
+ const lb = labels || {};
4
5
  ---
5
6
 
6
7
  {targetDate && (
7
8
  <div class="countdown-module">
8
9
  <div class="countdown-title">{title}</div>
9
10
  <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>
11
+ <span class="countdown-num" id="cdDays">00</span><span class="countdown-label">{lb.days || '天'}</span>
12
+ <span class="countdown-num" id="cdHours">00</span><span class="countdown-label">{lb.hours || '时'}</span>
13
+ <span class="countdown-num" id="cdMinutes">00</span><span class="countdown-label">{lb.minutes || '分'}</span>
14
+ <span class="countdown-num" id="cdSeconds">00</span><span class="countdown-label">{lb.seconds || '秒'}</span>
14
15
  </div>
15
16
  <a href={buttonLink} class="countdown-btn">{buttonText}</a>
16
17
  </div>
@@ -1,4 +1,6 @@
1
1
  ---
2
+ import { Image } from 'astro:assets';
3
+
2
4
  interface Props {
3
5
  organization?: string;
4
6
  contacts?: { label: string; value: string }[];
@@ -8,9 +10,10 @@ interface Props {
8
10
  extraLinks?: { text: string; url: string }[];
9
11
  qrCodes?: { src: string; alt: string }[];
10
12
  footerLogo?: string;
13
+ copyright?: string;
11
14
  }
12
15
 
13
- const { organization = '', contacts = [], records = [], policeRecord, links = [], extraLinks = [], qrCodes = [] } = Astro.props;
16
+ const { organization = '', contacts = [], records = [], policeRecord, links = [], extraLinks = [], qrCodes = [], copyright } = Astro.props;
14
17
  ---
15
18
 
16
19
  <footer class="footer">
@@ -24,9 +27,9 @@ const { organization = '', contacts = [], records = [], policeRecord, links = []
24
27
  <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
28
  )}
26
29
  {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>
30
+ <div class="footer-qr">{qrCodes.map(qr => (<div class="qr-item"><Image src={qr.src} alt={qr.alt} class="qr-img" /><span class="qr-alt">{qr.alt}</span></div>))}</div>
28
31
  )}
29
- <div class="footer-copyright">版权所有 &copy; {new Date().getFullYear()} {organization}</div>
32
+ <div class="footer-copyright">{(copyright || '版权所有 © {year} {org}').replace('{year}', String(new Date().getFullYear())).replace('{org}', organization)}</div>
30
33
  </div>
31
34
  </footer>
32
35
 
@@ -1,4 +1,6 @@
1
1
  ---
2
+ import { Image } from 'astro:assets';
3
+
2
4
  interface Props {
3
5
  siteName?: string;
4
6
  siteSubtitle?: string;
@@ -10,9 +12,11 @@ interface Props {
10
12
  searchEnabled?: boolean;
11
13
  searchPlaceholder?: string;
12
14
  };
15
+ fontSizeLabels?: { small?: string; medium?: string; large?: string };
13
16
  }
14
17
 
15
- const { siteName, siteSubtitle, siteLogo, topBar = {} } = Astro.props;
18
+ const { siteName, siteSubtitle, siteLogo, topBar = {}, fontSizeLabels } = Astro.props;
19
+ const fl = fontSizeLabels || {};
16
20
  const { enabled = true, quickLinks = [], rightLinks = [], searchEnabled = false, searchPlaceholder = '请输入关键字' } = topBar;
17
21
  ---
18
22
 
@@ -27,11 +31,11 @@ const { enabled = true, quickLinks = [], rightLinks = [], searchEnabled = false,
27
31
  </div>
28
32
  <div class="toolbar-right">
29
33
  <span class="font-size-group">
30
- <a href="#" class="font-btn font-small" data-size="small" title="小字号">小</a>
34
+ <a href="#" class="font-btn font-small" data-size="small" title="{fl.small || '小'}字号">{fl.small || '小'}</a>
31
35
  <span class="font-sep">|</span>
32
- <a href="#" class="font-btn font-medium" data-size="medium" title="中字号">中</a>
36
+ <a href="#" class="font-btn font-medium" data-size="medium" title="{fl.medium || '中'}字号">{fl.medium || '中'}</a>
33
37
  <span class="font-sep">|</span>
34
- <a href="#" class="font-btn font-large" data-size="large" title="大字号">大</a>
38
+ <a href="#" class="font-btn font-large" data-size="large" title="{fl.large || '大'}字号">{fl.large || '大'}</a>
35
39
  </span>
36
40
  {searchEnabled && (
37
41
  <form class="search-form" action="/search" method="get">
@@ -48,7 +52,7 @@ const { enabled = true, quickLinks = [], rightLinks = [], searchEnabled = false,
48
52
  )}
49
53
  <div class="header-main">
50
54
  <div class="container header-main-inner">
51
- {siteLogo && <a href="/" class="logo-link"><img src={siteLogo} alt={siteName} class="logo-img" /></a>}
55
+ {siteLogo && <a href="/" class="logo-link"><Image src={siteLogo} alt={siteName || ''} class="logo-img" /></a>}
52
56
  <div class="site-title-group">
53
57
  <h1 class="site-name"><a href="/">{siteName}</a></h1>
54
58
  {siteSubtitle && <p class="site-subtitle">{siteSubtitle}</p>}
@@ -1,4 +1,6 @@
1
1
  ---
2
+ import { Image } from 'astro:assets';
3
+
2
4
  interface Props {
3
5
  cards?: { src: string; alt: string; title: string; date: string; link?: string }[];
4
6
  }
@@ -11,7 +13,7 @@ const { cards = [] } = Astro.props;
11
13
  {cards.map(card => (
12
14
  <article class="headline-card">
13
15
  <a href={card.link || '#'}>
14
- <div class="card-img"><img src={card.src} alt={card.alt} loading="lazy" /></div>
16
+ <div class="card-img"><Image src={card.src} alt={card.alt} /></div>
15
17
  <div class="card-body"><h3 class="card-title">{card.title}</h3><time class="card-date">{card.date}</time></div>
16
18
  </a>
17
19
  </article>
@@ -7,8 +7,8 @@ interface NavItem {
7
7
  dropdown?: { text: string; url: string; target?: string }[];
8
8
  }
9
9
 
10
- interface Props { items?: NavItem[] }
11
- const { items = [] } = Astro.props;
10
+ interface Props { items?: NavItem[]; dropdownArrow?: string }
11
+ const { items = [], dropdownArrow = '▼' } = Astro.props;
12
12
  ---
13
13
 
14
14
  <nav class="nav">
@@ -20,7 +20,7 @@ const { items = [] } = Astro.props;
20
20
  class:list={{ 'nav-link': true, highlight: item.highlight }}
21
21
  >
22
22
  {item.text}
23
- {item.dropdown && item.dropdown.length > 0 && <span class="nav-arrow">▼</span>}
23
+ {item.dropdown && item.dropdown.length > 0 && <span class="nav-arrow">{dropdownArrow}</span>}
24
24
  </a>
25
25
  {item.dropdown && item.dropdown.length > 0 && (
26
26
  <div class="dropdown-menu">
@@ -1,14 +1,17 @@
1
1
  ---
2
2
  interface Props {
3
3
  links?: { text: string; url: string; target?: string }[];
4
+ title?: string;
5
+ expandText?: string;
6
+ collapseText?: string;
4
7
  }
5
- const { links = [] } = Astro.props;
8
+ const { links = [], title = '快速通道', expandText = '展开更多', collapseText = '收起' } = Astro.props;
6
9
  ---
7
10
 
8
11
  {links.length > 0 && (
9
12
  <section class="ql-section"><div class="container">
10
13
  <div class="ql-bar">
11
- <div class="ql-label">快速通道</div>
14
+ <div class="ql-label">{title}</div>
12
15
  <div class="ql-list" id="qlList">
13
16
  {links.map(link => (
14
17
  <a href={link.url} target={link.target || '_self'} class="ql-item">
@@ -16,13 +19,13 @@ const { links = [] } = Astro.props;
16
19
  </a>
17
20
  ))}
18
21
  </div>
19
- {links.length > 4 && <button class="ql-toggle" id="qlToggle">展开更多</button>}
22
+ {links.length > 4 && <button class="ql-toggle" id="qlToggle" data-expand={expandText} data-collapse={collapseText}>{expandText}</button>}
20
23
  </div>
21
24
  </div></section>
22
25
  )}
23
26
 
24
27
  <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')?'收起':'展开更多'})})();
28
+ (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')?co:ex})})();
26
29
  </script>
27
30
 
28
31
  <style>
@@ -60,6 +60,17 @@ interface Props {
60
60
  fontFamily?: string;
61
61
  };
62
62
  defaultFontSize?: string;
63
+ /** UI 文本配置 */
64
+ ui?: {
65
+ moreText?: string;
66
+ dropdownArrow?: string;
67
+ copyright?: string;
68
+ quickLinks?: { title?: string; expandText?: string; collapseText?: string };
69
+ friendLinks?: { title?: string };
70
+ fontSizeLabels?: { small?: string; medium?: string; large?: string };
71
+ countdown?: { labels?: { days?: string; hours?: string; minutes?: string; seconds?: string } };
72
+ dateFormat?: string;
73
+ };
63
74
  }
64
75
 
65
76
  const {
@@ -75,6 +86,7 @@ const {
75
86
  footer = { organization: '' },
76
87
  colors = {},
77
88
  defaultFontSize = 'medium',
89
+ ui = {},
78
90
  } = Astro.props;
79
91
 
80
92
  const fontSizeAttr = defaultFontSize;
@@ -106,7 +118,7 @@ const cssVars = `
106
118
  <meta name="keywords" content={keywords} />
107
119
  <link rel="icon" href={siteIcon} type="image/svg+xml" />
108
120
  <link rel="stylesheet" href="/styles/global.css" />
109
- <style is:global set:html={cssVars}></style>
121
+ <Fragment set:html={`<style>${cssVars}</style>`} />
110
122
  </head>
111
123
  <body>
112
124
  <Header
@@ -114,8 +126,9 @@ const cssVars = `
114
126
  siteSubtitle={siteSubtitle}
115
127
  siteLogo={siteLogo}
116
128
  topBar={topBar}
129
+ fontSizeLabels={ui.fontSizeLabels}
117
130
  />
118
- <Nav items={navItems} />
131
+ <Nav items={navItems} dropdownArrow={ui.dropdownArrow} />
119
132
  <main>
120
133
  <slot />
121
134
  </main>
@@ -128,6 +141,7 @@ const cssVars = `
128
141
  extraLinks={footer.extraLinks}
129
142
  qrCodes={footer.qrCodes}
130
143
  footerLogo={footer.footerLogo}
144
+ copyright={ui.copyright}
131
145
  />
132
146
  </body>
133
147
  </html>