Compare commits
No commits in common. "main" and "feat/ui" have entirely different histories.
14
.claude/settings.local.json
Normal file
14
.claude/settings.local.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash",
|
||||
"ReadFiles",
|
||||
"WriteFiles",
|
||||
"EditFiles",
|
||||
"WebFetch(domain:3d.hunyuan.tencent.com)",
|
||||
"WebSearch",
|
||||
"WebFetch(domain:cloud.tencent.com)",
|
||||
"WebFetch(domain:cloud.tencent.com.cn)"
|
||||
]
|
||||
}
|
||||
}
|
||||
437
.claude/skills/design-system/SKILL.md
Normal file
437
.claude/skills/design-system/SKILL.md
Normal file
@ -0,0 +1,437 @@
|
||||
---
|
||||
name: design-system
|
||||
description: "比赛管理系统设计规范。当用户提出页面开发需求时自动应用。主色: #0958d9 蓝色主题。包含: 颜色系统、间距、圆角、阴影、字体、组件规范(按钮、卡片、表单、标签)、页面布局、动画效果。适用: 新增页面、修改样式、组件开发、UI调整。"
|
||||
---
|
||||
|
||||
# 比赛管理系统 - 设计规范 Skill
|
||||
|
||||
当用户提出页面开发、UI修改、组件创建等需求时,**必须遵循以下设计规范**。
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
每个新页面的 `<style scoped lang="scss">` 开头必须包含以下变量:
|
||||
|
||||
```scss
|
||||
// ==========================================
|
||||
// 项目统一设计变量 - 必须复制
|
||||
// ==========================================
|
||||
$primary: #0958d9;
|
||||
$primary-light: #1677ff;
|
||||
$primary-dark: #003eb3;
|
||||
$secondary: #4096ff;
|
||||
$success: #52c41a;
|
||||
$warning: #faad14;
|
||||
$error: #ff4d4f;
|
||||
|
||||
$background: #f5f5f5;
|
||||
$surface: #ffffff;
|
||||
|
||||
$text: rgba(0, 0, 0, 0.85);
|
||||
$text-secondary: rgba(0, 0, 0, 0.65);
|
||||
$text-muted: rgba(0, 0, 0, 0.45);
|
||||
|
||||
$border: #d9d9d9;
|
||||
$border-light: #e8e8e8;
|
||||
|
||||
$gradient-primary: linear-gradient(135deg, $primary 0%, $primary-light 100%);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 色彩系统
|
||||
|
||||
### 主色(蓝色主题)
|
||||
|
||||
| 变量 | 色值 | 用途 |
|
||||
|------|------|------|
|
||||
| `$primary` | `#0958d9` | 主色、按钮、链接 |
|
||||
| `$primary-light` | `#1677ff` | 悬停态 |
|
||||
| `$primary-dark` | `#003eb3` | 激活态 |
|
||||
| `$secondary` | `#4096ff` | 辅助蓝 |
|
||||
|
||||
### 功能色
|
||||
|
||||
| 变量 | 色值 | 用途 |
|
||||
|------|------|------|
|
||||
| `$success` | `#52c41a` | 成功状态 |
|
||||
| `$warning` | `#faad14` | 警告状态 |
|
||||
| `$error` | `#ff4d4f` | 错误状态 |
|
||||
|
||||
### 中性色
|
||||
|
||||
| 变量 | 色值 | 用途 |
|
||||
|------|------|------|
|
||||
| `$background` | `#f5f5f5` | 页面背景 |
|
||||
| `$surface` | `#ffffff` | 卡片/容器背景 |
|
||||
| `$text` | `rgba(0,0,0,0.85)` | 主文本 |
|
||||
| `$text-secondary` | `rgba(0,0,0,0.65)` | 次要文本 |
|
||||
| `$text-muted` | `rgba(0,0,0,0.45)` | 弱化文本 |
|
||||
| `$border-light` | `#e8e8e8` | 边框 |
|
||||
|
||||
---
|
||||
|
||||
## 间距规范
|
||||
|
||||
基于 **8px** 基准:
|
||||
|
||||
| 尺寸 | 值 | 场景 |
|
||||
|------|------|------|
|
||||
| xs | 4px | 图标间距 |
|
||||
| sm | 8px | 元素内间距 |
|
||||
| md | 12px | 卡片内元素 |
|
||||
| base | 16px | 最常用 |
|
||||
| lg | 20px | 区块间距 |
|
||||
| xl | 24px | 大间距 |
|
||||
| 2xl | 32px | 模块分隔 |
|
||||
|
||||
---
|
||||
|
||||
## 圆角规范
|
||||
|
||||
| 尺寸 | 值 | 场景 |
|
||||
|------|------|------|
|
||||
| sm | 4px | 小元素 |
|
||||
| base | 6px | 默认 |
|
||||
| md | 8px | 卡片 |
|
||||
| lg | 10px | 返回按钮 |
|
||||
| xl | 12px | 大卡片 |
|
||||
| pill | 20px | 标签 |
|
||||
| full | 50% | 头像 |
|
||||
|
||||
---
|
||||
|
||||
## 阴影规范
|
||||
|
||||
| 等级 | 定义 | 场景 |
|
||||
|------|------|------|
|
||||
| sm | `0 1px 2px rgba(0,0,0,0.03)` | 轻微 |
|
||||
| base | `0 2px 8px rgba(0,0,0,0.06)` | 卡片默认 |
|
||||
| md | `0 4px 12px rgba(0,0,0,0.08)` | 悬停 |
|
||||
| lg | `0 8px 24px rgba(0,0,0,0.12)` | 弹窗 |
|
||||
|
||||
---
|
||||
|
||||
## 字体规范
|
||||
|
||||
| 层级 | 大小 | 权重 | 场景 |
|
||||
|------|------|------|------|
|
||||
| h1 | 36px | 700 | 大标题 |
|
||||
| h2 | 26px | 700 | 区块标题 |
|
||||
| h3 | 22px | 600 | 卡片标题 |
|
||||
| h4 | 18px | 600 | 小标题 |
|
||||
| body | 14px | 400 | 正文 |
|
||||
| small | 12px | 400 | 标签 |
|
||||
|
||||
---
|
||||
|
||||
## 组件规范
|
||||
|
||||
### 返回按钮(统一样式)
|
||||
|
||||
```scss
|
||||
.back-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px !important;
|
||||
border: 1px solid rgba($primary, 0.3) !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease !important;
|
||||
|
||||
&:hover {
|
||||
background: rgba($primary, 0.1) !important;
|
||||
border-color: $primary !important;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
模板代码:
|
||||
```vue
|
||||
<a-button type="text" class="back-btn" @click="handleBack">
|
||||
<template #icon><ArrowLeftOutlined /></template>
|
||||
</a-button>
|
||||
```
|
||||
|
||||
### 主按钮
|
||||
|
||||
```scss
|
||||
.primary-btn {
|
||||
background: $gradient-primary !important;
|
||||
border: none !important;
|
||||
color: #fff !important;
|
||||
font-weight: 500 !important;
|
||||
transition: all 0.3s ease !important;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(1.1);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 24px rgba($primary, 0.2);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 卡片
|
||||
|
||||
```scss
|
||||
.card {
|
||||
background: $surface;
|
||||
border: 1px solid $border-light;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba($primary, 0.3);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 标签
|
||||
|
||||
```scss
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
|
||||
&.tag-success {
|
||||
background: rgba($success, 0.1);
|
||||
color: $success;
|
||||
border: 1px solid rgba($success, 0.3);
|
||||
}
|
||||
|
||||
&.tag-primary {
|
||||
background: rgba($primary, 0.1);
|
||||
color: $primary;
|
||||
border: 1px solid rgba($primary, 0.3);
|
||||
}
|
||||
|
||||
&.tag-error {
|
||||
background: rgba($error, 0.1);
|
||||
color: $error;
|
||||
border: 1px solid rgba($error, 0.3);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 面板/浮层
|
||||
|
||||
```scss
|
||||
.panel {
|
||||
background: rgba($surface, 0.85);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba($primary, 0.2);
|
||||
border-radius: 12px;
|
||||
|
||||
.panel-header {
|
||||
padding: 14px 16px;
|
||||
background: rgba($primary, 0.05);
|
||||
border-bottom: 1px solid rgba($primary, 0.1);
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 页面布局规范
|
||||
|
||||
### 页面头部(64px 高)
|
||||
|
||||
```scss
|
||||
.page-header {
|
||||
height: 64px;
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: $surface;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
background: $gradient-primary;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 全屏页面容器
|
||||
|
||||
```scss
|
||||
.fullscreen-page {
|
||||
min-height: 100vh;
|
||||
background: $background;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
overflow: auto;
|
||||
}
|
||||
```
|
||||
|
||||
### 卡片网格
|
||||
|
||||
```scss
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 动画规范
|
||||
|
||||
### 过渡时间
|
||||
|
||||
- 标准过渡:`0.3s ease`
|
||||
- 所有交互元素必须添加过渡
|
||||
|
||||
### 悬停效果
|
||||
|
||||
```scss
|
||||
// 卡片悬停
|
||||
transform: translateY(-4px);
|
||||
|
||||
// 按钮悬停
|
||||
transform: translateY(-2px);
|
||||
|
||||
// 小元素悬停
|
||||
transform: translateY(-1px);
|
||||
```
|
||||
|
||||
### 常用动画
|
||||
|
||||
```scss
|
||||
// 脉冲
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(0.8); }
|
||||
}
|
||||
|
||||
// 旋转
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 加载/错误状态
|
||||
|
||||
### 加载遮罩
|
||||
|
||||
```scss
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: $background;
|
||||
z-index: 10;
|
||||
}
|
||||
```
|
||||
|
||||
### 错误卡片
|
||||
|
||||
```scss
|
||||
.error-card {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
background: $surface;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 16px;
|
||||
|
||||
.error-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin: 0 auto 20px;
|
||||
background: $error;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 滚动条
|
||||
|
||||
```scss
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: rgba($primary, 0.05);
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba($primary, 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 响应式断点
|
||||
|
||||
```scss
|
||||
$breakpoint-sm: 640px;
|
||||
$breakpoint-md: 768px;
|
||||
$breakpoint-lg: 1024px;
|
||||
$breakpoint-xl: 1280px;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 开发检查清单
|
||||
|
||||
新增页面必须确认:
|
||||
|
||||
- [ ] 复制了颜色变量到 style 开头
|
||||
- [ ] 返回按钮使用 40x40px + 10px 圆角
|
||||
- [ ] 卡片悬停有 `translateY(-4px)` 效果
|
||||
- [ ] 页面头部高度 64px
|
||||
- [ ] 所有过渡动画 0.3s
|
||||
- [ ] 使用 `$gradient-primary` 作为主渐变
|
||||
- [ ] 加载状态使用蓝色系
|
||||
|
||||
---
|
||||
|
||||
## 参考页面
|
||||
|
||||
- `frontend/src/views/workbench/ai-3d/Index.vue` - 3D Lab 主页
|
||||
- `frontend/src/views/workbench/ai-3d/Generate.vue` - 生成页
|
||||
- `frontend/src/views/model/ModelViewer.vue` - 模型预览页
|
||||
228
.claude/skills/ui-ux-pro-max/SKILL.md
Normal file
228
.claude/skills/ui-ux-pro-max/SKILL.md
Normal file
@ -0,0 +1,228 @@
|
||||
---
|
||||
name: ui-ux-pro-max
|
||||
description: "UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 8 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, mobile app, .html, .tsx, .vue, .svelte. Elements: button, modal, navbar, sidebar, card, table, form, chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, flat design. Topics: color palette, accessibility, animation, layout, typography, font pairing, spacing, hover, shadow, gradient."
|
||||
---
|
||||
|
||||
# UI/UX Pro Max - Design Intelligence
|
||||
|
||||
Searchable database of UI styles, color palettes, font pairings, chart types, product recommendations, UX guidelines, and stack-specific best practices.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Check if Python is installed:
|
||||
|
||||
```bash
|
||||
python3 --version || python --version
|
||||
```
|
||||
|
||||
If Python is not installed, install it based on user's OS:
|
||||
|
||||
**macOS:**
|
||||
```bash
|
||||
brew install python3
|
||||
```
|
||||
|
||||
**Ubuntu/Debian:**
|
||||
```bash
|
||||
sudo apt update && sudo apt install python3
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
winget install Python.Python.3.12
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How to Use This Skill
|
||||
|
||||
When user requests UI/UX work (design, build, create, implement, review, fix, improve), follow this workflow:
|
||||
|
||||
### Step 1: Analyze User Requirements
|
||||
|
||||
Extract key information from user request:
|
||||
- **Product type**: SaaS, e-commerce, portfolio, dashboard, landing page, etc.
|
||||
- **Style keywords**: minimal, playful, professional, elegant, dark mode, etc.
|
||||
- **Industry**: healthcare, fintech, gaming, education, etc.
|
||||
- **Stack**: React, Vue, Next.js, or default to `html-tailwind`
|
||||
|
||||
### Step 2: Search Relevant Domains
|
||||
|
||||
Use `search.py` multiple times to gather comprehensive information. Search until you have enough context.
|
||||
|
||||
```bash
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "<keyword>" --domain <domain> [-n <max_results>]
|
||||
```
|
||||
|
||||
**Recommended search order:**
|
||||
|
||||
1. **Product** - Get style recommendations for product type
|
||||
2. **Style** - Get detailed style guide (colors, effects, frameworks)
|
||||
3. **Typography** - Get font pairings with Google Fonts imports
|
||||
4. **Color** - Get color palette (Primary, Secondary, CTA, Background, Text, Border)
|
||||
5. **Landing** - Get page structure (if landing page)
|
||||
6. **Chart** - Get chart recommendations (if dashboard/analytics)
|
||||
7. **UX** - Get best practices and anti-patterns
|
||||
8. **Stack** - Get stack-specific guidelines (default: html-tailwind)
|
||||
|
||||
### Step 3: Stack Guidelines (Default: html-tailwind)
|
||||
|
||||
If user doesn't specify a stack, **default to `html-tailwind`**.
|
||||
|
||||
```bash
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "<keyword>" --stack html-tailwind
|
||||
```
|
||||
|
||||
Available stacks: `html-tailwind`, `react`, `nextjs`, `vue`, `svelte`, `swiftui`, `react-native`, `flutter`
|
||||
|
||||
---
|
||||
|
||||
## Search Reference
|
||||
|
||||
### Available Domains
|
||||
|
||||
| Domain | Use For | Example Keywords |
|
||||
|--------|---------|------------------|
|
||||
| `product` | Product type recommendations | SaaS, e-commerce, portfolio, healthcare, beauty, service |
|
||||
| `style` | UI styles, colors, effects | glassmorphism, minimalism, dark mode, brutalism |
|
||||
| `typography` | Font pairings, Google Fonts | elegant, playful, professional, modern |
|
||||
| `color` | Color palettes by product type | saas, ecommerce, healthcare, beauty, fintech, service |
|
||||
| `landing` | Page structure, CTA strategies | hero, hero-centric, testimonial, pricing, social-proof |
|
||||
| `chart` | Chart types, library recommendations | trend, comparison, timeline, funnel, pie |
|
||||
| `ux` | Best practices, anti-patterns | animation, accessibility, z-index, loading |
|
||||
| `prompt` | AI prompts, CSS keywords | (style name) |
|
||||
|
||||
### Available Stacks
|
||||
|
||||
| Stack | Focus |
|
||||
|-------|-------|
|
||||
| `html-tailwind` | Tailwind utilities, responsive, a11y (DEFAULT) |
|
||||
| `react` | State, hooks, performance, patterns |
|
||||
| `nextjs` | SSR, routing, images, API routes |
|
||||
| `vue` | Composition API, Pinia, Vue Router |
|
||||
| `svelte` | Runes, stores, SvelteKit |
|
||||
| `swiftui` | Views, State, Navigation, Animation |
|
||||
| `react-native` | Components, Navigation, Lists |
|
||||
| `flutter` | Widgets, State, Layout, Theming |
|
||||
|
||||
---
|
||||
|
||||
## Example Workflow
|
||||
|
||||
**User request:** "Làm landing page cho dịch vụ chăm sóc da chuyên nghiệp"
|
||||
|
||||
**AI should:**
|
||||
|
||||
```bash
|
||||
# 1. Search product type
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "beauty spa wellness service" --domain product
|
||||
|
||||
# 2. Search style (based on industry: beauty, elegant)
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "elegant minimal soft" --domain style
|
||||
|
||||
# 3. Search typography
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "elegant luxury" --domain typography
|
||||
|
||||
# 4. Search color palette
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "beauty spa wellness" --domain color
|
||||
|
||||
# 5. Search landing page structure
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "hero-centric social-proof" --domain landing
|
||||
|
||||
# 6. Search UX guidelines
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "animation" --domain ux
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "accessibility" --domain ux
|
||||
|
||||
# 7. Search stack guidelines (default: html-tailwind)
|
||||
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "layout responsive" --stack html-tailwind
|
||||
```
|
||||
|
||||
**Then:** Synthesize all search results and implement the design.
|
||||
|
||||
---
|
||||
|
||||
## Tips for Better Results
|
||||
|
||||
1. **Be specific with keywords** - "healthcare SaaS dashboard" > "app"
|
||||
2. **Search multiple times** - Different keywords reveal different insights
|
||||
3. **Combine domains** - Style + Typography + Color = Complete design system
|
||||
4. **Always check UX** - Search "animation", "z-index", "accessibility" for common issues
|
||||
5. **Use stack flag** - Get implementation-specific best practices
|
||||
6. **Iterate** - If first search doesn't match, try different keywords
|
||||
|
||||
---
|
||||
|
||||
## Common Rules for Professional UI
|
||||
|
||||
These are frequently overlooked issues that make UI look unprofessional:
|
||||
|
||||
### Icons & Visual Elements
|
||||
|
||||
| Rule | Do | Don't |
|
||||
|------|----|----- |
|
||||
| **No emoji icons** | Use SVG icons (Heroicons, Lucide, Simple Icons) | Use emojis like 🎨 🚀 ⚙️ as UI icons |
|
||||
| **Stable hover states** | Use color/opacity transitions on hover | Use scale transforms that shift layout |
|
||||
| **Correct brand logos** | Research official SVG from Simple Icons | Guess or use incorrect logo paths |
|
||||
| **Consistent icon sizing** | Use fixed viewBox (24x24) with w-6 h-6 | Mix different icon sizes randomly |
|
||||
|
||||
### Interaction & Cursor
|
||||
|
||||
| Rule | Do | Don't |
|
||||
|------|----|----- |
|
||||
| **Cursor pointer** | Add `cursor-pointer` to all clickable/hoverable cards | Leave default cursor on interactive elements |
|
||||
| **Hover feedback** | Provide visual feedback (color, shadow, border) | No indication element is interactive |
|
||||
| **Smooth transitions** | Use `transition-colors duration-200` | Instant state changes or too slow (>500ms) |
|
||||
|
||||
### Light/Dark Mode Contrast
|
||||
|
||||
| Rule | Do | Don't |
|
||||
|------|----|----- |
|
||||
| **Glass card light mode** | Use `bg-white/80` or higher opacity | Use `bg-white/10` (too transparent) |
|
||||
| **Text contrast light** | Use `#0F172A` (slate-900) for text | Use `#94A3B8` (slate-400) for body text |
|
||||
| **Muted text light** | Use `#475569` (slate-600) minimum | Use gray-400 or lighter |
|
||||
| **Border visibility** | Use `border-gray-200` in light mode | Use `border-white/10` (invisible) |
|
||||
|
||||
### Layout & Spacing
|
||||
|
||||
| Rule | Do | Don't |
|
||||
|------|----|----- |
|
||||
| **Floating navbar** | Add `top-4 left-4 right-4` spacing | Stick navbar to `top-0 left-0 right-0` |
|
||||
| **Content padding** | Account for fixed navbar height | Let content hide behind fixed elements |
|
||||
| **Consistent max-width** | Use same `max-w-6xl` or `max-w-7xl` | Mix different container widths |
|
||||
|
||||
---
|
||||
|
||||
## Pre-Delivery Checklist
|
||||
|
||||
Before delivering UI code, verify these items:
|
||||
|
||||
### Visual Quality
|
||||
- [ ] No emojis used as icons (use SVG instead)
|
||||
- [ ] All icons from consistent icon set (Heroicons/Lucide)
|
||||
- [ ] Brand logos are correct (verified from Simple Icons)
|
||||
- [ ] Hover states don't cause layout shift
|
||||
- [ ] Use theme colors directly (bg-primary) not var() wrapper
|
||||
|
||||
### Interaction
|
||||
- [ ] All clickable elements have `cursor-pointer`
|
||||
- [ ] Hover states provide clear visual feedback
|
||||
- [ ] Transitions are smooth (150-300ms)
|
||||
- [ ] Focus states visible for keyboard navigation
|
||||
|
||||
### Light/Dark Mode
|
||||
- [ ] Light mode text has sufficient contrast (4.5:1 minimum)
|
||||
- [ ] Glass/transparent elements visible in light mode
|
||||
- [ ] Borders visible in both modes
|
||||
- [ ] Test both modes before delivery
|
||||
|
||||
### Layout
|
||||
- [ ] Floating elements have proper spacing from edges
|
||||
- [ ] No content hidden behind fixed navbars
|
||||
- [ ] Responsive at 320px, 768px, 1024px, 1440px
|
||||
- [ ] No horizontal scroll on mobile
|
||||
|
||||
### Accessibility
|
||||
- [ ] All images have alt text
|
||||
- [ ] Form inputs have labels
|
||||
- [ ] Color is not the only indicator
|
||||
- [ ] `prefers-reduced-motion` respected
|
||||
26
.claude/skills/ui-ux-pro-max/data/charts.csv
Normal file
26
.claude/skills/ui-ux-pro-max/data/charts.csv
Normal file
@ -0,0 +1,26 @@
|
||||
No,Data Type,Keywords,Best Chart Type,Secondary Options,Color Guidance,Performance Impact,Accessibility Notes,Library Recommendation,Interactive Level
|
||||
1,Trend Over Time,"trend, time-series, line, growth, timeline, progress",Line Chart,"Area Chart, Smooth Area",Primary: #0080FF. Multiple series: use distinct colors. Fill: 20% opacity,⚡ Excellent (optimized),✓ Clear line patterns for colorblind users. Add pattern overlays.,"Chart.js, Recharts, ApexCharts",Hover + Zoom
|
||||
2,Compare Categories,"compare, categories, bar, comparison, ranking",Bar Chart (Horizontal or Vertical),"Column Chart, Grouped Bar",Each bar: distinct color. Category: grouped same color. Sorted: descending order,⚡ Excellent,✓ Easy to compare. Add value labels on bars for clarity.,"Chart.js, Recharts, D3.js",Hover + Sort
|
||||
3,Part-to-Whole,"part-to-whole, pie, donut, percentage, proportion, share",Pie Chart or Donut,"Stacked Bar, Treemap",Colors: 5-6 max. Contrasting palette. Large slices first. Use labels.,⚡ Good (limit 6 slices),⚠ Hard for accessibility. Better: Stacked bar with legend. Avoid pie if >5 items.,"Chart.js, Recharts, D3.js",Hover + Drill
|
||||
4,Correlation/Distribution,"correlation, distribution, scatter, relationship, pattern",Scatter Plot or Bubble Chart,"Heat Map, Matrix",Color axis: gradient (blue-red). Size: relative. Opacity: 0.6-0.8 to show density,⚠ Moderate (many points),⚠ Provide data table alternative. Use pattern + color distinction.,"D3.js, Plotly, Recharts",Hover + Brush
|
||||
5,Heatmap/Intensity,"heatmap, heat-map, intensity, density, matrix",Heat Map or Choropleth,"Grid Heat Map, Bubble Heat",Gradient: Cool (blue) to Hot (red). Scale: clear legend. Divergent for ±data,⚡ Excellent (color CSS),⚠ Colorblind: Use pattern overlay. Provide numerical legend.,"D3.js, Plotly, ApexCharts",Hover + Zoom
|
||||
6,Geographic Data,"geographic, map, location, region, geo, spatial","Choropleth Map, Bubble Map",Geographic Heat Map,Regional: single color gradient or categorized colors. Legend: clear scale,⚠ Moderate (rendering),⚠ Include text labels for regions. Provide data table alternative.,"D3.js, Mapbox, Leaflet",Pan + Zoom + Drill
|
||||
7,Funnel/Flow,funnel/flow,"Funnel Chart, Sankey",Waterfall (for flows),Stages: gradient (starting color → ending color). Show conversion %,⚡ Good,✓ Clear stage labels + percentages. Good for accessibility if labeled.,"D3.js, Recharts, Custom SVG",Hover + Drill
|
||||
8,Performance vs Target,performance-vs-target,Gauge Chart or Bullet Chart,"Dial, Thermometer",Performance: Red→Yellow→Green gradient. Target: marker line. Threshold colors,⚡ Good,✓ Add numerical value + percentage label beside gauge.,"D3.js, ApexCharts, Custom SVG",Hover
|
||||
9,Time-Series Forecast,time-series-forecast,Line with Confidence Band,Ribbon Chart,Actual: solid line #0080FF. Forecast: dashed #FF9500. Band: light shading,⚡ Good,✓ Clearly distinguish actual vs forecast. Add legend.,"Chart.js, ApexCharts, Plotly",Hover + Toggle
|
||||
10,Anomaly Detection,anomaly-detection,Line Chart with Highlights,Scatter with Alert,Normal: blue #0080FF. Anomaly: red #FF0000 circle/square marker + alert,⚡ Good,✓ Circle/marker for anomalies. Add text alert annotation.,"D3.js, Plotly, ApexCharts",Hover + Alert
|
||||
11,Hierarchical/Nested Data,hierarchical/nested-data,Treemap,"Sunburst, Nested Donut, Icicle",Parent: distinct hues. Children: lighter shades. White borders 2-3px.,⚠ Moderate,⚠ Poor - provide table alternative. Label large areas.,"D3.js, Recharts, ApexCharts",Hover + Drilldown
|
||||
12,Flow/Process Data,flow/process-data,Sankey Diagram,"Alluvial, Chord Diagram",Gradient from source to target. Opacity 0.4-0.6 for flows.,⚠ Moderate,⚠ Poor - provide flow table alternative.,"D3.js (d3-sankey), Plotly",Hover + Drilldown
|
||||
13,Cumulative Changes,cumulative-changes,Waterfall Chart,"Stacked Bar, Cascade",Increases: #4CAF50. Decreases: #F44336. Start: #2196F3. End: #0D47A1.,⚡ Good,✓ Good - clear directional colors with labels.,"ApexCharts, Highcharts, Plotly",Hover
|
||||
14,Multi-Variable Comparison,multi-variable-comparison,Radar/Spider Chart,"Parallel Coordinates, Grouped Bar",Single: #0080FF 20% fill. Multiple: distinct colors per dataset.,⚡ Good,⚠ Moderate - limit 5-8 axes. Add data table.,"Chart.js, Recharts, ApexCharts",Hover + Toggle
|
||||
15,Stock/Trading OHLC,stock/trading-ohlc,Candlestick Chart,"OHLC Bar, Heikin-Ashi",Bullish: #26A69A. Bearish: #EF5350. Volume: 40% opacity below.,⚡ Good,⚠ Moderate - provide OHLC data table.,"Lightweight Charts (TradingView), ApexCharts",Real-time + Hover + Zoom
|
||||
16,Relationship/Connection Data,relationship/connection-data,Network Graph,"Hierarchical Tree, Adjacency Matrix",Node types: categorical colors. Edges: #90A4AE 60% opacity.,❌ Poor (500+ nodes struggles),❌ Very Poor - provide adjacency list alternative.,"D3.js (d3-force), Vis.js, Cytoscape.js",Drilldown + Hover + Drag
|
||||
17,Distribution/Statistical,distribution/statistical,Box Plot,"Violin Plot, Beeswarm",Box: #BBDEFB. Border: #1976D2. Median: #D32F2F. Outliers: #F44336.,⚡ Excellent,"✓ Good - include stats table (min, Q1, median, Q3, max).","Plotly, D3.js, Chart.js (plugin)",Hover
|
||||
18,Performance vs Target (Compact),performance-vs-target-(compact),Bullet Chart,"Gauge, Progress Bar","Ranges: #FFCDD2, #FFF9C4, #C8E6C9. Performance: #1976D2. Target: black 3px.",⚡ Excellent,✓ Excellent - compact with clear values.,"D3.js, Plotly, Custom SVG",Hover
|
||||
19,Proportional/Percentage,proportional/percentage,Waffle Chart,"Pictogram, Stacked Bar 100%",10x10 grid. 3-5 categories max. 2-3px spacing between squares.,⚡ Good,✓ Good - better than pie for accessibility.,"D3.js, React-Waffle, Custom CSS Grid",Hover
|
||||
20,Hierarchical Proportional,hierarchical-proportional,Sunburst Chart,"Treemap, Icicle, Circle Packing",Center to outer: darker to lighter. 15-20% lighter per level.,⚠ Moderate,⚠ Poor - provide hierarchy table alternative.,"D3.js (d3-hierarchy), Recharts, ApexCharts",Drilldown + Hover
|
||||
21,Root Cause Analysis,"root cause, decomposition, tree, hierarchy, drill-down, ai-split",Decomposition Tree,"Decision Tree, Flow Chart",Nodes: #2563EB (Primary) vs #EF4444 (Negative impact). Connectors: Neutral grey.,⚠ Moderate (calculation heavy),✓ clear hierarchy. Allow keyboard navigation for nodes.,"Power BI (native), React-Flow, Custom D3.js",Drill + Expand
|
||||
22,3D Spatial Data,"3d, spatial, immersive, terrain, molecular, volumetric",3D Scatter/Surface Plot,"Volumetric Rendering, Point Cloud",Depth cues: lighting/shading. Z-axis: color gradient (cool to warm).,❌ Heavy (WebGL required),❌ Poor - requires alternative 2D view or data table.,"Three.js, Deck.gl, Plotly 3D",Rotate + Zoom + VR
|
||||
23,Real-Time Streaming,"streaming, real-time, ticker, live, velocity, pulse",Streaming Area Chart,"Ticker Tape, Moving Gauge",Current: Bright Pulse (#00FF00). History: Fading opacity. Grid: Dark.,⚡ Optimized (canvas/webgl),⚠ Flashing elements - provide pause button. High contrast.,Smoothed D3.js, CanvasJS, SciChart,Real-time + Pause
|
||||
24,Sentiment/Emotion,"sentiment, emotion, nlp, opinion, feeling",Word Cloud with Sentiment,"Sentiment Arc, Radar Chart",Positive: #22C55E. Negative: #EF4444. Neutral: #94A3B8. Size = Frequency.,⚡ Good,⚠ Word clouds poor for screen readers. Use list view.,"D3-cloud, Highcharts, Nivo",Hover + Filter
|
||||
25,Process Mining,"process, mining, variants, path, bottleneck, log",Process Map / Graph,"Directed Acyclic Graph (DAG), Petri Net",Happy path: #10B981 (Thick). Deviations: #F59E0B (Thin). Bottlenecks: #EF4444.,⚠ Moderate to Heavy,⚠ Complex graphs hard to navigate. Provide path summary.,"React-Flow, Cytoscape.js, Recharts",Drag + Node-Click
|
||||
|
Can't render this file because it has a wrong number of fields in line 24.
|
97
.claude/skills/ui-ux-pro-max/data/colors.csv
Normal file
97
.claude/skills/ui-ux-pro-max/data/colors.csv
Normal file
@ -0,0 +1,97 @@
|
||||
No,Product Type,Keywords,Primary (Hex),Secondary (Hex),CTA (Hex),Background (Hex),Text (Hex),Border (Hex),Notes
|
||||
1,SaaS (General),"saas, general",#2563EB,#3B82F6,#F97316,#F8FAFC,#1E293B,#E2E8F0,Trust blue + accent contrast
|
||||
2,Micro SaaS,"micro, saas",#2563EB,#3B82F6,#F97316,#F8FAFC,#1E293B,#E2E8F0,Vibrant primary + white space
|
||||
3,E-commerce,commerce,#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Brand primary + success green
|
||||
4,E-commerce Luxury,"commerce, luxury",#1C1917,#44403C,#CA8A04,#FAFAF9,#0C0A09,#D6D3D1,Premium colors + minimal accent
|
||||
5,Service Landing Page,"service, landing, page",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Brand primary + trust colors
|
||||
6,B2B Service,"b2b, service",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Professional blue + neutral grey
|
||||
7,Financial Dashboard,"financial, dashboard",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark bg + red/green alerts + trust blue
|
||||
8,Analytics Dashboard,"analytics, dashboard",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Cool→Hot gradients + neutral grey
|
||||
9,Healthcare App,"healthcare, app",#0891B2,#22D3EE,#059669,#ECFEFF,#164E63,#A5F3FC,Calm blue + health green + trust
|
||||
10,Educational App,"educational, app",#4F46E5,#818CF8,#F97316,#EEF2FF,#1E1B4B,#C7D2FE,Playful colors + clear hierarchy
|
||||
11,Creative Agency,"creative, agency",#EC4899,#F472B6,#06B6D4,#FDF2F8,#831843,#FBCFE8,Bold primaries + artistic freedom
|
||||
12,Portfolio/Personal,"portfolio, personal",#18181B,#3F3F46,#2563EB,#FAFAFA,#09090B,#E4E4E7,Brand primary + artistic interpretation
|
||||
13,Gaming,gaming,#7C3AED,#A78BFA,#F43F5E,#0F0F23,#E2E8F0,#4C1D95,Vibrant + neon + immersive colors
|
||||
14,Government/Public Service,"government, public, service",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Professional blue + high contrast
|
||||
15,Fintech/Crypto,"fintech, crypto",#F59E0B,#FBBF24,#8B5CF6,#0F172A,#F8FAFC,#334155,Dark tech colors + trust + vibrant accents
|
||||
16,Social Media App,"social, media, app",#2563EB,#60A5FA,#F43F5E,#F8FAFC,#1E293B,#DBEAFE,Vibrant + engagement colors
|
||||
17,Productivity Tool,"productivity, tool",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Clear hierarchy + functional colors
|
||||
18,Design System/Component Library,"design, system, component, library",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Clear hierarchy + code-like structure
|
||||
19,AI/Chatbot Platform,"chatbot, platform",#7C3AED,#A78BFA,#06B6D4,#FAF5FF,#1E1B4B,#DDD6FE,Neutral + AI Purple (#6366F1)
|
||||
20,NFT/Web3 Platform,"nft, web3, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark + Neon + Gold (#FFD700)
|
||||
21,Creator Economy Platform,"creator, economy, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Vibrant + Brand colors
|
||||
22,Sustainability/ESG Platform,"sustainability, esg, platform",#7C3AED,#A78BFA,#06B6D4,#FAF5FF,#1E1B4B,#DDD6FE,Green (#228B22) + Earth tones
|
||||
23,Remote Work/Collaboration Tool,"remote, work, collaboration, tool",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Calm Blue + Neutral grey
|
||||
24,Mental Health App,"mental, health, app",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Calm Pastels + Trust colors
|
||||
25,Pet Tech App,"pet, tech, app",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Playful + Warm colors
|
||||
26,Smart Home/IoT Dashboard,"smart, home, iot, dashboard",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark + Status indicator colors
|
||||
27,EV/Charging Ecosystem,"charging, ecosystem",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Electric Blue (#009CD1) + Green
|
||||
28,Subscription Box Service,"subscription, box, service",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Brand + Excitement colors
|
||||
29,Podcast Platform,"podcast, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark + Audio waveform accents
|
||||
30,Dating App,"dating, app",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Warm + Romantic (Pink/Red gradients)
|
||||
31,Micro-Credentials/Badges Platform,"micro, credentials, badges, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Trust Blue + Gold (#FFD700)
|
||||
32,Knowledge Base/Documentation,"knowledge, base, documentation",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Clean hierarchy + minimal color
|
||||
33,Hyperlocal Services,"hyperlocal, services",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Location markers + Trust colors
|
||||
34,Beauty/Spa/Wellness Service,"beauty, spa, wellness, service",#10B981,#34D399,#8B5CF6,#ECFDF5,#064E3B,#A7F3D0,Soft pastels (Pink #FFB6C1 Sage #90EE90) + Cream + Gold accents
|
||||
35,Luxury/Premium Brand,"luxury, premium, brand",#1C1917,#44403C,#CA8A04,#FAFAF9,#0C0A09,#D6D3D1,Black + Gold (#FFD700) + White + Minimal accent
|
||||
36,Restaurant/Food Service,"restaurant, food, service",#DC2626,#F87171,#CA8A04,#FEF2F2,#450A0A,#FECACA,Warm colors (Orange Red Brown) + appetizing imagery
|
||||
37,Fitness/Gym App,"fitness, gym, app",#DC2626,#F87171,#16A34A,#FEF2F2,#1F2937,#FECACA,Energetic (Orange #FF6B35 Electric Blue) + Dark bg
|
||||
38,Real Estate/Property,"real, estate, property",#0F766E,#14B8A6,#0369A1,#F0FDFA,#134E4A,#99F6E4,Trust Blue (#0077B6) + Gold accents + White
|
||||
39,Travel/Tourism Agency,"travel, tourism, agency",#EC4899,#F472B6,#06B6D4,#FDF2F8,#831843,#FBCFE8,Vibrant destination colors + Sky Blue + Warm accents
|
||||
40,Hotel/Hospitality,"hotel, hospitality",#1E3A8A,#3B82F6,#CA8A04,#F8FAFC,#1E40AF,#BFDBFE,Warm neutrals + Gold (#D4AF37) + Brand accent
|
||||
41,Wedding/Event Planning,"wedding, event, planning",#7C3AED,#A78BFA,#F97316,#FAF5FF,#4C1D95,#DDD6FE,Soft Pink (#FFD6E0) + Gold + Cream + Sage
|
||||
42,Legal Services,"legal, services",#1E3A8A,#1E40AF,#B45309,#F8FAFC,#0F172A,#CBD5E1,Navy Blue (#1E3A5F) + Gold + White
|
||||
43,Insurance Platform,"insurance, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Trust Blue (#0066CC) + Green (security) + Neutral
|
||||
44,Banking/Traditional Finance,"banking, traditional, finance",#0F766E,#14B8A6,#0369A1,#F0FDFA,#134E4A,#99F6E4,Navy (#0A1628) + Trust Blue + Gold accents
|
||||
45,Online Course/E-learning,"online, course, learning",#0D9488,#2DD4BF,#EA580C,#F0FDFA,#134E4A,#5EEAD4,Vibrant learning colors + Progress green
|
||||
46,Non-profit/Charity,"non, profit, charity",#0891B2,#22D3EE,#F97316,#ECFEFF,#164E63,#A5F3FC,Cause-related colors + Trust + Warm
|
||||
47,Music Streaming,"music, streaming",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark (#121212) + Vibrant accents + Album art colors
|
||||
48,Video Streaming/OTT,"video, streaming, ott",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark bg + Content poster colors + Brand accent
|
||||
49,Job Board/Recruitment,"job, board, recruitment",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Professional Blue + Success Green + Neutral
|
||||
50,Marketplace (P2P),"marketplace, p2p",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Trust colors + Category colors + Success green
|
||||
51,Logistics/Delivery,"logistics, delivery",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Blue (#2563EB) + Orange (tracking) + Green (delivered)
|
||||
52,Agriculture/Farm Tech,"agriculture, farm, tech",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Earth Green (#4A7C23) + Brown + Sky Blue
|
||||
53,Construction/Architecture,"construction, architecture",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Grey (#4A4A4A) + Orange (safety) + Blueprint Blue
|
||||
54,Automotive/Car Dealership,"automotive, car, dealership",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Brand colors + Metallic accents + Dark/Light
|
||||
55,Photography Studio,"photography, studio",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Black + White + Minimal accent
|
||||
56,Coworking Space,"coworking, space",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Energetic colors + Wood tones + Brand accent
|
||||
57,Cleaning Service,"cleaning, service",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Fresh Blue (#00B4D8) + Clean White + Green
|
||||
58,Home Services (Plumber/Electrician),"home, services, plumber, electrician",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Trust Blue + Safety Orange + Professional grey
|
||||
59,Childcare/Daycare,"childcare, daycare",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Playful pastels + Safe colors + Warm accents
|
||||
60,Senior Care/Elderly,"senior, care, elderly",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Calm Blue + Warm neutrals + Large text
|
||||
61,Medical Clinic,"medical, clinic",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Medical Blue (#0077B6) + Trust White + Calm Green
|
||||
62,Pharmacy/Drug Store,"pharmacy, drug, store",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Pharmacy Green + Trust Blue + Clean White
|
||||
63,Dental Practice,"dental, practice",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Fresh Blue + White + Smile Yellow accent
|
||||
64,Veterinary Clinic,"veterinary, clinic",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Caring Blue + Pet-friendly colors + Warm accents
|
||||
65,Florist/Plant Shop,"florist, plant, shop",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Natural Green + Floral pinks/purples + Earth tones
|
||||
66,Bakery/Cafe,"bakery, cafe",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Warm Brown + Cream + Appetizing accents
|
||||
67,Coffee Shop,"coffee, shop",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Coffee Brown (#6F4E37) + Cream + Warm accents
|
||||
68,Brewery/Winery,"brewery, winery",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Deep amber/burgundy + Gold + Craft aesthetic
|
||||
69,Airline,airline,#7C3AED,#A78BFA,#06B6D4,#FAF5FF,#1E1B4B,#DDD6FE,Sky Blue + Brand colors + Trust accents
|
||||
70,News/Media Platform,"news, media, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Brand colors + High contrast + Category colors
|
||||
71,Magazine/Blog,"magazine, blog",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Editorial colors + Brand primary + Clean white
|
||||
72,Freelancer Platform,"freelancer, platform",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Professional Blue + Success Green + Neutral
|
||||
73,Consulting Firm,"consulting, firm",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Navy + Gold + Professional grey
|
||||
74,Marketing Agency,"marketing, agency",#EC4899,#F472B6,#06B6D4,#FDF2F8,#831843,#FBCFE8,Bold brand colors + Creative freedom
|
||||
75,Event Management,"event, management",#7C3AED,#A78BFA,#F97316,#FAF5FF,#4C1D95,#DDD6FE,Event theme colors + Excitement accents
|
||||
76,Conference/Webinar Platform,"conference, webinar, platform",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Professional Blue + Video accent + Brand
|
||||
77,Membership/Community,"membership, community",#7C3AED,#A78BFA,#F97316,#FAF5FF,#4C1D95,#DDD6FE,Community brand colors + Engagement accents
|
||||
78,Newsletter Platform,"newsletter, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Brand primary + Clean white + CTA accent
|
||||
79,Digital Products/Downloads,"digital, products, downloads",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Product category colors + Brand + Success green
|
||||
80,Church/Religious Organization,"church, religious, organization",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Warm Gold + Deep Purple/Blue + White
|
||||
81,Sports Team/Club,"sports, team, club",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Team colors + Energetic accents
|
||||
82,Museum/Gallery,"museum, gallery",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Art-appropriate neutrals + Exhibition accents
|
||||
83,Theater/Cinema,"theater, cinema",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark + Spotlight accents + Gold
|
||||
84,Language Learning App,"language, learning, app",#0D9488,#2DD4BF,#EA580C,#F0FDFA,#134E4A,#5EEAD4,Playful colors + Progress indicators + Country flags
|
||||
85,Coding Bootcamp,"coding, bootcamp",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Code editor colors + Brand + Success green
|
||||
86,Cybersecurity Platform,"cybersecurity, security, cyber, hacker",#00FF41,#0D0D0D,#00FF41,#000000,#E0E0E0,#1F1F1F,Matrix Green + Deep Black + Terminal feel
|
||||
87,Developer Tool / IDE,"developer, tool, ide, code, dev",#3B82F6,#1E293B,#2563EB,#0F172A,#F1F5F9,#334155,Dark syntax theme colors + Blue focus
|
||||
88,Biotech / Life Sciences,"biotech, science, biology, medical",#0EA5E9,#0284C7,#10B981,#F8FAFC,#0F172A,#E2E8F0,Sterile White + DNA Blue + Life Green
|
||||
89,Space Tech / Aerospace,"space, aerospace, tech, futuristic",#FFFFFF,#94A3B8,#3B82F6,#0B0B10,#F8FAFC,#1E293B,Deep Space Black + Star White + Metallic
|
||||
90,Architecture / Interior,"architecture, interior, design, luxury",#171717,#404040,#D4AF37,#FFFFFF,#171717,#E5E5E5,Monochrome + Gold Accent + High Imagery
|
||||
91,Quantum Computing,"quantum, qubit, tech",#00FFFF,#7B61FF,#FF00FF,#050510,#E0E0FF,#333344,Interference patterns + Neon + Deep Dark
|
||||
92,Biohacking / Longevity,"bio, health, science",#FF4D4D,#4D94FF,#00E676,#F5F5F7,#1C1C1E,#E5E5EA,Biological red/blue + Clinical white
|
||||
93,Autonomous Systems,"drone, robot, fleet",#00FF41,#008F11,#FF3333,#0D1117,#E6EDF3,#30363D,Terminal Green + Tactical Dark
|
||||
94,Generative AI Art,"art, gen-ai, creative",#111111,#333333,#FFFFFF,#FAFAFA,#000000,#E5E5E5,Canvas Neutral + High Contrast
|
||||
95,Spatial / Vision OS,"spatial, glass, vision",#FFFFFF,#E5E5E5,#007AFF,#888888,#000000,#FFFFFF,Glass opacity 20% + System Blue
|
||||
96,Climate Tech,"climate, green, energy",#2E8B57,#87CEEB,#FFD700,#F0FFF4,#1A3320,#C6E6C6,Nature Green + Solar Yellow + Air Blue
|
||||
|
31
.claude/skills/ui-ux-pro-max/data/landing.csv
Normal file
31
.claude/skills/ui-ux-pro-max/data/landing.csv
Normal file
@ -0,0 +1,31 @@
|
||||
No,Pattern Name,Keywords,Section Order,Primary CTA Placement,Color Strategy,Recommended Effects,Conversion Optimization
|
||||
1,Hero + Features + CTA,"hero, hero-centric, features, feature-rich, cta, call-to-action","1. Hero with headline/image, 2. Value prop, 3. Key features (3-5), 4. CTA section, 5. Footer",Hero (sticky) + Bottom,Hero: Brand primary or vibrant. Features: Card bg #FAFAFA. CTA: Contrasting accent color,"Hero parallax, feature card hover lift, CTA glow on hover",Deep CTA placement. Use contrasting color (at least 7:1 contrast ratio). Sticky navbar CTA.
|
||||
2,Hero + Testimonials + CTA,"hero, testimonials, social-proof, trust, reviews, cta","1. Hero, 2. Problem statement, 3. Solution overview, 4. Testimonials carousel, 5. CTA",Hero (sticky) + Post-testimonials,"Hero: Brand color. Testimonials: Light bg #F5F5F5. Quotes: Italic, muted color #666. CTA: Vibrant","Testimonial carousel slide animations, quote marks animations, avatar fade-in",Social proof before CTA. Use 3-5 testimonials. Include photo + name + role. CTA after social proof.
|
||||
3,Product Demo + Features,"demo, product-demo, features, showcase, interactive","1. Hero, 2. Product video/mockup (center), 3. Feature breakdown per section, 4. Comparison (optional), 5. CTA",Video center + CTA right/bottom,Video surround: Brand color overlay. Features: Icon color #0080FF. Text: Dark #222,"Video play button pulse, feature scroll reveals, demo interaction highlights",Embedded product demo increases engagement. Use interactive mockup if possible. Auto-play video muted.
|
||||
4,Minimal Single Column,"minimal, simple, direct, single-column, clean","1. Hero headline, 2. Short description, 3. Benefit bullets (3 max), 4. CTA, 5. Footer","Center, large CTA button",Minimalist: Brand + white #FFFFFF + accent. Buttons: High contrast 7:1+. Text: Black/Dark grey,Minimal hover effects. Smooth scroll. CTA scale on hover (subtle),Single CTA focus. Large typography. Lots of whitespace. No nav clutter. Mobile-first.
|
||||
5,Funnel (3-Step Conversion),"funnel, conversion, steps, wizard, onboarding","1. Hero, 2. Step 1 (problem), 3. Step 2 (solution), 4. Step 3 (action), 5. CTA progression",Each step: mini-CTA. Final: main CTA,"Step colors: 1 (Red/Problem), 2 (Orange/Process), 3 (Green/Solution). CTA: Brand color","Step number animations, progress bar fill, step transitions smooth scroll",Progressive disclosure. Show only essential info per step. Use progress indicators. Multiple CTAs.
|
||||
6,Comparison Table + CTA,"comparison, table, compare, versus, cta","1. Hero, 2. Problem intro, 3. Comparison table (product vs competitors), 4. Pricing (optional), 5. CTA",Table: Right column. CTA: Below table,Table: Alternating rows (white/light grey). Your product: Highlight #FFFACD (light yellow) or green. Text: Dark,"Table row hover highlight, price toggle animations, feature checkmark animations",Use comparison to show unique value. Highlight your product row. Include 'free trial' in pricing row.
|
||||
7,Lead Magnet + Form,"lead, form, signup, capture, email, magnet","1. Hero (benefit headline), 2. Lead magnet preview (ebook cover, checklist, etc), 3. Form (minimal fields), 4. CTA submit",Form CTA: Submit button,Lead magnet: Professional design. Form: Clean white bg. Inputs: Light border #CCCCCC. CTA: Brand color,"Form focus state animations, input validation animations, success confirmation animation",Form fields ≤ 3 for best conversion. Offer valuable lead magnet preview. Show form submission progress.
|
||||
8,Pricing Page + CTA,"pricing, plans, tiers, comparison, cta","1. Hero (pricing headline), 2. Price comparison cards, 3. Feature comparison table, 4. FAQ section, 5. Final CTA",Each card: CTA button. Sticky CTA in nav,"Free: Grey, Starter: Blue, Pro: Green/Gold, Enterprise: Dark. Cards: 1px border, shadow","Price toggle animation (monthly/yearly), card comparison highlight, FAQ accordion open/close",Recommend starter plan (pre-select/highlight). Show annual discount (20-30%). Use FAQs to address concerns.
|
||||
9,Video-First Hero,"video, hero, media, visual, engaging","1. Hero with video background, 2. Key features overlay, 3. Benefits section, 4. CTA",Overlay on video (center/bottom) + Bottom section,Dark overlay 60% on video. Brand accent for CTA. White text on dark.,"Video autoplay muted, parallax scroll, text fade-in on scroll",86% higher engagement with video. Add captions for accessibility. Compress video for performance.
|
||||
10,Scroll-Triggered Storytelling,"storytelling, scroll, narrative, story, immersive","1. Intro hook, 2. Chapter 1 (problem), 3. Chapter 2 (journey), 4. Chapter 3 (solution), 5. Climax CTA",End of each chapter (mini) + Final climax CTA,Progressive reveal. Each chapter has distinct color. Building intensity.,"ScrollTrigger animations, parallax layers, progressive disclosure, chapter transitions",Narrative increases time-on-page 3x. Use progress indicator. Mobile: simplify animations.
|
||||
11,AI Personalization Landing,"ai, personalization, smart, recommendation, dynamic","1. Dynamic hero (personalized), 2. Relevant features, 3. Tailored testimonials, 4. Smart CTA",Context-aware placement based on user segment,Adaptive based on user data. A/B test color variations per segment.,"Dynamic content swap, fade transitions, personalized product recommendations",20%+ conversion with personalization. Requires analytics integration. Fallback for new users.
|
||||
12,Waitlist/Coming Soon,"waitlist, coming-soon, launch, early-access, notify","1. Hero with countdown, 2. Product teaser/preview, 3. Email capture form, 4. Social proof (waitlist count)",Email form prominent (above fold) + Sticky form on scroll,Anticipation: Dark + accent highlights. Countdown in brand color. Urgency indicators.,"Countdown timer animation, email validation feedback, success confetti, social share buttons",Scarcity + exclusivity. Show waitlist count. Early access benefits. Referral program.
|
||||
13,Comparison Table Focus,"comparison, table, versus, compare, features","1. Hero (problem statement), 2. Comparison matrix (you vs competitors), 3. Feature deep-dive, 4. Winner CTA",After comparison table (highlighted row) + Bottom,Your product column highlighted (accent bg or green). Competitors neutral. Checkmarks green.,"Table row hover highlight, feature checkmark animations, sticky comparison header",Show value vs competitors. 35% higher conversion. Be factual. Include pricing if favorable.
|
||||
14,Pricing-Focused Landing,"pricing, price, cost, plans, subscription","1. Hero (value proposition), 2. Pricing cards (3 tiers), 3. Feature comparison, 4. FAQ, 5. Final CTA",Each pricing card + Sticky CTA in nav + Bottom,Popular plan highlighted (brand color border/bg). Free: grey. Enterprise: dark/premium.,"Price toggle monthly/annual animation, card hover lift, FAQ accordion smooth open",Annual discount 20-30%. Recommend mid-tier (most popular badge). Address objections in FAQ.
|
||||
15,App Store Style Landing,"app, mobile, download, store, install","1. Hero with device mockup, 2. Screenshots carousel, 3. Features with icons, 4. Reviews/ratings, 5. Download CTAs",Download buttons prominent (App Store + Play Store) throughout,Dark/light matching app store feel. Star ratings in gold. Screenshots with device frames.,"Device mockup rotations, screenshot slider, star rating animations, download button pulse",Show real screenshots. Include ratings (4.5+ stars). QR code for mobile. Platform-specific CTAs.
|
||||
16,FAQ/Documentation Landing,"faq, documentation, help, support, questions","1. Hero with search bar, 2. Popular categories, 3. FAQ accordion, 4. Contact/support CTA",Search bar prominent + Contact CTA for unresolved questions,"Clean, high readability. Minimal color. Category icons in brand color. Success green for resolved.","Search autocomplete, smooth accordion open/close, category hover, helpful feedback buttons",Reduce support tickets. Track search analytics. Show related articles. Contact escalation path.
|
||||
17,Immersive/Interactive Experience,"immersive, interactive, experience, 3d, animation","1. Full-screen interactive element, 2. Guided product tour, 3. Key benefits revealed, 4. CTA after completion",After interaction complete + Skip option for impatient users,Immersive experience colors. Dark background for focus. Highlight interactive elements.,"WebGL, 3D interactions, gamification elements, progress indicators, reward animations",40% higher engagement. Performance trade-off. Provide skip option. Mobile fallback essential.
|
||||
18,Event/Conference Landing,"event, conference, meetup, registration, schedule","1. Hero (date/location/countdown), 2. Speakers grid, 3. Agenda/schedule, 4. Sponsors, 5. Register CTA",Register CTA sticky + After speakers + Bottom,Urgency colors (countdown). Event branding. Speaker cards professional. Sponsor logos neutral.,"Countdown timer, speaker hover cards with bio, agenda tabs, early bird countdown",Early bird pricing with deadline. Social proof (past attendees). Speaker credibility. Multi-ticket discounts.
|
||||
19,Product Review/Ratings Focused,"reviews, ratings, testimonials, social-proof, stars","1. Hero (product + aggregate rating), 2. Rating breakdown, 3. Individual reviews, 4. Buy/CTA",After reviews summary + Buy button alongside reviews,Trust colors. Star ratings gold. Verified badge green. Review sentiment colors.,"Star fill animations, review filtering, helpful vote interactions, photo lightbox",User-generated content builds trust. Show verified purchases. Filter by rating. Respond to negative reviews.
|
||||
20,Community/Forum Landing,"community, forum, social, members, discussion","1. Hero (community value prop), 2. Popular topics/categories, 3. Active members showcase, 4. Join CTA",Join button prominent + After member showcase,"Warm, welcoming. Member photos add humanity. Topic badges in brand colors. Activity indicators green.","Member avatars animation, activity feed live updates, topic hover previews, join success celebration","Show active community (member count, posts today). Highlight benefits. Preview content. Easy onboarding."
|
||||
21,Before-After Transformation,"before-after, transformation, results, comparison","1. Hero (problem state), 2. Transformation slider/comparison, 3. How it works, 4. Results CTA",After transformation reveal + Bottom,Contrast: muted/grey (before) vs vibrant/colorful (after). Success green for results.,"Slider comparison interaction, before/after reveal animations, result counters, testimonial videos",Visual proof of value. 45% higher conversion. Real results. Specific metrics. Guarantee offer.
|
||||
22,Marketplace / Directory,"marketplace, directory, search, listing","1. Hero (Search focused), 2. Categories, 3. Featured Listings, 4. Trust/Safety, 5. CTA (Become a host/seller)",Hero Search Bar + Navbar 'List your item',Search: High contrast. Categories: Visual icons. Trust: Blue/Green.,Search autocomplete animation, map hover pins, card carousel,Search bar is the CTA. Reduce friction to search. Popular searches suggestions.
|
||||
23,Newsletter / Content First,"newsletter, content, writer, blog, subscribe","1. Hero (Value Prop + Form), 2. Recent Issues/Archives, 3. Social Proof (Subscriber count), 4. About Author",Hero inline form + Sticky header form,Minimalist. Paper-like background. Text focus. Accent color for Subscribe.,Text highlight animations, typewriter effect, subtle fade-in,Single field form (Email only). Show 'Join X,000 readers'. Read sample link.
|
||||
24,Webinar Registration,"webinar, registration, event, training, live","1. Hero (Topic + Timer + Form), 2. What you'll learn, 3. Speaker Bio, 4. Urgency/Bonuses, 5. Form (again)",Hero (Right side form) + Bottom anchor,Urgency: Red/Orange. Professional: Blue/Navy. Form: High contrast white.,Countdown timer, speaker avatar float, urgent ticker,Limited seats logic. 'Live' indicator. Auto-fill timezone.
|
||||
25,Enterprise Gateway,"enterprise, corporate, gateway, solutions, portal","1. Hero (Video/Mission), 2. Solutions by Industry, 3. Solutions by Role, 4. Client Logos, 5. Contact Sales",Contact Sales (Primary) + Login (Secondary),Corporate: Navy/Grey. High integrity. Conservative accents.,Slow video background, logo carousel, tab switching for industries,Path selection (I am a...). Mega menu navigation. Trust signals prominent.
|
||||
26,Portfolio Grid,"portfolio, grid, showcase, gallery, masonry","1. Hero (Name/Role), 2. Project Grid (Masonry), 3. About/Philosophy, 4. Contact",Project Card Hover + Footer Contact,Neutral background (let work shine). Text: Black/White. Accent: Minimal.,Image lazy load reveal, hover overlay info, lightbox view,Visuals first. Filter by category. Fast loading essential.
|
||||
27,Horizontal Scroll Journey,"horizontal, scroll, journey, gallery, storytelling, panoramic","1. Intro (Vertical), 2. The Journey (Horizontal Track), 3. Detail Reveal, 4. Vertical Footer","Floating Sticky CTA or End of Horizontal Track","Continuous palette transition. Chapter colors. Progress bar #000000.","Scroll-jacking (careful), parallax layers, horizontal slide, progress indicator","Immersive product discovery. High engagement. Keep navigation visible.
|
||||
28,Bento Grid Showcase,"bento, grid, features, modular, apple-style, showcase","1. Hero, 2. Bento Grid (Key Features), 3. Detail Cards, 4. Tech Specs, 5. CTA","Floating Action Button or Bottom of Grid","Card backgrounds: #F5F5F7 or Glass. Icons: Vibrant brand colors. Text: Dark.","Hover card scale (1.02), video inside cards, tilt effect, staggered reveal","Scannable value props. High information density without clutter. Mobile stack.
|
||||
29,Interactive 3D Configurator,"3d, configurator, customizer, interactive, product","1. Hero (Configurator), 2. Feature Highlight (synced), 3. Price/Specs, 4. Purchase","Inside Configurator UI + Sticky Bottom Bar","Neutral studio background. Product: Realistic materials. UI: Minimal overlay.","Real-time rendering, material swap animation, camera rotate/zoom, light reflection","Increases ownership feeling. 360 view reduces return rates. Direct add-to-cart.
|
||||
30,AI-Driven Dynamic Landing,"ai, dynamic, personalized, adaptive, generative","1. Prompt/Input Hero, 2. Generated Result Preview, 3. How it Works, 4. Value Prop","Input Field (Hero) + 'Try it' Buttons","Adaptive to user input. Dark mode for compute feel. Neon accents.","Typing text effects, shimmering generation loaders, morphing layouts","Immediate value demonstration. 'Show, don't tell'. Low friction start.
|
||||
|
Can't render this file because it has a wrong number of fields in line 23.
|
97
.claude/skills/ui-ux-pro-max/data/products.csv
Normal file
97
.claude/skills/ui-ux-pro-max/data/products.csv
Normal file
@ -0,0 +1,97 @@
|
||||
No,Product Type,Keywords,Primary Style Recommendation,Secondary Styles,Landing Page Pattern,Dashboard Style (if applicable),Color Palette Focus,Key Considerations
|
||||
1,SaaS (General),"app, b2b, cloud, general, saas, software, subscription",Glassmorphism + Flat Design,"Soft UI Evolution, Minimalism",Hero + Features + CTA,Data-Dense + Real-Time Monitoring,Trust blue + accent contrast,Balance modern feel with clarity. Focus on CTAs.
|
||||
2,Micro SaaS,"app, b2b, cloud, indie, micro, micro-saas, niche, saas, small, software, solo, subscription",Flat Design + Vibrant & Block,"Motion-Driven, Micro-interactions",Minimal & Direct + Demo,Executive Dashboard,Vibrant primary + white space,"Keep simple, show product quickly. Speed is key."
|
||||
3,E-commerce,"buy, commerce, e, ecommerce, products, retail, sell, shop, store",Vibrant & Block-based,"Aurora UI, Motion-Driven",Feature-Rich Showcase,Sales Intelligence Dashboard,Brand primary + success green,Engagement & conversions. High visual hierarchy.
|
||||
4,E-commerce Luxury,"buy, commerce, e, ecommerce, elegant, exclusive, high-end, luxury, premium, products, retail, sell, shop, store",Liquid Glass + Glassmorphism,"3D & Hyperrealism, Aurora UI",Feature-Rich Showcase,Sales Intelligence Dashboard,Premium colors + minimal accent,Elegance & sophistication. Premium materials.
|
||||
5,Service Landing Page,"appointment, booking, consultation, conversion, landing, marketing, page, service",Hero-Centric + Trust & Authority,"Social Proof-Focused, Storytelling",Hero-Centric Design,N/A - Analytics for conversions,Brand primary + trust colors,Social proof essential. Show expertise.
|
||||
6,B2B Service,"appointment, b, b2b, booking, business, consultation, corporate, enterprise, service",Trust & Authority + Minimal,"Feature-Rich, Conversion-Optimized",Feature-Rich Showcase,Sales Intelligence Dashboard,Professional blue + neutral grey,Credibility essential. Clear ROI messaging.
|
||||
7,Financial Dashboard,"admin, analytics, dashboard, data, financial, panel",Dark Mode (OLED) + Data-Dense,"Minimalism, Accessible & Ethical",N/A - Dashboard focused,Financial Dashboard,Dark bg + red/green alerts + trust blue,"High contrast, real-time updates, accuracy paramount."
|
||||
8,Analytics Dashboard,"admin, analytics, dashboard, data, panel",Data-Dense + Heat Map & Heatmap,"Minimalism, Dark Mode (OLED)",N/A - Analytics focused,Drill-Down Analytics + Comparative,Cool→Hot gradients + neutral grey,Clarity > aesthetics. Color-coded data priority.
|
||||
9,Healthcare App,"app, clinic, health, healthcare, medical, patient",Neumorphism + Accessible & Ethical,"Soft UI Evolution, Claymorphism (for patients)",Social Proof-Focused,User Behavior Analytics,Calm blue + health green + trust,Accessibility mandatory. Calming aesthetic.
|
||||
10,Educational App,"app, course, education, educational, learning, school, training",Claymorphism + Micro-interactions,"Vibrant & Block-based, Flat Design",Storytelling-Driven,User Behavior Analytics,Playful colors + clear hierarchy,Engagement & ease of use. Age-appropriate design.
|
||||
11,Creative Agency,"agency, creative, design, marketing, studio",Brutalism + Motion-Driven,"Retro-Futurism, Storytelling-Driven",Storytelling-Driven,N/A - Portfolio focused,Bold primaries + artistic freedom,Differentiation key. Wow-factor necessary.
|
||||
12,Portfolio/Personal,"creative, personal, portfolio, projects, showcase, work",Motion-Driven + Minimalism,"Brutalism, Aurora UI",Storytelling-Driven,N/A - Personal branding,Brand primary + artistic interpretation,Showcase work. Personality shine through.
|
||||
13,Gaming,"entertainment, esports, game, gaming, play",3D & Hyperrealism + Retro-Futurism,"Motion-Driven, Vibrant & Block",Feature-Rich Showcase,N/A - Game focused,Vibrant + neon + immersive colors,Immersion priority. Performance critical.
|
||||
14,Government/Public Service,"appointment, booking, consultation, government, public, service",Accessible & Ethical + Minimalism,"Flat Design, Inclusive Design",Minimal & Direct,Executive Dashboard,Professional blue + high contrast,WCAG AAA mandatory. Trust paramount.
|
||||
15,Fintech/Crypto,"banking, blockchain, crypto, defi, finance, fintech, money, nft, payment, web3",Glassmorphism + Dark Mode (OLED),"Retro-Futurism, Motion-Driven",Conversion-Optimized,Real-Time Monitoring + Predictive,Dark tech colors + trust + vibrant accents,Security perception. Real-time data critical.
|
||||
16,Social Media App,"app, community, content, entertainment, media, network, sharing, social, streaming, users, video",Vibrant & Block-based + Motion-Driven,"Aurora UI, Micro-interactions",Feature-Rich Showcase,User Behavior Analytics,Vibrant + engagement colors,Engagement & retention. Addictive design ethics.
|
||||
17,Productivity Tool,"collaboration, productivity, project, task, tool, workflow",Flat Design + Micro-interactions,"Minimalism, Soft UI Evolution",Interactive Product Demo,Drill-Down Analytics,Clear hierarchy + functional colors,Ease of use. Speed & efficiency focus.
|
||||
18,Design System/Component Library,"component, design, library, system",Minimalism + Accessible & Ethical,"Flat Design, Zero Interface",Feature-Rich Showcase,N/A - Dev focused,Clear hierarchy + code-like structure,Consistency. Developer-first approach.
|
||||
19,AI/Chatbot Platform,"ai, artificial-intelligence, automation, chatbot, machine-learning, ml, platform",AI-Native UI + Minimalism,"Zero Interface, Glassmorphism",Interactive Product Demo,AI/ML Analytics Dashboard,Neutral + AI Purple (#6366F1),Conversational UI. Streaming text. Context awareness. Minimal chrome.
|
||||
20,NFT/Web3 Platform,"nft, platform, web",Cyberpunk UI + Glassmorphism,"Aurora UI, 3D & Hyperrealism",Feature-Rich Showcase,Crypto/Blockchain Dashboard,Dark + Neon + Gold (#FFD700),Wallet integration. Transaction feedback. Gas fees display. Dark mode essential.
|
||||
21,Creator Economy Platform,"creator, economy, platform",Vibrant & Block-based + Bento Box Grid,"Motion-Driven, Aurora UI",Social Proof-Focused,User Behavior Analytics,Vibrant + Brand colors,Creator profiles. Monetization display. Engagement metrics. Social proof.
|
||||
22,Sustainability/ESG Platform,"ai, artificial-intelligence, automation, esg, machine-learning, ml, platform, sustainability",Organic Biophilic + Minimalism,"Accessible & Ethical, Flat Design",Trust & Authority,Energy/Utilities Dashboard,Green (#228B22) + Earth tones,Carbon footprint visuals. Progress indicators. Certification badges. Eco-friendly imagery.
|
||||
23,Remote Work/Collaboration Tool,"collaboration, remote, tool, work",Soft UI Evolution + Minimalism,"Glassmorphism, Micro-interactions",Feature-Rich Showcase,Drill-Down Analytics,Calm Blue + Neutral grey,Real-time collaboration. Status indicators. Video integration. Notification management.
|
||||
24,Mental Health App,"app, health, mental",Neumorphism + Accessible & Ethical,"Claymorphism, Soft UI Evolution",Social Proof-Focused,Healthcare Analytics,Calm Pastels + Trust colors,Calming aesthetics. Privacy-first. Crisis resources. Progress tracking. Accessibility mandatory.
|
||||
25,Pet Tech App,"app, pet, tech",Claymorphism + Vibrant & Block-based,"Micro-interactions, Flat Design",Storytelling-Driven,User Behavior Analytics,Playful + Warm colors,Pet profiles. Health tracking. Playful UI. Photo galleries. Vet integration.
|
||||
26,Smart Home/IoT Dashboard,"admin, analytics, dashboard, data, home, iot, panel, smart",Glassmorphism + Dark Mode (OLED),"Minimalism, AI-Native UI",Interactive Product Demo,Real-Time Monitoring,Dark + Status indicator colors,Device status. Real-time controls. Energy monitoring. Automation rules. Quick actions.
|
||||
27,EV/Charging Ecosystem,"charging, ecosystem, ev",Minimalism + Aurora UI,"Glassmorphism, Organic Biophilic",Hero-Centric Design,Energy/Utilities Dashboard,Electric Blue (#009CD1) + Green,Charging station maps. Range estimation. Cost calculation. Environmental impact.
|
||||
28,Subscription Box Service,"appointment, booking, box, consultation, membership, plan, recurring, service, subscription",Vibrant & Block-based + Motion-Driven,"Claymorphism, Aurora UI",Feature-Rich Showcase,E-commerce Analytics,Brand + Excitement colors,Unboxing experience. Personalization quiz. Subscription management. Product reveals.
|
||||
29,Podcast Platform,"platform, podcast",Dark Mode (OLED) + Minimalism,"Motion-Driven, Vibrant & Block-based",Storytelling-Driven,Media/Entertainment Dashboard,Dark + Audio waveform accents,Audio player UX. Episode discovery. Creator tools. Analytics for podcasters.
|
||||
30,Dating App,"app, dating",Vibrant & Block-based + Motion-Driven,"Aurora UI, Glassmorphism",Social Proof-Focused,User Behavior Analytics,Warm + Romantic (Pink/Red gradients),Profile cards. Swipe interactions. Match animations. Safety features. Video chat.
|
||||
31,Micro-Credentials/Badges Platform,"badges, credentials, micro, platform",Minimalism + Flat Design,"Accessible & Ethical, Swiss Modernism 2.0",Trust & Authority,Education Dashboard,Trust Blue + Gold (#FFD700),Credential verification. Badge display. Progress tracking. Issuer trust. LinkedIn integration.
|
||||
32,Knowledge Base/Documentation,"base, documentation, knowledge",Minimalism + Accessible & Ethical,"Swiss Modernism 2.0, Flat Design",FAQ/Documentation,N/A - Documentation focused,Clean hierarchy + minimal color,Search-first. Clear navigation. Code highlighting. Version switching. Feedback system.
|
||||
33,Hyperlocal Services,"appointment, booking, consultation, hyperlocal, service, services",Minimalism + Vibrant & Block-based,"Micro-interactions, Flat Design",Conversion-Optimized,Drill-Down Analytics + Map,Location markers + Trust colors,Map integration. Service categories. Provider profiles. Booking system. Reviews.
|
||||
34,Beauty/Spa/Wellness Service,"appointment, beauty, booking, consultation, service, spa, wellness",Soft UI Evolution + Neumorphism,"Glassmorphism, Minimalism",Hero-Centric Design + Social Proof,User Behavior Analytics,Soft pastels (Pink #FFB6C1 Sage #90EE90) + Cream + Gold accents,Calming aesthetic. Booking system. Service menu. Before/after gallery. Testimonials. Relaxing imagery.
|
||||
35,Luxury/Premium Brand,"brand, elegant, exclusive, high-end, luxury, premium",Liquid Glass + Glassmorphism,"Minimalism, 3D & Hyperrealism",Storytelling-Driven + Feature-Rich,Sales Intelligence Dashboard,Black + Gold (#FFD700) + White + Minimal accent,Elegance paramount. Premium imagery. Storytelling. High-quality visuals. Exclusive feel.
|
||||
36,Restaurant/Food Service,"appointment, booking, consultation, delivery, food, menu, order, restaurant, service",Vibrant & Block-based + Motion-Driven,"Claymorphism, Flat Design",Hero-Centric Design + Conversion,N/A - Booking focused,Warm colors (Orange Red Brown) + appetizing imagery,Menu display. Online ordering. Reservation system. Food photography. Location/hours prominent.
|
||||
37,Fitness/Gym App,"app, exercise, fitness, gym, health, workout",Vibrant & Block-based + Dark Mode (OLED),"Motion-Driven, Neumorphism",Feature-Rich Showcase,User Behavior Analytics,Energetic (Orange #FF6B35 Electric Blue) + Dark bg,Progress tracking. Workout plans. Community features. Achievements. Motivational design.
|
||||
38,Real Estate/Property,"buy, estate, housing, property, real, real-estate, rent",Glassmorphism + Minimalism,"Motion-Driven, 3D & Hyperrealism",Hero-Centric Design + Feature-Rich,Sales Intelligence Dashboard,Trust Blue (#0077B6) + Gold accents + White,Property listings. Virtual tours. Map integration. Agent profiles. Mortgage calculator. High-quality imagery.
|
||||
39,Travel/Tourism Agency,"agency, booking, creative, design, flight, hotel, marketing, studio, tourism, travel, vacation",Aurora UI + Motion-Driven,"Vibrant & Block-based, Glassmorphism",Storytelling-Driven + Hero-Centric,Booking Analytics,Vibrant destination colors + Sky Blue + Warm accents,Destination showcase. Booking system. Itinerary builder. Reviews. Inspiration galleries. Mobile-first.
|
||||
40,Hotel/Hospitality,"hospitality, hotel",Liquid Glass + Minimalism,"Glassmorphism, Soft UI Evolution",Hero-Centric Design + Social Proof,Revenue Management Dashboard,Warm neutrals + Gold (#D4AF37) + Brand accent,Room booking. Amenities showcase. Location maps. Guest reviews. Seasonal pricing. Luxury imagery.
|
||||
41,Wedding/Event Planning,"conference, event, meetup, planning, registration, ticket, wedding",Soft UI Evolution + Aurora UI,"Glassmorphism, Motion-Driven",Storytelling-Driven + Social Proof,N/A - Planning focused,Soft Pink (#FFD6E0) + Gold + Cream + Sage,Portfolio gallery. Vendor directory. Planning tools. Timeline. Budget tracker. Romantic aesthetic.
|
||||
42,Legal Services,"appointment, attorney, booking, compliance, consultation, contract, law, legal, service, services",Trust & Authority + Minimalism,"Accessible & Ethical, Swiss Modernism 2.0",Trust & Authority + Minimal,Case Management Dashboard,Navy Blue (#1E3A5F) + Gold + White,Credibility paramount. Practice areas. Attorney profiles. Case results. Contact forms. Professional imagery.
|
||||
43,Insurance Platform,"insurance, platform",Trust & Authority + Flat Design,"Accessible & Ethical, Minimalism",Conversion-Optimized + Trust,Claims Analytics Dashboard,Trust Blue (#0066CC) + Green (security) + Neutral,Quote calculator. Policy comparison. Claims process. Trust signals. Clear pricing. Security badges.
|
||||
44,Banking/Traditional Finance,"banking, finance, traditional",Minimalism + Accessible & Ethical,"Trust & Authority, Dark Mode (OLED)",Trust & Authority + Feature-Rich,Financial Dashboard,Navy (#0A1628) + Trust Blue + Gold accents,Security-first. Account overview. Transaction history. Mobile banking. Accessibility critical. Trust paramount.
|
||||
45,Online Course/E-learning,"course, e, learning, online",Claymorphism + Vibrant & Block-based,"Motion-Driven, Flat Design",Feature-Rich Showcase + Social Proof,Education Dashboard,Vibrant learning colors + Progress green,Course catalog. Progress tracking. Video player. Quizzes. Certificates. Community forums. Gamification.
|
||||
46,Non-profit/Charity,"charity, non, profit",Accessible & Ethical + Organic Biophilic,"Minimalism, Storytelling-Driven",Storytelling-Driven + Trust,Donation Analytics Dashboard,Cause-related colors + Trust + Warm,Impact stories. Donation flow. Transparency reports. Volunteer signup. Event calendar. Emotional connection.
|
||||
47,Music Streaming,"music, streaming",Dark Mode (OLED) + Vibrant & Block-based,"Motion-Driven, Aurora UI",Feature-Rich Showcase,Media/Entertainment Dashboard,Dark (#121212) + Vibrant accents + Album art colors,Audio player. Playlist management. Artist pages. Personalization. Social features. Waveform visualizations.
|
||||
48,Video Streaming/OTT,"ott, streaming, video",Dark Mode (OLED) + Motion-Driven,"Glassmorphism, Vibrant & Block-based",Hero-Centric Design + Feature-Rich,Media/Entertainment Dashboard,Dark bg + Content poster colors + Brand accent,Video player. Content discovery. Watchlist. Continue watching. Personalized recommendations. Thumbnail-heavy.
|
||||
49,Job Board/Recruitment,"board, job, recruitment",Flat Design + Minimalism,"Vibrant & Block-based, Accessible & Ethical",Conversion-Optimized + Feature-Rich,HR Analytics Dashboard,Professional Blue + Success Green + Neutral,Job listings. Search/filter. Company profiles. Application tracking. Resume upload. Salary insights.
|
||||
50,Marketplace (P2P),"buyers, listings, marketplace, p, platform, sellers",Vibrant & Block-based + Flat Design,"Micro-interactions, Trust & Authority",Feature-Rich Showcase + Social Proof,E-commerce Analytics,Trust colors + Category colors + Success green,Seller/buyer profiles. Listings. Reviews/ratings. Secure payment. Messaging. Search/filter. Trust badges.
|
||||
51,Logistics/Delivery,"delivery, logistics",Minimalism + Flat Design,"Dark Mode (OLED), Micro-interactions",Feature-Rich Showcase + Conversion,Real-Time Monitoring + Route Analytics,Blue (#2563EB) + Orange (tracking) + Green (delivered),Real-time tracking. Delivery scheduling. Route optimization. Driver management. Status updates. Map integration.
|
||||
52,Agriculture/Farm Tech,"agriculture, farm, tech",Organic Biophilic + Flat Design,"Minimalism, Accessible & Ethical",Feature-Rich Showcase + Trust,IoT Sensor Dashboard,Earth Green (#4A7C23) + Brown + Sky Blue,Crop monitoring. Weather data. IoT sensors. Yield tracking. Market prices. Sustainable imagery.
|
||||
53,Construction/Architecture,"architecture, construction",Minimalism + 3D & Hyperrealism,"Brutalism, Swiss Modernism 2.0",Hero-Centric Design + Feature-Rich,Project Management Dashboard,Grey (#4A4A4A) + Orange (safety) + Blueprint Blue,Project portfolio. 3D renders. Timeline. Material specs. Team collaboration. Blueprint aesthetic.
|
||||
54,Automotive/Car Dealership,"automotive, car, dealership",Motion-Driven + 3D & Hyperrealism,"Dark Mode (OLED), Glassmorphism",Hero-Centric Design + Feature-Rich,Sales Intelligence Dashboard,Brand colors + Metallic accents + Dark/Light,Vehicle showcase. 360° views. Comparison tools. Financing calculator. Test drive booking. High-quality imagery.
|
||||
55,Photography Studio,"photography, studio",Motion-Driven + Minimalism,"Aurora UI, Glassmorphism",Storytelling-Driven + Hero-Centric,N/A - Portfolio focused,Black + White + Minimal accent,Portfolio gallery. Before/after. Service packages. Booking system. Client galleries. Full-bleed imagery.
|
||||
56,Coworking Space,"coworking, space",Vibrant & Block-based + Glassmorphism,"Minimalism, Motion-Driven",Hero-Centric Design + Feature-Rich,Occupancy Dashboard,Energetic colors + Wood tones + Brand accent,Space tour. Membership plans. Booking system. Amenities. Community events. Virtual tour.
|
||||
57,Cleaning Service,"appointment, booking, cleaning, consultation, service",Soft UI Evolution + Flat Design,"Minimalism, Micro-interactions",Conversion-Optimized + Trust,Service Analytics,Fresh Blue (#00B4D8) + Clean White + Green,Service packages. Booking system. Price calculator. Before/after gallery. Reviews. Trust badges.
|
||||
58,Home Services (Plumber/Electrician),"appointment, booking, consultation, electrician, home, plumber, service, services",Flat Design + Trust & Authority,"Minimalism, Accessible & Ethical",Conversion-Optimized + Trust,Service Analytics,Trust Blue + Safety Orange + Professional grey,Service list. Emergency contact. Booking. Price transparency. Certifications. Local trust signals.
|
||||
59,Childcare/Daycare,"childcare, daycare",Claymorphism + Vibrant & Block-based,"Soft UI Evolution, Accessible & Ethical",Social Proof-Focused + Trust,Parent Dashboard,Playful pastels + Safe colors + Warm accents,Programs. Staff profiles. Safety certifications. Parent portal. Activity updates. Cheerful imagery.
|
||||
60,Senior Care/Elderly,"care, elderly, senior",Accessible & Ethical + Soft UI Evolution,"Minimalism, Neumorphism",Trust & Authority + Social Proof,Healthcare Analytics,Calm Blue + Warm neutrals + Large text,Care services. Staff qualifications. Facility tour. Family portal. Large touch targets. High contrast. Accessibility-first.
|
||||
61,Medical Clinic,"clinic, medical",Accessible & Ethical + Minimalism,"Neumorphism, Trust & Authority",Trust & Authority + Conversion,Healthcare Analytics,Medical Blue (#0077B6) + Trust White + Calm Green,Services. Doctor profiles. Online booking. Patient portal. Insurance info. HIPAA compliant. Trust signals.
|
||||
62,Pharmacy/Drug Store,"drug, pharmacy, store",Flat Design + Accessible & Ethical,"Minimalism, Trust & Authority",Conversion-Optimized + Trust,Inventory Dashboard,Pharmacy Green + Trust Blue + Clean White,Product catalog. Prescription upload. Refill reminders. Health info. Store locator. Safety certifications.
|
||||
63,Dental Practice,"dental, practice",Soft UI Evolution + Minimalism,"Accessible & Ethical, Trust & Authority",Social Proof-Focused + Conversion,Patient Analytics,Fresh Blue + White + Smile Yellow accent,Services. Dentist profiles. Before/after. Online booking. Insurance. Patient testimonials. Friendly imagery.
|
||||
64,Veterinary Clinic,"clinic, veterinary",Claymorphism + Accessible & Ethical,"Soft UI Evolution, Flat Design",Social Proof-Focused + Trust,Pet Health Dashboard,Caring Blue + Pet-friendly colors + Warm accents,Pet services. Vet profiles. Online booking. Pet portal. Emergency info. Friendly animal imagery.
|
||||
65,Florist/Plant Shop,"florist, plant, shop",Organic Biophilic + Vibrant & Block-based,"Aurora UI, Motion-Driven",Hero-Centric Design + Conversion,E-commerce Analytics,Natural Green + Floral pinks/purples + Earth tones,Product catalog. Occasion categories. Delivery scheduling. Care guides. Seasonal collections. Beautiful imagery.
|
||||
66,Bakery/Cafe,"bakery, cafe",Vibrant & Block-based + Soft UI Evolution,"Claymorphism, Motion-Driven",Hero-Centric Design + Conversion,N/A - Order focused,Warm Brown + Cream + Appetizing accents,Menu display. Online ordering. Location/hours. Catering. Seasonal specials. Appetizing photography.
|
||||
67,Coffee Shop,"coffee, shop",Minimalism + Organic Biophilic,"Soft UI Evolution, Flat Design",Hero-Centric Design + Conversion,N/A - Order focused,Coffee Brown (#6F4E37) + Cream + Warm accents,Menu. Online ordering. Loyalty program. Location. Story/origin. Cozy aesthetic.
|
||||
68,Brewery/Winery,"brewery, winery",Motion-Driven + Storytelling-Driven,"Dark Mode (OLED), Organic Biophilic",Storytelling-Driven + Hero-Centric,N/A - E-commerce focused,Deep amber/burgundy + Gold + Craft aesthetic,Product showcase. Story/heritage. Tasting notes. Events. Club membership. Artisanal imagery.
|
||||
69,Airline,"ai, airline, artificial-intelligence, automation, machine-learning, ml",Minimalism + Glassmorphism,"Motion-Driven, Accessible & Ethical",Conversion-Optimized + Feature-Rich,Operations Dashboard,Sky Blue + Brand colors + Trust accents,Flight search. Booking. Check-in. Boarding pass. Loyalty program. Route maps. Mobile-first.
|
||||
70,News/Media Platform,"content, entertainment, media, news, platform, streaming, video",Minimalism + Flat Design,"Dark Mode (OLED), Accessible & Ethical",Hero-Centric Design + Feature-Rich,Media Analytics Dashboard,Brand colors + High contrast + Category colors,Article layout. Breaking news. Categories. Search. Subscription. Mobile reading. Fast loading.
|
||||
71,Magazine/Blog,"articles, blog, content, magazine, posts, writing",Swiss Modernism 2.0 + Motion-Driven,"Minimalism, Aurora UI",Storytelling-Driven + Hero-Centric,Content Analytics,Editorial colors + Brand primary + Clean white,Article showcase. Category navigation. Author profiles. Newsletter signup. Related content. Typography-focused.
|
||||
72,Freelancer Platform,"freelancer, platform",Flat Design + Minimalism,"Vibrant & Block-based, Micro-interactions",Feature-Rich Showcase + Conversion,Marketplace Analytics,Professional Blue + Success Green + Neutral,Profile creation. Portfolio. Skill matching. Messaging. Payment. Reviews. Project management.
|
||||
73,Consulting Firm,"consulting, firm",Trust & Authority + Minimalism,"Swiss Modernism 2.0, Accessible & Ethical",Trust & Authority + Feature-Rich,N/A - Lead generation,Navy + Gold + Professional grey,Service areas. Case studies. Team profiles. Thought leadership. Contact. Professional credibility.
|
||||
74,Marketing Agency,"agency, creative, design, marketing, studio",Brutalism + Motion-Driven,"Vibrant & Block-based, Aurora UI",Storytelling-Driven + Feature-Rich,Campaign Analytics,Bold brand colors + Creative freedom,Portfolio. Case studies. Services. Team. Creative showcase. Results-focused. Bold aesthetic.
|
||||
75,Event Management,"conference, event, management, meetup, registration, ticket",Vibrant & Block-based + Motion-Driven,"Glassmorphism, Aurora UI",Hero-Centric Design + Feature-Rich,Event Analytics,Event theme colors + Excitement accents,Event showcase. Registration. Agenda. Speakers. Sponsors. Ticket sales. Countdown timer.
|
||||
76,Conference/Webinar Platform,"conference, platform, webinar",Glassmorphism + Minimalism,"Motion-Driven, Flat Design",Feature-Rich Showcase + Conversion,Attendee Analytics,Professional Blue + Video accent + Brand,Registration. Agenda. Speaker profiles. Live stream. Networking. Recording access. Virtual event features.
|
||||
77,Membership/Community,"community, membership",Vibrant & Block-based + Soft UI Evolution,"Bento Box Grid, Micro-interactions",Social Proof-Focused + Conversion,Community Analytics,Community brand colors + Engagement accents,Member benefits. Pricing tiers. Community showcase. Events. Member directory. Exclusive content.
|
||||
78,Newsletter Platform,"newsletter, platform",Minimalism + Flat Design,"Swiss Modernism 2.0, Accessible & Ethical",Minimal & Direct + Conversion,Email Analytics,Brand primary + Clean white + CTA accent,Subscribe form. Archive. About. Social proof. Sample content. Simple conversion.
|
||||
79,Digital Products/Downloads,"digital, downloads, products",Vibrant & Block-based + Motion-Driven,"Glassmorphism, Bento Box Grid",Feature-Rich Showcase + Conversion,E-commerce Analytics,Product category colors + Brand + Success green,Product showcase. Preview. Pricing. Instant delivery. License management. Customer reviews.
|
||||
80,Church/Religious Organization,"church, organization, religious",Accessible & Ethical + Soft UI Evolution,"Minimalism, Trust & Authority",Hero-Centric Design + Social Proof,N/A - Community focused,Warm Gold + Deep Purple/Blue + White,Service times. Events. Sermons. Community. Giving. Location. Welcoming imagery.
|
||||
81,Sports Team/Club,"club, sports, team",Vibrant & Block-based + Motion-Driven,"Dark Mode (OLED), 3D & Hyperrealism",Hero-Centric Design + Feature-Rich,Performance Analytics,Team colors + Energetic accents,Schedule. Roster. News. Tickets. Merchandise. Fan engagement. Action imagery.
|
||||
82,Museum/Gallery,"gallery, museum",Minimalism + Motion-Driven,"Swiss Modernism 2.0, 3D & Hyperrealism",Storytelling-Driven + Feature-Rich,Visitor Analytics,Art-appropriate neutrals + Exhibition accents,Exhibitions. Collections. Tickets. Events. Virtual tours. Educational content. Art-focused design.
|
||||
83,Theater/Cinema,"cinema, theater",Dark Mode (OLED) + Motion-Driven,"Vibrant & Block-based, Glassmorphism",Hero-Centric Design + Conversion,Booking Analytics,Dark + Spotlight accents + Gold,Showtimes. Seat selection. Trailers. Coming soon. Membership. Dramatic imagery.
|
||||
84,Language Learning App,"app, language, learning",Claymorphism + Vibrant & Block-based,"Micro-interactions, Flat Design",Feature-Rich Showcase + Social Proof,Learning Analytics,Playful colors + Progress indicators + Country flags,Lesson structure. Progress tracking. Gamification. Speaking practice. Community. Achievement badges.
|
||||
85,Coding Bootcamp,"bootcamp, coding",Dark Mode (OLED) + Minimalism,"Cyberpunk UI, Flat Design",Feature-Rich Showcase + Social Proof,Student Analytics,Code editor colors + Brand + Success green,Curriculum. Projects. Career outcomes. Alumni. Pricing. Application. Terminal aesthetic.
|
||||
86,Cybersecurity Platform,"cyber, security, platform",Cyberpunk UI + Dark Mode (OLED),"Neubrutalism, Minimal & Direct",Trust & Authority + Real-Time,Real-Time Monitoring + Heat Map,Matrix Green + Deep Black + Terminal feel,Data density. Threat visualization. Dark mode default.
|
||||
87,Developer Tool / IDE,"dev, developer, tool, ide",Dark Mode (OLED) + Minimalism,"Flat Design, Bento Box Grid",Minimal & Direct + Documentation,Real-Time Monitor + Terminal,Dark syntax theme colors + Blue focus,Keyboard shortcuts. Syntax highlighting. Fast performance.
|
||||
88,Biotech / Life Sciences,"biotech, biology, science",Glassmorphism + Clean Science,"Minimalism, Organic Biophilic",Storytelling-Driven + Research,Data-Dense + Predictive,Sterile White + DNA Blue + Life Green,Data accuracy. Cleanliness. Complex data viz.
|
||||
89,Space Tech / Aerospace,"aerospace, space, tech",Holographic / HUD + Dark Mode,"Glassmorphism, 3D & Hyperrealism",Immersive Experience + Hero,Real-Time Monitoring + 3D,Deep Space Black + Star White + Metallic,High-tech feel. Precision. Telemetry data.
|
||||
90,Architecture / Interior,"architecture, design, interior",Exaggerated Minimalism + High Imagery,"Swiss Modernism 2.0, Parallax",Portfolio Grid + Visuals,Project Management + Gallery,Monochrome + Gold Accent + High Imagery,High-res images. Typography. Space.
|
||||
91,Quantum Computing Interface,"quantum, computing, physics, qubit, future, science",Holographic / HUD + Dark Mode,"Glassmorphism, Spatial UI",Immersive/Interactive Experience,3D Spatial Data + Real-Time Monitor,Quantum Blue #00FFFF + Deep Black + Interference patterns,Visualize complexity. Qubit states. Probability clouds. High-tech trust.
|
||||
92,Biohacking / Longevity App,"biohacking, health, longevity, tracking, wellness, science",Biomimetic / Organic 2.0,"Minimalism, Dark Mode (OLED)",Data-Dense + Storytelling,Real-Time Monitor + Biological Data,Cellular Pink/Red + DNA Blue + Clean White,Personal data privacy. Scientific credibility. Biological visualizations.
|
||||
93,Autonomous Drone Fleet Manager,"drone, autonomous, fleet, aerial, logistics, robotics",HUD / Sci-Fi FUI,"Real-Time Monitor, Spatial UI",Real-Time Monitor,Geographic + Real-Time,Tactical Green #00FF00 + Alert Red + Map Dark,Real-time telemetry. 3D spatial awareness. Latency indicators. Safety alerts.
|
||||
94,Generative Art Platform,"art, generative, ai, creative, platform, gallery",Minimalism (Frame) + Gen Z Chaos,"Masonry Grid, Dark Mode",Bento Grid Showcase,Gallery / Portfolio,Neutral #F5F5F5 (Canvas) + User Content,Content is king. Fast loading. Creator attribution. Minting flow.
|
||||
95,Spatial Computing OS / App,"spatial, vr, ar, vision, os, immersive, mixed-reality",Spatial UI (VisionOS),"Glassmorphism, 3D & Hyperrealism",Immersive/Interactive Experience,Spatial Dashboard,Frosted Glass + System Colors + Depth,Gaze/Pinch interaction. Depth hierarchy. Environment awareness.
|
||||
96,Sustainable Energy / Climate Tech,"climate, energy, sustainable, green, tech, carbon",Organic Biophilic + E-Ink / Paper,"Data-Dense, Swiss Modernism",Interactive Demo + Data,Energy/Utilities Dashboard,Earth Green + Sky Blue + Solar Yellow,Data transparency. Impact visualization. Low-carbon web design.
|
||||
|
24
.claude/skills/ui-ux-pro-max/data/prompts.csv
Normal file
24
.claude/skills/ui-ux-pro-max/data/prompts.csv
Normal file
@ -0,0 +1,24 @@
|
||||
STT,Style Category,AI Prompt Keywords (Copy-Paste Ready),CSS/Technical Keywords,Implementation Checklist,Design System Variables
|
||||
1,Minimalism & Swiss Style,"Design a minimalist landing page. Use: white space, geometric layouts, sans-serif fonts, high contrast, grid-based structure, essential elements only. Avoid shadows and gradients. Focus on clarity and functionality.","display: grid, gap: 2rem, font-family: sans-serif, color: #000 or #FFF, max-width: 1200px, clean borders, no box-shadow unless necessary","☐ Grid-based layout 12-16 columns, ☐ Typography hierarchy clear, ☐ No unnecessary decorations, ☐ WCAG AAA contrast verified, ☐ Mobile responsive grid","--spacing: 2rem, --border-radius: 0px, --font-weight: 400-700, --shadow: none, --accent-color: single primary only"
|
||||
2,Neumorphism,"Create a neumorphic UI with soft 3D effects. Use light pastels, rounded corners (12-16px), subtle soft shadows (multiple layers), no hard lines, monochromatic color scheme with light/dark variations. Embossed/debossed effect on interactive elements.","border-radius: 12-16px, box-shadow: -5px -5px 15px rgba(0,0,0,0.1), 5px 5px 15px rgba(255,255,255,0.8), background: linear-gradient(145deg, color1, color2), transform: scale on press","☐ Rounded corners 12-16px consistent, ☐ Multiple shadow layers (2-3), ☐ Pastel color verified, ☐ Monochromatic palette checked, ☐ Press animation smooth 150ms","--border-radius: 14px, --shadow-soft-1: -5px -5px 15px, --shadow-soft-2: 5px 5px 15px, --color-light: #F5F5F5, --color-primary: single pastel"
|
||||
3,Glassmorphism,"Design a glassmorphic interface with frosted glass effect. Use backdrop blur (10-20px), translucent overlays (rgba 10-30% opacity), vibrant background colors, subtle borders, light source reflection, layered depth. Perfect for modern overlays and cards.","backdrop-filter: blur(15px), background: rgba(255, 255, 255, 0.15), border: 1px solid rgba(255,255,255,0.2), -webkit-backdrop-filter: blur(15px), z-index layering for depth","☐ Backdrop-filter blur 10-20px, ☐ Translucent white 15-30% opacity, ☐ Subtle border 1px light, ☐ Vibrant background verified, ☐ Text contrast 4.5:1 checked","--blur-amount: 15px, --glass-opacity: 0.15, --border-color: rgba(255,255,255,0.2), --background: vibrant color, --text-color: light/dark based on BG"
|
||||
4,Brutalism,"Create a brutalist design with raw, unpolished, stark aesthetic. Use pure primary colors (red, blue, yellow), black & white, no smooth transitions (instant), sharp corners, bold large typography, visible grid lines, default system fonts, intentional 'broken' design elements.","border-radius: 0px, transition: none or 0s, font-family: system-ui or monospace, font-weight: 700+, border: visible 2-4px, colors: #FF0000, #0000FF, #FFFF00, #000000, #FFFFFF","☐ No border-radius (0px), ☐ No transitions (instant), ☐ Bold typography (700+), ☐ Pure primary colors used, ☐ Visible grid/borders, ☐ Asymmetric layout intentional","--border-radius: 0px, --transition-duration: 0s, --font-weight: 700-900, --colors: primary only, --border-style: visible, --grid-visible: true"
|
||||
5,3D & Hyperrealism,"Build an immersive 3D interface using realistic textures, 3D models (Three.js/Babylon.js), complex shadows, realistic lighting, parallax scrolling (3-5 layers), physics-based motion. Include skeuomorphic elements with tactile detail.","transform: translate3d, perspective: 1000px, WebGL canvas, Three.js/Babylon.js library, box-shadow: complex multi-layer, background: complex gradients, filter: drop-shadow()","☐ WebGL/Three.js integrated, ☐ 3D models loaded, ☐ Parallax 3-5 layers, ☐ Realistic lighting verified, ☐ Complex shadows rendered, ☐ Physics animation smooth 300-400ms","--perspective: 1000px, --parallax-layers: 5, --lighting-intensity: realistic, --shadow-depth: 20-40%, --animation-duration: 300-400ms"
|
||||
6,Vibrant & Block-based,"Design an energetic, vibrant interface with bold block layouts, geometric shapes, high color contrast, large typography (32px+), animated background patterns, duotone effects. Perfect for startups and youth-focused apps. Use 4-6 contrasting colors from complementary/triadic schemes.","display: flex/grid with large gaps (48px+), font-size: 32px+, background: animated patterns (CSS), color: neon/vibrant colors, animation: continuous pattern movement","☐ Block layout with 48px+ gaps, ☐ Large typography 32px+, ☐ 4-6 vibrant colors max, ☐ Animated patterns active, ☐ Scroll-snap enabled, ☐ High contrast verified (7:1+)","--block-gap: 48px, --typography-size: 32px+, --color-palette: 4-6 vibrant colors, --animation: continuous pattern, --contrast-ratio: 7:1+"
|
||||
7,Dark Mode (OLED),"Create an OLED-optimized dark interface with deep black (#000000), dark grey (#121212), midnight blue accents. Use minimal glow effects, vibrant neon accents (green, blue, gold, purple), high contrast text. Optimize for eye comfort and OLED power saving.","background: #000000 or #121212, color: #FFFFFF or #E0E0E0, text-shadow: 0 0 10px neon-color (sparingly), filter: brightness(0.8) if needed, color-scheme: dark","☐ Deep black #000000 or #121212, ☐ Vibrant neon accents used, ☐ Text contrast 7:1+, ☐ Minimal glow effects, ☐ OLED power optimization, ☐ No white (#FFFFFF) background","--bg-black: #000000, --bg-dark-grey: #121212, --text-primary: #FFFFFF, --accent-neon: neon colors, --glow-effect: minimal, --oled-optimized: true"
|
||||
8,Accessible & Ethical,"Design with WCAG AAA compliance. Include: high contrast (7:1+), large text (16px+), keyboard navigation, screen reader compatibility, focus states visible (3-4px ring), semantic HTML, ARIA labels, skip links, reduced motion support (prefers-reduced-motion), 44x44px touch targets.","color-contrast: 7:1+, font-size: 16px+, outline: 3-4px on :focus-visible, aria-label, role attributes, @media (prefers-reduced-motion), touch-target: 44x44px, cursor: pointer","☐ WCAG AAA verified, ☐ 7:1+ contrast checked, ☐ Keyboard navigation tested, ☐ Screen reader tested, ☐ Focus visible 3-4px, ☐ Semantic HTML used, ☐ Touch targets 44x44px","--contrast-ratio: 7:1, --font-size-min: 16px, --focus-ring: 3-4px, --touch-target: 44x44px, --wcag-level: AAA, --keyboard-accessible: true, --sr-tested: true"
|
||||
9,Claymorphism,"Design a playful, toy-like interface with soft 3D, chunky elements, bubbly aesthetic, rounded edges (16-24px), thick borders (3-4px), double shadows (inner + outer), pastel colors, smooth animations. Perfect for children's apps and creative tools.","border-radius: 16-24px, border: 3-4px solid, box-shadow: inset -2px -2px 8px, 4px 4px 8px, background: pastel-gradient, animation: soft bounce (cubic-bezier 0.34, 1.56)","☐ Border-radius 16-24px, ☐ Thick borders 3-4px, ☐ Double shadows (inner+outer), ☐ Pastel colors used, ☐ Soft bounce animations, ☐ Playful interactions","--border-radius: 20px, --border-width: 3-4px, --shadow-inner: inset -2px -2px 8px, --shadow-outer: 4px 4px 8px, --color-palette: pastels, --animation: bounce"
|
||||
10,Aurora UI,"Create a vibrant gradient interface inspired by Northern Lights with mesh gradients, smooth color blends, flowing animations. Use complementary color pairs (blue-orange, purple-yellow), flowing background gradients, subtle continuous animations (8-12s loops), iridescent effects.","background: conic-gradient or radial-gradient with multiple stops, animation: @keyframes gradient (8-12s), background-size: 200% 200%, filter: saturate(1.2), blend-mode: screen or multiply","☐ Mesh/flowing gradients applied, ☐ 8-12s animation loop, ☐ Complementary colors used, ☐ Smooth color transitions, ☐ Iridescent effect subtle, ☐ Text contrast verified","--gradient-colors: complementary pairs, --animation-duration: 8-12s, --blend-mode: screen, --color-saturation: 1.2, --effect: iridescent, --loop-smooth: true"
|
||||
11,Retro-Futurism,"Build a retro-futuristic (cyberpunk/vaporwave) interface with neon colors (blue, pink, cyan), deep black background, 80s aesthetic, CRT scanlines, glitch effects, neon glow text/borders, monospace fonts, geometric patterns. Use neon text-shadow and animated glitch effects.","color: neon colors (#0080FF, #FF006E, #00FFFF), text-shadow: 0 0 10px neon, background: #000 or #1A1A2E, font-family: monospace, animation: glitch (skew+offset), filter: hue-rotate","☐ Neon colors used, ☐ CRT scanlines effect, ☐ Glitch animations active, ☐ Monospace font, ☐ Deep black background, ☐ Glow effects applied, ☐ 80s patterns present","--neon-colors: #0080FF #FF006E #00FFFF, --background: #000000, --font-family: monospace, --effect: glitch+glow, --scanline-opacity: 0.3, --crt-effect: true"
|
||||
12,Flat Design,"Create a flat, 2D interface with bold colors, no shadows/gradients, clean lines, simple geometric shapes, icon-heavy, typography-focused, minimal ornamentation. Use 4-6 solid, bright colors in a limited palette with high saturation.","box-shadow: none, background: solid color, border-radius: 0-4px, color: solid (no gradients), fill: solid, stroke: 1-2px, font: bold sans-serif, icons: simplified SVG","☐ No shadows/gradients, ☐ 4-6 solid colors max, ☐ Clean lines consistent, ☐ Simple shapes used, ☐ Icon-heavy layout, ☐ High saturation colors, ☐ Fast loading verified","--shadow: none, --color-palette: 4-6 solid, --border-radius: 2px, --gradient: none, --icons: simplified SVG, --animation: minimal 150-200ms"
|
||||
13,Skeuomorphism,"Design a realistic, textured interface with 3D depth, real-world metaphors (leather, wood, metal), complex gradients (8-12 stops), realistic shadows, grain/texture overlays, tactile press animations. Perfect for premium/luxury products.","background: complex gradient (8-12 stops), box-shadow: realistic multi-layer, background-image: texture overlay (noise, grain), filter: drop-shadow, transform: scale on press (300-500ms)","☐ Realistic textures applied, ☐ Complex gradients 8-12 stops, ☐ Multi-layer shadows, ☐ Texture overlays present, ☐ Tactile animations smooth, ☐ Depth effect pronounced","--gradient-stops: 8-12, --texture-overlay: noise+grain, --shadow-layers: 3+, --animation-duration: 300-500ms, --depth-effect: pronounced, --tactile: true"
|
||||
14,Liquid Glass,"Create a premium liquid glass effect with morphing shapes, flowing animations, chromatic aberration, iridescent gradients, smooth 400-600ms transitions. Use SVG morphing for shape changes, dynamic blur, smooth color transitions creating a fluid, premium feel.","animation: morphing SVG paths (400-600ms), backdrop-filter: blur + saturate, filter: hue-rotate + brightness, blend-mode: screen, background: iridescent gradient","☐ Morphing animations 400-600ms, ☐ Chromatic aberration applied, ☐ Dynamic blur active, ☐ Iridescent gradients, ☐ Smooth color transitions, ☐ Premium feel achieved","--morph-duration: 400-600ms, --blur-amount: 15px, --chromatic-aberration: true, --iridescent: true, --blend-mode: screen, --smooth-transitions: true"
|
||||
15,Motion-Driven,"Build an animation-heavy interface with scroll-triggered animations, microinteractions, parallax scrolling (3-5 layers), smooth transitions (300-400ms), entrance animations, page transitions. Use Intersection Observer for scroll effects, transform for performance, GPU acceleration.","animation: @keyframes scroll-reveal, transform: translateY/X, Intersection Observer API, will-change: transform, scroll-behavior: smooth, animation-duration: 300-400ms","☐ Scroll animations active, ☐ Parallax 3-5 layers, ☐ Entrance animations smooth, ☐ Page transitions fluid, ☐ GPU accelerated, ☐ Prefers-reduced-motion respected","--animation-duration: 300-400ms, --parallax-layers: 5, --scroll-behavior: smooth, --gpu-accelerated: true, --entrance-animation: true, --page-transition: smooth"
|
||||
16,Micro-interactions,"Design with delightful micro-interactions: small 50-100ms animations, gesture-based responses, tactile feedback, loading spinners, success/error states, subtle hover effects, haptic feedback triggers for mobile. Focus on responsive, contextual interactions.","animation: short 50-100ms, transition: hover states, @media (hover: hover) for desktop, :active for press, haptic-feedback CSS/API, loading animation smooth loop","☐ Micro-animations 50-100ms, ☐ Gesture-responsive, ☐ Tactile feedback visual/haptic, ☐ Loading spinners smooth, ☐ Success/error states clear, ☐ Hover effects subtle","--micro-animation-duration: 50-100ms, --gesture-responsive: true, --haptic-feedback: true, --loading-animation: smooth, --state-feedback: success+error"
|
||||
17,Inclusive Design,"Design for universal accessibility: high contrast (7:1+), large text (16px+), keyboard-only navigation, screen reader optimization, WCAG AAA compliance, symbol-based color indicators (not color-only), haptic feedback, voice interaction support, reduced motion options.","aria-* attributes complete, role attributes semantic, focus-visible: 3-4px ring, color-contrast: 7:1+, @media (prefers-reduced-motion), alt text on all images, form labels properly associated","☐ WCAG AAA verified, ☐ 7:1+ contrast all text, ☐ Keyboard accessible (Tab/Enter), ☐ Screen reader tested, ☐ Focus visible 3-4px, ☐ No color-only indicators, ☐ Haptic fallback","--contrast-ratio: 7:1, --font-size: 16px+, --keyboard-accessible: true, --sr-compatible: true, --wcag-level: AAA, --color-symbols: true, --haptic: enabled"
|
||||
18,Zero Interface,"Create a voice-first, gesture-based, AI-driven interface with minimal visible UI, progressive disclosure, voice recognition UI, gesture detection, AI predictions, smart suggestions, context-aware actions. Hide controls until needed.","voice-commands: Web Speech API, gesture-detection: touch events, AI-predictions: hidden by default (reveal on hover), progressive-disclosure: show on demand, minimal UI visible","☐ Voice commands responsive, ☐ Gesture detection active, ☐ AI predictions hidden/revealed, ☐ Progressive disclosure working, ☐ Minimal visible UI, ☐ Smart suggestions contextual","--voice-ui: enabled, --gesture-detection: active, --ai-predictions: smart, --progressive-disclosure: true, --visible-ui: minimal, --context-aware: true"
|
||||
19,Soft UI Evolution,"Design evolved neumorphism with improved contrast (WCAG AA+), modern aesthetics, subtle depth, accessibility focus. Use soft shadows (softer than flat but clearer than pure neumorphism), better color hierarchy, improved focus states, modern 200-300ms animations.","box-shadow: softer multi-layer (0 2px 4px), background: improved contrast pastels, border-radius: 8-12px, animation: 200-300ms smooth, outline: 2-3px on focus, contrast: 4.5:1+","☐ Improved contrast AA/AAA, ☐ Soft shadows modern, ☐ Border-radius 8-12px, ☐ Animations 200-300ms, ☐ Focus states visible, ☐ Color hierarchy clear","--shadow-soft: modern blend, --border-radius: 10px, --animation-duration: 200-300ms, --contrast-ratio: 4.5:1+, --color-hierarchy: improved, --wcag-level: AA+"
|
||||
20,Bento Grids,"Design a Bento Grid layout. Use: modular grid system, rounded corners (16-24px), different card sizes (1x1, 2x1, 2x2), card-based hierarchy, soft backgrounds (#F5F5F7), subtle borders, content-first, Apple-style aesthetic.","display: grid, grid-template-columns: repeat(auto-fit, minmax(...)), gap: 1rem, border-radius: 20px, background: #FFF, box-shadow: subtle","☐ Grid layout (CSS Grid), ☐ Rounded corners 16-24px, ☐ Varied card spans, ☐ Content fits card size, ☐ Responsive re-flow, ☐ Apple-like aesthetic","--grid-gap: 20px, --card-radius: 24px, --card-bg: #FFFFFF, --page-bg: #F5F5F7, --shadow: soft"
|
||||
21,Neubrutalism,"Design a neubrutalist interface. Use: high contrast, hard black borders (3px+), bright pop colors, no blur, sharp or slightly rounded corners, bold typography, hard shadows (offset 4px 4px), raw aesthetic but functional.","border: 3px solid black, box-shadow: 5px 5px 0px black, colors: #FFDB58 #FF6B6B #4ECDC4, font-weight: 700, no gradients","☐ Hard borders (2-4px), ☐ Hard offset shadows, ☐ High saturation colors, ☐ Bold typography, ☐ No blurs/gradients, ☐ Distinctive 'ugly-cute' look","--border-width: 3px, --shadow-offset: 4px, --shadow-color: #000, --colors: high saturation, --font: bold sans"
|
||||
22,HUD / Sci-Fi FUI,"Design a futuristic HUD (Heads Up Display) or FUI. Use: thin lines (1px), neon cyan/blue on black, technical markers, decorative brackets, data visualization, monospaced tech fonts, glowing elements, transparency.","border: 1px solid rgba(0,255,255,0.5), color: #00FFFF, background: transparent or rgba(0,0,0,0.8), font-family: monospace, text-shadow: 0 0 5px cyan","☐ Fine lines 1px, ☐ Neon glow text/borders, ☐ Monospaced font, ☐ Dark/Transparent BG, ☐ Decorative tech markers, ☐ Holographic feel","--hud-color: #00FFFF, --bg-color: rgba(0,10,20,0.9), --line-width: 1px, --glow: 0 0 5px, --font: monospace"
|
||||
23,Pixel Art,"Design a pixel art inspired interface. Use: pixelated fonts, 8-bit or 16-bit aesthetic, sharp edges (image-rendering: pixelated), limited color palette, blocky UI elements, retro gaming feel.","font-family: 'Press Start 2P', image-rendering: pixelated, box-shadow: 4px 0 0 #000 (pixel border), no anti-aliasing","☐ Pixelated fonts loaded, ☐ Images sharp (no blur), ☐ CSS box-shadow for pixel borders, ☐ Retro palette, ☐ Blocky layout","--pixel-size: 4px, --font: pixel font, --border-style: pixel-shadow, --anti-alias: none"
|
||||
|
53
.claude/skills/ui-ux-pro-max/data/stacks/flutter.csv
Normal file
53
.claude/skills/ui-ux-pro-max/data/stacks/flutter.csv
Normal file
@ -0,0 +1,53 @@
|
||||
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
|
||||
1,Widgets,Use StatelessWidget when possible,Immutable widgets are simpler,StatelessWidget for static UI,StatefulWidget for everything,class MyWidget extends StatelessWidget,class MyWidget extends StatefulWidget (static),Medium,https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html
|
||||
2,Widgets,Keep widgets small,Single responsibility principle,Extract widgets into smaller pieces,Large build methods,Column(children: [Header() Content()]),500+ line build method,Medium,
|
||||
3,Widgets,Use const constructors,Compile-time constants for performance,const MyWidget() when possible,Non-const for static widgets,const Text('Hello'),Text('Hello') for literals,High,https://dart.dev/guides/language/language-tour#constant-constructors
|
||||
4,Widgets,Prefer composition over inheritance,Combine widgets using children,Compose widgets,Extend widget classes,Container(child: MyContent()),class MyContainer extends Container,Medium,
|
||||
5,State,Use setState correctly,Minimal state in StatefulWidget,setState for UI state changes,setState for business logic,setState(() { _counter++; }),Complex logic in setState,Medium,https://api.flutter.dev/flutter/widgets/State/setState.html
|
||||
6,State,Avoid setState in build,Never call setState during build,setState in callbacks only,setState in build method,onPressed: () => setState(() {}),build() { setState(); },High,
|
||||
7,State,Use state management for complex apps,Provider Riverpod BLoC,State management for shared state,setState for global state,Provider.of<MyState>(context),Global setState calls,Medium,
|
||||
8,State,Prefer Riverpod or Provider,Recommended state solutions,Riverpod for new projects,InheritedWidget manually,ref.watch(myProvider),Custom InheritedWidget,Medium,https://riverpod.dev/
|
||||
9,State,Dispose resources,Clean up controllers and subscriptions,dispose() for cleanup,Memory leaks from subscriptions,@override void dispose() { controller.dispose(); },No dispose implementation,High,
|
||||
10,Layout,Use Column and Row,Basic layout widgets,Column Row for linear layouts,Stack for simple layouts,"Column(children: [Text(), Button()])",Stack for vertical list,Medium,https://api.flutter.dev/flutter/widgets/Column-class.html
|
||||
11,Layout,Use Expanded and Flexible,Control flex behavior,Expanded to fill space,Fixed sizes in flex containers,Expanded(child: Container()),Container(width: 200) in Row,Medium,
|
||||
12,Layout,Use SizedBox for spacing,Consistent spacing,SizedBox for gaps,Container for spacing only,SizedBox(height: 16),Container(height: 16),Low,
|
||||
13,Layout,Use LayoutBuilder for responsive,Respond to constraints,LayoutBuilder for adaptive layouts,Fixed sizes for responsive,LayoutBuilder(builder: (context constraints) {}),Container(width: 375),Medium,https://api.flutter.dev/flutter/widgets/LayoutBuilder-class.html
|
||||
14,Layout,Avoid deep nesting,Keep widget tree shallow,Extract deeply nested widgets,10+ levels of nesting,Extract widget to method or class,Column(Row(Column(Row(...)))),Medium,
|
||||
15,Lists,Use ListView.builder,Lazy list building,ListView.builder for long lists,ListView with children for large lists,"ListView.builder(itemCount: 100, itemBuilder: ...)",ListView(children: items.map(...).toList()),High,https://api.flutter.dev/flutter/widgets/ListView-class.html
|
||||
16,Lists,Provide itemExtent when known,Skip measurement,itemExtent for fixed height items,No itemExtent for uniform lists,ListView.builder(itemExtent: 50),ListView.builder without itemExtent,Medium,
|
||||
17,Lists,Use keys for stateful items,Preserve widget state,Key for stateful list items,No key for dynamic lists,ListTile(key: ValueKey(item.id)),ListTile without key,High,
|
||||
18,Lists,Use SliverList for custom scroll,Custom scroll effects,CustomScrollView with Slivers,Nested ListViews,CustomScrollView(slivers: [SliverList()]),ListView inside ListView,Medium,https://api.flutter.dev/flutter/widgets/SliverList-class.html
|
||||
19,Navigation,Use Navigator 2.0 or GoRouter,Declarative routing,go_router for navigation,Navigator.push for complex apps,GoRouter(routes: [...]),Navigator.push everywhere,Medium,https://pub.dev/packages/go_router
|
||||
20,Navigation,Use named routes,Organized navigation,Named routes for clarity,Anonymous routes,Navigator.pushNamed(context '/home'),Navigator.push(context MaterialPageRoute()),Low,
|
||||
21,Navigation,Handle back button (PopScope),Android back behavior and predictive back (Android 14+),Use PopScope widget (WillPopScope is deprecated),Use WillPopScope,"PopScope(canPop: false, onPopInvoked: (didPop) => ...)",WillPopScope(onWillPop: ...),High,https://api.flutter.dev/flutter/widgets/PopScope-class.html
|
||||
22,Navigation,Pass typed arguments,Type-safe route arguments,Typed route arguments,Dynamic arguments,MyRoute(id: '123'),arguments: {'id': '123'},Medium,
|
||||
23,Async,Use FutureBuilder,Async UI building,FutureBuilder for async data,setState for async,FutureBuilder(future: fetchData()),fetchData().then((d) => setState()),Medium,https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
|
||||
24,Async,Use StreamBuilder,Stream UI building,StreamBuilder for streams,Manual stream subscription,StreamBuilder(stream: myStream),stream.listen in initState,Medium,https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html
|
||||
25,Async,Handle loading and error states,Complete async UI states,ConnectionState checks,Only success state,if (snapshot.connectionState == ConnectionState.waiting),No loading indicator,High,
|
||||
26,Async,Cancel subscriptions,Clean up stream subscriptions,Cancel in dispose,Memory leaks,subscription.cancel() in dispose,No subscription cleanup,High,
|
||||
27,Theming,Use ThemeData,Consistent theming,ThemeData for app theme,Hardcoded colors,Theme.of(context).primaryColor,Color(0xFF123456) everywhere,Medium,https://api.flutter.dev/flutter/material/ThemeData-class.html
|
||||
28,Theming,Use ColorScheme,Material 3 color system,ColorScheme for colors,Individual color properties,colorScheme: ColorScheme.fromSeed(),primaryColor: Colors.blue,Medium,
|
||||
29,Theming,Access theme via context,Dynamic theme access,Theme.of(context),Static theme reference,Theme.of(context).textTheme.bodyLarge,TextStyle(fontSize: 16),Medium,
|
||||
30,Theming,Support dark mode,Respect system theme,darkTheme in MaterialApp,Light theme only,"MaterialApp(theme: light, darkTheme: dark)",MaterialApp(theme: light),Medium,
|
||||
31,Animation,Use implicit animations,Simple animations,AnimatedContainer AnimatedOpacity,Explicit for simple transitions,AnimatedContainer(duration: Duration()),AnimationController for fade,Low,https://api.flutter.dev/flutter/widgets/AnimatedContainer-class.html
|
||||
32,Animation,Use AnimationController for complex,Fine-grained control,AnimationController with Ticker,Implicit for complex sequences,AnimationController(vsync: this),AnimatedContainer for staggered,Medium,
|
||||
33,Animation,Dispose AnimationControllers,Clean up animation resources,dispose() for controllers,Memory leaks,controller.dispose() in dispose,No controller disposal,High,
|
||||
34,Animation,Use Hero for transitions,Shared element transitions,Hero for navigation animations,Manual shared element,Hero(tag: 'image' child: Image()),Custom shared element animation,Low,https://api.flutter.dev/flutter/widgets/Hero-class.html
|
||||
35,Forms,Use Form widget,Form validation,Form with GlobalKey,Individual validation,Form(key: _formKey child: ...),TextField without Form,Medium,https://api.flutter.dev/flutter/widgets/Form-class.html
|
||||
36,Forms,Use TextEditingController,Control text input,Controller for text fields,onChanged for all text,final controller = TextEditingController(),onChanged: (v) => setState(),Medium,
|
||||
37,Forms,Validate on submit,Form validation flow,_formKey.currentState!.validate(),Skip validation,if (_formKey.currentState!.validate()),Submit without validation,High,
|
||||
38,Forms,Dispose controllers,Clean up text controllers,dispose() for controllers,Memory leaks,controller.dispose() in dispose,No controller disposal,High,
|
||||
39,Performance,Use const widgets,Reduce rebuilds,const for static widgets,No const for literals,const Icon(Icons.add),Icon(Icons.add),High,
|
||||
40,Performance,Avoid rebuilding entire tree,Minimal rebuild scope,Isolate changing widgets,setState on parent,Consumer only around changing widget,setState on root widget,High,
|
||||
41,Performance,Use RepaintBoundary,Isolate repaints,RepaintBoundary for animations,Full screen repaints,RepaintBoundary(child: AnimatedWidget()),Animation without boundary,Medium,https://api.flutter.dev/flutter/widgets/RepaintBoundary-class.html
|
||||
42,Performance,Profile with DevTools,Measure before optimizing,Flutter DevTools profiling,Guess at performance,DevTools performance tab,Optimize without measuring,Medium,https://docs.flutter.dev/tools/devtools
|
||||
43,Accessibility,Use Semantics widget,Screen reader support,Semantics for accessibility,Missing accessibility info,Semantics(label: 'Submit button'),GestureDetector without semantics,High,https://api.flutter.dev/flutter/widgets/Semantics-class.html
|
||||
44,Accessibility,Support large fonts,MediaQuery text scaling,MediaQuery.textScaleFactor,Fixed font sizes,style: Theme.of(context).textTheme,TextStyle(fontSize: 14),High,
|
||||
45,Accessibility,Test with screen readers,TalkBack and VoiceOver,Test accessibility regularly,Skip accessibility testing,Regular TalkBack testing,No screen reader testing,High,
|
||||
46,Testing,Use widget tests,Test widget behavior,WidgetTester for UI tests,Unit tests only,testWidgets('...' (tester) async {}),Only test() for UI,Medium,https://docs.flutter.dev/testing
|
||||
47,Testing,Use integration tests,Full app testing,integration_test package,Manual testing only,IntegrationTestWidgetsFlutterBinding,Manual E2E testing,Medium,
|
||||
48,Testing,Mock dependencies,Isolate tests,Mockito or mocktail,Real dependencies in tests,when(mock.method()).thenReturn(),Real API calls in tests,Medium,
|
||||
49,Platform,Use Platform checks,Platform-specific code,Platform.isIOS Platform.isAndroid,Same code for all platforms,if (Platform.isIOS) {},Hardcoded iOS behavior,Medium,
|
||||
50,Platform,Use kIsWeb for web,Web platform detection,kIsWeb for web checks,Platform for web,if (kIsWeb) {},Platform.isWeb (doesn't exist),Medium,
|
||||
51,Packages,Use pub.dev packages,Community packages,Popular maintained packages,Custom implementations,cached_network_image,Custom image cache,Medium,https://pub.dev/
|
||||
52,Packages,Check package quality,Quality before adding,Pub points and popularity,Any package without review,100+ pub points,Unmaintained packages,Medium,
|
||||
|
56
.claude/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv
Normal file
56
.claude/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv
Normal file
@ -0,0 +1,56 @@
|
||||
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
|
||||
1,Animation,Use Tailwind animate utilities,Built-in animations are optimized and respect reduced-motion,Use animate-pulse animate-spin animate-ping,Custom @keyframes for simple effects,animate-pulse,@keyframes pulse {...},Medium,https://tailwindcss.com/docs/animation
|
||||
2,Animation,Limit bounce animations,Continuous bounce is distracting and causes motion sickness,Use animate-bounce sparingly on CTAs only,Multiple bounce animations on page,Single CTA with animate-bounce,5+ elements with animate-bounce,High,
|
||||
3,Animation,Transition duration,Use appropriate transition speeds for UI feedback,duration-150 to duration-300 for UI,duration-1000 or longer for UI elements,transition-all duration-200,transition-all duration-1000,Medium,https://tailwindcss.com/docs/transition-duration
|
||||
4,Animation,Hover transitions,Add smooth transitions on hover state changes,Add transition class with hover states,Instant hover changes without transition,hover:bg-gray-100 transition-colors,hover:bg-gray-100 (no transition),Low,
|
||||
5,Z-Index,Use Tailwind z-* scale,Consistent stacking context with predefined scale,z-0 z-10 z-20 z-30 z-40 z-50,Arbitrary z-index values,z-50 for modals,z-[9999],Medium,https://tailwindcss.com/docs/z-index
|
||||
6,Z-Index,Fixed elements z-index,Fixed navigation and modals need explicit z-index,z-50 for nav z-40 for dropdowns,Relying on DOM order for stacking,fixed top-0 z-50,fixed top-0 (no z-index),High,
|
||||
7,Z-Index,Negative z-index for backgrounds,Use negative z-index for decorative backgrounds,z-[-1] for background elements,Positive z-index for backgrounds,-z-10 for decorative,z-10 for background,Low,
|
||||
8,Layout,Container max-width,Limit content width for readability,max-w-7xl mx-auto for main content,Full-width content on large screens,max-w-7xl mx-auto px-4,w-full (no max-width),Medium,https://tailwindcss.com/docs/container
|
||||
9,Layout,Responsive padding,Adjust padding for different screen sizes,px-4 md:px-6 lg:px-8,Same padding all sizes,px-4 sm:px-6 lg:px-8,px-8 (same all sizes),Medium,
|
||||
10,Layout,Grid gaps,Use consistent gap utilities for spacing,gap-4 gap-6 gap-8,Margins on individual items,grid gap-6,grid with mb-4 on each item,Medium,https://tailwindcss.com/docs/gap
|
||||
11,Layout,Flexbox alignment,Use flex utilities for alignment,items-center justify-between,Multiple nested wrappers,flex items-center justify-between,Nested divs for alignment,Low,
|
||||
12,Images,Aspect ratio,Maintain consistent image aspect ratios,aspect-video aspect-square,No aspect ratio on containers,aspect-video rounded-lg,No aspect control,Medium,https://tailwindcss.com/docs/aspect-ratio
|
||||
13,Images,Object fit,Control image scaling within containers,object-cover object-contain,Stretched distorted images,object-cover w-full h-full,No object-fit,Medium,https://tailwindcss.com/docs/object-fit
|
||||
14,Images,Lazy loading,Defer loading of off-screen images,loading='lazy' on images,All images eager load,<img loading='lazy'>,<img> without lazy,High,
|
||||
15,Images,Responsive images,Serve appropriate image sizes,srcset and sizes attributes,Same large image all devices,srcset with multiple sizes,4000px image everywhere,High,
|
||||
16,Typography,Prose plugin,Use @tailwindcss/typography for rich text,prose prose-lg for article content,Custom styles for markdown,prose prose-lg max-w-none,Custom text styling,Medium,https://tailwindcss.com/docs/typography-plugin
|
||||
17,Typography,Line height,Use appropriate line height for readability,leading-relaxed for body text,Default tight line height,leading-relaxed (1.625),leading-none or leading-tight,Medium,https://tailwindcss.com/docs/line-height
|
||||
18,Typography,Font size scale,Use consistent text size scale,text-sm text-base text-lg text-xl,Arbitrary font sizes,text-lg,text-[17px],Low,https://tailwindcss.com/docs/font-size
|
||||
19,Typography,Text truncation,Handle long text gracefully,truncate or line-clamp-*,Overflow breaking layout,line-clamp-2,No overflow handling,Medium,https://tailwindcss.com/docs/text-overflow
|
||||
20,Colors,Opacity utilities,Use color opacity utilities,bg-black/50 text-white/80,Separate opacity class,bg-black/50,bg-black opacity-50,Low,https://tailwindcss.com/docs/background-color
|
||||
21,Colors,Dark mode,Support dark mode with dark: prefix,dark:bg-gray-900 dark:text-white,No dark mode support,dark:bg-gray-900,Only light theme,Medium,https://tailwindcss.com/docs/dark-mode
|
||||
22,Colors,Semantic colors,Use semantic color naming in config,primary secondary danger success,Generic color names in components,bg-primary,bg-blue-500 everywhere,Medium,
|
||||
23,Spacing,Consistent spacing scale,Use Tailwind spacing scale consistently,p-4 m-6 gap-8,Arbitrary pixel values,p-4 (1rem),p-[15px],Low,https://tailwindcss.com/docs/customizing-spacing
|
||||
24,Spacing,Negative margins,Use sparingly for overlapping effects,-mt-4 for overlapping elements,Negative margins for layout fixing,-mt-8 for card overlap,-m-2 to fix spacing issues,Medium,
|
||||
25,Spacing,Space between,Use space-y-* for vertical lists,space-y-4 on flex/grid column,Margin on each child,space-y-4,Each child has mb-4,Low,https://tailwindcss.com/docs/space
|
||||
26,Forms,Focus states,Always show focus indicators,focus:ring-2 focus:ring-blue-500,Remove focus outline,focus:ring-2 focus:ring-offset-2,focus:outline-none (no replacement),High,
|
||||
27,Forms,Input sizing,Consistent input dimensions,h-10 px-3 for inputs,Inconsistent input heights,h-10 w-full px-3,Various heights per input,Medium,
|
||||
28,Forms,Disabled states,Clear disabled styling,disabled:opacity-50 disabled:cursor-not-allowed,No disabled indication,disabled:opacity-50,Same style as enabled,Medium,
|
||||
29,Forms,Placeholder styling,Style placeholder text appropriately,placeholder:text-gray-400,Dark placeholder text,placeholder:text-gray-400,Default dark placeholder,Low,
|
||||
30,Responsive,Mobile-first approach,Start with mobile styles and add breakpoints,Default mobile + md: lg: xl:,Desktop-first approach,text-sm md:text-base,text-base max-md:text-sm,Medium,https://tailwindcss.com/docs/responsive-design
|
||||
31,Responsive,Breakpoint testing,Test at standard breakpoints,320 375 768 1024 1280 1536,Only test on development device,Test all breakpoints,Single device testing,High,
|
||||
32,Responsive,Hidden/shown utilities,Control visibility per breakpoint,hidden md:block,Different content per breakpoint,hidden md:flex,Separate mobile/desktop components,Low,https://tailwindcss.com/docs/display
|
||||
33,Buttons,Button sizing,Consistent button dimensions,px-4 py-2 or px-6 py-3,Inconsistent button sizes,px-4 py-2 text-sm,Various padding per button,Medium,
|
||||
34,Buttons,Touch targets,Minimum 44px touch target on mobile,min-h-[44px] on mobile,Small buttons on mobile,min-h-[44px] min-w-[44px],h-8 w-8 on mobile,High,
|
||||
35,Buttons,Loading states,Show loading feedback,disabled + spinner icon,Clickable during loading,<Button disabled><Spinner/></Button>,Button without loading state,High,
|
||||
36,Buttons,Icon buttons,Accessible icon-only buttons,aria-label on icon buttons,Icon button without label,<button aria-label='Close'><XIcon/></button>,<button><XIcon/></button>,High,
|
||||
37,Cards,Card structure,Consistent card styling,rounded-lg shadow-md p-6,Inconsistent card styles,rounded-2xl shadow-lg p-6,Mixed card styling,Low,
|
||||
38,Cards,Card hover states,Interactive cards should have hover feedback,hover:shadow-lg transition-shadow,No hover on clickable cards,hover:shadow-xl transition-shadow,Static cards that are clickable,Medium,
|
||||
39,Cards,Card spacing,Consistent internal card spacing,space-y-4 for card content,Inconsistent internal spacing,space-y-4 or p-6,Mixed mb-2 mb-4 mb-6,Low,
|
||||
40,Accessibility,Screen reader text,Provide context for screen readers,sr-only for hidden labels,Missing context for icons,<span class='sr-only'>Close menu</span>,No label for icon button,High,https://tailwindcss.com/docs/screen-readers
|
||||
41,Accessibility,Focus visible,Show focus only for keyboard users,focus-visible:ring-2,Focus on all interactions,focus-visible:ring-2,focus:ring-2 (shows on click too),Medium,
|
||||
42,Accessibility,Reduced motion,Respect user motion preferences,motion-reduce:animate-none,Ignore motion preferences,motion-reduce:transition-none,No reduced motion support,High,https://tailwindcss.com/docs/hover-focus-and-other-states#prefers-reduced-motion
|
||||
43,Performance,Configure content paths,Tailwind needs to know where classes are used,Use 'content' array in config,Use deprecated 'purge' option (v2),"content: ['./src/**/*.{js,ts,jsx,tsx}']",purge: [...],High,https://tailwindcss.com/docs/content-configuration
|
||||
44,Performance,JIT mode,Use JIT for faster builds and smaller bundles,JIT enabled (default in v3),Full CSS in development,Tailwind v3 defaults,Tailwind v2 without JIT,Medium,
|
||||
45,Performance,Avoid @apply bloat,Use @apply sparingly,Direct utilities in HTML,Heavy @apply usage,class='px-4 py-2 rounded',@apply px-4 py-2 rounded;,Low,https://tailwindcss.com/docs/reusing-styles
|
||||
46,Plugins,Official plugins,Use official Tailwind plugins,@tailwindcss/forms typography aspect-ratio,Custom implementations,@tailwindcss/forms,Custom form reset CSS,Medium,https://tailwindcss.com/docs/plugins
|
||||
47,Plugins,Custom utilities,Create utilities for repeated patterns,Custom utility in config,Repeated arbitrary values,Custom shadow utility,"shadow-[0_4px_20px_rgba(0,0,0,0.1)] everywhere",Medium,
|
||||
48,Layout,Container Queries,Use @container for component-based responsiveness,Use @container and @lg: etc.,Media queries for component internals,@container @lg:grid-cols-2,@media (min-width: ...) inside component,Medium,https://github.com/tailwindlabs/tailwindcss-container-queries
|
||||
49,Interactivity,Group and Peer,Style based on parent/sibling state,group-hover peer-checked,JS for simple state interactions,group-hover:text-blue-500,onMouseEnter={() => setHover(true)},Low,https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state
|
||||
50,Customization,Arbitrary Values,Use [] for one-off values,w-[350px] for specific needs,Creating config for single use,top-[117px] (if strictly needed),style={{ top: '117px' }},Low,https://tailwindcss.com/docs/adding-custom-styles#using-arbitrary-values
|
||||
51,Colors,Theme color variables,Define colors in Tailwind theme and use directly,bg-primary text-success border-cta,bg-[var(--color-primary)] text-[var(--color-success)],bg-primary,bg-[var(--color-primary)],Medium,https://tailwindcss.com/docs/customizing-colors
|
||||
52,Colors,Use bg-linear-to-* for gradients,Tailwind v4 uses bg-linear-to-* syntax for gradients,bg-linear-to-r bg-linear-to-b,bg-gradient-to-* (deprecated in v4),bg-linear-to-r from-blue-500 to-purple-500,bg-gradient-to-r from-blue-500 to-purple-500,Medium,https://tailwindcss.com/docs/background-image
|
||||
53,Layout,Use shrink-0 shorthand,Shorter class name for flex-shrink-0,shrink-0 shrink,flex-shrink-0 flex-shrink,shrink-0,flex-shrink-0,Low,https://tailwindcss.com/docs/flex-shrink
|
||||
54,Layout,Use size-* for square dimensions,Single utility for equal width and height,size-4 size-8 size-12,Separate h-* w-* for squares,size-6,h-6 w-6,Low,https://tailwindcss.com/docs/size
|
||||
55,Images,SVG explicit dimensions,Add width/height attributes to SVGs to prevent layout shift before CSS loads,<svg class='size-6' width='24' height='24'>,SVG without explicit dimensions,<svg class='size-6' width='24' height='24'>,<svg class='size-6'>,High,
|
||||
|
53
.claude/skills/ui-ux-pro-max/data/stacks/nextjs.csv
Normal file
53
.claude/skills/ui-ux-pro-max/data/stacks/nextjs.csv
Normal file
@ -0,0 +1,53 @@
|
||||
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
|
||||
1,Routing,Use App Router for new projects,App Router is the recommended approach in Next.js 14+,app/ directory with page.tsx,pages/ for new projects,app/dashboard/page.tsx,pages/dashboard.tsx,Medium,https://nextjs.org/docs/app
|
||||
2,Routing,Use file-based routing,Create routes by adding files in app directory,page.tsx for routes layout.tsx for layouts,Manual route configuration,app/blog/[slug]/page.tsx,Custom router setup,Medium,https://nextjs.org/docs/app/building-your-application/routing
|
||||
3,Routing,Colocate related files,Keep components styles tests with their routes,Component files alongside page.tsx,Separate components folder,app/dashboard/_components/,components/dashboard/,Low,
|
||||
4,Routing,Use route groups for organization,Group routes without affecting URL,Parentheses for route groups,Nested folders affecting URL,(marketing)/about/page.tsx,marketing/about/page.tsx,Low,https://nextjs.org/docs/app/building-your-application/routing/route-groups
|
||||
5,Routing,Handle loading states,Use loading.tsx for route loading UI,loading.tsx alongside page.tsx,Manual loading state management,app/dashboard/loading.tsx,useState for loading in page,Medium,https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming
|
||||
6,Routing,Handle errors with error.tsx,Catch errors at route level,error.tsx with reset function,try/catch in every component,app/dashboard/error.tsx,try/catch in page component,High,https://nextjs.org/docs/app/building-your-application/routing/error-handling
|
||||
7,Rendering,Use Server Components by default,Server Components reduce client JS bundle,Keep components server by default,Add 'use client' unnecessarily,export default function Page(),('use client') for static content,High,https://nextjs.org/docs/app/building-your-application/rendering/server-components
|
||||
8,Rendering,Mark Client Components explicitly,'use client' for interactive components,Add 'use client' only when needed,Server Component with hooks/events,('use client') for onClick useState,No directive with useState,High,https://nextjs.org/docs/app/building-your-application/rendering/client-components
|
||||
9,Rendering,Push Client Components down,Keep Client Components as leaf nodes,Client wrapper for interactive parts only,Mark page as Client Component,<InteractiveButton/> in Server Page,('use client') on page.tsx,High,
|
||||
10,Rendering,Use streaming for better UX,Stream content with Suspense boundaries,Suspense for slow data fetches,Wait for all data before render,<Suspense><SlowComponent/></Suspense>,await allData then render,Medium,https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming
|
||||
11,Rendering,Choose correct rendering strategy,SSG for static SSR for dynamic ISR for semi-static,generateStaticParams for known paths,SSR for static content,export const revalidate = 3600,fetch without cache config,Medium,
|
||||
12,DataFetching,Fetch data in Server Components,Fetch directly in async Server Components,async function Page() { const data = await fetch() },useEffect for initial data,const data = await fetch(url),useEffect(() => fetch(url)),High,https://nextjs.org/docs/app/building-your-application/data-fetching
|
||||
13,DataFetching,Configure caching explicitly (Next.js 15+),Next.js 15 changed defaults to uncached for fetch,Explicitly set cache: 'force-cache' for static data,Assume default is cached (it's not in Next.js 15),fetch(url { cache: 'force-cache' }),fetch(url) // Uncached in v15,High,https://nextjs.org/docs/app/building-your-application/upgrading/version-15
|
||||
14,DataFetching,Deduplicate fetch requests,React and Next.js dedupe same requests,Same fetch call in multiple components,Manual request deduplication,Multiple components fetch same URL,Custom cache layer,Low,
|
||||
15,DataFetching,Use Server Actions for mutations,Server Actions for form submissions,action={serverAction} in forms,API route for every mutation,<form action={createPost}>,<form onSubmit={callApiRoute}>,Medium,https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations
|
||||
16,DataFetching,Revalidate data appropriately,Use revalidatePath/revalidateTag after mutations,Revalidate after Server Action,'use client' with manual refetch,revalidatePath('/posts'),router.refresh() everywhere,Medium,https://nextjs.org/docs/app/building-your-application/caching#revalidating
|
||||
17,Images,Use next/image for optimization,Automatic image optimization and lazy loading,<Image> component for all images,<img> tags directly,<Image src={} alt={} width={} height={}>,<img src={}/>,High,https://nextjs.org/docs/app/building-your-application/optimizing/images
|
||||
18,Images,Provide width and height,Prevent layout shift with dimensions,width and height props or fill,Missing dimensions,<Image width={400} height={300}/>,<Image src={url}/>,High,
|
||||
19,Images,Use fill for responsive images,Fill container with object-fit,fill prop with relative parent,Fixed dimensions for responsive,"<Image fill className=""object-cover""/>",<Image width={window.width}/>,Medium,
|
||||
20,Images,Configure remote image domains,Whitelist external image sources,remotePatterns in next.config.js,Allow all domains,remotePatterns: [{ hostname: 'cdn.example.com' }],domains: ['*'],High,https://nextjs.org/docs/app/api-reference/components/image#remotepatterns
|
||||
21,Images,Use priority for LCP images,Mark above-fold images as priority,priority prop on hero images,All images with priority,<Image priority src={hero}/>,<Image priority/> on every image,Medium,
|
||||
22,Fonts,Use next/font for fonts,Self-hosted fonts with zero layout shift,next/font/google or next/font/local,External font links,import { Inter } from 'next/font/google',"<link href=""fonts.googleapis.com""/>",Medium,https://nextjs.org/docs/app/building-your-application/optimizing/fonts
|
||||
23,Fonts,Apply font to layout,Set font in root layout for consistency,className on body in layout.tsx,Font in individual pages,<body className={inter.className}>,Each page imports font,Low,
|
||||
24,Fonts,Use variable fonts,Variable fonts reduce bundle size,Single variable font file,Multiple font weights as files,Inter({ subsets: ['latin'] }),Inter_400 Inter_500 Inter_700,Low,
|
||||
25,Metadata,Use generateMetadata for dynamic,Generate metadata based on params,export async function generateMetadata(),Hardcoded metadata everywhere,generateMetadata({ params }),export const metadata = {},Medium,https://nextjs.org/docs/app/building-your-application/optimizing/metadata
|
||||
26,Metadata,Include OpenGraph images,Add OG images for social sharing,opengraph-image.tsx or og property,Missing social preview images,opengraph: { images: ['/og.png'] },No OG configuration,Medium,
|
||||
27,Metadata,Use metadata API,Export metadata object for static metadata,export const metadata = {},Manual head tags,export const metadata = { title: 'Page' },<head><title>Page</title></head>,Medium,
|
||||
28,API,Use Route Handlers for APIs,app/api routes for API endpoints,app/api/users/route.ts,pages/api for new projects,export async function GET(request),export default function handler,Medium,https://nextjs.org/docs/app/building-your-application/routing/route-handlers
|
||||
29,API,Return proper Response objects,Use NextResponse for API responses,NextResponse.json() for JSON,Plain objects or res.json(),return NextResponse.json({ data }),return { data },Medium,
|
||||
30,API,Handle HTTP methods explicitly,Export named functions for methods,Export GET POST PUT DELETE,Single handler for all methods,export async function POST(),switch(req.method),Low,
|
||||
31,API,Validate request body,Validate input before processing,Zod or similar for validation,Trust client input,const body = schema.parse(await req.json()),const body = await req.json(),High,
|
||||
32,Middleware,Use middleware for auth,Protect routes with middleware.ts,middleware.ts at root,Auth check in every page,export function middleware(request),if (!session) redirect in page,Medium,https://nextjs.org/docs/app/building-your-application/routing/middleware
|
||||
33,Middleware,Match specific paths,Configure middleware matcher,config.matcher for specific routes,Run middleware on all routes,matcher: ['/dashboard/:path*'],No matcher config,Medium,
|
||||
34,Middleware,Keep middleware edge-compatible,Middleware runs on Edge runtime,Edge-compatible code only,Node.js APIs in middleware,Edge-compatible auth check,fs.readFile in middleware,High,
|
||||
35,Environment,Use NEXT_PUBLIC prefix,Client-accessible env vars need prefix,NEXT_PUBLIC_ for client vars,Server vars exposed to client,NEXT_PUBLIC_API_URL,API_SECRET in client code,High,https://nextjs.org/docs/app/building-your-application/configuring/environment-variables
|
||||
36,Environment,Validate env vars,Check required env vars exist,Validate on startup,Undefined env at runtime,if (!process.env.DATABASE_URL) throw,process.env.DATABASE_URL (might be undefined),High,
|
||||
37,Environment,Use .env.local for secrets,Local env file for development secrets,.env.local gitignored,Secrets in .env committed,.env.local with secrets,.env with DATABASE_PASSWORD,High,
|
||||
38,Performance,Analyze bundle size,Use @next/bundle-analyzer,Bundle analyzer in dev,Ship large bundles blindly,ANALYZE=true npm run build,No bundle analysis,Medium,https://nextjs.org/docs/app/building-your-application/optimizing/bundle-analyzer
|
||||
39,Performance,Use dynamic imports,Code split with next/dynamic,dynamic() for heavy components,Import everything statically,const Chart = dynamic(() => import('./Chart')),import Chart from './Chart',Medium,https://nextjs.org/docs/app/building-your-application/optimizing/lazy-loading
|
||||
40,Performance,Avoid layout shifts,Reserve space for dynamic content,Skeleton loaders aspect ratios,Content popping in,"<Skeleton className=""h-48""/>",No placeholder for async content,High,
|
||||
41,Performance,Use Partial Prerendering,Combine static and dynamic in one route,Static shell with Suspense holes,Full dynamic or static pages,Static header + dynamic content,Entire page SSR,Low,https://nextjs.org/docs/app/building-your-application/rendering/partial-prerendering
|
||||
42,Link,Use next/link for navigation,Client-side navigation with prefetching,"<Link href=""""> for internal links",<a> for internal navigation,"<Link href=""/about"">About</Link>","<a href=""/about"">About</a>",High,https://nextjs.org/docs/app/api-reference/components/link
|
||||
43,Link,Prefetch strategically,Control prefetching behavior,prefetch={false} for low-priority,Prefetch all links,<Link prefetch={false}>,Default prefetch on every link,Low,
|
||||
44,Link,Use scroll option appropriately,Control scroll behavior on navigation,scroll={false} for tabs pagination,Always scroll to top,<Link scroll={false}>,Manual scroll management,Low,
|
||||
45,Config,Use next.config.js correctly,Configure Next.js behavior,Proper config options,Deprecated or wrong options,images: { remotePatterns: [] },images: { domains: [] },Medium,https://nextjs.org/docs/app/api-reference/next-config-js
|
||||
46,Config,Enable strict mode,Catch potential issues early,reactStrictMode: true,Strict mode disabled,reactStrictMode: true,reactStrictMode: false,Medium,
|
||||
47,Config,Configure redirects and rewrites,Use config for URL management,redirects() rewrites() in config,Manual redirect handling,redirects: async () => [...],res.redirect in pages,Medium,https://nextjs.org/docs/app/api-reference/next-config-js/redirects
|
||||
48,Deployment,Use Vercel for easiest deploy,Vercel optimized for Next.js,Deploy to Vercel,Self-host without knowledge,vercel deploy,Complex Docker setup for simple app,Low,https://nextjs.org/docs/app/building-your-application/deploying
|
||||
49,Deployment,Configure output for self-hosting,Set output option for deployment target,output: 'standalone' for Docker,Default output for containers,output: 'standalone',No output config for Docker,Medium,https://nextjs.org/docs/app/building-your-application/deploying#self-hosting
|
||||
50,Security,Sanitize user input,Never trust user input,Escape sanitize validate all input,Direct interpolation of user data,DOMPurify.sanitize(userInput),dangerouslySetInnerHTML={{ __html: userInput }},High,
|
||||
51,Security,Use CSP headers,Content Security Policy for XSS protection,Configure CSP in next.config.js,No security headers,headers() with CSP,No CSP configuration,High,https://nextjs.org/docs/app/building-your-application/configuring/content-security-policy
|
||||
52,Security,Validate Server Action input,Server Actions are public endpoints,Validate and authorize in Server Action,Trust Server Action input,Auth check + validation in action,Direct database call without check,High,
|
||||
|
51
.claude/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv
Normal file
51
.claude/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv
Normal file
@ -0,0 +1,51 @@
|
||||
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
|
||||
1,Installation,Add Nuxt UI module,Install and configure Nuxt UI in your Nuxt project,pnpm add @nuxt/ui and add to modules,Manual component imports,"modules: ['@nuxt/ui']","import { UButton } from '@nuxt/ui'",High,https://ui.nuxt.com/docs/getting-started/installation/nuxt
|
||||
2,Installation,Import Tailwind and Nuxt UI CSS,Required CSS imports in main.css file,@import tailwindcss and @import @nuxt/ui,Skip CSS imports,"@import ""tailwindcss""; @import ""@nuxt/ui"";",No CSS imports,High,https://ui.nuxt.com/docs/getting-started/installation/nuxt
|
||||
3,Installation,Wrap app with UApp component,UApp provides global configs for Toast Tooltip and overlays,<UApp> wrapper in app.vue,Skip UApp wrapper,<UApp><NuxtPage/></UApp>,<NuxtPage/> without wrapper,High,https://ui.nuxt.com/docs/components/app
|
||||
4,Components,Use U prefix for components,All Nuxt UI components use U prefix by default,UButton UInput UModal,Button Input Modal,<UButton>Click</UButton>,<Button>Click</Button>,Medium,https://ui.nuxt.com/docs/getting-started/installation/nuxt
|
||||
5,Components,Use semantic color props,Use semantic colors like primary secondary error,color="primary" color="error",Hardcoded colors,"<UButton color=""primary"">","<UButton class=""bg-green-500"">",Medium,https://ui.nuxt.com/docs/getting-started/theme/design-system
|
||||
6,Components,Use variant prop for styling,Nuxt UI provides solid outline soft subtle ghost link variants,variant="soft" variant="outline",Custom button classes,"<UButton variant=""soft"">","<UButton class=""border bg-transparent"">",Medium,https://ui.nuxt.com/docs/components/button
|
||||
7,Components,Use size prop consistently,Components support xs sm md lg xl sizes,size="sm" size="lg",Arbitrary sizing classes,"<UButton size=""lg"">","<UButton class=""text-xl px-6"">",Low,https://ui.nuxt.com/docs/components/button
|
||||
8,Icons,Use icon prop with Iconify format,Nuxt UI supports Iconify icons via icon prop,icon="lucide:home" icon="heroicons:user",i-lucide-home format,"<UButton icon=""lucide:home"">","<UButton icon=""i-lucide-home"">",Medium,https://ui.nuxt.com/docs/getting-started/integrations/icons/nuxt
|
||||
9,Icons,Use leadingIcon and trailingIcon,Position icons with dedicated props for clarity,leadingIcon="lucide:plus" trailingIcon="lucide:arrow-right",Manual icon positioning,"<UButton leadingIcon=""lucide:plus"">","<UButton><Icon name=""lucide:plus""/>Add</UButton>",Low,https://ui.nuxt.com/docs/components/button
|
||||
10,Theming,Configure colors in app.config.ts,Runtime color configuration without restart,ui.colors.primary in app.config.ts,Hardcoded colors in components,"defineAppConfig({ ui: { colors: { primary: 'blue' } } })","<UButton class=""bg-blue-500"">",High,https://ui.nuxt.com/docs/getting-started/theme/design-system
|
||||
11,Theming,Use @theme directive for custom colors,Define design tokens in CSS with Tailwind @theme,@theme { --color-brand-500: #xxx },Inline color definitions,@theme { --color-brand-500: #ef4444; },:style="{ color: '#ef4444' }",Medium,https://ui.nuxt.com/docs/getting-started/theme/design-system
|
||||
12,Theming,Extend semantic colors in nuxt.config,Register new colors like tertiary in theme.colors,theme.colors array in ui config,Use undefined colors,"ui: { theme: { colors: ['primary', 'tertiary'] } }","<UButton color=""tertiary""> without config",Medium,https://ui.nuxt.com/docs/getting-started/theme/design-system
|
||||
13,Forms,Use UForm with schema validation,UForm supports Zod Yup Joi Valibot schemas,:schema prop with validation schema,Manual form validation,"<UForm :schema=""schema"" :state=""state"">",Manual @blur validation,High,https://ui.nuxt.com/docs/components/form
|
||||
14,Forms,Use UFormField for field wrapper,Provides label error message and validation display,UFormField with name prop,Manual error handling,"<UFormField name=""email"" label=""Email"">",<div><label>Email</label><UInput/><span>error</span></div>,Medium,https://ui.nuxt.com/docs/components/form-field
|
||||
15,Forms,Handle form submit with @submit,UForm emits submit event with validated data,@submit handler on UForm,@click on submit button,"<UForm @submit=""onSubmit"">","<UButton @click=""onSubmit"">",Medium,https://ui.nuxt.com/docs/components/form
|
||||
16,Forms,Use validateOn prop for validation timing,Control when validation triggers (blur change input),validateOn="['blur']" for performance,Always validate on input,"<UForm :validateOn=""['blur', 'change']"">","<UForm> (validates on every keystroke)",Low,https://ui.nuxt.com/docs/components/form
|
||||
17,Overlays,Use v-model:open for overlay control,Modal Slideover Drawer use v-model:open,v-model:open for controlled state,Manual show/hide logic,"<UModal v-model:open=""isOpen"">",<UModal v-if="isOpen">,Medium,https://ui.nuxt.com/docs/components/modal
|
||||
18,Overlays,Use useOverlay composable for programmatic overlays,Open overlays programmatically without template refs,useOverlay().open(MyModal),Template ref and manual control,"const overlay = useOverlay(); overlay.open(MyModal, { props })","const modal = ref(); modal.value.open()",Medium,https://ui.nuxt.com/docs/components/modal
|
||||
19,Overlays,Use title and description props,Built-in header support for overlays,title="Confirm" description="Are you sure?",Manual header content,"<UModal title=""Confirm"" description=""Are you sure?"">","<UModal><template #header><h2>Confirm</h2></template>",Low,https://ui.nuxt.com/docs/components/modal
|
||||
20,Dashboard,Use UDashboardSidebar for navigation,Provides collapsible resizable sidebar with mobile support,UDashboardSidebar with header default footer slots,Custom sidebar implementation,<UDashboardSidebar><template #header>...</template></UDashboardSidebar>,<aside class="w-64 border-r">,Medium,https://ui.nuxt.com/docs/components/dashboard-sidebar
|
||||
21,Dashboard,Use UDashboardGroup for layout,Wraps dashboard components with sidebar state management,UDashboardGroup > UDashboardSidebar + UDashboardPanel,Manual layout flex containers,<UDashboardGroup><UDashboardSidebar/><UDashboardPanel/></UDashboardGroup>,"<div class=""flex""><aside/><main/></div>",Medium,https://ui.nuxt.com/docs/components/dashboard-group
|
||||
22,Dashboard,Use UDashboardNavbar for top navigation,Responsive navbar with mobile menu support,UDashboardNavbar in dashboard layout,Custom navbar implementation,<UDashboardNavbar :links="navLinks"/>,<nav class="border-b">,Low,https://ui.nuxt.com/docs/components/dashboard-navbar
|
||||
23,Tables,Use UTable with data and columns props,Powered by TanStack Table with built-in features,:data and :columns props,Manual table markup,"<UTable :data=""users"" :columns=""columns""/>","<table><tr v-for=""user in users"">",High,https://ui.nuxt.com/docs/components/table
|
||||
24,Tables,Define columns with accessorKey,Column definitions use accessorKey for data binding,accessorKey: 'email' in column def,String column names only,"{ accessorKey: 'email', header: 'Email' }","['name', 'email']",Medium,https://ui.nuxt.com/docs/components/table
|
||||
25,Tables,Use cell slot for custom rendering,Customize cell content with scoped slots,#cell-columnName slot,Override entire table,<template #cell-status="{ row }">,Manual column render function,Medium,https://ui.nuxt.com/docs/components/table
|
||||
26,Tables,Enable sorting with sortable column option,Add sortable: true to column definition,sortable: true in column,Manual sort implementation,"{ accessorKey: 'name', sortable: true }",@click="sortBy('name')",Low,https://ui.nuxt.com/docs/components/table
|
||||
27,Navigation,Use UNavigationMenu for nav links,Horizontal or vertical navigation with dropdown support,UNavigationMenu with items array,Manual nav with v-for,"<UNavigationMenu :items=""navItems""/>","<nav><a v-for=""item in items"">",Medium,https://ui.nuxt.com/docs/components/navigation-menu
|
||||
28,Navigation,Use UBreadcrumb for page hierarchy,Automatic breadcrumb with NuxtLink support,:items array with label and to,Manual breadcrumb links,"<UBreadcrumb :items=""breadcrumbs""/>","<nav><span v-for=""crumb in crumbs"">",Low,https://ui.nuxt.com/docs/components/breadcrumb
|
||||
29,Navigation,Use UTabs for tabbed content,Tab navigation with content panels,UTabs with items containing slot content,Manual tab state,"<UTabs :items=""tabs""/>","<div><button @click=""tab=1"">",Medium,https://ui.nuxt.com/docs/components/tabs
|
||||
30,Feedback,Use useToast for notifications,Composable for toast notifications,useToast().add({ title description }),Alert components for toasts,"const toast = useToast(); toast.add({ title: 'Saved' })",<UAlert v-if="showSuccess">,High,https://ui.nuxt.com/docs/components/toast
|
||||
31,Feedback,Use UAlert for inline messages,Static alert messages with icon and actions,UAlert with title description color,Toast for static messages,"<UAlert title=""Warning"" color=""warning""/>",useToast for inline alerts,Medium,https://ui.nuxt.com/docs/components/alert
|
||||
32,Feedback,Use USkeleton for loading states,Placeholder content during data loading,USkeleton with appropriate size,Spinner for content loading,<USkeleton class="h-4 w-32"/>,<UIcon name="lucide:loader" class="animate-spin"/>,Low,https://ui.nuxt.com/docs/components/skeleton
|
||||
33,Color Mode,Use UColorModeButton for theme toggle,Built-in light/dark mode toggle button,UColorModeButton component,Manual color mode logic,<UColorModeButton/>,"<button @click=""toggleColorMode"">",Low,https://ui.nuxt.com/docs/components/color-mode-button
|
||||
34,Color Mode,Use UColorModeSelect for theme picker,Dropdown to select system light or dark mode,UColorModeSelect component,Custom select for theme,<UColorModeSelect/>,"<USelect v-model=""colorMode"" :items=""modes""/>",Low,https://ui.nuxt.com/docs/components/color-mode-select
|
||||
35,Customization,Use ui prop for component styling,Override component styles via ui prop,ui prop with slot class overrides,Global CSS overrides,"<UButton :ui=""{ base: 'rounded-full' }""/>",<UButton class="!rounded-full"/>,Medium,https://ui.nuxt.com/docs/getting-started/theme/components
|
||||
36,Customization,Configure default variants in nuxt.config,Set default color and size for all components,theme.defaultVariants in ui config,Repeat props on every component,"ui: { theme: { defaultVariants: { color: 'neutral' } } }","<UButton color=""neutral""> everywhere",Medium,https://ui.nuxt.com/docs/getting-started/installation/nuxt
|
||||
37,Customization,Use app.config.ts for theme overrides,Runtime theme customization,defineAppConfig with ui key,nuxt.config for runtime values,"defineAppConfig({ ui: { button: { defaultVariants: { size: 'sm' } } } })","nuxt.config ui.button.size: 'sm'",Medium,https://ui.nuxt.com/docs/getting-started/theme/components
|
||||
38,Performance,Enable component detection,Tree-shake unused component CSS,experimental.componentDetection: true,Include all component CSS,"ui: { experimental: { componentDetection: true } }","ui: {} (includes all CSS)",Low,https://ui.nuxt.com/docs/getting-started/installation/nuxt
|
||||
39,Performance,Use UTable virtualize for large data,Enable virtualization for 1000+ rows,:virtualize prop on UTable,Render all rows,"<UTable :data=""largeData"" virtualize/>","<UTable :data=""largeData""/>",Medium,https://ui.nuxt.com/docs/components/table
|
||||
40,Accessibility,Use semantic component props,Components have built-in ARIA support,Use title description label props,Skip accessibility props,"<UModal title=""Settings"">","<UModal><h2>Settings</h2>",Medium,https://ui.nuxt.com/docs/components/modal
|
||||
41,Accessibility,Use UFormField for form accessibility,Automatic label-input association,UFormField wraps inputs,Manual id and for attributes,"<UFormField label=""Email""><UInput/></UFormField>","<label for=""email"">Email</label><UInput id=""email""/>",High,https://ui.nuxt.com/docs/components/form-field
|
||||
42,Content,Use UContentToc for table of contents,Automatic TOC with active heading highlight,UContentToc with :links,Manual TOC implementation,"<UContentToc :links=""toc""/>","<nav><a v-for=""heading in headings"">",Low,https://ui.nuxt.com/docs/components/content-toc
|
||||
43,Content,Use UContentSearch for docs search,Command palette for documentation search,UContentSearch with Nuxt Content,Custom search implementation,<UContentSearch/>,<UCommandPalette :groups="searchResults"/>,Low,https://ui.nuxt.com/docs/components/content-search
|
||||
44,AI/Chat,Use UChatMessages for chat UI,Designed for Vercel AI SDK integration,UChatMessages with messages array,Custom chat message list,"<UChatMessages :messages=""messages""/>","<div v-for=""msg in messages"">",Medium,https://ui.nuxt.com/docs/components/chat-messages
|
||||
45,AI/Chat,Use UChatPrompt for input,Enhanced textarea for AI prompts,UChatPrompt with v-model,Basic textarea,<UChatPrompt v-model="prompt"/>,<UTextarea v-model="prompt"/>,Medium,https://ui.nuxt.com/docs/components/chat-prompt
|
||||
46,Editor,Use UEditor for rich text,TipTap-based editor with toolbar support,UEditor with v-model:content,Custom TipTap setup,"<UEditor v-model:content=""content""/>",Manual TipTap initialization,Medium,https://ui.nuxt.com/docs/components/editor
|
||||
47,Links,Use to prop for navigation,UButton and ULink support NuxtLink to prop,to="/dashboard" for internal links,href for internal navigation,"<UButton to=""/dashboard"">","<UButton href=""/dashboard"">",Medium,https://ui.nuxt.com/docs/components/button
|
||||
48,Links,Use external prop for outside links,Explicitly mark external links,target="_blank" with external URLs,Forget rel="noopener","<UButton to=""https://example.com"" target=""_blank"">","<UButton href=""https://..."">",Low,https://ui.nuxt.com/docs/components/link
|
||||
49,Loading,Use loadingAuto on buttons,Automatic loading state from @click promise,loadingAuto prop on UButton,Manual loading state,"<UButton loadingAuto @click=""async () => await save()"">","<UButton :loading=""isLoading"" @click=""save"">",Low,https://ui.nuxt.com/docs/components/button
|
||||
50,Loading,Use UForm loadingAuto,Auto-disable form during submit,loadingAuto on UForm (default true),Manual form disabled state,"<UForm @submit=""handleSubmit"">","<UForm :disabled=""isSubmitting"">",Low,https://ui.nuxt.com/docs/components/form
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 94.
|
59
.claude/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv
Normal file
59
.claude/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv
Normal file
@ -0,0 +1,59 @@
|
||||
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
|
||||
1,Routing,Use file-based routing,Create routes by adding files in pages directory,pages/ directory with index.vue,Manual route configuration,pages/dashboard/index.vue,Custom router setup,Medium,https://nuxt.com/docs/getting-started/routing
|
||||
2,Routing,Use dynamic route parameters,Create dynamic routes with bracket syntax,[id].vue for dynamic params,Hardcoded routes for dynamic content,pages/posts/[id].vue,pages/posts/post1.vue,Medium,https://nuxt.com/docs/getting-started/routing
|
||||
3,Routing,Use catch-all routes,Handle multiple path segments with [...slug],[...slug].vue for catch-all,Multiple nested dynamic routes,pages/[...slug].vue,pages/[a]/[b]/[c].vue,Low,https://nuxt.com/docs/getting-started/routing
|
||||
4,Routing,Define page metadata with definePageMeta,Set page-level configuration and middleware,definePageMeta for layout middleware title,Manual route meta configuration,"definePageMeta({ layout: 'admin', middleware: 'auth' })",router.beforeEach for page config,High,https://nuxt.com/docs/api/utils/define-page-meta
|
||||
5,Routing,Use validate for route params,Validate dynamic route parameters before rendering,validate function in definePageMeta,Manual validation in setup,"definePageMeta({ validate: (route) => /^\d+$/.test(route.params.id) })",if (!valid) navigateTo('/404'),Medium,https://nuxt.com/docs/api/utils/define-page-meta
|
||||
6,Rendering,Use SSR by default,Server-side rendering is enabled by default,Keep ssr: true (default),Disable SSR unnecessarily,ssr: true (default),ssr: false for all pages,High,https://nuxt.com/docs/guide/concepts/rendering
|
||||
7,Rendering,Use .client suffix for client-only components,Mark components to render only on client,ComponentName.client.vue suffix,v-if with process.client check,Comments.client.vue,<div v-if="process.client"><Comments/></div>,Medium,https://nuxt.com/docs/guide/directory-structure/components
|
||||
8,Rendering,Use .server suffix for server-only components,Mark components to render only on server,ComponentName.server.vue suffix,Manual server check,HeavyMarkdown.server.vue,v-if="process.server",Low,https://nuxt.com/docs/guide/directory-structure/components
|
||||
9,DataFetching,Use useFetch for simple data fetching,Wrapper around useAsyncData for URL fetching,useFetch for API calls,$fetch in onMounted,"const { data } = await useFetch('/api/posts')","onMounted(async () => { data.value = await $fetch('/api/posts') })",High,https://nuxt.com/docs/api/composables/use-fetch
|
||||
10,DataFetching,Use useAsyncData for complex fetching,Fine-grained control over async data,useAsyncData for CMS or custom fetching,useFetch for non-URL data sources,"const { data } = await useAsyncData('posts', () => cms.getPosts())","const { data } = await useFetch(() => cms.getPosts())",Medium,https://nuxt.com/docs/api/composables/use-async-data
|
||||
11,DataFetching,Use $fetch for non-reactive requests,$fetch for event handlers and non-component code,$fetch in event handlers or server routes,useFetch in click handlers,"async function submit() { await $fetch('/api/submit', { method: 'POST' }) }","async function submit() { await useFetch('/api/submit') }",High,https://nuxt.com/docs/api/utils/dollarfetch
|
||||
12,DataFetching,Use lazy option for non-blocking fetch,Defer data fetching for better initial load,lazy: true for below-fold content,Blocking fetch for non-critical data,"useFetch('/api/comments', { lazy: true })",await useFetch('/api/comments') for footer,Medium,https://nuxt.com/docs/api/composables/use-fetch
|
||||
13,DataFetching,Use server option to control fetch location,Choose where data is fetched,server: false for client-only data,Server fetch for user-specific client data,"useFetch('/api/user-preferences', { server: false })",useFetch for localStorage-dependent data,Medium,https://nuxt.com/docs/api/composables/use-fetch
|
||||
14,DataFetching,Use pick to reduce payload size,Select only needed fields from response,pick option for large responses,Fetching entire objects when few fields needed,"useFetch('/api/user', { pick: ['id', 'name'] })",useFetch('/api/user') then destructure,Low,https://nuxt.com/docs/api/composables/use-fetch
|
||||
15,DataFetching,Use transform for data manipulation,Transform data before storing in state,transform option for data shaping,Manual transformation after fetch,"useFetch('/api/posts', { transform: (posts) => posts.map(p => p.title) })",const titles = data.value.map(p => p.title),Low,https://nuxt.com/docs/api/composables/use-fetch
|
||||
16,DataFetching,Handle loading and error states,Always handle pending and error states,Check status pending error refs,Ignoring loading states,"<div v-if=""status === 'pending'"">Loading...</div>",No loading indicator,High,https://nuxt.com/docs/getting-started/data-fetching
|
||||
17,Lifecycle,Avoid side effects in script setup root,Move side effects to lifecycle hooks,Side effects in onMounted,setInterval in root script setup,"onMounted(() => { interval = setInterval(...) })","<script setup>setInterval(...)</script>",High,https://nuxt.com/docs/guide/concepts/nuxt-lifecycle
|
||||
18,Lifecycle,Use onMounted for DOM access,Access DOM only after component is mounted,onMounted for DOM manipulation,Direct DOM access in setup,"onMounted(() => { document.getElementById('el') })","<script setup>document.getElementById('el')</script>",High,https://nuxt.com/docs/api/composables/on-mounted
|
||||
19,Lifecycle,Use nextTick for post-render access,Wait for DOM updates before accessing elements,await nextTick() after state changes,Immediate DOM access after state change,"count.value++; await nextTick(); el.value.focus()","count.value++; el.value.focus()",Medium,https://nuxt.com/docs/api/utils/next-tick
|
||||
20,Lifecycle,Use onPrehydrate for pre-hydration logic,Run code before Nuxt hydrates the page,onPrehydrate for client setup,onMounted for hydration-critical code,"onPrehydrate(() => { console.log(window) })",onMounted for pre-hydration needs,Low,https://nuxt.com/docs/api/composables/on-prehydrate
|
||||
21,Server,Use server/api for API routes,Create API endpoints in server/api directory,server/api/users.ts for /api/users,Manual Express setup,server/api/hello.ts -> /api/hello,app.get('/api/hello'),High,https://nuxt.com/docs/guide/directory-structure/server
|
||||
22,Server,Use defineEventHandler for handlers,Define server route handlers,defineEventHandler for all handlers,export default function,"export default defineEventHandler((event) => { return { hello: 'world' } })","export default function(req, res) {}",High,https://nuxt.com/docs/guide/directory-structure/server
|
||||
23,Server,Use server/routes for non-api routes,Routes without /api prefix,server/routes for custom paths,server/api for non-api routes,server/routes/sitemap.xml.ts,server/api/sitemap.xml.ts,Medium,https://nuxt.com/docs/guide/directory-structure/server
|
||||
24,Server,Use getQuery and readBody for input,Access query params and request body,getQuery(event) readBody(event),Direct event access,"const { id } = getQuery(event)",event.node.req.query,Medium,https://nuxt.com/docs/guide/directory-structure/server
|
||||
25,Server,Validate server input,Always validate input in server handlers,Zod or similar for validation,Trust client input,"const body = await readBody(event); schema.parse(body)",const body = await readBody(event),High,https://nuxt.com/docs/guide/directory-structure/server
|
||||
26,State,Use useState for shared reactive state,SSR-friendly shared state across components,useState for cross-component state,ref for shared state,"const count = useState('count', () => 0)",const count = ref(0) in composable,High,https://nuxt.com/docs/api/composables/use-state
|
||||
27,State,Use unique keys for useState,Prevent state conflicts with unique keys,Descriptive unique keys for each state,Generic or duplicate keys,"useState('user-preferences', () => ({}))",useState('data') in multiple places,Medium,https://nuxt.com/docs/api/composables/use-state
|
||||
28,State,Use Pinia for complex state,Pinia for advanced state management,@pinia/nuxt for complex apps,Custom state management,useMainStore() with Pinia,Custom reactive store implementation,Medium,https://nuxt.com/docs/getting-started/state-management
|
||||
29,State,Use callOnce for one-time async operations,Ensure async operations run only once,callOnce for store initialization,Direct await in component,"await callOnce(store.fetch)",await store.fetch() on every render,Medium,https://nuxt.com/docs/api/utils/call-once
|
||||
30,SEO,Use useSeoMeta for SEO tags,Type-safe SEO meta tag management,useSeoMeta for meta tags,useHead for simple meta,"useSeoMeta({ title: 'Home', ogTitle: 'Home', description: '...' })","useHead({ meta: [{ name: 'description', content: '...' }] })",High,https://nuxt.com/docs/api/composables/use-seo-meta
|
||||
31,SEO,Use reactive values in useSeoMeta,Dynamic SEO tags with refs or getters,Computed getters for dynamic values,Static values for dynamic content,"useSeoMeta({ title: () => post.value.title })","useSeoMeta({ title: post.value.title })",Medium,https://nuxt.com/docs/api/composables/use-seo-meta
|
||||
32,SEO,Use useHead for non-meta head elements,Scripts styles links in head,useHead for scripts and links,useSeoMeta for scripts,"useHead({ script: [{ src: '/analytics.js' }] })","useSeoMeta({ script: '...' })",Medium,https://nuxt.com/docs/api/composables/use-head
|
||||
33,SEO,Include OpenGraph tags,Add OG tags for social sharing,ogTitle ogDescription ogImage,Missing social preview,"useSeoMeta({ ogImage: '/og.png', twitterCard: 'summary_large_image' })",No OG configuration,Medium,https://nuxt.com/docs/api/composables/use-seo-meta
|
||||
34,Middleware,Use defineNuxtRouteMiddleware,Define route middleware properly,defineNuxtRouteMiddleware wrapper,export default function,"export default defineNuxtRouteMiddleware((to, from) => {})","export default function(to, from) {}",High,https://nuxt.com/docs/guide/directory-structure/middleware
|
||||
35,Middleware,Use navigateTo for redirects,Redirect in middleware with navigateTo,return navigateTo('/login'),router.push in middleware,"if (!auth) return navigateTo('/login')","if (!auth) router.push('/login')",High,https://nuxt.com/docs/api/utils/navigate-to
|
||||
36,Middleware,Reference middleware in definePageMeta,Apply middleware to specific pages,middleware array in definePageMeta,Global middleware for page-specific,definePageMeta({ middleware: ['auth'] }),Global auth check for one page,Medium,https://nuxt.com/docs/guide/directory-structure/middleware
|
||||
37,Middleware,Use .global suffix for global middleware,Apply middleware to all routes,auth.global.ts for app-wide auth,Manual middleware on every page,middleware/auth.global.ts,middleware: ['auth'] on every page,Medium,https://nuxt.com/docs/guide/directory-structure/middleware
|
||||
38,ErrorHandling,Use createError for errors,Create errors with proper status codes,createError with statusCode,throw new Error,"throw createError({ statusCode: 404, statusMessage: 'Not Found' })",throw new Error('Not Found'),High,https://nuxt.com/docs/api/utils/create-error
|
||||
39,ErrorHandling,Use NuxtErrorBoundary for local errors,Handle errors within component subtree,NuxtErrorBoundary for component errors,Global error page for local errors,"<NuxtErrorBoundary @error=""log""><template #error=""{ error }"">",error.vue for component errors,Medium,https://nuxt.com/docs/getting-started/error-handling
|
||||
40,ErrorHandling,Use clearError to recover from errors,Clear error state and optionally redirect,clearError({ redirect: '/' }),Manual error state reset,clearError({ redirect: '/home' }),error.value = null,Medium,https://nuxt.com/docs/api/utils/clear-error
|
||||
41,ErrorHandling,Use short statusMessage,Keep statusMessage brief for security,Short generic messages,Detailed error info in statusMessage,"createError({ statusCode: 400, statusMessage: 'Bad Request' })","createError({ statusMessage: 'Invalid user ID: 123' })",High,https://nuxt.com/docs/getting-started/error-handling
|
||||
42,Link,Use NuxtLink for internal navigation,Client-side navigation with prefetching,<NuxtLink to> for internal links,<a href> for internal links,<NuxtLink to="/about">About</NuxtLink>,<a href="/about">About</a>,High,https://nuxt.com/docs/api/components/nuxt-link
|
||||
43,Link,Configure prefetch behavior,Control when prefetching occurs,prefetchOn for interaction-based,Default prefetch for low-priority,"<NuxtLink prefetch-on=""interaction"">",Always default prefetch,Low,https://nuxt.com/docs/api/components/nuxt-link
|
||||
44,Link,Use useRouter for programmatic navigation,Navigate programmatically,useRouter().push() for navigation,Direct window.location,"const router = useRouter(); router.push('/dashboard')",window.location.href = '/dashboard',Medium,https://nuxt.com/docs/api/composables/use-router
|
||||
45,Link,Use navigateTo in composables,Navigate outside components,navigateTo() in middleware or plugins,useRouter in non-component code,return navigateTo('/login'),router.push in middleware,Medium,https://nuxt.com/docs/api/utils/navigate-to
|
||||
46,AutoImports,Leverage auto-imports,Use auto-imported composables directly,Direct use of ref computed useFetch,Manual imports for Nuxt composables,"const count = ref(0)","import { ref } from 'vue'; const count = ref(0)",Medium,https://nuxt.com/docs/guide/concepts/auto-imports
|
||||
47,AutoImports,Use #imports for explicit imports,Explicit imports when needed,#imports for clarity or disabled auto-imports,"import from 'vue' when auto-import enabled","import { ref } from '#imports'","import { ref } from 'vue'",Low,https://nuxt.com/docs/guide/concepts/auto-imports
|
||||
48,AutoImports,Configure third-party auto-imports,Add external package auto-imports,imports.presets in nuxt.config,Manual imports everywhere,"imports: { presets: [{ from: 'vue-i18n', imports: ['useI18n'] }] }",import { useI18n } everywhere,Low,https://nuxt.com/docs/guide/concepts/auto-imports
|
||||
49,Plugins,Use defineNuxtPlugin,Define plugins properly,defineNuxtPlugin wrapper,export default function,"export default defineNuxtPlugin((nuxtApp) => {})","export default function(ctx) {}",High,https://nuxt.com/docs/guide/directory-structure/plugins
|
||||
50,Plugins,Use provide for injection,Provide helpers across app,return { provide: {} } for type safety,nuxtApp.provide without types,"return { provide: { hello: (name) => `Hello ${name}!` } }","nuxtApp.provide('hello', fn)",Medium,https://nuxt.com/docs/guide/directory-structure/plugins
|
||||
51,Plugins,Use .client or .server suffix,Control plugin execution environment,plugin.client.ts for client-only,if (process.client) checks,analytics.client.ts,"if (process.client) { // analytics }",Medium,https://nuxt.com/docs/guide/directory-structure/plugins
|
||||
52,Environment,Use runtimeConfig for env vars,Access environment variables safely,runtimeConfig in nuxt.config,process.env directly,"runtimeConfig: { apiSecret: '', public: { apiBase: '' } }",process.env.API_SECRET in components,High,https://nuxt.com/docs/guide/going-further/runtime-config
|
||||
53,Environment,Use NUXT_ prefix for env override,Override config with environment variables,NUXT_API_SECRET NUXT_PUBLIC_API_BASE,Custom env var names,NUXT_PUBLIC_API_BASE=https://api.example.com,API_BASE=https://api.example.com,High,https://nuxt.com/docs/guide/going-further/runtime-config
|
||||
54,Environment,Access public config with useRuntimeConfig,Get public config in components,useRuntimeConfig().public,Direct process.env access,const config = useRuntimeConfig(); config.public.apiBase,process.env.NUXT_PUBLIC_API_BASE,High,https://nuxt.com/docs/api/composables/use-runtime-config
|
||||
55,Environment,Keep secrets in private config,Server-only secrets in runtimeConfig root,runtimeConfig.apiSecret (server only),Secrets in public config,runtimeConfig: { dbPassword: '' },runtimeConfig: { public: { dbPassword: '' } },High,https://nuxt.com/docs/guide/going-further/runtime-config
|
||||
56,Performance,Use Lazy prefix for code splitting,Lazy load components with Lazy prefix,<LazyComponent> for below-fold,Eager load all components,<LazyMountainsList v-if="show"/>,<MountainsList/> for hidden content,Medium,https://nuxt.com/docs/guide/directory-structure/components
|
||||
57,Performance,Use useLazyFetch for non-blocking data,Alias for useFetch with lazy: true,useLazyFetch for secondary data,useFetch for all requests,"const { data } = useLazyFetch('/api/comments')",await useFetch for comments section,Medium,https://nuxt.com/docs/api/composables/use-lazy-fetch
|
||||
58,Performance,Use lazy hydration for interactivity,Delay component hydration until needed,LazyComponent with hydration strategy,Immediate hydration for all,<LazyModal hydrate-on-visible/>,<Modal/> in footer,Low,https://nuxt.com/docs/guide/going-further/experimental-features
|
||||
|
Can't render this file because it contains an unexpected character in line 8 and column 193.
|
52
.claude/skills/ui-ux-pro-max/data/stacks/react-native.csv
Normal file
52
.claude/skills/ui-ux-pro-max/data/stacks/react-native.csv
Normal file
@ -0,0 +1,52 @@
|
||||
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
|
||||
1,Components,Use functional components,Hooks-based components are standard,Functional components with hooks,Class components,const App = () => { },class App extends Component,Medium,https://reactnative.dev/docs/intro-react
|
||||
2,Components,Keep components small,Single responsibility principle,Split into smaller components,Large monolithic components,<Header /><Content /><Footer />,500+ line component,Medium,
|
||||
3,Components,Use TypeScript,Type safety for props and state,TypeScript for new projects,JavaScript without types,const Button: FC<Props> = () => { },const Button = (props) => { },Medium,
|
||||
4,Components,Colocate component files,Keep related files together,Component folder with styles,Flat structure,components/Button/index.tsx styles.ts,components/Button.tsx styles/button.ts,Low,
|
||||
5,Styling,Use StyleSheet.create,Optimized style objects,StyleSheet for all styles,Inline style objects,StyleSheet.create({ container: {} }),style={{ margin: 10 }},High,https://reactnative.dev/docs/stylesheet
|
||||
6,Styling,Avoid inline styles,Prevent object recreation,Styles in StyleSheet,Inline style objects in render,style={styles.container},"style={{ margin: 10, padding: 5 }}",Medium,
|
||||
7,Styling,Use flexbox for layout,React Native uses flexbox,flexDirection alignItems justifyContent,Absolute positioning everywhere,flexDirection: 'row',position: 'absolute' everywhere,Medium,https://reactnative.dev/docs/flexbox
|
||||
8,Styling,Handle platform differences,Platform-specific styles,Platform.select or .ios/.android files,Same styles for both platforms,"Platform.select({ ios: {}, android: {} })",Hardcoded iOS values,Medium,https://reactnative.dev/docs/platform-specific-code
|
||||
9,Styling,Use responsive dimensions,Scale for different screens,Dimensions or useWindowDimensions,Fixed pixel values,useWindowDimensions(),width: 375,Medium,
|
||||
10,Navigation,Use React Navigation,Standard navigation library,React Navigation for routing,Manual navigation management,createStackNavigator(),Custom navigation state,Medium,https://reactnavigation.org/
|
||||
11,Navigation,Type navigation params,Type-safe navigation,Typed navigation props,Untyped navigation,"navigation.navigate<RootStackParamList>('Home', { id })","navigation.navigate('Home', { id })",Medium,
|
||||
12,Navigation,Use deep linking,Support URL-based navigation,Configure linking prop,No deep link support,linking: { prefixes: [] },No linking configuration,Medium,https://reactnavigation.org/docs/deep-linking/
|
||||
13,Navigation,Handle back button,Android back button handling,useFocusEffect with BackHandler,Ignore back button,BackHandler.addEventListener,No back handler,High,
|
||||
14,State,Use useState for local state,Simple component state,useState for UI state,Class component state,"const [count, setCount] = useState(0)",this.state = { count: 0 },Medium,
|
||||
15,State,Use useReducer for complex state,Complex state logic,useReducer for related state,Multiple useState for related values,useReducer(reducer initialState),5+ useState calls,Medium,
|
||||
16,State,Use context sparingly,Context for global state,Context for theme auth locale,Context for frequently changing data,ThemeContext for app theme,Context for list item data,Medium,
|
||||
17,State,Consider Zustand or Redux,External state management,Zustand for simple Redux for complex,useState for global state,create((set) => ({ })),Prop drilling global state,Medium,
|
||||
18,Lists,Use FlatList for long lists,Virtualized list rendering,FlatList for 50+ items,ScrollView with map,<FlatList data={items} />,<ScrollView>{items.map()}</ScrollView>,High,https://reactnative.dev/docs/flatlist
|
||||
19,Lists,Provide keyExtractor,Unique keys for list items,keyExtractor with stable ID,Index as key,keyExtractor={(item) => item.id},"keyExtractor={(_, index) => index}",High,
|
||||
20,Lists,Optimize renderItem,Memoize list item components,React.memo for list items,Inline render function,renderItem={({ item }) => <MemoizedItem item={item} />},renderItem={({ item }) => <View>...</View>},High,
|
||||
21,Lists,Use getItemLayout for fixed height,Skip measurement for performance,getItemLayout when height known,Dynamic measurement for fixed items,"getItemLayout={(_, index) => ({ length: 50, offset: 50 * index, index })}",No getItemLayout for fixed height,Medium,
|
||||
22,Lists,Implement windowSize,Control render window,Smaller windowSize for memory,Default windowSize for large lists,windowSize={5},windowSize={21} for huge lists,Medium,
|
||||
23,Performance,Use React.memo,Prevent unnecessary re-renders,memo for pure components,No memoization,export default memo(MyComponent),export default MyComponent,Medium,
|
||||
24,Performance,Use useCallback for handlers,Stable function references,useCallback for props,New function on every render,"useCallback(() => {}, [deps])",() => handlePress(),Medium,
|
||||
25,Performance,Use useMemo for expensive ops,Cache expensive calculations,useMemo for heavy computations,Recalculate every render,"useMemo(() => expensive(), [deps])",const result = expensive(),Medium,
|
||||
26,Performance,Avoid anonymous functions in JSX,Prevent re-renders,Named handlers or useCallback,Inline arrow functions,onPress={handlePress},onPress={() => doSomething()},Medium,
|
||||
27,Performance,Use Hermes engine,Improved startup and memory,Enable Hermes in build,JavaScriptCore for new projects,hermes_enabled: true,hermes_enabled: false,Medium,https://reactnative.dev/docs/hermes
|
||||
28,Images,Use expo-image,Modern performant image component for React Native,"Use expo-image for caching, blurring, and performance",Use default Image for heavy lists or unmaintained libraries,<Image source={url} cachePolicy='memory-disk' /> (expo-image),<FastImage source={url} />,Medium,https://docs.expo.dev/versions/latest/sdk/image/
|
||||
29,Images,Specify image dimensions,Prevent layout shifts,width and height for remote images,No dimensions for network images,<Image style={{ width: 100 height: 100 }} />,<Image source={{ uri }} /> no size,High,
|
||||
30,Images,Use resizeMode,Control image scaling,resizeMode cover contain,Stretch images,"resizeMode=""cover""",No resizeMode,Low,
|
||||
31,Forms,Use controlled inputs,State-controlled form fields,value + onChangeText,Uncontrolled inputs,<TextInput value={text} onChangeText={setText} />,<TextInput defaultValue={text} />,Medium,
|
||||
32,Forms,Handle keyboard,Manage keyboard visibility,KeyboardAvoidingView,Content hidden by keyboard,"<KeyboardAvoidingView behavior=""padding"">",No keyboard handling,High,https://reactnative.dev/docs/keyboardavoidingview
|
||||
33,Forms,Use proper keyboard types,Appropriate keyboard for input,keyboardType for input type,Default keyboard for all,"keyboardType=""email-address""","keyboardType=""default"" for email",Low,
|
||||
34,Touch,Use Pressable,Modern touch handling,Pressable for touch interactions,TouchableOpacity for new code,<Pressable onPress={} />,<TouchableOpacity onPress={} />,Low,https://reactnative.dev/docs/pressable
|
||||
35,Touch,Provide touch feedback,Visual feedback on press,Ripple or opacity change,No feedback on press,android_ripple={{ color: 'gray' }},No press feedback,Medium,
|
||||
36,Touch,Set hitSlop for small targets,Increase touch area,hitSlop for icons and small buttons,Tiny touch targets,hitSlop={{ top: 10 bottom: 10 }},44x44 with no hitSlop,Medium,
|
||||
37,Animation,Use Reanimated,High-performance animations,react-native-reanimated,Animated API for complex,useSharedValue useAnimatedStyle,Animated.timing for gesture,Medium,https://docs.swmansion.com/react-native-reanimated/
|
||||
38,Animation,Run on UI thread,worklets for smooth animation,Run animations on UI thread,JS thread animations,runOnUI(() => {}),Animated on JS thread,High,
|
||||
39,Animation,Use gesture handler,Native gesture recognition,react-native-gesture-handler,JS-based gesture handling,<GestureDetector>,<View onTouchMove={} />,Medium,https://docs.swmansion.com/react-native-gesture-handler/
|
||||
40,Async,Handle loading states,Show loading indicators,ActivityIndicator during load,Empty screen during load,{isLoading ? <ActivityIndicator /> : <Content />},No loading state,Medium,
|
||||
41,Async,Handle errors gracefully,Error boundaries and fallbacks,Error UI for failed requests,Crash on error,{error ? <ErrorView /> : <Content />},No error handling,High,
|
||||
42,Async,Cancel async operations,Cleanup on unmount,AbortController or cleanup,Memory leaks from async,useEffect cleanup,No cleanup for subscriptions,High,
|
||||
43,Accessibility,Add accessibility labels,Describe UI elements,accessibilityLabel for all interactive,Missing labels,"accessibilityLabel=""Submit form""",<Pressable> without label,High,https://reactnative.dev/docs/accessibility
|
||||
44,Accessibility,Use accessibility roles,Semantic meaning,accessibilityRole for elements,Wrong roles,"accessibilityRole=""button""",No role for button,Medium,
|
||||
45,Accessibility,Support screen readers,Test with TalkBack/VoiceOver,Test with screen readers,Skip accessibility testing,Regular TalkBack testing,No screen reader testing,High,
|
||||
46,Testing,Use React Native Testing Library,Component testing,render and fireEvent,Enzyme or manual testing,render(<Component />),shallow(<Component />),Medium,https://callstack.github.io/react-native-testing-library/
|
||||
47,Testing,Test on real devices,Real device behavior,Test on iOS and Android devices,Simulator only,Device testing in CI,Simulator only testing,High,
|
||||
48,Testing,Use Detox for E2E,End-to-end testing,Detox for critical flows,Manual E2E testing,detox test,Manual testing only,Medium,https://wix.github.io/Detox/
|
||||
49,Native,Use native modules carefully,Bridge has overhead,Batch native calls,Frequent bridge crossing,Batch updates,Call native on every keystroke,High,
|
||||
50,Native,Use Expo when possible,Simplified development,Expo for standard features,Bare RN for simple apps,expo install package,react-native link package,Low,https://docs.expo.dev/
|
||||
51,Native,Handle permissions,Request permissions properly,Check and request permissions,Assume permissions granted,PermissionsAndroid.request(),Access without permission check,High,https://reactnative.dev/docs/permissionsandroid
|
||||
|
54
.claude/skills/ui-ux-pro-max/data/stacks/react.csv
Normal file
54
.claude/skills/ui-ux-pro-max/data/stacks/react.csv
Normal file
@ -0,0 +1,54 @@
|
||||
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
|
||||
1,State,Use useState for local state,Simple component state should use useState hook,useState for form inputs toggles counters,Class components this.state,"const [count, setCount] = useState(0)",this.state = { count: 0 },Medium,https://react.dev/reference/react/useState
|
||||
2,State,Lift state up when needed,Share state between siblings by lifting to parent,Lift shared state to common ancestor,Prop drilling through many levels,Parent holds state passes down,Deep prop chains,Medium,https://react.dev/learn/sharing-state-between-components
|
||||
3,State,Use useReducer for complex state,Complex state logic benefits from reducer pattern,useReducer for state with multiple sub-values,Multiple useState for related values,useReducer with action types,5+ useState calls that update together,Medium,https://react.dev/reference/react/useReducer
|
||||
4,State,Avoid unnecessary state,Derive values from existing state when possible,Compute derived values in render,Store derivable values in state,const total = items.reduce(...),"const [total, setTotal] = useState(0)",High,https://react.dev/learn/choosing-the-state-structure
|
||||
5,State,Initialize state lazily,Use function form for expensive initial state,useState(() => computeExpensive()),useState(computeExpensive()),useState(() => JSON.parse(data)),useState(JSON.parse(data)),Medium,https://react.dev/reference/react/useState#avoiding-recreating-the-initial-state
|
||||
6,Effects,Clean up effects,Return cleanup function for subscriptions timers,Return cleanup function in useEffect,No cleanup for subscriptions,useEffect(() => { sub(); return unsub; }),useEffect(() => { subscribe(); }),High,https://react.dev/reference/react/useEffect#connecting-to-an-external-system
|
||||
7,Effects,Specify dependencies correctly,Include all values used inside effect in deps array,All referenced values in dependency array,Empty deps with external references,[value] when using value in effect,[] when using props/state in effect,High,https://react.dev/reference/react/useEffect#specifying-reactive-dependencies
|
||||
8,Effects,Avoid unnecessary effects,Don't use effects for transforming data or events,Transform data during render handle events directly,useEffect for derived state or event handling,const filtered = items.filter(...),useEffect(() => setFiltered(items.filter(...))),High,https://react.dev/learn/you-might-not-need-an-effect
|
||||
9,Effects,Use refs for non-reactive values,Store values that don't trigger re-renders in refs,useRef for interval IDs DOM elements,useState for values that don't need render,const intervalRef = useRef(null),"const [intervalId, setIntervalId] = useState()",Medium,https://react.dev/reference/react/useRef
|
||||
10,Rendering,Use keys properly,Stable unique keys for list items,Use stable IDs as keys,Array index as key for dynamic lists,key={item.id},key={index},High,https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key
|
||||
11,Rendering,Memoize expensive calculations,Use useMemo for costly computations,useMemo for expensive filtering/sorting,Recalculate every render,"useMemo(() => expensive(), [deps])",const result = expensiveCalc(),Medium,https://react.dev/reference/react/useMemo
|
||||
12,Rendering,Memoize callbacks passed to children,Use useCallback for functions passed as props,useCallback for handlers passed to memoized children,New function reference every render,"useCallback(() => {}, [deps])",const handler = () => {},Medium,https://react.dev/reference/react/useCallback
|
||||
13,Rendering,Use React.memo wisely,Wrap components that render often with same props,memo for pure components with stable props,memo everything or nothing,memo(ExpensiveList),memo(SimpleButton),Low,https://react.dev/reference/react/memo
|
||||
14,Rendering,Avoid inline object/array creation in JSX,Create objects outside render or memoize,Define style objects outside component,Inline objects in props,<div style={styles.container}>,<div style={{ margin: 10 }}>,Medium,
|
||||
15,Components,Keep components small and focused,Single responsibility for each component,One concern per component,Large multi-purpose components,<UserAvatar /><UserName />,<UserCard /> with 500 lines,Medium,
|
||||
16,Components,Use composition over inheritance,Compose components using children and props,Use children prop for flexibility,Inheritance hierarchies,<Card>{content}</Card>,class SpecialCard extends Card,Medium,https://react.dev/learn/thinking-in-react
|
||||
17,Components,Colocate related code,Keep related components and hooks together,Related files in same directory,Flat structure with many files,components/User/UserCard.tsx,components/UserCard.tsx + hooks/useUser.ts,Low,
|
||||
18,Components,Use fragments to avoid extra DOM,Fragment or <> for multiple elements without wrapper,<> for grouping without DOM node,Extra div wrappers,<>{items.map(...)}</>,<div>{items.map(...)}</div>,Low,https://react.dev/reference/react/Fragment
|
||||
19,Props,Destructure props,Destructure props for cleaner component code,Destructure in function signature,props.name props.value throughout,"function User({ name, age })",function User(props),Low,
|
||||
20,Props,Provide default props values,Use default parameters or defaultProps,Default values in destructuring,Undefined checks throughout,function Button({ size = 'md' }),if (size === undefined) size = 'md',Low,
|
||||
21,Props,Avoid prop drilling,Use context or composition for deeply nested data,Context for global data composition for UI,Passing props through 5+ levels,<UserContext.Provider>,<A user={u}><B user={u}><C user={u}>,Medium,https://react.dev/learn/passing-data-deeply-with-context
|
||||
22,Props,Validate props with TypeScript,Use TypeScript interfaces for prop types,interface Props { name: string },PropTypes or no validation,interface ButtonProps { onClick: () => void },Button.propTypes = {},Medium,
|
||||
23,Events,Use synthetic events correctly,React normalizes events across browsers,e.preventDefault() e.stopPropagation(),Access native event unnecessarily,onClick={(e) => e.preventDefault()},onClick={(e) => e.nativeEvent.preventDefault()},Low,https://react.dev/reference/react-dom/components/common#react-event-object
|
||||
24,Events,Avoid binding in render,Use arrow functions in class or hooks,Arrow functions in functional components,bind in render or constructor,const handleClick = () => {},this.handleClick.bind(this),Medium,
|
||||
25,Events,Pass event handlers not call results,Pass function reference not invocation,onClick={handleClick},onClick={handleClick()} causing immediate call,onClick={handleClick},onClick={handleClick()},High,
|
||||
26,Forms,Controlled components for forms,Use state to control form inputs,value + onChange for inputs,Uncontrolled inputs with refs,<input value={val} onChange={setVal}>,<input ref={inputRef}>,Medium,https://react.dev/reference/react-dom/components/input#controlling-an-input-with-a-state-variable
|
||||
27,Forms,Handle form submission properly,Prevent default and handle in submit handler,onSubmit with preventDefault,onClick on submit button only,<form onSubmit={handleSubmit}>,<button onClick={handleSubmit}>,Medium,
|
||||
28,Forms,Debounce rapid input changes,Debounce search/filter inputs,useDeferredValue or debounce for search,Filter on every keystroke,useDeferredValue(searchTerm),useEffect filtering on every change,Medium,https://react.dev/reference/react/useDeferredValue
|
||||
29,Hooks,Follow rules of hooks,Only call hooks at top level and in React functions,Hooks at component top level,Hooks in conditions loops or callbacks,"const [x, setX] = useState()","if (cond) { const [x, setX] = useState() }",High,https://react.dev/reference/rules/rules-of-hooks
|
||||
30,Hooks,Custom hooks for reusable logic,Extract shared stateful logic to custom hooks,useCustomHook for reusable patterns,Duplicate hook logic across components,const { data } = useFetch(url),Duplicate useEffect/useState in components,Medium,https://react.dev/learn/reusing-logic-with-custom-hooks
|
||||
31,Hooks,Name custom hooks with use prefix,Custom hooks must start with use,useFetch useForm useAuth,fetchData or getData for hook,function useFetch(url),function fetchData(url),High,
|
||||
32,Context,Use context for global data,Context for theme auth locale,Context for app-wide state,Context for frequently changing data,<ThemeContext.Provider>,Context for form field values,Medium,https://react.dev/learn/passing-data-deeply-with-context
|
||||
33,Context,Split contexts by concern,Separate contexts for different domains,ThemeContext + AuthContext,One giant AppContext,<ThemeProvider><AuthProvider>,<AppProvider value={{theme user...}}>,Medium,
|
||||
34,Context,Memoize context values,Prevent unnecessary re-renders with useMemo,useMemo for context value object,New object reference every render,"value={useMemo(() => ({...}), [])}","value={{ user, theme }}",High,
|
||||
35,Performance,Use React DevTools Profiler,Profile to identify performance bottlenecks,Profile before optimizing,Optimize without measuring,React DevTools Profiler,Guessing at bottlenecks,Medium,https://react.dev/learn/react-developer-tools
|
||||
36,Performance,Lazy load components,Use React.lazy for code splitting,lazy() for routes and heavy components,Import everything upfront,const Page = lazy(() => import('./Page')),import Page from './Page',Medium,https://react.dev/reference/react/lazy
|
||||
37,Performance,Virtualize long lists,Use windowing for lists over 100 items,react-window or react-virtual,Render thousands of DOM nodes,<VirtualizedList items={items}/>,{items.map(i => <Item />)},High,
|
||||
38,Performance,Batch state updates,React 18 auto-batches but be aware,Let React batch related updates,Manual batching with flushSync,setA(1); setB(2); // batched,flushSync(() => setA(1)),Low,https://react.dev/learn/queueing-a-series-of-state-updates
|
||||
39,ErrorHandling,Use error boundaries,Catch JavaScript errors in component tree,ErrorBoundary wrapping sections,Let errors crash entire app,<ErrorBoundary><App/></ErrorBoundary>,No error handling,High,https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
|
||||
40,ErrorHandling,Handle async errors,Catch errors in async operations,try/catch in async handlers,Unhandled promise rejections,try { await fetch() } catch(e) {},await fetch() // no catch,High,
|
||||
41,Testing,Test behavior not implementation,Test what user sees and does,Test renders and interactions,Test internal state or methods,expect(screen.getByText('Hello')),expect(component.state.name),Medium,https://testing-library.com/docs/react-testing-library/intro/
|
||||
42,Testing,Use testing-library queries,Use accessible queries,getByRole getByLabelText,getByTestId for everything,getByRole('button'),getByTestId('submit-btn'),Medium,https://testing-library.com/docs/queries/about#priority
|
||||
43,Accessibility,Use semantic HTML,Proper HTML elements for their purpose,button for clicks nav for navigation,div with onClick for buttons,<button onClick={...}>,<div onClick={...}>,High,https://react.dev/reference/react-dom/components#all-html-components
|
||||
44,Accessibility,Manage focus properly,Handle focus for modals dialogs,Focus trap in modals return focus on close,No focus management,useEffect to focus input,Modal without focus trap,High,
|
||||
45,Accessibility,Announce dynamic content,Use ARIA live regions for updates,aria-live for dynamic updates,Silent updates to screen readers,"<div aria-live=""polite"">{msg}</div>",<div>{msg}</div>,Medium,
|
||||
46,Accessibility,Label form controls,Associate labels with inputs,htmlFor matching input id,Placeholder as only label,"<label htmlFor=""email"">Email</label>","<input placeholder=""Email""/>",High,
|
||||
47,TypeScript,Type component props,Define interfaces for all props,interface Props with all prop types,any or missing types,interface Props { name: string },function Component(props: any),High,
|
||||
48,TypeScript,Type state properly,Provide types for useState,useState<Type>() for complex state,Inferred any types,useState<User | null>(null),useState(null),Medium,
|
||||
49,TypeScript,Type event handlers,Use React event types,React.ChangeEvent<HTMLInputElement>,Generic Event type,onChange: React.ChangeEvent<HTMLInputElement>,onChange: Event,Medium,
|
||||
50,TypeScript,Use generics for reusable components,Generic components for flexible typing,Generic props for list components,Union types for flexibility,<List<T> items={T[]}>,<List items={any[]}>,Medium,
|
||||
51,Patterns,Container/Presentational split,Separate data logic from UI,Container fetches presentational renders,Mixed data and UI in one,<UserContainer><UserView/></UserContainer>,<User /> with fetch and render,Low,
|
||||
52,Patterns,Render props for flexibility,Share code via render prop pattern,Render prop for customizable rendering,Duplicate logic across components,<DataFetcher render={data => ...}/>,Copy paste fetch logic,Low,https://react.dev/reference/react/cloneElement#passing-data-with-a-render-prop
|
||||
53,Patterns,Compound components,Related components sharing state,Tab + TabPanel sharing context,Prop drilling between related,<Tabs><Tab/><TabPanel/></Tabs>,<Tabs tabs={[]} panels={[...]}/>,Low,
|
||||
|
54
.claude/skills/ui-ux-pro-max/data/stacks/svelte.csv
Normal file
54
.claude/skills/ui-ux-pro-max/data/stacks/svelte.csv
Normal file
@ -0,0 +1,54 @@
|
||||
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
|
||||
1,Reactivity,Use $: for reactive statements,Automatic dependency tracking,$: for derived values,Manual recalculation,$: doubled = count * 2,let doubled; count && (doubled = count * 2),Medium,https://svelte.dev/docs/svelte-components#script-3-$-marks-a-statement-as-reactive
|
||||
2,Reactivity,Trigger reactivity with assignment,Svelte tracks assignments not mutations,Reassign arrays/objects to trigger update,Mutate without reassignment,"items = [...items, newItem]",items.push(newItem),High,https://svelte.dev/docs/svelte-components#script-2-assignments-are-reactive
|
||||
3,Reactivity,Use $state in Svelte 5,Runes for explicit reactivity,let count = $state(0),Implicit reactivity in Svelte 5,let count = $state(0),let count = 0 (Svelte 5),Medium,https://svelte.dev/blog/runes
|
||||
4,Reactivity,Use $derived for computed values,$derived replaces $: in Svelte 5,let doubled = $derived(count * 2),$: in Svelte 5,let doubled = $derived(count * 2),$: doubled = count * 2 (Svelte 5),Medium,
|
||||
5,Reactivity,Use $effect for side effects,$effect replaces $: side effects,Use $effect for subscriptions,$: for side effects in Svelte 5,$effect(() => console.log(count)),$: console.log(count) (Svelte 5),Medium,
|
||||
6,Props,Export let for props,Declare props with export let,export let propName,Props without export,export let count = 0,let count = 0,High,https://svelte.dev/docs/svelte-components#script-1-export-creates-a-component-prop
|
||||
7,Props,Use $props in Svelte 5,$props rune for prop access,let { name } = $props(),export let in Svelte 5,"let { name, age = 0 } = $props()",export let name; export let age = 0,Medium,
|
||||
8,Props,Provide default values,Default props with assignment,export let count = 0,Required props without defaults,export let count = 0,export let count,Low,
|
||||
9,Props,Use spread props,Pass through unknown props,{...$$restProps} on elements,Manual prop forwarding,<button {...$$restProps}>,<button class={$$props.class}>,Low,https://svelte.dev/docs/basic-markup#attributes-and-props
|
||||
10,Bindings,Use bind: for two-way binding,Simplified input handling,bind:value for inputs,on:input with manual update,<input bind:value={name}>,<input value={name} on:input={e => name = e.target.value}>,Low,https://svelte.dev/docs/element-directives#bind-property
|
||||
11,Bindings,Bind to DOM elements,Reference DOM nodes,bind:this for element reference,querySelector in onMount,<div bind:this={el}>,onMount(() => el = document.querySelector()),Medium,
|
||||
12,Bindings,Use bind:group for radios/checkboxes,Simplified group handling,bind:group for radio/checkbox groups,Manual checked handling,"<input type=""radio"" bind:group={selected}>","<input type=""radio"" checked={selected === value}>",Low,
|
||||
13,Events,Use on: for event handlers,Event directive syntax,on:click={handler},addEventListener in onMount,<button on:click={handleClick}>,onMount(() => btn.addEventListener()),Medium,https://svelte.dev/docs/element-directives#on-eventname
|
||||
14,Events,Forward events with on:event,Pass events to parent,on:click without handler,createEventDispatcher for DOM events,<button on:click>,"dispatch('click', event)",Low,
|
||||
15,Events,Use createEventDispatcher,Custom component events,dispatch for custom events,on:event for custom events,"dispatch('save', { data })",on:save without dispatch,Medium,https://svelte.dev/docs/svelte#createeventdispatcher
|
||||
16,Lifecycle,Use onMount for initialization,Run code after component mounts,onMount for setup and data fetching,Code in script body for side effects,onMount(() => fetchData()),fetchData() in script body,High,https://svelte.dev/docs/svelte#onmount
|
||||
17,Lifecycle,Return cleanup from onMount,Automatic cleanup on destroy,Return function from onMount,Separate onDestroy for paired cleanup,onMount(() => { sub(); return unsub }),onMount(sub); onDestroy(unsub),Medium,
|
||||
18,Lifecycle,Use onDestroy sparingly,Only when onMount cleanup not possible,onDestroy for non-mount cleanup,onDestroy for mount-related cleanup,onDestroy for store unsubscribe,onDestroy(() => clearInterval(id)),Low,
|
||||
19,Lifecycle,Avoid beforeUpdate/afterUpdate,Usually not needed,Reactive statements instead,beforeUpdate for derived state,$: if (x) doSomething(),beforeUpdate(() => doSomething()),Low,
|
||||
20,Stores,Use writable for mutable state,Basic reactive store,writable for shared mutable state,Local variables for shared state,const count = writable(0),let count = 0 in module,Medium,https://svelte.dev/docs/svelte-store#writable
|
||||
21,Stores,Use readable for read-only state,External data sources,readable for derived/external data,writable for read-only data,"readable(0, set => interval(set))",writable(0) for timer,Low,https://svelte.dev/docs/svelte-store#readable
|
||||
22,Stores,Use derived for computed stores,Combine or transform stores,derived for computed values,Manual subscription for derived,"derived(count, $c => $c * 2)",count.subscribe(c => doubled = c * 2),Medium,https://svelte.dev/docs/svelte-store#derived
|
||||
23,Stores,Use $ prefix for auto-subscription,Automatic subscribe/unsubscribe,$storeName in components,Manual subscription,{$count},count.subscribe(c => value = c),High,
|
||||
24,Stores,Clean up custom subscriptions,Unsubscribe when component destroys,Return unsubscribe from onMount,Leave subscriptions open,onMount(() => store.subscribe(fn)),store.subscribe(fn) in script,High,
|
||||
25,Slots,Use slots for composition,Content projection,<slot> for flexible content,Props for all content,<slot>Default</slot>,"<Component content=""text""/>",Medium,https://svelte.dev/docs/special-elements#slot
|
||||
26,Slots,Name slots for multiple areas,Multiple content areas,"<slot name=""header"">",Single slot for complex layouts,"<slot name=""header""><slot name=""footer"">",<slot> with complex conditionals,Low,
|
||||
27,Slots,Check slot content with $$slots,Conditional slot rendering,$$slots.name for conditional rendering,Always render slot wrapper,"{#if $$slots.footer}<slot name=""footer""/>{/if}","<div><slot name=""footer""/></div>",Low,
|
||||
28,Styling,Use scoped styles by default,Styles scoped to component,<style> for component styles,Global styles for component,:global() only when needed,<style> all global,Medium,https://svelte.dev/docs/svelte-components#style
|
||||
29,Styling,Use :global() sparingly,Escape scoping when needed,:global for third-party styling,Global for all styles,:global(.external-lib),<style> without scoping,Medium,
|
||||
30,Styling,Use CSS variables for theming,Dynamic styling,CSS custom properties,Inline styles for themes,"style=""--color: {color}""","style=""color: {color}""",Low,
|
||||
31,Transitions,Use built-in transitions,Svelte transition directives,transition:fade for simple effects,Manual CSS transitions,<div transition:fade>,<div class:fade={visible}>,Low,https://svelte.dev/docs/element-directives#transition-fn
|
||||
32,Transitions,Use in: and out: separately,Different enter/exit animations,in:fly out:fade for asymmetric,Same transition for both,<div in:fly out:fade>,<div transition:fly>,Low,
|
||||
33,Transitions,Add local modifier,Prevent ancestor trigger,transition:fade|local,Global transitions for lists,<div transition:slide|local>,<div transition:slide>,Medium,
|
||||
34,Actions,Use actions for DOM behavior,Reusable DOM logic,use:action for DOM enhancements,onMount for each usage,<div use:clickOutside>,onMount(() => setupClickOutside(el)),Medium,https://svelte.dev/docs/element-directives#use-action
|
||||
35,Actions,Return update and destroy,Lifecycle methods for actions,"Return { update, destroy }",Only initial setup,"return { update(params) {}, destroy() {} }",return destroy only,Medium,
|
||||
36,Actions,Pass parameters to actions,Configure action behavior,use:action={params},Hardcoded action behavior,<div use:tooltip={options}>,<div use:tooltip>,Low,
|
||||
37,Logic,Use {#if} for conditionals,Template conditionals,{#if} {:else if} {:else},Ternary in expressions,{#if cond}...{:else}...{/if},{cond ? a : b} for complex,Low,https://svelte.dev/docs/logic-blocks#if
|
||||
38,Logic,Use {#each} for lists,List rendering,{#each} with key,Map in expression,{#each items as item (item.id)},{items.map(i => `<div>${i}</div>`)},Medium,
|
||||
39,Logic,Always use keys in {#each},Proper list reconciliation,(item.id) for unique key,Index as key or no key,{#each items as item (item.id)},"{#each items as item, i (i)}",High,
|
||||
40,Logic,Use {#await} for promises,Handle async states,{#await} for loading/error states,Manual promise handling,{#await promise}...{:then}...{:catch},{#if loading}...{#if error},Medium,https://svelte.dev/docs/logic-blocks#await
|
||||
41,SvelteKit,Use +page.svelte for routes,File-based routing,+page.svelte for route components,Custom routing setup,routes/about/+page.svelte,routes/About.svelte,Medium,https://kit.svelte.dev/docs/routing
|
||||
42,SvelteKit,Use +page.js for data loading,Load data before render,load function in +page.js,onMount for data fetching,export function load() {},onMount(() => fetchData()),High,https://kit.svelte.dev/docs/load
|
||||
43,SvelteKit,Use +page.server.js for server-only,Server-side data loading,+page.server.js for sensitive data,+page.js for API keys,+page.server.js with DB access,+page.js with DB access,High,
|
||||
44,SvelteKit,Use form actions,Server-side form handling,+page.server.js actions,API routes for forms,export const actions = { default },fetch('/api/submit'),Medium,https://kit.svelte.dev/docs/form-actions
|
||||
45,SvelteKit,Use $app/stores for app state,$page $navigating $updated,$page for current page data,Manual URL parsing,import { page } from '$app/stores',window.location.pathname,Medium,https://kit.svelte.dev/docs/modules#$app-stores
|
||||
46,Performance,Use {#key} for forced re-render,Reset component state,{#key id} for fresh instance,Manual destroy/create,{#key item.id}<Component/>{/key},on:change={() => component = null},Low,https://svelte.dev/docs/logic-blocks#key
|
||||
47,Performance,Avoid unnecessary reactivity,Not everything needs $:,$: only for side effects,$: for simple assignments,$: if (x) console.log(x),$: y = x (when y = x works),Low,
|
||||
48,Performance,Use immutable compiler option,Skip equality checks,immutable: true for large lists,Default for all components,<svelte:options immutable/>,Default without immutable,Low,
|
||||
49,TypeScript,"Use lang=""ts"" in script",TypeScript support,"<script lang=""ts"">",JavaScript for typed projects,"<script lang=""ts"">",<script> with JSDoc,Medium,https://svelte.dev/docs/typescript
|
||||
50,TypeScript,Type props with interface,Explicit prop types,interface $$Props for types,Untyped props,interface $$Props { name: string },export let name,Medium,
|
||||
51,TypeScript,Type events with createEventDispatcher,Type-safe events,createEventDispatcher<Events>(),Untyped dispatch,createEventDispatcher<{ save: Data }>(),createEventDispatcher(),Medium,
|
||||
52,Accessibility,Use semantic elements,Proper HTML in templates,button nav main appropriately,div for everything,<button on:click>,<div on:click>,High,
|
||||
53,Accessibility,Add aria to dynamic content,Accessible state changes,aria-live for updates,Silent dynamic updates,"<div aria-live=""polite"">{message}</div>",<div>{message}</div>,Medium,
|
||||
|
51
.claude/skills/ui-ux-pro-max/data/stacks/swiftui.csv
Normal file
51
.claude/skills/ui-ux-pro-max/data/stacks/swiftui.csv
Normal file
@ -0,0 +1,51 @@
|
||||
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
|
||||
1,Views,Use struct for views,SwiftUI views are value types,struct MyView: View,class MyView: View,struct ContentView: View { var body: some View },class ContentView: View,High,https://developer.apple.com/documentation/swiftui/view
|
||||
2,Views,Keep views small and focused,Single responsibility for each view,Extract subviews for complex layouts,Large monolithic views,Extract HeaderView FooterView,500+ line View struct,Medium,
|
||||
3,Views,Use body computed property,body returns the view hierarchy,var body: some View { },func body() -> some View,"var body: some View { Text(""Hello"") }",func body() -> Text,High,
|
||||
4,Views,Prefer composition over inheritance,Compose views using ViewBuilder,Combine smaller views,Inheritance hierarchies,VStack { Header() Content() },class SpecialView extends BaseView,Medium,
|
||||
5,State,Use @State for local state,Simple value types owned by view,@State for view-local primitives,@State for shared data,@State private var count = 0,@State var sharedData: Model,High,https://developer.apple.com/documentation/swiftui/state
|
||||
6,State,Use @Binding for two-way data,Pass mutable state to child views,@Binding for child input,@State in child for parent data,@Binding var isOn: Bool,$isOn to pass binding,Medium,https://developer.apple.com/documentation/swiftui/binding
|
||||
7,State,Use @StateObject for reference types,ObservableObject owned by view,@StateObject for view-created objects,@ObservedObject for owned objects,@StateObject private var vm = ViewModel(),@ObservedObject var vm = ViewModel(),High,https://developer.apple.com/documentation/swiftui/stateobject
|
||||
8,State,Use @ObservedObject for injected objects,Reference types passed from parent,@ObservedObject for injected dependencies,@StateObject for injected objects,@ObservedObject var vm: ViewModel,@StateObject var vm: ViewModel (injected),High,https://developer.apple.com/documentation/swiftui/observedobject
|
||||
9,State,Use @EnvironmentObject for shared state,App-wide state injection,@EnvironmentObject for global state,Prop drilling through views,@EnvironmentObject var settings: Settings,Pass settings through 5 views,Medium,https://developer.apple.com/documentation/swiftui/environmentobject
|
||||
10,State,Use @Published in ObservableObject,Automatically publish property changes,@Published for observed properties,Manual objectWillChange calls,@Published var items: [Item] = [],var items: [Item] { didSet { objectWillChange.send() } },Medium,
|
||||
11,Observable,Use @Observable macro (iOS 17+),Modern observation without Combine,@Observable class for view models,ObservableObject for new projects,@Observable class ViewModel { },class ViewModel: ObservableObject,Medium,https://developer.apple.com/documentation/observation
|
||||
12,Observable,Use @Bindable for @Observable,Create bindings from @Observable,@Bindable var vm for bindings,@Binding with @Observable,@Bindable var viewModel,$viewModel.name with @Observable,Medium,
|
||||
13,Layout,Use VStack HStack ZStack,Standard stack-based layouts,Stacks for linear arrangements,GeometryReader for simple layouts,VStack { Text() Image() },GeometryReader for vertical list,Medium,https://developer.apple.com/documentation/swiftui/vstack
|
||||
14,Layout,Use LazyVStack LazyHStack for lists,Lazy loading for performance,Lazy stacks for long lists,Regular stacks for 100+ items,LazyVStack { ForEach(items) },VStack { ForEach(largeArray) },High,https://developer.apple.com/documentation/swiftui/lazyvstack
|
||||
15,Layout,Use GeometryReader sparingly,Only when needed for sizing,GeometryReader for responsive layouts,GeometryReader everywhere,GeometryReader for aspect ratio,GeometryReader wrapping everything,Medium,
|
||||
16,Layout,Use spacing and padding consistently,Consistent spacing throughout app,Design system spacing values,Magic numbers for spacing,.padding(16) or .padding(),".padding(13), .padding(17)",Low,
|
||||
17,Layout,Use frame modifiers correctly,Set explicit sizes when needed,.frame(maxWidth: .infinity),Fixed sizes for responsive content,.frame(maxWidth: .infinity),.frame(width: 375),Medium,
|
||||
18,Modifiers,Order modifiers correctly,Modifier order affects rendering,Background before padding for full coverage,Wrong modifier order,.padding().background(Color.red),.background(Color.red).padding(),High,
|
||||
19,Modifiers,Create custom ViewModifiers,Reusable modifier combinations,ViewModifier for repeated styling,Duplicate modifier chains,struct CardStyle: ViewModifier,.shadow().cornerRadius() everywhere,Medium,https://developer.apple.com/documentation/swiftui/viewmodifier
|
||||
20,Modifiers,Use conditional modifiers carefully,Avoid changing view identity,if-else with same view type,Conditional that changes view identity,Text(title).foregroundColor(isActive ? .blue : .gray),if isActive { Text().bold() } else { Text() },Medium,
|
||||
21,Navigation,Use NavigationStack (iOS 16+),Modern navigation with type-safe paths,NavigationStack with navigationDestination,NavigationView for new projects,NavigationStack { },NavigationView { } (deprecated),Medium,https://developer.apple.com/documentation/swiftui/navigationstack
|
||||
22,Navigation,Use navigationDestination,Type-safe navigation destinations,.navigationDestination(for:),NavigationLink(destination:),.navigationDestination(for: Item.self),NavigationLink(destination: DetailView()),Medium,
|
||||
23,Navigation,Use @Environment for dismiss,Programmatic navigation dismissal,@Environment(\.dismiss) var dismiss,presentationMode (deprecated),@Environment(\.dismiss) var dismiss,@Environment(\.presentationMode),Low,
|
||||
24,Lists,Use List for scrollable content,Built-in scrolling and styling,List for standard scrollable content,ScrollView + VStack for simple lists,List { ForEach(items) { } },ScrollView { VStack { ForEach } },Low,https://developer.apple.com/documentation/swiftui/list
|
||||
25,Lists,Provide stable identifiers,Use Identifiable or explicit id,Identifiable protocol or id parameter,Index as identifier,ForEach(items) where Item: Identifiable,"ForEach(items.indices, id: \.self)",High,
|
||||
26,Lists,Use onDelete and onMove,Standard list editing,onDelete for swipe to delete,Custom delete implementation,.onDelete(perform: delete),.onTapGesture for delete,Low,
|
||||
27,Forms,Use Form for settings,Grouped input controls,Form for settings screens,Manual grouping for forms,Form { Section { Toggle() } },VStack { Toggle() },Low,https://developer.apple.com/documentation/swiftui/form
|
||||
28,Forms,Use @FocusState for keyboard,Manage keyboard focus,@FocusState for text field focus,Manual first responder handling,@FocusState private var isFocused: Bool,UIKit first responder,Medium,https://developer.apple.com/documentation/swiftui/focusstate
|
||||
29,Forms,Validate input properly,Show validation feedback,Real-time validation feedback,Submit without validation,TextField with validation state,TextField without error handling,Medium,
|
||||
30,Async,Use .task for async work,Automatic cancellation on view disappear,.task for view lifecycle async,onAppear with Task,.task { await loadData() },onAppear { Task { await loadData() } },Medium,https://developer.apple.com/documentation/swiftui/view/task(priority:_:)
|
||||
31,Async,Handle loading states,Show progress during async operations,ProgressView during loading,Empty view during load,if isLoading { ProgressView() },No loading indicator,Medium,
|
||||
32,Async,Use @MainActor for UI updates,Ensure UI updates on main thread,@MainActor on view models,Manual DispatchQueue.main,@MainActor class ViewModel,DispatchQueue.main.async,Medium,
|
||||
33,Animation,Use withAnimation,Animate state changes,withAnimation for state transitions,No animation for state changes,withAnimation { isExpanded.toggle() },isExpanded.toggle(),Low,https://developer.apple.com/documentation/swiftui/withanimation(_:_:)
|
||||
34,Animation,Use .animation modifier,Apply animations to views,.animation(.spring()) on view,Manual animation timing,.animation(.easeInOut),CABasicAnimation equivalent,Low,
|
||||
35,Animation,Respect reduced motion,Check accessibility settings,Check accessibilityReduceMotion,Ignore motion preferences,@Environment(\.accessibilityReduceMotion),Always animate regardless,High,
|
||||
36,Preview,Use #Preview macro (Xcode 15+),Modern preview syntax,#Preview for view previews,PreviewProvider protocol,#Preview { ContentView() },struct ContentView_Previews: PreviewProvider,Low,
|
||||
37,Preview,Create multiple previews,Test different states and devices,Multiple previews for states,Single preview only,"#Preview(""Light"") { } #Preview(""Dark"") { }",Single preview configuration,Low,
|
||||
38,Preview,Use preview data,Dedicated preview mock data,Static preview data,Production data in previews,Item.preview for preview,Fetch real data in preview,Low,
|
||||
39,Performance,Avoid expensive body computations,Body should be fast to compute,Precompute in view model,Heavy computation in body,vm.computedValue in body,Complex calculation in body,High,
|
||||
40,Performance,Use Equatable views,Skip unnecessary view updates,Equatable for complex views,Default equality for all views,struct MyView: View Equatable,No Equatable conformance,Medium,
|
||||
41,Performance,Profile with Instruments,Measure before optimizing,Use SwiftUI Instruments,Guess at performance issues,Profile with Instruments,Optimize without measuring,Medium,
|
||||
42,Accessibility,Add accessibility labels,Describe UI elements,.accessibilityLabel for context,Missing labels,".accessibilityLabel(""Close button"")",Button without label,High,https://developer.apple.com/documentation/swiftui/view/accessibilitylabel(_:)-1d7jv
|
||||
43,Accessibility,Support Dynamic Type,Respect text size preferences,Scalable fonts and layouts,Fixed font sizes,.font(.body) with Dynamic Type,.font(.system(size: 16)),High,
|
||||
44,Accessibility,Use semantic views,Proper accessibility traits,Correct accessibilityTraits,Wrong semantic meaning,Button for actions Image for display,Image that acts like button,Medium,
|
||||
45,Testing,Use ViewInspector for testing,Third-party view testing,ViewInspector for unit tests,UI tests only,ViewInspector assertions,Only XCUITest,Medium,
|
||||
46,Testing,Test view models,Unit test business logic,XCTest for view model,Skip view model testing,Test ViewModel methods,No unit tests,Medium,
|
||||
47,Testing,Use preview as visual test,Previews catch visual regressions,Multiple preview configurations,No visual verification,Preview different states,Single preview only,Low,
|
||||
48,Architecture,Use MVVM pattern,Separate view and logic,ViewModel for business logic,Logic in View,ObservableObject ViewModel,@State for complex logic,Medium,
|
||||
49,Architecture,Keep views dumb,Views display view model state,View reads from ViewModel,Business logic in View,view.items from vm.items,Complex filtering in View,Medium,
|
||||
50,Architecture,Use dependency injection,Inject dependencies for testing,Initialize with dependencies,Hard-coded dependencies,init(service: ServiceProtocol),let service = RealService(),Medium,
|
||||
|
50
.claude/skills/ui-ux-pro-max/data/stacks/vue.csv
Normal file
50
.claude/skills/ui-ux-pro-max/data/stacks/vue.csv
Normal file
@ -0,0 +1,50 @@
|
||||
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
|
||||
1,Composition,Use Composition API for new projects,Composition API offers better TypeScript support and logic reuse,<script setup> for components,Options API for new projects,<script setup>,export default { data() },Medium,https://vuejs.org/guide/extras/composition-api-faq.html
|
||||
2,Composition,Use script setup syntax,Cleaner syntax with automatic exports,<script setup> with defineProps,setup() function manually,<script setup>,<script> setup() { return {} },Low,https://vuejs.org/api/sfc-script-setup.html
|
||||
3,Reactivity,Use ref for primitives,ref() for primitive values that need reactivity,ref() for strings numbers booleans,reactive() for primitives,const count = ref(0),const count = reactive(0),Medium,https://vuejs.org/guide/essentials/reactivity-fundamentals.html
|
||||
4,Reactivity,Use reactive for objects,reactive() for complex objects and arrays,reactive() for objects with multiple properties,ref() for complex objects,const state = reactive({ user: null }),const state = ref({ user: null }),Medium,
|
||||
5,Reactivity,Access ref values with .value,Remember .value in script unwrap in template,Use .value in script,Forget .value in script,count.value++,count++ (in script),High,
|
||||
6,Reactivity,Use computed for derived state,Computed properties cache and update automatically,computed() for derived values,Methods for derived values,const doubled = computed(() => count.value * 2),const doubled = () => count.value * 2,Medium,https://vuejs.org/guide/essentials/computed.html
|
||||
7,Reactivity,Use shallowRef for large objects,Avoid deep reactivity for performance,shallowRef for large data structures,ref for large nested objects,const bigData = shallowRef(largeObject),const bigData = ref(largeObject),Medium,https://vuejs.org/api/reactivity-advanced.html#shallowref
|
||||
8,Watchers,Use watchEffect for simple cases,Auto-tracks dependencies,watchEffect for simple reactive effects,watch with explicit deps when not needed,watchEffect(() => console.log(count.value)),"watch(count, (val) => console.log(val))",Low,https://vuejs.org/guide/essentials/watchers.html
|
||||
9,Watchers,Use watch for specific sources,Explicit control over what to watch,watch with specific refs,watchEffect for complex conditional logic,"watch(userId, fetchUser)",watchEffect with conditionals,Medium,
|
||||
10,Watchers,Clean up side effects,Return cleanup function in watchers,Return cleanup in watchEffect,Leave subscriptions open,watchEffect((onCleanup) => { onCleanup(unsub) }),watchEffect without cleanup,High,
|
||||
11,Props,Define props with defineProps,Type-safe prop definitions,defineProps with TypeScript,Props without types,defineProps<{ msg: string }>(),defineProps(['msg']),Medium,https://vuejs.org/guide/typescript/composition-api.html#typing-component-props
|
||||
12,Props,Use withDefaults for default values,Provide defaults for optional props,withDefaults with defineProps,Defaults in destructuring,"withDefaults(defineProps<Props>(), { count: 0 })",const { count = 0 } = defineProps(),Medium,
|
||||
13,Props,Avoid mutating props,Props should be read-only,Emit events to parent for changes,Direct prop mutation,"emit('update:modelValue', newVal)",props.modelValue = newVal,High,
|
||||
14,Emits,Define emits with defineEmits,Type-safe event emissions,defineEmits with types,Emit without definition,defineEmits<{ change: [id: number] }>(),"emit('change', id) without define",Medium,https://vuejs.org/guide/typescript/composition-api.html#typing-component-emits
|
||||
15,Emits,Use v-model for two-way binding,Simplified parent-child data flow,v-model with modelValue prop,:value + @input manually,"<Child v-model=""value""/>","<Child :value=""value"" @input=""value = $event""/>",Low,https://vuejs.org/guide/components/v-model.html
|
||||
16,Lifecycle,Use onMounted for DOM access,DOM is ready in onMounted,onMounted for DOM operations,Access DOM in setup directly,onMounted(() => el.value.focus()),el.value.focus() in setup,High,https://vuejs.org/api/composition-api-lifecycle.html
|
||||
17,Lifecycle,Clean up in onUnmounted,Remove listeners and subscriptions,onUnmounted for cleanup,Leave listeners attached,onUnmounted(() => window.removeEventListener()),No cleanup on unmount,High,
|
||||
18,Lifecycle,Avoid onBeforeMount for data,Use onMounted or setup for data fetching,Fetch in onMounted or setup,Fetch in onBeforeMount,onMounted(async () => await fetchData()),onBeforeMount(async () => await fetchData()),Low,
|
||||
19,Components,Use single-file components,Keep template script style together,.vue files for components,Separate template/script files,Component.vue with all parts,Component.js + Component.html,Low,
|
||||
20,Components,Use PascalCase for components,Consistent component naming,PascalCase in imports and templates,kebab-case in script,<MyComponent/>,<my-component/>,Low,https://vuejs.org/style-guide/rules-strongly-recommended.html
|
||||
21,Components,Prefer composition over mixins,Composables replace mixins,Composables for shared logic,Mixins for code reuse,const { data } = useApi(),mixins: [apiMixin],Medium,
|
||||
22,Composables,Name composables with use prefix,Convention for composable functions,useFetch useAuth useForm,getData or fetchApi,export function useFetch(),export function fetchData(),Medium,https://vuejs.org/guide/reusability/composables.html
|
||||
23,Composables,Return refs from composables,Maintain reactivity when destructuring,Return ref values,Return reactive objects that lose reactivity,return { data: ref(null) },return reactive({ data: null }),Medium,
|
||||
24,Composables,Accept ref or value params,Use toValue for flexible inputs,toValue() or unref() for params,Only accept ref or only value,const val = toValue(maybeRef),const val = maybeRef.value,Low,https://vuejs.org/api/reactivity-utilities.html#tovalue
|
||||
25,Templates,Use v-bind shorthand,Cleaner template syntax,:prop instead of v-bind:prop,Full v-bind syntax,"<div :class=""cls"">","<div v-bind:class=""cls"">",Low,
|
||||
26,Templates,Use v-on shorthand,Cleaner event binding,@event instead of v-on:event,Full v-on syntax,"<button @click=""handler"">","<button v-on:click=""handler"">",Low,
|
||||
27,Templates,Avoid v-if with v-for,v-if has higher priority causes issues,Wrap in template or computed filter,v-if on same element as v-for,<template v-for><div v-if>,<div v-for v-if>,High,https://vuejs.org/style-guide/rules-essential.html#avoid-v-if-with-v-for
|
||||
28,Templates,Use key with v-for,Proper list rendering and updates,Unique key for each item,Index as key for dynamic lists,"v-for=""item in items"" :key=""item.id""","v-for=""(item, i) in items"" :key=""i""",High,
|
||||
29,State,Use Pinia for global state,Official state management for Vue 3,Pinia stores for shared state,Vuex for new projects,const store = useCounterStore(),Vuex with mutations,Medium,https://pinia.vuejs.org/
|
||||
30,State,Define stores with defineStore,Composition API style stores,Setup stores with defineStore,Options stores for complex state,"defineStore('counter', () => {})","defineStore('counter', { state })",Low,
|
||||
31,State,Use storeToRefs for destructuring,Maintain reactivity when destructuring,storeToRefs(store),Direct destructuring,const { count } = storeToRefs(store),const { count } = store,High,https://pinia.vuejs.org/core-concepts/#destructuring-from-a-store
|
||||
32,Routing,Use useRouter and useRoute,Composition API router access,useRouter() useRoute() in setup,this.$router this.$route,const router = useRouter(),this.$router.push(),Medium,https://router.vuejs.org/guide/advanced/composition-api.html
|
||||
33,Routing,Lazy load route components,Code splitting for routes,() => import() for components,Static imports for all routes,component: () => import('./Page.vue'),component: Page,Medium,https://router.vuejs.org/guide/advanced/lazy-loading.html
|
||||
34,Routing,Use navigation guards,Protect routes and handle redirects,beforeEach for auth checks,Check auth in each component,router.beforeEach((to) => {}),Check auth in onMounted,Medium,
|
||||
35,Performance,Use v-once for static content,Skip re-renders for static elements,v-once on never-changing content,v-once on dynamic content,<div v-once>{{ staticText }}</div>,<div v-once>{{ dynamicText }}</div>,Low,https://vuejs.org/api/built-in-directives.html#v-once
|
||||
36,Performance,Use v-memo for expensive lists,Memoize list items,v-memo with dependency array,Re-render entire list always,"<div v-for v-memo=""[item.id]"">",<div v-for> without memo,Medium,https://vuejs.org/api/built-in-directives.html#v-memo
|
||||
37,Performance,Use shallowReactive for flat objects,Avoid deep reactivity overhead,shallowReactive for flat state,reactive for simple objects,shallowReactive({ count: 0 }),reactive({ count: 0 }),Low,
|
||||
38,Performance,Use defineAsyncComponent,Lazy load heavy components,defineAsyncComponent for modals dialogs,Import all components eagerly,defineAsyncComponent(() => import()),import HeavyComponent from,Medium,https://vuejs.org/guide/components/async.html
|
||||
39,TypeScript,Use generic components,Type-safe reusable components,Generic with defineComponent,Any types in components,"<script setup lang=""ts"" generic=""T"">",<script setup> without types,Medium,https://vuejs.org/guide/typescript/composition-api.html
|
||||
40,TypeScript,Type template refs,Proper typing for DOM refs,ref<HTMLInputElement>(null),ref(null) without type,const input = ref<HTMLInputElement>(null),const input = ref(null),Medium,
|
||||
41,TypeScript,Use PropType for complex props,Type complex prop types,PropType<User> for object props,Object without type,type: Object as PropType<User>,type: Object,Medium,
|
||||
42,Testing,Use Vue Test Utils,Official testing library,mount shallowMount for components,Manual DOM testing,import { mount } from '@vue/test-utils',document.createElement,Medium,https://test-utils.vuejs.org/
|
||||
43,Testing,Test component behavior,Focus on inputs and outputs,Test props emit and rendered output,Test internal implementation,expect(wrapper.text()).toContain(),expect(wrapper.vm.internalState),Medium,
|
||||
44,Forms,Use v-model modifiers,Built-in input handling,.lazy .number .trim modifiers,Manual input parsing,"<input v-model.number=""age"">","<input v-model=""age""> then parse",Low,https://vuejs.org/guide/essentials/forms.html#modifiers
|
||||
45,Forms,Use VeeValidate or FormKit,Form validation libraries,VeeValidate for complex forms,Manual validation logic,useField useForm from vee-validate,Custom validation in each input,Medium,
|
||||
46,Accessibility,Use semantic elements,Proper HTML elements in templates,button nav main for purpose,div for everything,<button @click>,<div @click>,High,
|
||||
47,Accessibility,Bind aria attributes dynamically,Keep ARIA in sync with state,":aria-expanded=""isOpen""",Static ARIA values,":aria-expanded=""menuOpen""","aria-expanded=""true""",Medium,
|
||||
48,SSR,Use Nuxt for SSR,Full-featured SSR framework,Nuxt 3 for SSR apps,Manual SSR setup,npx nuxi init my-app,Custom SSR configuration,Medium,https://nuxt.com/
|
||||
49,SSR,Handle hydration mismatches,Client/server content must match,ClientOnly for browser-only content,Different content server/client,<ClientOnly><BrowserWidget/></ClientOnly>,<div>{{ Date.now() }}</div>,High,
|
||||
|
59
.claude/skills/ui-ux-pro-max/data/styles.csv
Normal file
59
.claude/skills/ui-ux-pro-max/data/styles.csv
Normal file
@ -0,0 +1,59 @@
|
||||
STT,Style Category,Type,Keywords,Primary Colors,Secondary Colors,Effects & Animation,Best For,Do Not Use For,Light Mode ✓,Dark Mode ✓,Performance,Accessibility,Mobile-Friendly,Conversion-Focused,Framework Compatibility,Era/Origin,Complexity
|
||||
1,Minimalism & Swiss Style,General,"Clean, simple, spacious, functional, white space, high contrast, geometric, sans-serif, grid-based, essential","Monochromatic, Black #000000, White #FFFFFF","Neutral (Beige #F5F1E8, Grey #808080, Taupe #B38B6D), Primary accent","Subtle hover (200-250ms), smooth transitions, sharp shadows if any, clear type hierarchy, fast loading","Enterprise apps, dashboards, documentation sites, SaaS platforms, professional tools","Creative portfolios, entertainment, playful brands, artistic experiments",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ High,◐ Medium,"Tailwind 10/10, Bootstrap 9/10, MUI 9/10",1950s Swiss,Low
|
||||
2,Neumorphism,General,"Soft UI, embossed, debossed, convex, concave, light source, subtle depth, rounded (12-16px), monochromatic","Light pastels: Soft Blue #C8E0F4, Soft Pink #F5E0E8, Soft Grey #E8E8E8","Tints/shades (±30%), gradient subtlety, color harmony","Soft box-shadow (multiple: -5px -5px 15px, 5px 5px 15px), smooth press (150ms), inner subtle shadow","Health/wellness apps, meditation platforms, fitness trackers, minimal interaction UIs","Complex apps, critical accessibility, data-heavy dashboards, high-contrast required",✓ Full,◐ Partial,⚡ Good,⚠ Low contrast,✓ Good,◐ Medium,"Tailwind 8/10, CSS-in-JS 9/10",2020s Modern,Medium
|
||||
3,Glassmorphism,General,"Frosted glass, transparent, blurred background, layered, vibrant background, light source, depth, multi-layer","Translucent white: rgba(255,255,255,0.1-0.3)","Vibrant: Electric Blue #0080FF, Neon Purple #8B00FF, Vivid Pink #FF1493, Teal #20B2AA","Backdrop blur (10-20px), subtle border (1px solid rgba white 0.2), light reflection, Z-depth","Modern SaaS, financial dashboards, high-end corporate, lifestyle apps, modal overlays, navigation","Low-contrast backgrounds, critical accessibility, performance-limited, dark text on dark",✓ Full,✓ Full,⚠ Good,⚠ Ensure 4.5:1,✓ Good,✓ High,"Tailwind 9/10, MUI 8/10, Chakra 8/10",2020s Modern,Medium
|
||||
4,Brutalism,General,"Raw, unpolished, stark, high contrast, plain text, default fonts, visible borders, asymmetric, anti-design","Primary: Red #FF0000, Blue #0000FF, Yellow #FFFF00, Black #000000, White #FFFFFF","Limited: Neon Green #00FF00, Hot Pink #FF00FF, minimal secondary","No smooth transitions (instant), sharp corners (0px), bold typography (700+), visible grid, large blocks","Design portfolios, artistic projects, counter-culture brands, editorial/media sites, tech blogs","Corporate environments, conservative industries, critical accessibility, customer-facing professional",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,◐ Medium,✗ Low,"Tailwind 10/10, Bootstrap 7/10",1950s Brutalist,Low
|
||||
5,3D & Hyperrealism,General,"Depth, realistic textures, 3D models, spatial navigation, tactile, skeuomorphic elements, rich detail, immersive","Deep Navy #001F3F, Forest Green #228B22, Burgundy #800020, Gold #FFD700, Silver #C0C0C0","Complex gradients (5-10 stops), realistic lighting, shadow variations (20-40% darker)","WebGL/Three.js 3D, realistic shadows (layers), physics lighting, parallax (3-5 layers), smooth 3D (300-400ms)","Gaming, product showcase, immersive experiences, high-end e-commerce, architectural viz, VR/AR","Low-end mobile, performance-limited, critical accessibility, data tables/forms",◐ Partial,◐ Partial,❌ Poor,⚠ Not accessible,✗ Low,◐ Medium,"Three.js 10/10, R3F 10/10, Babylon.js 10/10",2020s Modern,High
|
||||
6,Vibrant & Block-based,General,"Bold, energetic, playful, block layout, geometric shapes, high color contrast, duotone, modern, energetic","Neon Green #39FF14, Electric Purple #BF00FF, Vivid Pink #FF1493, Bright Cyan #00FFFF, Sunburst #FFAA00","Complementary: Orange #FF7F00, Shocking Pink #FF006E, Lime #CCFF00, triadic schemes","Large sections (48px+ gaps), animated patterns, bold hover (color shift), scroll-snap, large type (32px+), 200-300ms","Startups, creative agencies, gaming, social media, youth-focused, entertainment, consumer","Financial institutions, healthcare, formal business, government, conservative, elderly",✓ Full,✓ Full,⚡ Good,◐ Ensure WCAG,✓ High,✓ High,"Tailwind 10/10, Chakra 9/10, Styled 9/10",2020s Modern,Medium
|
||||
7,Dark Mode (OLED),General,"Dark theme, low light, high contrast, deep black, midnight blue, eye-friendly, OLED, night mode, power efficient","Deep Black #000000, Dark Grey #121212, Midnight Blue #0A0E27","Vibrant accents: Neon Green #39FF14, Electric Blue #0080FF, Gold #FFD700, Plasma Purple #BF00FF","Minimal glow (text-shadow: 0 0 10px), dark-to-light transitions, low white emission, high readability, visible focus","Night-mode apps, coding platforms, entertainment, eye-strain prevention, OLED devices, low-light","Print-first content, high-brightness outdoor, color-accuracy-critical",✗ No,✓ Only,⚡ Excellent,✓ WCAG AAA,✓ High,◐ Low,"Tailwind 10/10, MUI 10/10, Chakra 10/10",2020s Modern,Low
|
||||
8,Accessible & Ethical,General,"High contrast, large text (16px+), keyboard navigation, screen reader friendly, WCAG compliant, focus state, semantic","WCAG AA/AAA (4.5:1 min), simple primary, clear secondary, high luminosity (7:1+)","Symbol-based colors (not color-only), supporting patterns, inclusive combinations","Clear focus rings (3-4px), ARIA labels, skip links, responsive design, reduced motion, 44x44px touch targets","Government, healthcare, education, inclusive products, large audience, legal compliance, public",None - accessibility universal,✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ High,✓ High,"All frameworks 10/10",Universal,Low
|
||||
9,Claymorphism,General,"Soft 3D, chunky, playful, toy-like, bubbly, thick borders (3-4px), double shadows, rounded (16-24px)","Pastel: Soft Peach #FDBCB4, Baby Blue #ADD8E6, Mint #98FF98, Lilac #E6E6FA, light BG","Soft gradients (pastel-to-pastel), light/dark variations (20-30%), gradient subtle","Inner+outer shadows (subtle, no hard lines), soft press (200ms ease-out), fluffy elements, smooth transitions","Educational apps, children's apps, SaaS platforms, creative tools, fun-focused, onboarding, casual games","Formal corporate, professional services, data-critical, serious/medical, legal apps, finance",✓ Full,◐ Partial,⚡ Good,⚠ Ensure 4.5:1,✓ High,✓ High,"Tailwind 9/10, CSS-in-JS 9/10",2020s Modern,Medium
|
||||
10,Aurora UI,General,"Vibrant gradients, smooth blend, Northern Lights effect, mesh gradient, luminous, atmospheric, abstract","Complementary: Blue-Orange, Purple-Yellow, Electric Blue #0080FF, Magenta #FF1493, Cyan #00FFFF","Smooth transitions (Blue→Purple→Pink→Teal), iridescent effects, blend modes (screen, multiply)","Large flowing CSS/SVG gradients, subtle 8-12s animations, depth via color layering, smooth morph","Modern SaaS, creative agencies, branding, music platforms, lifestyle, premium products, hero sections","Data-heavy dashboards, critical accessibility, content-heavy where distraction issues",✓ Full,✓ Full,⚠ Good,⚠ Text contrast,✓ Good,✓ High,"Tailwind 9/10, CSS-in-JS 10/10",2020s Modern,Medium
|
||||
11,Retro-Futurism,General,"Vintage sci-fi, 80s aesthetic, neon glow, geometric patterns, CRT scanlines, pixel art, cyberpunk, synthwave","Neon Blue #0080FF, Hot Pink #FF006E, Cyan #00FFFF, Deep Black #1A1A2E, Purple #5D34D0","Metallic Silver #C0C0C0, Gold #FFD700, duotone, 80s Pink #FF10F0, neon accents","CRT scanlines (::before overlay), neon glow (text-shadow+box-shadow), glitch effects (skew/offset keyframes)","Gaming, entertainment, music platforms, tech brands, artistic projects, nostalgic, cyberpunk","Conservative industries, critical accessibility, professional/corporate, elderly, legal/finance",✓ Full,✓ Dark focused,⚠ Moderate,⚠ High contrast/strain,◐ Medium,◐ Medium,"Tailwind 8/10, CSS-in-JS 9/10",1980s Retro,Medium
|
||||
12,Flat Design,General,"2D, minimalist, bold colors, no shadows, clean lines, simple shapes, typography-focused, modern, icon-heavy","Solid bright: Red, Orange, Blue, Green, limited palette (4-6 max)","Complementary colors, muted secondaries, high saturation, clean accents","No gradients/shadows, simple hover (color/opacity shift), fast loading, clean transitions (150-200ms ease), minimal icons","Web apps, mobile apps, cross-platform, startup MVPs, user-friendly, SaaS, dashboards, corporate","Complex 3D, premium/luxury, artistic portfolios, immersive experiences, high-detail",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ High,✓ High,"Tailwind 10/10, Bootstrap 10/10, MUI 9/10",2010s Modern,Low
|
||||
13,Skeuomorphism,General,"Realistic, texture, depth, 3D appearance, real-world metaphors, shadows, gradients, tactile, detailed, material","Rich realistic: wood, leather, metal colors, detailed gradients (8-12 stops), metallic effects","Realistic lighting gradients, shadow variations (30-50% darker), texture overlays, material colors","Realistic shadows (layers), depth (perspective), texture details (noise, grain), realistic animations (300-500ms)","Legacy apps, gaming, immersive storytelling, premium products, luxury, realistic simulations, education","Modern enterprise, critical accessibility, low-performance, web (use Flat/Modern)",◐ Partial,◐ Partial,❌ Poor,⚠ Textures reduce readability,✗ Low,◐ Medium,"CSS-in-JS 7/10, Custom 8/10",2007-2012 iOS,High
|
||||
14,Liquid Glass,General,"Flowing glass, morphing, smooth transitions, fluid effects, translucent, animated blur, iridescent, chromatic aberration","Vibrant iridescent (rainbow spectrum), translucent base with opacity shifts, gradient fluidity","Chromatic aberration (Red-Cyan), iridescent oil-spill, fluid gradient blends, holographic effects","Morphing elements (SVG/CSS), fluid animations (400-600ms curves), dynamic blur (backdrop-filter), color transitions","Premium SaaS, high-end e-commerce, creative platforms, branding experiences, luxury portfolios","Performance-limited, critical accessibility, complex data, budget projects",✓ Full,✓ Full,⚠ Moderate-Poor,⚠ Text contrast,◐ Medium,✓ High,"Framer Motion 10/10, GSAP 10/10",2020s Modern,High
|
||||
15,Motion-Driven,General,"Animation-heavy, microinteractions, smooth transitions, scroll effects, parallax, entrance anim, page transitions","Bold colors emphasize movement, high contrast animated, dynamic gradients, accent action colors","Transitional states, success (Green #22C55E), error (Red #EF4444), neutral feedback","Scroll anim (Intersection Observer), hover (300-400ms), entrance, parallax (3-5 layers), page transitions","Portfolio sites, storytelling platforms, interactive experiences, entertainment apps, creative, SaaS","Data dashboards, critical accessibility, low-power devices, content-heavy, motion-sensitive",✓ Full,✓ Full,⚠ Good,⚠ Prefers-reduced-motion,✓ Good,✓ High,"GSAP 10/10, Framer Motion 10/10",2020s Modern,High
|
||||
16,Micro-interactions,General,"Small animations, gesture-based, tactile feedback, subtle animations, contextual interactions, responsive","Subtle color shifts (10-20%), feedback: Green #22C55E, Red #EF4444, Amber #F59E0B","Accent feedback, neutral supporting, clear action indicators","Small hover (50-100ms), loading spinners, success/error state anim, gesture-triggered (swipe/pinch), haptic","Mobile apps, touchscreen UIs, productivity tools, user-friendly, consumer apps, interactive components","Desktop-only, critical performance, accessibility-first (alternatives needed)",✓ Full,✓ Full,⚡ Excellent,✓ Good,✓ High,✓ High,"Framer Motion 10/10, React Spring 9/10",2020s Modern,Medium
|
||||
17,Inclusive Design,General,"Accessible, color-blind friendly, high contrast, haptic feedback, voice interaction, screen reader, WCAG AAA, universal","WCAG AAA (7:1+ contrast), avoid red-green only, symbol-based indicators, high contrast primary","Supporting patterns (stripes, dots, hatch), symbols, combinations, clear non-color indicators","Haptic feedback (vibration), voice guidance, focus indicators (4px+ ring), motion options, alt content, semantic","Public services, education, healthcare, finance, government, accessible consumer, inclusive",None - accessibility universal,✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ High,✓ High,"All frameworks 10/10",Universal,Low
|
||||
18,Zero Interface,General,"Minimal visible UI, voice-first, gesture-based, AI-driven, invisible controls, predictive, context-aware, ambient","Neutral backgrounds: Soft white #FAFAFA, light grey #F0F0F0, warm off-white #F5F1E8","Subtle feedback: light green, light red, minimal UI elements, soft accents","Voice recognition UI, gesture detection, AI predictions (smooth reveal), progressive disclosure, smart suggestions","Voice assistants, AI platforms, future-forward UX, smart home, contextual computing, ambient experiences","Complex workflows, data-entry heavy, traditional systems, legacy support, explicit control",✓ Full,✓ Full,⚡ Excellent,✓ Excellent,✓ High,✓ High,"Tailwind 10/10, Custom 10/10",2020s AI-Era,Low
|
||||
19,Soft UI Evolution,General,"Evolved soft UI, better contrast, modern aesthetics, subtle depth, accessibility-focused, improved shadows, hybrid","Improved contrast pastels: Soft Blue #87CEEB, Soft Pink #FFB6C1, Soft Green #90EE90, better hierarchy","Better combinations, accessible secondary, supporting with improved contrast, modern accents","Improved shadows (softer than flat, clearer than neumorphism), modern (200-300ms), focus visible, WCAG AA/AAA","Modern enterprise apps, SaaS platforms, health/wellness, modern business tools, professional, hybrid","Extreme minimalism, critical performance, systems without modern OS",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA+,✓ High,✓ High,"Tailwind 9/10, MUI 9/10, Chakra 9/10",2020s Modern,Medium
|
||||
20,Hero-Centric Design,Landing Page,"Large hero section, compelling headline, high-contrast CTA, product showcase, value proposition, hero image/video, dramatic visual","Brand primary color, white/light backgrounds for contrast, accent color for CTA","Supporting colors for secondary CTAs, accent highlights, trust elements (testimonials, logos)","Smooth scroll reveal, fade-in animations on hero, subtle background parallax, CTA glow/pulse effect","SaaS landing pages, product launches, service landing pages, B2B platforms, tech companies","Complex navigation, multi-page experiences, data-heavy applications",✓ Full,✓ Full,⚡ Good,✓ WCAG AA,✓ Full,✓ Very High,"Tailwind 10/10, Bootstrap 9/10",2020s Modern,Medium
|
||||
21,Conversion-Optimized,Landing Page,"Form-focused, minimalist design, single CTA focus, high contrast, urgency elements, trust signals, social proof, clear value","Primary brand color, high-contrast white/light backgrounds, warning/urgency colors for time-limited offers","Secondary CTA color (muted), trust element colors (testimonial highlights), accent for key benefits","Hover states on CTA (color shift, slight scale), form field focus animations, loading spinner, success feedback","E-commerce product pages, free trial signups, lead generation, SaaS pricing pages, limited-time offers","Complex feature explanations, multi-product showcases, technical documentation",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,✓ Full (mobile-optimized),✓ Very High
|
||||
22,Feature-Rich Showcase,Landing Page,"Multiple feature sections, grid layout, benefit cards, visual feature demonstrations, interactive elements, problem-solution pairs","Primary brand, bright secondary colors for feature cards, contrasting accent for CTAs","Supporting colors for: benefits (green), problems (red/orange), features (blue/purple), social proof (neutral)","Card hover effects (lift/scale), icon animations on scroll, feature toggle animations, smooth section transitions","Enterprise SaaS, software tools landing pages, platform services, complex product explanations, B2B products","Simple product pages, early-stage startups with few features, entertainment landing pages",✓ Full,✓ Full,⚡ Good,✓ WCAG AA,✓ Good,✓ High
|
||||
23,Minimal & Direct,Landing Page,"Minimal text, white space heavy, single column layout, direct messaging, clean typography, visual-centric, fast-loading","Monochromatic primary, white background, single accent color for CTA, black/dark grey text","Minimal secondary colors, reserved for critical CTAs only, neutral supporting elements","Very subtle hover effects, minimal animations, fast page load (no heavy animations), smooth scroll","Simple service landing pages, indie products, consulting services, micro SaaS, freelancer portfolios","Feature-heavy products, complex explanations, multi-product showcases",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ Full,✓ High
|
||||
24,Social Proof-Focused,Landing Page,"Testimonials prominent, client logos displayed, case studies sections, reviews/ratings, user avatars, success metrics, credibility markers","Primary brand, trust colors (blue), success/growth colors (green), neutral backgrounds","Testimonial highlight colors, logo grid backgrounds (light grey), badge/achievement colors","Testimonial carousel animations, logo grid fade-in, stat counter animations (number count-up), review star ratings","B2B SaaS, professional services, premium products, e-commerce conversion pages, established brands","Startup MVPs, products without users, niche/experimental products",✓ Full,✓ Full,⚡ Good,✓ WCAG AA,✓ Full,✓ High
|
||||
25,Interactive Product Demo,Landing Page,"Embedded product mockup/video, interactive elements, product walkthrough, step-by-step guides, hover-to-reveal features, embedded demos","Primary brand, interface colors matching product, demo highlight colors for interactive elements","Product UI colors, tutorial step colors (numbered progression), hover state indicators","Product animation playback, step progression animations, hover reveal effects, smooth zoom on interaction","SaaS platforms, tool/software products, productivity apps landing pages, developer tools, productivity software","Simple services, consulting, non-digital products, complexity-averse audiences",✓ Full,✓ Full,⚠ Good (video/interactive),✓ WCAG AA,✓ Good,✓ Very High
|
||||
26,Trust & Authority,Landing Page,"Certificates/badges displayed, expert credentials, case studies with metrics, before/after comparisons, industry recognition, security badges","Professional colors (blue/grey), trust colors, certification badge colors (gold/silver accents)","Certificate highlight colors, metric showcase colors, comparison highlight (success green)","Badge hover effects, metric pulse animations, certificate carousel, smooth stat reveal","Healthcare/medical landing pages, financial services, enterprise software, premium/luxury products, legal services","Casual products, entertainment, viral/social-first products",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ Full,✓ High
|
||||
27,Storytelling-Driven,Landing Page,"Narrative flow, visual story progression, section transitions, consistent character/brand voice, emotional messaging, journey visualization","Brand primary, warm/emotional colors, varied accent colors per story section, high visual variety","Story section color coding, emotional state colors (calm, excitement, success), transitional gradients","Section-to-section animations, scroll-triggered reveals, character/icon animations, morphing transitions, parallax narrative","Brand/startup stories, mission-driven products, premium/lifestyle brands, documentary-style products, educational","Technical/complex products (unless narrative-driven), traditional enterprise software",✓ Full,✓ Full,⚠ Moderate (animations),✓ WCAG AA,✓ Good,✓ High
|
||||
28,Data-Dense Dashboard,BI/Analytics,"Multiple charts/widgets, data tables, KPI cards, minimal padding, grid layout, space-efficient, maximum data visibility","Neutral primary (light grey/white #F5F5F5), data colors (blue/green/red), dark text #333333","Chart colors: success (green #22C55E), warning (amber #F59E0B), alert (red #EF4444), neutral (grey)","Hover tooltips, chart zoom on click, row highlighting on hover, smooth filter animations, data loading spinners","Business intelligence dashboards, financial analytics, enterprise reporting, operational dashboards, data warehousing","Marketing dashboards, consumer-facing analytics, simple reporting",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,◐ Medium,✗ Not applicable
|
||||
29,Heat Map & Heatmap Style,BI/Analytics,"Color-coded grid/matrix, data intensity visualization, geographical heat maps, correlation matrices, cell-based representation, gradient coloring","Gradient scale: Cool (blue #0080FF) to hot (red #FF0000), neutral middle (white/yellow)","Support gradients: Light (cool blue) to dark (warm red), divergent for positive/negative data, monochromatic options","Color gradient transitions on data change, cell highlighting on hover, tooltip reveal on click, smooth color animation","Geographical analysis, performance matrices, correlation analysis, user behavior heatmaps, temperature/intensity data","Linear data representation, categorical comparisons (use bar charts), small datasets",✓ Full,✓ Full (with adjustments),⚡ Excellent,⚠ Colorblind considerations,◐ Medium,✗ Not applicable
|
||||
30,Executive Dashboard,BI/Analytics,"High-level KPIs, large key metrics, minimal detail, summary view, trend indicators, at-a-glance insights, executive summary","Brand colors, professional palette (blue/grey/white), accent for KPIs, red for alerts/concerns","KPI highlight colors: positive (green), negative (red), neutral (grey), trend arrow colors","KPI value animations (count-up), trend arrow direction animations, metric card hover lift, alert pulse effect","C-suite dashboards, business summary reports, decision-maker dashboards, strategic planning views","Detailed analyst dashboards, technical deep-dives, operational monitoring",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,✗ Low (not mobile-optimized),✗ Not applicable
|
||||
31,Real-Time Monitoring,BI/Analytics,"Live data updates, status indicators, alert notifications, streaming data visualization, active monitoring, streaming charts","Alert colors: critical (red #FF0000), warning (orange #FFA500), normal (green #22C55E), updating (blue animation)","Status indicator colors, chart line colors varying by metric, streaming data highlight colors","Real-time chart animations, alert pulse/glow, status indicator blink animation, smooth data stream updates, loading effect","System monitoring dashboards, DevOps dashboards, real-time analytics, stock market dashboards, live event tracking","Historical analysis, long-term trend reports, archived data dashboards",✓ Full,✓ Full,⚡ Good (real-time load),✓ WCAG AA,◐ Medium,✗ Not applicable
|
||||
32,Drill-Down Analytics,BI/Analytics,"Hierarchical data exploration, expandable sections, interactive drill-down paths, summary-to-detail flow, context preservation","Primary brand, breadcrumb colors, drill-level indicator colors, hierarchy depth colors","Drill-down path indicator colors, level-specific colors, highlight colors for selected level, transition colors","Drill-down expand animations, breadcrumb click transitions, smooth detail reveal, level change smooth, data reload animation","Sales analytics, product analytics, funnel analysis, multi-dimensional data exploration, business intelligence","Simple linear data, single-metric dashboards, streaming real-time dashboards",✓ Full,✓ Full,⚡ Good,✓ WCAG AA,◐ Medium,✗ Not applicable
|
||||
33,Comparative Analysis Dashboard,BI/Analytics,"Side-by-side comparisons, period-over-period metrics, A/B test results, regional comparisons, performance benchmarks","Comparison colors: primary (blue), comparison (orange/purple), delta indicator (green/red)","Winning metric color (green), losing metric color (red), neutral comparison (grey), benchmark colors","Comparison bar animations (grow to value), delta indicator animations (direction arrows), highlight on compare","Period-over-period reporting, A/B test dashboards, market comparison, competitive analysis, regional performance","Single metric dashboards, future projections (use forecasting), real-time only (no historical)",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,◐ Medium,✗ Not applicable
|
||||
34,Predictive Analytics,BI/Analytics,"Forecast lines, confidence intervals, trend projections, scenario modeling, AI-driven insights, anomaly detection visualization","Forecast line color (distinct from actual), confidence interval shading, anomaly highlight (red alert), trend colors","High confidence (dark color), low confidence (light color), anomaly colors (red/orange), normal trend (green/blue)","Forecast line animation on draw, confidence band fade-in, anomaly pulse alert, smoothing function animations","Forecasting dashboards, anomaly detection systems, trend prediction dashboards, AI-powered analytics, budget planning","Historical-only dashboards, simple reporting, real-time operational dashboards",✓ Full,✓ Full,⚠ Good (computation),✓ WCAG AA,◐ Medium,✗ Not applicable
|
||||
35,User Behavior Analytics,BI/Analytics,"Funnel visualization, user flow diagrams, conversion tracking, engagement metrics, user journey mapping, cohort analysis","Funnel stage colors: high engagement (green), drop-off (red), conversion (blue), user flow arrows (grey)","Stage completion colors (success), abandonment colors (warning), engagement levels (gradient), cohort colors","Funnel animation (fill-down), flow diagram animations (connection draw), conversion pulse, engagement bar fill","Conversion funnel analysis, user journey tracking, engagement analytics, cohort analysis, retention tracking","Real-time operational metrics, technical system monitoring, financial transactions",✓ Full,✓ Full,⚡ Good,✓ WCAG AA,✓ Good,✗ Not applicable
|
||||
36,Financial Dashboard,BI/Analytics,"Revenue metrics, profit/loss visualization, budget tracking, financial ratios, portfolio performance, cash flow, audit trail","Financial colors: profit (green #22C55E), loss (red #EF4444), neutral (grey), trust (dark blue #003366)","Revenue highlight (green), expenses (red), budget variance (orange/red), balance (grey), accuracy (blue)","Number animations (count-up), trend direction indicators, percentage change animations, profit/loss color transitions","Financial reporting, accounting dashboards, portfolio tracking, budget monitoring, banking analytics","Simple business dashboards, entertainment/social metrics, non-financial data",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✗ Low,✗ Not applicable
|
||||
37,Sales Intelligence Dashboard,BI/Analytics,"Deal pipeline, sales metrics, territory performance, sales rep leaderboard, win-loss analysis, quota tracking, forecast accuracy","Sales colors: won (green), lost (red), in-progress (blue), blocked (orange), quota met (gold), quota missed (grey)","Pipeline stage colors, rep performance colors, quota achievement colors, forecast accuracy colors","Deal movement animations, metric updates, leaderboard ranking changes, gauge needle movements, status change highlights","CRM dashboards, sales management, opportunity tracking, performance management, quota planning","Marketing analytics, customer support metrics, HR dashboards",✓ Full,✓ Full,⚡ Good,✓ WCAG AA,◐ Medium,✗ Not applicable,"Recharts 9/10, Chart.js 9/10",2020s Modern,Medium
|
||||
38,Neubrutalism,General,"Bold borders, black outlines, primary colors, thick shadows, no gradients, flat colors, 45° shadows, playful, Gen Z","#FFEB3B (Yellow), #FF5252 (Red), #2196F3 (Blue), #000000 (Black borders)","Limited accent colors, high contrast combinations, no gradients allowed","box-shadow: 4px 4px 0 #000, border: 3px solid #000, no gradients, sharp corners (0px), bold typography","Gen Z brands, startups, creative agencies, Figma-style apps, Notion-style interfaces, tech blogs","Luxury brands, finance, healthcare, conservative industries (too playful)",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ High,✓ High,"Tailwind 10/10, Bootstrap 8/10",2020s Modern,Low
|
||||
39,Bento Box Grid,General,"Modular cards, asymmetric grid, varied sizes, Apple-style, dashboard tiles, negative space, clean hierarchy, cards","Neutral base + brand accent, #FFFFFF, #F5F5F5, brand primary","Subtle gradients, shadow variations, accent highlights for interactive cards","grid-template with varied spans, rounded-xl (16px), subtle shadows, hover scale (1.02), smooth transitions","Dashboards, product pages, portfolios, Apple-style marketing, feature showcases, SaaS","Dense data tables, text-heavy content, real-time monitoring",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,✓ High,✓ High,"Tailwind 10/10, CSS Grid 10/10",2020s Apple,Low
|
||||
40,Y2K Aesthetic,General,"Neon pink, chrome, metallic, bubblegum, iridescent, glossy, retro-futurism, 2000s, futuristic nostalgia","#FF69B4 (Hot Pink), #00FFFF (Cyan), #C0C0C0 (Silver), #9400D3 (Purple)","Metallic gradients, glossy overlays, iridescent effects, chrome textures","linear-gradient metallic, glossy buttons, 3D chrome effects, glow animations, bubble shapes","Fashion brands, music platforms, Gen Z brands, nostalgia marketing, entertainment, youth-focused","B2B enterprise, healthcare, finance, conservative industries, elderly users",✓ Full,◐ Partial,⚠ Good,⚠ Check contrast,✓ Good,✓ High,"Tailwind 8/10, CSS-in-JS 9/10",Y2K 2000s,Medium
|
||||
41,Cyberpunk UI,General,"Neon, dark mode, terminal, HUD, sci-fi, glitch, dystopian, futuristic, matrix, tech noir","#00FF00 (Matrix Green), #FF00FF (Magenta), #00FFFF (Cyan), #0D0D0D (Dark)","Neon gradients, scanline overlays, glitch colors, terminal green accents","Neon glow (text-shadow), glitch animations (skew/offset), scanlines (::before overlay), terminal fonts","Gaming platforms, tech products, crypto apps, sci-fi applications, developer tools, entertainment","Corporate enterprise, healthcare, family apps, conservative brands, elderly users",✗ No,✓ Only,⚠ Moderate,⚠ Limited (dark+neon),◐ Medium,◐ Medium,"Tailwind 8/10, Custom CSS 10/10",2020s Cyberpunk,Medium
|
||||
42,Organic Biophilic,General,"Nature, organic shapes, green, sustainable, rounded, flowing, wellness, earthy, natural textures","#228B22 (Forest Green), #8B4513 (Earth Brown), #87CEEB (Sky Blue), #F5F5DC (Beige)","Natural gradients, earth tones, sky blues, organic textures, wood/stone colors","Rounded corners (16-24px), organic curves (border-radius variations), natural shadows, flowing SVG shapes","Wellness apps, sustainability brands, eco products, health apps, meditation, organic food brands","Tech-focused products, gaming, industrial, urban brands",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,✓ High,✓ High,"Tailwind 10/10, CSS 10/10",2020s Sustainable,Low
|
||||
43,AI-Native UI,General,"Chatbot, conversational, voice, assistant, agentic, ambient, minimal chrome, streaming text, AI interactions","Neutral + single accent, #6366F1 (AI Purple), #10B981 (Success), #F5F5F5 (Background)","Status indicators, streaming highlights, context card colors, subtle accent variations","Typing indicators (3-dot pulse), streaming text animations, pulse animations, context cards, smooth reveals","AI products, chatbots, voice assistants, copilots, AI-powered tools, conversational interfaces","Traditional forms, data-heavy dashboards, print-first content",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,✓ High,✓ High,"Tailwind 10/10, React 10/10",2020s AI-Era,Low
|
||||
44,Memphis Design,General,"80s, geometric, playful, postmodern, shapes, patterns, squiggles, triangles, neon, abstract, bold","#FF71CE (Hot Pink), #FFCE5C (Yellow), #86CCCA (Teal), #6A7BB4 (Blue Purple)","Complementary geometric colors, pattern fills, contrasting accent shapes","transform: rotate(), clip-path: polygon(), mix-blend-mode, repeating patterns, bold shapes","Creative agencies, music sites, youth brands, event promotion, artistic portfolios, entertainment","Corporate finance, healthcare, legal, elderly users, conservative brands",✓ Full,✓ Full,⚡ Excellent,⚠ Check contrast,✓ Good,◐ Medium,"Tailwind 9/10, CSS 10/10",1980s Postmodern,Medium
|
||||
45,Vaporwave,General,"Synthwave, retro-futuristic, 80s-90s, neon, glitch, nostalgic, sunset gradient, dreamy, aesthetic","#FF71CE (Pink), #01CDFE (Cyan), #05FFA1 (Mint), #B967FF (Purple)","Sunset gradients, glitch overlays, VHS effects, neon accents, pastel variations","text-shadow glow, linear-gradient, filter: hue-rotate(), glitch animations, retro scan lines","Music platforms, gaming, creative portfolios, tech startups, entertainment, artistic projects","Business apps, e-commerce, education, healthcare, enterprise software",✓ Full,✓ Dark focused,⚠ Moderate,⚠ Poor (motion),◐ Medium,◐ Medium,"Tailwind 8/10, CSS-in-JS 9/10",1980s-90s Retro,Medium
|
||||
46,Dimensional Layering,General,"Depth, overlapping, z-index, layers, 3D, shadows, elevation, floating, cards, spatial hierarchy","Neutral base (#FFFFFF, #F5F5F5, #E0E0E0) + brand accent for elevated elements","Shadow variations (sm/md/lg/xl), elevation colors, highlight colors for top layers","z-index stacking, box-shadow elevation (4 levels), transform: translateZ(), backdrop-filter, parallax","Dashboards, card layouts, modals, navigation, product showcases, SaaS interfaces","Print-style layouts, simple blogs, low-end devices, flat design requirements",✓ Full,✓ Full,⚠ Good,⚠ Moderate (SR issues),✓ Good,✓ High,"Tailwind 10/10, MUI 10/10, Chakra 10/10",2020s Modern,Medium
|
||||
47,Exaggerated Minimalism,General,"Bold minimalism, oversized typography, high contrast, negative space, loud minimal, statement design","#000000 (Black), #FFFFFF (White), single vibrant accent only","Minimal - single accent color, no secondary colors, extreme restraint","font-size: clamp(3rem 10vw 12rem), font-weight: 900, letter-spacing: -0.05em, massive whitespace","Fashion, architecture, portfolios, agency landing pages, luxury brands, editorial","E-commerce catalogs, dashboards, forms, data-heavy, elderly users, complex apps",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,✓ High,✓ High,"Tailwind 10/10, Typography.js 10/10",2020s Modern,Low
|
||||
48,Kinetic Typography,General,"Motion text, animated type, moving letters, dynamic, typing effect, morphing, scroll-triggered text","Flexible - high contrast recommended, bold colors for emphasis, animation-friendly palette","Accent colors for emphasis, transition colors, gradient text fills","@keyframes text animation, typing effect, background-clip: text, GSAP ScrollTrigger, split text","Hero sections, marketing sites, video platforms, storytelling, creative portfolios, landing pages","Long-form content, accessibility-critical, data interfaces, forms, elderly users",✓ Full,✓ Full,⚠ Moderate,❌ Poor (motion),✓ Good,✓ Very High,"GSAP 10/10, Framer Motion 10/10",2020s Modern,High
|
||||
49,Parallax Storytelling,General,"Scroll-driven, narrative, layered scrolling, immersive, progressive disclosure, cinematic, scroll-triggered","Story-dependent, often gradients and natural colors, section-specific palettes","Section transition colors, depth layer colors, narrative mood colors","transform: translateY(scroll), position: fixed/sticky, perspective: 1px, scroll-triggered animations","Brand storytelling, product launches, case studies, portfolios, annual reports, marketing campaigns","E-commerce, dashboards, mobile-first, SEO-critical, accessibility-required",✓ Full,✓ Full,❌ Poor,❌ Poor (motion),✗ Low,✓ High,"GSAP ScrollTrigger 10/10, Locomotive Scroll 10/10",2020s Modern,High
|
||||
50,Swiss Modernism 2.0,General,"Grid system, Helvetica, modular, asymmetric, international style, rational, clean, mathematical spacing","#000000, #FFFFFF, #F5F5F5, single vibrant accent only","Minimal secondary, accent for emphasis only, no gradients","display: grid, grid-template-columns: repeat(12 1fr), gap: 1rem, mathematical ratios, clear hierarchy","Corporate sites, architecture, editorial, SaaS, museums, professional services, documentation","Playful brands, children's sites, entertainment, gaming, emotional storytelling",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ High,✓ High,"Tailwind 10/10, Bootstrap 9/10, Foundation 10/10",1950s Swiss + 2020s,Low
|
||||
51,HUD / Sci-Fi FUI,General,"Futuristic, technical, wireframe, neon, data, transparency, iron man, sci-fi, interface","Neon Cyan #00FFFF, Holographic Blue #0080FF, Alert Red #FF0000","Transparent Black, Grid Lines #333333","Glow effects, scanning animations, ticker text, blinking markers, fine line drawing","Sci-fi games, space tech, cybersecurity, movie props, immersive dashboards","Standard corporate, reading heavy content, accessible public services",✓ Low,✓ Full,⚠ Moderate (renders),⚠ Poor (thin lines),◐ Medium,✗ Low,"React 9/10, Canvas 10/10",2010s Sci-Fi,High
|
||||
52,Pixel Art,General,"Retro, 8-bit, 16-bit, gaming, blocky, nostalgic, pixelated, arcade","Primary colors (NES Palette), brights, limited palette","Black outlines, shading via dithering or block colors","Frame-by-frame sprite animation, blinking cursor, instant transitions, marquee text","Indie games, retro tools, creative portfolios, nostalgia marketing, Web3/NFT","Professional corporate, modern SaaS, high-res photography sites",✓ Full,✓ Full,⚡ Excellent,✓ Good (if contrast ok),✓ High,◐ Medium,"CSS (box-shadow) 8/10, Canvas 10/10",1980s Arcade,Medium
|
||||
53,Bento Grids,General,"Apple-style, modular, cards, organized, clean, hierarchy, grid, rounded, soft","Off-white #F5F5F7, Clean White #FFFFFF, Text #1D1D1F","Subtle accents, soft shadows, blurred backdrops","Hover scale (1.02), soft shadow expansion, smooth layout shifts, content reveal","Product features, dashboards, personal sites, marketing summaries, galleries","Long-form reading, data tables, complex forms",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,✓ High,✓ High,"CSS Grid 10/10, Tailwind 10/10",2020s Apple/Linear,Low
|
||||
54,Neubrutalism,General,"Bold, ugly-cute, raw, high contrast, flat, hard shadows, distinct, playful, loud","Pop Yellow #FFDE59, Bright Red #FF5757, Black #000000","Lavender #CBA6F7, Mint #76E0C2","Hard hover shifts (4px), marquee scrolling, jitter animations, bold borders","Design tools, creative agencies, Gen Z brands, personal blogs, gumroad-style","Banking, legal, healthcare, serious enterprise, elderly users",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ High,✓ High,"Tailwind 10/10, Plain CSS 10/10",2020s Modern Retro,Low
|
||||
55,Spatial UI (VisionOS),General,"Glass, depth, immersion, spatial, translucent, gaze, gesture, apple, vision-pro","Frosted Glass #FFFFFF (15-30% opacity), System White","Vibrant system colors for active states, deep shadows for depth","Parallax depth, dynamic lighting response, gaze-hover effects, smooth scale on focus","Spatial computing apps, VR/AR interfaces, immersive media, futuristic dashboards","Text-heavy documents, high-contrast requirements, non-3D capable devices",✓ Full,✓ Full,⚠ Moderate (blur cost),⚠ Contrast risks,✓ High (if adapted),✓ High,"SwiftUI, React (Three.js/Fiber)",2024 Spatial Era,High
|
||||
56,E-Ink / Paper,General,"Paper-like, matte, high contrast, texture, reading, calm, slow tech, monochrome","Off-White #FDFBF7, Paper White #F5F5F5, Ink Black #1A1A1A","Pencil Grey #4A4A4A, Highlighter Yellow #FFFF00 (accent)","No motion blur, distinct page turns, grain/noise texture, sharp transitions (no fade)","Reading apps, digital newspapers, minimal journals, distraction-free writing, slow-living brands","Gaming, video platforms, high-energy marketing, dark mode dependent apps",✓ Full,✗ Low (inverted only),⚡ Excellent,✓ WCAG AAA,✓ High,✓ Medium,"Tailwind 10/10, CSS 10/10",2020s Digital Well-being,Low
|
||||
57,Gen Z Chaos / Maximalism,General,"Chaos, clutter, stickers, raw, collage, mixed media, loud, internet culture, ironic","Clashing Brights: #FF00FF, #00FF00, #FFFF00, #0000FF","Gradients, rainbow, glitch, noise, heavily saturated mix","Marquee scrolls, jitter, sticker layering, GIF overload, random placement, drag-and-drop","Gen Z lifestyle brands, music artists, creative portfolios, viral marketing, fashion","Corporate, government, healthcare, banking, serious tools",✓ Full,✓ Full,⚠ Poor (heavy assets),❌ Poor,◐ Medium,✓ High (Viral),CSS-in-JS 8/10,2023+ Internet Core,High
|
||||
58,Biomimetic / Organic 2.0,General,"Nature-inspired, cellular, fluid, breathing, generative, algorithms, life-like","Cellular Pink #FF9999, Chlorophyll Green #00FF41, Bioluminescent Blue","Deep Ocean #001E3C, Coral #FF7F50, Organic gradients","Breathing animations, fluid morphing, generative growth, physics-based movement","Sustainability tech, biotech, advanced health, meditation, generative art platforms","Standard SaaS, data grids, strict corporate, accounting",✓ Full,✓ Full,⚠ Moderate,✓ Good,✓ Good,✓ High,"Canvas 10/10, WebGL 10/10",2024+ Generative,High
|
||||
|
Can't render this file because it has a wrong number of fields in line 22.
|
58
.claude/skills/ui-ux-pro-max/data/typography.csv
Normal file
58
.claude/skills/ui-ux-pro-max/data/typography.csv
Normal file
@ -0,0 +1,58 @@
|
||||
STT,Font Pairing Name,Category,Heading Font,Body Font,Mood/Style Keywords,Best For,Google Fonts URL,CSS Import,Tailwind Config,Notes
|
||||
1,Classic Elegant,"Serif + Sans",Playfair Display,Inter,"elegant, luxury, sophisticated, timeless, premium, editorial","Luxury brands, fashion, spa, beauty, editorial, magazines, high-end e-commerce","https://fonts.google.com/share?selection.family=Inter:wght@300;400;500;600;700|Playfair+Display:wght@400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Playfair+Display:wght@400;500;600;700&display=swap');","fontFamily: { serif: ['Playfair Display', 'serif'], sans: ['Inter', 'sans-serif'] }","High contrast between elegant heading and clean body. Perfect for luxury/premium."
|
||||
2,Modern Professional,"Sans + Sans",Poppins,Open Sans,"modern, professional, clean, corporate, friendly, approachable","SaaS, corporate sites, business apps, startups, professional services","https://fonts.google.com/share?selection.family=Open+Sans:wght@300;400;500;600;700|Poppins:wght@400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&family=Poppins:wght@400;500;600;700&display=swap');","fontFamily: { heading: ['Poppins', 'sans-serif'], body: ['Open Sans', 'sans-serif'] }","Geometric Poppins for headings, humanist Open Sans for readability."
|
||||
3,Tech Startup,"Sans + Sans",Space Grotesk,DM Sans,"tech, startup, modern, innovative, bold, futuristic","Tech companies, startups, SaaS, developer tools, AI products","https://fonts.google.com/share?selection.family=DM+Sans:wght@400;500;700|Space+Grotesk:wght@400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Space+Grotesk:wght@400;500;600;700&display=swap');","fontFamily: { heading: ['Space Grotesk', 'sans-serif'], body: ['DM Sans', 'sans-serif'] }","Space Grotesk has unique character, DM Sans is highly readable."
|
||||
4,Editorial Classic,"Serif + Serif",Cormorant Garamond,Libre Baskerville,"editorial, classic, literary, traditional, refined, bookish","Publishing, blogs, news sites, literary magazines, book covers","https://fonts.google.com/share?selection.family=Cormorant+Garamond:wght@400;500;600;700|Libre+Baskerville:wght@400;700","@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600;700&family=Libre+Baskerville:wght@400;700&display=swap');","fontFamily: { heading: ['Cormorant Garamond', 'serif'], body: ['Libre Baskerville', 'serif'] }","All-serif pairing for traditional editorial feel."
|
||||
5,Minimal Swiss,"Sans + Sans",Inter,Inter,"minimal, clean, swiss, functional, neutral, professional","Dashboards, admin panels, documentation, enterprise apps, design systems","https://fonts.google.com/share?selection.family=Inter:wght@300;400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');","fontFamily: { sans: ['Inter', 'sans-serif'] }","Single font family with weight variations. Ultimate simplicity."
|
||||
6,Playful Creative,"Display + Sans",Fredoka,Nunito,"playful, friendly, fun, creative, warm, approachable","Children's apps, educational, gaming, creative tools, entertainment","https://fonts.google.com/share?selection.family=Fredoka:wght@400;500;600;700|Nunito:wght@300;400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Nunito:wght@300;400;500;600;700&display=swap');","fontFamily: { heading: ['Fredoka', 'sans-serif'], body: ['Nunito', 'sans-serif'] }","Rounded, friendly fonts perfect for playful UIs."
|
||||
7,Bold Statement,"Display + Sans",Bebas Neue,Source Sans 3,"bold, impactful, strong, dramatic, modern, headlines","Marketing sites, portfolios, agencies, event pages, sports","https://fonts.google.com/share?selection.family=Bebas+Neue|Source+Sans+3:wght@300;400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Source+Sans+3:wght@300;400;500;600;700&display=swap');","fontFamily: { display: ['Bebas Neue', 'sans-serif'], body: ['Source Sans 3', 'sans-serif'] }","Bebas Neue for large headlines only. All-caps display font."
|
||||
8,Wellness Calm,"Serif + Sans",Lora,Raleway,"calm, wellness, health, relaxing, natural, organic","Health apps, wellness, spa, meditation, yoga, organic brands","https://fonts.google.com/share?selection.family=Lora:wght@400;500;600;700|Raleway:wght@300;400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Lora:wght@400;500;600;700&family=Raleway:wght@300;400;500;600;700&display=swap');","fontFamily: { serif: ['Lora', 'serif'], sans: ['Raleway', 'sans-serif'] }","Lora's organic curves with Raleway's elegant simplicity."
|
||||
9,Developer Mono,"Mono + Sans",JetBrains Mono,IBM Plex Sans,"code, developer, technical, precise, functional, hacker","Developer tools, documentation, code editors, tech blogs, CLI apps","https://fonts.google.com/share?selection.family=IBM+Plex+Sans:wght@300;400;500;600;700|JetBrains+Mono:wght@400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap');","fontFamily: { mono: ['JetBrains Mono', 'monospace'], sans: ['IBM Plex Sans', 'sans-serif'] }","JetBrains for code, IBM Plex for UI. Developer-focused."
|
||||
10,Retro Vintage,"Display + Serif",Abril Fatface,Merriweather,"retro, vintage, nostalgic, dramatic, decorative, bold","Vintage brands, breweries, restaurants, creative portfolios, posters","https://fonts.google.com/share?selection.family=Abril+Fatface|Merriweather:wght@300;400;700","@import url('https://fonts.googleapis.com/css2?family=Abril+Fatface&family=Merriweather:wght@300;400;700&display=swap');","fontFamily: { display: ['Abril Fatface', 'serif'], body: ['Merriweather', 'serif'] }","Abril Fatface for hero headlines only. High-impact vintage feel."
|
||||
11,Geometric Modern,"Sans + Sans",Outfit,Work Sans,"geometric, modern, clean, balanced, contemporary, versatile","General purpose, portfolios, agencies, modern brands, landing pages","https://fonts.google.com/share?selection.family=Outfit:wght@300;400;500;600;700|Work+Sans:wght@300;400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Work+Sans:wght@300;400;500;600;700&display=swap');","fontFamily: { heading: ['Outfit', 'sans-serif'], body: ['Work Sans', 'sans-serif'] }","Both geometric but Outfit more distinctive for headings."
|
||||
12,Luxury Serif,"Serif + Sans",Cormorant,Montserrat,"luxury, high-end, fashion, elegant, refined, premium","Fashion brands, luxury e-commerce, jewelry, high-end services","https://fonts.google.com/share?selection.family=Cormorant:wght@400;500;600;700|Montserrat:wght@300;400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Cormorant:wght@400;500;600;700&family=Montserrat:wght@300;400;500;600;700&display=swap');","fontFamily: { serif: ['Cormorant', 'serif'], sans: ['Montserrat', 'sans-serif'] }","Cormorant's elegance with Montserrat's geometric precision."
|
||||
13,Friendly SaaS,"Sans + Sans",Plus Jakarta Sans,Plus Jakarta Sans,"friendly, modern, saas, clean, approachable, professional","SaaS products, web apps, dashboards, B2B, productivity tools","https://fonts.google.com/share?selection.family=Plus+Jakarta+Sans:wght@300;400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap');","fontFamily: { sans: ['Plus Jakarta Sans', 'sans-serif'] }","Single versatile font. Modern alternative to Inter."
|
||||
14,News Editorial,"Serif + Sans",Newsreader,Roboto,"news, editorial, journalism, trustworthy, readable, informative","News sites, blogs, magazines, journalism, content-heavy sites","https://fonts.google.com/share?selection.family=Newsreader:wght@400;500;600;700|Roboto:wght@300;400;500;700","@import url('https://fonts.googleapis.com/css2?family=Newsreader:wght@400;500;600;700&family=Roboto:wght@300;400;500;700&display=swap');","fontFamily: { serif: ['Newsreader', 'serif'], sans: ['Roboto', 'sans-serif'] }","Newsreader designed for long-form reading. Roboto for UI."
|
||||
15,Handwritten Charm,"Script + Sans",Caveat,Quicksand,"handwritten, personal, friendly, casual, warm, charming","Personal blogs, invitations, creative portfolios, lifestyle brands","https://fonts.google.com/share?selection.family=Caveat:wght@400;500;600;700|Quicksand:wght@300;400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Caveat:wght@400;500;600;700&family=Quicksand:wght@300;400;500;600;700&display=swap');","fontFamily: { script: ['Caveat', 'cursive'], sans: ['Quicksand', 'sans-serif'] }","Use Caveat sparingly for accents. Quicksand for body."
|
||||
16,Corporate Trust,"Sans + Sans",Lexend,Source Sans 3,"corporate, trustworthy, accessible, readable, professional, clean","Enterprise, government, healthcare, finance, accessibility-focused","https://fonts.google.com/share?selection.family=Lexend:wght@300;400;500;600;700|Source+Sans+3:wght@300;400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Lexend:wght@300;400;500;600;700&family=Source+Sans+3:wght@300;400;500;600;700&display=swap');","fontFamily: { heading: ['Lexend', 'sans-serif'], body: ['Source Sans 3', 'sans-serif'] }","Lexend designed for readability. Excellent accessibility."
|
||||
17,Brutalist Raw,"Mono + Mono",Space Mono,Space Mono,"brutalist, raw, technical, monospace, minimal, stark","Brutalist designs, developer portfolios, experimental, tech art","https://fonts.google.com/share?selection.family=Space+Mono:wght@400;700","@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap');","fontFamily: { mono: ['Space Mono', 'monospace'] }","All-mono for raw brutalist aesthetic. Limited weights."
|
||||
18,Fashion Forward,"Sans + Sans",Syne,Manrope,"fashion, avant-garde, creative, bold, artistic, edgy","Fashion brands, creative agencies, art galleries, design studios","https://fonts.google.com/share?selection.family=Manrope:wght@300;400;500;600;700|Syne:wght@400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@300;400;500;600;700&family=Syne:wght@400;500;600;700&display=swap');","fontFamily: { heading: ['Syne', 'sans-serif'], body: ['Manrope', 'sans-serif'] }","Syne's unique character for headlines. Manrope for readability."
|
||||
19,Soft Rounded,"Sans + Sans",Varela Round,Nunito Sans,"soft, rounded, friendly, approachable, warm, gentle","Children's products, pet apps, friendly brands, wellness, soft UI","https://fonts.google.com/share?selection.family=Nunito+Sans:wght@300;400;500;600;700|Varela+Round","@import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@300;400;500;600;700&family=Varela+Round&display=swap');","fontFamily: { heading: ['Varela Round', 'sans-serif'], body: ['Nunito Sans', 'sans-serif'] }","Both rounded and friendly. Perfect for soft UI designs."
|
||||
20,Premium Sans,"Sans + Sans",Satoshi,General Sans,"premium, modern, clean, sophisticated, versatile, balanced","Premium brands, modern agencies, SaaS, portfolios, startups","https://fonts.google.com/share?selection.family=DM+Sans:wght@400;500;700","@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap');","fontFamily: { sans: ['DM Sans', 'sans-serif'] }","Note: Satoshi/General Sans on Fontshare. DM Sans as Google alternative."
|
||||
21,Vietnamese Friendly,"Sans + Sans",Be Vietnam Pro,Noto Sans,"vietnamese, international, readable, clean, multilingual, accessible","Vietnamese sites, multilingual apps, international products","https://fonts.google.com/share?selection.family=Be+Vietnam+Pro:wght@300;400;500;600;700|Noto+Sans:wght@300;400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:wght@300;400;500;600;700&family=Noto+Sans:wght@300;400;500;600;700&display=swap');","fontFamily: { sans: ['Be Vietnam Pro', 'Noto Sans', 'sans-serif'] }","Be Vietnam Pro excellent Vietnamese support. Noto as fallback."
|
||||
22,Japanese Elegant,"Serif + Sans",Noto Serif JP,Noto Sans JP,"japanese, elegant, traditional, modern, multilingual, readable","Japanese sites, Japanese restaurants, cultural sites, anime/manga","https://fonts.google.com/share?selection.family=Noto+Sans+JP:wght@300;400;500;700|Noto+Serif+JP:wght@400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@300;400;500;700&family=Noto+Serif+JP:wght@400;500;600;700&display=swap');","fontFamily: { serif: ['Noto Serif JP', 'serif'], sans: ['Noto Sans JP', 'sans-serif'] }","Noto fonts excellent Japanese support. Traditional + modern feel."
|
||||
23,Korean Modern,"Sans + Sans",Noto Sans KR,Noto Sans KR,"korean, modern, clean, professional, multilingual, readable","Korean sites, K-beauty, K-pop, Korean businesses, multilingual","https://fonts.google.com/share?selection.family=Noto+Sans+KR:wght@300;400;500;700","@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap');","fontFamily: { sans: ['Noto Sans KR', 'sans-serif'] }","Clean Korean typography. Single font with weight variations."
|
||||
24,Chinese Traditional,"Serif + Sans",Noto Serif TC,Noto Sans TC,"chinese, traditional, elegant, cultural, multilingual, readable","Traditional Chinese sites, cultural content, Taiwan/Hong Kong markets","https://fonts.google.com/share?selection.family=Noto+Sans+TC:wght@300;400;500;700|Noto+Serif+TC:wght@400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@300;400;500;700&family=Noto+Serif+TC:wght@400;500;600;700&display=swap');","fontFamily: { serif: ['Noto Serif TC', 'serif'], sans: ['Noto Sans TC', 'sans-serif'] }","Traditional Chinese character support. Elegant pairing."
|
||||
25,Chinese Simplified,"Sans + Sans",Noto Sans SC,Noto Sans SC,"chinese, simplified, modern, professional, multilingual, readable","Simplified Chinese sites, mainland China market, business apps","https://fonts.google.com/share?selection.family=Noto+Sans+SC:wght@300;400;500;700","@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap');","fontFamily: { sans: ['Noto Sans SC', 'sans-serif'] }","Simplified Chinese support. Clean modern look."
|
||||
26,Arabic Elegant,"Serif + Sans",Noto Naskh Arabic,Noto Sans Arabic,"arabic, elegant, traditional, cultural, RTL, readable","Arabic sites, Middle East market, Islamic content, bilingual sites","https://fonts.google.com/share?selection.family=Noto+Naskh+Arabic:wght@400;500;600;700|Noto+Sans+Arabic:wght@300;400;500;700","@import url('https://fonts.googleapis.com/css2?family=Noto+Naskh+Arabic:wght@400;500;600;700&family=Noto+Sans+Arabic:wght@300;400;500;700&display=swap');","fontFamily: { serif: ['Noto Naskh Arabic', 'serif'], sans: ['Noto Sans Arabic', 'sans-serif'] }","RTL support. Naskh for traditional, Sans for modern Arabic."
|
||||
27,Thai Modern,"Sans + Sans",Noto Sans Thai,Noto Sans Thai,"thai, modern, readable, clean, multilingual, accessible","Thai sites, Southeast Asia, tourism, Thai restaurants","https://fonts.google.com/share?selection.family=Noto+Sans+Thai:wght@300;400;500;700","@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+Thai:wght@300;400;500;700&display=swap');","fontFamily: { sans: ['Noto Sans Thai', 'sans-serif'] }","Clean Thai typography. Excellent readability."
|
||||
28,Hebrew Modern,"Sans + Sans",Noto Sans Hebrew,Noto Sans Hebrew,"hebrew, modern, RTL, clean, professional, readable","Hebrew sites, Israeli market, Jewish content, bilingual sites","https://fonts.google.com/share?selection.family=Noto+Sans+Hebrew:wght@300;400;500;700","@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+Hebrew:wght@300;400;500;700&display=swap');","fontFamily: { sans: ['Noto Sans Hebrew', 'sans-serif'] }","RTL support. Clean modern Hebrew typography."
|
||||
29,Legal Professional,"Serif + Sans",EB Garamond,Lato,"legal, professional, traditional, trustworthy, formal, authoritative","Law firms, legal services, contracts, formal documents, government","https://fonts.google.com/share?selection.family=EB+Garamond:wght@400;500;600;700|Lato:wght@300;400;700","@import url('https://fonts.googleapis.com/css2?family=EB+Garamond:wght@400;500;600;700&family=Lato:wght@300;400;700&display=swap');","fontFamily: { serif: ['EB Garamond', 'serif'], sans: ['Lato', 'sans-serif'] }","EB Garamond for authority. Lato for clean body text."
|
||||
30,Medical Clean,"Sans + Sans",Figtree,Noto Sans,"medical, clean, accessible, professional, healthcare, trustworthy","Healthcare, medical clinics, pharma, health apps, accessibility","https://fonts.google.com/share?selection.family=Figtree:wght@300;400;500;600;700|Noto+Sans:wght@300;400;500;700","@import url('https://fonts.googleapis.com/css2?family=Figtree:wght@300;400;500;600;700&family=Noto+Sans:wght@300;400;500;700&display=swap');","fontFamily: { heading: ['Figtree', 'sans-serif'], body: ['Noto Sans', 'sans-serif'] }","Clean, accessible fonts for medical contexts."
|
||||
31,Financial Trust,"Sans + Sans",IBM Plex Sans,IBM Plex Sans,"financial, trustworthy, professional, corporate, banking, serious","Banks, finance, insurance, investment, fintech, enterprise","https://fonts.google.com/share?selection.family=IBM+Plex+Sans:wght@300;400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600;700&display=swap');","fontFamily: { sans: ['IBM Plex Sans', 'sans-serif'] }","IBM Plex conveys trust and professionalism. Excellent for data."
|
||||
32,Real Estate Luxury,"Serif + Sans",Cinzel,Josefin Sans,"real estate, luxury, elegant, sophisticated, property, premium","Real estate, luxury properties, architecture, interior design","https://fonts.google.com/share?selection.family=Cinzel:wght@400;500;600;700|Josefin+Sans:wght@300;400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;500;600;700&family=Josefin+Sans:wght@300;400;500;600;700&display=swap');","fontFamily: { serif: ['Cinzel', 'serif'], sans: ['Josefin Sans', 'sans-serif'] }","Cinzel's elegance for headlines. Josefin for modern body."
|
||||
33,Restaurant Menu,"Serif + Sans",Playfair Display SC,Karla,"restaurant, menu, culinary, elegant, foodie, hospitality","Restaurants, cafes, food blogs, culinary, hospitality","https://fonts.google.com/share?selection.family=Karla:wght@300;400;500;600;700|Playfair+Display+SC:wght@400;700","@import url('https://fonts.googleapis.com/css2?family=Karla:wght@300;400;500;600;700&family=Playfair+Display+SC:wght@400;700&display=swap');","fontFamily: { display: ['Playfair Display SC', 'serif'], sans: ['Karla', 'sans-serif'] }","Small caps Playfair for menu headers. Karla for descriptions."
|
||||
34,Art Deco,"Display + Sans",Poiret One,Didact Gothic,"art deco, vintage, 1920s, elegant, decorative, gatsby","Vintage events, art deco themes, luxury hotels, classic cocktails","https://fonts.google.com/share?selection.family=Didact+Gothic|Poiret+One","@import url('https://fonts.googleapis.com/css2?family=Didact+Gothic&family=Poiret+One&display=swap');","fontFamily: { display: ['Poiret One', 'sans-serif'], sans: ['Didact Gothic', 'sans-serif'] }","Poiret One for art deco headlines only. Didact for body."
|
||||
35,Magazine Style,"Serif + Sans",Libre Bodoni,Public Sans,"magazine, editorial, publishing, refined, journalism, print","Magazines, online publications, editorial content, journalism","https://fonts.google.com/share?selection.family=Libre+Bodoni:wght@400;500;600;700|Public+Sans:wght@300;400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Libre+Bodoni:wght@400;500;600;700&family=Public+Sans:wght@300;400;500;600;700&display=swap');","fontFamily: { serif: ['Libre Bodoni', 'serif'], sans: ['Public Sans', 'sans-serif'] }","Bodoni's editorial elegance. Public Sans for clean UI."
|
||||
36,Crypto/Web3,"Sans + Sans",Orbitron,Exo 2,"crypto, web3, futuristic, tech, blockchain, digital","Crypto platforms, NFT, blockchain, web3, futuristic tech","https://fonts.google.com/share?selection.family=Exo+2:wght@300;400;500;600;700|Orbitron:wght@400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Exo+2:wght@300;400;500;600;700&family=Orbitron:wght@400;500;600;700&display=swap');","fontFamily: { display: ['Orbitron', 'sans-serif'], body: ['Exo 2', 'sans-serif'] }","Orbitron for futuristic headers. Exo 2 for readable body."
|
||||
37,Gaming Bold,"Display + Sans",Russo One,Chakra Petch,"gaming, bold, action, esports, competitive, energetic","Gaming, esports, action games, competitive sports, entertainment","https://fonts.google.com/share?selection.family=Chakra+Petch:wght@300;400;500;600;700|Russo+One","@import url('https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@300;400;500;600;700&family=Russo+One&display=swap');","fontFamily: { display: ['Russo One', 'sans-serif'], body: ['Chakra Petch', 'sans-serif'] }","Russo One for impact. Chakra Petch for techy body text."
|
||||
38,Indie/Craft,"Display + Sans",Amatic SC,Cabin,"indie, craft, handmade, artisan, organic, creative","Craft brands, indie products, artisan, handmade, organic products","https://fonts.google.com/share?selection.family=Amatic+SC:wght@400;700|Cabin:wght@400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Amatic+SC:wght@400;700&family=Cabin:wght@400;500;600;700&display=swap');","fontFamily: { display: ['Amatic SC', 'sans-serif'], sans: ['Cabin', 'sans-serif'] }","Amatic for handwritten feel. Cabin for readable body."
|
||||
39,Startup Bold,"Sans + Sans",Clash Display,Satoshi,"startup, bold, modern, innovative, confident, dynamic","Startups, pitch decks, product launches, bold brands","https://fonts.google.com/share?selection.family=Outfit:wght@400;500;600;700|Rubik:wght@300;400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=Rubik:wght@300;400;500;600;700&display=swap');","fontFamily: { heading: ['Outfit', 'sans-serif'], body: ['Rubik', 'sans-serif'] }","Note: Clash Display on Fontshare. Outfit as Google alternative."
|
||||
40,E-commerce Clean,"Sans + Sans",Rubik,Nunito Sans,"ecommerce, clean, shopping, product, retail, conversion","E-commerce, online stores, product pages, retail, shopping","https://fonts.google.com/share?selection.family=Nunito+Sans:wght@300;400;500;600;700|Rubik:wght@300;400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@300;400;500;600;700&family=Rubik:wght@300;400;500;600;700&display=swap');","fontFamily: { heading: ['Rubik', 'sans-serif'], body: ['Nunito Sans', 'sans-serif'] }","Clean readable fonts perfect for product descriptions."
|
||||
41,Academic/Research,"Serif + Sans",Crimson Pro,Atkinson Hyperlegible,"academic, research, scholarly, accessible, readable, educational","Universities, research papers, academic journals, educational","https://fonts.google.com/share?selection.family=Atkinson+Hyperlegible:wght@400;700|Crimson+Pro:wght@400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:wght@400;700&family=Crimson+Pro:wght@400;500;600;700&display=swap');","fontFamily: { serif: ['Crimson Pro', 'serif'], sans: ['Atkinson Hyperlegible', 'sans-serif'] }","Crimson for scholarly headlines. Atkinson for accessibility."
|
||||
42,Dashboard Data,"Mono + Sans",Fira Code,Fira Sans,"dashboard, data, analytics, code, technical, precise","Dashboards, analytics, data visualization, admin panels","https://fonts.google.com/share?selection.family=Fira+Code:wght@400;500;600;700|Fira+Sans:wght@300;400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600;700&family=Fira+Sans:wght@300;400;500;600;700&display=swap');","fontFamily: { mono: ['Fira Code', 'monospace'], sans: ['Fira Sans', 'sans-serif'] }","Fira family cohesion. Code for data, Sans for labels."
|
||||
43,Music/Entertainment,"Display + Sans",Righteous,Poppins,"music, entertainment, fun, energetic, bold, performance","Music platforms, entertainment, events, festivals, performers","https://fonts.google.com/share?selection.family=Poppins:wght@300;400;500;600;700|Righteous","@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&family=Righteous&display=swap');","fontFamily: { display: ['Righteous', 'sans-serif'], sans: ['Poppins', 'sans-serif'] }","Righteous for bold entertainment headers. Poppins for body."
|
||||
44,Minimalist Portfolio,"Sans + Sans",Archivo,Space Grotesk,"minimal, portfolio, designer, creative, clean, artistic","Design portfolios, creative professionals, minimalist brands","https://fonts.google.com/share?selection.family=Archivo:wght@300;400;500;600;700|Space+Grotesk:wght@300;400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Archivo:wght@300;400;500;600;700&family=Space+Grotesk:wght@300;400;500;600;700&display=swap');","fontFamily: { heading: ['Space Grotesk', 'sans-serif'], body: ['Archivo', 'sans-serif'] }","Space Grotesk for distinctive headers. Archivo for clean body."
|
||||
45,Kids/Education,"Display + Sans",Baloo 2,Comic Neue,"kids, education, playful, friendly, colorful, learning","Children's apps, educational games, kid-friendly content","https://fonts.google.com/share?selection.family=Baloo+2:wght@400;500;600;700|Comic+Neue:wght@300;400;700","@import url('https://fonts.googleapis.com/css2?family=Baloo+2:wght@400;500;600;700&family=Comic+Neue:wght@300;400;700&display=swap');","fontFamily: { display: ['Baloo 2', 'sans-serif'], sans: ['Comic Neue', 'sans-serif'] }","Fun, playful fonts for children. Comic Neue is readable comic style."
|
||||
46,Wedding/Romance,"Script + Serif",Great Vibes,Cormorant Infant,"wedding, romance, elegant, script, invitation, feminine","Wedding sites, invitations, romantic brands, bridal","https://fonts.google.com/share?selection.family=Cormorant+Infant:wght@300;400;500;600;700|Great+Vibes","@import url('https://fonts.googleapis.com/css2?family=Cormorant+Infant:wght@300;400;500;600;700&family=Great+Vibes&display=swap');","fontFamily: { script: ['Great Vibes', 'cursive'], serif: ['Cormorant Infant', 'serif'] }","Great Vibes for elegant accents. Cormorant for readable text."
|
||||
47,Science/Tech,"Sans + Sans",Exo,Roboto Mono,"science, technology, research, data, futuristic, precise","Science, research, tech documentation, data-heavy sites","https://fonts.google.com/share?selection.family=Exo:wght@300;400;500;600;700|Roboto+Mono:wght@300;400;500;700","@import url('https://fonts.googleapis.com/css2?family=Exo:wght@300;400;500;600;700&family=Roboto+Mono:wght@300;400;500;700&display=swap');","fontFamily: { sans: ['Exo', 'sans-serif'], mono: ['Roboto Mono', 'monospace'] }","Exo for modern tech feel. Roboto Mono for code/data."
|
||||
48,Accessibility First,"Sans + Sans",Atkinson Hyperlegible,Atkinson Hyperlegible,"accessible, readable, inclusive, WCAG, dyslexia-friendly, clear","Accessibility-critical sites, government, healthcare, inclusive design","https://fonts.google.com/share?selection.family=Atkinson+Hyperlegible:wght@400;700","@import url('https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:wght@400;700&display=swap');","fontFamily: { sans: ['Atkinson Hyperlegible', 'sans-serif'] }","Designed for maximum legibility. Excellent for accessibility."
|
||||
49,Sports/Fitness,"Sans + Sans",Barlow Condensed,Barlow,"sports, fitness, athletic, energetic, condensed, action","Sports, fitness, gyms, athletic brands, competition","https://fonts.google.com/share?selection.family=Barlow+Condensed:wght@400;500;600;700|Barlow:wght@300;400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@400;500;600;700&family=Barlow:wght@300;400;500;600;700&display=swap');","fontFamily: { display: ['Barlow Condensed', 'sans-serif'], body: ['Barlow', 'sans-serif'] }","Condensed for impact headlines. Regular Barlow for body."
|
||||
50,Luxury Minimalist,"Serif + Sans",Bodoni Moda,Jost,"luxury, minimalist, high-end, sophisticated, refined, premium","Luxury minimalist brands, high-end fashion, premium products","https://fonts.google.com/share?selection.family=Bodoni+Moda:wght@400;500;600;700|Jost:wght@300;400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Bodoni+Moda:wght@400;500;600;700&family=Jost:wght@300;400;500;600;700&display=swap');","fontFamily: { serif: ['Bodoni Moda', 'serif'], sans: ['Jost', 'sans-serif'] }","Bodoni's high contrast elegance. Jost for geometric body."
|
||||
51,Tech/HUD Mono,"Mono + Mono",Share Tech Mono,Fira Code,"tech, futuristic, hud, sci-fi, data, monospaced, precise","Sci-fi interfaces, developer tools, cybersecurity, dashboards","https://fonts.google.com/share?selection.family=Fira+Code:wght@300;400;500;600;700|Share+Tech+Mono","@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&family=Share+Tech+Mono&display=swap');","fontFamily: { hud: ['Share Tech Mono', 'monospace'], code: ['Fira Code', 'monospace'] }","Share Tech Mono has that classic sci-fi look."
|
||||
52,Pixel Retro,"Display + Sans",Press Start 2P,VT323,"pixel, retro, gaming, 8-bit, nostalgic, arcade","Pixel art games, retro websites, creative portfolios","https://fonts.google.com/share?selection.family=Press+Start+2P|VT323","@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=VT323&display=swap');","fontFamily: { pixel: ['Press Start 2P', 'cursive'], terminal: ['VT323', 'monospace'] }","Press Start 2P is very wide/large. VT323 is better for body text."
|
||||
53,Neubrutalist Bold,"Display + Sans",Lexend Mega,Public Sans,"bold, neubrutalist, loud, strong, geometric, quirky","Neubrutalist designs, Gen Z brands, bold marketing","https://fonts.google.com/share?selection.family=Lexend+Mega:wght@100..900|Public+Sans:wght@100..900","@import url('https://fonts.googleapis.com/css2?family=Lexend+Mega:wght@100..900&family=Public+Sans:wght@100..900&display=swap');","fontFamily: { mega: ['Lexend Mega', 'sans-serif'], body: ['Public Sans', 'sans-serif'] }","Lexend Mega has distinct character and variable weight."
|
||||
54,Academic/Archival,"Serif + Serif",EB Garamond,Crimson Text,"academic, old-school, university, research, serious, traditional","University sites, archives, research papers, history","https://fonts.google.com/share?selection.family=Crimson+Text:wght@400;600;700|EB+Garamond:wght@400;500;600;700;800","@import url('https://fonts.googleapis.com/css2?family=Crimson+Text:wght@400;600;700&family=EB+Garamond:wght@400;500;600;700;800&display=swap');","fontFamily: { classic: ['EB Garamond', 'serif'], text: ['Crimson Text', 'serif'] }","Classic academic aesthetic. Very legible."
|
||||
55,Spatial Clear,"Sans + Sans",Inter,Inter,"spatial, legible, glass, system, clean, neutral","Spatial computing, AR/VR, glassmorphism interfaces","https://fonts.google.com/share?selection.family=Inter:wght@300;400;500;600","@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap');","fontFamily: { sans: ['Inter', 'sans-serif'] }","Optimized for readability on dynamic backgrounds."
|
||||
56,Kinetic Motion,"Display + Mono",Syncopate,Space Mono,"kinetic, motion, futuristic, speed, wide, tech","Music festivals, automotive, high-energy brands","https://fonts.google.com/share?selection.family=Space+Mono:wght@400;700|Syncopate:wght@400;700","@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syncopate:wght@400;700&display=swap');","fontFamily: { display: ['Syncopate', 'sans-serif'], mono: ['Space Mono', 'monospace'] }","Syncopate's wide stance works well with motion effects."
|
||||
57,Gen Z Brutal,"Display + Sans",Anton,Epilogue,"brutal, loud, shouty, meme, internet, bold","Gen Z marketing, streetwear, viral campaigns","https://fonts.google.com/share?selection.family=Anton|Epilogue:wght@400;500;600;700","@import url('https://fonts.googleapis.com/css2?family=Anton&family=Epilogue:wght@400;500;600;700&display=swap');","fontFamily: { display: ['Anton', 'sans-serif'], body: ['Epilogue', 'sans-serif'] }","Anton is impactful and condensed. Good for stickers/badges."
|
||||
|
100
.claude/skills/ui-ux-pro-max/data/ux-guidelines.csv
Normal file
100
.claude/skills/ui-ux-pro-max/data/ux-guidelines.csv
Normal file
@ -0,0 +1,100 @@
|
||||
No,Category,Issue,Platform,Description,Do,Don't,Code Example Good,Code Example Bad,Severity
|
||||
1,Navigation,Smooth Scroll,Web,Anchor links should scroll smoothly to target section,Use scroll-behavior: smooth on html element,Jump directly without transition,html { scroll-behavior: smooth; },<a href='#section'> without CSS,High
|
||||
2,Navigation,Sticky Navigation,Web,Fixed nav should not obscure content,Add padding-top to body equal to nav height,Let nav overlap first section content,pt-20 (if nav is h-20),No padding compensation,Medium
|
||||
3,Navigation,Active State,All,Current page/section should be visually indicated,Highlight active nav item with color/underline,No visual feedback on current location,text-primary border-b-2,All links same style,Medium
|
||||
4,Navigation,Back Button,Mobile,Users expect back to work predictably,Preserve navigation history properly,Break browser/app back button behavior,history.pushState(),location.replace(),High
|
||||
5,Navigation,Deep Linking,All,URLs should reflect current state for sharing,Update URL on state/view changes,Static URLs for dynamic content,Use query params or hash,Single URL for all states,Medium
|
||||
6,Navigation,Breadcrumbs,Web,Show user location in site hierarchy,Use for sites with 3+ levels of depth,Use for flat single-level sites,Home > Category > Product,Only on deep nested pages,Low
|
||||
7,Animation,Excessive Motion,All,Too many animations cause distraction and motion sickness,Animate 1-2 key elements per view maximum,Animate everything that moves,Single hero animation,animate-bounce on 5+ elements,High
|
||||
8,Animation,Duration Timing,All,Animations should feel responsive not sluggish,Use 150-300ms for micro-interactions,Use animations longer than 500ms for UI,transition-all duration-200,duration-1000,Medium
|
||||
9,Animation,Reduced Motion,All,Respect user's motion preferences,Check prefers-reduced-motion media query,Ignore accessibility motion settings,@media (prefers-reduced-motion: reduce),No motion query check,High
|
||||
10,Animation,Loading States,All,Show feedback during async operations,Use skeleton screens or spinners,Leave UI frozen with no feedback,animate-pulse skeleton,Blank screen while loading,High
|
||||
11,Animation,Hover vs Tap,All,Hover effects don't work on touch devices,Use click/tap for primary interactions,Rely only on hover for important actions,onClick handler,onMouseEnter only,High
|
||||
12,Animation,Continuous Animation,All,Infinite animations are distracting,Use for loading indicators only,Use for decorative elements,animate-spin on loader,animate-bounce on icons,Medium
|
||||
13,Animation,Transform Performance,Web,Some CSS properties trigger expensive repaints,Use transform and opacity for animations,Animate width/height/top/left properties,transform: translateY(),top: 10px animation,Medium
|
||||
14,Animation,Easing Functions,All,Linear motion feels robotic,Use ease-out for entering ease-in for exiting,Use linear for UI transitions,ease-out,linear,Low
|
||||
15,Layout,Z-Index Management,Web,Stacking context conflicts cause hidden elements,Define z-index scale system (10 20 30 50),Use arbitrary large z-index values,z-10 z-20 z-50,z-[9999],High
|
||||
16,Layout,Overflow Hidden,Web,Hidden overflow can clip important content,Test all content fits within containers,Blindly apply overflow-hidden,overflow-auto with scroll,overflow-hidden truncating content,Medium
|
||||
17,Layout,Fixed Positioning,Web,Fixed elements can overlap or be inaccessible,Account for safe areas and other fixed elements,Stack multiple fixed elements carelessly,Fixed nav + fixed bottom with gap,Multiple overlapping fixed elements,Medium
|
||||
18,Layout,Stacking Context,Web,New stacking contexts reset z-index,Understand what creates new stacking context,Expect z-index to work across contexts,Parent with z-index isolates children,z-index: 9999 not working,Medium
|
||||
19,Layout,Content Jumping,Web,Layout shift when content loads is jarring,Reserve space for async content,Let images/content push layout around,aspect-ratio or fixed height,No dimensions on images,High
|
||||
20,Layout,Viewport Units,Web,100vh can be problematic on mobile browsers,Use dvh or account for mobile browser chrome,Use 100vh for full-screen mobile layouts,min-h-dvh or min-h-screen,h-screen on mobile,Medium
|
||||
21,Layout,Container Width,Web,Content too wide is hard to read,Limit max-width for text content (65-75ch),Let text span full viewport width,max-w-prose or max-w-3xl,Full width paragraphs,Medium
|
||||
22,Touch,Touch Target Size,Mobile,Small buttons are hard to tap accurately,Minimum 44x44px touch targets,Tiny clickable areas,min-h-[44px] min-w-[44px],w-6 h-6 buttons,High
|
||||
23,Touch,Touch Spacing,Mobile,Adjacent touch targets need adequate spacing,Minimum 8px gap between touch targets,Tightly packed clickable elements,gap-2 between buttons,gap-0 or gap-1,Medium
|
||||
24,Touch,Gesture Conflicts,Mobile,Custom gestures can conflict with system,Avoid horizontal swipe on main content,Override system gestures,Vertical scroll primary,Horizontal swipe carousel only,Medium
|
||||
25,Touch,Tap Delay,Mobile,300ms tap delay feels laggy,Use touch-action CSS or fastclick,Default mobile tap handling,touch-action: manipulation,No touch optimization,Medium
|
||||
26,Touch,Pull to Refresh,Mobile,Accidental refresh is frustrating,Disable where not needed,Enable by default everywhere,overscroll-behavior: contain,Default overscroll,Low
|
||||
27,Touch,Haptic Feedback,Mobile,Tactile feedback improves interaction feel,Use for confirmations and important actions,Overuse vibration feedback,navigator.vibrate(10),Vibrate on every tap,Low
|
||||
28,Interaction,Focus States,All,Keyboard users need visible focus indicators,Use visible focus rings on interactive elements,Remove focus outline without replacement,focus:ring-2 focus:ring-blue-500,outline-none without alternative,High
|
||||
29,Interaction,Hover States,Web,Visual feedback on interactive elements,Change cursor and add subtle visual change,No hover feedback on clickable elements,hover:bg-gray-100 cursor-pointer,No hover style,Medium
|
||||
30,Interaction,Active States,All,Show immediate feedback on press/click,Add pressed/active state visual change,No feedback during interaction,active:scale-95,No active state,Medium
|
||||
31,Interaction,Disabled States,All,Clearly indicate non-interactive elements,Reduce opacity and change cursor,Confuse disabled with normal state,opacity-50 cursor-not-allowed,Same style as enabled,Medium
|
||||
32,Interaction,Loading Buttons,All,Prevent double submission during async actions,Disable button and show loading state,Allow multiple clicks during processing,disabled={loading} spinner,Button clickable while loading,High
|
||||
33,Interaction,Error Feedback,All,Users need to know when something fails,Show clear error messages near problem,Silent failures with no feedback,Red border + error message,No indication of error,High
|
||||
34,Interaction,Success Feedback,All,Confirm successful actions to users,Show success message or visual change,No confirmation of completed action,Toast notification or checkmark,Action completes silently,Medium
|
||||
35,Interaction,Confirmation Dialogs,All,Prevent accidental destructive actions,Confirm before delete/irreversible actions,Delete without confirmation,Are you sure modal,Direct delete on click,High
|
||||
36,Accessibility,Color Contrast,All,Text must be readable against background,Minimum 4.5:1 ratio for normal text,Low contrast text,#333 on white (7:1),#999 on white (2.8:1),High
|
||||
37,Accessibility,Color Only,All,Don't convey information by color alone,Use icons/text in addition to color,Red/green only for error/success,Red text + error icon,Red border only for error,High
|
||||
38,Accessibility,Alt Text,All,Images need text alternatives,Descriptive alt text for meaningful images,Empty or missing alt attributes,alt='Dog playing in park',alt='' for content images,High
|
||||
39,Accessibility,Heading Hierarchy,Web,Screen readers use headings for navigation,Use sequential heading levels h1-h6,Skip heading levels or misuse for styling,h1 then h2 then h3,h1 then h4,Medium
|
||||
40,Accessibility,ARIA Labels,All,Interactive elements need accessible names,Add aria-label for icon-only buttons,Icon buttons without labels,aria-label='Close menu',<button><Icon/></button>,High
|
||||
41,Accessibility,Keyboard Navigation,Web,All functionality accessible via keyboard,Tab order matches visual order,Keyboard traps or illogical tab order,tabIndex for custom order,Unreachable elements,High
|
||||
42,Accessibility,Screen Reader,All,Content should make sense when read aloud,Use semantic HTML and ARIA properly,Div soup with no semantics,<nav> <main> <article>,<div> for everything,Medium
|
||||
43,Accessibility,Form Labels,All,Inputs must have associated labels,Use label with for attribute or wrap input,Placeholder-only inputs,<label for='email'>,placeholder='Email' only,High
|
||||
44,Accessibility,Error Messages,All,Error messages must be announced,Use aria-live or role=alert for errors,Visual-only error indication,role='alert',Red border only,High
|
||||
45,Accessibility,Skip Links,Web,Allow keyboard users to skip navigation,Provide skip to main content link,No skip link on nav-heavy pages,Skip to main content link,100 tabs to reach content,Medium
|
||||
46,Performance,Image Optimization,All,Large images slow page load,Use appropriate size and format (WebP),Unoptimized full-size images,srcset with multiple sizes,4000px image for 400px display,High
|
||||
47,Performance,Lazy Loading,All,Load content as needed,Lazy load below-fold images and content,Load everything upfront,loading='lazy',All images eager load,Medium
|
||||
48,Performance,Code Splitting,Web,Large bundles slow initial load,Split code by route/feature,Single large bundle,dynamic import(),All code in main bundle,Medium
|
||||
49,Performance,Caching,Web,Repeat visits should be fast,Set appropriate cache headers,No caching strategy,Cache-Control headers,Every request hits server,Medium
|
||||
50,Performance,Font Loading,Web,Web fonts can block rendering,Use font-display swap or optional,Invisible text during font load,font-display: swap,FOIT (Flash of Invisible Text),Medium
|
||||
51,Performance,Third Party Scripts,Web,External scripts can block rendering,Load non-critical scripts async/defer,Synchronous third-party scripts,async or defer attribute,<script src='...'> in head,Medium
|
||||
52,Performance,Bundle Size,Web,Large JavaScript slows interaction,Monitor and minimize bundle size,Ignore bundle size growth,Bundle analyzer,No size monitoring,Medium
|
||||
53,Performance,Render Blocking,Web,CSS/JS can block first paint,Inline critical CSS defer non-critical,Large blocking CSS files,Critical CSS inline,All CSS in head,Medium
|
||||
54,Forms,Input Labels,All,Every input needs a visible label,Always show label above or beside input,Placeholder as only label,<label>Email</label><input>,placeholder='Email' only,High
|
||||
55,Forms,Error Placement,All,Errors should appear near the problem,Show error below related input,Single error message at top of form,Error under each field,All errors at form top,Medium
|
||||
56,Forms,Inline Validation,All,Validate as user types or on blur,Validate on blur for most fields,Validate only on submit,onBlur validation,Submit-only validation,Medium
|
||||
57,Forms,Input Types,All,Use appropriate input types,Use email tel number url etc,Text input for everything,type='email',type='text' for email,Medium
|
||||
58,Forms,Autofill Support,Web,Help browsers autofill correctly,Use autocomplete attribute properly,Block or ignore autofill,autocomplete='email',autocomplete='off' everywhere,Medium
|
||||
59,Forms,Required Indicators,All,Mark required fields clearly,Use asterisk or (required) text,No indication of required fields,* required indicator,Guess which are required,Medium
|
||||
60,Forms,Password Visibility,All,Let users see password while typing,Toggle to show/hide password,No visibility toggle,Show/hide password button,Password always hidden,Medium
|
||||
61,Forms,Submit Feedback,All,Confirm form submission status,Show loading then success/error state,No feedback after submit,Loading -> Success message,Button click with no response,High
|
||||
62,Forms,Input Affordance,All,Inputs should look interactive,Use distinct input styling,Inputs that look like plain text,Border/background on inputs,Borderless inputs,Medium
|
||||
63,Forms,Mobile Keyboards,Mobile,Show appropriate keyboard for input type,Use inputmode attribute,Default keyboard for all inputs,inputmode='numeric',Text keyboard for numbers,Medium
|
||||
64,Responsive,Mobile First,Web,Design for mobile then enhance for larger,Start with mobile styles then add breakpoints,Desktop-first causing mobile issues,Default mobile + md: lg: xl:,Desktop default + max-width queries,Medium
|
||||
65,Responsive,Breakpoint Testing,Web,Test at all common screen sizes,Test at 320 375 414 768 1024 1440,Only test on your device,Multiple device testing,Single device development,Medium
|
||||
66,Responsive,Touch Friendly,Web,Mobile layouts need touch-sized targets,Increase touch targets on mobile,Same tiny buttons on mobile,Larger buttons on mobile,Desktop-sized targets on mobile,High
|
||||
67,Responsive,Readable Font Size,All,Text must be readable on all devices,Minimum 16px body text on mobile,Tiny text on mobile,text-base or larger,text-xs for body text,High
|
||||
68,Responsive,Viewport Meta,Web,Set viewport for mobile devices,Use width=device-width initial-scale=1,Missing or incorrect viewport,<meta name='viewport'...>,No viewport meta tag,High
|
||||
69,Responsive,Horizontal Scroll,Web,Avoid horizontal scrolling,Ensure content fits viewport width,Content wider than viewport,max-w-full overflow-x-hidden,Horizontal scrollbar on mobile,High
|
||||
70,Responsive,Image Scaling,Web,Images should scale with container,Use max-width: 100% on images,Fixed width images overflow,max-w-full h-auto,width='800' fixed,Medium
|
||||
71,Responsive,Table Handling,Web,Tables can overflow on mobile,Use horizontal scroll or card layout,Wide tables breaking layout,overflow-x-auto wrapper,Table overflows viewport,Medium
|
||||
72,Typography,Line Height,All,Adequate line height improves readability,Use 1.5-1.75 for body text,Cramped or excessive line height,leading-relaxed (1.625),leading-none (1),Medium
|
||||
73,Typography,Line Length,Web,Long lines are hard to read,Limit to 65-75 characters per line,Full-width text on large screens,max-w-prose,Full viewport width text,Medium
|
||||
74,Typography,Font Size Scale,All,Consistent type hierarchy aids scanning,Use consistent modular scale,Random font sizes,Type scale (12 14 16 18 24 32),Arbitrary sizes,Medium
|
||||
75,Typography,Font Loading,Web,Fonts should load without layout shift,Reserve space with fallback font,Layout shift when fonts load,font-display: swap + similar fallback,No fallback font,Medium
|
||||
76,Typography,Contrast Readability,All,Body text needs good contrast,Use darker text on light backgrounds,Gray text on gray background,text-gray-900 on white,text-gray-400 on gray-100,High
|
||||
77,Typography,Heading Clarity,All,Headings should stand out from body,Clear size/weight difference,Headings similar to body text,Bold + larger size,Same size as body,Medium
|
||||
78,Feedback,Loading Indicators,All,Show system status during waits,Show spinner/skeleton for operations > 300ms,No feedback during loading,Skeleton or spinner,Frozen UI,High
|
||||
79,Feedback,Empty States,All,Guide users when no content exists,Show helpful message and action,Blank empty screens,No items yet. Create one!,Empty white space,Medium
|
||||
80,Feedback,Error Recovery,All,Help users recover from errors,Provide clear next steps,Error without recovery path,Try again button + help link,Error message only,Medium
|
||||
81,Feedback,Progress Indicators,All,Show progress for multi-step processes,Step indicators or progress bar,No indication of progress,Step 2 of 4 indicator,No step information,Medium
|
||||
82,Feedback,Toast Notifications,All,Transient messages for non-critical info,Auto-dismiss after 3-5 seconds,Toasts that never disappear,Auto-dismiss toast,Persistent toast,Medium
|
||||
83,Feedback,Confirmation Messages,All,Confirm successful actions,Brief success message,Silent success,Saved successfully toast,No confirmation,Medium
|
||||
84,Content,Truncation,All,Handle long content gracefully,Truncate with ellipsis and expand option,Overflow or broken layout,line-clamp-2 with expand,Overflow or cut off,Medium
|
||||
85,Content,Date Formatting,All,Use locale-appropriate date formats,Use relative or locale-aware dates,Ambiguous date formats,2 hours ago or locale format,01/02/03,Low
|
||||
86,Content,Number Formatting,All,Format large numbers for readability,Use thousand separators or abbreviations,Long unformatted numbers,"1.2K or 1,234",1234567,Low
|
||||
87,Content,Placeholder Content,All,Show realistic placeholders during dev,Use realistic sample data,Lorem ipsum everywhere,Real sample content,Lorem ipsum,Low
|
||||
88,Onboarding,User Freedom,All,Users should be able to skip tutorials,Provide Skip and Back buttons,Force linear unskippable tour,Skip Tutorial button,Locked overlay until finished,Medium
|
||||
89,Search,Autocomplete,Web,Help users find results faster,Show predictions as user types,Require full type and enter,Debounced fetch + dropdown,No suggestions,Medium
|
||||
90,Search,No Results,Web,Dead ends frustrate users,Show 'No results' with suggestions,Blank screen or '0 results',Try searching for X instead,No results found.,Medium
|
||||
91,Data Entry,Bulk Actions,Web,Editing one by one is tedious,Allow multi-select and bulk edit,Single row actions only,Checkbox column + Action bar,Repeated actions per row,Low
|
||||
92,AI Interaction,Disclaimer,All,Users need to know they talk to AI,Clearly label AI generated content,Present AI as human,AI Assistant label,Fake human name without label,High
|
||||
93,AI Interaction,Streaming,All,Waiting for full text is slow,Stream text response token by token,Show loading spinner for 10s+,Typewriter effect,Spinner until 100% complete,Medium
|
||||
94,Spatial UI,Gaze Hover,VisionOS,Elements should respond to eye tracking before pinch,Scale/highlight element on look,Static element until pinch,hoverEffect(),onTap only,High
|
||||
95,Spatial UI,Depth Layering,VisionOS,UI needs Z-depth to separate content from environment,Use glass material and z-offset,Flat opaque panels blocking view,.glassBackgroundEffect(),bg-white,Medium
|
||||
96,Sustainability,Auto-Play Video,Web,Video consumes massive data and energy,Click-to-play or pause when off-screen,Auto-play high-res video loops,playsInline muted preload='none',autoplay loop,Medium
|
||||
97,Sustainability,Asset Weight,Web,Heavy 3D/Image assets increase carbon footprint,Compress and lazy load 3D models,Load 50MB textures,Draco compression,Raw .obj files,Medium
|
||||
98,AI Interaction,Feedback Loop,All,AI needs user feedback to improve,Thumps up/down or 'Regenerate',Static output only,Feedback component,Read-only text,Low
|
||||
99,Accessibility,Motion Sensitivity,All,Parallax/Scroll-jacking causes nausea,Respect prefers-reduced-motion,Force scroll effects,@media (prefers-reduced-motion),ScrollTrigger.create(),High
|
||||
|
Binary file not shown.
238
.claude/skills/ui-ux-pro-max/scripts/core.py
Normal file
238
.claude/skills/ui-ux-pro-max/scripts/core.py
Normal file
@ -0,0 +1,238 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
UI/UX Pro Max Core - BM25 search engine for UI/UX style guides
|
||||
"""
|
||||
|
||||
import csv
|
||||
import re
|
||||
from pathlib import Path
|
||||
from math import log
|
||||
from collections import defaultdict
|
||||
|
||||
# ============ CONFIGURATION ============
|
||||
DATA_DIR = Path(__file__).parent.parent / "data"
|
||||
MAX_RESULTS = 3
|
||||
|
||||
CSV_CONFIG = {
|
||||
"style": {
|
||||
"file": "styles.csv",
|
||||
"search_cols": ["Style Category", "Keywords", "Best For", "Type"],
|
||||
"output_cols": ["Style Category", "Type", "Keywords", "Primary Colors", "Effects & Animation", "Best For", "Performance", "Accessibility", "Framework Compatibility", "Complexity"]
|
||||
},
|
||||
"prompt": {
|
||||
"file": "prompts.csv",
|
||||
"search_cols": ["Style Category", "AI Prompt Keywords (Copy-Paste Ready)", "CSS/Technical Keywords"],
|
||||
"output_cols": ["Style Category", "AI Prompt Keywords (Copy-Paste Ready)", "CSS/Technical Keywords", "Implementation Checklist"]
|
||||
},
|
||||
"color": {
|
||||
"file": "colors.csv",
|
||||
"search_cols": ["Product Type", "Keywords", "Notes"],
|
||||
"output_cols": ["Product Type", "Keywords", "Primary (Hex)", "Secondary (Hex)", "CTA (Hex)", "Background (Hex)", "Text (Hex)", "Border (Hex)", "Notes"]
|
||||
},
|
||||
"chart": {
|
||||
"file": "charts.csv",
|
||||
"search_cols": ["Data Type", "Keywords", "Best Chart Type", "Accessibility Notes"],
|
||||
"output_cols": ["Data Type", "Keywords", "Best Chart Type", "Secondary Options", "Color Guidance", "Accessibility Notes", "Library Recommendation", "Interactive Level"]
|
||||
},
|
||||
"landing": {
|
||||
"file": "landing.csv",
|
||||
"search_cols": ["Pattern Name", "Keywords", "Conversion Optimization", "Section Order"],
|
||||
"output_cols": ["Pattern Name", "Keywords", "Section Order", "Primary CTA Placement", "Color Strategy", "Conversion Optimization"]
|
||||
},
|
||||
"product": {
|
||||
"file": "products.csv",
|
||||
"search_cols": ["Product Type", "Keywords", "Primary Style Recommendation", "Key Considerations"],
|
||||
"output_cols": ["Product Type", "Keywords", "Primary Style Recommendation", "Secondary Styles", "Landing Page Pattern", "Dashboard Style (if applicable)", "Color Palette Focus"]
|
||||
},
|
||||
"ux": {
|
||||
"file": "ux-guidelines.csv",
|
||||
"search_cols": ["Category", "Issue", "Description", "Platform"],
|
||||
"output_cols": ["Category", "Issue", "Platform", "Description", "Do", "Don't", "Code Example Good", "Code Example Bad", "Severity"]
|
||||
},
|
||||
"typography": {
|
||||
"file": "typography.csv",
|
||||
"search_cols": ["Font Pairing Name", "Category", "Mood/Style Keywords", "Best For", "Heading Font", "Body Font"],
|
||||
"output_cols": ["Font Pairing Name", "Category", "Heading Font", "Body Font", "Mood/Style Keywords", "Best For", "Google Fonts URL", "CSS Import", "Tailwind Config", "Notes"]
|
||||
}
|
||||
}
|
||||
|
||||
STACK_CONFIG = {
|
||||
"html-tailwind": {"file": "stacks/html-tailwind.csv"},
|
||||
"react": {"file": "stacks/react.csv"},
|
||||
"nextjs": {"file": "stacks/nextjs.csv"},
|
||||
"vue": {"file": "stacks/vue.csv"},
|
||||
"nuxtjs": {"file": "stacks/nuxtjs.csv"},
|
||||
"nuxt-ui": {"file": "stacks/nuxt-ui.csv"},
|
||||
"svelte": {"file": "stacks/svelte.csv"},
|
||||
"swiftui": {"file": "stacks/swiftui.csv"},
|
||||
"react-native": {"file": "stacks/react-native.csv"},
|
||||
"flutter": {"file": "stacks/flutter.csv"}
|
||||
}
|
||||
|
||||
# Common columns for all stacks
|
||||
_STACK_COLS = {
|
||||
"search_cols": ["Category", "Guideline", "Description", "Do", "Don't"],
|
||||
"output_cols": ["Category", "Guideline", "Description", "Do", "Don't", "Code Good", "Code Bad", "Severity", "Docs URL"]
|
||||
}
|
||||
|
||||
AVAILABLE_STACKS = list(STACK_CONFIG.keys())
|
||||
|
||||
|
||||
# ============ BM25 IMPLEMENTATION ============
|
||||
class BM25:
|
||||
"""BM25 ranking algorithm for text search"""
|
||||
|
||||
def __init__(self, k1=1.5, b=0.75):
|
||||
self.k1 = k1
|
||||
self.b = b
|
||||
self.corpus = []
|
||||
self.doc_lengths = []
|
||||
self.avgdl = 0
|
||||
self.idf = {}
|
||||
self.doc_freqs = defaultdict(int)
|
||||
self.N = 0
|
||||
|
||||
def tokenize(self, text):
|
||||
"""Lowercase, split, remove punctuation, filter short words"""
|
||||
text = re.sub(r'[^\w\s]', ' ', str(text).lower())
|
||||
return [w for w in text.split() if len(w) > 2]
|
||||
|
||||
def fit(self, documents):
|
||||
"""Build BM25 index from documents"""
|
||||
self.corpus = [self.tokenize(doc) for doc in documents]
|
||||
self.N = len(self.corpus)
|
||||
if self.N == 0:
|
||||
return
|
||||
self.doc_lengths = [len(doc) for doc in self.corpus]
|
||||
self.avgdl = sum(self.doc_lengths) / self.N
|
||||
|
||||
for doc in self.corpus:
|
||||
seen = set()
|
||||
for word in doc:
|
||||
if word not in seen:
|
||||
self.doc_freqs[word] += 1
|
||||
seen.add(word)
|
||||
|
||||
for word, freq in self.doc_freqs.items():
|
||||
self.idf[word] = log((self.N - freq + 0.5) / (freq + 0.5) + 1)
|
||||
|
||||
def score(self, query):
|
||||
"""Score all documents against query"""
|
||||
query_tokens = self.tokenize(query)
|
||||
scores = []
|
||||
|
||||
for idx, doc in enumerate(self.corpus):
|
||||
score = 0
|
||||
doc_len = self.doc_lengths[idx]
|
||||
term_freqs = defaultdict(int)
|
||||
for word in doc:
|
||||
term_freqs[word] += 1
|
||||
|
||||
for token in query_tokens:
|
||||
if token in self.idf:
|
||||
tf = term_freqs[token]
|
||||
idf = self.idf[token]
|
||||
numerator = tf * (self.k1 + 1)
|
||||
denominator = tf + self.k1 * (1 - self.b + self.b * doc_len / self.avgdl)
|
||||
score += idf * numerator / denominator
|
||||
|
||||
scores.append((idx, score))
|
||||
|
||||
return sorted(scores, key=lambda x: x[1], reverse=True)
|
||||
|
||||
|
||||
# ============ SEARCH FUNCTIONS ============
|
||||
def _load_csv(filepath):
|
||||
"""Load CSV and return list of dicts"""
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
return list(csv.DictReader(f))
|
||||
|
||||
|
||||
def _search_csv(filepath, search_cols, output_cols, query, max_results):
|
||||
"""Core search function using BM25"""
|
||||
if not filepath.exists():
|
||||
return []
|
||||
|
||||
data = _load_csv(filepath)
|
||||
|
||||
# Build documents from search columns
|
||||
documents = [" ".join(str(row.get(col, "")) for col in search_cols) for row in data]
|
||||
|
||||
# BM25 search
|
||||
bm25 = BM25()
|
||||
bm25.fit(documents)
|
||||
ranked = bm25.score(query)
|
||||
|
||||
# Get top results with score > 0
|
||||
results = []
|
||||
for idx, score in ranked[:max_results]:
|
||||
if score > 0:
|
||||
row = data[idx]
|
||||
results.append({col: row.get(col, "") for col in output_cols if col in row})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def detect_domain(query):
|
||||
"""Auto-detect the most relevant domain from query"""
|
||||
query_lower = query.lower()
|
||||
|
||||
domain_keywords = {
|
||||
"color": ["color", "palette", "hex", "#", "rgb"],
|
||||
"chart": ["chart", "graph", "visualization", "trend", "bar", "pie", "scatter", "heatmap", "funnel"],
|
||||
"landing": ["landing", "page", "cta", "conversion", "hero", "testimonial", "pricing", "section"],
|
||||
"product": ["saas", "ecommerce", "e-commerce", "fintech", "healthcare", "gaming", "portfolio", "crypto", "dashboard"],
|
||||
"prompt": ["prompt", "css", "implementation", "variable", "checklist", "tailwind"],
|
||||
"style": ["style", "design", "ui", "minimalism", "glassmorphism", "neumorphism", "brutalism", "dark mode", "flat", "aurora"],
|
||||
"ux": ["ux", "usability", "accessibility", "wcag", "touch", "scroll", "animation", "keyboard", "navigation", "mobile"],
|
||||
"typography": ["font", "typography", "heading", "serif", "sans"]
|
||||
}
|
||||
|
||||
scores = {domain: sum(1 for kw in keywords if kw in query_lower) for domain, keywords in domain_keywords.items()}
|
||||
best = max(scores, key=scores.get)
|
||||
return best if scores[best] > 0 else "style"
|
||||
|
||||
|
||||
def search(query, domain=None, max_results=MAX_RESULTS):
|
||||
"""Main search function with auto-domain detection"""
|
||||
if domain is None:
|
||||
domain = detect_domain(query)
|
||||
|
||||
config = CSV_CONFIG.get(domain, CSV_CONFIG["style"])
|
||||
filepath = DATA_DIR / config["file"]
|
||||
|
||||
if not filepath.exists():
|
||||
return {"error": f"File not found: {filepath}", "domain": domain}
|
||||
|
||||
results = _search_csv(filepath, config["search_cols"], config["output_cols"], query, max_results)
|
||||
|
||||
return {
|
||||
"domain": domain,
|
||||
"query": query,
|
||||
"file": config["file"],
|
||||
"count": len(results),
|
||||
"results": results
|
||||
}
|
||||
|
||||
|
||||
def search_stack(query, stack, max_results=MAX_RESULTS):
|
||||
"""Search stack-specific guidelines"""
|
||||
if stack not in STACK_CONFIG:
|
||||
return {"error": f"Unknown stack: {stack}. Available: {', '.join(AVAILABLE_STACKS)}"}
|
||||
|
||||
filepath = DATA_DIR / STACK_CONFIG[stack]["file"]
|
||||
|
||||
if not filepath.exists():
|
||||
return {"error": f"Stack file not found: {filepath}", "stack": stack}
|
||||
|
||||
results = _search_csv(filepath, _STACK_COLS["search_cols"], _STACK_COLS["output_cols"], query, max_results)
|
||||
|
||||
return {
|
||||
"domain": "stack",
|
||||
"stack": stack,
|
||||
"query": query,
|
||||
"file": STACK_CONFIG[stack]["file"],
|
||||
"count": len(results),
|
||||
"results": results
|
||||
}
|
||||
61
.claude/skills/ui-ux-pro-max/scripts/search.py
Normal file
61
.claude/skills/ui-ux-pro-max/scripts/search.py
Normal file
@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
UI/UX Pro Max Search - BM25 search engine for UI/UX style guides
|
||||
Usage: python search.py "<query>" [--domain <domain>] [--stack <stack>] [--max-results 3]
|
||||
|
||||
Domains: style, prompt, color, chart, landing, product, ux, typography
|
||||
Stacks: html-tailwind, react, nextjs
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from core import CSV_CONFIG, AVAILABLE_STACKS, MAX_RESULTS, search, search_stack
|
||||
|
||||
|
||||
def format_output(result):
|
||||
"""Format results for Claude consumption (token-optimized)"""
|
||||
if "error" in result:
|
||||
return f"Error: {result['error']}"
|
||||
|
||||
output = []
|
||||
if result.get("stack"):
|
||||
output.append(f"## UI Pro Max Stack Guidelines")
|
||||
output.append(f"**Stack:** {result['stack']} | **Query:** {result['query']}")
|
||||
else:
|
||||
output.append(f"## UI Pro Max Search Results")
|
||||
output.append(f"**Domain:** {result['domain']} | **Query:** {result['query']}")
|
||||
output.append(f"**Source:** {result['file']} | **Found:** {result['count']} results\n")
|
||||
|
||||
for i, row in enumerate(result['results'], 1):
|
||||
output.append(f"### Result {i}")
|
||||
for key, value in row.items():
|
||||
value_str = str(value)
|
||||
if len(value_str) > 300:
|
||||
value_str = value_str[:300] + "..."
|
||||
output.append(f"- **{key}:** {value_str}")
|
||||
output.append("")
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="UI Pro Max Search")
|
||||
parser.add_argument("query", help="Search query")
|
||||
parser.add_argument("--domain", "-d", choices=list(CSV_CONFIG.keys()), help="Search domain")
|
||||
parser.add_argument("--stack", "-s", choices=AVAILABLE_STACKS, help="Stack-specific search (html-tailwind, react, nextjs)")
|
||||
parser.add_argument("--max-results", "-n", type=int, default=MAX_RESULTS, help="Max results (default: 3)")
|
||||
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Stack search takes priority
|
||||
if args.stack:
|
||||
result = search_stack(args.query, args.stack, args.max_results)
|
||||
else:
|
||||
result = search(args.query, args.domain, args.max_results)
|
||||
|
||||
if args.json:
|
||||
import json
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(format_output(result))
|
||||
607
.claude/ui-design-system.md
Normal file
607
.claude/ui-design-system.md
Normal file
@ -0,0 +1,607 @@
|
||||
# 比赛管理系统 - UI设计系统文档
|
||||
|
||||
> 版本: 1.0
|
||||
> 更新日期: 2026-01-14
|
||||
> 框架: Vue 3 + Ant Design Vue + Tailwind CSS
|
||||
|
||||
---
|
||||
|
||||
## 📋 目录
|
||||
|
||||
1. [设计理念](#设计理念)
|
||||
2. [色彩系统](#色彩系统)
|
||||
3. [布局规范](#布局规范)
|
||||
4. [组件规范](#组件规范)
|
||||
5. [双主题架构](#双主题架构)
|
||||
6. [风格统一建议](#风格统一建议)
|
||||
|
||||
---
|
||||
|
||||
## 🎨 设计理念
|
||||
|
||||
### 整体定位
|
||||
- **企业级应用**: 专业、可靠、易用
|
||||
- **教育科技**: 现代、创新、智能
|
||||
- **双主题并存**: 传统业务管理 + 创新AI功能
|
||||
|
||||
### 设计原则
|
||||
1. **一致性优先**: 相同功能使用相同的交互模式
|
||||
2. **效率至上**: 减少用户操作步骤,提升工作效率
|
||||
3. **清晰明确**: 信息层级分明,状态反馈清晰
|
||||
4. **渐进增强**: 基础功能稳定,高级功能突出
|
||||
|
||||
---
|
||||
|
||||
## 🎨 色彩系统
|
||||
|
||||
### 主题A:标准业务主题(用于普通页面)
|
||||
|
||||
#### 主色调 - 蓝色系
|
||||
```scss
|
||||
--ant-color-primary: #0958d9 // 主色(Ant Design blue-6)
|
||||
--ant-color-primary-hover: #1677ff // 悬停色(blue-5)
|
||||
--ant-color-primary-active: #003eb3 // 激活色(blue-7)
|
||||
--ant-color-primary-bg: #e6f7ff // 主色背景(blue-0)
|
||||
--ant-color-primary-bg-hover: #bae7ff // 主色背景悬停(blue-1)
|
||||
```
|
||||
|
||||
#### 功能色
|
||||
```scss
|
||||
--ant-color-success: #52c41a // 成功(绿色)
|
||||
--ant-color-error: #ff4d4f // 错误(红色)
|
||||
--ant-color-warning: #faad14 // 警告(黄色)
|
||||
--ant-color-info: #0958d9 // 信息(蓝色)
|
||||
```
|
||||
|
||||
#### 中性色
|
||||
```scss
|
||||
// 文字
|
||||
--ant-color-text: rgba(0, 0, 0, 0.85) // 主文字
|
||||
--ant-color-text-secondary: rgba(0, 0, 0, 0.45) // 次要文字
|
||||
--ant-color-text-tertiary: rgba(0, 0, 0, 0.25) // 辅助文字
|
||||
|
||||
// 边框
|
||||
--ant-color-border: #d9d9d9 // 主边框
|
||||
--ant-color-border-secondary: rgba(0, 0, 0, 0.06) // 次边框
|
||||
|
||||
// 背景
|
||||
--ant-color-bg-container: #ffffff // 容器背景
|
||||
--ant-color-bg-layout: #f5f5f5 // 布局背景
|
||||
```
|
||||
|
||||
#### 侧边栏主题
|
||||
```scss
|
||||
--sidebar-bg: #f5f5f5 // 侧边栏背景
|
||||
--sidebar-menu-item-hover: #e6f7ff // 菜单项悬停
|
||||
--sidebar-menu-item-selected-bg: #e6f7ff // 菜单项选中背景
|
||||
--sidebar-menu-text: rgba(0, 0, 0, 0.85) // 菜单文字
|
||||
--sidebar-menu-text-selected: #0958d9 // 选中文字
|
||||
```
|
||||
|
||||
### 主题B:现代创意主题(用于3D建模页面)
|
||||
|
||||
#### 主色调 - 深色科技风
|
||||
```scss
|
||||
$primary: #7c3aed // 深紫色
|
||||
$primary-light: #a78bfa // 浅紫色
|
||||
$primary-dark: #5b21b6 // 深紫暗色
|
||||
$secondary: #06b6d4 // 青蓝色
|
||||
$accent: #f43f5e // 粉红强调色
|
||||
$success: #10b981 // 成功绿
|
||||
```
|
||||
|
||||
#### 背景与表面
|
||||
```scss
|
||||
$background: #0a0a12 // 主背景(深黑)
|
||||
$surface: #12121e // 卡片表面(深灰)
|
||||
$surface-light: #1a1a2e // 浅表面
|
||||
```
|
||||
|
||||
#### 文字色
|
||||
```scss
|
||||
$text: #e2e8f0 // 主文字(浅灰)
|
||||
$text-muted: #94a3b8 // 次要文字
|
||||
```
|
||||
|
||||
#### 渐变
|
||||
```scss
|
||||
$gradient-primary: linear-gradient(135deg, #7c3aed 0%, #ec4899 100%)
|
||||
$gradient-secondary: linear-gradient(135deg, #06b6d4 0%, #7c3aed 100%)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📐 布局规范
|
||||
|
||||
### 间距系统(8px基础单位)
|
||||
|
||||
```scss
|
||||
$spacing-xs: 8px // 超小间距
|
||||
$spacing-sm: 16px // 小间距
|
||||
$spacing-md: 24px // 中间距
|
||||
$spacing-lg: 32px // 大间距
|
||||
$spacing-xl: 48px // 超大间距
|
||||
```
|
||||
|
||||
### 标准布局
|
||||
|
||||
#### 侧边栏布局(BasicLayout)
|
||||
```
|
||||
- 侧边栏宽度: 200px
|
||||
- 侧边栏背景: #f5f5f5
|
||||
- Logo高度: 64px
|
||||
- 菜单项外边距: 4px 8px
|
||||
- 菜单项圆角: 6px
|
||||
- 内容区内边距: 20px
|
||||
```
|
||||
|
||||
#### 卡片间距
|
||||
```scss
|
||||
.card {
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
background: #fff;
|
||||
}
|
||||
```
|
||||
|
||||
#### 搜索表单
|
||||
```scss
|
||||
.search-form {
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
}
|
||||
```
|
||||
|
||||
### 栅格系统
|
||||
- 使用Ant Design 24栅格系统
|
||||
- 常用布局: `<a-col :span="24" :md="12" :lg="8">`
|
||||
- 响应式断点: xs(480), sm(576), md(768), lg(992), xl(1200), xxl(1600)
|
||||
|
||||
---
|
||||
|
||||
## 🧩 组件规范
|
||||
|
||||
### 按钮
|
||||
|
||||
#### 类型与用途
|
||||
```vue
|
||||
<!-- 主要操作 -->
|
||||
<a-button type="primary">创建</a-button>
|
||||
|
||||
<!-- 次要操作 -->
|
||||
<a-button>取消</a-button>
|
||||
|
||||
<!-- 文字按钮 -->
|
||||
<a-button type="link">查看详情</a-button>
|
||||
|
||||
<!-- 危险操作 -->
|
||||
<a-button danger>删除</a-button>
|
||||
```
|
||||
|
||||
#### 尺寸
|
||||
```vue
|
||||
<a-button size="large">大按钮</a-button>
|
||||
<a-button>默认按钮</a-button>
|
||||
<a-button size="small">小按钮</a-button>
|
||||
```
|
||||
|
||||
#### 按钮组合间距
|
||||
```scss
|
||||
.button-group {
|
||||
.ant-btn + .ant-btn {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 表格
|
||||
|
||||
#### 标准配置
|
||||
```typescript
|
||||
{
|
||||
bordered: false, // 不显示边框(默认)
|
||||
size: 'middle', // 默认尺寸
|
||||
pagination: {
|
||||
defaultPageSize: 10, // 默认每页10条
|
||||
showSizeChanger: true, // 显示切换器
|
||||
showQuickJumper: true, // 显示快速跳转
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
},
|
||||
rowKey: 'id', // 行键
|
||||
loading: false, // 加载状态
|
||||
}
|
||||
```
|
||||
|
||||
#### 列宽建议
|
||||
```
|
||||
- ID列: 80px
|
||||
- 名称列: 120-200px
|
||||
- 描述列: 200-300px
|
||||
- 时间列: 180px
|
||||
- 状态列: 100px
|
||||
- 操作列: 120-200px (fixed: 'right')
|
||||
```
|
||||
|
||||
### 表单
|
||||
|
||||
#### 布局
|
||||
```vue
|
||||
<a-form
|
||||
:model="form"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<!-- 表单项 -->
|
||||
</a-form>
|
||||
```
|
||||
|
||||
#### 输入框宽度
|
||||
```
|
||||
- 短文本(名称、编号): 200px
|
||||
- 中文本(描述): 300-400px
|
||||
- 长文本(详情): 100%
|
||||
- 选择框(状态、类型): 120-200px
|
||||
- 日期选择: 200px
|
||||
- 日期范围: 280px
|
||||
```
|
||||
|
||||
### 标签(Tags)
|
||||
|
||||
#### 颜色语义
|
||||
```vue
|
||||
<!-- 状态标签 -->
|
||||
<a-tag color="success">已发布</a-tag>
|
||||
<a-tag color="processing">进行中</a-tag>
|
||||
<a-tag color="default">草稿</a-tag>
|
||||
<a-tag color="error">已关闭</a-tag>
|
||||
|
||||
<!-- 类型标签 -->
|
||||
<a-tag color="blue">个人赛</a-tag>
|
||||
<a-tag color="purple">团队赛</a-tag>
|
||||
```
|
||||
|
||||
### 模态框与抽屉
|
||||
|
||||
#### 模态框
|
||||
```vue
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
title="标题"
|
||||
width="600px"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<!-- 内容 -->
|
||||
</a-modal>
|
||||
```
|
||||
|
||||
#### 抽屉
|
||||
```vue
|
||||
<a-drawer
|
||||
v-model:open="visible"
|
||||
title="标题"
|
||||
width="600px"
|
||||
placement="right"
|
||||
>
|
||||
<!-- 内容 -->
|
||||
</a-drawer>
|
||||
```
|
||||
|
||||
### 卡片
|
||||
|
||||
#### 标准卡片
|
||||
```vue
|
||||
<a-card title="标题" class="mb-4">
|
||||
<!-- 内容 -->
|
||||
</a-card>
|
||||
```
|
||||
|
||||
#### 无边框卡片
|
||||
```vue
|
||||
<a-card :bordered="false" class="mb-4">
|
||||
<!-- 内容 -->
|
||||
</a-card>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌓 双主题架构
|
||||
|
||||
### 主题A:标准业务主题
|
||||
|
||||
**适用页面:**
|
||||
- 赛事管理(contests)
|
||||
- 学校管理(school)
|
||||
- 作业管理(homework)
|
||||
- 系统管理(system)
|
||||
- 登录页面(auth)
|
||||
|
||||
**设计特征:**
|
||||
- ✅ 浅色背景(#ffffff, #f5f5f5)
|
||||
- ✅ 蓝色主题(#0958d9)
|
||||
- ✅ 标准Ant Design组件样式
|
||||
- ✅ 简洁的过渡动画(0.3s)
|
||||
- ✅ 表格为主要数据展示方式
|
||||
- ✅ 圆角: 6-8px
|
||||
- ✅ 轻微阴影效果
|
||||
|
||||
**代码示例:**
|
||||
```vue
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- 搜索区 -->
|
||||
<a-card class="search-card mb-4">
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="搜索">
|
||||
<a-input v-model:value="keyword" placeholder="请输入" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary">查询</a-button>
|
||||
<a-button class="ml-2">重置</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<a-card>
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
/>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### 主题B:现代创意主题
|
||||
|
||||
**适用页面:**
|
||||
- 3D建模实验室(workbench/ai-3d)
|
||||
- AI相关创新功能
|
||||
|
||||
**设计特征:**
|
||||
- ✅ 深色背景(#0a0a12)
|
||||
- ✅ 紫色+青蓝色主题(#7c3aed, #06b6d4)
|
||||
- ✅ 玻璃态效果(backdrop-filter: blur())
|
||||
- ✅ 复杂动画(浮动、旋转、脉冲)
|
||||
- ✅ 卡片网格布局
|
||||
- ✅ 圆角: 12-20px
|
||||
- ✅ 发光效果(glow)
|
||||
- ✅ 渐变背景
|
||||
|
||||
**代码示例:**
|
||||
```vue
|
||||
<template>
|
||||
<div class="ai-3d-container">
|
||||
<!-- 动画背景 -->
|
||||
<div class="bg-animation">
|
||||
<div class="bg-gradient bg-gradient-1"></div>
|
||||
<div class="bg-gradient bg-gradient-2"></div>
|
||||
<div class="bg-grid"></div>
|
||||
</div>
|
||||
|
||||
<!-- 左侧面板 -->
|
||||
<div class="left-panel">
|
||||
<!-- 内容 -->
|
||||
</div>
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<div class="right-panel">
|
||||
<!-- 内容 -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$primary: #7c3aed;
|
||||
$secondary: #06b6d4;
|
||||
$background: #0a0a12;
|
||||
|
||||
.ai-3d-container {
|
||||
min-height: 100vh;
|
||||
background: $background;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bg-animation {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bg-gradient {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(120px);
|
||||
opacity: 0.4;
|
||||
animation: float 25s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
width: 380px;
|
||||
background: rgba(18, 18, 30, 0.8);
|
||||
backdrop-filter: blur(40px);
|
||||
border-right: 1px solid rgba($primary, 0.1);
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-30px, 30px) scale(0.9);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 风格统一建议
|
||||
|
||||
### 问题分析
|
||||
目前3D建模页面与其他业务页面风格差异较大,可能导致:
|
||||
- 用户体验割裂感
|
||||
- 视觉不统一
|
||||
- 品牌识别度降低
|
||||
|
||||
### 统一方案
|
||||
|
||||
#### 方案A:保守统一(推荐用于企业应用)
|
||||
**目标:** 将3D页面统一到标准业务主题
|
||||
|
||||
**调整内容:**
|
||||
1. **色彩调整**
|
||||
- 主色由紫色改为蓝色(#7c3aed → #0958d9)
|
||||
- 背景改为浅色(#0a0a12 → #f5f5f5)
|
||||
- 文字改为深色(#e2e8f0 → rgba(0,0,0,0.85))
|
||||
|
||||
2. **布局调整**
|
||||
- 使用标准卡片布局
|
||||
- 保持双面板结构,但使用浅色背景
|
||||
- 移除动画背景
|
||||
|
||||
3. **组件调整**
|
||||
- 圆角统一为8px
|
||||
- 移除玻璃态效果
|
||||
- 简化动画效果
|
||||
|
||||
4. **保留特色**
|
||||
- 保留双面板布局(体现AI功能特殊性)
|
||||
- 保留历史记录网格展示
|
||||
- 保留进度动画反馈
|
||||
|
||||
**优点:** 风格统一,专业稳重
|
||||
**缺点:** 失去部分科技感和创新感
|
||||
|
||||
#### 方案B:渐进融合(推荐用于创新型应用)
|
||||
**目标:** 在保留创新感的同时,与主题色对齐
|
||||
|
||||
**调整内容:**
|
||||
1. **色彩微调**
|
||||
- 主色保持紫色,但添加蓝色辅助色
|
||||
- 使用浅色变体(深蓝色背景 #0f172a 代替纯黑)
|
||||
- 增加白色卡片区域(保留部分深色背景)
|
||||
|
||||
2. **融合元素**
|
||||
- 顶部添加面包屑/返回按钮(与其他页面一致)
|
||||
- 左侧面板使用浅色变体
|
||||
- 右侧内容区使用白色背景
|
||||
|
||||
3. **动画优化**
|
||||
- 保留关键动画(生成按钮、加载动画)
|
||||
- 简化背景动画(减少动画数量)
|
||||
- 统一过渡时间(0.3s)
|
||||
|
||||
**优点:** 既保留创新感,又提升统一性
|
||||
**缺点:** 需要较多调整工作
|
||||
|
||||
#### 方案C:品牌双主题(适合大型平台)
|
||||
**目标:** 建立明确的双主题设计系统
|
||||
|
||||
**调整内容:**
|
||||
1. **主题切换**
|
||||
- 在设置中提供主题切换选项
|
||||
- 标准业务使用浅色主题
|
||||
- AI创新功能使用深色主题
|
||||
- 统一过渡动画
|
||||
|
||||
2. **主题桥接**
|
||||
- 在AI页面顶部添加主题标识
|
||||
- 使用渐变过渡效果
|
||||
- 统一图标和字体
|
||||
|
||||
3. **文档规范**
|
||||
- 明确两套主题的使用场景
|
||||
- 制定切换规则
|
||||
- 统一组件库
|
||||
|
||||
**优点:** 灵活性高,可支持品牌升级
|
||||
**缺点:** 开发和维护成本高
|
||||
|
||||
### 快速优化建议(不改变整体风格)
|
||||
|
||||
如果暂时不做大规模调整,可以先做以下小优化:
|
||||
|
||||
1. **统一圆角**
|
||||
- 将3D页面的圆角从12-20px调整为8-12px
|
||||
|
||||
2. **添加面包屑**
|
||||
- 在3D页面顶部添加返回路径
|
||||
|
||||
3. **统一过渡时间**
|
||||
- 将所有动画过渡统一为0.3s
|
||||
|
||||
4. **添加品牌标识**
|
||||
- 在3D页面显著位置添加系统Logo
|
||||
|
||||
5. **统一字体**
|
||||
- 确保字体大小和行高与其他页面一致
|
||||
|
||||
---
|
||||
|
||||
## 📚 参考资源
|
||||
|
||||
### 设计规范
|
||||
- [Ant Design 设计价值观](https://ant.design/docs/spec/values-cn)
|
||||
- [Material Design 色彩系统](https://material.io/design/color)
|
||||
- [8点网格系统](https://spec.fm/specifics/8-pt-grid)
|
||||
|
||||
### 颜色工具
|
||||
- [Coolors](https://coolors.co/) - 配色方案生成
|
||||
- [Adobe Color](https://color.adobe.com/) - 色彩搭配
|
||||
- [Contrast Checker](https://webaim.org/resources/contrastchecker/) - 对比度检查
|
||||
|
||||
### 组件库文档
|
||||
- [Ant Design Vue](https://antdv.com/components/overview-cn)
|
||||
- [Tailwind CSS](https://tailwindcss.com/docs)
|
||||
|
||||
---
|
||||
|
||||
## 📝 更新日志
|
||||
|
||||
### v1.0 (2026-01-14)
|
||||
- ✅ 初始版本
|
||||
- ✅ 完成主题色彩系统梳理
|
||||
- ✅ 完成布局规范定义
|
||||
- ✅ 完成组件规范文档
|
||||
- ✅ 完成双主题对比分析
|
||||
- ✅ 提出风格统一方案
|
||||
|
||||
---
|
||||
|
||||
## 📞 联系与反馈
|
||||
|
||||
如有设计系统相关问题或建议,请通过以下方式反馈:
|
||||
- 项目路径: `C:\Users\82788\Desktop\work\competition-management-system`
|
||||
- 文档位置: `.claude/ui-design-system.md`
|
||||
|
||||
---
|
||||
|
||||
**文档结束**
|
||||
156
.cursor/CHROME_DEVTOOLS_MCP_SETUP.md
Normal file
156
.cursor/CHROME_DEVTOOLS_MCP_SETUP.md
Normal file
@ -0,0 +1,156 @@
|
||||
# Chrome DevTools MCP 安装和配置说明
|
||||
|
||||
## ✅ 安装状态
|
||||
|
||||
chrome-devtools-mcp 已通过 pnpm 全局安装完成。
|
||||
|
||||
## 📝 配置步骤
|
||||
|
||||
### ✅ 已配置(推荐方式)
|
||||
|
||||
项目已配置使用包装脚本,确保使用正确的 Node.js 版本。配置文件位于 `.cursor/mcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"chrome-devtools": {
|
||||
"command": "/Users/wwzh/Awesome/runfast/competition-management-system/.cursor/scripts/chrome-devtools-mcp.sh"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 方法 1:在 Cursor 用户设置中配置(备选)
|
||||
|
||||
如果项目配置不工作,可以在 Cursor 用户设置中添加:
|
||||
|
||||
1. 打开 Cursor
|
||||
2. 按 `Cmd + Shift + P` (macOS) 或 `Ctrl + Shift + P` (Windows/Linux) 打开命令面板
|
||||
3. 输入 "Preferences: Open User Settings (JSON)"
|
||||
4. 在 `settings.json` 文件中添加以下配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"chrome-devtools": {
|
||||
"command": "/Users/wwzh/Awesome/runfast/competition-management-system/.cursor/scripts/chrome-devtools-mcp.sh"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 方法 2:使用 npx(需要修复 npm 缓存权限)
|
||||
|
||||
如果 npm 缓存权限问题已修复,可以使用:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"chrome-devtools": {
|
||||
"command": "npx",
|
||||
"args": ["chrome-devtools-mcp@latest"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**修复 npm 缓存权限**(需要 sudo 权限):
|
||||
```bash
|
||||
sudo chown -R $(whoami) ~/.npm
|
||||
```
|
||||
|
||||
## 🔍 验证安装
|
||||
|
||||
安装完成后,重启 Cursor,然后在 Chat 中应该可以看到 Chrome DevTools MCP 相关的工具。
|
||||
|
||||
## 📚 使用说明
|
||||
|
||||
Chrome DevTools MCP 提供了以下功能:
|
||||
- 浏览器导航和页面快照
|
||||
- 控制台消息查看
|
||||
- 网络请求监控
|
||||
- 页面元素交互(点击、输入等)
|
||||
- 截图功能
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **Node.js 版本要求**:建议使用 Node.js 22.12.0 或更高版本(当前版本:20.19.0 LTS)
|
||||
2. **Chrome 浏览器**:需要安装最新版本的 Google Chrome
|
||||
3. **首次使用**:可能需要登录配置,之后会保存登录状态
|
||||
|
||||
## 🔧 故障排除
|
||||
|
||||
### 问题:MCP 服务器启动失败
|
||||
|
||||
**检查步骤:**
|
||||
|
||||
1. **验证包装脚本是否可执行**:
|
||||
```bash
|
||||
cd /Users/wwzh/Awesome/runfast/competition-management-system
|
||||
source ~/.nvm/nvm.sh
|
||||
.cursor/scripts/chrome-devtools-mcp.sh --version
|
||||
```
|
||||
应该输出:`0.10.2`
|
||||
|
||||
2. **检查 Node.js 版本**:
|
||||
```bash
|
||||
source ~/.nvm/nvm.sh
|
||||
node --version
|
||||
```
|
||||
应该显示:`v20.19.0`
|
||||
|
||||
3. **检查配置文件**:
|
||||
```bash
|
||||
cat .cursor/mcp.json
|
||||
```
|
||||
确认路径正确
|
||||
|
||||
4. **手动测试运行**:
|
||||
```bash
|
||||
source ~/.nvm/nvm.sh
|
||||
node /Users/wwzh/Library/pnpm/global/5/.pnpm/chrome-devtools-mcp@0.10.2/node_modules/chrome-devtools-mcp/build/src/index.js --version
|
||||
```
|
||||
|
||||
5. **检查 Cursor 日志**:
|
||||
- 打开 Cursor
|
||||
- 查看输出面板(View → Output)
|
||||
- 选择 "MCP" 或 "Chrome DevTools" 查看错误信息
|
||||
|
||||
6. **重启 Cursor IDE**:
|
||||
配置更改后需要完全重启 Cursor
|
||||
|
||||
### 常见错误
|
||||
|
||||
**错误:command not found**
|
||||
- 确保包装脚本路径正确
|
||||
- 检查脚本是否有执行权限:`chmod +x .cursor/scripts/chrome-devtools-mcp.sh`
|
||||
|
||||
**错误:Node.js version mismatch**
|
||||
- 确保使用 Node.js 20.19.0:`nvm use 20.19.0`
|
||||
- 检查包装脚本中的 nvm 路径是否正确
|
||||
|
||||
**错误:npm cache permission denied**
|
||||
- 如果使用 npx 方式,需要修复权限:
|
||||
```bash
|
||||
sudo chown -R $(whoami) ~/.npm
|
||||
```
|
||||
|
||||
### 重新安装
|
||||
|
||||
如果问题持续存在:
|
||||
|
||||
```bash
|
||||
# 1. 卸载旧版本
|
||||
pnpm remove -g chrome-devtools-mcp
|
||||
|
||||
# 2. 清理缓存
|
||||
pnpm store prune
|
||||
|
||||
# 3. 重新安装
|
||||
source ~/.nvm/nvm.sh
|
||||
pnpm add -g chrome-devtools-mcp@latest
|
||||
|
||||
# 4. 验证安装
|
||||
pnpm list -g chrome-devtools-mcp
|
||||
```
|
||||
|
||||
167
.cursor/MIGRATION_SUMMARY.md
Normal file
167
.cursor/MIGRATION_SUMMARY.md
Normal file
@ -0,0 +1,167 @@
|
||||
# Cursor Rules 迁移总结
|
||||
|
||||
## ✅ 完成的改进
|
||||
|
||||
根据 [Cursor 官方文档](https://cursor.com/cn/docs/context/rules) 的最佳实践,已将项目规则系统现代化。
|
||||
|
||||
### 1. 规则拆分 ✨
|
||||
|
||||
原来的 283 行单一文件 `.cursorrules` 已拆分为 **8 个模块化规则**:
|
||||
|
||||
#### 主规则(`.cursor/rules/`)
|
||||
|
||||
| 规则文件 | 类型 | 大小 | 说明 |
|
||||
|---------|------|------|------|
|
||||
| `project-overview.mdc` | Always Apply | ~50 行 | 项目概述和技术栈 |
|
||||
| `multi-tenant.mdc` | Always Apply | ~100 行 | ⚠️ 多租户隔离规范(核心安全) |
|
||||
| `backend-architecture.mdc` | Apply to Files | ~200 行 | NestJS 后端架构规范 |
|
||||
| `frontend-architecture.mdc` | Apply to Files | ~250 行 | Vue 3 前端架构规范 |
|
||||
| `database-design.mdc` | Apply to Files | ~200 行 | Prisma 数据库设计规范 |
|
||||
| `code-review-checklist.mdc` | Manual | ~150 行 | 代码审查清单 |
|
||||
|
||||
#### 嵌套规则
|
||||
|
||||
| 规则文件 | 作用域 | 说明 |
|
||||
|---------|--------|------|
|
||||
| `backend/.cursor/rules/backend-specific.mdc` | backend/ | 后端特定规范和脚本 |
|
||||
| `frontend/.cursor/rules/frontend-specific.mdc` | frontend/ | 前端特定规范和组件 |
|
||||
|
||||
### 2. 使用 MDC 格式 📝
|
||||
|
||||
所有规则文件使用标准 MDC 格式,支持元数据:
|
||||
|
||||
```md
|
||||
---
|
||||
description: 规则描述
|
||||
globs:
|
||||
- "backend/**/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# 规则内容...
|
||||
```
|
||||
|
||||
### 3. 智能应用策略 🎯
|
||||
|
||||
- **Always Apply**: 关键规则(项目概述、多租户)始终生效
|
||||
- **File Matching**: 后端/前端规则仅在相关文件时应用
|
||||
- **Nested Rules**: 子目录规则只在该目录下生效
|
||||
- **Manual**: 代码审查清单按需引用 `@code-review-checklist`
|
||||
|
||||
### 4. 创建 AGENTS.md 🚀
|
||||
|
||||
添加了简化版快速参考文件:
|
||||
- 纯 Markdown 格式,无元数据
|
||||
- 包含最重要的规则和快速参考
|
||||
- 易于阅读和分享
|
||||
|
||||
### 5. 完整文档 📚
|
||||
|
||||
创建了详细的使用指南 `.cursor/RULES_README.md`:
|
||||
- 规则文件结构说明
|
||||
- 使用方式指导
|
||||
- 迁移指南
|
||||
- 最佳实践
|
||||
|
||||
## 📊 改进效果
|
||||
|
||||
### 性能优化
|
||||
- ✅ 每个规则 < 500 行(符合最佳实践)
|
||||
- ✅ 按需加载,减少不必要的上下文
|
||||
- ✅ 嵌套规则提高针对性
|
||||
|
||||
### 可维护性
|
||||
- ✅ 模块化设计,易于更新单个规则
|
||||
- ✅ 版本控制友好
|
||||
- ✅ 清晰的职责分离
|
||||
|
||||
### 可扩展性
|
||||
- ✅ 轻松添加新规则
|
||||
- ✅ 支持子目录特定规则
|
||||
- ✅ 规则可以引用其他文件
|
||||
|
||||
## 🎯 使用建议
|
||||
|
||||
### 日常开发
|
||||
|
||||
```bash
|
||||
# 开发时规则自动生效
|
||||
# 不需要手动操作
|
||||
|
||||
# 需要代码审查时
|
||||
在 Chat 中输入:@code-review-checklist
|
||||
```
|
||||
|
||||
### 添加新规则
|
||||
|
||||
```bash
|
||||
# 方法 1: 使用命令
|
||||
Cmd/Ctrl + Shift + P → "New Cursor Rule"
|
||||
|
||||
# 方法 2: 手动创建
|
||||
# 在 .cursor/rules/ 创建新的 .mdc 文件
|
||||
```
|
||||
|
||||
### 查看规则状态
|
||||
|
||||
```bash
|
||||
# 打开 Cursor Settings
|
||||
Cmd/Ctrl + ,
|
||||
|
||||
# 进入 Rules 选项卡
|
||||
查看所有规则的状态和类型
|
||||
```
|
||||
|
||||
## ⚠️ 重要变更
|
||||
|
||||
### 1. 旧文件状态
|
||||
- `.cursorrules` 已标记为 DEPRECATED
|
||||
- 文件保留作为备份
|
||||
- 所有功能已迁移到新系统
|
||||
|
||||
### 2. 多租户规则
|
||||
- 设为 **Always Apply**
|
||||
- 确保所有生成的代码都包含租户隔离检查
|
||||
- 这是系统安全的核心保障
|
||||
|
||||
### 3. 嵌套规则生效
|
||||
- 在 `backend/` 目录工作时,后端特定规则自动应用
|
||||
- 在 `frontend/` 目录工作时,前端特定规则自动应用
|
||||
|
||||
## 📈 下一步
|
||||
|
||||
### 可选的进一步优化
|
||||
|
||||
1. **添加模块特定规则**
|
||||
```
|
||||
backend/src/contests/.cursor/rules/
|
||||
└── contests-specific.mdc
|
||||
```
|
||||
|
||||
2. **创建模板规则**
|
||||
- 控制器模板
|
||||
- 服务模板
|
||||
- 组件模板
|
||||
|
||||
3. **团队规则(如果有 Team 计划)**
|
||||
- 在 Cursor Dashboard 配置团队级规则
|
||||
- 强制执行组织标准
|
||||
|
||||
## 🔗 相关资源
|
||||
|
||||
- 📖 [规则使用指南](./.cursor/RULES_README.md)
|
||||
- 🚀 [快速参考](../AGENTS.md)
|
||||
- 📚 [Cursor 官方文档](https://cursor.com/cn/docs/context/rules)
|
||||
|
||||
## 💬 反馈
|
||||
|
||||
如有问题或建议,可以:
|
||||
1. 更新规则文件并测试
|
||||
2. 查看官方文档获取最新功能
|
||||
3. 分享最佳实践给团队
|
||||
|
||||
---
|
||||
|
||||
**迁移完成时间**: 2025-11-27
|
||||
**符合标准**: Cursor Rules Best Practices v1.0
|
||||
|
||||
128
.cursor/RULES_README.md
Normal file
128
.cursor/RULES_README.md
Normal file
@ -0,0 +1,128 @@
|
||||
# Cursor Rules 使用指南
|
||||
|
||||
本项目使用 Cursor 的新规则系统(Project Rules + AGENTS.md),遵循 [官方最佳实践](https://cursor.com/cn/docs/context/rules)。
|
||||
|
||||
## 📁 规则文件结构
|
||||
|
||||
```
|
||||
competition-management-system/
|
||||
├── .cursor/rules/ # 项目规则目录
|
||||
│ ├── project-overview.mdc # 项目概述(Always Apply)
|
||||
│ ├── multi-tenant.mdc # 多租户规范(Always Apply)⚠️
|
||||
│ ├── backend-architecture.mdc # 后端架构(Apply to backend files)
|
||||
│ ├── frontend-architecture.mdc # 前端架构(Apply to frontend files)
|
||||
│ ├── database-design.mdc # 数据库设计(Apply to prisma files)
|
||||
│ └── code-review-checklist.mdc # 代码审查清单(Manual)
|
||||
├── backend/.cursor/rules/
|
||||
│ └── backend-specific.mdc # 后端特定规范(嵌套规则)
|
||||
├── frontend/.cursor/rules/
|
||||
│ └── frontend-specific.mdc # 前端特定规范(嵌套规则)
|
||||
├── AGENTS.md # 简化版指令(Quick Reference)
|
||||
└── .cursorrules # 已废弃,保留作为备份
|
||||
```
|
||||
|
||||
## 🎯 规则类型说明
|
||||
|
||||
### 1. Always Apply(总是应用)
|
||||
- `project-overview.mdc` - 项目技术栈和基本信息
|
||||
- `multi-tenant.mdc` - **多租户数据隔离规范(最重要)**
|
||||
|
||||
### 2. Apply to Specific Files(文件匹配)
|
||||
- `backend-architecture.mdc` - 匹配 `backend/**/*.ts`
|
||||
- `frontend-architecture.mdc` - 匹配 `frontend/**/*.vue` 和 `frontend/**/*.ts`
|
||||
- `database-design.mdc` - 匹配 `backend/prisma/**/*.prisma`
|
||||
|
||||
### 3. Nested Rules(嵌套规则)
|
||||
- `backend/.cursor/rules/backend-specific.mdc` - 仅作用于 backend 目录
|
||||
- `frontend/.cursor/rules/frontend-specific.mdc` - 仅作用于 frontend 目录
|
||||
|
||||
### 4. Apply Manually(手动触发)
|
||||
- `code-review-checklist.mdc` - 在 Chat 中使用 `@code-review-checklist` 引用
|
||||
|
||||
## 🚀 使用方式
|
||||
|
||||
### 在 Chat 中引用规则
|
||||
|
||||
```
|
||||
# 自动应用
|
||||
规则会根据上下文自动应用
|
||||
|
||||
# 手动引用
|
||||
@code-review-checklist 请检查我的代码
|
||||
|
||||
# 引用特定文件
|
||||
@backend-architecture 如何创建一个新的模块?
|
||||
```
|
||||
|
||||
### 查看和管理规则
|
||||
|
||||
1. 打开 Cursor Settings(Cmd/Ctrl + ,)
|
||||
2. 进入 **Rules** 选项卡
|
||||
3. 查看所有规则的状态和类型
|
||||
|
||||
### 编辑规则
|
||||
|
||||
直接编辑 `.cursor/rules/` 目录中的 `.mdc` 文件,Cursor 会自动重新加载。
|
||||
|
||||
## 📖 快速参考
|
||||
|
||||
### 对于快速查阅
|
||||
使用 `AGENTS.md`(纯 Markdown,无元数据):
|
||||
```bash
|
||||
cat AGENTS.md
|
||||
```
|
||||
|
||||
### 对于详细规范
|
||||
查看 `.cursor/rules/` 中的具体规则文件。
|
||||
|
||||
## 🔄 从旧版本迁移
|
||||
|
||||
旧的 `.cursorrules` 文件已被拆分为多个小规则文件:
|
||||
|
||||
| 旧内容 | 新位置 |
|
||||
|-------|--------|
|
||||
| 项目概述 | `project-overview.mdc` |
|
||||
| 后端规范 | `backend-architecture.mdc` + `backend-specific.mdc` |
|
||||
| 前端规范 | `frontend-architecture.mdc` + `frontend-specific.mdc` |
|
||||
| 数据库规范 | `database-design.mdc` |
|
||||
| 多租户规范 | `multi-tenant.mdc` |
|
||||
| 代码审查 | `code-review-checklist.mdc` |
|
||||
|
||||
## 💡 最佳实践
|
||||
|
||||
### 1. 规则大小
|
||||
- 每个规则文件 < 500 行
|
||||
- 聚焦单一主题
|
||||
- 提供具体示例
|
||||
|
||||
### 2. 嵌套规则
|
||||
- 在子目录创建 `.cursor/rules/` 针对特定区域
|
||||
- 子规则会与父规则合并
|
||||
- 更具体的规则优先级更高
|
||||
|
||||
### 3. 规则复用
|
||||
- 将重复的提示词转换为规则
|
||||
- 使用 `@rule-name` 在对话中引用
|
||||
- 避免每次重复输入相同指令
|
||||
|
||||
## ⚠️ 重要提醒
|
||||
|
||||
### 多租户隔离
|
||||
`multi-tenant.mdc` 规则设为 **Always Apply**,确保所有代码生成都包含租户隔离检查。这是系统安全的核心!
|
||||
|
||||
### 规则优先级
|
||||
规则应用顺序:**Team Rules → Project Rules → User Rules**
|
||||
|
||||
## 🔗 参考链接
|
||||
|
||||
- [Cursor Rules 官方文档](https://cursor.com/cn/docs/context/rules)
|
||||
- [MDC 格式说明](https://cursor.com/cn/docs/context/rules#规则结构)
|
||||
- [最佳实践](https://cursor.com/cn/docs/context/rules#最佳实践)
|
||||
|
||||
## 📝 更新日志
|
||||
|
||||
- **2025-11-27**: 从 `.cursorrules` 迁移到新的 Project Rules 系统
|
||||
- 拆分为 6 个主规则 + 2 个嵌套规则
|
||||
- 添加 AGENTS.md 作为快速参考
|
||||
- 遵循 Cursor 官方最佳实践
|
||||
|
||||
7
.cursor/mcp.json
Normal file
7
.cursor/mcp.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"chrome-devtools": {
|
||||
"command": "/Users/wwzh/Awesome/runfast/competition-management-system/.cursor/scripts/chrome-devtools-mcp.sh"
|
||||
}
|
||||
}
|
||||
}
|
||||
221
.cursor/rules/backend-architecture.mdc
Normal file
221
.cursor/rules/backend-architecture.mdc
Normal file
@ -0,0 +1,221 @@
|
||||
---
|
||||
description: NestJS 后端架构规范和模块结构
|
||||
globs:
|
||||
- "backend/**/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# 后端架构规范
|
||||
|
||||
## 模块结构
|
||||
|
||||
每个功能模块应包含:
|
||||
|
||||
- `module.ts` - 模块定义
|
||||
- `controller.ts` - 控制器
|
||||
- `service.ts` - 服务层
|
||||
- `dto/` - 数据传输对象目录
|
||||
|
||||
### 命名规范
|
||||
|
||||
- 模块命名使用复数形式:`users`, `roles`, `contests`
|
||||
- 子模块放在父模块目录下:`contests/works/`, `contests/teams/`
|
||||
|
||||
### 目录结构示例
|
||||
|
||||
```
|
||||
src/
|
||||
├── contests/
|
||||
│ ├── contests.module.ts
|
||||
│ ├── contests/
|
||||
│ │ ├── contests.module.ts
|
||||
│ │ ├── contests.controller.ts
|
||||
│ │ ├── contests.service.ts
|
||||
│ │ └── dto/
|
||||
│ ├── works/
|
||||
│ │ ├── works.module.ts
|
||||
│ │ ├── works.controller.ts
|
||||
│ │ ├── works.service.ts
|
||||
│ │ └── dto/
|
||||
│ └── teams/
|
||||
│ └── ...
|
||||
```
|
||||
|
||||
## 服务层 (Service)
|
||||
|
||||
### 基本规范
|
||||
|
||||
- 使用 `@Injectable()` 装饰器
|
||||
- 构造函数注入依赖,使用 `private readonly`
|
||||
- 所有数据库操作通过 PrismaService
|
||||
- **禁止直接使用 SQL**
|
||||
|
||||
### 标准方法命名
|
||||
|
||||
- `create` - 创建
|
||||
- `findAll` - 查询列表(支持分页)
|
||||
- `findOne` - 查询单个
|
||||
- `update` - 更新
|
||||
- `remove` - 删除(软删除或级联)
|
||||
|
||||
### 示例
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async create(createDto: CreateUserDto, tenantId: number) {
|
||||
return this.prisma.user.create({
|
||||
data: {
|
||||
...createDto,
|
||||
tenantId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findAll(tenantId: number, skip?: number, take?: number) {
|
||||
return this.prisma.user.findMany({
|
||||
where: { tenantId, validState: 1 },
|
||||
skip,
|
||||
take,
|
||||
include: {
|
||||
roles: {
|
||||
include: { role: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 控制器层 (Controller)
|
||||
|
||||
### 基本规范
|
||||
|
||||
- 使用 `@Controller()` 装饰器,路径使用复数形式
|
||||
- 所有路由默认需要认证(除非使用 `@Public()` 装饰器)
|
||||
- 使用 REST 风格的 HTTP 方法装饰器
|
||||
|
||||
### 装饰器使用
|
||||
|
||||
```typescript
|
||||
@Controller("users")
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@Post()
|
||||
@RequirePermission("user:create")
|
||||
async create(
|
||||
@Body() createDto: CreateUserDto,
|
||||
@CurrentTenantId() tenantId: number,
|
||||
@CurrentUser() user: any
|
||||
) {
|
||||
return this.usersService.create(createDto, tenantId);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@RequirePermission("user:read")
|
||||
async findAll(
|
||||
@CurrentTenantId() tenantId: number,
|
||||
@Query("skip") skip?: number,
|
||||
@Query("take") take?: number
|
||||
) {
|
||||
return this.usersService.findAll(tenantId, skip, take);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get("public-info")
|
||||
async getPublicInfo() {
|
||||
return { version: "1.0.0" };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 常用装饰器
|
||||
|
||||
- `@CurrentTenantId()` - 获取当前租户ID
|
||||
- `@CurrentUser()` - 获取当前用户信息
|
||||
- `@RequirePermission('module:action')` - 权限检查
|
||||
- `@Public()` - 公开接口,无需认证
|
||||
|
||||
## DTO 规范
|
||||
|
||||
### 命名规范
|
||||
|
||||
- 创建:`CreateXxxDto`
|
||||
- 更新:`UpdateXxxDto`
|
||||
- 查询:`QueryXxxDto`
|
||||
|
||||
### 验证规则
|
||||
|
||||
使用 `class-validator` 装饰器:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
IsString,
|
||||
IsEmail,
|
||||
IsOptional,
|
||||
IsArray,
|
||||
IsNumber,
|
||||
} from "class-validator";
|
||||
|
||||
export class CreateUserDto {
|
||||
@IsString()
|
||||
username: string;
|
||||
|
||||
@IsString()
|
||||
password: string;
|
||||
|
||||
@IsString()
|
||||
nickname: string;
|
||||
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
email?: string;
|
||||
|
||||
@IsArray()
|
||||
@IsNumber({}, { each: true })
|
||||
@IsOptional()
|
||||
roleIds?: number[];
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
使用 NestJS 内置异常,消息使用中文:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
UnauthorizedException,
|
||||
ForbiddenException,
|
||||
} from "@nestjs/common";
|
||||
|
||||
// 示例
|
||||
if (!user) {
|
||||
throw new NotFoundException("用户不存在");
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
throw new BadRequestException("数据验证失败");
|
||||
}
|
||||
```
|
||||
|
||||
## 权限控制
|
||||
|
||||
权限字符串格式:`模块:操作`
|
||||
|
||||
```typescript
|
||||
@RequirePermission('contest:create') // 创建竞赛
|
||||
@RequirePermission('user:update') // 更新用户
|
||||
@RequirePermission('role:delete') // 删除角色
|
||||
```
|
||||
|
||||
## 代码风格
|
||||
|
||||
- 导入顺序:NestJS 核心 → 第三方库 → 本地模块
|
||||
- 使用 async/await,避免 Promise.then()
|
||||
- 使用解构赋值提高代码可读性
|
||||
- 复杂逻辑必须添加注释
|
||||
112
.cursor/rules/code-review-checklist.mdc
Normal file
112
.cursor/rules/code-review-checklist.mdc
Normal file
@ -0,0 +1,112 @@
|
||||
---
|
||||
description: 代码审查检查清单(手动应用)
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# 代码审查检查清单
|
||||
|
||||
在提交代码前,请确保以下各项都已检查:
|
||||
|
||||
## 多租户数据隔离
|
||||
|
||||
- [ ] 所有数据库查询包含 `tenantId` 条件
|
||||
- [ ] 创建数据时设置了 `tenantId`
|
||||
- [ ] 更新/删除操作验证了 `tenantId`
|
||||
- [ ] 新的 Prisma 模型包含了必需的审计字段
|
||||
|
||||
## 数据验证
|
||||
|
||||
- [ ] DTO 验证规则完整
|
||||
- [ ] 前端和后端都进行了数据验证
|
||||
- [ ] 使用了 TypeScript 类型定义
|
||||
- [ ] 处理了所有必填字段
|
||||
|
||||
## 错误处理
|
||||
|
||||
- [ ] 所有异步操作都有错误处理
|
||||
- [ ] 错误信息清晰明确
|
||||
- [ ] 使用了合适的 HTTP 状态码
|
||||
- [ ] 前端显示了友好的错误提示
|
||||
|
||||
## 权限控制
|
||||
|
||||
- [ ] 后端使用了 `@RequirePermission()` 装饰器
|
||||
- [ ] 前端路由配置了 `permissions` meta
|
||||
- [ ] 权限验证失败返回 403
|
||||
- [ ] 遵循最小权限原则
|
||||
|
||||
## 代码质量
|
||||
|
||||
- [ ] 代码格式符合 ESLint/Prettier 规范
|
||||
- [ ] 复杂逻辑添加了注释
|
||||
- [ ] 变量和函数命名清晰
|
||||
- [ ] 无硬编码配置(使用环境变量)
|
||||
- [ ] 无调试代码(console.log 等)
|
||||
|
||||
## 性能优化
|
||||
|
||||
- [ ] 数据库查询使用了 `include` 预加载
|
||||
- [ ] 使用了 `select` 精简字段
|
||||
- [ ] 实现了分页查询
|
||||
- [ ] 避免了 N+1 查询
|
||||
- [ ] 前端组件按需加载
|
||||
|
||||
## 安全性
|
||||
|
||||
- [ ] 敏感数据加密存储
|
||||
- [ ] API 需要认证(除非 `@Public()`)
|
||||
- [ ] 防止了 SQL 注入(使用 Prisma)
|
||||
- [ ] 防止了 XSS 攻击
|
||||
- [ ] Token 过期时间合理
|
||||
|
||||
## 测试
|
||||
|
||||
- [ ] 核心业务逻辑有单元测试
|
||||
- [ ] 测试覆盖率 > 80%
|
||||
- [ ] 所有测试通过
|
||||
|
||||
## Git 提交
|
||||
|
||||
- [ ] 提交信息清晰(使用中文)
|
||||
- [ ] 提交信息格式:`类型: 描述`
|
||||
- [ ] 一次提交只做一件事
|
||||
- [ ] 不包含敏感信息
|
||||
|
||||
## 文档
|
||||
|
||||
- [ ] 复杂功能有文档说明
|
||||
- [ ] API 接口有注释
|
||||
- [ ] README 更新(如有必要)
|
||||
|
||||
## 使用建议
|
||||
|
||||
### 在提交前运行
|
||||
|
||||
```bash
|
||||
# 后端
|
||||
cd backend
|
||||
pnpm lint # 代码检查
|
||||
pnpm test # 运行测试
|
||||
pnpm build # 确保能成功构建
|
||||
|
||||
# 前端
|
||||
cd frontend
|
||||
pnpm lint # 代码检查
|
||||
pnpm build # 确保能成功构建
|
||||
```
|
||||
|
||||
### 自动化检查
|
||||
|
||||
考虑使用 Git hooks(如 husky)自动执行检查:
|
||||
- pre-commit: 运行 lint
|
||||
- pre-push: 运行测试
|
||||
|
||||
### Code Review 关注点
|
||||
|
||||
审查他人代码时,重点关注:
|
||||
1. **数据安全**:租户隔离是否完整
|
||||
2. **权限控制**:是否正确验证权限
|
||||
3. **错误处理**:是否处理所有异常情况
|
||||
4. **代码质量**:是否易于理解和维护
|
||||
5. **性能**:是否有明显的性能问题
|
||||
278
.cursor/rules/database-design.mdc
Normal file
278
.cursor/rules/database-design.mdc
Normal file
@ -0,0 +1,278 @@
|
||||
---
|
||||
description: Prisma 数据库设计规范和最佳实践
|
||||
globs:
|
||||
- "backend/prisma/**/*.prisma"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# 数据库设计规范
|
||||
|
||||
## Prisma Schema 规范
|
||||
|
||||
### 表结构要求
|
||||
|
||||
所有业务表必须包含以下字段:
|
||||
|
||||
```prisma
|
||||
model YourModel {
|
||||
id Int @id @default(autoincrement())
|
||||
tenantId Int @map("tenant_id") /// 租户ID(必填)
|
||||
|
||||
// 业务字段...
|
||||
name String
|
||||
description String?
|
||||
|
||||
// 审计字段
|
||||
validState Int @default(1) @map("valid_state") /// 1-有效,2-失效
|
||||
creator Int? /// 创建人ID
|
||||
modifier Int? /// 修改人ID
|
||||
createTime DateTime @default(now()) @map("create_time")
|
||||
modifyTime DateTime @updatedAt @map("modify_time")
|
||||
|
||||
// 关系
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
creatorUser User? @relation("YourModelCreator", fields: [creator], references: [id], onDelete: SetNull)
|
||||
modifierUser User? @relation("YourModelModifier", fields: [modifier], references: [id], onDelete: SetNull)
|
||||
|
||||
@@map("your_table_name")
|
||||
}
|
||||
```
|
||||
|
||||
### 字段命名规范
|
||||
|
||||
- Prisma 模型使用 camelCase:`tenantId`, `createTime`
|
||||
- 数据库列使用 snake_case:`tenant_id`, `create_time`
|
||||
- 使用 `@map()` 映射字段名
|
||||
- 使用 `@@map()` 映射表名
|
||||
|
||||
### 状态字段
|
||||
|
||||
使用 `validState` 表示数据有效性:
|
||||
|
||||
- `1` - 有效
|
||||
- `2` - 失效(软删除)
|
||||
|
||||
```prisma
|
||||
validState Int @default(1) @map("valid_state")
|
||||
```
|
||||
|
||||
## 关系设计
|
||||
|
||||
### 一对多关系
|
||||
|
||||
```prisma
|
||||
model Tenant {
|
||||
id Int @id @default(autoincrement())
|
||||
users User[]
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
tenantId Int @map("tenant_id")
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
```
|
||||
|
||||
### 多对多关系
|
||||
|
||||
使用显式中间表:
|
||||
|
||||
```prisma
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
roles UserRole[]
|
||||
}
|
||||
|
||||
model Role {
|
||||
id Int @id @default(autoincrement())
|
||||
users UserRole[]
|
||||
}
|
||||
|
||||
model UserRole {
|
||||
userId Int @map("user_id")
|
||||
roleId Int @map("role_id")
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([userId, roleId])
|
||||
@@map("user_roles")
|
||||
}
|
||||
```
|
||||
|
||||
### 一对一关系
|
||||
|
||||
```prisma
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
teacher Teacher?
|
||||
}
|
||||
|
||||
model Teacher {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int @unique @map("user_id")
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
```
|
||||
|
||||
### 级联删除规则
|
||||
|
||||
- 强依赖关系:`onDelete: Cascade`
|
||||
- 弱依赖关系:`onDelete: SetNull`(字段必须可选)
|
||||
- 保护性关系:`onDelete: Restrict`
|
||||
|
||||
## 索引设计
|
||||
|
||||
### 自动索引
|
||||
|
||||
- 主键自动创建索引
|
||||
- 外键字段自动创建索引
|
||||
- `@unique` 字段自动创建唯一索引
|
||||
|
||||
### 复合索引
|
||||
|
||||
```prisma
|
||||
model User {
|
||||
tenantId Int
|
||||
username String
|
||||
|
||||
@@unique([tenantId, username])
|
||||
@@index([tenantId, validState])
|
||||
}
|
||||
```
|
||||
|
||||
### 性能优化索引
|
||||
|
||||
为频繁查询的字段添加索引:
|
||||
|
||||
```prisma
|
||||
model Contest {
|
||||
tenantId Int
|
||||
status Int
|
||||
startTime DateTime
|
||||
|
||||
@@index([tenantId, status])
|
||||
@@index([tenantId, startTime])
|
||||
}
|
||||
```
|
||||
|
||||
## Prisma 查询最佳实践
|
||||
|
||||
### 使用 include 预加载关联
|
||||
|
||||
避免 N+1 查询问题:
|
||||
|
||||
```typescript
|
||||
// ✅ 好的做法 - 使用 include 预加载
|
||||
const users = await prisma.user.findMany({
|
||||
where: { tenantId },
|
||||
include: {
|
||||
roles: {
|
||||
include: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// ❌ 不好的做法 - N+1 查询
|
||||
const users = await prisma.user.findMany({
|
||||
where: { tenantId },
|
||||
});
|
||||
for (const user of users) {
|
||||
user.roles = await prisma.userRole.findMany({
|
||||
where: { userId: user.id },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 select 精简字段
|
||||
|
||||
只查询需要的字段:
|
||||
|
||||
```typescript
|
||||
const users = await prisma.user.findMany({
|
||||
where: { tenantId },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
nickname: true,
|
||||
// 不查询 password 等敏感字段
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 分页查询
|
||||
|
||||
```typescript
|
||||
const users = await prisma.user.findMany({
|
||||
where: { tenantId, validState: 1 },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
orderBy: { createTime: "desc" },
|
||||
});
|
||||
|
||||
const total = await prisma.user.count({
|
||||
where: { tenantId, validState: 1 },
|
||||
});
|
||||
```
|
||||
|
||||
### 事务处理
|
||||
|
||||
使用 `$transaction` 确保数据一致性:
|
||||
|
||||
```typescript
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// 创建用户
|
||||
const user = await tx.user.create({
|
||||
data: {
|
||||
username: "test",
|
||||
tenantId,
|
||||
},
|
||||
});
|
||||
|
||||
// 创建用户角色关联
|
||||
await tx.userRole.createMany({
|
||||
data: roleIds.map((roleId) => ({
|
||||
userId: user.id,
|
||||
roleId,
|
||||
})),
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 数据迁移
|
||||
|
||||
### 创建迁移
|
||||
|
||||
```bash
|
||||
# 开发环境 - 创建并应用迁移
|
||||
pnpm prisma:migrate:dev
|
||||
|
||||
# 生产环境 - 只应用迁移
|
||||
pnpm prisma:migrate:deploy
|
||||
```
|
||||
|
||||
### 迁移命名
|
||||
|
||||
使用描述性的迁移名称:
|
||||
|
||||
```bash
|
||||
prisma migrate dev --name add_contest_module
|
||||
prisma migrate dev --name add_user_avatar_field
|
||||
```
|
||||
|
||||
### 迁移注意事项
|
||||
|
||||
- 迁移前备份数据库
|
||||
- 测试迁移在开发环境的执行
|
||||
- 生产环境使用 `migrate deploy` 而不是 `migrate dev`
|
||||
- 不要手动修改已应用的迁移文件
|
||||
|
||||
## 性能优化清单
|
||||
|
||||
- [ ] 频繁查询的字段添加了索引
|
||||
- [ ] 使用 `include` 预加载关联数据
|
||||
- [ ] 使用 `select` 只查询需要的字段
|
||||
- [ ] 实现了分页查询
|
||||
- [ ] 复杂操作使用了事务
|
||||
- [ ] 避免了 N+1 查询问题
|
||||
348
.cursor/rules/frontend-architecture.mdc
Normal file
348
.cursor/rules/frontend-architecture.mdc
Normal file
@ -0,0 +1,348 @@
|
||||
---
|
||||
description: Vue 3 前端架构规范和组件开发
|
||||
globs:
|
||||
- "frontend/**/*.vue"
|
||||
- "frontend/**/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# 前端架构规范
|
||||
|
||||
## 组件结构
|
||||
|
||||
### 目录组织
|
||||
|
||||
- 页面组件放在 `views/` 目录下,按模块组织
|
||||
- 公共组件放在 `components/` 目录下
|
||||
- 组件命名使用 PascalCase
|
||||
|
||||
### 组件语法
|
||||
|
||||
使用 Vue 3 Composition API 的 `<script setup lang="ts">` 语法:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import { getUsers, type User } from "@/api/users";
|
||||
|
||||
const loading = ref(false);
|
||||
const users = ref<User[]>([]);
|
||||
|
||||
const activeUsers = computed(() =>
|
||||
users.value.filter((u) => u.validState === 1)
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchUsers();
|
||||
});
|
||||
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
users.value = await getUsers();
|
||||
} catch (error) {
|
||||
message.error("获取用户列表失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<a-spin :spinning="loading">
|
||||
<a-table :dataSource="activeUsers" />
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## API 调用规范
|
||||
|
||||
### 目录结构
|
||||
|
||||
所有 API 调用放在 `api/` 目录下,按模块组织:
|
||||
|
||||
```
|
||||
api/
|
||||
├── users.ts
|
||||
├── roles.ts
|
||||
├── contests.ts
|
||||
└── auth.ts
|
||||
```
|
||||
|
||||
### API 函数命名
|
||||
|
||||
- `getXxx` - 获取数据
|
||||
- `createXxx` - 创建数据
|
||||
- `updateXxx` - 更新数据
|
||||
- `deleteXxx` - 删除数据
|
||||
|
||||
### 示例
|
||||
|
||||
```typescript
|
||||
// api/users.ts
|
||||
import request from "@/utils/request";
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
nickname: string;
|
||||
email?: string;
|
||||
validState: number;
|
||||
}
|
||||
|
||||
export interface CreateUserDto {
|
||||
username: string;
|
||||
password: string;
|
||||
nickname: string;
|
||||
email?: string;
|
||||
roleIds?: number[];
|
||||
}
|
||||
|
||||
export const getUsers = (params?: { skip?: number; take?: number }) => {
|
||||
return request.get<User[]>("/users", { params });
|
||||
};
|
||||
|
||||
export const createUser = (data: CreateUserDto) => {
|
||||
return request.post<User>("/users", data);
|
||||
};
|
||||
|
||||
export const updateUser = (id: number, data: Partial<CreateUserDto>) => {
|
||||
return request.put<User>(`/users/${id}`, data);
|
||||
};
|
||||
|
||||
export const deleteUser = (id: number) => {
|
||||
return request.delete(`/users/${id}`);
|
||||
};
|
||||
```
|
||||
|
||||
## 状态管理 (Pinia)
|
||||
|
||||
### Store 规范
|
||||
|
||||
- Store 文件放在 `stores/` 目录下
|
||||
- 使用 `defineStore()` 定义 store
|
||||
- Store 命名使用 camelCase + Store 后缀
|
||||
|
||||
### 示例
|
||||
|
||||
```typescript
|
||||
// stores/auth.ts
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, computed } from "vue";
|
||||
import { login, getUserInfo, type LoginDto, type User } from "@/api/auth";
|
||||
|
||||
export const useAuthStore = defineStore("auth", () => {
|
||||
const token = ref<string | null>(localStorage.getItem("token"));
|
||||
const user = ref<User | null>(null);
|
||||
const menus = ref<any[]>([]);
|
||||
|
||||
const isAuthenticated = computed(() => !!token.value && !!user.value);
|
||||
|
||||
const loginAction = async (loginDto: LoginDto) => {
|
||||
const {
|
||||
accessToken,
|
||||
user: userInfo,
|
||||
menus: userMenus,
|
||||
} = await login(loginDto);
|
||||
token.value = accessToken;
|
||||
user.value = userInfo;
|
||||
menus.value = userMenus;
|
||||
localStorage.setItem("token", accessToken);
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
token.value = null;
|
||||
user.value = null;
|
||||
menus.value = [];
|
||||
localStorage.removeItem("token");
|
||||
};
|
||||
|
||||
return {
|
||||
token,
|
||||
user,
|
||||
menus,
|
||||
isAuthenticated,
|
||||
loginAction,
|
||||
logout,
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
## 路由管理
|
||||
|
||||
### 路由规范
|
||||
|
||||
- 路由配置在 `router/index.ts`
|
||||
- 支持动态路由(基于菜单权限)
|
||||
- 路由路径包含租户编码:`/:tenantCode/xxx`
|
||||
- 路由 meta 包含权限信息
|
||||
|
||||
### 示例
|
||||
|
||||
```typescript
|
||||
{
|
||||
path: '/:tenantCode/users',
|
||||
name: 'Users',
|
||||
component: () => import('@/views/users/Index.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
permissions: ['user:read'],
|
||||
roles: ['admin'],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## 表单验证
|
||||
|
||||
### 使用 VeeValidate + Zod
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { useForm } from "vee-validate";
|
||||
import { toTypedSchema } from "@vee-validate/zod";
|
||||
import * as z from "zod";
|
||||
|
||||
const schema = z.object({
|
||||
username: z.string().min(3, "用户名至少3个字符"),
|
||||
password: z.string().min(6, "密码至少6个字符"),
|
||||
email: z.string().email("邮箱格式不正确").optional(),
|
||||
});
|
||||
|
||||
const { defineField, handleSubmit, errors } = useForm({
|
||||
validationSchema: toTypedSchema(schema),
|
||||
});
|
||||
|
||||
const [username] = defineField("username");
|
||||
const [password] = defineField("password");
|
||||
const [email] = defineField("email");
|
||||
|
||||
const onSubmit = handleSubmit(async (values) => {
|
||||
try {
|
||||
await createUser(values);
|
||||
message.success("创建成功");
|
||||
} catch (error) {
|
||||
message.error("创建失败");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-form @submit.prevent="onSubmit">
|
||||
<a-form-item
|
||||
:help="errors.username"
|
||||
:validateStatus="errors.username ? 'error' : ''"
|
||||
>
|
||||
<a-input v-model:value="username" placeholder="用户名" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
:help="errors.password"
|
||||
:validateStatus="errors.password ? 'error' : ''"
|
||||
>
|
||||
<a-input-password v-model:value="password" placeholder="密码" />
|
||||
</a-form-item>
|
||||
<a-button type="primary" html-type="submit">提交</a-button>
|
||||
</a-form>
|
||||
</template>
|
||||
```
|
||||
|
||||
## UI 组件规范
|
||||
|
||||
### Ant Design Vue
|
||||
|
||||
- 使用 Ant Design Vue 组件库
|
||||
- 遵循 Ant Design 设计规范
|
||||
|
||||
### 样式
|
||||
|
||||
- 使用 Tailwind CSS 工具类
|
||||
- 复杂样式使用 SCSS
|
||||
- 响应式设计,移动端优先
|
||||
|
||||
### 状态管理
|
||||
|
||||
组件必须有 loading 和 error 状态:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<a-spin :spinning="loading">
|
||||
<a-alert
|
||||
v-if="error"
|
||||
type="error"
|
||||
:message="error"
|
||||
closable
|
||||
@close="error = null"
|
||||
/>
|
||||
<div v-else>
|
||||
<!-- 内容 -->
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## TypeScript 类型定义
|
||||
|
||||
### 类型文件组织
|
||||
|
||||
- TypeScript 类型定义放在 `types/` 目录下
|
||||
- 接口类型使用 `interface`
|
||||
- 数据模型使用 `type`
|
||||
- 导出类型供其他模块使用
|
||||
|
||||
### 示例
|
||||
|
||||
```typescript
|
||||
// types/user.ts
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
nickname: string;
|
||||
email?: string;
|
||||
tenantId: number;
|
||||
validState: number;
|
||||
createTime: string;
|
||||
modifyTime: string;
|
||||
}
|
||||
|
||||
export type CreateUserParams = Omit<
|
||||
User,
|
||||
"id" | "tenantId" | "createTime" | "modifyTime"
|
||||
>;
|
||||
|
||||
export type UserRole = {
|
||||
userId: number;
|
||||
roleId: number;
|
||||
role: Role;
|
||||
};
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 路由懒加载
|
||||
|
||||
```typescript
|
||||
const routes = [
|
||||
{
|
||||
path: "/users",
|
||||
component: () => import("@/views/users/Index.vue"),
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### 组件按需加载
|
||||
|
||||
```typescript
|
||||
import { defineAsyncComponent } from "vue";
|
||||
|
||||
const AsyncComponent = defineAsyncComponent(
|
||||
() => import("@/components/HeavyComponent.vue")
|
||||
);
|
||||
```
|
||||
|
||||
### 避免不必要的重新渲染
|
||||
|
||||
使用 `computed`、`watchEffect` 和 `memo` 优化性能。
|
||||
101
.cursor/rules/multi-tenant.mdc
Normal file
101
.cursor/rules/multi-tenant.mdc
Normal file
@ -0,0 +1,101 @@
|
||||
---
|
||||
description: 多租户数据隔离规范(所有涉及数据库操作的代码必须遵守)
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 多租户处理规范
|
||||
|
||||
⚠️ **极其重要**:所有业务数据查询必须包含 `tenantId` 条件!
|
||||
|
||||
## 核心原则
|
||||
|
||||
### 1. 数据库查询
|
||||
|
||||
- **必须**:所有业务表查询必须包含 `tenantId` 条件
|
||||
- 超级租户(`isSuper = 1`)可以访问所有租户数据
|
||||
|
||||
```typescript
|
||||
// ✅ 正确示例
|
||||
const users = await this.prisma.user.findMany({
|
||||
where: {
|
||||
tenantId,
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
|
||||
// ❌ 错误示例 - 缺少 tenantId
|
||||
const users = await this.prisma.user.findMany({
|
||||
where: {
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 获取租户ID
|
||||
|
||||
在控制器中使用 `@CurrentTenantId()` 装饰器:
|
||||
|
||||
```typescript
|
||||
@Get()
|
||||
async findAll(@CurrentTenantId() tenantId: number) {
|
||||
return this.service.findAll(tenantId);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 创建数据
|
||||
|
||||
创建数据时自动设置 `tenantId`:
|
||||
|
||||
```typescript
|
||||
async create(createDto: CreateDto, tenantId: number) {
|
||||
return this.prisma.model.create({
|
||||
data: {
|
||||
...createDto,
|
||||
tenantId,
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 更新/删除数据
|
||||
|
||||
更新或删除前验证数据属于当前租户:
|
||||
|
||||
```typescript
|
||||
async update(id: number, updateDto: UpdateDto, tenantId: number) {
|
||||
// 先验证数据属于当前租户
|
||||
const existing = await this.prisma.model.findFirst({
|
||||
where: { id, tenantId },
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
throw new NotFoundException('数据不存在或不属于当前租户');
|
||||
}
|
||||
|
||||
return this.prisma.model.update({
|
||||
where: { id },
|
||||
data: updateDto,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 数据库表设计
|
||||
|
||||
所有业务表必须包含:
|
||||
|
||||
- `tenantId`: Int - 租户ID(必填)
|
||||
- `creator`: Int? - 创建人ID
|
||||
- `modifier`: Int? - 修改人ID
|
||||
- `createTime`: DateTime @default(now())
|
||||
- `modifyTime`: DateTime @updatedAt
|
||||
- `validState`: Int @default(1) - 有效状态(1-有效,2-失效)
|
||||
|
||||
## 审查清单
|
||||
|
||||
在代码审查时,重点检查:
|
||||
|
||||
- [ ] 所有 `findMany`、`findFirst`、`findUnique` 包含 `tenantId` 条件
|
||||
- [ ] 创建操作设置了 `tenantId`
|
||||
- [ ] 更新/删除操作验证了 `tenantId`
|
||||
- [ ] 新的 Prisma 模型包含了 `tenantId` 字段
|
||||
41
.cursor/rules/project-overview.mdc
Normal file
41
.cursor/rules/project-overview.mdc
Normal file
@ -0,0 +1,41 @@
|
||||
---
|
||||
description: 项目概述和技术栈信息
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 项目概述
|
||||
|
||||
这是一个多租户的竞赛管理系统,采用前后端分离架构。
|
||||
|
||||
## 技术栈
|
||||
|
||||
### 后端
|
||||
|
||||
- **框架**: NestJS + TypeScript
|
||||
- **数据库**: MySQL 8.0
|
||||
- **ORM**: Prisma
|
||||
- **认证**: JWT + RBAC (基于角色的访问控制)
|
||||
|
||||
### 前端
|
||||
|
||||
- **框架**: Vue 3 + TypeScript
|
||||
- **构建工具**: Vite
|
||||
- **UI 组件库**: Ant Design Vue
|
||||
- **状态管理**: Pinia
|
||||
- **路由**: Vue Router 4
|
||||
- **表单验证**: VeeValidate + Zod
|
||||
|
||||
## 核心特性
|
||||
|
||||
- **多租户架构**: 数据完全隔离,每个租户使用独立的 tenantId
|
||||
- **RBAC 权限系统**: 基于角色的细粒度权限控制
|
||||
- **动态菜单系统**: 基于权限的动态路由和菜单
|
||||
- **审计日志**: 完整的操作审计追踪
|
||||
|
||||
## 代码风格
|
||||
|
||||
- 使用 TypeScript 严格模式
|
||||
- 使用 ESLint 和 Prettier 格式化代码
|
||||
- 注释使用中文
|
||||
- Git 提交信息使用中文,格式:`类型: 描述`
|
||||
14
.cursor/scripts/chrome-devtools-mcp.sh
Executable file
14
.cursor/scripts/chrome-devtools-mcp.sh
Executable file
@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
# Chrome DevTools MCP wrapper script
|
||||
# Ensures correct Node.js version is used
|
||||
|
||||
# Load nvm if available
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
|
||||
# Use Node.js 20.19.0 (or default)
|
||||
nvm use default 2>/dev/null || nvm use 20.19.0 2>/dev/null || true
|
||||
|
||||
# Run chrome-devtools-mcp
|
||||
exec node "/Users/wwzh/Library/pnpm/global/5/.pnpm/chrome-devtools-mcp@0.10.2/node_modules/chrome-devtools-mcp/build/src/index.js" "$@"
|
||||
|
||||
100
.cursorignore
Normal file
100
.cursorignore
Normal file
@ -0,0 +1,100 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
*/node_modules/
|
||||
.pnpm-store/
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
*/dist/
|
||||
build/
|
||||
*/build/
|
||||
*.tsbuildinfo
|
||||
|
||||
# Environment variables (may contain sensitive information)
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
*/.env
|
||||
*/.env.local
|
||||
*/.env.*.local
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Testing coverage (keep test files for context)
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# Database files
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Lock files (too large and not needed for context)
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.tmp
|
||||
|
||||
# Prisma migrations SQL files (generated, but may be useful for context)
|
||||
# Uncomment if you want to ignore migration SQL files:
|
||||
# backend/prisma/migrations/**/*.sql
|
||||
|
||||
# Compiled JavaScript files (keep TypeScript source and config files)
|
||||
*.js
|
||||
*.js.map
|
||||
*.d.ts
|
||||
!*.config.js
|
||||
!*.config.ts
|
||||
!vite.config.js
|
||||
!tailwind.config.js
|
||||
!postcss.config.js
|
||||
|
||||
# Frontend build artifacts
|
||||
frontend/dist/
|
||||
frontend/dist-ssr/
|
||||
frontend/.vite/
|
||||
|
||||
# Backend build artifacts
|
||||
backend/dist/
|
||||
|
||||
# Large binary files
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
*.7z
|
||||
|
||||
# Documentation build outputs (if any)
|
||||
docs/_build/
|
||||
docs/.vuepress/dist/
|
||||
|
||||
# Cache directories
|
||||
.cache/
|
||||
.parcel-cache/
|
||||
.next/
|
||||
.nuxt/
|
||||
.vuepress/dist/
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
293
.cursorrules
Normal file
293
.cursorrules
Normal file
@ -0,0 +1,293 @@
|
||||
# Competition Management System - Cursor User Rules (DEPRECATED)
|
||||
|
||||
⚠️ **此文件已废弃** - 请使用新的规则系统:
|
||||
- 项目规则:`.cursor/rules/*.mdc`
|
||||
- 快速参考:`AGENTS.md`
|
||||
- 说明文档:`.cursor/RULES_README.md`
|
||||
|
||||
---
|
||||
|
||||
以下内容保留作为备份,但不再使用:
|
||||
|
||||
# Competition Management System - Cursor User Rules
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个多租户的竞赛管理系统,采用前后端分离架构:
|
||||
- **后端**: NestJS + TypeScript + Prisma + MySQL
|
||||
- **前端**: Vue 3 + TypeScript + Vite + Ant Design Vue + Pinia
|
||||
- **认证**: JWT + RBAC (基于角色的访问控制)
|
||||
- **架构**: 多租户架构,数据完全隔离
|
||||
|
||||
## 后端开发规范
|
||||
|
||||
### 1. 模块结构
|
||||
|
||||
- 每个功能模块应包含:`module.ts`, `controller.ts`, `service.ts`, `dto/` 目录
|
||||
- 模块命名使用复数形式(如 `users`, `roles`, `contests`)
|
||||
- 子模块放在父模块目录下(如 `contests/works/`, `contests/teams/`)
|
||||
|
||||
### 2. 服务层 (Service)
|
||||
|
||||
- 所有数据库操作必须通过 PrismaService,禁止直接使用 SQL
|
||||
- 服务方法必须处理租户隔离:所有查询必须包含 `tenantId` 条件
|
||||
- 使用 `@Injectable()` 装饰器
|
||||
- 构造函数注入依赖,使用 private readonly
|
||||
- 方法命名:`create`, `findAll`, `findOne`, `update`, `remove`
|
||||
- 查询方法应支持分页:使用 `skip` 和 `take` 参数
|
||||
|
||||
### 3. 控制器层 (Controller)
|
||||
|
||||
- 使用 `@Controller()` 装饰器,路径使用复数形式
|
||||
- 所有路由默认需要认证(除非使用 `@Public()` 装饰器)
|
||||
- 使用 `@Get()`, `@Post()`, `@Put()`, `@Delete()`, `@Patch()` 装饰器
|
||||
- 从请求中获取租户ID:使用 `@CurrentTenantId()` 装饰器或从 JWT token 中提取
|
||||
- 使用 `@CurrentUser()` 装饰器获取当前用户信息
|
||||
- 权限控制:使用 `@RequirePermission()` 装饰器
|
||||
- 返回统一响应格式:使用 TransformInterceptor(自动处理)
|
||||
|
||||
### 4. DTO (Data Transfer Object)
|
||||
|
||||
- 所有 DTO 放在 `dto/` 目录下
|
||||
- 使用 `class-validator` 进行验证
|
||||
- 命名规范:
|
||||
- 创建:`CreateXxxDto`
|
||||
- 更新:`UpdateXxxDto`
|
||||
- 查询:`QueryXxxDto`
|
||||
- 必填字段使用验证装饰器(如 `@IsString()`, `@IsNumber()`)
|
||||
- 可选字段使用 `@IsOptional()`
|
||||
- 数组字段使用 `@IsArray()` 和 `@IsNumber({}, { each: true })`
|
||||
|
||||
### 5. 数据库操作 (Prisma)
|
||||
|
||||
- 所有表必须包含 `tenantId` 字段(租户隔离)
|
||||
- 所有表必须包含审计字段:`creator`, `modifier`, `createTime`, `modifyTime`
|
||||
- 使用 Prisma 的 `include` 和 `select` 优化查询
|
||||
- 关联查询使用嵌套 include,避免 N+1 问题
|
||||
- 删除操作使用软删除(`validState` 字段)或级联删除
|
||||
- 事务操作使用 `prisma.$transaction()`
|
||||
|
||||
### 6. 多租户处理
|
||||
|
||||
- **必须**:所有业务数据查询必须包含 `tenantId` 条件
|
||||
- 从 JWT token 或请求头中获取租户ID
|
||||
- 创建数据时自动设置 `tenantId`
|
||||
- 更新/删除时验证数据属于当前租户
|
||||
- 超级租户(`isSuper = 1`)可以访问所有租户数据
|
||||
|
||||
### 7. 权限控制
|
||||
|
||||
- 使用 `@RequirePermission()` 装饰器进行权限检查
|
||||
- 权限字符串格式:`模块:操作`(如 `contest:create`, `user:update`)
|
||||
- 角色权限通过 RolesGuard 自动检查
|
||||
- 权限验证失败返回 403 Forbidden
|
||||
|
||||
### 8. 错误处理
|
||||
|
||||
- 使用 NestJS 内置异常:`NotFoundException`, `BadRequestException`, `UnauthorizedException`, `ForbiddenException`
|
||||
- 自定义异常消息使用中文
|
||||
- 错误信息要清晰明确,便于调试
|
||||
|
||||
### 9. 代码风格
|
||||
|
||||
- 使用 TypeScript 严格模式
|
||||
- 使用 ESLint 和 Prettier 格式化代码
|
||||
- 导入顺序:NestJS 核心 → 第三方库 → 本地模块
|
||||
- 使用 async/await,避免 Promise.then()
|
||||
- 使用解构赋值提高代码可读性
|
||||
|
||||
## 前端开发规范
|
||||
|
||||
### 1. 组件结构
|
||||
|
||||
- 页面组件放在 `views/` 目录下,按模块组织
|
||||
- 公共组件放在 `components/` 目录下
|
||||
- 使用 `<script setup lang="ts">` 语法
|
||||
- 组件命名使用 PascalCase
|
||||
|
||||
### 2. API 调用
|
||||
|
||||
- 所有 API 调用放在 `api/` 目录下,按模块组织
|
||||
- 使用 axios 实例(已配置拦截器)
|
||||
- API 函数命名:`getXxx`, `createXxx`, `updateXxx`, `deleteXxx`
|
||||
- 使用 TypeScript 类型定义请求和响应
|
||||
|
||||
### 3. 状态管理 (Pinia)
|
||||
|
||||
- Store 文件放在 `stores/` 目录下
|
||||
- 使用 `defineStore()` 定义 store
|
||||
- Store 命名使用 camelCase + Store 后缀(如 `authStore`)
|
||||
|
||||
### 4. 路由管理
|
||||
|
||||
- 路由配置在 `router/index.ts`
|
||||
- 支持动态路由(基于菜单权限)
|
||||
- 路由路径包含租户编码:`/:tenantCode/xxx`
|
||||
- 路由 meta 包含权限信息:`permissions`, `roles`
|
||||
|
||||
### 5. 表单验证
|
||||
|
||||
- 使用 VeeValidate + Zod 进行表单验证
|
||||
- 验证规则定义在组件内或单独的 schema 文件
|
||||
- 错误提示使用中文
|
||||
|
||||
### 6. UI 组件
|
||||
|
||||
- 使用 Ant Design Vue 组件库
|
||||
- 样式使用 Tailwind CSS + SCSS
|
||||
- 响应式设计:移动端优先
|
||||
- 组件要有 loading 和 error 状态
|
||||
|
||||
### 7. 类型定义
|
||||
|
||||
- TypeScript 类型定义放在 `types/` 目录下
|
||||
- 接口类型使用 `interface`,数据模型使用 `type`
|
||||
- 导出类型供其他模块使用
|
||||
|
||||
## 数据库设计规范
|
||||
|
||||
### 1. 表结构
|
||||
|
||||
- 所有业务表必须包含 `tenantId` 字段
|
||||
- 所有表必须包含审计字段:
|
||||
- `creator`: Int? - 创建人ID
|
||||
- `modifier`: Int? - 修改人ID
|
||||
- `createTime`: DateTime @default(now()) - 创建时间
|
||||
- `modifyTime`: DateTime @updatedAt - 修改时间
|
||||
- 状态字段使用 `validState`: Int @default(1)(1-有效,2-失效)
|
||||
- 表名使用复数形式,映射使用 `@@map("table_name")`
|
||||
|
||||
### 2. 关系设计
|
||||
|
||||
- 使用 Prisma 关系定义外键
|
||||
- 级联删除:`onDelete: Cascade`
|
||||
- 可选关联:`onDelete: SetNull`
|
||||
- 一对一关系:使用 `?` 标记可选
|
||||
|
||||
### 3. 索引
|
||||
|
||||
- 主键自动索引
|
||||
- 外键字段自动索引
|
||||
- 查询频繁的字段添加索引
|
||||
- 唯一约束使用 `@unique`
|
||||
|
||||
### 4. 迁移
|
||||
|
||||
- 使用 Prisma Migrate 管理数据库迁移
|
||||
- 迁移文件命名:`YYYYMMDDHHMMSS_description`
|
||||
- 迁移前备份数据库
|
||||
- 生产环境使用 `prisma migrate deploy`
|
||||
|
||||
## 通用开发规范
|
||||
|
||||
### 1. Git 提交
|
||||
|
||||
- 提交信息使用中文
|
||||
- 格式:`类型: 描述`(如 `feat: 添加竞赛管理功能`)
|
||||
- 类型:`feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
|
||||
|
||||
### 2. 注释
|
||||
|
||||
- 复杂逻辑必须添加注释
|
||||
- 函数注释说明参数和返回值
|
||||
- 使用中文注释
|
||||
|
||||
### 3. 测试
|
||||
|
||||
- 单元测试文件命名:`*.spec.ts`
|
||||
- 测试覆盖率要求:核心业务逻辑 > 80%
|
||||
- 使用 Jest 进行测试
|
||||
|
||||
### 4. 环境配置
|
||||
|
||||
- 使用 `.env.development` 和 `.env.production`
|
||||
- 敏感信息不要提交到 Git
|
||||
- 配置项通过 `@nestjs/config` 管理
|
||||
|
||||
### 5. 日志
|
||||
|
||||
- 使用 NestJS Logger
|
||||
- 日志级别:`error`, `warn`, `log`, `debug`, `verbose`
|
||||
- 记录关键操作和错误信息
|
||||
|
||||
## 安全规范
|
||||
|
||||
### 1. 认证授权
|
||||
|
||||
- 所有 API 默认需要 JWT 认证
|
||||
- 密码使用 bcrypt 加密(salt rounds: 10)
|
||||
- Token 过期时间合理设置
|
||||
- 敏感操作需要额外验证
|
||||
|
||||
### 2. 数据验证
|
||||
|
||||
- 前端和后端都要进行数据验证
|
||||
- 使用 DTO 和 class-validator 验证输入
|
||||
- 防止 SQL 注入:使用 Prisma(参数化查询)
|
||||
- 防止 XSS:前端转义用户输入
|
||||
|
||||
### 3. 权限控制
|
||||
|
||||
- 最小权限原则
|
||||
- 前端显示控制 + 后端权限验证
|
||||
- 租户数据隔离必须严格检查
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 1. 数据库查询
|
||||
|
||||
- 避免 N+1 查询,使用 `include` 预加载
|
||||
- 使用 `select` 只查询需要的字段
|
||||
- 分页查询必须实现
|
||||
- 大表查询添加索引
|
||||
|
||||
### 2. API 响应
|
||||
|
||||
- 响应数据精简,避免返回不必要字段
|
||||
- 使用分页减少单次数据量
|
||||
- 长时间操作使用异步处理
|
||||
|
||||
### 3. 前端优化
|
||||
|
||||
- 路由懒加载
|
||||
- 组件按需加载
|
||||
- 图片使用 CDN 或压缩
|
||||
- 避免不必要的重新渲染
|
||||
|
||||
## 代码审查检查清单
|
||||
|
||||
- [ ] 所有数据库查询包含 `tenantId` 条件
|
||||
- [ ] DTO 验证规则完整
|
||||
- [ ] 错误处理完善
|
||||
- [ ] 权限检查正确
|
||||
- [ ] 代码格式符合规范
|
||||
- [ ] 注释清晰
|
||||
- [ ] 无硬编码配置
|
||||
- [ ] 类型定义完整
|
||||
- [ ] 无控制台日志(生产环境)
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何获取当前租户ID?
|
||||
A: 在控制器中使用 `@CurrentTenantId()` 装饰器,或在服务中从 JWT token 提取。
|
||||
|
||||
### Q: 如何创建新的业务模块?
|
||||
A:
|
||||
1. 在 Prisma schema 中定义模型
|
||||
2. 运行 `prisma migrate dev`
|
||||
3. 创建模块目录和文件(module, controller, service, dto)
|
||||
4. 在 `app.module.ts` 中注册模块
|
||||
5. 创建前端 API 和页面
|
||||
|
||||
### Q: 如何处理多租户数据隔离?
|
||||
A:
|
||||
- 查询时始终包含 `where: { tenantId }`
|
||||
- 创建时自动设置 `tenantId`
|
||||
- 更新/删除前验证数据属于当前租户
|
||||
|
||||
### Q: 如何添加新的权限?
|
||||
A:
|
||||
1. 在数据库中创建权限记录
|
||||
2. 在路由或控制器方法上使用 `@RequirePermission('module:action')`
|
||||
3. 在前端路由 meta 中添加 `permissions` 字段
|
||||
|
||||
47
.gitignore
vendored
Normal file
47
.gitignore
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
*/node_modules/
|
||||
.pnpm-store/
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
*/dist/
|
||||
build/
|
||||
*/build/
|
||||
|
||||
# pnpm
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
*/.env
|
||||
*/.env.local
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# Prisma
|
||||
backend/prisma/migrations/
|
||||
|
||||
tmpclaude-*
|
||||
8
.npmrc
Normal file
8
.npmrc
Normal file
@ -0,0 +1,8 @@
|
||||
# pnpm 配置
|
||||
shamefully-hoist=true
|
||||
strict-peer-dependencies=false
|
||||
auto-install-peers=true
|
||||
|
||||
# 使用国内镜像(可选,根据需要取消注释)
|
||||
# registry=https://registry.npmmirror.com
|
||||
|
||||
128
AGENTS.md
Normal file
128
AGENTS.md
Normal file
@ -0,0 +1,128 @@
|
||||
# Competition Management System - Agent Instructions
|
||||
|
||||
这是一个多租户的竞赛管理系统,采用 NestJS + Vue 3 技术栈。
|
||||
|
||||
## 🚨 最重要的规则
|
||||
|
||||
### 多租户数据隔离
|
||||
|
||||
**所有数据库查询必须包含 `tenantId` 条件!** 这是系统安全的核心。
|
||||
|
||||
```typescript
|
||||
// ✅ 正确
|
||||
const users = await prisma.user.findMany({
|
||||
where: { tenantId, validState: 1 }
|
||||
});
|
||||
|
||||
// ❌ 错误 - 绝对不允许
|
||||
const users = await prisma.user.findMany();
|
||||
```
|
||||
|
||||
## 后端开发
|
||||
|
||||
### 模块结构
|
||||
- 使用 NestJS 标准模块结构:module、controller、service、dto
|
||||
- 所有数据操作通过 Prisma,禁止直接 SQL
|
||||
- 使用 `@Injectable()`、`@Controller()` 装饰器
|
||||
|
||||
### 权限控制
|
||||
- 使用 `@RequirePermission('module:action')` 装饰器
|
||||
- 格式:`user:create`、`contest:update`、`role:delete`
|
||||
- 所有路由默认需要认证,公开接口使用 `@Public()`
|
||||
|
||||
### DTO 验证
|
||||
```typescript
|
||||
export class CreateUserDto {
|
||||
@IsString()
|
||||
username: string;
|
||||
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
email?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 错误处理
|
||||
- 使用 NestJS 内置异常,消息用中文
|
||||
- `NotFoundException`、`BadRequestException`、`ForbiddenException`
|
||||
|
||||
## 前端开发
|
||||
|
||||
### 组件开发
|
||||
- 使用 Vue 3 `<script setup lang="ts">` 语法
|
||||
- 页面组件放在 `views/` 目录,按模块组织
|
||||
- 使用 Ant Design Vue 组件库
|
||||
|
||||
### API 调用
|
||||
- API 文件放在 `api/` 目录,按模块组织
|
||||
- 函数命名:`getXxx`、`createXxx`、`updateXxx`、`deleteXxx`
|
||||
- 使用 TypeScript 类型定义
|
||||
|
||||
### 路由
|
||||
- 路由路径必须包含租户编码:`/:tenantCode/users`
|
||||
- 使用动态路由(基于菜单权限)
|
||||
|
||||
### 状态管理
|
||||
- 使用 Pinia,store 命名:`useAuthStore`、`useUserStore`
|
||||
|
||||
## 数据库设计
|
||||
|
||||
### 必需字段
|
||||
所有业务表必须包含:
|
||||
```prisma
|
||||
model YourModel {
|
||||
id Int @id @default(autoincrement())
|
||||
tenantId Int @map("tenant_id")
|
||||
validState Int @default(1) @map("valid_state")
|
||||
creator Int?
|
||||
modifier Int?
|
||||
createTime DateTime @default(now()) @map("create_time")
|
||||
modifyTime DateTime @updatedAt @map("modify_time")
|
||||
}
|
||||
```
|
||||
|
||||
### 性能优化
|
||||
- 使用 `include` 预加载,避免 N+1 查询
|
||||
- 使用 `select` 精简字段
|
||||
- 实现分页查询
|
||||
|
||||
## 代码风格
|
||||
|
||||
- TypeScript 严格模式
|
||||
- 使用 async/await,避免 Promise.then()
|
||||
- 使用中文注释
|
||||
- Git 提交信息:`类型: 描述`(如 `feat: 添加用户管理`)
|
||||
|
||||
## 快速参考
|
||||
|
||||
### 创建新模块
|
||||
1. 在 Prisma schema 定义模型
|
||||
2. 运行 `pnpm prisma:migrate:dev`
|
||||
3. 创建 NestJS 模块(module、controller、service、dto)
|
||||
4. 在 `app.module.ts` 注册
|
||||
5. 创建前端 API 和页面
|
||||
|
||||
### 常用装饰器
|
||||
- `@CurrentTenantId()` - 获取租户ID
|
||||
- `@CurrentUser()` - 获取当前用户
|
||||
- `@RequirePermission()` - 权限检查
|
||||
- `@Public()` - 公开接口
|
||||
|
||||
### 开发命令
|
||||
```bash
|
||||
# 后端
|
||||
cd backend
|
||||
pnpm start:dev # 启动开发服务器
|
||||
pnpm prisma:migrate:dev # 数据库迁移
|
||||
pnpm init:admin # 初始化管理员
|
||||
|
||||
# 前端
|
||||
cd frontend
|
||||
pnpm dev # 启动开发服务器
|
||||
pnpm build # 构建生产版本
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
💡 **记住**:租户隔离是系统的核心安全机制,所有数据操作都必须验证 `tenantId`!
|
||||
|
||||
304
README.md
304
README.md
@ -1,3 +1,303 @@
|
||||
# library-picturebook-workshop
|
||||
# 比赛管理系统
|
||||
|
||||
一个基于 Vue 3 + NestJS 的现代化比赛管理系统,支持用户管理、角色权限、菜单管理、数据字典、系统配置和日志记录等核心功能。
|
||||
|
||||
## 技术栈
|
||||
|
||||
### 前端
|
||||
- **框架**: Vue 3 + TypeScript
|
||||
- **构建工具**: Vite
|
||||
- **UI 组件库**: Ant Design Vue
|
||||
- **样式方案**: Tailwind CSS + SCSS + CSS Modules
|
||||
- **状态管理**: Pinia
|
||||
- **路由**: Vue Router 4
|
||||
- **HTTP 客户端**: Axios
|
||||
- **表单验证**: VeeValidate + Zod
|
||||
|
||||
### 后端
|
||||
- **框架**: NestJS + TypeScript
|
||||
- **数据库**: MySQL 8.0
|
||||
- **ORM**: Prisma
|
||||
- **认证授权**: JWT + RBAC (基于角色的访问控制)
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
competition-management-system/
|
||||
├── frontend/ # 前端项目
|
||||
│ ├── src/
|
||||
│ │ ├── api/ # API 接口
|
||||
│ │ ├── assets/ # 静态资源
|
||||
│ │ ├── components/# 公共组件
|
||||
│ │ ├── layouts/ # 布局组件
|
||||
│ │ ├── router/ # 路由配置
|
||||
│ │ ├── stores/ # Pinia 状态管理
|
||||
│ │ ├── styles/ # 样式文件
|
||||
│ │ ├── types/ # TypeScript 类型定义
|
||||
│ │ ├── utils/ # 工具函数
|
||||
│ │ └── views/ # 页面组件
|
||||
│ └── package.json
|
||||
│
|
||||
└── backend/ # 后端项目
|
||||
├── prisma/ # Prisma 配置
|
||||
│ └── schema.prisma
|
||||
├── src/
|
||||
│ ├── auth/ # 认证模块
|
||||
│ ├── users/ # 用户管理
|
||||
│ ├── roles/ # 角色管理
|
||||
│ ├── menus/ # 菜单管理
|
||||
│ ├── dict/ # 数据字典
|
||||
│ ├── config/ # 系统配置
|
||||
│ ├── logs/ # 日志记录
|
||||
│ └── prisma/ # Prisma 服务
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Node.js >= 18.0.0
|
||||
- pnpm >= 8.0.0
|
||||
- MySQL >= 8.0
|
||||
|
||||
### 安装 pnpm
|
||||
|
||||
如果还没有安装 pnpm,可以通过以下方式安装:
|
||||
|
||||
```bash
|
||||
# 使用 npm 安装
|
||||
npm install -g pnpm
|
||||
|
||||
# 或使用 corepack(Node.js 16.13+)
|
||||
corepack enable
|
||||
corepack prepare pnpm@latest --activate
|
||||
```
|
||||
|
||||
### 快速安装(推荐)
|
||||
|
||||
在项目根目录执行:
|
||||
|
||||
```bash
|
||||
# 安装所有依赖(前端 + 后端)
|
||||
pnpm install
|
||||
|
||||
# 或分别安装
|
||||
pnpm --filter frontend install
|
||||
pnpm --filter backend install
|
||||
```
|
||||
|
||||
### 后端设置
|
||||
|
||||
1. 进入后端目录:
|
||||
```bash
|
||||
cd backend
|
||||
```
|
||||
|
||||
2. 安装依赖(如果未在根目录安装):
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
3. 配置环境变量,创建 `.env` 文件:
|
||||
```env
|
||||
DATABASE_URL="mysql://user:password@localhost:3306/competition_management?schema=public"
|
||||
JWT_SECRET="your-secret-key-change-in-production"
|
||||
PORT=3001
|
||||
```
|
||||
|
||||
4. 初始化数据库:
|
||||
```bash
|
||||
# 生成 Prisma Client
|
||||
pnpm prisma:generate
|
||||
|
||||
# 运行数据库迁移
|
||||
pnpm prisma:migrate
|
||||
```
|
||||
|
||||
5. 启动开发服务器:
|
||||
```bash
|
||||
# 方式1:在后端目录
|
||||
pnpm start:dev
|
||||
|
||||
# 方式2:在根目录
|
||||
pnpm dev:backend
|
||||
```
|
||||
|
||||
后端服务将在 `http://localhost:3001` 启动。
|
||||
|
||||
### 前端设置
|
||||
|
||||
1. 进入前端目录:
|
||||
```bash
|
||||
cd frontend
|
||||
```
|
||||
|
||||
2. 安装依赖(如果未在根目录安装):
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
3. 启动开发服务器:
|
||||
```bash
|
||||
# 方式1:在前端目录
|
||||
pnpm dev
|
||||
|
||||
# 方式2:在根目录
|
||||
pnpm dev:frontend
|
||||
```
|
||||
|
||||
前端应用将在 `http://localhost:3000` 启动。
|
||||
|
||||
### 同时启动前后端
|
||||
|
||||
在项目根目录执行:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
这将同时启动前端和后端开发服务器。
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 1. 用户管理
|
||||
- 用户列表查询(分页)
|
||||
- 用户创建、编辑、删除
|
||||
- 用户角色分配
|
||||
|
||||
### 2. 角色权限 (RBAC)
|
||||
- 角色管理(创建、编辑、删除)
|
||||
- 权限分配
|
||||
- 基于角色的访问控制
|
||||
|
||||
### 3. 菜单管理
|
||||
- 菜单树形结构管理
|
||||
- 菜单权限配置
|
||||
- 动态路由生成
|
||||
|
||||
### 4. 数据字典
|
||||
- 字典类型管理
|
||||
- 字典项管理
|
||||
- 字典数据查询
|
||||
|
||||
### 5. 系统配置
|
||||
- 系统参数配置
|
||||
- 配置项管理
|
||||
|
||||
### 6. 日志记录
|
||||
- 操作日志记录
|
||||
- 日志查询和统计
|
||||
|
||||
## API 文档
|
||||
|
||||
### 认证接口
|
||||
|
||||
- `POST /api/auth/login` - 用户登录
|
||||
- `GET /api/auth/user-info` - 获取当前用户信息
|
||||
- `POST /api/auth/logout` - 用户登出
|
||||
|
||||
### 用户管理
|
||||
|
||||
- `GET /api/users` - 获取用户列表
|
||||
- `GET /api/users/:id` - 获取用户详情
|
||||
- `POST /api/users` - 创建用户
|
||||
- `PATCH /api/users/:id` - 更新用户
|
||||
- `DELETE /api/users/:id` - 删除用户
|
||||
|
||||
### 角色管理
|
||||
|
||||
- `GET /api/roles` - 获取角色列表
|
||||
- `GET /api/roles/:id` - 获取角色详情
|
||||
- `POST /api/roles` - 创建角色
|
||||
- `PATCH /api/roles/:id` - 更新角色
|
||||
- `DELETE /api/roles/:id` - 删除角色
|
||||
|
||||
### 菜单管理
|
||||
|
||||
- `GET /api/menus` - 获取菜单列表(树形结构)
|
||||
- `GET /api/menus/:id` - 获取菜单详情
|
||||
- `POST /api/menus` - 创建菜单
|
||||
- `PATCH /api/menus/:id` - 更新菜单
|
||||
- `DELETE /api/menus/:id` - 删除菜单
|
||||
|
||||
### 数据字典
|
||||
|
||||
- `GET /api/dict` - 获取字典列表
|
||||
- `GET /api/dict/code/:code` - 根据代码获取字典
|
||||
- `GET /api/dict/:id` - 获取字典详情
|
||||
- `POST /api/dict` - 创建字典
|
||||
- `PATCH /api/dict/:id` - 更新字典
|
||||
- `DELETE /api/dict/:id` - 删除字典
|
||||
|
||||
### 系统配置
|
||||
|
||||
- `GET /api/config` - 获取配置列表
|
||||
- `GET /api/config/key/:key` - 根据键获取配置
|
||||
- `GET /api/config/:id` - 获取配置详情
|
||||
- `POST /api/config` - 创建配置
|
||||
- `PATCH /api/config/:id` - 更新配置
|
||||
- `DELETE /api/config/:id` - 删除配置
|
||||
|
||||
### 日志记录
|
||||
|
||||
- `GET /api/logs` - 获取日志列表
|
||||
- `GET /api/logs/:id` - 获取日志详情
|
||||
- `POST /api/logs` - 创建日志
|
||||
|
||||
## 开发规范
|
||||
|
||||
### 代码风格
|
||||
- 使用 ESLint 和 Prettier 进行代码格式化
|
||||
- 遵循 TypeScript 严格模式
|
||||
- 使用语义化的提交信息
|
||||
|
||||
### 提交规范
|
||||
- `feat`: 新功能
|
||||
- `fix`: 修复 bug
|
||||
- `docs`: 文档更新
|
||||
- `style`: 代码格式调整
|
||||
- `refactor`: 代码重构
|
||||
- `test`: 测试相关
|
||||
- `chore`: 构建/工具相关
|
||||
|
||||
## 部署
|
||||
|
||||
### 前端构建
|
||||
```bash
|
||||
# 方式1:在前端目录
|
||||
cd frontend
|
||||
pnpm build
|
||||
|
||||
# 方式2:在根目录
|
||||
pnpm build:frontend
|
||||
```
|
||||
|
||||
构建产物在 `frontend/dist` 目录。
|
||||
|
||||
### 后端构建
|
||||
```bash
|
||||
# 方式1:在后端目录
|
||||
cd backend
|
||||
pnpm build
|
||||
pnpm start:prod
|
||||
|
||||
# 方式2:在根目录
|
||||
pnpm build:backend
|
||||
cd backend
|
||||
pnpm start:prod
|
||||
```
|
||||
|
||||
### 同时构建前后端
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
图书馆绘本创作活动 - 幼儿绘本创作与展示平台
|
||||
122
backend/.cursor/rules/backend-specific.mdc
Normal file
122
backend/.cursor/rules/backend-specific.mdc
Normal file
@ -0,0 +1,122 @@
|
||||
---
|
||||
description: 后端特定的开发规范(仅作用于 backend 目录)
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 后端特定规范
|
||||
|
||||
本规则仅作用于 `backend/` 目录。
|
||||
|
||||
## NestJS 最佳实践
|
||||
|
||||
### 依赖注入
|
||||
|
||||
始终使用构造函数注入:
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly otherService: OtherService,
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
### 全局模块
|
||||
|
||||
PrismaModule 已设为全局模块,无需在每个模块中导入。
|
||||
|
||||
### 环境变量
|
||||
|
||||
使用 `@nestjs/config` 的 ConfigService:
|
||||
|
||||
```typescript
|
||||
constructor(private configService: ConfigService) {
|
||||
const jwtSecret = this.configService.get<string>('JWT_SECRET');
|
||||
}
|
||||
```
|
||||
|
||||
## 测试规范
|
||||
|
||||
### 单元测试
|
||||
|
||||
```typescript
|
||||
describe('UsersService', () => {
|
||||
let service: UsersService;
|
||||
let prisma: PrismaService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
providers: [
|
||||
UsersService,
|
||||
{
|
||||
provide: PrismaService,
|
||||
useValue: mockPrismaService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<UsersService>(UsersService);
|
||||
prisma = module.get<PrismaService>(PrismaService);
|
||||
});
|
||||
|
||||
it('should create a user', async () => {
|
||||
const dto = { username: 'test', password: 'pass123' };
|
||||
const result = await service.create(dto, 1);
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 日志记录
|
||||
|
||||
使用 NestJS Logger:
|
||||
|
||||
```typescript
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
export class MyService {
|
||||
private readonly logger = new Logger(MyService.name);
|
||||
|
||||
async someMethod() {
|
||||
this.logger.log('执行某操作');
|
||||
this.logger.warn('警告信息');
|
||||
this.logger.error('错误信息', error.stack);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 常用脚本
|
||||
|
||||
```bash
|
||||
# 开发
|
||||
pnpm start:dev
|
||||
|
||||
# 数据库迁移
|
||||
pnpm prisma:migrate:dev
|
||||
|
||||
# 初始化管理员
|
||||
pnpm init:admin
|
||||
|
||||
# 初始化菜单
|
||||
pnpm init:menus
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── auth/ # 认证模块
|
||||
├── users/ # 用户管理
|
||||
├── roles/ # 角色管理
|
||||
├── permissions/ # 权限管理
|
||||
├── menus/ # 菜单管理
|
||||
├── tenants/ # 租户管理
|
||||
├── school/ # 学校管理
|
||||
├── contests/ # 竞赛管理
|
||||
├── common/ # 公共模块
|
||||
├── prisma/ # Prisma 服务
|
||||
└── main.ts # 入口文件
|
||||
```
|
||||
26
backend/.eslintrc.js
Normal file
26
backend/.eslintrc.js
Normal file
@ -0,0 +1,26 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
};
|
||||
|
||||
47
backend/.gitignore
vendored
Normal file
47
backend/.gitignore
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
# compiled output
|
||||
/dist
|
||||
/node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Tests
|
||||
/coverage
|
||||
/.nyc_output
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
.env.development
|
||||
.env.production
|
||||
.env.test
|
||||
.env.staging
|
||||
# 保留示例文件
|
||||
!.env*.example
|
||||
|
||||
4
backend/.npmrc
Normal file
4
backend/.npmrc
Normal file
@ -0,0 +1,4 @@
|
||||
# 后端 pnpm 配置
|
||||
shamefully-hoist=true
|
||||
strict-peer-dependencies=false
|
||||
|
||||
5
backend/.prettierrc
Normal file
5
backend/.prettierrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
|
||||
300
backend/data/menus.json
Normal file
300
backend/data/menus.json
Normal file
@ -0,0 +1,300 @@
|
||||
[
|
||||
{
|
||||
"name": "赛事活动",
|
||||
"path": "/activities",
|
||||
"icon": "FlagOutlined",
|
||||
"component": null,
|
||||
"parentId": null,
|
||||
"sort": 1,
|
||||
"permission": "activity:read",
|
||||
"children": [
|
||||
{
|
||||
"name": "活动列表",
|
||||
"path": "/activities",
|
||||
"icon": "UnorderedListOutlined",
|
||||
"component": "contests/Activities",
|
||||
"sort": 1,
|
||||
"permission": "activity:read"
|
||||
},
|
||||
{
|
||||
"name": "我的报名",
|
||||
"path": "/activities/registrations",
|
||||
"icon": "UserAddOutlined",
|
||||
"component": "contests/registrations/Index",
|
||||
"sort": 2,
|
||||
"permission": "registration:create"
|
||||
},
|
||||
{
|
||||
"name": "我的作品",
|
||||
"path": "/activities/works",
|
||||
"icon": "FileTextOutlined",
|
||||
"component": "contests/works/Index",
|
||||
"sort": 3,
|
||||
"permission": "work:create"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "学校管理",
|
||||
"path": "/school",
|
||||
"icon": "BankOutlined",
|
||||
"component": null,
|
||||
"parentId": null,
|
||||
"sort": 2,
|
||||
"permission": "school:read",
|
||||
"children": [
|
||||
{
|
||||
"name": "学校信息",
|
||||
"path": "/school/schools",
|
||||
"icon": "BankOutlined",
|
||||
"component": "school/schools/Index",
|
||||
"sort": 1,
|
||||
"permission": "school:read"
|
||||
},
|
||||
{
|
||||
"name": "部门管理",
|
||||
"path": "/school/departments",
|
||||
"icon": "ApartmentOutlined",
|
||||
"component": "school/departments/Index",
|
||||
"sort": 2,
|
||||
"permission": "department:read"
|
||||
},
|
||||
{
|
||||
"name": "年级管理",
|
||||
"path": "/school/grades",
|
||||
"icon": "AppstoreOutlined",
|
||||
"component": "school/grades/Index",
|
||||
"sort": 3,
|
||||
"permission": "grade:read"
|
||||
},
|
||||
{
|
||||
"name": "班级管理",
|
||||
"path": "/school/classes",
|
||||
"icon": "TeamOutlined",
|
||||
"component": "school/classes/Index",
|
||||
"sort": 4,
|
||||
"permission": "class:read"
|
||||
},
|
||||
{
|
||||
"name": "教师管理",
|
||||
"path": "/school/teachers",
|
||||
"icon": "UserOutlined",
|
||||
"component": "school/teachers/Index",
|
||||
"sort": 5,
|
||||
"permission": "teacher:read"
|
||||
},
|
||||
{
|
||||
"name": "学生管理",
|
||||
"path": "/school/students",
|
||||
"icon": "UsergroupAddOutlined",
|
||||
"component": "school/students/Index",
|
||||
"sort": 6,
|
||||
"permission": "student:read"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "赛事管理",
|
||||
"path": "/contests",
|
||||
"icon": "TrophyOutlined",
|
||||
"component": null,
|
||||
"parentId": null,
|
||||
"sort": 3,
|
||||
"permission": "contest:create",
|
||||
"children": [
|
||||
{
|
||||
"name": "赛事列表",
|
||||
"path": "/contests",
|
||||
"icon": "UnorderedListOutlined",
|
||||
"component": "contests/Index",
|
||||
"sort": 1,
|
||||
"permission": "contest:create"
|
||||
},
|
||||
{
|
||||
"name": "评委管理",
|
||||
"path": "/contests/judges",
|
||||
"icon": "SolutionOutlined",
|
||||
"component": "contests/judges/Index",
|
||||
"sort": 2,
|
||||
"permission": "judge:read"
|
||||
},
|
||||
{
|
||||
"name": "报名管理",
|
||||
"path": "/contests/registrations",
|
||||
"icon": "UserAddOutlined",
|
||||
"component": "contests/registrations/Index",
|
||||
"sort": 3,
|
||||
"permission": "registration:approve"
|
||||
},
|
||||
{
|
||||
"name": "作品管理",
|
||||
"path": "/contests/works",
|
||||
"icon": "FileTextOutlined",
|
||||
"component": "contests/works/Index",
|
||||
"sort": 4,
|
||||
"permission": "contest:read"
|
||||
},
|
||||
{
|
||||
"name": "评审进度",
|
||||
"path": "/contests/review-progress",
|
||||
"icon": "AuditOutlined",
|
||||
"component": "contests/reviews/Progress",
|
||||
"sort": 5,
|
||||
"permission": "review-rule:read"
|
||||
},
|
||||
{
|
||||
"name": "评审规则",
|
||||
"path": "/contests/reviews",
|
||||
"icon": "CheckCircleOutlined",
|
||||
"component": "contests/reviews/Index",
|
||||
"sort": 6,
|
||||
"permission": "review-rule:read"
|
||||
},
|
||||
{
|
||||
"name": "赛果发布",
|
||||
"path": "/contests/results",
|
||||
"icon": "TrophyOutlined",
|
||||
"component": "contests/results/Index",
|
||||
"sort": 7,
|
||||
"permission": "contest:create"
|
||||
},
|
||||
{
|
||||
"name": "通知管理",
|
||||
"path": "/contests/notices",
|
||||
"icon": "BellOutlined",
|
||||
"component": "contests/notices/Index",
|
||||
"sort": 8,
|
||||
"permission": "notice:create"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "作业管理",
|
||||
"path": "/homework",
|
||||
"icon": "FormOutlined",
|
||||
"component": null,
|
||||
"parentId": null,
|
||||
"sort": 4,
|
||||
"permission": "homework:read",
|
||||
"children": [
|
||||
{
|
||||
"name": "作业列表",
|
||||
"path": "/homework",
|
||||
"icon": "FileTextOutlined",
|
||||
"component": "homework/Index",
|
||||
"sort": 1,
|
||||
"permission": "homework:create"
|
||||
},
|
||||
{
|
||||
"name": "评审规则",
|
||||
"path": "/homework/review-rules",
|
||||
"icon": "CheckCircleOutlined",
|
||||
"component": "homework/ReviewRules",
|
||||
"sort": 2,
|
||||
"permission": "homework-review-rule:read"
|
||||
},
|
||||
{
|
||||
"name": "我的作业",
|
||||
"path": "/homework/my",
|
||||
"icon": "BookOutlined",
|
||||
"component": "homework/StudentList",
|
||||
"sort": 3,
|
||||
"permission": "homework-submission:create"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "系统管理",
|
||||
"path": "/system",
|
||||
"icon": "SettingOutlined",
|
||||
"component": null,
|
||||
"parentId": null,
|
||||
"sort": 9,
|
||||
"permission": "user:read",
|
||||
"children": [
|
||||
{
|
||||
"name": "用户管理",
|
||||
"path": "/system/users",
|
||||
"icon": "UserOutlined",
|
||||
"component": "system/users/Index",
|
||||
"sort": 1,
|
||||
"permission": "user:read"
|
||||
},
|
||||
{
|
||||
"name": "角色管理",
|
||||
"path": "/system/roles",
|
||||
"icon": "TeamOutlined",
|
||||
"component": "system/roles/Index",
|
||||
"sort": 2,
|
||||
"permission": "role:read"
|
||||
},
|
||||
{
|
||||
"name": "菜单管理",
|
||||
"path": "/system/menus",
|
||||
"icon": "MenuOutlined",
|
||||
"component": "system/menus/Index",
|
||||
"sort": 3,
|
||||
"permission": "menu:read"
|
||||
},
|
||||
{
|
||||
"name": "数据字典",
|
||||
"path": "/system/dict",
|
||||
"icon": "BookOutlined",
|
||||
"component": "system/dict/Index",
|
||||
"sort": 4,
|
||||
"permission": "dict:read"
|
||||
},
|
||||
{
|
||||
"name": "系统配置",
|
||||
"path": "/system/config",
|
||||
"icon": "ToolOutlined",
|
||||
"component": "system/config/Index",
|
||||
"sort": 5,
|
||||
"permission": "config:read"
|
||||
},
|
||||
{
|
||||
"name": "日志记录",
|
||||
"path": "/system/logs",
|
||||
"icon": "FileTextOutlined",
|
||||
"component": "system/logs/Index",
|
||||
"sort": 6,
|
||||
"permission": "log:read"
|
||||
},
|
||||
{
|
||||
"name": "权限管理",
|
||||
"path": "/system/permissions",
|
||||
"icon": "SafetyOutlined",
|
||||
"component": "system/permissions/Index",
|
||||
"sort": 7,
|
||||
"permission": "permission:read"
|
||||
},
|
||||
{
|
||||
"name": "租户管理",
|
||||
"path": "/system/tenants",
|
||||
"icon": "TeamOutlined",
|
||||
"component": "system/tenants/Index",
|
||||
"sort": 8,
|
||||
"permission": "tenant:read"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "工作台",
|
||||
"path": "/workbench",
|
||||
"icon": "DashboardOutlined",
|
||||
"component": null,
|
||||
"parentId": null,
|
||||
"sort": 10,
|
||||
"permission": "ai-3d:read",
|
||||
"children": [
|
||||
{
|
||||
"name": "3D建模实验室",
|
||||
"path": "/workbench/3d-lab",
|
||||
"icon": "ExperimentOutlined",
|
||||
"component": "workbench/ai-3d/Index",
|
||||
"sort": 1,
|
||||
"permission": "ai-3d:read"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
737
backend/data/permissions.json
Normal file
737
backend/data/permissions.json
Normal file
@ -0,0 +1,737 @@
|
||||
[
|
||||
{
|
||||
"code": "ai-3d:read",
|
||||
"resource": "ai-3d",
|
||||
"action": "read",
|
||||
"name": "使用3D建模实验室",
|
||||
"description": "允许使用AI 3D建模实验室"
|
||||
},
|
||||
{
|
||||
"code": "ai-3d:create",
|
||||
"resource": "ai-3d",
|
||||
"action": "create",
|
||||
"name": "创建3D模型任务",
|
||||
"description": "允许创建AI 3D模型生成任务"
|
||||
},
|
||||
{
|
||||
"code": "user:create",
|
||||
"resource": "user",
|
||||
"action": "create",
|
||||
"name": "创建用户",
|
||||
"description": "允许创建新用户"
|
||||
},
|
||||
{
|
||||
"code": "user:read",
|
||||
"resource": "user",
|
||||
"action": "read",
|
||||
"name": "查看用户",
|
||||
"description": "允许查看用户列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "user:update",
|
||||
"resource": "user",
|
||||
"action": "update",
|
||||
"name": "更新用户",
|
||||
"description": "允许更新用户信息"
|
||||
},
|
||||
{
|
||||
"code": "user:delete",
|
||||
"resource": "user",
|
||||
"action": "delete",
|
||||
"name": "删除用户",
|
||||
"description": "允许删除用户"
|
||||
},
|
||||
{
|
||||
"code": "user:password:update",
|
||||
"resource": "user",
|
||||
"action": "password:update",
|
||||
"name": "修改用户密码",
|
||||
"description": "允许修改用户密码"
|
||||
},
|
||||
{
|
||||
"code": "role:create",
|
||||
"resource": "role",
|
||||
"action": "create",
|
||||
"name": "创建角色",
|
||||
"description": "允许创建新角色"
|
||||
},
|
||||
{
|
||||
"code": "role:read",
|
||||
"resource": "role",
|
||||
"action": "read",
|
||||
"name": "查看角色",
|
||||
"description": "允许查看角色列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "role:update",
|
||||
"resource": "role",
|
||||
"action": "update",
|
||||
"name": "更新角色",
|
||||
"description": "允许更新角色信息"
|
||||
},
|
||||
{
|
||||
"code": "role:delete",
|
||||
"resource": "role",
|
||||
"action": "delete",
|
||||
"name": "删除角色",
|
||||
"description": "允许删除角色"
|
||||
},
|
||||
{
|
||||
"code": "role:assign",
|
||||
"resource": "role",
|
||||
"action": "assign",
|
||||
"name": "分配角色",
|
||||
"description": "允许给用户分配角色"
|
||||
},
|
||||
{
|
||||
"code": "permission:create",
|
||||
"resource": "permission",
|
||||
"action": "create",
|
||||
"name": "创建权限",
|
||||
"description": "允许创建新权限"
|
||||
},
|
||||
{
|
||||
"code": "permission:read",
|
||||
"resource": "permission",
|
||||
"action": "read",
|
||||
"name": "查看权限",
|
||||
"description": "允许查看权限列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "permission:update",
|
||||
"resource": "permission",
|
||||
"action": "update",
|
||||
"name": "更新权限",
|
||||
"description": "允许更新权限信息"
|
||||
},
|
||||
{
|
||||
"code": "permission:delete",
|
||||
"resource": "permission",
|
||||
"action": "delete",
|
||||
"name": "删除权限",
|
||||
"description": "允许删除权限"
|
||||
},
|
||||
{
|
||||
"code": "menu:create",
|
||||
"resource": "menu",
|
||||
"action": "create",
|
||||
"name": "创建菜单",
|
||||
"description": "允许创建新菜单"
|
||||
},
|
||||
{
|
||||
"code": "menu:read",
|
||||
"resource": "menu",
|
||||
"action": "read",
|
||||
"name": "查看菜单",
|
||||
"description": "允许查看菜单列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "menu:update",
|
||||
"resource": "menu",
|
||||
"action": "update",
|
||||
"name": "更新菜单",
|
||||
"description": "允许更新菜单信息"
|
||||
},
|
||||
{
|
||||
"code": "menu:delete",
|
||||
"resource": "menu",
|
||||
"action": "delete",
|
||||
"name": "删除菜单",
|
||||
"description": "允许删除菜单"
|
||||
},
|
||||
{
|
||||
"code": "tenant:create",
|
||||
"resource": "tenant",
|
||||
"action": "create",
|
||||
"name": "创建租户",
|
||||
"description": "允许创建租户"
|
||||
},
|
||||
{
|
||||
"code": "tenant:read",
|
||||
"resource": "tenant",
|
||||
"action": "read",
|
||||
"name": "查看租户",
|
||||
"description": "允许查看租户列表"
|
||||
},
|
||||
{
|
||||
"code": "tenant:update",
|
||||
"resource": "tenant",
|
||||
"action": "update",
|
||||
"name": "更新租户",
|
||||
"description": "允许更新租户信息"
|
||||
},
|
||||
{
|
||||
"code": "tenant:delete",
|
||||
"resource": "tenant",
|
||||
"action": "delete",
|
||||
"name": "删除租户",
|
||||
"description": "允许删除租户"
|
||||
},
|
||||
{
|
||||
"code": "school:create",
|
||||
"resource": "school",
|
||||
"action": "create",
|
||||
"name": "创建学校",
|
||||
"description": "允许创建学校信息"
|
||||
},
|
||||
{
|
||||
"code": "school:read",
|
||||
"resource": "school",
|
||||
"action": "read",
|
||||
"name": "查看学校",
|
||||
"description": "允许查看学校信息"
|
||||
},
|
||||
{
|
||||
"code": "school:update",
|
||||
"resource": "school",
|
||||
"action": "update",
|
||||
"name": "更新学校",
|
||||
"description": "允许更新学校信息"
|
||||
},
|
||||
{
|
||||
"code": "school:delete",
|
||||
"resource": "school",
|
||||
"action": "delete",
|
||||
"name": "删除学校",
|
||||
"description": "允许删除学校信息"
|
||||
},
|
||||
{
|
||||
"code": "department:create",
|
||||
"resource": "department",
|
||||
"action": "create",
|
||||
"name": "创建部门",
|
||||
"description": "允许创建部门"
|
||||
},
|
||||
{
|
||||
"code": "department:read",
|
||||
"resource": "department",
|
||||
"action": "read",
|
||||
"name": "查看部门",
|
||||
"description": "允许查看部门列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "department:update",
|
||||
"resource": "department",
|
||||
"action": "update",
|
||||
"name": "更新部门",
|
||||
"description": "允许更新部门信息"
|
||||
},
|
||||
{
|
||||
"code": "department:delete",
|
||||
"resource": "department",
|
||||
"action": "delete",
|
||||
"name": "删除部门",
|
||||
"description": "允许删除部门"
|
||||
},
|
||||
{
|
||||
"code": "grade:create",
|
||||
"resource": "grade",
|
||||
"action": "create",
|
||||
"name": "创建年级",
|
||||
"description": "允许创建年级"
|
||||
},
|
||||
{
|
||||
"code": "grade:read",
|
||||
"resource": "grade",
|
||||
"action": "read",
|
||||
"name": "查看年级",
|
||||
"description": "允许查看年级列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "grade:update",
|
||||
"resource": "grade",
|
||||
"action": "update",
|
||||
"name": "更新年级",
|
||||
"description": "允许更新年级信息"
|
||||
},
|
||||
{
|
||||
"code": "grade:delete",
|
||||
"resource": "grade",
|
||||
"action": "delete",
|
||||
"name": "删除年级",
|
||||
"description": "允许删除年级"
|
||||
},
|
||||
{
|
||||
"code": "class:create",
|
||||
"resource": "class",
|
||||
"action": "create",
|
||||
"name": "创建班级",
|
||||
"description": "允许创建班级"
|
||||
},
|
||||
{
|
||||
"code": "class:read",
|
||||
"resource": "class",
|
||||
"action": "read",
|
||||
"name": "查看班级",
|
||||
"description": "允许查看班级列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "class:update",
|
||||
"resource": "class",
|
||||
"action": "update",
|
||||
"name": "更新班级",
|
||||
"description": "允许更新班级信息"
|
||||
},
|
||||
{
|
||||
"code": "class:delete",
|
||||
"resource": "class",
|
||||
"action": "delete",
|
||||
"name": "删除班级",
|
||||
"description": "允许删除班级"
|
||||
},
|
||||
{
|
||||
"code": "teacher:create",
|
||||
"resource": "teacher",
|
||||
"action": "create",
|
||||
"name": "创建教师",
|
||||
"description": "允许创建教师"
|
||||
},
|
||||
{
|
||||
"code": "teacher:read",
|
||||
"resource": "teacher",
|
||||
"action": "read",
|
||||
"name": "查看教师",
|
||||
"description": "允许查看教师列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "teacher:update",
|
||||
"resource": "teacher",
|
||||
"action": "update",
|
||||
"name": "更新教师",
|
||||
"description": "允许更新教师信息"
|
||||
},
|
||||
{
|
||||
"code": "teacher:delete",
|
||||
"resource": "teacher",
|
||||
"action": "delete",
|
||||
"name": "删除教师",
|
||||
"description": "允许删除教师"
|
||||
},
|
||||
{
|
||||
"code": "student:create",
|
||||
"resource": "student",
|
||||
"action": "create",
|
||||
"name": "创建学生",
|
||||
"description": "允许创建学生"
|
||||
},
|
||||
{
|
||||
"code": "student:read",
|
||||
"resource": "student",
|
||||
"action": "read",
|
||||
"name": "查看学生",
|
||||
"description": "允许查看学生列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "student:update",
|
||||
"resource": "student",
|
||||
"action": "update",
|
||||
"name": "更新学生",
|
||||
"description": "允许更新学生信息"
|
||||
},
|
||||
{
|
||||
"code": "student:delete",
|
||||
"resource": "student",
|
||||
"action": "delete",
|
||||
"name": "删除学生",
|
||||
"description": "允许删除学生"
|
||||
},
|
||||
{
|
||||
"code": "contest:create",
|
||||
"resource": "contest",
|
||||
"action": "create",
|
||||
"name": "创建赛事",
|
||||
"description": "允许创建赛事"
|
||||
},
|
||||
{
|
||||
"code": "contest:read",
|
||||
"resource": "contest",
|
||||
"action": "read",
|
||||
"name": "查看赛事",
|
||||
"description": "允许查看赛事列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "contest:update",
|
||||
"resource": "contest",
|
||||
"action": "update",
|
||||
"name": "更新赛事",
|
||||
"description": "允许更新赛事信息"
|
||||
},
|
||||
{
|
||||
"code": "contest:delete",
|
||||
"resource": "contest",
|
||||
"action": "delete",
|
||||
"name": "删除赛事",
|
||||
"description": "允许删除赛事"
|
||||
},
|
||||
{
|
||||
"code": "contest:publish",
|
||||
"resource": "contest",
|
||||
"action": "publish",
|
||||
"name": "发布赛事",
|
||||
"description": "允许发布/取消发布赛事"
|
||||
},
|
||||
{
|
||||
"code": "contest:finish",
|
||||
"resource": "contest",
|
||||
"action": "finish",
|
||||
"name": "结束赛事",
|
||||
"description": "允许结束赛事"
|
||||
},
|
||||
{
|
||||
"code": "review-rule:create",
|
||||
"resource": "review-rule",
|
||||
"action": "create",
|
||||
"name": "创建评审规则",
|
||||
"description": "允许创建评审规则"
|
||||
},
|
||||
{
|
||||
"code": "review-rule:read",
|
||||
"resource": "review-rule",
|
||||
"action": "read",
|
||||
"name": "查看评审规则",
|
||||
"description": "允许查看评审规则"
|
||||
},
|
||||
{
|
||||
"code": "review-rule:update",
|
||||
"resource": "review-rule",
|
||||
"action": "update",
|
||||
"name": "更新评审规则",
|
||||
"description": "允许更新评审规则"
|
||||
},
|
||||
{
|
||||
"code": "review-rule:delete",
|
||||
"resource": "review-rule",
|
||||
"action": "delete",
|
||||
"name": "删除评审规则",
|
||||
"description": "允许删除评审规则"
|
||||
},
|
||||
{
|
||||
"code": "judge:create",
|
||||
"resource": "judge",
|
||||
"action": "create",
|
||||
"name": "添加评委",
|
||||
"description": "允许添加评委"
|
||||
},
|
||||
{
|
||||
"code": "judge:read",
|
||||
"resource": "judge",
|
||||
"action": "read",
|
||||
"name": "查看评委",
|
||||
"description": "允许查看评委列表"
|
||||
},
|
||||
{
|
||||
"code": "judge:update",
|
||||
"resource": "judge",
|
||||
"action": "update",
|
||||
"name": "更新评委",
|
||||
"description": "允许更新评委信息"
|
||||
},
|
||||
{
|
||||
"code": "judge:delete",
|
||||
"resource": "judge",
|
||||
"action": "delete",
|
||||
"name": "删除评委",
|
||||
"description": "允许删除评委"
|
||||
},
|
||||
{
|
||||
"code": "judge:assign",
|
||||
"resource": "judge",
|
||||
"action": "assign",
|
||||
"name": "分配评委",
|
||||
"description": "允许为赛事分配评委"
|
||||
},
|
||||
{
|
||||
"code": "registration:create",
|
||||
"resource": "registration",
|
||||
"action": "create",
|
||||
"name": "创建报名",
|
||||
"description": "允许报名赛事"
|
||||
},
|
||||
{
|
||||
"code": "registration:read",
|
||||
"resource": "registration",
|
||||
"action": "read",
|
||||
"name": "查看报名",
|
||||
"description": "允许查看报名记录"
|
||||
},
|
||||
{
|
||||
"code": "registration:update",
|
||||
"resource": "registration",
|
||||
"action": "update",
|
||||
"name": "更新报名",
|
||||
"description": "允许更新报名信息"
|
||||
},
|
||||
{
|
||||
"code": "registration:delete",
|
||||
"resource": "registration",
|
||||
"action": "delete",
|
||||
"name": "取消报名",
|
||||
"description": "允许取消报名"
|
||||
},
|
||||
{
|
||||
"code": "registration:approve",
|
||||
"resource": "registration",
|
||||
"action": "approve",
|
||||
"name": "审核报名",
|
||||
"description": "允许审核报名"
|
||||
},
|
||||
{
|
||||
"code": "work:create",
|
||||
"resource": "work",
|
||||
"action": "create",
|
||||
"name": "上传作品",
|
||||
"description": "允许上传参赛作品"
|
||||
},
|
||||
{
|
||||
"code": "work:read",
|
||||
"resource": "work",
|
||||
"action": "read",
|
||||
"name": "查看作品",
|
||||
"description": "允许查看参赛作品"
|
||||
},
|
||||
{
|
||||
"code": "work:update",
|
||||
"resource": "work",
|
||||
"action": "update",
|
||||
"name": "更新作品",
|
||||
"description": "允许更新作品信息"
|
||||
},
|
||||
{
|
||||
"code": "work:delete",
|
||||
"resource": "work",
|
||||
"action": "delete",
|
||||
"name": "删除作品",
|
||||
"description": "允许删除作品"
|
||||
},
|
||||
{
|
||||
"code": "work:submit",
|
||||
"resource": "work",
|
||||
"action": "submit",
|
||||
"name": "提交作品",
|
||||
"description": "允许提交作品"
|
||||
},
|
||||
{
|
||||
"code": "review:read",
|
||||
"resource": "review",
|
||||
"action": "read",
|
||||
"name": "查看评审任务",
|
||||
"description": "允许查看待评审作品"
|
||||
},
|
||||
{
|
||||
"code": "review:score",
|
||||
"resource": "review",
|
||||
"action": "score",
|
||||
"name": "评审打分",
|
||||
"description": "允许对作品打分"
|
||||
},
|
||||
{
|
||||
"code": "notice:create",
|
||||
"resource": "notice",
|
||||
"action": "create",
|
||||
"name": "创建公告",
|
||||
"description": "允许创建赛事公告"
|
||||
},
|
||||
{
|
||||
"code": "notice:read",
|
||||
"resource": "notice",
|
||||
"action": "read",
|
||||
"name": "查看公告",
|
||||
"description": "允许查看赛事公告"
|
||||
},
|
||||
{
|
||||
"code": "notice:update",
|
||||
"resource": "notice",
|
||||
"action": "update",
|
||||
"name": "更新公告",
|
||||
"description": "允许更新公告信息"
|
||||
},
|
||||
{
|
||||
"code": "notice:delete",
|
||||
"resource": "notice",
|
||||
"action": "delete",
|
||||
"name": "删除公告",
|
||||
"description": "允许删除公告"
|
||||
},
|
||||
{
|
||||
"code": "homework:create",
|
||||
"resource": "homework",
|
||||
"action": "create",
|
||||
"name": "创建作业",
|
||||
"description": "允许创建作业"
|
||||
},
|
||||
{
|
||||
"code": "homework:read",
|
||||
"resource": "homework",
|
||||
"action": "read",
|
||||
"name": "查看作业",
|
||||
"description": "允许查看作业列表"
|
||||
},
|
||||
{
|
||||
"code": "homework:update",
|
||||
"resource": "homework",
|
||||
"action": "update",
|
||||
"name": "更新作业",
|
||||
"description": "允许更新作业信息"
|
||||
},
|
||||
{
|
||||
"code": "homework:delete",
|
||||
"resource": "homework",
|
||||
"action": "delete",
|
||||
"name": "删除作业",
|
||||
"description": "允许删除作业"
|
||||
},
|
||||
{
|
||||
"code": "homework:publish",
|
||||
"resource": "homework",
|
||||
"action": "publish",
|
||||
"name": "发布作业",
|
||||
"description": "允许发布作业"
|
||||
},
|
||||
{
|
||||
"code": "homework-submission:create",
|
||||
"resource": "homework-submission",
|
||||
"action": "create",
|
||||
"name": "提交作业",
|
||||
"description": "允许提交作业"
|
||||
},
|
||||
{
|
||||
"code": "homework-submission:read",
|
||||
"resource": "homework-submission",
|
||||
"action": "read",
|
||||
"name": "查看作业提交",
|
||||
"description": "允许查看作业提交记录"
|
||||
},
|
||||
{
|
||||
"code": "homework-submission:update",
|
||||
"resource": "homework-submission",
|
||||
"action": "update",
|
||||
"name": "更新作业提交",
|
||||
"description": "允许更新提交的作业"
|
||||
},
|
||||
{
|
||||
"code": "homework-review-rule:create",
|
||||
"resource": "homework-review-rule",
|
||||
"action": "create",
|
||||
"name": "创建作业评审规则",
|
||||
"description": "允许创建作业评审规则"
|
||||
},
|
||||
{
|
||||
"code": "homework-review-rule:read",
|
||||
"resource": "homework-review-rule",
|
||||
"action": "read",
|
||||
"name": "查看作业评审规则",
|
||||
"description": "允许查看作业评审规则"
|
||||
},
|
||||
{
|
||||
"code": "homework-review-rule:update",
|
||||
"resource": "homework-review-rule",
|
||||
"action": "update",
|
||||
"name": "更新作业评审规则",
|
||||
"description": "允许更新作业评审规则"
|
||||
},
|
||||
{
|
||||
"code": "homework-review-rule:delete",
|
||||
"resource": "homework-review-rule",
|
||||
"action": "delete",
|
||||
"name": "删除作业评审规则",
|
||||
"description": "允许删除作业评审规则"
|
||||
},
|
||||
{
|
||||
"code": "homework-score:create",
|
||||
"resource": "homework-score",
|
||||
"action": "create",
|
||||
"name": "作业评分",
|
||||
"description": "允许对作业评分"
|
||||
},
|
||||
{
|
||||
"code": "homework-score:read",
|
||||
"resource": "homework-score",
|
||||
"action": "read",
|
||||
"name": "查看作业评分",
|
||||
"description": "允许查看作业评分"
|
||||
},
|
||||
{
|
||||
"code": "dict:create",
|
||||
"resource": "dict",
|
||||
"action": "create",
|
||||
"name": "创建字典",
|
||||
"description": "允许创建新字典"
|
||||
},
|
||||
{
|
||||
"code": "dict:read",
|
||||
"resource": "dict",
|
||||
"action": "read",
|
||||
"name": "查看字典",
|
||||
"description": "允许查看字典列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "dict:update",
|
||||
"resource": "dict",
|
||||
"action": "update",
|
||||
"name": "更新字典",
|
||||
"description": "允许更新字典信息"
|
||||
},
|
||||
{
|
||||
"code": "dict:delete",
|
||||
"resource": "dict",
|
||||
"action": "delete",
|
||||
"name": "删除字典",
|
||||
"description": "允许删除字典"
|
||||
},
|
||||
{
|
||||
"code": "config:create",
|
||||
"resource": "config",
|
||||
"action": "create",
|
||||
"name": "创建配置",
|
||||
"description": "允许创建新配置"
|
||||
},
|
||||
{
|
||||
"code": "config:read",
|
||||
"resource": "config",
|
||||
"action": "read",
|
||||
"name": "查看配置",
|
||||
"description": "允许查看配置列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "config:update",
|
||||
"resource": "config",
|
||||
"action": "update",
|
||||
"name": "更新配置",
|
||||
"description": "允许更新配置信息"
|
||||
},
|
||||
{
|
||||
"code": "config:delete",
|
||||
"resource": "config",
|
||||
"action": "delete",
|
||||
"name": "删除配置",
|
||||
"description": "允许删除配置"
|
||||
},
|
||||
{
|
||||
"code": "log:read",
|
||||
"resource": "log",
|
||||
"action": "read",
|
||||
"name": "查看日志",
|
||||
"description": "允许查看系统日志"
|
||||
},
|
||||
{
|
||||
"code": "log:delete",
|
||||
"resource": "log",
|
||||
"action": "delete",
|
||||
"name": "删除日志",
|
||||
"description": "允许删除系统日志"
|
||||
},
|
||||
{
|
||||
"code": "activity:read",
|
||||
"resource": "activity",
|
||||
"action": "read",
|
||||
"name": "查看赛事活动",
|
||||
"description": "允许查看已发布的赛事活动"
|
||||
},
|
||||
{
|
||||
"code": "activity:guidance",
|
||||
"resource": "activity",
|
||||
"action": "guidance",
|
||||
"name": "指导学生",
|
||||
"description": "允许指导学生参赛"
|
||||
}
|
||||
]
|
||||
184
backend/docs/ADMIN_ACCOUNT.md
Normal file
184
backend/docs/ADMIN_ACCOUNT.md
Normal file
@ -0,0 +1,184 @@
|
||||
# 超级管理员账号说明
|
||||
|
||||
## 📋 账号信息
|
||||
|
||||
### 登录凭据
|
||||
|
||||
- **用户名**: `admin`
|
||||
- **密码**: `cms@admin`
|
||||
- **昵称**: 超级管理员
|
||||
- **邮箱**: admin@example.com
|
||||
- **角色**: super_admin (超级管理员)
|
||||
|
||||
## 🔐 权限说明
|
||||
|
||||
超级管理员拥有系统所有权限,共 **27 个权限**:
|
||||
|
||||
### 用户管理权限
|
||||
|
||||
- `user:create` - 创建用户
|
||||
- `user:read` - 查看用户
|
||||
- `user:update` - 更新用户
|
||||
- `user:delete` - 删除用户
|
||||
|
||||
### 角色管理权限
|
||||
|
||||
- `role:create` - 创建角色
|
||||
- `role:read` - 查看角色
|
||||
- `role:update` - 更新角色
|
||||
- `role:delete` - 删除角色
|
||||
- `role:assign` - 分配角色
|
||||
|
||||
### 权限管理权限
|
||||
|
||||
- `permission:create` - 创建权限
|
||||
- `permission:read` - 查看权限
|
||||
- `permission:update` - 更新权限
|
||||
- `permission:delete` - 删除权限
|
||||
|
||||
### 菜单管理权限
|
||||
|
||||
- `menu:create` - 创建菜单
|
||||
- `menu:read` - 查看菜单
|
||||
- `menu:update` - 更新菜单
|
||||
- `menu:delete` - 删除菜单
|
||||
|
||||
### 数据字典权限
|
||||
|
||||
- `dict:create` - 创建字典
|
||||
- `dict:read` - 查看字典
|
||||
- `dict:update` - 更新字典
|
||||
- `dict:delete` - 删除字典
|
||||
|
||||
### 系统配置权限
|
||||
|
||||
- `config:create` - 创建配置
|
||||
- `config:read` - 查看配置
|
||||
- `config:update` - 更新配置
|
||||
- `config:delete` - 删除配置
|
||||
|
||||
### 日志管理权限
|
||||
|
||||
- `log:read` - 查看日志
|
||||
- `log:delete` - 删除日志
|
||||
|
||||
## 🚀 使用方法
|
||||
|
||||
### 1. 登录系统
|
||||
|
||||
使用以下 API 登录:
|
||||
|
||||
```bash
|
||||
POST /api/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "cms@admin"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "admin",
|
||||
"nickname": "超级管理员",
|
||||
"email": "admin@example.com",
|
||||
"avatar": null,
|
||||
"roles": ["super_admin"],
|
||||
"permissions": [
|
||||
"user:create",
|
||||
"user:read",
|
||||
"user:update",
|
||||
"user:delete"
|
||||
// ... 所有 27 个权限
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用 Token 访问 API
|
||||
|
||||
```bash
|
||||
GET /api/users
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
```
|
||||
|
||||
## 🔄 重新初始化
|
||||
|
||||
如果需要重新初始化超级管理员账号,可以运行:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
pnpm init:admin
|
||||
```
|
||||
|
||||
脚本会:
|
||||
|
||||
- ✅ 创建/更新所有基础权限(27个)
|
||||
- ✅ 创建/更新超级管理员角色
|
||||
- ✅ 创建/更新 admin 用户
|
||||
- ✅ 分配角色给用户
|
||||
|
||||
**注意**: 如果用户已存在,密码会被重置为 `cms@admin`
|
||||
|
||||
## 🔍 验证账号
|
||||
|
||||
验证超级管理员账号是否创建成功:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
node scripts/verify-admin.js
|
||||
```
|
||||
|
||||
## ⚠️ 安全建议
|
||||
|
||||
1. **首次登录后立即修改密码**
|
||||
2. **生产环境使用强密码**
|
||||
3. **定期更换密码**
|
||||
4. **不要将密码提交到版本控制**
|
||||
|
||||
## 📝 修改密码
|
||||
|
||||
可以通过以下方式修改密码:
|
||||
|
||||
### 方式一:通过 API
|
||||
|
||||
```bash
|
||||
PATCH /api/users/1
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"password": "new_strong_password"
|
||||
}
|
||||
```
|
||||
|
||||
### 方式二:通过数据库
|
||||
|
||||
```sql
|
||||
-- 需要先使用 bcrypt 加密密码
|
||||
UPDATE users
|
||||
SET password = '<bcrypt_hashed_password>'
|
||||
WHERE username = 'admin';
|
||||
```
|
||||
|
||||
### 方式三:通过脚本
|
||||
|
||||
可以修改 `scripts/init-admin.ts` 中的密码,然后重新运行脚本。
|
||||
|
||||
## 🎯 下一步
|
||||
|
||||
1. ✅ 使用 admin 账号登录系统
|
||||
2. ✅ 创建其他角色(如:编辑、查看者等)
|
||||
3. ✅ 创建其他用户并分配角色
|
||||
4. ✅ 配置菜单权限
|
||||
5. ✅ 开始使用系统
|
||||
271
backend/docs/CONTEST_JUDGE_DESIGN.md
Normal file
271
backend/docs/CONTEST_JUDGE_DESIGN.md
Normal file
@ -0,0 +1,271 @@
|
||||
# 比赛评委存储设计说明
|
||||
|
||||
## 📋 设计决策
|
||||
|
||||
### 问题:是否需要专门的评委表?
|
||||
|
||||
**结论:需要创建 `ContestJudge` 关联表,但不需要单独的评委信息表。**
|
||||
|
||||
## 🎯 设计分析
|
||||
|
||||
### 1. 为什么需要 `ContestJudge` 关联表?
|
||||
|
||||
#### 1.1 业务需求
|
||||
|
||||
1. **比赛与评委的多对多关系**
|
||||
- 一个比赛可以有多个评委
|
||||
- 一个评委可以评审多个比赛
|
||||
- 需要管理这种多对多关系
|
||||
|
||||
2. **评委管理功能**
|
||||
- 查询某个比赛的所有评委列表
|
||||
- 批量添加/删除比赛的评委
|
||||
- 管理评委在特定比赛中的特殊信息
|
||||
|
||||
3. **评委特殊属性**
|
||||
- 评审专业领域(specialty):如"创意设计"、"技术实现"等
|
||||
- 评审权重(weight):用于加权平均计算最终得分
|
||||
- 评委说明(description):在该比赛中的特殊说明
|
||||
|
||||
#### 1.2 当前设计的不足
|
||||
|
||||
**之前的设计**:
|
||||
- 评委信息只存储在 `ContestWorkJudgeAssignment` 表中
|
||||
- 无法直接查询某个比赛有哪些评委
|
||||
- 无法批量管理比赛的评委列表
|
||||
- 无法存储评委在特定比赛中的特殊信息
|
||||
|
||||
**问题场景**:
|
||||
```typescript
|
||||
// ❌ 无法直接查询比赛的评委列表
|
||||
// 需要从作品分配表中去重查询,效率低且不准确
|
||||
|
||||
// ✅ 有了 ContestJudge 表后
|
||||
const judges = await prisma.contestJudge.findMany({
|
||||
where: { contestId: contestId }
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 为什么不需要单独的评委信息表?
|
||||
|
||||
#### 2.1 评委就是用户
|
||||
|
||||
- 评委本身就是系统中的 `User`
|
||||
- 基本信息(姓名、账号等)存储在 `User` 表
|
||||
- 如果是教师,详细信息存储在 `Teacher` 表
|
||||
- 如果是外部专家,可以创建普通 `User` 账号
|
||||
|
||||
#### 2.2 统一用户体系
|
||||
|
||||
- 系统采用统一的用户体系
|
||||
- 通过角色和权限区分用户身份
|
||||
- 评委通过角色(如 `judge`)和权限(如 `review:score`)控制
|
||||
|
||||
## 📊 数据模型设计
|
||||
|
||||
### 1. 表结构
|
||||
|
||||
```prisma
|
||||
model ContestJudge {
|
||||
id Int @id @default(autoincrement())
|
||||
contestId Int @map("contest_id") /// 比赛id
|
||||
judgeId Int @map("judge_id") /// 评委用户id
|
||||
specialty String? /// 评审专业领域(可选)
|
||||
weight Decimal? @db.Decimal(3, 2) /// 评审权重(可选)
|
||||
description String? @db.Text /// 评委在该比赛中的说明
|
||||
creator Int? /// 创建人ID
|
||||
modifier Int? /// 修改人ID
|
||||
createTime DateTime @default(now()) @map("create_time")
|
||||
modifyTime DateTime @updatedAt @map("modify_time")
|
||||
validState Int @default(1) @map("valid_state")
|
||||
|
||||
contest Contest @relation(fields: [contestId], references: [id])
|
||||
judge User @relation(fields: [judgeId], references: [id])
|
||||
|
||||
@@unique([contestId, judgeId])
|
||||
@@index([contestId])
|
||||
@@index([judgeId])
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 数据关系
|
||||
|
||||
```
|
||||
Contest (比赛)
|
||||
↓ (1:N)
|
||||
ContestJudge (比赛评委关联)
|
||||
↓ (N:1)
|
||||
User (用户/评委)
|
||||
↓ (1:1, 可选)
|
||||
Teacher (教师信息,如果是教师评委)
|
||||
```
|
||||
|
||||
### 3. 与其他表的关系
|
||||
|
||||
```
|
||||
ContestJudge (比赛评委)
|
||||
↓
|
||||
ContestWorkJudgeAssignment (作品分配)
|
||||
↓
|
||||
ContestWorkScore (作品评分)
|
||||
```
|
||||
|
||||
**关系说明**:
|
||||
- `ContestJudge`:定义哪些评委可以评审某个比赛
|
||||
- `ContestWorkJudgeAssignment`:将具体作品分配给评委
|
||||
- `ContestWorkScore`:记录评委的评分结果
|
||||
|
||||
## 🔄 业务流程
|
||||
|
||||
### 1. 添加评委到比赛
|
||||
|
||||
```typescript
|
||||
// 1. 创建比赛评委关联
|
||||
const contestJudge = await prisma.contestJudge.create({
|
||||
data: {
|
||||
contestId: contestId,
|
||||
judgeId: userId,
|
||||
specialty: "创意设计",
|
||||
weight: 1.2, // 权重1.2倍
|
||||
description: "专业设计领域评委"
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 查询比赛的评委列表
|
||||
|
||||
```typescript
|
||||
// 查询某个比赛的所有评委
|
||||
const judges = await prisma.contestJudge.findMany({
|
||||
where: {
|
||||
contestId: contestId,
|
||||
validState: 1
|
||||
},
|
||||
include: {
|
||||
judge: {
|
||||
include: {
|
||||
teacher: true // 如果是教师,获取教师信息
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 分配作品给评委
|
||||
|
||||
```typescript
|
||||
// 分配作品时,验证评委是否属于该比赛
|
||||
const contestJudge = await prisma.contestJudge.findFirst({
|
||||
where: {
|
||||
contestId: contestId,
|
||||
judgeId: judgeId,
|
||||
validState: 1
|
||||
}
|
||||
});
|
||||
|
||||
if (!contestJudge) {
|
||||
throw new Error('该评委不属于此比赛');
|
||||
}
|
||||
|
||||
// 创建作品分配记录
|
||||
const assignment = await prisma.contestWorkJudgeAssignment.create({
|
||||
data: {
|
||||
contestId: contestId,
|
||||
workId: workId,
|
||||
judgeId: judgeId
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 计算加权平均分
|
||||
|
||||
```typescript
|
||||
// 获取所有评委的评分和权重
|
||||
const scores = await prisma.contestWorkScore.findMany({
|
||||
where: { workId: workId },
|
||||
include: {
|
||||
judge: {
|
||||
include: {
|
||||
contestJudges: {
|
||||
where: { contestId: contestId }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 计算加权平均分
|
||||
let totalWeightedScore = 0;
|
||||
let totalWeight = 0;
|
||||
|
||||
for (const score of scores) {
|
||||
const contestJudge = score.judge.contestJudges[0];
|
||||
const weight = contestJudge?.weight || 1.0;
|
||||
totalWeightedScore += score.totalScore * weight;
|
||||
totalWeight += weight;
|
||||
}
|
||||
|
||||
const finalScore = totalWeightedScore / totalWeight;
|
||||
```
|
||||
|
||||
## ✅ 设计优势
|
||||
|
||||
### 1. 清晰的业务逻辑
|
||||
|
||||
- **比赛评委管理**:通过 `ContestJudge` 表统一管理
|
||||
- **作品分配**:通过 `ContestWorkJudgeAssignment` 表管理
|
||||
- **评分记录**:通过 `ContestWorkScore` 表记录
|
||||
|
||||
### 2. 灵活的扩展性
|
||||
|
||||
- 支持评委专业领域分类
|
||||
- 支持评审权重设置
|
||||
- 支持评委说明信息
|
||||
|
||||
### 3. 高效的查询
|
||||
|
||||
- 直接查询比赛的评委列表
|
||||
- 支持评委维度的统计分析
|
||||
- 支持权重计算
|
||||
|
||||
### 4. 数据一致性
|
||||
|
||||
- 通过外键约束保证数据完整性
|
||||
- 删除比赛时,级联删除评委关联
|
||||
- 删除用户时,级联删除评委关联
|
||||
|
||||
## 📝 使用建议
|
||||
|
||||
### 1. 评委添加流程
|
||||
|
||||
```
|
||||
1. 确保用户已创建(User 表)
|
||||
2. 为用户分配评委角色和权限
|
||||
3. 创建 ContestJudge 记录,关联比赛和用户
|
||||
4. 可选:设置专业领域、权重等信息
|
||||
```
|
||||
|
||||
### 2. 评委验证
|
||||
|
||||
在分配作品给评委时,应该验证:
|
||||
- 该评委是否属于该比赛(查询 `ContestJudge` 表)
|
||||
- 该评委是否有效(`validState = 1`)
|
||||
|
||||
### 3. 权限控制
|
||||
|
||||
- 只有比赛管理员可以添加/删除评委
|
||||
- 评委只能查看和评分分配给自己的作品
|
||||
- 通过 RBAC 权限系统控制访问
|
||||
|
||||
## 🔍 总结
|
||||
|
||||
**评委存储方案**:
|
||||
- ✅ **需要** `ContestJudge` 关联表:管理比赛与评委的多对多关系
|
||||
- ❌ **不需要** 单独的评委信息表:评委信息通过 `User` 和 `Teacher` 表存储
|
||||
|
||||
**核心设计原则**:
|
||||
1. 评委就是用户,统一用户体系
|
||||
2. 通过关联表管理比赛与评委的关系
|
||||
3. 支持评委在特定比赛中的特殊属性
|
||||
4. 保证数据一致性和查询效率
|
||||
|
||||
183
backend/docs/DATABASE_SETUP.md
Normal file
183
backend/docs/DATABASE_SETUP.md
Normal file
@ -0,0 +1,183 @@
|
||||
# 数据库配置指南
|
||||
|
||||
## 1. 创建数据库
|
||||
|
||||
首先需要在 MySQL 中创建数据库:
|
||||
|
||||
```sql
|
||||
CREATE DATABASE db_competition_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
## 2. 配置环境变量
|
||||
|
||||
### 方式一:复制示例文件
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
### 方式二:手动创建 .env 文件
|
||||
|
||||
在 `backend` 目录下创建 `.env` 文件,内容如下:
|
||||
|
||||
```env
|
||||
DATABASE_URL="mysql://root:password@localhost:3306/competition_management?schema=public"
|
||||
JWT_SECRET="your-secret-key-change-in-production"
|
||||
PORT=3001
|
||||
NODE_ENV=development
|
||||
```
|
||||
|
||||
## 3. 配置说明
|
||||
|
||||
### DATABASE_URL 格式
|
||||
|
||||
```
|
||||
mysql://用户名:密码@主机:端口/数据库名?参数
|
||||
```
|
||||
|
||||
**示例:**
|
||||
|
||||
- 本地 MySQL,默认端口:
|
||||
|
||||
```
|
||||
DATABASE_URL="mysql://root:password@localhost:3306/competition_management?schema=public"
|
||||
```
|
||||
|
||||
- 远程 MySQL:
|
||||
|
||||
```
|
||||
DATABASE_URL="mysql://user:password@192.168.1.100:3306/competition_management?schema=public"
|
||||
```
|
||||
|
||||
- 使用 SSL:
|
||||
|
||||
```
|
||||
DATABASE_URL="mysql://user:password@localhost:3306/competition_management?schema=public&sslmode=require"
|
||||
```
|
||||
|
||||
- 包含特殊字符的密码(需要 URL 编码):
|
||||
```
|
||||
DATABASE_URL="mysql://user:p%40ssw0rd@localhost:3306/competition_management?schema=public"
|
||||
```
|
||||
|
||||
### JWT_SECRET
|
||||
|
||||
用于 JWT token 签名的密钥,生产环境必须使用强随机字符串。
|
||||
|
||||
**生成方式:**
|
||||
|
||||
```bash
|
||||
# 使用 Node.js
|
||||
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||
|
||||
# 或使用 openssl
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
## 4. 初始化数据库
|
||||
|
||||
配置好 `.env` 文件后,执行以下命令初始化数据库:
|
||||
|
||||
```bash
|
||||
# 生成 Prisma Client
|
||||
pnpm prisma:generate
|
||||
|
||||
# 运行数据库迁移(创建表结构)
|
||||
pnpm prisma:migrate
|
||||
|
||||
# 或使用开发模式(会提示输入迁移名称)
|
||||
pnpm prisma:migrate dev
|
||||
```
|
||||
|
||||
## 5. 验证连接
|
||||
|
||||
### 方式一:使用 Prisma Studio
|
||||
|
||||
```bash
|
||||
pnpm prisma:studio
|
||||
```
|
||||
|
||||
这会打开一个可视化界面,可以在浏览器中查看和管理数据库。
|
||||
|
||||
### 方式二:测试连接
|
||||
|
||||
启动后端服务:
|
||||
|
||||
```bash
|
||||
pnpm start:dev
|
||||
```
|
||||
|
||||
如果连接成功,服务会正常启动;如果失败,会显示具体的错误信息。
|
||||
|
||||
## 6. 常见问题
|
||||
|
||||
### 问题 1: 连接被拒绝
|
||||
|
||||
**错误信息:** `Can't reach database server`
|
||||
|
||||
**解决方案:**
|
||||
|
||||
- 检查 MySQL 服务是否启动
|
||||
- 检查主机和端口是否正确
|
||||
- 检查防火墙设置
|
||||
|
||||
### 问题 2: 认证失败
|
||||
|
||||
**错误信息:** `Access denied for user`
|
||||
|
||||
**解决方案:**
|
||||
|
||||
- 检查用户名和密码是否正确
|
||||
- 确认用户有访问该数据库的权限
|
||||
- 如果密码包含特殊字符,需要进行 URL 编码
|
||||
|
||||
### 问题 3: 数据库不存在
|
||||
|
||||
**错误信息:** `Unknown database`
|
||||
|
||||
**解决方案:**
|
||||
|
||||
- 先创建数据库(见步骤 1)
|
||||
- 检查数据库名称是否正确
|
||||
|
||||
### 问题 4: 字符集问题
|
||||
|
||||
**解决方案:**
|
||||
创建数据库时指定字符集:
|
||||
|
||||
```sql
|
||||
CREATE DATABASE competition_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
## 7. 生产环境配置
|
||||
|
||||
生产环境建议:
|
||||
|
||||
1. **使用环境变量管理工具**(如 AWS Secrets Manager、Azure Key Vault)
|
||||
2. **使用连接池**(Prisma 默认已配置)
|
||||
3. **启用 SSL 连接**
|
||||
4. **定期备份数据库**
|
||||
5. **使用强密码和 JWT_SECRET**
|
||||
|
||||
## 8. 数据库迁移
|
||||
|
||||
### 创建新迁移
|
||||
|
||||
```bash
|
||||
pnpm prisma:migrate dev --name migration_name
|
||||
```
|
||||
|
||||
### 应用迁移(生产环境)
|
||||
|
||||
```bash
|
||||
pnpm prisma:migrate deploy
|
||||
```
|
||||
|
||||
### 重置数据库(开发环境)
|
||||
|
||||
```bash
|
||||
pnpm prisma:migrate reset
|
||||
```
|
||||
|
||||
**注意:** 这会删除所有数据,仅用于开发环境!
|
||||
165
backend/docs/DATABASE_URL_SOURCE.md
Normal file
165
backend/docs/DATABASE_URL_SOURCE.md
Normal file
@ -0,0 +1,165 @@
|
||||
# DATABASE_URL 来源说明
|
||||
|
||||
## 📍 定义位置
|
||||
|
||||
`DATABASE_URL` 在 `schema.prisma` 中定义:
|
||||
|
||||
```prisma
|
||||
datasource db {
|
||||
provider = "mysql"
|
||||
url = env("DATABASE_URL") // ← 从这里读取环境变量
|
||||
}
|
||||
```
|
||||
|
||||
## 🔄 加载流程
|
||||
|
||||
### 1. 配置文件定义
|
||||
|
||||
`DATABASE_URL` 定义在环境配置文件中:
|
||||
|
||||
**当前配置**:`.development.env` 文件
|
||||
```env
|
||||
DATABASE_URL="mysql://root:woshimima@localhost:3306/db_competition_management?schema=public"
|
||||
```
|
||||
|
||||
### 2. NestJS ConfigModule 加载
|
||||
|
||||
在 `app.module.ts` 中配置:
|
||||
|
||||
```typescript
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
envFilePath: ['.development.env'], // ← 从这里加载环境变量
|
||||
})
|
||||
```
|
||||
|
||||
**加载顺序**:
|
||||
1. NestJS ConfigModule 读取 `.development.env` 文件
|
||||
2. 将文件中的 `DATABASE_URL` 加载到 `process.env.DATABASE_URL`
|
||||
3. 应用启动时,所有模块都可以通过 `ConfigService` 访问
|
||||
|
||||
### 3. Prisma 读取
|
||||
|
||||
Prisma 在以下时机读取 `DATABASE_URL`:
|
||||
|
||||
1. **生成 Prisma Client 时**:
|
||||
```bash
|
||||
npx prisma generate
|
||||
```
|
||||
- 读取 `process.env.DATABASE_URL`
|
||||
- 生成类型定义(不连接数据库)
|
||||
|
||||
2. **运行迁移时**:
|
||||
```bash
|
||||
npx prisma migrate dev
|
||||
npx prisma migrate deploy
|
||||
```
|
||||
- 读取 `process.env.DATABASE_URL`
|
||||
- 连接到数据库执行迁移
|
||||
|
||||
3. **应用运行时**:
|
||||
- `PrismaService` 初始化时读取 `process.env.DATABASE_URL`
|
||||
- 建立数据库连接
|
||||
|
||||
## 📂 配置文件优先级
|
||||
|
||||
根据 `app.module.ts` 的配置:
|
||||
|
||||
```typescript
|
||||
envFilePath: ['.development.env']
|
||||
```
|
||||
|
||||
**当前配置**:
|
||||
- ✅ 优先加载:`.development.env`
|
||||
- ⚠️ 注意:如果设置了 `ignoreEnvFile: true`,则不会加载文件,只使用系统环境变量
|
||||
|
||||
## 🔍 验证 DATABASE_URL 来源
|
||||
|
||||
### 方法 1:查看环境变量(应用运行时)
|
||||
|
||||
```bash
|
||||
# 启动应用后,访问配置验证接口
|
||||
curl http://localhost:3001/api/config-verification/env-info
|
||||
```
|
||||
|
||||
### 方法 2:查看启动日志
|
||||
|
||||
应用启动时会在控制台显示:
|
||||
```
|
||||
=== 环境配置验证 ===
|
||||
DATABASE_URL: 已设置 mysql://root:woshimima@localhost:3306/db_competition_management?schema=public
|
||||
```
|
||||
|
||||
### 方法 3:检查配置文件
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
cat .development.env | grep DATABASE_URL
|
||||
```
|
||||
|
||||
### 方法 4:在代码中验证
|
||||
|
||||
```typescript
|
||||
// 在任何服务中
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
const dbUrl = this.configService.get('DATABASE_URL');
|
||||
console.log('DATABASE_URL:', dbUrl);
|
||||
```
|
||||
|
||||
## 🔐 环境变量来源优先级
|
||||
|
||||
Prisma 读取 `DATABASE_URL` 的优先级:
|
||||
|
||||
1. **系统环境变量**(最高优先级)
|
||||
```bash
|
||||
export DATABASE_URL="mysql://..."
|
||||
```
|
||||
|
||||
2. **.env 文件**(通过 ConfigModule 加载)
|
||||
- `.development.env`
|
||||
- `.env`
|
||||
|
||||
3. **默认值**(如果都没有设置,Prisma 会报错)
|
||||
|
||||
## 📝 DATABASE_URL 格式
|
||||
|
||||
```
|
||||
mysql://用户名:密码@主机:端口/数据库名?参数
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```env
|
||||
# 本地数据库
|
||||
DATABASE_URL="mysql://root:password@localhost:3306/db_competition_management?schema=public"
|
||||
|
||||
# 远程数据库
|
||||
DATABASE_URL="mysql://user:pass@192.168.1.100:3306/db_name?schema=public"
|
||||
|
||||
# 带 SSL
|
||||
DATABASE_URL="mysql://user:pass@host:3306/db_name?schema=public&sslmode=require"
|
||||
```
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **密码包含特殊字符**:需要进行 URL 编码
|
||||
```env
|
||||
# 密码: p@ssw0rd
|
||||
DATABASE_URL="mysql://user:p%40ssw0rd@localhost:3306/db"
|
||||
```
|
||||
|
||||
2. **配置文件安全**:
|
||||
- `.development.env` 不应提交到 Git
|
||||
- 生产环境使用环境变量或密钥管理服务
|
||||
|
||||
3. **Prisma 读取时机**:
|
||||
- Prisma 直接读取 `process.env.DATABASE_URL`
|
||||
- 不依赖 NestJS ConfigModule(但 ConfigModule 会将文件内容加载到 `process.env`)
|
||||
|
||||
## 🔧 当前配置总结
|
||||
|
||||
- **配置文件**:`.development.env`
|
||||
- **配置项**:`DATABASE_URL="mysql://root:woshimima@localhost:3306/db_competition_management?schema=public"`
|
||||
- **加载方式**:NestJS ConfigModule → `process.env` → Prisma
|
||||
- **验证方式**:启动日志或 `/api/config-verification/env-info` 接口
|
||||
|
||||
290
backend/docs/ENVIRONMENT_CONFIG.md
Normal file
290
backend/docs/ENVIRONMENT_CONFIG.md
Normal file
@ -0,0 +1,290 @@
|
||||
# 环境配置指南
|
||||
|
||||
## 环境区分方案
|
||||
|
||||
项目支持通过 `NODE_ENV` 环境变量和不同的 `.env` 文件来区分开发和生产环境。
|
||||
|
||||
## 配置文件结构
|
||||
|
||||
```
|
||||
backend/
|
||||
├── .env # 默认配置(可选,作为后备)
|
||||
├── .env.development # 开发环境配置
|
||||
├── .env.production # 生产环境配置
|
||||
└── .env.test # 测试环境配置(可选)
|
||||
```
|
||||
|
||||
## 配置优先级
|
||||
|
||||
配置文件按以下优先级加载:
|
||||
|
||||
1. `.env.${NODE_ENV}` - 根据当前环境加载(最高优先级)
|
||||
2. `.env` - 默认配置文件(后备)
|
||||
|
||||
例如:
|
||||
- `NODE_ENV=development` → 加载 `.env.development`
|
||||
- `NODE_ENV=production` → 加载 `.env.production`
|
||||
- 未设置 `NODE_ENV` → 默认加载 `.env.development`,然后 `.env`
|
||||
|
||||
## 开发环境配置
|
||||
|
||||
### 创建 `.env.development` 文件
|
||||
|
||||
```env
|
||||
# 开发环境配置
|
||||
NODE_ENV=development
|
||||
|
||||
# 开发数据库(本地数据库)
|
||||
DATABASE_URL="mysql://root:password@localhost:3306/competition_management_dev?schema=public"
|
||||
|
||||
# JWT 密钥(开发环境可以使用简单密钥)
|
||||
JWT_SECRET="dev-secret-key-not-for-production"
|
||||
|
||||
# 服务器端口
|
||||
PORT=3001
|
||||
|
||||
# 日志级别
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# CORS 配置(开发环境允许所有来源)
|
||||
CORS_ORIGIN=*
|
||||
```
|
||||
|
||||
### 开发环境数据库命名建议
|
||||
|
||||
- 数据库名:`competition_management_dev`
|
||||
- 便于区分:开发和生产使用不同的数据库
|
||||
- 安全:避免误操作生产数据
|
||||
|
||||
## 生产环境配置
|
||||
|
||||
### 创建 `.env.production` 文件
|
||||
|
||||
```env
|
||||
# 生产环境配置
|
||||
NODE_ENV=production
|
||||
|
||||
# 生产数据库(远程或云数据库)
|
||||
DATABASE_URL="mysql://prod_user:strong_password@prod-db-host:3306/competition_management?schema=public&sslmode=require"
|
||||
|
||||
# JWT 密钥(必须使用强随机字符串)
|
||||
# 生成方式: openssl rand -hex 32
|
||||
JWT_SECRET="your-production-secret-key-must-be-strong-and-random-64-chars"
|
||||
|
||||
# 服务器端口
|
||||
PORT=3001
|
||||
|
||||
# 日志级别
|
||||
LOG_LEVEL=error
|
||||
|
||||
# CORS 配置(生产环境指定具体域名)
|
||||
CORS_ORIGIN=https://yourdomain.com
|
||||
|
||||
# 数据库连接池配置
|
||||
DB_POOL_MIN=2
|
||||
DB_POOL_MAX=10
|
||||
|
||||
# SSL/TLS 配置
|
||||
SSL_ENABLED=true
|
||||
```
|
||||
|
||||
### 生产环境数据库配置要点
|
||||
|
||||
1. **使用独立的数据库服务器**
|
||||
2. **启用 SSL 连接**(`sslmode=require`)
|
||||
3. **使用强密码**
|
||||
4. **限制数据库用户权限**(最小权限原则)
|
||||
5. **定期备份**
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 开发环境
|
||||
|
||||
```bash
|
||||
# 方式 1: 设置环境变量后启动
|
||||
NODE_ENV=development pnpm start:dev
|
||||
|
||||
# 方式 2: 在 package.json 中配置(推荐)
|
||||
# 已自动配置,直接运行:
|
||||
pnpm start:dev
|
||||
```
|
||||
|
||||
### 生产环境
|
||||
|
||||
```bash
|
||||
# 方式 1: 设置环境变量后启动
|
||||
NODE_ENV=production pnpm start:prod
|
||||
|
||||
# 方式 2: 在部署脚本中设置
|
||||
export NODE_ENV=production
|
||||
pnpm start:prod
|
||||
```
|
||||
|
||||
### 测试环境(可选)
|
||||
|
||||
```bash
|
||||
# 创建 .env.test 文件
|
||||
NODE_ENV=test
|
||||
DATABASE_URL="mysql://root:password@localhost:3306/competition_management_test?schema=public"
|
||||
JWT_SECRET="test-secret-key"
|
||||
PORT=3002
|
||||
|
||||
# 运行测试
|
||||
NODE_ENV=test pnpm test
|
||||
```
|
||||
|
||||
## 数据库命名规范
|
||||
|
||||
建议使用以下命名规范来区分不同环境的数据库:
|
||||
|
||||
| 环境 | 数据库名 | 说明 |
|
||||
|------|---------|------|
|
||||
| 开发 | `competition_management_dev` | 开发环境数据库 |
|
||||
| 测试 | `competition_management_test` | 测试环境数据库 |
|
||||
| 生产 | `competition_management` | 生产环境数据库 |
|
||||
| 预发布 | `competition_management_staging` | 预发布环境数据库 |
|
||||
|
||||
## 创建不同环境的数据库
|
||||
|
||||
### 开发环境数据库
|
||||
|
||||
```sql
|
||||
CREATE DATABASE competition_management_dev
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
### 生产环境数据库
|
||||
|
||||
```sql
|
||||
CREATE DATABASE competition_management
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
## 环境变量管理最佳实践
|
||||
|
||||
### 1. 使用 .gitignore
|
||||
|
||||
确保 `.env*` 文件不被提交到版本控制:
|
||||
|
||||
```gitignore
|
||||
# .env files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
.env.development
|
||||
.env.production
|
||||
.env.test
|
||||
```
|
||||
|
||||
### 2. 提供示例文件
|
||||
|
||||
创建 `.env.example` 或 `.env.*.example` 文件作为模板:
|
||||
|
||||
```bash
|
||||
# 开发环境示例
|
||||
cp .env.development.example .env.development
|
||||
|
||||
# 生产环境示例
|
||||
cp .env.production.example .env.production
|
||||
```
|
||||
|
||||
### 3. 使用环境变量管理工具(生产环境)
|
||||
|
||||
- **Docker**: 使用 `docker-compose.yml` 中的 `env_file`
|
||||
- **Kubernetes**: 使用 `ConfigMap` 和 `Secret`
|
||||
- **云平台**:
|
||||
- AWS: Secrets Manager
|
||||
- Azure: Key Vault
|
||||
- GCP: Secret Manager
|
||||
|
||||
### 4. 验证配置
|
||||
|
||||
在应用启动时验证必要的环境变量:
|
||||
|
||||
```typescript
|
||||
// 可以在 main.ts 中添加验证
|
||||
if (!process.env.DATABASE_URL) {
|
||||
throw new Error('DATABASE_URL is required');
|
||||
}
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 创建开发环境配置
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
|
||||
# 创建开发环境配置文件
|
||||
cat > .env.development << EOF
|
||||
NODE_ENV=development
|
||||
DATABASE_URL="mysql://root:password@localhost:3306/competition_management_dev?schema=public"
|
||||
JWT_SECRET="dev-secret-key"
|
||||
PORT=3001
|
||||
EOF
|
||||
```
|
||||
|
||||
### 2. 创建生产环境配置
|
||||
|
||||
```bash
|
||||
# 创建生产环境配置文件(不要提交到 Git)
|
||||
cat > .env.production << EOF
|
||||
NODE_ENV=production
|
||||
DATABASE_URL="mysql://prod_user:password@prod-host:3306/competition_management?schema=public&sslmode=require"
|
||||
JWT_SECRET="$(openssl rand -hex 32)"
|
||||
PORT=3001
|
||||
EOF
|
||||
```
|
||||
|
||||
### 3. 初始化数据库
|
||||
|
||||
```bash
|
||||
# 开发环境
|
||||
NODE_ENV=development pnpm prisma:migrate dev
|
||||
|
||||
# 生产环境(部署时)
|
||||
NODE_ENV=production pnpm prisma:migrate deploy
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何确保使用正确的环境配置?
|
||||
|
||||
A: 在启动应用前检查 `NODE_ENV` 环境变量:
|
||||
```bash
|
||||
echo $NODE_ENV # 应该显示 development 或 production
|
||||
```
|
||||
|
||||
### Q: 生产环境配置应该存储在哪里?
|
||||
|
||||
A:
|
||||
- **不要提交到 Git**
|
||||
- 使用环境变量管理工具(如 Docker secrets、K8s secrets)
|
||||
- 或使用云平台提供的密钥管理服务
|
||||
|
||||
### Q: 如何在不同环境间切换?
|
||||
|
||||
A: 通过设置 `NODE_ENV` 环境变量:
|
||||
```bash
|
||||
# 开发环境
|
||||
export NODE_ENV=development
|
||||
pnpm start:dev
|
||||
|
||||
# 生产环境
|
||||
export NODE_ENV=production
|
||||
pnpm start:prod
|
||||
```
|
||||
|
||||
### Q: 数据库迁移如何区分环境?
|
||||
|
||||
A: Prisma 会根据 `DATABASE_URL` 环境变量自动使用对应的数据库:
|
||||
```bash
|
||||
# 开发环境迁移
|
||||
NODE_ENV=development pnpm prisma:migrate dev
|
||||
|
||||
# 生产环境迁移
|
||||
NODE_ENV=production pnpm prisma:migrate deploy
|
||||
```
|
||||
|
||||
254
backend/docs/ENV_CHANGE_GUIDE.md
Normal file
254
backend/docs/ENV_CHANGE_GUIDE.md
Normal file
@ -0,0 +1,254 @@
|
||||
# 修改 DATABASE_URL 后的操作指南
|
||||
|
||||
## 📋 操作决策树
|
||||
|
||||
```
|
||||
修改 DATABASE_URL
|
||||
│
|
||||
├─ 只改了连接信息(地址/端口/用户名/密码/数据库名)
|
||||
│ └─ schema.prisma 未修改
|
||||
│ ├─ 目标数据库已有表结构 → ✅ 只需重启应用
|
||||
│ └─ 目标数据库是空的 → ⚠️ 需要运行迁移
|
||||
│
|
||||
└─ 同时修改了 schema.prisma
|
||||
└─ ✅ 必须执行:生成 Client + 运行迁移
|
||||
```
|
||||
|
||||
## 🔄 场景 1:只修改连接信息(最常见)
|
||||
|
||||
### 情况 A:目标数据库已有表结构
|
||||
|
||||
**示例**:从本地数据库切换到远程数据库,但表结构已存在
|
||||
|
||||
```bash
|
||||
# 1. 修改 .development.env 文件
|
||||
DATABASE_URL="mysql://user:pass@new-host:3306/db_name?schema=public"
|
||||
|
||||
# 2. 重启应用即可(无需执行 Prisma 命令)
|
||||
npm run start:dev
|
||||
```
|
||||
|
||||
**原因**:
|
||||
|
||||
- Prisma Client 在应用启动时读取 `process.env.DATABASE_URL`
|
||||
- 如果目标数据库已有表结构,直接连接即可
|
||||
- 不需要重新生成 Client(类型定义没变)
|
||||
- 不需要运行迁移(表结构没变)
|
||||
|
||||
---
|
||||
|
||||
### 情况 B:目标数据库是空的(新数据库)
|
||||
|
||||
**示例**:切换到全新的数据库,还没有表结构
|
||||
|
||||
```bash
|
||||
# 1. 修改 .development.env 文件
|
||||
DATABASE_URL="mysql://user:pass@new-host:3306/new_db?schema=public"
|
||||
|
||||
# 2. 运行迁移创建表结构
|
||||
npm run prisma:migrate
|
||||
|
||||
# 或使用部署模式(生产环境)
|
||||
npm run prisma:migrate:deploy
|
||||
|
||||
# 3. 重启应用
|
||||
npm run start:dev
|
||||
```
|
||||
|
||||
**原因**:
|
||||
|
||||
- 新数据库没有表结构
|
||||
- 需要运行迁移来创建表
|
||||
- 迁移会读取 `process.env.DATABASE_URL` 连接到新数据库
|
||||
|
||||
---
|
||||
|
||||
## 🔄 场景 2:同时修改了 schema.prisma
|
||||
|
||||
**示例**:修改了数据库模型(添加/删除字段、表等)
|
||||
|
||||
```bash
|
||||
# 1. 修改 schema.prisma(添加字段、表等)
|
||||
|
||||
# 2. 生成 Prisma Client(必须)
|
||||
npm run prisma:generate
|
||||
|
||||
# 3. 创建并运行迁移(必须)
|
||||
npm run prisma:migrate
|
||||
# 会提示输入迁移名称,如:add_user_email_field
|
||||
|
||||
# 4. 重启应用
|
||||
npm run start:dev
|
||||
```
|
||||
|
||||
**原因**:
|
||||
|
||||
- schema.prisma 改变 → TypeScript 类型定义改变 → 需要重新生成 Client
|
||||
- 数据库结构改变 → 需要创建迁移并应用到数据库
|
||||
|
||||
---
|
||||
|
||||
## 📝 完整操作流程
|
||||
|
||||
### 开发环境(推荐流程)
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
|
||||
# 1. 修改 .development.env 中的 DATABASE_URL
|
||||
vim .development.env
|
||||
|
||||
# 2. 检查目标数据库是否有表结构
|
||||
# 方式 A:使用 Prisma Studio 查看
|
||||
npm run prisma:studio
|
||||
|
||||
# 方式 B:直接连接数据库查看
|
||||
mysql -h host -u user -p database -e "SHOW TABLES;"
|
||||
|
||||
# 3. 根据情况选择操作:
|
||||
|
||||
# 情况 1:数据库已有表结构 → 只需重启
|
||||
npm run start:dev
|
||||
|
||||
# 情况 2:数据库是空的 → 运行迁移
|
||||
npm run prisma:migrate
|
||||
npm run start:dev
|
||||
|
||||
# 情况 3:修改了 schema.prisma → 生成 + 迁移
|
||||
npm run prisma:generate
|
||||
npm run prisma:migrate
|
||||
npm run start:dev
|
||||
```
|
||||
|
||||
### 生产环境(部署流程)
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
|
||||
# 1. 修改生产环境配置文件或环境变量
|
||||
# 注意:生产环境通常使用环境变量,而不是文件
|
||||
|
||||
# 2. 生成 Prisma Client
|
||||
npm run prisma:generate
|
||||
|
||||
# 3. 运行迁移(生产环境使用 deploy,不会创建新迁移)
|
||||
NODE_ENV=production npm run prisma:migrate:deploy
|
||||
|
||||
# 4. 重启应用
|
||||
npm run start:prod
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 快速检查清单
|
||||
|
||||
修改 `DATABASE_URL` 后,按以下顺序检查:
|
||||
|
||||
- [ ] **只改了连接信息?**
|
||||
- [ ] 目标数据库有表 → ✅ 重启应用
|
||||
- [ ] 目标数据库为空 → ⚠️ 运行迁移
|
||||
|
||||
- [ ] **修改了 schema.prisma?**
|
||||
- [ ] 是 → ✅ 生成 Client + 运行迁移
|
||||
- [ ] 否 → 跳过
|
||||
|
||||
- [ ] **应用启动后验证**
|
||||
- [ ] 检查启动日志中的 DATABASE_URL
|
||||
- [ ] 访问 `/api/config-verification/env-info` 验证
|
||||
- [ ] 测试数据库操作是否正常
|
||||
|
||||
---
|
||||
|
||||
## 🔍 验证方法
|
||||
|
||||
### 1. 验证 DATABASE_URL 是否生效
|
||||
|
||||
```bash
|
||||
# 启动应用后查看日志
|
||||
npm run start:dev
|
||||
|
||||
# 应该看到:
|
||||
# DATABASE_URL: 已设置 mysql://...
|
||||
```
|
||||
|
||||
### 2. 验证数据库连接
|
||||
|
||||
```bash
|
||||
# 使用 Prisma Studio 连接
|
||||
npm run prisma:studio
|
||||
|
||||
# 如果能打开并看到表,说明连接成功
|
||||
```
|
||||
|
||||
### 3. 验证表结构
|
||||
|
||||
```bash
|
||||
# 检查迁移状态
|
||||
npx prisma migrate status
|
||||
|
||||
# 应该显示:All migrations have been successfully applied
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 常见错误
|
||||
|
||||
### 错误 1:连接失败
|
||||
|
||||
```
|
||||
Error: Can't reach database server
|
||||
```
|
||||
|
||||
**解决**:
|
||||
|
||||
- 检查 DATABASE_URL 格式是否正确
|
||||
- 检查数据库服务是否运行
|
||||
- 检查网络连接和防火墙
|
||||
|
||||
### 错误 2:表不存在
|
||||
|
||||
```
|
||||
Error: Table 'xxx' doesn't exist
|
||||
```
|
||||
|
||||
**解决**:
|
||||
|
||||
- 运行迁移:`npm run prisma:migrate`
|
||||
- 或使用:`npx prisma db push`(仅开发环境)
|
||||
|
||||
### 错误 3:迁移状态不一致
|
||||
|
||||
```
|
||||
Error: The migration failed to apply
|
||||
```
|
||||
|
||||
**解决**:
|
||||
|
||||
- 检查迁移历史:`npx prisma migrate status`
|
||||
- 重置数据库(仅开发环境):`npx prisma migrate reset`
|
||||
- 或手动修复迁移文件
|
||||
|
||||
---
|
||||
|
||||
## 📚 相关命令速查
|
||||
|
||||
| 操作 | 命令 | 说明 |
|
||||
| ----------- | ------------------------------- | ------------------------- |
|
||||
| 生成 Client | `npm run prisma:generate` | 根据 schema 生成类型 |
|
||||
| 创建迁移 | `npm run prisma:migrate` | 开发环境,会创建新迁移 |
|
||||
| 应用迁移 | `npm run prisma:migrate:deploy` | 生产环境,只应用已有迁移 |
|
||||
| 查看状态 | `npx prisma migrate status` | 查看迁移状态 |
|
||||
| 打开 Studio | `npm run prisma:studio` | 可视化数据库 |
|
||||
| 推送结构 | `npx prisma db push` | 直接同步 schema(仅开发) |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 总结
|
||||
|
||||
**修改 DATABASE_URL 后的最小操作**:
|
||||
|
||||
1. **只改连接信息 + 数据库有表** → ✅ **重启应用**
|
||||
2. **只改连接信息 + 数据库为空** → ⚠️ **运行迁移**
|
||||
3. **修改了 schema.prisma** → ✅ **生成 Client + 运行迁移**
|
||||
|
||||
**记住**:Prisma Client 在应用启动时读取 `process.env.DATABASE_URL`,所以修改后必须重启应用才能生效!
|
||||
219
backend/docs/MENU_INIT.md
Normal file
219
backend/docs/MENU_INIT.md
Normal file
@ -0,0 +1,219 @@
|
||||
# 菜单初始化指南
|
||||
|
||||
## 📋 概述
|
||||
|
||||
菜单初始化脚本会根据项目的前端路由配置,自动创建菜单数据到数据库中。脚本会创建树形结构的菜单,包括顶级菜单和子菜单。
|
||||
|
||||
## 🚀 使用方法
|
||||
|
||||
### 1. 执行初始化脚本
|
||||
|
||||
在 `backend` 目录下执行:
|
||||
|
||||
```bash
|
||||
pnpm init:menus
|
||||
```
|
||||
|
||||
或者使用 npm:
|
||||
|
||||
```bash
|
||||
npm run init:menus
|
||||
```
|
||||
|
||||
### 2. 脚本功能
|
||||
|
||||
脚本会根据 `frontend/src/router/index.ts` 中的路由配置,自动创建以下菜单结构:
|
||||
|
||||
```
|
||||
仪表盘 (/dashboard)
|
||||
系统管理 (/system)
|
||||
├── 用户管理 (/system/users)
|
||||
├── 角色管理 (/system/roles)
|
||||
├── 菜单管理 (/system/menus)
|
||||
├── 数据字典 (/system/dict)
|
||||
├── 系统配置 (/system/config)
|
||||
└── 日志记录 (/system/logs)
|
||||
```
|
||||
|
||||
## 📝 菜单数据结构
|
||||
|
||||
### 顶级菜单
|
||||
|
||||
1. **仪表盘**
|
||||
- 路径: `/dashboard`
|
||||
- 图标: `DashboardOutlined`
|
||||
- 组件: `dashboard/Index`
|
||||
- 排序: 1
|
||||
|
||||
2. **系统管理**
|
||||
- 路径: `/system`
|
||||
- 图标: `SettingOutlined`
|
||||
- 组件: `null` (父菜单)
|
||||
- 排序: 10
|
||||
|
||||
### 系统管理子菜单
|
||||
|
||||
1. **用户管理**
|
||||
- 路径: `/system/users`
|
||||
- 图标: `UserOutlined`
|
||||
- 组件: `system/users/Index`
|
||||
- 排序: 1
|
||||
|
||||
2. **角色管理**
|
||||
- 路径: `/system/roles`
|
||||
- 图标: `TeamOutlined`
|
||||
- 组件: `system/roles/Index`
|
||||
- 排序: 2
|
||||
|
||||
3. **菜单管理**
|
||||
- 路径: `/system/menus`
|
||||
- 图标: `MenuOutlined`
|
||||
- 组件: `system/menus/Index`
|
||||
- 排序: 3
|
||||
|
||||
4. **数据字典**
|
||||
- 路径: `/system/dict`
|
||||
- 图标: `BookOutlined`
|
||||
- 组件: `system/dict/Index`
|
||||
- 排序: 4
|
||||
|
||||
5. **系统配置**
|
||||
- 路径: `/system/config`
|
||||
- 图标: `ToolOutlined`
|
||||
- 组件: `system/config/Index`
|
||||
- 排序: 5
|
||||
|
||||
6. **日志记录**
|
||||
- 路径: `/system/logs`
|
||||
- 图标: `FileTextOutlined`
|
||||
- 组件: `system/logs/Index`
|
||||
- 排序: 6
|
||||
|
||||
## 🔄 脚本特性
|
||||
|
||||
### 1. 幂等性
|
||||
|
||||
- 脚本支持重复执行
|
||||
- 如果菜单已存在(相同名称和父菜单),会更新现有菜单
|
||||
- 如果菜单不存在,会创建新菜单
|
||||
|
||||
### 2. 树形结构
|
||||
|
||||
- 自动处理父子菜单关系
|
||||
- 递归创建子菜单
|
||||
- 保持菜单层级结构
|
||||
|
||||
### 3. 数据更新
|
||||
|
||||
- 如果菜单已存在,会更新以下字段:
|
||||
- 路径 (path)
|
||||
- 图标 (icon)
|
||||
- 组件路径 (component)
|
||||
- 排序 (sort)
|
||||
- 有效状态 (validState)
|
||||
|
||||
## ⚙️ 自定义菜单数据
|
||||
|
||||
如果需要修改菜单数据,可以编辑 `backend/scripts/init-menus.ts` 文件中的 `menus` 数组:
|
||||
|
||||
```typescript
|
||||
const menus = [
|
||||
{
|
||||
name: '菜单名称',
|
||||
path: '/路由路径',
|
||||
icon: 'IconOutlined', // Ant Design Icons 图标名称
|
||||
component: '组件路径', // 相对于 views 目录的路径
|
||||
parentId: null, // null 表示顶级菜单
|
||||
sort: 1, // 排序值,越小越靠前
|
||||
children: [
|
||||
// 子菜单数组(可选)
|
||||
// ...
|
||||
],
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
## 🗑️ 清空现有菜单(可选)
|
||||
|
||||
如果需要清空所有现有菜单后重新创建,可以取消注释脚本中的以下代码:
|
||||
|
||||
```typescript
|
||||
// 清空现有菜单
|
||||
console.log('🗑️ 清空现有菜单...');
|
||||
await prisma.menu.deleteMany({});
|
||||
console.log('✅ 已清空现有菜单\n');
|
||||
```
|
||||
|
||||
**注意**: 清空菜单会删除所有现有菜单数据,请谨慎操作!
|
||||
|
||||
## 📊 执行结果示例
|
||||
|
||||
脚本执行成功后会显示:
|
||||
|
||||
```
|
||||
🚀 开始初始化菜单数据...
|
||||
|
||||
📝 创建菜单...
|
||||
|
||||
✓ 仪表盘 (/dashboard)
|
||||
✓ 系统管理 (/system)
|
||||
✓ 用户管理 (/system/users)
|
||||
✓ 角色管理 (/system/roles)
|
||||
✓ 菜单管理 (/system/menus)
|
||||
✓ 数据字典 (/system/dict)
|
||||
✓ 系统配置 (/system/config)
|
||||
✓ 日志记录 (/system/logs)
|
||||
|
||||
🔍 验证结果...
|
||||
|
||||
📊 初始化结果:
|
||||
顶级菜单数量: 2
|
||||
总菜单数量: 8
|
||||
|
||||
📋 菜单结构:
|
||||
├─ 仪表盘 (/dashboard)
|
||||
├─ 系统管理 (/system)
|
||||
│ ├─ 用户管理 (/system/users)
|
||||
│ ├─ 角色管理 (/system/roles)
|
||||
│ ├─ 菜单管理 (/system/menus)
|
||||
│ ├─ 数据字典 (/system/dict)
|
||||
│ ├─ 系统配置 (/system/config)
|
||||
│ └─ 日志记录 (/system/logs)
|
||||
|
||||
✅ 菜单初始化完成!
|
||||
|
||||
🎉 菜单初始化脚本执行完成!
|
||||
```
|
||||
|
||||
## 🔍 验证菜单数据
|
||||
|
||||
初始化完成后,可以通过以下方式验证:
|
||||
|
||||
### 方式一:使用 Prisma Studio
|
||||
|
||||
```bash
|
||||
pnpm prisma:studio
|
||||
```
|
||||
|
||||
在浏览器中打开 Prisma Studio,查看 `menus` 表的数据。
|
||||
|
||||
### 方式二:通过菜单管理页面
|
||||
|
||||
1. 登录系统
|
||||
2. 访问"系统管理" -> "菜单管理"
|
||||
3. 查看菜单列表,确认菜单已正确创建
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **数据库连接**: 确保 `.env` 文件中的 `DATABASE_URL` 配置正确
|
||||
2. **Prisma Client**: 确保已运行 `pnpm prisma:generate` 生成 Prisma Client
|
||||
3. **数据库迁移**: 确保已运行 `pnpm prisma:migrate` 创建数据库表结构
|
||||
4. **图标名称**: 图标名称必须是有效的 Ant Design Icons 组件名称
|
||||
5. **路径格式**: 路由路径必须以 `/` 开头
|
||||
6. **组件路径**: 组件路径是相对于 `frontend/src/views/` 目录的路径
|
||||
|
||||
## 🔗 相关文档
|
||||
|
||||
- [数据库配置指南](./DATABASE_SETUP.md)
|
||||
- [管理员账户初始化](./ADMIN_ACCOUNT.md)
|
||||
- [路由配置说明](../frontend/src/router/index.ts)
|
||||
312
backend/docs/MIGRATION_INCREMENTAL_GUIDE.md
Normal file
312
backend/docs/MIGRATION_INCREMENTAL_GUIDE.md
Normal file
@ -0,0 +1,312 @@
|
||||
# Prisma 增量迁移指南
|
||||
|
||||
## 📋 概述
|
||||
|
||||
Prisma 的迁移机制**已经内置了增量执行功能**。当你运行迁移命令时,Prisma 会自动:
|
||||
|
||||
- ✅ 只执行**新增的、未应用的**迁移
|
||||
- ✅ **跳过**已经执行过的迁移
|
||||
- ✅ 通过 `_prisma_migrations` 表跟踪迁移状态
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Prisma 如何跟踪迁移状态
|
||||
|
||||
Prisma 在数据库中维护一个特殊的表 `_prisma_migrations`,用于记录:
|
||||
|
||||
- 迁移名称(migration_name)
|
||||
- 应用时间(applied_at)
|
||||
- 迁移文件内容(checksum)
|
||||
- 其他元数据
|
||||
|
||||
每次迁移执行后,Prisma 会在这个表中记录一条记录,确保不会重复执行。
|
||||
|
||||
---
|
||||
|
||||
## 🚀 迁移命令对比
|
||||
|
||||
### 1. `prisma migrate deploy`(生产环境推荐)
|
||||
|
||||
**特点**:
|
||||
|
||||
- ✅ **只执行未应用的迁移**
|
||||
- ✅ 不会创建新迁移
|
||||
- ✅ 不会重置数据库
|
||||
- ✅ 适合生产环境
|
||||
|
||||
**使用场景**:
|
||||
|
||||
- 生产环境部署
|
||||
- CI/CD 流程
|
||||
- 多环境同步
|
||||
|
||||
**示例**:
|
||||
|
||||
```bash
|
||||
# 生产环境
|
||||
npm run prisma:migrate:deploy
|
||||
|
||||
# 或直接使用
|
||||
NODE_ENV=production prisma migrate deploy
|
||||
```
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
1. 读取 `prisma/migrations` 目录中的所有迁移文件
|
||||
2. 查询数据库中的 `_prisma_migrations` 表
|
||||
3. 对比找出未应用的迁移
|
||||
4. **只执行未应用的迁移**
|
||||
5. 在 `_prisma_migrations` 表中记录新应用的迁移
|
||||
|
||||
---
|
||||
|
||||
### 2. `prisma migrate dev`(开发环境推荐)
|
||||
|
||||
**特点**:
|
||||
|
||||
- ✅ 创建新迁移(如果有 schema 变更)
|
||||
- ✅ **只执行未应用的迁移**
|
||||
- ✅ 可能会重置开发数据库(如果使用 shadow database)
|
||||
- ✅ 适合开发环境
|
||||
|
||||
**使用场景**:
|
||||
|
||||
- 本地开发
|
||||
- Schema 变更后创建迁移
|
||||
|
||||
**示例**:
|
||||
|
||||
```bash
|
||||
# 开发环境
|
||||
npm run prisma:migrate
|
||||
|
||||
# 或直接使用
|
||||
prisma migrate dev
|
||||
```
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
1. 检查 schema.prisma 是否有变更
|
||||
2. 如果有变更,创建新迁移文件
|
||||
3. 查询 `_prisma_migrations` 表找出未应用的迁移
|
||||
4. **只执行未应用的迁移**(包括新创建的)
|
||||
5. 记录到 `_prisma_migrations` 表
|
||||
|
||||
---
|
||||
|
||||
## 📊 查看迁移状态
|
||||
|
||||
### 检查哪些迁移已应用
|
||||
|
||||
```bash
|
||||
# 查看迁移状态
|
||||
npx prisma migrate status
|
||||
|
||||
# 输出示例:
|
||||
# ✅ Database schema is up to date!
|
||||
#
|
||||
# The following migrations have been applied:
|
||||
# - 20251118035205_init
|
||||
# - 20251118041000_add_comments
|
||||
# - 20251118211424_change_log_content_to_text
|
||||
```
|
||||
|
||||
### 直接查询数据库
|
||||
|
||||
```sql
|
||||
-- 查看所有已应用的迁移
|
||||
SELECT * FROM _prisma_migrations ORDER BY applied_at DESC;
|
||||
|
||||
-- 查看迁移名称和状态
|
||||
SELECT migration_name, applied_at, finished_at
|
||||
FROM _prisma_migrations
|
||||
ORDER BY applied_at DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 实际使用场景
|
||||
|
||||
### 场景 1:生产环境部署
|
||||
|
||||
**情况**:生产数据库已经有部分迁移,现在要部署新版本
|
||||
|
||||
```bash
|
||||
# 1. 部署新代码(包含新的迁移文件)
|
||||
|
||||
# 2. 运行迁移(只会执行新增的迁移)
|
||||
npm run prisma:migrate:deploy
|
||||
|
||||
# Prisma 会自动:
|
||||
# - 检查 _prisma_migrations 表
|
||||
# - 找出未应用的迁移(如:20251120000000_new_feature)
|
||||
# - 只执行这个新迁移
|
||||
# - 跳过已执行的迁移(如:20251118035205_init)
|
||||
```
|
||||
|
||||
**结果**:
|
||||
|
||||
- ✅ 已执行的迁移不会重复执行
|
||||
- ✅ 只执行新增的迁移
|
||||
- ✅ 数据库结构同步到最新状态
|
||||
|
||||
---
|
||||
|
||||
### 场景 2:多环境同步
|
||||
|
||||
**情况**:开发环境有 3 个迁移,生产环境只有 2 个
|
||||
|
||||
```bash
|
||||
# 开发环境迁移:
|
||||
# - 20251118035205_init ✅
|
||||
# - 20251118041000_add_comments ✅
|
||||
# - 20251118211424_change_log_content_to_text ✅
|
||||
|
||||
# 生产环境迁移:
|
||||
# - 20251118035205_init ✅
|
||||
# - 20251118041000_add_comments ✅
|
||||
# - 20251118211424_change_log_content_to_text ❌(未应用)
|
||||
|
||||
# 在生产环境运行:
|
||||
npm run prisma:migrate:deploy
|
||||
|
||||
# Prisma 会:
|
||||
# - 跳过前两个已应用的迁移
|
||||
# - 只执行最后一个未应用的迁移
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景 3:回滚和修复
|
||||
|
||||
**情况**:某个迁移执行失败,需要修复
|
||||
|
||||
```bash
|
||||
# 1. 检查迁移状态
|
||||
npx prisma migrate status
|
||||
|
||||
# 2. 如果迁移失败,_prisma_migrations 表中不会有记录
|
||||
# 3. 修复迁移文件后,重新运行
|
||||
npm run prisma:migrate:deploy
|
||||
|
||||
# Prisma 会:
|
||||
# - 检查失败的迁移是否已记录
|
||||
# - 如果没有记录,会重新执行
|
||||
# - 如果已记录,会跳过
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
### 1. 不要手动修改 `_prisma_migrations` 表
|
||||
|
||||
这个表由 Prisma 自动管理,手动修改可能导致迁移状态不一致。
|
||||
|
||||
### 2. 迁移文件不要删除
|
||||
|
||||
即使迁移已执行,也不要删除 `prisma/migrations` 目录中的迁移文件。这些文件是迁移历史的一部分。
|
||||
|
||||
### 3. 生产环境使用 `migrate deploy`
|
||||
|
||||
```bash
|
||||
# ✅ 正确:生产环境
|
||||
prisma migrate deploy
|
||||
|
||||
# ❌ 错误:生产环境不要使用
|
||||
prisma migrate dev # 可能会重置数据库
|
||||
```
|
||||
|
||||
### 4. 迁移文件顺序很重要
|
||||
|
||||
Prisma 按照迁移文件名(时间戳)的顺序执行迁移。确保迁移文件名的时间戳顺序正确。
|
||||
|
||||
---
|
||||
|
||||
## 🔧 故障排查
|
||||
|
||||
### 问题 1:迁移状态不一致
|
||||
|
||||
**症状**:`prisma migrate status` 显示状态不一致
|
||||
|
||||
**解决**:
|
||||
|
||||
```bash
|
||||
# 1. 检查 _prisma_migrations 表
|
||||
SELECT * FROM _prisma_migrations;
|
||||
|
||||
# 2. 检查迁移文件
|
||||
ls -la prisma/migrations/
|
||||
|
||||
# 3. 如果迁移文件存在但未记录,手动标记(谨慎操作)
|
||||
# 或者重新运行迁移
|
||||
prisma migrate deploy
|
||||
```
|
||||
|
||||
### 问题 2:迁移重复执行
|
||||
|
||||
**症状**:迁移被重复执行
|
||||
|
||||
**原因**:`_prisma_migrations` 表中没有记录
|
||||
|
||||
**解决**:
|
||||
|
||||
```bash
|
||||
# 检查迁移记录
|
||||
npx prisma migrate status
|
||||
|
||||
# 如果显示迁移未应用,但数据库结构已存在
|
||||
# 可能需要手动标记迁移为已应用(谨慎操作)
|
||||
```
|
||||
|
||||
### 问题 3:迁移文件丢失
|
||||
|
||||
**症状**:迁移文件被删除,但数据库中有记录
|
||||
|
||||
**解决**:
|
||||
|
||||
```bash
|
||||
# 1. 从版本控制恢复迁移文件
|
||||
git checkout prisma/migrations/
|
||||
|
||||
# 2. 重新运行迁移检查
|
||||
npx prisma migrate status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 相关命令速查
|
||||
|
||||
| 命令 | 说明 | 使用场景 |
|
||||
| ----------------------- | ---------------------- | -------- |
|
||||
| `prisma migrate deploy` | 只执行未应用的迁移 | 生产环境 |
|
||||
| `prisma migrate dev` | 创建并执行迁移 | 开发环境 |
|
||||
| `prisma migrate status` | 查看迁移状态 | 所有环境 |
|
||||
| `prisma migrate reset` | 重置数据库(开发环境) | 开发环境 |
|
||||
| `prisma db push` | 直接同步 schema | 快速原型 |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 总结
|
||||
|
||||
**Prisma 迁移机制的核心特点**:
|
||||
|
||||
1. ✅ **自动增量执行**:只执行未应用的迁移
|
||||
2. ✅ **状态跟踪**:通过 `_prisma_migrations` 表跟踪
|
||||
3. ✅ **安全可靠**:不会重复执行已应用的迁移
|
||||
4. ✅ **环境区分**:`migrate deploy` 用于生产,`migrate dev` 用于开发
|
||||
|
||||
**最佳实践**:
|
||||
|
||||
- 🎯 生产环境:使用 `prisma migrate deploy`
|
||||
- 🎯 开发环境:使用 `prisma migrate dev`
|
||||
- 🎯 定期检查:使用 `prisma migrate status` 查看状态
|
||||
- 🎯 版本控制:提交所有迁移文件到 Git
|
||||
|
||||
---
|
||||
|
||||
## 🔗 相关文档
|
||||
|
||||
- [Prisma 官方迁移文档](https://www.prisma.io/docs/concepts/components/prisma-migrate)
|
||||
- [SCHEMA_CHANGE_GUIDE.md](./SCHEMA_CHANGE_GUIDE.md) - Schema 修改指南
|
||||
- [DATABASE_SETUP.md](./DATABASE_SETUP.md) - 数据库设置指南
|
||||
131
backend/docs/QUICK_START_ENV.md
Normal file
131
backend/docs/QUICK_START_ENV.md
Normal file
@ -0,0 +1,131 @@
|
||||
# 环境配置快速参考
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 创建开发环境配置
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
|
||||
# 创建开发环境配置文件
|
||||
cat > .env.development << 'EOF'
|
||||
NODE_ENV=development
|
||||
DATABASE_URL="mysql://root:password@localhost:3306/competition_management_dev?schema=public"
|
||||
JWT_SECRET="dev-secret-key"
|
||||
PORT=3001
|
||||
EOF
|
||||
```
|
||||
|
||||
### 2. 创建生产环境配置
|
||||
|
||||
```bash
|
||||
# 创建生产环境配置文件(不要提交到 Git)
|
||||
cat > .env.production << 'EOF'
|
||||
NODE_ENV=production
|
||||
DATABASE_URL="mysql://prod_user:strong_password@prod-host:3306/competition_management?schema=public&sslmode=require"
|
||||
JWT_SECRET="$(openssl rand -hex 32)"
|
||||
PORT=3001
|
||||
EOF
|
||||
```
|
||||
|
||||
### 3. 创建数据库
|
||||
|
||||
```sql
|
||||
-- 开发环境数据库
|
||||
CREATE DATABASE competition_management_dev
|
||||
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- 生产环境数据库
|
||||
CREATE DATABASE competition_management
|
||||
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
### 4. 初始化数据库
|
||||
|
||||
```bash
|
||||
# 开发环境
|
||||
pnpm prisma:generate
|
||||
pnpm prisma:migrate
|
||||
|
||||
# 生产环境(部署时)
|
||||
NODE_ENV=production pnpm prisma:migrate:deploy
|
||||
```
|
||||
|
||||
## 📋 环境区分总结
|
||||
|
||||
| 项目 | 开发环境 | 生产环境 |
|
||||
|------|---------|---------|
|
||||
| **配置文件** | `.env.development` | `.env.production` |
|
||||
| **数据库名** | `competition_management_dev` | `competition_management` |
|
||||
| **启动命令** | `pnpm start:dev` | `pnpm start:prod` |
|
||||
| **迁移命令** | `pnpm prisma:migrate` | `pnpm prisma:migrate:deploy` |
|
||||
| **Prisma Studio** | `pnpm prisma:studio:dev` | `pnpm prisma:studio:prod` |
|
||||
| **日志级别** | `debug` | `error` |
|
||||
| **CORS** | `*` (所有来源) | 指定域名 |
|
||||
| **SSL** | 可选 | 必须启用 |
|
||||
|
||||
## 🔑 关键区别
|
||||
|
||||
### 开发环境
|
||||
- ✅ 使用本地数据库
|
||||
- ✅ 简单的 JWT 密钥(便于开发)
|
||||
- ✅ 详细的日志输出
|
||||
- ✅ 允许所有 CORS 来源
|
||||
- ✅ 热重载支持
|
||||
|
||||
### 生产环境
|
||||
- ✅ 独立的数据库服务器
|
||||
- ✅ 强随机 JWT 密钥
|
||||
- ✅ 最小化日志输出
|
||||
- ✅ 限制 CORS 来源
|
||||
- ✅ 启用 SSL/TLS
|
||||
- ✅ 连接池优化
|
||||
|
||||
## 📝 配置文件示例
|
||||
|
||||
### `.env.development`
|
||||
```env
|
||||
NODE_ENV=development
|
||||
DATABASE_URL="mysql://root:password@localhost:3306/competition_management_dev?schema=public"
|
||||
JWT_SECRET="dev-secret-key"
|
||||
PORT=3001
|
||||
LOG_LEVEL=debug
|
||||
CORS_ORIGIN=*
|
||||
```
|
||||
|
||||
### `.env.production`
|
||||
```env
|
||||
NODE_ENV=production
|
||||
DATABASE_URL="mysql://prod_user:strong_password@prod-host:3306/competition_management?schema=public&sslmode=require"
|
||||
JWT_SECRET="your-production-secret-key-must-be-strong-and-random"
|
||||
PORT=3001
|
||||
LOG_LEVEL=error
|
||||
CORS_ORIGIN=https://yourdomain.com
|
||||
SSL_ENABLED=true
|
||||
DB_POOL_MIN=2
|
||||
DB_POOL_MAX=10
|
||||
```
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **不要提交 `.env` 文件到 Git**
|
||||
2. **生产环境必须使用强密码和 JWT_SECRET**
|
||||
3. **生产环境建议启用 SSL 连接**
|
||||
4. **定期备份生产数据库**
|
||||
5. **使用不同的数据库名称区分环境**
|
||||
|
||||
## 🔍 验证配置
|
||||
|
||||
```bash
|
||||
# 检查当前环境
|
||||
echo $NODE_ENV
|
||||
|
||||
# 验证数据库连接(开发环境)
|
||||
NODE_ENV=development pnpm prisma:studio
|
||||
|
||||
# 验证数据库连接(生产环境)
|
||||
NODE_ENV=production pnpm prisma:studio:prod
|
||||
```
|
||||
|
||||
更多详细信息请查看 [ENVIRONMENT_CONFIG.md](./ENVIRONMENT_CONFIG.md)
|
||||
|
||||
444
backend/docs/RBAC_EXAMPLES.md
Normal file
444
backend/docs/RBAC_EXAMPLES.md
Normal file
@ -0,0 +1,444 @@
|
||||
# RBAC 权限控制使用示例
|
||||
|
||||
## 📋 目录
|
||||
1. [基础使用](#基础使用)
|
||||
2. [角色控制示例](#角色控制示例)
|
||||
3. [权限控制示例](#权限控制示例)
|
||||
4. [完整示例](#完整示例)
|
||||
|
||||
## 🔧 基础使用
|
||||
|
||||
### 1. 创建权限
|
||||
|
||||
```typescript
|
||||
// 在数据库中创建权限
|
||||
const permissions = [
|
||||
{ code: 'user:create', resource: 'user', action: 'create', name: '创建用户' },
|
||||
{ code: 'user:read', resource: 'user', action: 'read', name: '查看用户' },
|
||||
{ code: 'user:update', resource: 'user', action: 'update', name: '更新用户' },
|
||||
{ code: 'user:delete', resource: 'user', action: 'delete', name: '删除用户' },
|
||||
{ code: 'role:create', resource: 'role', action: 'create', name: '创建角色' },
|
||||
{ code: 'role:read', resource: 'role', action: 'read', name: '查看角色' },
|
||||
];
|
||||
|
||||
for (const perm of permissions) {
|
||||
await prisma.permission.create({ data: perm });
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 创建角色并分配权限
|
||||
|
||||
```typescript
|
||||
// 创建管理员角色
|
||||
const adminRole = await prisma.role.create({
|
||||
data: {
|
||||
name: '管理员',
|
||||
code: 'admin',
|
||||
permissions: {
|
||||
create: [
|
||||
{ permission: { connect: { code: 'user:create' } } },
|
||||
{ permission: { connect: { code: 'user:read' } } },
|
||||
{ permission: { connect: { code: 'user:update' } } },
|
||||
{ permission: { connect: { code: 'user:delete' } } },
|
||||
{ permission: { connect: { code: 'role:create' } } },
|
||||
{ permission: { connect: { code: 'role:read' } } },
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 创建编辑角色(只有查看和更新权限)
|
||||
const editorRole = await prisma.role.create({
|
||||
data: {
|
||||
name: '编辑',
|
||||
code: 'editor',
|
||||
permissions: {
|
||||
create: [
|
||||
{ permission: { connect: { code: 'user:read' } } },
|
||||
{ permission: { connect: { code: 'user:update' } } },
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 给用户分配角色
|
||||
|
||||
```typescript
|
||||
// 给用户分配管理员角色
|
||||
await prisma.userRole.create({
|
||||
data: {
|
||||
user: { connect: { id: 1 } },
|
||||
role: { connect: { code: 'admin' } }
|
||||
}
|
||||
});
|
||||
|
||||
// 用户可以有多个角色
|
||||
await prisma.userRole.create({
|
||||
data: {
|
||||
user: { connect: { id: 1 } },
|
||||
role: { connect: { code: 'editor' } }
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 🎯 角色控制示例
|
||||
|
||||
### 在控制器中使用角色装饰器
|
||||
|
||||
```typescript
|
||||
import { Controller, Get, Post, Delete, UseGuards } from '@nestjs/common';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
|
||||
@Controller('users')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard) // 先验证 JWT,再验证角色
|
||||
export class UsersController {
|
||||
|
||||
// 所有已登录用户都可以查看
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.usersService.findAll();
|
||||
}
|
||||
|
||||
// 只有管理员和编辑可以创建用户
|
||||
@Post()
|
||||
@Roles('admin', 'editor')
|
||||
create(@Body() createUserDto: CreateUserDto) {
|
||||
return this.usersService.create(createUserDto);
|
||||
}
|
||||
|
||||
// 只有管理员可以删除用户
|
||||
@Delete(':id')
|
||||
@Roles('admin')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.usersService.remove(+id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔐 权限控制示例
|
||||
|
||||
### 创建权限守卫(可选扩展)
|
||||
|
||||
```typescript
|
||||
// src/auth/guards/permissions.guard.ts
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
|
||||
@Injectable()
|
||||
export class PermissionsGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const requiredPermissions = this.reflector.getAllAndOverride<string[]>(
|
||||
'permissions',
|
||||
[context.getHandler(), context.getClass()],
|
||||
);
|
||||
|
||||
if (!requiredPermissions) {
|
||||
return true; // 没有权限要求,允许访问
|
||||
}
|
||||
|
||||
const { user } = context.switchToHttp().getRequest();
|
||||
const userPermissions = user.permissions || [];
|
||||
|
||||
// 检查用户是否拥有任一所需权限
|
||||
return requiredPermissions.some((permission) =>
|
||||
userPermissions.includes(permission),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 创建权限装饰器
|
||||
|
||||
```typescript
|
||||
// src/auth/decorators/permissions.decorator.ts
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const PERMISSIONS_KEY = 'permissions';
|
||||
export const Permissions = (...permissions: string[]) =>
|
||||
SetMetadata(PERMISSIONS_KEY, permissions);
|
||||
```
|
||||
|
||||
### 使用权限控制
|
||||
|
||||
```typescript
|
||||
import { Controller, Get, Post, Delete, UseGuards } from '@nestjs/common';
|
||||
import { Permissions } from '../auth/decorators/permissions.decorator';
|
||||
import { PermissionsGuard } from '../auth/guards/permissions.guard';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
|
||||
@Controller('users')
|
||||
@UseGuards(JwtAuthGuard, PermissionsGuard)
|
||||
export class UsersController {
|
||||
|
||||
@Get()
|
||||
@Permissions('user:read') // 需要 user:read 权限
|
||||
findAll() {
|
||||
return this.usersService.findAll();
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Permissions('user:create') // 需要 user:create 权限
|
||||
create(@Body() createUserDto: CreateUserDto) {
|
||||
return this.usersService.create(createUserDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Permissions('user:delete') // 需要 user:delete 权限
|
||||
remove(@Param('id') id: string) {
|
||||
return this.usersService.remove(+id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📚 完整示例
|
||||
|
||||
### 完整的用户管理控制器
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
Query,
|
||||
UseGuards,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import { UsersService } from './users.service';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
|
||||
@Controller('users')
|
||||
@UseGuards(JwtAuthGuard) // 所有接口都需要登录
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
// 查看用户列表 - 所有已登录用户都可以访问
|
||||
@Get()
|
||||
findAll(@Query('page') page?: string, @Query('pageSize') pageSize?: string) {
|
||||
return this.usersService.findAll(
|
||||
page ? parseInt(page) : 1,
|
||||
pageSize ? parseInt(pageSize) : 10,
|
||||
);
|
||||
}
|
||||
|
||||
// 查看用户详情 - 所有已登录用户都可以访问
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.usersService.findOne(+id);
|
||||
}
|
||||
|
||||
// 创建用户 - 需要 admin 或 editor 角色
|
||||
@Post()
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles('admin', 'editor')
|
||||
create(@Body() createUserDto: CreateUserDto, @Request() req) {
|
||||
// req.user 包含当前用户信息(从 JWT 中提取)
|
||||
return this.usersService.create(createUserDto);
|
||||
}
|
||||
|
||||
// 更新用户 - 需要 admin 角色,或者用户自己更新自己
|
||||
@Patch(':id')
|
||||
@UseGuards(RolesGuard)
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() updateUserDto: UpdateUserDto,
|
||||
@Request() req,
|
||||
) {
|
||||
const userId = parseInt(id);
|
||||
const currentUserId = req.user.userId;
|
||||
|
||||
// 管理员可以更新任何人,普通用户只能更新自己
|
||||
if (req.user.roles?.includes('admin') || userId === currentUserId) {
|
||||
return this.usersService.update(userId, updateUserDto);
|
||||
}
|
||||
|
||||
throw new ForbiddenException('无权更新此用户');
|
||||
}
|
||||
|
||||
// 删除用户 - 只有管理员可以删除
|
||||
@Delete(':id')
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles('admin')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.usersService.remove(+id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔍 权限检查流程
|
||||
|
||||
### 1. 用户登录
|
||||
|
||||
```typescript
|
||||
// POST /api/auth/login
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "password123"
|
||||
}
|
||||
|
||||
// 返回
|
||||
{
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "admin",
|
||||
"nickname": "管理员",
|
||||
"roles": ["admin"], // 用户的角色列表
|
||||
"permissions": [ // 用户的所有权限(从角色中聚合)
|
||||
"user:create",
|
||||
"user:read",
|
||||
"user:update",
|
||||
"user:delete",
|
||||
"role:create",
|
||||
"role:read"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 访问受保护的接口
|
||||
|
||||
```typescript
|
||||
// 请求头
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
|
||||
// 流程
|
||||
1. JwtAuthGuard 验证 Token
|
||||
└─> 提取用户信息,添加到 req.user
|
||||
|
||||
2. RolesGuard 检查角色
|
||||
└─> 从 req.user.roles 中检查是否包含所需角色
|
||||
└─> 如果包含,允许访问;否则返回 403 Forbidden
|
||||
```
|
||||
|
||||
## 🎨 前端权限控制示例
|
||||
|
||||
### Vue 3 中使用权限
|
||||
|
||||
```typescript
|
||||
// stores/auth.ts
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const user = ref<User | null>(null);
|
||||
|
||||
// 检查是否有指定角色
|
||||
const hasRole = (role: string) => {
|
||||
return user.value?.roles?.includes(role) ?? false;
|
||||
};
|
||||
|
||||
// 检查是否有指定权限
|
||||
const hasPermission = (permission: string) => {
|
||||
return user.value?.permissions?.includes(permission) ?? false;
|
||||
};
|
||||
|
||||
// 检查是否有任一角色
|
||||
const hasAnyRole = (roles: string[]) => {
|
||||
return roles.some(role => hasRole(role));
|
||||
};
|
||||
|
||||
// 检查是否有任一权限
|
||||
const hasAnyPermission = (permissions: string[]) => {
|
||||
return permissions.some(perm => hasPermission(perm));
|
||||
};
|
||||
|
||||
return {
|
||||
user,
|
||||
hasRole,
|
||||
hasPermission,
|
||||
hasAnyRole,
|
||||
hasAnyPermission,
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### 在组件中使用
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<!-- 根据角色显示按钮 -->
|
||||
<a-button v-if="authStore.hasRole('admin')" @click="deleteUser">
|
||||
删除用户
|
||||
</a-button>
|
||||
|
||||
<!-- 根据权限显示按钮 -->
|
||||
<a-button v-if="authStore.hasPermission('user:create')" @click="createUser">
|
||||
创建用户
|
||||
</a-button>
|
||||
|
||||
<!-- 根据角色或权限显示 -->
|
||||
<a-button
|
||||
v-if="authStore.hasAnyRole(['admin', 'editor']) || authStore.hasPermission('user:update')"
|
||||
@click="editUser"
|
||||
>
|
||||
编辑用户
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
</script>
|
||||
```
|
||||
|
||||
### 路由守卫
|
||||
|
||||
```typescript
|
||||
// router/index.ts
|
||||
router.beforeEach((to, from, next) => {
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// 检查是否需要认证
|
||||
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
|
||||
next({ name: 'Login' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查角色
|
||||
if (to.meta.roles && !authStore.hasAnyRole(to.meta.roles)) {
|
||||
next({ name: 'Forbidden' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (to.meta.permissions && !authStore.hasAnyPermission(to.meta.permissions)) {
|
||||
next({ name: 'Forbidden' });
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
```
|
||||
|
||||
## 📊 权限矩阵示例
|
||||
|
||||
| 角色 | user:create | user:read | user:update | user:delete | role:create | role:read |
|
||||
|------|-------------|-----------|-------------|------------|-------------|-----------|
|
||||
| admin | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| editor | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
|
||||
| viewer | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
|
||||
## 🎯 总结
|
||||
|
||||
RBAC 权限控制的核心是:
|
||||
|
||||
1. **用户** ←→ **角色** ←→ **权限**
|
||||
2. 通过 `@Roles()` 装饰器控制接口访问
|
||||
3. 前端根据返回的 `roles` 和 `permissions` 控制 UI 显示
|
||||
4. 权限由 `resource:action` 组成,如 `user:create`
|
||||
|
||||
这样的设计既保证了安全性,又提供了良好的灵活性和可维护性!
|
||||
|
||||
397
backend/docs/RBAC_GUIDE.md
Normal file
397
backend/docs/RBAC_GUIDE.md
Normal file
@ -0,0 +1,397 @@
|
||||
# RBAC 权限控制详解
|
||||
|
||||
## 📚 什么是 RBAC?
|
||||
|
||||
**RBAC(Role-Based Access Control)** 即**基于角色的访问控制**,是一种权限管理模型。它的核心思想是:
|
||||
|
||||
> **用户 → 角色 → 权限**
|
||||
|
||||
通过给用户分配角色,角色拥有权限,从而间接地给用户授予权限。
|
||||
|
||||
## 🎯 RBAC 的核心概念
|
||||
|
||||
### 1. **用户(User)**
|
||||
|
||||
系统中的实际使用者,如:张三、李四
|
||||
|
||||
### 2. **角色(Role)**
|
||||
|
||||
一组权限的集合,如:管理员、编辑、访客
|
||||
|
||||
### 3. **权限(Permission)**
|
||||
|
||||
对资源的操作能力,如:创建用户、删除文章、查看报表
|
||||
|
||||
### 4. **资源(Resource)**
|
||||
|
||||
系统中的实体对象,如:用户、文章、订单
|
||||
|
||||
### 5. **操作(Action)**
|
||||
|
||||
对资源的操作类型,如:create(创建)、read(查看)、update(更新)、delete(删除)
|
||||
|
||||
## 🏗️ 项目中的 RBAC 架构
|
||||
|
||||
### 数据模型关系
|
||||
|
||||
```
|
||||
User (用户)
|
||||
↓ (多对多)
|
||||
UserRole (用户角色关联)
|
||||
↓
|
||||
Role (角色)
|
||||
↓ (多对多)
|
||||
RolePermission (角色权限关联)
|
||||
↓
|
||||
Permission (权限)
|
||||
├─ resource: 资源名称 (如: user, role, menu)
|
||||
└─ action: 操作类型 (如: create, read, update, delete)
|
||||
```
|
||||
|
||||
### 数据库表结构
|
||||
|
||||
#### 1. **users** - 用户表
|
||||
|
||||
存储系统用户的基本信息
|
||||
|
||||
#### 2. **roles** - 角色表
|
||||
|
||||
存储角色信息,如:
|
||||
|
||||
- `admin` - 管理员
|
||||
- `editor` - 编辑
|
||||
- `viewer` - 查看者
|
||||
|
||||
#### 3. **permissions** - 权限表
|
||||
|
||||
存储权限信息,权限由 `resource` + `action` 组成,如:
|
||||
|
||||
- `user:create` - 创建用户
|
||||
- `user:read` - 查看用户
|
||||
- `user:update` - 更新用户
|
||||
- `user:delete` - 删除用户
|
||||
- `role:create` - 创建角色
|
||||
- `menu:read` - 查看菜单
|
||||
|
||||
#### 4. **user_roles** - 用户角色关联表
|
||||
|
||||
用户和角色的多对多关系
|
||||
|
||||
#### 5. **role_permissions** - 角色权限关联表
|
||||
|
||||
角色和权限的多对多关系
|
||||
|
||||
## 🔄 RBAC 工作流程
|
||||
|
||||
### 1. **权限分配流程**
|
||||
|
||||
```
|
||||
1. 创建权限
|
||||
└─> 定义资源(resource)和操作(action)
|
||||
└─> 例如:user:create, user:read
|
||||
|
||||
2. 创建角色
|
||||
└─> 给角色分配权限
|
||||
└─> 例如:管理员角色 = [user:create, user:read, user:update, user:delete]
|
||||
|
||||
3. 给用户分配角色
|
||||
└─> 用户继承角色的所有权限
|
||||
└─> 例如:张三 = 管理员角色
|
||||
```
|
||||
|
||||
### 2. **权限验证流程**
|
||||
|
||||
```
|
||||
用户请求 API
|
||||
↓
|
||||
JWT 认证(验证用户身份)
|
||||
↓
|
||||
提取用户信息(包含 roles 和 permissions)
|
||||
↓
|
||||
RolesGuard 检查(检查用户是否有指定角色)
|
||||
↓
|
||||
PermissionGuard 检查(检查用户是否有指定权限)
|
||||
↓
|
||||
允许/拒绝访问
|
||||
```
|
||||
|
||||
## 💻 代码实现示例
|
||||
|
||||
### 1. **定义权限**
|
||||
|
||||
权限由 `resource` + `action` 组成:
|
||||
|
||||
```typescript
|
||||
// 权限示例
|
||||
{
|
||||
code: 'user:create', // 权限编码
|
||||
resource: 'user', // 资源:用户
|
||||
action: 'create', // 操作:创建
|
||||
name: '创建用户',
|
||||
description: '允许创建新用户'
|
||||
}
|
||||
|
||||
{
|
||||
code: 'user:read',
|
||||
resource: 'user',
|
||||
action: 'read',
|
||||
name: '查看用户',
|
||||
description: '允许查看用户列表和详情'
|
||||
}
|
||||
```
|
||||
|
||||
### 2. **创建角色并分配权限**
|
||||
|
||||
```typescript
|
||||
// 创建管理员角色
|
||||
const adminRole = await prisma.role.create({
|
||||
data: {
|
||||
name: '管理员',
|
||||
code: 'admin',
|
||||
permissions: {
|
||||
create: [
|
||||
{ permission: { connect: { code: 'user:create' } } },
|
||||
{ permission: { connect: { code: 'user:read' } } },
|
||||
{ permission: { connect: { code: 'user:update' } } },
|
||||
{ permission: { connect: { code: 'user:delete' } } },
|
||||
{ permission: { connect: { code: 'role:create' } } },
|
||||
// ... 更多权限
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 3. **给用户分配角色**
|
||||
|
||||
```typescript
|
||||
// 给用户分配管理员角色
|
||||
await prisma.userRole.create({
|
||||
data: {
|
||||
user: { connect: { id: userId } },
|
||||
role: { connect: { code: 'admin' } },
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 4. **在控制器中使用权限控制**
|
||||
|
||||
#### 方式一:使用角色装饰器
|
||||
|
||||
```typescript
|
||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||
|
||||
@Controller('users')
|
||||
@UseGuards(RolesGuard)
|
||||
export class UsersController {
|
||||
@Get()
|
||||
@Roles('admin', 'editor') // 需要 admin 或 editor 角色
|
||||
findAll() {
|
||||
// 只有拥有 admin 或 editor 角色的用户才能访问
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Roles('admin') // 只有 admin 角色可以删除
|
||||
remove() {
|
||||
// 只有管理员可以删除用户
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 方式二:使用权限装饰器(可扩展)
|
||||
|
||||
```typescript
|
||||
// 可以创建 PermissionGuard 和 @Permissions() 装饰器
|
||||
@Get()
|
||||
@Permissions('user:read') // 需要 user:read 权限
|
||||
findAll() {
|
||||
// 只有拥有 user:read 权限的用户才能访问
|
||||
}
|
||||
```
|
||||
|
||||
### 5. **获取用户权限**
|
||||
|
||||
```typescript
|
||||
// 在 AuthService 中
|
||||
private async getUserPermissions(userId: number): Promise<string[]> {
|
||||
const user = await this.usersService.findOne(userId);
|
||||
if (!user) return [];
|
||||
|
||||
const permissions = new Set<string>();
|
||||
|
||||
// 遍历用户的所有角色
|
||||
user.roles?.forEach((ur: any) => {
|
||||
// 遍历角色的所有权限
|
||||
ur.role.permissions?.forEach((rp: any) => {
|
||||
permissions.add(rp.permission.code);
|
||||
});
|
||||
});
|
||||
|
||||
return Array.from(permissions);
|
||||
// 返回: ['user:create', 'user:read', 'user:update', 'role:create', ...]
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 RBAC 的优势
|
||||
|
||||
### 1. **灵活性**
|
||||
|
||||
- ✅ 一个用户可以有多个角色
|
||||
- ✅ 一个角色可以有多个权限
|
||||
- ✅ 权限可以动态分配和回收
|
||||
|
||||
### 2. **可维护性**
|
||||
|
||||
- ✅ 权限变更只需修改角色,不需要逐个修改用户
|
||||
- ✅ 角色可以复用,减少重复配置
|
||||
|
||||
### 3. **可扩展性**
|
||||
|
||||
- ✅ 新增资源只需添加新的权限
|
||||
- ✅ 新增角色只需组合现有权限
|
||||
|
||||
### 4. **安全性**
|
||||
|
||||
- ✅ 最小权限原则:用户只获得必要的权限
|
||||
- ✅ 权限集中管理,便于审计
|
||||
|
||||
## 🎨 实际应用场景
|
||||
|
||||
### 场景 1:内容管理系统
|
||||
|
||||
```
|
||||
角色定义:
|
||||
- 超级管理员:所有权限
|
||||
- 内容管理员:文章 CRUD、评论管理
|
||||
- 编辑:文章创建、编辑
|
||||
- 作者:文章创建
|
||||
- 访客:文章查看
|
||||
|
||||
权限示例:
|
||||
- article:create
|
||||
- article:read
|
||||
- article:update
|
||||
- article:delete
|
||||
- comment:moderate
|
||||
```
|
||||
|
||||
### 场景 2:电商系统
|
||||
|
||||
```
|
||||
角色定义:
|
||||
- 平台管理员:所有权限
|
||||
- 店铺管理员:店铺管理、订单管理
|
||||
- 客服:订单查看、退款处理
|
||||
- 财务:订单查看、财务报表
|
||||
|
||||
权限示例:
|
||||
- order:create
|
||||
- order:read
|
||||
- order:update
|
||||
- order:refund
|
||||
- report:financial
|
||||
```
|
||||
|
||||
## 🔐 项目中的权限控制实现
|
||||
|
||||
### 1. **JWT 认证**
|
||||
|
||||
用户登录后获得 JWT Token,Token 中包含用户 ID
|
||||
|
||||
### 2. **JwtAuthGuard**
|
||||
|
||||
验证 JWT Token,提取用户信息
|
||||
|
||||
### 3. **RolesGuard**
|
||||
|
||||
检查用户是否拥有指定的角色
|
||||
|
||||
### 4. **权限获取**
|
||||
|
||||
登录时,系统会:
|
||||
|
||||
1. 查询用户的所有角色
|
||||
2. 查询角色关联的所有权限
|
||||
3. 合并所有权限并返回给前端
|
||||
|
||||
### 5. **前端权限控制**
|
||||
|
||||
前端可以根据返回的 `roles` 和 `permissions` 数组:
|
||||
|
||||
- 控制菜单显示
|
||||
- 控制按钮显示
|
||||
- 控制路由访问
|
||||
|
||||
## 📝 最佳实践
|
||||
|
||||
### 1. **权限命名规范**
|
||||
|
||||
```
|
||||
格式:resource:action
|
||||
示例:
|
||||
- user:create
|
||||
- user:read
|
||||
- user:update
|
||||
- user:delete
|
||||
- role:assign
|
||||
- menu:manage
|
||||
```
|
||||
|
||||
### 2. **角色命名规范**
|
||||
|
||||
```
|
||||
使用有意义的英文代码:
|
||||
- admin: 管理员
|
||||
- editor: 编辑
|
||||
- viewer: 查看者
|
||||
- guest: 访客
|
||||
```
|
||||
|
||||
### 3. **权限粒度**
|
||||
|
||||
- ✅ 不要过粗:避免一个权限包含太多操作
|
||||
- ✅ 不要过细:避免权限过多难以管理
|
||||
- ✅ 按业务模块划分:user、role、menu、dict 等
|
||||
|
||||
### 4. **默认角色**
|
||||
|
||||
建议创建以下默认角色:
|
||||
|
||||
- **超级管理员**:拥有所有权限
|
||||
- **普通用户**:基础查看权限
|
||||
- **访客**:只读权限
|
||||
|
||||
## 🚀 扩展功能
|
||||
|
||||
### 1. **权限继承**
|
||||
|
||||
可以实现角色继承,子角色继承父角色的权限
|
||||
|
||||
### 2. **动态权限**
|
||||
|
||||
可以根据数据范围动态控制权限,如:
|
||||
|
||||
- 用户只能管理自己创建的订单
|
||||
- 部门管理员只能管理本部门的用户
|
||||
|
||||
### 3. **权限缓存**
|
||||
|
||||
将用户权限缓存到 Redis,提高性能
|
||||
|
||||
### 4. **权限审计**
|
||||
|
||||
记录权限变更日志,便于追溯
|
||||
|
||||
## 📖 总结
|
||||
|
||||
RBAC 权限控制通过 **用户 → 角色 → 权限** 的三层关系,实现了灵活、可维护的权限管理系统。在你的项目中:
|
||||
|
||||
1. ✅ **用户** 通过 `user_roles` 表关联 **角色**
|
||||
2. ✅ **角色** 通过 `role_permissions` 表关联 **权限**
|
||||
3. ✅ **权限** 由 `resource` + `action` 组成
|
||||
4. ✅ 使用 `@Roles()` 装饰器控制接口访问
|
||||
5. ✅ 登录时返回用户的角色和权限列表
|
||||
|
||||
这样的设计既保证了安全性,又提供了良好的扩展性和可维护性!
|
||||
105
backend/docs/README.md
Normal file
105
backend/docs/README.md
Normal file
@ -0,0 +1,105 @@
|
||||
# 项目文档索引
|
||||
|
||||
本目录包含项目后端的所有指南和文档。
|
||||
|
||||
## 📚 文档分类
|
||||
|
||||
### 🚀 快速开始
|
||||
|
||||
- **[QUICK_START_ENV.md](./QUICK_START_ENV.md)** - 环境配置快速参考
|
||||
- 快速创建开发和生产环境配置
|
||||
- 环境区分总结表
|
||||
- 关键区别说明
|
||||
|
||||
### 🗄️ 数据库相关
|
||||
|
||||
- **[DATABASE_SETUP.md](./DATABASE_SETUP.md)** - 数据库配置指南
|
||||
- 创建数据库
|
||||
- DATABASE_URL 格式说明
|
||||
- 初始化数据库步骤
|
||||
- 验证连接方法
|
||||
|
||||
- **[DATABASE_URL_SOURCE.md](./DATABASE_URL_SOURCE.md)** - DATABASE_URL 来源说明
|
||||
- DATABASE_URL 的定义位置
|
||||
- 加载流程详解
|
||||
- 配置文件优先级
|
||||
- 验证方法
|
||||
|
||||
- **[SCHEMA_CHANGE_GUIDE.md](./SCHEMA_CHANGE_GUIDE.md)** - Prisma Schema 修改指南
|
||||
- 修改 schema.prisma 后的操作步骤
|
||||
- 生成 Prisma Client
|
||||
- 应用数据库迁移
|
||||
- 验证迁移是否成功
|
||||
|
||||
- **[ENV_CHANGE_GUIDE.md](./ENV_CHANGE_GUIDE.md)** - 修改 DATABASE_URL 后的操作指南
|
||||
- 操作决策树
|
||||
- 不同场景的处理方法
|
||||
- 完整操作流程
|
||||
- 常见错误解决
|
||||
|
||||
### ⚙️ 环境配置
|
||||
|
||||
- **[ENVIRONMENT_CONFIG.md](./ENVIRONMENT_CONFIG.md)** - 环境配置指南
|
||||
- 环境区分方案
|
||||
- 配置文件结构
|
||||
- 配置优先级
|
||||
- 开发/生产环境配置示例
|
||||
- 安全注意事项
|
||||
|
||||
### 🔐 权限管理
|
||||
|
||||
- **[RBAC_GUIDE.md](./RBAC_GUIDE.md)** - RBAC 权限系统指南
|
||||
- 权限系统架构
|
||||
- 权限模型说明
|
||||
- 使用示例
|
||||
- 最佳实践
|
||||
|
||||
- **[RBAC_EXAMPLES.md](./RBAC_EXAMPLES.md)** - RBAC 使用示例
|
||||
- 完整的权限配置示例
|
||||
- 常见场景实现
|
||||
- 代码示例
|
||||
|
||||
### 👤 账户管理
|
||||
|
||||
- **[ADMIN_ACCOUNT.md](./ADMIN_ACCOUNT.md)** - 管理员账户指南
|
||||
- 初始化管理员账户
|
||||
- 验证管理员账户
|
||||
- 账户管理说明
|
||||
|
||||
## 📖 文档使用建议
|
||||
|
||||
### 新项目设置流程
|
||||
|
||||
1. **环境配置** → [QUICK_START_ENV.md](./QUICK_START_ENV.md)
|
||||
2. **数据库设置** → [DATABASE_SETUP.md](./DATABASE_SETUP.md)
|
||||
3. **初始化管理员** → [ADMIN_ACCOUNT.md](./ADMIN_ACCOUNT.md)
|
||||
4. **权限配置** → [RBAC_GUIDE.md](./RBAC_GUIDE.md)
|
||||
|
||||
### 日常开发流程
|
||||
|
||||
- **修改数据库结构** → [SCHEMA_CHANGE_GUIDE.md](./SCHEMA_CHANGE_GUIDE.md)
|
||||
- **修改环境变量** → [ENV_CHANGE_GUIDE.md](./ENV_CHANGE_GUIDE.md)
|
||||
- **配置权限** → [RBAC_EXAMPLES.md](./RBAC_EXAMPLES.md)
|
||||
|
||||
### 问题排查
|
||||
|
||||
- **数据库连接问题** → [DATABASE_URL_SOURCE.md](./DATABASE_URL_SOURCE.md)
|
||||
- **环境配置问题** → [ENVIRONMENT_CONFIG.md](./ENVIRONMENT_CONFIG.md)
|
||||
- **迁移问题** → [SCHEMA_CHANGE_GUIDE.md](./SCHEMA_CHANGE_GUIDE.md)
|
||||
|
||||
## 🔍 快速查找
|
||||
|
||||
| 需求 | 文档 |
|
||||
|------|------|
|
||||
| 如何设置开发环境? | [QUICK_START_ENV.md](./QUICK_START_ENV.md) |
|
||||
| 如何配置数据库? | [DATABASE_SETUP.md](./DATABASE_SETUP.md) |
|
||||
| DATABASE_URL 从哪里来? | [DATABASE_URL_SOURCE.md](./DATABASE_URL_SOURCE.md) |
|
||||
| 修改 schema 后做什么? | [SCHEMA_CHANGE_GUIDE.md](./SCHEMA_CHANGE_GUIDE.md) |
|
||||
| 修改环境变量后做什么? | [ENV_CHANGE_GUIDE.md](./ENV_CHANGE_GUIDE.md) |
|
||||
| 如何配置权限? | [RBAC_GUIDE.md](./RBAC_GUIDE.md) |
|
||||
| 如何创建管理员? | [ADMIN_ACCOUNT.md](./ADMIN_ACCOUNT.md) |
|
||||
|
||||
## 📝 文档更新记录
|
||||
|
||||
- 2024-11-19: 创建文档索引,归档所有指南文件
|
||||
|
||||
128
backend/docs/SCHEMA_CHANGE_GUIDE.md
Normal file
128
backend/docs/SCHEMA_CHANGE_GUIDE.md
Normal file
@ -0,0 +1,128 @@
|
||||
# Prisma Schema 修改后的操作指南
|
||||
|
||||
## 修改 schema.prisma 后需要执行的步骤
|
||||
|
||||
### 1. 生成 Prisma Client(必须)
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
npx prisma generate
|
||||
# 或使用 npm script
|
||||
npm run prisma:generate
|
||||
```
|
||||
|
||||
**作用**:根据最新的 schema 重新生成 Prisma Client,使 TypeScript 类型和代码与数据库结构同步。
|
||||
|
||||
---
|
||||
|
||||
### 2. 应用数据库迁移(必须)
|
||||
|
||||
根据环境选择不同的方式:
|
||||
|
||||
#### 开发环境(推荐)
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
npx prisma migrate dev
|
||||
# 或使用 npm script
|
||||
npm run prisma:migrate
|
||||
```
|
||||
|
||||
**作用**:
|
||||
|
||||
- 应用待执行的迁移到数据库
|
||||
- 如果有新的迁移,会自动创建并应用
|
||||
- 会重置开发数据库(如果使用 shadow database)
|
||||
|
||||
#### 生产环境
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
npx prisma migrate deploy
|
||||
# 或使用 npm script
|
||||
npm run prisma:migrate:deploy
|
||||
```
|
||||
|
||||
**作用**:
|
||||
|
||||
- 仅应用待执行的迁移,不会创建新迁移
|
||||
- 不会重置数据库
|
||||
- 适合生产环境使用
|
||||
|
||||
#### 快速同步(仅开发环境,不推荐用于生产)
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
npx prisma db push
|
||||
```
|
||||
|
||||
**作用**:
|
||||
|
||||
- 直接将 schema 变更推送到数据库
|
||||
- 不创建迁移文件
|
||||
- 适合快速原型开发
|
||||
|
||||
---
|
||||
|
||||
### 3. 重启应用(如果正在运行)
|
||||
|
||||
应用迁移后,需要重启 NestJS 应用以加载新的 Prisma Client:
|
||||
|
||||
```bash
|
||||
# 如果使用 npm run start:dev,会自动重启
|
||||
# 如果使用其他方式启动,需要手动重启
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 当前状态
|
||||
|
||||
✅ **已完成**:
|
||||
|
||||
- schema.prisma 已修改(content 字段改为 TEXT)
|
||||
- 迁移文件已创建:`20251118211424_change_log_content_to_text`
|
||||
|
||||
⏳ **待执行**:
|
||||
|
||||
1. 生成 Prisma Client
|
||||
2. 应用数据库迁移
|
||||
3. 重启应用(如果正在运行)
|
||||
|
||||
---
|
||||
|
||||
## 执行顺序
|
||||
|
||||
```bash
|
||||
# 1. 生成 Prisma Client
|
||||
cd backend
|
||||
npx prisma generate
|
||||
|
||||
# 2. 应用迁移(开发环境)
|
||||
npx prisma migrate dev
|
||||
# 或生产环境
|
||||
npx prisma migrate deploy
|
||||
|
||||
# 3. 重启应用(如果需要)
|
||||
# 如果使用 start:dev,会自动重启
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验证迁移是否成功
|
||||
|
||||
```bash
|
||||
# 检查迁移状态
|
||||
npx prisma migrate status
|
||||
|
||||
# 查看数据库结构
|
||||
npx prisma studio
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **生产环境**:务必使用 `prisma migrate deploy`,不要使用 `prisma migrate dev`
|
||||
2. **备份数据**:在生产环境应用迁移前,建议先备份数据库
|
||||
3. **迁移冲突**:如果迁移失败,检查错误信息并解决后再继续
|
||||
4. **类型同步**:每次修改 schema 后都要运行 `prisma generate` 更新类型
|
||||
301
backend/docs/SCHOOL_MODULE_SCHEMA.md
Normal file
301
backend/docs/SCHOOL_MODULE_SCHEMA.md
Normal file
@ -0,0 +1,301 @@
|
||||
# 学校模块数据库设计文档
|
||||
|
||||
## 概述
|
||||
|
||||
本文档描述了学校管理系统的数据库表设计,包括学校信息、年级、班级、部门、教师和学生等核心实体。
|
||||
|
||||
## 设计原则
|
||||
|
||||
1. **租户隔离**:所有表都通过 `tenantId` 关联到 `Tenant` 表,实现多租户数据隔离
|
||||
2. **用户统一**:教师和学生都基于 `User` 表,通过一对一关系扩展特定信息
|
||||
3. **数据完整性**:使用外键约束和级联删除保证数据一致性
|
||||
4. **审计追踪**:所有表都包含创建人、修改人、创建时间、修改时间字段
|
||||
|
||||
## 表结构设计
|
||||
|
||||
### 1. 学校信息表 (School)
|
||||
|
||||
**说明**:扩展租户信息,存储学校的详细资料。与 `Tenant` 表一对一关系。
|
||||
|
||||
**字段说明**:
|
||||
- `id`: 主键
|
||||
- `tenantId`: 租户ID(唯一,一对一关联Tenant)
|
||||
- `address`: 学校地址
|
||||
- `phone`: 联系电话
|
||||
- `principal`: 校长姓名
|
||||
- `established`: 建校时间
|
||||
- `description`: 学校描述
|
||||
- `logo`: 学校Logo URL
|
||||
- `website`: 学校网站
|
||||
- `creator/modifier`: 创建人/修改人ID
|
||||
- `createTime/modifyTime`: 创建/修改时间
|
||||
|
||||
**关系**:
|
||||
- 一对一关联 `Tenant`
|
||||
- 多对一关联 `User` (创建人/修改人)
|
||||
|
||||
---
|
||||
|
||||
### 2. 年级表 (Grade)
|
||||
|
||||
**说明**:管理学校的年级信息,如一年级、二年级等。
|
||||
|
||||
**字段说明**:
|
||||
- `id`: 主键
|
||||
- `tenantId`: 租户ID
|
||||
- `name`: 年级名称(如:一年级、二年级)
|
||||
- `code`: 年级编码(在租户内唯一,如:grade_1, grade_2)
|
||||
- `level`: 年级级别(用于排序,如:1, 2, 3)
|
||||
- `description`: 年级描述
|
||||
- `validState`: 有效状态(1-有效,2-失效)
|
||||
- `creator/modifier`: 创建人/修改人ID
|
||||
- `createTime/modifyTime`: 创建/修改时间
|
||||
|
||||
**唯一约束**:
|
||||
- `[tenantId, code]`: 租户内年级编码唯一
|
||||
- `[tenantId, level]`: 租户内年级级别唯一
|
||||
|
||||
**关系**:
|
||||
- 多对一关联 `Tenant`
|
||||
- 一对多关联 `Class` (班级)
|
||||
- 多对一关联 `User` (创建人/修改人)
|
||||
|
||||
---
|
||||
|
||||
### 3. 部门表 (Department)
|
||||
|
||||
**说明**:管理学校的部门信息,支持树形结构(如:教务处 > 语文组)。
|
||||
|
||||
**字段说明**:
|
||||
- `id`: 主键
|
||||
- `tenantId`: 租户ID
|
||||
- `name`: 部门名称
|
||||
- `code`: 部门编码(在租户内唯一)
|
||||
- `parentId`: 父部门ID(支持树形结构)
|
||||
- `description`: 部门描述
|
||||
- `sort`: 排序
|
||||
- `validState`: 有效状态(1-有效,2-失效)
|
||||
- `creator/modifier`: 创建人/修改人ID
|
||||
- `createTime/modifyTime`: 创建/修改时间
|
||||
|
||||
**唯一约束**:
|
||||
- `[tenantId, code]`: 租户内部门编码唯一
|
||||
|
||||
**关系**:
|
||||
- 多对一关联 `Tenant`
|
||||
- 自关联(树形结构):`parent` 和 `children`
|
||||
- 一对多关联 `Teacher` (教师)
|
||||
- 多对一关联 `User` (创建人/修改人)
|
||||
|
||||
---
|
||||
|
||||
### 4. 班级表 (Class)
|
||||
|
||||
**说明**:管理班级信息,支持行政班级(教学班级)和兴趣班两种类型。
|
||||
|
||||
**字段说明**:
|
||||
- `id`: 主键
|
||||
- `tenantId`: 租户ID
|
||||
- `gradeId`: 年级ID
|
||||
- `name`: 班级名称(如:一年级1班、二年级2班)
|
||||
- `code`: 班级编码(在租户内唯一)
|
||||
- `type`: 班级类型(1-行政班级/教学班级,2-兴趣班)
|
||||
- `capacity`: 班级容量(可选)
|
||||
- `description`: 班级描述
|
||||
- `validState`: 有效状态(1-有效,2-失效)
|
||||
- `creator/modifier`: 创建人/修改人ID
|
||||
- `createTime/modifyTime`: 创建/修改时间
|
||||
|
||||
**唯一约束**:
|
||||
- `[tenantId, code]`: 租户内班级编码唯一
|
||||
|
||||
**关系**:
|
||||
- 多对一关联 `Tenant`
|
||||
- 多对一关联 `Grade` (年级)
|
||||
- 一对多关联 `Student` (学生,仅行政班级)
|
||||
- 一对多关联 `StudentInterestClass` (学生兴趣班关联)
|
||||
- 多对一关联 `User` (创建人/修改人)
|
||||
|
||||
**注意事项**:
|
||||
- `students` 关系仅用于行政班级(type=1),需要在应用层验证
|
||||
- 兴趣班通过 `StudentInterestClass` 表关联学生
|
||||
|
||||
---
|
||||
|
||||
### 5. 教师表 (Teacher)
|
||||
|
||||
**说明**:存储教师的详细信息,与 `User` 表一对一关系。
|
||||
|
||||
**字段说明**:
|
||||
- `id`: 主键
|
||||
- `userId`: 用户ID(唯一,一对一关联User)
|
||||
- `tenantId`: 租户ID
|
||||
- `departmentId`: 部门ID
|
||||
- `employeeNo`: 工号(在租户内唯一)
|
||||
- `phone`: 联系电话
|
||||
- `idCard`: 身份证号
|
||||
- `gender`: 性别(1-男,2-女)
|
||||
- `birthDate`: 出生日期
|
||||
- `hireDate`: 入职日期
|
||||
- `subject`: 任教科目(可选,如:语文、数学)
|
||||
- `title`: 职称(可选,如:高级教师、一级教师)
|
||||
- `description`: 教师描述
|
||||
- `validState`: 有效状态(1-有效,2-失效)
|
||||
- `creator/modifier`: 创建人/修改人ID
|
||||
- `createTime/modifyTime`: 创建/修改时间
|
||||
|
||||
**唯一约束**:
|
||||
- `userId`: 用户ID唯一(一对一)
|
||||
- `[tenantId, employeeNo]`: 租户内工号唯一
|
||||
|
||||
**关系**:
|
||||
- 一对一关联 `User`
|
||||
- 多对一关联 `Tenant`
|
||||
- 多对一关联 `Department` (部门)
|
||||
- 多对一关联 `User` (创建人/修改人)
|
||||
|
||||
---
|
||||
|
||||
### 6. 学生表 (Student)
|
||||
|
||||
**说明**:存储学生的详细信息,与 `User` 表一对一关系。
|
||||
|
||||
**字段说明**:
|
||||
- `id`: 主键
|
||||
- `userId`: 用户ID(唯一,一对一关联User)
|
||||
- `tenantId`: 租户ID
|
||||
- `classId`: 行政班级ID
|
||||
- `studentNo`: 学号(在租户内唯一)
|
||||
- `phone`: 联系电话
|
||||
- `idCard`: 身份证号
|
||||
- `gender`: 性别(1-男,2-女)
|
||||
- `birthDate`: 出生日期
|
||||
- `enrollmentDate`: 入学日期
|
||||
- `parentName`: 家长姓名
|
||||
- `parentPhone`: 家长电话
|
||||
- `address`: 家庭地址
|
||||
- `description`: 学生描述
|
||||
- `validState`: 有效状态(1-有效,2-失效)
|
||||
- `creator/modifier`: 创建人/修改人ID
|
||||
- `createTime/modifyTime`: 创建/修改时间
|
||||
|
||||
**唯一约束**:
|
||||
- `userId`: 用户ID唯一(一对一)
|
||||
- `[tenantId, studentNo]`: 租户内学号唯一
|
||||
|
||||
**关系**:
|
||||
- 一对一关联 `User`
|
||||
- 多对一关联 `Tenant`
|
||||
- 多对一关联 `Class` (行政班级)
|
||||
- 一对多关联 `StudentInterestClass` (兴趣班关联)
|
||||
- 多对一关联 `User` (创建人/修改人)
|
||||
|
||||
**注意事项**:
|
||||
- `classId` 必须关联行政班级(type=1),需要在应用层验证
|
||||
- 兴趣班通过 `StudentInterestClass` 表关联
|
||||
|
||||
---
|
||||
|
||||
### 7. 学生兴趣班关联表 (StudentInterestClass)
|
||||
|
||||
**说明**:学生和兴趣班的多对多关联表。
|
||||
|
||||
**字段说明**:
|
||||
- `id`: 主键
|
||||
- `studentId`: 学生ID
|
||||
- `classId`: 兴趣班ID(type=2的Class)
|
||||
|
||||
**唯一约束**:
|
||||
- `[studentId, classId]`: 学生和兴趣班组合唯一
|
||||
|
||||
**关系**:
|
||||
- 多对一关联 `Student`
|
||||
- 多对一关联 `Class` (兴趣班)
|
||||
|
||||
**注意事项**:
|
||||
- `classId` 必须关联兴趣班(type=2),需要在应用层验证
|
||||
|
||||
---
|
||||
|
||||
## 数据关系图
|
||||
|
||||
```
|
||||
Tenant (租户/学校)
|
||||
├── School (学校信息) [1:1]
|
||||
├── Grade (年级) [1:N]
|
||||
│ └── Class (班级) [1:N]
|
||||
│ ├── Student (学生) [1:N, 仅行政班级]
|
||||
│ └── StudentInterestClass [N:M, 仅兴趣班]
|
||||
├── Department (部门) [1:N, 树形结构]
|
||||
│ └── Teacher (教师) [1:N]
|
||||
└── User (用户) [1:N]
|
||||
├── Teacher [1:1]
|
||||
└── Student [1:1]
|
||||
```
|
||||
|
||||
## 业务规则
|
||||
|
||||
1. **学校与租户**:每个租户对应一个学校,通过 `School` 表扩展学校信息
|
||||
2. **年级管理**:年级按 `level` 排序,每个租户内级别唯一
|
||||
3. **班级类型**:
|
||||
- 行政班级(type=1):学生必须属于一个行政班级
|
||||
- 兴趣班(type=2):学生可以加入多个兴趣班
|
||||
4. **部门树形结构**:部门支持多级嵌套,通过 `parentId` 实现
|
||||
5. **教师归属**:教师必须归属于一个部门
|
||||
6. **学生归属**:学生必须属于一个行政班级,可以加入多个兴趣班
|
||||
|
||||
## 数据完整性约束
|
||||
|
||||
1. **级联删除**:
|
||||
- 删除租户时,级联删除所有相关数据
|
||||
- 删除年级时,级联删除所有班级
|
||||
- 删除用户时,级联删除教师/学生信息
|
||||
- 删除班级时,级联删除学生兴趣班关联
|
||||
|
||||
2. **限制删除**:
|
||||
- 删除部门时,如果存在教师,不允许删除(Restrict)
|
||||
- 删除班级时,如果存在学生,不允许删除(Restrict)
|
||||
|
||||
3. **唯一性约束**:
|
||||
- 租户内年级编码唯一
|
||||
- 租户内年级级别唯一
|
||||
- 租户内部门编码唯一
|
||||
- 租户内班级编码唯一
|
||||
- 租户内教师工号唯一
|
||||
- 租户内学生学号唯一
|
||||
|
||||
## 应用层验证建议
|
||||
|
||||
1. **班级类型验证**:
|
||||
- 创建学生时,`classId` 必须关联 `type=1` 的班级
|
||||
- 创建学生兴趣班关联时,`classId` 必须关联 `type=2` 的班级
|
||||
|
||||
2. **数据一致性**:
|
||||
- 教师/学生的 `tenantId` 必须与关联的 `User.tenantId` 一致
|
||||
- 班级的 `tenantId` 必须与关联的 `Grade.tenantId` 一致
|
||||
|
||||
3. **业务逻辑**:
|
||||
- 删除部门前,需要先转移或删除该部门下的所有教师
|
||||
- 删除班级前,需要先转移或删除该班级下的所有学生
|
||||
|
||||
## 迁移建议
|
||||
|
||||
1. 运行 Prisma 迁移生成 SQL:
|
||||
```bash
|
||||
npx prisma migrate dev --name add_school_module
|
||||
```
|
||||
|
||||
2. 数据初始化:
|
||||
- 为现有租户创建对应的 `School` 记录
|
||||
- 根据业务需求初始化年级数据
|
||||
|
||||
3. 数据迁移(如需要):
|
||||
- 如果已有教师/学生数据,需要创建对应的 `User` 记录并关联
|
||||
|
||||
## 后续扩展建议
|
||||
|
||||
1. **课程管理**:可以添加课程表、课程安排等
|
||||
2. **成绩管理**:可以添加成绩表、考试表等
|
||||
3. **考勤管理**:可以添加考勤记录表
|
||||
4. **通知公告**:可以添加通知表、公告表等
|
||||
|
||||
270
backend/docs/TENANT_GUIDE.md
Normal file
270
backend/docs/TENANT_GUIDE.md
Normal file
@ -0,0 +1,270 @@
|
||||
# 多租户系统实现指南
|
||||
|
||||
## 概述
|
||||
|
||||
本系统实现了完整的多租户架构,支持:
|
||||
- 每个租户独立的数据隔离(用户、角色、权限、菜单等)
|
||||
- 每个租户独立的访问链接(通过租户编码或域名)
|
||||
- 超级租户可以创建和管理其他租户
|
||||
- 超级租户可以为租户分配菜单
|
||||
|
||||
## 数据库设计
|
||||
|
||||
### 核心表结构
|
||||
|
||||
1. **Tenant(租户表)**
|
||||
- `id`: 租户ID
|
||||
- `name`: 租户名称
|
||||
- `code`: 租户编码(唯一,用于访问链接)
|
||||
- `domain`: 租户域名(可选,用于子域名访问)
|
||||
- `isSuper`: 是否为超级租户(0-否,1-是)
|
||||
- `validState`: 有效状态(1-有效,2-失效)
|
||||
|
||||
2. **TenantMenu(租户菜单关联表)**
|
||||
- `tenantId`: 租户ID
|
||||
- `menuId`: 菜单ID
|
||||
- 用于关联租户和菜单,实现菜单分配
|
||||
|
||||
3. **其他表添加租户字段**
|
||||
- `User`: 添加 `tenantId` 字段
|
||||
- `Role`: 添加 `tenantId` 字段
|
||||
- `Permission`: 添加 `tenantId` 字段
|
||||
- `Dict`: 添加 `tenantId` 字段
|
||||
- `Config`: 添加 `tenantId` 字段
|
||||
|
||||
### 唯一性约束调整
|
||||
|
||||
- `User.username`: 从全局唯一改为 `(tenantId, username)` 唯一
|
||||
- `User.email`: 从全局唯一改为 `(tenantId, email)` 唯一
|
||||
- `Role.name/code`: 从全局唯一改为 `(tenantId, name/code)` 唯一
|
||||
- `Permission.code`: 从全局唯一改为 `(tenantId, code)` 唯一
|
||||
- 其他类似字段也做了相应调整
|
||||
|
||||
## 租户识别机制
|
||||
|
||||
系统支持多种方式识别租户:
|
||||
|
||||
1. **请求头方式**(推荐)
|
||||
- `X-Tenant-Code`: 租户编码
|
||||
- `X-Tenant-Id`: 租户ID
|
||||
|
||||
2. **子域名方式**
|
||||
- 从 `Host` 请求头提取子域名
|
||||
- 匹配租户的 `code` 或 `domain` 字段
|
||||
|
||||
3. **JWT Token方式**
|
||||
- Token中包含 `tenantId` 字段
|
||||
- 登录时自动关联租户
|
||||
|
||||
4. **登录参数方式**
|
||||
- 登录接口支持 `tenantCode` 参数
|
||||
|
||||
## 使用流程
|
||||
|
||||
### 1. 数据库迁移
|
||||
|
||||
首先需要生成并执行数据库迁移:
|
||||
|
||||
```bash
|
||||
# 生成迁移文件
|
||||
npm run prisma:migrate:dev -- --name add_tenant_support
|
||||
|
||||
# 执行迁移
|
||||
npm run prisma:migrate
|
||||
```
|
||||
|
||||
### 2. 初始化超级租户
|
||||
|
||||
运行初始化脚本创建超级租户:
|
||||
|
||||
```bash
|
||||
npm run init:super-tenant
|
||||
```
|
||||
|
||||
这将创建:
|
||||
- 超级租户(code: `super`)
|
||||
- 超级管理员用户(username: `admin`, password: `admin123`)
|
||||
- 超级管理员角色
|
||||
- 基础权限
|
||||
|
||||
### 3. 创建普通租户
|
||||
|
||||
使用超级租户的管理员账号登录后,通过租户管理接口创建新租户:
|
||||
|
||||
```bash
|
||||
POST /api/tenants
|
||||
Headers:
|
||||
Authorization: Bearer <token>
|
||||
X-Tenant-Code: super
|
||||
Body:
|
||||
{
|
||||
"name": "租户A",
|
||||
"code": "tenant-a",
|
||||
"domain": "tenant-a.example.com",
|
||||
"description": "租户A的描述",
|
||||
"menuIds": [1, 2, 3] // 分配的菜单ID列表
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 为租户分配菜单
|
||||
|
||||
超级租户可以为租户分配菜单:
|
||||
|
||||
```bash
|
||||
PATCH /api/tenants/:id
|
||||
Headers:
|
||||
Authorization: Bearer <token>
|
||||
X-Tenant-Code: super
|
||||
Body:
|
||||
{
|
||||
"menuIds": [1, 2, 3, 4, 5]
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 租户用户登录
|
||||
|
||||
租户用户登录时需要指定租户:
|
||||
|
||||
```bash
|
||||
POST /api/auth/login
|
||||
Body:
|
||||
{
|
||||
"username": "user1",
|
||||
"password": "password123",
|
||||
"tenantCode": "tenant-a" // 可选,也可以从请求头获取
|
||||
}
|
||||
```
|
||||
|
||||
或者在请求头中指定:
|
||||
|
||||
```bash
|
||||
POST /api/auth/login
|
||||
Headers:
|
||||
X-Tenant-Code: tenant-a
|
||||
Body:
|
||||
{
|
||||
"username": "user1",
|
||||
"password": "password123"
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 访问租户数据
|
||||
|
||||
所有API请求都会自动根据租户ID过滤数据:
|
||||
|
||||
```bash
|
||||
GET /api/users
|
||||
Headers:
|
||||
Authorization: Bearer <token>
|
||||
X-Tenant-Code: tenant-a
|
||||
```
|
||||
|
||||
返回的数据只会包含该租户的用户。
|
||||
|
||||
## API接口
|
||||
|
||||
### 租户管理接口
|
||||
|
||||
- `POST /api/tenants` - 创建租户(需要 `tenant:create` 权限)
|
||||
- `GET /api/tenants` - 获取租户列表(需要 `tenant:read` 权限)
|
||||
- `GET /api/tenants/:id` - 获取租户详情(需要 `tenant:read` 权限)
|
||||
- `PATCH /api/tenants/:id` - 更新租户(需要 `tenant:update` 权限)
|
||||
- `DELETE /api/tenants/:id` - 删除租户(需要 `tenant:delete` 权限)
|
||||
- `GET /api/tenants/:id/menus` - 获取租户的菜单树(需要 `tenant:read` 权限)
|
||||
|
||||
### 其他接口
|
||||
|
||||
所有其他接口(用户、角色、权限等)都支持租户隔离,会自动根据请求中的租户信息过滤数据。
|
||||
|
||||
## 前端集成
|
||||
|
||||
### 1. 请求拦截器
|
||||
|
||||
在前端请求拦截器中添加租户信息:
|
||||
|
||||
```typescript
|
||||
// utils/request.ts
|
||||
service.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
const token = getToken();
|
||||
const tenantCode = getTenantCode(); // 从localStorage或store获取
|
||||
|
||||
if (token && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
if (tenantCode && config.headers) {
|
||||
config.headers['X-Tenant-Code'] = tenantCode;
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 2. 登录时保存租户信息
|
||||
|
||||
```typescript
|
||||
// 登录成功后
|
||||
localStorage.setItem('tenantCode', response.data.user.tenantCode);
|
||||
localStorage.setItem('tenantId', response.data.user.tenantId);
|
||||
```
|
||||
|
||||
### 3. 租户切换
|
||||
|
||||
如果需要支持租户切换,可以在前端实现租户选择器,切换时更新localStorage中的租户信息并重新加载数据。
|
||||
|
||||
## 权限控制
|
||||
|
||||
### 超级租户权限
|
||||
|
||||
超级租户的用户拥有所有权限,包括:
|
||||
- 创建、查看、更新、删除租户
|
||||
- 为租户分配菜单
|
||||
- 管理所有租户的数据(如果需要在超级租户中查看所有租户数据)
|
||||
|
||||
### 普通租户权限
|
||||
|
||||
普通租户的用户只能:
|
||||
- 管理自己租户内的数据
|
||||
- 查看分配给租户的菜单
|
||||
- 无法访问其他租户的数据
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **数据隔离**: 所有查询都会自动添加租户过滤条件,确保数据隔离
|
||||
2. **唯一性**: 用户名、邮箱等在租户内唯一,不同租户可以有相同的用户名
|
||||
3. **菜单管理**: 菜单是全局的(由超级租户管理),但通过 `TenantMenu` 表分配给各个租户
|
||||
4. **超级租户**: 超级租户不能被删除,且拥有所有权限
|
||||
5. **迁移数据**: 如果现有系统已有数据,需要编写迁移脚本将现有数据关联到超级租户
|
||||
|
||||
## 迁移现有数据
|
||||
|
||||
如果系统已有数据,需要将现有数据迁移到超级租户:
|
||||
|
||||
```sql
|
||||
-- 假设超级租户ID为1
|
||||
UPDATE users SET tenant_id = 1 WHERE tenant_id IS NULL;
|
||||
UPDATE roles SET tenant_id = 1 WHERE tenant_id IS NULL;
|
||||
UPDATE permissions SET tenant_id = 1 WHERE tenant_id IS NULL;
|
||||
-- 其他表类似
|
||||
```
|
||||
|
||||
## 故障排查
|
||||
|
||||
1. **租户识别失败**: 检查请求头是否正确设置,或检查JWT token中是否包含tenantId
|
||||
2. **数据查询为空**: 确认租户ID正确,且数据确实属于该租户
|
||||
3. **权限不足**: 确认用户角色有相应权限,且角色属于正确的租户
|
||||
|
||||
## 扩展功能
|
||||
|
||||
未来可以考虑的扩展:
|
||||
1. 租户级别的配置(每个租户可以有自己的系统配置)
|
||||
2. 租户级别的主题和品牌定制
|
||||
3. 租户级别的功能开关
|
||||
4. 租户使用统计和监控
|
||||
5. 租户数据导出和备份
|
||||
|
||||
226
backend/docs/TENANT_LOGIN_GUIDE.md
Normal file
226
backend/docs/TENANT_LOGIN_GUIDE.md
Normal file
@ -0,0 +1,226 @@
|
||||
# 租户登录使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
系统已完整支持多租户登录功能,每个租户可以独立访问系统,数据完全隔离。
|
||||
|
||||
## 租户识别方式
|
||||
|
||||
系统支持以下方式识别租户:
|
||||
|
||||
### 1. URL参数方式(推荐)
|
||||
|
||||
在登录页面URL中添加 `tenant` 参数:
|
||||
|
||||
```
|
||||
http://your-domain.com/login?tenant=tenant-a
|
||||
```
|
||||
|
||||
登录页面会自动识别租户编码,并在登录时自动发送。
|
||||
|
||||
### 2. 登录表单输入
|
||||
|
||||
如果URL中没有租户参数,登录页面会显示租户编码输入框,用户可以手动输入。
|
||||
|
||||
### 3. 请求头方式
|
||||
|
||||
前端会自动将租户信息添加到所有API请求的请求头中:
|
||||
- `X-Tenant-Code`: 租户编码
|
||||
- `X-Tenant-Id`: 租户ID
|
||||
|
||||
## 使用流程
|
||||
|
||||
### 方式一:通过URL参数访问(推荐)
|
||||
|
||||
1. **访问租户登录页面**
|
||||
```
|
||||
http://your-domain.com/login?tenant=tenant-a
|
||||
```
|
||||
|
||||
2. **输入用户名和密码**
|
||||
- 用户名:租户内的用户名
|
||||
- 密码:用户密码
|
||||
- 租户编码:已自动填充(从URL参数)
|
||||
|
||||
3. **登录成功**
|
||||
- 系统自动保存租户信息到 localStorage
|
||||
- 后续所有API请求都会自动携带租户信息
|
||||
- 用户只能看到和操作自己租户的数据
|
||||
|
||||
### 方式二:手动输入租户编码
|
||||
|
||||
1. **访问登录页面**
|
||||
```
|
||||
http://your-domain.com/login
|
||||
```
|
||||
|
||||
2. **输入租户信息**
|
||||
- 租户编码:输入租户编码(如:`tenant-a`)
|
||||
- 用户名:租户内的用户名
|
||||
- 密码:用户密码
|
||||
|
||||
3. **登录成功**
|
||||
- 系统保存租户信息
|
||||
- 后续请求自动携带租户信息
|
||||
|
||||
## 后端API使用
|
||||
|
||||
### 登录接口
|
||||
|
||||
**请求:**
|
||||
```bash
|
||||
POST /api/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "user1",
|
||||
"password": "password123",
|
||||
"tenantCode": "tenant-a" // 可选,也可以从请求头获取
|
||||
}
|
||||
```
|
||||
|
||||
**或者通过请求头:**
|
||||
```bash
|
||||
POST /api/auth/login
|
||||
X-Tenant-Code: tenant-a
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "user1",
|
||||
"password": "password123"
|
||||
}
|
||||
```
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "user1",
|
||||
"nickname": "用户1",
|
||||
"email": "user1@example.com",
|
||||
"tenantId": 2,
|
||||
"tenantCode": "tenant-a",
|
||||
"roles": ["admin"],
|
||||
"permissions": ["user:read", "user:create", ...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 其他API请求
|
||||
|
||||
登录后,所有API请求都会自动携带租户信息(通过JWT Token或请求头),后端会自动过滤数据:
|
||||
|
||||
```bash
|
||||
GET /api/users
|
||||
Authorization: Bearer <token>
|
||||
X-Tenant-Code: tenant-a # 自动添加
|
||||
```
|
||||
|
||||
返回的数据只会包含该租户的用户。
|
||||
|
||||
## 前端实现细节
|
||||
|
||||
### 1. 登录页面自动识别租户
|
||||
|
||||
登录页面 (`Login.vue`) 会:
|
||||
- 从URL参数 `?tenant=xxx` 获取租户编码
|
||||
- 如果URL中没有,从 localStorage 读取之前保存的租户编码
|
||||
- 如果都没有,显示租户输入框
|
||||
|
||||
### 2. 请求拦截器自动添加租户信息
|
||||
|
||||
所有API请求都会自动添加租户信息到请求头:
|
||||
|
||||
```typescript
|
||||
// utils/request.ts
|
||||
service.interceptors.request.use((config) => {
|
||||
const tenantCode = getTenantCode();
|
||||
const tenantId = getTenantId();
|
||||
|
||||
if (tenantCode) {
|
||||
config.headers['X-Tenant-Code'] = tenantCode;
|
||||
}
|
||||
if (tenantId) {
|
||||
config.headers['X-Tenant-Id'] = tenantId;
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 登录后保存租户信息
|
||||
|
||||
登录成功后,系统会自动保存:
|
||||
- Token
|
||||
- 租户编码 (tenantCode)
|
||||
- 租户ID (tenantId)
|
||||
|
||||
这些信息保存在 localStorage 中,页面刷新后仍然有效。
|
||||
|
||||
## 示例场景
|
||||
|
||||
### 场景1:租户A的用户登录
|
||||
|
||||
1. 访问:`http://your-domain.com/login?tenant=tenant-a`
|
||||
2. 输入用户名和密码
|
||||
3. 登录后只能看到租户A的数据
|
||||
|
||||
### 场景2:租户B的用户登录
|
||||
|
||||
1. 访问:`http://your-domain.com/login?tenant=tenant-b`
|
||||
2. 输入用户名和密码
|
||||
3. 登录后只能看到租户B的数据
|
||||
4. 租户A的数据完全不可见
|
||||
|
||||
### 场景3:超级租户管理员登录
|
||||
|
||||
1. 访问:`http://your-domain.com/login?tenant=super`
|
||||
2. 使用超级管理员账号登录
|
||||
3. 可以管理所有租户
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **租户编码必须唯一**:每个租户都有唯一的编码(code)
|
||||
2. **用户属于特定租户**:用户只能登录到自己所属的租户
|
||||
3. **数据完全隔离**:不同租户的数据完全隔离,无法互相访问
|
||||
4. **租户信息持久化**:登录后租户信息保存在 localStorage,刷新页面不会丢失
|
||||
5. **切换租户**:如果需要切换租户,需要先登出,然后使用新的租户编码登录
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 问题1:登录时提示"无法确定租户信息"
|
||||
|
||||
**原因**:没有提供租户编码或租户ID
|
||||
|
||||
**解决**:
|
||||
- 在URL中添加 `?tenant=xxx` 参数
|
||||
- 或者在登录表单中输入租户编码
|
||||
- 或者通过请求头 `X-Tenant-Code` 提供
|
||||
|
||||
### 问题2:登录时提示"用户不属于该租户"
|
||||
|
||||
**原因**:用户不属于指定的租户
|
||||
|
||||
**解决**:
|
||||
- 确认租户编码是否正确
|
||||
- 确认用户是否属于该租户
|
||||
- 联系管理员检查用户和租户的关联关系
|
||||
|
||||
### 问题3:登录后看不到数据
|
||||
|
||||
**原因**:可能是租户信息没有正确传递
|
||||
|
||||
**解决**:
|
||||
- 检查浏览器控制台的网络请求,确认请求头中是否包含 `X-Tenant-Code`
|
||||
- 检查 localStorage 中是否保存了租户信息
|
||||
- 确认后端是否正确识别了租户
|
||||
|
||||
## 开发建议
|
||||
|
||||
1. **使用URL参数方式**:这是最用户友好的方式,用户只需要记住租户的访问链接
|
||||
2. **提供租户选择器**:如果系统需要支持租户切换,可以在前端添加租户选择器
|
||||
3. **错误提示优化**:当租户信息缺失时,提供清晰的错误提示
|
||||
4. **租户信息显示**:在用户界面显示当前租户信息,让用户知道自己在哪个租户下操作
|
||||
|
||||
1
backend/docs/功能描述.md
Normal file
1
backend/docs/功能描述.md
Normal file
@ -0,0 +1 @@
|
||||
##
|
||||
9
backend/nest-cli.json
Normal file
9
backend/nest-cli.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
|
||||
10933
backend/package-lock.json
generated
Normal file
10933
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
115
backend/package.json
Normal file
115
backend/package.json
Normal file
@ -0,0 +1,115 @@
|
||||
{
|
||||
"name": "competition-management-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "比赛管理系统后端",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "set NODE_ENV=development&&nest start --watch",
|
||||
"start:debug": "NODE_ENV=development nest start --debug --watch",
|
||||
"start:prod": "NODE_ENV=production node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "NODE_ENV=test jest",
|
||||
"test:watch": "NODE_ENV=test jest --watch",
|
||||
"test:cov": "NODE_ENV=test jest --coverage",
|
||||
"test:debug": "NODE_ENV=test node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "NODE_ENV=test jest --config ./test/jest-e2e.json",
|
||||
"prisma:status:dev": "dotenv -e .env.development -- prisma migrate status",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:generate:dev": "dotenv -e .env.development -- prisma generate",
|
||||
"prisma:migrate": "prisma migrate dev",
|
||||
"prisma:migrate:dev": "dotenv -e .env.development -- prisma migrate dev --create-only --name add_contest_module",
|
||||
"prisma:migrate:deploy": "NODE_ENV=production prisma migrate deploy",
|
||||
"prisma:studio": "prisma studio",
|
||||
"prisma:studio:dev": "NODE_ENV=development prisma studio",
|
||||
"prisma:studio:prod": "NODE_ENV=production prisma studio",
|
||||
"init:admin": "ts-node scripts/init-admin.ts",
|
||||
"init:admin:permissions": "ts-node scripts/init-admin-permissions.ts",
|
||||
"init:menus": "ts-node scripts/init-menus.ts",
|
||||
"init:super-tenant": "ts-node scripts/init-super-tenant.ts",
|
||||
"init:linksea-tenant": "ts-node scripts/init-linksea-tenant.ts",
|
||||
"init:tenant-admin": "ts-node scripts/init-tenant-admin.ts",
|
||||
"init:tenant-admin:permissions": "ts-node scripts/init-tenant-admin.ts --permissions-only",
|
||||
"init:tenant-permissions": "ts-node scripts/init-tenant-permissions.ts",
|
||||
"init:tenant-menu-permissions": "ts-node scripts/init-tenant-menu-permissions.ts",
|
||||
"update:password": "ts-node scripts/update-password.ts",
|
||||
"fix:invalid-datetime": "ts-node scripts/fix-invalid-datetime.ts",
|
||||
"cleanup:tenant-permissions": "ts-node scripts/cleanup-tenant-permissions.ts",
|
||||
"init:roles:super": "ts-node scripts/init-roles-permissions.ts --super",
|
||||
"init:roles": "ts-node scripts/init-roles-permissions.ts",
|
||||
"init:roles:all": "ts-node scripts/init-roles-permissions.ts --all",
|
||||
"init:tenant": "ts-node scripts/init-tenant.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^10.3.3",
|
||||
"@nestjs/config": "^3.1.1",
|
||||
"@nestjs/core": "^10.3.3",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/mapped-types": "^2.1.0",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.3.3",
|
||||
"@nestjs/serve-static": "^4.0.0",
|
||||
"@prisma/client": "^6.19.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"bcrypt": "^6.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"cos-nodejs-sdk-v5": "^2.15.4",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.3.2",
|
||||
"@nestjs/schematics": "^10.1.0",
|
||||
"@nestjs/testing": "^10.3.3",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/node": "^20.11.5",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/passport-local": "^1.0.36",
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.19.1",
|
||||
"@typescript-eslint/parser": "^6.19.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"dotenv-cli": "^11.0.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.2.4",
|
||||
"prisma": "^6.19.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-jest": "^29.1.2",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
1098
backend/prisma/schema.prisma
Normal file
1098
backend/prisma/schema.prisma
Normal file
File diff suppressed because it is too large
Load Diff
60
backend/scripts/check-permissions.js
Normal file
60
backend/scripts/check-permissions.js
Normal file
@ -0,0 +1,60 @@
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
// 查找香港小学租户
|
||||
const tenant = await prisma.tenant.findFirst({
|
||||
where: { code: 'school001' }
|
||||
});
|
||||
|
||||
if (!tenant) {
|
||||
console.log('租户 school001 不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`租户: ${tenant.name} (${tenant.code})\n`);
|
||||
|
||||
// 查找该租户的 school_admin 角色
|
||||
const role = await prisma.role.findFirst({
|
||||
where: { tenantId: tenant.id, code: 'school_admin' },
|
||||
include: {
|
||||
permissions: {
|
||||
include: {
|
||||
permission: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
console.log('school_admin 角色不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`角色: ${role.name} (${role.code})`);
|
||||
console.log(`权限数量: ${role.permissions.length}\n`);
|
||||
|
||||
// 检查系统管理相关权限
|
||||
const systemPermissions = ['user:read', 'role:read', 'menu:read', 'permission:read'];
|
||||
console.log('系统管理相关权限:');
|
||||
systemPermissions.forEach(code => {
|
||||
const has = role.permissions.some(rp => rp.permission.code === code);
|
||||
console.log(` ${code}: ${has ? '✓' : '✗'}`);
|
||||
});
|
||||
|
||||
// 查找该租户的权限
|
||||
console.log('\n该租户所有权限:');
|
||||
const permissions = await prisma.permission.findMany({
|
||||
where: { tenantId: tenant.id }
|
||||
});
|
||||
permissions.forEach(p => {
|
||||
console.log(` ${p.code}`);
|
||||
});
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => prisma.$disconnect())
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
prisma.$disconnect();
|
||||
});
|
||||
64
backend/scripts/check-registrations.js
Normal file
64
backend/scripts/check-registrations.js
Normal file
@ -0,0 +1,64 @@
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
// 查找3D打印作品大赛
|
||||
const contest = await prisma.contest.findFirst({
|
||||
where: { contestName: { contains: '3D打印' } }
|
||||
});
|
||||
|
||||
if (!contest) {
|
||||
console.log('未找到3D打印作品大赛');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`赛事: ${contest.contestName} (ID: ${contest.id})\n`);
|
||||
|
||||
// 查找该赛事的所有报名记录
|
||||
const registrations = await prisma.contestRegistration.findMany({
|
||||
where: { contestId: contest.id },
|
||||
include: {
|
||||
user: true,
|
||||
contest: true
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`报名记录数量: ${registrations.length}\n`);
|
||||
|
||||
if (registrations.length > 0) {
|
||||
console.log('报名记录详情:');
|
||||
registrations.forEach(r => {
|
||||
console.log(` ID: ${r.id}, 用户: ${r.user?.username || 'N/A'}, 租户ID: ${r.tenantId}, 状态: ${r.status}`);
|
||||
});
|
||||
}
|
||||
|
||||
// 查找 xuesheng1 用户
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { username: 'xuesheng1' }
|
||||
});
|
||||
|
||||
if (user) {
|
||||
console.log(`\nxuesheng1 用户信息:`);
|
||||
console.log(` ID: ${user.id}, 租户ID: ${user.tenantId}`);
|
||||
|
||||
// 查找该用户的所有报名记录
|
||||
const userRegistrations = await prisma.contestRegistration.findMany({
|
||||
where: { userId: user.id },
|
||||
include: { contest: true }
|
||||
});
|
||||
|
||||
console.log(`\nxuesheng1 的所有报名记录 (${userRegistrations.length}):`);
|
||||
userRegistrations.forEach(r => {
|
||||
console.log(` 赛事: ${r.contest?.contestName}, 状态: ${r.status}, 租户ID: ${r.tenantId}`);
|
||||
});
|
||||
} else {
|
||||
console.log('\n未找到 xuesheng1 用户');
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => prisma.$disconnect())
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
prisma.$disconnect();
|
||||
});
|
||||
52
backend/scripts/check-student-permissions.js
Normal file
52
backend/scripts/check-student-permissions.js
Normal file
@ -0,0 +1,52 @@
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
// 查找香港小学租户
|
||||
const tenant = await prisma.tenant.findFirst({
|
||||
where: { code: 'school001' }
|
||||
});
|
||||
|
||||
if (!tenant) {
|
||||
console.log('租户 school001 不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`租户: ${tenant.name} (${tenant.code})\n`);
|
||||
|
||||
// 查找该租户的 student 角色
|
||||
const role = await prisma.role.findFirst({
|
||||
where: { tenantId: tenant.id, code: 'student' },
|
||||
include: {
|
||||
permissions: {
|
||||
include: {
|
||||
permission: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
console.log('student 角色不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`角色: ${role.name} (${role.code})`);
|
||||
console.log(`权限数量: ${role.permissions.length}\n`);
|
||||
|
||||
console.log('学生角色所有权限:');
|
||||
role.permissions.forEach(rp => {
|
||||
console.log(` ${rp.permission.code}`);
|
||||
});
|
||||
|
||||
// 检查是否有 registration:read 权限
|
||||
const hasRegistrationRead = role.permissions.some(rp => rp.permission.code === 'registration:read');
|
||||
console.log(`\nregistration:read 权限: ${hasRegistrationRead ? '有' : '无'}`);
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => prisma.$disconnect())
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
prisma.$disconnect();
|
||||
});
|
||||
22
backend/scripts/check-users.js
Normal file
22
backend/scripts/check-users.js
Normal file
@ -0,0 +1,22 @@
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const users = await prisma.user.findMany({
|
||||
include: { tenant: true },
|
||||
});
|
||||
|
||||
console.log('All users:');
|
||||
users.forEach(u => {
|
||||
const tenantName = u.tenant ? u.tenant.name : 'N/A';
|
||||
const tenantCode = u.tenant ? u.tenant.code : 'N/A';
|
||||
console.log(` Tenant: ${tenantName} (${tenantCode}), User: ${u.username}, ID: ${u.id}`);
|
||||
});
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => prisma.$disconnect())
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
prisma.$disconnect();
|
||||
});
|
||||
127
backend/scripts/cleanup-tenant-permissions.ts
Normal file
127
backend/scripts/cleanup-tenant-permissions.ts
Normal file
@ -0,0 +1,127 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
|
||||
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||
const envFile = `.env.${nodeEnv}`;
|
||||
const backendDir = path.resolve(__dirname, '..');
|
||||
const envPath = path.resolve(backendDir, envFile);
|
||||
|
||||
dotenv.config({ path: envPath });
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
dotenv.config({ path: path.resolve(backendDir, '.env') });
|
||||
}
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error('DATABASE_URL not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// 超级管理员专属权限(普通租户不应该有这些权限)
|
||||
const superAdminOnlyPermissions = [
|
||||
'tenant:create',
|
||||
'tenant:update',
|
||||
'tenant:delete',
|
||||
];
|
||||
|
||||
async function cleanupTenantPermissions() {
|
||||
try {
|
||||
console.log('🚀 开始清理普通租户的超级管理员权限...\n');
|
||||
|
||||
// 1. 获取所有非超级租户
|
||||
const normalTenants = await prisma.tenant.findMany({
|
||||
where: {
|
||||
isSuper: { not: 1 },
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`找到 ${normalTenants.length} 个普通租户\n`);
|
||||
|
||||
for (const tenant of normalTenants) {
|
||||
console.log(`处理租户: ${tenant.name} (${tenant.code})`);
|
||||
|
||||
// 2. 找到该租户下的超级管理员专属权限
|
||||
const permissionsToRemove = await prisma.permission.findMany({
|
||||
where: {
|
||||
tenantId: tenant.id,
|
||||
code: { in: superAdminOnlyPermissions },
|
||||
},
|
||||
});
|
||||
|
||||
if (permissionsToRemove.length === 0) {
|
||||
console.log(` ✓ 没有需要清理的权限\n`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const permissionIds = permissionsToRemove.map((p) => p.id);
|
||||
console.log(` 找到 ${permissionsToRemove.length} 个需要清理的权限: ${permissionsToRemove.map((p) => p.code).join(', ')}`);
|
||||
|
||||
// 3. 删除角色-权限关联
|
||||
const deletedRolePermissions = await prisma.rolePermission.deleteMany({
|
||||
where: {
|
||||
permissionId: { in: permissionIds },
|
||||
},
|
||||
});
|
||||
console.log(` 删除了 ${deletedRolePermissions.count} 条角色-权限关联`);
|
||||
|
||||
// 4. 删除权限记录
|
||||
const deletedPermissions = await prisma.permission.deleteMany({
|
||||
where: {
|
||||
id: { in: permissionIds },
|
||||
},
|
||||
});
|
||||
console.log(` 删除了 ${deletedPermissions.count} 条权限记录\n`);
|
||||
}
|
||||
|
||||
// 5. 更新租户管理菜单权限
|
||||
console.log('更新租户管理菜单权限...');
|
||||
const tenantMenu = await prisma.menu.findFirst({
|
||||
where: {
|
||||
name: '租户管理',
|
||||
path: '/system/tenants',
|
||||
},
|
||||
});
|
||||
|
||||
if (tenantMenu) {
|
||||
if (tenantMenu.permission !== 'tenant:update') {
|
||||
await prisma.menu.update({
|
||||
where: { id: tenantMenu.id },
|
||||
data: { permission: 'tenant:update' },
|
||||
});
|
||||
console.log(`✅ 菜单权限已更新为 tenant:update (原: ${tenantMenu.permission})`);
|
||||
} else {
|
||||
console.log('✅ 菜单权限已经是 tenant:update');
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ 未找到租户管理菜单');
|
||||
}
|
||||
|
||||
console.log('\n✅ 清理完成!');
|
||||
console.log('\n说明:');
|
||||
console.log(' - 普通租户现在只有 tenant:read 权限(用于读取租户列表)');
|
||||
console.log(' - 租户管理菜单需要 tenant:update 权限才能看到');
|
||||
console.log(' - 只有超级租户才有 tenant:create/update/delete 权限');
|
||||
} catch (error) {
|
||||
console.error('❌ 清理失败:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
cleanupTenantPermissions()
|
||||
.then(() => {
|
||||
console.log('\n🎉 脚本执行完成!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('\n💥 脚本执行失败:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
598
backend/scripts/init-admin-permissions.ts
Normal file
598
backend/scripts/init-admin-permissions.ts
Normal file
@ -0,0 +1,598 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
|
||||
// 加载环境变量(必须在其他导入之前)
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
|
||||
// 根据 NODE_ENV 加载对应的环境配置文件
|
||||
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||
const envFile = `.env.${nodeEnv}`;
|
||||
// scripts 目录的父目录就是 backend 目录
|
||||
const backendDir = path.resolve(__dirname, '..');
|
||||
const envPath = path.resolve(backendDir, envFile);
|
||||
|
||||
// 尝试加载环境特定的配置文件
|
||||
dotenv.config({ path: envPath });
|
||||
|
||||
// 如果环境特定文件不存在,尝试加载默认的 .env 文件
|
||||
if (!process.env.DATABASE_URL) {
|
||||
dotenv.config({ path: path.resolve(backendDir, '.env') });
|
||||
}
|
||||
|
||||
// 验证必要的环境变量
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error('❌ 错误: 未找到 DATABASE_URL 环境变量');
|
||||
console.error(` 请确保存在以下文件之一:`);
|
||||
console.error(` - ${envPath}`);
|
||||
console.error(` - ${path.resolve(backendDir, '.env')}`);
|
||||
console.error(` 或者设置 NODE_ENV 环境变量(当前: ${nodeEnv})`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// 定义所有基础权限
|
||||
const permissions = [
|
||||
// 用户管理权限
|
||||
{
|
||||
code: 'user:create',
|
||||
resource: 'user',
|
||||
action: 'create',
|
||||
name: '创建用户',
|
||||
description: '允许创建新用户',
|
||||
},
|
||||
{
|
||||
code: 'user:read',
|
||||
resource: 'user',
|
||||
action: 'read',
|
||||
name: '查看用户',
|
||||
description: '允许查看用户列表和详情',
|
||||
},
|
||||
{
|
||||
code: 'user:update',
|
||||
resource: 'user',
|
||||
action: 'update',
|
||||
name: '更新用户',
|
||||
description: '允许更新用户信息',
|
||||
},
|
||||
{
|
||||
code: 'user:delete',
|
||||
resource: 'user',
|
||||
action: 'delete',
|
||||
name: '删除用户',
|
||||
description: '允许删除用户',
|
||||
},
|
||||
|
||||
// 角色管理权限
|
||||
{
|
||||
code: 'role:create',
|
||||
resource: 'role',
|
||||
action: 'create',
|
||||
name: '创建角色',
|
||||
description: '允许创建新角色',
|
||||
},
|
||||
{
|
||||
code: 'role:read',
|
||||
resource: 'role',
|
||||
action: 'read',
|
||||
name: '查看角色',
|
||||
description: '允许查看角色列表和详情',
|
||||
},
|
||||
{
|
||||
code: 'role:update',
|
||||
resource: 'role',
|
||||
action: 'update',
|
||||
name: '更新角色',
|
||||
description: '允许更新角色信息',
|
||||
},
|
||||
{
|
||||
code: 'role:delete',
|
||||
resource: 'role',
|
||||
action: 'delete',
|
||||
name: '删除角色',
|
||||
description: '允许删除角色',
|
||||
},
|
||||
{
|
||||
code: 'role:assign',
|
||||
resource: 'role',
|
||||
action: 'assign',
|
||||
name: '分配角色',
|
||||
description: '允许给用户分配角色',
|
||||
},
|
||||
|
||||
// 权限管理权限
|
||||
{
|
||||
code: 'permission:create',
|
||||
resource: 'permission',
|
||||
action: 'create',
|
||||
name: '创建权限',
|
||||
description: '允许创建新权限',
|
||||
},
|
||||
{
|
||||
code: 'permission:read',
|
||||
resource: 'permission',
|
||||
action: 'read',
|
||||
name: '查看权限',
|
||||
description: '允许查看权限列表和详情',
|
||||
},
|
||||
{
|
||||
code: 'permission:update',
|
||||
resource: 'permission',
|
||||
action: 'update',
|
||||
name: '更新权限',
|
||||
description: '允许更新权限信息',
|
||||
},
|
||||
{
|
||||
code: 'permission:delete',
|
||||
resource: 'permission',
|
||||
action: 'delete',
|
||||
name: '删除权限',
|
||||
description: '允许删除权限',
|
||||
},
|
||||
|
||||
// 菜单管理权限
|
||||
{
|
||||
code: 'menu:create',
|
||||
resource: 'menu',
|
||||
action: 'create',
|
||||
name: '创建菜单',
|
||||
description: '允许创建新菜单',
|
||||
},
|
||||
{
|
||||
code: 'menu:read',
|
||||
resource: 'menu',
|
||||
action: 'read',
|
||||
name: '查看菜单',
|
||||
description: '允许查看菜单列表和详情',
|
||||
},
|
||||
{
|
||||
code: 'menu:update',
|
||||
resource: 'menu',
|
||||
action: 'update',
|
||||
name: '更新菜单',
|
||||
description: '允许更新菜单信息',
|
||||
},
|
||||
{
|
||||
code: 'menu:delete',
|
||||
resource: 'menu',
|
||||
action: 'delete',
|
||||
name: '删除菜单',
|
||||
description: '允许删除菜单',
|
||||
},
|
||||
|
||||
// 数据字典权限
|
||||
{
|
||||
code: 'dict:create',
|
||||
resource: 'dict',
|
||||
action: 'create',
|
||||
name: '创建字典',
|
||||
description: '允许创建新字典',
|
||||
},
|
||||
{
|
||||
code: 'dict:read',
|
||||
resource: 'dict',
|
||||
action: 'read',
|
||||
name: '查看字典',
|
||||
description: '允许查看字典列表和详情',
|
||||
},
|
||||
{
|
||||
code: 'dict:update',
|
||||
resource: 'dict',
|
||||
action: 'update',
|
||||
name: '更新字典',
|
||||
description: '允许更新字典信息',
|
||||
},
|
||||
{
|
||||
code: 'dict:delete',
|
||||
resource: 'dict',
|
||||
action: 'delete',
|
||||
name: '删除字典',
|
||||
description: '允许删除字典',
|
||||
},
|
||||
|
||||
// 系统配置权限
|
||||
{
|
||||
code: 'config:create',
|
||||
resource: 'config',
|
||||
action: 'create',
|
||||
name: '创建配置',
|
||||
description: '允许创建新配置',
|
||||
},
|
||||
{
|
||||
code: 'config:read',
|
||||
resource: 'config',
|
||||
action: 'read',
|
||||
name: '查看配置',
|
||||
description: '允许查看配置列表和详情',
|
||||
},
|
||||
{
|
||||
code: 'config:update',
|
||||
resource: 'config',
|
||||
action: 'update',
|
||||
name: '更新配置',
|
||||
description: '允许更新配置信息',
|
||||
},
|
||||
{
|
||||
code: 'config:delete',
|
||||
resource: 'config',
|
||||
action: 'delete',
|
||||
name: '删除配置',
|
||||
description: '允许删除配置',
|
||||
},
|
||||
|
||||
// 日志管理权限
|
||||
{
|
||||
code: 'log:read',
|
||||
resource: 'log',
|
||||
action: 'read',
|
||||
name: '查看日志',
|
||||
description: '允许查看系统日志',
|
||||
},
|
||||
{
|
||||
code: 'log:delete',
|
||||
resource: 'log',
|
||||
action: 'delete',
|
||||
name: '删除日志',
|
||||
description: '允许删除系统日志',
|
||||
},
|
||||
|
||||
// 赛事管理权限
|
||||
{
|
||||
code: 'contest:create',
|
||||
resource: 'contest',
|
||||
action: 'create',
|
||||
name: '创建赛事',
|
||||
description: '允许创建新赛事',
|
||||
},
|
||||
{
|
||||
code: 'contest:read',
|
||||
resource: 'contest',
|
||||
action: 'read',
|
||||
name: '查看赛事',
|
||||
description: '允许查看赛事列表和详情',
|
||||
},
|
||||
{
|
||||
code: 'contest:update',
|
||||
resource: 'contest',
|
||||
action: 'update',
|
||||
name: '更新赛事',
|
||||
description: '允许更新赛事信息',
|
||||
},
|
||||
{
|
||||
code: 'contest:delete',
|
||||
resource: 'contest',
|
||||
action: 'delete',
|
||||
name: '删除赛事',
|
||||
description: '允许删除赛事',
|
||||
},
|
||||
{
|
||||
code: 'contest:publish',
|
||||
resource: 'contest',
|
||||
action: 'publish',
|
||||
name: '发布赛事',
|
||||
description: '允许发布和撤回赛事',
|
||||
},
|
||||
|
||||
// 赛事公告权限
|
||||
{
|
||||
code: 'notice:create',
|
||||
resource: 'notice',
|
||||
action: 'create',
|
||||
name: '创建公告',
|
||||
description: '允许创建赛事公告',
|
||||
},
|
||||
{
|
||||
code: 'notice:read',
|
||||
resource: 'notice',
|
||||
action: 'read',
|
||||
name: '查看公告',
|
||||
description: '允许查看赛事公告',
|
||||
},
|
||||
{
|
||||
code: 'notice:update',
|
||||
resource: 'notice',
|
||||
action: 'update',
|
||||
name: '更新公告',
|
||||
description: '允许更新赛事公告',
|
||||
},
|
||||
{
|
||||
code: 'notice:delete',
|
||||
resource: 'notice',
|
||||
action: 'delete',
|
||||
name: '删除公告',
|
||||
description: '允许删除赛事公告',
|
||||
},
|
||||
|
||||
// 报名管理权限
|
||||
{
|
||||
code: 'registration:read',
|
||||
resource: 'registration',
|
||||
action: 'read',
|
||||
name: '查看报名',
|
||||
description: '允许查看报名列表',
|
||||
},
|
||||
{
|
||||
code: 'registration:audit',
|
||||
resource: 'registration',
|
||||
action: 'audit',
|
||||
name: '审核报名',
|
||||
description: '允许审核报名申请',
|
||||
},
|
||||
|
||||
// 作品管理权限
|
||||
{
|
||||
code: 'work:read',
|
||||
resource: 'work',
|
||||
action: 'read',
|
||||
name: '查看作品',
|
||||
description: '允许查看参赛作品',
|
||||
},
|
||||
{
|
||||
code: 'work:update',
|
||||
resource: 'work',
|
||||
action: 'update',
|
||||
name: '更新作品',
|
||||
description: '允许更新作品状态',
|
||||
},
|
||||
|
||||
// 评审管理权限
|
||||
{
|
||||
code: 'review:read',
|
||||
resource: 'review',
|
||||
action: 'read',
|
||||
name: '查看评审',
|
||||
description: '允许查看评审信息',
|
||||
},
|
||||
{
|
||||
code: 'review:assign',
|
||||
resource: 'review',
|
||||
action: 'assign',
|
||||
name: '分配评审',
|
||||
description: '允许分配作品给评委',
|
||||
},
|
||||
{
|
||||
code: 'review:score',
|
||||
resource: 'review',
|
||||
action: 'score',
|
||||
name: '评分',
|
||||
description: '允许对作品进行评分',
|
||||
},
|
||||
|
||||
// 评委管理权限
|
||||
{
|
||||
code: 'judge:create',
|
||||
resource: 'judge',
|
||||
action: 'create',
|
||||
name: '添加评委',
|
||||
description: '允许添加赛事评委',
|
||||
},
|
||||
{
|
||||
code: 'judge:read',
|
||||
resource: 'judge',
|
||||
action: 'read',
|
||||
name: '查看评委',
|
||||
description: '允许查看评委列表',
|
||||
},
|
||||
{
|
||||
code: 'judge:delete',
|
||||
resource: 'judge',
|
||||
action: 'delete',
|
||||
name: '移除评委',
|
||||
description: '允许移除赛事评委',
|
||||
},
|
||||
|
||||
// 赛果管理权限
|
||||
{
|
||||
code: 'result:read',
|
||||
resource: 'result',
|
||||
action: 'read',
|
||||
name: '查看赛果',
|
||||
description: '允许查看赛果信息',
|
||||
},
|
||||
{
|
||||
code: 'result:publish',
|
||||
resource: 'result',
|
||||
action: 'publish',
|
||||
name: '发布赛果',
|
||||
description: '允许发布赛事结果',
|
||||
},
|
||||
{
|
||||
code: 'result:award',
|
||||
resource: 'result',
|
||||
action: 'award',
|
||||
name: '设置奖项',
|
||||
description: '允许设置作品奖项',
|
||||
},
|
||||
|
||||
// 用户密码管理权限
|
||||
{
|
||||
code: 'user:password:update',
|
||||
resource: 'user',
|
||||
action: 'password:update',
|
||||
name: '修改用户密码',
|
||||
description: '允许修改用户密码',
|
||||
},
|
||||
];
|
||||
|
||||
async function initAdminPermissions() {
|
||||
try {
|
||||
console.log('🚀 开始为超级管理员(admin)用户初始化权限...\n');
|
||||
|
||||
// 1. 检查 admin 用户是否存在(先获取超级租户)
|
||||
console.log('👤 步骤 1: 检查 admin 用户...');
|
||||
const superTenant = await prisma.tenant.findUnique({
|
||||
where: { code: 'super' },
|
||||
});
|
||||
if (!superTenant) {
|
||||
console.error('❌ 错误: 超级租户不存在!');
|
||||
console.error(' 请先运行 pnpm init:super-tenant 创建超级租户');
|
||||
process.exit(1);
|
||||
}
|
||||
const adminUser = await prisma.user.findUnique({
|
||||
where: { tenantId_username: { tenantId: superTenant.id, username: 'admin' } },
|
||||
});
|
||||
|
||||
if (!adminUser) {
|
||||
console.error('❌ 错误: admin 用户不存在!');
|
||||
console.error(' 请先运行 pnpm init:admin 创建 admin 用户');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(
|
||||
`✅ admin 用户存在: ${adminUser.username} (${adminUser.nickname})\n`,
|
||||
);
|
||||
|
||||
// 2. 创建或更新所有权限
|
||||
console.log('📝 步骤 2: 确保所有权限存在...');
|
||||
const createdPermissions = [];
|
||||
for (const perm of permissions) {
|
||||
const permission = await prisma.permission.upsert({
|
||||
where: { tenantId_code: { tenantId: superTenant.id, code: perm.code } },
|
||||
update: {
|
||||
name: perm.name,
|
||||
resource: perm.resource,
|
||||
action: perm.action,
|
||||
description: perm.description,
|
||||
},
|
||||
create: { ...perm, tenantId: superTenant.id },
|
||||
});
|
||||
createdPermissions.push(permission);
|
||||
}
|
||||
console.log(`✅ 共确保 ${createdPermissions.length} 个权限存在\n`);
|
||||
|
||||
// 3. 创建或获取超级管理员角色
|
||||
console.log('👤 步骤 3: 确保超级管理员角色存在...');
|
||||
const adminRole = await prisma.role.upsert({
|
||||
where: { tenantId_code: { tenantId: superTenant.id, code: 'super_admin' } },
|
||||
update: {
|
||||
name: '超级管理员',
|
||||
description: '拥有系统所有权限的超级管理员角色',
|
||||
validState: 1,
|
||||
},
|
||||
create: {
|
||||
tenantId: superTenant.id,
|
||||
name: '超级管理员',
|
||||
code: 'super_admin',
|
||||
description: '拥有系统所有权限的超级管理员角色',
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
console.log(
|
||||
`✅ 超级管理员角色已确保存在: ${adminRole.name} (${adminRole.code})\n`,
|
||||
);
|
||||
|
||||
// 4. 确保超级管理员角色拥有所有权限
|
||||
console.log('🔗 步骤 4: 为超级管理员角色分配所有权限...');
|
||||
const existingRolePermissions = await prisma.rolePermission.findMany({
|
||||
where: { roleId: adminRole.id },
|
||||
select: { permissionId: true },
|
||||
});
|
||||
const existingPermissionIds = new Set(
|
||||
existingRolePermissions.map((rp) => rp.permissionId),
|
||||
);
|
||||
|
||||
let addedCount = 0;
|
||||
for (const permission of createdPermissions) {
|
||||
if (!existingPermissionIds.has(permission.id)) {
|
||||
await prisma.rolePermission.create({
|
||||
data: {
|
||||
roleId: adminRole.id,
|
||||
permissionId: permission.id,
|
||||
},
|
||||
});
|
||||
addedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (addedCount > 0) {
|
||||
console.log(`✅ 为超级管理员角色添加了 ${addedCount} 个权限\n`);
|
||||
} else {
|
||||
console.log(
|
||||
`✅ 超级管理员角色已拥有所有权限(${createdPermissions.length} 个)\n`,
|
||||
);
|
||||
}
|
||||
|
||||
// 5. 确保 admin 用户拥有超级管理员角色
|
||||
console.log('🔗 步骤 5: 确保 admin 用户拥有超级管理员角色...');
|
||||
const existingUserRole = await prisma.userRole.findUnique({
|
||||
where: {
|
||||
userId_roleId: {
|
||||
userId: adminUser.id,
|
||||
roleId: adminRole.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingUserRole) {
|
||||
await prisma.userRole.create({
|
||||
data: {
|
||||
userId: adminUser.id,
|
||||
roleId: adminRole.id,
|
||||
},
|
||||
});
|
||||
console.log(`✅ 已为 admin 用户分配超级管理员角色\n`);
|
||||
} else {
|
||||
console.log(`✅ admin 用户已拥有超级管理员角色\n`);
|
||||
}
|
||||
|
||||
// 6. 验证结果
|
||||
console.log('🔍 步骤 6: 验证结果...');
|
||||
const userWithRoles = await prisma.user.findUnique({
|
||||
where: { id: adminUser.id },
|
||||
include: {
|
||||
roles: {
|
||||
include: {
|
||||
role: {
|
||||
include: {
|
||||
permissions: {
|
||||
include: {
|
||||
permission: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const roleCodes = userWithRoles?.roles.map((ur) => ur.role.code) || [];
|
||||
const permissionCodes = new Set<string>();
|
||||
userWithRoles?.roles.forEach((ur) => {
|
||||
ur.role.permissions.forEach((rp) => {
|
||||
permissionCodes.add(rp.permission.code);
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`\n📊 初始化结果:`);
|
||||
console.log(` 用户名: ${adminUser.username}`);
|
||||
console.log(` 昵称: ${adminUser.nickname}`);
|
||||
console.log(` 角色: ${roleCodes.join(', ')}`);
|
||||
console.log(` 权限数量: ${permissionCodes.size}`);
|
||||
console.log(` 权限列表:`);
|
||||
Array.from(permissionCodes)
|
||||
.sort()
|
||||
.forEach((code) => {
|
||||
console.log(` - ${code}`);
|
||||
});
|
||||
console.log(`\n✅ 超级管理员权限初始化完成!`);
|
||||
} catch (error) {
|
||||
console.error('❌ 初始化失败:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// 执行初始化
|
||||
initAdminPermissions()
|
||||
.then(() => {
|
||||
console.log('\n🎉 权限初始化脚本执行完成!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('\n💥 权限初始化脚本执行失败:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
277
backend/scripts/init-admin.ts
Normal file
277
backend/scripts/init-admin.ts
Normal file
@ -0,0 +1,277 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
// 初始化超级管理员脚本(支持多租户)
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
|
||||
// 根据 NODE_ENV 加载对应的环境配置文件
|
||||
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||
const envFile = `.env.${nodeEnv}`;
|
||||
const backendDir = path.resolve(__dirname, '..');
|
||||
const envPath = path.resolve(backendDir, envFile);
|
||||
|
||||
dotenv.config({ path: envPath });
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
dotenv.config({ path: path.resolve(backendDir, '.env') });
|
||||
}
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error('❌ 错误: 未找到 DATABASE_URL 环境变量');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// 超级管理员基础权限
|
||||
const permissions = [
|
||||
// 工作台
|
||||
{ code: 'workbench:read', resource: 'workbench', action: 'read', name: '查看工作台', description: '允许查看工作台' },
|
||||
// 用户管理
|
||||
{ code: 'user:create', resource: 'user', action: 'create', name: '创建用户', description: '允许创建新用户' },
|
||||
{ code: 'user:read', resource: 'user', action: 'read', name: '查看用户', description: '允许查看用户列表和详情' },
|
||||
{ code: 'user:update', resource: 'user', action: 'update', name: '更新用户', description: '允许更新用户信息' },
|
||||
{ code: 'user:delete', resource: 'user', action: 'delete', name: '删除用户', description: '允许删除用户' },
|
||||
// 角色管理
|
||||
{ code: 'role:create', resource: 'role', action: 'create', name: '创建角色', description: '允许创建新角色' },
|
||||
{ code: 'role:read', resource: 'role', action: 'read', name: '查看角色', description: '允许查看角色列表和详情' },
|
||||
{ code: 'role:update', resource: 'role', action: 'update', name: '更新角色', description: '允许更新角色信息' },
|
||||
{ code: 'role:delete', resource: 'role', action: 'delete', name: '删除角色', description: '允许删除角色' },
|
||||
{ code: 'role:assign', resource: 'role', action: 'assign', name: '分配角色', description: '允许给用户分配角色' },
|
||||
// 权限管理
|
||||
{ code: 'permission:read', resource: 'permission', action: 'read', name: '查看权限', description: '允许查看权限列表' },
|
||||
// 菜单管理
|
||||
{ code: 'menu:read', resource: 'menu', action: 'read', name: '查看菜单', description: '允许查看菜单列表' },
|
||||
// 租户管理
|
||||
{ code: 'tenant:create', resource: 'tenant', action: 'create', name: '创建租户', description: '允许创建租户' },
|
||||
{ code: 'tenant:read', resource: 'tenant', action: 'read', name: '查看租户', description: '允许查看租户列表' },
|
||||
{ code: 'tenant:update', resource: 'tenant', action: 'update', name: '更新租户', description: '允许更新租户信息' },
|
||||
{ code: 'tenant:delete', resource: 'tenant', action: 'delete', name: '删除租户', description: '允许删除租户' },
|
||||
// 赛事管理
|
||||
{ code: 'contest:create', resource: 'contest', action: 'create', name: '创建赛事', description: '允许创建赛事' },
|
||||
{ code: 'contest:read', resource: 'contest', action: 'read', name: '查看赛事', description: '允许查看赛事列表' },
|
||||
{ code: 'contest:update', resource: 'contest', action: 'update', name: '更新赛事', description: '允许更新赛事信息' },
|
||||
{ code: 'contest:delete', resource: 'contest', action: 'delete', name: '删除赛事', description: '允许删除赛事' },
|
||||
{ code: 'contest:publish', resource: 'contest', action: 'publish', name: '发布赛事', description: '允许发布赛事' },
|
||||
{ code: 'contest:finish', resource: 'contest', action: 'finish', name: '结束赛事', description: '允许结束赛事' },
|
||||
// 评审规则
|
||||
{ code: 'review-rule:create', resource: 'review-rule', action: 'create', name: '创建评审规则', description: '允许创建评审规则' },
|
||||
{ code: 'review-rule:read', resource: 'review-rule', action: 'read', name: '查看评审规则', description: '允许查看评审规则' },
|
||||
{ code: 'review-rule:update', resource: 'review-rule', action: 'update', name: '更新评审规则', description: '允许更新评审规则' },
|
||||
{ code: 'review-rule:delete', resource: 'review-rule', action: 'delete', name: '删除评审规则', description: '允许删除评审规则' },
|
||||
// 评委管理
|
||||
{ code: 'judge:create', resource: 'judge', action: 'create', name: '添加评委', description: '允许添加评委' },
|
||||
{ code: 'judge:read', resource: 'judge', action: 'read', name: '查看评委', description: '允许查看评委列表' },
|
||||
{ code: 'judge:update', resource: 'judge', action: 'update', name: '更新评委', description: '允许更新评委信息' },
|
||||
{ code: 'judge:delete', resource: 'judge', action: 'delete', name: '删除评委', description: '允许删除评委' },
|
||||
{ code: 'judge:assign', resource: 'judge', action: 'assign', name: '分配评委', description: '允许为赛事分配评委' },
|
||||
// 报名管理
|
||||
{ code: 'registration:read', resource: 'registration', action: 'read', name: '查看报名', description: '允许查看报名记录' },
|
||||
{ code: 'registration:approve', resource: 'registration', action: 'approve', name: '审核报名', description: '允许审核报名' },
|
||||
// 作品管理
|
||||
{ code: 'work:read', resource: 'work', action: 'read', name: '查看作品', description: '允许查看参赛作品' },
|
||||
// 公告管理
|
||||
{ code: 'notice:create', resource: 'notice', action: 'create', name: '创建公告', description: '允许创建赛事公告' },
|
||||
{ code: 'notice:read', resource: 'notice', action: 'read', name: '查看公告', description: '允许查看赛事公告' },
|
||||
{ code: 'notice:update', resource: 'notice', action: 'update', name: '更新公告', description: '允许更新公告信息' },
|
||||
{ code: 'notice:delete', resource: 'notice', action: 'delete', name: '删除公告', description: '允许删除公告' },
|
||||
// 系统管理
|
||||
{ code: 'dict:create', resource: 'dict', action: 'create', name: '创建字典', description: '允许创建新字典' },
|
||||
{ code: 'dict:read', resource: 'dict', action: 'read', name: '查看字典', description: '允许查看字典列表和详情' },
|
||||
{ code: 'dict:update', resource: 'dict', action: 'update', name: '更新字典', description: '允许更新字典信息' },
|
||||
{ code: 'dict:delete', resource: 'dict', action: 'delete', name: '删除字典', description: '允许删除字典' },
|
||||
{ code: 'config:create', resource: 'config', action: 'create', name: '创建配置', description: '允许创建新配置' },
|
||||
{ code: 'config:read', resource: 'config', action: 'read', name: '查看配置', description: '允许查看配置列表和详情' },
|
||||
{ code: 'config:update', resource: 'config', action: 'update', name: '更新配置', description: '允许更新配置信息' },
|
||||
{ code: 'config:delete', resource: 'config', action: 'delete', name: '删除配置', description: '允许删除配置' },
|
||||
{ code: 'log:read', resource: 'log', action: 'read', name: '查看日志', description: '允许查看系统日志' },
|
||||
{ code: 'log:delete', resource: 'log', action: 'delete', name: '删除日志', description: '允许删除系统日志' },
|
||||
];
|
||||
|
||||
async function initAdmin() {
|
||||
try {
|
||||
console.log('🚀 开始初始化超级管理员...\n');
|
||||
|
||||
// 1. 获取或创建超级租户
|
||||
console.log('🏢 步骤 1: 获取超级租户...');
|
||||
let superTenant = await prisma.tenant.findFirst({
|
||||
where: { isSuper: 1, validState: 1 }
|
||||
});
|
||||
|
||||
if (!superTenant) {
|
||||
console.log(' 未找到超级租户,正在创建...');
|
||||
superTenant = await prisma.tenant.create({
|
||||
data: {
|
||||
name: '超级租户',
|
||||
code: 'super',
|
||||
isSuper: 1,
|
||||
validState: 1,
|
||||
}
|
||||
});
|
||||
console.log(` ✓ 创建超级租户: ${superTenant.name} (${superTenant.code})`);
|
||||
} else {
|
||||
console.log(` ✓ 找到超级租户: ${superTenant.name} (ID: ${superTenant.id})`);
|
||||
}
|
||||
|
||||
const tenantId = superTenant.id;
|
||||
|
||||
// 2. 创建权限
|
||||
console.log('\n📝 步骤 2: 创建基础权限...');
|
||||
const createdPermissions: any[] = [];
|
||||
|
||||
for (const perm of permissions) {
|
||||
// 使用 tenantId + code 作为唯一约束
|
||||
let permission = await prisma.permission.findFirst({
|
||||
where: { tenantId, code: perm.code }
|
||||
});
|
||||
|
||||
if (permission) {
|
||||
permission = await prisma.permission.update({
|
||||
where: { id: permission.id },
|
||||
data: { ...perm, tenantId }
|
||||
});
|
||||
} else {
|
||||
permission = await prisma.permission.create({
|
||||
data: { ...perm, tenantId, validState: 1 }
|
||||
});
|
||||
}
|
||||
|
||||
createdPermissions.push(permission);
|
||||
}
|
||||
console.log(` ✓ 共创建/更新 ${createdPermissions.length} 个权限`);
|
||||
|
||||
// 3. 创建超级管理员角色
|
||||
console.log('\n👤 步骤 3: 创建超级管理员角色...');
|
||||
let adminRole = await prisma.role.findFirst({
|
||||
where: { tenantId, code: 'super_admin' }
|
||||
});
|
||||
|
||||
if (adminRole) {
|
||||
adminRole = await prisma.role.update({
|
||||
where: { id: adminRole.id },
|
||||
data: {
|
||||
name: '超级管理员',
|
||||
description: '拥有系统所有权限的超级管理员角色',
|
||||
}
|
||||
});
|
||||
console.log(` ✓ 更新角色: ${adminRole.name}`);
|
||||
} else {
|
||||
adminRole = await prisma.role.create({
|
||||
data: {
|
||||
tenantId,
|
||||
name: '超级管理员',
|
||||
code: 'super_admin',
|
||||
description: '拥有系统所有权限的超级管理员角色',
|
||||
validState: 1,
|
||||
}
|
||||
});
|
||||
console.log(` ✓ 创建角色: ${adminRole.name}`);
|
||||
}
|
||||
|
||||
// 4. 分配权限给角色
|
||||
console.log('\n🔗 步骤 4: 分配权限给角色...');
|
||||
// 先获取已有的角色权限
|
||||
const existingRolePermissions = await prisma.rolePermission.findMany({
|
||||
where: { roleId: adminRole.id },
|
||||
select: { permissionId: true }
|
||||
});
|
||||
const existingPermissionIds = new Set(existingRolePermissions.map(rp => rp.permissionId));
|
||||
|
||||
let addedCount = 0;
|
||||
for (const perm of createdPermissions) {
|
||||
if (!existingPermissionIds.has(perm.id)) {
|
||||
await prisma.rolePermission.create({
|
||||
data: {
|
||||
roleId: adminRole.id,
|
||||
permissionId: perm.id,
|
||||
}
|
||||
});
|
||||
addedCount++;
|
||||
}
|
||||
}
|
||||
console.log(` ✓ 新增 ${addedCount} 个权限分配`);
|
||||
|
||||
// 5. 创建 admin 用户
|
||||
console.log('\n👤 步骤 5: 创建 admin 用户...');
|
||||
const password = `admin@${superTenant.code}`;
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
let adminUser = await prisma.user.findFirst({
|
||||
where: { tenantId, username: 'admin' }
|
||||
});
|
||||
|
||||
if (adminUser) {
|
||||
adminUser = await prisma.user.update({
|
||||
where: { id: adminUser.id },
|
||||
data: {
|
||||
password: hashedPassword,
|
||||
nickname: '超级管理员',
|
||||
validState: 1,
|
||||
}
|
||||
});
|
||||
console.log(` ✓ 更新用户: ${adminUser.username}`);
|
||||
} else {
|
||||
adminUser = await prisma.user.create({
|
||||
data: {
|
||||
tenantId,
|
||||
username: 'admin',
|
||||
password: hashedPassword,
|
||||
nickname: '超级管理员',
|
||||
validState: 1,
|
||||
}
|
||||
});
|
||||
console.log(` ✓ 创建用户: ${adminUser.username}`);
|
||||
}
|
||||
|
||||
// 6. 给用户分配角色
|
||||
console.log('\n🔗 步骤 6: 分配角色给用户...');
|
||||
const existingUserRole = await prisma.userRole.findFirst({
|
||||
where: { userId: adminUser.id, roleId: adminRole.id }
|
||||
});
|
||||
|
||||
if (!existingUserRole) {
|
||||
await prisma.userRole.create({
|
||||
data: {
|
||||
userId: adminUser.id,
|
||||
roleId: adminRole.id,
|
||||
}
|
||||
});
|
||||
console.log(` ✓ 分配角色: ${adminRole.name}`);
|
||||
} else {
|
||||
console.log(` ✓ 角色已分配: ${adminRole.name}`);
|
||||
}
|
||||
|
||||
// 7. 输出结果
|
||||
console.log('\n' + '='.repeat(50));
|
||||
console.log('🎉 超级管理员初始化完成!');
|
||||
console.log('='.repeat(50));
|
||||
console.log(` 租户编码: ${superTenant.code}`);
|
||||
console.log(` 用户名: admin`);
|
||||
console.log(` 密码: ${password}`);
|
||||
console.log(` 角色: ${adminRole.name}`);
|
||||
console.log(` 权限数量: ${createdPermissions.length}`);
|
||||
console.log('='.repeat(50));
|
||||
console.log('\n💡 提示: 请运行以下命令初始化菜单:');
|
||||
console.log(' npm run init:menus');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 初始化失败:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// 执行初始化
|
||||
initAdmin()
|
||||
.then(() => {
|
||||
console.log('\n✅ 初始化脚本执行完成!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('\n💥 初始化脚本执行失败:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
210
backend/scripts/init-linksea-tenant.ts
Normal file
210
backend/scripts/init-linksea-tenant.ts
Normal file
@ -0,0 +1,210 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
// 加载环境变量(必须在其他导入之前)
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
|
||||
// 根据 NODE_ENV 加载对应的环境配置文件
|
||||
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||
const envFile = `.env.${nodeEnv}`;
|
||||
// scripts 目录的父目录就是 backend 目录
|
||||
const backendDir = path.resolve(__dirname, '..');
|
||||
const envPath = path.resolve(backendDir, envFile);
|
||||
|
||||
// 尝试加载环境特定的配置文件
|
||||
dotenv.config({ path: envPath });
|
||||
|
||||
// 如果环境特定文件不存在,尝试加载默认的 .env 文件
|
||||
if (!process.env.DATABASE_URL) {
|
||||
dotenv.config({ path: path.resolve(backendDir, '.env') });
|
||||
}
|
||||
|
||||
// 验证必要的环境变量
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error('❌ 错误: 未找到 DATABASE_URL 环境变量');
|
||||
console.error(` 请确保存在以下文件之一:`);
|
||||
console.error(` - ${envPath}`);
|
||||
console.error(` - ${path.resolve(backendDir, '.env')}`);
|
||||
console.error(` 或者设置 NODE_ENV 环境变量(当前: ${nodeEnv})`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('🚀 开始创建 LinkSea 普通租户...\n');
|
||||
|
||||
const tenantCode = 'linksea';
|
||||
const menuNames = ['赛事管理', '系统管理'];
|
||||
|
||||
// 1. 查找或创建租户
|
||||
console.log(`📋 步骤 1: 查找或创建租户 "${tenantCode}"...`);
|
||||
let tenant = await prisma.tenant.findUnique({
|
||||
where: { code: tenantCode },
|
||||
});
|
||||
|
||||
if (!tenant) {
|
||||
// 创建普通租户
|
||||
tenant = await prisma.tenant.create({
|
||||
data: {
|
||||
name: 'LinkSea 租户',
|
||||
code: tenantCode,
|
||||
domain: tenantCode,
|
||||
description: 'LinkSea 普通租户',
|
||||
isSuper: 0,
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
console.log(`✅ 租户创建成功: ${tenant.name} (${tenant.code})\n`);
|
||||
} else {
|
||||
if (tenant.validState !== 1) {
|
||||
console.error(`❌ 错误: 租户 "${tenantCode}" 状态无效!`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`✅ 找到租户: ${tenant.name} (${tenant.code})\n`);
|
||||
}
|
||||
|
||||
// 2. 查找指定的菜单(顶级菜单)
|
||||
console.log(`📋 步骤 2: 查找菜单 "${menuNames.join('", "')}"...`);
|
||||
const menus = await prisma.menu.findMany({
|
||||
where: {
|
||||
name: { in: menuNames },
|
||||
parentId: null, // 只查找顶级菜单
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (menus.length === 0) {
|
||||
console.error(`❌ 错误: 未找到指定的菜单!`);
|
||||
console.error(` 请确保菜单 "${menuNames.join('", "')}" 已初始化`);
|
||||
console.error(` 运行: pnpm init:menus`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (menus.length !== menuNames.length) {
|
||||
const foundMenuNames = menus.map((m) => m.name);
|
||||
const missingMenus = menuNames.filter(
|
||||
(name) => !foundMenuNames.includes(name),
|
||||
);
|
||||
console.warn(`⚠️ 警告: 部分菜单未找到: ${missingMenus.join(', ')}`);
|
||||
console.log(` 找到的菜单: ${foundMenuNames.join(', ')}\n`);
|
||||
} else {
|
||||
console.log(`✅ 找到 ${menus.length} 个菜单:`);
|
||||
menus.forEach((menu) => {
|
||||
console.log(` ✓ ${menu.name}`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// 3. 递归获取菜单及其所有子菜单
|
||||
console.log(`📋 步骤 3: 获取菜单及其所有子菜单...`);
|
||||
const menuIds = new Set<number>();
|
||||
|
||||
// 递归函数:获取菜单及其所有子菜单的ID
|
||||
async function getMenuAndChildrenIds(menuId: number) {
|
||||
menuIds.add(menuId);
|
||||
|
||||
// 获取所有子菜单
|
||||
const children = await prisma.menu.findMany({
|
||||
where: {
|
||||
parentId: menuId,
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
|
||||
// 递归获取子菜单的子菜单
|
||||
for (const child of children) {
|
||||
await getMenuAndChildrenIds(child.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 为每个顶级菜单获取所有子菜单
|
||||
for (const menu of menus) {
|
||||
await getMenuAndChildrenIds(menu.id);
|
||||
}
|
||||
|
||||
const menuIdArray = Array.from(menuIds);
|
||||
console.log(`✅ 共找到 ${menuIdArray.length} 个菜单(包括子菜单)\n`);
|
||||
|
||||
// 4. 获取租户已分配的菜单
|
||||
console.log(`📋 步骤 4: 检查租户已分配的菜单...`);
|
||||
const existingTenantMenus = await prisma.tenantMenu.findMany({
|
||||
where: {
|
||||
tenantId: tenant.id,
|
||||
},
|
||||
select: {
|
||||
menuId: true,
|
||||
},
|
||||
});
|
||||
|
||||
const existingMenuIds = new Set(existingTenantMenus.map((tm) => tm.menuId));
|
||||
|
||||
// 5. 为租户分配菜单(只分配新的菜单)
|
||||
console.log(`📋 步骤 5: 为租户分配菜单...`);
|
||||
const menusToAdd = menuIdArray.filter((id) => !existingMenuIds.has(id));
|
||||
|
||||
if (menusToAdd.length === 0) {
|
||||
console.log(`✅ 租户已拥有所有指定的菜单\n`);
|
||||
} else {
|
||||
let addedCount = 0;
|
||||
const menuNamesToAdd: string[] = [];
|
||||
|
||||
for (const menuId of menusToAdd) {
|
||||
const menu = await prisma.menu.findUnique({
|
||||
where: { id: menuId },
|
||||
select: { name: true },
|
||||
});
|
||||
|
||||
await prisma.tenantMenu.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
menuId: menuId,
|
||||
},
|
||||
});
|
||||
addedCount++;
|
||||
if (menu) {
|
||||
menuNamesToAdd.push(menu.name);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ 为租户添加了 ${addedCount} 个菜单:`);
|
||||
menuNamesToAdd.forEach((name) => {
|
||||
console.log(` ✓ ${name}`);
|
||||
});
|
||||
console.log(
|
||||
`\n✅ 租户现在拥有 ${menuIdArray.length} 个菜单(包括子菜单)\n`,
|
||||
);
|
||||
}
|
||||
|
||||
// 6. 验证结果
|
||||
console.log('📊 初始化结果:');
|
||||
console.log('========================================');
|
||||
console.log('租户信息:');
|
||||
console.log(` 租户编码: ${tenant.code}`);
|
||||
console.log(` 租户名称: ${tenant.name}`);
|
||||
console.log(` 租户类型: ${tenant.isSuper === 1 ? '超级租户' : '普通租户'}`);
|
||||
console.log(` 访问链接: http://your-domain.com/?tenant=${tenant.code}`);
|
||||
console.log('========================================');
|
||||
console.log('分配的菜单:');
|
||||
console.log(` 顶级菜单: ${menuNames.join(', ')}`);
|
||||
console.log(` 菜单总数: ${menuIdArray.length} 个(包括子菜单)`);
|
||||
console.log('========================================');
|
||||
console.log('\n💡 提示:');
|
||||
console.log(' 如需创建管理员账号,请运行: pnpm init:tenant-admin linksea');
|
||||
console.log('========================================');
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => {
|
||||
console.log('\n🎉 LinkSea 租户创建脚本执行完成!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('\n💥 LinkSea 租户创建脚本执行失败:', error);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
277
backend/scripts/init-menus.ts
Normal file
277
backend/scripts/init-menus.ts
Normal file
@ -0,0 +1,277 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
// 加载环境变量(必须在其他导入之前)
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
|
||||
// 根据 NODE_ENV 加载对应的环境配置文件
|
||||
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||
const envFile = `.env.${nodeEnv}`;
|
||||
// scripts 目录的父目录就是 backend 目录
|
||||
const backendDir = path.resolve(__dirname, '..');
|
||||
const envPath = path.resolve(backendDir, envFile);
|
||||
|
||||
// 尝试加载环境特定的配置文件
|
||||
dotenv.config({ path: envPath });
|
||||
|
||||
// 如果环境特定文件不存在,尝试加载默认的 .env 文件
|
||||
if (!process.env.DATABASE_URL) {
|
||||
dotenv.config({ path: path.resolve(backendDir, '.env') });
|
||||
}
|
||||
|
||||
// 验证必要的环境变量
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error('❌ 错误: 未找到 DATABASE_URL 环境变量');
|
||||
console.error(` 请确保存在以下文件之一:`);
|
||||
console.error(` - ${envPath}`);
|
||||
console.error(` - ${path.resolve(backendDir, '.env')}`);
|
||||
console.error(` 或者设置 NODE_ENV 环境变量(当前: ${nodeEnv})`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import * as fs from 'fs';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// 从 JSON 文件加载菜单数据
|
||||
const menusFilePath = path.resolve(backendDir, 'data', 'menus.json');
|
||||
|
||||
if (!fs.existsSync(menusFilePath)) {
|
||||
console.error(`❌ 错误: 菜单数据文件不存在: ${menusFilePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const menus = JSON.parse(fs.readFileSync(menusFilePath, 'utf-8'));
|
||||
|
||||
// 超级租户可见的菜单名称(工作台只对普通租户可见)
|
||||
const SUPER_TENANT_MENUS = ['赛事活动', '赛事管理', '系统管理'];
|
||||
|
||||
// 普通租户可见的菜单名称
|
||||
const NORMAL_TENANT_MENUS = ['工作台', '学校管理', '赛事活动', '作业管理', '系统管理'];
|
||||
|
||||
// 普通租户在系统管理下排除的子菜单(只保留用户管理和角色管理)
|
||||
const NORMAL_TENANT_EXCLUDED_SYSTEM_MENUS = ['租户管理', '数据字典', '系统配置', '日志记录', '菜单管理', '权限管理'];
|
||||
|
||||
// 普通租户在赛事活动下排除的子菜单(只保留活动列表)
|
||||
const NORMAL_TENANT_EXCLUDED_ACTIVITY_MENUS = ['我的报名', '我的作品'];
|
||||
|
||||
async function initMenus() {
|
||||
try {
|
||||
console.log('🚀 开始初始化菜单数据...\n');
|
||||
|
||||
// 递归创建菜单
|
||||
async function createMenu(menuData: any, parentId: number | null = null) {
|
||||
const { children, ...menuFields } = menuData;
|
||||
|
||||
// 查找是否已存在相同名称和父菜单的菜单
|
||||
const existingMenu = await prisma.menu.findFirst({
|
||||
where: {
|
||||
name: menuFields.name,
|
||||
parentId: parentId,
|
||||
},
|
||||
});
|
||||
|
||||
let menu;
|
||||
if (existingMenu) {
|
||||
// 更新现有菜单
|
||||
menu = await prisma.menu.update({
|
||||
where: { id: existingMenu.id },
|
||||
data: {
|
||||
name: menuFields.name,
|
||||
path: menuFields.path || null,
|
||||
icon: menuFields.icon || null,
|
||||
component: menuFields.component || null,
|
||||
permission: menuFields.permission || null,
|
||||
parentId: parentId,
|
||||
sort: menuFields.sort || 0,
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// 创建新菜单
|
||||
menu = await prisma.menu.create({
|
||||
data: {
|
||||
name: menuFields.name,
|
||||
path: menuFields.path || null,
|
||||
icon: menuFields.icon || null,
|
||||
component: menuFields.component || null,
|
||||
permission: menuFields.permission || null,
|
||||
parentId: parentId,
|
||||
sort: menuFields.sort || 0,
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log(` ✓ ${menu.name} (${menu.path || '无路径'})`);
|
||||
|
||||
// 如果有子菜单,递归创建
|
||||
if (children && children.length > 0) {
|
||||
for (const child of children) {
|
||||
await createMenu(child, menu.id);
|
||||
}
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
// 清空现有菜单(重新初始化)
|
||||
console.log('🗑️ 清空现有菜单和租户菜单关联...');
|
||||
// 先删除租户菜单关联
|
||||
await prisma.tenantMenu.deleteMany({});
|
||||
// 再删除所有子菜单,再删除父菜单(避免外键约束问题)
|
||||
await prisma.menu.deleteMany({
|
||||
where: {
|
||||
parentId: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
await prisma.menu.deleteMany({
|
||||
where: {
|
||||
parentId: null,
|
||||
},
|
||||
});
|
||||
console.log('✅ 已清空现有菜单\n');
|
||||
|
||||
// 创建所有菜单
|
||||
console.log('📝 创建菜单...\n');
|
||||
for (const menu of menus) {
|
||||
await createMenu(menu);
|
||||
}
|
||||
|
||||
// 验证结果
|
||||
console.log('\n🔍 验证结果...');
|
||||
const allMenus = await prisma.menu.findMany({
|
||||
orderBy: [{ sort: 'asc' }, { id: 'asc' }],
|
||||
include: {
|
||||
children: {
|
||||
orderBy: {
|
||||
sort: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const topLevelMenus = allMenus.filter((m) => !m.parentId);
|
||||
const totalMenus = allMenus.length;
|
||||
|
||||
console.log(`\n📊 初始化结果:`);
|
||||
console.log(` 顶级菜单数量: ${topLevelMenus.length}`);
|
||||
console.log(` 总菜单数量: ${totalMenus}`);
|
||||
console.log(`\n📋 菜单结构:`);
|
||||
|
||||
function printMenuTree(menu: any, indent: string = '') {
|
||||
console.log(`${indent}├─ ${menu.name} (${menu.path || '无路径'})`);
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
menu.children.forEach((child: any, index: number) => {
|
||||
const isLast = index === menu.children.length - 1;
|
||||
const childIndent = indent + (isLast ? ' ' : '│ ');
|
||||
printMenuTree(child, childIndent);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
topLevelMenus.forEach((menu) => {
|
||||
printMenuTree(menu);
|
||||
});
|
||||
|
||||
// 为所有现有租户分配菜单(区分超级租户和普通租户)
|
||||
console.log(`\n📋 为所有租户分配菜单...`);
|
||||
const allTenants = await prisma.tenant.findMany({
|
||||
where: { validState: 1 },
|
||||
});
|
||||
|
||||
if (allTenants.length === 0) {
|
||||
console.log('⚠️ 没有找到任何有效租户,跳过菜单分配\n');
|
||||
} else {
|
||||
console.log(` 找到 ${allTenants.length} 个租户\n`);
|
||||
|
||||
// 获取超级租户菜单ID(工作台、赛事活动、赛事管理、系统管理及其子菜单)
|
||||
const superTenantMenuIds = new Set<number>();
|
||||
for (const menu of allMenus) {
|
||||
// 顶级菜单
|
||||
if (!menu.parentId && SUPER_TENANT_MENUS.includes(menu.name)) {
|
||||
superTenantMenuIds.add(menu.id);
|
||||
}
|
||||
// 子菜单(检查父菜单是否在超级租户菜单中)
|
||||
if (menu.parentId) {
|
||||
const parentMenu = allMenus.find(m => m.id === menu.parentId);
|
||||
if (parentMenu && SUPER_TENANT_MENUS.includes(parentMenu.name)) {
|
||||
superTenantMenuIds.add(menu.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取普通租户菜单ID(工作台、学校管理、赛事活动、作业管理、部分系统管理)
|
||||
const normalTenantMenuIds = new Set<number>();
|
||||
for (const menu of allMenus) {
|
||||
// 顶级菜单
|
||||
if (!menu.parentId && NORMAL_TENANT_MENUS.includes(menu.name)) {
|
||||
normalTenantMenuIds.add(menu.id);
|
||||
}
|
||||
// 子菜单
|
||||
if (menu.parentId) {
|
||||
const parentMenu = allMenus.find(m => m.id === menu.parentId);
|
||||
if (parentMenu && NORMAL_TENANT_MENUS.includes(parentMenu.name)) {
|
||||
// 系统管理下排除部分子菜单
|
||||
if (parentMenu.name === '系统管理' && NORMAL_TENANT_EXCLUDED_SYSTEM_MENUS.includes(menu.name)) {
|
||||
continue; // 跳过排除的菜单
|
||||
}
|
||||
// 赛事活动下排除部分子菜单(只保留活动列表)
|
||||
if (parentMenu.name === '赛事活动' && NORMAL_TENANT_EXCLUDED_ACTIVITY_MENUS.includes(menu.name)) {
|
||||
continue; // 跳过排除的菜单
|
||||
}
|
||||
normalTenantMenuIds.add(menu.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const tenant of allTenants) {
|
||||
const isSuperTenant = tenant.isSuper === 1;
|
||||
|
||||
// 确定要分配的菜单
|
||||
const menusToAssign = isSuperTenant
|
||||
? allMenus.filter(m => superTenantMenuIds.has(m.id))
|
||||
: allMenus.filter(m => normalTenantMenuIds.has(m.id));
|
||||
|
||||
// 为租户分配菜单
|
||||
let addedMenuCount = 0;
|
||||
for (const menu of menusToAssign) {
|
||||
await prisma.tenantMenu.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
menuId: menu.id,
|
||||
},
|
||||
});
|
||||
addedMenuCount++;
|
||||
}
|
||||
|
||||
const tenantType = isSuperTenant ? '(超级租户)' : '(普通租户)';
|
||||
console.log(
|
||||
` ✓ 租户 "${tenant.name}" ${tenantType}: 分配了 ${addedMenuCount} 个菜单`,
|
||||
);
|
||||
}
|
||||
console.log(`\n✅ 菜单分配完成!`);
|
||||
}
|
||||
|
||||
console.log(`\n✅ 菜单初始化完成!`);
|
||||
} catch (error) {
|
||||
console.error('\n💥 初始化菜单失败:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// 执行初始化
|
||||
initMenus()
|
||||
.then(() => {
|
||||
console.log('\n🎉 菜单初始化脚本执行完成!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('\n💥 菜单初始化脚本执行失败:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
576
backend/scripts/init-roles-permissions.ts
Normal file
576
backend/scripts/init-roles-permissions.ts
Normal file
@ -0,0 +1,576 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
|
||||
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||
const envFile = `.env.${nodeEnv}`;
|
||||
const backendDir = path.resolve(__dirname, '..');
|
||||
const envPath = path.resolve(backendDir, envFile);
|
||||
|
||||
dotenv.config({ path: envPath });
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
dotenv.config({ path: path.resolve(backendDir, '.env') });
|
||||
}
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error('DATABASE_URL not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// ============================================
|
||||
// 权限定义
|
||||
// ============================================
|
||||
|
||||
// 基础权限(所有角色共享的权限池)
|
||||
const allPermissions = [
|
||||
// AI 3D建模
|
||||
{ code: 'ai-3d:read', resource: 'ai-3d', action: 'read', name: '使用3D建模实验室', description: '允许使用AI 3D建模实验室' },
|
||||
{ code: 'ai-3d:create', resource: 'ai-3d', action: 'create', name: '创建3D模型任务', description: '允许创建AI 3D模型生成任务' },
|
||||
|
||||
// 用户管理
|
||||
{ code: 'user:create', resource: 'user', action: 'create', name: '创建用户', description: '允许创建新用户' },
|
||||
{ code: 'user:read', resource: 'user', action: 'read', name: '查看用户', description: '允许查看用户列表和详情' },
|
||||
{ code: 'user:update', resource: 'user', action: 'update', name: '更新用户', description: '允许更新用户信息' },
|
||||
{ code: 'user:delete', resource: 'user', action: 'delete', name: '删除用户', description: '允许删除用户' },
|
||||
{ code: 'user:password:update', resource: 'user', action: 'password:update', name: '重置密码', description: '允许重置用户密码' },
|
||||
|
||||
// 角色管理
|
||||
{ code: 'role:create', resource: 'role', action: 'create', name: '创建角色', description: '允许创建新角色' },
|
||||
{ code: 'role:read', resource: 'role', action: 'read', name: '查看角色', description: '允许查看角色列表和详情' },
|
||||
{ code: 'role:update', resource: 'role', action: 'update', name: '更新角色', description: '允许更新角色信息' },
|
||||
{ code: 'role:delete', resource: 'role', action: 'delete', name: '删除角色', description: '允许删除角色' },
|
||||
{ code: 'role:assign', resource: 'role', action: 'assign', name: '分配角色', description: '允许给用户分配角色' },
|
||||
|
||||
// 权限管理
|
||||
{ code: 'permission:create', resource: 'permission', action: 'create', name: '创建权限', description: '允许创建新权限' },
|
||||
{ code: 'permission:read', resource: 'permission', action: 'read', name: '查看权限', description: '允许查看权限列表' },
|
||||
{ code: 'permission:update', resource: 'permission', action: 'update', name: '更新权限', description: '允许更新权限信息' },
|
||||
{ code: 'permission:delete', resource: 'permission', action: 'delete', name: '删除权限', description: '允许删除权限' },
|
||||
|
||||
// 菜单管理
|
||||
{ code: 'menu:create', resource: 'menu', action: 'create', name: '创建菜单', description: '允许创建新菜单' },
|
||||
{ code: 'menu:read', resource: 'menu', action: 'read', name: '查看菜单', description: '允许查看菜单列表' },
|
||||
{ code: 'menu:update', resource: 'menu', action: 'update', name: '更新菜单', description: '允许更新菜单信息' },
|
||||
{ code: 'menu:delete', resource: 'menu', action: 'delete', name: '删除菜单', description: '允许删除菜单' },
|
||||
|
||||
// 租户管理(超级租户专属)
|
||||
{ code: 'tenant:create', resource: 'tenant', action: 'create', name: '创建租户', description: '允许创建租户' },
|
||||
{ code: 'tenant:read', resource: 'tenant', action: 'read', name: '查看租户', description: '允许查看租户列表' },
|
||||
{ code: 'tenant:update', resource: 'tenant', action: 'update', name: '更新租户', description: '允许更新租户信息' },
|
||||
{ code: 'tenant:delete', resource: 'tenant', action: 'delete', name: '删除租户', description: '允许删除租户' },
|
||||
|
||||
// 学校管理
|
||||
{ code: 'school:create', resource: 'school', action: 'create', name: '创建学校', description: '允许创建学校信息' },
|
||||
{ code: 'school:read', resource: 'school', action: 'read', name: '查看学校', description: '允许查看学校信息' },
|
||||
{ code: 'school:update', resource: 'school', action: 'update', name: '更新学校', description: '允许更新学校信息' },
|
||||
{ code: 'school:delete', resource: 'school', action: 'delete', name: '删除学校', description: '允许删除学校信息' },
|
||||
|
||||
// 部门管理
|
||||
{ code: 'department:create', resource: 'department', action: 'create', name: '创建部门', description: '允许创建部门' },
|
||||
{ code: 'department:read', resource: 'department', action: 'read', name: '查看部门', description: '允许查看部门列表' },
|
||||
{ code: 'department:update', resource: 'department', action: 'update', name: '更新部门', description: '允许更新部门信息' },
|
||||
{ code: 'department:delete', resource: 'department', action: 'delete', name: '删除部门', description: '允许删除部门' },
|
||||
|
||||
// 年级管理
|
||||
{ code: 'grade:create', resource: 'grade', action: 'create', name: '创建年级', description: '允许创建年级' },
|
||||
{ code: 'grade:read', resource: 'grade', action: 'read', name: '查看年级', description: '允许查看年级列表' },
|
||||
{ code: 'grade:update', resource: 'grade', action: 'update', name: '更新年级', description: '允许更新年级信息' },
|
||||
{ code: 'grade:delete', resource: 'grade', action: 'delete', name: '删除年级', description: '允许删除年级' },
|
||||
|
||||
// 班级管理
|
||||
{ code: 'class:create', resource: 'class', action: 'create', name: '创建班级', description: '允许创建班级' },
|
||||
{ code: 'class:read', resource: 'class', action: 'read', name: '查看班级', description: '允许查看班级列表' },
|
||||
{ code: 'class:update', resource: 'class', action: 'update', name: '更新班级', description: '允许更新班级信息' },
|
||||
{ code: 'class:delete', resource: 'class', action: 'delete', name: '删除班级', description: '允许删除班级' },
|
||||
|
||||
// 教师管理
|
||||
{ code: 'teacher:create', resource: 'teacher', action: 'create', name: '创建教师', description: '允许创建教师' },
|
||||
{ code: 'teacher:read', resource: 'teacher', action: 'read', name: '查看教师', description: '允许查看教师列表' },
|
||||
{ code: 'teacher:update', resource: 'teacher', action: 'update', name: '更新教师', description: '允许更新教师信息' },
|
||||
{ code: 'teacher:delete', resource: 'teacher', action: 'delete', name: '删除教师', description: '允许删除教师' },
|
||||
|
||||
// 学生管理
|
||||
{ code: 'student:create', resource: 'student', action: 'create', name: '创建学生', description: '允许创建学生' },
|
||||
{ code: 'student:read', resource: 'student', action: 'read', name: '查看学生', description: '允许查看学生列表' },
|
||||
{ code: 'student:update', resource: 'student', action: 'update', name: '更新学生', description: '允许更新学生信息' },
|
||||
{ code: 'student:delete', resource: 'student', action: 'delete', name: '删除学生', description: '允许删除学生' },
|
||||
|
||||
// 赛事管理(超级租户)
|
||||
{ code: 'contest:create', resource: 'contest', action: 'create', name: '创建赛事', description: '允许创建赛事' },
|
||||
{ code: 'contest:read', resource: 'contest', action: 'read', name: '查看赛事', description: '允许查看赛事列表' },
|
||||
{ code: 'contest:update', resource: 'contest', action: 'update', name: '更新赛事', description: '允许更新赛事信息' },
|
||||
{ code: 'contest:delete', resource: 'contest', action: 'delete', name: '删除赛事', description: '允许删除赛事' },
|
||||
{ code: 'contest:publish', resource: 'contest', action: 'publish', name: '发布赛事', description: '允许发布/取消发布赛事' },
|
||||
{ code: 'contest:finish', resource: 'contest', action: 'finish', name: '结束赛事', description: '允许结束赛事' },
|
||||
|
||||
// 评审规则管理
|
||||
{ code: 'review-rule:create', resource: 'review-rule', action: 'create', name: '创建评审规则', description: '允许创建评审规则' },
|
||||
{ code: 'review-rule:read', resource: 'review-rule', action: 'read', name: '查看评审规则', description: '允许查看评审规则' },
|
||||
{ code: 'review-rule:update', resource: 'review-rule', action: 'update', name: '更新评审规则', description: '允许更新评审规则' },
|
||||
{ code: 'review-rule:delete', resource: 'review-rule', action: 'delete', name: '删除评审规则', description: '允许删除评审规则' },
|
||||
|
||||
// 评委管理
|
||||
{ code: 'judge:create', resource: 'judge', action: 'create', name: '添加评委', description: '允许添加评委' },
|
||||
{ code: 'judge:read', resource: 'judge', action: 'read', name: '查看评委', description: '允许查看评委列表' },
|
||||
{ code: 'judge:update', resource: 'judge', action: 'update', name: '更新评委', description: '允许更新评委信息' },
|
||||
{ code: 'judge:delete', resource: 'judge', action: 'delete', name: '删除评委', description: '允许删除评委' },
|
||||
{ code: 'judge:assign', resource: 'judge', action: 'assign', name: '分配评委', description: '允许为赛事分配评委' },
|
||||
|
||||
// 赛事报名(学校端)
|
||||
{ code: 'registration:create', resource: 'registration', action: 'create', name: '创建报名', description: '允许报名赛事' },
|
||||
{ code: 'registration:read', resource: 'registration', action: 'read', name: '查看报名', description: '允许查看报名记录' },
|
||||
{ code: 'registration:update', resource: 'registration', action: 'update', name: '更新报名', description: '允许更新报名信息' },
|
||||
{ code: 'registration:delete', resource: 'registration', action: 'delete', name: '取消报名', description: '允许取消报名' },
|
||||
{ code: 'registration:approve', resource: 'registration', action: 'approve', name: '审核报名', description: '允许审核报名' },
|
||||
{ code: 'registration:audit', resource: 'registration', action: 'audit', name: '审核报名记录', description: '允许审核报名记录' },
|
||||
|
||||
// 参赛作品
|
||||
{ code: 'work:create', resource: 'work', action: 'create', name: '上传作品', description: '允许上传参赛作品' },
|
||||
{ code: 'work:read', resource: 'work', action: 'read', name: '查看作品', description: '允许查看参赛作品' },
|
||||
{ code: 'work:update', resource: 'work', action: 'update', name: '更新作品', description: '允许更新作品信息' },
|
||||
{ code: 'work:delete', resource: 'work', action: 'delete', name: '删除作品', description: '允许删除作品' },
|
||||
{ code: 'work:submit', resource: 'work', action: 'submit', name: '提交作品', description: '允许提交作品' },
|
||||
|
||||
// 作品评审(评委端)
|
||||
{ code: 'review:read', resource: 'review', action: 'read', name: '查看评审任务', description: '允许查看待评审作品' },
|
||||
{ code: 'review:score', resource: 'review', action: 'score', name: '评审打分', description: '允许对作品打分' },
|
||||
{ code: 'review:assign', resource: 'review', action: 'assign', name: '分配评审', description: '允许分配作品给评委' },
|
||||
|
||||
// 赛果管理
|
||||
{ code: 'result:read', resource: 'result', action: 'read', name: '查看赛果', description: '允许查看赛事结果' },
|
||||
{ code: 'result:publish', resource: 'result', action: 'publish', name: '发布赛果', description: '允许发布赛事结果' },
|
||||
{ code: 'result:award', resource: 'result', action: 'award', name: '设置奖项', description: '允许设置奖项等级' },
|
||||
|
||||
// 赛事公告
|
||||
{ code: 'notice:create', resource: 'notice', action: 'create', name: '创建公告', description: '允许创建赛事公告' },
|
||||
{ code: 'notice:read', resource: 'notice', action: 'read', name: '查看公告', description: '允许查看赛事公告' },
|
||||
{ code: 'notice:update', resource: 'notice', action: 'update', name: '更新公告', description: '允许更新公告信息' },
|
||||
{ code: 'notice:delete', resource: 'notice', action: 'delete', name: '删除公告', description: '允许删除公告' },
|
||||
|
||||
// 作业管理
|
||||
{ code: 'homework:create', resource: 'homework', action: 'create', name: '创建作业', description: '允许创建作业' },
|
||||
{ code: 'homework:read', resource: 'homework', action: 'read', name: '查看作业', description: '允许查看作业列表' },
|
||||
{ code: 'homework:update', resource: 'homework', action: 'update', name: '更新作业', description: '允许更新作业信息' },
|
||||
{ code: 'homework:delete', resource: 'homework', action: 'delete', name: '删除作业', description: '允许删除作业' },
|
||||
{ code: 'homework:publish', resource: 'homework', action: 'publish', name: '发布作业', description: '允许发布作业' },
|
||||
|
||||
// 作业提交
|
||||
{ code: 'homework-submission:create', resource: 'homework-submission', action: 'create', name: '提交作业', description: '允许提交作业' },
|
||||
{ code: 'homework-submission:read', resource: 'homework-submission', action: 'read', name: '查看作业提交', description: '允许查看作业提交记录' },
|
||||
{ code: 'homework-submission:update', resource: 'homework-submission', action: 'update', name: '更新作业提交', description: '允许更新提交的作业' },
|
||||
|
||||
// 作业评审规则
|
||||
{ code: 'homework-review-rule:create', resource: 'homework-review-rule', action: 'create', name: '创建作业评审规则', description: '允许创建作业评审规则' },
|
||||
{ code: 'homework-review-rule:read', resource: 'homework-review-rule', action: 'read', name: '查看作业评审规则', description: '允许查看作业评审规则' },
|
||||
{ code: 'homework-review-rule:update', resource: 'homework-review-rule', action: 'update', name: '更新作业评审规则', description: '允许更新作业评审规则' },
|
||||
{ code: 'homework-review-rule:delete', resource: 'homework-review-rule', action: 'delete', name: '删除作业评审规则', description: '允许删除作业评审规则' },
|
||||
|
||||
// 作业评分
|
||||
{ code: 'homework-score:create', resource: 'homework-score', action: 'create', name: '作业评分', description: '允许对作业评分' },
|
||||
{ code: 'homework-score:read', resource: 'homework-score', action: 'read', name: '查看作业评分', description: '允许查看作业评分' },
|
||||
|
||||
// 字典管理
|
||||
{ code: 'dict:create', resource: 'dict', action: 'create', name: '创建字典', description: '允许创建新字典' },
|
||||
{ code: 'dict:read', resource: 'dict', action: 'read', name: '查看字典', description: '允许查看字典列表和详情' },
|
||||
{ code: 'dict:update', resource: 'dict', action: 'update', name: '更新字典', description: '允许更新字典信息' },
|
||||
{ code: 'dict:delete', resource: 'dict', action: 'delete', name: '删除字典', description: '允许删除字典' },
|
||||
|
||||
// 系统配置
|
||||
{ code: 'config:create', resource: 'config', action: 'create', name: '创建配置', description: '允许创建新配置' },
|
||||
{ code: 'config:read', resource: 'config', action: 'read', name: '查看配置', description: '允许查看配置列表和详情' },
|
||||
{ code: 'config:update', resource: 'config', action: 'update', name: '更新配置', description: '允许更新配置信息' },
|
||||
{ code: 'config:delete', resource: 'config', action: 'delete', name: '删除配置', description: '允许删除配置' },
|
||||
|
||||
// 日志管理
|
||||
{ code: 'log:read', resource: 'log', action: 'read', name: '查看日志', description: '允许查看系统日志' },
|
||||
{ code: 'log:delete', resource: 'log', action: 'delete', name: '删除日志', description: '允许删除系统日志' },
|
||||
|
||||
// 赛事活动(学校端)
|
||||
{ code: 'activity:read', resource: 'activity', action: 'read', name: '查看赛事活动', description: '允许查看已发布的赛事活动' },
|
||||
{ code: 'activity:guidance', resource: 'activity', action: 'guidance', name: '指导学生', description: '允许指导学生参赛' },
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// 角色定义和权限映射
|
||||
// ============================================
|
||||
|
||||
// 超级租户角色
|
||||
const superTenantRoles = [
|
||||
{
|
||||
code: 'super_admin',
|
||||
name: '超级管理员',
|
||||
description: '系统超级管理员,管理赛事和系统配置',
|
||||
permissions: [
|
||||
// 系统管理
|
||||
'user:create', 'user:read', 'user:update', 'user:delete', 'user:password:update',
|
||||
'role:create', 'role:read', 'role:update', 'role:delete', 'role:assign',
|
||||
'permission:create', 'permission:read', 'permission:update', 'permission:delete',
|
||||
'menu:create', 'menu:read', 'menu:update', 'menu:delete',
|
||||
'tenant:create', 'tenant:read', 'tenant:update', 'tenant:delete',
|
||||
'dict:create', 'dict:read', 'dict:update', 'dict:delete',
|
||||
'config:create', 'config:read', 'config:update', 'config:delete',
|
||||
'log:read', 'log:delete',
|
||||
// 赛事管理
|
||||
'contest:create', 'contest:read', 'contest:update', 'contest:delete', 'contest:publish', 'contest:finish',
|
||||
'review-rule:create', 'review-rule:read', 'review-rule:update', 'review-rule:delete',
|
||||
'judge:create', 'judge:read', 'judge:update', 'judge:delete', 'judge:assign',
|
||||
'registration:read', 'registration:approve', 'registration:audit',
|
||||
'work:read', 'work:update',
|
||||
'review:read', 'review:assign',
|
||||
'result:read', 'result:publish', 'result:award',
|
||||
'notice:create', 'notice:read', 'notice:update', 'notice:delete',
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'judge',
|
||||
name: '评委',
|
||||
description: '赛事评委,可以评审作品',
|
||||
permissions: [
|
||||
'activity:read', // 查看赛事活动
|
||||
'work:read', // 查看待评审作品
|
||||
'review:read', // 查看评审任务
|
||||
'review:score', // 评审打分
|
||||
'notice:read', // 查看公告
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// 普通租户(学校)角色
|
||||
const normalTenantRoles = [
|
||||
{
|
||||
code: 'school_admin',
|
||||
name: '学校管理员',
|
||||
description: '学校管理员,管理学校信息、教师、学生等',
|
||||
permissions: [
|
||||
'user:create', 'user:read', 'user:update', 'user:delete',
|
||||
'role:create', 'role:read', 'role:update', 'role:delete', 'role:assign',
|
||||
'permission:read',
|
||||
'menu:read',
|
||||
// 学校管理
|
||||
'school:create', 'school:read', 'school:update', 'school:delete',
|
||||
'department:create', 'department:read', 'department:update', 'department:delete',
|
||||
'grade:create', 'grade:read', 'grade:update', 'grade:delete',
|
||||
'class:create', 'class:read', 'class:update', 'class:delete',
|
||||
'teacher:create', 'teacher:read', 'teacher:update', 'teacher:delete',
|
||||
'student:create', 'student:read', 'student:update', 'student:delete',
|
||||
// 赛事活动
|
||||
'activity:read',
|
||||
'notice:read',
|
||||
// 可以查看报名和作品
|
||||
'registration:read',
|
||||
'work:read',
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'teacher',
|
||||
name: '教师',
|
||||
description: '教师角色,可以报名赛事、指导学生、管理作业',
|
||||
permissions: [
|
||||
// AI 3D建模(工作台入口)
|
||||
'ai-3d:read', 'ai-3d:create', // 使用AI 3D建模实验室
|
||||
// 查看基础信息
|
||||
'grade:read',
|
||||
'class:read',
|
||||
'student:read',
|
||||
// 赛事活动
|
||||
'activity:read', // 查看赛事活动列表
|
||||
'activity:guidance', // 指导学生参赛
|
||||
'notice:read', // 查看赛事公告
|
||||
'registration:create', 'registration:read', 'registration:update', 'registration:delete', // 报名管理
|
||||
'work:create', 'work:read', 'work:update', 'work:submit', // 指导学生上传作品
|
||||
// 作业管理
|
||||
'homework:create', 'homework:read', 'homework:update', 'homework:delete', 'homework:publish',
|
||||
'homework-submission:read',
|
||||
'homework-review-rule:create', 'homework-review-rule:read', 'homework-review-rule:update', 'homework-review-rule:delete',
|
||||
'homework-score:create', 'homework-score:read',
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'student',
|
||||
name: '学生',
|
||||
description: '学生角色,可以查看赛事、上传作品、提交作业',
|
||||
permissions: [
|
||||
// AI 3D建模(工作台入口)
|
||||
'ai-3d:read', 'ai-3d:create', // 使用AI 3D建模实验室
|
||||
// 赛事活动
|
||||
'activity:read', // 查看赛事活动列表
|
||||
'notice:read', // 查看赛事公告
|
||||
'registration:read', // 查看自己的报名记录
|
||||
'work:create', 'work:read', 'work:update', 'work:submit', // 上传/管理自己的作品
|
||||
// 作业
|
||||
'homework:read', // 查看作业
|
||||
'homework-submission:create', 'homework-submission:read', 'homework-submission:update', // 提交作业
|
||||
'homework-score:read', // 查看自己的作业评分
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// 初始化函数
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 为租户创建权限
|
||||
*/
|
||||
async function createPermissions(tenantId: number, permissionCodes: string[]) {
|
||||
const createdPermissions: { [code: string]: number } = {};
|
||||
|
||||
for (const code of permissionCodes) {
|
||||
const permDef = allPermissions.find(p => p.code === code);
|
||||
if (!permDef) {
|
||||
console.log(` ⚠️ 权限定义不存在: ${code}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
let permission = await prisma.permission.findFirst({
|
||||
where: { tenantId, code },
|
||||
});
|
||||
|
||||
if (!permission) {
|
||||
permission = await prisma.permission.create({
|
||||
data: {
|
||||
tenantId,
|
||||
code: permDef.code,
|
||||
resource: permDef.resource,
|
||||
action: permDef.action,
|
||||
name: permDef.name,
|
||||
description: permDef.description,
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
console.log(` ✓ 创建权限: ${code}`);
|
||||
}
|
||||
|
||||
createdPermissions[code] = permission.id;
|
||||
}
|
||||
|
||||
return createdPermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为租户创建角色并分配权限
|
||||
*/
|
||||
async function createRoleWithPermissions(
|
||||
tenantId: number,
|
||||
roleConfig: { code: string; name: string; description: string; permissions: string[] },
|
||||
permissionMap: { [code: string]: number }
|
||||
) {
|
||||
// 创建或获取角色
|
||||
let role = await prisma.role.findFirst({
|
||||
where: { tenantId, code: roleConfig.code },
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
role = await prisma.role.create({
|
||||
data: {
|
||||
tenantId,
|
||||
code: roleConfig.code,
|
||||
name: roleConfig.name,
|
||||
description: roleConfig.description,
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
console.log(` ✓ 创建角色: ${roleConfig.name} (${roleConfig.code})`);
|
||||
} else {
|
||||
// 更新角色信息
|
||||
role = await prisma.role.update({
|
||||
where: { id: role.id },
|
||||
data: {
|
||||
name: roleConfig.name,
|
||||
description: roleConfig.description,
|
||||
},
|
||||
});
|
||||
console.log(` ✓ 更新角色: ${roleConfig.name} (${roleConfig.code})`);
|
||||
}
|
||||
|
||||
// 分配权限
|
||||
const existingRolePermissions = await prisma.rolePermission.findMany({
|
||||
where: { roleId: role.id },
|
||||
select: { permissionId: true },
|
||||
});
|
||||
const existingPermissionIds = new Set(existingRolePermissions.map(rp => rp.permissionId));
|
||||
|
||||
let addedCount = 0;
|
||||
for (const permCode of roleConfig.permissions) {
|
||||
const permissionId = permissionMap[permCode];
|
||||
if (!permissionId) {
|
||||
console.log(` ⚠️ 权限不存在: ${permCode}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!existingPermissionIds.has(permissionId)) {
|
||||
await prisma.rolePermission.create({
|
||||
data: {
|
||||
roleId: role.id,
|
||||
permissionId,
|
||||
},
|
||||
});
|
||||
addedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (addedCount > 0) {
|
||||
console.log(` 添加了 ${addedCount} 个权限`);
|
||||
}
|
||||
|
||||
return role;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化超级租户的角色和权限
|
||||
*/
|
||||
async function initSuperTenantRoles() {
|
||||
console.log('\n🚀 开始初始化超级租户角色和权限...\n');
|
||||
|
||||
// 查找超级租户
|
||||
const superTenant = await prisma.tenant.findFirst({
|
||||
where: { isSuper: 1, validState: 1 },
|
||||
});
|
||||
|
||||
if (!superTenant) {
|
||||
console.error('❌ 超级租户不存在!请先运行 init:super-tenant');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`找到超级租户: ${superTenant.name} (${superTenant.code})\n`);
|
||||
|
||||
// 收集所有需要的权限码
|
||||
const allPermissionCodes = new Set<string>();
|
||||
superTenantRoles.forEach(role => {
|
||||
role.permissions.forEach(code => allPermissionCodes.add(code));
|
||||
});
|
||||
|
||||
// 创建权限
|
||||
console.log('📝 创建权限...');
|
||||
const permissionMap = await createPermissions(superTenant.id, Array.from(allPermissionCodes));
|
||||
console.log(`✅ 共 ${Object.keys(permissionMap).length} 个权限\n`);
|
||||
|
||||
// 创建角色
|
||||
console.log('👥 创建角色...');
|
||||
for (const roleConfig of superTenantRoles) {
|
||||
await createRoleWithPermissions(superTenant.id, roleConfig, permissionMap);
|
||||
}
|
||||
|
||||
console.log('\n✅ 超级租户角色和权限初始化完成!');
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化普通租户的角色和权限
|
||||
*/
|
||||
async function initNormalTenantRoles(tenantCode: string) {
|
||||
console.log(`\n🚀 开始初始化租户 "${tenantCode}" 的角色和权限...\n`);
|
||||
|
||||
// 查找租户
|
||||
const tenant = await prisma.tenant.findFirst({
|
||||
where: { code: tenantCode, validState: 1 },
|
||||
});
|
||||
|
||||
if (!tenant) {
|
||||
console.error(`❌ 租户 "${tenantCode}" 不存在!`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (tenant.isSuper === 1) {
|
||||
console.log('⚠️ 这是超级租户,请使用 --super 选项');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`找到租户: ${tenant.name} (${tenant.code})\n`);
|
||||
|
||||
// 收集所有需要的权限码
|
||||
const allPermissionCodes = new Set<string>();
|
||||
normalTenantRoles.forEach(role => {
|
||||
role.permissions.forEach(code => allPermissionCodes.add(code));
|
||||
});
|
||||
|
||||
// 创建权限
|
||||
console.log('📝 创建权限...');
|
||||
const permissionMap = await createPermissions(tenant.id, Array.from(allPermissionCodes));
|
||||
console.log(`✅ 共 ${Object.keys(permissionMap).length} 个权限\n`);
|
||||
|
||||
// 创建角色
|
||||
console.log('👥 创建角色...');
|
||||
for (const roleConfig of normalTenantRoles) {
|
||||
await createRoleWithPermissions(tenant.id, roleConfig, permissionMap);
|
||||
}
|
||||
|
||||
// 输出角色信息
|
||||
console.log('\n📊 角色权限概览:');
|
||||
for (const roleConfig of normalTenantRoles) {
|
||||
console.log(` ${roleConfig.name} (${roleConfig.code}): ${roleConfig.permissions.length} 个权限`);
|
||||
}
|
||||
|
||||
console.log(`\n✅ 租户 "${tenantCode}" 角色和权限初始化完成!`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化所有普通租户的角色和权限
|
||||
*/
|
||||
async function initAllNormalTenantRoles() {
|
||||
console.log('\n🚀 开始初始化所有普通租户的角色和权限...\n');
|
||||
|
||||
// 查找所有普通租户
|
||||
const normalTenants = await prisma.tenant.findMany({
|
||||
where: { isSuper: { not: 1 }, validState: 1 },
|
||||
});
|
||||
|
||||
if (normalTenants.length === 0) {
|
||||
console.log('⚠️ 没有找到普通租户');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`找到 ${normalTenants.length} 个普通租户\n`);
|
||||
|
||||
for (const tenant of normalTenants) {
|
||||
await initNormalTenantRoles(tenant.code);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log('\n✅ 所有普通租户角色和权限初始化完成!');
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 主函数
|
||||
// ============================================
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const isSuper = args.includes('--super');
|
||||
const isAll = args.includes('--all');
|
||||
const tenantCode = args.find(arg => !arg.startsWith('--'));
|
||||
|
||||
try {
|
||||
if (isSuper) {
|
||||
await initSuperTenantRoles();
|
||||
} else if (isAll) {
|
||||
await initAllNormalTenantRoles();
|
||||
} else if (tenantCode) {
|
||||
await initNormalTenantRoles(tenantCode);
|
||||
} else {
|
||||
console.log('使用方法:');
|
||||
console.log(' 初始化超级租户角色: ts-node scripts/init-roles-permissions.ts --super');
|
||||
console.log(' 初始化指定租户角色: ts-node scripts/init-roles-permissions.ts <租户编码>');
|
||||
console.log(' 初始化所有普通租户: ts-node scripts/init-roles-permissions.ts --all');
|
||||
process.exit(1);
|
||||
}
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => {
|
||||
console.log('\n🎉 脚本执行完成!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('\n💥 脚本执行失败:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
322
backend/scripts/init-super-tenant.ts
Normal file
322
backend/scripts/init-super-tenant.ts
Normal file
@ -0,0 +1,322 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
// 加载环境变量(必须在其他导入之前)
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
|
||||
// 根据 NODE_ENV 加载对应的环境配置文件
|
||||
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||
const envFile = `.env.${nodeEnv}`;
|
||||
// scripts 目录的父目录就是 backend 目录
|
||||
const backendDir = path.resolve(__dirname, '..');
|
||||
const envPath = path.resolve(backendDir, envFile);
|
||||
|
||||
// 尝试加载环境特定的配置文件
|
||||
dotenv.config({ path: envPath });
|
||||
|
||||
// 如果环境特定文件不存在,尝试加载默认的 .env 文件
|
||||
if (!process.env.DATABASE_URL) {
|
||||
dotenv.config({ path: path.resolve(backendDir, '.env') });
|
||||
}
|
||||
|
||||
// 验证必要的环境变量
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error('❌ 错误: 未找到 DATABASE_URL 环境变量');
|
||||
console.error(` 请确保存在以下文件之一:`);
|
||||
console.error(` - ${envPath}`);
|
||||
console.error(` - ${path.resolve(backendDir, '.env')}`);
|
||||
console.error(` 或者设置 NODE_ENV 环境变量(当前: ${nodeEnv})`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('🚀 开始初始化超级租户...\n');
|
||||
|
||||
// 检查是否已存在超级租户
|
||||
let superTenant = await prisma.tenant.findFirst({
|
||||
where: { isSuper: 1 },
|
||||
});
|
||||
|
||||
if (superTenant) {
|
||||
console.log('⚠️ 超级租户已存在,将更新菜单分配');
|
||||
console.log(` 租户编码: ${superTenant.code}\n`);
|
||||
} else {
|
||||
// 创建超级租户
|
||||
superTenant = await prisma.tenant.create({
|
||||
data: {
|
||||
name: '超级租户',
|
||||
code: 'super',
|
||||
domain: 'super',
|
||||
description: '系统超级租户,拥有所有权限',
|
||||
isSuper: 1,
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('✅ 超级租户创建成功!');
|
||||
console.log(` 租户ID: ${superTenant.id}`);
|
||||
console.log(` 租户编码: ${superTenant.code}`);
|
||||
console.log(` 租户名称: ${superTenant.name}\n`);
|
||||
}
|
||||
|
||||
// 创建或获取超级管理员用户
|
||||
console.log('📋 步骤 2: 创建或获取超级管理员用户...\n');
|
||||
let superAdmin = await prisma.user.findFirst({
|
||||
where: {
|
||||
tenantId: superTenant.id,
|
||||
username: 'admin',
|
||||
},
|
||||
});
|
||||
|
||||
if (!superAdmin) {
|
||||
const hashedPassword = await bcrypt.hash('admin@super', 10);
|
||||
superAdmin = await prisma.user.create({
|
||||
data: {
|
||||
tenantId: superTenant.id,
|
||||
username: 'admin',
|
||||
password: hashedPassword,
|
||||
nickname: '超级管理员',
|
||||
email: 'admin@super.com',
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
console.log('✅ 超级管理员用户创建成功!');
|
||||
console.log(` 用户名: ${superAdmin.username}`);
|
||||
console.log(` 密码: admin@super`);
|
||||
console.log(` 用户ID: ${superAdmin.id}\n`);
|
||||
} else {
|
||||
console.log('✅ 超级管理员用户已存在');
|
||||
console.log(` 用户名: ${superAdmin.username}`);
|
||||
console.log(` 用户ID: ${superAdmin.id}\n`);
|
||||
}
|
||||
|
||||
// 创建或获取超级管理员角色
|
||||
console.log('📋 步骤 3: 创建或获取超级管理员角色...\n');
|
||||
let superAdminRole = await prisma.role.findFirst({
|
||||
where: {
|
||||
tenantId: superTenant.id,
|
||||
code: 'super_admin',
|
||||
},
|
||||
});
|
||||
|
||||
if (!superAdminRole) {
|
||||
superAdminRole = await prisma.role.create({
|
||||
data: {
|
||||
tenantId: superTenant.id,
|
||||
name: '超级管理员',
|
||||
code: 'super_admin',
|
||||
description: '超级管理员角色,拥有所有权限',
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
console.log('✅ 超级管理员角色创建成功!');
|
||||
console.log(` 角色编码: ${superAdminRole.code}\n`);
|
||||
} else {
|
||||
console.log('✅ 超级管理员角色已存在');
|
||||
console.log(` 角色编码: ${superAdminRole.code}\n`);
|
||||
}
|
||||
|
||||
// 将超级管理员角色分配给用户
|
||||
const existingUserRole = await prisma.userRole.findUnique({
|
||||
where: {
|
||||
userId_roleId: {
|
||||
userId: superAdmin.id,
|
||||
roleId: superAdminRole.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingUserRole) {
|
||||
await prisma.userRole.create({
|
||||
data: {
|
||||
userId: superAdmin.id,
|
||||
roleId: superAdminRole.id,
|
||||
},
|
||||
});
|
||||
console.log('✅ 超级管理员角色已分配给用户');
|
||||
} else {
|
||||
console.log('✅ 超级管理员角色已分配给用户,跳过');
|
||||
}
|
||||
console.log('💡 提示: 权限初始化请使用 init:admin:permissions 脚本\n');
|
||||
|
||||
// 为超级租户分配所有菜单
|
||||
console.log('📋 步骤 4: 为超级租户分配所有菜单...\n');
|
||||
|
||||
// 获取所有有效菜单
|
||||
const allMenus = await prisma.menu.findMany({
|
||||
where: {
|
||||
validState: 1,
|
||||
},
|
||||
orderBy: [{ sort: 'asc' }, { id: 'asc' }],
|
||||
});
|
||||
|
||||
if (allMenus.length === 0) {
|
||||
console.log('⚠️ 警告: 数据库中没有任何菜单');
|
||||
console.log(' 请先运行 pnpm init:menus 初始化菜单');
|
||||
} else {
|
||||
console.log(` 找到 ${allMenus.length} 个菜单\n`);
|
||||
|
||||
// 获取超级租户已分配的菜单
|
||||
const existingTenantMenus = await prisma.tenantMenu.findMany({
|
||||
where: {
|
||||
tenantId: superTenant.id,
|
||||
},
|
||||
select: {
|
||||
menuId: true,
|
||||
},
|
||||
});
|
||||
|
||||
const existingMenuIds = new Set(existingTenantMenus.map((tm) => tm.menuId));
|
||||
|
||||
// 为超级租户分配所有菜单(包括新增的菜单)
|
||||
let addedCount = 0;
|
||||
const menuNames: string[] = [];
|
||||
|
||||
for (const menu of allMenus) {
|
||||
if (!existingMenuIds.has(menu.id)) {
|
||||
await prisma.tenantMenu.create({
|
||||
data: {
|
||||
tenantId: superTenant.id,
|
||||
menuId: menu.id,
|
||||
},
|
||||
});
|
||||
addedCount++;
|
||||
menuNames.push(menu.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (addedCount > 0) {
|
||||
console.log(`✅ 为超级租户添加了 ${addedCount} 个菜单:`);
|
||||
menuNames.forEach((name) => {
|
||||
console.log(` ✓ ${name}`);
|
||||
});
|
||||
console.log(`\n✅ 超级租户现在拥有 ${allMenus.length} 个菜单\n`);
|
||||
} else {
|
||||
console.log(`✅ 超级租户已拥有所有菜单(${allMenus.length} 个)\n`);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建租户管理菜单(如果不存在)
|
||||
console.log('📋 步骤 5: 创建租户管理菜单(如果不存在)...\n');
|
||||
|
||||
// 查找系统管理菜单(父菜单)
|
||||
const systemMenu = await prisma.menu.findFirst({
|
||||
where: {
|
||||
name: '系统管理',
|
||||
parentId: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (systemMenu) {
|
||||
// 检查租户管理菜单是否已存在
|
||||
const existingTenantMenu = await prisma.menu.findFirst({
|
||||
where: {
|
||||
name: '租户管理',
|
||||
path: '/system/tenants',
|
||||
},
|
||||
});
|
||||
|
||||
let tenantMenu;
|
||||
if (!existingTenantMenu) {
|
||||
tenantMenu = await prisma.menu.create({
|
||||
data: {
|
||||
name: '租户管理',
|
||||
path: '/system/tenants',
|
||||
icon: 'TeamOutlined',
|
||||
component: 'system/tenants/Index',
|
||||
parentId: systemMenu.id,
|
||||
permission: 'tenant:update', // 只有超级租户才有此权限,普通租户只有 tenant:read
|
||||
sort: 7,
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
console.log('✅ 租户管理菜单创建成功');
|
||||
|
||||
// 为超级租户分配租户管理菜单
|
||||
await prisma.tenantMenu.create({
|
||||
data: {
|
||||
tenantId: superTenant.id,
|
||||
menuId: tenantMenu.id,
|
||||
},
|
||||
});
|
||||
console.log('✅ 租户管理菜单已分配给超级租户\n');
|
||||
} else {
|
||||
tenantMenu = existingTenantMenu;
|
||||
console.log('✅ 租户管理菜单已存在');
|
||||
|
||||
// 检查是否已分配
|
||||
const existingTenantMenuRelation = await prisma.tenantMenu.findFirst({
|
||||
where: {
|
||||
tenantId: superTenant.id,
|
||||
menuId: tenantMenu.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingTenantMenuRelation) {
|
||||
await prisma.tenantMenu.create({
|
||||
data: {
|
||||
tenantId: superTenant.id,
|
||||
menuId: tenantMenu.id,
|
||||
},
|
||||
});
|
||||
console.log('✅ 租户管理菜单已分配给超级租户\n');
|
||||
} else {
|
||||
console.log('✅ 租户管理菜单已分配给超级租户,跳过\n');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ 警告:未找到系统管理菜单,无法创建租户管理菜单\n');
|
||||
}
|
||||
|
||||
// 验证菜单分配结果
|
||||
const finalMenus = await prisma.tenantMenu.findMany({
|
||||
where: {
|
||||
tenantId: superTenant.id,
|
||||
},
|
||||
include: {
|
||||
menu: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('📊 初始化结果:');
|
||||
console.log('========================================');
|
||||
console.log('超级租户信息:');
|
||||
console.log(` 租户编码: ${superTenant.code}`);
|
||||
console.log(` 租户名称: ${superTenant.name}`);
|
||||
console.log(` 访问链接: http://your-domain.com/?tenant=${superTenant.code}`);
|
||||
console.log('========================================');
|
||||
console.log('超级管理员登录信息:');
|
||||
console.log(` 用户名: ${superAdmin.username}`);
|
||||
console.log(` 密码: admin@super`);
|
||||
console.log(` 租户编码: ${superTenant.code}`);
|
||||
console.log('========================================');
|
||||
console.log('菜单分配情况:');
|
||||
console.log(` 已分配菜单数: ${finalMenus.length}`);
|
||||
if (finalMenus.length > 0) {
|
||||
const topLevelMenus = finalMenus.filter((tm) => !tm.menu.parentId);
|
||||
console.log(` 顶级菜单数: ${topLevelMenus.length}`);
|
||||
}
|
||||
console.log('========================================');
|
||||
console.log('\n💡 提示:');
|
||||
console.log(' 权限初始化请使用: pnpm init:admin:permissions');
|
||||
console.log(' 菜单初始化请使用: pnpm init:menus');
|
||||
console.log('========================================');
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => {
|
||||
console.log('\n🎉 初始化脚本执行完成!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('\n💥 初始化脚本执行失败:', error);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
1324
backend/scripts/init-tenant-admin.ts
Normal file
1324
backend/scripts/init-tenant-admin.ts
Normal file
File diff suppressed because it is too large
Load Diff
429
backend/scripts/init-tenant-menu-permissions.ts
Normal file
429
backend/scripts/init-tenant-menu-permissions.ts
Normal file
@ -0,0 +1,429 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
// 加载环境变量(必须在其他导入之前)
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// 根据 NODE_ENV 加载对应的环境配置文件
|
||||
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||
const envFile = `.env.${nodeEnv}`;
|
||||
// scripts 目录的父目录就是 backend 目录
|
||||
const backendDir = path.resolve(__dirname, '..');
|
||||
const envPath = path.resolve(backendDir, envFile);
|
||||
|
||||
// 尝试加载环境特定的配置文件
|
||||
dotenv.config({ path: envPath });
|
||||
|
||||
// 如果环境特定文件不存在,尝试加载默认的 .env 文件
|
||||
if (!process.env.DATABASE_URL) {
|
||||
dotenv.config({ path: path.resolve(backendDir, '.env') });
|
||||
}
|
||||
|
||||
// 验证必要的环境变量
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error('❌ 错误: 未找到 DATABASE_URL 环境变量');
|
||||
console.error(` 请确保存在以下文件之一:`);
|
||||
console.error(` - ${envPath}`);
|
||||
console.error(` - ${path.resolve(backendDir, '.env')}`);
|
||||
console.error(` 或者设置 NODE_ENV 环境变量(当前: ${nodeEnv})`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// 从 JSON 文件加载权限数据
|
||||
const permissionsFilePath = path.resolve(backendDir, 'data', 'permissions.json');
|
||||
|
||||
if (!fs.existsSync(permissionsFilePath)) {
|
||||
console.error(`❌ 错误: 权限数据文件不存在: ${permissionsFilePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const permissions = JSON.parse(fs.readFileSync(permissionsFilePath, 'utf-8'));
|
||||
|
||||
async function initTenantMenuAndPermissions(tenantCode: string) {
|
||||
try {
|
||||
console.log(`🚀 开始为租户 "${tenantCode}" 初始化菜单和权限...\n`);
|
||||
|
||||
// 1. 查找或创建租户
|
||||
console.log(`📋 步骤 1: 查找或创建租户 "${tenantCode}"...`);
|
||||
let tenant = await prisma.tenant.findUnique({
|
||||
where: { code: tenantCode },
|
||||
});
|
||||
|
||||
if (!tenant) {
|
||||
// 创建租户
|
||||
tenant = await prisma.tenant.create({
|
||||
data: {
|
||||
name: `${tenantCode} 租户`,
|
||||
code: tenantCode,
|
||||
domain: tenantCode,
|
||||
description: `租户 ${tenantCode}`,
|
||||
isSuper: 0,
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
console.log(`✅ 租户创建成功: ${tenant.name} (${tenant.code})\n`);
|
||||
} else {
|
||||
if (tenant.validState !== 1) {
|
||||
console.error(`❌ 错误: 租户 "${tenantCode}" 状态无效!`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`✅ 找到租户: ${tenant.name} (${tenant.code})\n`);
|
||||
}
|
||||
|
||||
// 2. 查找或创建 admin 用户
|
||||
console.log(`👤 步骤 2: 查找或创建 admin 用户...`);
|
||||
let adminUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
tenantId: tenant.id,
|
||||
username: 'admin',
|
||||
},
|
||||
});
|
||||
|
||||
const password = `admin@${tenantCode}`;
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
if (!adminUser) {
|
||||
adminUser = await prisma.user.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
username: 'admin',
|
||||
password: hashedPassword,
|
||||
nickname: '管理员',
|
||||
email: `admin@${tenantCode}.com`,
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
console.log(`✅ admin 用户创建成功: ${adminUser.username}\n`);
|
||||
} else {
|
||||
// 更新密码(确保密码是最新的)
|
||||
adminUser = await prisma.user.update({
|
||||
where: { id: adminUser.id },
|
||||
data: {
|
||||
password: hashedPassword,
|
||||
nickname: '管理员',
|
||||
email: `admin@${tenantCode}.com`,
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
console.log(`✅ admin 用户已存在: ${adminUser.username}\n`);
|
||||
}
|
||||
|
||||
// 3. 查找或创建 admin 角色
|
||||
console.log(`👤 步骤 3: 查找或创建 admin 角色...`);
|
||||
let adminRole = await prisma.role.findFirst({
|
||||
where: {
|
||||
tenantId: tenant.id,
|
||||
code: 'admin',
|
||||
},
|
||||
});
|
||||
|
||||
if (!adminRole) {
|
||||
adminRole = await prisma.role.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
name: '管理员',
|
||||
code: 'admin',
|
||||
description: '租户管理员角色,拥有租户的所有权限',
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
console.log(`✅ admin 角色创建成功: ${adminRole.name} (${adminRole.code})\n`);
|
||||
} else {
|
||||
adminRole = await prisma.role.update({
|
||||
where: { id: adminRole.id },
|
||||
data: {
|
||||
name: '管理员',
|
||||
description: '租户管理员角色,拥有租户的所有权限',
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
console.log(`✅ admin 角色已存在: ${adminRole.name} (${adminRole.code})\n`);
|
||||
}
|
||||
|
||||
// 4. 为 admin 用户分配 admin 角色
|
||||
console.log(`🔗 步骤 4: 为 admin 用户分配 admin 角色...`);
|
||||
const existingUserRole = await prisma.userRole.findUnique({
|
||||
where: {
|
||||
userId_roleId: {
|
||||
userId: adminUser.id,
|
||||
roleId: adminRole.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingUserRole) {
|
||||
await prisma.userRole.create({
|
||||
data: {
|
||||
userId: adminUser.id,
|
||||
roleId: adminRole.id,
|
||||
},
|
||||
});
|
||||
console.log(`✅ 角色分配成功\n`);
|
||||
} else {
|
||||
console.log(`✅ 用户已拥有 admin 角色\n`);
|
||||
}
|
||||
|
||||
// 5. 初始化租户权限(如果不存在则创建)
|
||||
console.log(`📝 步骤 5: 初始化租户权限...`);
|
||||
const createdPermissions = [];
|
||||
|
||||
for (const perm of permissions) {
|
||||
// 检查权限是否已存在
|
||||
const existingPermission = await prisma.permission.findFirst({
|
||||
where: {
|
||||
tenantId: tenant.id,
|
||||
code: perm.code,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingPermission) {
|
||||
// 创建权限
|
||||
const permission = await prisma.permission.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
code: perm.code,
|
||||
resource: perm.resource,
|
||||
action: perm.action,
|
||||
name: perm.name,
|
||||
description: perm.description,
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
createdPermissions.push(permission);
|
||||
console.log(` ✓ 创建权限: ${perm.code} - ${perm.name}`);
|
||||
} else {
|
||||
// 更新现有权限(确保信息是最新的)
|
||||
const permission = await prisma.permission.update({
|
||||
where: { id: existingPermission.id },
|
||||
data: {
|
||||
name: perm.name,
|
||||
resource: perm.resource,
|
||||
action: perm.action,
|
||||
description: perm.description,
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
createdPermissions.push(permission);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ 共确保 ${createdPermissions.length} 个权限存在\n`);
|
||||
|
||||
// 获取租户的所有有效权限
|
||||
const tenantPermissions = await prisma.permission.findMany({
|
||||
where: {
|
||||
tenantId: tenant.id,
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
|
||||
// 6. 为 admin 角色分配所有权限
|
||||
console.log(`🔗 步骤 6: 为 admin 角色分配所有权限...`);
|
||||
const existingRolePermissions = await prisma.rolePermission.findMany({
|
||||
where: { roleId: adminRole.id },
|
||||
select: { permissionId: true },
|
||||
});
|
||||
const existingPermissionIds = new Set(
|
||||
existingRolePermissions.map((rp) => rp.permissionId),
|
||||
);
|
||||
|
||||
let addedPermissionCount = 0;
|
||||
for (const permission of tenantPermissions) {
|
||||
if (!existingPermissionIds.has(permission.id)) {
|
||||
await prisma.rolePermission.create({
|
||||
data: {
|
||||
roleId: adminRole.id,
|
||||
permissionId: permission.id,
|
||||
},
|
||||
});
|
||||
addedPermissionCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (addedPermissionCount > 0) {
|
||||
console.log(`✅ 为 admin 角色添加了 ${addedPermissionCount} 个权限`);
|
||||
console.log(`✅ admin 角色现在拥有 ${tenantPermissions.length} 个权限\n`);
|
||||
} else {
|
||||
console.log(
|
||||
`✅ admin 角色已拥有所有权限(${tenantPermissions.length} 个)\n`,
|
||||
);
|
||||
}
|
||||
|
||||
// 7. 为租户分配所有菜单
|
||||
console.log(`📋 步骤 7: 为租户分配所有菜单...`);
|
||||
|
||||
// 获取所有有效菜单
|
||||
const allMenus = await prisma.menu.findMany({
|
||||
where: {
|
||||
validState: 1,
|
||||
},
|
||||
orderBy: [{ sort: 'asc' }, { id: 'asc' }],
|
||||
});
|
||||
|
||||
if (allMenus.length === 0) {
|
||||
console.log('⚠️ 警告: 数据库中没有任何菜单');
|
||||
console.log(' 请先运行 pnpm init:menus 初始化菜单\n');
|
||||
} else {
|
||||
console.log(` 找到 ${allMenus.length} 个菜单\n`);
|
||||
|
||||
// 获取租户已分配的菜单
|
||||
const existingTenantMenus = await prisma.tenantMenu.findMany({
|
||||
where: {
|
||||
tenantId: tenant.id,
|
||||
},
|
||||
select: {
|
||||
menuId: true,
|
||||
},
|
||||
});
|
||||
|
||||
const existingMenuIds = new Set(existingTenantMenus.map((tm) => tm.menuId));
|
||||
|
||||
// 为租户分配所有菜单
|
||||
let addedMenuCount = 0;
|
||||
const menuNames: string[] = [];
|
||||
|
||||
for (const menu of allMenus) {
|
||||
if (!existingMenuIds.has(menu.id)) {
|
||||
await prisma.tenantMenu.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
menuId: menu.id,
|
||||
},
|
||||
});
|
||||
addedMenuCount++;
|
||||
menuNames.push(menu.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (addedMenuCount > 0) {
|
||||
console.log(`✅ 为租户添加了 ${addedMenuCount} 个菜单:`);
|
||||
menuNames.forEach((name) => {
|
||||
console.log(` ✓ ${name}`);
|
||||
});
|
||||
console.log(`\n✅ 租户现在拥有 ${allMenus.length} 个菜单\n`);
|
||||
} else {
|
||||
console.log(`✅ 租户已拥有所有菜单(${allMenus.length} 个)\n`);
|
||||
}
|
||||
}
|
||||
|
||||
// 8. 验证结果
|
||||
console.log('🔍 步骤 8: 验证结果...');
|
||||
const userWithRoles = await prisma.user.findUnique({
|
||||
where: { id: adminUser.id },
|
||||
include: {
|
||||
roles: {
|
||||
include: {
|
||||
role: {
|
||||
include: {
|
||||
permissions: {
|
||||
include: {
|
||||
permission: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const roleCodes = userWithRoles?.roles.map((ur) => ur.role.code) || [];
|
||||
const permissionCodes = new Set<string>();
|
||||
userWithRoles?.roles.forEach((ur) => {
|
||||
ur.role.permissions.forEach((rp) => {
|
||||
permissionCodes.add(rp.permission.code);
|
||||
});
|
||||
});
|
||||
|
||||
const finalMenus = await prisma.tenantMenu.findMany({
|
||||
where: {
|
||||
tenantId: tenant.id,
|
||||
},
|
||||
include: {
|
||||
menu: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`\n📊 初始化结果:`);
|
||||
console.log('========================================');
|
||||
console.log('租户信息:');
|
||||
console.log(` 租户名称: ${tenant.name}`);
|
||||
console.log(` 租户编码: ${tenant.code}`);
|
||||
console.log(` 访问链接: http://your-domain.com/?tenant=${tenant.code}`);
|
||||
console.log('========================================');
|
||||
console.log('管理员登录信息:');
|
||||
console.log(` 用户名: ${adminUser.username}`);
|
||||
console.log(` 密码: ${password}`);
|
||||
console.log(` 昵称: ${adminUser.nickname}`);
|
||||
console.log(` 邮箱: ${adminUser.email}`);
|
||||
console.log('========================================');
|
||||
console.log('角色和权限:');
|
||||
console.log(` 角色: ${roleCodes.join(', ')}`);
|
||||
console.log(` 权限数量: ${permissionCodes.size}`);
|
||||
if (permissionCodes.size > 0 && permissionCodes.size <= 20) {
|
||||
console.log(` 权限列表:`);
|
||||
Array.from(permissionCodes)
|
||||
.sort()
|
||||
.forEach((code) => {
|
||||
console.log(` - ${code}`);
|
||||
});
|
||||
} else if (permissionCodes.size > 20) {
|
||||
console.log(` 权限列表(前20个):`);
|
||||
Array.from(permissionCodes)
|
||||
.sort()
|
||||
.slice(0, 20)
|
||||
.forEach((code) => {
|
||||
console.log(` - ${code}`);
|
||||
});
|
||||
console.log(` ... 还有 ${permissionCodes.size - 20} 个权限`);
|
||||
}
|
||||
console.log('========================================');
|
||||
console.log('菜单分配:');
|
||||
console.log(` 已分配菜单数: ${finalMenus.length}`);
|
||||
if (finalMenus.length > 0) {
|
||||
const topLevelMenus = finalMenus.filter((tm) => !tm.menu.parentId);
|
||||
console.log(` 顶级菜单数: ${topLevelMenus.length}`);
|
||||
}
|
||||
console.log('========================================');
|
||||
console.log(`\n✅ 租户菜单和权限初始化完成!`);
|
||||
console.log(`\n💡 现在可以使用以下凭据登录:`);
|
||||
console.log(` 租户编码: ${tenant.code}`);
|
||||
console.log(` 用户名: ${adminUser.username}`);
|
||||
console.log(` 密码: ${password}`);
|
||||
} catch (error) {
|
||||
console.error('❌ 初始化失败:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// 获取命令行参数
|
||||
const tenantCode = process.argv[2];
|
||||
|
||||
if (!tenantCode) {
|
||||
console.error('❌ 错误: 请提供租户编码作为参数');
|
||||
console.error(' 使用方法:');
|
||||
console.error(' pnpm init:tenant-menu-permissions <租户编码>');
|
||||
console.error(' 示例:');
|
||||
console.error(' pnpm init:tenant-menu-permissions tenant1');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 执行初始化
|
||||
initTenantMenuAndPermissions(tenantCode)
|
||||
.then(() => {
|
||||
console.log('\n🎉 初始化脚本执行完成!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('\n💥 初始化脚本执行失败:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
429
backend/scripts/init-tenant.ts
Normal file
429
backend/scripts/init-tenant.ts
Normal file
@ -0,0 +1,429 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
// 初始化普通租户脚本(包含角色)
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
|
||||
// 根据 NODE_ENV 加载对应的环境配置文件
|
||||
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||
const envFile = `.env.${nodeEnv}`;
|
||||
const backendDir = path.resolve(__dirname, '..');
|
||||
const envPath = path.resolve(backendDir, envFile);
|
||||
|
||||
dotenv.config({ path: envPath });
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
dotenv.config({ path: path.resolve(backendDir, '.env') });
|
||||
}
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error('❌ 错误: 未找到 DATABASE_URL 环境变量');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import * as readline from 'readline';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// ============================================
|
||||
// 权限定义
|
||||
// ============================================
|
||||
const allPermissions = [
|
||||
// 工作台
|
||||
{ code: 'workbench:read', resource: 'workbench', action: 'read', name: '查看工作台', description: '允许查看工作台' },
|
||||
|
||||
// 用户管理
|
||||
{ code: 'user:create', resource: 'user', action: 'create', name: '创建用户', description: '允许创建新用户' },
|
||||
{ code: 'user:read', resource: 'user', action: 'read', name: '查看用户', description: '允许查看用户列表和详情' },
|
||||
{ code: 'user:update', resource: 'user', action: 'update', name: '更新用户', description: '允许更新用户信息' },
|
||||
{ code: 'user:delete', resource: 'user', action: 'delete', name: '删除用户', description: '允许删除用户' },
|
||||
|
||||
// 角色管理
|
||||
{ code: 'role:create', resource: 'role', action: 'create', name: '创建角色', description: '允许创建新角色' },
|
||||
{ code: 'role:read', resource: 'role', action: 'read', name: '查看角色', description: '允许查看角色列表和详情' },
|
||||
{ code: 'role:update', resource: 'role', action: 'update', name: '更新角色', description: '允许更新角色信息' },
|
||||
{ code: 'role:delete', resource: 'role', action: 'delete', name: '删除角色', description: '允许删除角色' },
|
||||
{ code: 'role:assign', resource: 'role', action: 'assign', name: '分配角色', description: '允许给用户分配角色' },
|
||||
|
||||
// 权限管理
|
||||
{ code: 'permission:read', resource: 'permission', action: 'read', name: '查看权限', description: '允许查看权限列表' },
|
||||
|
||||
// 菜单管理
|
||||
{ code: 'menu:read', resource: 'menu', action: 'read', name: '查看菜单', description: '允许查看菜单列表' },
|
||||
|
||||
// 学校管理
|
||||
{ code: 'school:create', resource: 'school', action: 'create', name: '创建学校', description: '允许创建学校信息' },
|
||||
{ code: 'school:read', resource: 'school', action: 'read', name: '查看学校', description: '允许查看学校信息' },
|
||||
{ code: 'school:update', resource: 'school', action: 'update', name: '更新学校', description: '允许更新学校信息' },
|
||||
{ code: 'school:delete', resource: 'school', action: 'delete', name: '删除学校', description: '允许删除学校信息' },
|
||||
|
||||
// 部门管理
|
||||
{ code: 'department:create', resource: 'department', action: 'create', name: '创建部门', description: '允许创建部门' },
|
||||
{ code: 'department:read', resource: 'department', action: 'read', name: '查看部门', description: '允许查看部门列表' },
|
||||
{ code: 'department:update', resource: 'department', action: 'update', name: '更新部门', description: '允许更新部门信息' },
|
||||
{ code: 'department:delete', resource: 'department', action: 'delete', name: '删除部门', description: '允许删除部门' },
|
||||
|
||||
// 年级管理
|
||||
{ code: 'grade:create', resource: 'grade', action: 'create', name: '创建年级', description: '允许创建年级' },
|
||||
{ code: 'grade:read', resource: 'grade', action: 'read', name: '查看年级', description: '允许查看年级列表' },
|
||||
{ code: 'grade:update', resource: 'grade', action: 'update', name: '更新年级', description: '允许更新年级信息' },
|
||||
{ code: 'grade:delete', resource: 'grade', action: 'delete', name: '删除年级', description: '允许删除年级' },
|
||||
|
||||
// 班级管理
|
||||
{ code: 'class:create', resource: 'class', action: 'create', name: '创建班级', description: '允许创建班级' },
|
||||
{ code: 'class:read', resource: 'class', action: 'read', name: '查看班级', description: '允许查看班级列表' },
|
||||
{ code: 'class:update', resource: 'class', action: 'update', name: '更新班级', description: '允许更新班级信息' },
|
||||
{ code: 'class:delete', resource: 'class', action: 'delete', name: '删除班级', description: '允许删除班级' },
|
||||
|
||||
// 教师管理
|
||||
{ code: 'teacher:create', resource: 'teacher', action: 'create', name: '创建教师', description: '允许创建教师' },
|
||||
{ code: 'teacher:read', resource: 'teacher', action: 'read', name: '查看教师', description: '允许查看教师列表' },
|
||||
{ code: 'teacher:update', resource: 'teacher', action: 'update', name: '更新教师', description: '允许更新教师信息' },
|
||||
{ code: 'teacher:delete', resource: 'teacher', action: 'delete', name: '删除教师', description: '允许删除教师' },
|
||||
|
||||
// 学生管理
|
||||
{ code: 'student:create', resource: 'student', action: 'create', name: '创建学生', description: '允许创建学生' },
|
||||
{ code: 'student:read', resource: 'student', action: 'read', name: '查看学生', description: '允许查看学生列表' },
|
||||
{ code: 'student:update', resource: 'student', action: 'update', name: '更新学生', description: '允许更新学生信息' },
|
||||
{ code: 'student:delete', resource: 'student', action: 'delete', name: '删除学生', description: '允许删除学生' },
|
||||
|
||||
// 赛事活动(参与者权限)
|
||||
{ code: 'activity:read', resource: 'activity', action: 'read', name: '查看赛事活动', description: '允许查看已发布的赛事活动' },
|
||||
{ code: 'activity:guidance', resource: 'activity', action: 'guidance', name: '指导学生', description: '允许指导学生参赛' },
|
||||
|
||||
// 赛事报名
|
||||
{ code: 'registration:create', resource: 'registration', action: 'create', name: '创建报名', description: '允许报名赛事' },
|
||||
{ code: 'registration:read', resource: 'registration', action: 'read', name: '查看报名', description: '允许查看报名记录' },
|
||||
{ code: 'registration:update', resource: 'registration', action: 'update', name: '更新报名', description: '允许更新报名信息' },
|
||||
{ code: 'registration:delete', resource: 'registration', action: 'delete', name: '取消报名', description: '允许取消报名' },
|
||||
|
||||
// 参赛作品
|
||||
{ code: 'work:create', resource: 'work', action: 'create', name: '上传作品', description: '允许上传参赛作品' },
|
||||
{ code: 'work:read', resource: 'work', action: 'read', name: '查看作品', description: '允许查看参赛作品' },
|
||||
{ code: 'work:update', resource: 'work', action: 'update', name: '更新作品', description: '允许更新作品信息' },
|
||||
{ code: 'work:delete', resource: 'work', action: 'delete', name: '删除作品', description: '允许删除作品' },
|
||||
{ code: 'work:submit', resource: 'work', action: 'submit', name: '提交作品', description: '允许提交作品' },
|
||||
|
||||
// 赛事公告
|
||||
{ code: 'notice:read', resource: 'notice', action: 'read', name: '查看公告', description: '允许查看赛事公告' },
|
||||
|
||||
// 作业管理
|
||||
{ code: 'homework:create', resource: 'homework', action: 'create', name: '创建作业', description: '允许创建作业' },
|
||||
{ code: 'homework:read', resource: 'homework', action: 'read', name: '查看作业', description: '允许查看作业列表' },
|
||||
{ code: 'homework:update', resource: 'homework', action: 'update', name: '更新作业', description: '允许更新作业信息' },
|
||||
{ code: 'homework:delete', resource: 'homework', action: 'delete', name: '删除作业', description: '允许删除作业' },
|
||||
{ code: 'homework:publish', resource: 'homework', action: 'publish', name: '发布作业', description: '允许发布作业' },
|
||||
|
||||
// 作业提交
|
||||
{ code: 'homework-submission:create', resource: 'homework-submission', action: 'create', name: '提交作业', description: '允许提交作业' },
|
||||
{ code: 'homework-submission:read', resource: 'homework-submission', action: 'read', name: '查看作业提交', description: '允许查看作业提交记录' },
|
||||
{ code: 'homework-submission:update', resource: 'homework-submission', action: 'update', name: '更新作业提交', description: '允许更新提交的作业' },
|
||||
|
||||
// 作业评审规则
|
||||
{ code: 'homework-review-rule:create', resource: 'homework-review-rule', action: 'create', name: '创建作业评审规则', description: '允许创建作业评审规则' },
|
||||
{ code: 'homework-review-rule:read', resource: 'homework-review-rule', action: 'read', name: '查看作业评审规则', description: '允许查看作业评审规则' },
|
||||
{ code: 'homework-review-rule:update', resource: 'homework-review-rule', action: 'update', name: '更新作业评审规则', description: '允许更新作业评审规则' },
|
||||
{ code: 'homework-review-rule:delete', resource: 'homework-review-rule', action: 'delete', name: '删除作业评审规则', description: '允许删除作业评审规则' },
|
||||
|
||||
// 作业评分
|
||||
{ code: 'homework-score:create', resource: 'homework-score', action: 'create', name: '作业评分', description: '允许对作业评分' },
|
||||
{ code: 'homework-score:read', resource: 'homework-score', action: 'read', name: '查看作业评分', description: '允许查看作业评分' },
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// 角色定义
|
||||
// ============================================
|
||||
const normalTenantRoles = [
|
||||
{
|
||||
code: 'school_admin',
|
||||
name: '学校管理员',
|
||||
description: '学校管理员,管理学校信息、教师、学生等',
|
||||
permissions: [
|
||||
'workbench:read',
|
||||
'user:create', 'user:read', 'user:update', 'user:delete',
|
||||
'role:create', 'role:read', 'role:update', 'role:delete', 'role:assign',
|
||||
'permission:read',
|
||||
'menu:read',
|
||||
// 学校管理
|
||||
'school:create', 'school:read', 'school:update', 'school:delete',
|
||||
'department:create', 'department:read', 'department:update', 'department:delete',
|
||||
'grade:create', 'grade:read', 'grade:update', 'grade:delete',
|
||||
'class:create', 'class:read', 'class:update', 'class:delete',
|
||||
'teacher:create', 'teacher:read', 'teacher:update', 'teacher:delete',
|
||||
'student:create', 'student:read', 'student:update', 'student:delete',
|
||||
// 赛事活动
|
||||
'activity:read',
|
||||
'notice:read',
|
||||
// 可以查看报名和作品
|
||||
'registration:read',
|
||||
'work:read',
|
||||
// 作业管理
|
||||
'homework:create', 'homework:read', 'homework:update', 'homework:delete', 'homework:publish',
|
||||
'homework-submission:read',
|
||||
'homework-review-rule:create', 'homework-review-rule:read', 'homework-review-rule:update', 'homework-review-rule:delete',
|
||||
'homework-score:read',
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'teacher',
|
||||
name: '教师',
|
||||
description: '教师角色,可以报名赛事、指导学生、管理作业',
|
||||
permissions: [
|
||||
'workbench:read',
|
||||
// 查看基础信息
|
||||
'grade:read',
|
||||
'class:read',
|
||||
'student:read',
|
||||
// 赛事活动
|
||||
'activity:read',
|
||||
'activity:guidance',
|
||||
'notice:read',
|
||||
'registration:create', 'registration:read', 'registration:update', 'registration:delete',
|
||||
'work:create', 'work:read', 'work:update', 'work:submit',
|
||||
// 作业管理
|
||||
'homework:create', 'homework:read', 'homework:update', 'homework:delete', 'homework:publish',
|
||||
'homework-submission:read',
|
||||
'homework-review-rule:create', 'homework-review-rule:read', 'homework-review-rule:update', 'homework-review-rule:delete',
|
||||
'homework-score:create', 'homework-score:read',
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'student',
|
||||
name: '学生',
|
||||
description: '学生角色,可以查看赛事、上传作品、提交作业',
|
||||
permissions: [
|
||||
'workbench:read',
|
||||
// 赛事活动
|
||||
'activity:read',
|
||||
'notice:read',
|
||||
'registration:read',
|
||||
'work:create', 'work:read', 'work:update', 'work:submit',
|
||||
// 作业
|
||||
'homework:read',
|
||||
'homework-submission:create', 'homework-submission:read', 'homework-submission:update',
|
||||
'homework-score:read',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// 创建 readline 接口用于用户输入
|
||||
function createReadlineInterface(): readline.Interface {
|
||||
return readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
}
|
||||
|
||||
// 提示用户输入
|
||||
function prompt(rl: readline.Interface, question: string): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
resolve(answer.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function initTenant() {
|
||||
const rl = createReadlineInterface();
|
||||
|
||||
try {
|
||||
console.log('🚀 开始创建普通租户...\n');
|
||||
|
||||
// 获取租户信息
|
||||
const tenantName = await prompt(rl, '请输入租户名称: ');
|
||||
if (!tenantName) {
|
||||
console.error('❌ 错误: 租户名称不能为空');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const tenantCode = await prompt(rl, '请输入租户编码(英文): ');
|
||||
if (!tenantCode) {
|
||||
console.error('❌ 错误: 租户编码不能为空');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 检查租户编码是否已存在
|
||||
const existingTenant = await prisma.tenant.findFirst({
|
||||
where: { code: tenantCode }
|
||||
});
|
||||
|
||||
if (existingTenant) {
|
||||
console.error(`❌ 错误: 租户编码 "${tenantCode}" 已存在`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
rl.close();
|
||||
|
||||
// 1. 创建租户
|
||||
console.log('\n🏢 步骤 1: 创建租户...');
|
||||
const tenant = await prisma.tenant.create({
|
||||
data: {
|
||||
name: tenantName,
|
||||
code: tenantCode,
|
||||
isSuper: 0,
|
||||
validState: 1,
|
||||
}
|
||||
});
|
||||
console.log(` ✓ 创建租户: ${tenant.name} (${tenant.code})`);
|
||||
|
||||
const tenantId = tenant.id;
|
||||
|
||||
// 2. 创建权限
|
||||
console.log('\n📝 步骤 2: 创建基础权限...');
|
||||
const createdPermissions: { [code: string]: number } = {};
|
||||
|
||||
for (const perm of allPermissions) {
|
||||
const permission = await prisma.permission.create({
|
||||
data: { ...perm, tenantId, validState: 1 }
|
||||
});
|
||||
createdPermissions[perm.code] = permission.id;
|
||||
}
|
||||
console.log(` ✓ 共创建 ${Object.keys(createdPermissions).length} 个权限`);
|
||||
|
||||
// 3. 创建角色并分配权限
|
||||
console.log('\n👥 步骤 3: 创建角色并分配权限...');
|
||||
const createdRoles: any[] = [];
|
||||
|
||||
for (const roleConfig of normalTenantRoles) {
|
||||
// 创建角色
|
||||
const role = await prisma.role.create({
|
||||
data: {
|
||||
tenantId,
|
||||
name: roleConfig.name,
|
||||
code: roleConfig.code,
|
||||
description: roleConfig.description,
|
||||
validState: 1,
|
||||
}
|
||||
});
|
||||
|
||||
// 分配权限给角色
|
||||
let permCount = 0;
|
||||
for (const permCode of roleConfig.permissions) {
|
||||
const permissionId = createdPermissions[permCode];
|
||||
if (permissionId) {
|
||||
await prisma.rolePermission.create({
|
||||
data: {
|
||||
roleId: role.id,
|
||||
permissionId,
|
||||
}
|
||||
});
|
||||
permCount++;
|
||||
}
|
||||
}
|
||||
|
||||
createdRoles.push({ ...role, permCount });
|
||||
console.log(` ✓ 创建角色: ${role.name} (${role.code}) - ${permCount} 个权限`);
|
||||
}
|
||||
|
||||
// 4. 创建 admin 用户
|
||||
console.log('\n👤 步骤 4: 创建 admin 用户...');
|
||||
const password = `admin@${tenant.code}`;
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
const adminUser = await prisma.user.create({
|
||||
data: {
|
||||
tenantId,
|
||||
username: 'admin',
|
||||
password: hashedPassword,
|
||||
nickname: '管理员',
|
||||
validState: 1,
|
||||
}
|
||||
});
|
||||
console.log(` ✓ 创建用户: ${adminUser.username}`);
|
||||
|
||||
// 5. 给用户分配 school_admin 角色
|
||||
console.log('\n🔗 步骤 5: 分配角色给用户...');
|
||||
const schoolAdminRole = createdRoles.find(r => r.code === 'school_admin');
|
||||
if (schoolAdminRole) {
|
||||
await prisma.userRole.create({
|
||||
data: {
|
||||
userId: adminUser.id,
|
||||
roleId: schoolAdminRole.id,
|
||||
}
|
||||
});
|
||||
console.log(` ✓ 分配角色: ${schoolAdminRole.name}`);
|
||||
}
|
||||
|
||||
// 6. 分配菜单给租户
|
||||
console.log('\n📋 步骤 6: 分配菜单给租户...');
|
||||
|
||||
// 普通租户可见的菜单
|
||||
const NORMAL_TENANT_MENUS = ['工作台', '学校管理', '赛事活动', '作业管理', '系统管理'];
|
||||
const NORMAL_TENANT_EXCLUDED_SYSTEM_MENUS = ['租户管理', '数据字典', '系统配置', '日志记录', '菜单管理', '权限管理'];
|
||||
const NORMAL_TENANT_EXCLUDED_ACTIVITY_MENUS = ['我的报名', '我的作品'];
|
||||
|
||||
const allMenus = await prisma.menu.findMany({
|
||||
orderBy: [{ sort: 'asc' }, { id: 'asc' }],
|
||||
});
|
||||
|
||||
const normalTenantMenuIds = new Set<number>();
|
||||
for (const menu of allMenus) {
|
||||
// 顶级菜单
|
||||
if (!menu.parentId && NORMAL_TENANT_MENUS.includes(menu.name)) {
|
||||
normalTenantMenuIds.add(menu.id);
|
||||
}
|
||||
// 子菜单
|
||||
if (menu.parentId) {
|
||||
const parentMenu = allMenus.find(m => m.id === menu.parentId);
|
||||
if (parentMenu && NORMAL_TENANT_MENUS.includes(parentMenu.name)) {
|
||||
// 系统管理下排除部分子菜单
|
||||
if (parentMenu.name === '系统管理' && NORMAL_TENANT_EXCLUDED_SYSTEM_MENUS.includes(menu.name)) {
|
||||
continue;
|
||||
}
|
||||
// 赛事活动下排除部分子菜单(只保留活动列表)
|
||||
if (parentMenu.name === '赛事活动' && NORMAL_TENANT_EXCLUDED_ACTIVITY_MENUS.includes(menu.name)) {
|
||||
continue;
|
||||
}
|
||||
normalTenantMenuIds.add(menu.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let menuCount = 0;
|
||||
for (const menuId of normalTenantMenuIds) {
|
||||
await prisma.tenantMenu.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
menuId: menuId,
|
||||
}
|
||||
});
|
||||
menuCount++;
|
||||
}
|
||||
console.log(` ✓ 分配 ${menuCount} 个菜单`);
|
||||
|
||||
// 7. 输出结果
|
||||
console.log('\n' + '='.repeat(50));
|
||||
console.log('🎉 普通租户创建完成!');
|
||||
console.log('='.repeat(50));
|
||||
console.log(` 租户名称: ${tenant.name}`);
|
||||
console.log(` 租户编码: ${tenant.code}`);
|
||||
console.log(` 用户名: admin`);
|
||||
console.log(` 密码: ${password}`);
|
||||
console.log(` 权限数量: ${Object.keys(createdPermissions).length}`);
|
||||
console.log(` 菜单数量: ${menuCount}`);
|
||||
console.log('\n 📊 角色列表:');
|
||||
for (const role of createdRoles) {
|
||||
console.log(` - ${role.name} (${role.code}): ${role.permCount} 个权限`);
|
||||
}
|
||||
console.log('='.repeat(50));
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 创建失败:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// 执行初始化
|
||||
initTenant()
|
||||
.then(() => {
|
||||
console.log('\n✅ 租户创建脚本执行完成!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('\n💥 租户创建脚本执行失败:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
58
backend/sql/add_tenant_menu.sql
Normal file
58
backend/sql/add_tenant_menu.sql
Normal file
@ -0,0 +1,58 @@
|
||||
-- 为超级租户添加租户管理菜单
|
||||
-- 注意:需要先查询系统管理菜单的ID,然后替换下面的 parent_id
|
||||
|
||||
-- 查询系统管理菜单的ID
|
||||
-- SELECT id FROM menus WHERE name = '系统管理' AND parent_id IS NULL;
|
||||
|
||||
-- 假设系统管理菜单的ID为某个值(需要根据实际情况调整)
|
||||
-- 这里使用子查询来动态获取系统管理菜单的ID
|
||||
|
||||
INSERT INTO menus (
|
||||
name,
|
||||
path,
|
||||
icon,
|
||||
component,
|
||||
parent_id,
|
||||
permission,
|
||||
sort,
|
||||
valid_state,
|
||||
create_time,
|
||||
modify_time
|
||||
)
|
||||
SELECT
|
||||
'租户管理',
|
||||
'/system/tenants',
|
||||
'TeamOutlined',
|
||||
'system/tenants/Index',
|
||||
id, -- 系统管理菜单的ID
|
||||
'tenant:read',
|
||||
7, -- 排序,放在其他系统管理菜单之后
|
||||
1,
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM menus
|
||||
WHERE name = '系统管理' AND parent_id IS NULL
|
||||
LIMIT 1;
|
||||
|
||||
-- 如果系统管理菜单不存在,可以手动指定ID:
|
||||
-- INSERT INTO menus (name, path, icon, component, parent_id, permission, sort, valid_state, create_time, modify_time)
|
||||
-- VALUES ('租户管理', '/system/tenants', 'TeamOutlined', 'system/tenants/Index', 2, 'tenant:read', 7, 1, NOW(), NOW());
|
||||
|
||||
-- 为超级租户分配租户管理菜单
|
||||
-- 假设超级租户的ID为1(需要根据实际情况调整)
|
||||
-- 假设租户管理菜单的ID为刚插入的菜单ID
|
||||
|
||||
INSERT INTO tenant_menus (tenant_id, menu_id)
|
||||
SELECT
|
||||
t.id AS tenant_id,
|
||||
m.id AS menu_id
|
||||
FROM tenants t
|
||||
CROSS JOIN menus m
|
||||
WHERE t.code = 'super' AND t.is_super = 1
|
||||
AND m.name = '租户管理' AND m.path = '/system/tenants'
|
||||
LIMIT 1;
|
||||
|
||||
-- 如果上面的查询没有结果,可以手动指定ID:
|
||||
-- INSERT INTO tenant_menus (tenant_id, menu_id)
|
||||
-- VALUES (1, (SELECT id FROM menus WHERE name = '租户管理' AND path = '/system/tenants' LIMIT 1));
|
||||
|
||||
276
backend/sql/competition.sql
Normal file
276
backend/sql/competition.sql
Normal file
@ -0,0 +1,276 @@
|
||||
-- ============================================
|
||||
-- 赛事管理模块数据库表结构
|
||||
-- ============================================
|
||||
|
||||
-- 1. 赛事表
|
||||
CREATE TABLE `t_contest` (
|
||||
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键id',
|
||||
`contest_name` varchar(127) NOT NULL COMMENT '赛事名称',
|
||||
`contest_type` varchar(31) NOT NULL COMMENT '赛事类型,字典:contest_type:individual/team',
|
||||
`contest_state` varchar(31) NOT NULL DEFAULT 'unpublished' COMMENT '赛事状态(未发布:unpublished 已发布:published)',
|
||||
`start_time` datetime NOT NULL COMMENT '赛事开始时间',
|
||||
`end_time` datetime NOT NULL COMMENT '赛事结束时间',
|
||||
`address` varchar(512) DEFAULT NULL COMMENT '线下地址',
|
||||
`content` text COMMENT '赛事详情',
|
||||
`contest_tenants` json DEFAULT NULL COMMENT '赛事参赛范围(授权租户ID数组)',
|
||||
`cover_url` varchar(255) DEFAULT NULL COMMENT '封面url',
|
||||
`poster_url` varchar(255) DEFAULT NULL COMMENT '海报url',
|
||||
`contact_name` varchar(63) DEFAULT NULL COMMENT '联系人',
|
||||
`contact_phone` varchar(63) DEFAULT NULL COMMENT '联系电话',
|
||||
`contact_qrcode` varchar(255) DEFAULT NULL COMMENT '联系人二维码',
|
||||
`organizers` json DEFAULT NULL COMMENT '主办单位数组',
|
||||
`co_organizers` json DEFAULT NULL COMMENT '协办单位数组',
|
||||
`sponsors` json DEFAULT NULL COMMENT '赞助单位数组',
|
||||
`register_start_time` datetime NOT NULL COMMENT '报名开始时间',
|
||||
`register_end_time` datetime NOT NULL COMMENT '报名结束时间',
|
||||
`register_state` varchar(31) DEFAULT NULL COMMENT '报名任务状态,映射写死:启动(started),已关闭(closed)',
|
||||
`submit_rule` varchar(31) NOT NULL DEFAULT 'once' COMMENT '提交规则:once/resubmit',
|
||||
`submit_start_time` datetime NOT NULL COMMENT '作品提交开始时间',
|
||||
`submit_end_time` datetime NOT NULL COMMENT '作品提交结束时间',
|
||||
`review_rule_id` int DEFAULT NULL COMMENT '评审规则id',
|
||||
`review_start_time` datetime NOT NULL COMMENT '评审开始时间',
|
||||
`review_end_time` datetime NOT NULL COMMENT '评审结束时间',
|
||||
`result_publish_time` datetime DEFAULT NULL COMMENT '结果发布时间',
|
||||
`creator` int DEFAULT NULL COMMENT '创建人ID',
|
||||
`modifier` int DEFAULT NULL COMMENT '修改人ID',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`modify_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`valid_state` int NOT NULL DEFAULT 1 COMMENT '有效状态(1-有效,2-失效)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_contest_name` (`contest_name`),
|
||||
KEY `idx_contest_state` (`contest_state`),
|
||||
KEY `idx_contest_time` (`start_time`, `end_time`),
|
||||
KEY `idx_review_rule` (`review_rule_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='赛事表';
|
||||
|
||||
-- 2. 赛事附件表
|
||||
CREATE TABLE `t_contest_attachment` (
|
||||
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键id',
|
||||
`contest_id` int NOT NULL COMMENT '赛事id',
|
||||
`file_name` varchar(100) NOT NULL COMMENT '文件名',
|
||||
`file_url` varchar(255) NOT NULL COMMENT '文件路径',
|
||||
`format` varchar(255) DEFAULT NULL COMMENT '文件类型(png,mp4)',
|
||||
`file_type` varchar(255) DEFAULT NULL COMMENT '素材类型(image,video)',
|
||||
`size` varchar(255) DEFAULT '0' COMMENT '文件大小',
|
||||
`creator` int DEFAULT NULL COMMENT '创建人ID',
|
||||
`modifier` int DEFAULT NULL COMMENT '修改人ID',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`modify_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`valid_state` int NOT NULL DEFAULT 1 COMMENT '有效状态(1-有效,2-失效)',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_contest` (`contest_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='赛事附件';
|
||||
|
||||
-- 3. 评审规则表
|
||||
CREATE TABLE `t_contest_review_rule` (
|
||||
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键id',
|
||||
`contest_id` int NOT NULL COMMENT '赛事id',
|
||||
`rule_name` varchar(127) NOT NULL COMMENT '规则名称',
|
||||
`dimensions` json NOT NULL COMMENT '评分维度配置JSON',
|
||||
`calculation_rule` varchar(31) DEFAULT 'average' COMMENT '计算规则:average/max/min/weighted',
|
||||
`creator` int DEFAULT NULL COMMENT '创建人ID',
|
||||
`modifier` int DEFAULT NULL COMMENT '修改人ID',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`modify_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`valid_state` int NOT NULL DEFAULT 1 COMMENT '有效状态(1-有效,2-失效)',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_contest` (`contest_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='评审规则表';
|
||||
|
||||
-- 4. 赛事团队表
|
||||
CREATE TABLE `t_contest_team` (
|
||||
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键id',
|
||||
`tenant_id` int NOT NULL COMMENT '团队所属租户ID',
|
||||
`contest_id` int NOT NULL COMMENT '赛事id',
|
||||
`team_name` varchar(127) NOT NULL COMMENT '团队名称(租户内唯一)',
|
||||
`leader_user_id` int NOT NULL COMMENT '团队负责人用户id',
|
||||
`max_members` int DEFAULT NULL COMMENT '团队最大成员数',
|
||||
`creator` int DEFAULT NULL COMMENT '创建人ID',
|
||||
`modifier` int DEFAULT NULL COMMENT '修改人ID',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`modify_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`valid_state` int NOT NULL DEFAULT 1 COMMENT '有效状态(1-有效,2-失效)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_team_name` (`tenant_id`,`contest_id`,`team_name`),
|
||||
KEY `idx_contest` (`contest_id`),
|
||||
KEY `idx_leader` (`leader_user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='赛事团队';
|
||||
|
||||
-- 5. 团队成员表
|
||||
CREATE TABLE `t_contest_team_member` (
|
||||
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键id',
|
||||
`tenant_id` int NOT NULL COMMENT '成员所属租户ID',
|
||||
`team_id` int NOT NULL COMMENT '团队id',
|
||||
`user_id` int NOT NULL COMMENT '成员用户id',
|
||||
`role` varchar(31) NOT NULL DEFAULT 'member' COMMENT '成员角色:member/leader/mentor',
|
||||
`creator` int DEFAULT NULL COMMENT '创建人ID',
|
||||
`modifier` int DEFAULT NULL COMMENT '修改人ID',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`modify_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_member_once` (`tenant_id`,`team_id`,`user_id`),
|
||||
KEY `idx_team` (`team_id`),
|
||||
KEY `idx_user` (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='团队成员';
|
||||
|
||||
-- 6. 赛事报名表
|
||||
CREATE TABLE `t_contest_registration` (
|
||||
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键id',
|
||||
`contest_id` int NOT NULL COMMENT '赛事id',
|
||||
`tenant_id` int NOT NULL COMMENT '所属租户ID(学校/机构)',
|
||||
`registration_type` varchar(20) DEFAULT NULL COMMENT '报名类型:individual(个人)/team(团队)',
|
||||
`team_id` int DEFAULT NULL COMMENT '团队id',
|
||||
`team_name` varchar(255) DEFAULT NULL COMMENT '团队名称快照(团队赛)',
|
||||
`user_id` int NOT NULL COMMENT '账号id',
|
||||
`account_no` varchar(64) NOT NULL COMMENT '报名账号(记录报名快照)',
|
||||
`account_name` varchar(100) NOT NULL COMMENT '报名账号名称(记录报名快照)',
|
||||
`role` varchar(63) DEFAULT NULL COMMENT '报名角色快照:leader(队长)/member(队员)/mentor(指导教师)',
|
||||
`registration_state` varchar(31) NOT NULL DEFAULT 'pending' COMMENT '报名状态:pending(待审核)、passed(已通过)、rejected(已拒绝)、withdrawn(已撤回)',
|
||||
`registrant` int DEFAULT NULL COMMENT '实际报名人用户ID(老师报名填老师用户ID)',
|
||||
`registration_time` datetime NOT NULL COMMENT '报名时间',
|
||||
`reason` varchar(1023) DEFAULT NULL COMMENT '审核理由',
|
||||
`operator` int DEFAULT NULL COMMENT '审核人用户ID',
|
||||
`operation_date` datetime DEFAULT NULL COMMENT '审核时间',
|
||||
`creator` int DEFAULT NULL COMMENT '创建人ID',
|
||||
`modifier` int DEFAULT NULL COMMENT '修改人ID',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`modify_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_contest_tenant` (`contest_id`, `tenant_id`),
|
||||
KEY `idx_user_contest` (`user_id`, `contest_id`),
|
||||
KEY `idx_team` (`team_id`),
|
||||
KEY `idx_registration_state` (`registration_state`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='赛事报名人员记录表';
|
||||
|
||||
-- 7. 参赛作品表
|
||||
CREATE TABLE `t_contest_work` (
|
||||
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键id',
|
||||
`tenant_id` int NOT NULL COMMENT '作品所属租户ID',
|
||||
`contest_id` int NOT NULL COMMENT '赛事id',
|
||||
`registration_id` int NOT NULL COMMENT '报名记录id(关联t_contest_registration.id)',
|
||||
`work_no` varchar(63) DEFAULT NULL COMMENT '作品编号(展示用唯一编号)',
|
||||
`title` varchar(255) NOT NULL COMMENT '作品标题',
|
||||
`description` text DEFAULT NULL COMMENT '作品说明',
|
||||
`files` json DEFAULT NULL COMMENT '作品文件列表(简易场景)',
|
||||
`version` int NOT NULL DEFAULT 1 COMMENT '作品版本号(递增)',
|
||||
`is_latest` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否最新版本:1是/0否',
|
||||
`status` varchar(31) NOT NULL DEFAULT 'submitted' COMMENT '作品状态:submitted/locked/reviewing/rejected/accepted',
|
||||
`submit_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '提交时间',
|
||||
`submitter_user_id` int DEFAULT NULL COMMENT '提交人用户id',
|
||||
`submitter_account_no` varchar(127) DEFAULT NULL COMMENT '提交人账号(手机号/学号)',
|
||||
`submit_source` varchar(31) NOT NULL DEFAULT 'teacher' COMMENT '提交来源:teacher/student/team_leader',
|
||||
`preview_url` varchar(255) DEFAULT NULL COMMENT '作品预览URL(3D/视频)',
|
||||
`ai_model_meta` json DEFAULT NULL COMMENT 'AI建模元数据(模型类型、版本、参数)',
|
||||
`creator` int DEFAULT NULL COMMENT '创建人ID',
|
||||
`modifier` int DEFAULT NULL COMMENT '修改人ID',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`modify_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`valid_state` int NOT NULL DEFAULT 1 COMMENT '有效状态(1-有效,2-失效)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_work_no` (`work_no`),
|
||||
KEY `idx_work_contest_latest` (`tenant_id`,`contest_id`,`is_latest`),
|
||||
KEY `idx_work_registration` (`registration_id`),
|
||||
KEY `idx_submit_filter` (`tenant_id`,`contest_id`,`submit_time`,`status`),
|
||||
KEY `idx_contest_status` (`contest_id`, `status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='参赛作品';
|
||||
|
||||
-- 8. 作品附件文件表
|
||||
CREATE TABLE `t_contest_work_attachment` (
|
||||
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键id',
|
||||
`tenant_id` int NOT NULL COMMENT '所属租户ID',
|
||||
`contest_id` int NOT NULL COMMENT '赛事id',
|
||||
`work_id` int NOT NULL COMMENT '作品id',
|
||||
`file_name` varchar(255) NOT NULL COMMENT '文件名',
|
||||
`file_url` varchar(255) NOT NULL COMMENT '文件路径',
|
||||
`format` varchar(255) DEFAULT NULL COMMENT '文件类型(png,mp4)',
|
||||
`file_type` varchar(255) DEFAULT NULL COMMENT '素材类型(image,video)',
|
||||
`size` varchar(255) DEFAULT '0' COMMENT '文件大小',
|
||||
`creator` int DEFAULT NULL COMMENT '创建人ID',
|
||||
`modifier` int DEFAULT NULL COMMENT '修改人ID',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`modify_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_work_file` (`tenant_id`,`contest_id`,`work_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='作品附件文件表';
|
||||
|
||||
-- 9. 比赛评委关联表(比赛与评委的多对多关系)
|
||||
CREATE TABLE `t_contest_judge` (
|
||||
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键id',
|
||||
`contest_id` int NOT NULL COMMENT '比赛id',
|
||||
`judge_id` int NOT NULL COMMENT '评委用户id',
|
||||
`specialty` varchar(255) DEFAULT NULL COMMENT '评审专业领域(可选)',
|
||||
`weight` decimal(3,2) DEFAULT NULL COMMENT '评审权重(可选,用于加权平均计算)',
|
||||
`description` text DEFAULT NULL COMMENT '评委在该比赛中的说明',
|
||||
`creator` int DEFAULT NULL COMMENT '创建人ID',
|
||||
`modifier` int DEFAULT NULL COMMENT '修改人ID',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`modify_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`valid_state` int NOT NULL DEFAULT 1 COMMENT '有效状态(1-有效,2-失效)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_contest_judge` (`contest_id`, `judge_id`),
|
||||
KEY `idx_contest` (`contest_id`),
|
||||
KEY `idx_judge` (`judge_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='比赛评委关联表';
|
||||
|
||||
-- 10. 作品分配表(评委分配作品)
|
||||
CREATE TABLE `t_contest_work_judge_assignment` (
|
||||
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键id',
|
||||
`contest_id` int NOT NULL COMMENT '赛事id',
|
||||
`work_id` int NOT NULL COMMENT '作品id',
|
||||
`judge_id` int NOT NULL COMMENT '评委用户id',
|
||||
`assignment_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '分配时间',
|
||||
`status` varchar(31) NOT NULL DEFAULT 'assigned' COMMENT '分配状态:assigned/reviewing/completed',
|
||||
`creator` int DEFAULT NULL COMMENT '创建人ID',
|
||||
`modifier` int DEFAULT NULL COMMENT '修改人ID',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`modify_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_work_judge` (`work_id`, `judge_id`),
|
||||
KEY `idx_contest_judge` (`contest_id`, `judge_id`),
|
||||
KEY `idx_work` (`work_id`),
|
||||
KEY `idx_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='作品分配表';
|
||||
|
||||
-- 11. 作品评分表
|
||||
CREATE TABLE `t_contest_work_score` (
|
||||
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键id',
|
||||
`tenant_id` int NOT NULL COMMENT '所属租户ID',
|
||||
`contest_id` int NOT NULL COMMENT '赛事id',
|
||||
`work_id` int NOT NULL COMMENT '作品id',
|
||||
`assignment_id` int NOT NULL COMMENT '分配记录id(关联t_contest_work_judge_assignment)',
|
||||
`judge_id` int NOT NULL COMMENT '评委用户id',
|
||||
`judge_name` varchar(127) NOT NULL COMMENT '评委姓名',
|
||||
`dimension_scores` json NOT NULL COMMENT '各维度评分JSON,格式:{"dimension1": 85, "dimension2": 90, ...}',
|
||||
`total_score` decimal(10,2) NOT NULL COMMENT '总分(根据评审规则计算)',
|
||||
`comments` text DEFAULT NULL COMMENT '评语',
|
||||
`score_time` datetime NOT NULL COMMENT '评分时间',
|
||||
`creator` int DEFAULT NULL COMMENT '创建人ID',
|
||||
`modifier` int DEFAULT NULL COMMENT '修改人ID',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`modify_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`valid_state` int NOT NULL DEFAULT 1 COMMENT '有效状态(1-有效,2-失效)',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_contest_work_judge` (`contest_id`, `work_id`, `judge_id`),
|
||||
KEY `idx_work` (`work_id`),
|
||||
KEY `idx_assignment` (`assignment_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='作品评分表';
|
||||
|
||||
-- 12. 赛事公告表
|
||||
CREATE TABLE `t_contest_notice` (
|
||||
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键id',
|
||||
`contest_id` int NOT NULL COMMENT '赛事id',
|
||||
`title` varchar(255) NOT NULL COMMENT '公告标题',
|
||||
`content` text NOT NULL COMMENT '公告内容',
|
||||
`notice_type` varchar(31) NOT NULL DEFAULT 'manual' COMMENT '公告类型:system/manual/urgent',
|
||||
`priority` int DEFAULT 0 COMMENT '优先级(数字越大优先级越高)',
|
||||
`publish_time` datetime DEFAULT NULL COMMENT '发布时间',
|
||||
`creator` int DEFAULT NULL COMMENT '创建人ID',
|
||||
`modifier` int DEFAULT NULL COMMENT '修改人ID',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`modify_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`valid_state` int NOT NULL DEFAULT 1 COMMENT '有效状态(1-有效,2-失效)',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_contest` (`contest_id`),
|
||||
KEY `idx_publish_time` (`publish_time`),
|
||||
KEY `idx_notice_type` (`notice_type`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='赛事公告表';
|
||||
375
backend/src/ai-3d/ai-3d.controller.ts
Normal file
375
backend/src/ai-3d/ai-3d.controller.ts
Normal file
@ -0,0 +1,375 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
Request,
|
||||
ParseIntPipe,
|
||||
Res,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import axios from 'axios';
|
||||
import AdmZip from 'adm-zip';
|
||||
import { AI3DService } from './ai-3d.service';
|
||||
import { CreateTaskDto } from './dto/create-task.dto';
|
||||
import { QueryTaskDto } from './dto/query-task.dto';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { CurrentTenantId } from '../auth/decorators/current-tenant-id.decorator';
|
||||
import { Public } from '../auth/decorators/public.decorator';
|
||||
|
||||
@Controller('ai-3d')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class AI3DController {
|
||||
private readonly logger = new Logger(AI3DController.name);
|
||||
|
||||
constructor(private readonly ai3dService: AI3DService) {}
|
||||
|
||||
/**
|
||||
* 创建生成任务
|
||||
* POST /api/ai-3d/generate
|
||||
*/
|
||||
@Post('generate')
|
||||
createTask(
|
||||
@Body() createTaskDto: CreateTaskDto,
|
||||
@CurrentTenantId() tenantId: number,
|
||||
@Request() req,
|
||||
) {
|
||||
const userId = req?.user?.userId;
|
||||
return this.ai3dService.createTask(userId, tenantId, createTaskDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务列表
|
||||
* GET /api/ai-3d/tasks
|
||||
*/
|
||||
@Get('tasks')
|
||||
getTasks(@Query() queryDto: QueryTaskDto, @Request() req) {
|
||||
const userId = req?.user?.userId;
|
||||
return this.ai3dService.getTasks(userId, queryDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务详情
|
||||
* GET /api/ai-3d/tasks/:id
|
||||
*/
|
||||
@Get('tasks/:id')
|
||||
getTask(@Param('id', ParseIntPipe) id: number, @Request() req) {
|
||||
const userId = req?.user?.userId;
|
||||
return this.ai3dService.getTask(userId, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试任务
|
||||
* POST /api/ai-3d/tasks/:id/retry
|
||||
*/
|
||||
@Post('tasks/:id/retry')
|
||||
retryTask(@Param('id', ParseIntPipe) id: number, @Request() req) {
|
||||
const userId = req?.user?.userId;
|
||||
return this.ai3dService.retryTask(userId, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除任务
|
||||
* DELETE /api/ai-3d/tasks/:id
|
||||
*/
|
||||
@Delete('tasks/:id')
|
||||
deleteTask(@Param('id', ParseIntPipe) id: number, @Request() req) {
|
||||
const userId = req?.user?.userId;
|
||||
return this.ai3dService.deleteTask(userId, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 代理模型文件(解决CORS问题)
|
||||
* GET /api/ai-3d/proxy-model
|
||||
*/
|
||||
@Get('proxy-model')
|
||||
@Public() // 允许公开访问,因为模型URL已经包含签名
|
||||
async proxyModel(@Query('url') url: string, @Res() res: Response) {
|
||||
if (!url) {
|
||||
throw new HttpException('URL参数不能为空', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
this.logger.log(`[proxy-model] 开始处理请求,URL长度: ${url.length}`);
|
||||
|
||||
try {
|
||||
// URL解码(处理URL编码)
|
||||
let decodedUrl: string;
|
||||
try {
|
||||
decodedUrl = decodeURIComponent(url);
|
||||
this.logger.log(`[proxy-model] URL解码成功`);
|
||||
} catch (e) {
|
||||
// 如果解码失败,使用原始URL
|
||||
decodedUrl = url;
|
||||
this.logger.warn(`[proxy-model] URL解码失败,使用原始URL`);
|
||||
}
|
||||
|
||||
// 验证URL是否为腾讯云COS链接(安全验证)
|
||||
if (
|
||||
!decodedUrl.includes('tencentcos.cn') &&
|
||||
!decodedUrl.includes('qcloud.com')
|
||||
) {
|
||||
throw new HttpException(
|
||||
'不支持的URL来源,仅支持腾讯云COS链接',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
// 验证URL格式
|
||||
try {
|
||||
new URL(decodedUrl);
|
||||
} catch (e) {
|
||||
throw new HttpException('URL格式无效', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
this.logger.log(`[proxy-model] 开始下载文件...`);
|
||||
|
||||
// 从源URL获取文件
|
||||
const response = await axios.get(decodedUrl, {
|
||||
responseType: 'arraybuffer', // 使用arraybuffer以便处理ZIP
|
||||
timeout: 120000, // 120秒超时(ZIP文件可能较大)
|
||||
maxContentLength: 100 * 1024 * 1024, // 最大100MB
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
Accept: '*/*',
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`[proxy-model] 文件下载成功,大小: ${response.data.byteLength} bytes`,
|
||||
);
|
||||
|
||||
let fileData: Buffer = Buffer.from(response.data);
|
||||
let contentType =
|
||||
response.headers['content-type'] || 'application/octet-stream';
|
||||
let contentLength = fileData.length;
|
||||
|
||||
this.logger.log(`[proxy-model] Content-Type: ${contentType}`);
|
||||
|
||||
// 如果是ZIP文件,解压并提取3D模型文件
|
||||
if (
|
||||
decodedUrl.toLowerCase().includes('.zip') ||
|
||||
contentType.includes('zip') ||
|
||||
contentType.includes('application/zip')
|
||||
) {
|
||||
this.logger.log(`[proxy-model] 检测到ZIP文件,开始解压...`);
|
||||
try {
|
||||
const zip = new AdmZip(fileData);
|
||||
const zipEntries = zip.getEntries();
|
||||
|
||||
this.logger.log(
|
||||
`[proxy-model] ZIP解压成功,包含 ${zipEntries.length} 个文件`,
|
||||
);
|
||||
|
||||
// 列出所有文件便于调试
|
||||
const allFiles = zipEntries.map((e) => e.entryName);
|
||||
this.logger.log(`[proxy-model] ZIP文件列表: ${allFiles.join(', ')}`);
|
||||
|
||||
// 按优先级查找3D模型文件: GLB > GLTF > OBJ
|
||||
let modelEntry = zipEntries.find((entry) =>
|
||||
entry.entryName.toLowerCase().endsWith('.glb'),
|
||||
);
|
||||
|
||||
if (!modelEntry) {
|
||||
modelEntry = zipEntries.find((entry) =>
|
||||
entry.entryName.toLowerCase().endsWith('.gltf'),
|
||||
);
|
||||
}
|
||||
|
||||
if (!modelEntry) {
|
||||
modelEntry = zipEntries.find((entry) =>
|
||||
entry.entryName.toLowerCase().endsWith('.obj'),
|
||||
);
|
||||
}
|
||||
|
||||
if (modelEntry) {
|
||||
this.logger.log(
|
||||
`[proxy-model] 找到模型文件: ${modelEntry.entryName}`,
|
||||
);
|
||||
fileData = modelEntry.getData();
|
||||
const entryName = modelEntry.entryName.toLowerCase();
|
||||
let modelType = 'glb'; // 默认类型
|
||||
if (entryName.endsWith('.glb')) {
|
||||
contentType = 'model/gltf-binary';
|
||||
modelType = 'glb';
|
||||
} else if (entryName.endsWith('.gltf')) {
|
||||
contentType = 'model/gltf+json';
|
||||
modelType = 'gltf';
|
||||
} else if (entryName.endsWith('.obj')) {
|
||||
contentType = 'text/plain'; // OBJ 是文本格式
|
||||
modelType = 'obj';
|
||||
}
|
||||
contentLength = fileData.length;
|
||||
this.logger.log(
|
||||
`[proxy-model] 模型类型: ${modelType}, 大小: ${contentLength} bytes`,
|
||||
);
|
||||
// 添加自定义头部,告知前端实际的模型类型
|
||||
res.setHeader('X-Model-Type', modelType);
|
||||
} else {
|
||||
// 列出ZIP中的所有文件,便于调试
|
||||
const fileList = zipEntries.map((e) => e.entryName).join(', ');
|
||||
this.logger.error(
|
||||
`[proxy-model] ZIP中未找到3D模型文件。ZIP内容: ${fileList}`,
|
||||
);
|
||||
throw new HttpException(
|
||||
`ZIP文件中未找到3D模型文件(GLB/GLTF/OBJ)。ZIP内容: ${fileList}`,
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
} catch (zipError: any) {
|
||||
this.logger.error(
|
||||
`[proxy-model] ZIP处理失败: ${zipError.message}`,
|
||||
zipError.stack,
|
||||
);
|
||||
if (zipError instanceof HttpException) {
|
||||
throw zipError;
|
||||
}
|
||||
throw new HttpException(
|
||||
`ZIP解压失败: ${zipError.message}`,
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
res.setHeader('Content-Type', contentType);
|
||||
res.setHeader('Content-Length', contentLength);
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
res.setHeader('Cache-Control', 'public, max-age=3600'); // 缓存1小时
|
||||
|
||||
// 发送文件数据
|
||||
this.logger.log(`[proxy-model] 发送响应,大小: ${contentLength} bytes`);
|
||||
res.send(fileData);
|
||||
} catch (error: any) {
|
||||
this.logger.error(
|
||||
`[proxy-model] 请求处理失败: ${error.message}`,
|
||||
error.stack,
|
||||
);
|
||||
|
||||
if (error instanceof HttpException) {
|
||||
throw error;
|
||||
}
|
||||
if (error.response) {
|
||||
res.status(error.response.status).json({
|
||||
message: `代理请求失败: ${error.response.statusText}`,
|
||||
status: error.response.status,
|
||||
});
|
||||
} else if (error.code === 'ECONNABORTED') {
|
||||
res.status(HttpStatus.REQUEST_TIMEOUT).json({
|
||||
message: '请求超时,请稍后重试',
|
||||
});
|
||||
} else if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
|
||||
res.status(HttpStatus.BAD_GATEWAY).json({
|
||||
message: `无法连接到目标服务器: ${error.message}`,
|
||||
});
|
||||
} else {
|
||||
res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
|
||||
message: `代理请求失败: ${error.message}`,
|
||||
error:
|
||||
process.env.NODE_ENV === 'development' ? error.stack : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 代理预览图(解决CORS问题)
|
||||
* GET /api/ai-3d/proxy-preview
|
||||
*/
|
||||
@Get('proxy-preview')
|
||||
@Public() // 允许公开访问,因为预览图URL已经包含签名
|
||||
async proxyPreview(@Query('url') url: string, @Res() res: Response) {
|
||||
if (!url) {
|
||||
throw new HttpException('URL参数不能为空', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
// URL解码(处理URL编码)
|
||||
let decodedUrl: string;
|
||||
try {
|
||||
decodedUrl = decodeURIComponent(url);
|
||||
} catch (e) {
|
||||
// 如果解码失败,使用原始URL
|
||||
decodedUrl = url;
|
||||
}
|
||||
|
||||
// 验证URL是否为腾讯云COS链接(安全验证)
|
||||
if (
|
||||
!decodedUrl.includes('tencentcos.cn') &&
|
||||
!decodedUrl.includes('qcloud.com')
|
||||
) {
|
||||
throw new HttpException(
|
||||
'不支持的URL来源,仅支持腾讯云COS链接',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
// 验证URL格式
|
||||
try {
|
||||
new URL(decodedUrl);
|
||||
} catch (e) {
|
||||
throw new HttpException('URL格式无效', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// 从源URL获取图片
|
||||
const response = await axios.get(decodedUrl, {
|
||||
responseType: 'arraybuffer',
|
||||
timeout: 30000, // 30秒超时
|
||||
maxContentLength: 10 * 1024 * 1024, // 最大10MB
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
Accept: 'image/*',
|
||||
},
|
||||
});
|
||||
|
||||
const imageData = Buffer.from(response.data);
|
||||
const contentType = response.headers['content-type'] || 'image/jpeg';
|
||||
const contentLength = imageData.length;
|
||||
|
||||
// 设置响应头
|
||||
res.setHeader('Content-Type', contentType);
|
||||
res.setHeader('Content-Length', contentLength);
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
res.setHeader('Cache-Control', 'public, max-age=86400'); // 缓存24小时
|
||||
|
||||
// 发送图片数据
|
||||
res.send(imageData);
|
||||
} catch (error: any) {
|
||||
if (error instanceof HttpException) {
|
||||
throw error;
|
||||
}
|
||||
if (error.response) {
|
||||
res.status(error.response.status).json({
|
||||
message: `代理请求失败: ${error.response.statusText}`,
|
||||
status: error.response.status,
|
||||
});
|
||||
} else if (error.code === 'ECONNABORTED') {
|
||||
res.status(HttpStatus.REQUEST_TIMEOUT).json({
|
||||
message: '请求超时,请稍后重试',
|
||||
});
|
||||
} else if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
|
||||
res.status(HttpStatus.BAD_GATEWAY).json({
|
||||
message: `无法连接到目标服务器: ${error.message}`,
|
||||
});
|
||||
} else {
|
||||
res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
|
||||
message: `代理请求失败: ${error.message}`,
|
||||
error:
|
||||
process.env.NODE_ENV === 'development' ? error.stack : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
39
backend/src/ai-3d/ai-3d.module.ts
Normal file
39
backend/src/ai-3d/ai-3d.module.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { AI3DController } from './ai-3d.controller';
|
||||
import { AI3DService } from './ai-3d.service';
|
||||
import { MockAI3DProvider } from './providers/mock.provider';
|
||||
import { HunyuanAI3DProvider } from './providers/hunyuan.provider';
|
||||
import { AI3D_PROVIDER } from './providers/ai-3d-provider.interface';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, ConfigModule],
|
||||
controllers: [AI3DController],
|
||||
providers: [
|
||||
AI3DService,
|
||||
MockAI3DProvider,
|
||||
HunyuanAI3DProvider,
|
||||
{
|
||||
provide: AI3D_PROVIDER,
|
||||
useFactory: (
|
||||
configService: ConfigService,
|
||||
mockProvider: MockAI3DProvider,
|
||||
hunyuanProvider: HunyuanAI3DProvider,
|
||||
) => {
|
||||
const provider = configService.get<string>('AI_3D_PROVIDER') || 'mock';
|
||||
|
||||
switch (provider.toLowerCase()) {
|
||||
case 'hunyuan':
|
||||
return hunyuanProvider;
|
||||
case 'mock':
|
||||
default:
|
||||
return mockProvider;
|
||||
}
|
||||
},
|
||||
inject: [ConfigService, MockAI3DProvider, HunyuanAI3DProvider],
|
||||
},
|
||||
],
|
||||
exports: [AI3DService],
|
||||
})
|
||||
export class AI3DModule {}
|
||||
303
backend/src/ai-3d/ai-3d.service.ts
Normal file
303
backend/src/ai-3d/ai-3d.service.ts
Normal file
@ -0,0 +1,303 @@
|
||||
import {
|
||||
Injectable,
|
||||
Inject,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateTaskDto } from './dto/create-task.dto';
|
||||
import { QueryTaskDto } from './dto/query-task.dto';
|
||||
import { AI3DProvider, AI3D_PROVIDER } from './providers/ai-3d-provider.interface';
|
||||
|
||||
// 配置常量
|
||||
const MAX_CONCURRENT_TASKS = 3; // 每用户最大并行任务数
|
||||
const TASK_TIMEOUT_MS = 10 * 60 * 1000; // 10分钟超时
|
||||
const MAX_RETRY_COUNT = 3; // 最大重试次数
|
||||
|
||||
@Injectable()
|
||||
export class AI3DService {
|
||||
private readonly logger = new Logger(AI3DService.name);
|
||||
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
@Inject(AI3D_PROVIDER) private ai3dProvider: AI3DProvider,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 创建生成任务
|
||||
*/
|
||||
async createTask(
|
||||
userId: number,
|
||||
tenantId: number,
|
||||
dto: CreateTaskDto,
|
||||
) {
|
||||
// 1. 检查用户当前进行中的任务数量
|
||||
const activeTaskCount = await this.prisma.aI3DTask.count({
|
||||
where: {
|
||||
userId,
|
||||
status: { in: ['pending', 'processing'] },
|
||||
},
|
||||
});
|
||||
|
||||
if (activeTaskCount >= MAX_CONCURRENT_TASKS) {
|
||||
throw new BadRequestException(
|
||||
`您当前有 ${activeTaskCount} 个任务正在处理中,最多同时处理 ${MAX_CONCURRENT_TASKS} 个任务,请等待完成后再提交`,
|
||||
);
|
||||
}
|
||||
|
||||
// 2. 创建数据库记录
|
||||
const task = await this.prisma.aI3DTask.create({
|
||||
data: {
|
||||
userId,
|
||||
tenantId,
|
||||
inputType: dto.inputType,
|
||||
inputContent: dto.inputContent,
|
||||
status: 'pending',
|
||||
},
|
||||
});
|
||||
|
||||
// 3. 提交到 AI 服务
|
||||
try {
|
||||
const externalTaskId = await this.ai3dProvider.submitTask(
|
||||
dto.inputType,
|
||||
dto.inputContent,
|
||||
{
|
||||
generateType: dto.generateType,
|
||||
faceCount: dto.faceCount,
|
||||
},
|
||||
);
|
||||
|
||||
// 4. 更新状态为处理中
|
||||
await this.prisma.aI3DTask.update({
|
||||
where: { id: task.id },
|
||||
data: {
|
||||
status: 'processing',
|
||||
externalTaskId,
|
||||
},
|
||||
});
|
||||
|
||||
// 5. 启动轮询检查任务状态
|
||||
this.pollTaskStatus(task.id, externalTaskId, Date.now());
|
||||
|
||||
this.logger.log(`任务 ${task.id} 创建成功,外部ID: ${externalTaskId}`);
|
||||
|
||||
return this.getTask(userId, task.id);
|
||||
} catch (error) {
|
||||
// 提交失败,更新状态
|
||||
await this.prisma.aI3DTask.update({
|
||||
where: { id: task.id },
|
||||
data: {
|
||||
status: 'failed',
|
||||
errorMessage: error.message || 'AI服务提交失败',
|
||||
completeTime: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.error(`任务 ${task.id} 提交失败: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务列表
|
||||
*/
|
||||
async getTasks(userId: number, query: QueryTaskDto) {
|
||||
const { page = 1, pageSize = 10, status } = query;
|
||||
|
||||
const where: any = { userId };
|
||||
if (status) {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
const [list, total] = await Promise.all([
|
||||
this.prisma.aI3DTask.findMany({
|
||||
where,
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
orderBy: { createTime: 'desc' },
|
||||
}),
|
||||
this.prisma.aI3DTask.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
list,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务详情
|
||||
*/
|
||||
async getTask(userId: number, id: number) {
|
||||
const task = await this.prisma.aI3DTask.findFirst({
|
||||
where: { id, userId },
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
throw new NotFoundException('任务不存在');
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除任务
|
||||
*/
|
||||
async deleteTask(userId: number, id: number) {
|
||||
const task = await this.getTask(userId, id);
|
||||
|
||||
await this.prisma.aI3DTask.delete({
|
||||
where: { id: task.id },
|
||||
});
|
||||
|
||||
this.logger.log(`任务 ${id} 已删除`);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试任务
|
||||
*/
|
||||
async retryTask(userId: number, id: number) {
|
||||
const task = await this.prisma.aI3DTask.findFirst({
|
||||
where: { id, userId },
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
throw new NotFoundException('任务不存在');
|
||||
}
|
||||
|
||||
// 只有失败或超时的任务可以重试
|
||||
if (!['failed', 'timeout'].includes(task.status)) {
|
||||
throw new BadRequestException('只有失败或超时的任务可以重试');
|
||||
}
|
||||
|
||||
// 检查重试次数
|
||||
if (task.retryCount >= MAX_RETRY_COUNT) {
|
||||
throw new BadRequestException(
|
||||
`已达到最大重试次数 ${MAX_RETRY_COUNT} 次,请创建新任务`,
|
||||
);
|
||||
}
|
||||
|
||||
// 检查并发限制
|
||||
const activeTaskCount = await this.prisma.aI3DTask.count({
|
||||
where: {
|
||||
userId,
|
||||
status: { in: ['pending', 'processing'] },
|
||||
},
|
||||
});
|
||||
|
||||
if (activeTaskCount >= MAX_CONCURRENT_TASKS) {
|
||||
throw new BadRequestException(
|
||||
`您当前有 ${activeTaskCount} 个任务正在处理中,请等待完成后再重试`,
|
||||
);
|
||||
}
|
||||
|
||||
// 重置任务状态
|
||||
await this.prisma.aI3DTask.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'pending',
|
||||
errorMessage: null,
|
||||
completeTime: null,
|
||||
retryCount: { increment: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
// 重新提交任务
|
||||
try {
|
||||
const externalTaskId = await this.ai3dProvider.submitTask(
|
||||
task.inputType as 'text' | 'image',
|
||||
task.inputContent,
|
||||
);
|
||||
|
||||
await this.prisma.aI3DTask.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'processing',
|
||||
externalTaskId,
|
||||
},
|
||||
});
|
||||
|
||||
this.pollTaskStatus(id, externalTaskId, Date.now());
|
||||
|
||||
this.logger.log(`任务 ${id} 重试成功,外部ID: ${externalTaskId}`);
|
||||
|
||||
return this.getTask(userId, id);
|
||||
} catch (error) {
|
||||
await this.prisma.aI3DTask.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'failed',
|
||||
errorMessage: error.message || 'AI服务提交失败',
|
||||
completeTime: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.error(`任务 ${id} 重试失败: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮询检查任务状态
|
||||
*/
|
||||
private async pollTaskStatus(
|
||||
taskId: number,
|
||||
externalTaskId: string,
|
||||
startTime: number,
|
||||
) {
|
||||
const checkStatus = async () => {
|
||||
// 1. 检查是否超时
|
||||
if (Date.now() - startTime > TASK_TIMEOUT_MS) {
|
||||
await this.prisma.aI3DTask.update({
|
||||
where: { id: taskId },
|
||||
data: {
|
||||
status: 'timeout',
|
||||
errorMessage: '任务处理超时,请重试',
|
||||
completeTime: new Date(),
|
||||
},
|
||||
});
|
||||
this.logger.warn(`任务 ${taskId} 超时`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 查询外部任务状态
|
||||
try {
|
||||
const result = await this.ai3dProvider.queryTask(externalTaskId);
|
||||
|
||||
if (result.status === 'completed' || result.status === 'failed') {
|
||||
await this.prisma.aI3DTask.update({
|
||||
where: { id: taskId },
|
||||
data: {
|
||||
status: result.status,
|
||||
resultUrl: result.resultUrl,
|
||||
previewUrl: result.previewUrl,
|
||||
resultUrls: result.resultUrls || null,
|
||||
previewUrls: result.previewUrls || null,
|
||||
errorMessage: result.errorMessage,
|
||||
completeTime: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`任务 ${taskId} ${result.status === 'completed' ? '完成' : '失败'}`,
|
||||
);
|
||||
} else {
|
||||
// 继续轮询,每2秒检查一次
|
||||
setTimeout(checkStatus, 2000);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`轮询任务 ${taskId} 状态出错: ${error.message}`);
|
||||
// 出错后延长轮询间隔,每5秒重试
|
||||
setTimeout(checkStatus, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
// 首次检查延迟2秒
|
||||
setTimeout(checkStatus, 2000);
|
||||
}
|
||||
}
|
||||
43
backend/src/ai-3d/dto/create-task.dto.ts
Normal file
43
backend/src/ai-3d/dto/create-task.dto.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import {
|
||||
IsString,
|
||||
IsIn,
|
||||
IsNotEmpty,
|
||||
MaxLength,
|
||||
IsOptional,
|
||||
IsInt,
|
||||
Min,
|
||||
Max,
|
||||
} from 'class-validator';
|
||||
|
||||
/**
|
||||
* 模型生成类型
|
||||
* Normal: 带纹理
|
||||
* LowPoly: 低多边形
|
||||
* Geometry: 白模
|
||||
* Sketch: 草图
|
||||
*/
|
||||
export type GenerateType = 'Normal' | 'LowPoly' | 'Geometry' | 'Sketch';
|
||||
|
||||
export class CreateTaskDto {
|
||||
@IsString()
|
||||
@IsIn(['text', 'image'], { message: '输入类型必须是 text 或 image' })
|
||||
inputType: 'text' | 'image';
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '输入内容不能为空' })
|
||||
@MaxLength(2000, { message: '输入内容最多2000个字符' })
|
||||
inputContent: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsIn(['Normal', 'LowPoly', 'Geometry', 'Sketch'], {
|
||||
message: '模型类型必须是 Normal、LowPoly、Geometry 或 Sketch',
|
||||
})
|
||||
generateType?: GenerateType;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt({ message: '模型面数必须是整数' })
|
||||
@Min(10000, { message: '模型面数最小为10000' })
|
||||
@Max(1500000, { message: '模型面数最大为1500000' })
|
||||
faceCount?: number;
|
||||
}
|
||||
23
backend/src/ai-3d/dto/query-task.dto.ts
Normal file
23
backend/src/ai-3d/dto/query-task.dto.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { IsOptional, IsString, IsInt, Min, IsIn } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class QueryTaskDto {
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
page?: number = 1;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
pageSize?: number = 10;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsIn(['pending', 'processing', 'completed', 'failed', 'timeout'], {
|
||||
message: '状态必须是 pending、processing、completed、failed 或 timeout',
|
||||
})
|
||||
status?: string;
|
||||
}
|
||||
53
backend/src/ai-3d/providers/ai-3d-provider.interface.ts
Normal file
53
backend/src/ai-3d/providers/ai-3d-provider.interface.ts
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* AI 3D 生成结果
|
||||
*/
|
||||
export interface AI3DGenerateResult {
|
||||
taskId: string; // 外部任务ID
|
||||
status: 'pending' | 'processing' | 'completed' | 'failed';
|
||||
resultUrl?: string; // 3D模型URL(单个结果,兼容旧数据)
|
||||
previewUrl?: string; // 预览图URL(单个结果,兼容旧数据)
|
||||
resultUrls?: string[]; // 3D模型URL数组(多个结果,文生3D生成4个)
|
||||
previewUrls?: string[]; // 预览图URL数组(多个结果)
|
||||
errorMessage?: string; // 错误信息
|
||||
}
|
||||
|
||||
/**
|
||||
* 模型生成配置选项
|
||||
*/
|
||||
export interface AI3DGenerateOptions {
|
||||
/** 模型生成类型:Normal-带纹理, LowPoly-低多边形, Geometry-白模, Sketch-草图 */
|
||||
generateType?: 'Normal' | 'LowPoly' | 'Geometry' | 'Sketch';
|
||||
/** 模型面数:10000-1500000,默认500000 */
|
||||
faceCount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 3D 服务提供者接口
|
||||
* 支持 Mock、腾讯混元、Meshy 等实现
|
||||
*/
|
||||
export interface AI3DProvider {
|
||||
/**
|
||||
* 提交生成任务
|
||||
* @param inputType 输入类型:text | image
|
||||
* @param inputContent 输入内容:文字描述或图片URL
|
||||
* @param options 可选配置项(仅文生3D支持)
|
||||
* @returns 外部任务ID
|
||||
*/
|
||||
submitTask(
|
||||
inputType: 'text' | 'image',
|
||||
inputContent: string,
|
||||
options?: AI3DGenerateOptions,
|
||||
): Promise<string>;
|
||||
|
||||
/**
|
||||
* 查询任务状态
|
||||
* @param taskId 外部任务ID
|
||||
* @returns 任务状态和结果
|
||||
*/
|
||||
queryTask(taskId: string): Promise<AI3DGenerateResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 3D Provider 注入令牌
|
||||
*/
|
||||
export const AI3D_PROVIDER = 'AI3D_PROVIDER';
|
||||
281
backend/src/ai-3d/providers/hunyuan.provider.ts
Normal file
281
backend/src/ai-3d/providers/hunyuan.provider.ts
Normal file
@ -0,0 +1,281 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
AI3DProvider,
|
||||
AI3DGenerateResult,
|
||||
AI3DGenerateOptions,
|
||||
} from './ai-3d-provider.interface';
|
||||
import { TencentCloudSigner } from '../utils/tencent-cloud-sign';
|
||||
import { ZipHandler } from '../utils/zip-handler';
|
||||
|
||||
/**
|
||||
* 腾讯混元 3D Provider
|
||||
* 文档:https://cloud.tencent.com/document/api/1804/123447
|
||||
* API概览:https://cloud.tencent.com/document/product/1804/120838
|
||||
*
|
||||
* 重要说明:
|
||||
* - 默认提供3个并发任务,最多同时处理3个任务
|
||||
* - 每个任务会消耗一定积分(根据资源包计费)
|
||||
* - 如果遇到"资源不足"错误,可能是:并发数达到上限、积分不足、或服务暂时不可用
|
||||
*/
|
||||
@Injectable()
|
||||
export class HunyuanAI3DProvider implements AI3DProvider {
|
||||
private readonly logger = new Logger(HunyuanAI3DProvider.name);
|
||||
private readonly apiHost = 'ai3d.tencentcloudapi.com';
|
||||
private readonly apiVersion = '2025-05-13'; // 使用正确的API版本
|
||||
private readonly secretId: string;
|
||||
private readonly secretKey: string;
|
||||
private readonly region: string;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.secretId = this.configService.get<string>('TENCENT_SECRET_ID');
|
||||
this.secretKey = this.configService.get<string>('TENCENT_SECRET_KEY');
|
||||
this.region =
|
||||
this.configService.get<string>('TENCENT_REGION') || 'ap-guangzhou';
|
||||
|
||||
if (!this.secretId || !this.secretKey) {
|
||||
this.logger.warn(
|
||||
'未配置腾讯云密钥,请设置 TENCENT_SECRET_ID 和 TENCENT_SECRET_KEY 环境变量',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交生成任务
|
||||
*/
|
||||
async submitTask(
|
||||
inputType: 'text' | 'image',
|
||||
inputContent: string,
|
||||
options?: AI3DGenerateOptions,
|
||||
): Promise<string> {
|
||||
try {
|
||||
// 构造请求参数
|
||||
const payload: any = {};
|
||||
|
||||
if (inputType === 'text') {
|
||||
// 文生3D:使用 Prompt
|
||||
payload.Prompt = inputContent;
|
||||
|
||||
// 文生3D支持额外参数
|
||||
if (options?.generateType) {
|
||||
payload.GenerateType = options.generateType;
|
||||
}
|
||||
if (options?.faceCount) {
|
||||
payload.FaceCount = options.faceCount;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`提交文生3D任务: ${inputContent.substring(0, 50)}... ` +
|
||||
`[类型: ${options?.generateType || 'Normal'}, 面数: ${options?.faceCount || 500000}]`,
|
||||
);
|
||||
} else {
|
||||
// 图生3D:使用 ImageUrl 或 ImageBase64
|
||||
if (
|
||||
inputContent.startsWith('http://') ||
|
||||
inputContent.startsWith('https://')
|
||||
) {
|
||||
payload.ImageUrl = inputContent;
|
||||
} else {
|
||||
// 假设是 Base64 编码的图片
|
||||
payload.ImageBase64 = inputContent;
|
||||
}
|
||||
|
||||
// 图生3D也支持模型类型
|
||||
if (options?.generateType) {
|
||||
payload.GenerateType = options.generateType;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`提交图生3D任务: ${inputContent.substring(0, 50)}... ` +
|
||||
`[类型: ${options?.generateType || 'Normal'}]`,
|
||||
);
|
||||
}
|
||||
|
||||
// 生成签名和请求头
|
||||
const headers = TencentCloudSigner.sign({
|
||||
secretId: this.secretId,
|
||||
secretKey: this.secretKey,
|
||||
service: 'ai3d',
|
||||
host: this.apiHost,
|
||||
region: this.region,
|
||||
action: 'SubmitHunyuanTo3DProJob',
|
||||
version: this.apiVersion,
|
||||
payload,
|
||||
});
|
||||
|
||||
// 发送请求
|
||||
const response = await axios.post(`https://${this.apiHost}`, payload, {
|
||||
headers,
|
||||
});
|
||||
|
||||
// 检查响应
|
||||
if (response.data.Response?.Error) {
|
||||
const error = response.data.Response.Error;
|
||||
this.logger.error(`混元3D API错误: ${error.Code} - ${error.Message}`);
|
||||
|
||||
// 对特定错误提供更友好的提示
|
||||
if (error.Code === 'ResourceInsufficient') {
|
||||
const friendlyMessage =
|
||||
'资源不足。可能原因:1) 并发任务数已达到上限(默认3个),请等待其他任务完成;' +
|
||||
'2) 积分余额不足,请检查腾讯云控制台的积分余额;' +
|
||||
'3) 服务暂时不可用,请稍后重试。';
|
||||
throw new Error(`混元3D API错误: ${friendlyMessage}`);
|
||||
}
|
||||
|
||||
throw new Error(`混元3D API错误: ${error.Message}`);
|
||||
}
|
||||
|
||||
const jobId = response.data.Response?.JobId;
|
||||
if (!jobId) {
|
||||
this.logger.error('混元3D API未返回JobId');
|
||||
throw new Error('混元3D API未返回任务ID');
|
||||
}
|
||||
|
||||
this.logger.log(`混元3D任务创建成功: ${jobId}`);
|
||||
return jobId;
|
||||
} catch (error) {
|
||||
this.logger.error(`提交混元3D任务失败: ${error.message}`, error.stack);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询任务状态
|
||||
*/
|
||||
async queryTask(taskId: string): Promise<AI3DGenerateResult> {
|
||||
try {
|
||||
// 构造请求参数
|
||||
const payload = {
|
||||
JobId: taskId,
|
||||
};
|
||||
|
||||
// 生成签名和请求头
|
||||
const headers = TencentCloudSigner.sign({
|
||||
secretId: this.secretId,
|
||||
secretKey: this.secretKey,
|
||||
service: 'ai3d',
|
||||
host: this.apiHost,
|
||||
region: this.region,
|
||||
action: 'QueryHunyuanTo3DProJob',
|
||||
version: this.apiVersion,
|
||||
payload,
|
||||
});
|
||||
|
||||
// 发送请求
|
||||
const response = await axios.post(`https://${this.apiHost}`, payload, {
|
||||
headers,
|
||||
});
|
||||
|
||||
// 检查响应
|
||||
if (response.data.Response?.Error) {
|
||||
const error = response.data.Response.Error;
|
||||
this.logger.error(`混元3D查询错误: ${error.Code} - ${error.Message}`);
|
||||
return {
|
||||
taskId,
|
||||
status: 'failed',
|
||||
errorMessage: error.Message,
|
||||
};
|
||||
}
|
||||
|
||||
const result = response.data.Response;
|
||||
|
||||
// 映射任务状态
|
||||
// 混元状态: WAIT(等待中)| RUN(运行中)| FAIL(失败)| DONE(完成)
|
||||
const statusMap: Record<
|
||||
string,
|
||||
'pending' | 'processing' | 'completed' | 'failed'
|
||||
> = {
|
||||
WAIT: 'processing',
|
||||
RUN: 'processing',
|
||||
FAIL: 'failed',
|
||||
DONE: 'completed',
|
||||
};
|
||||
|
||||
const status = statusMap[result.Status] || 'processing';
|
||||
|
||||
// 构造返回结果
|
||||
const generateResult: AI3DGenerateResult = {
|
||||
taskId,
|
||||
status,
|
||||
};
|
||||
|
||||
// 如果任务完成,提取模型URL
|
||||
// 根据API文档,返回的是 ResultFile3Ds 数组
|
||||
if (status === 'completed' && result.ResultFile3Ds?.length > 0) {
|
||||
const file3Ds = result.ResultFile3Ds;
|
||||
// 提取所有模型URL和预览图URL
|
||||
const urls = file3Ds.map((file: any) => file.Url).filter(Boolean);
|
||||
const previewUrls = file3Ds
|
||||
.map((file: any) => file.PreviewImageUrl)
|
||||
.filter(Boolean);
|
||||
|
||||
// 处理.zip文件:下载并解压
|
||||
if (urls.length > 0) {
|
||||
const firstUrl = urls[0];
|
||||
|
||||
// 检查是否是.zip文件
|
||||
if (firstUrl.toLowerCase().endsWith('.zip')) {
|
||||
this.logger.log(`检测到ZIP文件,开始下载并解压: ${firstUrl}`);
|
||||
try {
|
||||
const extracted = await ZipHandler.downloadAndExtract(firstUrl);
|
||||
|
||||
// 使用解压后的文件URL
|
||||
generateResult.resultUrl = extracted.modelUrl;
|
||||
generateResult.resultUrls = [extracted.modelUrl];
|
||||
|
||||
if (extracted.previewUrl) {
|
||||
generateResult.previewUrl = extracted.previewUrl;
|
||||
generateResult.previewUrls = [extracted.previewUrl];
|
||||
} else if (previewUrls.length > 0) {
|
||||
// 如果ZIP中没有预览图,使用API返回的预览图
|
||||
generateResult.previewUrl = previewUrls[0];
|
||||
generateResult.previewUrls = previewUrls;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`ZIP文件处理完成,模型URL: ${extracted.modelUrl}`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(`处理ZIP文件失败: ${error.message}`);
|
||||
// ZIP处理失败,尝试直接返回原始URL
|
||||
generateResult.resultUrl = firstUrl;
|
||||
generateResult.resultUrls = urls;
|
||||
generateResult.previewUrl = previewUrls[0];
|
||||
generateResult.previewUrls = previewUrls;
|
||||
}
|
||||
} else {
|
||||
// 不是ZIP文件,直接使用URL
|
||||
generateResult.resultUrl = firstUrl;
|
||||
generateResult.resultUrls = urls;
|
||||
|
||||
if (previewUrls.length > 0) {
|
||||
generateResult.previewUrl = previewUrls[0];
|
||||
generateResult.previewUrls = previewUrls;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`混元3D任务 ${taskId} 完成: ${generateResult.resultUrls?.length || 0} 个模型文件`,
|
||||
);
|
||||
} else if (status === 'failed') {
|
||||
// 失败原因:根据文档,错误信息在 ErrorMessage 字段
|
||||
generateResult.errorMessage =
|
||||
result.ErrorMessage || result.ErrorCode || '生成失败';
|
||||
this.logger.warn(
|
||||
`混元3D任务 ${taskId} 失败: ${generateResult.errorMessage}`,
|
||||
);
|
||||
}
|
||||
|
||||
return generateResult;
|
||||
} catch (error) {
|
||||
this.logger.error(`查询混元3D任务失败: ${error.message}`, error.stack);
|
||||
return {
|
||||
taskId,
|
||||
status: 'failed',
|
||||
errorMessage: `查询任务失败: ${error.message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
179
backend/src/ai-3d/providers/mock.provider.ts
Normal file
179
backend/src/ai-3d/providers/mock.provider.ts
Normal file
@ -0,0 +1,179 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { AI3DProvider, AI3DGenerateResult } from './ai-3d-provider.interface';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
interface MockTask {
|
||||
status: 'pending' | 'processing' | 'completed' | 'failed';
|
||||
startTime: number;
|
||||
inputType: string;
|
||||
inputContent: string;
|
||||
resultUrl?: string;
|
||||
previewUrl?: string;
|
||||
resultUrls?: string[];
|
||||
previewUrls?: string[];
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock AI 3D Provider
|
||||
* 用于开发阶段模拟 AI 3D 生成服务
|
||||
*/
|
||||
@Injectable()
|
||||
export class MockAI3DProvider implements AI3DProvider {
|
||||
private readonly logger = new Logger(MockAI3DProvider.name);
|
||||
private tasks = new Map<string, MockTask>();
|
||||
|
||||
// 模拟完成时间范围(毫秒)
|
||||
private readonly MIN_COMPLETION_TIME = 5000; // 5秒
|
||||
private readonly MAX_COMPLETION_TIME = 15000; // 15秒
|
||||
|
||||
// 模拟成功率
|
||||
private readonly SUCCESS_RATE = 0.9; // 90% 成功率
|
||||
|
||||
// 示例 3D 模型 URL(使用公开可访问的 GLB 文件)
|
||||
private readonly SAMPLE_MODELS = [
|
||||
// three.js 官方示例模型
|
||||
'https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf',
|
||||
'https://threejs.org/examples/models/gltf/LittlestTokyo.glb',
|
||||
'https://threejs.org/examples/models/gltf/Soldier.glb',
|
||||
'https://threejs.org/examples/models/gltf/RobotExpressive/RobotExpressive.glb',
|
||||
];
|
||||
|
||||
// 示例预览图(使用占位图服务)
|
||||
private readonly SAMPLE_PREVIEWS = [
|
||||
'https://picsum.photos/seed/model1/400/300',
|
||||
'https://picsum.photos/seed/model2/400/300',
|
||||
'https://picsum.photos/seed/model3/400/300',
|
||||
'https://picsum.photos/seed/model4/400/300',
|
||||
];
|
||||
|
||||
async submitTask(
|
||||
inputType: 'text' | 'image',
|
||||
inputContent: string,
|
||||
): Promise<string> {
|
||||
const taskId = uuidv4();
|
||||
|
||||
this.logger.log(
|
||||
`Mock: 创建任务 ${taskId}, 类型: ${inputType}, 内容: ${inputContent.substring(0, 50)}...`,
|
||||
);
|
||||
|
||||
// 创建任务记录
|
||||
this.tasks.set(taskId, {
|
||||
status: 'processing',
|
||||
startTime: Date.now(),
|
||||
inputType,
|
||||
inputContent,
|
||||
});
|
||||
|
||||
// 模拟异步完成
|
||||
const completionTime =
|
||||
this.MIN_COMPLETION_TIME +
|
||||
Math.random() * (this.MAX_COMPLETION_TIME - this.MIN_COMPLETION_TIME);
|
||||
|
||||
setTimeout(() => {
|
||||
this.completeTask(taskId);
|
||||
}, completionTime);
|
||||
|
||||
return taskId;
|
||||
}
|
||||
|
||||
async queryTask(taskId: string): Promise<AI3DGenerateResult> {
|
||||
const task = this.tasks.get(taskId);
|
||||
|
||||
if (!task) {
|
||||
this.logger.warn(`Mock: 任务 ${taskId} 不存在`);
|
||||
return {
|
||||
taskId,
|
||||
status: 'failed',
|
||||
errorMessage: '任务不存在',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
taskId,
|
||||
status: task.status,
|
||||
resultUrl: task.resultUrl,
|
||||
previewUrl: task.previewUrl,
|
||||
resultUrls: task.resultUrls,
|
||||
previewUrls: task.previewUrls,
|
||||
errorMessage: task.errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟任务完成
|
||||
*/
|
||||
private completeTask(taskId: string): void {
|
||||
const task = this.tasks.get(taskId);
|
||||
if (!task) return;
|
||||
|
||||
// 根据成功率决定是否成功
|
||||
const isSuccess = Math.random() < this.SUCCESS_RATE;
|
||||
|
||||
if (isSuccess) {
|
||||
// 文生3D生成4个不同角度的模型
|
||||
if (task.inputType === 'text') {
|
||||
const resultUrls: string[] = [];
|
||||
const previewUrls: string[] = [];
|
||||
|
||||
// 生成4个模型结果,使用不同的示例模型
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const modelIndex = i % this.SAMPLE_MODELS.length;
|
||||
const previewIndex = i % this.SAMPLE_PREVIEWS.length;
|
||||
resultUrls.push(this.SAMPLE_MODELS[modelIndex]);
|
||||
previewUrls.push(this.SAMPLE_PREVIEWS[previewIndex]);
|
||||
}
|
||||
|
||||
task.status = 'completed';
|
||||
task.resultUrls = resultUrls;
|
||||
task.previewUrls = previewUrls;
|
||||
// 兼容旧字段,使用第一个结果
|
||||
task.resultUrl = resultUrls[0];
|
||||
task.previewUrl = previewUrls[0];
|
||||
|
||||
this.logger.log(
|
||||
`Mock: 文生3D任务 ${taskId} 完成, 生成 ${resultUrls.length} 个模型`,
|
||||
);
|
||||
} else {
|
||||
// 图生3D只生成1个模型
|
||||
const modelIndex = Math.floor(
|
||||
Math.random() * this.SAMPLE_MODELS.length,
|
||||
);
|
||||
const modelUrl = this.SAMPLE_MODELS[modelIndex];
|
||||
const previewUrl = this.SAMPLE_PREVIEWS[modelIndex % this.SAMPLE_PREVIEWS.length];
|
||||
|
||||
task.status = 'completed';
|
||||
task.resultUrl = modelUrl;
|
||||
task.previewUrl = previewUrl;
|
||||
task.resultUrls = [modelUrl];
|
||||
task.previewUrls = [previewUrl];
|
||||
|
||||
this.logger.log(`Mock: 图生3D任务 ${taskId} 完成, 模型: ${modelUrl}`);
|
||||
}
|
||||
} else {
|
||||
task.status = 'failed';
|
||||
task.errorMessage = '模拟生成失败:AI 服务暂时不可用';
|
||||
|
||||
this.logger.warn(`Mock: 任务 ${taskId} 失败`);
|
||||
}
|
||||
|
||||
this.tasks.set(taskId, task);
|
||||
|
||||
// 清理过期任务(保留1小时)
|
||||
this.cleanupOldTasks();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理超过1小时的任务记录
|
||||
*/
|
||||
private cleanupOldTasks(): void {
|
||||
const oneHourAgo = Date.now() - 60 * 60 * 1000;
|
||||
|
||||
for (const [taskId, task] of this.tasks.entries()) {
|
||||
if (task.startTime < oneHourAgo) {
|
||||
this.tasks.delete(taskId);
|
||||
this.logger.debug(`Mock: 清理过期任务 ${taskId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
101
backend/src/ai-3d/utils/tencent-cloud-sign.ts
Normal file
101
backend/src/ai-3d/utils/tencent-cloud-sign.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
export interface TencentCloudSignOptions {
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
service: string;
|
||||
host: string;
|
||||
region?: string;
|
||||
action: string;
|
||||
version: string;
|
||||
payload?: any;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 腾讯云 API 签名 v3
|
||||
* 文档:https://cloud.tencent.com/document/api/213/30654
|
||||
*/
|
||||
export class TencentCloudSigner {
|
||||
private static readonly ALGORITHM = 'TC3-HMAC-SHA256';
|
||||
private static readonly SIGNED_HEADERS = 'content-type;host;x-tc-action';
|
||||
|
||||
/**
|
||||
* 生成签名和请求头
|
||||
*/
|
||||
static sign(options: TencentCloudSignOptions): Record<string, string> {
|
||||
const timestamp = options.timestamp || Math.floor(Date.now() / 1000);
|
||||
const date = new Date(timestamp * 1000)
|
||||
.toISOString()
|
||||
.substr(0, 10);
|
||||
|
||||
// 1. 拼接规范请求串
|
||||
const payload = options.payload ? JSON.stringify(options.payload) : '';
|
||||
const hashedRequestPayload = this.sha256Hex(payload);
|
||||
const canonicalRequest = [
|
||||
'POST',
|
||||
'/',
|
||||
'',
|
||||
`content-type:application/json`,
|
||||
`host:${options.host}`,
|
||||
`x-tc-action:${options.action.toLowerCase()}`,
|
||||
'',
|
||||
this.SIGNED_HEADERS,
|
||||
hashedRequestPayload,
|
||||
].join('\n');
|
||||
|
||||
// 2. 拼接待签名字符串
|
||||
const hashedCanonicalRequest = this.sha256Hex(canonicalRequest);
|
||||
const credentialScope = `${date}/${options.service}/tc3_request`;
|
||||
const stringToSign = [
|
||||
this.ALGORITHM,
|
||||
timestamp.toString(),
|
||||
credentialScope,
|
||||
hashedCanonicalRequest,
|
||||
].join('\n');
|
||||
|
||||
// 3. 计算签名
|
||||
const secretDate = this.hmacSha256(
|
||||
`TC3${options.secretKey}`,
|
||||
date,
|
||||
);
|
||||
const secretService = this.hmacSha256(secretDate, options.service);
|
||||
const secretSigning = this.hmacSha256(secretService, 'tc3_request');
|
||||
const signature = this.hmacSha256Hex(secretSigning, stringToSign);
|
||||
|
||||
// 4. 拼接 Authorization
|
||||
const authorization = `${this.ALGORITHM} Credential=${options.secretId}/${credentialScope}, SignedHeaders=${this.SIGNED_HEADERS}, Signature=${signature}`;
|
||||
|
||||
// 5. 返回请求头
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'Host': options.host,
|
||||
'X-TC-Action': options.action,
|
||||
'X-TC-Version': options.version,
|
||||
'X-TC-Timestamp': timestamp.toString(),
|
||||
'X-TC-Region': options.region || 'ap-guangzhou',
|
||||
'Authorization': authorization,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* SHA256 哈希(十六进制)
|
||||
*/
|
||||
private static sha256Hex(data: string): string {
|
||||
return crypto.createHash('sha256').update(data, 'utf8').digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* HMAC-SHA256(Buffer)
|
||||
*/
|
||||
private static hmacSha256(key: string | Buffer, data: string): Buffer {
|
||||
return crypto.createHmac('sha256', key).update(data, 'utf8').digest();
|
||||
}
|
||||
|
||||
/**
|
||||
* HMAC-SHA256(十六进制)
|
||||
*/
|
||||
private static hmacSha256Hex(key: Buffer, data: string): string {
|
||||
return crypto.createHmac('sha256', key).update(data, 'utf8').digest('hex');
|
||||
}
|
||||
}
|
||||
186
backend/src/ai-3d/utils/zip-handler.ts
Normal file
186
backend/src/ai-3d/utils/zip-handler.ts
Normal file
@ -0,0 +1,186 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as AdmZip from 'adm-zip';
|
||||
import axios from 'axios';
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
export class ZipHandler {
|
||||
private static readonly logger = new Logger(ZipHandler.name);
|
||||
|
||||
/**
|
||||
* 下载并解压.zip文件,提取3D模型文件
|
||||
* @param zipUrl ZIP文件的URL
|
||||
* @param outputDir 输出目录(默认为 backend/uploads/ai-3d)
|
||||
* @returns 提取的3D模型文件路径和预览图路径
|
||||
*/
|
||||
static async downloadAndExtract(
|
||||
zipUrl: string,
|
||||
outputDir?: string,
|
||||
): Promise<{
|
||||
modelPath: string;
|
||||
previewPath?: string;
|
||||
modelUrl: string;
|
||||
previewUrl?: string;
|
||||
}> {
|
||||
try {
|
||||
// 1. 设置输出目录
|
||||
const baseDir =
|
||||
outputDir ||
|
||||
path.join(process.cwd(), 'uploads', 'ai-3d', Date.now().toString());
|
||||
if (!fs.existsSync(baseDir)) {
|
||||
fs.mkdirSync(baseDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 2. 下载ZIP文件
|
||||
this.logger.log(`开始下载ZIP文件: ${zipUrl}`);
|
||||
const zipPath = path.join(baseDir, 'model.zip');
|
||||
await this.downloadFile(zipUrl, zipPath);
|
||||
this.logger.log(`ZIP文件下载完成: ${zipPath}`);
|
||||
|
||||
// 3. 解压ZIP文件
|
||||
this.logger.log(`开始解压ZIP文件`);
|
||||
const extractDir = path.join(baseDir, 'extracted');
|
||||
await this.extractZip(zipPath, extractDir);
|
||||
this.logger.log(`ZIP文件解压完成: ${extractDir}`);
|
||||
|
||||
// 4. 查找3D模型文件和预览图
|
||||
const files = this.getAllFiles(extractDir);
|
||||
const modelFile = this.findModelFile(files);
|
||||
const previewFile = this.findPreviewImage(files);
|
||||
|
||||
if (!modelFile) {
|
||||
throw new Error('在ZIP文件中未找到3D模型文件(.glb, .gltf)');
|
||||
}
|
||||
|
||||
this.logger.log(`找到3D模型文件: ${modelFile}`);
|
||||
if (previewFile) {
|
||||
this.logger.log(`找到预览图: ${previewFile}`);
|
||||
}
|
||||
|
||||
// 5. 生成可访问的URL
|
||||
// 假设模型文件在 uploads/ai-3d/timestamp/extracted/model.glb
|
||||
// URL应该是 /api/uploads/ai-3d/timestamp/extracted/model.glb
|
||||
const relativeModelPath = path.relative(
|
||||
path.join(process.cwd(), 'uploads'),
|
||||
modelFile,
|
||||
);
|
||||
const modelUrl = `/api/uploads/${relativeModelPath.replace(/\\/g, '/')}`;
|
||||
|
||||
let previewUrl: string | undefined;
|
||||
if (previewFile) {
|
||||
const relativePreviewPath = path.relative(
|
||||
path.join(process.cwd(), 'uploads'),
|
||||
previewFile,
|
||||
);
|
||||
previewUrl = `/api/uploads/${relativePreviewPath.replace(/\\/g, '/')}`;
|
||||
}
|
||||
|
||||
// 6. 删除原始ZIP文件以节省空间
|
||||
try {
|
||||
fs.unlinkSync(zipPath);
|
||||
this.logger.log(`已删除原始ZIP文件: ${zipPath}`);
|
||||
} catch (err) {
|
||||
this.logger.warn(`删除ZIP文件失败: ${err.message}`);
|
||||
}
|
||||
|
||||
return {
|
||||
modelPath: modelFile,
|
||||
previewPath: previewFile,
|
||||
modelUrl,
|
||||
previewUrl,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`处理ZIP文件失败: ${error.message}`, error.stack);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
*/
|
||||
private static async downloadFile(
|
||||
url: string,
|
||||
outputPath: string,
|
||||
): Promise<void> {
|
||||
const response = await axios.get(url, {
|
||||
responseType: 'arraybuffer',
|
||||
timeout: 60000, // 60秒超时
|
||||
});
|
||||
|
||||
fs.writeFileSync(outputPath, response.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解压ZIP文件
|
||||
*/
|
||||
private static async extractZip(
|
||||
zipPath: string,
|
||||
outputDir: string,
|
||||
): Promise<void> {
|
||||
const zip = new AdmZip(zipPath);
|
||||
zip.extractAllTo(outputDir, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归获取目录下的所有文件
|
||||
*/
|
||||
private static getAllFiles(dir: string): string[] {
|
||||
const files: string[] = [];
|
||||
|
||||
const traverse = (currentDir: string) => {
|
||||
const items = fs.readdirSync(currentDir);
|
||||
|
||||
for (const item of items) {
|
||||
const fullPath = path.join(currentDir, item);
|
||||
const stat = fs.statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
traverse(fullPath);
|
||||
} else {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
traverse(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找3D模型文件(.glb, .gltf)
|
||||
*/
|
||||
private static findModelFile(files: string[]): string | undefined {
|
||||
// 优先查找.glb文件(二进制格式,更常用)
|
||||
const glbFile = files.find((file) => file.toLowerCase().endsWith('.glb'));
|
||||
if (glbFile) return glbFile;
|
||||
|
||||
// 其次查找.gltf文件
|
||||
const gltfFile = files.find((file) =>
|
||||
file.toLowerCase().endsWith('.gltf'),
|
||||
);
|
||||
if (gltfFile) return gltfFile;
|
||||
|
||||
// 其他可能的3D格式
|
||||
const otherFormats = ['.obj', '.fbx', '.stl'];
|
||||
for (const format of otherFormats) {
|
||||
const file = files.find((f) => f.toLowerCase().endsWith(format));
|
||||
if (file) return file;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找预览图(.jpg, .jpeg, .png)
|
||||
*/
|
||||
private static findPreviewImage(files: string[]): string | undefined {
|
||||
const imageExtensions = ['.jpg', '.jpeg', '.png', '.webp'];
|
||||
|
||||
for (const ext of imageExtensions) {
|
||||
const imageFile = files.find((file) => file.toLowerCase().endsWith(ext));
|
||||
if (imageFile) return imageFile;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
89
backend/src/app.module.ts
Normal file
89
backend/src/app.module.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import { APP_GUARD, APP_INTERCEPTOR, APP_FILTER } from '@nestjs/core';
|
||||
import { join } from 'path';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { UsersModule } from './users/users.module';
|
||||
import { RolesModule } from './roles/roles.module';
|
||||
import { PermissionsModule } from './permissions/permissions.module';
|
||||
import { MenusModule } from './menus/menus.module';
|
||||
import { DictModule } from './dict/dict.module';
|
||||
import { ConfigModule as SystemConfigModule } from './config/config.module';
|
||||
import { LogsModule } from './logs/logs.module';
|
||||
import { TenantsModule } from './tenants/tenants.module';
|
||||
import { SchoolModule } from './school/school.module';
|
||||
import { ContestsModule } from './contests/contests.module';
|
||||
import { JudgesManagementModule } from './judges-management/judges-management.module';
|
||||
import { UploadModule } from './upload/upload.module';
|
||||
import { HomeworkModule } from './homework/homework.module';
|
||||
import { OssModule } from './oss/oss.module';
|
||||
import { AI3DModule } from './ai-3d/ai-3d.module';
|
||||
import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from './auth/guards/roles.guard';
|
||||
import { TransformInterceptor } from './common/interceptors/transform.interceptor';
|
||||
import { LoggingInterceptor } from './common/interceptors/logging.interceptor';
|
||||
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
// envFilePath 指定配置文件路径
|
||||
// 如果需要后备文件,可以取消下面的注释,但要注意 .env 会覆盖 .development.env 的值
|
||||
envFilePath: [
|
||||
'.env',
|
||||
`.env.${process.env.NODE_ENV || 'development'}`, // 优先加载
|
||||
],
|
||||
}),
|
||||
// 静态文件服务 - 提供 uploads 目录的访问
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: join(process.cwd(), 'uploads'),
|
||||
serveRoot: '/api/uploads',
|
||||
serveStaticOptions: {
|
||||
index: false,
|
||||
},
|
||||
}),
|
||||
PrismaModule,
|
||||
AuthModule,
|
||||
UsersModule,
|
||||
RolesModule,
|
||||
PermissionsModule,
|
||||
MenusModule,
|
||||
DictModule,
|
||||
SystemConfigModule,
|
||||
LogsModule,
|
||||
TenantsModule,
|
||||
SchoolModule,
|
||||
ContestsModule,
|
||||
JudgesManagementModule,
|
||||
UploadModule,
|
||||
HomeworkModule,
|
||||
OssModule,
|
||||
AI3DModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: JwtAuthGuard,
|
||||
},
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: RolesGuard,
|
||||
},
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: LoggingInterceptor, // 日志拦截器,先执行
|
||||
},
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: TransformInterceptor, // 响应转换拦截器
|
||||
},
|
||||
{
|
||||
provide: APP_FILTER,
|
||||
useClass: HttpExceptionFilter,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
41
backend/src/auth/auth.controller.ts
Normal file
41
backend/src/auth/auth.controller.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Get,
|
||||
Body,
|
||||
UseGuards,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { AuthService } from './auth.service';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
import { Public } from './decorators/public.decorator';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
@Public()
|
||||
@UseGuards(AuthGuard('local'))
|
||||
@Post('login')
|
||||
async login(@Body() loginDto: LoginDto, @Request() req) {
|
||||
// 从请求头或请求体获取租户ID
|
||||
const tenantId = req.headers['x-tenant-id']
|
||||
? parseInt(req.headers['x-tenant-id'], 10)
|
||||
: req.user?.tenantId;
|
||||
|
||||
return this.authService.login(req.user, tenantId);
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Get('user-info')
|
||||
async getUserInfo(@Request() req) {
|
||||
return this.authService.getUserInfo(req.user.userId);
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Post('logout')
|
||||
async logout() {
|
||||
return { message: '登出成功' };
|
||||
}
|
||||
}
|
||||
30
backend/src/auth/auth.module.ts
Normal file
30
backend/src/auth/auth.module.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||
import { LocalStrategy } from './strategies/local.strategy';
|
||||
import { RolesGuard } from './guards/roles.guard';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UsersModule,
|
||||
PrismaModule,
|
||||
PassportModule,
|
||||
JwtModule.registerAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
secret: config.get<string>('JWT_SECRET') || 'your-secret-key',
|
||||
signOptions: { expiresIn: '7d' },
|
||||
}),
|
||||
}),
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, JwtStrategy, LocalStrategy, RolesGuard],
|
||||
exports: [AuthService, RolesGuard],
|
||||
})
|
||||
export class AuthModule {}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user