Initial commit: MilliSim4 full-stack project (Vue3 + ASP.NET Core10)
This commit is contained in:
parent
403b7b730f
commit
cc9b68e4d4
43
.gitignore
vendored
Normal file
43
.gitignore
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
# ============================================================
|
||||
# MilliSim4 .gitignore
|
||||
# 规则:配置文件允许提交,构建产物排除,文档文件允许提交
|
||||
# ============================================================
|
||||
|
||||
# ======== 构建产物 ========
|
||||
# .NET 后端编译输出
|
||||
**/bin/
|
||||
**/obj/
|
||||
|
||||
# 前端构建输出
|
||||
frontend/dist/
|
||||
frontend/node_modules/
|
||||
|
||||
# 发布包
|
||||
*.publish.zip
|
||||
publish/
|
||||
|
||||
# ======== 日志文件 ========
|
||||
**/logs/
|
||||
*.log
|
||||
backend-startup.log
|
||||
frontend-startup.log
|
||||
|
||||
# ======== 运行时用户数据 ========
|
||||
# 上传的文件和数据库备份(环境相关,不入库)
|
||||
**/wwwroot/uploads/
|
||||
**/wwwroot/backups/
|
||||
|
||||
# ======== Python 缓存 ========
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# ======== 操作系统/IDE 临时文件 ========
|
||||
Thumbs.db
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# ======== 敏感临时文件 ========
|
||||
*.user
|
||||
*.suo
|
||||
187
.trae/documents/about-us-module-and-reset.md
Normal file
187
.trae/documents/about-us-module-and-reset.md
Normal file
@ -0,0 +1,187 @@
|
||||
# 关于我们模块 + 数据库重置 + COS 清理 实施计划
|
||||
|
||||
## Context(背景与目标)
|
||||
|
||||
用户提出三项需求:
|
||||
1. **数据库重置**:清空所有业务数据,仅保留 admin 账户,重置后保留种子数据(分类/平台/标签/示例项目/轮播图/合作伙伴由 DbSeeder 自动补回)。
|
||||
2. **COS 资源全删**:清空腾讯云 COS 存储桶内所有应用上传的文件(保留 `backups/` 下的数据库备份)。
|
||||
3. **关于我们模块**:
|
||||
- 前台 About 页去掉"联系我们"模块
|
||||
- 后台新增"关于我们"管理模块:控制前台是否显示该页 + 全部内容可编辑(内联所见即所得,编辑样式与前台一致)
|
||||
|
||||
当前 About 页 100% 硬编码([About.vue](file:///d:/TraeProject/MilliSim4/frontend/src/views/frontend/About.vue)),无任何后端存储。本计划从零构建内容存储 + 后台内联编辑 + 前台动态渲染。
|
||||
|
||||
## 关键设计决策(经用户确认 + 架构验证)
|
||||
|
||||
1. **存储方式**:全部 About 内容作为单个 JSON 字符串存入 SystemSettings 表(Key=`AboutContent`),与现有 PlatformName/ShowPartners 等配置一致。避免新建表 + 改 seeder 补丁的复杂度。
|
||||
2. **编辑交互**:使用无边框 `el-input`/`el-textarea`(autosize),通过 hover/focus 样式提示可编辑性。不使用 `contenteditable`(Vue 响应性陷阱:光标跳动)。
|
||||
3. **数组排序**:上下箭头按钮,不引入拖拽库。
|
||||
4. **ShowAbout 控制**:隐藏导航链接 + 路由守卫拦截直接访问(重定向到首页)。
|
||||
5. **重置安全**:要求输入 "RESET" 确认 + 重置前自动创建一次数据库备份 + 操作日志记录。
|
||||
6. **COS 清理范围**:仅删除已知上传前缀(avatars/banners/logos/projects/webgl/packages/common/icons/videos/categories/platforms/partners),保留 `backups/`。
|
||||
7. **AboutContent 不进 PublicSettingsDto**:public-settings 仅加 `showAbout`(小布尔值,供导航/路由用);AboutContent 由独立端点加载,仅 About 页消费。
|
||||
|
||||
## 实施步骤
|
||||
|
||||
### 阶段 A:后端基础设施
|
||||
|
||||
#### A1. FileStorageService 增加批量删除能力
|
||||
**文件**:`d:\TraeProject\MilliSim4\backend\MilliSim.Api\Services\Infrastructure\FileStorageService.cs`
|
||||
|
||||
- 接口 `IFileStorageService` 新增:
|
||||
- `Task<List<string>> ListAllObjectsAsync()` — 分页列出桶内所有对象 key(每页 1000,循环至结束)
|
||||
- `Task<int> DeleteObjectsAsync(IEnumerable<string> keys)` — 批量删除(`DeleteMultiObjectRequest`,每批最多 1000)
|
||||
- 实现:复用私有 `GetStorageConfigAsync()` + `GetCosXml(config)`;Local 模式则遍历 `wwwroot/uploads` 目录删除文件
|
||||
- 不暴露到控制器单独使用,仅供 factory-reset 调用
|
||||
|
||||
#### A2. AboutContent DTO + 端点
|
||||
**文件**:`d:\TraeProject\MilliSim4\backend\MilliSim.Api\Common\Dtos\Dtos.cs`
|
||||
|
||||
新增 `AboutContentDto`:
|
||||
```csharp
|
||||
public class AboutContentDto
|
||||
{
|
||||
public string HeroTag { get; set; } = "关于我们";
|
||||
public string HeroTitle { get; set; } = "";
|
||||
public string HeroDesc { get; set; } = "";
|
||||
public string IntroTitle { get; set; } = "公司简介";
|
||||
public List<string> IntroParagraphs { get; set; } = new();
|
||||
public List<AboutStatDto> Stats { get; set; } = new();
|
||||
public string MissionTitle { get; set; } = "使命与愿景";
|
||||
public List<AboutMissionCardDto> MissionCards { get; set; } = new();
|
||||
public string TimelineTitle { get; set; } = "发展历程";
|
||||
public List<AboutTimelineItemDto> Timeline { get; set; } = new();
|
||||
}
|
||||
public class AboutStatDto { public string Number { get; set; } = ""; public string Label { get; set; } = ""; }
|
||||
public class AboutMissionCardDto { public string Title { get; set; } = ""; public string Icon { get; set; } = "rocket"; public string Desc { get; set; } = ""; }
|
||||
public class AboutTimelineItemDto { public string Year { get; set; } = ""; public string Title { get; set; } = ""; public string Desc { get; set; } = ""; }
|
||||
|
||||
public class AboutSettingsDto
|
||||
{
|
||||
public bool ShowAbout { get; set; } = true;
|
||||
public AboutContentDto Content { get; set; } = new();
|
||||
}
|
||||
```
|
||||
|
||||
`PublicSettingsDto` 仅新增 `public bool ShowAbout { get; set; } = true;`(不加 AboutContent)。
|
||||
|
||||
#### A3. SystemService 关于我们读写
|
||||
**文件**:`d:\TraeProject\MilliSim4\backend\MilliSim.Api\Services\SystemService.cs`
|
||||
|
||||
- `GetAboutSettingsAsync()` → 读 SystemSettings 中 `ShowAbout` + `AboutContent`(JSON 反序列化),返回 `AboutSettingsDto`
|
||||
- `UpdateAboutSettingsAsync(AboutSettingsDto dto)` → 序列化 Content 为 JSON 存入 `AboutContent` key,`ShowAbout` 存 bool 字符串
|
||||
- `GetPublicSettingsAsync()` 中加入 `ShowAbout` 读取
|
||||
|
||||
#### A4. SystemController 端点
|
||||
**文件**:`d:\TraeProject\MilliSim4\backend\MilliSim.Api\Controllers\SystemController.cs`
|
||||
|
||||
- `GET /api/system/about` → `[AllowAnonymous]` 返回 `AboutSettingsDto`(前台 About 页消费)
|
||||
- `PUT /api/system/about` → `[Authorize(Policy="Admin")]` 保存
|
||||
|
||||
#### A5. 工厂重置端点
|
||||
**文件**:`d:\TraeProject\MilliSim4\backend\MilliSim.Api\Services\SystemService.cs` + `SystemController.cs`
|
||||
|
||||
- `POST /api/system/factory-reset` → `[Authorize(Policy="Admin")]`,body 含 `ConfirmText`(必须等于 "RESET")
|
||||
- 逻辑:
|
||||
1. 校验 `ConfirmText == "RESET"`,否则返回 400
|
||||
2. 先调 `CreateBackupAsync()` 创建备份(保留安全网)
|
||||
3. `SET FOREIGN_KEY_CHECKS=0`
|
||||
4. 按依赖顺序 DELETE FROM 所有 21 张表(Users 仅删 `Username != 'admin'`,并清空 admin 的 Avatar/UpdatedAt 字段)
|
||||
5. `SET FOREIGN_KEY_CHECKS=1`
|
||||
6. 调 `DbSeeder.SeedAsync(db)` 重新补种子(admin 已存在跳过;分类/平台/标签/项目/轮播图/合作伙伴/设置按"为空才补"逻辑重新填入;AboutContent/ShowAbout 默认值也会被补入)
|
||||
7. 调 `_fileStorage.ListAllObjectsAsync()` 列出所有对象,过滤掉 `backups/` 前缀,调 `DeleteObjectsAsync()` 批量删除
|
||||
8. 记操作日志
|
||||
9. 返回 `{ TablesCleared, ObjectsDeleted, BackupId }`
|
||||
|
||||
#### A6. DbSeeder 补种子
|
||||
**文件**:`d:\TraeProject\MilliSim4\backend\MilliSim.Api\Data\DbSeeder.cs`
|
||||
|
||||
- 在 `SeedAsync` 的 SystemSettings 补种子段加入:
|
||||
- `ShowAbout` = "true"
|
||||
- `AboutContent` = 默认 JSON(当前 About.vue 的硬编码内容序列化)
|
||||
- 仅在 key 不存在时插入(与现有逻辑一致)
|
||||
|
||||
### 阶段 B:前端 — 关于我们动态化 + 去掉联系模块
|
||||
|
||||
#### B1. 类型 + API + Store
|
||||
**文件**:
|
||||
- `d:\TraeProject\MilliSim4\frontend\src\types\index.ts`:加 `AboutContent` / `AboutStat` / `AboutMissionCard` / `AboutTimelineItem` 接口;`PublicSettings` 加 `showAbout: boolean`
|
||||
- `d:\TraeProject\MilliSim4\frontend\src\api\index.ts`:`systemApi.getAbout()` GET `/system/about`;`systemApi.updateAbout(payload)` PUT;`systemApi.factoryReset(confirmText)` POST
|
||||
- `d:\TraeProject\MilliSim4\frontend\src\stores\system.ts`:`settings` 加 `showAbout`(默认 true);加 `aboutContent` ref + `loadAbout()` 方法(About 页 onMounted 调用)
|
||||
|
||||
#### B2. About.vue 动态化
|
||||
**文件**:`d:\TraeProject\MilliSim4\frontend\src\views\frontend\About.vue`
|
||||
|
||||
- 去掉"联系我们"section(原 L101-130)+ 相关 CTA
|
||||
- script:`onMounted` 调 `systemStore.loadAbout()` 获取内容;用 `computed` 映射 store 中的 aboutContent
|
||||
- template:所有硬编码文本替换为 `{{ about.heroTag }}` 等绑定;timeline 用 `v-for="item in about.timeline"`
|
||||
- 无数据时显示骨架屏/loading
|
||||
|
||||
#### B3. 路由 + 导航守卫
|
||||
**文件**:
|
||||
- `d:\TraeProject\MilliSim4\frontend\src\router\index.ts`:`/about` 路由 `meta.requiresAbout: true`;`beforeEach` 守卫中 `if (to.meta.requiresAbout && !systemStore.settings.showAbout) return '/'`,并先 `await systemStore.ensureLoaded()`
|
||||
- `d:\TraeProject\MilliSim4\frontend\src\layouts\FrontendLayout.vue`:nav 菜单"关于我们"项加 `v-if="systemStore.settings.showAbout"`;footer 链接同理
|
||||
|
||||
### 阶段 C:后台 — 关于我们内联编辑页
|
||||
|
||||
#### C1. 新建 AboutSettings.vue
|
||||
**文件**:`d:\TraeProject\MilliSim4\frontend\src\views\admin\AboutSettings.vue`(新建)
|
||||
|
||||
布局完全复刻前台 About.vue 结构,但所有文字改为可编辑:
|
||||
- 顶部工具栏:`ShowAbout` el-switch + "保存" el-button(loading 状态)
|
||||
- Hero 区:3 个无边框 el-input(tag/title/desc),inherit 前台字体样式
|
||||
- 公司简介:`el-textarea :autosize` × N 段(可增删),4 个统计卡片(number + label 各一输入),每卡右上角 hover 显示删除按钮 + 上下移按钮
|
||||
- 使命与愿景:3 张卡片(title + icon 选择 + desc textarea),增删/排序
|
||||
- 发展历程:时间线项(year + title + desc),增删/排序
|
||||
- 编辑态样式:`.editable:hover { outline: 1px dashed var(--color-primary); }` `.editable:focus { outline: 2px solid var(--color-primary); background: var(--color-primary-light-9); }`
|
||||
- 数组操作按钮:hover 时浮现"上移/下移/删除","添加"按钮在列表末尾
|
||||
- `onMounted` 调 `systemApi.getAbout()` 加载;保存调 `systemApi.updateAbout()`;保存后 `systemStore.load()` 刷新前台状态
|
||||
- 图标选择用 el-select 下拉(预设 SvgIcon 列表:rocket/globe/heart/target/users/star 等)
|
||||
|
||||
#### C2. 路由 + 菜单
|
||||
**文件**:
|
||||
- `d:\TraeProject\MilliSim4\frontend\src\router\index.ts`:加 `/admin/about-settings` 路由(admin auth)
|
||||
- `d:\TraeProject\MilliSim4\frontend\src\layouts\AdminLayout.vue`:侧栏菜单加入"关于我们"项(icon: `info` 或 `building`)
|
||||
|
||||
### 阶段 D:后台 — 工厂重置入口
|
||||
|
||||
#### D1. Settings.vue 危险操作卡片
|
||||
**文件**:`d:\TraeProject\MilliSim4\frontend\src\views\admin\Settings.vue`
|
||||
|
||||
- 在页面末尾加"危险操作"卡片:
|
||||
- 标题 + 警告说明("将清空所有业务数据并删除 COS 资源,仅保留 admin 账户,操作前会自动创建备份")
|
||||
- el-input 要求输入 "RESET" 才能激活按钮
|
||||
- el-button danger "恢复出厂设置"(disabled 直到输入正确)
|
||||
- 二次确认弹窗 `showActionConfirm`
|
||||
- 调 `systemApi.factoryReset(confirmText)`;成功后 `ElMessage.success` + 提示重新登录(admin 密码不变)
|
||||
|
||||
### 阶段 E:编译验证 + 重启
|
||||
|
||||
1. 杀掉后端进程(PID 13788/22680)→ `dotnet build` → 重启后端
|
||||
2. 前端 Vite HMR 自动加载,确认无编译错误
|
||||
3. 浏览器测试场景
|
||||
|
||||
## 验证步骤
|
||||
|
||||
### 后端验证
|
||||
1. `dotnet build` 0 错误 0 警告
|
||||
2. 启动后端,确认 `PendingUploads` + 新增设置 key(AboutContent/ShowAbout)建表/补种子成功(查日志)
|
||||
3. Swagger 确认新端点存在:GET/PUT `/api/system/about`、POST `/api/system/factory-reset`
|
||||
|
||||
### 前端验证
|
||||
1. Vite HMR 无编译错误
|
||||
2. 访问 `/about` 确认内容从后端加载(非硬编码)
|
||||
3. 关闭 ShowAbout 后导航不显示"关于我们",直接访问 `/about` 重定向到首页
|
||||
4. 后台 `/admin/about-settings` 编辑任意文字 → 保存 → 刷新前台确认生效
|
||||
5. 数组增删/排序正常
|
||||
6. 后台"恢复出厂设置"输入 RESET → 执行 → 确认数据库清空 + COS 清空 + admin 可登录 + 种子数据恢复
|
||||
|
||||
## 假设与决策
|
||||
|
||||
1. **不删 admin 账户**:DbSeeder 对 admin 用户按 username 幂等,重置后保留原密码 `admin123`。
|
||||
2. **保留 backups/ 前缀**:工厂重置不删 `backups/` 下的数据库备份文件,作为最后安全网。
|
||||
3. **重置前自动备份**:调用现有 `CreateBackupAsync`,即使重置失败也有回滚依据。
|
||||
4. **AboutContent 默认值**:取当前 About.vue 硬编码内容作为默认 JSON,保证未配置时前台显示不变。
|
||||
5. **不引入拖拽库**:数组排序用上下箭头按钮,与现有 SortOrder 字段约定一致。
|
||||
6. **PublicSettingsDto 不含 AboutContent**:避免每次应用启动加载大 JSON;AboutContent 由 About 页独立加载。
|
||||
7. **factory-reset 同步删 COS**:DB 清空后立即删 COS,保证一致性;COS 删除失败不回滚 DB(DB 已是干净状态,COS 残留可手动清理,记录日志)。
|
||||
536
.trae/documents/about-us-module-finish-plan.md
Normal file
536
.trae/documents/about-us-module-finish-plan.md
Normal file
@ -0,0 +1,536 @@
|
||||
# 关于我们模块 + 工厂重置 — 收尾实施计划
|
||||
|
||||
## 摘要
|
||||
|
||||
本计划承接上一会话已完成的工作,完成剩余的前端改造与新增页面、编译验证、服务重启。整体目标:
|
||||
|
||||
1. 前台"关于我们"页面去掉"联系我们"模块,并改为从后端动态加载内容(已基本完成)
|
||||
2. 后台新增"关于我们"管理模块:控制前台是否显示该页 + 全部内容**内联所见即所得**编辑
|
||||
3. 在系统设置页新增"危险操作"卡片,提供恢复出厂设置入口(清库 + 清 COS)
|
||||
|
||||
## 当前状态分析
|
||||
|
||||
### 已完成(上一会话)
|
||||
|
||||
**后端(编译通过,0 错误):**
|
||||
|
||||
* [FileStorageService.cs](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Services/Infrastructure/FileStorageService.cs) — 新增 `ListAllObjectsAsync()` 与 `DeleteObjectsAsync(IEnumerable<string>)`,支持 COS 分页列出 + 批量删除(每批 1000),Local 模式回退到 Directory.GetFiles
|
||||
|
||||
* [Dtos.cs](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Common/Dtos/Dtos.cs) — `PublicSettingsDto` 加 `ShowAbout`;新增 `AboutStatDto`/`AboutMissionCardDto`/`AboutTimelineItemDto`/`AboutContentDto`/`AboutSettingsDto`/`FactoryResetRequest`/`FactoryResetResultDto`
|
||||
|
||||
* [SystemService.cs](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Services/SystemService.cs) — `GetPublicSettingsAsync` 加 `ShowAbout`;新增 `GetAboutSettingsAsync`、`UpdateAboutSettingsAsync(AboutSettingsDto)`、`FactoryResetAsync(FactoryResetRequest)`
|
||||
|
||||
* [ISystemService.cs](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Services/ISystemService.cs) — 新增 3 个方法签名
|
||||
|
||||
* [SystemController.cs](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Controllers/SystemController.cs) — 新增 `GET /api/system/about`(AllowAnonymous)、`PUT /api/system/about`(Admin)、`POST /api/system/factory-reset`(Admin)
|
||||
|
||||
* [DbSeeder.cs](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Data/DbSeeder.cs) — allSettings 新增 `ShowAbout=true` 与 `AboutContent=JSON(默认内容)`;工厂重置后重新种子可恢复默认数据
|
||||
|
||||
**前端(部分完成):**
|
||||
|
||||
* [types/index.ts](file:///d:/TraeProject/MilliSim4/frontend/src/types/index.ts) — `PublicSettings` 加 `showAbout`;新增 `AboutStat`/`AboutMissionCard`/`AboutTimelineItem`/`AboutContent`/`AboutSettings`/`FactoryResetResult` 接口
|
||||
|
||||
* [api/index.ts](file:///d:/TraeProject/MilliSim4/frontend/src/api/index.ts) — systemApi 加 `getAbout()`、`updateAbout(data)`、`factoryReset(confirmText)`
|
||||
|
||||
* [stores/system.ts](file:///d:/TraeProject/MilliSim4/frontend/src/stores/system.ts) — 加 `aboutContent` ref、`aboutLoaded` ref、`loadAbout()` 方法
|
||||
|
||||
* [About.vue](file:///d:/TraeProject/MilliSim4/frontend/src/views/frontend/About.vue) — 已重写,去掉"联系我们"模块,所有内容从 `systemStore.aboutContent` 加载,骨架屏 loading
|
||||
|
||||
### 待完成
|
||||
|
||||
* **B 收尾**:[router/index.ts](file:///d:/TraeProject/MilliSim4/frontend/src/router/index.ts) 与 [FrontendLayout.vue](file:///d:/TraeProject/MilliSim4/frontend/src/layouts/FrontendLayout.vue) 改造未完成
|
||||
|
||||
* **C**:[AboutSettings.vue](file:///d:/TraeProject/MilliSim4/frontend/src/views/admin/AboutSettings.vue) 与配套 CSS 文件尚未创建
|
||||
|
||||
* **D**:[Settings.vue](file:///d:/TraeProject/MilliSim4/frontend/src/views/admin/Settings.vue) 危险操作卡片未添加
|
||||
|
||||
* **E**:编译验证 + 重启前后端
|
||||
|
||||
## 关键决策(用户已确认)
|
||||
|
||||
1. **数据库重置范围**:保留种子数据(DbSeeder.SeedAsync 重新补回分类/平台/标签/示例项目/轮播图/合作伙伴;admin 账户保留,密码 admin123)
|
||||
2. **编辑交互方式**:内联所见即所得(非表单式),编辑样式与前台 About 页一致,每个内容可直接编辑
|
||||
|
||||
## 提议变更
|
||||
|
||||
### 阶段 B 收尾 — 路由 + 前台导航
|
||||
|
||||
#### 文件 1:[frontend/src/router/index.ts](file:///d:/TraeProject/MilliSim4/frontend/src/router/index.ts)
|
||||
|
||||
**变更 1(L28)**:About 路由 meta 增加 `requiresAbout: true`:
|
||||
|
||||
```ts
|
||||
{ path: 'about', name: 'About', component: () => import('@/views/frontend/About.vue'), meta: { title: '关于我们', requiresAbout: true } },
|
||||
```
|
||||
|
||||
**变更 2(L84 后,notifications 路由前)**:admin children 增加"关于我们"管理路由:
|
||||
|
||||
```ts
|
||||
{ path: 'system/about', name: 'AdminAboutSettings', component: () => import('@/views/admin/AboutSettings.vue'), meta: { title: '关于我们', admin: true } },
|
||||
```
|
||||
|
||||
**变更 3(L103-139 beforeEach 守卫)**:守卫改为 async,并在权限校验前加 `showAbout` 检查。完整守卫:
|
||||
|
||||
```ts
|
||||
router.beforeEach(async (to, _from, next) => {
|
||||
document.title = `${to.meta.title || ''} - 毫秒数仿`
|
||||
const userStore = useUserStore()
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
// 确保 systemStore 已加载(用于 showAbout 判断)
|
||||
await systemStore.ensureLoaded()
|
||||
|
||||
// 校验关于我们页面是否对外显示
|
||||
if (to.meta.requiresAbout && !systemStore.settings.showAbout) {
|
||||
next('/')
|
||||
return
|
||||
}
|
||||
|
||||
// 校验登录状态
|
||||
if (to.meta.auth && !userStore.isLoggedIn) {
|
||||
next(to.path.startsWith('/admin') ? '/admin/login' : '/login')
|
||||
return
|
||||
}
|
||||
// ... 其余 staff/admin/pm/sales 校验保持不变
|
||||
next()
|
||||
})
|
||||
```
|
||||
|
||||
> 注意:原守卫是同步函数,需改为 `async`,因为 `ensureLoaded()` 返回 Promise。系统已在 main.ts 启动时调 `systemStore.load()`,此处 `ensureLoaded` 通常是 no-op,不会引入明显延迟。
|
||||
|
||||
#### 文件 2:[frontend/src/layouts/FrontendLayout.vue](file:///d:/TraeProject/MilliSim4/frontend/src/layouts/FrontendLayout.vue)
|
||||
|
||||
**变更 1(L192-198 menuItems)**:改为 computed,让"关于我们"项根据 `systemStore.settings.showAbout` 过滤:
|
||||
|
||||
```ts
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
// ...
|
||||
const allMenuItems = [
|
||||
{ path: '/', label: '首页', icon: 'home' },
|
||||
{ path: '/projects', label: '项目中心', icon: 'grid' },
|
||||
{ path: '/tech', label: '技术能力', icon: 'zap' },
|
||||
{ path: '/about', label: '关于我们', icon: 'building' },
|
||||
{ path: '/contact', label: '联系咨询', icon: 'message' },
|
||||
]
|
||||
const menuItems = computed(() =>
|
||||
systemStore.settings.showAbout
|
||||
? allMenuItems
|
||||
: allMenuItems.filter(i => i.path !== '/about')
|
||||
)
|
||||
```
|
||||
|
||||
**变更 2(L132-138 footer 关于我们 列)**:footer 中"公司介绍"链接加 `v-if="systemStore.settings.showAbout"`:
|
||||
|
||||
```html
|
||||
<li v-if="systemStore.settings.showAbout"><router-link to="/about">公司介绍</router-link></li>
|
||||
```
|
||||
|
||||
### 阶段 C — 后台内联编辑页 AboutSettings.vue
|
||||
|
||||
#### 文件 3:[frontend/src/views/admin/AboutSettings.vue](file:///d:/TraeProject/MilliSim4/frontend/src/views/admin/AboutSettings.vue)(新建)
|
||||
|
||||
**目标**:内联所见即所得编辑器,编辑样式尽量与前台 About 一致,每个文本内容可直接点击编辑。
|
||||
|
||||
**结构**:
|
||||
|
||||
* 顶部页头卡片(与其他 admin 页一致),含"显示开关"和"保存"按钮
|
||||
|
||||
* 内容区按 Hero / 公司简介 / 使命愿景 / 发展历程分块,每块都是可编辑卡片
|
||||
|
||||
* 列表型数据(introParagraphs / stats / missionCards / timeline)支持增删和上下移动
|
||||
|
||||
* 使用 `el-input`(无边框 + autosize)实现内联编辑,hover/focus 时显示虚线边框提示可编辑
|
||||
|
||||
**关键交互**:
|
||||
|
||||
* `showAbout` 开关:el-switch,change 时立即调 `updateAbout` 保存(不依赖主保存按钮)
|
||||
|
||||
* 主"保存"按钮:保存全部内容到 `AboutContent`
|
||||
|
||||
* 列表增删:每项右侧悬浮显示"+ 添加 / 删除 / 上移 / 下移"图标按钮
|
||||
|
||||
* 保存后调 `systemStore.loadAbout()` 刷新 store,使前台立即生效
|
||||
|
||||
**核心代码骨架**:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="admin-page about-settings-page">
|
||||
<!-- 页头 -->
|
||||
<div class="page-header-card">
|
||||
<div class="page-title-area">
|
||||
<div class="page-title-icon"><SvgIcon name="building" :size="20" color="#fff" /></div>
|
||||
<div class="page-title-text">
|
||||
<h2>关于我们管理</h2>
|
||||
<p>控制前台"关于我们"页面是否显示,并编辑全部内容(所见即所得)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-header-actions">
|
||||
<el-switch v-model="form.showAbout" active-text="前台显示" @change="onToggleShowAbout" />
|
||||
<el-button type="primary" :loading="saving" @click="saveAll">
|
||||
<SvgIcon name="save" :size="14" color="#fff" /><span>保存全部内容</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-skeleton v-if="loading" :rows="12" animated />
|
||||
|
||||
<template v-else>
|
||||
<!-- Hero 块 -->
|
||||
<div class="editor-card hero-editor">
|
||||
<div class="editor-section-title">主视觉 Hero</div>
|
||||
<div class="hero-preview">
|
||||
<el-input v-model="form.content.heroTag" class="inline-input hero-tag-input" placeholder="标签" />
|
||||
<el-input v-model="form.content.heroTitle" class="inline-input hero-title-input" placeholder="主标题" />
|
||||
<el-input v-model="form.content.heroDesc" type="textarea" :autosize="{ minRows: 2 }" class="inline-input hero-desc-input" placeholder="副标题描述" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 公司简介 -->
|
||||
<div class="editor-card">
|
||||
<div class="editor-section-title">
|
||||
<span>公司简介</span>
|
||||
<el-button text @click="addIntroParagraph"><SvgIcon name="plus" :size="14" /> 段落</el-button>
|
||||
</div>
|
||||
<el-input v-model="form.content.introTitle" class="inline-input block-title-input" placeholder="板块标题" />
|
||||
<div v-for="(p, i) in form.content.introParagraphs" :key="i" class="list-item">
|
||||
<el-input v-model="form.content.introParagraphs[i]" type="textarea" :autosize="{ minRows: 2 }" class="inline-input" />
|
||||
<div class="list-actions">
|
||||
<button @click="moveItem(form.content.introParagraphs, i, -1)" :disabled="i === 0"><SvgIcon name="arrowUp" :size="14" /></button>
|
||||
<button @click="moveItem(form.content.introParagraphs, i, 1)" :disabled="i === form.content.introParagraphs.length - 1"><SvgIcon name="arrowDown" :size="14" /></button>
|
||||
<button @click="form.content.introParagraphs.splice(i, 1)"><SvgIcon name="trash" :size="14" /></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-section-subtitle">数据统计</div>
|
||||
<div v-for="(s, i) in form.content.stats" :key="i" class="stat-row list-item">
|
||||
<el-input v-model="form.content.stats[i].number" placeholder="数字" />
|
||||
<el-input v-model="form.content.stats[i].label" placeholder="标签" />
|
||||
<div class="list-actions">...</div>
|
||||
</div>
|
||||
<el-button text @click="form.content.stats.push({ number: '', label: '' })"><SvgIcon name="plus" :size="14" /> 统计项</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 使命愿景 -->
|
||||
<div class="editor-card">
|
||||
<div class="editor-section-title">
|
||||
<span>使命与愿景</span>
|
||||
<el-button text @click="form.content.missionCards.push({ title: '', icon: 'rocket', desc: '' })"><SvgIcon name="plus" :size="14" /> 卡片</el-button>
|
||||
</div>
|
||||
<el-input v-model="form.content.missionTitle" class="inline-input block-title-input" placeholder="板块标题" />
|
||||
<div v-for="(c, i) in form.content.missionCards" :key="i" class="mission-card list-item">
|
||||
<div class="mission-card-head">
|
||||
<el-input v-model="form.content.missionCards[i].title" placeholder="卡片标题" />
|
||||
<el-input v-model="form.content.missionCards[i].icon" placeholder="图标名 (如 rocket)" />
|
||||
<div class="list-actions">...</div>
|
||||
</div>
|
||||
<el-input v-model="form.content.missionCards[i].desc" type="textarea" :autosize="{ minRows: 2 }" placeholder="描述" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 发展历程 -->
|
||||
<div class="editor-card">
|
||||
<div class="editor-section-title">
|
||||
<span>发展历程</span>
|
||||
<el-button text @click="form.content.timeline.push({ year: '', title: '', desc: '' })"><SvgIcon name="plus" :size="14" /> 时间线项</el-button>
|
||||
</div>
|
||||
<el-input v-model="form.content.timelineTitle" class="inline-input block-title-input" placeholder="板块标题" />
|
||||
<div v-for="(t, i) in form.content.timeline" :key="i" class="timeline-row list-item">
|
||||
<el-input v-model="form.content.timeline[i].year" placeholder="年份" />
|
||||
<el-input v-model="form.content.timeline[i].title" placeholder="标题" />
|
||||
<el-input v-model="form.content.timeline[i].desc" type="textarea" :autosize="{ minRows: 1 }" placeholder="描述" />
|
||||
<div class="list-actions">...</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { systemApi } from '@/api'
|
||||
import { useSystemStore } from '@/stores/system'
|
||||
import SvgIcon from '@/components/SvgIcon.vue'
|
||||
import { showActionConfirm } from '@/utils/confirm'
|
||||
import type { AboutSettings, AboutContent } from '@/types'
|
||||
|
||||
const systemStore = useSystemStore()
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
|
||||
const form = reactive<AboutSettings>({
|
||||
showAbout: true,
|
||||
content: { heroTag: '', heroTitle: '', heroDesc: '', introTitle: '', introParagraphs: [], stats: [], missionTitle: '', missionCards: [], timelineTitle: '', timeline: [] },
|
||||
})
|
||||
|
||||
async function fetchAbout() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await systemApi.getAbout()
|
||||
if (res.data) {
|
||||
form.showAbout = res.data.showAbout !== false
|
||||
form.content = res.data.content || form.content
|
||||
}
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function moveItem<T>(arr: T[], i: number, dir: number) {
|
||||
const j = i + dir
|
||||
if (j < 0 || j >= arr.length) return
|
||||
;[arr[i], arr[j]] = [arr[j], arr[i]]
|
||||
}
|
||||
|
||||
async function onToggleShowAbout(val: boolean) {
|
||||
// 开关独立保存,不影响未保存的内容编辑
|
||||
try {
|
||||
await systemApi.updateAbout({ showAbout: val, content: form.content })
|
||||
await systemStore.loadAbout()
|
||||
ElMessage.success(val ? '已开启前台显示' : '已关闭前台显示')
|
||||
} catch { form.showAbout = !val }
|
||||
}
|
||||
|
||||
async function saveAll() {
|
||||
if (saving.value) return
|
||||
saving.value = true
|
||||
try {
|
||||
await systemApi.updateAbout({ showAbout: form.showAbout, content: form.content })
|
||||
await systemStore.loadAbout()
|
||||
ElMessage.success('保存成功')
|
||||
} finally { saving.value = false }
|
||||
}
|
||||
|
||||
function addIntroParagraph() {
|
||||
form.content.introParagraphs.push('')
|
||||
}
|
||||
|
||||
onMounted(fetchAbout)
|
||||
</script>
|
||||
|
||||
<style scoped src="./AboutSettings.css"></style>
|
||||
```
|
||||
|
||||
#### 文件 4:[frontend/src/views/admin/AboutSettings.css](file:///d:/TraeProject/MilliSim4/frontend/src/views/admin/AboutSettings.css)(新建)
|
||||
|
||||
**目标**:内联编辑样式 — 让 `el-input` 看起来像前台 About 页的文字,但 hover/focus 时显示虚线边框提示可编辑。
|
||||
|
||||
**关键样式**:
|
||||
|
||||
```css
|
||||
/* 内联输入:默认无边框、透明背景,模拟前台文本外观 */
|
||||
.inline-input :deep(.el-input__wrapper),
|
||||
.inline-input :deep(.el-textarea__inner) {
|
||||
background: transparent;
|
||||
box-shadow: none !important;
|
||||
border: 1px dashed transparent;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: border-color .2s;
|
||||
}
|
||||
.inline-input :deep(.el-input__wrapper):hover,
|
||||
.inline-input :deep(.el-textarea__inner):hover {
|
||||
border-color: var(--color-primary-light);
|
||||
}
|
||||
.inline-input :deep(.el-input__wrapper).is-focus,
|
||||
.inline-input :deep(.el-textarea__inner):focus {
|
||||
border-color: var(--color-primary);
|
||||
background: rgba(22, 119, 255, 0.04);
|
||||
}
|
||||
|
||||
/* Hero 区编辑样式(白字) */
|
||||
.hero-preview {
|
||||
background: var(--gradient-hero);
|
||||
padding: 40px 24px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.hero-preview .hero-tag-input :deep(.el-input__inner),
|
||||
.hero-preview .hero-title-input :deep(.el-input__inner),
|
||||
.hero-preview .hero-desc-input :deep(.el-textarea__inner) {
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
.hero-title-input :deep(.el-input__inner) { font-size: 32px; font-weight: 700; }
|
||||
|
||||
/* 列表项容器 */
|
||||
.list-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.list-item > .el-input,
|
||||
.list-item > .el-textarea { flex: 1; }
|
||||
.list-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
.list-actions button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-secondary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.list-actions button:hover:not(:disabled) {
|
||||
background: var(--color-bg-elevated);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.list-actions button:disabled { opacity: .3; cursor: not-allowed; }
|
||||
```
|
||||
|
||||
### 阶段 D — Settings.vue 危险操作卡片
|
||||
|
||||
#### 文件 5:[frontend/src/views/admin/Settings.vue](file:///d:/TraeProject/MilliSim4/frontend/src/views/admin/Settings.vue)
|
||||
|
||||
在"上传限制"卡片之后追加"危险操作"卡片:
|
||||
|
||||
```html
|
||||
<!-- ===== 6. 危险操作卡片 ===== -->
|
||||
<div class="form-card danger-card">
|
||||
<div class="form-section-title danger-title">
|
||||
<SvgIcon name="alert" :size="16" color="#E5484D" />
|
||||
<span>危险操作</span>
|
||||
</div>
|
||||
<div class="danger-body">
|
||||
<div class="danger-item">
|
||||
<div class="danger-info">
|
||||
<div class="danger-name">恢复出厂设置</div>
|
||||
<div class="danger-desc">
|
||||
清空所有业务数据(仅保留 admin 账户)并删除 COS 全部资源。系统会先自动创建数据备份,再重新写入种子数据(分类/平台/标签/示例项目/轮播图/合作伙伴)。
|
||||
</div>
|
||||
</div>
|
||||
<el-button type="danger" @click="openFactoryReset">
|
||||
<SvgIcon name="trash" :size="14" color="#fff" />
|
||||
<span>恢复出厂设置</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 恢复出厂设置确认弹窗 -->
|
||||
<el-dialog v-model="resetDialogVisible" title="恢复出厂设置确认" width="480px" :close-on-click-modal="false">
|
||||
<el-alert type="error" :closable="false" show-icon title="此操作不可逆" description="将清空所有业务数据并删除 COS 全部资源。系统会先自动创建数据备份。" style="margin-bottom: 16px" />
|
||||
<p style="margin-bottom: 8px">请输入 <code>RESET</code> 确认:</p>
|
||||
<el-input v-model="resetConfirmText" placeholder="请输入 RESET" />
|
||||
<template #footer>
|
||||
<el-button @click="resetDialogVisible = false">取消</el-button>
|
||||
<el-button type="danger" :loading="resetting" :disabled="resetConfirmText !== 'RESET'" @click="confirmFactoryReset">
|
||||
确认重置
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
```
|
||||
|
||||
**script 增加**:
|
||||
|
||||
```ts
|
||||
import { systemApi } from '@/api'
|
||||
// ...
|
||||
const resetDialogVisible = ref(false)
|
||||
const resetConfirmText = ref('')
|
||||
const resetting = ref(false)
|
||||
|
||||
function openFactoryReset() {
|
||||
resetConfirmText.value = ''
|
||||
resetDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function confirmFactoryReset() {
|
||||
if (resetting.value) return
|
||||
resetting.value = true
|
||||
try {
|
||||
const res = await systemApi.factoryReset(resetConfirmText.value)
|
||||
if (res.data) {
|
||||
ElMessage.success(`重置成功:已清空 ${res.data.deletedTables} 张表,删除 ${res.data.deletedCosObjects} 个 COS 对象`)
|
||||
resetDialogVisible.value = false
|
||||
// 刷新 systemStore(admin 头像被清空、Logo 等可能变化)
|
||||
await systemStore.load()
|
||||
await systemStore.loadAbout()
|
||||
}
|
||||
} catch {
|
||||
// 错误已由拦截器处理
|
||||
} finally {
|
||||
resetting.value = false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**关于 FactoryResetResultDto 字段**:后端 DTO 已定义 `DeletedTables`、`DeletedCosObjects`、`BackupId` 等字段(与前端 `FactoryResetResult` 接口对应)。
|
||||
|
||||
#### 文件 6:[frontend/src/views/admin/Settings.css](file:///d:/TraeProject/MilliSim4/frontend/src/views/admin/Settings.css)(追加样式)
|
||||
|
||||
追加:
|
||||
|
||||
```css
|
||||
.danger-card { border: 1px solid #E5484D; }
|
||||
.danger-title { color: #E5484D; }
|
||||
.danger-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.danger-info { flex: 1; }
|
||||
.danger-name { font-weight: 600; color: var(--color-text-primary); margin-bottom: 4px; }
|
||||
.danger-desc { font-size: 13px; color: var(--color-text-secondary); line-height: 1.6; }
|
||||
```
|
||||
|
||||
### 阶段 E — 编译验证 + 重启
|
||||
|
||||
1. **前端 HMR 验证**:保存所有改动后,Vite dev server(5173 端口)应自动 HMR;检查控制台无 TypeScript 错误
|
||||
2. **后端编译验证**:
|
||||
|
||||
* `taskkill /F /PID <pid>` 杀掉运行中的 MilliSim.Api.exe(如 PID 13788/22680,但需 `tasklist | findstr MilliSim` 确认当前 PID)
|
||||
|
||||
* `cd d:\TraeProject\MilliSim4\backend\MilliSim.Api && dotnet build`(预期 0 错误,仅有 2 个预存警告)
|
||||
|
||||
* 后台启动 `dotnet run --project d:\TraeProject\MilliSim4\backend\MilliSim.Api\MilliSim.Api.csproj`(run\_in\_background=true)
|
||||
3. **功能验证清单**(不强制要求用户操作,但建议):
|
||||
|
||||
* 访问 `/admin/system/about` → 加载默认内容 → 修改 Hero 标题 → 保存 → 访问前台 `/about` 看到新内容
|
||||
|
||||
* 切换"前台显示"开关为关闭 → 前台导航不再显示"关于我们" → 直接访问 `/about` 重定向到首页
|
||||
|
||||
* 在 Settings 页点"恢复出厂设置" → 输入 RESET → 确认 → 看到"重置成功"提示 → 数据库业务表清空,admin 仍可登录
|
||||
|
||||
## 假设与决策
|
||||
|
||||
1. **AboutSettings.vue 列表交互**:使用数组 `splice`/`push` 直接操作(reactive 深层响应),不引入额外的拖拽库;上下移动用图标按钮
|
||||
2. **图标名输入**:missionCards 的 `icon` 字段直接用文本输入框(让管理员填 `rocket`/`globe`/`heart` 等 SvgIcon 名称),不引入图标选择器,保持简单
|
||||
3. **保存策略**:`showAbout` 开关 change 即时保存(独立于内容);主"保存全部内容"按钮保存 `AboutContent` JSON。这样切换开关不会丢失未保存的内容编辑(因为 updateAbout 同时发送 showAbout + content)
|
||||
4. **路由守卫 async 化**:原 `beforeEach` 是同步,改为 async 会让首次路由跳转等待一次 `systemStore.load()`(main.ts 已先调过,通常是 no-op)
|
||||
5. **不修改 main.ts**:保持 main.ts 已有的 `systemStore.load()` 调用,不在启动时调 `loadAbout()`(About.vue 自己 onMounted 调即可,避免首次启动多一次请求)
|
||||
6. **危险操作不放在独立路由**:直接在 Settings.vue 末尾加卡片,符合"系统设置集中"的产品语义;不新建 `DangerZone.vue` 页
|
||||
7. **FactoryResetResult 字段命名**:后端 DTO 已使用 PascalCase(`DeletedTables`、`DeletedCosObjects`),前端 ApiResponse 拦截器已统一转 camelCase(`deletedTables`、`deletedCosObjects`),按前端命名规范使用
|
||||
|
||||
## 验证步骤
|
||||
|
||||
实施完成后:
|
||||
|
||||
1. 前端 Vite 控制台无 TS 报错,`/admin/system/about` 路由可访问
|
||||
2. 后端 `dotnet build` 输出 `Build succeeded, 0 Error`
|
||||
3. 访问后台"系统设置"侧边栏看到"关于我们"子菜单
|
||||
4. AboutSettings.vue 加载默认内容、修改保存、开关切换均生效
|
||||
5. 前台 `/about` 显示修改后的内容;关闭开关后导航隐藏、直接访问重定向到首页
|
||||
6. Settings.vue 危险操作卡片显示,输入 RESET 后调用工厂重置成功
|
||||
|
||||
## 任务清单(执行顺序)
|
||||
|
||||
* [ ] B1: 修改 router/index.ts(加 requiresAbout meta + admin 路由 + 守卫 showAbout 检查)
|
||||
|
||||
* [ ] B2: 修改 FrontendLayout.vue(menuItems 改 computed + footer 链接加 v-if)
|
||||
|
||||
* [ ] C1: 创建 AboutSettings.vue(内联所见即所得编辑器)
|
||||
|
||||
* [ ] C2: 创建 AboutSettings.css(内联编辑样式)
|
||||
|
||||
* [ ] D1: Settings.vue 追加危险操作卡片 + 工厂重置弹窗 + script 逻辑
|
||||
|
||||
* [ ] D2: Settings.css 追加 danger-card 样式
|
||||
|
||||
* [ ] E1: 杀掉后端进程 + dotnet build 验证
|
||||
|
||||
* [ ] E2: 重启后端 + 前端 HMR 验证
|
||||
|
||||
447
.trae/documents/authorization-system-refactor.md
Normal file
447
.trae/documents/authorization-system-refactor.md
Normal file
@ -0,0 +1,447 @@
|
||||
# 授权系统全面重构计划
|
||||
|
||||
## 摘要
|
||||
|
||||
本次重构解决用户反馈的 5 个授权相关问题:
|
||||
1. **续期 bug** — 一直提示"续期天数必须大于 0"
|
||||
2. **授权卡片重新设计** — 要有设计感,状态标签中文表示
|
||||
3. **批量授权窗口重新设计**
|
||||
4. **客户管理窗口重新设计**
|
||||
5. **授权逻辑重大问题** — 一个客户只能有一个授权记录(不论有效/已过期/已取消),且授权卡片可操作,客户卡片显示当前授权状态
|
||||
|
||||
## 当前状态分析
|
||||
|
||||
### Bug 根因(续期失败)
|
||||
|
||||
**后端** [`backend/MilliSim.Api/Controllers/AuthorizationsController.cs#L67-L73`](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Controllers/AuthorizationsController.cs#L67-L73):
|
||||
```csharp
|
||||
[HttpPost("{id:long}/renew")]
|
||||
public async Task<IActionResult> Renew(long id, [FromQuery] int days)
|
||||
```
|
||||
- 控制器使用 `[FromQuery] int days` 期望 query string
|
||||
|
||||
**前端** `frontend/src/api/index.ts`:
|
||||
```typescript
|
||||
renew: (id: number, days: number) => http.post(`/authorizations/${id}/renew`, { days })
|
||||
```
|
||||
- 前端发送 POST body `{ days: 30 }`
|
||||
|
||||
→ 后端 days 绑定为 0,触发 `if (days <= 0) return Fail("续期天数必须大于 0")`。
|
||||
|
||||
### 授权逻辑缺陷
|
||||
|
||||
**后端** [`backend/MilliSim.Api/Services/AuthorizationService.cs#L155-L157`](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Services/AuthorizationService.cs#L155-L157):
|
||||
```csharp
|
||||
var existing = await _db.Authorizations
|
||||
.FirstOrDefaultAsync(a => a.UserId == request.UserId
|
||||
&& a.Status == AuthorizationStatus.Active); // 只查 Active
|
||||
```
|
||||
- 现有授权检查只查 Active 状态,对 Cancelled/Expired 状态会创建新记录
|
||||
- 这导致一个客户可能有多条授权记录,违反"一个客户一个授权"的约束
|
||||
|
||||
### 关键代码位置
|
||||
|
||||
- `AuthorizationStatus` 枚举:`ApiResponse.cs` — Cancelled=0, Active=1, Expired=2(数字序列化)
|
||||
- `UserDto`:`Dtos.cs#L32-L46` — 不含授权信息字段
|
||||
- `UserService.GetUsersAsync`:`UserService.cs#L29-L90` — 不联表 Authorizations
|
||||
- 前端 `Authorizations.vue`(418 行):卡片含头像+名称+时间+授权人+状态徽章+续期/取消按钮
|
||||
- 前端 `Customers.vue`(539 行):卡片含头像+用户状态徽章+用户名/手机/邮箱/注册时间+操作按钮,**不显示授权状态**
|
||||
|
||||
## 提议的变更
|
||||
|
||||
### 后端修改
|
||||
|
||||
#### 1. 修复续期 bug — 改用 [FromBody] + DTO
|
||||
|
||||
**文件**: [`backend/MilliSim.Api/Controllers/AuthorizationsController.cs`](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Controllers/AuthorizationsController.cs)
|
||||
|
||||
修改续期端点(L67-L73):
|
||||
```csharp
|
||||
[HttpPost("{id:long}/renew")]
|
||||
[Authorize(Policy = "Staff")]
|
||||
public async Task<IActionResult> Renew(long id, [FromBody] RenewAuthorizationRequest request)
|
||||
{
|
||||
var result = await _authorizationService.RenewAuthorizationAsync(id, request.Days);
|
||||
return Ok(result);
|
||||
}
|
||||
```
|
||||
|
||||
**文件**: [`backend/MilliSim.Api/Common/Dtos/Dtos.cs`](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Common/Dtos/Dtos.cs)
|
||||
|
||||
在 `BatchAuthorizationRequest` 之后(约 L276 位置)添加新 DTO:
|
||||
```csharp
|
||||
public class RenewAuthorizationRequest
|
||||
{
|
||||
public int Days { get; set; } = 30;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 重构授权逻辑 — 一个客户一个授权记录
|
||||
|
||||
**文件**: [`backend/MilliSim.Api/Services/AuthorizationService.cs`](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Services/AuthorizationService.cs)
|
||||
|
||||
**关键变更**:将"检查 Active 才续期"改为"任意状态则复用原记录"。
|
||||
|
||||
`CreateAuthorizationAsync`(L132-L225)重构逻辑:
|
||||
```csharp
|
||||
// 查询客户任意状态的现有授权(一个客户只能有一个授权记录)
|
||||
var existing = await _db.Authorizations
|
||||
.FirstOrDefaultAsync(a => a.UserId == request.UserId);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
// 复用原记录:重置 StartTime/EndTime/Status/AuthorizedBy
|
||||
var now = DateTime.Now;
|
||||
existing.StartTime = now;
|
||||
existing.EndTime = now.AddDays(request.Days);
|
||||
existing.Status = AuthorizationStatus.Active;
|
||||
existing.AuthorizedBy = _currentUser.UserId;
|
||||
existing.ProjectId = null; // 客户级授权
|
||||
existing.UpdatedAt = now;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// 操作日志:标记"重新授权"或"开通"(根据原状态)
|
||||
var actionLabel = existing.Status == AuthorizationStatus.Cancelled ? "重新激活" : "重新授权";
|
||||
// ... 通知逻辑(区分续期/重新授权)
|
||||
|
||||
return ApiResponse<AuthorizationDto>.Success(MapToDto(...), "授权成功");
|
||||
}
|
||||
// 否则新建(同原逻辑)
|
||||
```
|
||||
|
||||
`BatchAuthorizeAsync`(L230-L333)同样改造:对每个 UserId 调用相同复用/新建逻辑。
|
||||
|
||||
`CancelAuthorizationAsync`(L400-L441)添加状态检查:
|
||||
```csharp
|
||||
if (entity.Status == AuthorizationStatus.Cancelled)
|
||||
{
|
||||
return ApiResponse.Fail("该授权已取消,无需重复操作");
|
||||
}
|
||||
if (entity.Status == AuthorizationStatus.Expired)
|
||||
{
|
||||
return ApiResponse.Fail("该授权已过期,请使用重新授权功能");
|
||||
}
|
||||
// 仅 Active 可取消
|
||||
```
|
||||
|
||||
`RenewAuthorizationAsync`(L338-L395)添加状态检查(仅 Active 可续期):
|
||||
```csharp
|
||||
if (entity.Status != AuthorizationStatus.Active)
|
||||
{
|
||||
return ApiResponse<AuthorizationDto>.Fail(
|
||||
entity.Status == AuthorizationStatus.Cancelled ? "已取消的授权请使用重新授权功能"
|
||||
: "已过期的授权请使用重新授权功能");
|
||||
}
|
||||
// 续期:EndTime += days
|
||||
```
|
||||
|
||||
**关键决策**:取消 `CreateAuthorizationAsync` 内部"已有 Active 就续期"的特殊分支 — 改为统一逻辑:客户有任何状态的授权都复用原记录(重置 StartTime + EndTime + Status=Active)。这样从 Customers.vue 点击"为客户授权"或"重新授权"都调用同一接口,后端自动处理复用。
|
||||
|
||||
#### 3. 客户列表返回授权信息
|
||||
|
||||
**文件**: [`backend/MilliSim.Api/Common/Dtos/Dtos.cs`](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Common/Dtos/Dtos.cs)
|
||||
|
||||
修改 `UserDto`(L32-L46)添加两个字段:
|
||||
```csharp
|
||||
public AuthorizationStatus? AuthorizationStatus { get; set; }
|
||||
public DateTime? AuthorizationEndTime { get; set; }
|
||||
```
|
||||
|
||||
**文件**: [`backend/MilliSim.Api/Services/UserService.cs`](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Services/UserService.cs)
|
||||
|
||||
修改 `GetUsersAsync`(L29-L90),在取出 `users` 列表后联表查询 Authorizations:
|
||||
|
||||
```csharp
|
||||
// 一个客户只有一个授权记录,按 UserId 一次性取出
|
||||
var userIds = users.Select(u => u.Id).ToList();
|
||||
var authDict = await _db.Authorizations
|
||||
.Where(a => userIds.Contains(a.UserId))
|
||||
.ToDictionaryAsync(a => a.UserId, a => a); // 一个客户一条记录
|
||||
|
||||
var items = users.Select(u =>
|
||||
{
|
||||
var dto = AuthService.MapToDto(u);
|
||||
if (u.CreatedBy.HasValue && creators.TryGetValue(u.CreatedBy.Value, out var name))
|
||||
{
|
||||
dto.CreatedByName = name;
|
||||
}
|
||||
if (authDict.TryGetValue(u.Id, out var auth))
|
||||
{
|
||||
dto.AuthorizationStatus = auth.Status;
|
||||
dto.AuthorizationEndTime = auth.EndTime;
|
||||
}
|
||||
return dto;
|
||||
}).ToList();
|
||||
```
|
||||
|
||||
> 注意:`MarkExpiredAsync` 不在 UserService 调用,授权状态可能有"过期未刷新"的窗口期。前端展示时若 `AuthorizationEndTime < now` 且 `AuthorizationStatus == Active`,UI 显示为"已过期"(前端容错)。
|
||||
|
||||
#### 4. 后端重启注意事项
|
||||
|
||||
后端运行在 `http://localhost:5041`(background job: `job-a1f30a33998f4065abe22b34018dcc7a`)。
|
||||
- 修改代码后需 `Stop-Process -Name "MilliSim.Api" -Force`,再 `dotnet build` 重新构建
|
||||
- 启动新进程
|
||||
|
||||
### 前端修改
|
||||
|
||||
#### 5. 类型定义
|
||||
|
||||
**文件**: [`frontend/src/types/index.ts`](file:///d:/TraeProject/MilliSim4/frontend/src/types/index.ts)
|
||||
|
||||
修改 `User` 接口(L61-L74)添加授权字段:
|
||||
```typescript
|
||||
export interface User {
|
||||
// ... 原有字段
|
||||
authorizationStatus?: AuthorizationStatus | null
|
||||
authorizationEndTime?: string | null
|
||||
}
|
||||
```
|
||||
|
||||
#### 6. 授权卡片重新设计 + 批量授权窗口重新设计
|
||||
|
||||
**文件**: [`frontend/src/views/admin/Authorizations.vue`](file:///d:/TraeProject/MilliSim4/frontend/src/views/admin/Authorizations.vue) + `Authorizations.css`
|
||||
|
||||
参考 `Categories.vue` / `Platforms.vue` 的设计语言(蓝紫渐变 + 卡片渐变背景层 + 状态胶囊)。
|
||||
|
||||
##### 授权卡片新结构
|
||||
|
||||
```
|
||||
┌────────────────────────────────────┐
|
||||
│ [渐变背景层] │
|
||||
│ [头像 56x56] [状态胶囊 中文] │
|
||||
│ │
|
||||
│ 客户昵称 (15px 600) │
|
||||
│ @用户名 │
|
||||
│ │
|
||||
│ 有效期:2025-01-01 至 2025-12-31 │
|
||||
│ 剩余天数:180 天 │
|
||||
│ 授权人:销售员 │
|
||||
│ 创建时间:2025-01-01 │
|
||||
│ ───────────────────────────────── │
|
||||
│ [按钮组 - 按状态显示] │
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
##### 状态徽章中文映射
|
||||
|
||||
新增工具函数(在 Authorizations.vue 的 script 中):
|
||||
```typescript
|
||||
const statusConfig: Record<AuthorizationStatus, { label: string; class: string }> = {
|
||||
[AuthorizationStatus.Active]: { label: '有效', class: 'active' },
|
||||
[AuthorizationStatus.Expired]: { label: '已过期', class: 'expired' },
|
||||
[AuthorizationStatus.Cancelled]: { label: '已取消', class: 'cancelled' },
|
||||
}
|
||||
```
|
||||
|
||||
CSS 新增 `.status-pill.expired`(橙色)和 `.status-pill.cancelled`(红色)样式:
|
||||
```css
|
||||
.status-pill.expired { color: #FA8C16; background: rgba(250, 140, 22, 0.12); }
|
||||
.status-pill.expired .status-dot { background: #FA8C16; }
|
||||
.status-pill.cancelled { color: #F5222D; background: rgba(245, 34, 45, 0.12); }
|
||||
.status-pill.cancelled .status-dot { background: #F5222D; }
|
||||
```
|
||||
|
||||
##### 按状态显示操作按钮
|
||||
|
||||
```typescript
|
||||
function getCardActions(status: AuthorizationStatus) {
|
||||
if (status === AuthorizationStatus.Active) {
|
||||
return [
|
||||
{ key: 'renew', label: '续期', icon: 'time', onClick: openRenewDialog },
|
||||
{ key: 'cancel', label: '取消授权', icon: 'close', onClick: handleCancel, danger: true },
|
||||
{ key: 'detail', label: '查看详情', icon: 'eye', onClick: openDetailDialog },
|
||||
]
|
||||
}
|
||||
// Cancelled / Expired
|
||||
return [
|
||||
{ key: 'reactivate', label: '重新授权', icon: 'refresh', onClick: openReactivateDialog },
|
||||
{ key: 'detail', label: '查看详情', icon: 'eye', onClick: openDetailDialog },
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
##### 新增对话框
|
||||
|
||||
- **续期对话框**(已有,复用):天数输入,调用 `authorizationApi.renew(id, days)`
|
||||
- **重新授权对话框**(新增):天数输入(默认 365),调用 `authorizationApi.create({ userId, days })` — 后端自动复用原记录
|
||||
- **查看详情对话框**(新增):展示完整授权信息(客户、授权人、起止时间、状态、剩余天数等)
|
||||
|
||||
##### 批量授权窗口重新设计
|
||||
|
||||
参考设计:
|
||||
```
|
||||
┌── 批量授权 ──────────────────────────┐
|
||||
│ [图标] 批量授权 │
|
||||
│ 为多个客户一次性开通授权 │
|
||||
│ │
|
||||
│ ┌─ 选择客户 ─────────┐ ┌─ 授权配置 ─┐│
|
||||
│ │ [搜索框] │ │ 授权天数: ││
|
||||
│ │ ☐ 用户A 未授权 │ │ [输入框] ││
|
||||
│ │ ☐ 用户B 有效 │ │ 默认 365 ││
|
||||
│ │ ☐ 用户C 已过期 │ │ ││
|
||||
│ │ ... │ │ ││
|
||||
│ └────────────────────┘ └────────────┘│
|
||||
│ │
|
||||
│ 已选 3 个客户:用户A、用户B、用户C │
|
||||
│ [取消] [确认授权] │
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
实现要点:
|
||||
- 左侧客户列表(显示客户名 + 当前授权状态徽章),支持多选
|
||||
- 右侧授权配置(仅天数输入,客户级授权不关联项目)
|
||||
- 底部显示已选客户数量 + 确认按钮
|
||||
- 调用 `authorizationApi.batch({ userIds, days })` — 后端批量处理复用/新建
|
||||
|
||||
#### 7. 客户管理卡片重新设计
|
||||
|
||||
**文件**: [`frontend/src/views/admin/Customers.vue`](file:///d:/TraeProject/MilliSim4/frontend/src/views/admin/Customers.vue) + `Customers.css`
|
||||
|
||||
##### 客户卡片新结构
|
||||
|
||||
```
|
||||
┌────────────────────────────────────┐
|
||||
│ [渐变背景层] │
|
||||
│ [头像 56x56] [用户状态胶囊] │
|
||||
│ │
|
||||
│ 客户昵称 (15px 600) │
|
||||
│ @用户名 │
|
||||
│ │
|
||||
│ 授权状态:[徽章 - 中文] │
|
||||
│ 到期时间:2025-12-31 (剩余 180天) │
|
||||
│ │
|
||||
│ 手机:138****0000 │
|
||||
│ 邮箱:user@example.com │
|
||||
│ 注册时间:2025-01-01 │
|
||||
│ ───────────────────────────────── │
|
||||
│ [按钮组] │
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
##### 授权状态徽章(中文)
|
||||
|
||||
```typescript
|
||||
const authStatusConfig: Record<number, { label: string; class: string; showExpiry: boolean }> = {
|
||||
[AuthorizationStatus.Active]: { label: '授权有效', class: 'active', showExpiry: true },
|
||||
[AuthorizationStatus.Expired]: { label: '已过期', class: 'expired', showExpiry: true },
|
||||
[AuthorizationStatus.Cancelled]: { label: '已取消', class: 'cancelled', showExpiry: false },
|
||||
}
|
||||
// 无授权
|
||||
const noAuthConfig = { label: '未授权', class: 'none', showExpiry: false }
|
||||
```
|
||||
|
||||
##### 授权按钮文案动态化
|
||||
|
||||
```typescript
|
||||
function getAuthButton(status?: AuthorizationStatus | null) {
|
||||
if (status === undefined || status === null) return { label: '为客户授权', icon: 'plus' }
|
||||
if (status === AuthorizationStatus.Active) return { label: '续期授权', icon: 'time' }
|
||||
return { label: '重新授权', icon: 'refresh' } // Cancelled / Expired
|
||||
}
|
||||
```
|
||||
|
||||
##### 操作按钮(卡片底部)
|
||||
|
||||
- 编辑(编辑客户信息)
|
||||
- 授权按钮(动态文案)
|
||||
- 重置密码
|
||||
- 删除(红色 danger)
|
||||
|
||||
授权按钮点击行为:
|
||||
- 无授权 / Cancelled / Expired → 打开授权对话框(天数输入),调用 `authorizationApi.create({ userId, days })`
|
||||
- Active → 打开续期对话框,调用 `authorizationApi.renew(authId, days)`
|
||||
|
||||
> 注:需要先调用 `authorizationApi.check(userId)` 或在卡片数据中带 `authorizationId`。考虑在 UserDto 额外返回 `authorizationId?: long?`,这样前端续期可直接用。**决策**:UserDto 也增加 `AuthorizationId?: long?` 字段,简化前端续期调用。
|
||||
|
||||
## 假设与决策
|
||||
|
||||
### 决策记录
|
||||
|
||||
1. **重新授权策略**:复用原记录(更新 Status=Active + 重置 StartTime/EndTime/AuthorizedBy)。原因:用户确认 + 历史可追溯 + 符合"一个客户一个授权"约束。
|
||||
|
||||
2. **客户卡片授权状态来源**:后端联表查询(UserDto 增加 AuthorizationStatus/AuthorizationEndTime/AuthorizationId 字段)。原因:用户确认 + 一次查询返回所有数据,避免 N+1 请求。
|
||||
|
||||
3. **续期 vs 重新授权 分工**:
|
||||
- **续期**(仅 Active):EndTime += days,状态不变
|
||||
- **重新授权**(Cancelled/Expired):调用 create API,后端自动复用原记录重置 StartTime=now, EndTime=now+days, Status=Active
|
||||
- 两个独立操作,前端通过不同对话框触发
|
||||
|
||||
4. **批量授权窗口的"复用"语义**:批量授权调用同一 API,对已有 Cancelled/Expired 状态的客户自动复用原记录,对已有 Active 状态的客户自动续期(EndTime += days)。
|
||||
|
||||
### 假设
|
||||
|
||||
- AuthorizationStatus 数字序列化已配置(项目记忆显示 ApiResponse.cs 已移除 [JsonConverter])
|
||||
- Authorizations.ProjectId 已通过 DbSeeder 改为可空(已完成)
|
||||
- 前端 `authorizationApi.renew(id, days)` 已发送 `{ days }` body(无需修改前端 API 调用,仅需后端 controller 接收方式调整)
|
||||
- 前端 Vite dev server 热更新中,修改 .vue 文件后自动生效
|
||||
|
||||
## 验证步骤
|
||||
|
||||
### 后端验证
|
||||
|
||||
1. **构建检查**:
|
||||
```powershell
|
||||
Stop-Process -Name "MilliSim.Api" -Force
|
||||
cd d:\TraeProject\MilliSim4\backend\MilliSim.Api
|
||||
dotnet build
|
||||
```
|
||||
确认无编译错误。
|
||||
|
||||
2. **启动后端**:通过 background 命令启动 `dotnet run --project backend/MilliSim.Api`
|
||||
|
||||
### 前端验证(浏览器手动测试)
|
||||
|
||||
1. **续期 bug 修复**:
|
||||
- 进入授权管理页面
|
||||
- 对一个 Active 状态的授权点击"续期"
|
||||
- 输入 30 天,确认提交
|
||||
- 预期:续期成功,不再提示"续期天数必须大于 0"
|
||||
|
||||
2. **授权逻辑 — 一个客户一个授权**:
|
||||
- 在客户管理页面,为客户 A 授权(首次创建)
|
||||
- 取消该授权
|
||||
- 再次点击"重新授权"为客户 A 授权
|
||||
- 预期:后端复用原记录(更新状态为 Active),不创建新记录
|
||||
- 数据库验证:`SELECT * FROM Authorizations WHERE UserId = ?` 只有一条记录
|
||||
|
||||
3. **客户卡片授权状态显示**:
|
||||
- 客户管理页面,每个客户卡片应显示授权状态徽章(有效/已过期/已取消/未授权)
|
||||
- 不同状态显示不同颜色和文案
|
||||
|
||||
4. **授权卡片操作按钮**:
|
||||
- Active 卡片:显示"续期/取消授权/查看详情"3 个按钮
|
||||
- Cancelled/Expired 卡片:显示"重新授权/查看详情"2 个按钮
|
||||
|
||||
5. **批量授权**:
|
||||
- 选择多个客户(含不同状态:未授权、有效、已过期、已取消)
|
||||
- 设置 30 天
|
||||
- 预期:所有客户都成功,原有记录被复用
|
||||
|
||||
6. **状态徽章中文**:
|
||||
- Active → "有效"(绿色)
|
||||
- Expired → "已过期"(橙色)
|
||||
- Cancelled → "已取消"(红色)
|
||||
|
||||
## 文件变更清单
|
||||
|
||||
### 后端
|
||||
1. `backend/MilliSim.Api/Controllers/AuthorizationsController.cs` — Renew 端点改 [FromBody]
|
||||
2. `backend/MilliSim.Api/Common/Dtos/Dtos.cs` — 添加 RenewAuthorizationRequest;UserDto 增加 3 个授权字段
|
||||
3. `backend/MilliSim.Api/Services/AuthorizationService.cs` — 重构 CreateAuthorization/BatchAuthorize(复用原记录);Cancel/Renew 增加状态检查
|
||||
4. `backend/MilliSim.Api/Services/UserService.cs` — GetUsersAsync 联表查询 Authorizations
|
||||
|
||||
### 前端
|
||||
5. `frontend/src/types/index.ts` — User 接口增加 authorizationStatus/authorizationEndTime/authorizationId 字段
|
||||
6. `frontend/src/views/admin/Authorizations.vue` + `Authorizations.css` — 卡片重新设计 + 新对话框 + 中文状态徽章
|
||||
7. `frontend/src/views/admin/Customers.vue` + `Customers.css` — 卡片重新设计 + 授权状态徽章 + 动态授权按钮
|
||||
|
||||
## 实施顺序
|
||||
|
||||
1. 后端 DTO + Controller 修复(续期 bug)
|
||||
2. 后端 AuthorizationService 重构(授权逻辑)
|
||||
3. 后端 UserService + UserDto 修改(联表查询)
|
||||
4. 后端构建 + 重启
|
||||
5. 前端 types/index.ts 修改
|
||||
6. 前端 Authorizations.vue + CSS 重新设计
|
||||
7. 前端 Customers.vue + CSS 重新设计
|
||||
8. 浏览器手动验证全部场景
|
||||
371
.trae/documents/customers-page-redesign.md
Normal file
371
.trae/documents/customers-page-redesign.md
Normal file
@ -0,0 +1,371 @@
|
||||
# 客户管理页面重新设计实施计划
|
||||
|
||||
## Summary
|
||||
|
||||
承接授权系统重构批次,前 6 项变更(后端续期 bug 修复、AuthorizationService 复用记录逻辑、UserService 联表查询、types/index.ts 字段扩展、Authorizations.vue/css 重新设计)已全部完成并验证。本计划仅聚焦最后一项剩余工作:**重新设计 `Customers.vue` + `Customers.css`**,让客户卡片显示当前授权状态,并将"授权"按钮的文案与行为根据授权状态动态化,与 `Authorizations.vue` 的蓝紫渐变设计语言保持一致。
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### 已就绪的基础设施(无需重复实现)
|
||||
|
||||
- **后端** `UserDto` 已新增 3 个字段:`AuthorizationId` / `AuthorizationStatus` / `AuthorizationEndTime`([Dtos.cs](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Common/Dtos/Dtos.cs#L49-L57))
|
||||
- **后端** `UserService.GetUsersAsync` 已联表查询 `Authorizations` 表,按 UserId 分组取最新一条填充到 UserDto([UserService.cs](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Services/UserService.cs#L82-L98))
|
||||
- **前端** `User` 接口已扩展 3 个可选字段([types/index.ts](file:///d:/TraeProject/MilliSim4/frontend/src/types/index.ts#L73-L78))
|
||||
- **前端** `Authorizations.vue` 已建立设计语言基准(蓝紫渐变 + 统计栏 + 状态胶囊 + 卡片渐变背景层),可作为本页面的视觉参考
|
||||
- **前端** `authorizationApi.renew(id, days)` 已发送 `{ days }` body,与后端 `[FromBody] RenewAuthorizationRequest` 修复匹配
|
||||
- **后端** `CreateAuthorizationAsync` 已重构为"复用原记录"逻辑(存在任意状态授权时复用,不新建),因此客户卡片上的"为客户授权"和"重新授权"都可调用 `authorizationApi.create({ userId, days })`
|
||||
|
||||
### 当前 Customers.vue 的不足
|
||||
|
||||
1. **卡片不显示授权状态**:仅显示用户状态徽章(启用/禁用),无授权信息
|
||||
2. **授权按钮文案固定**:"授权"图标按钮,无状态感知
|
||||
3. **授权弹窗过于简单**:仅一个天数输入框,无快捷按钮、无续期后到期时间预览
|
||||
4. **视觉设计缺乏设计感**:与重新设计后的 Authorizations.vue 风格不一致
|
||||
5. **续期无法在此页面完成**:必须跳转到授权管理页面
|
||||
|
||||
## Proposed Changes
|
||||
|
||||
### 文件 1: `d:\TraeProject\MilliSim4\frontend\src\views\admin\Customers.vue`
|
||||
|
||||
**保留**:页头卡片、筛选区(关键词 + 用户状态 TabFilter)、新增/编辑客户弹窗(含头像上传、账号信息、联系方式、状态设置三个分区)、重置密码、删除客户逻辑。
|
||||
|
||||
**变更 1:卡片结构重新设计**(替换原 L59-L112 的 `data-grid` 内容)
|
||||
|
||||
新卡片结构:
|
||||
```vue
|
||||
<div v-for="row in tableData" :key="row.id" class="data-item-card customer-card">
|
||||
<div class="card-gradient-bg"></div>
|
||||
<div class="card-content">
|
||||
<div class="card-top">
|
||||
<div class="card-avatar-wrap">
|
||||
<el-avatar :size="56" :src="row.avatar">
|
||||
<SvgIcon name="user" :size="28" />
|
||||
</el-avatar>
|
||||
</div>
|
||||
<!-- 授权状态胶囊(中文) -->
|
||||
<span class="status-pill" :class="getAuthStatusClass(row.authorizationStatus)">
|
||||
<span class="status-dot"></span>
|
||||
{{ getAuthStatusLabel(row.authorizationStatus) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-name">{{ row.nickname || row.username }}</div>
|
||||
<div class="card-sub">@{{ row.username }}</div>
|
||||
|
||||
<!-- 用户状态徽章 + 联系信息 -->
|
||||
<div class="card-info-list">
|
||||
<div class="card-info-row">
|
||||
<SvgIcon name="user" :size="13" color="var(--color-text-placeholder)" />
|
||||
<span class="info-label">用户状态</span>
|
||||
<span class="info-value" :class="row.status === 1 ? 'highlight' : 'inactive'">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="row.phone" class="card-info-row">
|
||||
<SvgIcon name="phone" :size="13" color="var(--color-text-placeholder)" />
|
||||
<span class="info-label">手机</span>
|
||||
<span class="info-value">{{ row.phone }}</span>
|
||||
</div>
|
||||
<div v-if="row.email" class="card-info-row">
|
||||
<SvgIcon name="mail" :size="13" color="var(--color-text-placeholder)" />
|
||||
<span class="info-label">邮箱</span>
|
||||
<span class="info-value text-ellipsis">{{ row.email }}</span>
|
||||
</div>
|
||||
<!-- 授权到期信息(仅有授权时显示) -->
|
||||
<div v-if="row.authorizationStatus === 1" class="card-info-row">
|
||||
<SvgIcon name="calendar" :size="13" color="var(--color-text-placeholder)" />
|
||||
<span class="info-label">到期</span>
|
||||
<span class="info-value" :class="{ 'near-expire': remainingDays(row.authorizationEndTime) <= 7 }">
|
||||
{{ formatTime(row.authorizationEndTime) }}
|
||||
<span class="remaining-days">(剩 {{ remainingDays(row.authorizationEndTime) }} 天)</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-info-row">
|
||||
<SvgIcon name="clock" :size="13" color="var(--color-text-placeholder)" />
|
||||
<span class="info-label">注册</span>
|
||||
<span class="info-value">{{ formatTime(row.createdAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<span class="card-creator">{{ row.createdByName || '系统' }}</span>
|
||||
<div class="card-actions">
|
||||
<button class="card-action-btn" title="编辑" @click="openEdit(row)">
|
||||
<SvgIcon name="edit" :size="15" />
|
||||
</button>
|
||||
<!-- 动态授权按钮:文案 + 行为按状态变化 -->
|
||||
<button
|
||||
class="card-action-btn"
|
||||
:class="getAuthButtonClass(row.authorizationStatus)"
|
||||
:title="getAuthButtonConfig(row.authorizationStatus).label"
|
||||
@click="openAuth(row)"
|
||||
>
|
||||
<SvgIcon :name="getAuthButtonConfig(row.authorizationStatus).icon" :size="15" />
|
||||
<span>{{ getAuthButtonConfig(row.authorizationStatus).label }}</span>
|
||||
</button>
|
||||
<button class="card-action-btn" title="重置密码" @click="handleResetPwd(row)">
|
||||
<SvgIcon name="lock" :size="15" />
|
||||
</button>
|
||||
<button class="card-action-btn danger" title="删除" @click="handleDelete(row)">
|
||||
<SvgIcon name="trash" :size="15" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**变更 2:脚本逻辑扩展**(在 `<script setup>` 内新增)
|
||||
|
||||
1. 导入 `AuthorizationStatus`:
|
||||
```typescript
|
||||
import type { User, AuthorizationStatus } from '@/types'
|
||||
```
|
||||
|
||||
2. 授权状态配置(中文 + 样式类):
|
||||
```typescript
|
||||
const authStatusConfig: Record<number, { label: string; class: string }> = {
|
||||
0: { label: '已取消', class: 'cancelled' },
|
||||
1: { label: '授权有效', class: 'active' },
|
||||
2: { label: '已过期', class: 'expired' },
|
||||
}
|
||||
const noAuthConfig = { label: '未授权', class: 'none' }
|
||||
|
||||
function getAuthStatusLabel(status?: AuthorizationStatus | null) {
|
||||
if (status === null || status === undefined) return noAuthConfig.label
|
||||
return authStatusConfig[status]?.label ?? noAuthConfig.label
|
||||
}
|
||||
function getAuthStatusClass(status?: AuthorizationStatus | null) {
|
||||
if (status === null || status === undefined) return noAuthConfig.class
|
||||
return authStatusConfig[status]?.class ?? noAuthConfig.class
|
||||
}
|
||||
```
|
||||
|
||||
3. 授权按钮文案 + 图标 + 样式类动态化:
|
||||
```typescript
|
||||
function getAuthButtonConfig(status?: AuthorizationStatus | null) {
|
||||
if (status === null || status === undefined)
|
||||
return { label: '为客户授权', icon: 'plus' }
|
||||
if (status === AuthorizationStatus.Active)
|
||||
return { label: '续期授权', icon: 'refresh' }
|
||||
return { label: '重新授权', icon: 'refresh' } // Cancelled / Expired
|
||||
}
|
||||
function getAuthButtonClass(status?: AuthorizationStatus | null) {
|
||||
// Active 不加 primary(避免与"续期"操作冲突视觉),其余状态加 primary 强调引导
|
||||
if (status === null || status === undefined) return 'primary'
|
||||
if (status === AuthorizationStatus.Active) return ''
|
||||
return 'primary'
|
||||
}
|
||||
```
|
||||
|
||||
4. 重写 `openAuth` 根据状态填充表单:
|
||||
```typescript
|
||||
const authForm = reactive({
|
||||
userId: 0,
|
||||
userLabel: '',
|
||||
days: 30,
|
||||
// 新增:操作模式与原授权 ID(用于续期调用 renew API)
|
||||
mode: 'create' as 'create' | 'renew',
|
||||
authorizationId: 0,
|
||||
originalStatusLabel: '',
|
||||
endTime: '',
|
||||
})
|
||||
|
||||
function openAuth(row: User) {
|
||||
authForm.userId = row.id
|
||||
authForm.userLabel = `${row.nickname || row.username}(@${row.username})`
|
||||
authForm.endTime = row.authorizationEndTime ?? ''
|
||||
authForm.originalStatusLabel = getAuthStatusLabel(row.authorizationStatus)
|
||||
if (row.authorizationStatus === AuthorizationStatus.Active && row.authorizationId) {
|
||||
authForm.mode = 'renew'
|
||||
authForm.authorizationId = row.authorizationId
|
||||
authForm.days = 30
|
||||
} else {
|
||||
// 无授权 / 已取消 / 已过期 → 调用 create(后端自动复用原记录或新建)
|
||||
authForm.mode = 'create'
|
||||
authForm.authorizationId = 0
|
||||
authForm.days = 365
|
||||
}
|
||||
authVisible.value = true
|
||||
}
|
||||
```
|
||||
|
||||
5. 重写 `saveAuth` 根据 mode 调用不同 API:
|
||||
```typescript
|
||||
async function saveAuth() {
|
||||
authSaving.value = true
|
||||
try {
|
||||
if (authForm.mode === 'renew') {
|
||||
await authorizationApi.renew(authForm.authorizationId, authForm.days)
|
||||
ElMessage.success('续期成功')
|
||||
} else {
|
||||
await authorizationApi.create({
|
||||
userId: authForm.userId,
|
||||
days: authForm.days,
|
||||
})
|
||||
ElMessage.success(authForm.originalStatusLabel === '未授权' ? '授权成功' : '重新授权成功')
|
||||
}
|
||||
authVisible.value = false
|
||||
fetchData()
|
||||
} catch {
|
||||
// 已处理
|
||||
} finally {
|
||||
authSaving.value = false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
6. 新增 `remainingDays` 工具函数(与 Authorizations.vue 一致):
|
||||
```typescript
|
||||
function remainingDays(endTime?: string | null) {
|
||||
if (!endTime) return 0
|
||||
const diff = dayjs(endTime).diff(dayjs(), 'day')
|
||||
return diff > 0 ? diff : 0
|
||||
}
|
||||
```
|
||||
|
||||
7. 新增 `renewAfterDate` 计算属性(用于弹窗内显示续期后到期时间):
|
||||
```typescript
|
||||
const renewAfterDate = computed(() => {
|
||||
if (!authForm.endTime) return '-'
|
||||
const base = dayjs(authForm.endTime).isAfter(dayjs()) ? dayjs(authForm.endTime) : dayjs()
|
||||
return base.add(authForm.days, 'day').format('YYYY-MM-DD HH:mm')
|
||||
})
|
||||
```
|
||||
|
||||
**变更 3:授权弹窗重新设计**(替换原 L258-L293 的 `auth-dialog`)
|
||||
|
||||
新弹窗结构:
|
||||
- 头部使用 `dialog-header-design` + `dialog-header-icon-lg`(add-mode 或 edit-mode)
|
||||
- 显示原授权状态(如有)
|
||||
- 授权范围(只读:全部项目)
|
||||
- 当前到期时间(仅 renew 模式显示)
|
||||
- 授权天数 + 快捷按钮 30/90/180/365 天
|
||||
- 续期模式显示"续期后新到期时间"
|
||||
- 底部按钮文案动态化:续期模式为"确认续期",新建/重新授权模式为"确认授权"
|
||||
|
||||
```vue
|
||||
<el-dialog v-model="authVisible" width="480px" custom-class="dialog-card" destroy-on-close>
|
||||
<template #header>
|
||||
<div class="dialog-header-design">
|
||||
<div class="dialog-header-icon-lg" :class="authForm.mode === 'renew' ? 'edit-mode' : 'add-mode'">
|
||||
<SvgIcon :name="authForm.mode === 'renew' ? 'refresh' : 'key'" :size="24" color="#fff" />
|
||||
</div>
|
||||
<div class="dialog-header-text">
|
||||
<h3>{{ authForm.mode === 'renew' ? '授权续期' : (authForm.originalStatusLabel === '未授权' ? '为客户授权' : '重新授权') }}</h3>
|
||||
<p>{{ authForm.mode === 'renew' ? '延长客户的项目授权有效期' : '为客户开通或重新开通访问权限' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :model="authForm" label-width="90px" class="designed-form">
|
||||
<el-form-item label="客户">
|
||||
<el-input :model-value="authForm.userLabel" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="authForm.originalStatusLabel !== '未授权'" label="原状态">
|
||||
<el-input :model-value="authForm.originalStatusLabel" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="授权范围">
|
||||
<el-input :model-value="'全部项目'" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="authForm.mode === 'renew'" label="当前到期">
|
||||
<el-input :model-value="formatTime(authForm.endTime)" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="授权天数" required>
|
||||
<el-input-number v-model="authForm.days" :min="1" :max="3650" style="width: 100%" />
|
||||
<div class="form-hint" v-if="authForm.mode === 'renew'">续期后新到期时间:{{ renewAfterDate }}</div>
|
||||
<div class="form-hint" v-else>{{ authForm.originalStatusLabel === '未授权' ? '授权后客户可访问全部项目' : '重新授权后将重置起止时间,复用原记录' }}</div>
|
||||
</el-form-item>
|
||||
<div class="quick-days">
|
||||
<el-button size="small" @click="authForm.days = 30">30 天</el-button>
|
||||
<el-button size="small" @click="authForm.days = 90">90 天</el-button>
|
||||
<el-button size="small" @click="authForm.days = 180">半年</el-button>
|
||||
<el-button size="small" @click="authForm.days = 365">1 年</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer-custom">
|
||||
<el-button @click="authVisible = false">
|
||||
<SvgIcon name="close" :size="14" />
|
||||
<span>取消</span>
|
||||
</el-button>
|
||||
<el-button type="primary" class="primary-btn" :loading="authSaving" @click="saveAuth">
|
||||
<SvgIcon name="check" :size="14" color="#fff" />
|
||||
<span>{{ authForm.mode === 'renew' ? '确认续期' : '确认授权' }}</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
```
|
||||
|
||||
### 文件 2: `d:\TraeProject\MilliSim4\frontend\src\views\admin\Customers.css`
|
||||
|
||||
**保留**:现有的页面头部装饰(`page-header-card::before/after`)、筛选区样式、新增/编辑弹窗的表单分区设计、响应式断点、无障碍动画偏好。
|
||||
|
||||
**变更 1:新增客户卡片设计样式**(与 Authorizations.css 设计语言对齐)
|
||||
|
||||
新增以下样式区块:
|
||||
|
||||
1. **卡片渐变背景层** `.customer-card .card-gradient-bg`(蓝紫渐变,hover 加深)
|
||||
2. **卡片顶部头像区域** `.customer-card .card-top` + `.card-avatar-wrap`(56×56 圆角,蓝紫渐变背景,hover 缩放旋转)
|
||||
3. **状态胶囊** `.status-pill` 及变体 `.active` / `.expired` / `.cancelled` / `.none`(与 Authorizations.css 一致的中文 + 颜色)
|
||||
4. **卡片信息列表** `.card-info-list` + `.card-info-row`(图标 + label + value 横向布局)
|
||||
5. **info-value 变体** `.highlight` / `.inactive` / `.near-expire`
|
||||
6. **到期剩余天数** `.remaining-days`(小字提示)
|
||||
7. **卡片底部操作** `.customer-card .card-footer` + `.card-action-btn` + `.primary` / `.danger` 变体
|
||||
8. **卡片底部创建人标识** `.card-creator`
|
||||
9. **弹窗设计样式** `.dialog-header-design` + `.dialog-header-icon-lg.add-mode` / `.edit-mode` + `.dialog-header-text` + `.designed-form` + `.form-hint` + `.quick-days`(与 Authorizations.css 一致)
|
||||
|
||||
**变更 2:移除旧的 `customer-head` / `status-badge` / `card-info` 等过时样式**
|
||||
|
||||
由于卡片结构变化,移除以下样式(避免冗余):
|
||||
- `.customers-page .customer-head`
|
||||
- `.customers-page .status-badge` 相关样式(如存在)
|
||||
- 原 `.customers-page .customer-card-content` 中的简单 padding(改为新的 `.card-content`)
|
||||
|
||||
**变更 3:响应式与无障碍**
|
||||
|
||||
保留现有的 767px 响应式断点逻辑,仅需在样式表底部补一条规则:在 767px 以下让 `.card-info-list` 仍然垂直堆叠(默认就是垂直,无需额外修改)。
|
||||
|
||||
## Assumptions & Decisions
|
||||
|
||||
### 决策 1:客户卡片上的"授权"按钮统一调用 `openAuth`
|
||||
|
||||
不再在卡片上直接放"续期/重新授权/取消授权"等多个按钮(那是授权管理页面的职责)。客户卡片只保留一个动态授权按钮 + 编辑 + 重置密码 + 删除四个按钮,避免视觉过载。
|
||||
|
||||
### 决策 2:续期不在此页面调用取消授权
|
||||
|
||||
取消授权操作放在授权管理页面,客户管理页面只负责"为客户授权/续期/重新授权"的入口,避免职责重叠。
|
||||
|
||||
### 决策 3:保留原有的"新增/编辑客户"弹窗设计
|
||||
|
||||
该弹窗已经使用 `form-avatar-section` + `form-section` + `form-section-title` 分区设计,视觉上符合预期,无需重写。仅授权弹窗需要重新设计。
|
||||
|
||||
### 决策 4:`status` 字段(用户启用/禁用)作为卡片信息列表的一行
|
||||
|
||||
不再作为头像旁边的徽章,改为信息列表中的"用户状态"行,让授权状态胶囊独占顶部右侧位置,视觉层次更清晰。
|
||||
|
||||
### 假设
|
||||
|
||||
- 后端 `UserService.GetUsersAsync` 已正确返回 `authorizationId` / `authorizationStatus` / `authorizationEndTime`(已在 Phase 1 验证)
|
||||
- 后端运行在 `http://localhost:5041`(background job `job-66befe03b12347d2b2643513f94454a5` 已启动)
|
||||
- 前端 Vite dev server 已在热更新模式运行
|
||||
- `authorizationApi.create({ userId, days })` 在客户已有 Cancelled/Expired 授权时会自动复用原记录(已在 AuthorizationService 中实现)
|
||||
- `authorizationApi.renew(authId, days)` 已发送 `{ days }` body,与后端 `[FromBody] RenewAuthorizationRequest` 匹配(已在 Phase 1 验证 [api/index.ts](file:///d:/TraeProject/MilliSim4/frontend/src/api/index.ts#L147-L148))
|
||||
|
||||
## Verification Steps
|
||||
|
||||
实施完成后:
|
||||
|
||||
1. **静态检查**:确认 `Customers.vue` 无 TypeScript 错误(Vite 热更新会自动报错)
|
||||
2. **视觉一致性**:打开客户管理页面,确认卡片视觉与授权管理页面一致(蓝紫渐变背景层、头像渐变背景、状态胶囊中文 + 颜色)
|
||||
3. **授权状态展示**:分别查看有授权(Active/Expired/Cancelled)和无授权的客户卡片,确认状态胶囊正确显示
|
||||
4. **授权按钮文案**:
|
||||
- 无授权客户 → "为客户授权" + primary 样式
|
||||
- Active 客户 → "续期授权" + 默认样式
|
||||
- Cancelled/Expired 客户 → "重新授权" + primary 样式
|
||||
5. **授权弹窗**:
|
||||
- 点击"为客户授权" → 弹窗标题"为客户授权",按钮"确认授权",调用 `create`
|
||||
- 点击"续期授权" → 弹窗标题"授权续期",显示当前到期 + 续期后到期预览,按钮"确认续期",调用 `renew`
|
||||
- 点击"重新授权" → 弹窗标题"重新授权",显示原状态,按钮"确认授权",调用 `create`
|
||||
6. **续期 bug 回归**:点击"续期授权",输入天数,确认后端返回成功(不再提示"续期天数必须大于 0")
|
||||
7. **客户卡片状态刷新**:授权操作成功后,`fetchData` 重新拉取列表,卡片状态胶囊自动更新
|
||||
8. **响应式**:在 767px 以下窗口宽度检查卡片和弹窗是否正常显示
|
||||
92
.trae/documents/upload-cleanup-completion.md
Normal file
92
.trae/documents/upload-cleanup-completion.md
Normal file
@ -0,0 +1,92 @@
|
||||
# 上传垃圾资源清理 - 收尾计划
|
||||
|
||||
## 概述
|
||||
|
||||
本计划承接前一会话的工作。前一会话已实现上传垃圾清理的完整三层防御机制(前端即时清理 + 后端保存时清理 + 后端 GC 兜底),并完成了 8 个页面的接入。经探查验证,发现**一个遗漏场景**:后台顶栏的个人资料弹窗(AdminLayout.vue)使用了 IconUpload 上传头像,但未接入上传会话清理机制。本计划补齐该场景并完成整体验证。
|
||||
|
||||
## 当前状态分析
|
||||
|
||||
### 已完成(无需改动)
|
||||
|
||||
- **前端基础设施**:`api/index.ts` 的 `deleteUpload` 方法、`useUploadSession.ts` composable
|
||||
- **上传组件**:`IconUpload.vue`、`VideoUpload.vue` 已支持 `uploadSession` prop + `currentKey` 跟踪
|
||||
- **8 个页面接入**:Categories / Platforms / Banners / Partners / Employees / Customers / Settings / ProjectEdit
|
||||
- **后端基础设施**:SystemController Upload/DELETE 端点、UploadTrackingService、UploadCleanupJob、各业务 Service 的 ConsumeAsync 调用
|
||||
|
||||
### 遗漏场景(本次修复)
|
||||
|
||||
- **AdminLayout.vue 个人资料弹窗**:使用 IconUpload 上传头像,但未接入上传会话。当用户上传新头像后点击"取消"或关闭弹窗,文件成为垃圾资源(仅靠 24h GC 兜底,不符合即时清理目标)。
|
||||
|
||||
### 范围外说明(不在本次修复)
|
||||
|
||||
- **ProjectPackage 软删除孤儿**:`DeletePackageAsync` 仅设 `IsDeleted = true`,不删除存储文件。这是实体级清理问题,不属于"上传垃圾清理"范畴(上传时立即消费,无 pending 状态),单独跟踪。
|
||||
- **WebGL 解压失败孤儿**:ZIP 上传成功但解压失败时,文件已落库存储。属于异常流程清理,单独跟踪。
|
||||
|
||||
## 计划改动
|
||||
|
||||
### 改动 1:AdminLayout.vue 接入上传会话清理
|
||||
|
||||
**文件**:`d:\TraeProject\MilliSim4\frontend\src\layouts\AdminLayout.vue`
|
||||
|
||||
**改动内容**(参照 Employees.vue 模式):
|
||||
|
||||
1. **导入 composable**(script 顶部):
|
||||
```typescript
|
||||
import { useUploadSession } from '@/composables/useUploadSession'
|
||||
```
|
||||
|
||||
2. **创建实例**(setup 区域):
|
||||
```typescript
|
||||
const uploadSession = useUploadSession()
|
||||
```
|
||||
|
||||
3. **个人资料 el-dialog 增加 @closed 事件**(约 L142):
|
||||
```vue
|
||||
<el-dialog v-model="profileVisible" ... @closed="uploadSession.cleanupPending()">
|
||||
```
|
||||
|
||||
4. **IconUpload 传入 uploadSession prop**(约 L157):
|
||||
```vue
|
||||
<IconUpload
|
||||
v-model="profileForm.avatar"
|
||||
folder="avatars"
|
||||
alt="头像"
|
||||
:upload-session="uploadSession"
|
||||
/>
|
||||
```
|
||||
|
||||
5. **saveProfile 成功后重置会话**(约 L259-276 内的成功分支):
|
||||
```typescript
|
||||
// 保存成功后清空 pending 列表:头像已被后端 ConsumeAsync 消费
|
||||
uploadSession.reset()
|
||||
```
|
||||
|
||||
**注意**:
|
||||
- AdminLayout 是常驻布局组件,不会频繁卸载,`onBeforeUnmount` 自动清理在此场景下主要起兜底作用;真正的清理依赖 `@closed` 事件。
|
||||
- 不需要改"取消"按钮逻辑,因为 `@closed` 事件在点击取消(关闭弹窗)时也会触发。
|
||||
|
||||
### 改动 2:验证步骤
|
||||
|
||||
**前端验证**:
|
||||
1. 后端 dev server 已运行(前一会话已启动,确认端口 5041 监听)
|
||||
2. 前端 Vite dev server 已运行(确认无 HMR 编译错误)
|
||||
3. 如有编译错误则修复
|
||||
|
||||
**功能场景测试**(建议用户在浏览器手动验证至少 2 个场景):
|
||||
- 场景 A:新增分类 → 上传图标 → 点击取消 → 确认图标被清理(检查后端日志 DELETE 请求)
|
||||
- 场景 B:编辑员工 → 上传新头像 → 点击取消 → 确认头像被清理
|
||||
- 场景 C:后台顶栏 → 点击头像 → 上传新头像 → 点击取消 → 确认头像被清理(本次新增场景)
|
||||
|
||||
## 假设与决策
|
||||
|
||||
1. **不动后端代码**:前一会话后端已完整实现,本次仅前端补漏。
|
||||
2. **不动 ProjectPackage 软删除逻辑**:属于实体生命周期管理,不在上传垃圾清理范畴。
|
||||
3. **保留 onBeforeUnmount 兜底**:AdminLayout 虽不常卸载,但 composable 的 onBeforeUnmount 仍是安全网,不删除。
|
||||
4. **使用 `@closed` 而非 `@close`**:`@closed` 在弹窗动画结束后触发,避免与销毁逻辑冲突(与已接入页面保持一致)。
|
||||
|
||||
## 验证步骤
|
||||
|
||||
1. 修改 AdminLayout.vue 后,查看 Vite HMR 日志确认无编译错误
|
||||
2. 检查后端进程仍在运行(端口 5041)
|
||||
3. 向用户报告:本次补漏 + 整体验证结果
|
||||
4. 建议用户手动测试场景 C(个人资料头像取消清理)
|
||||
294
.trae/documents/upload-garbage-cleanup-frontend.md
Normal file
294
.trae/documents/upload-garbage-cleanup-frontend.md
Normal file
@ -0,0 +1,294 @@
|
||||
# 上传垃圾资源清理 - 前端实施计划
|
||||
|
||||
## 概述
|
||||
|
||||
延续上一会话已完成的后端基础(PendingUpload 实体、UploadTrackingService、UploadCleanupJob、各业务 Service 的 Consume/Delete 旧文件逻辑、SystemController 的 Upload/DELETE 端点),本计划聚焦**前端集成**,确保所有上传场景在「取消表单、替换文件、关闭弹窗」时主动调用后端 DELETE 清理 pending 文件,避免形成垃圾资源。
|
||||
|
||||
## 当前状态分析
|
||||
|
||||
### 后端(已完成,需验证编译)
|
||||
|
||||
- `Common/Entities/Entities.cs`:新增 `PendingUpload` 实体(ObjectKey/Folder/UploadedBy/UploadedAt/ConsumedAt)
|
||||
- `Data/MilliSimDbContext.cs`:注册 `PendingUploads` DbSet + 两个索引
|
||||
- `Data/DbSeeder.cs`:`PatchDatabaseSchemaAsync` 末尾新增建表补丁
|
||||
- `Services/Infrastructure/UploadTrackingService.cs`:RegisterAsync / ConsumeAsync / DeletePendingAsync / CleanupExpiredAsync
|
||||
- `Services/UploadCleanupJob.cs`:IHostedService + Timer(启动 5 分钟后首次执行,之后每 6 小时一次,清理 24h 未消费文件)
|
||||
- `Program.cs`:注册 `IUploadTrackingService`(Scoped)+ `UploadCleanupJob`(Hosted)
|
||||
- `Controllers/SystemController.cs`:
|
||||
- Upload 端点:成功后 `RegisterAsync`,返回 `{ url, key }`
|
||||
- 新增 DELETE `/api/system/upload?key=xxx`:仅删 pending 状态文件
|
||||
- `Services/ProjectService.cs`、`ContentService.cs`、`UserService.cs`、`AuthService.cs`、`SystemService.cs`:
|
||||
- Create:保存后 `ConsumeAsync(ExtractKey(field))`
|
||||
- Update:记录 oldKey/newKey,保存后对比,不同则 `DeleteAsync(oldKey)` + `ConsumeAsync(newKey)`
|
||||
|
||||
### 前端(未实施,本计划范围)
|
||||
|
||||
- `api/index.ts`:`systemApi.upload` 返回类型仅 `{ url: string }`,缺 `key`;无 `deleteUpload` 方法
|
||||
- `components/IconUpload.vue`:XHR 上传只用 `response.data.url`,丢弃 `key`;`handleDelete` 仅 `emit('update:modelValue', '')`,不调后端删除
|
||||
- `components/VideoUpload.vue`:同 IconUpload 问题
|
||||
- 9 个表单页面均未跟踪上传会话、未在取消/关闭时清理 pending 文件
|
||||
|
||||
## 三层防御策略
|
||||
|
||||
1. **前端即时清理(本计划核心)**:`useUploadSession` composable 跟踪表单会话中的上传文件;取消表单时 `cleanupPending()` 批量删除;替换文件时 IconUpload/VideoUpload 自动调 DELETE 删除旧 pending 上传
|
||||
2. **后端保存时清理(已完成)**:各 Service Update 方法对比新旧 key,删除旧文件 + 消费新文件
|
||||
3. **后端 GC 兜底(已完成)**:`UploadCleanupJob` 每 6 小时清理 24h 未消费的 pending 文件
|
||||
|
||||
## 关键设计决策
|
||||
|
||||
- **不修改各 Service 的存储格式** — 后端 `ExtractKey` 幂等(签名URL/key 都能归一化),前端只需把上传返回的 `key` 原样存入 form,提交时随业务字段一起发送
|
||||
- **DELETE 端点只删 pending 文件** — `DeletePendingAsync` 检查 `ConsumedAt == null`,已保存到实体的文件不会被误删
|
||||
- **前端通过 key 跟踪上传** — IconUpload/VideoUpload 捕获 `UploadResultDto.key`,通过 `uploadSession.register(key)` 注册到会话
|
||||
- **替换文件即时清理** — IconUpload/VideoUpload 在 emit 新值前,若旧值是本次会话上传的 pending key,立即调 `deleteUpload(key)` 删除(不等表单取消)
|
||||
- **会话生命周期** — 弹窗页面:`openDialog` 时 `resetSession()`,`cancelDialog`/`closed` 时 `cleanupPending()`;页面级:`onMounted` 时创建,`onBeforeUnmount` 时 `cleanupPending()`,保存成功后 `resetSession()` 标记已消费
|
||||
|
||||
## 实施步骤
|
||||
|
||||
### 阶段 A:后端编译验证(前置)
|
||||
|
||||
1. 杀掉后端进程(PID 17372 或当前运行的 MilliSim.Api.exe)
|
||||
2. `dotnet build backend/MilliSim.Api/MilliSim.Api.csproj -c Debug`
|
||||
3. 预期:编译通过(可能有 2 个无害 nullable 警告:UserService.cs L186、ProjectService.cs L1021)
|
||||
4. 启动后端:`dotnet run --project backend/MilliSim.Api/MilliSim.Api.csproj`,确认 PendingUploads 表已建表、Swagger 显示 DELETE `/api/system/upload`
|
||||
|
||||
### 阶段 B:前端基础设施
|
||||
|
||||
#### B1. `frontend/src/api/index.ts`
|
||||
|
||||
修改 `systemApi`:
|
||||
|
||||
```typescript
|
||||
// 通用文件上传接口(返回 url 用于预览 + key 用于跟踪)
|
||||
upload: (formData: FormData) =>
|
||||
http.upload<ApiResponse<{ url: string; key: string }>>('/system/upload', formData),
|
||||
// 删除未使用的上传文件(仅限 pending 状态)
|
||||
deleteUpload: (key: string) =>
|
||||
http.delete<ApiResponse>('/system/upload', { key }),
|
||||
```
|
||||
|
||||
#### B2. 创建 `frontend/src/composables/useUploadSession.ts`
|
||||
|
||||
需先创建 `composables` 目录。
|
||||
|
||||
```typescript
|
||||
import { ref, onBeforeUnmount } from 'vue'
|
||||
import { systemApi } from '@/api'
|
||||
|
||||
interface UploadSession {
|
||||
/** 注册一个上传的 key(emit 前调用) */
|
||||
register(key: string): void
|
||||
/** 标记某个 key 已被保存消费(不再清理) */
|
||||
consume(key: string): void
|
||||
/** 取消表单/关闭弹窗时调用:批量删除所有未消费的 pending 文件 */
|
||||
cleanupPending(): Promise<void>
|
||||
/** 保存成功后重置会话(清空跟踪列表,不删文件) */
|
||||
reset(): void
|
||||
}
|
||||
|
||||
export function useUploadSession() {
|
||||
const pendingKeys = ref<string[]>([])
|
||||
|
||||
function register(key: string) {
|
||||
if (!key || pendingKeys.value.includes(key)) return
|
||||
pendingKeys.value.push(key)
|
||||
}
|
||||
|
||||
function consume(key: string) {
|
||||
if (!key) return
|
||||
pendingKeys.value = pendingKeys.value.filter(k => k !== key)
|
||||
}
|
||||
|
||||
async function cleanupPending() {
|
||||
const keys = [...pendingKeys.value]
|
||||
pendingKeys.value = []
|
||||
await Promise.all(
|
||||
keys.map(key =>
|
||||
systemApi.deleteUpload(key).catch(() => {/* 静默失败,不影响 UX */})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function reset() {
|
||||
pendingKeys.value = []
|
||||
}
|
||||
|
||||
// 组件卸载时自动清理(防止页面级表单忘记调用)
|
||||
onBeforeUnmount(() => {
|
||||
cleanupPending()
|
||||
})
|
||||
|
||||
return { register, consume, cleanupPending, reset }
|
||||
}
|
||||
```
|
||||
|
||||
### 阶段 C:上传组件改造
|
||||
|
||||
#### C1. `frontend/src/components/IconUpload.vue`
|
||||
|
||||
修改点:
|
||||
1. 新增 prop `uploadSession?`(可选,类型为 useUploadSession 返回的对象的方法集合)
|
||||
2. `uploadFile` 成功后:用 `response.data.key` 调 `uploadSession?.register(key)`,再 emit url
|
||||
3. `handleDelete`:若当前 `modelValue` 是本次会话上传的 key(即 uploadSession 中存在),调 `systemApi.deleteUpload(key)` 即时删除;emit `''`
|
||||
4. 替换图片场景:`uploadFile` 成功后,若旧 `modelValue` 是 pending key,先 `deleteUpload(oldKey)` 再注册新 key
|
||||
|
||||
具体改造:
|
||||
- props 增加 `uploadSession?: { register: (k: string) => void; consume: (k: string) => void }`
|
||||
- 新增内部状态 `currentKey`(保存最近一次上传返回的 key)
|
||||
- `uploadFile` 成功分支:
|
||||
```typescript
|
||||
if (response.code === 200 && response.data?.url) {
|
||||
const newKey = response.data.key || ''
|
||||
// 若旧值是 pending 上传,先清理(替换场景)
|
||||
if (props.uploadSession && currentKey.value && currentKey.value !== newKey) {
|
||||
// 旧的 currentKey 是上一次上传的 pending,本次替换前删除
|
||||
// 注意:只能删本次会话上传的,不能删已保存的(即 form 中来自后端的值)
|
||||
// 由于 IconUpload 在初始化时 modelValue 是签名URL,currentKey 为空,
|
||||
// 只有用户主动上传后 currentKey 才有值,所以可以放心删
|
||||
systemApi.deleteUpload(currentKey.value).catch(() => {})
|
||||
}
|
||||
currentKey.value = newKey
|
||||
props.uploadSession?.register(newKey)
|
||||
emit('update:modelValue', response.data.url)
|
||||
ElMessage.success('上传成功')
|
||||
}
|
||||
```
|
||||
- `handleDelete`:
|
||||
```typescript
|
||||
async function handleDelete() {
|
||||
if (currentKey.value) {
|
||||
// 仅删本次会话上传的,已保存的(currentKey 为空)不删
|
||||
await systemApi.deleteUpload(currentKey.value).catch(() => {})
|
||||
currentKey.value = ''
|
||||
}
|
||||
emit('update:modelValue', '')
|
||||
}
|
||||
```
|
||||
- 不再使用 `uploadSession.consume`,因为消费逻辑由后端 Service 在保存时通过 `ConsumeAsync` 处理;前端只需在取消表单时通过 `cleanupPending()` 批量清理剩余 pending
|
||||
|
||||
#### C2. `frontend/src/components/VideoUpload.vue`
|
||||
|
||||
与 IconUpload 相同的改造:新增 `uploadSession` prop + `currentKey` + 替换时清理旧 pending + handleDelete 调 DELETE。
|
||||
|
||||
### 阶段 D:9 个表单页面接入
|
||||
|
||||
所有页面统一模式:
|
||||
- 弹窗页面:在 setup 顶层 `const uploadSession = useUploadSession()`;handleAdd/handleEdit 时 `uploadSession.reset()`;取消按钮和 dialog 的 `closed` 事件调 `uploadSession.cleanupPending()`;handleSubmit 成功后 `uploadSession.reset()`;把 `:upload-session="uploadSession"` 传给所有 IconUpload/VideoUpload
|
||||
- 页面级:setup 顶层 `const uploadSession = useUploadSession()`;onMounted 时 `uploadSession.reset()`(清空上一次残留);保存成功后 `uploadSession.reset()`;离开页面前 onBeforeUnmount 由 composable 自动 cleanupPending
|
||||
|
||||
#### 弹窗页面(7 个)
|
||||
|
||||
| 文件 | 上传字段 | 改造点 |
|
||||
|------|---------|--------|
|
||||
| `views/admin/Categories.vue` | icon | useUploadSession + reset/cleanup/reset + 传 prop |
|
||||
| `views/admin/Platforms.vue` | icon | 同上 |
|
||||
| `views/admin/Tags.vue` | icon | 同上 |
|
||||
| `views/admin/Banners.vue` | image | 同上 |
|
||||
| `views/admin/Partners.vue` | logo | 同上 |
|
||||
| `views/admin/Employees.vue` | avatar | 同上 |
|
||||
| `views/admin/Customers.vue` | avatar | 同上 |
|
||||
|
||||
每个弹窗页面统一改造模式:
|
||||
1. 导入:`import { useUploadSession } from '@/composables/useUploadSession'`
|
||||
2. setup:`const uploadSession = useUploadSession()`
|
||||
3. `handleAdd`:在 `resetForm()` 后加 `uploadSession.reset()`
|
||||
4. `handleEdit`:在 `Object.assign(form, ...)` 后加 `uploadSession.reset()`(编辑模式不清理已保存的文件,仅开始新会话)
|
||||
5. 取消按钮:`@click="handleCancel"`,新增 `handleCancel` 函数:`uploadSession.cleanupPending(); dialogVisible.value = false`
|
||||
6. dialog 增加 `@closed="uploadSession.cleanupPending()"`(点击右上角 X 关闭时也触发)
|
||||
7. `handleSubmit` 成功后:`uploadSession.reset()`(标记已消费,防止 onBeforeUnmount 重复清理)
|
||||
8. 模板中所有 `<IconUpload>` 增加 `:upload-session="uploadSession"`
|
||||
|
||||
#### 页面级页面(2 个)
|
||||
|
||||
**`views/admin/Settings.vue`** — 5 个 Logo
|
||||
1. 导入 useUploadSession
|
||||
2. setup:`const uploadSession = useUploadSession()`
|
||||
3. `fetchSettings` 完成后 `uploadSession.reset()`(加载完成后开始新会话)
|
||||
4. `saveLogo` 成功后 `uploadSession.reset()`(5 个 Logo 一起保存)
|
||||
5. 模板中 5 个 IconUpload 都传 `:upload-session="uploadSession"`
|
||||
6. onBeforeUnmount 由 composable 自动 cleanupPending
|
||||
|
||||
**`views/admin/ProjectEdit.vue`** — coverImage + videoUrl
|
||||
1. 导入 useUploadSession
|
||||
2. setup:`const uploadSession = useUploadSession()`
|
||||
3. `fetchProject` 完成后 `uploadSession.reset()`
|
||||
4. `handleSave` 成功后(无论 create 还是 update):`uploadSession.reset()`
|
||||
5. `goBack` 函数:先 `await uploadSession.cleanupPending()` 再 `router.push`
|
||||
6. 模板中 IconUpload(coverImage)和 VideoUpload(videoUrl)都传 `:upload-session="uploadSession"`
|
||||
|
||||
## 验证步骤
|
||||
|
||||
### 后端编译验证
|
||||
1. 杀进程 → `dotnet build` → 启动后端
|
||||
2. 浏览器访问 `http://localhost:5041/swagger`,确认 DELETE `/api/system/upload` 端点存在
|
||||
3. 查看启动日志,确认 `PendingUploads` 表建表 SQL 执行成功
|
||||
|
||||
### 前端 HMR 验证
|
||||
1. Vite dev server 已运行(http://localhost:5173)
|
||||
2. 改动文件后观察 HMR 日志无编译错误
|
||||
3. `npm run build` 验证生产构建通过
|
||||
|
||||
### 功能场景测试(关键)
|
||||
|
||||
#### 场景 1:新增分类 - 取消表单
|
||||
1. 进入分类管理 → 点击「新增分类」
|
||||
2. 上传一个图标(看到预览)
|
||||
3. **不点确定,点取消**
|
||||
4. 预期:后端日志显示 DELETE `/api/system/upload` 调用,COS/本地存储中该文件被删除
|
||||
|
||||
#### 场景 2:新增分类 - 替换图标后取消
|
||||
1. 新增分类 → 上传图标 A
|
||||
2. 点击「更换图片」→ 上传图标 B
|
||||
3. 预期:图标 A 立即被 DELETE(前端替换时清理)
|
||||
4. 点击取消
|
||||
5. 预期:图标 B 被 DELETE(cleanupPending 清理)
|
||||
|
||||
#### 场景 3:编辑分类 - 替换图标后保存
|
||||
1. 编辑已有分类 → 上传新图标
|
||||
2. 点击确定保存
|
||||
3. 预期:后端 Service Update 方法删除旧文件(已保存的 COS 文件)+ ConsumeAsync 新文件
|
||||
|
||||
#### 场景 4:编辑分类 - 替换图标后取消
|
||||
1. 编辑已有分类(已有图标 A)→ 上传新图标 B
|
||||
2. 点击取消
|
||||
3. 预期:图标 B 被 DELETE(pending 清理);图标 A 保留(已消费,不在 pending 列表)
|
||||
|
||||
#### 场景 5:编辑分类 - 删除图标后取消
|
||||
1. 编辑已有分类(已有图标 A)→ 点击图标上的「删除」按钮
|
||||
2. 预期:图标 A **不会被删除**(currentKey 为空,因为 A 是后端加载的已保存文件,不是本次会话上传的)
|
||||
3. 点击取消
|
||||
4. 预期:无 pending 文件需要清理(currentKey 为空)
|
||||
|
||||
#### 场景 6:项目编辑 - 封面+视频,取消
|
||||
1. 进入项目编辑 → 替换封面 + 上传新视频
|
||||
2. 点击「取消」返回列表
|
||||
3. 预期:goBack 调用 cleanupPending,新上传的封面和视频都被 DELETE
|
||||
|
||||
#### 场景 7:系统设置 - Logo 上传后切换页面
|
||||
1. 进入系统设置 → 上传新 Logo(未点保存)
|
||||
2. 直接点击侧边栏跳转到其他页面
|
||||
3. 预期:onBeforeUnmount 触发 cleanupPending,新 Logo 被 DELETE
|
||||
|
||||
#### 场景 8:GC 兜底
|
||||
1. 上传文件后强制关闭浏览器(模拟用户中途离开)
|
||||
2. 等待 24 小时(或临时调整 `UploadCleanupJob.ExpireHours` 为 0 立即触发)
|
||||
3. 预期:后端 GC 任务清理该 pending 文件
|
||||
|
||||
## 假设与决策
|
||||
|
||||
- **假设 1**:后端编译只需通过,无新错误(之前已有 2 个无害 nullable 警告)
|
||||
- **假设 2**:前端 Vite dev server 在 5173 端口可用
|
||||
- **决策 1**:IconUpload 内部维护 `currentKey`,仅当 currentKey 非空时(即用户本次会话主动上传)才在 delete/replace 时调 DELETE;从后端加载的已保存文件(currentKey 为空)不删,避免误删已消费文件
|
||||
- **决策 2**:`useUploadSession` 通过 onBeforeUnmount 自动 cleanup,作为兜底;显式调用 cleanupPending 用于「取消按钮」「goBack」等需要立即清理的场景
|
||||
- **决策 3**:保存成功后必须调 `reset()` 清空 pending 列表,防止 onBeforeUnmount 重复 DELETE 已被后端消费的文件(DeletePendingAsync 会因 ConsumedAt != null 而拒绝删除,但调用本身是无意义的网络请求)
|
||||
- **决策 4**:cleanupPending 内部对每个 DELETE 用 `.catch(() => {})` 静默失败,不阻塞 UX
|
||||
- **决策 5**:批量并发删除使用 `Promise.all`,不串行等待,避免取消表单时长时间卡顿
|
||||
|
||||
## 实施顺序
|
||||
|
||||
1. 阶段 A:后端编译验证
|
||||
2. 阶段 B1:api/index.ts 改造
|
||||
3. 阶段 B2:useUploadSession composable
|
||||
4. 阶段 C1:IconUpload.vue 改造
|
||||
5. 阶段 C2:VideoUpload.vue 改造
|
||||
6. 阶段 D:9 个页面逐一改造(先 Categories.vue 跑通,再批量套用模式)
|
||||
7. 验证:HMR + 8 个功能场景测试
|
||||
383
.trae/documents/upload-garbage-cleanup.md
Normal file
383
.trae/documents/upload-garbage-cleanup.md
Normal file
@ -0,0 +1,383 @@
|
||||
# 垃圾资源清理机制实现计划
|
||||
|
||||
## Context
|
||||
|
||||
当前系统所有上传文件(图标、图片、视频、Logo 等)一旦上传到 COS/本地存储,就永远不会被删除。即使用户取消表单、替换文件、或删除实体,存储中的文件仍然残留,形成垃圾资源。本方案通过三层防御彻底解决此问题:
|
||||
|
||||
1. **前端即时清理**:取消表单时删除本次上传的文件;替换文件时立即删除旧的上传
|
||||
2. **后端保存时清理**:更新实体时如果文件 URL 变化,删除旧文件
|
||||
3. **后端 GC 兜底**:PendingUploads 表 + 定时任务,24 小时后清理未消费的孤立文件(应对浏览器关闭/刷新等极端情况)
|
||||
|
||||
## 关键架构决策(基于代码验证)
|
||||
|
||||
### 决策 1:不修改各 Service 的存储格式
|
||||
经代码验证,当前有两类存储格式:
|
||||
- **存对象键**:ProjectService(CoverImage/VideoUrl 调 ExtractKey)、ContentService(Banner.Image/Partner.Logo 调 ExtractKey)
|
||||
- **存签名URL/原值**:ProjectService(Category.Icon/Platform.Icon/Tag.Icon 直接赋值)、UserService(Avatar)、AuthService(Avatar)、SystemService(Logo 直接存值)
|
||||
|
||||
`FileStorageService.ExtractKey` 是幂等的:传入签名URL→提取key,传入key→原样返回,传入本地URL→去前导斜杠。因此**在清理/消费时调用 ExtractKey 即可归一化**,无需修改存储格式。修改存储格式会破坏前端读取路径(如 GetCategoriesAsync 返回 Icon 原值,前端 `<img :src>` 直接渲染),风险过大。
|
||||
|
||||
### 决策 2:DELETE 端点只删 pending 文件
|
||||
为防止前端误删已被实体引用的文件,DELETE `/api/system/upload` 只删除 `ConsumedAt == null` 的 PendingUpload 记录对应文件。已被消费(保存到实体)的文件通过 Service 层 Update 方法在替换时删除。
|
||||
|
||||
### 决策 3:前端通过 key 跟踪上传
|
||||
`UploadResultDto` 返回 `{ Url, Key }`。当前 IconUpload/VideoUpload 只用 Url,丢弃 Key。改造后捕获 Key,传给 `useUploadSession` 进行跟踪。DELETE 端点接收 key 参数。
|
||||
|
||||
## 垃圾场景覆盖
|
||||
|
||||
| 场景 | 前端 | 后端 | GC 兜底 |
|
||||
|------|------|------|---------|
|
||||
| 新建上传→取消弹窗 | `cleanupPending()` 调 DELETE key 删除文件 | — | 24h 后 GC |
|
||||
| 编辑上传新文件→取消弹窗 | `cleanupPending()` 删除新文件(旧文件不受影响) | — | 24h 后 GC |
|
||||
| 上传 A→上传 B 替换 | `deleteByUrl(A)` 立即调 DELETE 删 A | — | — |
|
||||
| 上传→关闭浏览器 | 无法处理 | — | 24h 后 GC |
|
||||
| 保存时替换文件 | `clear()` 不删文件 | Update 对比新旧,ExtractKey(old)≠ExtractKey(new) 则删旧文件 | — |
|
||||
| 保存新增文件 | `clear()` 不删文件 | ConsumeAsync(ExtractKey(new)) 标记消费 | — |
|
||||
| 删除实体(软删除) | — | 保留文件(支持恢复) | — |
|
||||
|
||||
## 后端实现
|
||||
|
||||
### 1. 新增实体 PendingUpload(`backend/MilliSim.Api/Common/Entities/Entities.cs` 末尾)
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 待消费上传文件记录(用于垃圾资源清理跟踪)
|
||||
/// </summary>
|
||||
public class PendingUpload : BaseEntity
|
||||
{
|
||||
/// <summary>COS 对象键或本地访问 URL</summary>
|
||||
[Required, StringLength(500)]
|
||||
public string ObjectKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>上传目录(icons/videos/logos/packages 等)</summary>
|
||||
[StringLength(50)]
|
||||
public string Folder { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>上传者用户 ID</summary>
|
||||
public long? UploadedBy { get; set; }
|
||||
|
||||
/// <summary>上传时间</summary>
|
||||
public DateTime UploadedAt { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>消费时间(null=待消费,非null=已保存到实体)</summary>
|
||||
public DateTime? ConsumedAt { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
> 注意:用 `ConsumedAt` 是否为 null 区分状态。BaseEntity 的全局软删除过滤器不影响(IsDeleted 始终为 false)。
|
||||
|
||||
### 2. DbContext 注册(`backend/MilliSim.Api/Data/MilliSimDbContext.cs`)
|
||||
|
||||
- L32 附近添加:`public DbSet<PendingUpload> PendingUploads => Set<PendingUpload>();`
|
||||
- OnModelCreating 中添加索引:
|
||||
```csharp
|
||||
modelBuilder.Entity<PendingUpload>().HasIndex(p => p.ObjectKey);
|
||||
modelBuilder.Entity<PendingUpload>().HasIndex(p => p.ConsumedAt);
|
||||
```
|
||||
|
||||
### 3. DbSeeder 建表补丁(`backend/MilliSim.Api/Data/DbSeeder.cs` 的 `PatchDatabaseSchemaAsync`)
|
||||
|
||||
在 try 块末尾添加 PendingUploads 表检测建表(EnsureCreated 不更新已有库结构,需手动建表):
|
||||
|
||||
```csharp
|
||||
// ===== PendingUploads 表:垃圾资源清理跟踪 =====
|
||||
var pendingTableExists = await db.Database.SqlQueryRaw<string>(
|
||||
"SELECT TABLE_NAME AS `Value` FROM INFORMATION_SCHEMA.TABLES " +
|
||||
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'PendingUploads'"
|
||||
).FirstOrDefaultAsync();
|
||||
|
||||
if (pendingTableExists == null)
|
||||
{
|
||||
await db.Database.ExecuteSqlRawAsync(@"
|
||||
CREATE TABLE `PendingUploads` (
|
||||
`Id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`ObjectKey` VARCHAR(500) NOT NULL,
|
||||
`Folder` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`UploadedBy` BIGINT NULL,
|
||||
`UploadedAt` DATETIME NOT NULL,
|
||||
`ConsumedAt` DATETIME NULL,
|
||||
`CreatedAt` DATETIME NOT NULL,
|
||||
`UpdatedAt` DATETIME NOT NULL,
|
||||
`IsDeleted` BIT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`Id`),
|
||||
INDEX `IX_PendingUploads_ObjectKey` (`ObjectKey`),
|
||||
INDEX `IX_PendingUploads_ConsumedAt` (`ConsumedAt`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 新增 UploadTrackingService(`backend/MilliSim.Api/Services/Infrastructure/UploadTrackingService.cs`)
|
||||
|
||||
```csharp
|
||||
public interface IUploadTrackingService
|
||||
{
|
||||
Task RegisterAsync(string objectKey, string folder, long? uploadedBy);
|
||||
Task ConsumeAsync(string objectKey);
|
||||
Task<bool> DeletePendingAsync(string objectKey); // 返回是否实际删除(仅删 pending)
|
||||
Task<int> CleanupExpiredAsync(int expireHours = 24);
|
||||
}
|
||||
|
||||
public class UploadTrackingService : IUploadTrackingService
|
||||
{
|
||||
// RegisterAsync: 插入 PendingUpload 记录(ObjectKey, Folder, UploadedBy, UploadedAt=now, ConsumedAt=null)
|
||||
// ConsumeAsync: 按 ObjectKey 查找 ConsumedAt==null 的记录,设 ConsumedAt=now;找不到则静默跳过
|
||||
// DeletePendingAsync: 查 ConsumedAt==null && ObjectKey==key 的记录,调 _fileStorage.DeleteAsync(key) 删文件 + 删记录;找不到返回 false
|
||||
// CleanupExpiredAsync: 查 ConsumedAt==null && UploadedAt < now-expireHours,逐条删文件 + 删记录,返回清理数量
|
||||
}
|
||||
```
|
||||
|
||||
依赖注入:`MilliSimDbContext`、`IFileStorageService`、`ILogger<UploadTrackingService>`。
|
||||
|
||||
### 5. 修改 SystemController(`backend/MilliSim.Api/Controllers/SystemController.cs`)
|
||||
|
||||
**构造函数**:注入 `IUploadTrackingService`、`ICurrentUserService`。
|
||||
|
||||
**Upload 方法**(L147-175):上传成功后注册 PendingUpload:
|
||||
```csharp
|
||||
var fileUrl = await _fileStorage.UploadAsync(file, folder);
|
||||
var signedUrl = _fileStorage.GetSignedUrl(fileUrl);
|
||||
// 注册到 PendingUpload 跟踪表
|
||||
await _uploadTracking.RegisterAsync(fileUrl, folder, _currentUser.UserId);
|
||||
return ApiResponse<UploadResultDto>.Success(new UploadResultDto { Url = signedUrl, Key = fileUrl });
|
||||
```
|
||||
|
||||
**新增 DELETE 端点**(在 Upload 方法后):
|
||||
```csharp
|
||||
[HttpDelete("upload")]
|
||||
[Authorize(Policy = "Staff")]
|
||||
public async Task<ApiResponse> DeleteUpload([FromQuery] string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
return ApiResponse.Fail("文件 key 不能为空");
|
||||
var deleted = await _uploadTracking.DeletePendingAsync(key);
|
||||
return ApiResponse.Success(deleted ? "已删除" : "文件不存在或已使用");
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 修改各业务 Service(消费 + 旧文件删除)
|
||||
|
||||
**核心模式**:每个 Service 注入 `IUploadTrackingService`。Create 方法保存后调 `ConsumeAsync(ExtractKey(fileValue))`;Update 方法先记录旧值,保存后对比新旧,不同则删旧文件 + 消费新文件。
|
||||
|
||||
> `ExtractKey` 幂等:签名URL→key,key→key。因此无论存储格式如何都能正确归一化。
|
||||
|
||||
| Service | 方法 | 文件字段 | 处理 |
|
||||
|---------|------|---------|------|
|
||||
| `ProjectService` | CreateProjectAsync L128 | CoverImage, VideoUrl | 保存后 ConsumeAsync(ExtractKey(CoverImage))、ConsumeAsync(ExtractKey(VideoUrl)) |
|
||||
| `ProjectService` | UpdateProjectAsync L157 | CoverImage, VideoUrl | 记录旧 CoverImage/VideoUrl;保存后 ExtractKey(旧)≠ExtractKey(新) 则 DeleteAsync(旧);ConsumeAsync(新)。VideoUrl 跳过 `/api/projects/` 代理 URL |
|
||||
| `ProjectService` | CreateCategoryAsync L449 | Icon | 保存后 ConsumeAsync(ExtractKey(Icon)) |
|
||||
| `ProjectService` | UpdateCategoryAsync L467 | Icon | 记录旧 Icon;保存后 ExtractKey(旧)≠ExtractKey(新) 则 DeleteAsync(ExtractKey(旧));ConsumeAsync(ExtractKey(新)) |
|
||||
| `ProjectService` | Create/UpdatePlatformAsync | Icon | 同 Category |
|
||||
| `ProjectService` | Create/UpdateTagAsync | Icon | 同 Category |
|
||||
| `ContentService` | CreateBannerAsync L63 | Image | 保存后 ConsumeAsync(ExtractKey(Image)) |
|
||||
| `ContentService` | UpdateBannerAsync L89 | Image | 记录旧 Image;ExtractKey(旧)≠ExtractKey(新) 则 DeleteAsync;ConsumeAsync(新) |
|
||||
| `ContentService` | Create/UpdatePartnerAsync | Logo | 同 Banner |
|
||||
| `UserService` | CreateUserAsync L139 | Avatar | 保存后 ConsumeAsync(ExtractKey(Avatar)) |
|
||||
| `UserService` | UpdateUserAsync L199 | Avatar | 记录旧 Avatar;ExtractKey(旧)≠ExtractKey(新) 则 DeleteAsync;ConsumeAsync(新) |
|
||||
| `AuthService` | UpdateProfileAsync L140 | Avatar | 注入 IFileStorageService + IUploadTrackingService;同 UpdateUserAsync |
|
||||
| `SystemService` | UpdateSettingsAsync L54 | Logo* 五项 | 查旧值→保存→对比 LogoFrontend/LogoBackend/LogoFavicon/LogoLogin/LogoFooter,不同则删旧+消费新 |
|
||||
|
||||
**SystemService.UpdateSettingsAsync 特殊处理**(Logo 五项):
|
||||
```csharp
|
||||
// 保存前查询旧 Logo 值
|
||||
var logoKeys = new[] { "LogoFrontend", "LogoBackend", "LogoFavicon", "LogoLogin", "LogoFooter" };
|
||||
var oldLogos = await _db.SystemSettings
|
||||
.Where(s => logoKeys.Contains(s.Key))
|
||||
.ToDictionaryAsync(s => s.Key, s => s.Value);
|
||||
|
||||
// ... 原有保存逻辑 ...
|
||||
|
||||
// 保存后对比 Logo 变化
|
||||
foreach (var logoKey in logoKeys)
|
||||
{
|
||||
if (request.Settings.TryGetValue(logoKey, out var newValue))
|
||||
{
|
||||
var oldKey = _fileStorage.ExtractKey(oldLogos.GetValueOrDefault(logoKey) ?? "");
|
||||
var newKey = _fileStorage.ExtractKey(newValue);
|
||||
if (!string.IsNullOrEmpty(oldKey) && oldKey != newKey)
|
||||
{
|
||||
await _fileStorage.DeleteAsync(oldKey);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(newKey))
|
||||
{
|
||||
await _uploadTracking.ConsumeAsync(newKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. 新增 GC 后台任务(`backend/MilliSim.Api/Services/UploadCleanupJob.cs`)
|
||||
|
||||
参照 `AuthorizationNotificationJob` 模式(IHostedService + Timer + CreateScope):
|
||||
- 每 6 小时执行一次 `CleanupExpiredAsync(24)`
|
||||
- 清理 `ConsumedAt == null && UploadedAt < now - 24h` 的记录及对应文件
|
||||
|
||||
### 8. Program.cs 注册
|
||||
|
||||
```csharp
|
||||
builder.Services.AddScoped<IUploadTrackingService, UploadTrackingService>();
|
||||
builder.Services.AddHostedService<UploadCleanupJob>();
|
||||
```
|
||||
|
||||
## 前端实现
|
||||
|
||||
### 9. 新增 API 方法(`frontend/src/api/index.ts` 的 systemApi)
|
||||
|
||||
```typescript
|
||||
// 删除未使用的上传文件(仅限 pending 状态)
|
||||
deleteUpload: (key: string) => http.delete<ApiResponse>('/system/upload', { params: { key } }),
|
||||
```
|
||||
|
||||
同时修正 upload 方法的返回类型:
|
||||
```typescript
|
||||
upload: (formData: FormData) => http.upload<ApiResponse<{ url: string; key: string }>>('/system/upload', formData),
|
||||
```
|
||||
|
||||
### 10. 新增 Composable(`frontend/src/composables/useUploadSession.ts`)
|
||||
|
||||
需先创建 `frontend/src/composables/` 目录。
|
||||
|
||||
```typescript
|
||||
import { reactive } from 'vue'
|
||||
import { systemApi } from '@/api'
|
||||
|
||||
interface TrackedUpload {
|
||||
key: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export function useUploadSession() {
|
||||
const tracked = reactive<TrackedUpload[]>([])
|
||||
|
||||
/** 上传成功后注册跟踪 */
|
||||
function track(key: string, url: string) {
|
||||
if (key) tracked.push({ key, url })
|
||||
}
|
||||
|
||||
/** 替换时删除旧上传(仅在本次会话跟踪中的) */
|
||||
async function deleteByUrl(url: string) {
|
||||
if (!url) return
|
||||
const idx = tracked.findIndex(t => t.url === url)
|
||||
if (idx === -1) return // 不在跟踪列表中(可能是已保存的旧文件),不删
|
||||
const item = tracked[idx]
|
||||
try {
|
||||
await systemApi.deleteUpload(item.key)
|
||||
} catch { /* 忽略删除失败 */ }
|
||||
tracked.splice(idx, 1)
|
||||
}
|
||||
|
||||
/** 取消表单时删除全部跟踪文件 */
|
||||
async function cleanupPending() {
|
||||
for (const item of [...tracked]) {
|
||||
try {
|
||||
await systemApi.deleteUpload(item.key)
|
||||
} catch { /* 忽略 */ }
|
||||
}
|
||||
tracked.length = 0
|
||||
}
|
||||
|
||||
/** 保存成功后清空跟踪(不删文件,后端标记消费) */
|
||||
function clear() {
|
||||
tracked.length = 0
|
||||
}
|
||||
|
||||
return { tracked, track, deleteByUrl, cleanupPending, clear }
|
||||
}
|
||||
```
|
||||
|
||||
### 11. 修改 IconUpload.vue / VideoUpload.vue
|
||||
|
||||
**IconUpload.vue 改造点**:
|
||||
|
||||
1. 新增可选 prop:`uploadSession?: ReturnType<typeof useUploadSession>`
|
||||
2. `uploadFile` 成功后:
|
||||
- 如果当前 `modelValue` 在 session 跟踪中(替换场景),先 `uploadSession.deleteByUrl(modelValue)`
|
||||
- 如果有 session,`uploadSession.track(response.data.key, response.data.url)`
|
||||
- `emit('update:modelValue', response.data.url)`
|
||||
3. `handleDelete`:如果有 session 且当前值在跟踪中,`uploadSession.deleteByUrl(modelValue)` 后再 emit 空
|
||||
4. XHR 响应类型增加 `key` 字段:`{ code: number; data?: { url: string; key: string } }`
|
||||
|
||||
**VideoUpload.vue**:同样改造(如有)。
|
||||
|
||||
> 不传 uploadSession 时行为完全不变(向后兼容)。
|
||||
|
||||
### 12. 修改表单页面
|
||||
|
||||
**弹窗模式页面**(Categories/Banners/Partners/Platforms/Tags/Employees/Customers):
|
||||
```typescript
|
||||
import { useUploadSession } from '@/composables/useUploadSession'
|
||||
const uploadSession = useUploadSession()
|
||||
|
||||
function handleAdd() {
|
||||
uploadSession.clear() // 重置跟踪
|
||||
resetForm()
|
||||
// ...
|
||||
}
|
||||
function handleEdit(row) {
|
||||
uploadSession.clear() // 重置跟踪(编辑已有数据,旧文件不在跟踪中)
|
||||
// 填充表单...
|
||||
}
|
||||
// 取消按钮改为 async
|
||||
async function handleCancel() {
|
||||
await uploadSession.cleanupPending()
|
||||
dialogVisible.value = false
|
||||
}
|
||||
async function handleSubmit() {
|
||||
// ... 保存逻辑 ...
|
||||
uploadSession.clear() // 保存成功,不删文件
|
||||
dialogVisible.value = false
|
||||
}
|
||||
```
|
||||
|
||||
模板传入:`<IconUpload v-model="form.icon" :upload-session="uploadSession" folder="..." />`
|
||||
|
||||
**页面级模式**(Settings/ProjectEdit):
|
||||
```typescript
|
||||
const uploadSession = useUploadSession()
|
||||
// 保存成功后
|
||||
function onSaveSuccess() {
|
||||
uploadSession.clear()
|
||||
}
|
||||
// 离开页面时清理(onBeforeRouteLeave 或 onBeforeUnmount)
|
||||
import { onBeforeRouteLeave } from 'vue-router'
|
||||
onBeforeRouteLeave(async () => {
|
||||
await uploadSession.cleanupPending()
|
||||
})
|
||||
```
|
||||
|
||||
**Settings.vue**:5 个 Logo 共享一个 uploadSession,saveLogo 成功后 clear()。
|
||||
|
||||
**ProjectEdit.vue**:CoverImage + VideoUpload 共享一个 uploadSession,保存成功后 clear(),goBack 前 cleanupPending()。
|
||||
|
||||
### 需修改的页面清单
|
||||
|
||||
| 页面 | 类型 | 上传组件 | 字段 |
|
||||
|------|------|---------|------|
|
||||
| Categories.vue | 弹窗 | IconUpload | icon |
|
||||
| Platforms.vue | 弹窗 | IconUpload | icon |
|
||||
| Tags.vue | 弹窗 | IconUpload | icon |
|
||||
| Banners.vue | 弹窗 | IconUpload | image |
|
||||
| Partners.vue | 弹窗 | IconUpload | logo |
|
||||
| Employees.vue | 弹窗 | IconUpload | avatar |
|
||||
| Customers.vue | 弹窗 | IconUpload | avatar |
|
||||
| Settings.vue | 页面级 | IconUpload×5 | Logo*5 |
|
||||
| ProjectEdit.vue | 页面级 | IconUpload + VideoUpload | coverImage + video |
|
||||
|
||||
## 实现顺序
|
||||
|
||||
1. **后端基础**:PendingUpload 实体 → DbContext → DbSeeder → UploadTrackingService → Program.cs 注册
|
||||
2. **后端端点**:SystemController Upload 注册 + DELETE 端点
|
||||
3. **后端 GC**:UploadCleanupJob → Program.cs 注册
|
||||
4. **后端业务**:各 Service 注入 + 消费 + 旧文件删除(ProjectService、ContentService、UserService、AuthService、SystemService)
|
||||
5. **前端基础**:API 方法 → useUploadSession composable
|
||||
6. **前端组件**:IconUpload + VideoUpload 改造
|
||||
7. **前端页面**:9 个页面统一接入
|
||||
|
||||
## 验证方案
|
||||
|
||||
1. 新建分类→上传图标→取消→检查 DELETE `/api/system/upload?key=xxx` 被调用,文件已删
|
||||
2. 新建分类→上传 A→上传 B 替换→检查 A 的 DELETE 被调用
|
||||
3. 新建分类→上传图标→保存→检查无 DELETE 调用,PendingUpload 记录 ConsumedAt 已设置
|
||||
4. 编辑分类→替换图标→保存→检查旧文件被删(后端 Service 层 DeleteAsync)
|
||||
5. 上传图标→关闭浏览器→等待 GC 执行→检查文件已清理
|
||||
6. 后端编译通过 + 前端 HMR 无报错
|
||||
7. 确认已有数据的图标显示正常(未改存储格式,不应受影响)
|
||||
292
.trae/skills/ui-ux-pro-max/SKILL.md
Normal file
292
.trae/skills/ui-ux-pro-max/SKILL.md
Normal file
@ -0,0 +1,292 @@
|
||||
---
|
||||
name: ui-ux-pro-max
|
||||
description: UI/UX design intelligence with searchable database
|
||||
---
|
||||
# ui-ux-pro-max
|
||||
|
||||
Comprehensive design guide for web and mobile applications. Contains 67 styles, 96 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 13 technology stacks. Searchable database with priority-based recommendations.
|
||||
|
||||
## 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: Generate Design System (REQUIRED)
|
||||
|
||||
**Always start with `--design-system`** to get comprehensive recommendations with reasoning:
|
||||
|
||||
```bash
|
||||
python3 skills/ui-ux-pro-max/scripts/search.py "<product_type> <industry> <keywords>" --design-system [-p "Project Name"]
|
||||
```
|
||||
|
||||
This command:
|
||||
1. Searches 5 domains in parallel (product, style, color, landing, typography)
|
||||
2. Applies reasoning rules from `ui-reasoning.csv` to select best matches
|
||||
3. Returns complete design system: pattern, style, colors, typography, effects
|
||||
4. Includes anti-patterns to avoid
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
python3 skills/ui-ux-pro-max/scripts/search.py "beauty spa wellness service" --design-system -p "Serenity Spa"
|
||||
```
|
||||
|
||||
### Step 2b: Persist Design System (Master + Overrides Pattern)
|
||||
|
||||
To save the design system for hierarchical retrieval across sessions, add `--persist`:
|
||||
|
||||
```bash
|
||||
python3 skills/ui-ux-pro-max/scripts/search.py "<query>" --design-system --persist -p "Project Name"
|
||||
```
|
||||
|
||||
This creates:
|
||||
- `design-system/MASTER.md` — Global Source of Truth with all design rules
|
||||
- `design-system/pages/` — Folder for page-specific overrides
|
||||
|
||||
**With page-specific override:**
|
||||
```bash
|
||||
python3 skills/ui-ux-pro-max/scripts/search.py "<query>" --design-system --persist -p "Project Name" --page "dashboard"
|
||||
```
|
||||
|
||||
This also creates:
|
||||
- `design-system/pages/dashboard.md` — Page-specific deviations from Master
|
||||
|
||||
**How hierarchical retrieval works:**
|
||||
1. When building a specific page (e.g., "Checkout"), first check `design-system/pages/checkout.md`
|
||||
2. If the page file exists, its rules **override** the Master file
|
||||
3. If not, use `design-system/MASTER.md` exclusively
|
||||
|
||||
### Step 3: Supplement with Detailed Searches (as needed)
|
||||
|
||||
After getting the design system, use domain searches to get additional details:
|
||||
|
||||
```bash
|
||||
python3 skills/ui-ux-pro-max/scripts/search.py "<keyword>" --domain <domain> [-n <max_results>]
|
||||
```
|
||||
|
||||
**When to use detailed searches:**
|
||||
|
||||
| Need | Domain | Example |
|
||||
|------|--------|---------|
|
||||
| More style options | `style` | `--domain style "glassmorphism dark"` |
|
||||
| Chart recommendations | `chart` | `--domain chart "real-time dashboard"` |
|
||||
| UX best practices | `ux` | `--domain ux "animation accessibility"` |
|
||||
| Alternative fonts | `typography` | `--domain typography "elegant luxury"` |
|
||||
| Landing structure | `landing` | `--domain landing "hero social-proof"` |
|
||||
|
||||
### Step 4: Stack Guidelines (Default: html-tailwind)
|
||||
|
||||
Get implementation-specific best practices. If user doesn't specify a stack, **default to `html-tailwind`**.
|
||||
|
||||
```bash
|
||||
python3 skills/ui-ux-pro-max/scripts/search.py "<keyword>" --stack html-tailwind
|
||||
```
|
||||
|
||||
Available stacks: `html-tailwind`, `react`, `nextjs`, `vue`, `svelte`, `swiftui`, `react-native`, `flutter`, `shadcn`, `jetpack-compose`
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
| `react` | React/Next.js performance | waterfall, bundle, suspense, memo, rerender, cache |
|
||||
| `web` | Web interface guidelines | aria, focus, keyboard, semantic, virtualize |
|
||||
| `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 |
|
||||
| `shadcn` | shadcn/ui components, theming, forms, patterns |
|
||||
| `jetpack-compose` | Composables, Modifiers, State Hoisting, Recomposition |
|
||||
|
||||
---
|
||||
|
||||
## Example Workflow
|
||||
|
||||
**User request:** "Làm landing page cho dịch vụ chăm sóc da chuyên nghiệp"
|
||||
|
||||
### Step 1: Analyze Requirements
|
||||
- Product type: Beauty/Spa service
|
||||
- Style keywords: elegant, professional, soft
|
||||
- Industry: Beauty/Wellness
|
||||
- Stack: html-tailwind (default)
|
||||
|
||||
### Step 2: Generate Design System (REQUIRED)
|
||||
|
||||
```bash
|
||||
python3 skills/ui-ux-pro-max/scripts/search.py "beauty spa wellness service elegant" --design-system -p "Serenity Spa"
|
||||
```
|
||||
|
||||
**Output:** Complete design system with pattern, style, colors, typography, effects, and anti-patterns.
|
||||
|
||||
### Step 3: Supplement with Detailed Searches (as needed)
|
||||
|
||||
```bash
|
||||
# Get UX guidelines for animation and accessibility
|
||||
python3 skills/ui-ux-pro-max/scripts/search.py "animation accessibility" --domain ux
|
||||
|
||||
# Get alternative typography options if needed
|
||||
python3 skills/ui-ux-pro-max/scripts/search.py "elegant luxury serif" --domain typography
|
||||
```
|
||||
|
||||
### Step 4: Stack Guidelines
|
||||
|
||||
```bash
|
||||
python3 skills/ui-ux-pro-max/scripts/search.py "layout responsive form" --stack html-tailwind
|
||||
```
|
||||
|
||||
**Then:** Synthesize design system + detailed searches and implement the design.
|
||||
|
||||
---
|
||||
|
||||
## Output Formats
|
||||
|
||||
The `--design-system` flag supports two output formats:
|
||||
|
||||
```bash
|
||||
# ASCII box (default) - best for terminal display
|
||||
python3 skills/ui-ux-pro-max/scripts/search.py "fintech crypto" --design-system
|
||||
|
||||
# Markdown - best for documentation
|
||||
python3 skills/ui-ux-pro-max/scripts/search.py "fintech crypto" --design-system -f markdown
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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 375px, 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
.trae/skills/ui-ux-pro-max/data/charts.csv
Normal file
26
.trae/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
|
||||
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
|
||||
|
97
.trae/skills/ui-ux-pro-max/data/colors.csv
Normal file
97
.trae/skills/ui-ux-pro-max/data/colors.csv
Normal file
@ -0,0 +1,97 @@
|
||||
No,Product Type,Primary (Hex),Secondary (Hex),CTA (Hex),Background (Hex),Text (Hex),Border (Hex),Notes
|
||||
1,SaaS (General),#2563EB,#3B82F6,#F97316,#F8FAFC,#1E293B,#E2E8F0,Trust blue + orange CTA contrast
|
||||
2,Micro SaaS,#6366F1,#818CF8,#10B981,#F5F3FF,#1E1B4B,#E0E7FF,Indigo primary + emerald CTA
|
||||
3,E-commerce,#059669,#10B981,#F97316,#ECFDF5,#064E3B,#A7F3D0,Success green + urgency orange
|
||||
4,E-commerce Luxury,#1C1917,#44403C,#CA8A04,#FAFAF9,#0C0A09,#D6D3D1,Premium dark + gold accent
|
||||
5,Service Landing Page,#0EA5E9,#38BDF8,#F97316,#F0F9FF,#0C4A6E,#BAE6FD,Sky blue trust + warm CTA
|
||||
6,B2B Service,#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Professional navy + blue CTA
|
||||
7,Financial Dashboard,#0F172A,#1E293B,#22C55E,#020617,#F8FAFC,#334155,Dark bg + green positive indicators
|
||||
8,Analytics Dashboard,#1E40AF,#3B82F6,#F59E0B,#F8FAFC,#1E3A8A,#DBEAFE,Blue data + amber highlights
|
||||
9,Healthcare App,#0891B2,#22D3EE,#059669,#ECFEFF,#164E63,#A5F3FC,Calm cyan + health green
|
||||
10,Educational App,#4F46E5,#818CF8,#F97316,#EEF2FF,#1E1B4B,#C7D2FE,Playful indigo + energetic orange
|
||||
11,Creative Agency,#EC4899,#F472B6,#06B6D4,#FDF2F8,#831843,#FBCFE8,Bold pink + cyan accent
|
||||
12,Portfolio/Personal,#18181B,#3F3F46,#2563EB,#FAFAFA,#09090B,#E4E4E7,Monochrome + blue accent
|
||||
13,Gaming,#7C3AED,#A78BFA,#F43F5E,#0F0F23,#E2E8F0,#4C1D95,Neon purple + rose action
|
||||
14,Government/Public Service,#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,High contrast navy + blue
|
||||
15,Fintech/Crypto,#F59E0B,#FBBF24,#8B5CF6,#0F172A,#F8FAFC,#334155,Gold trust + purple tech
|
||||
16,Social Media App,#E11D48,#FB7185,#2563EB,#FFF1F2,#881337,#FECDD3,Vibrant rose + engagement blue
|
||||
17,Productivity Tool,#0D9488,#14B8A6,#F97316,#F0FDFA,#134E4A,#99F6E4,Teal focus + action orange
|
||||
18,Design System/Component Library,#4F46E5,#6366F1,#F97316,#EEF2FF,#312E81,#C7D2FE,Indigo brand + doc hierarchy
|
||||
19,AI/Chatbot Platform,#7C3AED,#A78BFA,#06B6D4,#FAF5FF,#1E1B4B,#DDD6FE,AI purple + cyan interactions
|
||||
20,NFT/Web3 Platform,#8B5CF6,#A78BFA,#FBBF24,#0F0F23,#F8FAFC,#4C1D95,Purple tech + gold value
|
||||
21,Creator Economy Platform,#EC4899,#F472B6,#F97316,#FDF2F8,#831843,#FBCFE8,Creator pink + engagement orange
|
||||
22,Sustainability/ESG Platform,#059669,#10B981,#0891B2,#ECFDF5,#064E3B,#A7F3D0,Nature green + ocean blue
|
||||
23,Remote Work/Collaboration Tool,#6366F1,#818CF8,#10B981,#F5F3FF,#312E81,#E0E7FF,Calm indigo + success green
|
||||
24,Mental Health App,#8B5CF6,#C4B5FD,#10B981,#FAF5FF,#4C1D95,#EDE9FE,Calming lavender + wellness green
|
||||
25,Pet Tech App,#F97316,#FB923C,#2563EB,#FFF7ED,#9A3412,#FED7AA,Playful orange + trust blue
|
||||
26,Smart Home/IoT Dashboard,#1E293B,#334155,#22C55E,#0F172A,#F8FAFC,#475569,Dark tech + status green
|
||||
27,EV/Charging Ecosystem,#0891B2,#22D3EE,#22C55E,#ECFEFF,#164E63,#A5F3FC,Electric cyan + eco green
|
||||
28,Subscription Box Service,#D946EF,#E879F9,#F97316,#FDF4FF,#86198F,#F5D0FE,Excitement purple + urgency orange
|
||||
29,Podcast Platform,#1E1B4B,#312E81,#F97316,#0F0F23,#F8FAFC,#4338CA,Dark audio + warm accent
|
||||
30,Dating App,#E11D48,#FB7185,#F97316,#FFF1F2,#881337,#FECDD3,Romantic rose + warm orange
|
||||
31,Micro-Credentials/Badges Platform,#0369A1,#0EA5E9,#CA8A04,#F0F9FF,#0C4A6E,#BAE6FD,Trust blue + achievement gold
|
||||
32,Knowledge Base/Documentation,#475569,#64748B,#2563EB,#F8FAFC,#1E293B,#E2E8F0,Neutral grey + link blue
|
||||
33,Hyperlocal Services,#059669,#10B981,#F97316,#ECFDF5,#064E3B,#A7F3D0,Location green + action orange
|
||||
34,Beauty/Spa/Wellness Service,#EC4899,#F9A8D4,#8B5CF6,#FDF2F8,#831843,#FBCFE8,Soft pink + lavender luxury
|
||||
35,Luxury/Premium Brand,#1C1917,#44403C,#CA8A04,#FAFAF9,#0C0A09,#D6D3D1,Premium black + gold accent
|
||||
36,Restaurant/Food Service,#DC2626,#F87171,#CA8A04,#FEF2F2,#450A0A,#FECACA,Appetizing red + warm gold
|
||||
37,Fitness/Gym App,#F97316,#FB923C,#22C55E,#1F2937,#F8FAFC,#374151,Energy orange + success green
|
||||
38,Real Estate/Property,#0F766E,#14B8A6,#0369A1,#F0FDFA,#134E4A,#99F6E4,Trust teal + professional blue
|
||||
39,Travel/Tourism Agency,#0EA5E9,#38BDF8,#F97316,#F0F9FF,#0C4A6E,#BAE6FD,Sky blue + adventure orange
|
||||
40,Hotel/Hospitality,#1E3A8A,#3B82F6,#CA8A04,#F8FAFC,#1E40AF,#BFDBFE,Luxury navy + gold service
|
||||
41,Wedding/Event Planning,#DB2777,#F472B6,#CA8A04,#FDF2F8,#831843,#FBCFE8,Romantic pink + elegant gold
|
||||
42,Legal Services,#1E3A8A,#1E40AF,#B45309,#F8FAFC,#0F172A,#CBD5E1,Authority navy + trust gold
|
||||
43,Insurance Platform,#0369A1,#0EA5E9,#22C55E,#F0F9FF,#0C4A6E,#BAE6FD,Security blue + protected green
|
||||
44,Banking/Traditional Finance,#0F172A,#1E3A8A,#CA8A04,#F8FAFC,#020617,#E2E8F0,Trust navy + premium gold
|
||||
45,Online Course/E-learning,#0D9488,#2DD4BF,#F97316,#F0FDFA,#134E4A,#5EEAD4,Progress teal + achievement orange
|
||||
46,Non-profit/Charity,#0891B2,#22D3EE,#F97316,#ECFEFF,#164E63,#A5F3FC,Compassion blue + action orange
|
||||
47,Music Streaming,#1E1B4B,#4338CA,#22C55E,#0F0F23,#F8FAFC,#312E81,Dark audio + play green
|
||||
48,Video Streaming/OTT,#0F0F23,#1E1B4B,#E11D48,#000000,#F8FAFC,#312E81,Cinema dark + play red
|
||||
49,Job Board/Recruitment,#0369A1,#0EA5E9,#22C55E,#F0F9FF,#0C4A6E,#BAE6FD,Professional blue + success green
|
||||
50,Marketplace (P2P),#7C3AED,#A78BFA,#22C55E,#FAF5FF,#4C1D95,#DDD6FE,Trust purple + transaction green
|
||||
51,Logistics/Delivery,#2563EB,#3B82F6,#F97316,#EFF6FF,#1E40AF,#BFDBFE,Tracking blue + delivery orange
|
||||
52,Agriculture/Farm Tech,#15803D,#22C55E,#CA8A04,#F0FDF4,#14532D,#BBF7D0,Earth green + harvest gold
|
||||
53,Construction/Architecture,#64748B,#94A3B8,#F97316,#F8FAFC,#334155,#E2E8F0,Industrial grey + safety orange
|
||||
54,Automotive/Car Dealership,#1E293B,#334155,#DC2626,#F8FAFC,#0F172A,#E2E8F0,Premium dark + action red
|
||||
55,Photography Studio,#18181B,#27272A,#F8FAFC,#000000,#FAFAFA,#3F3F46,Pure black + white contrast
|
||||
56,Coworking Space,#F59E0B,#FBBF24,#2563EB,#FFFBEB,#78350F,#FDE68A,Energetic amber + booking blue
|
||||
57,Cleaning Service,#0891B2,#22D3EE,#22C55E,#ECFEFF,#164E63,#A5F3FC,Fresh cyan + clean green
|
||||
58,Home Services (Plumber/Electrician),#1E40AF,#3B82F6,#F97316,#EFF6FF,#1E3A8A,#BFDBFE,Professional blue + urgent orange
|
||||
59,Childcare/Daycare,#F472B6,#FBCFE8,#22C55E,#FDF2F8,#9D174D,#FCE7F3,Soft pink + safe green
|
||||
60,Senior Care/Elderly,#0369A1,#38BDF8,#22C55E,#F0F9FF,#0C4A6E,#E0F2FE,Calm blue + reassuring green
|
||||
61,Medical Clinic,#0891B2,#22D3EE,#22C55E,#F0FDFA,#134E4A,#CCFBF1,Medical teal + health green
|
||||
62,Pharmacy/Drug Store,#15803D,#22C55E,#0369A1,#F0FDF4,#14532D,#BBF7D0,Pharmacy green + trust blue
|
||||
63,Dental Practice,#0EA5E9,#38BDF8,#FBBF24,#F0F9FF,#0C4A6E,#BAE6FD,Fresh blue + smile yellow
|
||||
64,Veterinary Clinic,#0D9488,#14B8A6,#F97316,#F0FDFA,#134E4A,#99F6E4,Caring teal + warm orange
|
||||
65,Florist/Plant Shop,#15803D,#22C55E,#EC4899,#F0FDF4,#14532D,#BBF7D0,Natural green + floral pink
|
||||
66,Bakery/Cafe,#92400E,#B45309,#F8FAFC,#FEF3C7,#78350F,#FDE68A,Warm brown + cream white
|
||||
67,Coffee Shop,#78350F,#92400E,#FBBF24,#FEF3C7,#451A03,#FDE68A,Coffee brown + warm gold
|
||||
68,Brewery/Winery,#7C2D12,#B91C1C,#CA8A04,#FEF2F2,#450A0A,#FECACA,Deep burgundy + craft gold
|
||||
69,Airline,#1E3A8A,#3B82F6,#F97316,#EFF6FF,#1E40AF,#BFDBFE,Sky blue + booking orange
|
||||
70,News/Media Platform,#DC2626,#EF4444,#1E40AF,#FEF2F2,#450A0A,#FECACA,Breaking red + link blue
|
||||
71,Magazine/Blog,#18181B,#3F3F46,#EC4899,#FAFAFA,#09090B,#E4E4E7,Editorial black + accent pink
|
||||
72,Freelancer Platform,#6366F1,#818CF8,#22C55E,#EEF2FF,#312E81,#C7D2FE,Creative indigo + hire green
|
||||
73,Consulting Firm,#0F172A,#334155,#CA8A04,#F8FAFC,#020617,#E2E8F0,Authority navy + premium gold
|
||||
74,Marketing Agency,#EC4899,#F472B6,#06B6D4,#FDF2F8,#831843,#FBCFE8,Bold pink + creative cyan
|
||||
75,Event Management,#7C3AED,#A78BFA,#F97316,#FAF5FF,#4C1D95,#DDD6FE,Excitement purple + action orange
|
||||
76,Conference/Webinar Platform,#1E40AF,#3B82F6,#22C55E,#EFF6FF,#1E3A8A,#BFDBFE,Professional blue + join green
|
||||
77,Membership/Community,#7C3AED,#A78BFA,#22C55E,#FAF5FF,#4C1D95,#DDD6FE,Community purple + join green
|
||||
78,Newsletter Platform,#0369A1,#0EA5E9,#F97316,#F0F9FF,#0C4A6E,#BAE6FD,Trust blue + subscribe orange
|
||||
79,Digital Products/Downloads,#6366F1,#818CF8,#22C55E,#EEF2FF,#312E81,#C7D2FE,Digital indigo + buy green
|
||||
80,Church/Religious Organization,#7C3AED,#A78BFA,#CA8A04,#FAF5FF,#4C1D95,#DDD6FE,Spiritual purple + warm gold
|
||||
81,Sports Team/Club,#DC2626,#EF4444,#FBBF24,#FEF2F2,#7F1D1D,#FECACA,Team red + championship gold
|
||||
82,Museum/Gallery,#18181B,#27272A,#F8FAFC,#FAFAFA,#09090B,#E4E4E7,Gallery black + white space
|
||||
83,Theater/Cinema,#1E1B4B,#312E81,#CA8A04,#0F0F23,#F8FAFC,#4338CA,Dramatic dark + spotlight gold
|
||||
84,Language Learning App,#4F46E5,#818CF8,#22C55E,#EEF2FF,#312E81,#C7D2FE,Learning indigo + progress green
|
||||
85,Coding Bootcamp,#0F172A,#1E293B,#22C55E,#020617,#F8FAFC,#334155,Terminal dark + success green
|
||||
86,Cybersecurity Platform,#00FF41,#0D0D0D,#FF3333,#000000,#E0E0E0,#1F1F1F,Matrix green + alert red
|
||||
87,Developer Tool / IDE,#1E293B,#334155,#22C55E,#0F172A,#F8FAFC,#475569,Code dark + run green
|
||||
88,Biotech / Life Sciences,#0EA5E9,#0284C7,#10B981,#F0F9FF,#0C4A6E,#BAE6FD,DNA blue + life green
|
||||
89,Space Tech / Aerospace,#F8FAFC,#94A3B8,#3B82F6,#0B0B10,#F8FAFC,#1E293B,Star white + launch blue
|
||||
90,Architecture / Interior,#171717,#404040,#D4AF37,#FFFFFF,#171717,#E5E5E5,Minimal black + accent gold
|
||||
91,Quantum Computing,#00FFFF,#7B61FF,#FF00FF,#050510,#E0E0FF,#333344,Quantum cyan + interference purple
|
||||
92,Biohacking / Longevity,#FF4D4D,#4D94FF,#00E676,#F5F5F7,#1C1C1E,#E5E5EA,Bio red/blue + vitality green
|
||||
93,Autonomous Systems,#00FF41,#008F11,#FF3333,#0D1117,#E6EDF3,#30363D,Terminal green + alert red
|
||||
94,Generative AI Art,#18181B,#3F3F46,#EC4899,#FAFAFA,#09090B,#E4E4E7,Canvas neutral + creative pink
|
||||
95,Spatial / Vision OS,#FFFFFF,#E5E5E5,#007AFF,#888888,#000000,#CCCCCC,Glass white + system blue
|
||||
96,Climate Tech,#059669,#10B981,#FBBF24,#ECFDF5,#064E3B,#A7F3D0,Nature green + solar gold
|
||||
|
101
.trae/skills/ui-ux-pro-max/data/icons.csv
Normal file
101
.trae/skills/ui-ux-pro-max/data/icons.csv
Normal file
@ -0,0 +1,101 @@
|
||||
No,Category,Icon Name,Keywords,Library,Import Code,Usage,Best For,Style
|
||||
1,Navigation,menu,hamburger menu navigation toggle bars,Lucide,import { Menu } from 'lucide-react',<Menu />,Mobile navigation drawer toggle sidebar,Outline
|
||||
2,Navigation,arrow-left,back previous return navigate,Lucide,import { ArrowLeft } from 'lucide-react',<ArrowLeft />,Back button breadcrumb navigation,Outline
|
||||
3,Navigation,arrow-right,next forward continue navigate,Lucide,import { ArrowRight } from 'lucide-react',<ArrowRight />,Forward button next step CTA,Outline
|
||||
4,Navigation,chevron-down,dropdown expand accordion select,Lucide,import { ChevronDown } from 'lucide-react',<ChevronDown />,Dropdown toggle accordion header,Outline
|
||||
5,Navigation,chevron-up,collapse close accordion minimize,Lucide,import { ChevronUp } from 'lucide-react',<ChevronUp />,Accordion collapse minimize,Outline
|
||||
6,Navigation,home,homepage main dashboard start,Lucide,import { Home } from 'lucide-react',<Home />,Home navigation main page,Outline
|
||||
7,Navigation,x,close cancel dismiss remove exit,Lucide,import { X } from 'lucide-react',<X />,Modal close dismiss button,Outline
|
||||
8,Navigation,external-link,open new tab external link,Lucide,import { ExternalLink } from 'lucide-react',<ExternalLink />,External link indicator,Outline
|
||||
9,Action,plus,add create new insert,Lucide,import { Plus } from 'lucide-react',<Plus />,Add button create new item,Outline
|
||||
10,Action,minus,remove subtract decrease delete,Lucide,import { Minus } from 'lucide-react',<Minus />,Remove item quantity decrease,Outline
|
||||
11,Action,trash-2,delete remove discard bin,Lucide,import { Trash2 } from 'lucide-react',<Trash2 />,Delete action destructive,Outline
|
||||
12,Action,edit,pencil modify change update,Lucide,import { Edit } from 'lucide-react',<Edit />,Edit button modify content,Outline
|
||||
13,Action,save,disk store persist save,Lucide,import { Save } from 'lucide-react',<Save />,Save button persist changes,Outline
|
||||
14,Action,download,export save file download,Lucide,import { Download } from 'lucide-react',<Download />,Download file export,Outline
|
||||
15,Action,upload,import file attach upload,Lucide,import { Upload } from 'lucide-react',<Upload />,Upload file import,Outline
|
||||
16,Action,copy,duplicate clipboard paste,Lucide,import { Copy } from 'lucide-react',<Copy />,Copy to clipboard,Outline
|
||||
17,Action,share,social distribute send,Lucide,import { Share } from 'lucide-react',<Share />,Share button social,Outline
|
||||
18,Action,search,find lookup filter query,Lucide,import { Search } from 'lucide-react',<Search />,Search input bar,Outline
|
||||
19,Action,filter,sort refine narrow options,Lucide,import { Filter } from 'lucide-react',<Filter />,Filter dropdown sort,Outline
|
||||
20,Action,settings,gear cog preferences config,Lucide,import { Settings } from 'lucide-react',<Settings />,Settings page configuration,Outline
|
||||
21,Status,check,success done complete verified,Lucide,import { Check } from 'lucide-react',<Check />,Success state checkmark,Outline
|
||||
22,Status,check-circle,success verified approved complete,Lucide,import { CheckCircle } from 'lucide-react',<CheckCircle />,Success badge verified,Outline
|
||||
23,Status,x-circle,error failed cancel rejected,Lucide,import { XCircle } from 'lucide-react',<XCircle />,Error state failed,Outline
|
||||
24,Status,alert-triangle,warning caution attention danger,Lucide,import { AlertTriangle } from 'lucide-react',<AlertTriangle />,Warning message caution,Outline
|
||||
25,Status,alert-circle,info notice information help,Lucide,import { AlertCircle } from 'lucide-react',<AlertCircle />,Info notice alert,Outline
|
||||
26,Status,info,information help tooltip details,Lucide,import { Info } from 'lucide-react',<Info />,Information tooltip help,Outline
|
||||
27,Status,loader,loading spinner processing wait,Lucide,import { Loader } from 'lucide-react',<Loader className="animate-spin" />,Loading state spinner,Outline
|
||||
28,Status,clock,time schedule pending wait,Lucide,import { Clock } from 'lucide-react',<Clock />,Pending time schedule,Outline
|
||||
29,Communication,mail,email message inbox letter,Lucide,import { Mail } from 'lucide-react',<Mail />,Email contact inbox,Outline
|
||||
30,Communication,message-circle,chat comment bubble conversation,Lucide,import { MessageCircle } from 'lucide-react',<MessageCircle />,Chat comment message,Outline
|
||||
31,Communication,phone,call mobile telephone contact,Lucide,import { Phone } from 'lucide-react',<Phone />,Phone contact call,Outline
|
||||
32,Communication,send,submit dispatch message airplane,Lucide,import { Send } from 'lucide-react',<Send />,Send message submit,Outline
|
||||
33,Communication,bell,notification alert ring reminder,Lucide,import { Bell } from 'lucide-react',<Bell />,Notification bell alert,Outline
|
||||
34,User,user,profile account person avatar,Lucide,import { User } from 'lucide-react',<User />,User profile account,Outline
|
||||
35,User,users,team group people members,Lucide,import { Users } from 'lucide-react',<Users />,Team group members,Outline
|
||||
36,User,user-plus,add invite new member,Lucide,import { UserPlus } from 'lucide-react',<UserPlus />,Add user invite,Outline
|
||||
37,User,log-in,signin authenticate enter,Lucide,import { LogIn } from 'lucide-react',<LogIn />,Login signin,Outline
|
||||
38,User,log-out,signout exit leave logout,Lucide,import { LogOut } from 'lucide-react',<LogOut />,Logout signout,Outline
|
||||
39,Media,image,photo picture gallery thumbnail,Lucide,import { Image } from 'lucide-react',<Image />,Image photo gallery,Outline
|
||||
40,Media,video,movie film play record,Lucide,import { Video } from 'lucide-react',<Video />,Video player media,Outline
|
||||
41,Media,play,start video audio media,Lucide,import { Play } from 'lucide-react',<Play />,Play button video audio,Outline
|
||||
42,Media,pause,stop halt video audio,Lucide,import { Pause } from 'lucide-react',<Pause />,Pause button media,Outline
|
||||
43,Media,volume-2,sound audio speaker music,Lucide,import { Volume2 } from 'lucide-react',<Volume2 />,Volume audio sound,Outline
|
||||
44,Media,mic,microphone record voice audio,Lucide,import { Mic } from 'lucide-react',<Mic />,Microphone voice record,Outline
|
||||
45,Media,camera,photo capture snapshot picture,Lucide,import { Camera } from 'lucide-react',<Camera />,Camera photo capture,Outline
|
||||
46,Commerce,shopping-cart,cart checkout basket buy,Lucide,import { ShoppingCart } from 'lucide-react',<ShoppingCart />,Shopping cart e-commerce,Outline
|
||||
47,Commerce,shopping-bag,purchase buy store bag,Lucide,import { ShoppingBag } from 'lucide-react',<ShoppingBag />,Shopping bag purchase,Outline
|
||||
48,Commerce,credit-card,payment card checkout stripe,Lucide,import { CreditCard } from 'lucide-react',<CreditCard />,Payment credit card,Outline
|
||||
49,Commerce,dollar-sign,money price currency cost,Lucide,import { DollarSign } from 'lucide-react',<DollarSign />,Price money currency,Outline
|
||||
50,Commerce,tag,label price discount sale,Lucide,import { Tag } from 'lucide-react',<Tag />,Price tag label,Outline
|
||||
51,Commerce,gift,present reward bonus offer,Lucide,import { Gift } from 'lucide-react',<Gift />,Gift reward offer,Outline
|
||||
52,Commerce,percent,discount sale offer promo,Lucide,import { Percent } from 'lucide-react',<Percent />,Discount percentage sale,Outline
|
||||
53,Data,bar-chart,analytics statistics graph metrics,Lucide,import { BarChart } from 'lucide-react',<BarChart />,Bar chart analytics,Outline
|
||||
54,Data,pie-chart,statistics distribution breakdown,Lucide,import { PieChart } from 'lucide-react',<PieChart />,Pie chart distribution,Outline
|
||||
55,Data,trending-up,growth increase positive trend,Lucide,import { TrendingUp } from 'lucide-react',<TrendingUp />,Growth trend positive,Outline
|
||||
56,Data,trending-down,decline decrease negative trend,Lucide,import { TrendingDown } from 'lucide-react',<TrendingDown />,Decline trend negative,Outline
|
||||
57,Data,activity,pulse heartbeat monitor live,Lucide,import { Activity } from 'lucide-react',<Activity />,Activity monitor pulse,Outline
|
||||
58,Data,database,storage server data backend,Lucide,import { Database } from 'lucide-react',<Database />,Database storage,Outline
|
||||
59,Files,file,document page paper doc,Lucide,import { File } from 'lucide-react',<File />,File document,Outline
|
||||
60,Files,file-text,document text page article,Lucide,import { FileText } from 'lucide-react',<FileText />,Text document article,Outline
|
||||
61,Files,folder,directory organize group files,Lucide,import { Folder } from 'lucide-react',<Folder />,Folder directory,Outline
|
||||
62,Files,folder-open,expanded browse files view,Lucide,import { FolderOpen } from 'lucide-react',<FolderOpen />,Open folder browse,Outline
|
||||
63,Files,paperclip,attachment attach file link,Lucide,import { Paperclip } from 'lucide-react',<Paperclip />,Attachment paperclip,Outline
|
||||
64,Files,link,url hyperlink chain connect,Lucide,import { Link } from 'lucide-react',<Link />,Link URL hyperlink,Outline
|
||||
65,Files,clipboard,paste copy buffer notes,Lucide,import { Clipboard } from 'lucide-react',<Clipboard />,Clipboard paste,Outline
|
||||
66,Layout,grid,tiles gallery layout dashboard,Lucide,import { Grid } from 'lucide-react',<Grid />,Grid layout gallery,Outline
|
||||
67,Layout,list,rows table lines items,Lucide,import { List } from 'lucide-react',<List />,List view rows,Outline
|
||||
68,Layout,columns,layout split dual sidebar,Lucide,import { Columns } from 'lucide-react',<Columns />,Column layout split,Outline
|
||||
69,Layout,maximize,fullscreen expand enlarge zoom,Lucide,import { Maximize } from 'lucide-react',<Maximize />,Fullscreen maximize,Outline
|
||||
70,Layout,minimize,reduce shrink collapse exit,Lucide,import { Minimize } from 'lucide-react',<Minimize />,Minimize reduce,Outline
|
||||
71,Layout,sidebar,panel drawer navigation menu,Lucide,import { Sidebar } from 'lucide-react',<Sidebar />,Sidebar panel,Outline
|
||||
72,Social,heart,like love favorite wishlist,Lucide,import { Heart } from 'lucide-react',<Heart />,Like favorite love,Outline
|
||||
73,Social,star,rating review favorite bookmark,Lucide,import { Star } from 'lucide-react',<Star />,Star rating favorite,Outline
|
||||
74,Social,thumbs-up,like approve agree positive,Lucide,import { ThumbsUp } from 'lucide-react',<ThumbsUp />,Like approve thumb,Outline
|
||||
75,Social,thumbs-down,dislike disapprove disagree negative,Lucide,import { ThumbsDown } from 'lucide-react',<ThumbsDown />,Dislike disapprove,Outline
|
||||
76,Social,bookmark,save later favorite mark,Lucide,import { Bookmark } from 'lucide-react',<Bookmark />,Bookmark save,Outline
|
||||
77,Social,flag,report mark important highlight,Lucide,import { Flag } from 'lucide-react',<Flag />,Flag report,Outline
|
||||
78,Device,smartphone,mobile phone device touch,Lucide,import { Smartphone } from 'lucide-react',<Smartphone />,Mobile smartphone,Outline
|
||||
79,Device,tablet,ipad device touch screen,Lucide,import { Tablet } from 'lucide-react',<Tablet />,Tablet device,Outline
|
||||
80,Device,monitor,desktop screen computer display,Lucide,import { Monitor } from 'lucide-react',<Monitor />,Desktop monitor,Outline
|
||||
81,Device,laptop,notebook computer portable device,Lucide,import { Laptop } from 'lucide-react',<Laptop />,Laptop computer,Outline
|
||||
82,Device,printer,print document output paper,Lucide,import { Printer } from 'lucide-react',<Printer />,Printer print,Outline
|
||||
83,Security,lock,secure password protected private,Lucide,import { Lock } from 'lucide-react',<Lock />,Lock secure,Outline
|
||||
84,Security,unlock,open access unsecure public,Lucide,import { Unlock } from 'lucide-react',<Unlock />,Unlock open,Outline
|
||||
85,Security,shield,protection security safe guard,Lucide,import { Shield } from 'lucide-react',<Shield />,Shield protection,Outline
|
||||
86,Security,key,password access unlock login,Lucide,import { Key } from 'lucide-react',<Key />,Key password,Outline
|
||||
87,Security,eye,view show visible password,Lucide,import { Eye } from 'lucide-react',<Eye />,Show password view,Outline
|
||||
88,Security,eye-off,hide invisible password hidden,Lucide,import { EyeOff } from 'lucide-react',<EyeOff />,Hide password,Outline
|
||||
89,Location,map-pin,location marker place address,Lucide,import { MapPin } from 'lucide-react',<MapPin />,Location pin marker,Outline
|
||||
90,Location,map,directions navigate geography location,Lucide,import { Map } from 'lucide-react',<Map />,Map directions,Outline
|
||||
91,Location,navigation,compass direction pointer arrow,Lucide,import { Navigation } from 'lucide-react',<Navigation />,Navigation compass,Outline
|
||||
92,Location,globe,world international global web,Lucide,import { Globe } from 'lucide-react',<Globe />,Globe world,Outline
|
||||
93,Time,calendar,date schedule event appointment,Lucide,import { Calendar } from 'lucide-react',<Calendar />,Calendar date,Outline
|
||||
94,Time,refresh-cw,reload sync update refresh,Lucide,import { RefreshCw } from 'lucide-react',<RefreshCw />,Refresh reload,Outline
|
||||
95,Time,rotate-ccw,undo back revert history,Lucide,import { RotateCcw } from 'lucide-react',<RotateCcw />,Undo revert,Outline
|
||||
96,Time,rotate-cw,redo forward repeat history,Lucide,import { RotateCw } from 'lucide-react',<RotateCw />,Redo forward,Outline
|
||||
97,Development,code,develop programming syntax html,Lucide,import { Code } from 'lucide-react',<Code />,Code development,Outline
|
||||
98,Development,terminal,console cli command shell,Lucide,import { Terminal } from 'lucide-react',<Terminal />,Terminal console,Outline
|
||||
99,Development,git-branch,version control branch merge,Lucide,import { GitBranch } from 'lucide-react',<GitBranch />,Git branch,Outline
|
||||
100,Development,github,repository code open source,Lucide,import { Github } from 'lucide-react',<Github />,GitHub repository,Outline
|
||||
|
Can't render this file because it contains an unexpected character in line 28 and column 113.
|
31
.trae/skills/ui-ux-pro-max/data/landing.csv
Normal file
31
.trae/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."
|
||||
|
97
.trae/skills/ui-ux-pro-max/data/products.csv
Normal file
97
.trae/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.
|
||||
|
45
.trae/skills/ui-ux-pro-max/data/react-performance.csv
Normal file
45
.trae/skills/ui-ux-pro-max/data/react-performance.csv
Normal file
@ -0,0 +1,45 @@
|
||||
No,Category,Issue,Keywords,Platform,Description,Do,Don't,Code Example Good,Code Example Bad,Severity
|
||||
1,Async Waterfall,Defer Await,async await defer branch,React/Next.js,Move await into branches where actually used to avoid blocking unused code paths,Move await operations into branches where they're needed,Await at top of function blocking all branches,"if (skip) return { skipped: true }; const data = await fetch()","const data = await fetch(); if (skip) return { skipped: true }",Critical
|
||||
2,Async Waterfall,Promise.all Parallel,promise all parallel concurrent,React/Next.js,Execute independent async operations concurrently using Promise.all(),Use Promise.all() for independent operations,Sequential await for independent operations,"const [user, posts] = await Promise.all([fetchUser(), fetchPosts()])","const user = await fetchUser(); const posts = await fetchPosts()",Critical
|
||||
3,Async Waterfall,Dependency Parallelization,better-all dependency parallel,React/Next.js,Use better-all for operations with partial dependencies to maximize parallelism,Use better-all to start each task at earliest possible moment,Wait for unrelated data before starting dependent fetch,"await all({ user() {}, config() {}, profile() { return fetch((await this.$.user).id) } })","const [user, config] = await Promise.all([...]); const profile = await fetchProfile(user.id)",Critical
|
||||
4,Async Waterfall,API Route Optimization,api route waterfall promise,React/Next.js,In API routes start independent operations immediately even if not awaited yet,Start promises early and await late,Sequential awaits in API handlers,"const sessionP = auth(); const configP = fetchConfig(); const session = await sessionP","const session = await auth(); const config = await fetchConfig()",Critical
|
||||
5,Async Waterfall,Suspense Boundaries,suspense streaming boundary,React/Next.js,Use Suspense to show wrapper UI faster while data loads,Wrap async components in Suspense boundaries,Await data blocking entire page render,"<Suspense fallback={<Skeleton />}><DataDisplay /></Suspense>","const data = await fetchData(); return <DataDisplay data={data} />",High
|
||||
6,Bundle Size,Barrel Imports,barrel import direct path,React/Next.js,Import directly from source files instead of barrel files to avoid loading unused modules,Import directly from source path,Import from barrel/index files,"import Check from 'lucide-react/dist/esm/icons/check'","import { Check } from 'lucide-react'",Critical
|
||||
7,Bundle Size,Dynamic Imports,dynamic import lazy next,React/Next.js,Use next/dynamic to lazy-load large components not needed on initial render,Use dynamic() for heavy components,Import heavy components at top level,"const Monaco = dynamic(() => import('./monaco'), { ssr: false })","import { MonacoEditor } from './monaco-editor'",Critical
|
||||
8,Bundle Size,Defer Third Party,analytics defer third-party,React/Next.js,Load analytics and logging after hydration since they don't block interaction,Load non-critical scripts after hydration,Include analytics in main bundle,"const Analytics = dynamic(() => import('@vercel/analytics'), { ssr: false })","import { Analytics } from '@vercel/analytics/react'",Medium
|
||||
9,Bundle Size,Conditional Loading,conditional module lazy,React/Next.js,Load large data or modules only when a feature is activated,Dynamic import when feature enabled,Import large modules unconditionally,"useEffect(() => { if (enabled) import('./heavy.js') }, [enabled])","import { heavyData } from './heavy.js'",High
|
||||
10,Bundle Size,Preload Intent,preload hover focus intent,React/Next.js,Preload heavy bundles on hover/focus before they're needed,Preload on user intent signals,Load only on click,"onMouseEnter={() => import('./editor')}","onClick={() => import('./editor')}",Medium
|
||||
11,Server,React.cache Dedup,react cache deduplicate request,React/Next.js,Use React.cache() for server-side request deduplication within single request,Wrap data fetchers with cache(),Fetch same data multiple times in tree,"export const getUser = cache(async () => await db.user.find())","export async function getUser() { return await db.user.find() }",Medium
|
||||
12,Server,LRU Cache Cross-Request,lru cache cross request,React/Next.js,Use LRU cache for data shared across sequential requests,Use LRU for cross-request caching,Refetch same data on every request,"const cache = new LRUCache({ max: 1000, ttl: 5*60*1000 })","Always fetch from database",High
|
||||
13,Server,Minimize Serialization,serialization rsc boundary,React/Next.js,Only pass fields that client actually uses across RSC boundaries,Pass only needed fields to client components,Pass entire objects to client,"<Profile name={user.name} />","<Profile user={user} /> // 50 fields serialized",High
|
||||
14,Server,Parallel Fetching,parallel fetch component composition,React/Next.js,Restructure components to parallelize data fetching in RSC,Use component composition for parallel fetches,Sequential fetches in parent component,"<Header /><Sidebar /> // both fetch in parallel","const header = await fetchHeader(); return <><div>{header}</div><Sidebar /></>",Critical
|
||||
15,Server,After Non-blocking,after non-blocking logging,React/Next.js,Use Next.js after() to schedule work after response is sent,Use after() for logging/analytics,Block response for non-critical operations,"after(async () => { await logAction() }); return Response.json(data)","await logAction(); return Response.json(data)",Medium
|
||||
16,Client,SWR Deduplication,swr dedup cache revalidate,React/Next.js,Use SWR for automatic request deduplication and caching,Use useSWR for client data fetching,Manual fetch in useEffect,"const { data } = useSWR('/api/users', fetcher)","useEffect(() => { fetch('/api/users').then(setUsers) }, [])",Medium-High
|
||||
17,Client,Event Listener Dedup,event listener deduplicate global,React/Next.js,Share global event listeners across component instances,Use useSWRSubscription for shared listeners,Register listener per component instance,"useSWRSubscription('global-keydown', () => { window.addEventListener... })","useEffect(() => { window.addEventListener('keydown', handler) }, [])",Low
|
||||
18,Rerender,Defer State Reads,state read callback subscription,React/Next.js,Don't subscribe to state only used in callbacks,Read state on-demand in callbacks,Subscribe to state used only in handlers,"const handleClick = () => { const params = new URLSearchParams(location.search) }","const params = useSearchParams(); const handleClick = () => { params.get('ref') }",Medium
|
||||
19,Rerender,Memoized Components,memo extract expensive,React/Next.js,Extract expensive work into memoized components for early returns,Extract to memo() components,Compute expensive values before early return,"const UserAvatar = memo(({ user }) => ...); if (loading) return <Skeleton />","const avatar = useMemo(() => compute(user)); if (loading) return <Skeleton />",Medium
|
||||
20,Rerender,Narrow Dependencies,effect dependency primitive,React/Next.js,Specify primitive dependencies instead of objects in effects,Use primitive values in dependency arrays,Use object references as dependencies,"useEffect(() => { console.log(user.id) }, [user.id])","useEffect(() => { console.log(user.id) }, [user])",Low
|
||||
21,Rerender,Derived State,derived boolean subscription,React/Next.js,Subscribe to derived booleans instead of continuous values,Use derived boolean state,Subscribe to continuous values,"const isMobile = useMediaQuery('(max-width: 767px)')","const width = useWindowWidth(); const isMobile = width < 768",Medium
|
||||
22,Rerender,Functional setState,functional setstate callback,React/Next.js,Use functional setState updates for stable callbacks and no stale closures,Use functional form: setState(curr => ...),Reference state directly in setState,"setItems(curr => [...curr, newItem])","setItems([...items, newItem]) // items in deps",Medium
|
||||
23,Rerender,Lazy State Init,usestate lazy initialization,React/Next.js,Pass function to useState for expensive initial values,Use function form for expensive init,Compute expensive value directly,"useState(() => buildSearchIndex(items))","useState(buildSearchIndex(items)) // runs every render",Medium
|
||||
24,Rerender,Transitions,starttransition non-urgent,React/Next.js,Mark frequent non-urgent state updates as transitions,Use startTransition for non-urgent updates,Block UI on every state change,"startTransition(() => setScrollY(window.scrollY))","setScrollY(window.scrollY) // blocks on every scroll",Medium
|
||||
25,Rendering,SVG Animation Wrapper,svg animation wrapper div,React/Next.js,Wrap SVG in div and animate wrapper for hardware acceleration,Animate div wrapper around SVG,Animate SVG element directly,"<div class='animate-spin'><svg>...</svg></div>","<svg class='animate-spin'>...</svg>",Low
|
||||
26,Rendering,Content Visibility,content-visibility auto,React/Next.js,Apply content-visibility: auto to defer off-screen rendering,Use content-visibility for long lists,Render all list items immediately,".item { content-visibility: auto; contain-intrinsic-size: 0 80px }","Render 1000 items without optimization",High
|
||||
27,Rendering,Hoist Static JSX,hoist static jsx element,React/Next.js,Extract static JSX outside components to avoid re-creation,Hoist static elements to module scope,Create static elements inside components,"const skeleton = <div class='animate-pulse' />; function C() { return skeleton }","function C() { return <div class='animate-pulse' /> }",Low
|
||||
28,Rendering,Hydration No Flicker,hydration mismatch flicker,React/Next.js,Use inline script to set client-only data before hydration,Inject sync script for client-only values,Use useEffect causing flash,"<script dangerouslySetInnerHTML={{ __html: 'el.className = localStorage.theme' }} />","useEffect(() => setTheme(localStorage.theme), []) // flickers",Medium
|
||||
29,Rendering,Conditional Render,conditional render ternary,React/Next.js,Use ternary instead of && when condition can be 0 or NaN,Use explicit ternary for conditionals,Use && with potentially falsy numbers,"{count > 0 ? <Badge>{count}</Badge> : null}","{count && <Badge>{count}</Badge>} // renders '0'",Low
|
||||
30,Rendering,Activity Component,activity show hide preserve,React/Next.js,Use Activity component to preserve state/DOM for toggled components,Use Activity for expensive toggle components,Unmount/remount on visibility toggle,"<Activity mode={isOpen ? 'visible' : 'hidden'}><Menu /></Activity>","{isOpen && <Menu />} // loses state",Medium
|
||||
31,JS Perf,Batch DOM CSS,batch dom css reflow,React/Next.js,Group CSS changes via classes or cssText to minimize reflows,Use class toggle or cssText,Change styles one property at a time,"element.classList.add('highlighted')","el.style.width='100px'; el.style.height='200px'",Medium
|
||||
32,JS Perf,Index Map Lookup,map index lookup find,React/Next.js,Build Map for repeated lookups instead of multiple .find() calls,Build index Map for O(1) lookups,Use .find() in loops,"const byId = new Map(users.map(u => [u.id, u])); byId.get(id)","users.find(u => u.id === order.userId) // O(n) each time",Low-Medium
|
||||
33,JS Perf,Cache Property Access,cache property loop,React/Next.js,Cache object property lookups in hot paths,Cache values before loops,Access nested properties in loops,"const val = obj.config.settings.value; for (...) process(val)","for (...) process(obj.config.settings.value)",Low-Medium
|
||||
34,JS Perf,Cache Function Results,memoize cache function,React/Next.js,Use module-level Map to cache repeated function results,Use Map cache for repeated calls,Recompute same values repeatedly,"const cache = new Map(); if (cache.has(x)) return cache.get(x)","slugify(name) // called 100 times same input",Medium
|
||||
35,JS Perf,Cache Storage API,localstorage cache read,React/Next.js,Cache localStorage/sessionStorage reads in memory,Cache storage reads in Map,Read storage on every call,"if (!cache.has(key)) cache.set(key, localStorage.getItem(key))","localStorage.getItem('theme') // every call",Low-Medium
|
||||
36,JS Perf,Combine Iterations,combine filter map loop,React/Next.js,Combine multiple filter/map into single loop,Single loop for multiple categorizations,Chain multiple filter() calls,"for (u of users) { if (u.isAdmin) admins.push(u); if (u.isTester) testers.push(u) }","users.filter(admin); users.filter(tester); users.filter(inactive)",Low-Medium
|
||||
37,JS Perf,Length Check First,length check array compare,React/Next.js,Check array lengths before expensive comparisons,Early return if lengths differ,Always run expensive comparison,"if (a.length !== b.length) return true; // then compare","a.sort().join() !== b.sort().join() // even when lengths differ",Medium-High
|
||||
38,JS Perf,Early Return,early return exit function,React/Next.js,Return early when result is determined to skip processing,Return immediately on first error,Process all items then check errors,"for (u of users) { if (!u.email) return { error: 'Email required' } }","let hasError; for (...) { if (!email) hasError=true }; if (hasError)...",Low-Medium
|
||||
39,JS Perf,Hoist RegExp,regexp hoist module,React/Next.js,Don't create RegExp inside render - hoist or memoize,Hoist RegExp to module scope,Create RegExp every render,"const EMAIL_RE = /^[^@]+@[^@]+$/; function validate() { EMAIL_RE.test(x) }","function C() { const re = new RegExp(pattern); re.test(x) }",Low-Medium
|
||||
40,JS Perf,Loop Min Max,loop min max sort,React/Next.js,Use loop for min/max instead of sort - O(n) vs O(n log n),Single pass loop for min/max,Sort array to find min/max,"let max = arr[0]; for (x of arr) if (x > max) max = x","arr.sort((a,b) => b-a)[0] // O(n log n)",Low
|
||||
41,JS Perf,Set Map Lookups,set map includes has,React/Next.js,Use Set/Map for O(1) lookups instead of array.includes(),Convert to Set for membership checks,Use .includes() for repeated checks,"const allowed = new Set(['a','b']); allowed.has(id)","const allowed = ['a','b']; allowed.includes(id)",Low-Medium
|
||||
42,JS Perf,toSorted Immutable,tosorted sort immutable,React/Next.js,Use toSorted() instead of sort() to avoid mutating arrays,Use toSorted() for immutability,Mutate arrays with sort(),"users.toSorted((a,b) => a.name.localeCompare(b.name))","users.sort((a,b) => a.name.localeCompare(b.name)) // mutates",Medium-High
|
||||
43,Advanced,Event Handler Refs,useeffectevent ref handler,React/Next.js,Store callbacks in refs for stable effect subscriptions,Use useEffectEvent for stable handlers,Re-subscribe on every callback change,"const onEvent = useEffectEvent(handler); useEffect(() => { listen(onEvent) }, [])","useEffect(() => { listen(handler) }, [handler]) // re-subscribes",Low
|
||||
44,Advanced,useLatest Hook,uselatest ref callback,React/Next.js,Access latest values in callbacks without adding to dependency arrays,Use useLatest for fresh values in stable callbacks,Add callback to effect dependencies,"const cbRef = useLatest(cb); useEffect(() => { setTimeout(() => cbRef.current()) }, [])","useEffect(() => { setTimeout(() => cb()) }, [cb]) // re-runs",Low
|
||||
|
54
.trae/skills/ui-ux-pro-max/data/stacks/astro.csv
Normal file
54
.trae/skills/ui-ux-pro-max/data/stacks/astro.csv
Normal file
@ -0,0 +1,54 @@
|
||||
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
|
||||
1,Architecture,Use Islands Architecture,Astro's partial hydration only loads JS for interactive components,Interactive components with client directives,Hydrate entire page like traditional SPA,<Counter client:load />,Everything as client component,High,https://docs.astro.build/en/concepts/islands/
|
||||
2,Architecture,Default to zero JS,Astro ships zero JS by default - add only when needed,Static components without client directive,Add client:load to everything,<Header /> (static),<Header client:load /> (unnecessary),High,https://docs.astro.build/en/basics/astro-components/
|
||||
3,Architecture,Choose right client directive,Different directives for different hydration timing,client:visible for below-fold client:idle for non-critical,client:load for everything,<Comments client:visible />,<Comments client:load />,Medium,https://docs.astro.build/en/reference/directives-reference/#client-directives
|
||||
4,Architecture,Use content collections,Type-safe content management for blogs docs,Content collections for structured content,Loose markdown files without schema,const posts = await getCollection('blog'),import.meta.glob('./posts/*.md'),High,https://docs.astro.build/en/guides/content-collections/
|
||||
5,Architecture,Define collection schemas,Zod schemas for content validation,Schema with required fields and types,No schema validation,"defineCollection({ schema: z.object({...}) })",defineCollection({}),High,https://docs.astro.build/en/guides/content-collections/#defining-a-collection-schema
|
||||
6,Routing,Use file-based routing,Create routes by adding .astro files in pages/,pages/ directory for routes,Manual route configuration,src/pages/about.astro,Custom router setup,Medium,https://docs.astro.build/en/basics/astro-pages/
|
||||
7,Routing,Dynamic routes with brackets,Use [param] for dynamic routes,Bracket notation for params,Query strings for dynamic content,pages/blog/[slug].astro,pages/blog.astro?slug=x,Medium,https://docs.astro.build/en/guides/routing/#dynamic-routes
|
||||
8,Routing,Use getStaticPaths for SSG,Generate static pages at build time,getStaticPaths for known dynamic routes,Fetch at runtime for static content,"export async function getStaticPaths() { return [...] }",No getStaticPaths with dynamic route,High,https://docs.astro.build/en/reference/api-reference/#getstaticpaths
|
||||
9,Routing,Enable SSR when needed,Server-side rendering for dynamic content,output: 'server' or 'hybrid' for dynamic,SSR for purely static sites,"export const prerender = false;",SSR for static blog,Medium,https://docs.astro.build/en/guides/server-side-rendering/
|
||||
10,Components,Keep .astro for static,Use .astro components for static content,Astro components for layout structure,React/Vue for static markup,<Layout><slot /></Layout>,<ReactLayout>{children}</ReactLayout>,High,
|
||||
11,Components,Use framework components for interactivity,React Vue Svelte for complex interactivity,Framework component with client directive,Astro component with inline scripts,<ReactCounter client:load />,<script> in .astro for complex state,Medium,https://docs.astro.build/en/guides/framework-components/
|
||||
12,Components,Pass data via props,Astro components receive props in frontmatter,Astro.props for component data,Global state for simple data,"const { title } = Astro.props;",Import global store,Low,https://docs.astro.build/en/basics/astro-components/#component-props
|
||||
13,Components,Use slots for composition,Named and default slots for flexible layouts,<slot /> for child content,Props for HTML content,<slot name="header" />,<Component header={<div>...</div>} />,Medium,https://docs.astro.build/en/basics/astro-components/#slots
|
||||
14,Components,Colocate component styles,Scoped styles in component file,<style> in same .astro file,Separate CSS files for component styles,<style> .card { } </style>,import './Card.css',Low,
|
||||
15,Styling,Use scoped styles by default,Astro scopes styles to component automatically,<style> for component-specific styles,Global styles for everything,<style> h1 { } </style> (scoped),<style is:global> for everything,Medium,https://docs.astro.build/en/guides/styling/#scoped-styles
|
||||
16,Styling,Use is:global sparingly,Global styles only when truly needed,is:global for base styles or overrides,is:global for component styles,<style is:global> body { } </style>,<style is:global> .card { } </style>,Medium,
|
||||
17,Styling,Integrate Tailwind properly,Use @astrojs/tailwind integration,Official Tailwind integration,Manual Tailwind setup,npx astro add tailwind,Manual PostCSS config,Low,https://docs.astro.build/en/guides/integrations-guide/tailwind/
|
||||
18,Styling,Use CSS variables for theming,Define tokens in :root,CSS custom properties for themes,Hardcoded colors everywhere,:root { --primary: #3b82f6; },color: #3b82f6; everywhere,Medium,
|
||||
19,Data,Fetch in frontmatter,Data fetching in component frontmatter,Top-level await in frontmatter,useEffect for initial data,const data = await fetch(url),client-side fetch on mount,High,https://docs.astro.build/en/guides/data-fetching/
|
||||
20,Data,Use Astro.glob for local files,Import multiple local files,Astro.glob for markdown/data files,Manual imports for each file,const posts = await Astro.glob('./posts/*.md'),"import post1; import post2;",Medium,
|
||||
21,Data,Prefer content collections over glob,Type-safe collections for structured content,getCollection() for blog/docs,Astro.glob for structured content,await getCollection('blog'),await Astro.glob('./blog/*.md'),High,
|
||||
22,Data,Use environment variables correctly,Import.meta.env for env vars,PUBLIC_ prefix for client vars,Expose secrets to client,import.meta.env.PUBLIC_API_URL,import.meta.env.SECRET in client,High,https://docs.astro.build/en/guides/environment-variables/
|
||||
23,Performance,Preload critical assets,Use link preload for important resources,Preload fonts above-fold images,No preload hints,"<link rel=""preload"" href=""font.woff2"" as=""font"">",No preload for critical assets,Medium,
|
||||
24,Performance,Optimize images with astro:assets,Built-in image optimization,<Image /> component for optimization,<img> for local images,"import { Image } from 'astro:assets';","<img src=""./image.jpg"">",High,https://docs.astro.build/en/guides/images/
|
||||
25,Performance,Use picture for responsive images,Multiple formats and sizes,<Picture /> for art direction,Single image size for all screens,<Picture /> with multiple sources,<Image /> with single size,Medium,
|
||||
26,Performance,Lazy load below-fold content,Defer loading non-critical content,loading=lazy for images client:visible for components,Load everything immediately,"<img loading=""lazy"">",No lazy loading,Medium,
|
||||
27,Performance,Minimize client directives,Each directive adds JS bundle,Audit client: usage regularly,Sprinkle client:load everywhere,Only interactive components hydrated,Every component with client:load,High,
|
||||
28,ViewTransitions,Enable View Transitions,Smooth page transitions,<ViewTransitions /> in head,Full page reloads,"import { ViewTransitions } from 'astro:transitions';",No transition API,Medium,https://docs.astro.build/en/guides/view-transitions/
|
||||
29,ViewTransitions,Use transition:name,Named elements for morphing,transition:name for persistent elements,Unnamed transitions,"<header transition:name=""header"">",<header> without name,Low,
|
||||
30,ViewTransitions,Handle transition:persist,Keep state across navigations,transition:persist for media players,Re-initialize on every navigation,"<video transition:persist id=""player"">",Video restarts on navigation,Medium,
|
||||
31,ViewTransitions,Add fallback for no-JS,Graceful degradation,Content works without JS,Require JS for basic navigation,Static content accessible,Broken without ViewTransitions JS,High,
|
||||
32,SEO,Use built-in SEO component,Head management for meta tags,Astro SEO integration or manual head,No meta tags,"<title>{title}</title><meta name=""description"">",No SEO tags,High,
|
||||
33,SEO,Generate sitemap,Automatic sitemap generation,@astrojs/sitemap integration,Manual sitemap maintenance,npx astro add sitemap,Hand-written sitemap.xml,Medium,https://docs.astro.build/en/guides/integrations-guide/sitemap/
|
||||
34,SEO,Add RSS feed for content,RSS for blogs and content sites,@astrojs/rss for feed generation,No RSS feed,rss() helper in pages/rss.xml.js,No feed for blog,Low,https://docs.astro.build/en/guides/rss/
|
||||
35,SEO,Use canonical URLs,Prevent duplicate content issues,Astro.url for canonical generation,"<link rel=""canonical"" href={Astro.url}>",No canonical tags,Medium,
|
||||
36,Integrations,Use official integrations,Astro's integration system,npx astro add for integrations,Manual configuration,npx astro add react,Manual React setup,Medium,https://docs.astro.build/en/guides/integrations-guide/
|
||||
37,Integrations,Configure integrations in astro.config,Centralized configuration,integrations array in config,Scattered configuration,"integrations: [react(), tailwind()]",Multiple config files,Low,
|
||||
38,Integrations,Use adapter for deployment,Platform-specific adapters,Correct adapter for host,Wrong or no adapter,@astrojs/vercel for Vercel,No adapter for SSR,High,https://docs.astro.build/en/guides/deploy/
|
||||
39,TypeScript,Enable TypeScript,Type safety for Astro projects,tsconfig.json with astro types,No TypeScript,Astro TypeScript template,JavaScript only,Medium,https://docs.astro.build/en/guides/typescript/
|
||||
40,TypeScript,Type component props,Define prop interfaces,Props interface in frontmatter,Untyped props,"interface Props { title: string }",No props typing,Medium,
|
||||
41,TypeScript,Use strict mode,Catch errors early,strict: true in tsconfig,Loose TypeScript config,strictest template,base template,Low,
|
||||
42,Markdown,Use MDX for components,Components in markdown content,@astrojs/mdx for interactive docs,Plain markdown with workarounds,<Component /> in .mdx,HTML in .md files,Medium,https://docs.astro.build/en/guides/integrations-guide/mdx/
|
||||
43,Markdown,Configure markdown plugins,Extend markdown capabilities,remarkPlugins rehypePlugins in config,Manual HTML for features,remarkPlugins: [remarkToc],Manual TOC in every post,Low,
|
||||
44,Markdown,Use frontmatter for metadata,Structured post metadata,Frontmatter with typed schema,Inline metadata,title date in frontmatter,# Title as first line,Medium,
|
||||
45,API,Use API routes for endpoints,Server endpoints in pages/api,pages/api/[endpoint].ts for APIs,External API for simple endpoints,pages/api/posts.json.ts,Separate Express server,Medium,https://docs.astro.build/en/guides/endpoints/
|
||||
46,API,Return proper responses,Use Response object,new Response() with headers,Plain objects,return new Response(JSON.stringify(data)),return data,Medium,
|
||||
47,API,Handle methods correctly,Export named method handlers,export GET POST handlers,Single default export,export const GET = async () => {},export default async () => {},Low,
|
||||
48,Security,Sanitize user content,Prevent XSS in dynamic content,set:html only for trusted content,set:html with user input,"<Fragment set:html={sanitized} />","<div set:html={userInput} />",High,
|
||||
49,Security,Use HTTPS in production,Secure connections,HTTPS for all production sites,HTTP in production,https://example.com,http://example.com,High,
|
||||
50,Security,Validate API input,Check and sanitize all input,Zod validation for API routes,Trust all input,const body = schema.parse(data),const body = await request.json(),High,
|
||||
51,Build,Use hybrid rendering,Mix static and dynamic pages,output: 'hybrid' for flexibility,All SSR or all static,prerender per-page basis,Single rendering mode,Medium,https://docs.astro.build/en/guides/server-side-rendering/#hybrid-rendering
|
||||
52,Build,Analyze bundle size,Monitor JS bundle impact,Build output shows bundle sizes,Ignore bundle growth,Check astro build output,No size monitoring,Medium,
|
||||
53,Build,Use prefetch,Preload linked pages,prefetch integration,No prefetch for navigation,npx astro add prefetch,Manual prefetch,Low,https://docs.astro.build/en/guides/prefetch/
|
||||
|
Can't render this file because it contains an unexpected character in line 14 and column 147.
|
53
.trae/skills/ui-ux-pro-max/data/stacks/flutter.csv
Normal file
53
.trae/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
.trae/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv
Normal file
56
.trae/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
.trae/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv
Normal file
53
.trae/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv
Normal file
@ -0,0 +1,53 @@
|
||||
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
|
||||
1,Composable,Pure UI composables,Composable functions should only render UI,Accept state and callbacks,Calling usecase/repo,Pure UI composable,Business logic in UI,High,https://developer.android.com/jetpack/compose/mental-model
|
||||
2,Composable,Small composables,Each composable has single responsibility,Split into components,Huge composable,Reusable UI,Monolithic UI,Medium,
|
||||
3,Composable,Stateless by default,Prefer stateless composables,Hoist state,Local mutable state,Stateless UI,Hidden state,High,https://developer.android.com/jetpack/compose/state#state-hoisting
|
||||
4,State,Single source of truth,UI state comes from one source,StateFlow from VM,Multiple states,Unified UiState,Scattered state,High,https://developer.android.com/topic/architecture/ui-layer
|
||||
5,State,Model UI State,Use sealed interface/data class,UiState.Loading,Boolean flags,Explicit state,Flag hell,High,
|
||||
6,State,remember only UI state,remember for UI-only state,"Scroll, animation",Business state,Correct remember,Misuse remember,High,https://developer.android.com/jetpack/compose/state
|
||||
7,State,rememberSaveable,Persist state across config,rememberSaveable,remember,State survives,State lost,High,https://developer.android.com/jetpack/compose/state#restore-ui-state
|
||||
8,State,derivedStateOf,Optimize recomposition,derivedStateOf,Recompute always,Optimized,Jank,Medium,https://developer.android.com/jetpack/compose/performance
|
||||
9,SideEffect,LaunchedEffect keys,Use correct keys,LaunchedEffect(id),LaunchedEffect(Unit),Scoped effect,Infinite loop,High,https://developer.android.com/jetpack/compose/side-effects
|
||||
10,SideEffect,rememberUpdatedState,Avoid stale lambdas,rememberUpdatedState,Capture directly,Safe callback,Stale state,Medium,https://developer.android.com/jetpack/compose/side-effects
|
||||
11,SideEffect,DisposableEffect,Clean up resources,onDispose,No cleanup,No leak,Memory leak,High,
|
||||
12,Architecture,Unidirectional data flow,UI → VM → State,onEvent,Two-way binding,Predictable flow,Hard debug,High,https://developer.android.com/topic/architecture
|
||||
13,Architecture,No business logic in UI,Logic belongs to VM,Collect state,Call repo,Clean UI,Fat UI,High,
|
||||
14,Architecture,Expose immutable state,Expose StateFlow,asStateFlow,Mutable exposed,Safe API,State mutation,High,
|
||||
15,Lifecycle,Lifecycle-aware collect,Use collectAsStateWithLifecycle,Lifecycle aware,collectAsState,No leak,Leak,High,https://developer.android.com/jetpack/compose/lifecycle
|
||||
16,Navigation,Event-based navigation,VM emits navigation event,"VM: Channel + receiveAsFlow(), V: Collect with Dispatchers.Main.immediate",Nav in UI,Decoupled nav,Using State / SharedFlow for navigation -> event is replayed and navigation fires again (StateFlow),High,https://developer.android.com/jetpack/compose/navigation
|
||||
17,Navigation,Typed routes,Use sealed routes,sealed class Route,String routes,Type-safe,Runtime crash,Medium,
|
||||
18,Performance,Stable parameters,Prefer immutable/stable params,@Immutable,Mutable params,Stable recomposition,Extra recomposition,High,https://developer.android.com/jetpack/compose/performance
|
||||
19,Performance,Use key in Lazy,Provide stable keys,key=id,No key,Stable list,Item jump,High,
|
||||
20,Performance,Avoid heavy work,No heavy computation in UI,Precompute in VM,Compute in UI,Smooth UI,Jank,High,
|
||||
21,Performance,Remember expensive objects,remember heavy objects,remember,Recreate each recomposition,Efficient,Wasteful,Medium,
|
||||
22,Theming,Design system,Centralized theme,Material3 tokens,Hardcoded values,Consistent UI,Inconsistent,High,https://developer.android.com/jetpack/compose/themes
|
||||
23,Theming,Dark mode support,Theme-based colors,colorScheme,Fixed color,Adaptive UI,Broken dark,Medium,
|
||||
24,Layout,Prefer Modifier over extra layouts,Use Modifier to adjust layout instead of adding wrapper composables,Use Modifier.padding(),Wrap content with extra Box,Padding via modifier,Box just for padding,High,https://developer.android.com/jetpack/compose/modifiers
|
||||
25,Layout,Avoid deep layout nesting,Deep layout trees increase measure & layout cost,Keep layout flat,Box ? Column ? Box ? Row,Flat hierarchy,Deep nested tree,High,
|
||||
26,Layout,Use Row/Column for linear layout,Linear layouts are simpler and more performant,Use Row / Column,Custom layout for simple cases,Row/Column usage,Over-engineered layout,High,
|
||||
27,Layout,Use Box only for overlapping content,Box should be used only when children overlap,Stack elements,Use Box as Column,Proper overlay,Misused Box,Medium,
|
||||
28,Layout,Prefer LazyColumn over Column scroll,Lazy layouts are virtualized and efficient,LazyColumn,Column.verticalScroll(),Lazy list,Scrollable Column,High,https://developer.android.com/jetpack/compose/lists
|
||||
29,Layout,Avoid nested scroll containers,Nested scrolling causes UX & performance issues,Single scroll container,Scroll inside scroll,One scroll per screen,Nested scroll,High,
|
||||
30,Layout,Avoid fillMaxSize by default,fillMaxSize may break parent constraints,Use exact size,Fill max everywhere,Constraint-aware size,Overfilled layout,Medium,
|
||||
31,Layout,Avoid intrinsic size unless necessary,Intrinsic measurement is expensive,Explicit sizing,IntrinsicSize.Min,Predictable layout,Expensive measure,High,https://developer.android.com/jetpack/compose/layout/intrinsics
|
||||
32,Layout,Use Arrangement and Alignment APIs,Declare layout intent explicitly,Use Arrangement / Alignment,Manual spacing hacks,Declarative spacing,Magic spacing,High,
|
||||
33,Layout,Extract reusable layout patterns,Repeated layouts should be shared,Create layout composable,Copy-paste layouts,Reusable scaffold,Duplicated layout,High,
|
||||
34,Theming,No hardcoded text style,Use typography,MaterialTheme.typography,Hardcode sp,Scalable,Inconsistent,Medium,
|
||||
35,Testing,Stateless UI testing,Composable easy to test,Pass state,Hidden state,Testable,Hard test,High,https://developer.android.com/jetpack/compose/testing
|
||||
36,Testing,Use testTag,Stable UI selectors,Modifier.testTag,Find by text,Stable tests,Flaky tests,Medium,
|
||||
37,Preview,Multiple previews,Preview multiple states,@Preview,Single preview,Better dev UX,Misleading,Low,https://developer.android.com/jetpack/compose/tooling/preview
|
||||
38,DI,Inject VM via Hilt,Use hiltViewModel,@HiltViewModel,Manual VM,Clean DI,Coupling,High,https://developer.android.com/training/dependency-injection/hilt-jetpack
|
||||
39,DI,No DI in UI,Inject in VM,Constructor inject,Inject composable,Proper scope,Wrong scope,High,
|
||||
40,Accessibility,Content description,Accessible UI,contentDescription,Ignore a11y,Inclusive,A11y fail,Medium,https://developer.android.com/jetpack/compose/accessibility
|
||||
41,Accessibility,Semantics,Use semantics API,Modifier.semantics,None,Testable a11y,Invisible,Medium,
|
||||
42,Animation,Compose animation APIs,Use animate*AsState,AnimatedVisibility,Manual anim,Smooth,Jank,Medium,https://developer.android.com/jetpack/compose/animation
|
||||
43,Animation,Avoid animation logic in VM,Animation is UI concern,Animate in UI,Animate in VM,Correct layering,Mixed concern,Low,
|
||||
44,Modularization,Feature-based UI modules,UI per feature,:feature:ui,God module,Scalable,Tight coupling,High,https://developer.android.com/topic/modularization
|
||||
45,Modularization,Public UI contracts,Expose minimal UI API,Interface/Route,Expose impl,Encapsulated,Leaky module,Medium,
|
||||
46,State,Snapshot state only,Use Compose state,mutableStateOf,Custom observable,Compose aware,Buggy UI,Medium,
|
||||
47,State,Avoid mutable collections,Immutable list/map,PersistentList,MutableList,Stable UI,Silent bug,High,
|
||||
48,Lifecycle,RememberCoroutineScope usage,Only for UI jobs,UI coroutine,Long jobs,Scoped job,Leak,Medium,https://developer.android.com/jetpack/compose/side-effects#remembercoroutinescope
|
||||
49,Interop,Interop View carefully,Use AndroidView,Isolated usage,Mix everywhere,Safe interop,Messy UI,Low,https://developer.android.com/jetpack/compose/interop
|
||||
50,Interop,Avoid legacy patterns,No LiveData in UI,StateFlow,LiveData,Modern stack,Legacy debt,Medium,
|
||||
51,Debug,Use layout inspector,Inspect recomposition,Tools,Blind debug,Fast debug,Guessing,Low,https://developer.android.com/studio/debug/layout-inspector
|
||||
52,Debug,Enable recomposition counts,Track recomposition,Debug flags,Ignore,Performance aware,Hidden jank,Low,
|
||||
|
53
.trae/skills/ui-ux-pro-max/data/stacks/nextjs.csv
Normal file
53
.trae/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
.trae/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv
Normal file
51
.trae/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
.trae/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv
Normal file
59
.trae/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
.trae/skills/ui-ux-pro-max/data/stacks/react-native.csv
Normal file
52
.trae/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
.trae/skills/ui-ux-pro-max/data/stacks/react.csv
Normal file
54
.trae/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,
|
||||
|
61
.trae/skills/ui-ux-pro-max/data/stacks/shadcn.csv
Normal file
61
.trae/skills/ui-ux-pro-max/data/stacks/shadcn.csv
Normal file
@ -0,0 +1,61 @@
|
||||
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
|
||||
1,Setup,Use CLI for installation,Install components via shadcn CLI for proper setup,npx shadcn@latest add component-name,Manual copy-paste from docs,npx shadcn@latest add button,Copy component code manually,High,https://ui.shadcn.com/docs/cli
|
||||
2,Setup,Initialize project properly,Run init command to set up components.json and globals.css,npx shadcn@latest init before adding components,Skip init and add components directly,npx shadcn@latest init,npx shadcn@latest add button (without init),High,https://ui.shadcn.com/docs/installation
|
||||
3,Setup,Configure path aliases,Set up proper import aliases in tsconfig and components.json,Use @/components/ui path aliases,Relative imports like ../../components,import { Button } from "@/components/ui/button",import { Button } from "../../components/ui/button",Medium,https://ui.shadcn.com/docs/installation
|
||||
4,Theming,Use CSS variables for colors,Define colors as CSS variables in globals.css for theming,CSS variables in :root and .dark,Hardcoded color values in components,bg-primary text-primary-foreground,bg-blue-500 text-white,High,https://ui.shadcn.com/docs/theming
|
||||
5,Theming,Follow naming convention,Use semantic color names with foreground pattern,primary/primary-foreground secondary/secondary-foreground,Generic color names,--primary --primary-foreground,--blue --light-blue,Medium,https://ui.shadcn.com/docs/theming
|
||||
6,Theming,Support dark mode,Include .dark class styles for all custom CSS,Define both :root and .dark color schemes,Only light mode colors,.dark { --background: 240 10% 3.9%; },No .dark class styles,High,https://ui.shadcn.com/docs/dark-mode
|
||||
7,Components,Use component variants,Leverage cva variants for consistent styling,Use variant prop for different styles,Inline conditional classes,<Button variant="destructive">,<Button className={isError ? "bg-red-500" : "bg-blue-500"}>,Medium,https://ui.shadcn.com/docs/components/button
|
||||
8,Components,Compose with className,Add custom classes via className prop for overrides,Extend with className for one-off customizations,Modify component source directly,<Button className="w-full">,Edit button.tsx to add w-full,Medium,https://ui.shadcn.com/docs/components/button
|
||||
9,Components,Use size variants consistently,Apply size prop for consistent sizing across components,size="sm" size="lg" for sizing,Mix size classes inconsistently,<Button size="lg">,<Button className="text-lg px-8 py-4">,Medium,https://ui.shadcn.com/docs/components/button
|
||||
10,Components,Prefer compound components,Use provided sub-components for complex UI,Card + CardHeader + CardContent pattern,Single component with many props,<Card><CardHeader><CardTitle>,<Card title="x" content="y" footer="z">,Medium,https://ui.shadcn.com/docs/components/card
|
||||
11,Dialog,Use Dialog for modal content,Dialog component for overlay modal windows,Dialog for confirmations forms details,Alert for modal content,<Dialog><DialogContent>,<Alert> styled as modal,High,https://ui.shadcn.com/docs/components/dialog
|
||||
12,Dialog,Handle dialog state properly,Use open and onOpenChange for controlled dialogs,Controlled state with useState,Uncontrolled with default open only,"<Dialog open={open} onOpenChange={setOpen}>","<Dialog defaultOpen={true}>",Medium,https://ui.shadcn.com/docs/components/dialog
|
||||
13,Dialog,Include proper dialog structure,Use DialogHeader DialogTitle DialogDescription,Complete semantic structure,Missing title or description,<DialogHeader><DialogTitle><DialogDescription>,<DialogContent><p>Content</p></DialogContent>,High,https://ui.shadcn.com/docs/components/dialog
|
||||
14,Sheet,Use Sheet for side panels,Sheet component for slide-out panels and drawers,Sheet for navigation filters settings,Dialog for side content,<Sheet side="right">,<Dialog> with slide animation,Medium,https://ui.shadcn.com/docs/components/sheet
|
||||
15,Sheet,Specify sheet side,Set side prop for sheet slide direction,Explicit side="left" or side="right",Default side without consideration,<Sheet><SheetContent side="left">,<Sheet><SheetContent>,Low,https://ui.shadcn.com/docs/components/sheet
|
||||
16,Form,Use Form with react-hook-form,Integrate Form component with react-hook-form for validation,useForm + Form + FormField pattern,Custom form handling without Form,<Form {...form}><FormField control={form.control}>,<form onSubmit={handleSubmit}>,High,https://ui.shadcn.com/docs/components/form
|
||||
17,Form,Use FormField for inputs,Wrap inputs in FormField for proper labeling and errors,FormField + FormItem + FormLabel + FormControl,Input without FormField wrapper,<FormField><FormItem><FormLabel><FormControl><Input>,<Input onChange={...}>,High,https://ui.shadcn.com/docs/components/form
|
||||
18,Form,Display form messages,Use FormMessage for validation error display,FormMessage after FormControl,Custom error text without FormMessage,<FormControl><Input/></FormControl><FormMessage/>,<Input/>{error && <span>{error}</span>},Medium,https://ui.shadcn.com/docs/components/form
|
||||
19,Form,Use Zod for validation,Define form schema with Zod for type-safe validation,zodResolver with form schema,Manual validation logic,zodResolver(formSchema),validate: (values) => { if (!values.email) },Medium,https://ui.shadcn.com/docs/components/form
|
||||
20,Select,Use Select for dropdowns,Select component for option selection,Select for choosing from list,Native select element,<Select><SelectTrigger><SelectContent>,<select><option>,Medium,https://ui.shadcn.com/docs/components/select
|
||||
21,Select,Structure Select properly,Include Trigger Value Content and Items,Complete Select structure,Missing SelectValue or SelectContent,<SelectTrigger><SelectValue/></SelectTrigger><SelectContent><SelectItem>,<Select><option>,High,https://ui.shadcn.com/docs/components/select
|
||||
22,Command,Use Command for search,Command component for searchable lists and palettes,Command for command palette search,Input with custom dropdown,<Command><CommandInput><CommandList>,<Input><div className="dropdown">,Medium,https://ui.shadcn.com/docs/components/command
|
||||
23,Command,Group command items,Use CommandGroup for categorized items,CommandGroup with heading for sections,Flat list without grouping,<CommandGroup heading="Suggestions"><CommandItem>,<CommandItem> without groups,Low,https://ui.shadcn.com/docs/components/command
|
||||
24,Table,Use Table for data display,Table component for structured data,Table for tabular data display,Div grid for table-like layouts,<Table><TableHeader><TableBody><TableRow>,<div className="grid">,Medium,https://ui.shadcn.com/docs/components/table
|
||||
25,Table,Include proper table structure,Use TableHeader TableBody TableRow TableCell,Semantic table structure,Missing thead or tbody,<TableHeader><TableRow><TableHead>,<Table><TableRow> without header,High,https://ui.shadcn.com/docs/components/table
|
||||
26,DataTable,Use DataTable for complex tables,Combine Table with TanStack Table for features,DataTable pattern for sorting filtering pagination,Custom table implementation,useReactTable + Table components,Custom sort filter pagination logic,Medium,https://ui.shadcn.com/docs/components/data-table
|
||||
27,Tabs,Use Tabs for content switching,Tabs component for tabbed interfaces,Tabs for related content sections,Custom tab implementation,<Tabs><TabsList><TabsTrigger><TabsContent>,<div onClick={() => setTab(...)},Medium,https://ui.shadcn.com/docs/components/tabs
|
||||
28,Tabs,Set default tab value,Specify defaultValue for initial tab,defaultValue on Tabs component,No default leaving first tab,<Tabs defaultValue="account">,<Tabs> without defaultValue,Low,https://ui.shadcn.com/docs/components/tabs
|
||||
29,Accordion,Use Accordion for collapsible,Accordion for expandable content sections,Accordion for FAQ settings panels,Custom collapse implementation,<Accordion><AccordionItem><AccordionTrigger>,<div onClick={() => setOpen(!open)}>,Medium,https://ui.shadcn.com/docs/components/accordion
|
||||
30,Accordion,Choose accordion type,Use type="single" or type="multiple" appropriately,type="single" for one open type="multiple" for many,Default type without consideration,<Accordion type="single" collapsible>,<Accordion> without type,Low,https://ui.shadcn.com/docs/components/accordion
|
||||
31,Toast,Use Sonner for toasts,Sonner integration for toast notifications,toast() from sonner for notifications,Custom toast implementation,toast("Event created"),setShowToast(true),Medium,https://ui.shadcn.com/docs/components/sonner
|
||||
32,Toast,Add Toaster to layout,Include Toaster component in root layout,<Toaster /> in app layout,Toaster in individual pages,app/layout.tsx: <Toaster />,page.tsx: <Toaster />,High,https://ui.shadcn.com/docs/components/sonner
|
||||
33,Toast,Use toast variants,Apply toast.success toast.error for context,Semantic toast methods,Generic toast for all messages,toast.success("Saved!") toast.error("Failed"),toast("Saved!") toast("Failed"),Medium,https://ui.shadcn.com/docs/components/sonner
|
||||
34,Popover,Use Popover for floating content,Popover for dropdown menus and floating panels,Popover for contextual actions,Absolute positioned divs,<Popover><PopoverTrigger><PopoverContent>,<div className="relative"><div className="absolute">,Medium,https://ui.shadcn.com/docs/components/popover
|
||||
35,Popover,Handle popover alignment,Use align and side props for positioning,Explicit alignment configuration,Default alignment for all,<PopoverContent align="start" side="bottom">,<PopoverContent>,Low,https://ui.shadcn.com/docs/components/popover
|
||||
36,DropdownMenu,Use DropdownMenu for actions,DropdownMenu for action lists and context menus,DropdownMenu for user menu actions,Popover for action lists,<DropdownMenu><DropdownMenuTrigger><DropdownMenuContent>,<Popover> for menu actions,Medium,https://ui.shadcn.com/docs/components/dropdown-menu
|
||||
37,DropdownMenu,Group menu items,Use DropdownMenuGroup and DropdownMenuSeparator,Organized menu with separators,Flat list of items,<DropdownMenuGroup><DropdownMenuItem><DropdownMenuSeparator>,<DropdownMenuItem> without organization,Low,https://ui.shadcn.com/docs/components/dropdown-menu
|
||||
38,Tooltip,Use Tooltip for hints,Tooltip for icon buttons and truncated text,Tooltip for additional context,Title attribute for tooltips,<Tooltip><TooltipTrigger><TooltipContent>,<button title="Delete">,Medium,https://ui.shadcn.com/docs/components/tooltip
|
||||
39,Tooltip,Add TooltipProvider,Wrap app or section in TooltipProvider,TooltipProvider at app level,TooltipProvider per tooltip,<TooltipProvider><App/></TooltipProvider>,<Tooltip><TooltipProvider>,High,https://ui.shadcn.com/docs/components/tooltip
|
||||
40,Skeleton,Use Skeleton for loading,Skeleton component for loading placeholders,Skeleton matching content layout,Spinner for content loading,<Skeleton className="h-4 w-[200px]"/>,<Spinner/> for card loading,Medium,https://ui.shadcn.com/docs/components/skeleton
|
||||
41,Skeleton,Match skeleton dimensions,Size skeleton to match loaded content,Skeleton same size as expected content,Generic skeleton size,<Skeleton className="h-12 w-12 rounded-full"/>,<Skeleton/> without sizing,Medium,https://ui.shadcn.com/docs/components/skeleton
|
||||
42,AlertDialog,Use AlertDialog for confirms,AlertDialog for destructive action confirmation,AlertDialog for delete confirmations,Dialog for confirmations,<AlertDialog><AlertDialogTrigger><AlertDialogContent>,<Dialog> for delete confirmation,High,https://ui.shadcn.com/docs/components/alert-dialog
|
||||
43,AlertDialog,Include action buttons,Use AlertDialogAction and AlertDialogCancel,Standard confirm/cancel pattern,Custom buttons in AlertDialog,<AlertDialogCancel>Cancel</AlertDialogCancel><AlertDialogAction>,<Button>Cancel</Button><Button>Confirm</Button>,Medium,https://ui.shadcn.com/docs/components/alert-dialog
|
||||
44,Sidebar,Use Sidebar for navigation,Sidebar component for app navigation,Sidebar for main app navigation,Custom sidebar implementation,<SidebarProvider><Sidebar><SidebarContent>,<div className="w-64 fixed">,Medium,https://ui.shadcn.com/docs/components/sidebar
|
||||
45,Sidebar,Wrap in SidebarProvider,Use SidebarProvider for sidebar state management,SidebarProvider at layout level,Sidebar without provider,<SidebarProvider><Sidebar></SidebarProvider>,<Sidebar> without provider,High,https://ui.shadcn.com/docs/components/sidebar
|
||||
46,Sidebar,Use SidebarTrigger,Include SidebarTrigger for mobile toggle,SidebarTrigger for responsive toggle,Custom toggle button,<SidebarTrigger/>,<Button onClick={() => toggleSidebar()}>,Medium,https://ui.shadcn.com/docs/components/sidebar
|
||||
47,Chart,Use Chart for data viz,Chart component with Recharts integration,Chart component for dashboards,Direct Recharts without wrapper,<ChartContainer config={chartConfig}>,<ResponsiveContainer><BarChart>,Medium,https://ui.shadcn.com/docs/components/chart
|
||||
48,Chart,Define chart config,Create chartConfig for consistent theming,chartConfig with color definitions,Inline colors in charts,"{ desktop: { label: ""Desktop"", color: ""#2563eb"" } }",<Bar fill="#2563eb"/>,Medium,https://ui.shadcn.com/docs/components/chart
|
||||
49,Chart,Use ChartTooltip,Apply ChartTooltip for interactive charts,ChartTooltip with ChartTooltipContent,Recharts Tooltip directly,<ChartTooltip content={<ChartTooltipContent/>}/>,<Tooltip/> from recharts,Low,https://ui.shadcn.com/docs/components/chart
|
||||
50,Blocks,Use blocks for scaffolding,Start from shadcn blocks for common layouts,npx shadcn@latest add dashboard-01,Build dashboard from scratch,npx shadcn@latest add login-01,Custom login page from scratch,Medium,https://ui.shadcn.com/blocks
|
||||
51,Blocks,Customize block components,Modify copied block code to fit needs,Edit block files after installation,Use blocks without modification,Customize dashboard-01 layout,Use dashboard-01 as-is,Low,https://ui.shadcn.com/blocks
|
||||
52,A11y,Use semantic components,Shadcn components have built-in ARIA,Rely on component accessibility,Override ARIA attributes,<Button> has button role,<div role="button">,High,https://ui.shadcn.com/docs/components/button
|
||||
53,A11y,Maintain focus management,Dialog Sheet handle focus automatically,Let components manage focus,Custom focus handling,<Dialog> traps focus,document.querySelector().focus(),High,https://ui.shadcn.com/docs/components/dialog
|
||||
54,A11y,Provide labels,Use FormLabel and aria-label appropriately,FormLabel for form inputs,Placeholder as only label,<FormLabel>Email</FormLabel><Input/>,<Input placeholder="Email"/>,High,https://ui.shadcn.com/docs/components/form
|
||||
55,Performance,Import components individually,Import only needed components,Named imports from component files,Import all from index,import { Button } from "@/components/ui/button",import { Button Card Dialog } from "@/components/ui",Medium,
|
||||
56,Performance,Lazy load dialogs,Dynamic import for heavy dialog content,React.lazy for dialog content,Import all dialogs upfront,const HeavyContent = lazy(() => import('./Heavy')),import HeavyContent from './Heavy',Medium,
|
||||
57,Customization,Extend variants with cva,Add new variants using class-variance-authority,Extend buttonVariants for new styles,Inline classes for variants,"variants: { size: { xl: ""h-14 px-8"" } }",className="h-14 px-8",Medium,https://ui.shadcn.com/docs/components/button
|
||||
58,Customization,Create custom components,Build new components following shadcn patterns,Use cn() and cva for custom components,Different patterns for custom,const Custom = ({ className }) => <div className={cn("base" className)}>,const Custom = ({ style }) => <div style={style}>,Medium,
|
||||
59,Patterns,Use asChild for composition,asChild prop for component composition,Slot pattern with asChild,Wrapper divs for composition,<Button asChild><Link href="/">,<Button><Link href="/"></Link></Button>,Medium,https://ui.shadcn.com/docs/components/button
|
||||
60,Patterns,Combine with React Hook Form,Form + useForm for complete forms,RHF Controller with shadcn inputs,Custom form state management,<FormField control={form.control} name="email">,<Input value={email} onChange={(e) => setEmail(e.target.value)},High,https://ui.shadcn.com/docs/components/form
|
||||
|
Can't render this file because it contains an unexpected character in line 4 and column 188.
|
54
.trae/skills/ui-ux-pro-max/data/stacks/svelte.csv
Normal file
54
.trae/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
.trae/skills/ui-ux-pro-max/data/stacks/swiftui.csv
Normal file
51
.trae/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
.trae/skills/ui-ux-pro-max/data/stacks/vue.csv
Normal file
50
.trae/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,
|
||||
|
68
.trae/skills/ui-ux-pro-max/data/styles.csv
Normal file
68
.trae/skills/ui-ux-pro-max/data/styles.csv
Normal file
@ -0,0 +1,68 @@
|
||||
No,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,AI Prompt Keywords,CSS/Technical Keywords,Implementation Checklist,Design System Variables
|
||||
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,"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,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,"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,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,"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,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,"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,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,"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,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,"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),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,"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,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,"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,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,"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,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,"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,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,"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,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,"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,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,"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,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,"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,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,"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,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,"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,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,"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,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,"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,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,"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,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,"Design a hero-centric landing page. Use: full-width hero section, compelling headline (60-80 chars), high-contrast CTA button, product screenshot or video, value proposition above fold, gradient or image background, clear visual hierarchy.","min-height: 100vh, display: flex, align-items: center, background: linear-gradient or image, text-shadow for readability, max-width: 800px for text, button with hover scale (1.05)","☐ Hero section full viewport height, ☐ Headline visible above fold, ☐ CTA button high contrast, ☐ Background image optimized (WebP), ☐ Text readable on background, ☐ Mobile responsive layout","--hero-min-height: 100vh, --headline-size: clamp(2rem, 5vw, 4rem), --cta-padding: 1rem 2rem, --overlay-opacity: 0.5, --text-shadow: 0 2px 4px rgba(0,0,0,0.3)"
|
||||
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,"Tailwind 10/10, Bootstrap 9/10",2020s Modern,Medium,"Design a conversion-optimized landing page. Use: single primary CTA, minimal distractions, trust badges, urgency elements (limited time), social proof (testimonials), clear value proposition, form above fold, progress indicators.","form with focus states, input:focus ring, button: primary color high contrast, position: sticky for CTA, max-width: 600px for form, loading spinner, success/error states","☐ Single primary CTA visible, ☐ Form fields minimal (3-5), ☐ Trust badges present, ☐ Social proof above fold, ☐ Mobile form optimized, ☐ Loading states implemented, ☐ A/B test ready","--cta-color: high contrast primary, --form-max-width: 600px, --input-height: 48px, --focus-ring: 3px solid accent, --success-color: #22C55E, --error-color: #EF4444"
|
||||
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,"Tailwind 10/10, Bootstrap 9/10",2020s Modern,Medium,"Design a feature showcase landing page. Use: grid layout for features (3-4 columns), feature cards with icons, benefit-focused copy, alternating sections, comparison tables, interactive demos, problem-solution pairs.","display: grid, grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)), gap: 2rem, card hover effects (translateY -4px), icon containers, alternating background colors","☐ Feature grid responsive, ☐ Icons consistent style, ☐ Card hover effects smooth, ☐ Alternating sections contrast, ☐ Benefits clearly stated, ☐ Mobile stacks properly","--card-padding: 2rem, --card-radius: 12px, --icon-size: 48px, --grid-gap: 2rem, --section-padding: 4rem 0, --hover-transform: translateY(-4px)"
|
||||
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,"Tailwind 10/10, Bootstrap 9/10",2020s Modern,Medium,"Design a minimal direct landing page. Use: single column layout, maximum white space, essential content only, one CTA, clean typography, no decorative elements, fast loading, direct messaging.","max-width: 680px, margin: 0 auto, padding: 4rem 2rem, font-size: 18-20px, line-height: 1.6, minimal animations, no box-shadow, clean borders only","☐ Single column centered, ☐ White space generous, ☐ One primary CTA only, ☐ No decorative images, ☐ Page weight < 500KB, ☐ Load time < 2s","--content-max-width: 680px, --spacing-large: 4rem, --font-size-body: 18px, --line-height: 1.6, --color-text: #1a1a1a, --color-bg: #ffffff"
|
||||
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,"Tailwind 10/10, Bootstrap 9/10",2020s Modern,Medium,"Design a social proof landing page. Use: testimonials with photos, client logos grid, case study cards, review ratings (stars), user count metrics, success stories, trust indicators, before/after comparisons.","testimonial cards with avatar, logo grid (grayscale filter), star rating SVGs, counter animations (count-up), blockquote styling, carousel for testimonials, metric cards","☐ Testimonials with real photos, ☐ Logo grid 6-12 logos, ☐ Star ratings accessible, ☐ Metrics animated on scroll, ☐ Case studies linked, ☐ Mobile carousel works","--avatar-size: 64px, --logo-height: 40px, --star-color: #FBBF24, --metric-font-size: 3rem, --testimonial-bg: #F9FAFB, --blockquote-border: 4px solid accent"
|
||||
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,"Tailwind 10/10, Bootstrap 9/10",2020s Modern,Medium,"Design an interactive demo landing page. Use: embedded product mockup, video walkthrough, step-by-step guide, hover-to-reveal features, live demo button, screenshot carousel, feature highlights on interaction.","video element with controls, position: relative for overlays, hover reveal (opacity transition), step indicators, modal for full demo, screenshot lightbox, play button overlay","☐ Demo video loads fast, ☐ Fallback for no-JS, ☐ Step indicators clear, ☐ Hover states obvious, ☐ Mobile touch friendly, ☐ Demo CTA prominent","--video-aspect-ratio: 16/9, --overlay-bg: rgba(0,0,0,0.7), --step-indicator-size: 32px, --play-button-size: 80px, --transition-duration: 300ms"
|
||||
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,"Tailwind 10/10, Bootstrap 9/10",2020s Modern,Medium,"Design a trust-focused landing page. Use: certification badges, security indicators, expert credentials, industry awards, case study metrics, compliance logos (GDPR, SOC2), guarantee badges, professional photography.","badge grid layout, shield icons, lock icons for security, certificate styling, metric cards with icons, professional color scheme (blue/grey), subtle shadows for depth","☐ Security badges visible, ☐ Certifications verified, ☐ Metrics with sources, ☐ Professional imagery, ☐ Guarantee clearly stated, ☐ Contact info accessible","--badge-height: 48px, --trust-color: #1E40AF, --security-green: #059669, --card-shadow: 0 4px 6px rgba(0,0,0,0.1), --metric-highlight: #F59E0B"
|
||||
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,"Tailwind 10/10, Bootstrap 9/10",2020s Modern,Medium,"Design a storytelling landing page. Use: narrative flow sections, scroll-triggered reveals, chapter-like structure, emotional imagery, brand journey visualization, founder story, mission statement, timeline progression.","scroll-snap sections, Intersection Observer for reveals, parallax backgrounds, section transitions, timeline CSS, narrative typography (varied sizes), image-text alternating","☐ Story flows naturally, ☐ Scroll reveals smooth, ☐ Sections timed well, ☐ Emotional hooks present, ☐ Mobile story readable, ☐ Skip option available","--section-min-height: 100vh, --reveal-duration: 600ms, --narrative-font: serif, --chapter-spacing: 8rem, --timeline-color: accent, --parallax-speed: 0.5"
|
||||
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,"Recharts 9/10, Chart.js 9/10, D3.js 10/10",2020s Modern,Medium,"Design a data-dense dashboard. Use: multiple chart widgets, KPI cards row, data tables with sorting, minimal padding (8-12px), efficient grid layout, filter sidebar, dense but readable typography, maximum information density.","display: grid, grid-template-columns: repeat(12, 1fr), gap: 8px, padding: 12px, font-size: 12-14px, overflow: auto for tables, compact card design, sticky headers","☐ Grid layout 12 columns, ☐ KPI cards responsive, ☐ Tables sortable, ☐ Filters functional, ☐ Loading states for data, ☐ Export functionality","--grid-gap: 8px, --card-padding: 12px, --font-size-small: 12px, --table-row-height: 36px, --sidebar-width: 240px, --header-height: 56px"
|
||||
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,"Recharts 9/10, Chart.js 9/10, D3.js 10/10",2020s Modern,Medium,"Design a heatmap visualization. Use: color gradient scale (cool to hot), cell-based grid, intensity legend, hover tooltips, geographic or matrix layout, divergent color scheme for +/- values, accessible color alternatives.","display: grid, background: linear-gradient for legend, cell hover states, tooltip positioning, color scale (blue→white→red), SVG for geographic, canvas for large datasets","☐ Color scale clear, ☐ Legend visible, ☐ Tooltips informative, ☐ Colorblind alternatives, ☐ Zoom/pan for geo, ☐ Performance for large data","--heatmap-cool: #0080FF, --heatmap-neutral: #FFFFFF, --heatmap-hot: #FF0000, --cell-size: 24px, --legend-width: 200px, --tooltip-bg: rgba(0,0,0,0.9)"
|
||||
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,"Recharts 9/10, Chart.js 9/10, D3.js 10/10",2020s Modern,Medium,"Design an executive dashboard. Use: large KPI cards (4-6 max), trend sparklines, high-level summary only, clean layout with white space, traffic light indicators (red/yellow/green), at-a-glance insights, minimal detail.","display: flex for KPI row, large font-size (24-48px) for metrics, sparkline SVG inline, status indicators (border-left color), card shadows for hierarchy, responsive breakpoints","☐ KPIs 4-6 maximum, ☐ Trends visible, ☐ Status colors clear, ☐ One-page view, ☐ Mobile simplified, ☐ Print-friendly layout","--kpi-font-size: 48px, --sparkline-height: 32px, --status-green: #22C55E, --status-yellow: #F59E0B, --status-red: #EF4444, --card-min-width: 280px"
|
||||
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,"Recharts 9/10, Chart.js 9/10, D3.js 10/10",2020s Modern,Medium,"Design a real-time monitoring dashboard. Use: live status indicators (pulsing), streaming charts, alert notifications, connection status, auto-refresh indicators, critical alerts prominent, system health overview.","animation: pulse for live, WebSocket for streaming, position: fixed for alerts, status-dot with animation, chart real-time updates, notification toast, connection indicator","☐ Live updates working, ☐ Alert sounds optional, ☐ Connection status shown, ☐ Auto-refresh indicated, ☐ Critical alerts prominent, ☐ Offline fallback","--pulse-animation: pulse 2s infinite, --alert-z-index: 1000, --live-indicator: #22C55E, --critical-color: #DC2626, --update-interval: 5s, --toast-duration: 5s"
|
||||
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,"Recharts 9/10, Chart.js 9/10, D3.js 10/10",2020s Modern,Medium,"Design a drill-down analytics dashboard. Use: breadcrumb navigation, expandable sections, summary-to-detail flow, back button prominent, level indicators, context preservation, hierarchical data display.","breadcrumb nav with separators, details/summary for expand, transition for drill animation, position: sticky breadcrumb, nested grid layouts, smooth scroll to detail","☐ Breadcrumbs clear, ☐ Back navigation easy, ☐ Expand animation smooth, ☐ Context preserved, ☐ Mobile drill works, ☐ Deep links supported","--breadcrumb-separator: /, --expand-duration: 300ms, --level-indent: 24px, --back-button-size: 40px, --context-bar-height: 48px, --drill-transition: 300ms ease"
|
||||
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,"Recharts 9/10, Chart.js 9/10, D3.js 10/10",2020s Modern,Medium,"Design a comparison dashboard. Use: side-by-side metrics, period selectors (vs last month), delta indicators (+/-), benchmark lines, A/B comparison tables, winning/losing highlights, percentage change badges.","display: flex for side-by-side, gap for comparison spacing, color coding (green up, red down), arrow indicators, diff highlighting, comparison table zebra striping","☐ Period selector works, ☐ Deltas calculated, ☐ Colors meaningful, ☐ Benchmarks shown, ☐ Mobile stacks properly, ☐ Export comparison","--positive-color: #22C55E, --negative-color: #EF4444, --neutral-color: #6B7280, --comparison-gap: 2rem, --arrow-size: 16px, --badge-padding: 4px 8px"
|
||||
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,"Recharts 9/10, Chart.js 9/10, D3.js 10/10",2020s Modern,Medium,"Design a predictive analytics dashboard. Use: forecast lines (dashed), confidence intervals (shaded bands), trend projections, anomaly highlights, scenario toggles, AI insight cards, probability indicators.","stroke-dasharray for forecast lines, fill-opacity for confidence bands, anomaly markers (circles), tooltip for predictions, toggle switches for scenarios, gradient for probability","☐ Forecast line distinct, ☐ Confidence bands visible, ☐ Anomalies highlighted, ☐ Scenarios switchable, ☐ Predictions dated, ☐ Accuracy shown","--forecast-dash: 5 5, --confidence-opacity: 0.2, --anomaly-color: #F59E0B, --prediction-color: #8B5CF6, --scenario-toggle-width: 48px, --ai-accent: #6366F1"
|
||||
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,"Recharts 9/10, Chart.js 9/10, D3.js 10/10",2020s Modern,Medium,"Design a user behavior analytics dashboard. Use: funnel visualization, user flow diagrams (Sankey), conversion metrics, engagement heatmaps, cohort tables, retention curves, session replay indicators.","SVG funnel with gradients, Sankey diagram library, percentage labels, cohort grid cells, retention chart (line/area), click heatmap overlay, session timeline","☐ Funnel stages clear, ☐ Flow diagram readable, ☐ Conversions calculated, ☐ Cohorts comparable, ☐ Retention trends visible, ☐ Privacy compliant","--funnel-width: 100%, --stage-colors: gradient, --flow-opacity: 0.6, --cohort-cell-size: 40px, --retention-line-color: #3B82F6, --engagement-scale: 5 levels"
|
||||
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,"Recharts 9/10, Chart.js 9/10, D3.js 10/10",2020s Modern,Medium,"Design a financial dashboard. Use: revenue/expense charts, profit margins, budget vs actual, cash flow waterfall, financial ratios, audit trail table, currency formatting, period comparisons.","number formatting (Intl.NumberFormat), waterfall chart (positive/negative bars), variance coloring, table with totals row, sparkline for trends, sticky column headers","☐ Currency formatted, ☐ Decimals consistent, ☐ P&L clear, ☐ Budget variance shown, ☐ Audit trail complete, ☐ Export to Excel","--currency-symbol: $, --decimal-places: 2, --profit-color: #22C55E, --loss-color: #EF4444, --variance-threshold: 10%, --table-header-bg: #F3F4F6"
|
||||
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,"Design a sales intelligence dashboard. Use: pipeline funnel, deal cards (kanban), quota gauges, leaderboard table, territory map, win/loss ratios, forecast accuracy, activity timeline.","kanban columns (flex), gauge chart (SVG arc), leaderboard ranking styles, map integration (Mapbox/Google), timeline vertical, deal card with status border","☐ Pipeline stages shown, ☐ Deals draggable, ☐ Quotas visualized, ☐ Rankings updated, ☐ Territory clickable, ☐ CRM integration","--pipeline-colors: stage gradient, --gauge-track: #E5E7EB, --gauge-fill: primary, --rank-1-color: #FFD700, --rank-2-color: #C0C0C0, --rank-3-color: #CD7F32"
|
||||
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,"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"
|
||||
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,"Design a Bento Box grid layout. Use: modular cards with varied sizes (1x1, 2x1, 2x2), Apple-style aesthetic, rounded corners (16-24px), soft shadows, clean hierarchy, asymmetric grid, neutral backgrounds (#F5F5F7), hover effects.","display: grid, grid-template-columns: repeat(4, 1fr), grid-auto-rows: 200px, gap: 16px, border-radius: 24px, background: #FFFFFF, box-shadow: 0 4px 6px rgba(0,0,0,0.05)","☐ Grid responsive (4→2→1 cols), ☐ Card spans varied, ☐ Rounded corners consistent, ☐ Shadows subtle, ☐ Content fits cards, ☐ Hover scale (1.02)","--grid-gap: 16px, --card-radius: 24px, --card-bg: #FFFFFF, --page-bg: #F5F5F7, --shadow: 0 4px 6px rgba(0,0,0,0.05), --hover-scale: 1.02"
|
||||
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,"Design a Y2K aesthetic interface. Use: neon pink/cyan colors, chrome/metallic textures, bubblegum gradients, glossy buttons, iridescent effects, 2000s futurism, star/sparkle decorations, bubble shapes, tech-optimistic vibe.","background: linear-gradient(135deg, #FF69B4, #00FFFF), filter: drop-shadow for glow, border-radius: 50% for bubbles, metallic gradients (silver/chrome), text-shadow: neon glow, ::before for sparkles","☐ Neon colors balanced, ☐ Chrome effects visible, ☐ Glossy buttons styled, ☐ Bubble shapes decorative, ☐ Sparkle animations, ☐ Retro fonts loaded","--neon-pink: #FF69B4, --neon-cyan: #00FFFF, --chrome-silver: #C0C0C0, --glossy-gradient: linear-gradient(180deg, white 0%, transparent 50%), --glow-blur: 10px"
|
||||
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,"Design a cyberpunk interface. Use: neon colors on dark (#0D0D0D), terminal/HUD aesthetic, glitch effects, scanlines overlay, matrix green accents, monospace fonts, angular shapes, dystopian tech feel.","background: #0D0D0D, color: #00FF00 or #FF00FF, font-family: monospace, text-shadow: 0 0 10px neon, animation: glitch (transform skew), ::before scanlines (repeating-linear-gradient)","☐ Dark background only, ☐ Neon accents visible, ☐ Glitch effect subtle, ☐ Scanlines optional, ☐ Monospace font, ☐ Terminal aesthetic","--bg-dark: #0D0D0D, --neon-green: #00FF00, --neon-magenta: #FF00FF, --neon-cyan: #00FFFF, --scanline-opacity: 0.1, --glitch-duration: 0.3s"
|
||||
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,"Design a biophilic organic interface. Use: nature-inspired colors (greens, browns), organic curved shapes, rounded corners (16-24px), natural textures (wood, stone), flowing SVG elements, wellness aesthetic, earthy palette.","border-radius: 16-24px (varied), background: earth tones, SVG organic shapes (blob), box-shadow: natural soft, color: #228B22 #8B4513 #87CEEB, texture overlays (subtle)","☐ Earth tones dominant, ☐ Organic curves present, ☐ Natural textures subtle, ☐ Green accents, ☐ Rounded everywhere, ☐ Calming feel","--forest-green: #228B22, --earth-brown: #8B4513, --sky-blue: #87CEEB, --cream-bg: #F5F5DC, --organic-radius: 24px, --shadow-soft: 0 8px 32px rgba(0,0,0,0.08)"
|
||||
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,"Design an AI-native interface. Use: minimal chrome, conversational layout, streaming text area, typing indicators (3-dot pulse), context cards, subtle AI accent color (#6366F1), clean input field, response bubbles.","chat bubble layout (flex-direction: column), typing animation (3 dots pulse), streaming text (overflow: hidden + animation), input: sticky bottom, context cards (border-left accent), minimal borders","☐ Chat layout responsive, ☐ Typing indicator smooth, ☐ Input always visible, ☐ Context cards styled, ☐ AI responses distinct, ☐ User messages aligned right","--ai-accent: #6366F1, --user-bubble-bg: #E0E7FF, --ai-bubble-bg: #F9FAFB, --input-height: 48px, --typing-dot-size: 8px, --message-gap: 16px"
|
||||
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,"Design a Memphis style interface. Use: bold geometric shapes (triangles, squiggles, circles), bright clashing colors, 80s postmodern aesthetic, playful patterns, dotted textures, asymmetric layouts, decorative elements.","clip-path: polygon() for shapes, background: repeating patterns, transform: rotate() for tilted elements, mix-blend-mode for overlays, border: dashed/dotted patterns, bold sans-serif","☐ Geometric shapes visible, ☐ Colors bold/clashing, ☐ Patterns present, ☐ Layout asymmetric, ☐ Playful decorations, ☐ 80s vibe achieved","--memphis-pink: #FF71CE, --memphis-yellow: #FFCE5C, --memphis-teal: #86CCCA, --memphis-purple: #6A7BB4, --pattern-size: 20px, --shape-rotation: 15deg"
|
||||
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,"Design a vaporwave aesthetic interface. Use: sunset gradients (pink/cyan/purple), 80s-90s nostalgia, glitch effects, Greek statue imagery, palm trees, grid patterns, neon glow, retro-futuristic feel, dreamy atmosphere.","background: linear-gradient(180deg, #FF71CE, #01CDFE, #B967FF), filter: hue-rotate(), text-shadow: neon glow, retro grid (perspective + linear-gradient), VHS scanlines","☐ Sunset gradient present, ☐ Neon glow applied, ☐ Retro grid visible, ☐ Glitch effects subtle, ☐ Dreamy atmosphere, ☐ 80s-90s aesthetic","--vapor-pink: #FF71CE, --vapor-cyan: #01CDFE, --vapor-mint: #05FFA1, --vapor-purple: #B967FF, --grid-color: rgba(255,255,255,0.1), --glow-intensity: 15px"
|
||||
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,"Design with dimensional layering. Use: z-index depth (multiple layers), overlapping cards, elevation shadows (4 levels), floating elements, parallax depth, backdrop blur for hierarchy, spatial UI feel.","z-index: 1-4 levels, box-shadow: elevation scale (sm/md/lg/xl), transform: translateZ(), backdrop-filter: blur(), position: relative for stacking, parallax on scroll","☐ Layers clearly defined, ☐ Shadows show depth, ☐ Overlaps intentional, ☐ Hierarchy clear, ☐ Performance optimized, ☐ Mobile depth maintained","--elevation-1: 0 1px 3px rgba(0,0,0,0.1), --elevation-2: 0 4px 6px rgba(0,0,0,0.1), --elevation-3: 0 10px 20px rgba(0,0,0,0.1), --elevation-4: 0 20px 40px rgba(0,0,0,0.15), --blur-amount: 8px"
|
||||
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,"Design with exaggerated minimalism. Use: oversized typography (clamp 3rem-12rem), extreme negative space, black/white primary, single accent color only, bold statements, minimal elements, dramatic contrast.","font-size: clamp(3rem, 10vw, 12rem), font-weight: 900, letter-spacing: -0.05em, color: #000 or #FFF, padding: 8rem+, single accent, no decorations","☐ Typography oversized, ☐ White space extreme, ☐ Black/white dominant, ☐ Single accent only, ☐ Elements minimal, ☐ Statement clear","--type-giant: clamp(3rem, 10vw, 12rem), --type-weight: 900, --spacing-huge: 8rem, --color-primary: #000000, --color-bg: #FFFFFF, --accent: single color only"
|
||||
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,"Design with kinetic typography. Use: animated text, scroll-triggered reveals, typing effects, letter-by-letter animations, morphing text, gradient text fills, oversized hero text, text as the main visual element.","@keyframes for text animation, background-clip: text, GSAP SplitText, typing effect (steps()), transform on letters, scroll-triggered (Intersection Observer), variable fonts for morphing","☐ Text animations smooth, ☐ Prefers-reduced-motion respected, ☐ Fallback for no-JS, ☐ Mobile performance ok, ☐ Typing effect timed, ☐ Scroll triggers work","--text-animation-duration: 1s, --letter-delay: 0.05s, --typing-speed: 100ms, --gradient-text: linear-gradient(90deg, #color1, #color2), --morph-duration: 0.5s"
|
||||
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,"Design a parallax storytelling page. Use: scroll-driven narrative, layered backgrounds (3-5 layers), fixed/sticky sections, cinematic transitions, progressive disclosure, full-screen chapters, depth perception.","position: fixed/sticky, transform: translateY(calc()), perspective: 1px, z-index layering, scroll-snap-type, Intersection Observer for triggers, will-change: transform","☐ Layers parallax smoothly, ☐ Story flows naturally, ☐ Mobile alternative provided, ☐ Performance optimized, ☐ Skip option available, ☐ Reduced motion fallback","--parallax-speed-bg: 0.3, --parallax-speed-mid: 0.6, --parallax-speed-fg: 1, --section-height: 100vh, --transition-duration: 600ms, --perspective: 1px"
|
||||
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,"Design with Swiss Modernism 2.0. Use: strict grid system (12 columns), Helvetica/Inter fonts, mathematical spacing, asymmetric balance, high contrast, minimal decoration, clean hierarchy, single accent color.","display: grid, grid-template-columns: repeat(12, 1fr), gap: 1rem (8px base unit), font-family: Inter/Helvetica, font-weight: 400-700, color: #000/#FFF, single accent","☐ 12-column grid strict, ☐ Spacing mathematical, ☐ Typography hierarchy clear, ☐ Single accent only, ☐ No decorations, ☐ High contrast verified","--grid-columns: 12, --grid-gap: 1rem, --base-unit: 8px, --font-primary: Inter, --color-text: #000000, --color-bg: #FFFFFF, --accent: single vibrant"
|
||||
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,"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"
|
||||
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,"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,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,"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"
|
||||
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,"Design a VisionOS-style spatial interface. Use: frosted glass panels, depth layers, translucent backgrounds (15-30% opacity), vibrant colors for active states, gaze-hover effects, floating windows, immersive feel.","backdrop-filter: blur(40px) saturate(180%), background: rgba(255,255,255,0.2), border-radius: 24px, box-shadow: 0 8px 32px rgba(0,0,0,0.1), transform: scale on focus, depth via shadows","☐ Glass effect visible, ☐ Depth layers clear, ☐ Hover states defined, ☐ Colors vibrant on active, ☐ Floating feel achieved, ☐ Contrast maintained","--glass-bg: rgba(255,255,255,0.2), --glass-blur: 40px, --glass-saturate: 180%, --window-radius: 24px, --depth-shadow: 0 8px 32px rgba(0,0,0,0.1), --focus-scale: 1.02"
|
||||
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,"Design an e-ink/paper style interface. Use: high contrast black on off-white, paper texture, no animations (instant transitions), reading-focused, minimal UI chrome, distraction-free, calm aesthetic, monochrome.","background: #FDFBF7 (paper white), color: #1A1A1A, transition: none, font-family: serif for reading, no gradients, border: 1px solid #E0E0E0, texture overlay (noise)","☐ Paper background color, ☐ High contrast text, ☐ No animations, ☐ Reading optimized, ☐ Distraction-free, ☐ Print-friendly","--paper-bg: #FDFBF7, --ink-color: #1A1A1A, --pencil-grey: #4A4A4A, --border-color: #E0E0E0, --font-reading: Georgia, --transition: none"
|
||||
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,"Design a Gen Z chaos maximalist interface. Use: clashing bright colors, sticker overlays, collage aesthetic, raw/unpolished feel, mixed media, ironic elements, loud typography, GIF-heavy, internet culture references.","mix-blend-mode: multiply/screen, transform: rotate(random), animation: jitter, marquee text, position: absolute for scattered elements, filter: saturate(150%), z-index chaos","☐ Colors clash intentionally, ☐ Stickers/overlays present, ☐ Layout chaotic but usable, ☐ GIFs optimized, ☐ Mobile scrollable, ☐ Performance acceptable","--chaos-pink: #FF00FF, --chaos-green: #00FF00, --chaos-yellow: #FFFF00, --chaos-blue: #0000FF, --jitter-amount: 5deg, --saturate: 150%"
|
||||
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,"Design a biomimetic organic interface. Use: cellular/fluid shapes, breathing animations, generative patterns, bioluminescent colors, physics-based movement, nature algorithms, life-like elements, flowing gradients.","SVG morphing (SMIL or GSAP), canvas for generative, animation: breathing (scale pulse), filter: blur for organic, clip-path for cellular, WebGL for advanced, physics libraries","☐ Organic shapes present, ☐ Animations feel alive, ☐ Generative elements, ☐ Performance monitored, ☐ Mobile fallback, ☐ Accessibility alt content","--cellular-pink: #FF9999, --chlorophyll: #00FF41, --bioluminescent: #00FFFF, --breathing-duration: 4s, --morph-ease: cubic-bezier(0.4, 0, 0.2, 1), --organic-blur: 20px"
|
||||
59,Anti-Polish / Raw Aesthetic,General,"Hand-drawn, collage, scanned textures, unfinished, imperfect, authentic, human, sketch, raw marks, creative process","Paper White #FAFAF8, Pencil Grey #4A4A4A, Marker Black #1A1A1A, Kraft Brown #C4A77D","Watercolor washes, pencil shading, ink splatters, tape textures, aged paper tones","No smooth transitions, hand-drawn animations, paper texture overlays, jitter effects, sketch reveal","Creative portfolios, artist sites, indie brands, handmade products, authentic storytelling, editorial","Corporate enterprise, fintech, healthcare, government, polished SaaS",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,✓ High,✓ High,"CSS 10/10, SVG 10/10",2025+ Anti-Digital,Low,"Design with anti-polish raw aesthetic. Use: hand-drawn elements, scanned textures, unfinished look, paper/pencil textures, collage style, authentic imperfection, sketch marks, tape/sticker overlays, human touch.","background: url(paper-texture.png), filter: grayscale() contrast(), border: hand-drawn SVG, transform: rotate(small random), no smooth transitions, sketch-style fonts, opacity variations","☐ Textures loaded, ☐ Hand-drawn elements present, ☐ Imperfections intentional, ☐ Authentic feel achieved, ☐ Performance ok with textures, ☐ Accessibility maintained","--paper-bg: #FAFAF8, --pencil-color: #4A4A4A, --marker-black: #1A1A1A, --kraft-brown: #C4A77D, --sketch-rotation: random(-3deg, 3deg), --texture-opacity: 0.3"
|
||||
60,Tactile Digital / Deformable UI,General,"Jelly buttons, chrome, clay, squishy, deformable, bouncy, physical, tactile feedback, press response","Gradient metallics, Chrome Silver #C0C0C0, Jelly Pink #FF9ECD, Soft Blue #87CEEB","Glossy highlights, shadow depth, reflection effects, material-specific colors","Press deformation (scale + squish), bounce-back (cubic-bezier), material response, haptic-like feedback, spring physics","Modern mobile apps, playful brands, entertainment, gaming UI, consumer products, interactive demos","Enterprise software, data dashboards, accessibility-critical, professional tools",✓ Full,✓ Full,⚠ Good,⚠ Motion sensitive,✓ High,✓ Very High,"Framer Motion 10/10, React Spring 10/10, GSAP 10/10",2025+ Tactile Era,Medium,"Design a tactile deformable interface. Use: jelly/squishy buttons, press deformation effect, bounce-back animations, chrome/clay materials, spring physics, haptic-like feedback, material response, 3D depth on interaction.","transform: scale(0.95) on active, animation: bounce (cubic-bezier(0.34, 1.56, 0.64, 1)), box-shadow: inset for press, filter: brightness on press, spring physics (react-spring/framer-motion)","☐ Press effect visible, ☐ Bounce-back smooth, ☐ Material feels tactile, ☐ Spring physics tuned, ☐ Mobile touch responsive, ☐ Reduced motion option","--press-scale: 0.95, --bounce-duration: 400ms, --spring-stiffness: 300, --spring-damping: 20, --material-glossy: linear-gradient(135deg, white 0%, transparent 60%), --depth-shadow: 0 10px 30px rgba(0,0,0,0.2)"
|
||||
61,Nature Distilled,General,"Muted earthy, skin tones, wood, soil, sand, terracotta, warmth, organic materials, handmade warmth","Terracotta #C67B5C, Sand Beige #D4C4A8, Warm Clay #B5651D, Soft Cream #F5F0E1","Earth Brown #8B4513, Olive Green #6B7B3C, Warm Stone #9C8B7A, muted gradients","Subtle parallax, natural easing (ease-out), texture overlays, grain effects, soft shadows","Wellness brands, sustainable products, artisan goods, organic food, spa/beauty, home decor","Tech startups, gaming, nightlife, corporate finance, high-energy brands",✓ Full,◐ Partial,⚡ Excellent,✓ WCAG AA,✓ High,✓ High,"Tailwind 10/10, CSS 10/10",2025+ Handmade Warmth,Low,"Design with nature distilled aesthetic. Use: muted earthy colors (terracotta, sand, olive), organic materials feel, warm tones, handmade warmth, natural textures, artisan quality, sustainable vibe, soft gradients.","background: warm earth tones, color: #C67B5C #D4C4A8 #6B7B3C, border-radius: organic (varied), box-shadow: soft natural, texture overlays (grain), font: humanist sans-serif","☐ Earth tones dominant, ☐ Warm feel achieved, ☐ Textures subtle, ☐ Handmade quality, ☐ Sustainable messaging, ☐ Calming aesthetic","--terracotta: #C67B5C, --sand-beige: #D4C4A8, --warm-clay: #B5651D, --soft-cream: #F5F0E1, --olive-green: #6B7B3C, --grain-opacity: 0.1"
|
||||
62,Interactive Cursor Design,General,"Custom cursor, cursor as tool, hover effects, cursor feedback, pointer transformation, cursor trail, magnetic cursor","Brand-dependent, cursor accent color, high contrast for visibility","Trail colors, hover state colors, magnetic zone indicators, feedback colors","Cursor scale on hover, magnetic pull to elements, cursor morphing, trail effects, blend mode cursors, click feedback","Creative portfolios, interactive experiences, agency sites, product showcases, gaming, entertainment","Mobile-first (no cursor), accessibility-critical, data-heavy dashboards, forms",✓ Full,✓ Full,⚡ Good,⚠ Not for touch/SR,✗ No cursor,✓ High,"GSAP 10/10, Framer Motion 10/10, Custom JS 10/10",2025+ Interactive,Medium,"Design with interactive cursor effects. Use: custom cursor, cursor morphing on hover, magnetic cursor pull, cursor trails, blend mode cursors, click feedback animations, cursor as interaction tool, pointer transformation.","cursor: none (custom), position: fixed for cursor element, mix-blend-mode: difference, transform on hover targets, magnetic effect (JS position lerp), trail with opacity fade, scale on click","☐ Custom cursor works, ☐ Hover morph smooth, ☐ Magnetic pull subtle, ☐ Trail performance ok, ☐ Click feedback visible, ☐ Touch fallback provided","--cursor-size: 20px, --cursor-hover-scale: 1.5, --magnetic-distance: 100px, --trail-length: 10, --trail-fade: 0.1, --blend-mode: difference"
|
||||
63,Voice-First Multimodal,General,"Voice UI, multimodal, audio feedback, conversational, hands-free, ambient, contextual, speech recognition","Calm neutrals: Soft White #FAFAFA, Muted Blue #6B8FAF, Gentle Purple #9B8FBB","Audio waveform colors, status indicators (listening/processing/speaking), success/error tones","Voice waveform visualization, listening pulse, processing spinner, speak animation, smooth transitions","Voice assistants, accessibility apps, hands-free tools, smart home, automotive UI, cooking apps","Visual-heavy content, data entry, complex forms, noisy environments",✓ Full,✓ Full,⚡ Excellent,✓ Excellent,✓ High,✓ High,"Web Speech API 10/10, React 10/10",2025+ Voice Era,Medium,"Design a voice-first multimodal interface. Use: voice waveform visualization, listening state indicator, speaking animation, minimal visible UI, audio feedback cues, hands-free optimized, conversational flow, ambient design.","Web Speech API integration, canvas for waveform, animation: pulse for listening, status indicators (color change), audio visualization (Web Audio API), minimal chrome, large touch targets","☐ Voice recognition works, ☐ Visual feedback clear, ☐ Listening state obvious, ☐ Speaking animation smooth, ☐ Fallback UI provided, ☐ Accessibility excellent","--listening-color: #6B8FAF, --speaking-color: #22C55E, --waveform-height: 60px, --pulse-duration: 1.5s, --indicator-size: 24px, --voice-accent: #9B8FBB"
|
||||
64,3D Product Preview,General,"360 product view, rotatable, zoomable, touch-to-spin, AR preview, product configurator, interactive 3D model","Product-dependent, neutral backgrounds: Soft Grey #E8E8E8, Pure White #FFFFFF","Shadow gradients, reflection planes, environment lighting colors, accent highlights","Drag-to-rotate, pinch-to-zoom, spin animation, AR placement, material switching, smooth orbit controls","E-commerce, furniture, fashion, automotive, electronics, jewelry, product configurators","Content-heavy sites, blogs, dashboards, low-bandwidth, accessibility-critical",◐ Partial,◐ Partial,❌ Poor (3D rendering),⚠ Alt content needed,◐ Medium,✓ Very High,"Three.js 10/10, model-viewer 10/10, Spline 9/10",2025+ E-commerce 3D,High,"Design a 3D product preview interface. Use: 360° rotation, drag-to-spin, pinch-to-zoom, AR preview button, material/color switcher, hotspot annotations, orbit controls, product configurator, smooth rendering.","Three.js or model-viewer, OrbitControls, touch events for rotation, WebXR for AR, canvas with WebGL, loading placeholder, LOD for performance, environment lighting","☐ 3D model loads fast, ☐ Rotation smooth, ☐ Zoom works (pinch/scroll), ☐ AR button functional, ☐ Colors switchable, ☐ Mobile touch works","--canvas-bg: #F5F5F5, --hotspot-color: #3B82F6, --loading-spinner: primary, --rotation-speed: 0.5, --zoom-min: 0.5, --zoom-max: 2"
|
||||
65,Gradient Mesh / Aurora Evolved,General,"Complex gradients, mesh gradients, multi-color blend, aurora effect, flowing colors, iridescent, holographic, prismatic","Multi-stop gradients: Cyan #00FFFF, Magenta #FF00FF, Yellow #FFFF00, Blue #0066FF, Green #00FF66","Complementary mesh points, smooth color transitions, iridescent overlays, chromatic shifts","CSS mesh-gradient (experimental), SVG gradients, canvas gradients, smooth color morphing, flowing animation","Hero sections, backgrounds, creative brands, music platforms, fashion, lifestyle, premium products","Data interfaces, text-heavy content, accessibility-critical, conservative brands",✓ Full,✓ Full,⚠ Good,⚠ Text contrast,✓ Good,✓ High,"CSS 8/10, SVG 10/10, Canvas 10/10",2025+ Gradient Evolution,Medium,"Design with gradient mesh aurora effect. Use: multi-color mesh gradients, flowing color transitions, aurora/northern lights feel, iridescent overlays, holographic shimmer, prismatic effects, smooth color morphing.","background: conic-gradient or mesh (SVG), animation: gradient flow (background-position), filter: hue-rotate for shimmer, mix-blend-mode: screen, canvas for complex mesh, multiple gradient layers","☐ Mesh gradient visible, ☐ Colors flow smoothly, ☐ Aurora effect achieved, ☐ Performance acceptable, ☐ Text remains readable, ☐ Mobile renders ok","--mesh-color-1: #00FFFF, --mesh-color-2: #FF00FF, --mesh-color-3: #FFFF00, --mesh-color-4: #00FF66, --flow-duration: 10s, --shimmer-intensity: 0.3"
|
||||
66,Editorial Grid / Magazine,General,"Magazine layout, asymmetric grid, editorial typography, pull quotes, drop caps, column layout, print-inspired","High contrast: Black #000000, White #FFFFFF, accent brand color","Muted supporting, pull quote highlights, byline colors, section dividers","Smooth scroll, reveal on scroll, parallax images, text animations, page-flip transitions","News sites, blogs, magazines, editorial content, long-form articles, journalism, publishing","Dashboards, apps, e-commerce catalogs, real-time data, short-form content",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ High,✓ Medium,"CSS Grid 10/10, Tailwind 10/10",2020s Editorial Digital,Low,"Design an editorial magazine layout. Use: asymmetric grid, pull quotes, drop caps, multi-column text, large imagery, bylines, section dividers, print-inspired typography, article hierarchy, white space balance.","display: grid with named areas, column-count for text, ::first-letter for drop caps, blockquote styling, figure/figcaption, gap variations, font: serif for body, variable widths","☐ Grid asymmetric, ☐ Typography editorial, ☐ Pull quotes styled, ☐ Drop caps present, ☐ Images large/impactful, ☐ Mobile reflows well","--grid-cols: asymmetric, --body-font: Georgia/Merriweather, --heading-font: bold sans, --drop-cap-size: 4em, --pull-quote-size: 1.5em, --column-gap: 2rem"
|
||||
67,Chromatic Aberration / RGB Split,General,"RGB split, color fringing, glitch, retro tech, VHS, analog error, distortion, lens effect","Offset RGB: Red #FF0000, Green #00FF00, Blue #0000FF, Black #000000","Neon accents, scan lines, noise overlays, error colors","RGB offset animation, glitch timing, scan line movement, noise flicker, distortion on hover","Music platforms, gaming, tech brands, creative portfolios, nightlife, entertainment, video platforms","Corporate, healthcare, finance, accessibility-critical, elderly users",✓ Full,✓ Dark preferred,⚠ Good,⚠ Can cause strain,◐ Medium,✓ High,"CSS filters 10/10, GSAP 10/10",2020s Retro-Tech,Medium,"Design with chromatic aberration RGB split effect. Use: color channel offset (R/G/B), glitch aesthetic, retro tech feel, VHS error look, lens distortion, scan lines, noise overlay, analog imperfection.","filter: drop-shadow with offset colors, text-shadow: RGB offset (-2px 0 red, 2px 0 cyan), animation: glitch (random offset), ::before for scanlines, mix-blend-mode: screen for overlays","☐ RGB split visible, ☐ Glitch effect controlled, ☐ Scan lines subtle, ☐ Performance ok, ☐ Readability maintained, ☐ Reduced motion option","--rgb-offset: 2px, --red-channel: #FF0000, --green-channel: #00FF00, --blue-channel: #0000FF, --glitch-duration: 0.3s, --scanline-opacity: 0.1"
|
||||
68,Vintage Analog / Retro Film,General,"Film grain, VHS, cassette tape, polaroid, analog warmth, faded colors, light leaks, vintage photography","Faded Cream #F5E6C8, Warm Sepia #D4A574, Muted Teal #4A7B7C, Soft Pink #E8B4B8","Grain overlays, light leak oranges, shadow blues, vintage paper tones, desaturated accents","Film grain overlay, VHS tracking effect, polaroid shake, fade-in transitions, light leak animations","Photography portfolios, music/vinyl brands, vintage fashion, nostalgia marketing, film industry, cafes","Modern tech, SaaS, healthcare, children's apps, corporate enterprise",✓ Full,◐ Partial,⚡ Good,✓ WCAG AA,✓ High,✓ High,"CSS filters 10/10, Canvas 9/10",1970s-90s Analog Revival,Medium,"Design with vintage analog film aesthetic. Use: film grain overlay, faded/desaturated colors, warm sepia tones, light leaks, VHS tracking effect, polaroid frame, analog warmth, nostalgic photography feel.","filter: sepia() contrast() saturate(0.8), background: noise texture overlay, animation: VHS tracking (transform skew), light leak gradient overlay, border for polaroid frame, grain via SVG filter","☐ Film grain visible, ☐ Colors faded/warm, ☐ Light leaks present, ☐ Nostalgic feel achieved, ☐ Performance with filters, ☐ Images look vintage","--sepia-amount: 20%, --contrast: 1.1, --saturation: 0.8, --grain-opacity: 0.15, --light-leak-color: rgba(255,200,100,0.2), --warm-tint: #F5E6C8"
|
||||
|
58
.trae/skills/ui-ux-pro-max/data/typography.csv
Normal file
58
.trae/skills/ui-ux-pro-max/data/typography.csv
Normal file
@ -0,0 +1,58 @@
|
||||
No,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."
|
||||
|
101
.trae/skills/ui-ux-pro-max/data/ui-reasoning.csv
Normal file
101
.trae/skills/ui-ux-pro-max/data/ui-reasoning.csv
Normal file
@ -0,0 +1,101 @@
|
||||
No,UI_Category,Recommended_Pattern,Style_Priority,Color_Mood,Typography_Mood,Key_Effects,Decision_Rules,Anti_Patterns,Severity
|
||||
1,SaaS (General),Hero + Features + CTA,Glassmorphism + Flat Design,Trust blue + Accent contrast,Professional + Hierarchy,Subtle hover (200-250ms) + Smooth transitions,"{""if_ux_focused"": ""prioritize-minimalism"", ""if_data_heavy"": ""add-glassmorphism""}",Excessive animation + Dark mode by default,HIGH
|
||||
2,Micro SaaS,Minimal & Direct + Demo,Flat Design + Vibrant & Block,Vibrant primary + White space,Bold + Clean typography,Large CTA hover (300ms) + Scroll reveal,"{""if_quick_onboarding"": ""reduce-steps"", ""if_demo_available"": ""feature-interactive-demo""}",Complex onboarding flow + Cluttered layout,HIGH
|
||||
3,E-commerce,Feature-Rich Showcase,Vibrant & Block-based,Brand primary + Success green,Engaging + Clear hierarchy,Card hover lift (200ms) + Scale effect,"{""if_luxury"": ""switch-to-liquid-glass"", ""if_conversion_focused"": ""add-urgency-colors""}",Flat design without depth + Text-heavy pages,HIGH
|
||||
4,E-commerce Luxury,Feature-Rich Showcase,Liquid Glass + Glassmorphism,Premium colors + Minimal accent,Elegant + Refined typography,Chromatic aberration + Fluid animations (400-600ms),"{""if_checkout"": ""emphasize-trust"", ""if_hero_needed"": ""use-3d-hyperrealism""}",Vibrant & Block-based + Playful colors,HIGH
|
||||
5,Healthcare App,Social Proof-Focused,Neumorphism + Accessible & Ethical,Calm blue + Health green,Readable + Large type (16px+),Soft box-shadow + Smooth press (150ms),"{""must_have"": ""wcag-aaa-compliance"", ""if_medication"": ""red-alert-colors""}",Bright neon colors + Motion-heavy animations + AI purple/pink gradients,HIGH
|
||||
6,Fintech/Crypto,Conversion-Optimized,Glassmorphism + Dark Mode (OLED),Dark tech colors + Vibrant accents,Modern + Confident typography,Real-time chart animations + Alert pulse/glow,"{""must_have"": ""security-badges"", ""if_real_time"": ""add-streaming-data""}",Light backgrounds + No security indicators,HIGH
|
||||
7,Education,Feature-Rich Showcase,Claymorphism + Micro-interactions,Playful colors + Clear hierarchy,Friendly + Engaging typography,Soft press (200ms) + Fluffy elements,"{""if_gamification"": ""add-progress-animation"", ""if_children"": ""increase-playfulness""}",Dark modes + Complex jargon,MEDIUM
|
||||
8,Portfolio/Personal,Storytelling-Driven,Motion-Driven + Minimalism,Brand primary + Artistic,Expressive + Variable typography,Parallax (3-5 layers) + Scroll-triggered reveals,"{""if_creative_field"": ""add-brutalism"", ""if_minimal_portfolio"": ""reduce-motion""}",Corporate templates + Generic layouts,MEDIUM
|
||||
9,Government/Public,Minimal & Direct,Accessible & Ethical + Minimalism,Professional blue + High contrast,Clear + Large typography,Clear focus rings (3-4px) + Skip links,"{""must_have"": ""wcag-aaa"", ""must_have"": ""keyboard-navigation""}",Ornate design + Low contrast + Motion effects + AI purple/pink gradients,HIGH
|
||||
10,Fintech (Banking),Trust & Authority,Minimalism + Accessible & Ethical,Navy + Trust Blue + Gold,Professional + Trustworthy,Smooth state transitions + Number animations,"{""must_have"": ""security-first"", ""if_dashboard"": ""use-dark-mode""}",Playful design + Unclear fees + AI purple/pink gradients,HIGH
|
||||
11,Social Media App,Feature-Rich Showcase,Vibrant & Block-based + Motion-Driven,Vibrant + Engagement colors,Modern + Bold typography,Large scroll animations + Icon animations,"{""if_engagement_metric"": ""add-motion"", ""if_content_focused"": ""minimize-chrome""}",Heavy skeuomorphism + Accessibility ignored,MEDIUM
|
||||
12,Startup Landing,Hero-Centric + Trust,Motion-Driven + Vibrant & Block,Bold primaries + Accent contrast,Modern + Energetic typography,Scroll-triggered animations + Parallax,"{""if_pre_launch"": ""use-waitlist-pattern"", ""if_video_ready"": ""add-hero-video""}",Static design + No video + Poor mobile,HIGH
|
||||
13,Gaming,Feature-Rich Showcase,3D & Hyperrealism + Retro-Futurism,Vibrant + Neon + Immersive,Bold + Impactful typography,WebGL 3D rendering + Glitch effects,"{""if_competitive"": ""add-real-time-stats"", ""if_casual"": ""increase-playfulness""}",Minimalist design + Static assets,HIGH
|
||||
14,Creative Agency,Storytelling-Driven,Brutalism + Motion-Driven,Bold primaries + Artistic freedom,Bold + Expressive typography,CRT scanlines + Neon glow + Glitch effects,"{""must_have"": ""case-studies"", ""if_boutique"": ""increase-artistic-freedom""}",Corporate minimalism + Hidden portfolio,HIGH
|
||||
15,Wellness/Mental Health,Social Proof-Focused,Neumorphism + Accessible & Ethical,Calm Pastels + Trust colors,Calming + Readable typography,Soft press + Breathing animations,"{""must_have"": ""privacy-first"", ""if_meditation"": ""add-breathing-animation""}",Bright neon + Motion overload,HIGH
|
||||
16,Restaurant/Food,Hero-Centric + Conversion,Vibrant & Block-based + Motion-Driven,Warm colors (Orange Red Brown),Appetizing + Clear typography,Food image reveal + Menu hover effects,"{""must_have"": ""high_quality_images"", ""if_delivery"": ""emphasize-speed""}",Low-quality imagery + Outdated hours,HIGH
|
||||
17,Real Estate,Hero-Centric + Feature-Rich,Glassmorphism + Minimalism,Trust Blue + Gold + White,Professional + Confident,3D property tour zoom + Map hover,"{""if_luxury"": ""add-3d-models"", ""must_have"": ""map-integration""}",Poor photos + No virtual tours,HIGH
|
||||
18,Travel/Tourism,Storytelling-Driven + Hero,Aurora UI + Motion-Driven,Vibrant destination + Sky Blue,Inspirational + Engaging,Destination parallax + Itinerary animations,"{""if_experience_focused"": ""use-storytelling"", ""must_have"": ""mobile-booking""}",Generic photos + Complex booking,HIGH
|
||||
19,SaaS Dashboard,Data-Dense Dashboard,Data-Dense + Heat Map,Cool to Hot gradients + Neutral grey,Clear + Readable typography,Hover tooltips + Chart zoom + Real-time pulse,"{""must_have"": ""real-time-updates"", ""if_large_dataset"": ""prioritize-performance""}",Ornate design + Slow rendering,HIGH
|
||||
20,B2B SaaS Enterprise,Feature-Rich Showcase,Trust & Authority + Minimal,Professional blue + Neutral grey,Formal + Clear typography,Subtle section transitions + Feature reveals,"{""must_have"": ""case-studies"", ""must_have"": ""roi-messaging""}",Playful design + Hidden features + AI purple/pink gradients,HIGH
|
||||
21,Music/Entertainment,Feature-Rich Showcase,Dark Mode (OLED) + Vibrant & Block-based,Dark (#121212) + Vibrant accents + Album art colors,Modern + Bold typography,Waveform visualization + Playlist animations,"{""must_have"": ""audio-player-ux"", ""if_discovery_focused"": ""add-playlist-recommendations""}",Cluttered layout + Poor audio player UX,HIGH
|
||||
22,Video Streaming/OTT,Hero-Centric + Feature-Rich,Dark Mode (OLED) + Motion-Driven,Dark bg + Poster colors + Brand accent,Bold + Engaging typography,Video player animations + Content carousel (parallax),"{""must_have"": ""continue-watching"", ""if_personalized"": ""add-recommendations""}",Static layout + Slow video player,HIGH
|
||||
23,Job Board/Recruitment,Conversion-Optimized + Feature-Rich,Flat Design + Minimalism,Professional Blue + Success Green + Neutral,Clear + Professional typography,Search/filter animations + Application flow,"{""must_have"": ""advanced-search"", ""if_salary_focused"": ""highlight-compensation""}",Outdated forms + Hidden filters,HIGH
|
||||
24,Marketplace (P2P),Feature-Rich Showcase + Social Proof,Vibrant & Block-based + Flat Design,Trust colors + Category colors + Success green,Modern + Engaging typography,Review star animations + Listing hover effects,"{""must_have"": ""seller-profiles"", ""must_have"": ""secure-payment""}",Low trust signals + Confusing layout,HIGH
|
||||
25,Logistics/Delivery,Feature-Rich Showcase + Real-Time,Minimalism + Flat Design,Blue (#2563EB) + Orange (tracking) + Green,Clear + Functional typography,Real-time tracking animation + Status pulse,"{""must_have"": ""tracking-map"", ""must_have"": ""delivery-updates""}",Static tracking + No map integration + AI purple/pink gradients,HIGH
|
||||
26,Agriculture/Farm Tech,Feature-Rich Showcase,Organic Biophilic + Flat Design,Earth Green (#4A7C23) + Brown + Sky Blue,Clear + Informative typography,Data visualization + Weather animations,"{""must_have"": ""sensor-dashboard"", ""if_crop_focused"": ""add-health-indicators""}",Generic design + Ignored accessibility + AI purple/pink gradients,MEDIUM
|
||||
27,Construction/Architecture,Hero-Centric + Feature-Rich,Minimalism + 3D & Hyperrealism,Grey (#4A4A4A) + Orange (safety) + Blueprint Blue,Professional + Bold typography,3D model viewer + Timeline animations,"{""must_have"": ""project-portfolio"", ""if_team_collaboration"": ""add-real-time-updates""}",2D-only layouts + Poor image quality + AI purple/pink gradients,HIGH
|
||||
28,Automotive/Car Dealership,Hero-Centric + Feature-Rich,Motion-Driven + 3D & Hyperrealism,Brand colors + Metallic + Dark/Light,Bold + Confident typography,360 product view + Configurator animations,"{""must_have"": ""vehicle-comparison"", ""must_have"": ""financing-calculator""}",Static product pages + Poor UX,HIGH
|
||||
29,Photography Studio,Storytelling-Driven + Hero-Centric,Motion-Driven + Minimalism,Black + White + Minimal accent,Elegant + Minimal typography,Full-bleed gallery + Before/after reveal,"{""must_have"": ""portfolio-showcase"", ""if_booking"": ""add-calendar-system""}",Heavy text + Poor image showcase,HIGH
|
||||
30,Coworking Space,Hero-Centric + Feature-Rich,Vibrant & Block-based + Glassmorphism,Energetic colors + Wood tones + Brand,Modern + Engaging typography,Space tour video + Amenity reveal animations,"{""must_have"": ""virtual-tour"", ""must_have"": ""booking-system""}",Outdated photos + Confusing layout,MEDIUM
|
||||
31,Cleaning Service,Conversion-Optimized + Trust,Soft UI Evolution + Flat Design,Fresh Blue (#00B4D8) + Clean White + Green,Friendly + Clear typography,Before/after gallery + Service package reveal,"{""must_have"": ""price-transparency"", ""must_have"": ""trust-badges""}",Poor before/after imagery + Hidden pricing,HIGH
|
||||
32,Home Services,Conversion-Optimized + Trust,Flat Design + Trust & Authority,Trust Blue + Safety Orange + Grey,Professional + Clear typography,Emergency contact highlight + Service menu animations,"{""must_have"": ""emergency-contact"", ""must_have"": ""certifications-display""}",Hidden contact info + No certifications,HIGH
|
||||
33,Childcare/Daycare,Social Proof-Focused + Trust,Claymorphism + Vibrant & Block-based,Playful pastels + Safe colors + Warm,Friendly + Playful typography,Parent portal animations + Activity gallery reveal,"{""must_have"": ""parent-communication"", ""must_have"": ""safety-certifications""}",Generic design + Hidden safety info,HIGH
|
||||
34,Senior Care/Elderly,Trust & Authority + Accessible,Accessible & Ethical + Soft UI Evolution,Calm Blue + Warm neutrals + Large text,Large + Clear typography (18px+),Large touch targets + Clear navigation,"{""must_have"": ""wcag-aaa"", ""must_have"": ""family-portal""}",Small text + Complex navigation + AI purple/pink gradients,HIGH
|
||||
35,Medical Clinic,Trust & Authority + Conversion,Accessible & Ethical + Minimalism,Medical Blue (#0077B6) + Trust White,Professional + Readable typography,Online booking flow + Doctor profile reveals,"{""must_have"": ""appointment-booking"", ""must_have"": ""insurance-info""}",Outdated interface + Confusing booking + AI purple/pink gradients,HIGH
|
||||
36,Pharmacy/Drug Store,Conversion-Optimized + Trust,Flat Design + Accessible & Ethical,Pharmacy Green + Trust Blue + Clean White,Clear + Functional typography,Prescription upload flow + Refill reminders,"{""must_have"": ""prescription-management"", ""must_have"": ""drug-interaction-warnings""}",Confusing layout + Privacy concerns + AI purple/pink gradients,HIGH
|
||||
37,Dental Practice,Social Proof-Focused + Conversion,Soft UI Evolution + Minimalism,Fresh Blue + White + Smile Yellow,Friendly + Professional typography,Before/after gallery + Patient testimonial carousel,"{""must_have"": ""before-after-gallery"", ""must_have"": ""appointment-system""}",Poor imagery + No testimonials,HIGH
|
||||
38,Veterinary Clinic,Social Proof-Focused + Trust,Claymorphism + Accessible & Ethical,Caring Blue + Pet colors + Warm,Friendly + Welcoming typography,Pet profile management + Service animations,"{""must_have"": ""pet-portal"", ""must_have"": ""emergency-contact""}",Generic design + Hidden services,MEDIUM
|
||||
39,News/Media Platform,Hero-Centric + Feature-Rich,Minimalism + Flat Design,Brand colors + High contrast,Clear + Readable typography,Breaking news badge + Article reveal animations,"{""must_have"": ""mobile-first-reading"", ""must_have"": ""category-navigation""}",Cluttered layout + Slow loading,HIGH
|
||||
40,Legal Services,Trust & Authority + Minimal,Trust & Authority + Minimalism,Navy Blue (#1E3A5F) + Gold + White,Professional + Authoritative typography,Practice area reveal + Attorney profile animations,"{""must_have"": ""case-results"", ""must_have"": ""credential-display""}",Outdated design + Hidden credentials + AI purple/pink gradients,HIGH
|
||||
41,Beauty/Spa/Wellness Service,Hero-Centric + Social Proof,Soft UI Evolution + Neumorphism,Soft pastels (Pink Sage Cream) + Gold accents,Elegant + Calming typography,Soft shadows + Smooth transitions (200-300ms) + Gentle hover,"{""must_have"": ""booking-system"", ""must_have"": ""before-after-gallery"", ""if_luxury"": ""add-gold-accents""}",Bright neon colors + Harsh animations + Dark mode,HIGH
|
||||
42,Service Landing Page,Hero-Centric + Trust & Authority,Minimalism + Social Proof-Focused,Brand primary + Trust colors,Professional + Clear typography,Testimonial carousel + CTA hover (200ms),"{""must_have"": ""social-proof"", ""must_have"": ""clear-cta""}",Complex navigation + Hidden contact info,HIGH
|
||||
43,B2B Service,Feature-Rich Showcase + Trust,Trust & Authority + Minimalism,Professional blue + Neutral grey,Formal + Clear typography,Section transitions + Feature reveals,"{""must_have"": ""case-studies"", ""must_have"": ""roi-messaging""}",Playful design + Hidden credentials + AI purple/pink gradients,HIGH
|
||||
44,Financial Dashboard,Data-Dense Dashboard,Dark Mode (OLED) + Data-Dense,Dark bg + Red/Green alerts + Trust blue,Clear + Readable typography,Real-time number animations + Alert pulse,"{""must_have"": ""real-time-updates"", ""must_have"": ""high-contrast""}",Light mode default + Slow rendering,HIGH
|
||||
45,Analytics Dashboard,Data-Dense + Drill-Down,Data-Dense + Heat Map,Cool→Hot gradients + Neutral grey,Clear + Functional typography,Hover tooltips + Chart zoom + Filter animations,"{""must_have"": ""data-export"", ""if_large_dataset"": ""virtualize-lists""}",Ornate design + No filtering,HIGH
|
||||
46,Productivity Tool,Interactive Demo + Feature-Rich,Flat Design + Micro-interactions,Clear hierarchy + Functional colors,Clean + Efficient typography,Quick actions (150ms) + Task animations,"{""must_have"": ""keyboard-shortcuts"", ""if_collaboration"": ""add-real-time-cursors""}",Complex onboarding + Slow performance,HIGH
|
||||
47,Design System/Component Library,Feature-Rich + Documentation,Minimalism + Accessible & Ethical,Clear hierarchy + Code-like structure,Monospace + Clear typography,Code copy animations + Component previews,"{""must_have"": ""search"", ""must_have"": ""code-examples""}",Poor documentation + No live preview,HIGH
|
||||
48,AI/Chatbot Platform,Interactive Demo + Minimal,AI-Native UI + Minimalism,Neutral + AI Purple (#6366F1),Modern + Clear typography,Streaming text + Typing indicators + Fade-in,"{""must_have"": ""conversational-ui"", ""must_have"": ""context-awareness""}",Heavy chrome + Slow response feedback,HIGH
|
||||
49,NFT/Web3 Platform,Feature-Rich Showcase,Cyberpunk UI + Glassmorphism,Dark + Neon + Gold (#FFD700),Bold + Modern typography,Wallet connect animations + Transaction feedback,"{""must_have"": ""wallet-integration"", ""must_have"": ""gas-fees-display""}",Light mode default + No transaction status,HIGH
|
||||
50,Creator Economy Platform,Social Proof + Feature-Rich,Vibrant & Block-based + Bento Box Grid,Vibrant + Brand colors,Modern + Bold typography,Engagement counter animations + Profile reveals,"{""must_have"": ""creator-profiles"", ""must_have"": ""monetization-display""}",Generic layout + Hidden earnings,MEDIUM
|
||||
51,Sustainability/ESG Platform,Trust & Authority + Data,Organic Biophilic + Minimalism,Green (#228B22) + Earth tones,Clear + Informative typography,Progress indicators + Impact animations,"{""must_have"": ""data-transparency"", ""must_have"": ""certification-badges""}",Greenwashing visuals + No data,HIGH
|
||||
52,Remote Work/Collaboration,Feature-Rich + Real-Time,Soft UI Evolution + Minimalism,Calm Blue + Neutral grey,Clean + Readable typography,Real-time presence indicators + Notification badges,"{""must_have"": ""status-indicators"", ""must_have"": ""video-integration""}",Cluttered interface + No presence,HIGH
|
||||
53,Pet Tech App,Storytelling + Feature-Rich,Claymorphism + Vibrant & Block-based,Playful + Warm colors,Friendly + Playful typography,Pet profile animations + Health tracking charts,"{""must_have"": ""pet-profiles"", ""if_health"": ""add-vet-integration""}",Generic design + No personality,MEDIUM
|
||||
54,Smart Home/IoT Dashboard,Real-Time Monitoring,Glassmorphism + Dark Mode (OLED),Dark + Status indicator colors,Clear + Functional typography,Device status pulse + Quick action animations,"{""must_have"": ""real-time-controls"", ""must_have"": ""energy-monitoring""}",Slow updates + No automation,HIGH
|
||||
55,EV/Charging Ecosystem,Hero-Centric + Feature-Rich,Minimalism + Aurora UI,Electric Blue (#009CD1) + Green,Modern + Clear typography,Range estimation animations + Map interactions,"{""must_have"": ""charging-map"", ""must_have"": ""range-calculator""}",Poor map UX + Hidden costs,HIGH
|
||||
56,Subscription Box Service,Feature-Rich + Conversion,Vibrant & Block-based + Motion-Driven,Brand + Excitement colors,Engaging + Clear typography,Unboxing reveal animations + Product carousel,"{""must_have"": ""personalization-quiz"", ""must_have"": ""subscription-management""}",Confusing pricing + No unboxing preview,HIGH
|
||||
57,Podcast Platform,Storytelling + Feature-Rich,Dark Mode (OLED) + Minimalism,Dark + Audio waveform accents,Modern + Clear typography,Waveform visualizations + Episode transitions,"{""must_have"": ""audio-player-ux"", ""must_have"": ""episode-discovery""}",Poor audio player + Cluttered layout,HIGH
|
||||
58,Dating App,Social Proof + Feature-Rich,Vibrant & Block-based + Motion-Driven,Warm + Romantic (Pink/Red gradients),Modern + Friendly typography,Profile card swipe + Match animations,"{""must_have"": ""profile-cards"", ""must_have"": ""safety-features""}",Generic profiles + No safety,HIGH
|
||||
59,Micro-Credentials/Badges,Trust & Authority + Feature,Minimalism + Flat Design,Trust Blue + Gold (#FFD700),Professional + Clear typography,Badge reveal animations + Progress tracking,"{""must_have"": ""credential-verification"", ""must_have"": ""progress-display""}",No verification + Hidden progress,MEDIUM
|
||||
60,Knowledge Base/Documentation,FAQ + Minimal,Minimalism + Accessible & Ethical,Clean hierarchy + Minimal color,Clear + Readable typography,Search highlight + Smooth scrolling,"{""must_have"": ""search-first"", ""must_have"": ""version-switching""}",Poor navigation + No search,HIGH
|
||||
61,Hyperlocal Services,Conversion + Feature-Rich,Minimalism + Vibrant & Block-based,Location markers + Trust colors,Clear + Functional typography,Map hover + Provider card reveals,"{""must_have"": ""map-integration"", ""must_have"": ""booking-system""}",No map + Hidden reviews,HIGH
|
||||
62,Luxury/Premium Brand,Storytelling + Feature-Rich,Liquid Glass + Glassmorphism,Black + Gold (#FFD700) + White,Elegant + Refined typography,Slow parallax + Premium reveals (400-600ms),"{""must_have"": ""high-quality-imagery"", ""must_have"": ""storytelling""}",Cheap visuals + Fast animations,HIGH
|
||||
63,Fitness/Gym App,Feature-Rich + Data,Vibrant & Block-based + Dark Mode (OLED),Energetic (Orange #FF6B35) + Dark bg,Bold + Motivational typography,Progress ring animations + Achievement unlocks,"{""must_have"": ""progress-tracking"", ""must_have"": ""workout-plans""}",Static design + No gamification,HIGH
|
||||
64,Hotel/Hospitality,Hero-Centric + Social Proof,Liquid Glass + Minimalism,Warm neutrals + Gold (#D4AF37),Elegant + Welcoming typography,Room gallery + Amenity reveals,"{""must_have"": ""room-booking"", ""must_have"": ""virtual-tour""}",Poor photos + Complex booking,HIGH
|
||||
65,Wedding/Event Planning,Storytelling + Social Proof,Soft UI Evolution + Aurora UI,Soft Pink (#FFD6E0) + Gold + Cream,Elegant + Romantic typography,Gallery reveals + Timeline animations,"{""must_have"": ""portfolio-gallery"", ""must_have"": ""planning-tools""}",Generic templates + No portfolio,HIGH
|
||||
66,Insurance Platform,Conversion + Trust,Trust & Authority + Flat Design,Trust Blue (#0066CC) + Green + Neutral,Clear + Professional typography,Quote calculator animations + Policy comparison,"{""must_have"": ""quote-calculator"", ""must_have"": ""policy-comparison""}",Confusing pricing + No trust signals + AI purple/pink gradients,HIGH
|
||||
67,Banking/Traditional Finance,Trust & Authority + Feature,Minimalism + Accessible & Ethical,Navy (#0A1628) + Trust Blue + Gold,Professional + Trustworthy typography,Smooth number animations + Security indicators,"{""must_have"": ""security-first"", ""must_have"": ""accessibility""}",Playful design + Poor security UX + AI purple/pink gradients,HIGH
|
||||
68,Online Course/E-learning,Feature-Rich + Social Proof,Claymorphism + Vibrant & Block-based,Vibrant learning colors + Progress green,Friendly + Engaging typography,Progress bar animations + Certificate reveals,"{""must_have"": ""progress-tracking"", ""must_have"": ""video-player""}",Boring design + No gamification,HIGH
|
||||
69,Non-profit/Charity,Storytelling + Trust,Accessible & Ethical + Organic Biophilic,Cause-related colors + Trust + Warm,Heartfelt + Readable typography,Impact counter animations + Story reveals,"{""must_have"": ""impact-stories"", ""must_have"": ""donation-transparency""}",No impact data + Hidden financials,HIGH
|
||||
70,Florist/Plant Shop,Hero-Centric + Conversion,Organic Biophilic + Vibrant & Block-based,Natural Green + Floral pinks/purples,Elegant + Natural typography,Product reveal + Seasonal transitions,"{""must_have"": ""delivery-scheduling"", ""must_have"": ""care-guides""}",Poor imagery + No seasonal content,MEDIUM
|
||||
71,Bakery/Cafe,Hero-Centric + Conversion,Vibrant & Block-based + Soft UI Evolution,Warm Brown + Cream + Appetizing accents,Warm + Inviting typography,Menu hover + Order animations,"{""must_have"": ""menu-display"", ""must_have"": ""online-ordering""}",Poor food photos + Hidden hours,HIGH
|
||||
72,Coffee Shop,Hero-Centric + Minimal,Minimalism + Organic Biophilic,Coffee Brown (#6F4E37) + Cream + Warm,Cozy + Clean typography,Menu transitions + Loyalty animations,"{""must_have"": ""menu"", ""if_loyalty"": ""add-rewards-system""}",Generic design + No atmosphere,MEDIUM
|
||||
73,Brewery/Winery,Storytelling + Hero-Centric,Motion-Driven + Storytelling-Driven,Deep amber/burgundy + Gold + Craft,Artisanal + Heritage typography,Tasting note reveals + Heritage timeline,"{""must_have"": ""product-showcase"", ""must_have"": ""story-heritage""}",Generic product pages + No story,HIGH
|
||||
74,Airline,Conversion + Feature-Rich,Minimalism + Glassmorphism,Sky Blue + Brand colors + Trust,Clear + Professional typography,Flight search animations + Boarding pass reveals,"{""must_have"": ""flight-search"", ""must_have"": ""mobile-first""}",Complex booking + Poor mobile,HIGH
|
||||
75,Magazine/Blog,Storytelling + Hero-Centric,Swiss Modernism 2.0 + Motion-Driven,Editorial colors + Brand + Clean white,Editorial + Elegant typography,Article transitions + Category reveals,"{""must_have"": ""article-showcase"", ""must_have"": ""newsletter-signup""}",Poor typography + Slow loading,HIGH
|
||||
76,Freelancer Platform,Feature-Rich + Conversion,Flat Design + Minimalism,Professional Blue + Success Green,Clear + Professional typography,Skill match animations + Review reveals,"{""must_have"": ""portfolio-display"", ""must_have"": ""skill-matching""}",Poor profiles + No reviews,HIGH
|
||||
77,Consulting Firm,Trust & Authority + Minimal,Trust & Authority + Minimalism,Navy + Gold + Professional grey,Authoritative + Clear typography,Case study reveals + Team profiles,"{""must_have"": ""case-studies"", ""must_have"": ""thought-leadership""}",Generic content + No credentials + AI purple/pink gradients,HIGH
|
||||
78,Marketing Agency,Storytelling + Feature-Rich,Brutalism + Motion-Driven,Bold brand colors + Creative freedom,Bold + Expressive typography,Portfolio reveals + Results animations,"{""must_have"": ""portfolio"", ""must_have"": ""results-metrics""}",Boring design + Hidden work,HIGH
|
||||
79,Event Management,Hero-Centric + Feature-Rich,Vibrant & Block-based + Motion-Driven,Event theme colors + Excitement accents,Bold + Engaging typography,Countdown timer + Registration flow,"{""must_have"": ""registration"", ""must_have"": ""agenda-display""}",Confusing registration + No countdown,HIGH
|
||||
80,Conference/Webinar Platform,Feature-Rich + Conversion,Glassmorphism + Minimalism,Professional Blue + Video accent,Professional + Clear typography,Live stream integration + Agenda transitions,"{""must_have"": ""registration"", ""must_have"": ""speaker-profiles""}",Poor video UX + No networking,HIGH
|
||||
81,Membership/Community,Social Proof + Conversion,Vibrant & Block-based + Soft UI Evolution,Community brand colors + Engagement,Friendly + Engaging typography,Member counter + Benefit reveals,"{""must_have"": ""member-benefits"", ""must_have"": ""pricing-tiers""}",Hidden benefits + No community proof,HIGH
|
||||
82,Newsletter Platform,Minimal + Conversion,Minimalism + Flat Design,Brand primary + Clean white + CTA,Clean + Readable typography,Subscribe form + Archive reveals,"{""must_have"": ""subscribe-form"", ""must_have"": ""sample-content""}",Complex signup + No preview,MEDIUM
|
||||
83,Digital Products/Downloads,Feature-Rich + Conversion,Vibrant & Block-based + Motion-Driven,Product colors + Brand + Success green,Modern + Clear typography,Product preview + Instant delivery animations,"{""must_have"": ""product-preview"", ""must_have"": ""instant-delivery""}",No preview + Slow delivery,HIGH
|
||||
84,Church/Religious Organization,Hero-Centric + Social Proof,Accessible & Ethical + Soft UI Evolution,Warm Gold + Deep Purple/Blue + White,Welcoming + Clear typography,Service time highlights + Event calendar,"{""must_have"": ""service-times"", ""must_have"": ""community-events""}",Outdated design + Hidden info,MEDIUM
|
||||
85,Sports Team/Club,Hero-Centric + Feature-Rich,Vibrant & Block-based + Motion-Driven,Team colors + Energetic accents,Bold + Impactful typography,Score animations + Schedule reveals,"{""must_have"": ""schedule"", ""must_have"": ""roster""}",Static content + Poor fan engagement,HIGH
|
||||
86,Museum/Gallery,Storytelling + Feature-Rich,Minimalism + Motion-Driven,Art-appropriate neutrals + Exhibition accents,Elegant + Minimal typography,Virtual tour + Collection reveals,"{""must_have"": ""virtual-tour"", ""must_have"": ""exhibition-info""}",Cluttered layout + No online access,HIGH
|
||||
87,Theater/Cinema,Hero-Centric + Conversion,Dark Mode (OLED) + Motion-Driven,Dark + Spotlight accents + Gold,Dramatic + Bold typography,Seat selection + Trailer reveals,"{""must_have"": ""showtimes"", ""must_have"": ""seat-selection""}",Poor booking UX + No trailers,HIGH
|
||||
88,Language Learning App,Feature-Rich + Social Proof,Claymorphism + Vibrant & Block-based,Playful colors + Progress indicators,Friendly + Clear typography,Progress animations + Achievement unlocks,"{""must_have"": ""progress-tracking"", ""must_have"": ""gamification""}",Boring design + No motivation,HIGH
|
||||
89,Coding Bootcamp,Feature-Rich + Social Proof,Dark Mode (OLED) + Minimalism,Code editor colors + Brand + Success,Technical + Clear typography,Terminal animations + Career outcome reveals,"{""must_have"": ""curriculum"", ""must_have"": ""career-outcomes""}",Light mode only + Hidden results,HIGH
|
||||
90,Cybersecurity Platform,Trust & Authority + Real-Time,Cyberpunk UI + Dark Mode (OLED),Matrix Green (#00FF00) + Deep Black,Technical + Clear typography,Threat visualization + Alert animations,"{""must_have"": ""real-time-monitoring"", ""must_have"": ""threat-display""}",Light mode + Poor data viz,HIGH
|
||||
91,Developer Tool/IDE,Minimal + Documentation,Dark Mode (OLED) + Minimalism,Dark syntax theme + Blue focus,Monospace + Functional typography,Syntax highlighting + Command palette,"{""must_have"": ""keyboard-shortcuts"", ""must_have"": ""documentation""}",Light mode default + Slow performance,HIGH
|
||||
92,Biotech/Life Sciences,Storytelling + Data,Glassmorphism + Clean Science,Sterile White + DNA Blue + Life Green,Scientific + Clear typography,Data visualization + Research reveals,"{""must_have"": ""data-accuracy"", ""must_have"": ""clean-aesthetic""}",Cluttered data + Poor credibility,HIGH
|
||||
93,Space Tech/Aerospace,Immersive + Feature-Rich,Holographic/HUD + Dark Mode,Deep Space Black + Star White + Metallic,Futuristic + Precise typography,Telemetry animations + 3D renders,"{""must_have"": ""high-tech-feel"", ""must_have"": ""precision-data""}",Generic design + No immersion,HIGH
|
||||
94,Architecture/Interior,Portfolio + Hero-Centric,Exaggerated Minimalism + High Imagery,Monochrome + Gold Accent + High Imagery,Architectural + Elegant typography,Project gallery + Blueprint reveals,"{""must_have"": ""high-res-images"", ""must_have"": ""project-portfolio""}",Poor imagery + Cluttered layout,HIGH
|
||||
95,Quantum Computing,Immersive + Interactive,Holographic/HUD + Dark Mode,Quantum Blue (#00FFFF) + Deep Black,Futuristic + Scientific typography,Probability visualizations + Qubit state animations,"{""must_have"": ""complexity-visualization"", ""must_have"": ""scientific-credibility""}",Generic tech design + No viz,HIGH
|
||||
96,Biohacking/Longevity App,Data-Dense + Storytelling,Biomimetic/Organic 2.0 + Minimalism,Cellular Pink/Red + DNA Blue + White,Scientific + Clear typography,Biological data viz + Progress animations,"{""must_have"": ""data-privacy"", ""must_have"": ""scientific-credibility""}",Generic health app + No privacy,HIGH
|
||||
97,Autonomous Drone Fleet,Real-Time + Feature-Rich,HUD/Sci-Fi FUI + Real-Time,Tactical Green + Alert Red + Map Dark,Technical + Functional typography,Telemetry animations + 3D spatial awareness,"{""must_have"": ""real-time-telemetry"", ""must_have"": ""safety-alerts""}",Slow updates + Poor spatial viz,HIGH
|
||||
98,Generative Art Platform,Showcase + Feature-Rich,Minimalism + Gen Z Chaos,Neutral (#F5F5F5) + User Content,Minimal + Content-focused typography,Gallery masonry + Minting animations,"{""must_have"": ""fast-loading"", ""must_have"": ""creator-attribution""}",Heavy chrome + Slow loading,HIGH
|
||||
99,Spatial Computing OS,Immersive + Interactive,Spatial UI (VisionOS) + Glassmorphism,Frosted Glass + System Colors + Depth,Spatial + Readable typography,Depth hierarchy + Gaze interactions,"{""must_have"": ""depth-hierarchy"", ""must_have"": ""environment-awareness""}",2D design + No spatial depth,HIGH
|
||||
100,Sustainable Energy/Climate,Data + Trust,Organic Biophilic + E-Ink/Paper,Earth Green + Sky Blue + Solar Yellow,Clear + Informative typography,Impact viz + Progress animations,"{""must_have"": ""data-transparency"", ""must_have"": ""impact-visualization""}",Greenwashing + No real data,HIGH
|
||||
|
100
.trae/skills/ui-ux-pro-max/data/ux-guidelines.csv
Normal file
100
.trae/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
|
||||
|
31
.trae/skills/ui-ux-pro-max/data/web-interface.csv
Normal file
31
.trae/skills/ui-ux-pro-max/data/web-interface.csv
Normal file
@ -0,0 +1,31 @@
|
||||
No,Category,Issue,Keywords,Platform,Description,Do,Don't,Code Example Good,Code Example Bad,Severity
|
||||
1,Accessibility,Icon Button Labels,icon button aria-label,Web,Icon-only buttons must have accessible names,Add aria-label to icon buttons,Icon button without label,"<button aria-label='Close'><XIcon /></button>","<button><XIcon /></button>",Critical
|
||||
2,Accessibility,Form Control Labels,form input label aria,Web,All form controls need labels or aria-label,Use label element or aria-label,Input without accessible name,"<label for='email'>Email</label><input id='email' />","<input placeholder='Email' />",Critical
|
||||
3,Accessibility,Keyboard Handlers,keyboard onclick onkeydown,Web,Interactive elements must support keyboard interaction,Add onKeyDown alongside onClick,Click-only interaction,"<div onClick={fn} onKeyDown={fn} tabIndex={0}>","<div onClick={fn}>",High
|
||||
4,Accessibility,Semantic HTML,semantic button a label,Web,Use semantic HTML before ARIA attributes,Use button/a/label elements,Div with role attribute,"<button onClick={fn}>Submit</button>","<div role='button' onClick={fn}>Submit</div>",High
|
||||
5,Accessibility,Aria Live,aria-live polite async,Web,Async updates need aria-live for screen readers,Add aria-live='polite' for dynamic content,Silent async updates,"<div aria-live='polite'>{status}</div>","<div>{status}</div> // no announcement",Medium
|
||||
6,Accessibility,Decorative Icons,aria-hidden decorative icon,Web,Decorative icons should be hidden from screen readers,Add aria-hidden='true' to decorative icons,Decorative icon announced,"<Icon aria-hidden='true' />","<Icon /> // announced as 'image'",Medium
|
||||
7,Focus,Visible Focus States,focus-visible outline ring,Web,All interactive elements need visible focus states,Use :focus-visible with ring/outline,No focus indication,"focus-visible:ring-2 focus-visible:ring-blue-500","outline-none // no replacement",Critical
|
||||
8,Focus,Never Remove Outline,outline-none focus replacement,Web,Never remove outline without providing replacement,Replace outline with visible alternative,Remove outline completely,"focus:outline-none focus:ring-2","focus:outline-none // nothing else",Critical
|
||||
9,Focus,Checkbox Radio Hit Target,checkbox radio label target,Web,Checkbox/radio must share hit target with label,Wrap input and label together,Separate tiny checkbox,"<label class='flex gap-2'><input type='checkbox' /><span>Option</span></label>","<input type='checkbox' id='x' /><label for='x'>Option</label>",Medium
|
||||
10,Forms,Autocomplete Attribute,autocomplete input form,Web,Inputs need autocomplete attribute for autofill,Add appropriate autocomplete value,Missing autocomplete,"<input autocomplete='email' type='email' />","<input type='email' />",High
|
||||
11,Forms,Semantic Input Types,input type email tel url,Web,Use semantic input type attributes,Use email/tel/url/number types,text type for everything,"<input type='email' />","<input type='text' /> // for email",Medium
|
||||
12,Forms,Never Block Paste,paste onpaste password,Web,Never prevent paste functionality,Allow paste on all inputs,Block paste on password/code,"<input type='password' />","<input onPaste={e => e.preventDefault()} />",High
|
||||
13,Forms,Spellcheck Disable,spellcheck email code,Web,Disable spellcheck on emails and codes,Set spellcheck='false' on codes,Spellcheck on technical input,"<input spellCheck='false' type='email' />","<input type='email' /> // red squiggles",Low
|
||||
14,Forms,Submit Button Enabled,submit button disabled loading,Web,Keep submit enabled and show spinner during requests,Show loading spinner keep enabled,Disable button during submit,"<button>{loading ? <Spinner /> : 'Submit'}</button>","<button disabled={loading}>Submit</button>",Medium
|
||||
15,Forms,Inline Errors,error message inline focus,Web,Show error messages inline near the problem field,Inline error with focus on first error,Single error at top,"<input /><span class='text-red-500'>{error}</span>","<div class='error'>{allErrors}</div> // at top",High
|
||||
16,Performance,Virtualize Lists,virtualize list 50 items,Web,Virtualize lists exceeding 50 items,Use virtual list for large datasets,Render all items,"<VirtualList items={items} />","items.map(item => <Item />)",High
|
||||
17,Performance,Avoid Layout Reads,layout read render getboundingclientrect,Web,Avoid layout reads during render phase,Read layout in effects or callbacks,getBoundingClientRect in render,"useEffect(() => { el.getBoundingClientRect() })","const rect = el.getBoundingClientRect() // in render",Medium
|
||||
18,Performance,Batch DOM Operations,batch dom write read,Web,Group DOM operations to minimize reflows,Batch writes then reads,Interleave reads and writes,"writes.forEach(w => w()); reads.forEach(r => r())","write(); read(); write(); read(); // thrashing",Medium
|
||||
19,Performance,Preconnect CDN,preconnect link cdn,Web,Add preconnect links for CDN domains,Preconnect to known domains,"<link rel='preconnect' href='https://cdn.example.com' />","// no preconnect hint",Low
|
||||
20,Performance,Lazy Load Images,lazy loading image below-fold,Web,Lazy-load images below the fold,Use loading='lazy' for below-fold images,Load all images eagerly,"<img loading='lazy' src='...' />","<img src='...' /> // above fold only",Medium
|
||||
21,State,URL Reflects State,url state query params,Web,URL should reflect current UI state,Sync filters/tabs/pagination to URL,State only in memory,"?tab=settings&page=2","useState only // lost on refresh",High
|
||||
22,State,Deep Linking,deep link stateful component,Web,Stateful components should support deep-linking,Enable sharing current view via URL,No shareable state,"router.push({ query: { ...filters } })","setFilters(f) // not in URL",Medium
|
||||
23,State,Confirm Destructive Actions,confirm destructive delete modal,Web,Destructive actions require confirmation,Show confirmation dialog before delete,Delete without confirmation,"if (confirm('Delete?')) delete()","onClick={delete} // no confirmation",High
|
||||
24,Typography,Proper Unicode,unicode ellipsis quotes,Web,Use proper Unicode characters,Use ... curly quotes proper dashes,ASCII approximations,"'Hello...' with proper ellipsis","'Hello...' with three dots",Low
|
||||
25,Typography,Text Overflow,truncate line-clamp overflow,Web,Handle text overflow properly,Use truncate/line-clamp/break-words,Text overflows container,"<p class='truncate'>Long text...</p>","<p>Long text...</p> // overflows",Medium
|
||||
26,Typography,Non-Breaking Spaces,nbsp unit brand,Web,Use non-breaking spaces for units and brand names,Use between number and unit,"10 kg or Next.js 14","10 kg // may wrap",Low
|
||||
27,Anti-Pattern,No Zoom Disable,viewport zoom disable,Web,Never disable zoom in viewport meta,Allow user zoom,"<meta name='viewport' content='width=device-width'>","<meta name='viewport' content='maximum-scale=1'>",Critical
|
||||
28,Anti-Pattern,No Transition All,transition all specific,Web,Avoid transition: all - specify properties,Transition specific properties,transition: all,"transition-colors duration-200","transition-all duration-200",Medium
|
||||
29,Anti-Pattern,Outline Replacement,outline-none ring focus,Web,Never use outline-none without replacement,Provide visible focus replacement,Remove outline with nothing,"focus:outline-none focus:ring-2 focus:ring-blue-500","focus:outline-none // alone",Critical
|
||||
30,Anti-Pattern,No Hardcoded Dates,date format intl locale,Web,Use Intl for date/number formatting,Use Intl.DateTimeFormat,Hardcoded date format,"new Intl.DateTimeFormat('en').format(date)","date.toLocaleDateString() // or manual format",Medium
|
||||
|
Can't render this file because it has a wrong number of fields in line 20.
|
253
.trae/skills/ui-ux-pro-max/scripts/core.py
Normal file
253
.trae/skills/ui-ux-pro-max/scripts/core.py
Normal file
@ -0,0 +1,253 @@
|
||||
#!/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", "AI Prompt Keywords"],
|
||||
"output_cols": ["Style Category", "Type", "Keywords", "Primary Colors", "Effects & Animation", "Best For", "Performance", "Accessibility", "Framework Compatibility", "Complexity", "AI Prompt Keywords", "CSS/Technical Keywords", "Implementation Checklist", "Design System Variables"]
|
||||
},
|
||||
"color": {
|
||||
"file": "colors.csv",
|
||||
"search_cols": ["Product Type", "Notes"],
|
||||
"output_cols": ["Product Type", "Primary (Hex)", "Secondary (Hex)", "CTA (Hex)", "Background (Hex)", "Text (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"]
|
||||
},
|
||||
"icons": {
|
||||
"file": "icons.csv",
|
||||
"search_cols": ["Category", "Icon Name", "Keywords", "Best For"],
|
||||
"output_cols": ["Category", "Icon Name", "Keywords", "Library", "Import Code", "Usage", "Best For", "Style"]
|
||||
},
|
||||
"react": {
|
||||
"file": "react-performance.csv",
|
||||
"search_cols": ["Category", "Issue", "Keywords", "Description"],
|
||||
"output_cols": ["Category", "Issue", "Platform", "Description", "Do", "Don't", "Code Example Good", "Code Example Bad", "Severity"]
|
||||
},
|
||||
"web": {
|
||||
"file": "web-interface.csv",
|
||||
"search_cols": ["Category", "Issue", "Keywords", "Description"],
|
||||
"output_cols": ["Category", "Issue", "Platform", "Description", "Do", "Don't", "Code Example Good", "Code Example Bad", "Severity"]
|
||||
}
|
||||
}
|
||||
|
||||
STACK_CONFIG = {
|
||||
"html-tailwind": {"file": "stacks/html-tailwind.csv"},
|
||||
"react": {"file": "stacks/react.csv"},
|
||||
"nextjs": {"file": "stacks/nextjs.csv"},
|
||||
"astro": {"file": "stacks/astro.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"},
|
||||
"shadcn": {"file": "stacks/shadcn.csv"},
|
||||
"jetpack-compose": {"file": "stacks/jetpack-compose.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"],
|
||||
"style": ["style", "design", "ui", "minimalism", "glassmorphism", "neumorphism", "brutalism", "dark mode", "flat", "aurora", "prompt", "css", "implementation", "variable", "checklist", "tailwind"],
|
||||
"ux": ["ux", "usability", "accessibility", "wcag", "touch", "scroll", "animation", "keyboard", "navigation", "mobile"],
|
||||
"typography": ["font", "typography", "heading", "serif", "sans"],
|
||||
"icons": ["icon", "icons", "lucide", "heroicons", "symbol", "glyph", "pictogram", "svg icon"],
|
||||
"react": ["react", "next.js", "nextjs", "suspense", "memo", "usecallback", "useeffect", "rerender", "bundle", "waterfall", "barrel", "dynamic import", "rsc", "server component"],
|
||||
"web": ["aria", "focus", "outline", "semantic", "virtualize", "autocomplete", "form", "input type", "preconnect"]
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
1067
.trae/skills/ui-ux-pro-max/scripts/design_system.py
Normal file
1067
.trae/skills/ui-ux-pro-max/scripts/design_system.py
Normal file
File diff suppressed because it is too large
Load Diff
114
.trae/skills/ui-ux-pro-max/scripts/search.py
Normal file
114
.trae/skills/ui-ux-pro-max/scripts/search.py
Normal file
@ -0,0 +1,114 @@
|
||||
#!/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]
|
||||
python search.py "<query>" --design-system [-p "Project Name"]
|
||||
python search.py "<query>" --design-system --persist [-p "Project Name"] [--page "dashboard"]
|
||||
|
||||
Domains: style, prompt, color, chart, landing, product, ux, typography
|
||||
Stacks: html-tailwind, react, nextjs
|
||||
|
||||
Persistence (Master + Overrides pattern):
|
||||
--persist Save design system to design-system/MASTER.md
|
||||
--page Also create a page-specific override file in design-system/pages/
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import io
|
||||
from core import CSV_CONFIG, AVAILABLE_STACKS, MAX_RESULTS, search, search_stack
|
||||
from design_system import generate_design_system, persist_design_system
|
||||
|
||||
# Force UTF-8 for stdout/stderr to handle emojis on Windows (cp1252 default)
|
||||
if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8':
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
if sys.stderr.encoding and sys.stderr.encoding.lower() != 'utf-8':
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
||||
|
||||
|
||||
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")
|
||||
# Design system generation
|
||||
parser.add_argument("--design-system", "-ds", action="store_true", help="Generate complete design system recommendation")
|
||||
parser.add_argument("--project-name", "-p", type=str, default=None, help="Project name for design system output")
|
||||
parser.add_argument("--format", "-f", choices=["ascii", "markdown"], default="ascii", help="Output format for design system")
|
||||
# Persistence (Master + Overrides pattern)
|
||||
parser.add_argument("--persist", action="store_true", help="Save design system to design-system/MASTER.md (creates hierarchical structure)")
|
||||
parser.add_argument("--page", type=str, default=None, help="Create page-specific override file in design-system/pages/")
|
||||
parser.add_argument("--output-dir", "-o", type=str, default=None, help="Output directory for persisted files (default: current directory)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Design system takes priority
|
||||
if args.design_system:
|
||||
result = generate_design_system(
|
||||
args.query,
|
||||
args.project_name,
|
||||
args.format,
|
||||
persist=args.persist,
|
||||
page=args.page,
|
||||
output_dir=args.output_dir
|
||||
)
|
||||
print(result)
|
||||
|
||||
# Print persistence confirmation
|
||||
if args.persist:
|
||||
project_slug = args.project_name.lower().replace(' ', '-') if args.project_name else "default"
|
||||
print("\n" + "=" * 60)
|
||||
print(f"✅ Design system persisted to design-system/{project_slug}/")
|
||||
print(f" 📄 design-system/{project_slug}/MASTER.md (Global Source of Truth)")
|
||||
if args.page:
|
||||
page_filename = args.page.lower().replace(' ', '-')
|
||||
print(f" 📄 design-system/{project_slug}/pages/{page_filename}.md (Page Overrides)")
|
||||
print("")
|
||||
print(f"📖 Usage: When building a page, check design-system/{project_slug}/pages/[page].md first.")
|
||||
print(f" If exists, its rules override MASTER.md. Otherwise, use MASTER.md.")
|
||||
print("=" * 60)
|
||||
# Stack search
|
||||
elif args.stack:
|
||||
result = search_stack(args.query, args.stack, args.max_results)
|
||||
if args.json:
|
||||
import json
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(format_output(result))
|
||||
# Domain search
|
||||
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))
|
||||
35
.vscode/launch.json
vendored
Normal file
35
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// 使用 IntelliSense 找出 C# 调试存在哪些属性
|
||||
// 将悬停用于现有属性的说明
|
||||
// 有关详细信息,请访问 https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md。
|
||||
"name": ".NET Core Launch (web)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
// 如果已更改目标框架,请确保更新程序路径。
|
||||
"program": "${workspaceFolder}/backend/MilliSim.Api/bin/Debug/net10.0/MilliSim.Api.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/backend/MilliSim.Api",
|
||||
"stopAtEntry": false,
|
||||
// 启用在启动 ASP.NET Core 时启动 Web 浏览器。有关详细信息: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
|
||||
"serverReadyAction": {
|
||||
"action": "openExternally",
|
||||
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
|
||||
},
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"sourceFileMap": {
|
||||
"/Views": "${workspaceFolder}/Views"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": ".NET Core Attach",
|
||||
"type": "coreclr",
|
||||
"request": "attach"
|
||||
}
|
||||
]
|
||||
}
|
||||
41
.vscode/tasks.json
vendored
Normal file
41
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/backend/MilliSim.Api/MilliSim.Api.csproj",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "publish",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"publish",
|
||||
"${workspaceFolder}/backend/MilliSim.Api/MilliSim.Api.csproj",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "watch",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/backend/MilliSim.Api/MilliSim.Api.csproj"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
625
backend/MilliSim.Api/Common/Dtos/Dtos.cs
Normal file
625
backend/MilliSim.Api/Common/Dtos/Dtos.cs
Normal file
@ -0,0 +1,625 @@
|
||||
using MilliSim.Common.Models;
|
||||
|
||||
namespace MilliSim.Common.Dtos;
|
||||
|
||||
// ===== 认证 DTO =====
|
||||
public class LoginRequest
|
||||
{
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
public string CaptchaId { get; set; } = string.Empty;
|
||||
public string CaptchaCode { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class LoginResponse
|
||||
{
|
||||
public string Token { get; set; } = string.Empty;
|
||||
public long UserId { get; set; }
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string Nickname { get; set; } = string.Empty;
|
||||
public string Avatar { get; set; } = string.Empty;
|
||||
public UserRole Role { get; set; }
|
||||
public string RoleName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class ChangePasswordRequest
|
||||
{
|
||||
public string OldPassword { get; set; } = string.Empty;
|
||||
public string NewPassword { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// ===== 用户 DTO =====
|
||||
public class UserDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string Nickname { get; set; } = string.Empty;
|
||||
public string Avatar { get; set; } = string.Empty;
|
||||
public string Phone { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public UserRole Role { get; set; }
|
||||
public string RoleName { get; set; } = string.Empty;
|
||||
public UserStatus Status { get; set; }
|
||||
public long? CreatedBy { get; set; }
|
||||
public string CreatedByName { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
/// <summary>
|
||||
/// 当前授权 ID(一个客户最多一条授权记录,可能为 null)
|
||||
/// </summary>
|
||||
public long? AuthorizationId { get; set; }
|
||||
/// <summary>
|
||||
/// 当前授权状态(Cancelled=0, Active=1, Expired=2)
|
||||
/// </summary>
|
||||
public AuthorizationStatus? AuthorizationStatus { get; set; }
|
||||
/// <summary>
|
||||
/// 当前授权到期时间
|
||||
/// </summary>
|
||||
public DateTime? AuthorizationEndTime { get; set; }
|
||||
}
|
||||
|
||||
public class CreateUserRequest
|
||||
{
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string Nickname { get; set; } = string.Empty;
|
||||
public string Phone { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public UserRole Role { get; set; } = UserRole.Customer;
|
||||
/// <summary>
|
||||
/// 默认密码 hm123456(8位含字母+数字,符合密码强度校验)
|
||||
/// </summary>
|
||||
public string Password { get; set; } = "hm123456";
|
||||
/// <summary>
|
||||
/// 头像(本地上传后的URL)
|
||||
/// </summary>
|
||||
public string Avatar { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class UpdateUserRequest
|
||||
{
|
||||
public string Nickname { get; set; } = string.Empty;
|
||||
public string Avatar { get; set; } = string.Empty;
|
||||
public string Phone { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public UserStatus Status { get; set; } = UserStatus.Active;
|
||||
}
|
||||
|
||||
public class UpdateProfileRequest
|
||||
{
|
||||
public string Nickname { get; set; } = string.Empty;
|
||||
public string Avatar { get; set; } = string.Empty;
|
||||
public string Phone { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class UserQueryRequest : PageRequest
|
||||
{
|
||||
public UserRole? Role { get; set; }
|
||||
public long? CreatedBy { get; set; }
|
||||
public UserStatus? Status { get; set; }
|
||||
/// <summary>
|
||||
/// 是否只查询员工(排除客户角色)。Employees 页面传 true,避免返回客户数据
|
||||
/// </summary>
|
||||
public bool StaffOnly { get; set; }
|
||||
}
|
||||
|
||||
// ===== 项目 DTO =====
|
||||
public class ProjectDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public long CategoryId { get; set; }
|
||||
public string CategoryName { get; set; } = string.Empty;
|
||||
public string CoverImage { get; set; } = string.Empty;
|
||||
public string VideoUrl { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// 视频封面(已废弃,保留字段返回默认值)
|
||||
/// </summary>
|
||||
public string VideoCover { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public string Platforms { get; set; } = string.Empty;
|
||||
public List<string> PlatformList { get; set; } = new();
|
||||
/// <summary>
|
||||
/// 平台信息(含图标URL),供前端展示平台图标
|
||||
/// </summary>
|
||||
public List<PlatformItemDto> PlatformItems { get; set; } = new();
|
||||
public ProjectStatus Status { get; set; }
|
||||
/// <summary>
|
||||
/// 是否推荐(已废弃,保留字段返回默认值)
|
||||
/// </summary>
|
||||
public bool IsFeatured { get; set; }
|
||||
/// <summary>
|
||||
/// 排序值(已废弃,保留字段返回默认值)
|
||||
/// </summary>
|
||||
public int SortOrder { get; set; }
|
||||
public int ViewCount { get; set; }
|
||||
public int ExperienceCount { get; set; }
|
||||
public int FavoriteCount { get; set; }
|
||||
public DateTime? PublishedAt { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public List<string> Tags { get; set; } = new();
|
||||
/// <summary>
|
||||
/// 标签信息(含颜色),供前端展示标签颜色
|
||||
/// </summary>
|
||||
public List<TagItemDto> TagItems { get; set; } = new();
|
||||
/// <summary>
|
||||
/// 分类图标URL,供前端展示分类图标
|
||||
/// </summary>
|
||||
public string CategoryIcon { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// 截图列表(已废弃,保留字段返回空列表)
|
||||
/// </summary>
|
||||
public List<string> Screenshots { get; set; } = new();
|
||||
public bool IsFavorited { get; set; }
|
||||
}
|
||||
|
||||
public class CreateProjectRequest
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public long CategoryId { get; set; }
|
||||
public string CoverImage { get; set; } = string.Empty;
|
||||
public string VideoUrl { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// 视频封面(已废弃,可选字段,不再使用)
|
||||
/// </summary>
|
||||
public string VideoCover { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public string Platforms { get; set; } = string.Empty;
|
||||
public ProjectStatus Status { get; set; } = ProjectStatus.Offline;
|
||||
/// <summary>
|
||||
/// 是否推荐(已废弃,可选字段,不再使用)
|
||||
/// </summary>
|
||||
public bool IsFeatured { get; set; }
|
||||
/// <summary>
|
||||
/// 排序值(已废弃,可选字段,不再使用)
|
||||
/// </summary>
|
||||
public int SortOrder { get; set; }
|
||||
public List<long> TagIds { get; set; } = new();
|
||||
/// <summary>
|
||||
/// 截图列表(已废弃,可选字段,不再使用)
|
||||
/// </summary>
|
||||
public List<string> Screenshots { get; set; } = new();
|
||||
/// <summary>
|
||||
/// 资源包唤醒协议更新列表(仅更新已上传资源包的唤醒协议参数,不涉及文件上传)
|
||||
/// </summary>
|
||||
public List<PackageUpdateDto> Packages { get; set; } = new();
|
||||
}
|
||||
|
||||
public class UpdateProjectRequest : CreateProjectRequest { }
|
||||
|
||||
/// <summary>
|
||||
/// 资源包唤醒协议更新 DTO(用于项目保存时同步更新已上传资源包的唤醒协议参数)
|
||||
/// </summary>
|
||||
public class PackageUpdateDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public PackageType Type { get; set; }
|
||||
public string WakeProtocolPrefix { get; set; } = string.Empty;
|
||||
public string WakeProtocolParam { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 体验记录 DTO:用于个人中心展示用户的项目体验历史
|
||||
/// </summary>
|
||||
public class ExperienceLogDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long ProjectId { get; set; }
|
||||
/// <summary>项目名称(已删除的项目返回空字符串,前端兜底显示"已删除项目")</summary>
|
||||
public string ProjectName { get; set; } = string.Empty;
|
||||
/// <summary>项目封面签名URL(已删除项目返回空字符串)</summary>
|
||||
public string ProjectCover { get; set; } = string.Empty;
|
||||
/// <summary>体验时长(秒)</summary>
|
||||
public int DurationSeconds { get; set; }
|
||||
/// <summary>体验时间</summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class ProjectQueryRequest : PageRequest
|
||||
{
|
||||
public long? CategoryId { get; set; }
|
||||
public string? Platforms { get; set; }
|
||||
public ProjectStatus? Status { get; set; }
|
||||
public bool? IsFeatured { get; set; }
|
||||
public string SortBy { get; set; } = "latest";
|
||||
}
|
||||
|
||||
// ===== 项目关联的图标信息(用于前端展示) =====
|
||||
public class PlatformItemDto
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class TagItemDto
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// 标签的 Icon 字段存储的是颜色 hex 值(如 #1677FF),用于前端标签背景色
|
||||
/// </summary>
|
||||
public string Color { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// ===== 分类/平台/标签 DTO =====
|
||||
public class CategoryDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
public UserStatus Status { get; set; }
|
||||
}
|
||||
|
||||
public class CreateCategoryRequest
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
|
||||
public class PlatformDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
public UserStatus Status { get; set; }
|
||||
}
|
||||
|
||||
public class CreatePlatformRequest
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
|
||||
public class TagDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
public UserStatus Status { get; set; }
|
||||
}
|
||||
|
||||
public class CreateTagRequest
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
|
||||
// ===== 资源包 DTO =====
|
||||
public class PackageDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long ProjectId { get; set; }
|
||||
public PackageType Type { get; set; }
|
||||
/// <summary>
|
||||
/// 原始文件名(上传时的文件名)
|
||||
/// </summary>
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
public string Url { get; set; } = string.Empty;
|
||||
public long FileSize { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
/// <summary>
|
||||
/// 唤醒协议前缀(Android/PC 平台使用,固定为 millisim)
|
||||
/// </summary>
|
||||
public string WakeProtocolPrefix { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// 唤醒协议参数(Android/PC 平台使用)
|
||||
/// </summary>
|
||||
public string WakeProtocolParam { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// WebGL 解压后的根路径对象键(仅 WebGL 类型使用,用于在线运行)
|
||||
/// </summary>
|
||||
public string ExtractedPath { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// ===== 授权 DTO =====
|
||||
public class AuthorizationDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long UserId { get; set; }
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string UserNickname { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// 项目 ID(已废弃,客户级授权不再关联项目)
|
||||
/// </summary>
|
||||
public long? ProjectId { get; set; }
|
||||
public string ProjectName { get; set; } = string.Empty;
|
||||
public long AuthorizedBy { get; set; }
|
||||
public string AuthorizedByName { get; set; } = string.Empty;
|
||||
public DateTime StartTime { get; set; }
|
||||
public DateTime EndTime { get; set; }
|
||||
public AuthorizationStatus Status { get; set; }
|
||||
public string StatusName { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class CreateAuthorizationRequest
|
||||
{
|
||||
public long UserId { get; set; }
|
||||
public int Days { get; set; } = 365;
|
||||
}
|
||||
|
||||
public class BatchAuthorizationRequest
|
||||
{
|
||||
public List<long> UserIds { get; set; } = new();
|
||||
public int Days { get; set; } = 365;
|
||||
}
|
||||
|
||||
public class RenewAuthorizationRequest
|
||||
{
|
||||
public int Days { get; set; } = 30;
|
||||
}
|
||||
|
||||
public class AuthorizationQueryRequest : PageRequest
|
||||
{
|
||||
public long? UserId { get; set; }
|
||||
public AuthorizationStatus? Status { get; set; }
|
||||
}
|
||||
|
||||
// ===== 内容 DTO =====
|
||||
public class BannerDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Image { get; set; } = string.Empty;
|
||||
public string Link { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
public UserStatus Status { get; set; }
|
||||
}
|
||||
|
||||
public class CreateBannerRequest
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Image { get; set; } = string.Empty;
|
||||
public string Link { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
public UserStatus Status { get; set; } = UserStatus.Active;
|
||||
}
|
||||
|
||||
public class PartnerDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Logo { get; set; } = string.Empty;
|
||||
public string Link { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsVisible { get; set; }
|
||||
}
|
||||
|
||||
public class CreatePartnerRequest
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Logo { get; set; } = string.Empty;
|
||||
public string Link { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsVisible { get; set; } = true;
|
||||
}
|
||||
|
||||
public class ConsultationDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Contact { get; set; } = string.Empty;
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public bool IsRead { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class CreateConsultationRequest
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Contact { get; set; } = string.Empty;
|
||||
public string Content { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// ===== 通知 DTO =====
|
||||
public class NotificationDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public NotificationType Type { get; set; }
|
||||
public string TypeName { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public bool IsRead { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
// ===== 系统设置 DTO =====
|
||||
public class SystemSettingDto
|
||||
{
|
||||
public string Key { get; set; } = string.Empty;
|
||||
public string Value { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 公开系统设置 DTO(无需鉴权):用于前台/后台 Layout 显示平台名、Logo、备案号等
|
||||
/// </summary>
|
||||
public class PublicSettingsDto
|
||||
{
|
||||
public string PlatformName { get; set; } = string.Empty;
|
||||
public string Slogan { get; set; } = string.Empty;
|
||||
public string CompanyName { get; set; } = string.Empty;
|
||||
public string Contact { get; set; } = string.Empty;
|
||||
/// <summary>客服热线/电话</summary>
|
||||
public string ContactPhone { get; set; } = string.Empty;
|
||||
/// <summary>商务邮箱</summary>
|
||||
public string ContactEmail { get; set; } = string.Empty;
|
||||
/// <summary>公司地址</summary>
|
||||
public string ContactAddress { get; set; } = string.Empty;
|
||||
/// <summary>官方网站</summary>
|
||||
public string ContactWebsite { get; set; } = string.Empty;
|
||||
/// <summary>工作时间</summary>
|
||||
public string ContactWorkTime { get; set; } = string.Empty;
|
||||
public string ICP { get; set; } = string.Empty;
|
||||
public string LogoFrontend { get; set; } = string.Empty;
|
||||
public string LogoBackend { get; set; } = string.Empty;
|
||||
public string LogoFavicon { get; set; } = string.Empty;
|
||||
public string LogoLogin { get; set; } = string.Empty;
|
||||
public string LogoFooter { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// 前台首页是否显示合作伙伴模块
|
||||
/// </summary>
|
||||
public bool ShowPartners { get; set; } = true;
|
||||
/// <summary>
|
||||
/// 前台是否显示"关于我们"页面
|
||||
/// </summary>
|
||||
public bool ShowAbout { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 文件上传结果DTO
|
||||
/// </summary>
|
||||
public class UploadResultDto
|
||||
{
|
||||
/// <summary>可访问的签名URL(用于预览显示)</summary>
|
||||
public string Url { get; set; } = string.Empty;
|
||||
/// <summary>对象键或本地路径(用于数据库存储)</summary>
|
||||
public string Key { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class UpdateSettingsRequest
|
||||
{
|
||||
public Dictionary<string, string> Settings { get; set; } = new();
|
||||
}
|
||||
|
||||
// ===== 关于我们内容 DTO =====
|
||||
public class AboutStatDto
|
||||
{
|
||||
public string Number { get; set; } = "";
|
||||
public string Label { get; set; } = "";
|
||||
}
|
||||
|
||||
public class AboutMissionCardDto
|
||||
{
|
||||
public string Title { get; set; } = "";
|
||||
public string Icon { get; set; } = "rocket";
|
||||
public string Desc { get; set; } = "";
|
||||
}
|
||||
|
||||
public class AboutTimelineItemDto
|
||||
{
|
||||
public string Year { get; set; } = "";
|
||||
public string Title { get; set; } = "";
|
||||
public string Desc { get; set; } = "";
|
||||
}
|
||||
|
||||
public class AboutContentDto
|
||||
{
|
||||
public string HeroTag { get; set; } = "关于我们";
|
||||
public string HeroTitle { get; set; } = "";
|
||||
public string HeroDesc { get; set; } = "";
|
||||
public string IntroTitle { get; set; } = "公司简介";
|
||||
public List<string> IntroParagraphs { get; set; } = new();
|
||||
public List<AboutStatDto> Stats { get; set; } = new();
|
||||
public string MissionTitle { get; set; } = "使命与愿景";
|
||||
public List<AboutMissionCardDto> MissionCards { get; set; } = new();
|
||||
public string TimelineTitle { get; set; } = "发展历程";
|
||||
public List<AboutTimelineItemDto> Timeline { get; set; } = new();
|
||||
}
|
||||
|
||||
public class AboutSettingsDto
|
||||
{
|
||||
public bool ShowAbout { get; set; } = true;
|
||||
public AboutContentDto Content { get; set; } = new();
|
||||
}
|
||||
|
||||
public class FactoryResetRequest
|
||||
{
|
||||
public string ConfirmText { get; set; } = "";
|
||||
}
|
||||
|
||||
public class FactoryResetResultDto
|
||||
{
|
||||
public int TablesCleared { get; set; }
|
||||
public int ObjectsDeleted { get; set; }
|
||||
public long BackupId { get; set; }
|
||||
}
|
||||
|
||||
public class OperationLogDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long UserId { get; set; }
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string Nickname { get; set; } = string.Empty;
|
||||
public string Module { get; set; } = string.Empty;
|
||||
public string Operation { get; set; } = string.Empty;
|
||||
public string Detail { get; set; } = string.Empty;
|
||||
public string Ip { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class OperationLogQueryRequest : PageRequest
|
||||
{
|
||||
public long? UserId { get; set; }
|
||||
public string? Username { get; set; }
|
||||
public string? Module { get; set; }
|
||||
public DateTime? StartDate { get; set; }
|
||||
public DateTime? EndDate { get; set; }
|
||||
}
|
||||
|
||||
// ===== 备份 DTO =====
|
||||
public class BackupDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
public long FileSize { get; set; }
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public string Url { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
// ===== 唤醒协议 DTO =====
|
||||
public class WakeProtocolDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string TerminalType { get; set; } = string.Empty;
|
||||
public string ProtocolPrefix { get; set; } = string.Empty;
|
||||
public string ParamTemplate { get; set; } = string.Empty;
|
||||
public UserStatus Status { get; set; }
|
||||
}
|
||||
|
||||
public class CreateWakeProtocolRequest
|
||||
{
|
||||
public string TerminalType { get; set; } = string.Empty;
|
||||
public string ProtocolPrefix { get; set; } = string.Empty;
|
||||
public string ParamTemplate { get; set; } = string.Empty;
|
||||
public UserStatus Status { get; set; } = UserStatus.Active;
|
||||
}
|
||||
|
||||
// ===== 工作台 DTO =====
|
||||
public class DashboardStats
|
||||
{
|
||||
public int ProjectCount { get; set; }
|
||||
public int CustomerCount { get; set; }
|
||||
public int TodayVisits { get; set; }
|
||||
public int ActiveAuthorizations { get; set; }
|
||||
public int UnreadMessages { get; set; }
|
||||
}
|
||||
|
||||
public class TrendData
|
||||
{
|
||||
public string Date { get; set; } = string.Empty;
|
||||
public int Visits { get; set; }
|
||||
public int Experiences { get; set; }
|
||||
}
|
||||
|
||||
public class CategoryDistribution
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public int Count { get; set; }
|
||||
}
|
||||
|
||||
public class DashboardData
|
||||
{
|
||||
public DashboardStats Stats { get; set; } = new();
|
||||
public List<TrendData> Trends { get; set; } = new();
|
||||
public List<CategoryDistribution> CategoryDistribution { get; set; } = new();
|
||||
public List<OperationLogDto> RecentActivities { get; set; } = new();
|
||||
public List<ProjectDto> HotProjects { get; set; } = new();
|
||||
}
|
||||
479
backend/MilliSim.Api/Common/Entities/Entities.cs
Normal file
479
backend/MilliSim.Api/Common/Entities/Entities.cs
Normal file
@ -0,0 +1,479 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using MilliSim.Common.Models;
|
||||
|
||||
namespace MilliSim.Common.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 实体基类
|
||||
/// </summary>
|
||||
public abstract class BaseEntity
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public long Id { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.Now;
|
||||
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.Now;
|
||||
|
||||
public bool IsDeleted { get; set; } = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户表(员工 + 客户)
|
||||
/// </summary>
|
||||
public class User : BaseEntity
|
||||
{
|
||||
[Required, StringLength(50)]
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
[Required, StringLength(255)]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(50)]
|
||||
public string Nickname { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(500)]
|
||||
public string Avatar { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(20)]
|
||||
public string Phone { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(100)]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
public UserRole Role { get; set; } = UserRole.Customer;
|
||||
|
||||
public UserStatus Status { get; set; } = UserStatus.Active;
|
||||
|
||||
/// <summary>
|
||||
/// 创建人 ID(销售创建客户时记录)
|
||||
/// </summary>
|
||||
public long? CreatedBy { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public string RoleName => Role.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 项目表
|
||||
/// </summary>
|
||||
public class Project : BaseEntity
|
||||
{
|
||||
[Required, StringLength(100)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public long CategoryId { get; set; }
|
||||
|
||||
[StringLength(500)]
|
||||
public string CoverImage { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(500)]
|
||||
public string VideoUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 视频封面(已废弃,保留字段避免数据库迁移问题)
|
||||
/// </summary>
|
||||
[StringLength(500)]
|
||||
public string VideoCover { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(1000)]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 平台标签(逗号分隔)
|
||||
/// </summary>
|
||||
[StringLength(200)]
|
||||
public string Platforms { get; set; } = string.Empty;
|
||||
|
||||
public ProjectStatus Status { get; set; } = ProjectStatus.Offline;
|
||||
|
||||
/// <summary>
|
||||
/// 是否推荐(已废弃,保留字段避免数据库迁移问题)
|
||||
/// </summary>
|
||||
public bool IsFeatured { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 排序值(已废弃,保留字段避免数据库迁移问题)
|
||||
/// </summary>
|
||||
public int SortOrder { get; set; } = 0;
|
||||
|
||||
public int ViewCount { get; set; } = 0;
|
||||
|
||||
public int ExperienceCount { get; set; } = 0;
|
||||
|
||||
public int FavoriteCount { get; set; } = 0;
|
||||
|
||||
public DateTime? PublishedAt { get; set; }
|
||||
|
||||
[ForeignKey(nameof(CategoryId))]
|
||||
public ProjectCategory? Category { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 项目分类表
|
||||
/// </summary>
|
||||
public class ProjectCategory : BaseEntity
|
||||
{
|
||||
[Required, StringLength(50)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(500)]
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
|
||||
public int SortOrder { get; set; } = 0;
|
||||
|
||||
public UserStatus Status { get; set; } = UserStatus.Active;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 平台类型表
|
||||
/// </summary>
|
||||
public class PlatformType : BaseEntity
|
||||
{
|
||||
[Required, StringLength(50)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(500)]
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
|
||||
public int SortOrder { get; set; } = 0;
|
||||
|
||||
public UserStatus Status { get; set; } = UserStatus.Active;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 特性标签表
|
||||
/// </summary>
|
||||
public class FeatureTag : BaseEntity
|
||||
{
|
||||
[Required, StringLength(50)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(500)]
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
|
||||
public int SortOrder { get; set; } = 0;
|
||||
|
||||
public UserStatus Status { get; set; } = UserStatus.Active;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 项目标签关联表
|
||||
/// </summary>
|
||||
public class ProjectTagRelation : BaseEntity
|
||||
{
|
||||
public long ProjectId { get; set; }
|
||||
|
||||
public long TagId { get; set; }
|
||||
|
||||
[ForeignKey(nameof(ProjectId))]
|
||||
public Project? Project { get; set; }
|
||||
|
||||
[ForeignKey(nameof(TagId))]
|
||||
public FeatureTag? Tag { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 项目截图表(已废弃,保留表避免数据库迁移问题,不再使用)
|
||||
/// </summary>
|
||||
public class ProjectScreenshot : BaseEntity
|
||||
{
|
||||
public long ProjectId { get; set; }
|
||||
|
||||
[StringLength(500)]
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
public int SortOrder { get; set; } = 0;
|
||||
|
||||
[ForeignKey(nameof(ProjectId))]
|
||||
public Project? Project { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 项目资源包表
|
||||
/// </summary>
|
||||
public class ProjectPackage : BaseEntity
|
||||
{
|
||||
public long ProjectId { get; set; }
|
||||
|
||||
public PackageType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 原始文件名(上传时的文件名,非重命名后的对象键)
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(500)]
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
public long FileSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 唤醒协议前缀(固定为 millisim,Android/PC 平台使用)
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string WakeProtocolPrefix { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 唤醒协议参数(Android/PC 平台使用,用户输入)
|
||||
/// </summary>
|
||||
[StringLength(500)]
|
||||
public string WakeProtocolParam { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// WebGL 解压后的根路径对象键(仅 WebGL 类型使用,用于在线运行)
|
||||
/// </summary>
|
||||
[StringLength(500)]
|
||||
public string ExtractedPath { get; set; } = string.Empty;
|
||||
|
||||
[ForeignKey(nameof(ProjectId))]
|
||||
public Project? Project { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 收藏表
|
||||
/// </summary>
|
||||
public class Favorite : BaseEntity
|
||||
{
|
||||
public long UserId { get; set; }
|
||||
|
||||
public long ProjectId { get; set; }
|
||||
|
||||
[ForeignKey(nameof(UserId))]
|
||||
public User? User { get; set; }
|
||||
|
||||
[ForeignKey(nameof(ProjectId))]
|
||||
public Project? Project { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 体验记录表
|
||||
/// </summary>
|
||||
public class ExperienceLog : BaseEntity
|
||||
{
|
||||
public long UserId { get; set; }
|
||||
|
||||
public long ProjectId { get; set; }
|
||||
|
||||
public int DurationSeconds { get; set; }
|
||||
|
||||
[ForeignKey(nameof(UserId))]
|
||||
public User? User { get; set; }
|
||||
|
||||
[ForeignKey(nameof(ProjectId))]
|
||||
public Project? Project { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 授权记录表(客户级授权,授权后可运行所有项目)
|
||||
/// </summary>
|
||||
public class Authorization : BaseEntity
|
||||
{
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 项目 ID(已废弃,改为客户级授权,保留字段避免数据库迁移问题)
|
||||
/// </summary>
|
||||
public long? ProjectId { get; set; }
|
||||
|
||||
public long AuthorizedBy { get; set; }
|
||||
|
||||
public DateTime StartTime { get; set; } = DateTime.Now;
|
||||
|
||||
public DateTime EndTime { get; set; }
|
||||
|
||||
public AuthorizationStatus Status { get; set; } = AuthorizationStatus.Active;
|
||||
|
||||
[ForeignKey(nameof(UserId))]
|
||||
public User? User { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 轮播图表
|
||||
/// </summary>
|
||||
public class Banner : BaseEntity
|
||||
{
|
||||
[StringLength(100)]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[Required, StringLength(500)]
|
||||
public string Image { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(500)]
|
||||
public string Link { get; set; } = string.Empty;
|
||||
|
||||
public int SortOrder { get; set; } = 0;
|
||||
|
||||
public UserStatus Status { get; set; } = UserStatus.Active;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 合作伙伴表
|
||||
/// </summary>
|
||||
public class Partner : BaseEntity
|
||||
{
|
||||
[Required, StringLength(100)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[Required, StringLength(500)]
|
||||
public string Logo { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(500)]
|
||||
public string Link { get; set; } = string.Empty;
|
||||
|
||||
public int SortOrder { get; set; } = 0;
|
||||
|
||||
public bool IsVisible { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 咨询消息表
|
||||
/// </summary>
|
||||
public class Consultation : BaseEntity
|
||||
{
|
||||
[Required, StringLength(50)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[Required, StringLength(20)]
|
||||
public string Contact { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(1000)]
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
public bool IsRead { get; set; } = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知表
|
||||
/// </summary>
|
||||
public class Notification : BaseEntity
|
||||
{
|
||||
public NotificationType Type { get; set; }
|
||||
|
||||
[Required, StringLength(200)]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(1000)]
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
public long? UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 业务事件 ID(防重复)
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? EventId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知已读记录表
|
||||
/// </summary>
|
||||
public class NotificationRead : BaseEntity
|
||||
{
|
||||
public long UserId { get; set; }
|
||||
|
||||
public long NotificationId { get; set; }
|
||||
|
||||
[ForeignKey(nameof(NotificationId))]
|
||||
public Notification? Notification { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 操作日志表
|
||||
/// </summary>
|
||||
public class OperationLog : BaseEntity
|
||||
{
|
||||
public long UserId { get; set; }
|
||||
|
||||
[StringLength(50)]
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(50)]
|
||||
public string Module { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(50)]
|
||||
public string Operation { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(1000)]
|
||||
public string Detail { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(50)]
|
||||
public string Ip { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 系统设置表
|
||||
/// </summary>
|
||||
public class SystemSetting : BaseEntity
|
||||
{
|
||||
[Required, StringLength(100)]
|
||||
public string Key { get; set; } = string.Empty;
|
||||
|
||||
[Column(TypeName = "longtext")]
|
||||
public string Value { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(200)]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 备份记录表
|
||||
/// </summary>
|
||||
public class Backup : BaseEntity
|
||||
{
|
||||
[Required, StringLength(200)]
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
public long FileSize { get; set; }
|
||||
|
||||
[StringLength(20)]
|
||||
public string Type { get; set; } = "Manual";
|
||||
|
||||
[StringLength(500)]
|
||||
public string Url { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 唤醒协议配置表
|
||||
/// </summary>
|
||||
public class WakeProtocol : BaseEntity
|
||||
{
|
||||
[Required, StringLength(50)]
|
||||
public string TerminalType { get; set; } = string.Empty;
|
||||
|
||||
[Required, StringLength(200)]
|
||||
public string ProtocolPrefix { get; set; } = string.Empty;
|
||||
|
||||
[Column(TypeName = "text")]
|
||||
public string ParamTemplate { get; set; } = string.Empty;
|
||||
|
||||
public UserStatus Status { get; set; } = UserStatus.Active;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 待消费上传文件记录(用于垃圾资源清理跟踪)
|
||||
/// </summary>
|
||||
public class PendingUpload : BaseEntity
|
||||
{
|
||||
/// <summary>COS 对象键或本地访问 URL</summary>
|
||||
[Required, StringLength(500)]
|
||||
public string ObjectKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>上传目录(icons/videos/logos/packages 等)</summary>
|
||||
[StringLength(50)]
|
||||
public string Folder { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>上传者用户 ID</summary>
|
||||
public long? UploadedBy { get; set; }
|
||||
|
||||
/// <summary>上传时间</summary>
|
||||
public DateTime UploadedAt { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>消费时间(null=待消费,非null=已保存到实体)</summary>
|
||||
public DateTime? ConsumedAt { get; set; }
|
||||
}
|
||||
144
backend/MilliSim.Api/Common/Models/ApiResponse.cs
Normal file
144
backend/MilliSim.Api/Common/Models/ApiResponse.cs
Normal file
@ -0,0 +1,144 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MilliSim.Common.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 统一 API 响应模型
|
||||
/// </summary>
|
||||
public class ApiResponse<T>
|
||||
{
|
||||
public int Code { get; set; } = 200;
|
||||
public string Message { get; set; } = "success";
|
||||
public T? Data { get; set; }
|
||||
|
||||
public static ApiResponse<T> Success(T? data = default, string message = "success")
|
||||
=> new() { Code = 200, Message = message, Data = data };
|
||||
|
||||
public static ApiResponse<T> Fail(string message, int code = 400)
|
||||
=> new() { Code = code, Message = message, Data = default };
|
||||
}
|
||||
|
||||
public class ApiResponse : ApiResponse<object>
|
||||
{
|
||||
public static ApiResponse Success(string message = "success")
|
||||
=> new() { Code = 200, Message = message, Data = null };
|
||||
|
||||
public new static ApiResponse Fail(string message, int code = 400)
|
||||
=> new() { Code = code, Message = message, Data = null };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页结果
|
||||
/// </summary>
|
||||
public class PagedResult<T>
|
||||
{
|
||||
public List<T> Items { get; set; } = new();
|
||||
public long Total { get; set; }
|
||||
public int PageIndex { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
|
||||
public static PagedResult<T> Create(List<T> items, long total, int pageIndex, int pageSize)
|
||||
=> new() { Items = items, Total = total, PageIndex = pageIndex, PageSize = pageSize };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页请求
|
||||
/// </summary>
|
||||
public class PageRequest
|
||||
{
|
||||
private int _pageIndex = 1;
|
||||
private int _pageSize = 12;
|
||||
|
||||
public int PageIndex
|
||||
{
|
||||
get => _pageIndex;
|
||||
set => _pageIndex = value < 1 ? 1 : value;
|
||||
}
|
||||
|
||||
public int PageSize
|
||||
{
|
||||
get => _pageSize;
|
||||
set => _pageSize = value is < 1 or > 100 ? 12 : value;
|
||||
}
|
||||
|
||||
public string? Keyword { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户角色枚举
|
||||
/// 注意:不使用 JsonStringEnumConverter,序列化为数字,与前端数字枚举一致
|
||||
/// </summary>
|
||||
public enum UserRole
|
||||
{
|
||||
Guest = 0,
|
||||
Customer = 1,
|
||||
Sales = 2,
|
||||
ProductManager = 3,
|
||||
Admin = 4
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户状态
|
||||
/// 注意:不使用 JsonStringEnumConverter,序列化为数字,与前端数字枚举一致
|
||||
/// </summary>
|
||||
public enum UserStatus
|
||||
{
|
||||
Disabled = 0,
|
||||
Active = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 项目状态
|
||||
/// 注意:不使用 JsonStringEnumConverter,序列化为数字,与前端 ProjectStatus 数字枚举一致
|
||||
/// </summary>
|
||||
public enum ProjectStatus
|
||||
{
|
||||
Offline = 0,
|
||||
Published = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 授权状态
|
||||
/// 注意:不使用 JsonStringEnumConverter,序列化为数字,与前端数字枚举一致
|
||||
/// </summary>
|
||||
public enum AuthorizationStatus
|
||||
{
|
||||
Cancelled = 0,
|
||||
Active = 1,
|
||||
Expired = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知类型
|
||||
/// 注意:不使用 JsonStringEnumConverter,序列化为数字,与前端数字枚举一致
|
||||
/// </summary>
|
||||
public enum NotificationType
|
||||
{
|
||||
System = 0,
|
||||
Project = 1,
|
||||
Account = 2,
|
||||
Authorization = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 资源包类型
|
||||
/// 注意:不使用 JsonStringEnumConverter,序列化为数字,与前端 PackageType 数字枚举一致
|
||||
/// </summary>
|
||||
public enum PackageType
|
||||
{
|
||||
WebGL = 0,
|
||||
Android = 1,
|
||||
Windows = 2,
|
||||
Mac = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 存储模式
|
||||
/// 注意:不使用 JsonStringEnumConverter,序列化为数字,与前端数字枚举一致
|
||||
/// </summary>
|
||||
public enum StorageMode
|
||||
{
|
||||
Local = 0,
|
||||
Cos = 1
|
||||
}
|
||||
145
backend/MilliSim.Api/Common/Models/BeijingTimeConverter.cs
Normal file
145
backend/MilliSim.Api/Common/Models/BeijingTimeConverter.cs
Normal file
@ -0,0 +1,145 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MilliSim.Common.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 北京时间(UTC+8)JSON转换器
|
||||
/// 将 DateTime 序列化为北京时间格式 "yyyy-MM-dd HH:mm:ss"
|
||||
/// </summary>
|
||||
public class BeijingTimeConverter : JsonConverter<DateTime>
|
||||
{
|
||||
/// <summary>
|
||||
/// 北京时区信息(UTC+8)
|
||||
/// </summary>
|
||||
private static readonly TimeZoneInfo BeijingTimeZone = TimeZoneInfo.FindSystemTimeZoneById(
|
||||
OperatingSystem.IsWindows() ? "China Standard Time" : "Asia/Shanghai");
|
||||
|
||||
/// <summary>
|
||||
/// 北京时间格式
|
||||
/// </summary>
|
||||
private const string DateTimeFormat = "yyyy-MM-dd HH:mm:ss";
|
||||
|
||||
/// <summary>
|
||||
/// 读取JSON并转换为DateTime
|
||||
/// </summary>
|
||||
/// <param name="reader">JSON读取器</param>
|
||||
/// <param name="typeToConvert">要转换的类型</param>
|
||||
/// <param name="options">序列化选项</param>
|
||||
/// <returns>DateTime对象</returns>
|
||||
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
// 读取字符串值
|
||||
var dateString = reader.GetString();
|
||||
if (string.IsNullOrEmpty(dateString))
|
||||
return DateTime.MinValue;
|
||||
|
||||
// 尝试解析为北京时间
|
||||
if (DateTime.TryParseExact(dateString, DateTimeFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var result))
|
||||
{
|
||||
// 将北京时间转换为本地时间(按Kind处理)
|
||||
return DateTime.SpecifyKind(result, DateTimeKind.Local);
|
||||
}
|
||||
|
||||
// 兜底:使用默认解析
|
||||
return DateTime.Parse(dateString, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将DateTime写入JSON(转换为北京时间格式)
|
||||
/// </summary>
|
||||
/// <param name="writer">JSON写入器</param>
|
||||
/// <param name="value">DateTime值</param>
|
||||
/// <param name="options">序列化选项</param>
|
||||
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
|
||||
{
|
||||
// 将DateTime转换为北京时间
|
||||
DateTime beijingTime;
|
||||
if (value.Kind == DateTimeKind.Utc)
|
||||
{
|
||||
// UTC时间转换为北京时间
|
||||
beijingTime = TimeZoneInfo.ConvertTimeFromUtc(value, BeijingTimeZone);
|
||||
}
|
||||
else if (value.Kind == DateTimeKind.Unspecified)
|
||||
{
|
||||
// 未指定类型的时间,假定为本地时间并转换为北京时间
|
||||
beijingTime = TimeZoneInfo.ConvertTime(value, BeijingTimeZone);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 已经是本地时间,转换为北京时间
|
||||
beijingTime = TimeZoneInfo.ConvertTime(value, BeijingTimeZone);
|
||||
}
|
||||
|
||||
// 格式化为 "yyyy-MM-dd HH:mm:ss" 并写入
|
||||
writer.WriteStringValue(beijingTime.ToString(DateTimeFormat, CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可空DateTime的北京时间JSON转换器
|
||||
/// </summary>
|
||||
public class NullableBeijingTimeConverter : JsonConverter<DateTime?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 北京时区信息(UTC+8)
|
||||
/// </summary>
|
||||
private static readonly TimeZoneInfo BeijingTimeZone = TimeZoneInfo.FindSystemTimeZoneById(
|
||||
OperatingSystem.IsWindows() ? "China Standard Time" : "Asia/Shanghai");
|
||||
|
||||
/// <summary>
|
||||
/// 北京时间格式
|
||||
/// </summary>
|
||||
private const string DateTimeFormat = "yyyy-MM-dd HH:mm:ss";
|
||||
|
||||
/// <summary>
|
||||
/// 读取JSON并转换为可空DateTime
|
||||
/// </summary>
|
||||
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
return null;
|
||||
|
||||
var dateString = reader.GetString();
|
||||
if (string.IsNullOrEmpty(dateString))
|
||||
return null;
|
||||
|
||||
if (DateTime.TryParseExact(dateString, DateTimeFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var result))
|
||||
{
|
||||
return DateTime.SpecifyKind(result, DateTimeKind.Local);
|
||||
}
|
||||
|
||||
return DateTime.Parse(dateString, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将可空DateTime写入JSON(转换为北京时间格式)
|
||||
/// </summary>
|
||||
public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
return;
|
||||
}
|
||||
|
||||
// 将DateTime转换为北京时间
|
||||
DateTime beijingTime;
|
||||
var dt = value.Value;
|
||||
if (dt.Kind == DateTimeKind.Utc)
|
||||
{
|
||||
beijingTime = TimeZoneInfo.ConvertTimeFromUtc(dt, BeijingTimeZone);
|
||||
}
|
||||
else if (dt.Kind == DateTimeKind.Unspecified)
|
||||
{
|
||||
beijingTime = TimeZoneInfo.ConvertTime(dt, BeijingTimeZone);
|
||||
}
|
||||
else
|
||||
{
|
||||
beijingTime = TimeZoneInfo.ConvertTime(dt, BeijingTimeZone);
|
||||
}
|
||||
|
||||
writer.WriteStringValue(beijingTime.ToString(DateTimeFormat, CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
67
backend/MilliSim.Api/Controllers/AuthController.cs
Normal file
67
backend/MilliSim.Api/Controllers/AuthController.cs
Normal file
@ -0,0 +1,67 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Common.Models;
|
||||
using MilliSim.Services;
|
||||
|
||||
namespace MilliSim.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/auth")]
|
||||
public class AuthController : ControllerBase
|
||||
{
|
||||
private readonly IAuthService _authService;
|
||||
|
||||
public AuthController(IAuthService authService)
|
||||
{
|
||||
_authService = authService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取验证码
|
||||
/// </summary>
|
||||
[HttpGet("captcha")]
|
||||
public async Task<ApiResponse<CaptchaResult>> GetCaptcha()
|
||||
{
|
||||
return await _authService.GetCaptchaAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 登录
|
||||
/// </summary>
|
||||
[HttpPost("login")]
|
||||
public async Task<ApiResponse<LoginResponse>> Login([FromBody] LoginRequest request)
|
||||
{
|
||||
return await _authService.LoginAsync(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 修改密码
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
[HttpPost("change-password")]
|
||||
public async Task<ApiResponse> ChangePassword([FromBody] ChangePasswordRequest request)
|
||||
{
|
||||
return await _authService.ChangePasswordAsync(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取个人信息
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
[HttpGet("profile")]
|
||||
public async Task<ApiResponse<UserDto>> GetProfile()
|
||||
{
|
||||
return await _authService.GetProfileAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新个人信息
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
[HttpPost("profile")]
|
||||
public async Task<ApiResponse<UserDto>> UpdateProfile([FromBody] UpdateProfileRequest request)
|
||||
{
|
||||
return await _authService.UpdateProfileAsync(request);
|
||||
}
|
||||
}
|
||||
107
backend/MilliSim.Api/Controllers/AuthorizationsController.cs
Normal file
107
backend/MilliSim.Api/Controllers/AuthorizationsController.cs
Normal file
@ -0,0 +1,107 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Services;
|
||||
using IAuthorizationService = MilliSim.Services.IAuthorizationService;
|
||||
|
||||
namespace MilliSim.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/authorizations")]
|
||||
public class AuthorizationsController : ControllerBase
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
||||
public AuthorizationsController(IAuthorizationService authorizationService)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 授权列表(销售只能看自己客户的授权)
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[Authorize(Policy = "Staff")]
|
||||
public async Task<IActionResult> GetList([FromQuery] AuthorizationQueryRequest request)
|
||||
{
|
||||
var result = await _authorizationService.GetAuthorizationsAsync(request);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 授权详情
|
||||
/// </summary>
|
||||
[HttpGet("{id:long}")]
|
||||
[Authorize(Policy = "Staff")]
|
||||
public async Task<IActionResult> GetById(long id)
|
||||
{
|
||||
var result = await _authorizationService.GetAuthorizationByIdAsync(id);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建授权(默认 365 天)
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[Authorize(Policy = "Staff")]
|
||||
public async Task<IActionResult> Create([FromBody] CreateAuthorizationRequest request)
|
||||
{
|
||||
var result = await _authorizationService.CreateAuthorizationAsync(request);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量授权
|
||||
/// </summary>
|
||||
[HttpPost("batch")]
|
||||
[Authorize(Policy = "Staff")]
|
||||
public async Task<IActionResult> Batch([FromBody] BatchAuthorizationRequest request)
|
||||
{
|
||||
var result = await _authorizationService.BatchAuthorizeAsync(request);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 续期(在原到期时间基础上加天数)
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}/renew")]
|
||||
[Authorize(Policy = "Staff")]
|
||||
public async Task<IActionResult> Renew(long id, [FromBody] RenewAuthorizationRequest request)
|
||||
{
|
||||
var result = await _authorizationService.RenewAuthorizationAsync(id, request.Days);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消授权
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}/cancel")]
|
||||
[Authorize(Policy = "Staff")]
|
||||
public async Task<IActionResult> Cancel(long id)
|
||||
{
|
||||
var result = await _authorizationService.CancelAuthorizationAsync(id);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查用户是否有授权(客户级授权,授权后可运行所有项目)
|
||||
/// </summary>
|
||||
[HttpGet("check")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> Check([FromQuery] long userId)
|
||||
{
|
||||
var result = await _authorizationService.CheckAuthorizationAsync(userId);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前客户的授权列表
|
||||
/// </summary>
|
||||
[HttpGet("my")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> My()
|
||||
{
|
||||
var result = await _authorizationService.GetMyAuthorizationsAsync();
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
61
backend/MilliSim.Api/Controllers/BannersController.cs
Normal file
61
backend/MilliSim.Api/Controllers/BannersController.cs
Normal file
@ -0,0 +1,61 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Services;
|
||||
|
||||
namespace MilliSim.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/banners")]
|
||||
public class BannersController : ControllerBase
|
||||
{
|
||||
private readonly IContentService _contentService;
|
||||
|
||||
public BannersController(IContentService contentService)
|
||||
{
|
||||
_contentService = contentService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 轮播图列表(前台传 onlyActive=true)
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetList([FromQuery] bool onlyActive = false)
|
||||
{
|
||||
var result = await _contentService.GetBannersAsync(onlyActive);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建轮播图
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<IActionResult> Create([FromBody] CreateBannerRequest request)
|
||||
{
|
||||
var result = await _contentService.CreateBannerAsync(request);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新轮播图
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<IActionResult> Update(long id, [FromBody] CreateBannerRequest request)
|
||||
{
|
||||
var result = await _contentService.UpdateBannerAsync(id, request);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除轮播图
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}/delete")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<IActionResult> Delete(long id)
|
||||
{
|
||||
var result = await _contentService.DeleteBannerAsync(id);
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
58
backend/MilliSim.Api/Controllers/CategoriesController.cs
Normal file
58
backend/MilliSim.Api/Controllers/CategoriesController.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Common.Models;
|
||||
using MilliSim.Services;
|
||||
|
||||
namespace MilliSim.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/categories")]
|
||||
public class CategoriesController : ControllerBase
|
||||
{
|
||||
private readonly IProjectService _projectService;
|
||||
|
||||
public CategoriesController(IProjectService projectService)
|
||||
{
|
||||
_projectService = projectService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分类列表
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<ApiResponse<List<CategoryDto>>> GetCategories()
|
||||
{
|
||||
return await _projectService.GetCategoriesAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建分类
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse<CategoryDto>> CreateCategory([FromBody] CreateCategoryRequest request)
|
||||
{
|
||||
return await _projectService.CreateCategoryAsync(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新分类
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse<CategoryDto>> UpdateCategory(long id, [FromBody] CreateCategoryRequest request)
|
||||
{
|
||||
return await _projectService.UpdateCategoryAsync(id, request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除分类
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}/delete")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse> DeleteCategory(long id)
|
||||
{
|
||||
return await _projectService.DeleteCategoryAsync(id);
|
||||
}
|
||||
}
|
||||
62
backend/MilliSim.Api/Controllers/ConsultationsController.cs
Normal file
62
backend/MilliSim.Api/Controllers/ConsultationsController.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Common.Models;
|
||||
using MilliSim.Services;
|
||||
|
||||
namespace MilliSim.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/consultations")]
|
||||
public class ConsultationsController : ControllerBase
|
||||
{
|
||||
private readonly IContentService _contentService;
|
||||
|
||||
public ConsultationsController(IContentService contentService)
|
||||
{
|
||||
_contentService = contentService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 咨询消息列表
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[Authorize(Policy = "Staff")]
|
||||
public async Task<IActionResult> GetList([FromQuery] PageRequest request, [FromQuery] bool? isRead)
|
||||
{
|
||||
var result = await _contentService.GetConsultationsAsync(request, isRead);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提交咨询(公开)
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create([FromBody] CreateConsultationRequest request)
|
||||
{
|
||||
var result = await _contentService.CreateConsultationAsync(request);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记已读
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}/read")]
|
||||
[Authorize(Policy = "Staff")]
|
||||
public async Task<IActionResult> MarkAsRead(long id)
|
||||
{
|
||||
var result = await _contentService.MarkAsReadAsync(id);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除咨询消息
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}/delete")]
|
||||
[Authorize(Policy = "Staff")]
|
||||
public async Task<IActionResult> Delete(long id)
|
||||
{
|
||||
var result = await _contentService.DeleteConsultationAsync(id);
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
25
backend/MilliSim.Api/Controllers/DashboardController.cs
Normal file
25
backend/MilliSim.Api/Controllers/DashboardController.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Services;
|
||||
|
||||
namespace MilliSim.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/dashboard")]
|
||||
[Authorize(Policy = "Staff")]
|
||||
public class DashboardController : ControllerBase
|
||||
{
|
||||
private readonly IDashboardService _dashboardService;
|
||||
|
||||
public DashboardController(IDashboardService dashboardService)
|
||||
{
|
||||
_dashboardService = dashboardService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ApiResponse<DashboardData>> GetDashboard()
|
||||
{
|
||||
return await _dashboardService.GetDashboardDataAsync();
|
||||
}
|
||||
}
|
||||
50
backend/MilliSim.Api/Controllers/NotificationsController.cs
Normal file
50
backend/MilliSim.Api/Controllers/NotificationsController.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Common.Models;
|
||||
using MilliSim.Services;
|
||||
|
||||
namespace MilliSim.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/notifications")]
|
||||
[Authorize]
|
||||
public class NotificationsController : ControllerBase
|
||||
{
|
||||
private readonly INotificationService _notificationService;
|
||||
|
||||
public NotificationsController(INotificationService notificationService)
|
||||
{
|
||||
_notificationService = notificationService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ApiResponse<PagedResult<NotificationDto>>> GetNotifications([FromQuery] NotificationType? type, [FromQuery] PageRequest request)
|
||||
{
|
||||
return await _notificationService.GetNotificationsAsync(type, request);
|
||||
}
|
||||
|
||||
[HttpGet("unread-count")]
|
||||
public async Task<ApiResponse<int>> GetUnreadCount()
|
||||
{
|
||||
return await _notificationService.GetUnreadCountAsync();
|
||||
}
|
||||
|
||||
[HttpPost("{id}/read")]
|
||||
public async Task<ApiResponse> MarkAsRead(long id)
|
||||
{
|
||||
return await _notificationService.MarkAsReadAsync(id);
|
||||
}
|
||||
|
||||
[HttpPost("read-all")]
|
||||
public async Task<ApiResponse> MarkAllAsRead()
|
||||
{
|
||||
return await _notificationService.MarkAllAsReadAsync();
|
||||
}
|
||||
|
||||
[HttpPost("{id}/delete")]
|
||||
public async Task<ApiResponse> DeleteNotification(long id)
|
||||
{
|
||||
return await _notificationService.DeleteNotificationAsync(id);
|
||||
}
|
||||
}
|
||||
61
backend/MilliSim.Api/Controllers/PartnersController.cs
Normal file
61
backend/MilliSim.Api/Controllers/PartnersController.cs
Normal file
@ -0,0 +1,61 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Services;
|
||||
|
||||
namespace MilliSim.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/partners")]
|
||||
public class PartnersController : ControllerBase
|
||||
{
|
||||
private readonly IContentService _contentService;
|
||||
|
||||
public PartnersController(IContentService contentService)
|
||||
{
|
||||
_contentService = contentService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 合作伙伴列表(前台传 onlyVisible=true)
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetList([FromQuery] bool onlyVisible = false)
|
||||
{
|
||||
var result = await _contentService.GetPartnersAsync(onlyVisible);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建合作伙伴
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<IActionResult> Create([FromBody] CreatePartnerRequest request)
|
||||
{
|
||||
var result = await _contentService.CreatePartnerAsync(request);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新合作伙伴
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<IActionResult> Update(long id, [FromBody] CreatePartnerRequest request)
|
||||
{
|
||||
var result = await _contentService.UpdatePartnerAsync(id, request);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除合作伙伴
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}/delete")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<IActionResult> Delete(long id)
|
||||
{
|
||||
var result = await _contentService.DeletePartnerAsync(id);
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
58
backend/MilliSim.Api/Controllers/PlatformsController.cs
Normal file
58
backend/MilliSim.Api/Controllers/PlatformsController.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Common.Models;
|
||||
using MilliSim.Services;
|
||||
|
||||
namespace MilliSim.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/platforms")]
|
||||
public class PlatformsController : ControllerBase
|
||||
{
|
||||
private readonly IProjectService _projectService;
|
||||
|
||||
public PlatformsController(IProjectService projectService)
|
||||
{
|
||||
_projectService = projectService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 平台列表
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<ApiResponse<List<PlatformDto>>> GetPlatforms()
|
||||
{
|
||||
return await _projectService.GetPlatformsAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建平台
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse<PlatformDto>> CreatePlatform([FromBody] CreatePlatformRequest request)
|
||||
{
|
||||
return await _projectService.CreatePlatformAsync(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新平台
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse<PlatformDto>> UpdatePlatform(long id, [FromBody] CreatePlatformRequest request)
|
||||
{
|
||||
return await _projectService.UpdatePlatformAsync(id, request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除平台
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}/delete")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse> DeletePlatform(long id)
|
||||
{
|
||||
return await _projectService.DeletePlatformAsync(id);
|
||||
}
|
||||
}
|
||||
302
backend/MilliSim.Api/Controllers/ProjectsController.cs
Normal file
302
backend/MilliSim.Api/Controllers/ProjectsController.cs
Normal file
@ -0,0 +1,302 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Common.Models;
|
||||
using MilliSim.Services;
|
||||
|
||||
namespace MilliSim.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/projects")]
|
||||
public class ProjectsController : ControllerBase
|
||||
{
|
||||
private readonly IProjectService _projectService;
|
||||
|
||||
public ProjectsController(IProjectService projectService)
|
||||
{
|
||||
_projectService = projectService;
|
||||
}
|
||||
|
||||
// ===== 前台接口 =====
|
||||
|
||||
/// <summary>
|
||||
/// 公开项目列表
|
||||
/// </summary>
|
||||
[HttpGet("public")]
|
||||
public async Task<ApiResponse<PagedResult<ProjectDto>>> GetPublicProjects([FromQuery] ProjectQueryRequest request)
|
||||
{
|
||||
return await _projectService.GetPublishedProjectsAsync(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 公开项目详情(自增浏览数)
|
||||
/// </summary>
|
||||
[HttpGet("public/{id:long}")]
|
||||
public async Task<ApiResponse<ProjectDto>> GetPublicProject(long id)
|
||||
{
|
||||
return await _projectService.GetProjectByIdAsync(id, incrementView: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 相关项目
|
||||
/// </summary>
|
||||
[HttpGet("related/{id:long}")]
|
||||
public async Task<ApiResponse<List<ProjectDto>>> GetRelatedProjects(long id, [FromQuery] int count = 4)
|
||||
{
|
||||
return await _projectService.GetRelatedProjectsAsync(id, count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 收藏切换
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}/favorite")]
|
||||
[Authorize]
|
||||
public async Task<ApiResponse> ToggleFavorite(long id)
|
||||
{
|
||||
return await _projectService.ToggleFavoriteAsync(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 我的收藏
|
||||
/// </summary>
|
||||
[HttpGet("favorites")]
|
||||
[Authorize]
|
||||
public async Task<ApiResponse<PagedResult<ProjectDto>>> GetFavorites([FromQuery] PageRequest request)
|
||||
{
|
||||
return await _projectService.GetFavoritesAsync(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录体验
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}/experience")]
|
||||
[Authorize]
|
||||
public async Task<ApiResponse> RecordExperience(long id, [FromBody] RecordExperienceRequest request)
|
||||
{
|
||||
return await _projectService.RecordExperienceAsync(id, request.DurationSeconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 体验记录
|
||||
/// </summary>
|
||||
[HttpGet("experience-logs")]
|
||||
[Authorize]
|
||||
public async Task<ApiResponse<PagedResult<ExperienceLogDto>>> GetExperienceLogs([FromQuery] PageRequest request)
|
||||
{
|
||||
return await _projectService.GetExperienceLogsAsync(request);
|
||||
}
|
||||
|
||||
// ===== 后台接口 =====
|
||||
|
||||
/// <summary>
|
||||
/// 全部项目列表
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[Authorize(Policy = "ProductManager")]
|
||||
public async Task<ApiResponse<PagedResult<ProjectDto>>> GetProjects([FromQuery] ProjectQueryRequest request)
|
||||
{
|
||||
return await _projectService.GetProjectsAsync(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 项目详情
|
||||
/// </summary>
|
||||
[HttpGet("{id:long}")]
|
||||
[Authorize(Policy = "ProductManager")]
|
||||
public async Task<ApiResponse<ProjectDto>> GetProject(long id)
|
||||
{
|
||||
return await _projectService.GetProjectByIdAsync(id, incrementView: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建项目
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[Authorize(Policy = "ProductManager")]
|
||||
public async Task<ApiResponse<ProjectDto>> CreateProject([FromBody] CreateProjectRequest request)
|
||||
{
|
||||
return await _projectService.CreateProjectAsync(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新项目
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}")]
|
||||
[Authorize(Policy = "ProductManager")]
|
||||
public async Task<ApiResponse<ProjectDto>> UpdateProject(long id, [FromBody] UpdateProjectRequest request)
|
||||
{
|
||||
return await _projectService.UpdateProjectAsync(id, request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除项目
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}/delete")]
|
||||
[Authorize(Policy = "ProductManager")]
|
||||
public async Task<ApiResponse> DeleteProject(long id)
|
||||
{
|
||||
return await _projectService.DeleteProjectAsync(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发布/下架切换
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}/toggle-publish")]
|
||||
[Authorize(Policy = "ProductManager")]
|
||||
public async Task<ApiResponse> TogglePublish(long id)
|
||||
{
|
||||
return await _projectService.TogglePublishAsync(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 推荐切换
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}/toggle-featured")]
|
||||
[Authorize(Policy = "ProductManager")]
|
||||
public async Task<ApiResponse> ToggleFeatured(long id)
|
||||
{
|
||||
return await _projectService.ToggleFeaturedAsync(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 资源包列表(公开接口,前台项目详情页需要显示运行按钮)
|
||||
/// </summary>
|
||||
[HttpGet("{id:long}/packages")]
|
||||
public async Task<ApiResponse<List<PackageDto>>> GetPackages(long id)
|
||||
{
|
||||
return await _projectService.GetPackagesAsync(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传资源包
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}/packages")]
|
||||
[Authorize(Policy = "ProductManager")]
|
||||
public async Task<ApiResponse<PackageDto>> UploadPackage(long id, [FromForm] PackageType type, [FromForm] string? wakeProtocolPrefix, [FromForm] string? wakeProtocolParam, IFormFile file)
|
||||
{
|
||||
return await _projectService.UploadPackageAsync(id, type, file, wakeProtocolPrefix ?? string.Empty, wakeProtocolParam ?? string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 WebGL 在线运行入口 URL(公开接口,前台在线运行使用)
|
||||
/// </summary>
|
||||
[HttpGet("{id:long}/webgl-run-url")]
|
||||
public async Task<ApiResponse<string>> GetWebGLRunUrl(long id)
|
||||
{
|
||||
return await _projectService.GetWebGLRunUrlAsync(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WebGL 在线运行 - 代理 index.html(公开接口)
|
||||
/// 浏览器无法直接渲染 COS 私有桶的 index.html,通过后端代理读取并注入 base 标签
|
||||
/// 资源文件通过 webgl-resource 端点 302 重定向到签名 URL 加载
|
||||
/// </summary>
|
||||
[HttpGet("{id:long}/webgl-index")]
|
||||
public async Task<IActionResult> GetWebGLIndex(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var html = await _projectService.GetWebGLIndexHtmlAsync(id);
|
||||
return Content(html, "text/html", System.Text.Encoding.UTF8);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return NotFound(new { message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WebGL 资源代理 - 302 重定向到签名 URL(公开接口)
|
||||
/// index.html 中的 base 标签将相对路径指向此端点
|
||||
/// 浏览器请求资源时,此端点生成签名 URL 并重定向,浏览器直接从 COS 加载
|
||||
/// </summary>
|
||||
[HttpGet("{id:long}/webgl-resource/{*path}")]
|
||||
public async Task<IActionResult> GetWebGLResource(long id, string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var signedUrl = await _projectService.GetWebGLResourceUrlAsync(id, path);
|
||||
return Redirect(signedUrl);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return NotFound(new { message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 视频代理 - 流式转发视频内容,不暴露 COS 签名直链(防盗链)
|
||||
/// 前端 video src 指向此端点,后端流式代理 COS 文件
|
||||
/// 支持 Range 请求(视频拖动进度条)
|
||||
/// </summary>
|
||||
[HttpGet("{id:long}/video")]
|
||||
public async Task<IActionResult> GetVideo(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var rangeHeader = Request.Headers.Range.ToString();
|
||||
var (stream, contentType, fileSize, isPartial, contentRange) = await _projectService.GetVideoStreamAsync(id, rangeHeader);
|
||||
// 防止浏览器缓存视频到磁盘
|
||||
Response.Headers.CacheControl = "no-store, no-cache, must-revalidate";
|
||||
Response.Headers.Pragma = "no-cache";
|
||||
Response.Headers.Expires = "0";
|
||||
// 内联播放,不触发下载
|
||||
Response.Headers.ContentDisposition = "inline";
|
||||
// 支持 Range 请求时设置 Content-Range 和 206 状态码
|
||||
if (isPartial && !string.IsNullOrEmpty(contentRange))
|
||||
{
|
||||
Response.Headers.ContentRange = contentRange;
|
||||
Response.StatusCode = 206;
|
||||
}
|
||||
if (fileSize.HasValue)
|
||||
{
|
||||
Response.Headers.ContentLength = fileSize.Value;
|
||||
}
|
||||
return new FileStreamResult(stream, contentType);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return NotFound(new { message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除资源包
|
||||
/// </summary>
|
||||
[HttpPost("packages/{id:long}/delete")]
|
||||
[Authorize(Policy = "ProductManager")]
|
||||
public async Task<ApiResponse> DeletePackage(long id)
|
||||
{
|
||||
return await _projectService.DeletePackageAsync(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载资源包(需要登录)
|
||||
/// 通过后端代理流式返回文件,使用原始文件名设置 Content-Disposition
|
||||
/// 解决:1.安卓端直接打开 COS 签名URL下载失败 2.下载文件名使用原始文件名而非 COS 对象键
|
||||
/// </summary>
|
||||
[HttpGet("packages/{id:long}/download")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> DownloadPackage(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (stream, contentType, fileName) = await _projectService.DownloadPackageAsync(id);
|
||||
// attachment 触发浏览器下载,filename 使用原始文件名
|
||||
return File(stream, contentType, fileName);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return NotFound(new { message = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new { message = $"下载失败: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录体验请求
|
||||
/// </summary>
|
||||
public record RecordExperienceRequest(int DurationSeconds);
|
||||
332
backend/MilliSim.Api/Controllers/SystemController.cs
Normal file
332
backend/MilliSim.Api/Controllers/SystemController.cs
Normal file
@ -0,0 +1,332 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Common.Models;
|
||||
using MilliSim.Data;
|
||||
using MilliSim.Services;
|
||||
using MilliSim.Services.Infrastructure;
|
||||
|
||||
namespace MilliSim.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/system")]
|
||||
public class SystemController : ControllerBase
|
||||
{
|
||||
private readonly ISystemService _systemService;
|
||||
private readonly IFileStorageService _fileStorage;
|
||||
private readonly IUploadTrackingService _uploadTracking;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
private readonly IOperationLogService _operationLog;
|
||||
private readonly MilliSimDbContext _db;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="systemService">系统服务</param>
|
||||
/// <param name="fileStorage">文件存储服务</param>
|
||||
/// <param name="uploadTracking">上传跟踪服务</param>
|
||||
/// <param name="currentUser">当前用户服务</param>
|
||||
/// <param name="operationLog">操作日志服务</param>
|
||||
/// <param name="db">数据库上下文</param>
|
||||
public SystemController(
|
||||
ISystemService systemService,
|
||||
IFileStorageService fileStorage,
|
||||
IUploadTrackingService uploadTracking,
|
||||
ICurrentUserService currentUser,
|
||||
IOperationLogService operationLog,
|
||||
MilliSimDbContext db)
|
||||
{
|
||||
_systemService = systemService;
|
||||
_fileStorage = fileStorage;
|
||||
_uploadTracking = uploadTracking;
|
||||
_currentUser = currentUser;
|
||||
_operationLog = operationLog;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
// ===== 系统设置 =====
|
||||
|
||||
[HttpGet("settings")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse<List<SystemSettingDto>>> GetSettings()
|
||||
{
|
||||
return await _systemService.GetSettingsAsync();
|
||||
}
|
||||
|
||||
[HttpPost("settings")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse> UpdateSettings([FromBody] UpdateSettingsRequest request)
|
||||
{
|
||||
return await _systemService.UpdateSettingsAsync(request);
|
||||
}
|
||||
|
||||
[HttpGet("settings/{key}")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse<SystemSettingDto>> GetSetting(string key)
|
||||
{
|
||||
return await _systemService.GetSettingAsync(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取公开系统设置(无需鉴权):返回平台名、Logo、备案号等,供前台/后台 Layout 显示
|
||||
/// </summary>
|
||||
[HttpGet("public-settings")]
|
||||
[AllowAnonymous]
|
||||
public async Task<ApiResponse<PublicSettingsDto>> GetPublicSettings()
|
||||
{
|
||||
return await _systemService.GetPublicSettingsAsync();
|
||||
}
|
||||
|
||||
// ===== 关于我们内容管理 =====
|
||||
|
||||
/// <summary>
|
||||
/// 获取关于我们设置(公开端点,前台 About 页消费)
|
||||
/// </summary>
|
||||
[HttpGet("about")]
|
||||
[AllowAnonymous]
|
||||
public async Task<ApiResponse<AboutSettingsDto>> GetAboutSettings()
|
||||
{
|
||||
return await _systemService.GetAboutSettingsAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新关于我们设置(仅管理员)
|
||||
/// </summary>
|
||||
[HttpPost("about")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse> UpdateAboutSettings([FromBody] AboutSettingsDto dto)
|
||||
{
|
||||
return await _systemService.UpdateAboutSettingsAsync(dto);
|
||||
}
|
||||
|
||||
// ===== 工厂重置 =====
|
||||
|
||||
/// <summary>
|
||||
/// 恢复出厂设置:清空所有业务数据(保留 admin)+ 删除 COS 资源(保留 backups/)
|
||||
/// 需要在请求体中输入 ConfirmText = "RESET" 确认
|
||||
/// </summary>
|
||||
[HttpPost("factory-reset")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse<FactoryResetResultDto>> FactoryReset([FromBody] FactoryResetRequest request)
|
||||
{
|
||||
return await _systemService.FactoryResetAsync(request);
|
||||
}
|
||||
|
||||
// ===== 操作日志 =====
|
||||
|
||||
[HttpGet("logs")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse<PagedResult<OperationLogDto>>> GetLogs([FromQuery] OperationLogQueryRequest request)
|
||||
{
|
||||
return await _systemService.GetOperationLogsAsync(request);
|
||||
}
|
||||
|
||||
[HttpPost("logs/delete")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse> ClearLogs([FromQuery] DateTime? beforeDate)
|
||||
{
|
||||
return await _systemService.ClearLogsAsync(beforeDate);
|
||||
}
|
||||
|
||||
// ===== 数据备份 =====
|
||||
|
||||
[HttpGet("backups")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse<List<BackupDto>>> GetBackups()
|
||||
{
|
||||
return await _systemService.GetBackupsAsync();
|
||||
}
|
||||
|
||||
[HttpPost("backups")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse<BackupDto>> CreateBackup()
|
||||
{
|
||||
return await _systemService.CreateBackupAsync();
|
||||
}
|
||||
|
||||
[HttpPost("backups/{id}/delete")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse> DeleteBackup(long id)
|
||||
{
|
||||
return await _systemService.DeleteBackupAsync(id);
|
||||
}
|
||||
|
||||
[HttpGet("backups/{id}/download")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<IActionResult> DownloadBackup(long id)
|
||||
{
|
||||
var result = await _systemService.DownloadBackupAsync(id);
|
||||
if (result.Code != 200 || result.Data.Stream == null)
|
||||
return NotFound(new { code = result.Code, message = result.Message });
|
||||
|
||||
return File(result.Data.Stream, "application/octet-stream", result.Data.FileName);
|
||||
}
|
||||
|
||||
// ===== 唤醒协议 =====
|
||||
|
||||
[HttpGet("wake-protocols")]
|
||||
public async Task<ApiResponse<List<WakeProtocolDto>>> GetWakeProtocols()
|
||||
{
|
||||
return await _systemService.GetWakeProtocolsAsync();
|
||||
}
|
||||
|
||||
[HttpPost("wake-protocols")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse<WakeProtocolDto>> CreateWakeProtocol([FromBody] CreateWakeProtocolRequest request)
|
||||
{
|
||||
return await _systemService.CreateWakeProtocolAsync(request);
|
||||
}
|
||||
|
||||
[HttpPost("wake-protocols/{id}")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse<WakeProtocolDto>> UpdateWakeProtocol(long id, [FromBody] CreateWakeProtocolRequest request)
|
||||
{
|
||||
return await _systemService.UpdateWakeProtocolAsync(id, request);
|
||||
}
|
||||
|
||||
[HttpPost("wake-protocols/{id}/delete")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse> DeleteWakeProtocol(long id)
|
||||
{
|
||||
return await _systemService.DeleteWakeProtocolAsync(id);
|
||||
}
|
||||
|
||||
// ===== 本地存储目录测试 =====
|
||||
|
||||
/// <summary>
|
||||
/// 测试本地存储目录是否可读写(保存存储配置前校验)
|
||||
/// </summary>
|
||||
/// <param name="localBasePath">本地存储根目录(相对或绝对路径)</param>
|
||||
[HttpPost("test-local-path")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse> TestLocalPath([FromQuery] string localBasePath)
|
||||
{
|
||||
return await _systemService.TestLocalPathAsync(localBasePath);
|
||||
}
|
||||
|
||||
// ===== 通用文件上传 =====
|
||||
|
||||
[HttpPost("upload")]
|
||||
[Authorize]
|
||||
[RequestSizeLimit(2147483648)] // 2GB
|
||||
[RequestFormLimits(MultipartBodyLengthLimit = 2147483648)]
|
||||
public async Task<ApiResponse<UploadResultDto>> Upload([FromForm] IFormFile file, [FromForm] string folder)
|
||||
{
|
||||
// 检查文件是否为空
|
||||
if (file == null || file.Length == 0)
|
||||
return ApiResponse<UploadResultDto>.Fail("请选择要上传的文件");
|
||||
|
||||
// 默认目录
|
||||
if (string.IsNullOrWhiteSpace(folder))
|
||||
folder = "common";
|
||||
|
||||
// 从数据库读取上传大小限制,并验证文件大小
|
||||
var limit = await GetUploadLimitAsync(file.FileName);
|
||||
if (file.Length > limit)
|
||||
{
|
||||
var limitMB = limit / 1024.0 / 1024.0;
|
||||
return ApiResponse<UploadResultDto>.Fail($"文件大小超过限制(最大 {limitMB:F1}MB)");
|
||||
}
|
||||
|
||||
// 上传文件,获取对象键或本地URL
|
||||
var fileUrl = await _fileStorage.UploadAsync(file, folder);
|
||||
// 生成可访问的签名URL(COS模式下必须,本地模式直接返回)
|
||||
var signedUrl = _fileStorage.GetSignedUrl(fileUrl);
|
||||
// 注册到 PendingUpload 跟踪表(用于垃圾资源清理)
|
||||
await _uploadTracking.RegisterAsync(fileUrl, folder, _currentUser.IsAuthenticated ? _currentUser.UserId : null);
|
||||
// 返回对象键(用于存储)和签名URL(用于预览)
|
||||
return ApiResponse<UploadResultDto>.Success(new UploadResultDto { Url = signedUrl, Key = fileUrl });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除未使用的上传文件(仅限 pending 状态,已被实体消费的文件不会被删除)
|
||||
/// 用于前端取消表单、替换文件时即时清理垃圾资源
|
||||
/// </summary>
|
||||
/// <param name="key">文件对象键</param>
|
||||
[HttpPost("upload/delete")]
|
||||
[Authorize]
|
||||
public async Task<ApiResponse> DeleteUpload([FromQuery] string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
return ApiResponse.Fail("文件 key 不能为空");
|
||||
|
||||
var deleted = await _uploadTracking.DeletePendingAsync(key);
|
||||
return ApiResponse.Success(deleted ? "已删除" : "文件不存在或已使用");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据文件扩展名从数据库读取上传大小限制
|
||||
/// </summary>
|
||||
/// <param name="fileName">文件名</param>
|
||||
/// <returns>上传大小限制(字节)</returns>
|
||||
private async Task<long> GetUploadLimitAsync(string fileName)
|
||||
{
|
||||
// 根据文件扩展名确定限制类型
|
||||
var ext = Path.GetExtension(fileName).ToLower();
|
||||
var key = ext switch
|
||||
{
|
||||
// 图片类型
|
||||
".jpg" or ".jpeg" or ".png" or ".gif" or ".svg" or ".webp" => "UploadLimitImage",
|
||||
// 视频类型
|
||||
".mp4" or ".webm" or ".avi" or ".mov" or ".mkv" => "UploadLimitVideo",
|
||||
// 资源包类型
|
||||
".zip" or ".rar" or ".7z" or ".apk" or ".exe" or ".dmg" => "UploadLimitPackage",
|
||||
// 文档类型(默认)
|
||||
_ => "UploadLimitDocument"
|
||||
};
|
||||
|
||||
// 从数据库读取限制值
|
||||
var setting = await _db.SystemSettings.FirstOrDefaultAsync(s => s.Key == key);
|
||||
if (setting != null && long.TryParse(setting.Value, out var limit))
|
||||
return limit;
|
||||
|
||||
// 默认限制 20MB
|
||||
return 20L * 1024 * 1024;
|
||||
}
|
||||
|
||||
// ===== COS → Local 迁移 =====
|
||||
|
||||
/// <summary>
|
||||
/// 将 COS 中所有文件迁移到本地存储
|
||||
/// 用于从 COS 切换到 Local 模式后,补齐旧数据对应的本地文件
|
||||
/// </summary>
|
||||
[HttpPost("migrate-cos-to-local")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse> MigrateCosToLocal()
|
||||
{
|
||||
try
|
||||
{
|
||||
var count = await _fileStorage.MigrateCosToLocalAsync();
|
||||
await _operationLog.LogAsync("系统设置", "迁移",
|
||||
$"COS 迁移到本地存储完成,共迁移 {count} 个文件");
|
||||
return ApiResponse.Success($"迁移完成,共下载 {count} 个文件到本地");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ApiResponse.Fail($"迁移失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 迁移已有的 APK/IPA 文件扩展名
|
||||
/// COS 禁止通过默认域名分发 APK/IPA 文件(返回 DownloadForbidden 403)
|
||||
/// 将对象键从 xxx.apk 复制为 xxx.apk.bin,删除旧对象,更新数据库
|
||||
/// </summary>
|
||||
[HttpPost("migrate-apk-extensions")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse> MigrateApkExtensions()
|
||||
{
|
||||
try
|
||||
{
|
||||
var count = await _fileStorage.MigrateApkExtensionsAsync();
|
||||
await _operationLog.LogAsync("系统设置", "迁移",
|
||||
$"APK/IPA 扩展名迁移完成,共迁移 {count} 个文件");
|
||||
return ApiResponse.Success($"迁移完成,共处理 {count} 个 APK/IPA 文件");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ApiResponse.Fail($"迁移失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
58
backend/MilliSim.Api/Controllers/TagsController.cs
Normal file
58
backend/MilliSim.Api/Controllers/TagsController.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Common.Models;
|
||||
using MilliSim.Services;
|
||||
|
||||
namespace MilliSim.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/tags")]
|
||||
public class TagsController : ControllerBase
|
||||
{
|
||||
private readonly IProjectService _projectService;
|
||||
|
||||
public TagsController(IProjectService projectService)
|
||||
{
|
||||
_projectService = projectService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标签列表
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<ApiResponse<List<TagDto>>> GetTags()
|
||||
{
|
||||
return await _projectService.GetTagsAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建标签
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse<TagDto>> CreateTag([FromBody] CreateTagRequest request)
|
||||
{
|
||||
return await _projectService.CreateTagAsync(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新标签
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse<TagDto>> UpdateTag(long id, [FromBody] CreateTagRequest request)
|
||||
{
|
||||
return await _projectService.UpdateTagAsync(id, request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除标签
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}/delete")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse> DeleteTag(long id)
|
||||
{
|
||||
return await _projectService.DeleteTagAsync(id);
|
||||
}
|
||||
}
|
||||
90
backend/MilliSim.Api/Controllers/UsersController.cs
Normal file
90
backend/MilliSim.Api/Controllers/UsersController.cs
Normal file
@ -0,0 +1,90 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Common.Models;
|
||||
using MilliSim.Services;
|
||||
|
||||
namespace MilliSim.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/users")]
|
||||
[Authorize]
|
||||
public class UsersController : ControllerBase
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public UsersController(IUserService userService)
|
||||
{
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户列表
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[Authorize(Policy = "Staff")]
|
||||
public async Task<ApiResponse<PagedResult<UserDto>>> GetUsers([FromQuery] UserQueryRequest request)
|
||||
{
|
||||
return await _userService.GetUsersAsync(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户详情
|
||||
/// </summary>
|
||||
[HttpGet("{id:long}")]
|
||||
[Authorize(Policy = "Staff")]
|
||||
public async Task<ApiResponse<UserDto>> GetUserById(long id)
|
||||
{
|
||||
return await _userService.GetUserByIdAsync(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建用户
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[Authorize(Policy = "Staff")]
|
||||
public async Task<ApiResponse<UserDto>> CreateUser([FromBody] CreateUserRequest request)
|
||||
{
|
||||
return await _userService.CreateUserAsync(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新用户
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}")]
|
||||
[Authorize(Policy = "Staff")]
|
||||
public async Task<ApiResponse<UserDto>> UpdateUser(long id, [FromBody] UpdateUserRequest request)
|
||||
{
|
||||
return await _userService.UpdateUserAsync(id, request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除用户(软删除)
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}/delete")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse> DeleteUser(long id)
|
||||
{
|
||||
return await _userService.DeleteUserAsync(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置密码
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}/reset-password")]
|
||||
[Authorize(Policy = "Staff")]
|
||||
public async Task<ApiResponse> ResetPassword(long id)
|
||||
{
|
||||
return await _userService.ResetPasswordAsync(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换用户状态
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}/toggle-status")]
|
||||
[Authorize(Policy = "Admin")]
|
||||
public async Task<ApiResponse> ToggleStatus(long id)
|
||||
{
|
||||
return await _userService.ToggleStatusAsync(id);
|
||||
}
|
||||
}
|
||||
421
backend/MilliSim.Api/Data/DbSeeder.cs
Normal file
421
backend/MilliSim.Api/Data/DbSeeder.cs
Normal file
@ -0,0 +1,421 @@
|
||||
using BCrypt.Net;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MilliSim.Common.Entities;
|
||||
using MilliSim.Common.Models;
|
||||
using MilliSim.Common.Dtos;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace MilliSim.Data;
|
||||
|
||||
public static class DbSeeder
|
||||
{
|
||||
public static async Task SeedAsync(MilliSimDbContext db)
|
||||
{
|
||||
// 先执行数据库结构补丁(为已存在的表添加新字段)
|
||||
await PatchDatabaseSchemaAsync(db);
|
||||
|
||||
// 管理员
|
||||
if (!db.Users.Any(u => u.Username == "admin"))
|
||||
{
|
||||
db.Users.Add(new User
|
||||
{
|
||||
Username = "admin",
|
||||
Password = BCryptHelper.HashPassword("admin123"),
|
||||
Nickname = "系统管理员",
|
||||
Role = UserRole.Admin,
|
||||
Status = UserStatus.Active,
|
||||
Email = "admin@millisim.com",
|
||||
Phone = "13800000000"
|
||||
});
|
||||
}
|
||||
|
||||
// 产品经理
|
||||
if (!db.Users.Any(u => u.Username == "pm"))
|
||||
{
|
||||
db.Users.Add(new User
|
||||
{
|
||||
Username = "pm",
|
||||
Password = BCryptHelper.HashPassword("123456"),
|
||||
Nickname = "产品经理",
|
||||
Role = UserRole.ProductManager,
|
||||
Status = UserStatus.Active,
|
||||
Email = "pm@millisim.com",
|
||||
Phone = "13800000001"
|
||||
});
|
||||
}
|
||||
|
||||
// 销售
|
||||
if (!db.Users.Any(u => u.Username == "sales"))
|
||||
{
|
||||
db.Users.Add(new User
|
||||
{
|
||||
Username = "sales",
|
||||
Password = BCryptHelper.HashPassword("123456"),
|
||||
Nickname = "销售员",
|
||||
Role = UserRole.Sales,
|
||||
Status = UserStatus.Active,
|
||||
Email = "sales@millisim.com",
|
||||
Phone = "13800000002"
|
||||
});
|
||||
}
|
||||
|
||||
// 分类
|
||||
if (!db.ProjectCategories.Any())
|
||||
{
|
||||
db.ProjectCategories.AddRange(
|
||||
new ProjectCategory { Name = "工业仿真", Icon = "factory", SortOrder = 1, Status = UserStatus.Active },
|
||||
new ProjectCategory { Name = "医疗仿真", Icon = "medical", SortOrder = 2, Status = UserStatus.Active },
|
||||
new ProjectCategory { Name = "教育仿真", Icon = "education", SortOrder = 3, Status = UserStatus.Active },
|
||||
new ProjectCategory { Name = "军事仿真", Icon = "military", SortOrder = 4, Status = UserStatus.Active },
|
||||
new ProjectCategory { Name = "建筑仿真", Icon = "building", SortOrder = 5, Status = UserStatus.Active }
|
||||
);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// 平台类型
|
||||
if (!db.PlatformTypes.Any())
|
||||
{
|
||||
db.PlatformTypes.AddRange(
|
||||
new PlatformType { Name = "WebGL", Icon = "web", SortOrder = 1, Status = UserStatus.Active },
|
||||
new PlatformType { Name = "Android", Icon = "android", SortOrder = 2, Status = UserStatus.Active },
|
||||
new PlatformType { Name = "Windows", Icon = "windows", SortOrder = 3, Status = UserStatus.Active },
|
||||
new PlatformType { Name = "Mac", Icon = "apple", SortOrder = 4, Status = UserStatus.Active },
|
||||
new PlatformType { Name = "VR", Icon = "vr", SortOrder = 5, Status = UserStatus.Active }
|
||||
);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// 特性标签
|
||||
if (!db.FeatureTags.Any())
|
||||
{
|
||||
db.FeatureTags.AddRange(
|
||||
new FeatureTag { Name = "高精度", Icon = "precision", SortOrder = 1, Status = UserStatus.Active },
|
||||
new FeatureTag { Name = "实时渲染", Icon = "render", SortOrder = 2, Status = UserStatus.Active },
|
||||
new FeatureTag { Name = "物理引擎", Icon = "physics", SortOrder = 3, Status = UserStatus.Active },
|
||||
new FeatureTag { Name = "多人协作", Icon = "collaboration", SortOrder = 4, Status = UserStatus.Active },
|
||||
new FeatureTag { Name = "VR支持", Icon = "vr", SortOrder = 5, Status = UserStatus.Active }
|
||||
);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// 示例项目
|
||||
if (!db.Projects.Any())
|
||||
{
|
||||
var categories = db.ProjectCategories.OrderBy(c => c.SortOrder).ToList();
|
||||
db.Projects.AddRange(
|
||||
new Project
|
||||
{
|
||||
Name = "智能工厂生产线仿真",
|
||||
CategoryId = categories[0].Id,
|
||||
CoverImage = "",
|
||||
Description = "基于Unity开发的智能工厂生产线三维仿真系统,支持产线布局规划与工艺流程模拟。",
|
||||
Content = "<p>智能工厂生产线仿真系统,采用物理引擎真实模拟生产流程。</p>",
|
||||
Platforms = "WebGL,Windows",
|
||||
Status = ProjectStatus.Published,
|
||||
IsFeatured = true,
|
||||
SortOrder = 1,
|
||||
ViewCount = 1280,
|
||||
ExperienceCount = 356,
|
||||
FavoriteCount = 89,
|
||||
PublishedAt = DateTime.Now.AddDays(-10)
|
||||
},
|
||||
new Project
|
||||
{
|
||||
Name = "人体解剖学虚拟仿真",
|
||||
CategoryId = categories[1].Id,
|
||||
CoverImage = "",
|
||||
Description = "三维人体解剖学教学仿真,支持器官拆解与结构学习。",
|
||||
Content = "<p>人体解剖学虚拟仿真教学系统。</p>",
|
||||
Platforms = "WebGL,VR",
|
||||
Status = ProjectStatus.Published,
|
||||
IsFeatured = true,
|
||||
SortOrder = 2,
|
||||
ViewCount = 980,
|
||||
ExperienceCount = 245,
|
||||
FavoriteCount = 67,
|
||||
PublishedAt = DateTime.Now.AddDays(-5)
|
||||
},
|
||||
new Project
|
||||
{
|
||||
Name = "化学反应实验仿真",
|
||||
CategoryId = categories[2].Id,
|
||||
CoverImage = "",
|
||||
Description = "虚拟化学实验室,支持多种化学反应模拟与安全培训。",
|
||||
Content = "<p>化学反应实验仿真系统。</p>",
|
||||
Platforms = "WebGL,Android",
|
||||
Status = ProjectStatus.Published,
|
||||
IsFeatured = false,
|
||||
SortOrder = 3,
|
||||
ViewCount = 560,
|
||||
ExperienceCount = 120,
|
||||
FavoriteCount = 34,
|
||||
PublishedAt = DateTime.Now.AddDays(-2)
|
||||
}
|
||||
);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// 轮播图
|
||||
if (!db.Banners.Any())
|
||||
{
|
||||
db.Banners.AddRange(
|
||||
new Banner { Title = "毫秒数仿 - 专业虚拟仿真平台", Image = "", Link = "/projects", SortOrder = 1, Status = UserStatus.Active },
|
||||
new Banner { Title = "海量项目资源", Image = "", Link = "/projects", SortOrder = 2, Status = UserStatus.Active },
|
||||
new Banner { Title = "在线体验 WebGL", Image = "", Link = "/projects", SortOrder = 3, Status = UserStatus.Active }
|
||||
);
|
||||
}
|
||||
|
||||
// 系统设置(补充缺失的设置项)
|
||||
var allSettings = new List<(string Key, string Value, string Description)>
|
||||
{
|
||||
// ===== 基础信息 =====
|
||||
("PlatformName", "毫秒数仿", "平台名称"),
|
||||
("Slogan", "专业虚拟仿真项目资产库", "平台标语"),
|
||||
("CompanyName", "毫秒数仿科技有限公司", "公司名称"),
|
||||
("Contact", "400-888-8888", "联系方式"),
|
||||
("ICP", "沪ICP备XXXXXXXX号", "备案号"),
|
||||
|
||||
// ===== Logo 配置 =====
|
||||
("LogoFrontend", "", "前台Logo"),
|
||||
("LogoBackend", "", "后台Logo"),
|
||||
("LogoFavicon", "", "浏览器图标"),
|
||||
("LogoLogin", "", "登录页Logo"),
|
||||
("LogoFooter", "", "页脚Logo"),
|
||||
|
||||
// ===== 资源存储配置(默认COS私有读写) =====
|
||||
("StorageMode", "Cos", "存储模式:Local=本地存储,Cos=腾讯云COS"),
|
||||
("CosSecretId", "AKIDvmQp11oNjtqa7sTr23sTzIPVlvvmR6lO", "腾讯云COS SecretId"),
|
||||
("CosSecretKey", "VqHWm083mKjzIXdZN7sTtIct69z9B2gL", "腾讯云COS SecretKey"),
|
||||
("CosBucket", "millisim-1300718495", "腾讯云COS 存储桶名称"),
|
||||
("CosRegion", "ap-guangzhou", "腾讯云COS 地域"),
|
||||
("CosSignedUrlExpireMinutes", "60", "COS临时URL有效期(分钟)"),
|
||||
|
||||
// ===== 本地存储配置 =====
|
||||
("LocalBasePath", "wwwroot/uploads", "本地存储根目录"),
|
||||
("LocalBaseUrl", "/uploads", "本地访问基础URL"),
|
||||
|
||||
// ===== 上传大小限制(字节) =====
|
||||
("UploadLimitImage", "5242880", "图片上传限制(字节) 5MB"),
|
||||
("UploadLimitVideo", "524288000", "视频上传限制(字节) 500MB"),
|
||||
("UploadLimitPackage", "2147483648", "资源包上传限制(字节) 2GB"),
|
||||
("UploadLimitDocument", "20971520", "文档上传限制(字节) 20MB"),
|
||||
|
||||
// ===== 备份配置 =====
|
||||
("BackupAutoEnabled", "true", "是否启用自动备份"),
|
||||
("BackupAutoTime", "02:00", "自动备份时间(HH:mm)"),
|
||||
("BackupRetentionDays", "30", "备份保留天数"),
|
||||
|
||||
// ===== 日志配置 =====
|
||||
("LogRetentionDays", "90", "日志保留天数"),
|
||||
|
||||
// ===== 首页模块显示 =====
|
||||
("ShowPartners", "true", "是否在前台首页显示合作伙伴模块"),
|
||||
("ShowAbout", "true", "是否在前台显示关于我们页面"),
|
||||
|
||||
// ===== 关于我们页面内容(JSON) =====
|
||||
("AboutContent", JsonSerializer.Serialize(new AboutContentDto
|
||||
{
|
||||
HeroTag = "关于我们",
|
||||
HeroTitle = "用技术连接虚拟与现实",
|
||||
HeroDesc = "毫秒数仿科技有限公司,专注工业仿真与数字孪生技术创新,致力于让每一个仿真场景都触手可及。",
|
||||
IntroTitle = "公司简介",
|
||||
IntroParagraphs = new List<string>
|
||||
{
|
||||
"毫秒数仿科技有限公司成立于 2020 年,是一家专注于工业仿真与数字孪生领域的国家高新技术企业。公司总部位于上海,在北京、深圳、成都设有研发中心,业务覆盖全国及海外市场。",
|
||||
"我们以\u201C让工业仿真触手可及\u201D为使命,自主研发了基于 WebGL 的高性能仿真引擎与多端运行框架,服务于制造、医疗、教育、军事、建筑等多个行业领域,累计交付项目超过 500 个,服务客户包括多家世界 500 强企业。",
|
||||
"公司拥有一支由资深图形学专家、仿真工程师、行业顾问组成的精英团队,核心成员来自国内外知名高校与企业,具备深厚的技术积累与丰富的行业经验。"
|
||||
},
|
||||
Stats = new List<AboutStatDto>
|
||||
{
|
||||
new() { Number = "500+", Label = "交付项目" },
|
||||
new() { Number = "200+", Label = "服务客户" },
|
||||
new() { Number = "50+", Label = "团队成员" },
|
||||
new() { Number = "30+", Label = "行业领域" }
|
||||
},
|
||||
MissionTitle = "使命与愿景",
|
||||
MissionCards = new List<AboutMissionCardDto>
|
||||
{
|
||||
new() { Title = "使命", Icon = "rocket", Desc = "让工业仿真触手可及,以技术创新驱动产业升级。" },
|
||||
new() { Title = "愿景", Icon = "globe", Desc = "成为全球领先的工业仿真与数字孪生平台,连接虚拟与现实,赋能千行百业。" },
|
||||
new() { Title = "价值观", Icon = "heart", Desc = "客户至上、技术驱动、开放协作、追求卓越。" }
|
||||
},
|
||||
TimelineTitle = "发展历程",
|
||||
Timeline = new List<AboutTimelineItemDto>
|
||||
{
|
||||
new() { Year = "2020", Title = "公司成立", Desc = "毫秒数仿在上海张江正式成立,开启工业仿真技术探索之路。" },
|
||||
new() { Year = "2021", Title = "引擎发布", Desc = "自主研发的 WebGL 仿真引擎 v1.0 正式发布,支持基础渲染与交互。" },
|
||||
new() { Year = "2022", Title = "多端覆盖", Desc = "完成 Web、Android、Windows、iOS 多端运行框架,业务覆盖 10+ 行业。" },
|
||||
new() { Year = "2023", Title = "数字孪生", Desc = "推出数字孪生平台,融合实时数据驱动与协同交互能力,服务客户超 200 家。" },
|
||||
new() { Year = "2024", Title = "生态构建", Desc = "开放平台能力,构建合作伙伴生态,累计交付项目突破 500 个。" }
|
||||
}
|
||||
}), "关于我们页面内容(JSON 格式)")
|
||||
};
|
||||
|
||||
var existingKeys = db.SystemSettings.Select(s => s.Key).ToHashSet();
|
||||
var missingSettings = allSettings
|
||||
.Where(s => !existingKeys.Contains(s.Key))
|
||||
.Select(s => new SystemSetting { Key = s.Key, Value = s.Value, Description = s.Description })
|
||||
.ToList();
|
||||
|
||||
if (missingSettings.Count > 0)
|
||||
{
|
||||
db.SystemSettings.AddRange(missingSettings);
|
||||
}
|
||||
|
||||
// 合作伙伴
|
||||
if (!db.Partners.Any())
|
||||
{
|
||||
db.Partners.AddRange(
|
||||
new Partner { Name = "腾讯云", Logo = "", Link = "#", SortOrder = 1, IsVisible = true },
|
||||
new Partner { Name = "Unity", Logo = "", Link = "#", SortOrder = 2, IsVisible = true }
|
||||
);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 数据库结构补丁:为已存在的表添加新字段
|
||||
/// 因为使用 EnsureCreated() 不会更新已存在数据库的表结构,
|
||||
/// 所以需要手动执行 ALTER TABLE 添加新字段
|
||||
/// </summary>
|
||||
private static async Task PatchDatabaseSchemaAsync(MilliSimDbContext db)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查 ProjectPackages 表是否需要添加新字段
|
||||
var columns = await db.Database.SqlQueryRaw<string>(
|
||||
"SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ProjectPackages'"
|
||||
).ToListAsync();
|
||||
|
||||
var columnSet = columns.ToHashSet();
|
||||
|
||||
// 添加 WakeProtocolPrefix 字段
|
||||
if (!columnSet.Contains("WakeProtocolPrefix"))
|
||||
{
|
||||
await db.Database.ExecuteSqlRawAsync(
|
||||
"ALTER TABLE `ProjectPackages` ADD COLUMN `WakeProtocolPrefix` VARCHAR(50) NULL DEFAULT ''");
|
||||
}
|
||||
|
||||
// 添加 WakeProtocolParam 字段
|
||||
if (!columnSet.Contains("WakeProtocolParam"))
|
||||
{
|
||||
await db.Database.ExecuteSqlRawAsync(
|
||||
"ALTER TABLE `ProjectPackages` ADD COLUMN `WakeProtocolParam` VARCHAR(500) NULL DEFAULT ''");
|
||||
}
|
||||
|
||||
// 添加 ExtractedPath 字段
|
||||
if (!columnSet.Contains("ExtractedPath"))
|
||||
{
|
||||
await db.Database.ExecuteSqlRawAsync(
|
||||
"ALTER TABLE `ProjectPackages` ADD COLUMN `ExtractedPath` VARCHAR(500) NULL DEFAULT ''");
|
||||
}
|
||||
|
||||
// 添加 FileName 字段(原始文件名)
|
||||
if (!columnSet.Contains("FileName"))
|
||||
{
|
||||
await db.Database.ExecuteSqlRawAsync(
|
||||
"ALTER TABLE `ProjectPackages` ADD COLUMN `FileName` VARCHAR(255) NULL DEFAULT ''");
|
||||
}
|
||||
|
||||
// ===== 修复 Authorizations.ProjectId:客户级授权不再关联项目 =====
|
||||
// 1) 移除外键约束 FK_Authorizations_Projects_ProjectId(若存在)
|
||||
// 2) 让 ProjectId 列可空(实体已声明为 long?)
|
||||
// 注意:SqlQueryRaw<string> 要求列别名必须是 Value
|
||||
var fkName = await db.Database.SqlQueryRaw<string>(
|
||||
"SELECT CONSTRAINT_NAME AS `Value` FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS " +
|
||||
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'Authorizations' " +
|
||||
"AND CONSTRAINT_TYPE = 'FOREIGN KEY' AND CONSTRAINT_NAME = 'FK_Authorizations_Projects_ProjectId'"
|
||||
).FirstOrDefaultAsync();
|
||||
|
||||
if (fkName != null)
|
||||
{
|
||||
await db.Database.ExecuteSqlRawAsync(
|
||||
"ALTER TABLE `Authorizations` DROP FOREIGN KEY `FK_Authorizations_Projects_ProjectId`");
|
||||
}
|
||||
|
||||
// 让 ProjectId 列可空
|
||||
var projectIdNullable = await db.Database.SqlQueryRaw<string>(
|
||||
"SELECT IS_NULLABLE AS `Value` FROM INFORMATION_SCHEMA.COLUMNS " +
|
||||
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'Authorizations' AND COLUMN_NAME = 'ProjectId'"
|
||||
).FirstOrDefaultAsync();
|
||||
|
||||
if (projectIdNullable != null && projectIdNullable != "YES")
|
||||
{
|
||||
await db.Database.ExecuteSqlRawAsync(
|
||||
"ALTER TABLE `Authorizations` MODIFY COLUMN `ProjectId` BIGINT NULL");
|
||||
}
|
||||
|
||||
// ===== 修复分类/平台/标签表 Description 字段:实体模型已移除 Description,但 DB 列可能仍为 NOT NULL =====
|
||||
// 导致 DbSeeder 插入新记录时失败("Field 'Description' doesn't have a default value")
|
||||
var descTablesToPatch = new[] { "ProjectCategories", "PlatformTypes", "FeatureTags" };
|
||||
foreach (var table in descTablesToPatch)
|
||||
{
|
||||
var descInfo = await db.Database.SqlQueryRaw<string>(
|
||||
"SELECT IS_NULLABLE AS `Value` FROM INFORMATION_SCHEMA.COLUMNS " +
|
||||
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '" + table + "' AND COLUMN_NAME = 'Description'"
|
||||
).FirstOrDefaultAsync();
|
||||
|
||||
if (descInfo != null && descInfo != "YES")
|
||||
{
|
||||
await db.Database.ExecuteSqlRawAsync(
|
||||
"ALTER TABLE `" + table + "` MODIFY COLUMN `Description` VARCHAR(200) NULL DEFAULT ''");
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 修复 ProjectPackages.Version 字段:实体模型已移除 Version,但 DB 列可能仍为 NOT NULL =====
|
||||
var versionInfo = await db.Database.SqlQueryRaw<string>(
|
||||
"SELECT IS_NULLABLE AS `Value` FROM INFORMATION_SCHEMA.COLUMNS " +
|
||||
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ProjectPackages' AND COLUMN_NAME = 'Version'"
|
||||
).FirstOrDefaultAsync();
|
||||
|
||||
if (versionInfo != null && versionInfo != "YES")
|
||||
{
|
||||
await db.Database.ExecuteSqlRawAsync(
|
||||
"ALTER TABLE `ProjectPackages` MODIFY COLUMN `Version` VARCHAR(50) NULL DEFAULT ''");
|
||||
}
|
||||
|
||||
// ===== PendingUploads 表:垃圾资源清理跟踪 =====
|
||||
var pendingTableExists = await db.Database.SqlQueryRaw<string>(
|
||||
"SELECT TABLE_NAME AS `Value` FROM INFORMATION_SCHEMA.TABLES " +
|
||||
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'PendingUploads'"
|
||||
).FirstOrDefaultAsync();
|
||||
|
||||
if (pendingTableExists == null)
|
||||
{
|
||||
await db.Database.ExecuteSqlRawAsync(@"
|
||||
CREATE TABLE `PendingUploads` (
|
||||
`Id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`ObjectKey` VARCHAR(500) NOT NULL,
|
||||
`Folder` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`UploadedBy` BIGINT NULL,
|
||||
`UploadedAt` DATETIME NOT NULL,
|
||||
`ConsumedAt` DATETIME NULL,
|
||||
`CreatedAt` DATETIME NOT NULL,
|
||||
`UpdatedAt` DATETIME NOT NULL,
|
||||
`IsDeleted` BIT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`Id`),
|
||||
INDEX `IX_PendingUploads_ObjectKey` (`ObjectKey`),
|
||||
INDEX `IX_PendingUploads_ConsumedAt` (`ConsumedAt`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略补丁错误(如数据库不存在时 INFORMATION_SCHEMA 查询失败)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class BCryptHelper
|
||||
{
|
||||
public static string HashPassword(string password)
|
||||
{
|
||||
return BCrypt.Net.BCrypt.HashPassword(password, 12);
|
||||
}
|
||||
|
||||
public static bool VerifyPassword(string password, string hash)
|
||||
{
|
||||
return BCrypt.Net.BCrypt.Verify(password, hash);
|
||||
}
|
||||
}
|
||||
76
backend/MilliSim.Api/Data/MilliSimDbContext.cs
Normal file
76
backend/MilliSim.Api/Data/MilliSimDbContext.cs
Normal file
@ -0,0 +1,76 @@
|
||||
using System.Linq.Expressions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MilliSim.Common.Entities;
|
||||
|
||||
namespace MilliSim.Data;
|
||||
|
||||
public class MilliSimDbContext : DbContext
|
||||
{
|
||||
public MilliSimDbContext(DbContextOptions<MilliSimDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<Project> Projects => Set<Project>();
|
||||
public DbSet<ProjectCategory> ProjectCategories => Set<ProjectCategory>();
|
||||
public DbSet<PlatformType> PlatformTypes => Set<PlatformType>();
|
||||
public DbSet<FeatureTag> FeatureTags => Set<FeatureTag>();
|
||||
public DbSet<ProjectTagRelation> ProjectTagRelations => Set<ProjectTagRelation>();
|
||||
public DbSet<ProjectScreenshot> ProjectScreenshots => Set<ProjectScreenshot>();
|
||||
public DbSet<ProjectPackage> ProjectPackages => Set<ProjectPackage>();
|
||||
public DbSet<Favorite> Favorites => Set<Favorite>();
|
||||
public DbSet<ExperienceLog> ExperienceLogs => Set<ExperienceLog>();
|
||||
public DbSet<Authorization> Authorizations => Set<Authorization>();
|
||||
public DbSet<Banner> Banners => Set<Banner>();
|
||||
public DbSet<Partner> Partners => Set<Partner>();
|
||||
public DbSet<Consultation> Consultations => Set<Consultation>();
|
||||
public DbSet<Notification> Notifications => Set<Notification>();
|
||||
public DbSet<NotificationRead> NotificationReads => Set<NotificationRead>();
|
||||
public DbSet<OperationLog> OperationLogs => Set<OperationLog>();
|
||||
public DbSet<SystemSetting> SystemSettings => Set<SystemSetting>();
|
||||
public DbSet<Backup> Backups => Set<Backup>();
|
||||
public DbSet<WakeProtocol> WakeProtocols => Set<WakeProtocol>();
|
||||
public DbSet<PendingUpload> PendingUploads => Set<PendingUpload>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// 软删除全局过滤器
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
if (typeof(BaseEntity).IsAssignableFrom(entityType.ClrType))
|
||||
{
|
||||
var parameter = Expression.Parameter(entityType.ClrType, "e");
|
||||
var property = Expression.Property(parameter, nameof(BaseEntity.IsDeleted));
|
||||
var falseValue = Expression.Constant(false);
|
||||
var comparison = Expression.Equal(property, falseValue);
|
||||
var lambda = Expression.Lambda(comparison, parameter);
|
||||
modelBuilder.Entity(entityType.ClrType).HasQueryFilter((dynamic)lambda);
|
||||
}
|
||||
}
|
||||
|
||||
// 唯一索引
|
||||
modelBuilder.Entity<User>().HasIndex(u => u.Username).IsUnique();
|
||||
modelBuilder.Entity<SystemSetting>().HasIndex(s => s.Key).IsUnique();
|
||||
|
||||
// PendingUpload 索引(用于垃圾资源清理查询)
|
||||
modelBuilder.Entity<PendingUpload>().HasIndex(p => p.ObjectKey);
|
||||
modelBuilder.Entity<PendingUpload>().HasIndex(p => p.ConsumedAt);
|
||||
|
||||
// 字符集
|
||||
modelBuilder.Entity<Project>().Property(p => p.Content).HasColumnType("longtext");
|
||||
|
||||
// 精度
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
foreach (var property in entityType.GetProperties())
|
||||
{
|
||||
if (property.ClrType == typeof(DateTime))
|
||||
{
|
||||
property.SetColumnType("datetime");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
backend/MilliSim.Api/GlobalUsings.cs
Normal file
3
backend/MilliSim.Api/GlobalUsings.cs
Normal file
@ -0,0 +1,3 @@
|
||||
// 全局 using 指令
|
||||
global using MilliSim.Common.Models;
|
||||
global using MilliSim.Common.Entities;
|
||||
28
backend/MilliSim.Api/MilliSim.Api.csproj
Normal file
28
backend/MilliSim.Api/MilliSim.Api.csproj
Normal file
@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>MilliSim</RootNamespace>
|
||||
<AssemblyName>MilliSim.Api</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageReference Include="Tencent.QCloud.Cos.Sdk" Version="5.4.40" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.OpenApi" Version="1.6.22" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
6
backend/MilliSim.Api/MilliSim.Api.http
Normal file
6
backend/MilliSim.Api/MilliSim.Api.http
Normal file
@ -0,0 +1,6 @@
|
||||
@MilliSim.Api_HostAddress = http://localhost:5041
|
||||
|
||||
GET {{MilliSim.Api_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
279
backend/MilliSim.Api/Program.cs
Normal file
279
backend/MilliSim.Api/Program.cs
Normal file
@ -0,0 +1,279 @@
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using MilliSim.Data;
|
||||
using MilliSim.Services.Infrastructure;
|
||||
using MilliSim.Services;
|
||||
using Serilog;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// 配置 Kestrel 服务器,允许大文件上传(最大 2GB)
|
||||
builder.WebHost.ConfigureKestrel(options =>
|
||||
{
|
||||
options.Limits.MaxRequestBodySize = 2147483648; // 2GB
|
||||
});
|
||||
|
||||
// 配置表单选项,允许大文件上传
|
||||
builder.Services.Configure<FormOptions>(options =>
|
||||
{
|
||||
options.MultipartBodyLengthLimit = 2147483648; // 2GB
|
||||
});
|
||||
|
||||
// Serilog
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(builder.Configuration)
|
||||
.WriteTo.Console()
|
||||
.WriteTo.File("logs/log-.txt", rollingInterval: RollingInterval.Day)
|
||||
.CreateLogger();
|
||||
builder.Host.UseSerilog();
|
||||
|
||||
// 数据库
|
||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
|
||||
var serverVersion = ServerVersion.AutoDetect(new MySqlConnector.MySqlConnection(connectionString));
|
||||
builder.Services.AddDbContext<MilliSimDbContext>(options =>
|
||||
options.UseMySql(connectionString, serverVersion, mysql =>
|
||||
{
|
||||
mysql.MigrationsAssembly("MilliSim.Api");
|
||||
}));
|
||||
|
||||
// 基础服务
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddScoped<ICurrentUserService, CurrentUserService>();
|
||||
builder.Services.AddScoped<IJwtService, JwtService>();
|
||||
builder.Services.AddSingleton<ICaptchaService, CaptchaService>();
|
||||
builder.Services.AddScoped<IFileStorageService, FileStorageService>();
|
||||
builder.Services.AddScoped<IOperationLogService, OperationLogService>();
|
||||
builder.Services.AddScoped<IUploadTrackingService, UploadTrackingService>();
|
||||
|
||||
// 业务服务
|
||||
builder.Services.AddScoped<IAuthService, AuthService>();
|
||||
builder.Services.AddScoped<IUserService, UserService>();
|
||||
builder.Services.AddScoped<IProjectService, ProjectService>();
|
||||
builder.Services.AddScoped<IAuthorizationService, AuthorizationService>();
|
||||
builder.Services.AddScoped<IContentService, ContentService>();
|
||||
builder.Services.AddScoped<ISystemService, SystemService>();
|
||||
builder.Services.AddScoped<INotificationService, NotificationService>();
|
||||
builder.Services.AddScoped<IDashboardService, DashboardService>();
|
||||
|
||||
// 后台定时任务:授权到期提醒
|
||||
builder.Services.AddHostedService<AuthorizationNotificationJob>();
|
||||
|
||||
// 后台定时任务:垃圾资源清理(每 6 小时清理 24h 未消费的上传文件)
|
||||
builder.Services.AddHostedService<UploadCleanupJob>();
|
||||
|
||||
// JWT 认证
|
||||
var secretKey = builder.Configuration["Jwt:SecretKey"]!;
|
||||
var issuer = builder.Configuration["Jwt:Issuer"];
|
||||
var audience = builder.Configuration["Jwt:Audience"];
|
||||
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = issuer,
|
||||
ValidAudience = audience,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)),
|
||||
ClockSkew = TimeSpan.Zero
|
||||
};
|
||||
// 允许通过 query string 的 access_token 传递 JWT(用于下载端点,浏览器无法在 window.open/锚点点击中设置 Authorization 头)
|
||||
options.Events = new JwtBearerEvents
|
||||
{
|
||||
OnMessageReceived = context =>
|
||||
{
|
||||
var accessToken = context.Request.Query["access_token"];
|
||||
if (!string.IsNullOrEmpty(accessToken))
|
||||
{
|
||||
context.Token = accessToken;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
builder.Services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddPolicy("Admin", policy => policy.RequireRole("Admin", "4"));
|
||||
options.AddPolicy("Sales", policy => policy.RequireRole("Sales", "2", "Admin", "4"));
|
||||
options.AddPolicy("ProductManager", policy => policy.RequireRole("ProductManager", "3", "Admin", "4"));
|
||||
options.AddPolicy("Staff", policy => policy.RequireRole("Sales", "2", "ProductManager", "3", "Admin", "4"));
|
||||
});
|
||||
|
||||
// CORS
|
||||
var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? Array.Empty<string>();
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("MilliSimCors", policy =>
|
||||
{
|
||||
policy.WithOrigins(allowedOrigins)
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
.AllowCredentials();
|
||||
});
|
||||
});
|
||||
|
||||
// Swagger
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(c =>
|
||||
{
|
||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "MilliSim API", Version = "v1" });
|
||||
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||
{
|
||||
Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
|
||||
Name = "Authorization",
|
||||
In = ParameterLocation.Header,
|
||||
Type = SecuritySchemeType.ApiKey,
|
||||
Scheme = "Bearer"
|
||||
});
|
||||
c.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" }
|
||||
},
|
||||
Array.Empty<string>()
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 控制器
|
||||
builder.Services.AddControllers()
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase;
|
||||
options.JsonSerializerOptions.WriteIndented = false;
|
||||
// 添加北京时间转换器,将DateTime序列化为北京时间格式 "yyyy-MM-dd HH:mm:ss"
|
||||
options.JsonSerializerOptions.Converters.Add(new BeijingTimeConverter());
|
||||
options.JsonSerializerOptions.Converters.Add(new NullableBeijingTimeConverter());
|
||||
});
|
||||
|
||||
// 静态文件(通过 app.UseStaticFiles() 启用)
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// 自动迁移与种子
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<MilliSimDbContext>();
|
||||
try
|
||||
{
|
||||
db.Database.EnsureCreated();
|
||||
await DbSeeder.SeedAsync(db);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "数据库初始化失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 中间件
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
// 注意:不使用 app.UseStaticFiles()(默认暴露 wwwroot/ 下所有文件,含 backups/ 数据库备份)
|
||||
// 仅通过下方受控的 /uploads/ 中间件暴露上传资源,其他 wwwroot 路径一律不可直接访问
|
||||
|
||||
// 读取本地存储配置(LocalBasePath/LocalBaseUrl):供签名验证中间件与静态文件中间件共用
|
||||
// 在应用启动时读取一次,运行时若后台修改 LocalBaseUrl 需重启服务才生效(安全权衡:避免每请求查库)
|
||||
string localBaseUrl = "/uploads";
|
||||
string localBasePath = "wwwroot/uploads";
|
||||
try
|
||||
{
|
||||
using var storageScope = app.Services.CreateScope();
|
||||
var storageDb = storageScope.ServiceProvider.GetRequiredService<MilliSimDbContext>();
|
||||
var localBasePathSetting = await storageDb.SystemSettings.FirstOrDefaultAsync(s => s.Key == "LocalBasePath");
|
||||
var localBaseUrlSetting = await storageDb.SystemSettings.FirstOrDefaultAsync(s => s.Key == "LocalBaseUrl");
|
||||
localBasePath = localBasePathSetting?.Value ?? "wwwroot/uploads";
|
||||
localBaseUrl = localBaseUrlSetting?.Value ?? "/uploads";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "读取本地存储配置失败,使用默认值 /uploads");
|
||||
}
|
||||
|
||||
// 安全中间件1:拦截 /backups/ 路径的直接访问,数据库备份只能通过认证 API 下载
|
||||
// (/backups/ 物理目录位于 wwwroot/backups/,不能被静态文件中间件暴露)
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
if (context.Request.Path.StartsWithSegments("/backups", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
context.Response.StatusCode = 404;
|
||||
return;
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
// 安全中间件2:验证 Local 模式 /uploads/ 资源的签名URL(与 COS 临时签名URL对齐)
|
||||
// Local 模式 GetSignedUrl 返回 /uploads/{path}?exp={ts}&sig={hmac},此中间件校验签名与过期时间
|
||||
// 无效/过期/缺失签名一律返回 403,防止 Local 模式资源被永久链接盗用
|
||||
// 注意:必须在 UseStaticFiles 之前注册,否则静态文件中间件会先放行
|
||||
var localSigningKey = app.Configuration["Jwt:SecretKey"]!;
|
||||
var localBaseUrlPrefix = localBaseUrl.TrimEnd('/') + "/";
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
var path = context.Request.Path.Value ?? string.Empty;
|
||||
if (path.StartsWith(localBaseUrlPrefix, StringComparison.OrdinalIgnoreCase)
|
||||
|| path.Equals(localBaseUrl, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var exp = context.Request.Query["exp"].FirstOrDefault();
|
||||
var sig = context.Request.Query["sig"].FirstOrDefault();
|
||||
if (!FileStorageService.ValidateLocalSignedUrl(context.Request.Path, exp, sig, localSigningKey))
|
||||
{
|
||||
context.Response.StatusCode = 403;
|
||||
return;
|
||||
}
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
// 配置本地存储静态文件中间件:根据数据库 LocalBasePath/LocalBaseUrl 配置
|
||||
// 支持相对路径(相对于 ContentRootPath)或绝对路径,使本地模式上传的文件可通过 HTTP 访问
|
||||
try
|
||||
{
|
||||
var absolutePath = Path.IsPathRooted(localBasePath)
|
||||
? localBasePath
|
||||
: Path.Combine(app.Environment.ContentRootPath, localBasePath);
|
||||
|
||||
if (!Directory.Exists(absolutePath))
|
||||
Directory.CreateDirectory(absolutePath);
|
||||
|
||||
// Unity WebGL 资源使用 .unityweb 扩展名(gzip 压缩的二进制流),默认 ContentTypeProvider 不识别会 404
|
||||
var contentTypeProvider = new FileExtensionContentTypeProvider();
|
||||
contentTypeProvider.Mappings[".unityweb"] = "application/octet-stream";
|
||||
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
FileProvider = new PhysicalFileProvider(absolutePath),
|
||||
RequestPath = localBaseUrl,
|
||||
ContentTypeProvider = contentTypeProvider
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "配置本地存储静态文件中间件失败");
|
||||
}
|
||||
|
||||
app.UseCors("MilliSimCors");
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.MapGet("/", () => Results.Redirect("/swagger"));
|
||||
|
||||
app.Run();
|
||||
14
backend/MilliSim.Api/Properties/launchSettings.json
Normal file
14
backend/MilliSim.Api/Properties/launchSettings.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
238
backend/MilliSim.Api/Services/AuthService.cs
Normal file
238
backend/MilliSim.Api/Services/AuthService.cs
Normal file
@ -0,0 +1,238 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Common.Entities;
|
||||
using MilliSim.Common.Models;
|
||||
using MilliSim.Data;
|
||||
using MilliSim.Services.Infrastructure;
|
||||
|
||||
namespace MilliSim.Services;
|
||||
|
||||
public class AuthService : IAuthService
|
||||
{
|
||||
private readonly MilliSimDbContext _db;
|
||||
private readonly IJwtService _jwtService;
|
||||
private readonly ICaptchaService _captchaService;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
private readonly IFileStorageService _fileStorage;
|
||||
private readonly IUploadTrackingService _uploadTracking;
|
||||
|
||||
public AuthService(
|
||||
MilliSimDbContext db,
|
||||
IJwtService jwtService,
|
||||
ICaptchaService captchaService,
|
||||
ICurrentUserService currentUser,
|
||||
IFileStorageService fileStorage,
|
||||
IUploadTrackingService uploadTracking)
|
||||
{
|
||||
_db = db;
|
||||
_jwtService = jwtService;
|
||||
_captchaService = captchaService;
|
||||
_currentUser = currentUser;
|
||||
_fileStorage = fileStorage;
|
||||
_uploadTracking = uploadTracking;
|
||||
}
|
||||
|
||||
public Task<ApiResponse<CaptchaResult>> GetCaptchaAsync()
|
||||
{
|
||||
var (captchaId, svg) = _captchaService.GenerateCaptcha();
|
||||
var result = new CaptchaResult { CaptchaId = captchaId, Svg = svg };
|
||||
return Task.FromResult(ApiResponse<CaptchaResult>.Success(result));
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<LoginResponse>> LoginAsync(LoginRequest request)
|
||||
{
|
||||
// 验证码校验
|
||||
if (!_captchaService.ValidateCaptcha(request.CaptchaId, request.CaptchaCode))
|
||||
{
|
||||
return ApiResponse<LoginResponse>.Fail("验证码错误或已失效", 400);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrWhiteSpace(request.Password))
|
||||
{
|
||||
return ApiResponse<LoginResponse>.Fail("用户名或密码不能为空", 400);
|
||||
}
|
||||
|
||||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Username == request.Username);
|
||||
if (user == null)
|
||||
{
|
||||
return ApiResponse<LoginResponse>.Fail("用户名或密码错误", 400);
|
||||
}
|
||||
|
||||
if (!BCryptHelper.VerifyPassword(request.Password, user.Password))
|
||||
{
|
||||
return ApiResponse<LoginResponse>.Fail("用户名或密码错误", 400);
|
||||
}
|
||||
|
||||
if (user.Status == UserStatus.Disabled)
|
||||
{
|
||||
return ApiResponse<LoginResponse>.Fail("账号已被禁用,请联系管理员", 403);
|
||||
}
|
||||
|
||||
var token = _jwtService.GenerateToken(user);
|
||||
|
||||
var response = new LoginResponse
|
||||
{
|
||||
Token = token,
|
||||
UserId = user.Id,
|
||||
Username = user.Username,
|
||||
Nickname = user.Nickname,
|
||||
Avatar = string.IsNullOrEmpty(user.Avatar) ? "" : _fileStorage.GetSignedUrl(user.Avatar),
|
||||
Role = user.Role,
|
||||
RoleName = user.Role.ToString()
|
||||
};
|
||||
|
||||
return ApiResponse<LoginResponse>.Success(response);
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> ChangePasswordAsync(ChangePasswordRequest request)
|
||||
{
|
||||
var userId = _currentUser.UserId;
|
||||
if (userId == 0)
|
||||
{
|
||||
return ApiResponse.Fail("用户未登录", 401);
|
||||
}
|
||||
|
||||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == userId);
|
||||
if (user == null)
|
||||
{
|
||||
return ApiResponse.Fail("用户不存在", 404);
|
||||
}
|
||||
|
||||
if (!BCryptHelper.VerifyPassword(request.OldPassword, user.Password))
|
||||
{
|
||||
return ApiResponse.Fail("原密码错误", 400);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.NewPassword))
|
||||
{
|
||||
return ApiResponse.Fail("新密码不能为空", 400);
|
||||
}
|
||||
|
||||
if (request.NewPassword == request.OldPassword)
|
||||
{
|
||||
return ApiResponse.Fail("新密码不能与原密码相同", 400);
|
||||
}
|
||||
|
||||
var strengthError = ValidatePasswordStrength(request.NewPassword);
|
||||
if (strengthError != null)
|
||||
{
|
||||
return ApiResponse.Fail(strengthError, 400);
|
||||
}
|
||||
|
||||
user.Password = BCryptHelper.HashPassword(request.NewPassword);
|
||||
user.UpdatedAt = DateTime.Now;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return ApiResponse.Success("密码修改成功");
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<UserDto>> GetProfileAsync()
|
||||
{
|
||||
var userId = _currentUser.UserId;
|
||||
if (userId == 0)
|
||||
{
|
||||
return ApiResponse<UserDto>.Fail("用户未登录", 401);
|
||||
}
|
||||
|
||||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == userId);
|
||||
if (user == null)
|
||||
{
|
||||
return ApiResponse<UserDto>.Fail("用户不存在", 404);
|
||||
}
|
||||
|
||||
return ApiResponse<UserDto>.Success(MapToDto(user, _fileStorage));
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<UserDto>> UpdateProfileAsync(UpdateProfileRequest request)
|
||||
{
|
||||
var userId = _currentUser.UserId;
|
||||
if (userId == 0)
|
||||
{
|
||||
return ApiResponse<UserDto>.Fail("用户未登录", 401);
|
||||
}
|
||||
|
||||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == userId);
|
||||
if (user == null)
|
||||
{
|
||||
return ApiResponse<UserDto>.Fail("用户不存在", 404);
|
||||
}
|
||||
|
||||
// 记录旧头像 key(用于替换时删除旧文件)
|
||||
var oldAvatarKey = _fileStorage.ExtractKey(user.Avatar);
|
||||
var newAvatarKey = oldAvatarKey;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Nickname))
|
||||
{
|
||||
user.Nickname = request.Nickname;
|
||||
}
|
||||
if (request.Avatar != null)
|
||||
{
|
||||
// 存储 COS 对象键而非签名URL,避免 URL 过期后无法刷新
|
||||
user.Avatar = _fileStorage.ExtractKey(request.Avatar);
|
||||
newAvatarKey = _fileStorage.ExtractKey(request.Avatar);
|
||||
}
|
||||
if (request.Phone != null)
|
||||
{
|
||||
user.Phone = request.Phone;
|
||||
}
|
||||
if (request.Email != null)
|
||||
{
|
||||
user.Email = request.Email;
|
||||
}
|
||||
user.UpdatedAt = DateTime.Now;
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// 头像替换:删除旧文件
|
||||
if (!string.IsNullOrEmpty(oldAvatarKey) && oldAvatarKey != newAvatarKey)
|
||||
{
|
||||
await _fileStorage.DeleteAsync(oldAvatarKey);
|
||||
}
|
||||
// 消费新上传的头像
|
||||
if (request.Avatar != null)
|
||||
{
|
||||
await _uploadTracking.ConsumeAsync(newAvatarKey);
|
||||
}
|
||||
|
||||
return ApiResponse<UserDto>.Success(MapToDto(user, _fileStorage));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 密码强度校验:8位以上含字母和数字
|
||||
/// </summary>
|
||||
internal static string? ValidatePasswordStrength(string password)
|
||||
{
|
||||
if (password.Length < 8)
|
||||
{
|
||||
return "密码长度不能少于8位";
|
||||
}
|
||||
if (!password.Any(char.IsLetter))
|
||||
{
|
||||
return "密码必须包含字母";
|
||||
}
|
||||
if (!password.Any(char.IsDigit))
|
||||
{
|
||||
return "密码必须包含数字";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static UserDto MapToDto(User user, IFileStorageService fileStorage)
|
||||
{
|
||||
return new UserDto
|
||||
{
|
||||
Id = user.Id,
|
||||
Username = user.Username,
|
||||
Nickname = user.Nickname,
|
||||
// 头像存储为 COS 对象键,读取时刷新为签名URL
|
||||
Avatar = string.IsNullOrEmpty(user.Avatar) ? "" : fileStorage.GetSignedUrl(user.Avatar),
|
||||
Phone = user.Phone,
|
||||
Email = user.Email,
|
||||
Role = user.Role,
|
||||
RoleName = user.Role.ToString(),
|
||||
Status = user.Status,
|
||||
CreatedBy = user.CreatedBy,
|
||||
CreatedAt = user.CreatedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MilliSim.Common.Models;
|
||||
using MilliSim.Data;
|
||||
|
||||
namespace MilliSim.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 后台定时任务:每天检查即将到期(7 天内)的授权,给被授权用户发送提醒通知。
|
||||
/// 使用 EventId = "auth-expiring-{authId}-{expiryDate}" 进行幂等,避免重复发送。
|
||||
/// </summary>
|
||||
public class AuthorizationNotificationJob : IHostedService, IDisposable
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private Timer? _timer;
|
||||
|
||||
// 检查间隔:6 小时(既能在到期日及时触发,又避免频繁扫描)
|
||||
private static readonly TimeSpan CheckInterval = TimeSpan.FromHours(6);
|
||||
// 提前提醒天数
|
||||
private const int ExpiringSoonDays = 7;
|
||||
|
||||
public AuthorizationNotificationJob(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// 启动后 30 秒执行第一次,之后每 6 小时一次
|
||||
_timer = new Timer(DoWork, null, TimeSpan.FromSeconds(30), CheckInterval);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_timer?.Change(Timeout.Infinite, 0);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_timer?.Dispose();
|
||||
}
|
||||
|
||||
private async void DoWork(object? state)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<MilliSimDbContext>();
|
||||
var notification = scope.ServiceProvider.GetRequiredService<INotificationService>();
|
||||
|
||||
var now = DateTime.Now;
|
||||
var deadline = now.AddDays(ExpiringSoonDays);
|
||||
|
||||
// 查询 7 天内即将到期、且尚未过期的有效授权
|
||||
var expiringSoon = await db.Authorizations
|
||||
.Where(a => a.Status == AuthorizationStatus.Active
|
||||
&& a.EndTime > now
|
||||
&& a.EndTime <= deadline)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var auth in expiringSoon)
|
||||
{
|
||||
var daysRemaining = Math.Max(1, (int)Math.Ceiling((auth.EndTime - now).TotalDays));
|
||||
// 按到期日期去重:同一天内只发一次
|
||||
var eventId = $"auth-expiring-{auth.Id}-{auth.EndTime:yyyyMMdd}";
|
||||
try
|
||||
{
|
||||
await notification.CreateNotificationAsync(
|
||||
NotificationType.Authorization,
|
||||
"授权即将到期",
|
||||
$"您的客户级授权将在 {daysRemaining} 天后到期({auth.EndTime:yyyy-MM-dd}),请及时联系销售续期。",
|
||||
userId: auth.UserId,
|
||||
eventId: eventId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 单条失败不影响其他通知
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 后台任务异常不应影响应用运行
|
||||
}
|
||||
}
|
||||
}
|
||||
609
backend/MilliSim.Api/Services/AuthorizationService.cs
Normal file
609
backend/MilliSim.Api/Services/AuthorizationService.cs
Normal file
@ -0,0 +1,609 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Common.Entities;
|
||||
using MilliSim.Common.Models;
|
||||
using MilliSim.Data;
|
||||
using MilliSim.Services.Infrastructure;
|
||||
|
||||
namespace MilliSim.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 授权服务(客户级授权:对客户授权后,该客户能运行所有项目)
|
||||
/// </summary>
|
||||
public class AuthorizationService : IAuthorizationService
|
||||
{
|
||||
private readonly MilliSimDbContext _db;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
private readonly IOperationLogService _operationLog;
|
||||
private readonly INotificationService _notification;
|
||||
|
||||
public AuthorizationService(
|
||||
MilliSimDbContext db,
|
||||
ICurrentUserService currentUser,
|
||||
IOperationLogService operationLog,
|
||||
INotificationService notification)
|
||||
{
|
||||
_db = db;
|
||||
_currentUser = currentUser;
|
||||
_operationLog = operationLog;
|
||||
_notification = notification;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取授权列表(销售只能查看自己创建的客户的授权)
|
||||
/// </summary>
|
||||
public async Task<ApiResponse<PagedResult<AuthorizationDto>>> GetAuthorizationsAsync(AuthorizationQueryRequest request)
|
||||
{
|
||||
// 自动将过期授权标记为 Expired
|
||||
await MarkExpiredAsync();
|
||||
|
||||
var query = from a in _db.Authorizations
|
||||
join u in _db.Users on a.UserId equals u.Id into ug
|
||||
from u in ug.DefaultIfEmpty()
|
||||
join au in _db.Users on a.AuthorizedBy equals au.Id into aug
|
||||
from au in aug.DefaultIfEmpty()
|
||||
select new { a, u, au };
|
||||
|
||||
// 销售数据隔离:销售只能查看自己创建的客户的授权
|
||||
if (_currentUser.Role == UserRole.Sales)
|
||||
{
|
||||
query = query.Where(x => x.u.CreatedBy == _currentUser.UserId);
|
||||
}
|
||||
|
||||
if (request.UserId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.a.UserId == request.UserId.Value);
|
||||
}
|
||||
|
||||
if (request.Status.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.a.Status == request.Status.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Keyword))
|
||||
{
|
||||
var keyword = request.Keyword.Trim();
|
||||
query = query.Where(x =>
|
||||
(x.u != null && x.u.Username.Contains(keyword)) ||
|
||||
(x.u != null && x.u.Nickname.Contains(keyword)));
|
||||
}
|
||||
|
||||
var total = await query.LongCountAsync();
|
||||
|
||||
var items = await query
|
||||
.OrderByDescending(x => x.a.CreatedAt)
|
||||
.Skip((request.PageIndex - 1) * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
.Select(x => new AuthorizationDto
|
||||
{
|
||||
Id = x.a.Id,
|
||||
UserId = x.a.UserId,
|
||||
Username = x.u != null ? x.u.Username : string.Empty,
|
||||
UserNickname = x.u != null ? x.u.Nickname : string.Empty,
|
||||
AuthorizedBy = x.a.AuthorizedBy,
|
||||
AuthorizedByName = x.au != null ? x.au.Nickname : string.Empty,
|
||||
StartTime = x.a.StartTime,
|
||||
EndTime = x.a.EndTime,
|
||||
Status = x.a.Status,
|
||||
StatusName = x.a.Status.ToString(),
|
||||
CreatedAt = x.a.CreatedAt
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return ApiResponse<PagedResult<AuthorizationDto>>.Success(
|
||||
PagedResult<AuthorizationDto>.Create(items, total, request.PageIndex, request.PageSize));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取授权详情
|
||||
/// </summary>
|
||||
public async Task<ApiResponse<AuthorizationDto>> GetAuthorizationByIdAsync(long id)
|
||||
{
|
||||
await MarkExpiredAsync();
|
||||
|
||||
var entity = await _db.Authorizations
|
||||
.Include(a => a.User)
|
||||
.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
||||
if (entity == null)
|
||||
{
|
||||
return ApiResponse<AuthorizationDto>.Fail("授权不存在", 404);
|
||||
}
|
||||
|
||||
// 销售数据隔离
|
||||
if (_currentUser.Role == UserRole.Sales)
|
||||
{
|
||||
var customer = await _db.Users.FirstOrDefaultAsync(u => u.Id == entity.UserId);
|
||||
if (customer == null || customer.CreatedBy != _currentUser.UserId)
|
||||
{
|
||||
return ApiResponse<AuthorizationDto>.Fail("无权查看该授权", 403);
|
||||
}
|
||||
}
|
||||
|
||||
var authorizer = await _db.Users.FirstOrDefaultAsync(u => u.Id == entity.AuthorizedBy);
|
||||
|
||||
var dto = MapToDto(entity, entity.User, authorizer);
|
||||
return ApiResponse<AuthorizationDto>.Success(dto);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建/重新授权(客户级授权):一个客户只能有一个授权记录。
|
||||
/// 若客户已有授权(任意状态):Active 则续期(EndTime += days),Cancelled/Expired 则重置为 Active 重新开通。
|
||||
/// 若客户无授权:创建新记录。
|
||||
/// </summary>
|
||||
public async Task<ApiResponse<AuthorizationDto>> CreateAuthorizationAsync(CreateAuthorizationRequest request)
|
||||
{
|
||||
if (request.Days <= 0)
|
||||
{
|
||||
request.Days = 365;
|
||||
}
|
||||
|
||||
// 校验用户存在
|
||||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == request.UserId);
|
||||
if (user == null)
|
||||
{
|
||||
return ApiResponse<AuthorizationDto>.Fail("用户不存在");
|
||||
}
|
||||
|
||||
// 销售数据隔离:销售只能为自己创建的客户授权
|
||||
if (_currentUser.Role == UserRole.Sales && user.CreatedBy != _currentUser.UserId)
|
||||
{
|
||||
return ApiResponse<AuthorizationDto>.Fail("无权为该用户授权", 403);
|
||||
}
|
||||
|
||||
await MarkExpiredAsync();
|
||||
|
||||
// 一个客户只能有一个授权记录:查询任意状态的现有授权
|
||||
var existing = await _db.Authorizations
|
||||
.FirstOrDefaultAsync(a => a.UserId == request.UserId);
|
||||
|
||||
var now = DateTime.Now;
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
// 捕获原始状态,用于区分续期/重新授权的通知文案
|
||||
var wasActive = existing.Status == AuthorizationStatus.Active;
|
||||
|
||||
if (wasActive)
|
||||
{
|
||||
// 续期:在原 EndTime 基础上加天数(如果已过期则从当前时间开始)
|
||||
var baseTime = existing.EndTime > now ? existing.EndTime : now;
|
||||
existing.EndTime = baseTime.AddDays(request.Days);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 重新授权:重置起止时间和状态(Cancelled / Expired → Active)
|
||||
existing.StartTime = now;
|
||||
existing.EndTime = now.AddDays(request.Days);
|
||||
existing.Status = AuthorizationStatus.Active;
|
||||
existing.ProjectId = null; // 客户级授权
|
||||
}
|
||||
existing.AuthorizedBy = _currentUser.UserId;
|
||||
existing.UpdatedAt = now;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
var actionLabel = wasActive ? "续期" : "重新授权";
|
||||
await _operationLog.LogAsync("授权", actionLabel,
|
||||
$"用户 {user.Username} {actionLabel} {request.Days} 天,授权 ID: {existing.Id}");
|
||||
|
||||
// 通知被授权用户
|
||||
try
|
||||
{
|
||||
var title = wasActive ? "授权已续期" : "授权已重新开通";
|
||||
var content = wasActive
|
||||
? $"您的客户级授权已续期 {request.Days} 天,新到期时间:{existing.EndTime:yyyy-MM-dd}。"
|
||||
: $"您的客户级授权已重新开通 {request.Days} 天,到期时间:{existing.EndTime:yyyy-MM-dd},可体验全部项目。";
|
||||
await _notification.CreateNotificationAsync(
|
||||
NotificationType.Authorization,
|
||||
title,
|
||||
content,
|
||||
userId: user.Id,
|
||||
eventId: $"{(wasActive ? "auth-renewed" : "auth-reactivated")}-{existing.Id}-{existing.UpdatedAt:yyyyMMddHHmmss}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 通知发送失败不影响业务流程
|
||||
}
|
||||
|
||||
var authorizer = await _db.Users.FirstOrDefaultAsync(u => u.Id == existing.AuthorizedBy);
|
||||
var msg = wasActive ? "授权续期成功" : "授权重新开通成功";
|
||||
return ApiResponse<AuthorizationDto>.Success(MapToDto(existing, user, authorizer), msg);
|
||||
}
|
||||
|
||||
// 创建新授权(客户级授权,不关联项目)
|
||||
var entity = new Authorization
|
||||
{
|
||||
UserId = request.UserId,
|
||||
AuthorizedBy = _currentUser.UserId,
|
||||
StartTime = now,
|
||||
EndTime = now.AddDays(request.Days),
|
||||
Status = AuthorizationStatus.Active,
|
||||
ProjectId = null
|
||||
};
|
||||
|
||||
_db.Authorizations.Add(entity);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
await _operationLog.LogAsync("授权", "创建",
|
||||
$"为用户 {user.Username} 创建客户级授权,{request.Days} 天,授权 ID: {entity.Id}");
|
||||
|
||||
// 通知被授权用户:授权开通
|
||||
try
|
||||
{
|
||||
await _notification.CreateNotificationAsync(
|
||||
NotificationType.Authorization,
|
||||
"授权已开通",
|
||||
$"您已获得 {request.Days} 天客户级授权,可体验全部项目,到期时间:{entity.EndTime:yyyy-MM-dd}。",
|
||||
userId: user.Id,
|
||||
eventId: $"auth-created-{entity.Id}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 通知发送失败不影响业务流程
|
||||
}
|
||||
|
||||
var currentUser = await _db.Users.FirstOrDefaultAsync(u => u.Id == _currentUser.UserId);
|
||||
return ApiResponse<AuthorizationDto>.Success(MapToDto(entity, user, currentUser));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量授权:一个客户一个授权记录,对每个 UserId 调用复用/新建逻辑。
|
||||
/// Active 则续期,Cancelled/Expired 则重置为 Active 重新开通,无授权则新建。
|
||||
/// </summary>
|
||||
public async Task<ApiResponse> BatchAuthorizeAsync(BatchAuthorizationRequest request)
|
||||
{
|
||||
if (request.UserIds == null || request.UserIds.Count == 0)
|
||||
{
|
||||
return ApiResponse.Fail("用户列表不能为空");
|
||||
}
|
||||
|
||||
if (request.Days <= 0)
|
||||
{
|
||||
request.Days = 365;
|
||||
}
|
||||
|
||||
await MarkExpiredAsync();
|
||||
|
||||
var now = DateTime.Now;
|
||||
var successCount = 0;
|
||||
var skipCount = 0;
|
||||
|
||||
foreach (var userId in request.UserIds.Distinct())
|
||||
{
|
||||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == userId);
|
||||
if (user == null)
|
||||
{
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 销售数据隔离
|
||||
if (_currentUser.Role == UserRole.Sales && user.CreatedBy != _currentUser.UserId)
|
||||
{
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 一个客户一个授权记录:查询任意状态的现有授权
|
||||
var existing = await _db.Authorizations
|
||||
.FirstOrDefaultAsync(a => a.UserId == userId);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
var wasActive = existing.Status == AuthorizationStatus.Active;
|
||||
|
||||
if (wasActive)
|
||||
{
|
||||
// 续期
|
||||
var baseTime = existing.EndTime > now ? existing.EndTime : now;
|
||||
existing.EndTime = baseTime.AddDays(request.Days);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 重新授权(Cancelled / Expired → Active)
|
||||
existing.StartTime = now;
|
||||
existing.EndTime = now.AddDays(request.Days);
|
||||
existing.Status = AuthorizationStatus.Active;
|
||||
existing.ProjectId = null;
|
||||
}
|
||||
existing.AuthorizedBy = _currentUser.UserId;
|
||||
existing.UpdatedAt = now;
|
||||
successCount++;
|
||||
|
||||
// 通知用户
|
||||
try
|
||||
{
|
||||
var title = wasActive ? "授权已续期" : "授权已重新开通";
|
||||
var content = wasActive
|
||||
? $"您的客户级授权已续期 {request.Days} 天,新到期时间:{existing.EndTime:yyyy-MM-dd}。"
|
||||
: $"您的客户级授权已重新开通 {request.Days} 天,到期时间:{existing.EndTime:yyyy-MM-dd},可体验全部项目。";
|
||||
await _notification.CreateNotificationAsync(
|
||||
NotificationType.Authorization,
|
||||
title,
|
||||
content,
|
||||
userId: user.Id,
|
||||
eventId: $"{(wasActive ? "auth-renewed" : "auth-reactivated")}-{existing.Id}-{existing.UpdatedAt:yyyyMMddHHmmss}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 通知失败不影响业务
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 创建新授权(客户级授权,不关联项目)
|
||||
var entity = new Authorization
|
||||
{
|
||||
UserId = userId,
|
||||
AuthorizedBy = _currentUser.UserId,
|
||||
StartTime = now,
|
||||
EndTime = now.AddDays(request.Days),
|
||||
Status = AuthorizationStatus.Active,
|
||||
ProjectId = null
|
||||
};
|
||||
_db.Authorizations.Add(entity);
|
||||
successCount++;
|
||||
|
||||
// 先保存以获取 entity.Id
|
||||
try
|
||||
{
|
||||
await _db.SaveChangesAsync();
|
||||
await _notification.CreateNotificationAsync(
|
||||
NotificationType.Authorization,
|
||||
"授权已开通",
|
||||
$"您已获得 {request.Days} 天客户级授权,可体验全部项目,到期时间:{entity.EndTime:yyyy-MM-dd}。",
|
||||
userId: user.Id,
|
||||
eventId: $"auth-created-{entity.Id}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 通知失败不影响业务
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
await _operationLog.LogAsync("授权", "批量授权",
|
||||
$"批量授权:成功 {successCount} 条,跳过 {skipCount} 条,{request.Days} 天");
|
||||
|
||||
return ApiResponse.Success($"批量授权完成,成功 {successCount} 条,跳过 {skipCount} 条");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 续期授权(按授权 ID 续期)
|
||||
/// </summary>
|
||||
public async Task<ApiResponse<AuthorizationDto>> RenewAuthorizationAsync(long id, int days)
|
||||
{
|
||||
if (days <= 0)
|
||||
{
|
||||
return ApiResponse<AuthorizationDto>.Fail("续期天数必须大于 0");
|
||||
}
|
||||
|
||||
await MarkExpiredAsync();
|
||||
|
||||
var entity = await _db.Authorizations
|
||||
.Include(a => a.User)
|
||||
.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
||||
if (entity == null)
|
||||
{
|
||||
return ApiResponse<AuthorizationDto>.Fail("授权不存在", 404);
|
||||
}
|
||||
|
||||
// 销售数据隔离
|
||||
if (_currentUser.Role == UserRole.Sales)
|
||||
{
|
||||
var customer = await _db.Users.FirstOrDefaultAsync(u => u.Id == entity.UserId);
|
||||
if (customer == null || customer.CreatedBy != _currentUser.UserId)
|
||||
{
|
||||
return ApiResponse<AuthorizationDto>.Fail("无权操作该授权", 403);
|
||||
}
|
||||
}
|
||||
|
||||
// 仅 Active 状态的授权可续期;Cancelled/Expired 请使用重新授权功能
|
||||
if (entity.Status != AuthorizationStatus.Active)
|
||||
{
|
||||
var hint = entity.Status == AuthorizationStatus.Cancelled
|
||||
? "已取消的授权请使用重新授权功能"
|
||||
: "已过期的授权请使用重新授权功能";
|
||||
return ApiResponse<AuthorizationDto>.Fail(hint);
|
||||
}
|
||||
|
||||
// 续期:在原 EndTime 基础上加天数(如果已过期则从当前时间开始)
|
||||
var baseTime = entity.EndTime > DateTime.Now ? entity.EndTime : DateTime.Now;
|
||||
entity.EndTime = baseTime.AddDays(days);
|
||||
entity.Status = AuthorizationStatus.Active;
|
||||
entity.UpdatedAt = DateTime.Now;
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
var authorizer = await _db.Users.FirstOrDefaultAsync(u => u.Id == entity.AuthorizedBy);
|
||||
|
||||
await _operationLog.LogAsync("授权", "续期",
|
||||
$"授权 ID: {id} 续期 {days} 天");
|
||||
|
||||
// 通知被授权用户:续期
|
||||
try
|
||||
{
|
||||
await _notification.CreateNotificationAsync(
|
||||
NotificationType.Authorization,
|
||||
"授权已续期",
|
||||
$"您的客户级授权已续期 {days} 天,新到期时间:{entity.EndTime:yyyy-MM-dd}。",
|
||||
userId: entity.UserId,
|
||||
eventId: $"auth-renewed-{entity.Id}-{entity.UpdatedAt:yyyyMMddHHmmss}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 通知失败不影响业务
|
||||
}
|
||||
|
||||
return ApiResponse<AuthorizationDto>.Success(MapToDto(entity, entity.User, authorizer), "续期成功");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消授权(仅 Active 状态可取消)
|
||||
/// </summary>
|
||||
public async Task<ApiResponse> CancelAuthorizationAsync(long id)
|
||||
{
|
||||
var entity = await _db.Authorizations.FirstOrDefaultAsync(a => a.Id == id);
|
||||
if (entity == null)
|
||||
{
|
||||
return ApiResponse.Fail("授权不存在", 404);
|
||||
}
|
||||
|
||||
// 销售数据隔离
|
||||
if (_currentUser.Role == UserRole.Sales)
|
||||
{
|
||||
var customer = await _db.Users.FirstOrDefaultAsync(u => u.Id == entity.UserId);
|
||||
if (customer == null || customer.CreatedBy != _currentUser.UserId)
|
||||
{
|
||||
return ApiResponse.Fail("无权操作该授权", 403);
|
||||
}
|
||||
}
|
||||
|
||||
// 仅 Active 状态可取消;已取消/已过期的不可重复取消
|
||||
if (entity.Status == AuthorizationStatus.Cancelled)
|
||||
{
|
||||
return ApiResponse.Fail("该授权已取消,无需重复操作");
|
||||
}
|
||||
if (entity.Status == AuthorizationStatus.Expired)
|
||||
{
|
||||
return ApiResponse.Fail("该授权已过期,请使用重新授权功能");
|
||||
}
|
||||
|
||||
entity.Status = AuthorizationStatus.Cancelled;
|
||||
entity.UpdatedAt = DateTime.Now;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
await _operationLog.LogAsync("授权", "取消",
|
||||
$"取消授权 ID: {id}");
|
||||
|
||||
// 通知被授权用户:授权已取消
|
||||
try
|
||||
{
|
||||
await _notification.CreateNotificationAsync(
|
||||
NotificationType.Authorization,
|
||||
"授权已取消",
|
||||
"您的客户级授权已被取消。如有疑问请联系销售或客服。",
|
||||
userId: entity.UserId,
|
||||
eventId: $"auth-cancelled-{entity.Id}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 通知失败不影响业务
|
||||
}
|
||||
|
||||
return ApiResponse.Success("授权已取消");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查用户是否有任何有效授权(客户级授权,不检查项目)
|
||||
/// </summary>
|
||||
public async Task<ApiResponse<bool>> CheckAuthorizationAsync(long userId)
|
||||
{
|
||||
await MarkExpiredAsync();
|
||||
|
||||
var hasAuth = await _db.Authorizations
|
||||
.AnyAsync(a => a.UserId == userId
|
||||
&& a.Status == AuthorizationStatus.Active
|
||||
&& a.EndTime > DateTime.Now);
|
||||
|
||||
return ApiResponse<bool>.Success(hasAuth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前客户的授权列表
|
||||
/// </summary>
|
||||
public async Task<ApiResponse<List<AuthorizationDto>>> GetMyAuthorizationsAsync()
|
||||
{
|
||||
await MarkExpiredAsync();
|
||||
|
||||
var userId = _currentUser.UserId;
|
||||
|
||||
var query = from a in _db.Authorizations
|
||||
join u in _db.Users on a.UserId equals u.Id into ug
|
||||
from u in ug.DefaultIfEmpty()
|
||||
join au in _db.Users on a.AuthorizedBy equals au.Id into aug
|
||||
from au in aug.DefaultIfEmpty()
|
||||
where a.UserId == userId
|
||||
select new { a, u, au };
|
||||
|
||||
var items = await query
|
||||
.OrderByDescending(x => x.a.CreatedAt)
|
||||
.Select(x => new AuthorizationDto
|
||||
{
|
||||
Id = x.a.Id,
|
||||
UserId = x.a.UserId,
|
||||
Username = x.u != null ? x.u.Username : string.Empty,
|
||||
UserNickname = x.u != null ? x.u.Nickname : string.Empty,
|
||||
AuthorizedBy = x.a.AuthorizedBy,
|
||||
AuthorizedByName = x.au != null ? x.au.Nickname : string.Empty,
|
||||
StartTime = x.a.StartTime,
|
||||
EndTime = x.a.EndTime,
|
||||
Status = x.a.Status,
|
||||
StatusName = x.a.Status.ToString(),
|
||||
CreatedAt = x.a.CreatedAt
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return ApiResponse<List<AuthorizationDto>>.Success(items);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自动将过期授权标记为 Expired(EndTime < Now 且 Status == Active),并通知用户
|
||||
/// </summary>
|
||||
private async Task MarkExpiredAsync()
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var expired = await _db.Authorizations
|
||||
.Where(a => a.Status == AuthorizationStatus.Active && a.EndTime < now)
|
||||
.ToListAsync();
|
||||
|
||||
if (expired.Count > 0)
|
||||
{
|
||||
foreach (var item in expired)
|
||||
{
|
||||
item.Status = AuthorizationStatus.Expired;
|
||||
item.UpdatedAt = now;
|
||||
|
||||
// 通知用户:授权已过期
|
||||
try
|
||||
{
|
||||
await _notification.CreateNotificationAsync(
|
||||
NotificationType.Authorization,
|
||||
"授权已过期",
|
||||
"您的客户级授权已过期,请联系销售续期后继续体验项目。",
|
||||
userId: item.UserId,
|
||||
eventId: $"auth-expired-{item.Id}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 通知失败不影响业务
|
||||
}
|
||||
}
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 映射为 DTO(不再映射项目字段)
|
||||
/// </summary>
|
||||
private static AuthorizationDto MapToDto(Authorization entity, User? user, User? authorizer)
|
||||
{
|
||||
return new AuthorizationDto
|
||||
{
|
||||
Id = entity.Id,
|
||||
UserId = entity.UserId,
|
||||
Username = user?.Username ?? string.Empty,
|
||||
UserNickname = user?.Nickname ?? string.Empty,
|
||||
AuthorizedBy = entity.AuthorizedBy,
|
||||
AuthorizedByName = authorizer?.Nickname ?? string.Empty,
|
||||
StartTime = entity.StartTime,
|
||||
EndTime = entity.EndTime,
|
||||
Status = entity.Status,
|
||||
StatusName = entity.Status.ToString(),
|
||||
CreatedAt = entity.CreatedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
439
backend/MilliSim.Api/Services/ContentService.cs
Normal file
439
backend/MilliSim.Api/Services/ContentService.cs
Normal file
@ -0,0 +1,439 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Common.Entities;
|
||||
using MilliSim.Common.Models;
|
||||
using MilliSim.Data;
|
||||
using MilliSim.Services.Infrastructure;
|
||||
|
||||
namespace MilliSim.Services;
|
||||
|
||||
public class ContentService : IContentService
|
||||
{
|
||||
private readonly MilliSimDbContext _db;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
private readonly IOperationLogService _operationLog;
|
||||
private readonly IFileStorageService _fileStorage;
|
||||
private readonly INotificationService _notification;
|
||||
private readonly IUploadTrackingService _uploadTracking;
|
||||
|
||||
public ContentService(
|
||||
MilliSimDbContext db,
|
||||
ICurrentUserService currentUser,
|
||||
IOperationLogService operationLog,
|
||||
IFileStorageService fileStorage,
|
||||
INotificationService notification,
|
||||
IUploadTrackingService uploadTracking)
|
||||
{
|
||||
_db = db;
|
||||
_currentUser = currentUser;
|
||||
_operationLog = operationLog;
|
||||
_fileStorage = fileStorage;
|
||||
_notification = notification;
|
||||
_uploadTracking = uploadTracking;
|
||||
}
|
||||
|
||||
// ===== 轮播图 =====
|
||||
|
||||
public async Task<ApiResponse<List<BannerDto>>> GetBannersAsync(bool onlyActive = false)
|
||||
{
|
||||
var query = _db.Banners.AsQueryable();
|
||||
|
||||
if (onlyActive)
|
||||
{
|
||||
query = query.Where(b => b.Status == UserStatus.Active);
|
||||
}
|
||||
|
||||
// 先查询实体,再在内存中转换为DTO(因为需要调用GetSignedUrl生成签名URL)
|
||||
var entities = await query
|
||||
.OrderBy(b => b.SortOrder)
|
||||
.ThenByDescending(b => b.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
// 返回时将对象键转换为签名URL,确保前端能访问COS私有文件
|
||||
var items = entities.Select(b => new BannerDto
|
||||
{
|
||||
Id = b.Id,
|
||||
Title = b.Title,
|
||||
Image = _fileStorage.GetSignedUrl(b.Image),
|
||||
Link = b.Link,
|
||||
SortOrder = b.SortOrder,
|
||||
Status = b.Status
|
||||
}).ToList();
|
||||
|
||||
return ApiResponse<List<BannerDto>>.Success(items);
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<BannerDto>> CreateBannerAsync(CreateBannerRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Image))
|
||||
{
|
||||
return ApiResponse<BannerDto>.Fail("图片不能为空");
|
||||
}
|
||||
|
||||
var entity = new Banner
|
||||
{
|
||||
Title = request.Title,
|
||||
// 保存时将签名URL转换为对象键存储,避免签名URL过期导致数据丢失
|
||||
Image = _fileStorage.ExtractKey(request.Image),
|
||||
Link = request.Link,
|
||||
SortOrder = request.SortOrder,
|
||||
Status = request.Status
|
||||
};
|
||||
|
||||
_db.Banners.Add(entity);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// 消费上传跟踪
|
||||
await _uploadTracking.ConsumeAsync(entity.Image);
|
||||
|
||||
await _operationLog.LogAsync("内容-轮播图", "创建",
|
||||
$"创建轮播图:{request.Title},ID: {entity.Id}");
|
||||
|
||||
return ApiResponse<BannerDto>.Success(MapToDto(entity));
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<BannerDto>> UpdateBannerAsync(long id, CreateBannerRequest request)
|
||||
{
|
||||
var entity = await _db.Banners.FirstOrDefaultAsync(b => b.Id == id);
|
||||
if (entity == null)
|
||||
{
|
||||
return ApiResponse<BannerDto>.Fail("轮播图不存在", 404);
|
||||
}
|
||||
|
||||
var oldImageKey = entity.Image;
|
||||
var newImageKey = _fileStorage.ExtractKey(request.Image);
|
||||
|
||||
entity.Title = request.Title;
|
||||
// 保存时将签名URL转换为对象键存储,避免签名URL过期导致数据丢失
|
||||
entity.Image = newImageKey;
|
||||
entity.Link = request.Link;
|
||||
entity.SortOrder = request.SortOrder;
|
||||
entity.Status = request.Status;
|
||||
entity.UpdatedAt = DateTime.Now;
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// 图片替换:删除旧文件
|
||||
if (!string.IsNullOrEmpty(oldImageKey) && oldImageKey != newImageKey)
|
||||
{
|
||||
await _fileStorage.DeleteAsync(oldImageKey);
|
||||
}
|
||||
// 消费新上传的文件
|
||||
await _uploadTracking.ConsumeAsync(newImageKey);
|
||||
|
||||
await _operationLog.LogAsync("内容-轮播图", "更新",
|
||||
$"更新轮播图 ID: {id}");
|
||||
|
||||
return ApiResponse<BannerDto>.Success(MapToDto(entity));
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> DeleteBannerAsync(long id)
|
||||
{
|
||||
var entity = await _db.Banners.FirstOrDefaultAsync(b => b.Id == id);
|
||||
if (entity == null)
|
||||
{
|
||||
return ApiResponse.Fail("轮播图不存在", 404);
|
||||
}
|
||||
|
||||
entity.IsDeleted = true;
|
||||
entity.UpdatedAt = DateTime.Now;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
await _operationLog.LogAsync("内容-轮播图", "删除",
|
||||
$"删除轮播图 ID: {id}");
|
||||
|
||||
return ApiResponse.Success("删除成功");
|
||||
}
|
||||
|
||||
// ===== 合作伙伴 =====
|
||||
|
||||
public async Task<ApiResponse<List<PartnerDto>>> GetPartnersAsync(bool onlyVisible = false)
|
||||
{
|
||||
var query = _db.Partners.AsQueryable();
|
||||
|
||||
if (onlyVisible)
|
||||
{
|
||||
query = query.Where(p => p.IsVisible);
|
||||
}
|
||||
|
||||
// 先查询实体,再在内存中转换为DTO(因为需要调用GetSignedUrl生成签名URL)
|
||||
var entities = await query
|
||||
.OrderBy(p => p.SortOrder)
|
||||
.ThenByDescending(p => p.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
// 返回时将对象键转换为签名URL
|
||||
var items = entities.Select(p => new PartnerDto
|
||||
{
|
||||
Id = p.Id,
|
||||
Name = p.Name,
|
||||
Logo = _fileStorage.GetSignedUrl(p.Logo),
|
||||
Link = p.Link,
|
||||
SortOrder = p.SortOrder,
|
||||
IsVisible = p.IsVisible
|
||||
}).ToList();
|
||||
|
||||
return ApiResponse<List<PartnerDto>>.Success(items);
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<PartnerDto>> CreatePartnerAsync(CreatePartnerRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
return ApiResponse<PartnerDto>.Fail("名称不能为空");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Logo))
|
||||
{
|
||||
return ApiResponse<PartnerDto>.Fail("Logo 不能为空");
|
||||
}
|
||||
|
||||
var entity = new Partner
|
||||
{
|
||||
Name = request.Name,
|
||||
// 保存时将签名URL转换为对象键存储
|
||||
Logo = _fileStorage.ExtractKey(request.Logo),
|
||||
Link = request.Link,
|
||||
SortOrder = request.SortOrder,
|
||||
IsVisible = request.IsVisible
|
||||
};
|
||||
|
||||
_db.Partners.Add(entity);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// 消费上传跟踪
|
||||
await _uploadTracking.ConsumeAsync(entity.Logo);
|
||||
|
||||
await _operationLog.LogAsync("内容-合作伙伴", "创建",
|
||||
$"创建合作伙伴:{request.Name},ID: {entity.Id}");
|
||||
|
||||
return ApiResponse<PartnerDto>.Success(MapToDto(entity));
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<PartnerDto>> UpdatePartnerAsync(long id, CreatePartnerRequest request)
|
||||
{
|
||||
var entity = await _db.Partners.FirstOrDefaultAsync(p => p.Id == id);
|
||||
if (entity == null)
|
||||
{
|
||||
return ApiResponse<PartnerDto>.Fail("合作伙伴不存在", 404);
|
||||
}
|
||||
|
||||
var oldLogoKey = entity.Logo;
|
||||
var newLogoKey = _fileStorage.ExtractKey(request.Logo);
|
||||
|
||||
entity.Name = request.Name;
|
||||
// 保存时将签名URL转换为对象键存储
|
||||
entity.Logo = newLogoKey;
|
||||
entity.Link = request.Link;
|
||||
entity.SortOrder = request.SortOrder;
|
||||
entity.IsVisible = request.IsVisible;
|
||||
entity.UpdatedAt = DateTime.Now;
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// Logo 替换:删除旧文件
|
||||
if (!string.IsNullOrEmpty(oldLogoKey) && oldLogoKey != newLogoKey)
|
||||
{
|
||||
await _fileStorage.DeleteAsync(oldLogoKey);
|
||||
}
|
||||
// 消费新上传的文件
|
||||
await _uploadTracking.ConsumeAsync(newLogoKey);
|
||||
|
||||
await _operationLog.LogAsync("内容-合作伙伴", "更新",
|
||||
$"更新合作伙伴 ID: {id}");
|
||||
|
||||
return ApiResponse<PartnerDto>.Success(MapToDto(entity));
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> DeletePartnerAsync(long id)
|
||||
{
|
||||
var entity = await _db.Partners.FirstOrDefaultAsync(p => p.Id == id);
|
||||
if (entity == null)
|
||||
{
|
||||
return ApiResponse.Fail("合作伙伴不存在", 404);
|
||||
}
|
||||
|
||||
entity.IsDeleted = true;
|
||||
entity.UpdatedAt = DateTime.Now;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
await _operationLog.LogAsync("内容-合作伙伴", "删除",
|
||||
$"删除合作伙伴 ID: {id}");
|
||||
|
||||
return ApiResponse.Success("删除成功");
|
||||
}
|
||||
|
||||
// ===== 咨询消息 =====
|
||||
|
||||
public async Task<ApiResponse<PagedResult<ConsultationDto>>> GetConsultationsAsync(PageRequest request, bool? isRead = null)
|
||||
{
|
||||
var query = _db.Consultations.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Keyword))
|
||||
{
|
||||
var keyword = request.Keyword.Trim();
|
||||
query = query.Where(c =>
|
||||
c.Name.Contains(keyword) ||
|
||||
c.Contact.Contains(keyword) ||
|
||||
c.Content.Contains(keyword));
|
||||
}
|
||||
|
||||
// 按已读/未读状态过滤
|
||||
if (isRead.HasValue)
|
||||
{
|
||||
query = query.Where(c => c.IsRead == isRead.Value);
|
||||
}
|
||||
|
||||
var total = await query.LongCountAsync();
|
||||
|
||||
var items = await query
|
||||
.OrderByDescending(c => c.CreatedAt)
|
||||
.Skip((request.PageIndex - 1) * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
.Select(c => new ConsultationDto
|
||||
{
|
||||
Id = c.Id,
|
||||
Name = c.Name,
|
||||
Contact = c.Contact,
|
||||
Content = c.Content,
|
||||
IsRead = c.IsRead,
|
||||
CreatedAt = c.CreatedAt
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return ApiResponse<PagedResult<ConsultationDto>>.Success(
|
||||
PagedResult<ConsultationDto>.Create(items, total, request.PageIndex, request.PageSize));
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<ConsultationDto>> CreateConsultationAsync(CreateConsultationRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
return ApiResponse<ConsultationDto>.Fail("姓名不能为空");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Contact))
|
||||
{
|
||||
return ApiResponse<ConsultationDto>.Fail("联系方式不能为空");
|
||||
}
|
||||
|
||||
var entity = new Consultation
|
||||
{
|
||||
Name = request.Name,
|
||||
Contact = request.Contact,
|
||||
Content = request.Content,
|
||||
IsRead = false
|
||||
};
|
||||
|
||||
_db.Consultations.Add(entity);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// 通知所有管理员/销售:有新咨询需要处理
|
||||
try
|
||||
{
|
||||
var staffUsers = await _db.Users
|
||||
.Where(u => u.Role == UserRole.Admin || u.Role == UserRole.Sales)
|
||||
.Select(u => u.Id)
|
||||
.ToListAsync();
|
||||
var contentPreview = string.IsNullOrWhiteSpace(request.Content)
|
||||
? "(无内容)"
|
||||
: (request.Content.Length > 50 ? request.Content[..50] + "..." : request.Content);
|
||||
foreach (var staffId in staffUsers)
|
||||
{
|
||||
await _notification.CreateNotificationAsync(
|
||||
NotificationType.System,
|
||||
"新咨询消息",
|
||||
$"{request.Name}({request.Contact})提交了一条咨询:{contentPreview}",
|
||||
userId: staffId,
|
||||
eventId: $"consultation-{entity.Id}-notify-{staffId}");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 通知发送失败不影响咨询提交结果
|
||||
}
|
||||
|
||||
return ApiResponse<ConsultationDto>.Success(MapToDto(entity), "提交成功");
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> MarkAsReadAsync(long id)
|
||||
{
|
||||
var entity = await _db.Consultations.FirstOrDefaultAsync(c => c.Id == id);
|
||||
if (entity == null)
|
||||
{
|
||||
return ApiResponse.Fail("咨询消息不存在", 404);
|
||||
}
|
||||
|
||||
if (!entity.IsRead)
|
||||
{
|
||||
entity.IsRead = true;
|
||||
entity.UpdatedAt = DateTime.Now;
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return ApiResponse.Success("已标记为已读");
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> DeleteConsultationAsync(long id)
|
||||
{
|
||||
var entity = await _db.Consultations.FirstOrDefaultAsync(c => c.Id == id);
|
||||
if (entity == null)
|
||||
{
|
||||
return ApiResponse.Fail("咨询消息不存在", 404);
|
||||
}
|
||||
|
||||
entity.IsDeleted = true;
|
||||
entity.UpdatedAt = DateTime.Now;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
await _operationLog.LogAsync("内容-咨询", "删除",
|
||||
$"删除咨询消息 ID: {id}");
|
||||
|
||||
return ApiResponse.Success("删除成功");
|
||||
}
|
||||
|
||||
// ===== 私有映射方法 =====
|
||||
|
||||
// 改为非静态方法,以便调用 _fileStorage.GetSignedUrl 生成签名URL
|
||||
private BannerDto MapToDto(Banner entity)
|
||||
{
|
||||
return new BannerDto
|
||||
{
|
||||
Id = entity.Id,
|
||||
Title = entity.Title,
|
||||
// 返回时将对象键转换为签名URL,确保前端能访问COS私有文件
|
||||
Image = _fileStorage.GetSignedUrl(entity.Image),
|
||||
Link = entity.Link,
|
||||
SortOrder = entity.SortOrder,
|
||||
Status = entity.Status
|
||||
};
|
||||
}
|
||||
|
||||
// 改为非静态方法,以便调用 _fileStorage.GetSignedUrl 生成签名URL
|
||||
private PartnerDto MapToDto(Partner entity)
|
||||
{
|
||||
return new PartnerDto
|
||||
{
|
||||
Id = entity.Id,
|
||||
Name = entity.Name,
|
||||
// 返回时将对象键转换为签名URL
|
||||
Logo = _fileStorage.GetSignedUrl(entity.Logo),
|
||||
Link = entity.Link,
|
||||
SortOrder = entity.SortOrder,
|
||||
IsVisible = entity.IsVisible
|
||||
};
|
||||
}
|
||||
|
||||
private static ConsultationDto MapToDto(Consultation entity)
|
||||
{
|
||||
return new ConsultationDto
|
||||
{
|
||||
Id = entity.Id,
|
||||
Name = entity.Name,
|
||||
Contact = entity.Contact,
|
||||
Content = entity.Content,
|
||||
IsRead = entity.IsRead,
|
||||
CreatedAt = entity.CreatedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
163
backend/MilliSim.Api/Services/DashboardService.cs
Normal file
163
backend/MilliSim.Api/Services/DashboardService.cs
Normal file
@ -0,0 +1,163 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Common.Entities;
|
||||
using MilliSim.Common.Models;
|
||||
using MilliSim.Data;
|
||||
using MilliSim.Services.Infrastructure;
|
||||
|
||||
namespace MilliSim.Services;
|
||||
|
||||
public class DashboardService : IDashboardService
|
||||
{
|
||||
private readonly MilliSimDbContext _db;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
private readonly IFileStorageService _fileStorage;
|
||||
|
||||
public DashboardService(MilliSimDbContext db, ICurrentUserService currentUser, IFileStorageService fileStorage)
|
||||
{
|
||||
_db = db;
|
||||
_currentUser = currentUser;
|
||||
_fileStorage = fileStorage;
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<DashboardData>> GetDashboardDataAsync()
|
||||
{
|
||||
var userId = _currentUser.UserId;
|
||||
|
||||
// ===== Stats =====
|
||||
var projectCount = await _db.Projects.CountAsync();
|
||||
var customerCount = await _db.Users.CountAsync(u => u.Role == UserRole.Customer);
|
||||
|
||||
var todayStart = DateTime.Now.Date;
|
||||
var todayEnd = todayStart.AddDays(1);
|
||||
var todayVisits = await _db.OperationLogs
|
||||
.CountAsync(l => l.CreatedAt >= todayStart && l.CreatedAt < todayEnd);
|
||||
|
||||
var activeAuthorizations = await _db.Authorizations
|
||||
.CountAsync(a => a.Status == AuthorizationStatus.Active);
|
||||
|
||||
var unreadMessages = await _db.Notifications
|
||||
.Where(n => n.UserId == null || n.UserId == userId)
|
||||
.Where(n => !_db.NotificationReads.Any(r => r.NotificationId == n.Id && r.UserId == userId))
|
||||
.CountAsync();
|
||||
|
||||
// ===== Trends: 近 7 天 =====
|
||||
var startDate = DateTime.Now.Date.AddDays(-6);
|
||||
var endDate = DateTime.Now.Date.AddDays(1);
|
||||
|
||||
var visits = await _db.OperationLogs
|
||||
.Where(l => l.CreatedAt >= startDate && l.CreatedAt < endDate)
|
||||
.GroupBy(l => l.CreatedAt.Date)
|
||||
.Select(g => new { Date = g.Key, Count = g.Count() })
|
||||
.ToListAsync();
|
||||
|
||||
var experiences = await _db.ExperienceLogs
|
||||
.Where(l => l.CreatedAt >= startDate && l.CreatedAt < endDate)
|
||||
.GroupBy(l => l.CreatedAt.Date)
|
||||
.Select(g => new { Date = g.Key, Count = g.Count() })
|
||||
.ToListAsync();
|
||||
|
||||
var trends = new List<TrendData>();
|
||||
for (var i = 0; i < 7; i++)
|
||||
{
|
||||
var date = startDate.AddDays(i);
|
||||
trends.Add(new TrendData
|
||||
{
|
||||
Date = date.ToString("MM-dd"),
|
||||
Visits = visits.FirstOrDefault(v => v.Date == date)?.Count ?? 0,
|
||||
Experiences = experiences.FirstOrDefault(e => e.Date == date)?.Count ?? 0
|
||||
});
|
||||
}
|
||||
|
||||
// 如果没有真实数据则返回模拟数据
|
||||
if (trends.Sum(t => t.Visits + t.Experiences) == 0)
|
||||
{
|
||||
var random = new Random();
|
||||
foreach (var t in trends)
|
||||
{
|
||||
t.Visits = random.Next(50, 200);
|
||||
t.Experiences = random.Next(10, 80);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== CategoryDistribution: 按分类统计项目数 =====
|
||||
var categoryDistribution = await _db.Projects
|
||||
.Join(_db.ProjectCategories,
|
||||
p => p.CategoryId,
|
||||
c => c.Id,
|
||||
(p, c) => new { c.Name })
|
||||
.GroupBy(x => x.Name)
|
||||
.Select(g => new CategoryDistribution
|
||||
{
|
||||
Name = g.Key,
|
||||
Count = g.Count()
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
// ===== RecentActivities: 当前用户最近 10 条操作日志 =====
|
||||
var recentLogEntities = await _db.OperationLogs
|
||||
.Where(l => l.UserId == userId)
|
||||
.OrderByDescending(l => l.CreatedAt)
|
||||
.Take(10)
|
||||
.ToListAsync();
|
||||
|
||||
var recentUserIds = recentLogEntities.Select(l => l.UserId).Distinct().ToList();
|
||||
var recentNicknames = await _db.Users
|
||||
.Where(u => recentUserIds.Contains(u.Id))
|
||||
.ToDictionaryAsync(u => u.Id, u => u.Nickname);
|
||||
|
||||
var recentActivities = recentLogEntities.Select(l => new OperationLogDto
|
||||
{
|
||||
Id = l.Id,
|
||||
UserId = l.UserId,
|
||||
Username = l.Username,
|
||||
Nickname = recentNicknames.GetValueOrDefault(l.UserId) ?? string.Empty,
|
||||
Module = l.Module,
|
||||
Operation = l.Operation,
|
||||
Detail = l.Detail,
|
||||
Ip = l.Ip,
|
||||
CreatedAt = l.CreatedAt
|
||||
}).ToList();
|
||||
|
||||
// ===== HotProjects: 按 ExperienceCount 倒序取 6 个(封面需 COS 签名) =====
|
||||
var hotProjectEntities = await _db.Projects
|
||||
.Include(p => p.Category)
|
||||
.OrderByDescending(p => p.ExperienceCount)
|
||||
.Take(6)
|
||||
.ToListAsync();
|
||||
|
||||
var hotProjects = hotProjectEntities.Select(p => new ProjectDto
|
||||
{
|
||||
Id = p.Id,
|
||||
Name = p.Name,
|
||||
CategoryId = p.CategoryId,
|
||||
CategoryName = p.Category != null ? p.Category.Name : string.Empty,
|
||||
CoverImage = _fileStorage.GetSignedUrl(p.CoverImage),
|
||||
Description = p.Description,
|
||||
Status = p.Status,
|
||||
IsFeatured = p.IsFeatured,
|
||||
ViewCount = p.ViewCount,
|
||||
ExperienceCount = p.ExperienceCount,
|
||||
FavoriteCount = p.FavoriteCount,
|
||||
CreatedAt = p.CreatedAt
|
||||
}).ToList();
|
||||
|
||||
var data = new DashboardData
|
||||
{
|
||||
Stats = new DashboardStats
|
||||
{
|
||||
ProjectCount = projectCount,
|
||||
CustomerCount = customerCount,
|
||||
TodayVisits = todayVisits,
|
||||
ActiveAuthorizations = activeAuthorizations,
|
||||
UnreadMessages = unreadMessages
|
||||
},
|
||||
Trends = trends,
|
||||
CategoryDistribution = categoryDistribution,
|
||||
RecentActivities = recentActivities,
|
||||
HotProjects = hotProjects
|
||||
};
|
||||
|
||||
return ApiResponse<DashboardData>.Success(data);
|
||||
}
|
||||
}
|
||||
22
backend/MilliSim.Api/Services/IAuthService.cs
Normal file
22
backend/MilliSim.Api/Services/IAuthService.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Common.Models;
|
||||
|
||||
namespace MilliSim.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 验证码结果
|
||||
/// </summary>
|
||||
public class CaptchaResult
|
||||
{
|
||||
public string CaptchaId { get; set; } = string.Empty;
|
||||
public string Svg { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public interface IAuthService
|
||||
{
|
||||
Task<ApiResponse<CaptchaResult>> GetCaptchaAsync();
|
||||
Task<ApiResponse<LoginResponse>> LoginAsync(LoginRequest request);
|
||||
Task<ApiResponse> ChangePasswordAsync(ChangePasswordRequest request);
|
||||
Task<ApiResponse<UserDto>> GetProfileAsync();
|
||||
Task<ApiResponse<UserDto>> UpdateProfileAsync(UpdateProfileRequest request);
|
||||
}
|
||||
22
backend/MilliSim.Api/Services/IAuthorizationService.cs
Normal file
22
backend/MilliSim.Api/Services/IAuthorizationService.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Common.Models;
|
||||
|
||||
namespace MilliSim.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 授权服务接口(客户级授权:对客户授权后,该客户能运行所有项目)
|
||||
/// </summary>
|
||||
public interface IAuthorizationService
|
||||
{
|
||||
Task<ApiResponse<PagedResult<AuthorizationDto>>> GetAuthorizationsAsync(AuthorizationQueryRequest request);
|
||||
Task<ApiResponse<AuthorizationDto>> GetAuthorizationByIdAsync(long id);
|
||||
Task<ApiResponse<AuthorizationDto>> CreateAuthorizationAsync(CreateAuthorizationRequest request);
|
||||
Task<ApiResponse> BatchAuthorizeAsync(BatchAuthorizationRequest request);
|
||||
Task<ApiResponse<AuthorizationDto>> RenewAuthorizationAsync(long id, int days);
|
||||
Task<ApiResponse> CancelAuthorizationAsync(long id);
|
||||
/// <summary>
|
||||
/// 检查用户是否有任何有效授权(客户级授权,不检查项目)
|
||||
/// </summary>
|
||||
Task<ApiResponse<bool>> CheckAuthorizationAsync(long userId);
|
||||
Task<ApiResponse<List<AuthorizationDto>>> GetMyAuthorizationsAsync();
|
||||
}
|
||||
25
backend/MilliSim.Api/Services/IContentService.cs
Normal file
25
backend/MilliSim.Api/Services/IContentService.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Common.Models;
|
||||
|
||||
namespace MilliSim.Services;
|
||||
|
||||
public interface IContentService
|
||||
{
|
||||
// 轮播图
|
||||
Task<ApiResponse<List<BannerDto>>> GetBannersAsync(bool onlyActive = false);
|
||||
Task<ApiResponse<BannerDto>> CreateBannerAsync(CreateBannerRequest request);
|
||||
Task<ApiResponse<BannerDto>> UpdateBannerAsync(long id, CreateBannerRequest request);
|
||||
Task<ApiResponse> DeleteBannerAsync(long id);
|
||||
|
||||
// 合作伙伴
|
||||
Task<ApiResponse<List<PartnerDto>>> GetPartnersAsync(bool onlyVisible = false);
|
||||
Task<ApiResponse<PartnerDto>> CreatePartnerAsync(CreatePartnerRequest request);
|
||||
Task<ApiResponse<PartnerDto>> UpdatePartnerAsync(long id, CreatePartnerRequest request);
|
||||
Task<ApiResponse> DeletePartnerAsync(long id);
|
||||
|
||||
// 咨询消息
|
||||
Task<ApiResponse<PagedResult<ConsultationDto>>> GetConsultationsAsync(PageRequest request, bool? isRead = null);
|
||||
Task<ApiResponse<ConsultationDto>> CreateConsultationAsync(CreateConsultationRequest request);
|
||||
Task<ApiResponse> MarkAsReadAsync(long id);
|
||||
Task<ApiResponse> DeleteConsultationAsync(long id);
|
||||
}
|
||||
9
backend/MilliSim.Api/Services/IDashboardService.cs
Normal file
9
backend/MilliSim.Api/Services/IDashboardService.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Common.Models;
|
||||
|
||||
namespace MilliSim.Services;
|
||||
|
||||
public interface IDashboardService
|
||||
{
|
||||
Task<ApiResponse<DashboardData>> GetDashboardDataAsync();
|
||||
}
|
||||
14
backend/MilliSim.Api/Services/INotificationService.cs
Normal file
14
backend/MilliSim.Api/Services/INotificationService.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Common.Models;
|
||||
|
||||
namespace MilliSim.Services;
|
||||
|
||||
public interface INotificationService
|
||||
{
|
||||
Task<ApiResponse<PagedResult<NotificationDto>>> GetNotificationsAsync(NotificationType? type, PageRequest request);
|
||||
Task<ApiResponse<int>> GetUnreadCountAsync();
|
||||
Task<ApiResponse> MarkAsReadAsync(long id);
|
||||
Task<ApiResponse> MarkAllAsReadAsync();
|
||||
Task<ApiResponse> DeleteNotificationAsync(long id);
|
||||
Task<ApiResponse<NotificationDto>> CreateNotificationAsync(NotificationType type, string title, string content, long? userId = null, string? eventId = null);
|
||||
}
|
||||
69
backend/MilliSim.Api/Services/IProjectService.cs
Normal file
69
backend/MilliSim.Api/Services/IProjectService.cs
Normal file
@ -0,0 +1,69 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Common.Models;
|
||||
|
||||
namespace MilliSim.Services;
|
||||
|
||||
public interface IProjectService
|
||||
{
|
||||
// ===== 项目 =====
|
||||
Task<ApiResponse<PagedResult<ProjectDto>>> GetProjectsAsync(ProjectQueryRequest request);
|
||||
Task<ApiResponse<PagedResult<ProjectDto>>> GetPublishedProjectsAsync(ProjectQueryRequest request);
|
||||
Task<ApiResponse<ProjectDto>> GetProjectByIdAsync(long id, bool incrementView = false);
|
||||
Task<ApiResponse<ProjectDto>> CreateProjectAsync(CreateProjectRequest request);
|
||||
Task<ApiResponse<ProjectDto>> UpdateProjectAsync(long id, UpdateProjectRequest request);
|
||||
Task<ApiResponse> DeleteProjectAsync(long id);
|
||||
Task<ApiResponse> TogglePublishAsync(long id);
|
||||
Task<ApiResponse> ToggleFeaturedAsync(long id);
|
||||
Task<ApiResponse> ToggleFavoriteAsync(long projectId);
|
||||
Task<ApiResponse<PagedResult<ProjectDto>>> GetFavoritesAsync(PageRequest request);
|
||||
Task<ApiResponse> RecordExperienceAsync(long projectId, int durationSeconds);
|
||||
Task<ApiResponse<PagedResult<ExperienceLogDto>>> GetExperienceLogsAsync(PageRequest request);
|
||||
Task<ApiResponse<List<ProjectDto>>> GetRelatedProjectsAsync(long projectId, int count = 4);
|
||||
|
||||
// ===== 项目分类 =====
|
||||
Task<ApiResponse<List<CategoryDto>>> GetCategoriesAsync();
|
||||
Task<ApiResponse<CategoryDto>> CreateCategoryAsync(CreateCategoryRequest request);
|
||||
Task<ApiResponse<CategoryDto>> UpdateCategoryAsync(long id, CreateCategoryRequest request);
|
||||
Task<ApiResponse> DeleteCategoryAsync(long id);
|
||||
|
||||
// ===== 平台类型 =====
|
||||
Task<ApiResponse<List<PlatformDto>>> GetPlatformsAsync();
|
||||
Task<ApiResponse<PlatformDto>> CreatePlatformAsync(CreatePlatformRequest request);
|
||||
Task<ApiResponse<PlatformDto>> UpdatePlatformAsync(long id, CreatePlatformRequest request);
|
||||
Task<ApiResponse> DeletePlatformAsync(long id);
|
||||
|
||||
// ===== 特性标签 =====
|
||||
Task<ApiResponse<List<TagDto>>> GetTagsAsync();
|
||||
Task<ApiResponse<TagDto>> CreateTagAsync(CreateTagRequest request);
|
||||
Task<ApiResponse<TagDto>> UpdateTagAsync(long id, CreateTagRequest request);
|
||||
Task<ApiResponse> DeleteTagAsync(long id);
|
||||
|
||||
// ===== 资源包 =====
|
||||
Task<ApiResponse<List<PackageDto>>> GetPackagesAsync(long projectId);
|
||||
Task<ApiResponse<PackageDto>> UploadPackageAsync(long projectId, PackageType type, IFormFile file, string wakeProtocolPrefix, string wakeProtocolParam);
|
||||
Task<ApiResponse> DeletePackageAsync(long id);
|
||||
/// <summary>
|
||||
/// 下载资源包(返回文件流和原始文件名,用于 Content-Disposition)
|
||||
/// </summary>
|
||||
Task<(Stream Stream, string ContentType, string FileName)> DownloadPackageAsync(long packageId);
|
||||
/// <summary>
|
||||
/// 获取 WebGL 资源包的在线运行入口 URL(解压后的 index.html 签名 URL)
|
||||
/// </summary>
|
||||
Task<ApiResponse<string>> GetWebGLRunUrlAsync(long projectId);
|
||||
|
||||
/// <summary>
|
||||
/// 获取 WebGL 代理后的 index.html 内容(注入 base 标签,资源走代理重定向到签名URL)
|
||||
/// </summary>
|
||||
Task<string> GetWebGLIndexHtmlAsync(long projectId);
|
||||
|
||||
/// <summary>
|
||||
/// 获取 WebGL 资源文件的签名 URL(用于资源代理重定向)
|
||||
/// </summary>
|
||||
Task<string> GetWebGLResourceUrlAsync(long projectId, string resourcePath);
|
||||
|
||||
/// <summary>
|
||||
/// 获取视频流(防盗链代理,不暴露 COS 直链,支持 Range 请求)
|
||||
/// </summary>
|
||||
Task<(Stream Stream, string ContentType, long? FileSize, bool IsPartial, string? ContentRange)> GetVideoStreamAsync(long projectId, string? rangeHeader);
|
||||
}
|
||||
46
backend/MilliSim.Api/Services/ISystemService.cs
Normal file
46
backend/MilliSim.Api/Services/ISystemService.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Common.Models;
|
||||
|
||||
namespace MilliSim.Services;
|
||||
|
||||
public interface ISystemService
|
||||
{
|
||||
// 系统设置
|
||||
Task<ApiResponse<List<SystemSettingDto>>> GetSettingsAsync();
|
||||
Task<ApiResponse> UpdateSettingsAsync(UpdateSettingsRequest request);
|
||||
Task<ApiResponse<SystemSettingDto>> GetSettingAsync(string key);
|
||||
/// <summary>
|
||||
/// 获取公开系统设置(无需鉴权):返回平台名、Logo、备案号等供前台显示
|
||||
/// </summary>
|
||||
Task<ApiResponse<PublicSettingsDto>> GetPublicSettingsAsync();
|
||||
|
||||
// 关于我们内容管理
|
||||
Task<ApiResponse<AboutSettingsDto>> GetAboutSettingsAsync();
|
||||
Task<ApiResponse> UpdateAboutSettingsAsync(AboutSettingsDto dto);
|
||||
|
||||
// 工厂重置
|
||||
Task<ApiResponse<FactoryResetResultDto>> FactoryResetAsync(FactoryResetRequest request);
|
||||
|
||||
// 操作日志
|
||||
Task<ApiResponse<PagedResult<OperationLogDto>>> GetOperationLogsAsync(OperationLogQueryRequest request);
|
||||
Task<ApiResponse> ClearLogsAsync(DateTime? beforeDate);
|
||||
Task<ApiResponse> CleanExpiredLogsAsync();
|
||||
|
||||
// 数据备份
|
||||
Task<ApiResponse<List<BackupDto>>> GetBackupsAsync();
|
||||
Task<ApiResponse<BackupDto>> CreateBackupAsync();
|
||||
Task<ApiResponse> DeleteBackupAsync(long id);
|
||||
Task<ApiResponse<(Stream Stream, string FileName)>> DownloadBackupAsync(long id);
|
||||
Task CleanExpiredBackupsAsync();
|
||||
|
||||
// 唤醒协议
|
||||
Task<ApiResponse<List<WakeProtocolDto>>> GetWakeProtocolsAsync();
|
||||
Task<ApiResponse<WakeProtocolDto>> CreateWakeProtocolAsync(CreateWakeProtocolRequest request);
|
||||
Task<ApiResponse<WakeProtocolDto>> UpdateWakeProtocolAsync(long id, CreateWakeProtocolRequest request);
|
||||
Task<ApiResponse> DeleteWakeProtocolAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 测试本地存储目录是否可读写(保存存储配置前校验)
|
||||
/// </summary>
|
||||
Task<ApiResponse> TestLocalPathAsync(string localBasePath);
|
||||
}
|
||||
15
backend/MilliSim.Api/Services/IUserService.cs
Normal file
15
backend/MilliSim.Api/Services/IUserService.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Common.Models;
|
||||
|
||||
namespace MilliSim.Services;
|
||||
|
||||
public interface IUserService
|
||||
{
|
||||
Task<ApiResponse<PagedResult<UserDto>>> GetUsersAsync(UserQueryRequest request);
|
||||
Task<ApiResponse<UserDto>> GetUserByIdAsync(long id);
|
||||
Task<ApiResponse<UserDto>> CreateUserAsync(CreateUserRequest request);
|
||||
Task<ApiResponse<UserDto>> UpdateUserAsync(long id, UpdateUserRequest request);
|
||||
Task<ApiResponse> DeleteUserAsync(long id);
|
||||
Task<ApiResponse> ResetPasswordAsync(long id);
|
||||
Task<ApiResponse> ToggleStatusAsync(long id);
|
||||
}
|
||||
110
backend/MilliSim.Api/Services/Infrastructure/CaptchaService.cs
Normal file
110
backend/MilliSim.Api/Services/Infrastructure/CaptchaService.cs
Normal file
@ -0,0 +1,110 @@
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace MilliSim.Services.Infrastructure;
|
||||
|
||||
public interface ICaptchaService
|
||||
{
|
||||
(string CaptchaId, string Svg) GenerateCaptcha();
|
||||
bool ValidateCaptcha(string captchaId, string code);
|
||||
}
|
||||
|
||||
public class CaptchaService : ICaptchaService
|
||||
{
|
||||
private static readonly Dictionary<string, (string Code, DateTime Expire)> _store = new();
|
||||
private static readonly object _lock = new();
|
||||
private static readonly char[] _chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789".ToCharArray();
|
||||
|
||||
public (string CaptchaId, string Svg) GenerateCaptcha()
|
||||
{
|
||||
var code = new string(Enumerable.Range(0, 4).Select(_ => _chars[RandomNumberGenerator.GetInt32(_chars.Length)]).ToArray());
|
||||
var captchaId = Guid.NewGuid().ToString("N");
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_store[captchaId] = (code, DateTime.Now.AddMinutes(5));
|
||||
// 清理过期
|
||||
var expired = _store.Where(x => x.Value.Expire < DateTime.Now).Select(x => x.Key).ToList();
|
||||
foreach (var key in expired) _store.Remove(key);
|
||||
}
|
||||
|
||||
var svg = DrawSvg(code);
|
||||
return (captchaId, svg);
|
||||
}
|
||||
|
||||
public bool ValidateCaptcha(string captchaId, string code)
|
||||
{
|
||||
if (string.IsNullOrEmpty(captchaId) || string.IsNullOrEmpty(code))
|
||||
return false;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_store.TryGetValue(captchaId, out var item))
|
||||
return false;
|
||||
|
||||
_store.Remove(captchaId);
|
||||
|
||||
if (item.Expire < DateTime.Now)
|
||||
return false;
|
||||
|
||||
return string.Equals(item.Code, code, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
private static string DrawSvg(string code)
|
||||
{
|
||||
var random = new Random();
|
||||
var width = 120;
|
||||
var height = 40;
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.Append($"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{width}\" height=\"{height}\">");
|
||||
|
||||
// 背景
|
||||
sb.Append($"<rect width=\"{width}\" height=\"{height}\" fill=\"#E6F4FF\"/>");
|
||||
|
||||
// 干扰线
|
||||
for (var i = 0; i < 4; i++)
|
||||
{
|
||||
var x1 = random.Next(width);
|
||||
var y1 = random.Next(height);
|
||||
var x2 = random.Next(width);
|
||||
var y2 = random.Next(height);
|
||||
var color = GetRandomColor(random);
|
||||
sb.Append($"<line x1=\"{x1}\" y1=\"{y1}\" x2=\"{x2}\" y2=\"{y2}\" stroke=\"{color}\" stroke-width=\"1\" opacity=\"0.5\"/>");
|
||||
}
|
||||
|
||||
// 干扰点
|
||||
for (var i = 0; i < 20; i++)
|
||||
{
|
||||
var x = random.Next(width);
|
||||
var y = random.Next(height);
|
||||
var color = GetRandomColor(random);
|
||||
sb.Append($"<circle cx=\"{x}\" cy=\"{y}\" r=\"1\" fill=\"{color}\" opacity=\"0.5\"/>");
|
||||
}
|
||||
|
||||
// 文字
|
||||
var colors = new[] { "#0958D9", "#1677FF", "#52C41A", "#722ED1", "#FA8C16" };
|
||||
for (var i = 0; i < code.Length; i++)
|
||||
{
|
||||
var x = 15 + i * 25;
|
||||
var y = 28 + random.Next(-3, 4);
|
||||
var angle = random.Next(-15, 16);
|
||||
var color = colors[random.Next(colors.Length)];
|
||||
var fontSize = random.Next(22, 28);
|
||||
sb.Append($"<text x=\"{x}\" y=\"{y}\" font-size=\"{fontSize}\" font-family=\"Arial\" font-weight=\"bold\" fill=\"{color}\" transform=\"rotate({angle} {x} {y})\">{code[i]}</text>");
|
||||
}
|
||||
|
||||
sb.Append("</svg>");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GetRandomColor(Random random)
|
||||
{
|
||||
var colors = new[] { "#0958D9", "#1677FF", "#52C41A", "#722ED1", "#FA8C16", "#13C2C2" };
|
||||
return colors[random.Next(colors.Length)];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using MilliSim.Common.Models;
|
||||
|
||||
namespace MilliSim.Services.Infrastructure;
|
||||
|
||||
public interface ICurrentUserService
|
||||
{
|
||||
long UserId { get; }
|
||||
string Username { get; }
|
||||
UserRole Role { get; }
|
||||
string Nickname { get; }
|
||||
bool IsAuthenticated { get; }
|
||||
string Ip { get; }
|
||||
}
|
||||
|
||||
public class CurrentUserService : ICurrentUserService
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public CurrentUserService(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
private ClaimsPrincipal? User => _httpContextAccessor.HttpContext?.User;
|
||||
|
||||
public long UserId
|
||||
{
|
||||
get
|
||||
{
|
||||
var id = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
return long.TryParse(id, out var result) ? result : 0;
|
||||
}
|
||||
}
|
||||
|
||||
public string Username => User?.FindFirst(ClaimTypes.Name)?.Value ?? string.Empty;
|
||||
|
||||
public UserRole Role
|
||||
{
|
||||
get
|
||||
{
|
||||
var role = User?.FindFirst("role")?.Value;
|
||||
return int.TryParse(role, out var result) ? (UserRole)result : UserRole.Guest;
|
||||
}
|
||||
}
|
||||
|
||||
public string Nickname => User?.FindFirst("nickname")?.Value ?? string.Empty;
|
||||
|
||||
public bool IsAuthenticated => User?.Identity?.IsAuthenticated ?? false;
|
||||
|
||||
public string Ip
|
||||
{
|
||||
get
|
||||
{
|
||||
var context = _httpContextAccessor.HttpContext;
|
||||
if (context == null) return string.Empty;
|
||||
|
||||
var forwarded = context.Request.Headers["X-Forwarded-For"].FirstOrDefault();
|
||||
if (!string.IsNullOrEmpty(forwarded))
|
||||
return forwarded.Split(',')[0].Trim();
|
||||
|
||||
return context.Connection.RemoteIpAddress?.ToString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
1201
backend/MilliSim.Api/Services/Infrastructure/FileStorageService.cs
Normal file
1201
backend/MilliSim.Api/Services/Infrastructure/FileStorageService.cs
Normal file
File diff suppressed because it is too large
Load Diff
87
backend/MilliSim.Api/Services/Infrastructure/JwtService.cs
Normal file
87
backend/MilliSim.Api/Services/Infrastructure/JwtService.cs
Normal file
@ -0,0 +1,87 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using MilliSim.Common.Entities;
|
||||
using MilliSim.Common.Models;
|
||||
|
||||
namespace MilliSim.Services.Infrastructure;
|
||||
|
||||
public interface IJwtService
|
||||
{
|
||||
string GenerateToken(User user);
|
||||
ClaimsPrincipal? ValidateToken(string token);
|
||||
}
|
||||
|
||||
public class JwtService : IJwtService
|
||||
{
|
||||
private readonly IConfiguration _config;
|
||||
|
||||
public JwtService(IConfiguration config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public string GenerateToken(User user)
|
||||
{
|
||||
var secretKey = _config["Jwt:SecretKey"]!;
|
||||
var issuer = _config["Jwt:Issuer"];
|
||||
var audience = _config["Jwt:Audience"];
|
||||
var expireDays = int.Parse(_config["Jwt:ExpireDays"] ?? "7");
|
||||
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
|
||||
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
||||
new(ClaimTypes.Name, user.Username),
|
||||
new(ClaimTypes.Role, user.Role.ToString()),
|
||||
new("nickname", user.Nickname),
|
||||
new("role", ((int)user.Role).ToString())
|
||||
};
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: issuer,
|
||||
audience: audience,
|
||||
claims: claims,
|
||||
expires: DateTime.Now.AddDays(expireDays),
|
||||
signingCredentials: credentials
|
||||
);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
|
||||
public ClaimsPrincipal? ValidateToken(string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var secretKey = _config["Jwt:SecretKey"]!;
|
||||
var issuer = _config["Jwt:Issuer"];
|
||||
var audience = _config["Jwt:Audience"];
|
||||
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
|
||||
var parameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = issuer,
|
||||
ValidAudience = audience,
|
||||
IssuerSigningKey = key,
|
||||
ClockSkew = TimeSpan.Zero
|
||||
};
|
||||
|
||||
return handler.ValidateToken(token, parameters, out _);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MilliSim.Data;
|
||||
using MilliSim.Common.Entities;
|
||||
|
||||
namespace MilliSim.Services.Infrastructure;
|
||||
|
||||
public interface IOperationLogService
|
||||
{
|
||||
Task LogAsync(string module, string operation, string detail);
|
||||
}
|
||||
|
||||
public class OperationLogService : IOperationLogService
|
||||
{
|
||||
private readonly MilliSimDbContext _db;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
|
||||
public OperationLogService(MilliSimDbContext db, ICurrentUserService currentUser)
|
||||
{
|
||||
_db = db;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
public async Task LogAsync(string module, string operation, string detail)
|
||||
{
|
||||
var log = new OperationLog
|
||||
{
|
||||
UserId = _currentUser.UserId,
|
||||
Username = _currentUser.Username,
|
||||
Module = module,
|
||||
Operation = operation,
|
||||
Detail = detail,
|
||||
Ip = _currentUser.Ip
|
||||
};
|
||||
|
||||
_db.OperationLogs.Add(log);
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,146 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MilliSim.Common.Entities;
|
||||
using MilliSim.Data;
|
||||
|
||||
namespace MilliSim.Services.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// 上传文件跟踪服务接口:管理 PendingUpload 记录,实现垃圾资源清理
|
||||
/// </summary>
|
||||
public interface IUploadTrackingService
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册上传文件(上传成功后调用)
|
||||
/// </summary>
|
||||
Task RegisterAsync(string objectKey, string folder, long? uploadedBy);
|
||||
|
||||
/// <summary>
|
||||
/// 消费上传文件(保存到实体后调用,标记为已使用)
|
||||
/// </summary>
|
||||
Task ConsumeAsync(string objectKey);
|
||||
|
||||
/// <summary>
|
||||
/// 删除待消费的上传文件(取消表单/替换文件时调用)
|
||||
/// 仅删除 ConsumedAt == null 的记录,防止误删已使用的文件
|
||||
/// </summary>
|
||||
/// <returns>是否实际删除了文件</returns>
|
||||
Task<bool> DeletePendingAsync(string objectKey);
|
||||
|
||||
/// <summary>
|
||||
/// 清理过期的待消费上传文件(GC 定时任务调用)
|
||||
/// </summary>
|
||||
/// <param name="expireHours">过期时间(小时)</param>
|
||||
/// <returns>清理的文件数量</returns>
|
||||
Task<int> CleanupExpiredAsync(int expireHours = 24);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传文件跟踪服务实现
|
||||
/// </summary>
|
||||
public class UploadTrackingService : IUploadTrackingService
|
||||
{
|
||||
private readonly MilliSimDbContext _db;
|
||||
private readonly IFileStorageService _fileStorage;
|
||||
private readonly ILogger<UploadTrackingService> _logger;
|
||||
|
||||
public UploadTrackingService(
|
||||
MilliSimDbContext db,
|
||||
IFileStorageService fileStorage,
|
||||
ILogger<UploadTrackingService> logger)
|
||||
{
|
||||
_db = db;
|
||||
_fileStorage = fileStorage;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task RegisterAsync(string objectKey, string folder, long? uploadedBy)
|
||||
{
|
||||
if (string.IsNullOrEmpty(objectKey)) return;
|
||||
|
||||
_db.PendingUploads.Add(new PendingUpload
|
||||
{
|
||||
ObjectKey = objectKey,
|
||||
Folder = folder ?? string.Empty,
|
||||
UploadedBy = uploadedBy,
|
||||
UploadedAt = DateTime.Now,
|
||||
ConsumedAt = null
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task ConsumeAsync(string objectKey)
|
||||
{
|
||||
if (string.IsNullOrEmpty(objectKey)) return;
|
||||
|
||||
var pending = await _db.PendingUploads
|
||||
.Where(p => p.ObjectKey == objectKey && p.ConsumedAt == null)
|
||||
.ToListAsync();
|
||||
|
||||
if (pending.Count == 0) return;
|
||||
|
||||
var now = DateTime.Now;
|
||||
foreach (var item in pending)
|
||||
{
|
||||
item.ConsumedAt = now;
|
||||
item.UpdatedAt = now;
|
||||
}
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> DeletePendingAsync(string objectKey)
|
||||
{
|
||||
if (string.IsNullOrEmpty(objectKey)) return false;
|
||||
|
||||
var pending = await _db.PendingUploads
|
||||
.Where(p => p.ObjectKey == objectKey && p.ConsumedAt == null)
|
||||
.ToListAsync();
|
||||
|
||||
if (pending.Count == 0) return false;
|
||||
|
||||
// 删除存储中的文件
|
||||
try
|
||||
{
|
||||
await _fileStorage.DeleteAsync(objectKey);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "删除上传文件失败:{ObjectKey}", objectKey);
|
||||
}
|
||||
|
||||
// 删除 PendingUpload 记录
|
||||
_db.PendingUploads.RemoveRange(pending);
|
||||
await _db.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<int> CleanupExpiredAsync(int expireHours = 24)
|
||||
{
|
||||
var cutoff = DateTime.Now.AddHours(-expireHours);
|
||||
var expired = await _db.PendingUploads
|
||||
.Where(p => p.ConsumedAt == null && p.UploadedAt < cutoff)
|
||||
.ToListAsync();
|
||||
|
||||
if (expired.Count == 0) return 0;
|
||||
|
||||
var deletedCount = 0;
|
||||
foreach (var item in expired)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _fileStorage.DeleteAsync(item.ObjectKey);
|
||||
deletedCount++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "GC 清理删除文件失败:{ObjectKey}", item.ObjectKey);
|
||||
}
|
||||
}
|
||||
|
||||
_db.PendingUploads.RemoveRange(expired);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("垃圾资源 GC 清理:共清理 {Count} 个过期上传文件", deletedCount);
|
||||
return deletedCount;
|
||||
}
|
||||
}
|
||||
172
backend/MilliSim.Api/Services/NotificationService.cs
Normal file
172
backend/MilliSim.Api/Services/NotificationService.cs
Normal file
@ -0,0 +1,172 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Common.Entities;
|
||||
using MilliSim.Common.Models;
|
||||
using MilliSim.Data;
|
||||
using MilliSim.Services.Infrastructure;
|
||||
|
||||
namespace MilliSim.Services;
|
||||
|
||||
public class NotificationService : INotificationService
|
||||
{
|
||||
private readonly MilliSimDbContext _db;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
|
||||
public NotificationService(MilliSimDbContext db, ICurrentUserService currentUser)
|
||||
{
|
||||
_db = db;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将通知类型枚举转为中文名称
|
||||
/// </summary>
|
||||
private static string GetTypeName(NotificationType type) => type switch
|
||||
{
|
||||
NotificationType.System => "系统",
|
||||
NotificationType.Project => "项目",
|
||||
NotificationType.Account => "账号",
|
||||
NotificationType.Authorization => "授权",
|
||||
_ => "通知"
|
||||
};
|
||||
|
||||
public async Task<ApiResponse<PagedResult<NotificationDto>>> GetNotificationsAsync(NotificationType? type, PageRequest request)
|
||||
{
|
||||
var userId = _currentUser.UserId;
|
||||
var query = _db.Notifications
|
||||
.Where(n => n.UserId == null || n.UserId == userId);
|
||||
|
||||
if (type.HasValue)
|
||||
query = query.Where(n => n.Type == type.Value);
|
||||
|
||||
if (!string.IsNullOrEmpty(request.Keyword))
|
||||
query = query.Where(n => n.Title.Contains(request.Keyword) || n.Content.Contains(request.Keyword));
|
||||
|
||||
var total = await query.CountAsync();
|
||||
var items = await query
|
||||
.OrderByDescending(n => n.CreatedAt)
|
||||
.Skip((request.PageIndex - 1) * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
.Select(n => new NotificationDto
|
||||
{
|
||||
Id = n.Id,
|
||||
Type = n.Type,
|
||||
Title = n.Title,
|
||||
Content = n.Content,
|
||||
IsRead = _db.NotificationReads.Any(r => r.NotificationId == n.Id && r.UserId == userId),
|
||||
CreatedAt = n.CreatedAt
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
// 在内存中设置中文类型名(避免 EF 翻译 switch 表达式)
|
||||
foreach (var item in items)
|
||||
item.TypeName = GetTypeName(item.Type);
|
||||
|
||||
return ApiResponse<PagedResult<NotificationDto>>.Success(
|
||||
PagedResult<NotificationDto>.Create(items, total, request.PageIndex, request.PageSize));
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<int>> GetUnreadCountAsync()
|
||||
{
|
||||
var userId = _currentUser.UserId;
|
||||
var count = await _db.Notifications
|
||||
.Where(n => n.UserId == null || n.UserId == userId)
|
||||
.Where(n => !_db.NotificationReads.Any(r => r.NotificationId == n.Id && r.UserId == userId))
|
||||
.CountAsync();
|
||||
|
||||
return ApiResponse<int>.Success(count);
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> MarkAsReadAsync(long id)
|
||||
{
|
||||
var userId = _currentUser.UserId;
|
||||
var notification = await _db.Notifications
|
||||
.FirstOrDefaultAsync(n => n.Id == id && (n.UserId == null || n.UserId == userId));
|
||||
if (notification == null)
|
||||
return ApiResponse.Fail("通知不存在", 404);
|
||||
|
||||
var exists = await _db.NotificationReads
|
||||
.AnyAsync(r => r.NotificationId == id && r.UserId == userId);
|
||||
if (!exists)
|
||||
{
|
||||
_db.NotificationReads.Add(new NotificationRead
|
||||
{
|
||||
UserId = userId,
|
||||
NotificationId = id
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return ApiResponse.Success("已标记为已读");
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> MarkAllAsReadAsync()
|
||||
{
|
||||
var userId = _currentUser.UserId;
|
||||
var unreadIds = await _db.Notifications
|
||||
.Where(n => n.UserId == null || n.UserId == userId)
|
||||
.Where(n => !_db.NotificationReads.Any(r => r.NotificationId == n.Id && r.UserId == userId))
|
||||
.Select(n => n.Id)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var nid in unreadIds)
|
||||
{
|
||||
_db.NotificationReads.Add(new NotificationRead
|
||||
{
|
||||
UserId = userId,
|
||||
NotificationId = nid
|
||||
});
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return ApiResponse.Success($"已将 {unreadIds.Count} 条通知标记为已读");
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> DeleteNotificationAsync(long id)
|
||||
{
|
||||
var userId = _currentUser.UserId;
|
||||
var notification = await _db.Notifications
|
||||
.FirstOrDefaultAsync(n => n.Id == id && (n.UserId == null || n.UserId == userId));
|
||||
if (notification == null)
|
||||
return ApiResponse.Fail("通知不存在", 404);
|
||||
|
||||
notification.IsDeleted = true;
|
||||
notification.UpdatedAt = DateTime.Now;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return ApiResponse.Success("通知已删除");
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<NotificationDto>> CreateNotificationAsync(NotificationType type, string title, string content, long? userId = null, string? eventId = null)
|
||||
{
|
||||
// 根据 eventId 防重复
|
||||
if (!string.IsNullOrEmpty(eventId))
|
||||
{
|
||||
var exists = await _db.Notifications.AnyAsync(n => n.EventId == eventId);
|
||||
if (exists)
|
||||
return ApiResponse<NotificationDto>.Fail("通知已存在", 409);
|
||||
}
|
||||
|
||||
var notification = new Notification
|
||||
{
|
||||
Type = type,
|
||||
Title = title,
|
||||
Content = content,
|
||||
UserId = userId,
|
||||
EventId = eventId
|
||||
};
|
||||
_db.Notifications.Add(notification);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return ApiResponse<NotificationDto>.Success(new NotificationDto
|
||||
{
|
||||
Id = notification.Id,
|
||||
Type = notification.Type,
|
||||
TypeName = GetTypeName(notification.Type),
|
||||
Title = notification.Title,
|
||||
Content = notification.Content,
|
||||
IsRead = false,
|
||||
CreatedAt = notification.CreatedAt
|
||||
});
|
||||
}
|
||||
}
|
||||
1373
backend/MilliSim.Api/Services/ProjectService.cs
Normal file
1373
backend/MilliSim.Api/Services/ProjectService.cs
Normal file
File diff suppressed because it is too large
Load Diff
985
backend/MilliSim.Api/Services/SystemService.cs
Normal file
985
backend/MilliSim.Api/Services/SystemService.cs
Normal file
@ -0,0 +1,985 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Common.Entities;
|
||||
using MilliSim.Common.Models;
|
||||
using MilliSim.Data;
|
||||
using MilliSim.Services.Infrastructure;
|
||||
|
||||
namespace MilliSim.Services;
|
||||
|
||||
public class SystemService : ISystemService
|
||||
{
|
||||
private readonly MilliSimDbContext _db;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly IWebHostEnvironment _env;
|
||||
private readonly IOperationLogService _operationLog;
|
||||
private readonly IFileStorageService _fileStorage;
|
||||
private readonly IUploadTrackingService _uploadTracking;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="db">数据库上下文</param>
|
||||
/// <param name="config">配置</param>
|
||||
/// <param name="env">Web主机环境</param>
|
||||
/// <param name="operationLog">操作日志服务</param>
|
||||
/// <param name="fileStorage">文件存储服务</param>
|
||||
/// <param name="uploadTracking">上传跟踪服务</param>
|
||||
public SystemService(MilliSimDbContext db, IConfiguration config, IWebHostEnvironment env,
|
||||
IOperationLogService operationLog, IFileStorageService fileStorage,
|
||||
IUploadTrackingService uploadTracking)
|
||||
{
|
||||
_db = db;
|
||||
_config = config;
|
||||
_env = env;
|
||||
_operationLog = operationLog;
|
||||
_fileStorage = fileStorage;
|
||||
_uploadTracking = uploadTracking;
|
||||
}
|
||||
|
||||
// ===== 系统设置 =====
|
||||
|
||||
public async Task<ApiResponse<List<SystemSettingDto>>> GetSettingsAsync()
|
||||
{
|
||||
var settings = await _db.SystemSettings
|
||||
.OrderBy(s => s.Id)
|
||||
.Select(s => new SystemSettingDto
|
||||
{
|
||||
Key = s.Key,
|
||||
Value = s.Value,
|
||||
Description = s.Description
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
// Logo 字段在 DB 中存储的是对象键,返回前需通过 GetSignedUrl 转换为可访问的 URL
|
||||
var logoKeys = new[] { "LogoFrontend", "LogoBackend", "LogoFavicon", "LogoLogin", "LogoFooter" };
|
||||
foreach (var s in settings)
|
||||
{
|
||||
if (logoKeys.Contains(s.Key) && !string.IsNullOrEmpty(s.Value))
|
||||
{
|
||||
s.Value = _fileStorage.GetSignedUrl(s.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return ApiResponse<List<SystemSettingDto>>.Success(settings);
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> UpdateSettingsAsync(UpdateSettingsRequest request)
|
||||
{
|
||||
if (request.Settings == null || request.Settings.Count == 0)
|
||||
return ApiResponse.Fail("没有需要更新的设置");
|
||||
|
||||
var keys = request.Settings.Keys.ToList();
|
||||
var settings = await _db.SystemSettings
|
||||
.Where(s => keys.Contains(s.Key))
|
||||
.ToListAsync();
|
||||
|
||||
// Logo 五项的 key 集合(用于对比旧值并清理旧文件)
|
||||
var logoKeys = new[] { "LogoFrontend", "LogoBackend", "LogoFavicon", "LogoLogin", "LogoFooter" };
|
||||
|
||||
// 保存前记录旧 Logo 值(用于对比变更后删除旧文件)
|
||||
var oldLogoValues = new Dictionary<string, string>();
|
||||
foreach (var setting in settings)
|
||||
{
|
||||
if (logoKeys.Contains(setting.Key))
|
||||
{
|
||||
oldLogoValues[setting.Key] = setting.Value;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var setting in settings)
|
||||
{
|
||||
if (request.Settings.TryGetValue(setting.Key, out var value))
|
||||
{
|
||||
// Logo 字段需通过 ExtractKey 转为对象键存储,避免签名 URL 过期导致数据丢失
|
||||
setting.Value = logoKeys.Contains(setting.Key) ? _fileStorage.ExtractKey(value) : value;
|
||||
setting.UpdatedAt = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// 保存后对比 Logo 变化:替换则删旧文件,新文件消费标记
|
||||
foreach (var logoKey in logoKeys)
|
||||
{
|
||||
if (!request.Settings.TryGetValue(logoKey, out var newValue)) continue;
|
||||
|
||||
var oldKey = _fileStorage.ExtractKey(oldLogoValues.GetValueOrDefault(logoKey) ?? "");
|
||||
var newKey = _fileStorage.ExtractKey(newValue);
|
||||
if (!string.IsNullOrEmpty(oldKey) && oldKey != newKey)
|
||||
{
|
||||
await _fileStorage.DeleteAsync(oldKey);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(newKey))
|
||||
{
|
||||
await _uploadTracking.ConsumeAsync(newKey);
|
||||
}
|
||||
}
|
||||
|
||||
// 存储相关设置变更时清除缓存,确保后续请求读取最新配置
|
||||
var storageKeys = new[] { "StorageMode", "CosSecretId", "CosSecretKey", "CosBucket", "CosRegion",
|
||||
"CosSignedUrlExpireMinutes", "LocalBasePath", "LocalBaseUrl" };
|
||||
if (request.Settings.Keys.Any(k => storageKeys.Contains(k)))
|
||||
{
|
||||
_fileStorage.ClearConfigCache();
|
||||
}
|
||||
|
||||
await _operationLog.LogAsync("系统设置", "更新", $"更新了 {settings.Count} 项设置");
|
||||
|
||||
return ApiResponse.Success("设置更新成功");
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<SystemSettingDto>> GetSettingAsync(string key)
|
||||
{
|
||||
var setting = await _db.SystemSettings.FirstOrDefaultAsync(s => s.Key == key);
|
||||
if (setting == null)
|
||||
return ApiResponse<SystemSettingDto>.Fail("设置项不存在", 404);
|
||||
|
||||
return ApiResponse<SystemSettingDto>.Success(new SystemSettingDto
|
||||
{
|
||||
Key = setting.Key,
|
||||
Value = setting.Value,
|
||||
Description = setting.Description
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取公开系统设置(无需鉴权):返回平台名、Logo(签名URL)、备案号等供前台显示
|
||||
/// Logo 字段在库中存储的是上传时返回的签名URL,这里通过 ExtractKey + GetSignedUrl 刷新为最新签名URL,避免过期
|
||||
/// </summary>
|
||||
public async Task<ApiResponse<PublicSettingsDto>> GetPublicSettingsAsync()
|
||||
{
|
||||
var keys = new List<string>
|
||||
{
|
||||
"PlatformName", "Slogan", "CompanyName", "Contact", "ICP",
|
||||
"ContactPhone", "ContactEmail", "ContactAddress", "ContactWebsite", "ContactWorkTime",
|
||||
"LogoFrontend", "LogoBackend", "LogoFavicon", "LogoLogin", "LogoFooter",
|
||||
"ShowPartners", "ShowAbout"
|
||||
};
|
||||
|
||||
var settings = await _db.SystemSettings
|
||||
.Where(s => keys.Contains(s.Key))
|
||||
.Select(s => new { s.Key, s.Value })
|
||||
.ToListAsync();
|
||||
|
||||
string Get(string key) => settings.FirstOrDefault(s => s.Key == key)?.Value ?? string.Empty;
|
||||
|
||||
string RefreshLogo(string key)
|
||||
{
|
||||
var raw = Get(key);
|
||||
if (string.IsNullOrEmpty(raw)) return string.Empty;
|
||||
var objectKey = _fileStorage.ExtractKey(raw);
|
||||
return string.IsNullOrEmpty(objectKey) ? raw : _fileStorage.GetSignedUrl(objectKey);
|
||||
}
|
||||
|
||||
var dto = new PublicSettingsDto
|
||||
{
|
||||
PlatformName = Get("PlatformName"),
|
||||
Slogan = Get("Slogan"),
|
||||
CompanyName = Get("CompanyName"),
|
||||
Contact = Get("Contact"),
|
||||
ContactPhone = Get("ContactPhone"),
|
||||
ContactEmail = Get("ContactEmail"),
|
||||
ContactAddress = Get("ContactAddress"),
|
||||
ContactWebsite = Get("ContactWebsite"),
|
||||
ContactWorkTime = Get("ContactWorkTime"),
|
||||
ICP = Get("ICP"),
|
||||
LogoFrontend = RefreshLogo("LogoFrontend"),
|
||||
LogoBackend = RefreshLogo("LogoBackend"),
|
||||
LogoFavicon = RefreshLogo("LogoFavicon"),
|
||||
LogoLogin = RefreshLogo("LogoLogin"),
|
||||
LogoFooter = RefreshLogo("LogoFooter"),
|
||||
ShowPartners = Get("ShowPartners") != "false",
|
||||
ShowAbout = Get("ShowAbout") != "false",
|
||||
};
|
||||
|
||||
return ApiResponse<PublicSettingsDto>.Success(dto);
|
||||
}
|
||||
|
||||
// ===== 关于我们内容管理 =====
|
||||
|
||||
/// <summary>
|
||||
/// 获取关于我们设置(公开端点,前台 About 页消费)
|
||||
/// </summary>
|
||||
public async Task<ApiResponse<AboutSettingsDto>> GetAboutSettingsAsync()
|
||||
{
|
||||
var keys = new List<string> { "ShowAbout", "AboutContent" };
|
||||
var settings = await _db.SystemSettings
|
||||
.Where(s => keys.Contains(s.Key))
|
||||
.Select(s => new { s.Key, s.Value })
|
||||
.ToListAsync();
|
||||
|
||||
string Get(string key) => settings.FirstOrDefault(s => s.Key == key)?.Value ?? string.Empty;
|
||||
|
||||
var dto = new AboutSettingsDto
|
||||
{
|
||||
ShowAbout = Get("ShowAbout") != "false",
|
||||
};
|
||||
|
||||
var contentJson = Get("AboutContent");
|
||||
if (!string.IsNullOrEmpty(contentJson))
|
||||
{
|
||||
try
|
||||
{
|
||||
dto.Content = System.Text.Json.JsonSerializer.Deserialize<AboutContentDto>(contentJson) ?? new AboutContentDto();
|
||||
// 刷新使命卡片图标的签名URL(存储的是COS对象键,读取时转换为签名URL)
|
||||
if (dto.Content.MissionCards != null)
|
||||
{
|
||||
foreach (var card in dto.Content.MissionCards)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(card.Icon) && card.Icon.Contains('/'))
|
||||
{
|
||||
card.Icon = _fileStorage.GetSignedUrl(card.Icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
dto.Content = new AboutContentDto();
|
||||
}
|
||||
}
|
||||
|
||||
return ApiResponse<AboutSettingsDto>.Success(dto);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新关于我们设置(Admin 端点)
|
||||
/// </summary>
|
||||
public async Task<ApiResponse> UpdateAboutSettingsAsync(AboutSettingsDto dto)
|
||||
{
|
||||
var keys = new List<string> { "ShowAbout", "AboutContent" };
|
||||
var settings = await _db.SystemSettings
|
||||
.Where(s => keys.Contains(s.Key))
|
||||
.ToListAsync();
|
||||
|
||||
var settingDict = settings.ToDictionary(s => s.Key, s => s);
|
||||
|
||||
if (!settingDict.TryGetValue("ShowAbout", out var showAboutSetting))
|
||||
{
|
||||
showAboutSetting = new SystemSetting { Key = "ShowAbout", Description = "前台是否显示关于我们页面" };
|
||||
_db.SystemSettings.Add(showAboutSetting);
|
||||
}
|
||||
showAboutSetting.Value = dto.ShowAbout ? "true" : "false";
|
||||
showAboutSetting.UpdatedAt = DateTime.Now;
|
||||
|
||||
if (!settingDict.TryGetValue("AboutContent", out var contentSetting))
|
||||
{
|
||||
contentSetting = new SystemSetting { Key = "AboutContent", Description = "关于我们页面内容(JSON)" };
|
||||
_db.SystemSettings.Add(contentSetting);
|
||||
}
|
||||
// 保存前将使命卡片图标的签名URL转换为COS对象键,避免签名过期
|
||||
var contentToSave = dto.Content ?? new AboutContentDto();
|
||||
if (contentToSave.MissionCards != null)
|
||||
{
|
||||
foreach (var card in contentToSave.MissionCards)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(card.Icon))
|
||||
{
|
||||
card.Icon = _fileStorage.ExtractKey(card.Icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
contentSetting.Value = System.Text.Json.JsonSerializer.Serialize(contentToSave);
|
||||
contentSetting.UpdatedAt = DateTime.Now;
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
await _operationLog.LogAsync("关于我们", "更新", "更新了关于我们页面内容");
|
||||
|
||||
return ApiResponse.Success("保存成功");
|
||||
}
|
||||
|
||||
// ===== 工厂重置 =====
|
||||
|
||||
/// <summary>
|
||||
/// 恢复出厂设置:清空所有业务数据(保留 admin 账户)+ 删除 COS 资源(保留 backups/)
|
||||
/// 重置前自动创建数据库备份作为安全网
|
||||
/// </summary>
|
||||
public async Task<ApiResponse<FactoryResetResultDto>> FactoryResetAsync(FactoryResetRequest request)
|
||||
{
|
||||
if (request?.ConfirmText != "RESET")
|
||||
{
|
||||
return ApiResponse<FactoryResetResultDto>.Fail("确认文本不正确,请输入 RESET");
|
||||
}
|
||||
|
||||
// 1. 重置前先创建数据库备份
|
||||
var backupResult = await CreateBackupAsync();
|
||||
var backupId = backupResult.Data?.Id ?? 0;
|
||||
|
||||
// 2. 列出所有 COS 对象(在清库前读取配置,保证配置可用)
|
||||
// 容错:即使 COS 列举失败,仍继续清库,确保数据库恢复出厂状态
|
||||
List<string> keysToDelete = new();
|
||||
try
|
||||
{
|
||||
var allKeys = await _fileStorage.ListAllObjectsAsync();
|
||||
// 过滤掉 backups/ 前缀(保留数据库备份作为安全网)
|
||||
keysToDelete = allKeys.Where(k => !k.StartsWith("backups/", StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _operationLog.LogAsync("工厂重置", "COS列举失败", $"列举对象失败,将继续清库。错误:{ex.Message}");
|
||||
}
|
||||
|
||||
// 3. 清空所有业务表(保留 admin 账户)
|
||||
var tables = new[]
|
||||
{
|
||||
"ProjectTagRelations", "ProjectScreenshots", "ProjectPackages",
|
||||
"Favorites", "ExperienceLogs", "Authorizations",
|
||||
"NotificationReads", "Notifications",
|
||||
"Consultations", "OperationLogs",
|
||||
"PendingUploads",
|
||||
"Projects", "Banners", "Partners",
|
||||
"ProjectCategories", "PlatformTypes", "FeatureTags",
|
||||
"WakeProtocols",
|
||||
"Backups",
|
||||
"SystemSettings"
|
||||
};
|
||||
|
||||
await _db.Database.ExecuteSqlRawAsync("SET FOREIGN_KEY_CHECKS = 0");
|
||||
try
|
||||
{
|
||||
foreach (var table in tables)
|
||||
{
|
||||
await _db.Database.ExecuteSqlRawAsync("DELETE FROM `" + table + "`");
|
||||
}
|
||||
// Users 表:仅删除非 admin 用户,并清空 admin 的 avatar(避免引用已删除的 COS 文件)
|
||||
await _db.Database.ExecuteSqlRawAsync("DELETE FROM `Users` WHERE `Username` != 'admin'");
|
||||
await _db.Database.ExecuteSqlRawAsync("UPDATE `Users` SET `Avatar` = '' WHERE `Username` = 'admin'");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _db.Database.ExecuteSqlRawAsync("SET FOREIGN_KEY_CHECKS = 1");
|
||||
}
|
||||
|
||||
// 4. 重新执行种子(admin 已存在跳过;分类/平台/标签/项目/轮播图/合作伙伴/设置按为空才补)
|
||||
await DbSeeder.SeedAsync(_db);
|
||||
|
||||
// 5. 批量删除 COS 资源
|
||||
var deletedCount = 0;
|
||||
try
|
||||
{
|
||||
deletedCount = await _fileStorage.DeleteObjectsAsync(keysToDelete);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// COS 删除失败不回滚 DB(DB 已是干净状态),仅记录日志
|
||||
await _operationLog.LogAsync("工厂重置", "COS清理失败", ex.Message);
|
||||
}
|
||||
|
||||
await _operationLog.LogAsync("工厂重置", "执行", $"清空 {tables.Length + 1} 张表,删除 {deletedCount} 个 COS 对象,备份ID {backupId}");
|
||||
|
||||
return ApiResponse<FactoryResetResultDto>.Success(new FactoryResetResultDto
|
||||
{
|
||||
TablesCleared = tables.Length + 1, // +1 for Users
|
||||
ObjectsDeleted = deletedCount,
|
||||
BackupId = backupId
|
||||
}, "工厂重置完成");
|
||||
}
|
||||
|
||||
// ===== 操作日志 =====
|
||||
|
||||
public async Task<ApiResponse<PagedResult<OperationLogDto>>> GetOperationLogsAsync(OperationLogQueryRequest request)
|
||||
{
|
||||
var query = _db.OperationLogs.AsQueryable();
|
||||
|
||||
if (request.UserId.HasValue)
|
||||
query = query.Where(l => l.UserId == request.UserId.Value);
|
||||
|
||||
if (!string.IsNullOrEmpty(request.Username))
|
||||
query = query.Where(l => l.Username.Contains(request.Username));
|
||||
|
||||
if (!string.IsNullOrEmpty(request.Module))
|
||||
query = query.Where(l => l.Module.Contains(request.Module));
|
||||
|
||||
if (request.StartDate.HasValue)
|
||||
query = query.Where(l => l.CreatedAt >= request.StartDate.Value);
|
||||
|
||||
if (request.EndDate.HasValue)
|
||||
query = query.Where(l => l.CreatedAt <= request.EndDate.Value);
|
||||
|
||||
if (!string.IsNullOrEmpty(request.Keyword))
|
||||
query = query.Where(l =>
|
||||
l.Username.Contains(request.Keyword) ||
|
||||
l.Operation.Contains(request.Keyword) ||
|
||||
l.Detail.Contains(request.Keyword));
|
||||
|
||||
var total = await query.CountAsync();
|
||||
var logEntities = await query
|
||||
.OrderByDescending(l => l.CreatedAt)
|
||||
.Skip((request.PageIndex - 1) * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
.ToListAsync();
|
||||
|
||||
var userIds = logEntities.Select(l => l.UserId).Distinct().ToList();
|
||||
var nicknames = await _db.Users
|
||||
.Where(u => userIds.Contains(u.Id))
|
||||
.ToDictionaryAsync(u => u.Id, u => u.Nickname);
|
||||
|
||||
var items = logEntities.Select(l => new OperationLogDto
|
||||
{
|
||||
Id = l.Id,
|
||||
UserId = l.UserId,
|
||||
Username = l.Username,
|
||||
Nickname = nicknames.GetValueOrDefault(l.UserId) ?? string.Empty,
|
||||
Module = l.Module,
|
||||
Operation = l.Operation,
|
||||
Detail = l.Detail,
|
||||
Ip = l.Ip,
|
||||
CreatedAt = l.CreatedAt
|
||||
}).ToList();
|
||||
|
||||
return ApiResponse<PagedResult<OperationLogDto>>.Success(
|
||||
PagedResult<OperationLogDto>.Create(items, total, request.PageIndex, request.PageSize));
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> ClearLogsAsync(DateTime? beforeDate)
|
||||
{
|
||||
int count;
|
||||
if (beforeDate.HasValue)
|
||||
{
|
||||
count = await _db.OperationLogs
|
||||
.Where(l => l.CreatedAt < beforeDate.Value)
|
||||
.ExecuteDeleteAsync();
|
||||
await _operationLog.LogAsync("操作日志", "清理", $"清理了 {beforeDate.Value:yyyy-MM-dd} 之前的日志 {count} 条");
|
||||
}
|
||||
else
|
||||
{
|
||||
count = await _db.OperationLogs.ExecuteDeleteAsync();
|
||||
await _operationLog.LogAsync("操作日志", "清理", $"清空了全部日志 {count} 条");
|
||||
}
|
||||
|
||||
return ApiResponse.Success($"已清理 {count} 条日志");
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> CleanExpiredLogsAsync()
|
||||
{
|
||||
var retentionDays = int.Parse(await GetSettingValueAsync("LogRetentionDays", "90"));
|
||||
var cutoff = DateTime.Now.AddDays(-retentionDays);
|
||||
var count = await _db.OperationLogs
|
||||
.Where(l => l.CreatedAt < cutoff)
|
||||
.ExecuteDeleteAsync();
|
||||
return ApiResponse.Success($"已清理 {retentionDays} 天前的过期日志 {count} 条");
|
||||
}
|
||||
|
||||
// ===== 数据备份 =====
|
||||
|
||||
public async Task<ApiResponse<List<BackupDto>>> GetBackupsAsync()
|
||||
{
|
||||
var backups = await _db.Backups
|
||||
.OrderByDescending(b => b.CreatedAt)
|
||||
.Select(b => new BackupDto
|
||||
{
|
||||
Id = b.Id,
|
||||
FileName = b.FileName,
|
||||
FileSize = b.FileSize,
|
||||
Type = b.Type,
|
||||
Url = b.Url,
|
||||
CreatedAt = b.CreatedAt
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var b in backups)
|
||||
{
|
||||
b.Type = MapBackupType(b.Type);
|
||||
}
|
||||
|
||||
return ApiResponse<List<BackupDto>>.Success(backups);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将备份类型映射为中文:Manual->手动,Auto->自动,其余原样返回
|
||||
/// </summary>
|
||||
private static string MapBackupType(string type) => type switch
|
||||
{
|
||||
"Manual" => "手动",
|
||||
"Auto" => "自动",
|
||||
_ => string.IsNullOrEmpty(type) ? "手动" : type
|
||||
};
|
||||
|
||||
public async Task<ApiResponse<BackupDto>> CreateBackupAsync()
|
||||
{
|
||||
// 生成本地备份文件
|
||||
var dir = GetBackupDir();
|
||||
var fileName = $"backup_{DateTime.Now:yyyyMMddHHmmss}.sql";
|
||||
var filePath = Path.Combine(dir, fileName);
|
||||
var localUrl = $"/backups/{fileName}";
|
||||
|
||||
// 尝试使用 mysqldump 导出
|
||||
var success = await TryMySqlDumpAsync(filePath);
|
||||
if (!success)
|
||||
{
|
||||
// 回退:使用 SQL 脚本生成
|
||||
try
|
||||
{
|
||||
await GenerateSqlScriptAsync(filePath);
|
||||
success = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (File.Exists(filePath)) File.Delete(filePath);
|
||||
return ApiResponse<BackupDto>.Fail($"备份失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
// 在 COS 上传前先保存文件大小,避免上传后本地文件被删除导致 FileNotFoundException
|
||||
var fileSize = fileInfo.Length;
|
||||
|
||||
// 读取存储模式(Local=本地存储,Cos=腾讯云COS)
|
||||
var storageMode = await GetSettingValueAsync("StorageMode", "Cos");
|
||||
string backupUrl = localUrl;
|
||||
|
||||
// 如果是COS模式,将备份文件上传到COS
|
||||
if (storageMode.Equals("Cos", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
// 创建FormFile用于上传到COS
|
||||
FileStream? fileStream = null;
|
||||
try
|
||||
{
|
||||
fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
||||
var formFile = new FormFile(fileStream, 0, fileStream.Length, "file", fileName)
|
||||
{
|
||||
Headers = new HeaderDictionary(),
|
||||
ContentType = "application/sql"
|
||||
};
|
||||
|
||||
// 上传备份文件到COS的backups目录
|
||||
backupUrl = await _fileStorage.UploadAsync(formFile, "backups");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 确保文件流被关闭
|
||||
fileStream?.Dispose();
|
||||
}
|
||||
|
||||
// 上传成功后删除本地临时文件
|
||||
if (File.Exists(filePath)) File.Delete(filePath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// COS上传失败,使用本地文件作为兜底
|
||||
backupUrl = localUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建备份记录(使用预先保存的文件大小)
|
||||
var backup = new Backup
|
||||
{
|
||||
FileName = fileName,
|
||||
FileSize = fileSize,
|
||||
Type = "Manual",
|
||||
Url = backupUrl
|
||||
};
|
||||
_db.Backups.Add(backup);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
await _operationLog.LogAsync("数据备份", "创建", $"手动创建备份 {fileName}");
|
||||
|
||||
return ApiResponse<BackupDto>.Success(new BackupDto
|
||||
{
|
||||
Id = backup.Id,
|
||||
FileName = backup.FileName,
|
||||
FileSize = backup.FileSize,
|
||||
Type = MapBackupType(backup.Type),
|
||||
Url = backup.Url,
|
||||
CreatedAt = backup.CreatedAt
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> DeleteBackupAsync(long id)
|
||||
{
|
||||
var backup = await _db.Backups.FindAsync(id);
|
||||
if (backup == null)
|
||||
return ApiResponse.Fail("备份不存在", 404);
|
||||
|
||||
// 判断备份文件存储位置(URL以"/"开头为本地文件,否则为COS对象键)
|
||||
if (backup.Url.StartsWith("/"))
|
||||
{
|
||||
// 本地文件删除
|
||||
var dir = GetBackupDir();
|
||||
var filePath = Path.Combine(dir, backup.FileName);
|
||||
if (File.Exists(filePath))
|
||||
File.Delete(filePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// COS文件删除
|
||||
await _fileStorage.DeleteAsync(backup.Url);
|
||||
}
|
||||
|
||||
_db.Backups.Remove(backup);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
await _operationLog.LogAsync("数据备份", "删除", $"删除备份 {backup.FileName}");
|
||||
|
||||
return ApiResponse.Success("备份已删除");
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<(Stream Stream, string FileName)>> DownloadBackupAsync(long id)
|
||||
{
|
||||
var backup = await _db.Backups.FindAsync(id);
|
||||
if (backup == null)
|
||||
return ApiResponse<(Stream, string)>.Fail("备份不存在", 404);
|
||||
|
||||
// 判断备份文件存储位置(URL以"/"开头为本地文件,否则为COS对象键)
|
||||
if (backup.Url.StartsWith("/"))
|
||||
{
|
||||
// 本地文件下载
|
||||
var dir = GetBackupDir();
|
||||
var filePath = Path.Combine(dir, backup.FileName);
|
||||
if (!File.Exists(filePath))
|
||||
return ApiResponse<(Stream, string)>.Fail("备份文件不存在", 404);
|
||||
|
||||
var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
return ApiResponse<(Stream, string)>.Success((stream, backup.FileName));
|
||||
}
|
||||
else
|
||||
{
|
||||
// COS文件下载
|
||||
try
|
||||
{
|
||||
var (stream, _, _) = await _fileStorage.DownloadAsync(backup.Url);
|
||||
return ApiResponse<(Stream, string)>.Success((stream, backup.FileName));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return ApiResponse<(Stream, string)>.Fail("备份文件不存在", 404);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CleanExpiredBackupsAsync()
|
||||
{
|
||||
// 读取备份保留天数
|
||||
var retentionDays = int.Parse(await GetSettingValueAsync("BackupRetentionDays", "30"));
|
||||
var cutoff = DateTime.Now.AddDays(-retentionDays);
|
||||
// 查询过期备份
|
||||
var expired = await _db.Backups
|
||||
.Where(b => b.CreatedAt < cutoff)
|
||||
.ToListAsync();
|
||||
|
||||
// 遍历删除过期备份文件
|
||||
foreach (var b in expired)
|
||||
{
|
||||
// 判断备份文件存储位置(URL以"/"开头为本地文件,否则为COS对象键)
|
||||
if (b.Url.StartsWith("/"))
|
||||
{
|
||||
// 本地文件删除
|
||||
var dir = GetBackupDir();
|
||||
var filePath = Path.Combine(dir, b.FileName);
|
||||
if (File.Exists(filePath))
|
||||
File.Delete(filePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// COS文件删除
|
||||
await _fileStorage.DeleteAsync(b.Url);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除数据库记录
|
||||
_db.Backups.RemoveRange(expired);
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// ===== 唤醒协议 =====
|
||||
|
||||
public async Task<ApiResponse<List<WakeProtocolDto>>> GetWakeProtocolsAsync()
|
||||
{
|
||||
var list = await _db.WakeProtocols
|
||||
.OrderBy(w => w.Id)
|
||||
.Select(w => new WakeProtocolDto
|
||||
{
|
||||
Id = w.Id,
|
||||
TerminalType = w.TerminalType,
|
||||
ProtocolPrefix = w.ProtocolPrefix,
|
||||
ParamTemplate = w.ParamTemplate,
|
||||
Status = w.Status
|
||||
})
|
||||
.ToListAsync();
|
||||
return ApiResponse<List<WakeProtocolDto>>.Success(list);
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<WakeProtocolDto>> CreateWakeProtocolAsync(CreateWakeProtocolRequest request)
|
||||
{
|
||||
var wp = new WakeProtocol
|
||||
{
|
||||
TerminalType = request.TerminalType,
|
||||
ProtocolPrefix = request.ProtocolPrefix,
|
||||
ParamTemplate = request.ParamTemplate,
|
||||
Status = request.Status
|
||||
};
|
||||
_db.WakeProtocols.Add(wp);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
await _operationLog.LogAsync("唤醒协议", "创建", $"创建唤醒协议 {wp.TerminalType}");
|
||||
|
||||
return ApiResponse<WakeProtocolDto>.Success(MapToDto(wp));
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<WakeProtocolDto>> UpdateWakeProtocolAsync(long id, CreateWakeProtocolRequest request)
|
||||
{
|
||||
var wp = await _db.WakeProtocols.FindAsync(id);
|
||||
if (wp == null)
|
||||
return ApiResponse<WakeProtocolDto>.Fail("唤醒协议不存在", 404);
|
||||
|
||||
wp.TerminalType = request.TerminalType;
|
||||
wp.ProtocolPrefix = request.ProtocolPrefix;
|
||||
wp.ParamTemplate = request.ParamTemplate;
|
||||
wp.Status = request.Status;
|
||||
wp.UpdatedAt = DateTime.Now;
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
await _operationLog.LogAsync("唤醒协议", "更新", $"更新唤醒协议 {wp.TerminalType}");
|
||||
|
||||
return ApiResponse<WakeProtocolDto>.Success(MapToDto(wp));
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> DeleteWakeProtocolAsync(long id)
|
||||
{
|
||||
var wp = await _db.WakeProtocols.FindAsync(id);
|
||||
if (wp == null)
|
||||
return ApiResponse.Fail("唤醒协议不存在", 404);
|
||||
|
||||
wp.IsDeleted = true;
|
||||
wp.UpdatedAt = DateTime.Now;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
await _operationLog.LogAsync("唤醒协议", "删除", $"删除唤醒协议 {wp.TerminalType}");
|
||||
|
||||
return ApiResponse.Success("删除成功");
|
||||
}
|
||||
|
||||
// ===== 本地存储目录测试 =====
|
||||
|
||||
/// <summary>
|
||||
/// 测试本地存储目录是否可读写(保存存储配置前校验)
|
||||
/// 支持相对路径(相对于 ContentRootPath)或绝对路径
|
||||
/// </summary>
|
||||
public Task<ApiResponse> TestLocalPathAsync(string localBasePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(localBasePath))
|
||||
return Task.FromResult(ApiResponse.Fail("存储根目录不能为空"));
|
||||
|
||||
try
|
||||
{
|
||||
// 解析为绝对路径
|
||||
var absolutePath = Path.IsPathRooted(localBasePath)
|
||||
? localBasePath
|
||||
: Path.Combine(_env.ContentRootPath, localBasePath);
|
||||
|
||||
// 确保目录存在
|
||||
Directory.CreateDirectory(absolutePath);
|
||||
|
||||
// 测试写入
|
||||
var testFile = Path.Combine(absolutePath, $".millisim_test_{Guid.NewGuid():N}");
|
||||
File.WriteAllText(testFile, "test");
|
||||
// 测试读取
|
||||
var content = File.ReadAllText(testFile);
|
||||
// 清理测试文件
|
||||
File.Delete(testFile);
|
||||
|
||||
if (content != "test")
|
||||
return Task.FromResult(ApiResponse.Fail("目录可写但读取内容不匹配,请检查目录权限"));
|
||||
|
||||
return Task.FromResult(ApiResponse.Success($"目录可读写:{absolutePath}"));
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return Task.FromResult(ApiResponse.Fail("目录无访问权限,请选择其他目录或修改权限"));
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return Task.FromResult(ApiResponse.Fail($"路径格式无效:{ex.Message}"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Task.FromResult(ApiResponse.Fail($"目录测试失败:{ex.Message}"));
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 辅助方法 =====
|
||||
|
||||
private async Task<string> GetSettingValueAsync(string key, string defaultValue)
|
||||
{
|
||||
var setting = await _db.SystemSettings.FirstOrDefaultAsync(s => s.Key == key);
|
||||
return setting?.Value ?? defaultValue;
|
||||
}
|
||||
|
||||
private string GetBackupDir()
|
||||
{
|
||||
var root = _env.WebRootPath ?? Path.Combine(_env.ContentRootPath, "wwwroot");
|
||||
var dir = Path.Combine(root, "backups");
|
||||
Directory.CreateDirectory(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
private static WakeProtocolDto MapToDto(WakeProtocol wp) => new()
|
||||
{
|
||||
Id = wp.Id,
|
||||
TerminalType = wp.TerminalType,
|
||||
ProtocolPrefix = wp.ProtocolPrefix,
|
||||
ParamTemplate = wp.ParamTemplate,
|
||||
Status = wp.Status
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 尝试使用 mysqldump 导出数据库
|
||||
/// </summary>
|
||||
private async Task<bool> TryMySqlDumpAsync(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var connectionString = _config.GetConnectionString("DefaultConnection")!;
|
||||
var builder = new MySqlConnector.MySqlConnectionStringBuilder(connectionString);
|
||||
var host = builder.Server;
|
||||
var port = builder.Port;
|
||||
var database = builder.Database;
|
||||
var user = builder.UserID;
|
||||
var password = builder.Password;
|
||||
|
||||
var args = new StringBuilder();
|
||||
args.Append($"-h{host} -P{port} -u{user}");
|
||||
if (!string.IsNullOrEmpty(password))
|
||||
args.Append($" -p\"{password}\"");
|
||||
args.Append(" --default-character-set=utf8mb4 --single-transaction --routines --triggers --skip-lock-tables");
|
||||
args.Append($" \"{database}\"");
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "mysqldump",
|
||||
Arguments = args.ToString(),
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using var process = Process.Start(psi);
|
||||
if (process == null) return false;
|
||||
|
||||
// 并发读取 stderr 防止缓冲区死锁
|
||||
var stderrTask = process.StandardError.ReadToEndAsync();
|
||||
|
||||
await using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
|
||||
{
|
||||
await process.StandardOutput.BaseStream.CopyToAsync(fileStream);
|
||||
await fileStream.FlushAsync();
|
||||
}
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
await stderrTask;
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
if (File.Exists(filePath)) File.Delete(filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
var fi = new FileInfo(filePath);
|
||||
if (fi.Length == 0)
|
||||
{
|
||||
File.Delete(filePath);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (File.Exists(filePath)) File.Delete(filePath);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 回退方案:直接查询数据库生成 SQL 脚本
|
||||
/// </summary>
|
||||
private async Task GenerateSqlScriptAsync(string filePath)
|
||||
{
|
||||
var connectionString = _config.GetConnectionString("DefaultConnection")!;
|
||||
await using var connection = new MySqlConnector.MySqlConnection(connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("-- MilliSim Database Backup");
|
||||
sb.AppendLine($"-- Generated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
|
||||
sb.AppendLine("SET NAMES utf8mb4;");
|
||||
sb.AppendLine("SET FOREIGN_KEY_CHECKS=0;");
|
||||
sb.AppendLine();
|
||||
|
||||
var tables = new List<string>();
|
||||
await using (var cmd = connection.CreateCommand())
|
||||
{
|
||||
cmd.CommandText = "SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'BASE TABLE'";
|
||||
await using var reader = await cmd.ExecuteReaderAsync();
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
tables.Add(reader.GetString(0));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var table in tables)
|
||||
{
|
||||
// 表结构
|
||||
await using (var createCmd = connection.CreateCommand())
|
||||
{
|
||||
createCmd.CommandText = $"SHOW CREATE TABLE `{table}`";
|
||||
await using var createReader = await createCmd.ExecuteReaderAsync();
|
||||
if (await createReader.ReadAsync())
|
||||
{
|
||||
sb.AppendLine($"-- ----------------------------");
|
||||
sb.AppendLine($"-- Table structure for {table}");
|
||||
sb.AppendLine($"-- ----------------------------");
|
||||
sb.AppendLine($"DROP TABLE IF EXISTS `{table}`;");
|
||||
sb.AppendLine(createReader.GetString(1) + ";");
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
// 表数据
|
||||
await using (var dataCmd = connection.CreateCommand())
|
||||
{
|
||||
dataCmd.CommandText = $"SELECT * FROM `{table}`";
|
||||
await using var dataReader = await dataCmd.ExecuteReaderAsync();
|
||||
var columns = new List<string>();
|
||||
for (var i = 0; i < dataReader.FieldCount; i++)
|
||||
{
|
||||
columns.Add(dataReader.GetName(i));
|
||||
}
|
||||
|
||||
var hasData = false;
|
||||
while (await dataReader.ReadAsync())
|
||||
{
|
||||
hasData = true;
|
||||
var values = new List<string>();
|
||||
for (var i = 0; i < dataReader.FieldCount; i++)
|
||||
{
|
||||
values.Add(FormatSqlValue(dataReader.GetValue(i)));
|
||||
}
|
||||
sb.AppendLine($"INSERT INTO `{table}` (`{string.Join("`, `", columns)}`) VALUES ({string.Join(", ", values)});");
|
||||
}
|
||||
if (hasData) sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine("SET FOREIGN_KEY_CHECKS=1;");
|
||||
|
||||
await File.WriteAllTextAsync(filePath, sb.ToString());
|
||||
}
|
||||
|
||||
private static string FormatSqlValue(object? value)
|
||||
{
|
||||
if (value == null || value == DBNull.Value) return "NULL";
|
||||
if (value is bool b) return b ? "1" : "0";
|
||||
if (value is DateTime dt) return $"'{dt:yyyy-MM-dd HH:mm:ss}'";
|
||||
if (value is byte[] bytes) return $"0x{Convert.ToHexString(bytes)}";
|
||||
if (value is sbyte or short or int or long or byte or ushort or uint or ulong) return value.ToString()!;
|
||||
if (value is float or double or decimal) return value.ToString()!;
|
||||
var s = value.ToString() ?? string.Empty;
|
||||
return "'" + s.Replace("\\", "\\\\").Replace("'", "\\'").Replace("\n", "\\n").Replace("\r", "\\r") + "'";
|
||||
}
|
||||
}
|
||||
56
backend/MilliSim.Api/Services/UploadCleanupJob.cs
Normal file
56
backend/MilliSim.Api/Services/UploadCleanupJob.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using MilliSim.Services.Infrastructure;
|
||||
|
||||
namespace MilliSim.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 后台定时任务:垃圾资源清理。
|
||||
/// 每 6 小时扫描 PendingUploads 表,清理 24 小时前上传但未被消费(未保存到实体)的孤立文件。
|
||||
/// 应对浏览器关闭、刷新等极端情况下前端无法触发清理的场景。
|
||||
/// </summary>
|
||||
public class UploadCleanupJob : IHostedService, IDisposable
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private Timer? _timer;
|
||||
|
||||
// 检查间隔:6 小时
|
||||
private static readonly TimeSpan CheckInterval = TimeSpan.FromHours(6);
|
||||
// 过期时间:24 小时未消费则清理
|
||||
private const int ExpireHours = 24;
|
||||
|
||||
public UploadCleanupJob(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// 启动后 5 分钟执行第一次(避免启动时并发),之后每 6 小时一次
|
||||
_timer = new Timer(DoWork, null, TimeSpan.FromMinutes(5), CheckInterval);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_timer?.Change(Timeout.Infinite, 0);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_timer?.Dispose();
|
||||
}
|
||||
|
||||
private async void DoWork(object? state)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var uploadTracking = scope.ServiceProvider.GetRequiredService<IUploadTrackingService>();
|
||||
await uploadTracking.CleanupExpiredAsync(ExpireHours);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 后台任务异常不应影响应用运行
|
||||
}
|
||||
}
|
||||
}
|
||||
332
backend/MilliSim.Api/Services/UserService.cs
Normal file
332
backend/MilliSim.Api/Services/UserService.cs
Normal file
@ -0,0 +1,332 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MilliSim.Common.Dtos;
|
||||
using MilliSim.Common.Entities;
|
||||
using MilliSim.Common.Models;
|
||||
using MilliSim.Data;
|
||||
using MilliSim.Services.Infrastructure;
|
||||
|
||||
namespace MilliSim.Services;
|
||||
|
||||
public class UserService : IUserService
|
||||
{
|
||||
private readonly MilliSimDbContext _db;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
private readonly IOperationLogService _operationLog;
|
||||
private readonly INotificationService _notification;
|
||||
private readonly IFileStorageService _fileStorage;
|
||||
private readonly IUploadTrackingService _uploadTracking;
|
||||
|
||||
public UserService(
|
||||
MilliSimDbContext db,
|
||||
ICurrentUserService currentUser,
|
||||
IOperationLogService operationLog,
|
||||
INotificationService notification,
|
||||
IFileStorageService fileStorage,
|
||||
IUploadTrackingService uploadTracking)
|
||||
{
|
||||
_db = db;
|
||||
_currentUser = currentUser;
|
||||
_operationLog = operationLog;
|
||||
_notification = notification;
|
||||
_fileStorage = fileStorage;
|
||||
_uploadTracking = uploadTracking;
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<PagedResult<UserDto>>> GetUsersAsync(UserQueryRequest request)
|
||||
{
|
||||
var query = _db.Users.AsQueryable();
|
||||
|
||||
// 销售数据隔离:销售只能看自己创建的客户
|
||||
if (_currentUser.Role == UserRole.Sales)
|
||||
{
|
||||
query = query.Where(u => u.CreatedBy == _currentUser.UserId);
|
||||
}
|
||||
|
||||
if (request.Role.HasValue)
|
||||
{
|
||||
query = query.Where(u => u.Role == request.Role.Value);
|
||||
}
|
||||
else if (request.StaffOnly)
|
||||
{
|
||||
// 员工管理页面:未指定具体角色时,排除客户角色
|
||||
query = query.Where(u => u.Role != UserRole.Customer);
|
||||
}
|
||||
|
||||
if (request.Status.HasValue)
|
||||
{
|
||||
query = query.Where(u => u.Status == request.Status.Value);
|
||||
}
|
||||
|
||||
if (request.CreatedBy.HasValue)
|
||||
{
|
||||
query = query.Where(u => u.CreatedBy == request.CreatedBy.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Keyword))
|
||||
{
|
||||
var keyword = request.Keyword.Trim();
|
||||
query = query.Where(u =>
|
||||
u.Username.Contains(keyword) ||
|
||||
u.Nickname.Contains(keyword) ||
|
||||
u.Phone.Contains(keyword) ||
|
||||
u.Email.Contains(keyword));
|
||||
}
|
||||
|
||||
var total = await query.LongCountAsync();
|
||||
|
||||
var users = await query
|
||||
.OrderByDescending(u => u.CreatedAt)
|
||||
.Skip((request.PageIndex - 1) * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
.ToListAsync();
|
||||
|
||||
// 预加载创建人名称
|
||||
var creatorIds = users.Where(u => u.CreatedBy.HasValue).Select(u => u.CreatedBy!.Value).Distinct().ToList();
|
||||
var creators = await _db.Users
|
||||
.Where(u => creatorIds.Contains(u.Id))
|
||||
.ToDictionaryAsync(u => u.Id, u => u.Nickname);
|
||||
|
||||
// 预加载客户授权信息(一个客户最多一条授权记录,历史数据可能多条,取最新一条)
|
||||
var userIds = users.Select(u => u.Id).ToList();
|
||||
var authList = await _db.Authorizations
|
||||
.Where(a => userIds.Contains(a.UserId))
|
||||
.ToListAsync();
|
||||
var authDict = authList
|
||||
.GroupBy(a => a.UserId)
|
||||
.ToDictionary(g => g.Key, g => g.OrderByDescending(a => a.CreatedAt).First());
|
||||
|
||||
var items = users.Select(u =>
|
||||
{
|
||||
var dto = AuthService.MapToDto(u, _fileStorage);
|
||||
if (u.CreatedBy.HasValue && creators.TryGetValue(u.CreatedBy.Value, out var name))
|
||||
{
|
||||
dto.CreatedByName = name;
|
||||
}
|
||||
if (authDict.TryGetValue(u.Id, out var auth))
|
||||
{
|
||||
dto.AuthorizationId = auth.Id;
|
||||
dto.AuthorizationStatus = auth.Status;
|
||||
dto.AuthorizationEndTime = auth.EndTime;
|
||||
}
|
||||
return dto;
|
||||
}).ToList();
|
||||
|
||||
var result = PagedResult<UserDto>.Create(items, total, request.PageIndex, request.PageSize);
|
||||
return ApiResponse<PagedResult<UserDto>>.Success(result);
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<UserDto>> GetUserByIdAsync(long id)
|
||||
{
|
||||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == id);
|
||||
if (user == null)
|
||||
{
|
||||
return ApiResponse<UserDto>.Fail("用户不存在", 404);
|
||||
}
|
||||
|
||||
// 销售数据隔离
|
||||
if (_currentUser.Role == UserRole.Sales && user.CreatedBy != _currentUser.UserId)
|
||||
{
|
||||
return ApiResponse<UserDto>.Fail("无权查看该用户", 403);
|
||||
}
|
||||
|
||||
var dto = AuthService.MapToDto(user, _fileStorage);
|
||||
if (user.CreatedBy.HasValue)
|
||||
{
|
||||
var creator = await _db.Users.FirstOrDefaultAsync(u => u.Id == user.CreatedBy.Value);
|
||||
if (creator != null)
|
||||
{
|
||||
dto.CreatedByName = creator.Nickname;
|
||||
}
|
||||
}
|
||||
|
||||
return ApiResponse<UserDto>.Success(dto);
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<UserDto>> CreateUserAsync(CreateUserRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Username))
|
||||
{
|
||||
return ApiResponse<UserDto>.Fail("用户名不能为空", 400);
|
||||
}
|
||||
|
||||
var exists = await _db.Users.AnyAsync(u => u.Username == request.Username);
|
||||
if (exists)
|
||||
{
|
||||
return ApiResponse<UserDto>.Fail("用户名已存在", 400);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Password))
|
||||
{
|
||||
return ApiResponse<UserDto>.Fail("密码不能为空", 400);
|
||||
}
|
||||
|
||||
var strengthError = AuthService.ValidatePasswordStrength(request.Password);
|
||||
if (strengthError != null)
|
||||
{
|
||||
return ApiResponse<UserDto>.Fail(strengthError, 400);
|
||||
}
|
||||
|
||||
var user = new User
|
||||
{
|
||||
Username = request.Username,
|
||||
Password = BCryptHelper.HashPassword(request.Password),
|
||||
Nickname = string.IsNullOrWhiteSpace(request.Nickname) ? request.Username : request.Nickname,
|
||||
Phone = request.Phone ?? string.Empty,
|
||||
Email = request.Email ?? string.Empty,
|
||||
Avatar = _fileStorage.ExtractKey(request.Avatar ?? string.Empty),
|
||||
Role = request.Role,
|
||||
Status = UserStatus.Active,
|
||||
CreatedBy = _currentUser.UserId
|
||||
};
|
||||
|
||||
_db.Users.Add(user);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// 消费上传跟踪(头像)
|
||||
if (!string.IsNullOrEmpty(request.Avatar))
|
||||
{
|
||||
await _uploadTracking.ConsumeAsync(_fileStorage.ExtractKey(request.Avatar));
|
||||
}
|
||||
|
||||
await _operationLog.LogAsync("用户管理", "创建用户", $"创建用户 {user.Username}({user.Role})");
|
||||
|
||||
// 给新用户发送欢迎通知
|
||||
try
|
||||
{
|
||||
await _notification.CreateNotificationAsync(
|
||||
NotificationType.Account,
|
||||
"欢迎加入平台",
|
||||
$"{user.Nickname},欢迎加入!您可以在项目中心浏览和体验已发布的项目,如有疑问请联系销售或客服。",
|
||||
userId: user.Id,
|
||||
eventId: $"user-welcome-{user.Id}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 通知发送失败不影响用户创建结果
|
||||
}
|
||||
|
||||
return ApiResponse<UserDto>.Success(AuthService.MapToDto(user, _fileStorage));
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<UserDto>> UpdateUserAsync(long id, UpdateUserRequest request)
|
||||
{
|
||||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == id);
|
||||
if (user == null)
|
||||
{
|
||||
return ApiResponse<UserDto>.Fail("用户不存在", 404);
|
||||
}
|
||||
|
||||
// 销售数据隔离
|
||||
if (_currentUser.Role == UserRole.Sales && user.CreatedBy != _currentUser.UserId)
|
||||
{
|
||||
return ApiResponse<UserDto>.Fail("无权修改该用户", 403);
|
||||
}
|
||||
|
||||
// 记录旧头像 key(用于替换时删除旧文件)
|
||||
var oldAvatarKey = _fileStorage.ExtractKey(user.Avatar);
|
||||
var newAvatarKey = oldAvatarKey;
|
||||
|
||||
if (request.Nickname != null)
|
||||
{
|
||||
user.Nickname = request.Nickname;
|
||||
}
|
||||
if (request.Avatar != null)
|
||||
{
|
||||
user.Avatar = _fileStorage.ExtractKey(request.Avatar);
|
||||
newAvatarKey = _fileStorage.ExtractKey(request.Avatar);
|
||||
}
|
||||
if (request.Phone != null)
|
||||
{
|
||||
user.Phone = request.Phone;
|
||||
}
|
||||
if (request.Email != null)
|
||||
{
|
||||
user.Email = request.Email;
|
||||
}
|
||||
|
||||
// 更新状态(所有 Staff 角色均可修改)
|
||||
user.Status = request.Status;
|
||||
|
||||
user.UpdatedAt = DateTime.Now;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// 头像替换:删除旧文件
|
||||
if (!string.IsNullOrEmpty(oldAvatarKey) && oldAvatarKey != newAvatarKey)
|
||||
{
|
||||
await _fileStorage.DeleteAsync(oldAvatarKey);
|
||||
}
|
||||
// 消费新上传的头像
|
||||
if (request.Avatar != null)
|
||||
{
|
||||
await _uploadTracking.ConsumeAsync(newAvatarKey);
|
||||
}
|
||||
|
||||
await _operationLog.LogAsync("用户管理", "更新用户", $"更新用户 {user.Username}");
|
||||
|
||||
return ApiResponse<UserDto>.Success(AuthService.MapToDto(user, _fileStorage));
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> DeleteUserAsync(long id)
|
||||
{
|
||||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == id);
|
||||
if (user == null)
|
||||
{
|
||||
return ApiResponse.Fail("用户不存在", 404);
|
||||
}
|
||||
|
||||
if (user.Id == _currentUser.UserId)
|
||||
{
|
||||
return ApiResponse.Fail("不能删除当前登录用户", 400);
|
||||
}
|
||||
|
||||
// 软删除
|
||||
user.IsDeleted = true;
|
||||
user.UpdatedAt = DateTime.Now;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
await _operationLog.LogAsync("用户管理", "删除用户", $"删除用户 {user.Username}");
|
||||
|
||||
return ApiResponse.Success("删除成功");
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> ResetPasswordAsync(long id)
|
||||
{
|
||||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == id);
|
||||
if (user == null)
|
||||
{
|
||||
return ApiResponse.Fail("用户不存在", 404);
|
||||
}
|
||||
|
||||
const string defaultPassword = "hm123456";
|
||||
user.Password = BCryptHelper.HashPassword(defaultPassword);
|
||||
user.UpdatedAt = DateTime.Now;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
await _operationLog.LogAsync("用户管理", "重置密码", $"重置用户 {user.Username} 的密码");
|
||||
|
||||
return ApiResponse.Success("密码已重置为 hm123456");
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> ToggleStatusAsync(long id)
|
||||
{
|
||||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == id);
|
||||
if (user == null)
|
||||
{
|
||||
return ApiResponse.Fail("用户不存在", 404);
|
||||
}
|
||||
|
||||
if (user.Id == _currentUser.UserId)
|
||||
{
|
||||
return ApiResponse.Fail("不能禁用当前登录用户", 400);
|
||||
}
|
||||
|
||||
user.Status = user.Status == UserStatus.Active ? UserStatus.Disabled : UserStatus.Active;
|
||||
user.UpdatedAt = DateTime.Now;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
var action = user.Status == UserStatus.Active ? "启用" : "禁用";
|
||||
await _operationLog.LogAsync("用户管理", "切换状态", $"{action}用户 {user.Username}");
|
||||
|
||||
return ApiResponse.Success($"{action}成功");
|
||||
}
|
||||
}
|
||||
8
backend/MilliSim.Api/appsettings.Development.json
Normal file
8
backend/MilliSim.Api/appsettings.Development.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
21
backend/MilliSim.Api/appsettings.json
Normal file
21
backend/MilliSim.Api/appsettings.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=sh-cdb-67f2jqfa.sql.tencentcdb.com;Port=27319;Database=millisim4;Uid=unity;Pwd=unity@1478;CharSet=utf8mb4;"
|
||||
},
|
||||
"Jwt": {
|
||||
"SecretKey": "MilliSim2026SecretKeyForJwtTokenGeneration_8f7e6d5c4b3a2918",
|
||||
"Issuer": "MilliSim",
|
||||
"Audience": "MilliSim",
|
||||
"ExpireDays": 7
|
||||
},
|
||||
"Cors": {
|
||||
"AllowedOrigins": [ "http://localhost:5173", "http://localhost:3000", "http://localhost:8080" ]
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
178
backend/deploy-to-iis.ps1
Normal file
178
backend/deploy-to-iis.ps1
Normal file
@ -0,0 +1,178 @@
|
||||
# ============================================================================
|
||||
# MilliSim 后端部署脚本(IIS 独立后端 + 反向代理架构)
|
||||
# 在部署服务器上以管理员身份运行 PowerShell 执行此脚本
|
||||
# 用法:.\deploy-to-iis.ps1 -DeployDir "C:\MilliSim\Backend" -ZipPath "C:\Deploy\MilliSim.Api.publish.clean.zip"
|
||||
# ============================================================================
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$DeployDir,
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$ZipPath,
|
||||
[string]$ServiceName = "MilliSimApi"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "================================================" -ForegroundColor Cyan
|
||||
Write-Host " MilliSim 后端部署脚本" -ForegroundColor Cyan
|
||||
Write-Host "================================================" -ForegroundColor Cyan
|
||||
Write-Host "部署目录: $DeployDir"
|
||||
Write-Host "发布包: $ZipPath"
|
||||
Write-Host "服务名: $ServiceName"
|
||||
Write-Host ""
|
||||
|
||||
# 1. 前置检查
|
||||
Write-Host "[1/7] 前置检查..." -ForegroundColor Yellow
|
||||
if (-not (Test-Path $ZipPath)) { throw "发布包不存在: $ZipPath" }
|
||||
if (-not (Test-Path $DeployDir)) { throw "部署目录不存在: $DeployDir" }
|
||||
if (-not (Test-Path "$DeployDir\MilliSim.Api.dll")) { throw "部署目录下未找到 MilliSim.Api.dll,确认是否正确的部署目录" }
|
||||
Write-Host " [OK] 检查通过" -ForegroundColor Green
|
||||
|
||||
# 2. 备份当前部署目录
|
||||
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
||||
$backupDir = "$DeployDir.bak_$timestamp"
|
||||
Write-Host "[2/7] 备份当前部署目录到: $backupDir" -ForegroundColor Yellow
|
||||
Copy-Item -Path $DeployDir -Destination $backupDir -Recurse -Force
|
||||
Write-Host " [OK] 备份完成" -ForegroundColor Green
|
||||
|
||||
# 3. 停止后端服务
|
||||
Write-Host "[3/7] 停止后端服务..." -ForegroundColor Yellow
|
||||
$serviceStopped = $false
|
||||
try {
|
||||
$svc = Get-Service -Name $ServiceName -ErrorAction Stop
|
||||
if ($svc.Status -eq "Running") {
|
||||
Stop-Service -Name $ServiceName -Force
|
||||
Start-Sleep -Seconds 2
|
||||
Write-Host " [OK] Windows 服务 '$ServiceName' 已停止" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " [SKIP] 服务已处于停止状态" -ForegroundColor Gray
|
||||
}
|
||||
$serviceStopped = $true
|
||||
} catch {
|
||||
Write-Host " [INFO] 未找到 Windows 服务 '$ServiceName',尝试查找 dotnet 进程..." -ForegroundColor Gray
|
||||
$procs = Get-Process -Name "dotnet","MilliSim.Api" -ErrorAction SilentlyContinue | Where-Object { $_.Path -like "*$DeployDir*" }
|
||||
if ($procs) {
|
||||
$procs | Stop-Process -Force
|
||||
Start-Sleep -Seconds 2
|
||||
Write-Host " [OK] 已终止 dotnet 进程: $($procs.Id -join ', ')" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " [INFO] 未找到运行中的后端进程" -ForegroundColor Gray
|
||||
}
|
||||
}
|
||||
|
||||
# 4. 备份当前 appsettings.json(保留服务器配置)
|
||||
$appSettingsPath = "$DeployDir\appsettings.json"
|
||||
$appSettingsBackup = "$DeployDir\appsettings.json.server_backup"
|
||||
if (Test-Path $appSettingsPath) {
|
||||
Copy-Item -Path $appSettingsPath -Destination $appSettingsBackup -Force
|
||||
Write-Host "[4/7] 已备份服务器 appsettings.json 到: $appSettingsBackup" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[4/7] 部署目录无 appsettings.json,跳过备份" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
# 5. 解压新代码(不删除 wwwroot/uploads 和 wwwroot/backups 的文件)
|
||||
Write-Host "[5/7] 解压新代码到部署目录..." -ForegroundColor Yellow
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
$zip = [System.IO.Compression.ZipFile]::OpenRead($ZipPath)
|
||||
try {
|
||||
foreach ($entry in $zip.Entries) {
|
||||
# 跳过 wwwroot/uploads 和 wwwroot/backups 下的所有文件(保留服务器上的上传文件)
|
||||
if ($entry.FullName -match "^wwwroot/uploads/" -and $entry.Length -gt 0) { continue }
|
||||
if ($entry.FullName -match "^wwwroot/backups/" -and $entry.Length -gt 0) { continue }
|
||||
|
||||
$targetPath = Join-Path $DeployDir $entry.FullName
|
||||
$targetDir = Split-Path $targetPath -Parent
|
||||
if (-not (Test-Path $targetDir)) { New-Item -ItemType Directory -Path $targetDir -Force | Out-Null }
|
||||
|
||||
if ($entry.Length -gt 0) {
|
||||
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $targetPath, $true)
|
||||
} elseif (-not (Test-Path $targetPath)) {
|
||||
New-Item -ItemType Directory -Path $targetPath -Force | Out-Null
|
||||
}
|
||||
}
|
||||
Write-Host " [OK] 解压完成(已保留 wwwroot/uploads 和 wwwroot/backups 中的文件)" -ForegroundColor Green
|
||||
} finally {
|
||||
$zip.Dispose()
|
||||
}
|
||||
|
||||
# 6. 恢复服务器 appsettings.json(如果存在备份)
|
||||
if (Test-Path $appSettingsBackup) {
|
||||
Copy-Item -Path $appSettingsBackup -Destination $appSettingsPath -Force
|
||||
Write-Host "[6/7] 已恢复服务器 appsettings.json(保留端口/连接字符串配置)" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[6/7] 未恢复 appsettings.json(使用发布包默认配置)" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
# 7. 启动后端服务
|
||||
Write-Host "[7/7] 启动后端服务..." -ForegroundColor Yellow
|
||||
if ($serviceStopped) {
|
||||
try {
|
||||
Start-Service -Name $ServiceName -ErrorAction Stop
|
||||
Start-Sleep -Seconds 3
|
||||
$svc = Get-Service -Name $ServiceName
|
||||
if ($svc.Status -eq "Running") {
|
||||
Write-Host " [OK] Windows 服务 '$ServiceName' 已启动" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " [WARN] 服务启动状态: $($svc.Status)" -ForegroundColor Yellow
|
||||
}
|
||||
} catch {
|
||||
Write-Host " [ERROR] 服务启动失败: $_" -ForegroundColor Red
|
||||
Write-Host " 请手动启动服务或检查日志" -ForegroundColor Yellow
|
||||
}
|
||||
} else {
|
||||
Write-Host " [INFO] 之前未通过 Windows 服务停止,请用原启动方式重启后端" -ForegroundColor Gray
|
||||
Write-Host " 例如: cd $DeployDir; dotnet MilliSim.Api.dll --urls http://localhost:30001" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
# 验证
|
||||
Write-Host ""
|
||||
Write-Host "================================================" -ForegroundColor Cyan
|
||||
Write-Host " 部署完成,开始验证" -ForegroundColor Cyan
|
||||
Write-Host "================================================" -ForegroundColor Cyan
|
||||
Start-Sleep -Seconds 5
|
||||
|
||||
Write-Host "[验证 1] 检查后端端口 30001 是否监听..." -ForegroundColor Yellow
|
||||
$conn = Test-NetConnection -ComputerName localhost -Port 30001 -WarningAction SilentlyContinue
|
||||
if ($conn.TcpTestSucceeded) {
|
||||
Write-Host " [OK] 端口 30001 监听正常" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " [FAIL] 端口 30001 未监听,请检查后端启动日志" -ForegroundColor Red
|
||||
}
|
||||
|
||||
Write-Host "[验证 2] 调用 /api/system/public-settings ..." -ForegroundColor Yellow
|
||||
try {
|
||||
$resp = Invoke-WebRequest -Uri "http://localhost:30001/api/system/public-settings" -UseBasicParsing -TimeoutSec 10
|
||||
Write-Host " [OK] HTTP $($resp.StatusCode)" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host " [FAIL] 请求失败: $_" -ForegroundColor Red
|
||||
}
|
||||
|
||||
Write-Host "[验证 3] 检查 /uploads/ 路径是否由后端服务 ..." -ForegroundColor Yellow
|
||||
try {
|
||||
$resp = Invoke-WebRequest -Uri "http://localhost:30001/uploads/" -UseBasicParsing -TimeoutSec 10
|
||||
Write-Host " [OK] HTTP $($resp.StatusCode)" -ForegroundColor Green
|
||||
} catch {
|
||||
$code = $_.Exception.Response.StatusCode.value__
|
||||
if ($code -eq 404) {
|
||||
Write-Host " [OK] HTTP 404(目录浏览禁用,符合预期)" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " [INFO] HTTP $code" -ForegroundColor Gray
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "================================================" -ForegroundColor Cyan
|
||||
Write-Host " 部署脚本结束" -ForegroundColor Cyan
|
||||
Write-Host "================================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "下一步:在浏览器访问前端页面,验证以下 4 个问题是否全部解决:" -ForegroundColor White
|
||||
Write-Host " 1. 员工管理页面头像加载(应返回 /uploads/avatars/xxx.png,不再请求 COS)"
|
||||
Write-Host " 2. 轮播图列表页图片加载"
|
||||
Write-Host " 3. 项目封面/视频加载"
|
||||
Write-Host " 4. WebGL 资源加载(.unityweb 文件应返回 200 + application/octet-stream)"
|
||||
Write-Host ""
|
||||
Write-Host "如需回滚:删除 $DeployDir,重命名 $backupDir 为 $DeployDir,重启服务" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
Write-Host "如服务器上缺少 COS 模式时上传的旧文件,可调用迁移端点补齐:"
|
||||
Write-Host " Invoke-WebRequest -Uri 'http://localhost:30001/api/system/migrate-cos-to-local' -Method POST -Headers @{Authorization='Bearer <admin token>'}"
|
||||
97
build.bat
Normal file
97
build.bat
Normal file
@ -0,0 +1,97 @@
|
||||
@echo off
|
||||
chcp 65001 >nul 2>&1
|
||||
cd /d "%~dp0"
|
||||
title MilliSim4 前后端打包工具
|
||||
echo ========================================
|
||||
echo MilliSim4 前后端打包脚本
|
||||
echo 后端 -^> publish\backend\
|
||||
echo 前端 -^> publish\frontend\
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
REM ===== 1. 停止后端进程(释放 bin 目录锁)=====
|
||||
echo [1/4] 停止后端进程...
|
||||
set "killed=0"
|
||||
for /f "tokens=5" %%a in ('netstat -ano ^| findstr ":5000 " ^| findstr "LISTENING"') do (
|
||||
echo - 停止进程 PID: %%a
|
||||
taskkill /F /PID %%a >nul 2>&1
|
||||
set "killed=1"
|
||||
)
|
||||
taskkill /F /IM MilliSim.Api.exe >nul 2>&1
|
||||
if "%killed%"=="1" (
|
||||
echo - 已停止后端进程,等待端口释放...
|
||||
timeout /t 2 /nobreak >nul
|
||||
) else (
|
||||
echo - 没有发现运行中的后端进程
|
||||
)
|
||||
echo.
|
||||
|
||||
REM ===== 2. 打包后端 (Release) =====
|
||||
echo [2/4] 打包后端 (Release)...
|
||||
if not exist "%~dp0backend\MilliSim.Api\MilliSim.Api.csproj" (
|
||||
echo [错误] 未找到后端项目文件!
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
dotnet publish "%~dp0backend\MilliSim.Api\MilliSim.Api.csproj" -c Release -o "%~dp0publish\backend" --nologo
|
||||
if %errorlevel% neq 0 (
|
||||
echo.
|
||||
echo [错误] 后端打包失败!请检查编译错误。
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo 后端打包完成 -^> publish\backend\
|
||||
echo.
|
||||
|
||||
REM ===== 3. 打包前端 (Production) =====
|
||||
echo [3/4] 打包前端 (Production)...
|
||||
if not exist "%~dp0frontend\package.json" (
|
||||
echo [错误] 未找到前端项目文件!
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
cd /d "%~dp0frontend"
|
||||
call npm run build
|
||||
if %errorlevel% neq 0 (
|
||||
echo.
|
||||
echo [错误] 前端打包失败!请检查编译错误。
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
cd /d "%~dp0"
|
||||
echo 前端编译完成
|
||||
echo.
|
||||
|
||||
REM ===== 4. 复制前端产物到 publish\frontend =====
|
||||
echo [4/4] 复制前端产物到 publish\frontend\...
|
||||
|
||||
REM 清空旧的产物目录
|
||||
if exist "%~dp0publish\frontend\" rmdir /s /q "%~dp0publish\frontend\" 2>nul
|
||||
mkdir "%~dp0publish\frontend\" 2>nul
|
||||
|
||||
REM 复制新产物(含 frontend\public\web.config,Vite 构建时会自动拷贝到 dist\)
|
||||
xcopy "%~dp0frontend\dist\*" "%~dp0publish\frontend\" /E /Y /Q >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo [错误] 复制前端产物失败!
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo - 已复制前端产物(含 web.config)
|
||||
echo.
|
||||
|
||||
REM ===== 完成 =====
|
||||
echo ========================================
|
||||
echo 打包完成!
|
||||
echo ========================================
|
||||
echo.
|
||||
echo 后端产物: %~dp0publish\backend\
|
||||
echo 前端产物: %~dp0publish\frontend\
|
||||
echo.
|
||||
echo 部署说明:
|
||||
echo 1. 服务器需安装 .NET 10 Hosting Bundle
|
||||
echo 2. 服务器需安装 IIS URL Rewrite 模块
|
||||
echo 3. 后端:publish\backend\ 部署为 IIS 站点(应用池: 无托管代码)
|
||||
echo 4. 前端:publish\frontend\ 部署为独立 IIS 站点
|
||||
echo 5. 配置前端的 API 反向代理指向后端地址
|
||||
echo.
|
||||
pause
|
||||
11
frontend/auto-imports.d.ts
vendored
Normal file
11
frontend/auto-imports.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const ElMessage: typeof import('element-plus/es')['ElMessage']
|
||||
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
|
||||
}
|
||||
64
frontend/components.d.ts
vendored
Normal file
64
frontend/components.d.ts
vendored
Normal file
@ -0,0 +1,64 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
CardContainer: typeof import('./src/components/CardContainer.vue')['default']
|
||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
|
||||
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCarousel: typeof import('element-plus/es')['ElCarousel']
|
||||
ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
EmptyState: typeof import('./src/components/EmptyState.vue')['default']
|
||||
IconUpload: typeof import('./src/components/IconUpload.vue')['default']
|
||||
PageHeader: typeof import('./src/components/PageHeader.vue')['default']
|
||||
ProjectCard: typeof import('./src/components/ProjectCard.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SearchBar: typeof import('./src/components/SearchBar.vue')['default']
|
||||
StatCard: typeof import('./src/components/StatCard.vue')['default']
|
||||
SvgIcon: typeof import('./src/components/SvgIcon.vue')['default']
|
||||
TabFilter: typeof import('./src/components/TabFilter.vue')['default']
|
||||
VideoUpload: typeof import('./src/components/VideoUpload.vue')['default']
|
||||
Watermark: typeof import('./src/components/Watermark.vue')['default']
|
||||
}
|
||||
export interface ComponentCustomProperties {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
}
|
||||
}
|
||||
21
frontend/env.d.ts
vendored
Normal file
21
frontend/env.d.ts
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
declare module '@wangeditor/editor-for-vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const Editor: DefineComponent<any, any, any>
|
||||
const Toolbar: DefineComponent<any, any, any>
|
||||
export { Editor, Toolbar }
|
||||
}
|
||||
|
||||
declare module '@wangeditor/editor' {
|
||||
export const IDomEditor: any
|
||||
export const IEditorConfig: any
|
||||
export const createEditor: any
|
||||
export const createToolbar: any
|
||||
}
|
||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>毫秒数仿 - 专业虚拟仿真平台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
3660
frontend/package-lock.json
generated
Normal file
3660
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
frontend/package.json
Normal file
36
frontend/package.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "millisim-frontend",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"axios": "^1.7.9",
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^5.5.1",
|
||||
"element-plus": "^2.9.1",
|
||||
"pinia": "^2.3.0",
|
||||
"video.js": "^8.21.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-echarts": "^7.0.3",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^26.0.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"sass": "^1.83.0",
|
||||
"typescript": "~5.7.2",
|
||||
"unplugin-auto-import": "^0.19.0",
|
||||
"unplugin-vue-components": "^0.28.0",
|
||||
"vite": "^6.0.5",
|
||||
"vue-tsc": "^2.2.0"
|
||||
}
|
||||
}
|
||||
76
frontend/public/web.config
Normal file
76
frontend/public/web.config
Normal file
@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<system.webServer>
|
||||
<!-- Remove WebDAV module AND handler: WebDAV intercepts PUT/DELETE causing 405
|
||||
The MODULE (not just handler) must be removed, otherwise WebDAV intercepts
|
||||
PUT/DELETE at the module stage before URL Rewrite can proxy the request -->
|
||||
<modules>
|
||||
<remove name="WebDAVModule" />
|
||||
</modules>
|
||||
<handlers>
|
||||
<remove name="WebDAV" />
|
||||
</handlers>
|
||||
|
||||
<!-- Allow large uploads (2GB) + all HTTP verbs (PUT/DELETE) -->
|
||||
<security>
|
||||
<requestFiltering>
|
||||
<verbs allowUnlisted="true" />
|
||||
<requestLimits maxAllowedContentLength="2147483648" />
|
||||
</requestFiltering>
|
||||
</security>
|
||||
|
||||
<!-- Reverse proxy rules + SPA fallback
|
||||
NOTE: ARR proxy must be enabled at SERVER level (one-time):
|
||||
Set-WebConfigurationProperty -Filter system.webServer/proxy -name enabled -value true -PSPath 'IIS:\'
|
||||
Do NOT put <proxy enabled="true" /> here - it causes 0x8007000d when server-level proxy is off -->
|
||||
<rewrite>
|
||||
<rules>
|
||||
<!-- Proxy API requests to backend Kestrel on port 30001 -->
|
||||
<rule name="API Proxy" stopProcessing="true">
|
||||
<match url="^api/(.*)" />
|
||||
<action type="Rewrite" url="http://localhost:30001/api/{R:1}" />
|
||||
</rule>
|
||||
<!-- Proxy /uploads/ to backend (Local storage mode serves files via backend) -->
|
||||
<rule name="Uploads Proxy" stopProcessing="true">
|
||||
<match url="^uploads/(.*)" />
|
||||
<action type="Rewrite" url="http://localhost:30001/uploads/{R:1}" />
|
||||
</rule>
|
||||
<!-- Block /backups/ direct access: DB backups only via authenticated API -->
|
||||
<rule name="Block Backups" stopProcessing="true">
|
||||
<match url="^backups/(.*)" />
|
||||
<action type="CustomResponse" statusCode="404" statusReason="Not Found" />
|
||||
</rule>
|
||||
<!-- SPA fallback: non-file, non-directory requests go to index.html -->
|
||||
<rule name="SPA Fallback" stopProcessing="true">
|
||||
<match url=".*" />
|
||||
<conditions logicalGrouping="MatchAll">
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
|
||||
<add input="{REQUEST_URI}" pattern="^/api/" negate="true" />
|
||||
<add input="{REQUEST_URI}" pattern="^/uploads/" negate="true" />
|
||||
</conditions>
|
||||
<action type="Rewrite" url="index.html" />
|
||||
</rule>
|
||||
</rules>
|
||||
</rewrite>
|
||||
|
||||
<!-- Static content MIME types (including Unity WebGL .unityweb) -->
|
||||
<staticContent>
|
||||
<remove fileExtension=".woff" />
|
||||
<remove fileExtension=".woff2" />
|
||||
<remove fileExtension=".mjs" />
|
||||
<remove fileExtension=".unityweb" />
|
||||
<mimeMap fileExtension=".woff" mimeType="application/font-woff" />
|
||||
<mimeMap fileExtension=".woff2" mimeType="application/font-woff2" />
|
||||
<mimeMap fileExtension=".mjs" mimeType="application/javascript" />
|
||||
<mimeMap fileExtension=".unityweb" mimeType="application/octet-stream" />
|
||||
</staticContent>
|
||||
|
||||
<defaultDocument>
|
||||
<files>
|
||||
<clear />
|
||||
<add value="index.html" />
|
||||
</files>
|
||||
</defaultDocument>
|
||||
</system.webServer>
|
||||
</configuration>
|
||||
19
frontend/src/App.vue
Normal file
19
frontend/src/App.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
onMounted(() => {
|
||||
// 登录页不自动拉取 profile,避免旧 token 401 干扰新登录流程
|
||||
const path = window.location.pathname
|
||||
const isLoginPage = path === '/login' || path === '/admin/login'
|
||||
if (userStore.isLoggedIn && !isLoginPage) {
|
||||
userStore.fetchProfile()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
260
frontend/src/api/index.ts
Normal file
260
frontend/src/api/index.ts
Normal file
@ -0,0 +1,260 @@
|
||||
/**
|
||||
* API 接口统一管理模块
|
||||
* 按业务模块组织所有后端接口调用,统一通过 http 工具发起请求
|
||||
*/
|
||||
import { http } from '@/utils/request'
|
||||
import type { ApiResponse, PagedResult, PageRequest } from '@/types'
|
||||
import type {
|
||||
User, LoginRequest, LoginResponse, Project, ProjectQuery, Category,
|
||||
Platform, Tag, Authorization, Banner, Partner, Consultation,
|
||||
Notification, SystemSetting, PublicSettings, OperationLog, Backup, WakeProtocol,
|
||||
DashboardData, Package, CreateAuthorizationParams, BatchAuthorizationParams,
|
||||
AboutSettings, FactoryResetResult, ExperienceLog,
|
||||
} from '@/types'
|
||||
|
||||
// ===== 认证 API:负责登录、获取用户信息、修改资料等 =====
|
||||
export const authApi = {
|
||||
// 获取图形验证码
|
||||
getCaptcha: () => http.get<ApiResponse<{ captchaId: string; svg: string }>>('/auth/captcha'),
|
||||
// 用户登录
|
||||
login: (data: LoginRequest) => http.post<ApiResponse<LoginResponse>>('/auth/login', data),
|
||||
// 获取当前登录用户信息
|
||||
getProfile: () => http.get<ApiResponse<User>>('/auth/profile'),
|
||||
// 更新当前用户资料
|
||||
updateProfile: (data: Partial<User>) => http.put<ApiResponse<User>>('/auth/profile', data),
|
||||
// 修改登录密码
|
||||
changePassword: (data: { oldPassword: string; newPassword: string }) =>
|
||||
http.post<ApiResponse>('/auth/change-password', data),
|
||||
}
|
||||
|
||||
// ===== 用户 API:后台用户管理相关接口 =====
|
||||
export const userApi = {
|
||||
// 分页查询用户列表
|
||||
getList: (params: any) => http.get<ApiResponse<PagedResult<User>>>('/users', params),
|
||||
// 根据 ID 获取用户详情
|
||||
getById: (id: number) => http.get<ApiResponse<User>>(`/users/${id}`),
|
||||
// 创建新用户
|
||||
create: (data: any) => http.post<ApiResponse<User>>('/users', data),
|
||||
// 更新用户信息
|
||||
update: (id: number, data: any) => http.put<ApiResponse<User>>(`/users/${id}`, data),
|
||||
// 删除用户
|
||||
delete: (id: number) => http.delete<ApiResponse>(`/users/${id}`),
|
||||
// 重置用户密码
|
||||
resetPassword: (id: number) => http.post<ApiResponse>(`/users/${id}/reset-password`),
|
||||
// 切换用户启用/禁用状态
|
||||
toggleStatus: (id: number) => http.post<ApiResponse>(`/users/${id}/toggle-status`),
|
||||
}
|
||||
|
||||
// ===== 项目 API:包含前台展示与后台管理两类接口 =====
|
||||
export const projectApi = {
|
||||
// ---- 前台接口 ----
|
||||
// 获取已发布项目列表(公开)
|
||||
getPublicList: (params: ProjectQuery) =>
|
||||
http.get<ApiResponse<PagedResult<Project>>>('/projects/public', params),
|
||||
// 获取项目详情(公开)
|
||||
getPublicById: (id: number) => http.get<ApiResponse<Project>>(`/projects/public/${id}`),
|
||||
// 获取相关推荐项目
|
||||
getRelated: (id: number) => http.get<ApiResponse<Project[]>>(`/projects/related/${id}`),
|
||||
// 切换项目收藏状态
|
||||
toggleFavorite: (projectId: number) => http.post<ApiResponse>(`/projects/${projectId}/favorite`),
|
||||
// 获取当前用户收藏的项目列表
|
||||
getFavorites: (params: PageRequest) =>
|
||||
http.get<ApiResponse<PagedResult<Project>>>('/projects/favorites', params),
|
||||
// 记录用户体验时长
|
||||
recordExperience: (projectId: number, durationSeconds: number) =>
|
||||
http.post<ApiResponse>(`/projects/${projectId}/experience`, { durationSeconds }),
|
||||
// 获取用户体验记录
|
||||
getExperienceLogs: (params: PageRequest) =>
|
||||
http.get<ApiResponse<PagedResult<ExperienceLog>>>('/projects/experience-logs', params),
|
||||
|
||||
// ---- 后台接口 ----
|
||||
// 分页查询项目列表(后台)
|
||||
getList: (params: any) => http.get<ApiResponse<PagedResult<Project>>>('/projects', params),
|
||||
// 根据 ID 获取项目详情(后台)
|
||||
getById: (id: number) => http.get<ApiResponse<Project>>(`/projects/${id}`),
|
||||
// 创建新项目
|
||||
create: (data: any) => http.post<ApiResponse<Project>>('/projects', data),
|
||||
// 更新项目信息
|
||||
update: (id: number, data: any) => http.put<ApiResponse<Project>>(`/projects/${id}`, data),
|
||||
// 删除项目
|
||||
delete: (id: number) => http.delete<ApiResponse>(`/projects/${id}`),
|
||||
// 切换项目发布状态
|
||||
togglePublish: (id: number) => http.post<ApiResponse>(`/projects/${id}/toggle-publish`),
|
||||
// 切换项目精选状态
|
||||
toggleFeatured: (id: number) => http.post<ApiResponse>(`/projects/${id}/toggle-featured`),
|
||||
// 获取项目的安装包列表
|
||||
getPackages: (projectId: number) =>
|
||||
http.get<ApiResponse<Package[]>>(`/projects/${projectId}/packages`),
|
||||
// 上传项目安装包
|
||||
uploadPackage: (projectId: number, formData: FormData) =>
|
||||
http.upload<ApiResponse<Package>>(`/projects/${projectId}/packages`, formData),
|
||||
// 删除安装包
|
||||
deletePackage: (id: number) => http.delete<ApiResponse>(`/projects/packages/${id}`),
|
||||
// 获取 WebGL 在线运行入口 URL(解压后的 index.html 签名 URL)
|
||||
getWebGLRunUrl: (projectId: number) =>
|
||||
http.get<ApiResponse<string>>(`/projects/${projectId}/webgl-run-url`),
|
||||
}
|
||||
|
||||
// ===== 分类 API:项目分类的增删改查 =====
|
||||
export const categoryApi = {
|
||||
// 获取全部分类
|
||||
getList: () => http.get<ApiResponse<Category[]>>('/categories'),
|
||||
// 创建分类
|
||||
create: (data: any) => http.post<ApiResponse<Category>>('/categories', data),
|
||||
// 更新分类
|
||||
update: (id: number, data: any) => http.put<ApiResponse<Category>>(`/categories/${id}`, data),
|
||||
// 删除分类
|
||||
delete: (id: number) => http.delete<ApiResponse>(`/categories/${id}`),
|
||||
}
|
||||
|
||||
// ===== 平台 API:支持平台的增删改查 =====
|
||||
export const platformApi = {
|
||||
// 获取全部平台
|
||||
getList: () => http.get<ApiResponse<Platform[]>>('/platforms'),
|
||||
// 创建平台
|
||||
create: (data: any) => http.post<ApiResponse<Platform>>('/platforms', data),
|
||||
// 更新平台
|
||||
update: (id: number, data: any) => http.put<ApiResponse<Platform>>(`/platforms/${id}`, data),
|
||||
// 删除平台
|
||||
delete: (id: number) => http.delete<ApiResponse>(`/platforms/${id}`),
|
||||
}
|
||||
|
||||
// ===== 标签 API:项目标签的增删改查 =====
|
||||
export const tagApi = {
|
||||
// 获取全部标签
|
||||
getList: () => http.get<ApiResponse<Tag[]>>('/tags'),
|
||||
// 创建标签
|
||||
create: (data: any) => http.post<ApiResponse<Tag>>('/tags', data),
|
||||
// 更新标签
|
||||
update: (id: number, data: any) => http.put<ApiResponse<Tag>>(`/tags/${id}`, data),
|
||||
// 删除标签
|
||||
delete: (id: number) => http.delete<ApiResponse>(`/tags/${id}`),
|
||||
}
|
||||
|
||||
// ===== 授权 API:客户级授权管理(授权后可访问全部项目) =====
|
||||
export const authorizationApi = {
|
||||
// 分页查询授权列表
|
||||
getList: (params: any) =>
|
||||
http.get<ApiResponse<PagedResult<Authorization>>>('/authorizations', params),
|
||||
// 获取授权详情
|
||||
getById: (id: number) => http.get<ApiResponse<Authorization>>(`/authorizations/${id}`),
|
||||
// 创建授权(客户级授权,参数不含 projectId)
|
||||
create: (data: CreateAuthorizationParams) =>
|
||||
http.post<ApiResponse<Authorization>>('/authorizations', data),
|
||||
// 批量创建授权(客户级授权,参数不含 projectIds)
|
||||
batch: (data: BatchAuthorizationParams) =>
|
||||
http.post<ApiResponse>('/authorizations/batch', data),
|
||||
// 续期授权
|
||||
renew: (id: number, days: number) =>
|
||||
http.post<ApiResponse<Authorization>>(`/authorizations/${id}/renew`, { days }),
|
||||
// 取消授权
|
||||
cancel: (id: number) => http.post<ApiResponse>(`/authorizations/${id}/cancel`),
|
||||
// 检查用户是否拥有授权(客户级授权,只需传 userId)
|
||||
check: (userId: number) =>
|
||||
http.get<ApiResponse<boolean>>('/authorizations/check', { userId }),
|
||||
// 获取当前用户的授权列表
|
||||
getMy: () => http.get<ApiResponse<Authorization[]>>('/authorizations/my'),
|
||||
}
|
||||
|
||||
// ===== 内容 API:轮播图、合作伙伴、咨询消息等运营内容 =====
|
||||
export const contentApi = {
|
||||
// ---- 轮播图 ----
|
||||
// 获取轮播图列表,onlyActive 为 true 时只返回启用项
|
||||
getBanners: (onlyActive = false) =>
|
||||
http.get<ApiResponse<Banner[]>>('/banners', { onlyActive }),
|
||||
createBanner: (data: any) => http.post<ApiResponse<Banner>>('/banners', data),
|
||||
updateBanner: (id: number, data: any) => http.put<ApiResponse<Banner>>(`/banners/${id}`, data),
|
||||
deleteBanner: (id: number) => http.delete<ApiResponse>(`/banners/${id}`),
|
||||
|
||||
// ---- 合作伙伴 ----
|
||||
// 获取合作伙伴列表,onlyVisible 为 true 时只返回可见项
|
||||
getPartners: (onlyVisible = false) =>
|
||||
http.get<ApiResponse<Partner[]>>('/partners', { onlyVisible }),
|
||||
createPartner: (data: any) => http.post<ApiResponse<Partner>>('/partners', data),
|
||||
updatePartner: (id: number, data: any) => http.put<ApiResponse<Partner>>(`/partners/${id}`, data),
|
||||
deletePartner: (id: number) => http.delete<ApiResponse>(`/partners/${id}`),
|
||||
|
||||
// ---- 咨询 ----
|
||||
// 分页获取咨询消息
|
||||
getConsultations: (params: PageRequest) =>
|
||||
http.get<ApiResponse<PagedResult<Consultation>>>('/consultations', params),
|
||||
// 提交咨询消息
|
||||
createConsultation: (data: any) => http.post<ApiResponse<Consultation>>('/consultations', data),
|
||||
// 标记咨询消息为已读
|
||||
markConsultationRead: (id: number) => http.post<ApiResponse>(`/consultations/${id}/read`),
|
||||
// 删除咨询消息
|
||||
deleteConsultation: (id: number) => http.delete<ApiResponse>(`/consultations/${id}`),
|
||||
}
|
||||
|
||||
// ===== 通知 API:系统消息通知管理 =====
|
||||
export const notificationApi = {
|
||||
// 分页获取通知列表
|
||||
getList: (params: any) =>
|
||||
http.get<ApiResponse<PagedResult<Notification>>>('/notifications', params),
|
||||
// 获取未读通知数量
|
||||
getUnreadCount: () => http.get<ApiResponse<number>>('/notifications/unread-count'),
|
||||
// 标记单条通知为已读
|
||||
markAsRead: (id: number) => http.post<ApiResponse>(`/notifications/${id}/read`),
|
||||
// 标记全部通知为已读
|
||||
markAllAsRead: () => http.post<ApiResponse>('/notifications/read-all'),
|
||||
// 删除通知
|
||||
delete: (id: number) => http.delete<ApiResponse>(`/notifications/${id}`),
|
||||
}
|
||||
|
||||
// ===== 系统 API:系统设置、日志、备份、唤醒协议等 =====
|
||||
export const systemApi = {
|
||||
// 获取全部系统设置
|
||||
getSettings: () => http.get<ApiResponse<SystemSetting[]>>('/system/settings'),
|
||||
// 批量更新系统设置
|
||||
updateSettings: (data: any) => http.put<ApiResponse>('/system/settings', data),
|
||||
// 根据 key 获取单项系统设置
|
||||
getSetting: (key: string) => http.get<ApiResponse<SystemSetting>>(`/system/settings/${key}`),
|
||||
// 获取公开系统设置(无需鉴权):返回平台名、Logo、备案号等
|
||||
getPublicSettings: () => http.get<ApiResponse<PublicSettings>>('/system/public-settings'),
|
||||
// 分页查询操作日志
|
||||
getLogs: (params: any) =>
|
||||
http.get<ApiResponse<PagedResult<OperationLog>>>('/system/logs', params),
|
||||
// 清理日志,可指定清理指定日期之前的记录
|
||||
clearLogs: (beforeDate?: string) => http.delete<ApiResponse>('/system/logs', { beforeDate }),
|
||||
// 获取数据备份列表
|
||||
getBackups: () => http.get<ApiResponse<Backup[]>>('/system/backups'),
|
||||
// 创建数据备份
|
||||
createBackup: () => http.post<ApiResponse<Backup>>('/system/backups'),
|
||||
// 删除数据备份
|
||||
deleteBackup: (id: number) => http.delete<ApiResponse>(`/system/backups/${id}`),
|
||||
// 获取唤醒协议列表
|
||||
getWakeProtocols: () => http.get<ApiResponse<WakeProtocol[]>>('/system/wake-protocols'),
|
||||
// 创建唤醒协议
|
||||
createWakeProtocol: (data: any) =>
|
||||
http.post<ApiResponse<WakeProtocol>>('/system/wake-protocols', data),
|
||||
// 更新唤醒协议
|
||||
updateWakeProtocol: (id: number, data: any) =>
|
||||
http.put<ApiResponse<WakeProtocol>>(`/system/wake-protocols/${id}`, data),
|
||||
// 删除唤醒协议
|
||||
deleteWakeProtocol: (id: number) =>
|
||||
http.delete<ApiResponse>(`/system/wake-protocols/${id}`),
|
||||
// 通用文件上传接口(返回 url 用于预览 + key 用于跟踪清理)
|
||||
upload: (formData: FormData) =>
|
||||
http.upload<ApiResponse<{ url: string; key: string }>>('/system/upload', formData),
|
||||
// 删除未使用的上传文件(仅限 pending 状态,已被实体消费的不会被删)
|
||||
deleteUpload: (key: string) => http.delete<ApiResponse>('/system/upload', { key }),
|
||||
// 获取关于我们设置(公开端点,前台 About 页消费)
|
||||
getAbout: () => http.get<ApiResponse<AboutSettings>>('/system/about'),
|
||||
// 更新关于我们设置(仅管理员)
|
||||
updateAbout: (data: AboutSettings) => http.put<ApiResponse>('/system/about', data),
|
||||
// 恢复出厂设置:清空所有业务数据 + 删除 COS 资源
|
||||
factoryReset: (confirmText: string) =>
|
||||
http.post<ApiResponse<FactoryResetResult>>('/system/factory-reset', { confirmText }),
|
||||
// 测试本地存储目录是否可读写(保存存储配置前校验)
|
||||
testLocalPath: (localBasePath: string) =>
|
||||
http.post<ApiResponse>('/system/test-local-path', undefined, { params: { localBasePath } }),
|
||||
// 将 COS 中所有文件迁移到本地存储(补齐切换模式前的旧文件)
|
||||
migrateCosToLocal: () =>
|
||||
http.post<ApiResponse>('/system/migrate-cos-to-local'),
|
||||
}
|
||||
|
||||
// ===== 工作台 API:后台首页数据看板 =====
|
||||
export const dashboardApi = {
|
||||
// 获取工作台统计数据
|
||||
getData: () => http.get<ApiResponse<DashboardData>>('/dashboard'),
|
||||
}
|
||||
237
frontend/src/components/CardContainer.vue
Normal file
237
frontend/src/components/CardContainer.vue
Normal file
@ -0,0 +1,237 @@
|
||||
/**
|
||||
* 卡片容器组件
|
||||
* 用于管理后台的卡片式布局
|
||||
* 支持网格布局、悬停效果、操作按钮
|
||||
*/
|
||||
<template>
|
||||
<div class="card-grid" :style="gridStyle">
|
||||
<div
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
class="data-card"
|
||||
:class="{ 'data-card--clickable': clickable }"
|
||||
@click="clickable && $emit('card-click', item)"
|
||||
>
|
||||
<!-- 卡片头部 -->
|
||||
<div v-if="$slots.header || item.image || item.icon" class="card-header">
|
||||
<slot name="header" :item="item">
|
||||
<div v-if="item.image" class="card-image">
|
||||
<img :src="item.image" :alt="item.title" />
|
||||
</div>
|
||||
<div v-else-if="item.icon" class="card-icon" :style="{ background: item.iconBg || 'var(--gradient-primary)' }">
|
||||
<SvgIcon :name="item.icon" :size="24" :color="'#FFFFFF'" />
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<!-- 卡片内容 -->
|
||||
<div class="card-body">
|
||||
<slot :item="item">
|
||||
<h3 class="card-title">{{ item.title }}</h3>
|
||||
<p v-if="item.description" class="card-desc">{{ item.description }}</p>
|
||||
<div v-if="item.tags && item.tags.length" class="card-tags">
|
||||
<span v-for="tag in item.tags" :key="tag" class="card-tag">{{ tag }}</span>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<!-- 卡片底部操作 -->
|
||||
<div v-if="$slots.actions" class="card-actions">
|
||||
<slot name="actions" :item="item" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="items.length === 0" class="card-empty">
|
||||
<SvgIcon name="inbox" :size="48" :color="'#D1D5DB'" />
|
||||
<p class="empty-text">{{ emptyText }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import SvgIcon from './SvgIcon.vue'
|
||||
|
||||
/** 卡片数据项接口 */
|
||||
export interface CardItem {
|
||||
id: string | number
|
||||
title?: string
|
||||
description?: string
|
||||
image?: string
|
||||
icon?: string
|
||||
iconBg?: string
|
||||
tags?: string[]
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
/** 卡片数据列表 */
|
||||
items: CardItem[]
|
||||
/** 每行卡片数量 */
|
||||
columns?: number
|
||||
/** 卡片间距(px) */
|
||||
gap?: number
|
||||
/** 是否可点击 */
|
||||
clickable?: boolean
|
||||
/** 空状态文本 */
|
||||
emptyText?: string
|
||||
}>(), {
|
||||
columns: 3,
|
||||
gap: 16,
|
||||
clickable: false,
|
||||
emptyText: '暂无数据'
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
'card-click': [item: CardItem]
|
||||
}>()
|
||||
|
||||
/** 网格样式 */
|
||||
const gridStyle = computed(() => ({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${props.columns}, 1fr)`,
|
||||
gap: `${props.gap}px`
|
||||
}))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-grid {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.data-card {
|
||||
background: var(--color-bg-elevated);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--color-border-light);
|
||||
overflow: hidden;
|
||||
transition: all var(--transition-base);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.data-card--clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.data-card--clickable:hover {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: var(--shadow-lg);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-image {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
overflow: hidden;
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.card-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform var(--transition-slow);
|
||||
}
|
||||
|
||||
.data-card--clickable:hover .card-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: var(--radius-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 16px 0 0 16px;
|
||||
box-shadow: var(--shadow-primary);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 16px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.card-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--color-primary);
|
||||
background: var(--color-primary-light);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
padding: 8px 16px;
|
||||
border-top: 1px solid var(--color-border-light);
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.card-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-placeholder);
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 1200px) {
|
||||
.card-grid {
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.card-grid {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user