feat: scaffold initial Cheatsheet Hub application

This commit establishes the foundational structure for the Cheatsheet Hub project.

It includes the setup of a Vue 3, Vite, and TailwindCSS stack, along with core application features:

- **Routing:** Configures `vue-router` for Home, Cheatsheet List, and Cheatsheet Detail pages.
- **State Management:** Implements a `pinia` store for theme management (dark/light mode) and mock data.
- **UI Components:** Adds reusable components like `NavBar`, `CheatsheetCard`, and `PrintButton`.
- **Styling:** Integrates TailwindCSS with custom base styles, dark mode support, and print-friendly optimizations.
- **Dependencies:** Adds key libraries including `marked` for content rendering.
This commit is contained in:
xiaomai
2025-09-17 18:03:45 +08:00
parent 7ea9a3c8f9
commit 3058f251bc
18 changed files with 7507 additions and 35 deletions

6432
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -16,9 +16,11 @@
"format": "prettier --write src/"
},
"dependencies": {
"marked": "^16.3.0",
"pinia": "^3.0.3",
"vue": "^3.5.18",
"vue-router": "^4.5.1"
"vue-router": "^4.5.1",
"axios": "^1.4.0"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.2",
@@ -35,6 +37,9 @@
"typescript": "~5.8.0",
"vite": "^7.0.6",
"vite-plugin-vue-devtools": "^8.0.0",
"vue-tsc": "^3.0.4"
"vue-tsc": "^3.0.4",
"tailwindcss": "^3.3.3",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.24"
}
}

589
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

67
prompt.md Normal file
View File

@@ -0,0 +1,67 @@
# Cheatsheet 网站项目 Scaffold
你是一名资深前端架构师,熟悉 **Vue 3 + Vite + TailwindCSS** 的现代前端开发栈。
请帮我生成一个 **Cheatsheet 网站** 的基础工程结构,要求如下:
## 1. 技术栈
* **框架**Vue 3 (Composition API)
* **构建工具**Vite
* **样式**TailwindCSS + 自定义 base.css / main.css
* **组件库**:官方 Vue 生态优先(可选 shadcn-vue / headlessui / radix-vue
* **工具库**
* `vue-router` (多视图切换:主页 / Cheatsheet 列表 / 单个 Cheatsheet 页面)
* `pinia` (全局状态管理,用于存储 Cheatsheet 配置、主题偏好)
* `axios``fetch`(数据获取,预留接口位)
## 2. 项目结构
请生成一个基础目录结构,包含:
```
src/
assets/ # 静态资源
components/ # 通用组件(按钮、卡片、导航栏)
views/ # 页面视图HomeView, CheatsheetListView, CheatsheetDetailView
store/ # Pinia 状态管理
router/ # vue-router 配置
styles/
base.css # Tailwind 基础覆盖样式
main.css # 全局样式入口
App.vue
main.js
```
## 3. 基础样式
*`base.css` 中:
* 定义打印优化样式(例如:去掉多余边框、背景)
* 定义默认字体、字号(便于打印的 A4 规格)
*`main.css` 中:
* 引入 Tailwind
* 定义全局主题色(默认浅色 / 深色模式切换)
## 4. 默认页面
请提供基础模板代码:
* **HomeView**:展示官方 CheatsheetDocker, Vim+ 推荐的自定义入口
* **CheatsheetListView**:显示所有 Cheatsheet 的卡片列表
* **CheatsheetDetailView**:单个 Cheatsheet 的内容,支持「打印模式」按钮
## 5. 通用组件
请生成:
* `NavBar.vue`(带首页 / Cheatsheet 列表 / 主题切换按钮)
* `CheatsheetCard.vue`(显示标题、简介、打印按钮)
* `PrintButton.vue`(触发浏览器打印)
## 6. 额外要求
* 所有组件保持简洁、易扩展,留出注释
* 尽量用 Tailwind Utility Class而不是额外 CSS
* 确保项目可以直接运行npm run dev

View File

@@ -1,11 +1,31 @@
<script setup lang="ts"></script>
<template>
<h1>You did it!</h1>
<p>
Visit <a href="https://vuejs.org/" target="_blank" rel="noopener">vuejs.org</a> to read the
documentation
</p>
<div :class="{ 'dark': isDark }">
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
<NavBar />
<main class="container mx-auto px-4 py-8">
<router-view />
</main>
</div>
</div>
</template>
<style scoped></style>
<script lang="ts">
import { mapState, mapWritableState } from 'pinia'
import { useAppStore } from './stores'
import NavBar from './components/NavBar.vue'
export default {
name: 'App',
components: {
NavBar
},
computed: {
...mapState(useAppStore, ['isDark']),
...mapWritableState(useAppStore, ['theme'])
},
mounted() {
// 初始化主题
document.documentElement.setAttribute('data-theme', this.theme)
}
}
</script>

View File

@@ -0,0 +1,29 @@
<template>
<div class="card hover:shadow-lg transition-shadow duration-200">
<h3 class="text-xl font-semibold mb-2">{{ cheatsheet.title }}</h3>
<p class="text-gray-600 dark:text-gray-400 mb-4">{{ cheatsheet.description }}</p>
<div class="flex justify-between items-center">
<router-link :to="`/cheatsheet/${cheatsheet.id}`" class="text-primary-500 hover:text-primary-600 font-medium">
View Details
</router-link>
<PrintButton :cheatsheet="cheatsheet" />
</div>
</div>
</template>
<script lang="ts">
import PrintButton from './PrintButton.vue'
export default {
name: 'CheatsheetCard',
components: {
PrintButton
},
props: {
cheatsheet: {
type: Object,
required: true
}
}
}
</script>

46
src/components/NavBar.vue Normal file
View File

@@ -0,0 +1,46 @@
<template>
<nav class="bg-white dark:bg-gray-800 shadow-sm no-print">
<div class="container mx-auto px-4">
<div class="flex justify-between items-center py-4">
<router-link to="/" class="text-xl font-bold text-primary-500">
Cheatsheet Hub
</router-link>
<div class="flex items-center space-x-4">
<router-link to="/" class="text-gray-700 dark:text-gray-300 hover:text-primary-500 transition-colors"
:class="{ 'text-primary-500 font-medium': $route.name === 'home' }">
Home
</router-link>
<router-link to="/cheatsheets"
class="text-gray-700 dark:text-gray-300 hover:text-primary-500 transition-colors"
:class="{ 'text-primary-500 font-medium': $route.name === 'cheatsheets' }">
Cheatsheets
</router-link>
<button @click="toggleTheme"
class="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
aria-label="Toggle theme">
<span v-if="isDark">🌙</span>
<span v-else></span>
</button>
</div>
</div>
</div>
</nav>
</template>
<script lang="ts">
import { mapState, mapActions } from 'pinia'
import { useAppStore } from '../stores'
export default {
name: 'NavBar',
computed: {
...mapState(useAppStore, ['isDark'])
},
methods: {
...mapActions(useAppStore, ['toggleTheme'])
}
}
</script>

View File

@@ -0,0 +1,27 @@
<template>
<button @click="handlePrint"
class="p-2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors no-print"
aria-label="Print cheatsheet">
<svg xmlns="http://www.w3.org/2000/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="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m4 4h6a2 2 0 002-2v-4a2 2 0 00-2-2h-6a2 2 0 00-2 2v4a2 2 0 002 2z" />
</svg>
</button>
</template>
<script lang="ts">
export default {
name: 'PrintButton',
props: {
cheatsheet: {
type: Object,
required: true
}
},
methods: {
handlePrint() {
window.print()
}
}
}
</script>

View File

@@ -1,3 +1,5 @@
import './styles/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'

View File

@@ -1,8 +1,29 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import CheatsheetListView from '../views/CheatsheetListView.vue'
import CheatsheetDetailView from '../views/CheatsheetDetailView.vue'
const routes = [
{
path: '/',
name: 'home',
component: HomeView,
},
{
path: '/cheatsheets',
name: 'cheatsheets',
component: CheatsheetListView,
},
{
path: '/cheatsheet/:id',
name: 'cheatsheet',
component: CheatsheetDetailView,
},
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [],
history: createWebHistory(),
routes,
})
export default router

42
src/stores/index.ts Normal file
View File

@@ -0,0 +1,42 @@
import { defineStore } from 'pinia'
export const useAppStore = defineStore('app', {
state: () => ({
theme: 'light',
cheatsheets: [
{
id: 'docker',
title: 'Docker Cheatsheet',
description: 'Essential Docker commands and tips',
content:
'# Docker Commands\n\n## Container Management\n- `docker run [image]` - Start a new container\n- `docker ps` - List running containers\n- `docker stop [container]` - Stop a container',
},
{
id: 'vim',
title: 'Vim Cheatsheet',
description: 'Vim commands for efficient editing',
content:
'# Vim Commands\n\n## Navigation\n- `h,j,k,l` - Move left, down, up, right\n- `w` - Move to next word\n- `b` - Move to previous word',
},
{
id: 'git',
title: 'Git Cheatsheet',
description: 'Common Git commands for version control',
content:
'# Git Commands\n\n## Basics\n- `git init` - Initialize a new repository\n- `git add [file]` - Stage changes\n- `git commit -m "message"` - Commit changes',
},
],
}),
getters: {
isDark: (state) => state.theme === 'dark',
},
actions: {
toggleTheme() {
this.theme = this.theme === 'light' ? 'dark' : 'light'
document.documentElement.setAttribute('data-theme', this.theme)
},
getCheatsheetById(id) {
return this.cheatsheets.find((c) => c.id === id)
},
},
})

53
src/styles/base.css Normal file
View File

@@ -0,0 +1,53 @@
/* 打印优化样式 */
@media print {
.no-print {
display: none !important;
}
* {
background: transparent !important;
color: black !important;
box-shadow: none !important;
text-shadow: none !important;
}
body {
font-size: 12pt;
line-height: 1.5;
font-family: 'Times New Roman', Times, serif;
}
/* 确保链接在打印时显示URL */
a[href]:after {
content: ' (' attr(href) ')';
}
/* 避免在内容中间分页 */
h1,
h2,
h3,
h4,
h5,
h6 {
page-break-after: avoid;
}
pre,
blockquote {
page-break-inside: avoid;
}
}
/* 默认字体和排版 */
body {
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #333;
}
/* A4规格尺寸参考 */
.a4-container {
width: 210mm;
min-height: 297mm;
}

32
src/styles/main.css Normal file
View File

@@ -0,0 +1,32 @@
/* These Tailwind directives must be processed by Tailwind CSS build tools like PostCSS.
If you see "Unknown at rule @tailwind", make sure your build pipeline includes Tailwind CSS.
If you want to use plain CSS, remove these lines and use the generated output from Tailwind. */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* 全局主题变量 */
:root {
--color-primary: #3b82f6;
--color-primary-dark: #2563eb;
--color-background: #ffffff;
--color-text: #1f2937;
}
[data-theme='dark'] {
--color-primary: #60a5fa;
--color-primary-dark: #3b82f6;
--color-background: #111827;
--color-text: #f3f4f6;
}
/* 自定义组件样式 */
@layer components {
.btn-primary {
@apply bg-primary-500 hover:bg-primary-600 text-white font-medium py-2 px-4 rounded transition-colors duration-200;
}
.card {
@apply bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 border border-gray-200 dark:border-gray-700;
}
}

View File

@@ -0,0 +1,54 @@
<template>
<div v-if="cheatsheet" class="a4-container mx-auto">
<div class="card mb-6 no-print">
<div class="flex justify-between items-center">
<h1 class="text-3xl font-bold">{{ cheatsheet.title }}</h1>
<PrintButton :cheatsheet="cheatsheet" />
</div>
<p class="text-gray-600 dark:text-gray-400 mt-2">{{ cheatsheet.description }}</p>
</div>
<div class="bg-white dark:bg-gray-800 p-8 rounded-lg shadow-md">
<div v-html="renderedContent" class="prose dark:prose-invert max-w-none"></div>
</div>
</div>
<div v-else class="text-center py-12">
<p class="text-xl">Cheatsheet not found</p>
<router-link to="/cheatsheets" class="text-primary-500 hover:underline mt-4 inline-block">
Back to all cheatsheets
</router-link>
</div>
</template>
<script lang="ts">
import { marked } from 'marked'
import { mapActions } from 'pinia'
import { useAppStore } from '../stores'
import PrintButton from '../components/PrintButton.vue'
export default {
name: 'CheatsheetDetailView',
components: {
PrintButton
},
data() {
return {
cheatsheet: null
}
},
computed: {
renderedContent() {
if (!this.cheatsheet?.content) return ''
return marked(this.cheatsheet.content)
}
},
async mounted() {
const cheatsheetId = this.$route.params.id
this.cheatsheet = this.getCheatsheetById(cheatsheetId)
},
methods: {
...mapActions(useAppStore, ['getCheatsheetById'])
}
}
</script>

View File

@@ -0,0 +1,25 @@
<template>
<div>
<h1 class="text-3xl font-bold mb-8">All Cheatsheets</h1>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<CheatsheetCard v-for="cheatsheet in cheatsheets" :key="cheatsheet.id" :cheatsheet="cheatsheet" />
</div>
</div>
</template>
<script lang="ts">
import { mapState } from 'pinia'
import { useAppStore } from '../stores'
import CheatsheetCard from '../components/CheatsheetCard.vue'
export default {
name: 'CheatsheetListView',
components: {
CheatsheetCard
},
computed: {
...mapState(useAppStore, ['cheatsheets'])
}
}
</script>

44
src/views/HomeView.vue Normal file
View File

@@ -0,0 +1,44 @@
<template>
<div>
<section class="mb-12">
<h1 class="text-4xl font-bold text-center mb-6">Welcome to Cheatsheet Hub</h1>
<p class="text-xl text-center text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
Your one-stop destination for all programming cheatsheets. Find, create, and share useful references.
</p>
</section>
<section>
<h2 class="text-2xl font-semibold mb-6">Popular Cheatsheets</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<CheatsheetCard v-for="cheatsheet in featuredCheatsheets" :key="cheatsheet.id" :cheatsheet="cheatsheet" />
</div>
</section>
<section class="mt-12">
<div class="card text-center">
<h2 class="text-2xl font-semibold mb-4">Can't find what you're looking for?</h2>
<p class="mb-6">Suggest a new cheatsheet or contribute to our growing collection.</p>
<button class="btn-primary">Suggest a Cheatsheet</button>
</div>
</section>
</div>
</template>
<script lang="ts">
import { mapState } from 'pinia'
import { useAppStore } from '../stores'
import CheatsheetCard from '../components/CheatsheetCard.vue'
export default {
name: 'HomeView',
components: {
CheatsheetCard
},
computed: {
...mapState(useAppStore, ['cheatsheets']),
featuredCheatsheets() {
return this.cheatsheets.slice(0, 3)
}
}
}
</script>

24
tailwind.config.js Normal file
View File

@@ -0,0 +1,24 @@
// tailwind.config.js
export default {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb', // This defines bg-primary-600
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
},
},
},
},
plugins: [],
darkMode: 'class',
}