Browse Source
- Change build:* naming from hyphen to colon (build:web-dev → build:web:dev) - Add missing build:web:test and build:web:prod scripts - Update build:electron:dev to include electron startup (build + start) - Remove hardcoded --mode electron to allow proper mode override - Add comprehensive mode override behavior documentation - Fix mode conflicts between hardcoded and passed --mode arguments The plan now properly supports: - Development builds with default --mode development - Testing builds with explicit --mode test override - Production builds with explicit --mode production override - Consistent naming across all platforms (web, capacitor, electron)web-serve-fix
11 changed files with 4209 additions and 0 deletions
@ -0,0 +1,310 @@ |
|||||
|
# Cursor Markdown Ruleset for TimeSafari Documentation |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
This ruleset enforces consistent markdown formatting standards across all project |
||||
|
documentation, ensuring readability, maintainability, and compliance with |
||||
|
markdownlint best practices. |
||||
|
|
||||
|
## General Formatting Standards |
||||
|
|
||||
|
### Line Length |
||||
|
|
||||
|
- **Maximum line length**: 80 characters |
||||
|
- **Exception**: Code blocks (JSON, shell, TypeScript, etc.) - no line length |
||||
|
enforcement |
||||
|
- **Rationale**: Ensures readability across different screen sizes and terminal |
||||
|
widths |
||||
|
|
||||
|
### Blank Lines |
||||
|
|
||||
|
- **Headings**: Must be surrounded by blank lines above and below |
||||
|
- **Lists**: Must be surrounded by blank lines above and below |
||||
|
- **Code blocks**: Must be surrounded by blank lines above and below |
||||
|
- **Maximum consecutive blank lines**: 1 (no multiple blank lines) |
||||
|
- **File start**: No blank lines at the beginning of the file |
||||
|
- **File end**: Single newline character at the end |
||||
|
|
||||
|
### Whitespace |
||||
|
|
||||
|
- **No trailing spaces**: Remove all trailing whitespace from lines |
||||
|
- **No tabs**: Use spaces for indentation |
||||
|
- **Consistent indentation**: 2 spaces for list items and nested content |
||||
|
|
||||
|
## Heading Standards |
||||
|
|
||||
|
### Format |
||||
|
|
||||
|
- **Style**: ATX-style headings (`#`, `##`, `###`, etc.) |
||||
|
- **Case**: Title case for general headings |
||||
|
- **Code references**: Use backticks for file names and technical terms |
||||
|
- ✅ `### Current package.json Scripts` |
||||
|
- ❌ `### Current Package.json Scripts` |
||||
|
|
||||
|
### Hierarchy |
||||
|
|
||||
|
- **H1 (#)**: Document title only |
||||
|
- **H2 (##)**: Major sections |
||||
|
- **H3 (###)**: Subsections |
||||
|
- **H4 (####)**: Sub-subsections |
||||
|
- **H5+**: Avoid deeper nesting |
||||
|
|
||||
|
## List Standards |
||||
|
|
||||
|
### Unordered Lists |
||||
|
|
||||
|
- **Marker**: Use `-` (hyphen) consistently |
||||
|
- **Indentation**: 2 spaces for nested items |
||||
|
- **Blank lines**: Surround lists with blank lines |
||||
|
|
||||
|
### Ordered Lists |
||||
|
|
||||
|
- **Format**: `1.`, `2.`, `3.` (sequential numbering) |
||||
|
- **Indentation**: 2 spaces for nested items |
||||
|
- **Blank lines**: Surround lists with blank lines |
||||
|
|
||||
|
### Task Lists |
||||
|
|
||||
|
- **Format**: `- [ ]` for incomplete, `- [x]` for complete |
||||
|
- **Use case**: Project planning, checklists, implementation tracking |
||||
|
|
||||
|
## Code Block Standards |
||||
|
|
||||
|
### Fenced Code Blocks |
||||
|
|
||||
|
- **Syntax**: Triple backticks with language specification |
||||
|
- **Languages**: `json`, `bash`, `typescript`, `javascript`, `yaml`, `markdown` |
||||
|
- **Blank lines**: Must be surrounded by blank lines above and below |
||||
|
- **Line length**: No enforcement within code blocks |
||||
|
|
||||
|
### Inline Code |
||||
|
|
||||
|
- **Format**: Single backticks for inline code references |
||||
|
- **Use case**: File names, commands, variables, properties |
||||
|
|
||||
|
## Special Content Standards |
||||
|
|
||||
|
### JSON Examples |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"property": "value", |
||||
|
"nested": { |
||||
|
"property": "value" |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Shell Commands |
||||
|
|
||||
|
```bash |
||||
|
# Command with comment |
||||
|
npm run build:web |
||||
|
|
||||
|
# Multi-line command |
||||
|
VITE_GIT_HASH=`git log -1 --pretty=format:%h` \ |
||||
|
vite build --config vite.config.web.mts |
||||
|
``` |
||||
|
|
||||
|
### TypeScript Examples |
||||
|
|
||||
|
```typescript |
||||
|
// Function with JSDoc |
||||
|
/** |
||||
|
* Get environment configuration |
||||
|
* @param env - Environment name |
||||
|
* @returns Environment config object |
||||
|
*/ |
||||
|
const getEnvironmentConfig = (env: string) => { |
||||
|
switch (env) { |
||||
|
case 'prod': |
||||
|
return { /* production settings */ }; |
||||
|
default: |
||||
|
return { /* development settings */ }; |
||||
|
} |
||||
|
}; |
||||
|
``` |
||||
|
|
||||
|
## File Structure Standards |
||||
|
|
||||
|
### Document Header |
||||
|
|
||||
|
```markdown |
||||
|
# Document Title |
||||
|
|
||||
|
**Author**: Matthew Raymer |
||||
|
**Date**: YYYY-MM-DD |
||||
|
**Status**: 🎯 **STATUS** - Brief description |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
Brief description of the document's purpose and scope. |
||||
|
``` |
||||
|
|
||||
|
### Section Organization |
||||
|
|
||||
|
1. **Overview/Introduction** |
||||
|
2. **Current State Analysis** |
||||
|
3. **Implementation Plan** |
||||
|
4. **Technical Details** |
||||
|
5. **Testing & Validation** |
||||
|
6. **Next Steps** |
||||
|
|
||||
|
## Markdownlint Configuration |
||||
|
|
||||
|
### Required Rules |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"MD013": { "code_blocks": false }, |
||||
|
"MD012": true, |
||||
|
"MD022": true, |
||||
|
"MD031": true, |
||||
|
"MD032": true, |
||||
|
"MD047": true, |
||||
|
"MD009": true |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Rule Explanations |
||||
|
|
||||
|
- **MD013**: Line length (disabled for code blocks) |
||||
|
- **MD012**: No multiple consecutive blank lines |
||||
|
- **MD022**: Headings should be surrounded by blank lines |
||||
|
- **MD031**: Fenced code blocks should be surrounded by blank lines |
||||
|
- **MD032**: Lists should be surrounded by blank lines |
||||
|
- **MD047**: Files should end with a single newline |
||||
|
- **MD009**: No trailing spaces |
||||
|
|
||||
|
## Validation Commands |
||||
|
|
||||
|
### Check Single File |
||||
|
|
||||
|
```bash |
||||
|
npx markdownlint docs/filename.md |
||||
|
``` |
||||
|
|
||||
|
### Check All Documentation |
||||
|
|
||||
|
```bash |
||||
|
npx markdownlint docs/ |
||||
|
``` |
||||
|
|
||||
|
### Auto-fix Common Issues |
||||
|
|
||||
|
```bash |
||||
|
# Remove trailing spaces |
||||
|
sed -i 's/[[:space:]]*$//' docs/filename.md |
||||
|
|
||||
|
# Remove multiple blank lines |
||||
|
sed -i '/^$/N;/^\n$/D' docs/filename.md |
||||
|
|
||||
|
# Add newline at end if missing |
||||
|
echo "" >> docs/filename.md |
||||
|
``` |
||||
|
|
||||
|
## Common Patterns |
||||
|
|
||||
|
### Implementation Plans |
||||
|
|
||||
|
```markdown |
||||
|
## Implementation Plan |
||||
|
|
||||
|
### Phase 1: Foundation (Day 1) |
||||
|
|
||||
|
#### 1.1 Component Setup |
||||
|
|
||||
|
- [ ] Create new component file |
||||
|
- [ ] Add basic structure |
||||
|
- [ ] Implement core functionality |
||||
|
|
||||
|
#### 1.2 Configuration |
||||
|
|
||||
|
- [ ] Update configuration files |
||||
|
- [ ] Add environment variables |
||||
|
- [ ] Test configuration loading |
||||
|
``` |
||||
|
|
||||
|
### Status Tracking |
||||
|
|
||||
|
```markdown |
||||
|
**Status**: ✅ **COMPLETE** - All phases finished |
||||
|
**Progress**: 75% (15/20 components) |
||||
|
**Next**: Ready for testing phase |
||||
|
``` |
||||
|
|
||||
|
### Performance Metrics |
||||
|
|
||||
|
```markdown |
||||
|
#### 📊 Performance Metrics |
||||
|
- **Build Time**: 2.3 seconds (50% faster than baseline) |
||||
|
- **Bundle Size**: 1.2MB (30% reduction) |
||||
|
- **Success Rate**: 100% (no failures in 50 builds) |
||||
|
``` |
||||
|
|
||||
|
## Enforcement |
||||
|
|
||||
|
### Pre-commit Hooks |
||||
|
|
||||
|
- Run markdownlint on all changed markdown files |
||||
|
- Block commits with linting violations |
||||
|
- Auto-fix common issues when possible |
||||
|
|
||||
|
### CI/CD Integration |
||||
|
|
||||
|
- Include markdownlint in build pipeline |
||||
|
- Generate reports for documentation quality |
||||
|
- Fail builds with critical violations |
||||
|
|
||||
|
### Team Guidelines |
||||
|
|
||||
|
- All documentation PRs must pass markdownlint |
||||
|
- Use provided templates for new documents |
||||
|
- Follow established patterns for consistency |
||||
|
|
||||
|
## Templates |
||||
|
|
||||
|
### New Document Template |
||||
|
|
||||
|
```markdown |
||||
|
# Document Title |
||||
|
|
||||
|
**Author**: Matthew Raymer |
||||
|
**Date**: YYYY-MM-DD |
||||
|
**Status**: 🎯 **PLANNING** - Ready for Implementation |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
Brief description of the document's purpose and scope. |
||||
|
|
||||
|
## Current State |
||||
|
|
||||
|
Description of current situation or problem. |
||||
|
|
||||
|
## Implementation Plan |
||||
|
|
||||
|
### Phase 1: Foundation |
||||
|
|
||||
|
- [ ] Task 1 |
||||
|
- [ ] Task 2 |
||||
|
|
||||
|
## Next Steps |
||||
|
|
||||
|
1. **Review and approve plan** |
||||
|
2. **Begin implementation** |
||||
|
3. **Test and validate** |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
**Status**: Ready for implementation |
||||
|
**Priority**: Medium |
||||
|
**Estimated Effort**: X days |
||||
|
**Dependencies**: None |
||||
|
**Stakeholders**: Development team |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
**Last Updated**: 2025-07-09 |
||||
|
**Version**: 1.0 |
||||
|
**Maintainer**: Matthew Raymer |
@ -0,0 +1,314 @@ |
|||||
|
--- |
||||
|
globs: *.md |
||||
|
alwaysApply: false |
||||
|
--- |
||||
|
# Cursor Markdown Ruleset for TimeSafari Documentation |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
This ruleset enforces consistent markdown formatting standards across all project |
||||
|
documentation, ensuring readability, maintainability, and compliance with |
||||
|
markdownlint best practices. |
||||
|
|
||||
|
## General Formatting Standards |
||||
|
|
||||
|
### Line Length |
||||
|
|
||||
|
- **Maximum line length**: 80 characters |
||||
|
- **Exception**: Code blocks (JSON, shell, TypeScript, etc.) - no line length |
||||
|
enforcement |
||||
|
- **Rationale**: Ensures readability across different screen sizes and terminal |
||||
|
widths |
||||
|
|
||||
|
### Blank Lines |
||||
|
|
||||
|
- **Headings**: Must be surrounded by blank lines above and below |
||||
|
- **Lists**: Must be surrounded by blank lines above and below |
||||
|
- **Code blocks**: Must be surrounded by blank lines above and below |
||||
|
- **Maximum consecutive blank lines**: 1 (no multiple blank lines) |
||||
|
- **File start**: No blank lines at the beginning of the file |
||||
|
- **File end**: Single newline character at the end |
||||
|
|
||||
|
### Whitespace |
||||
|
|
||||
|
- **No trailing spaces**: Remove all trailing whitespace from lines |
||||
|
- **No tabs**: Use spaces for indentation |
||||
|
- **Consistent indentation**: 2 spaces for list items and nested content |
||||
|
|
||||
|
## Heading Standards |
||||
|
|
||||
|
### Format |
||||
|
|
||||
|
- **Style**: ATX-style headings (`#`, `##`, `###`, etc.) |
||||
|
- **Case**: Title case for general headings |
||||
|
- **Code references**: Use backticks for file names and technical terms |
||||
|
- ✅ `### Current package.json Scripts` |
||||
|
- ❌ `### Current Package.json Scripts` |
||||
|
|
||||
|
### Hierarchy |
||||
|
|
||||
|
- **H1 (#)**: Document title only |
||||
|
- **H2 (##)**: Major sections |
||||
|
- **H3 (###)**: Subsections |
||||
|
- **H4 (####)**: Sub-subsections |
||||
|
- **H5+**: Avoid deeper nesting |
||||
|
|
||||
|
## List Standards |
||||
|
|
||||
|
### Unordered Lists |
||||
|
|
||||
|
- **Marker**: Use `-` (hyphen) consistently |
||||
|
- **Indentation**: 2 spaces for nested items |
||||
|
- **Blank lines**: Surround lists with blank lines |
||||
|
|
||||
|
### Ordered Lists |
||||
|
|
||||
|
- **Format**: `1.`, `2.`, `3.` (sequential numbering) |
||||
|
- **Indentation**: 2 spaces for nested items |
||||
|
- **Blank lines**: Surround lists with blank lines |
||||
|
|
||||
|
### Task Lists |
||||
|
|
||||
|
- **Format**: `- [ ]` for incomplete, `- [x]` for complete |
||||
|
- **Use case**: Project planning, checklists, implementation tracking |
||||
|
|
||||
|
## Code Block Standards |
||||
|
|
||||
|
### Fenced Code Blocks |
||||
|
|
||||
|
- **Syntax**: Triple backticks with language specification |
||||
|
- **Languages**: `json`, `bash`, `typescript`, `javascript`, `yaml`, `markdown` |
||||
|
- **Blank lines**: Must be surrounded by blank lines above and below |
||||
|
- **Line length**: No enforcement within code blocks |
||||
|
|
||||
|
### Inline Code |
||||
|
|
||||
|
- **Format**: Single backticks for inline code references |
||||
|
- **Use case**: File names, commands, variables, properties |
||||
|
|
||||
|
## Special Content Standards |
||||
|
|
||||
|
### JSON Examples |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"property": "value", |
||||
|
"nested": { |
||||
|
"property": "value" |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Shell Commands |
||||
|
|
||||
|
```bash |
||||
|
# Command with comment |
||||
|
npm run build:web |
||||
|
|
||||
|
# Multi-line command |
||||
|
VITE_GIT_HASH=`git log -1 --pretty=format:%h` \ |
||||
|
vite build --config vite.config.web.mts |
||||
|
``` |
||||
|
|
||||
|
### TypeScript Examples |
||||
|
|
||||
|
```typescript |
||||
|
// Function with JSDoc |
||||
|
/** |
||||
|
* Get environment configuration |
||||
|
* @param env - Environment name |
||||
|
* @returns Environment config object |
||||
|
*/ |
||||
|
const getEnvironmentConfig = (env: string) => { |
||||
|
switch (env) { |
||||
|
case 'prod': |
||||
|
return { /* production settings */ }; |
||||
|
default: |
||||
|
return { /* development settings */ }; |
||||
|
} |
||||
|
}; |
||||
|
``` |
||||
|
|
||||
|
## File Structure Standards |
||||
|
|
||||
|
### Document Header |
||||
|
|
||||
|
```markdown |
||||
|
# Document Title |
||||
|
|
||||
|
**Author**: Matthew Raymer |
||||
|
**Date**: YYYY-MM-DD |
||||
|
**Status**: 🎯 **STATUS** - Brief description |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
Brief description of the document's purpose and scope. |
||||
|
``` |
||||
|
|
||||
|
### Section Organization |
||||
|
|
||||
|
1. **Overview/Introduction** |
||||
|
2. **Current State Analysis** |
||||
|
3. **Implementation Plan** |
||||
|
4. **Technical Details** |
||||
|
5. **Testing & Validation** |
||||
|
6. **Next Steps** |
||||
|
|
||||
|
## Markdownlint Configuration |
||||
|
|
||||
|
### Required Rules |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"MD013": { "code_blocks": false }, |
||||
|
"MD012": true, |
||||
|
"MD022": true, |
||||
|
"MD031": true, |
||||
|
"MD032": true, |
||||
|
"MD047": true, |
||||
|
"MD009": true |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Rule Explanations |
||||
|
|
||||
|
- **MD013**: Line length (disabled for code blocks) |
||||
|
- **MD012**: No multiple consecutive blank lines |
||||
|
- **MD022**: Headings should be surrounded by blank lines |
||||
|
- **MD031**: Fenced code blocks should be surrounded by blank lines |
||||
|
- **MD032**: Lists should be surrounded by blank lines |
||||
|
- **MD047**: Files should end with a single newline |
||||
|
- **MD009**: No trailing spaces |
||||
|
|
||||
|
## Validation Commands |
||||
|
|
||||
|
### Check Single File |
||||
|
|
||||
|
```bash |
||||
|
npx markdownlint docs/filename.md |
||||
|
``` |
||||
|
|
||||
|
### Check All Documentation |
||||
|
|
||||
|
```bash |
||||
|
npx markdownlint docs/ |
||||
|
``` |
||||
|
|
||||
|
### Auto-fix Common Issues |
||||
|
|
||||
|
```bash |
||||
|
# Remove trailing spaces |
||||
|
sed -i 's/[[:space:]]*$//' docs/filename.md |
||||
|
|
||||
|
# Remove multiple blank lines |
||||
|
sed -i '/^$/N;/^\n$/D' docs/filename.md |
||||
|
|
||||
|
# Add newline at end if missing |
||||
|
echo "" >> docs/filename.md |
||||
|
``` |
||||
|
|
||||
|
## Common Patterns |
||||
|
|
||||
|
### Implementation Plans |
||||
|
|
||||
|
```markdown |
||||
|
## Implementation Plan |
||||
|
|
||||
|
### Phase 1: Foundation (Day 1) |
||||
|
|
||||
|
#### 1.1 Component Setup |
||||
|
|
||||
|
- [ ] Create new component file |
||||
|
- [ ] Add basic structure |
||||
|
- [ ] Implement core functionality |
||||
|
|
||||
|
#### 1.2 Configuration |
||||
|
|
||||
|
- [ ] Update configuration files |
||||
|
- [ ] Add environment variables |
||||
|
- [ ] Test configuration loading |
||||
|
``` |
||||
|
|
||||
|
### Status Tracking |
||||
|
|
||||
|
```markdown |
||||
|
**Status**: ✅ **COMPLETE** - All phases finished |
||||
|
**Progress**: 75% (15/20 components) |
||||
|
**Next**: Ready for testing phase |
||||
|
``` |
||||
|
|
||||
|
### Performance Metrics |
||||
|
|
||||
|
```markdown |
||||
|
#### 📊 Performance Metrics |
||||
|
- **Build Time**: 2.3 seconds (50% faster than baseline) |
||||
|
- **Bundle Size**: 1.2MB (30% reduction) |
||||
|
- **Success Rate**: 100% (no failures in 50 builds) |
||||
|
``` |
||||
|
|
||||
|
## Enforcement |
||||
|
|
||||
|
### Pre-commit Hooks |
||||
|
|
||||
|
- Run markdownlint on all changed markdown files |
||||
|
- Block commits with linting violations |
||||
|
- Auto-fix common issues when possible |
||||
|
|
||||
|
### CI/CD Integration |
||||
|
|
||||
|
- Include markdownlint in build pipeline |
||||
|
- Generate reports for documentation quality |
||||
|
- Fail builds with critical violations |
||||
|
|
||||
|
### Team Guidelines |
||||
|
|
||||
|
- All documentation PRs must pass markdownlint |
||||
|
- Use provided templates for new documents |
||||
|
- Follow established patterns for consistency |
||||
|
|
||||
|
## Templates |
||||
|
|
||||
|
### New Document Template |
||||
|
|
||||
|
```markdown |
||||
|
# Document Title |
||||
|
|
||||
|
**Author**: Matthew Raymer |
||||
|
**Date**: YYYY-MM-DD |
||||
|
**Status**: 🎯 **PLANNING** - Ready for Implementation |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
Brief description of the document's purpose and scope. |
||||
|
|
||||
|
## Current State |
||||
|
|
||||
|
Description of current situation or problem. |
||||
|
|
||||
|
## Implementation Plan |
||||
|
|
||||
|
### Phase 1: Foundation |
||||
|
|
||||
|
- [ ] Task 1 |
||||
|
- [ ] Task 2 |
||||
|
|
||||
|
## Next Steps |
||||
|
|
||||
|
1. **Review and approve plan** |
||||
|
2. **Begin implementation** |
||||
|
3. **Test and validate** |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
**Status**: Ready for implementation |
||||
|
**Priority**: Medium |
||||
|
**Estimated Effort**: X days |
||||
|
**Dependencies**: None |
||||
|
**Stakeholders**: Development team |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
**Last Updated**: 2025-07-09 |
||||
|
**Version**: 1.0 |
||||
|
**Maintainer**: Matthew Raymer |
@ -0,0 +1 @@ |
|||||
|
{"MD013": {"code_blocks": false}} |
@ -0,0 +1,569 @@ |
|||||
|
# Build Pattern Conversion Plan |
||||
|
|
||||
|
**Author**: Matthew Raymer |
||||
|
**Date**: 2025-07-09 |
||||
|
**Status**: 🎯 **PLANNING** - Ready for Implementation |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
Convert TimeSafari's build instruction pattern from the current script-based |
||||
|
approach to a new Vite `mode`-based pattern that provides better environment |
||||
|
management and consistency across all build targets. |
||||
|
|
||||
|
## Why Vite Mode Instead of NODE_ENV? |
||||
|
|
||||
|
### ✅ Vite's Native Mode System |
||||
|
|
||||
|
Vite is designed to work with `mode`, which: |
||||
|
|
||||
|
- Determines the `.env` file to load (e.g. `.env.production`, `.env.test`, etc.) |
||||
|
- Is passed to `defineConfig(({ mode }) => {...})` in `vite.config.ts` |
||||
|
- Is used to set behavior for dev/prod/test at config level |
||||
|
- Provides better integration with Vite's build system |
||||
|
|
||||
|
### 🚫 NODE_ENV Limitations |
||||
|
|
||||
|
`NODE_ENV` is legacy from Webpack-era tooling: |
||||
|
|
||||
|
- You can't change `NODE_ENV` manually and expect Vite to adapt |
||||
|
- Vite does not map `NODE_ENV` back to `mode` |
||||
|
- It's redundant with `mode` and might conflict with assumptions |
||||
|
- Limited integration with Vite's environment loading system |
||||
|
|
||||
|
### Usage Pattern |
||||
|
|
||||
|
```bash |
||||
|
# ✅ Correct: Use Vite's mode system |
||||
|
vite build --mode production |
||||
|
vite build --mode development |
||||
|
vite build --mode test |
||||
|
|
||||
|
# ⚠️ Only if third-party libraries require NODE_ENV |
||||
|
NODE_ENV=production vite build --mode production |
||||
|
``` |
||||
|
|
||||
|
### Development vs Build Environments |
||||
|
|
||||
|
**Development Environment:** |
||||
|
- **Build with defaults**: `npm run build:*` - Uses `--mode development` by default |
||||
|
- **Purpose**: Development builds for testing and debugging |
||||
|
- **Output**: Bundled files with development optimizations |
||||
|
|
||||
|
**Testing/Production Environments:** |
||||
|
- **Build with explicit mode**: `npm run build:* -- --mode test/production` |
||||
|
- **Purpose**: Validate and deploy the bundled application |
||||
|
- **Output**: Optimized, bundled files for specific environment |
||||
|
|
||||
|
### Mode Override Behavior |
||||
|
|
||||
|
**How `--mode` Override Works:** |
||||
|
|
||||
|
```bash |
||||
|
# Base script (no hardcoded mode) |
||||
|
"build:electron": "vite build --config vite.config.electron.mts" |
||||
|
|
||||
|
# Development (uses Vite's default: --mode development) |
||||
|
npm run build:electron |
||||
|
# Executes: vite build --config vite.config.electron.mts |
||||
|
|
||||
|
# Testing (explicitly overrides with --mode test) |
||||
|
npm run build:electron -- --mode test |
||||
|
# Executes: vite build --config vite.config.electron.mts --mode test |
||||
|
|
||||
|
# Production (explicitly overrides with --mode production) |
||||
|
npm run build:electron -- --mode production |
||||
|
# Executes: vite build --config vite.config.electron.mts --mode production |
||||
|
``` |
||||
|
|
||||
|
**Key Points:** |
||||
|
- Base scripts have **no hardcoded `--mode`** to allow override |
||||
|
- `npm run build:electron` defaults to `--mode development` |
||||
|
- `npm run build:electron -- --mode test` overrides to `--mode test` |
||||
|
- Vite uses the **last `--mode` argument** if multiple are provided |
||||
|
|
||||
|
### Capacitor Platform-Specific Commands |
||||
|
|
||||
|
Capacitor requires platform-specific sync commands after building: |
||||
|
|
||||
|
```bash |
||||
|
# General sync (copies web assets to all platforms) |
||||
|
npm run build:capacitor && npx cap sync |
||||
|
|
||||
|
# Platform-specific sync |
||||
|
npm run build:capacitor && npx cap sync android |
||||
|
npm run build:capacitor && npx cap sync ios |
||||
|
|
||||
|
# Environment-specific with platform sync |
||||
|
npm run build:capacitor -- --mode production && npx cap sync android |
||||
|
npm run build:capacitor -- --mode development && npx cap sync ios |
||||
|
``` |
||||
|
|
||||
|
### Electron Platform-Specific Commands |
||||
|
|
||||
|
Electron requires platform-specific build commands after the Vite build: |
||||
|
|
||||
|
```bash |
||||
|
# General Electron build (Vite build only) |
||||
|
npm run build:electron |
||||
|
|
||||
|
# Platform-specific builds |
||||
|
npm run build:electron:windows # Windows executable |
||||
|
npm run build:electron:mac # macOS app bundle |
||||
|
npm run build:electron:linux # Linux executable |
||||
|
|
||||
|
# Package-specific builds |
||||
|
npm run build:electron:appimage # Linux AppImage |
||||
|
npm run build:electron:dmg # macOS DMG installer |
||||
|
|
||||
|
# Environment-specific builds |
||||
|
npm run build:electron -- --mode development |
||||
|
npm run build:electron -- --mode test |
||||
|
npm run build:electron -- --mode production |
||||
|
|
||||
|
# Environment-specific with platform builds |
||||
|
npm run build:electron:windows -- --mode development |
||||
|
npm run build:electron:windows -- --mode test |
||||
|
npm run build:electron:windows -- --mode production |
||||
|
|
||||
|
npm run build:electron:mac -- --mode development |
||||
|
npm run build:electron:mac -- --mode test |
||||
|
npm run build:electron:mac -- --mode production |
||||
|
|
||||
|
npm run build:electron:linux -- --mode development |
||||
|
npm run build:electron:linux -- --mode test |
||||
|
npm run build:electron:linux -- --mode production |
||||
|
|
||||
|
# Environment-specific with package builds |
||||
|
npm run build:electron:appimage -- --mode development |
||||
|
npm run build:electron:appimage -- --mode test |
||||
|
npm run build:electron:appimage -- --mode production |
||||
|
|
||||
|
npm run build:electron:dmg -- --mode development |
||||
|
npm run build:electron:dmg -- --mode test |
||||
|
npm run build:electron:dmg -- --mode production |
||||
|
``` |
||||
|
|
||||
|
## Current State Analysis |
||||
|
|
||||
|
### Existing Build Scripts |
||||
|
|
||||
|
- **Web**: `build:web` - Uses vite.config.web.mts |
||||
|
- **Capacitor**: `build:capacitor` - Uses vite.config.capacitor.mts |
||||
|
- **Android**: `build:android` - Shell script wrapper |
||||
|
- **iOS**: `build:ios` - Shell script wrapper |
||||
|
- **Electron**: `build:electron` - Uses vite.config.electron.mts |
||||
|
- **Windows**: `build:electron:windows` - Windows executable |
||||
|
- **macOS**: `build:electron:mac` - macOS app bundle |
||||
|
- **Linux**: `build:electron:linux` - Linux executable |
||||
|
- **AppImage**: `build:electron:appimage` - Linux AppImage |
||||
|
- **DMG**: `build:electron:dmg` - macOS DMG installer |
||||
|
|
||||
|
### Current `package.json` Scripts |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"build:capacitor": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts", |
||||
|
"build:web": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.web.mts", |
||||
|
"build:electron": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode electron --config vite.config.electron.mts" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Target Pattern |
||||
|
|
||||
|
### New Vite Mode-Based Pattern |
||||
|
|
||||
|
```bash |
||||
|
# Development builds (defaults to --mode development) |
||||
|
npm run build:web-dev |
||||
|
npm run build:capacitor-dev |
||||
|
npm run build:electron-dev |
||||
|
|
||||
|
# Testing builds (bundle required) |
||||
|
npm run build:web -- --mode test |
||||
|
npm run build:capacitor -- --mode test && npx cap sync |
||||
|
npm run build:electron -- --mode test |
||||
|
|
||||
|
# Production builds (bundle required) |
||||
|
npm run build:web -- --mode production |
||||
|
npm run build:capacitor -- --mode production && npx cap sync |
||||
|
npm run build:electron -- --mode production |
||||
|
|
||||
|
# Docker builds |
||||
|
npm run build:web-docker -- --mode test |
||||
|
npm run build:web-docker -- --mode production |
||||
|
|
||||
|
# Capacitor platform-specific builds |
||||
|
npm run build:capacitor:android -- --mode test |
||||
|
npm run build:capacitor:android -- --mode production |
||||
|
|
||||
|
npm run build:capacitor:ios -- --mode test |
||||
|
npm run build:capacitor:ios -- --mode production |
||||
|
|
||||
|
# Electron platform-specific builds |
||||
|
npm run build:electron:windows -- --mode test |
||||
|
npm run build:electron:windows -- --mode production |
||||
|
|
||||
|
npm run build:electron:mac -- --mode test |
||||
|
npm run build:electron:mac -- --mode production |
||||
|
|
||||
|
npm run build:electron:linux -- --mode test |
||||
|
npm run build:electron:linux -- --mode production |
||||
|
|
||||
|
# Electron package-specific builds |
||||
|
npm run build:electron:appimage -- --mode test |
||||
|
npm run build:electron:appimage -- --mode production |
||||
|
|
||||
|
npm run build:electron:dmg -- --mode test |
||||
|
npm run build:electron:dmg -- --mode production |
||||
|
``` |
||||
|
|
||||
|
### New `package.json` Scripts Structure |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"build:web": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.web.mts", |
||||
|
"build:web:dev": "npm run build:web", |
||||
|
"build:web:test": "npm run build:web -- --mode test", |
||||
|
"build:web:prod": "npm run build:web -- --mode production" |
||||
|
"build:web:docker": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.web.mts", |
||||
|
"build:web:docker:test": "npm run build:web:docker -- --mode test", |
||||
|
"build:web:docker:prod": "npm run build:web:docker -- --mode production", |
||||
|
|
||||
|
"build:capacitor": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts", |
||||
|
"build:capacitor-dev": "npm run build:capacitor", |
||||
|
"build:capacitor:sync": "npm run build:capacitor && npx cap sync", |
||||
|
"build:capacitor:android": "npm run build:capacitor:sync && npx cap sync android", |
||||
|
"build:capacitor:ios": "npm run build:capacitor:sync && npx cap sync ios", |
||||
|
|
||||
|
"build:electron": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.electron.mts", |
||||
|
"build:electron:dev": "npm run build:electron && cd electron && npm run electron:start", |
||||
|
"build:electron:windows": "npm run build:electron && cd electron && npm run build:windows", |
||||
|
"build:electron:mac": "npm run build:electron && cd electron && npm run build:mac", |
||||
|
"build:electron:linux": "npm run build:electron && cd electron && npm run build:linux", |
||||
|
"build:electron:appimage": "npm run build:electron:linux && cd electron && npm run build:appimage", |
||||
|
"build:electron:dmg": "npm run build:electron:mac && cd electron && npm run build:dmg" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Implementation Plan |
||||
|
|
||||
|
### Phase 1: Environment Configuration (Day 1) |
||||
|
|
||||
|
#### 1.1 Update Vite Configurations |
||||
|
|
||||
|
- [ ] **vite.config.web.mts**: Add mode-based configuration |
||||
|
- [ ] **vite.config.capacitor.mts**: Add mode-based configuration |
||||
|
- [ ] **vite.config.electron.mts**: Add mode-based configuration |
||||
|
- [ ] **vite.config.common.mts**: Add environment-specific variables |
||||
|
|
||||
|
#### 1.2 Environment Variables Setup |
||||
|
|
||||
|
- [ ] Create `.env.development` file for development settings |
||||
|
- [ ] Create `.env.test` file for testing settings |
||||
|
- [ ] Create `.env.production` file for production settings |
||||
|
- [ ] Update `.env.example` with new pattern |
||||
|
|
||||
|
#### 1.3 Environment Detection Logic |
||||
|
|
||||
|
```typescript |
||||
|
// vite.config.common.mts |
||||
|
export default defineConfig(({ mode }) => { |
||||
|
const getEnvironmentConfig = (mode: string) => { |
||||
|
switch (mode) { |
||||
|
case 'production': |
||||
|
return { /* production settings */ }; |
||||
|
case 'test': |
||||
|
return { /* testing settings */ }; |
||||
|
default: |
||||
|
return { /* development settings */ }; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
return { |
||||
|
define: { |
||||
|
__DEV__: mode === 'development', |
||||
|
__TEST__: mode === 'test', |
||||
|
__PROD__: mode === 'production' |
||||
|
}, |
||||
|
// ... other config |
||||
|
}; |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
### Phase 2: Package.json Scripts Update (Day 1) |
||||
|
|
||||
|
#### 2.1 Web Build Scripts |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"build:web": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.web.mts", |
||||
|
"build:web-dev": "npm run build:web", |
||||
|
"build:web-test": "npm run build:web -- --mode test", |
||||
|
"build:web-prod": "npm run build:web -- --mode production" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### 2.2 Capacitor Build Scripts |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"build:capacitor": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts", |
||||
|
"build:capacitor-dev": "npm run build:capacitor", |
||||
|
"build:capacitor:sync": "npm run build:capacitor && npx cap sync", |
||||
|
"build:capacitor:android": "npm run build:capacitor:sync && npx cap sync android", |
||||
|
"build:capacitor:ios": "npm run build:capacitor:sync && npx cap sync ios", |
||||
|
"build:capacitor-test": "npm run build:capacitor -- --mode test && npx cap sync", |
||||
|
"build:capacitor-prod": "npm run build:capacitor -- --mode production && npx cap sync", |
||||
|
"build:capacitor:android-test": "npm run build:capacitor -- --mode test && npx cap sync android", |
||||
|
"build:capacitor:android-prod": "npm run build:capacitor -- --mode production && npx cap sync android", |
||||
|
"build:capacitor:ios-test": "npm run build:capacitor -- --mode test && npx cap sync ios", |
||||
|
"build:capacitor:ios-prod": "npm run build:capacitor -- --mode production && npx cap sync ios" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### 2.3 Electron Build Scripts |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"build:electron": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.electron.mts", |
||||
|
"build:electron-dev": "npm run build:electron", |
||||
|
"build:electron:windows": "npm run build:electron && cd electron && npm run build:windows", |
||||
|
"build:electron:mac": "npm run build:electron && cd electron && npm run build:mac", |
||||
|
"build:electron:linux": "npm run build:electron && cd electron && npm run build:linux", |
||||
|
"build:electron:appimage": "npm run build:electron:linux && cd electron && npm run build:appimage", |
||||
|
"build:electron:dmg": "npm run build:electron:mac && cd electron && npm run build:dmg", |
||||
|
"build:electron-test": "npm run build:electron -- --mode test", |
||||
|
"build:electron-prod": "npm run build:electron -- --mode production", |
||||
|
"build:electron:windows-test": "npm run build:electron -- --mode test && cd electron && npm run build:windows", |
||||
|
"build:electron:windows-prod": "npm run build:electron -- --mode production && cd electron && npm run build:windows", |
||||
|
"build:electron:mac-dev": "npm run build:electron -- --mode development && cd electron && npm run build:mac", |
||||
|
"build:electron:mac-test": "npm run build:electron -- --mode test && cd electron && npm run build:mac", |
||||
|
"build:electron:mac-prod": "npm run build:electron -- --mode production && cd electron && npm run build:mac", |
||||
|
"build:electron:linux-test": "npm run build:electron -- --mode test && cd electron && npm run build:linux", |
||||
|
"build:electron:linux-prod": "npm run build:electron -- --mode production && cd electron && npm run build:linux" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### 2.4 Docker Build Scripts |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"build:web-docker": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.web.mts", |
||||
|
"build:web-docker-test": "npm run build:web-docker -- --mode test", |
||||
|
"build:web-docker-prod": "npm run build:web-docker -- --mode production" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Phase 3: Shell Script Updates (Day 2) |
||||
|
|
||||
|
#### 3.1 Update build-electron.sh |
||||
|
|
||||
|
- [ ] Add mode-based environment support |
||||
|
- [ ] Update environment loading logic |
||||
|
- [ ] Add environment-specific build paths |
||||
|
- [ ] Update logging to show environment |
||||
|
|
||||
|
#### 3.2 Update build-android.sh |
||||
|
|
||||
|
- [ ] Add mode-based environment support |
||||
|
- [ ] Update environment detection |
||||
|
- [ ] Add environment-specific configurations |
||||
|
|
||||
|
#### 3.3 Update build-ios.sh |
||||
|
|
||||
|
- [ ] Add mode-based environment support |
||||
|
- [ ] Update environment detection |
||||
|
- [ ] Add environment-specific configurations |
||||
|
|
||||
|
### Phase 4: Documentation Updates (Day 2) |
||||
|
|
||||
|
#### 4.1 Update BUILDING.md |
||||
|
|
||||
|
- [ ] Document new Vite mode-based pattern |
||||
|
- [ ] Update build instructions |
||||
|
- [ ] Add environment-specific examples |
||||
|
- [ ] Update troubleshooting section |
||||
|
|
||||
|
#### 4.2 Update scripts/README.md |
||||
|
|
||||
|
- [ ] Document new Vite mode-based build patterns |
||||
|
- [ ] Update usage examples |
||||
|
- [ ] Add environment configuration guide |
||||
|
|
||||
|
#### 4.3 Update CI/CD Documentation |
||||
|
|
||||
|
- [ ] Update GitHub Actions workflows |
||||
|
- [ ] Update Docker build instructions |
||||
|
- [ ] Update deployment guides |
||||
|
|
||||
|
### Phase 5: Testing & Validation (Day 3) |
||||
|
|
||||
|
#### 5.1 Environment Testing |
||||
|
|
||||
|
- [ ] Test dev environment builds |
||||
|
- [ ] Test test environment builds |
||||
|
- [ ] Test prod environment builds |
||||
|
- [ ] Validate environment variables |
||||
|
|
||||
|
#### 5.2 Platform Testing |
||||
|
|
||||
|
- [ ] Test web builds across environments |
||||
|
- [ ] Test capacitor builds across environments |
||||
|
- [ ] Test capacitor android sync across environments |
||||
|
- [ ] Test capacitor ios sync across environments |
||||
|
- [ ] Test electron builds across environments |
||||
|
- [ ] Test electron windows builds across environments |
||||
|
- [ ] Test electron mac builds across environments |
||||
|
- [ ] Test electron linux builds across environments |
||||
|
- [ ] Test electron appimage builds across environments |
||||
|
- [ ] Test electron dmg builds across environments |
||||
|
- [ ] Test docker builds across environments |
||||
|
|
||||
|
#### 5.3 Integration Testing |
||||
|
|
||||
|
- [ ] Test with existing CI/CD pipelines |
||||
|
- [ ] Test with existing deployment scripts |
||||
|
- [ ] Test with existing development workflows |
||||
|
|
||||
|
## Environment-Specific Configurations |
||||
|
|
||||
|
### Development Environment (--mode development) |
||||
|
|
||||
|
```typescript |
||||
|
{ |
||||
|
VITE_API_URL: 'http://localhost:3000', |
||||
|
VITE_DEBUG: 'true', |
||||
|
VITE_LOG_LEVEL: 'debug', |
||||
|
VITE_ENABLE_DEV_TOOLS: 'true' |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Testing Environment (--mode test) |
||||
|
|
||||
|
```typescript |
||||
|
{ |
||||
|
VITE_API_URL: 'https://test-api.timesafari.com', |
||||
|
VITE_DEBUG: 'false', |
||||
|
VITE_LOG_LEVEL: 'info', |
||||
|
VITE_ENABLE_DEV_TOOLS: 'false' |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Production Environment (--mode production) |
||||
|
|
||||
|
```typescript |
||||
|
{ |
||||
|
VITE_API_URL: 'https://api.timesafari.com', |
||||
|
VITE_DEBUG: 'false', |
||||
|
VITE_LOG_LEVEL: 'warn', |
||||
|
VITE_ENABLE_DEV_TOOLS: 'false' |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Migration Strategy |
||||
|
|
||||
|
### Backward Compatibility |
||||
|
|
||||
|
- [ ] Keep existing script names as aliases |
||||
|
- [ ] Add deprecation warnings for old scripts |
||||
|
- [ ] Maintain existing CI/CD compatibility |
||||
|
- [ ] Provide migration guide for users |
||||
|
|
||||
|
### Gradual Rollout |
||||
|
|
||||
|
1. **Week 1**: Implement new scripts alongside existing ones |
||||
|
2. **Week 2**: Update CI/CD to use new pattern |
||||
|
3. **Week 3**: Update documentation and guides |
||||
|
4. **Week 4**: Deprecate old scripts with warnings |
||||
|
|
||||
|
## Success Metrics |
||||
|
|
||||
|
### Technical Metrics |
||||
|
|
||||
|
- [ ] All builds work with Vite mode-based pattern |
||||
|
- [ ] Environment variables properly loaded |
||||
|
- [ ] Build artifacts correctly generated |
||||
|
- [ ] No regression in existing functionality |
||||
|
|
||||
|
### Process Metrics |
||||
|
|
||||
|
- [ ] Reduced build script complexity |
||||
|
- [ ] Improved environment management |
||||
|
- [ ] Better developer experience |
||||
|
- [ ] Consistent build patterns |
||||
|
|
||||
|
## Risk Assessment |
||||
|
|
||||
|
### Low Risk |
||||
|
|
||||
|
- [ ] Environment variable changes |
||||
|
- [ ] Package.json script updates |
||||
|
- [ ] Documentation updates |
||||
|
|
||||
|
### Medium Risk |
||||
|
|
||||
|
- [ ] Vite configuration changes (mode-based) |
||||
|
- [ ] Shell script modifications |
||||
|
- [ ] CI/CD pipeline updates |
||||
|
|
||||
|
### High Risk |
||||
|
|
||||
|
- [ ] Breaking existing build processes |
||||
|
- [ ] Environment-specific bugs |
||||
|
- [ ] Deployment failures |
||||
|
|
||||
|
## Rollback Plan |
||||
|
|
||||
|
### Immediate Rollback |
||||
|
|
||||
|
- [ ] Revert package.json changes |
||||
|
- [ ] Restore original vite configs |
||||
|
- [ ] Restore original shell scripts |
||||
|
|
||||
|
### Gradual Rollback |
||||
|
|
||||
|
- [ ] Keep old scripts as primary |
||||
|
- [ ] Use new scripts as experimental |
||||
|
- [ ] Gather feedback before full migration |
||||
|
|
||||
|
## Timeline |
||||
|
|
||||
|
### Day 1: Foundation |
||||
|
|
||||
|
- [ ] Environment configuration setup |
||||
|
- [ ] Package.json script updates |
||||
|
- [ ] Basic testing |
||||
|
|
||||
|
### Day 2: Integration |
||||
|
|
||||
|
- [ ] Shell script updates |
||||
|
- [ ] Documentation updates |
||||
|
- [ ] Integration testing |
||||
|
|
||||
|
### Day 3: Validation |
||||
|
|
||||
|
- [ ] Comprehensive testing |
||||
|
- [ ] Performance validation |
||||
|
- [ ] Documentation review |
||||
|
|
||||
|
### Day 4: Deployment |
||||
|
|
||||
|
- [ ] CI/CD updates |
||||
|
- [ ] Production validation |
||||
|
- [ ] User communication |
||||
|
|
||||
|
## Next Steps |
||||
|
|
||||
|
1. **Review and approve plan** |
||||
|
2. **Set up development environment** |
||||
|
3. **Begin Phase 1 implementation** |
||||
|
4. **Create test cases** |
||||
|
5. **Start implementation** |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
**Status**: Ready for implementation |
||||
|
**Priority**: Medium |
||||
|
**Estimated Effort**: 3-4 days |
||||
|
**Dependencies**: None |
||||
|
**Stakeholders**: Development team, DevOps team |
@ -0,0 +1,662 @@ |
|||||
|
# Vue 3 + Vite + vue-facing-decorator: Lazy Loading Patterns & Best Practices |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
This document provides comprehensive guidance on implementing lazy loading and code splitting in Vue 3 applications using Vite and `vue-facing-decorator`. The patterns demonstrated here optimize bundle size, improve initial load times, and enhance user experience through progressive loading. |
||||
|
|
||||
|
**Author:** Matthew Raymer |
||||
|
**Version:** 1.0.0 |
||||
|
**Last Updated:** 2024 |
||||
|
|
||||
|
## Table of Contents |
||||
|
|
||||
|
1. [Why Lazy Loading Matters](#why-lazy-loading-matters) |
||||
|
2. [Vite Configuration for Optimal Code Splitting](#vite-configuration-for-optimal-code-splitting) |
||||
|
3. [Lazy Loading Patterns](#lazy-loading-patterns) |
||||
|
4. [Component-Level Lazy Loading](#component-level-lazy-loading) |
||||
|
5. [Route-Level Lazy Loading](#route-level-lazy-loading) |
||||
|
6. [Library-Level Lazy Loading](#library-level-lazy-loading) |
||||
|
7. [Performance Monitoring](#performance-monitoring) |
||||
|
8. [Best Practices](#best-practices) |
||||
|
9. [Common Pitfalls](#common-pitfalls) |
||||
|
10. [Examples](#examples) |
||||
|
|
||||
|
## Why Lazy Loading Matters |
||||
|
|
||||
|
### Performance Benefits |
||||
|
|
||||
|
- **Faster Initial Load**: Only load code needed for the current view |
||||
|
- **Reduced Bundle Size**: Split large applications into smaller chunks |
||||
|
- **Better Caching**: Independent chunks can be cached separately |
||||
|
- **Improved User Experience**: Progressive loading with loading states |
||||
|
|
||||
|
### When to Use Lazy Loading |
||||
|
|
||||
|
✅ **Good Candidates for Lazy Loading:** |
||||
|
- Heavy components (data processing, 3D rendering) |
||||
|
- Feature-specific components (QR scanner, file uploader) |
||||
|
- Route-based components |
||||
|
- Large third-party libraries (ThreeJS, Chart.js) |
||||
|
- Components with conditional rendering |
||||
|
|
||||
|
❌ **Avoid Lazy Loading:** |
||||
|
- Core UI components used everywhere |
||||
|
- Small utility components |
||||
|
- Components needed for initial render |
||||
|
- Components with frequent usage patterns |
||||
|
|
||||
|
## Vite Configuration for Optimal Code Splitting |
||||
|
|
||||
|
### Enhanced Build Configuration |
||||
|
|
||||
|
```typescript |
||||
|
// vite.config.optimized.mts |
||||
|
export async function createOptimizedBuildConfig(mode: string): Promise<UserConfig> { |
||||
|
return { |
||||
|
build: { |
||||
|
rollupOptions: { |
||||
|
output: { |
||||
|
// Enhanced manual chunks for better code splitting |
||||
|
manualChunks: { |
||||
|
// Vendor chunks for better caching |
||||
|
'vue-vendor': ['vue', 'vue-router', 'pinia'], |
||||
|
'ui-vendor': ['@fortawesome/fontawesome-svg-core', '@fortawesome/vue-fontawesome'], |
||||
|
'crypto-vendor': ['@ethersproject/wallet', '@ethersproject/hdnode'], |
||||
|
'sql-vendor': ['@jlongster/sql.js', 'absurd-sql', 'dexie'], |
||||
|
'qr-vendor': ['qrcode', 'jsqr', 'vue-qrcode-reader'], |
||||
|
'three-vendor': ['three', '@tweenjs/tween.js'], |
||||
|
'utils-vendor': ['luxon', 'ramda', 'zod', 'axios'], |
||||
|
// Platform-specific chunks |
||||
|
...(isCapacitor && { |
||||
|
'capacitor-vendor': ['@capacitor/core', '@capacitor/app'] |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Key Configuration Features |
||||
|
|
||||
|
1. **Manual Chunks**: Group related dependencies for better caching |
||||
|
2. **Platform-Specific Chunks**: Separate native and web dependencies |
||||
|
3. **Vendor Separation**: Keep third-party libraries separate from app code |
||||
|
4. **Dynamic Imports**: Enable automatic code splitting for dynamic imports |
||||
|
|
||||
|
## Lazy Loading Patterns |
||||
|
|
||||
|
### 1. Component-Level Lazy Loading |
||||
|
|
||||
|
#### Basic Pattern with defineAsyncComponent |
||||
|
|
||||
|
```typescript |
||||
|
import { Component, Vue } from 'vue-facing-decorator'; |
||||
|
import { defineAsyncComponent } from 'vue'; |
||||
|
|
||||
|
@Component({ |
||||
|
name: 'LazyLoadingExample', |
||||
|
components: { |
||||
|
LazyHeavyComponent: defineAsyncComponent({ |
||||
|
loader: () => import('./sub-components/HeavyComponent.vue'), |
||||
|
loadingComponent: { |
||||
|
template: '<div class="loading">Loading...</div>' |
||||
|
}, |
||||
|
errorComponent: { |
||||
|
template: '<div class="error">Failed to load</div>' |
||||
|
}, |
||||
|
delay: 200, |
||||
|
timeout: 10000 |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
export default class LazyLoadingExample extends Vue { |
||||
|
// Component logic |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### Advanced Pattern with Suspense |
||||
|
|
||||
|
```vue |
||||
|
<template> |
||||
|
<Suspense> |
||||
|
<template #default> |
||||
|
<LazyHeavyComponent v-if="showComponent" /> |
||||
|
</template> |
||||
|
<template #fallback> |
||||
|
<div class="loading-fallback"> |
||||
|
<div class="spinner"></div> |
||||
|
<p>Loading component...</p> |
||||
|
</div> |
||||
|
</template> |
||||
|
</Suspense> |
||||
|
</template> |
||||
|
``` |
||||
|
|
||||
|
### 2. Conditional Loading |
||||
|
|
||||
|
```typescript |
||||
|
@Component({ |
||||
|
name: 'ConditionalLazyLoading' |
||||
|
}) |
||||
|
export default class ConditionalLazyLoading extends Vue { |
||||
|
showHeavyComponent = false; |
||||
|
showQRScanner = false; |
||||
|
|
||||
|
// Lazy load based on user interaction |
||||
|
async toggleHeavyComponent(): Promise<void> { |
||||
|
this.showHeavyComponent = !this.showHeavyComponent; |
||||
|
|
||||
|
if (this.showHeavyComponent) { |
||||
|
// Preload component for better UX |
||||
|
await this.preloadComponent(() => import('./HeavyComponent.vue')); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async preloadComponent(loader: () => Promise<any>): Promise<void> { |
||||
|
try { |
||||
|
await loader(); |
||||
|
} catch (error) { |
||||
|
console.warn('Preload failed:', error); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### 3. Library-Level Lazy Loading |
||||
|
|
||||
|
```typescript |
||||
|
@Component({ |
||||
|
name: 'LibraryLazyLoading' |
||||
|
}) |
||||
|
export default class LibraryLazyLoading extends Vue { |
||||
|
private threeJS: any = null; |
||||
|
|
||||
|
async loadThreeJS(): Promise<void> { |
||||
|
if (!this.threeJS) { |
||||
|
// Lazy load ThreeJS only when needed |
||||
|
this.threeJS = await import('three'); |
||||
|
console.log('ThreeJS loaded successfully'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async initialize3DScene(): Promise<void> { |
||||
|
await this.loadThreeJS(); |
||||
|
|
||||
|
// Use ThreeJS after loading |
||||
|
const scene = new this.threeJS.Scene(); |
||||
|
// ... rest of 3D setup |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Component-Level Lazy Loading |
||||
|
|
||||
|
### Heavy Data Processing Component |
||||
|
|
||||
|
```typescript |
||||
|
// HeavyComponent.vue |
||||
|
@Component({ |
||||
|
name: 'HeavyComponent' |
||||
|
}) |
||||
|
export default class HeavyComponent extends Vue { |
||||
|
@Prop({ required: true }) readonly data!: any; |
||||
|
|
||||
|
async processData(): Promise<void> { |
||||
|
// Process data in batches to avoid blocking UI |
||||
|
const batchSize = 10; |
||||
|
const items = this.data.items; |
||||
|
|
||||
|
for (let i = 0; i < items.length; i += batchSize) { |
||||
|
const batch = items.slice(i, i + batchSize); |
||||
|
await this.processBatch(batch); |
||||
|
|
||||
|
// Allow UI to update |
||||
|
await this.$nextTick(); |
||||
|
await new Promise(resolve => setTimeout(resolve, 10)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Camera-Dependent Component |
||||
|
|
||||
|
```typescript |
||||
|
// QRScannerComponent.vue |
||||
|
@Component({ |
||||
|
name: 'QRScannerComponent' |
||||
|
}) |
||||
|
export default class QRScannerComponent extends Vue { |
||||
|
async mounted(): Promise<void> { |
||||
|
// Initialize camera only when component is mounted |
||||
|
await this.initializeCamera(); |
||||
|
} |
||||
|
|
||||
|
async initializeCamera(): Promise<void> { |
||||
|
try { |
||||
|
const devices = await navigator.mediaDevices.enumerateDevices(); |
||||
|
this.cameras = devices.filter(device => device.kind === 'videoinput'); |
||||
|
this.hasCamera = this.cameras.length > 0; |
||||
|
} catch (error) { |
||||
|
console.error('Camera initialization failed:', error); |
||||
|
this.hasCamera = false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Route-Level Lazy Loading |
||||
|
|
||||
|
### Vue Router Configuration |
||||
|
|
||||
|
```typescript |
||||
|
// router/index.ts |
||||
|
import { createRouter, createWebHistory } from 'vue-router'; |
||||
|
|
||||
|
const routes = [ |
||||
|
{ |
||||
|
path: '/', |
||||
|
name: 'Home', |
||||
|
component: () => import('@/views/HomeView.vue') |
||||
|
}, |
||||
|
{ |
||||
|
path: '/heavy-feature', |
||||
|
name: 'HeavyFeature', |
||||
|
component: () => import('@/views/HeavyFeatureView.vue'), |
||||
|
// Preload on hover for better UX |
||||
|
beforeEnter: (to, from, next) => { |
||||
|
if (from.name) { |
||||
|
// Preload component when navigating from another route |
||||
|
import('@/views/HeavyFeatureView.vue'); |
||||
|
} |
||||
|
next(); |
||||
|
} |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
export default createRouter({ |
||||
|
history: createWebHistory(), |
||||
|
routes |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
### Route Guards with Lazy Loading |
||||
|
|
||||
|
```typescript |
||||
|
// router/guards.ts |
||||
|
export async function lazyLoadGuard(to: any, from: any, next: any): Promise<void> { |
||||
|
if (to.meta.requiresHeavyFeature) { |
||||
|
try { |
||||
|
// Preload heavy feature before navigation |
||||
|
await import('@/components/HeavyFeature.vue'); |
||||
|
next(); |
||||
|
} catch (error) { |
||||
|
console.error('Failed to load heavy feature:', error); |
||||
|
next('/error'); |
||||
|
} |
||||
|
} else { |
||||
|
next(); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Library-Level Lazy Loading |
||||
|
|
||||
|
### Dynamic Library Loading |
||||
|
|
||||
|
```typescript |
||||
|
@Component({ |
||||
|
name: 'DynamicLibraryLoader' |
||||
|
}) |
||||
|
export default class DynamicLibraryLoader extends Vue { |
||||
|
private libraries: Map<string, any> = new Map(); |
||||
|
|
||||
|
async loadLibrary(name: string): Promise<any> { |
||||
|
if (this.libraries.has(name)) { |
||||
|
return this.libraries.get(name); |
||||
|
} |
||||
|
|
||||
|
let library: any; |
||||
|
|
||||
|
switch (name) { |
||||
|
case 'three': |
||||
|
library = await import('three'); |
||||
|
break; |
||||
|
case 'chart': |
||||
|
library = await import('chart.js'); |
||||
|
break; |
||||
|
case 'qr': |
||||
|
library = await import('jsqr'); |
||||
|
break; |
||||
|
default: |
||||
|
throw new Error(`Unknown library: ${name}`); |
||||
|
} |
||||
|
|
||||
|
this.libraries.set(name, library); |
||||
|
return library; |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Conditional Library Loading |
||||
|
|
||||
|
```typescript |
||||
|
@Component({ |
||||
|
name: 'ConditionalLibraryLoader' |
||||
|
}) |
||||
|
export default class ConditionalLibraryLoader extends Vue { |
||||
|
async loadPlatformSpecificLibrary(): Promise<void> { |
||||
|
if (process.env.VITE_PLATFORM === 'capacitor') { |
||||
|
// Load Capacitor-specific libraries |
||||
|
await import('@capacitor/camera'); |
||||
|
await import('@capacitor/filesystem'); |
||||
|
} else { |
||||
|
// Load web-specific libraries |
||||
|
await import('file-saver'); |
||||
|
await import('jszip'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Performance Monitoring |
||||
|
|
||||
|
### Bundle Analysis |
||||
|
|
||||
|
```bash |
||||
|
# Analyze bundle size |
||||
|
npm run build |
||||
|
npx vite-bundle-analyzer dist |
||||
|
|
||||
|
# Monitor chunk sizes |
||||
|
npx vite-bundle-analyzer dist --mode=treemap |
||||
|
``` |
||||
|
|
||||
|
### Runtime Performance Monitoring |
||||
|
|
||||
|
```typescript |
||||
|
@Component({ |
||||
|
name: 'PerformanceMonitor' |
||||
|
}) |
||||
|
export default class PerformanceMonitor extends Vue { |
||||
|
private performanceMetrics = { |
||||
|
componentLoadTime: 0, |
||||
|
renderTime: 0, |
||||
|
memoryUsage: 0 |
||||
|
}; |
||||
|
|
||||
|
private measureComponentLoad(componentName: string): void { |
||||
|
const startTime = performance.now(); |
||||
|
|
||||
|
return () => { |
||||
|
const loadTime = performance.now() - startTime; |
||||
|
this.performanceMetrics.componentLoadTime = loadTime; |
||||
|
|
||||
|
console.log(`${componentName} loaded in ${loadTime.toFixed(2)}ms`); |
||||
|
|
||||
|
// Send to analytics |
||||
|
this.trackPerformance(componentName, loadTime); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private trackPerformance(component: string, loadTime: number): void { |
||||
|
// Send to analytics service |
||||
|
if (window.gtag) { |
||||
|
window.gtag('event', 'component_load', { |
||||
|
component_name: component, |
||||
|
load_time: loadTime |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Best Practices |
||||
|
|
||||
|
### 1. Loading States |
||||
|
|
||||
|
Always provide meaningful loading states: |
||||
|
|
||||
|
```vue |
||||
|
<template> |
||||
|
<Suspense> |
||||
|
<template #default> |
||||
|
<LazyComponent /> |
||||
|
</template> |
||||
|
<template #fallback> |
||||
|
<div class="loading-state"> |
||||
|
<div class="spinner"></div> |
||||
|
<p>Loading {{ componentName }}...</p> |
||||
|
<p class="loading-tip">{{ loadingTip }}</p> |
||||
|
</div> |
||||
|
</template> |
||||
|
</Suspense> |
||||
|
</template> |
||||
|
``` |
||||
|
|
||||
|
### 2. Error Handling |
||||
|
|
||||
|
Implement comprehensive error handling: |
||||
|
|
||||
|
```typescript |
||||
|
const LazyComponent = defineAsyncComponent({ |
||||
|
loader: () => import('./HeavyComponent.vue'), |
||||
|
errorComponent: { |
||||
|
template: ` |
||||
|
<div class="error-state"> |
||||
|
<h3>Failed to load component</h3> |
||||
|
<p>{{ error.message }}</p> |
||||
|
<button @click="retry">Retry</button> |
||||
|
</div> |
||||
|
`, |
||||
|
props: ['error'], |
||||
|
methods: { |
||||
|
retry() { |
||||
|
this.$emit('retry'); |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
onError(error, retry, fail, attempts) { |
||||
|
if (attempts <= 3) { |
||||
|
console.warn(`Retrying component load (attempt ${attempts})`); |
||||
|
retry(); |
||||
|
} else { |
||||
|
console.error('Component failed to load after 3 attempts'); |
||||
|
fail(); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
### 3. Preloading Strategy |
||||
|
|
||||
|
Implement intelligent preloading: |
||||
|
|
||||
|
```typescript |
||||
|
@Component({ |
||||
|
name: 'SmartPreloader' |
||||
|
}) |
||||
|
export default class SmartPreloader extends Vue { |
||||
|
private preloadQueue: Array<() => Promise<any>> = []; |
||||
|
private isPreloading = false; |
||||
|
|
||||
|
// Preload based on user behavior |
||||
|
onUserHover(componentLoader: () => Promise<any>): void { |
||||
|
this.preloadQueue.push(componentLoader); |
||||
|
this.processPreloadQueue(); |
||||
|
} |
||||
|
|
||||
|
private async processPreloadQueue(): Promise<void> { |
||||
|
if (this.isPreloading || this.preloadQueue.length === 0) return; |
||||
|
|
||||
|
this.isPreloading = true; |
||||
|
|
||||
|
while (this.preloadQueue.length > 0) { |
||||
|
const loader = this.preloadQueue.shift(); |
||||
|
if (loader) { |
||||
|
try { |
||||
|
await loader(); |
||||
|
} catch (error) { |
||||
|
console.warn('Preload failed:', error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Small delay between preloads |
||||
|
await new Promise(resolve => setTimeout(resolve, 100)); |
||||
|
} |
||||
|
|
||||
|
this.isPreloading = false; |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### 4. Bundle Optimization |
||||
|
|
||||
|
Optimize bundle splitting: |
||||
|
|
||||
|
```typescript |
||||
|
// vite.config.ts |
||||
|
export default defineConfig({ |
||||
|
build: { |
||||
|
rollupOptions: { |
||||
|
output: { |
||||
|
manualChunks: (id) => { |
||||
|
// Group by feature |
||||
|
if (id.includes('qr')) return 'qr-feature'; |
||||
|
if (id.includes('three')) return '3d-feature'; |
||||
|
if (id.includes('chart')) return 'chart-feature'; |
||||
|
|
||||
|
// Group by vendor |
||||
|
if (id.includes('node_modules')) { |
||||
|
if (id.includes('vue')) return 'vue-vendor'; |
||||
|
if (id.includes('lodash')) return 'utils-vendor'; |
||||
|
return 'vendor'; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
## Common Pitfalls |
||||
|
|
||||
|
### 1. Over-Lazy Loading |
||||
|
|
||||
|
❌ **Don't lazy load everything:** |
||||
|
```typescript |
||||
|
// Bad: Lazy loading small components |
||||
|
const SmallButton = defineAsyncComponent(() => import('./SmallButton.vue')); |
||||
|
``` |
||||
|
|
||||
|
✅ **Do lazy load strategically:** |
||||
|
```typescript |
||||
|
// Good: Lazy load heavy components only |
||||
|
const HeavyDataProcessor = defineAsyncComponent(() => import('./HeavyDataProcessor.vue')); |
||||
|
``` |
||||
|
|
||||
|
### 2. Missing Loading States |
||||
|
|
||||
|
❌ **Don't leave users hanging:** |
||||
|
```vue |
||||
|
<template> |
||||
|
<LazyComponent v-if="show" /> |
||||
|
</template> |
||||
|
``` |
||||
|
|
||||
|
✅ **Do provide loading feedback:** |
||||
|
```vue |
||||
|
<template> |
||||
|
<Suspense> |
||||
|
<template #default> |
||||
|
<LazyComponent v-if="show" /> |
||||
|
</template> |
||||
|
<template #fallback> |
||||
|
<LoadingSpinner /> |
||||
|
</template> |
||||
|
</Suspense> |
||||
|
</template> |
||||
|
``` |
||||
|
|
||||
|
### 3. Ignoring Error States |
||||
|
|
||||
|
❌ **Don't ignore loading failures:** |
||||
|
```typescript |
||||
|
const LazyComponent = defineAsyncComponent(() => import('./Component.vue')); |
||||
|
``` |
||||
|
|
||||
|
✅ **Do handle errors gracefully:** |
||||
|
```typescript |
||||
|
const LazyComponent = defineAsyncComponent({ |
||||
|
loader: () => import('./Component.vue'), |
||||
|
errorComponent: ErrorFallback, |
||||
|
onError: (error, retry, fail) => { |
||||
|
console.error('Component load failed:', error); |
||||
|
// Retry once, then fail |
||||
|
retry(); |
||||
|
} |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
## Examples |
||||
|
|
||||
|
### Complete Lazy Loading Example |
||||
|
|
||||
|
See the following files for complete working examples: |
||||
|
|
||||
|
1. **`src/components/LazyLoadingExample.vue`** - Main example component |
||||
|
2. **`src/components/sub-components/HeavyComponent.vue`** - Data processing component |
||||
|
3. **`src/components/sub-components/QRScannerComponent.vue`** - Camera-dependent component |
||||
|
4. **`src/components/sub-components/ThreeJSViewer.vue`** - 3D rendering component |
||||
|
5. **`vite.config.optimized.mts`** - Optimized Vite configuration |
||||
|
|
||||
|
### Usage Example |
||||
|
|
||||
|
```vue |
||||
|
<template> |
||||
|
<div class="app"> |
||||
|
<h1>Lazy Loading Demo</h1> |
||||
|
|
||||
|
<LazyLoadingExample |
||||
|
:initial-load-heavy="false" |
||||
|
@qr-detected="handleQRCode" |
||||
|
@model-loaded="handleModelLoaded" |
||||
|
/> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue } from 'vue-facing-decorator'; |
||||
|
import LazyLoadingExample from '@/components/LazyLoadingExample.vue'; |
||||
|
|
||||
|
@Component({ |
||||
|
name: 'App', |
||||
|
components: { |
||||
|
LazyLoadingExample |
||||
|
} |
||||
|
}) |
||||
|
export default class App extends Vue { |
||||
|
handleQRCode(data: string): void { |
||||
|
console.log('QR code detected:', data); |
||||
|
} |
||||
|
|
||||
|
handleModelLoaded(info: any): void { |
||||
|
console.log('3D model loaded:', info); |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
``` |
||||
|
|
||||
|
## Conclusion |
||||
|
|
||||
|
Lazy loading with Vue 3 + Vite + `vue-facing-decorator` provides powerful tools for optimizing application performance. By implementing these patterns strategically, you can significantly improve initial load times while maintaining excellent user experience. |
||||
|
|
||||
|
Remember to: |
||||
|
- Use lazy loading for heavy components and features |
||||
|
- Provide meaningful loading and error states |
||||
|
- Monitor performance and bundle sizes |
||||
|
- Implement intelligent preloading strategies |
||||
|
- Handle errors gracefully |
||||
|
|
||||
|
For more information, see the Vue 3 documentation on [Async Components](https://vuejs.org/guide/components/async.html) and the Vite documentation on [Code Splitting](https://vitejs.dev/guide/features.html#code-splitting). |
@ -0,0 +1,293 @@ |
|||||
|
<template> |
||||
|
<div class="lazy-loading-example"> |
||||
|
<!-- Loading state with Suspense --> |
||||
|
<Suspense> |
||||
|
<template #default> |
||||
|
<!-- Main content with lazy-loaded components --> |
||||
|
<div class="content"> |
||||
|
<h1>Lazy Loading Example</h1> |
||||
|
|
||||
|
<!-- Lazy-loaded heavy component --> |
||||
|
<LazyHeavyComponent |
||||
|
v-if="showHeavyComponent" |
||||
|
:data="heavyComponentData" |
||||
|
@data-processed="handleDataProcessed" |
||||
|
/> |
||||
|
|
||||
|
<!-- Conditionally loaded components --> |
||||
|
<LazyQRScanner |
||||
|
v-if="showQRScanner" |
||||
|
@qr-detected="handleQRDetected" |
||||
|
/> |
||||
|
|
||||
|
<LazyThreeJSViewer |
||||
|
v-if="showThreeJS" |
||||
|
:model-url="threeJSModelUrl" |
||||
|
@model-loaded="handleModelLoaded" |
||||
|
/> |
||||
|
|
||||
|
<!-- Route-based lazy loading --> |
||||
|
<router-view v-slot="{ Component }"> |
||||
|
<component :is="Component" /> |
||||
|
</router-view> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<!-- Loading fallback --> |
||||
|
<template #fallback> |
||||
|
<div class="loading-fallback"> |
||||
|
<div class="spinner"></div> |
||||
|
<p>Loading components...</p> |
||||
|
</div> |
||||
|
</template> |
||||
|
</Suspense> |
||||
|
|
||||
|
<!-- Control buttons --> |
||||
|
<div class="controls"> |
||||
|
<button @click="toggleHeavyComponent"> |
||||
|
{{ showHeavyComponent ? 'Hide' : 'Show' }} Heavy Component |
||||
|
</button> |
||||
|
<button @click="toggleQRScanner"> |
||||
|
{{ showQRScanner ? 'Hide' : 'Show' }} QR Scanner |
||||
|
</button> |
||||
|
<button @click="toggleThreeJS"> |
||||
|
{{ showThreeJS ? 'Hide' : 'Show' }} 3D Viewer |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue, Prop, Watch } from 'vue-facing-decorator'; |
||||
|
import { defineAsyncComponent } from 'vue'; |
||||
|
|
||||
|
/** |
||||
|
* Lazy Loading Example Component |
||||
|
* |
||||
|
* Demonstrates various lazy loading patterns with vue-facing-decorator: |
||||
|
* - defineAsyncComponent for heavy components |
||||
|
* - Conditional loading based on user interaction |
||||
|
* - Suspense for loading states |
||||
|
* - Route-based lazy loading |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
@Component({ |
||||
|
name: 'LazyLoadingExample', |
||||
|
components: { |
||||
|
// Lazy-loaded components with loading and error states |
||||
|
LazyHeavyComponent: defineAsyncComponent({ |
||||
|
loader: () => import('./sub-components/HeavyComponent.vue'), |
||||
|
loadingComponent: { |
||||
|
template: '<div class="loading">Loading heavy component...</div>' |
||||
|
}, |
||||
|
errorComponent: { |
||||
|
template: '<div class="error">Failed to load heavy component</div>' |
||||
|
}, |
||||
|
delay: 200, // Show loading component after 200ms |
||||
|
timeout: 10000 // Timeout after 10 seconds |
||||
|
}), |
||||
|
|
||||
|
LazyQRScanner: defineAsyncComponent({ |
||||
|
loader: () => import('./sub-components/QRScannerComponent.vue'), |
||||
|
loadingComponent: { |
||||
|
template: '<div class="loading">Initializing QR scanner...</div>' |
||||
|
}, |
||||
|
errorComponent: { |
||||
|
template: '<div class="error">QR scanner not available</div>' |
||||
|
} |
||||
|
}), |
||||
|
|
||||
|
LazyThreeJSViewer: defineAsyncComponent({ |
||||
|
loader: () => import('./sub-components/ThreeJSViewer.vue'), |
||||
|
loadingComponent: { |
||||
|
template: '<div class="loading">Loading 3D viewer...</div>' |
||||
|
}, |
||||
|
errorComponent: { |
||||
|
template: '<div class="error">3D viewer failed to load</div>' |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
export default class LazyLoadingExample extends Vue { |
||||
|
// Component state |
||||
|
@Prop({ default: false }) readonly initialLoadHeavy!: boolean; |
||||
|
|
||||
|
// Reactive properties |
||||
|
showHeavyComponent = false; |
||||
|
showQRScanner = false; |
||||
|
showThreeJS = false; |
||||
|
|
||||
|
// Component data |
||||
|
heavyComponentData = { |
||||
|
items: Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `Item ${i}` })), |
||||
|
filters: { category: 'all', status: 'active' }, |
||||
|
sortBy: 'name' |
||||
|
}; |
||||
|
|
||||
|
threeJSModelUrl = '/models/lupine_plant/scene.gltf'; |
||||
|
|
||||
|
// Computed properties |
||||
|
get isLoadingAnyComponent(): boolean { |
||||
|
return this.showHeavyComponent || this.showQRScanner || this.showThreeJS; |
||||
|
} |
||||
|
|
||||
|
get componentCount(): number { |
||||
|
let count = 0; |
||||
|
if (this.showHeavyComponent) count++; |
||||
|
if (this.showQRScanner) count++; |
||||
|
if (this.showThreeJS) count++; |
||||
|
return count; |
||||
|
} |
||||
|
|
||||
|
// Lifecycle hooks |
||||
|
mounted(): void { |
||||
|
console.log('[LazyLoadingExample] Component mounted'); |
||||
|
|
||||
|
// Initialize based on props |
||||
|
if (this.initialLoadHeavy) { |
||||
|
this.showHeavyComponent = true; |
||||
|
} |
||||
|
|
||||
|
// Preload critical components |
||||
|
this.preloadCriticalComponents(); |
||||
|
} |
||||
|
|
||||
|
// Methods |
||||
|
toggleHeavyComponent(): void { |
||||
|
this.showHeavyComponent = !this.showHeavyComponent; |
||||
|
console.log('[LazyLoadingExample] Heavy component toggled:', this.showHeavyComponent); |
||||
|
} |
||||
|
|
||||
|
toggleQRScanner(): void { |
||||
|
this.showQRScanner = !this.showQRScanner; |
||||
|
console.log('[LazyLoadingExample] QR scanner toggled:', this.showQRScanner); |
||||
|
} |
||||
|
|
||||
|
toggleThreeJS(): void { |
||||
|
this.showThreeJS = !this.showThreeJS; |
||||
|
console.log('[LazyLoadingExample] ThreeJS viewer toggled:', this.showThreeJS); |
||||
|
} |
||||
|
|
||||
|
handleDataProcessed(data: any): void { |
||||
|
console.log('[LazyLoadingExample] Data processed:', data); |
||||
|
// Handle processed data from heavy component |
||||
|
} |
||||
|
|
||||
|
handleQRDetected(qrData: string): void { |
||||
|
console.log('[LazyLoadingExample] QR code detected:', qrData); |
||||
|
// Handle QR code data |
||||
|
} |
||||
|
|
||||
|
handleModelLoaded(modelInfo: any): void { |
||||
|
console.log('[LazyLoadingExample] 3D model loaded:', modelInfo); |
||||
|
// Handle 3D model loaded event |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Preload critical components for better UX |
||||
|
*/ |
||||
|
private preloadCriticalComponents(): void { |
||||
|
// Preload components that are likely to be used |
||||
|
if (process.env.NODE_ENV === 'production') { |
||||
|
// In production, preload based on user behavior patterns |
||||
|
this.preloadComponent(() => import('./sub-components/HeavyComponent.vue')); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Preload a component without rendering it |
||||
|
*/ |
||||
|
private preloadComponent(componentLoader: () => Promise<any>): void { |
||||
|
componentLoader().catch(error => { |
||||
|
console.warn('[LazyLoadingExample] Preload failed:', error); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// Watchers |
||||
|
@Watch('showHeavyComponent') |
||||
|
onHeavyComponentToggle(newValue: boolean): void { |
||||
|
if (newValue) { |
||||
|
// Component is being shown - could trigger analytics |
||||
|
console.log('[LazyLoadingExample] Heavy component shown'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Watch('componentCount') |
||||
|
onComponentCountChange(newCount: number): void { |
||||
|
console.log('[LazyLoadingExample] Active component count:', newCount); |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.lazy-loading-example { |
||||
|
padding: 20px; |
||||
|
max-width: 1200px; |
||||
|
margin: 0 auto; |
||||
|
} |
||||
|
|
||||
|
.content { |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.controls { |
||||
|
display: flex; |
||||
|
gap: 10px; |
||||
|
flex-wrap: wrap; |
||||
|
margin-top: 20px; |
||||
|
} |
||||
|
|
||||
|
.controls button { |
||||
|
padding: 10px 20px; |
||||
|
border: 1px solid #ccc; |
||||
|
border-radius: 4px; |
||||
|
background: #f8f9fa; |
||||
|
cursor: pointer; |
||||
|
transition: background-color 0.2s; |
||||
|
} |
||||
|
|
||||
|
.controls button:hover { |
||||
|
background: #e9ecef; |
||||
|
} |
||||
|
|
||||
|
.loading-fallback { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
padding: 40px; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.spinner { |
||||
|
width: 40px; |
||||
|
height: 40px; |
||||
|
border: 4px solid #f3f3f3; |
||||
|
border-top: 4px solid #3498db; |
||||
|
border-radius: 50%; |
||||
|
animation: spin 1s linear infinite; |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
@keyframes spin { |
||||
|
0% { transform: rotate(0deg); } |
||||
|
100% { transform: rotate(360deg); } |
||||
|
} |
||||
|
|
||||
|
.loading { |
||||
|
padding: 20px; |
||||
|
text-align: center; |
||||
|
color: #666; |
||||
|
} |
||||
|
|
||||
|
.error { |
||||
|
padding: 20px; |
||||
|
text-align: center; |
||||
|
color: #dc3545; |
||||
|
background: #f8d7da; |
||||
|
border: 1px solid #f5c6cb; |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,542 @@ |
|||||
|
<template> |
||||
|
<div class="heavy-component"> |
||||
|
<h2>Heavy Data Processing Component</h2> |
||||
|
|
||||
|
<!-- Data processing controls --> |
||||
|
<div class="controls"> |
||||
|
<button @click="processData" :disabled="isProcessing"> |
||||
|
{{ isProcessing ? 'Processing...' : 'Process Data' }} |
||||
|
</button> |
||||
|
<button @click="clearResults" :disabled="isProcessing"> |
||||
|
Clear Results |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Processing status --> |
||||
|
<div v-if="isProcessing" class="processing-status"> |
||||
|
<div class="progress-bar"> |
||||
|
<div class="progress-fill" :style="{ width: progress + '%' }"></div> |
||||
|
</div> |
||||
|
<p>Processing {{ processedCount }} of {{ totalItems }} items...</p> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Results display --> |
||||
|
<div v-if="processedData.length > 0" class="results"> |
||||
|
<h3>Processed Results ({{ processedData.length }} items)</h3> |
||||
|
|
||||
|
<!-- Filter controls --> |
||||
|
<div class="filters"> |
||||
|
<input |
||||
|
v-model="searchTerm" |
||||
|
placeholder="Search items..." |
||||
|
class="search-input" |
||||
|
/> |
||||
|
<select v-model="sortBy" class="sort-select"> |
||||
|
<option value="name">Sort by Name</option> |
||||
|
<option value="id">Sort by ID</option> |
||||
|
<option value="processed">Sort by Processed Date</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Results list --> |
||||
|
<div class="results-list"> |
||||
|
<div |
||||
|
v-for="item in filteredAndSortedData" |
||||
|
:key="item.id" |
||||
|
class="result-item" |
||||
|
> |
||||
|
<div class="item-header"> |
||||
|
<span class="item-name">{{ item.name }}</span> |
||||
|
<span class="item-id">#{{ item.id }}</span> |
||||
|
</div> |
||||
|
<div class="item-details"> |
||||
|
<span class="processed-date"> |
||||
|
Processed: {{ formatDate(item.processedAt) }} |
||||
|
</span> |
||||
|
<span class="processing-time"> |
||||
|
Time: {{ item.processingTime }}ms |
||||
|
</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Pagination --> |
||||
|
<div v-if="totalPages > 1" class="pagination"> |
||||
|
<button |
||||
|
@click="previousPage" |
||||
|
:disabled="currentPage === 1" |
||||
|
class="page-btn" |
||||
|
> |
||||
|
Previous |
||||
|
</button> |
||||
|
<span class="page-info"> |
||||
|
Page {{ currentPage }} of {{ totalPages }} |
||||
|
</span> |
||||
|
<button |
||||
|
@click="nextPage" |
||||
|
:disabled="currentPage === totalPages" |
||||
|
class="page-btn" |
||||
|
> |
||||
|
Next |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Performance metrics --> |
||||
|
<div v-if="performanceMetrics" class="performance-metrics"> |
||||
|
<h4>Performance Metrics</h4> |
||||
|
<div class="metrics-grid"> |
||||
|
<div class="metric"> |
||||
|
<span class="metric-label">Total Processing Time:</span> |
||||
|
<span class="metric-value">{{ performanceMetrics.totalTime }}ms</span> |
||||
|
</div> |
||||
|
<div class="metric"> |
||||
|
<span class="metric-label">Average per Item:</span> |
||||
|
<span class="metric-value">{{ performanceMetrics.averageTime }}ms</span> |
||||
|
</div> |
||||
|
<div class="metric"> |
||||
|
<span class="metric-label">Memory Usage:</span> |
||||
|
<span class="metric-value">{{ performanceMetrics.memoryUsage }}MB</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue, Prop, Emit } from 'vue-facing-decorator'; |
||||
|
|
||||
|
interface ProcessedItem { |
||||
|
id: number; |
||||
|
name: string; |
||||
|
processedAt: Date; |
||||
|
processingTime: number; |
||||
|
result: any; |
||||
|
} |
||||
|
|
||||
|
interface PerformanceMetrics { |
||||
|
totalTime: number; |
||||
|
averageTime: number; |
||||
|
memoryUsage: number; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Heavy Component for Data Processing |
||||
|
* |
||||
|
* Demonstrates a component that performs intensive data processing |
||||
|
* and would benefit from lazy loading to avoid blocking the main thread. |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
@Component({ |
||||
|
name: 'HeavyComponent' |
||||
|
}) |
||||
|
export default class HeavyComponent extends Vue { |
||||
|
@Prop({ required: true }) readonly data!: { |
||||
|
items: Array<{ id: number; name: string }>; |
||||
|
filters: Record<string, any>; |
||||
|
sortBy: string; |
||||
|
}; |
||||
|
|
||||
|
// Component state |
||||
|
isProcessing = false; |
||||
|
processedData: ProcessedItem[] = []; |
||||
|
progress = 0; |
||||
|
processedCount = 0; |
||||
|
totalItems = 0; |
||||
|
|
||||
|
// UI state |
||||
|
searchTerm = ''; |
||||
|
sortBy = 'name'; |
||||
|
currentPage = 1; |
||||
|
itemsPerPage = 50; |
||||
|
|
||||
|
// Performance tracking |
||||
|
performanceMetrics: PerformanceMetrics | null = null; |
||||
|
startTime = 0; |
||||
|
|
||||
|
// Computed properties |
||||
|
get filteredAndSortedData(): ProcessedItem[] { |
||||
|
let filtered = this.processedData; |
||||
|
|
||||
|
// Apply search filter |
||||
|
if (this.searchTerm) { |
||||
|
filtered = filtered.filter(item => |
||||
|
item.name.toLowerCase().includes(this.searchTerm.toLowerCase()) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// Apply sorting |
||||
|
filtered.sort((a, b) => { |
||||
|
switch (this.sortBy) { |
||||
|
case 'name': |
||||
|
return a.name.localeCompare(b.name); |
||||
|
case 'id': |
||||
|
return a.id - b.id; |
||||
|
case 'processed': |
||||
|
return b.processedAt.getTime() - a.processedAt.getTime(); |
||||
|
default: |
||||
|
return 0; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return filtered; |
||||
|
} |
||||
|
|
||||
|
get paginatedData(): ProcessedItem[] { |
||||
|
const start = (this.currentPage - 1) * this.itemsPerPage; |
||||
|
const end = start + this.itemsPerPage; |
||||
|
return this.filteredAndSortedData.slice(start, end); |
||||
|
} |
||||
|
|
||||
|
get totalPages(): number { |
||||
|
return Math.ceil(this.filteredAndSortedData.length / this.itemsPerPage); |
||||
|
} |
||||
|
|
||||
|
// Lifecycle hooks |
||||
|
mounted(): void { |
||||
|
console.log('[HeavyComponent] Component mounted with', this.data.items.length, 'items'); |
||||
|
this.totalItems = this.data.items.length; |
||||
|
} |
||||
|
|
||||
|
// Methods |
||||
|
async processData(): Promise<void> { |
||||
|
if (this.isProcessing) return; |
||||
|
|
||||
|
this.isProcessing = true; |
||||
|
this.progress = 0; |
||||
|
this.processedCount = 0; |
||||
|
this.processedData = []; |
||||
|
this.startTime = performance.now(); |
||||
|
|
||||
|
console.log('[HeavyComponent] Starting data processing...'); |
||||
|
|
||||
|
try { |
||||
|
// Process items in batches to avoid blocking the UI |
||||
|
const batchSize = 10; |
||||
|
const items = this.data.items; |
||||
|
|
||||
|
for (let i = 0; i < items.length; i += batchSize) { |
||||
|
const batch = items.slice(i, i + batchSize); |
||||
|
|
||||
|
// Process batch |
||||
|
await this.processBatch(batch); |
||||
|
|
||||
|
// Update progress |
||||
|
this.processedCount = Math.min(i + batchSize, items.length); |
||||
|
this.progress = (this.processedCount / items.length) * 100; |
||||
|
|
||||
|
// Allow UI to update |
||||
|
await this.$nextTick(); |
||||
|
|
||||
|
// Small delay to prevent overwhelming the UI |
||||
|
await new Promise(resolve => setTimeout(resolve, 10)); |
||||
|
} |
||||
|
|
||||
|
// Calculate performance metrics |
||||
|
this.calculatePerformanceMetrics(); |
||||
|
|
||||
|
// Emit completion event |
||||
|
this.$emit('data-processed', { |
||||
|
totalItems: this.processedData.length, |
||||
|
processingTime: performance.now() - this.startTime, |
||||
|
metrics: this.performanceMetrics |
||||
|
}); |
||||
|
|
||||
|
console.log('[HeavyComponent] Data processing completed'); |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('[HeavyComponent] Processing error:', error); |
||||
|
this.$emit('processing-error', error); |
||||
|
} finally { |
||||
|
this.isProcessing = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async processBatch(batch: Array<{ id: number; name: string }>): Promise<void> { |
||||
|
const processedBatch = await Promise.all( |
||||
|
batch.map(async (item) => { |
||||
|
const itemStartTime = performance.now(); |
||||
|
|
||||
|
// Simulate heavy processing |
||||
|
await this.simulateHeavyProcessing(item); |
||||
|
|
||||
|
const processingTime = performance.now() - itemStartTime; |
||||
|
|
||||
|
return { |
||||
|
id: item.id, |
||||
|
name: item.name, |
||||
|
processedAt: new Date(), |
||||
|
processingTime: Math.round(processingTime), |
||||
|
result: this.generateResult(item) |
||||
|
}; |
||||
|
}) |
||||
|
); |
||||
|
|
||||
|
this.processedData.push(...processedBatch); |
||||
|
} |
||||
|
|
||||
|
private async simulateHeavyProcessing(item: { id: number; name: string }): Promise<void> { |
||||
|
// Simulate CPU-intensive work |
||||
|
const complexity = item.name.length * item.id; |
||||
|
const iterations = Math.min(complexity, 1000); // Cap at 1000 iterations |
||||
|
|
||||
|
for (let i = 0; i < iterations; i++) { |
||||
|
// Simulate work |
||||
|
Math.sqrt(i) * Math.random(); |
||||
|
} |
||||
|
|
||||
|
// Simulate async work |
||||
|
await new Promise(resolve => setTimeout(resolve, Math.random() * 10)); |
||||
|
} |
||||
|
|
||||
|
private generateResult(item: { id: number; name: string }): any { |
||||
|
return { |
||||
|
hash: this.generateHash(item.name + item.id), |
||||
|
category: this.categorizeItem(item), |
||||
|
score: Math.random() * 100, |
||||
|
tags: this.generateTags(item) |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private generateHash(input: string): string { |
||||
|
let hash = 0; |
||||
|
for (let i = 0; i < input.length; i++) { |
||||
|
const char = input.charCodeAt(i); |
||||
|
hash = ((hash << 5) - hash) + char; |
||||
|
hash = hash & hash; // Convert to 32-bit integer |
||||
|
} |
||||
|
return hash.toString(16); |
||||
|
} |
||||
|
|
||||
|
private categorizeItem(item: { id: number; name: string }): string { |
||||
|
const categories = ['A', 'B', 'C', 'D', 'E']; |
||||
|
return categories[item.id % categories.length]; |
||||
|
} |
||||
|
|
||||
|
private generateTags(item: { id: number; name: string }): string[] { |
||||
|
const tags = ['important', 'urgent', 'review', 'archive', 'featured']; |
||||
|
return tags.filter((_, index) => (item.id + index) % 3 === 0); |
||||
|
} |
||||
|
|
||||
|
private calculatePerformanceMetrics(): void { |
||||
|
const totalTime = performance.now() - this.startTime; |
||||
|
const averageTime = totalTime / this.processedData.length; |
||||
|
|
||||
|
// Simulate memory usage calculation |
||||
|
const memoryUsage = this.processedData.length * 0.1; // 0.1MB per item |
||||
|
|
||||
|
this.performanceMetrics = { |
||||
|
totalTime: Math.round(totalTime), |
||||
|
averageTime: Math.round(averageTime), |
||||
|
memoryUsage: Math.round(memoryUsage * 100) / 100 |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
clearResults(): void { |
||||
|
this.processedData = []; |
||||
|
this.performanceMetrics = null; |
||||
|
this.searchTerm = ''; |
||||
|
this.currentPage = 1; |
||||
|
console.log('[HeavyComponent] Results cleared'); |
||||
|
} |
||||
|
|
||||
|
previousPage(): void { |
||||
|
if (this.currentPage > 1) { |
||||
|
this.currentPage--; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
nextPage(): void { |
||||
|
if (this.currentPage < this.totalPages) { |
||||
|
this.currentPage++; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
formatDate(date: Date): string { |
||||
|
return date.toLocaleString(); |
||||
|
} |
||||
|
|
||||
|
// Event emitters |
||||
|
@Emit('data-processed') |
||||
|
emitDataProcessed(data: any): any { |
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
@Emit('processing-error') |
||||
|
emitProcessingError(error: Error): Error { |
||||
|
return error; |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.heavy-component { |
||||
|
padding: 20px; |
||||
|
border: 1px solid #ddd; |
||||
|
border-radius: 8px; |
||||
|
background: #f9f9f9; |
||||
|
} |
||||
|
|
||||
|
.controls { |
||||
|
display: flex; |
||||
|
gap: 10px; |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.controls button { |
||||
|
padding: 8px 16px; |
||||
|
border: 1px solid #ccc; |
||||
|
border-radius: 4px; |
||||
|
background: #fff; |
||||
|
cursor: pointer; |
||||
|
transition: background-color 0.2s; |
||||
|
} |
||||
|
|
||||
|
.controls button:hover:not(:disabled) { |
||||
|
background: #e9ecef; |
||||
|
} |
||||
|
|
||||
|
.controls button:disabled { |
||||
|
opacity: 0.6; |
||||
|
cursor: not-allowed; |
||||
|
} |
||||
|
|
||||
|
.processing-status { |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.progress-bar { |
||||
|
width: 100%; |
||||
|
height: 20px; |
||||
|
background: #e9ecef; |
||||
|
border-radius: 10px; |
||||
|
overflow: hidden; |
||||
|
margin-bottom: 10px; |
||||
|
} |
||||
|
|
||||
|
.progress-fill { |
||||
|
height: 100%; |
||||
|
background: linear-gradient(90deg, #007bff, #0056b3); |
||||
|
transition: width 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
.results { |
||||
|
margin-top: 20px; |
||||
|
} |
||||
|
|
||||
|
.filters { |
||||
|
display: flex; |
||||
|
gap: 10px; |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.search-input, |
||||
|
.sort-select { |
||||
|
padding: 8px; |
||||
|
border: 1px solid #ccc; |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
|
||||
|
.search-input { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.results-list { |
||||
|
max-height: 400px; |
||||
|
overflow-y: auto; |
||||
|
border: 1px solid #ddd; |
||||
|
border-radius: 4px; |
||||
|
background: #fff; |
||||
|
} |
||||
|
|
||||
|
.result-item { |
||||
|
padding: 12px; |
||||
|
border-bottom: 1px solid #eee; |
||||
|
} |
||||
|
|
||||
|
.result-item:last-child { |
||||
|
border-bottom: none; |
||||
|
} |
||||
|
|
||||
|
.item-header { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
margin-bottom: 8px; |
||||
|
} |
||||
|
|
||||
|
.item-name { |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
.item-id { |
||||
|
color: #666; |
||||
|
font-size: 0.9em; |
||||
|
} |
||||
|
|
||||
|
.item-details { |
||||
|
display: flex; |
||||
|
gap: 20px; |
||||
|
font-size: 0.85em; |
||||
|
color: #666; |
||||
|
} |
||||
|
|
||||
|
.pagination { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
gap: 15px; |
||||
|
margin-top: 20px; |
||||
|
} |
||||
|
|
||||
|
.page-btn { |
||||
|
padding: 6px 12px; |
||||
|
border: 1px solid #ccc; |
||||
|
border-radius: 4px; |
||||
|
background: #fff; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
.page-btn:disabled { |
||||
|
opacity: 0.5; |
||||
|
cursor: not-allowed; |
||||
|
} |
||||
|
|
||||
|
.page-info { |
||||
|
font-size: 0.9em; |
||||
|
color: #666; |
||||
|
} |
||||
|
|
||||
|
.performance-metrics { |
||||
|
margin-top: 20px; |
||||
|
padding: 15px; |
||||
|
background: #e8f4fd; |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
|
||||
|
.metrics-grid { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
||||
|
gap: 15px; |
||||
|
margin-top: 10px; |
||||
|
} |
||||
|
|
||||
|
.metric { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
padding: 8px; |
||||
|
background: #fff; |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
|
||||
|
.metric-label { |
||||
|
font-weight: bold; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.metric-value { |
||||
|
color: #007bff; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,708 @@ |
|||||
|
<template> |
||||
|
<div class="qr-scanner-component"> |
||||
|
<h2>QR Code Scanner</h2> |
||||
|
|
||||
|
<!-- Camera controls --> |
||||
|
<div class="camera-controls"> |
||||
|
<button @click="startScanning" :disabled="isScanning || !hasCamera"> |
||||
|
{{ isScanning ? 'Scanning...' : 'Start Scanning' }} |
||||
|
</button> |
||||
|
<button @click="stopScanning" :disabled="!isScanning"> |
||||
|
Stop Scanning |
||||
|
</button> |
||||
|
<button @click="switchCamera" :disabled="!isScanning || cameras.length <= 1"> |
||||
|
Switch Camera |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Camera status --> |
||||
|
<div class="camera-status"> |
||||
|
<div v-if="!hasCamera" class="status-error"> |
||||
|
<p>Camera not available</p> |
||||
|
<p class="status-detail">This device doesn't have a camera or camera access is denied.</p> |
||||
|
</div> |
||||
|
|
||||
|
<div v-else-if="!isScanning" class="status-info"> |
||||
|
<p>Camera ready</p> |
||||
|
<p class="status-detail">Click "Start Scanning" to begin QR code detection.</p> |
||||
|
</div> |
||||
|
|
||||
|
<div v-else class="status-scanning"> |
||||
|
<p>Scanning for QR codes...</p> |
||||
|
<p class="status-detail">Point camera at a QR code to scan.</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Camera view --> |
||||
|
<div v-if="isScanning && hasCamera" class="camera-container"> |
||||
|
<video |
||||
|
ref="videoElement" |
||||
|
class="camera-video" |
||||
|
autoplay |
||||
|
playsinline |
||||
|
muted |
||||
|
></video> |
||||
|
|
||||
|
<!-- Scanning overlay --> |
||||
|
<div class="scanning-overlay"> |
||||
|
<div class="scan-frame"></div> |
||||
|
<div class="scan-line"></div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Scan results --> |
||||
|
<div v-if="scanResults.length > 0" class="scan-results"> |
||||
|
<h3>Scan Results ({{ scanResults.length }})</h3> |
||||
|
|
||||
|
<div class="results-list"> |
||||
|
<div |
||||
|
v-for="(result, index) in scanResults" |
||||
|
:key="index" |
||||
|
class="result-item" |
||||
|
> |
||||
|
<div class="result-header"> |
||||
|
<span class="result-number">#{{ index + 1 }}</span> |
||||
|
<span class="result-time">{{ formatTime(result.timestamp) }}</span> |
||||
|
</div> |
||||
|
<div class="result-content"> |
||||
|
<div class="qr-data"> |
||||
|
<strong>Data:</strong> {{ result.data }} |
||||
|
</div> |
||||
|
<div class="qr-format"> |
||||
|
<strong>Format:</strong> {{ result.format }} |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="result-actions"> |
||||
|
<button @click="copyToClipboard(result.data)" class="copy-btn"> |
||||
|
Copy |
||||
|
</button> |
||||
|
<button @click="removeResult(index)" class="remove-btn"> |
||||
|
Remove |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="results-actions"> |
||||
|
<button @click="clearResults" class="clear-btn"> |
||||
|
Clear All Results |
||||
|
</button> |
||||
|
<button @click="exportResults" class="export-btn"> |
||||
|
Export Results |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Settings panel --> |
||||
|
<div class="settings-panel"> |
||||
|
<h3>Scanner Settings</h3> |
||||
|
|
||||
|
<div class="setting-group"> |
||||
|
<label> |
||||
|
<input |
||||
|
type="checkbox" |
||||
|
v-model="settings.continuousScanning" |
||||
|
/> |
||||
|
Continuous Scanning |
||||
|
</label> |
||||
|
<p class="setting-description"> |
||||
|
Automatically scan multiple QR codes without stopping |
||||
|
</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="setting-group"> |
||||
|
<label> |
||||
|
<input |
||||
|
type="checkbox" |
||||
|
v-model="settings.audioFeedback" |
||||
|
/> |
||||
|
Audio Feedback |
||||
|
</label> |
||||
|
<p class="setting-description"> |
||||
|
Play sound when QR code is detected |
||||
|
</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="setting-group"> |
||||
|
<label> |
||||
|
<input |
||||
|
type="checkbox" |
||||
|
v-model="settings.vibrateOnScan" |
||||
|
/> |
||||
|
Vibration Feedback |
||||
|
</label> |
||||
|
<p class="setting-description"> |
||||
|
Vibrate device when QR code is detected |
||||
|
</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="setting-group"> |
||||
|
<label>Scan Interval (ms):</label> |
||||
|
<input |
||||
|
type="number" |
||||
|
v-model.number="settings.scanInterval" |
||||
|
min="100" |
||||
|
max="5000" |
||||
|
step="100" |
||||
|
/> |
||||
|
<p class="setting-description"> |
||||
|
Time between scans (lower = faster, higher = more accurate) |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue, Emit } from 'vue-facing-decorator'; |
||||
|
|
||||
|
interface ScanResult { |
||||
|
data: string; |
||||
|
format: string; |
||||
|
timestamp: Date; |
||||
|
} |
||||
|
|
||||
|
interface ScannerSettings { |
||||
|
continuousScanning: boolean; |
||||
|
audioFeedback: boolean; |
||||
|
vibrateOnScan: boolean; |
||||
|
scanInterval: number; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* QR Scanner Component |
||||
|
* |
||||
|
* Demonstrates lazy loading for camera-dependent features. |
||||
|
* This component would benefit from lazy loading as it requires |
||||
|
* camera permissions and heavy camera processing libraries. |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
@Component({ |
||||
|
name: 'QRScannerComponent' |
||||
|
}) |
||||
|
export default class QRScannerComponent extends Vue { |
||||
|
// Component state |
||||
|
isScanning = false; |
||||
|
hasCamera = false; |
||||
|
cameras: MediaDeviceInfo[] = []; |
||||
|
currentCameraIndex = 0; |
||||
|
|
||||
|
// Video element reference |
||||
|
videoElement: HTMLVideoElement | null = null; |
||||
|
|
||||
|
// Scan results |
||||
|
scanResults: ScanResult[] = []; |
||||
|
|
||||
|
// Scanner settings |
||||
|
settings: ScannerSettings = { |
||||
|
continuousScanning: true, |
||||
|
audioFeedback: true, |
||||
|
vibrateOnScan: true, |
||||
|
scanInterval: 500 |
||||
|
}; |
||||
|
|
||||
|
// Internal state |
||||
|
private stream: MediaStream | null = null; |
||||
|
private scanInterval: number | null = null; |
||||
|
private lastScanTime = 0; |
||||
|
|
||||
|
// Lifecycle hooks |
||||
|
async mounted(): Promise<void> { |
||||
|
console.log('[QRScannerComponent] Component mounted'); |
||||
|
await this.initializeCamera(); |
||||
|
} |
||||
|
|
||||
|
beforeUnmount(): void { |
||||
|
this.stopScanning(); |
||||
|
console.log('[QRScannerComponent] Component unmounting'); |
||||
|
} |
||||
|
|
||||
|
// Methods |
||||
|
async initializeCamera(): Promise<void> { |
||||
|
try { |
||||
|
// Check if camera is available |
||||
|
const devices = await navigator.mediaDevices.enumerateDevices(); |
||||
|
this.cameras = devices.filter(device => device.kind === 'videoinput'); |
||||
|
this.hasCamera = this.cameras.length > 0; |
||||
|
|
||||
|
if (this.hasCamera) { |
||||
|
console.log('[QRScannerComponent] Camera available:', this.cameras.length, 'devices'); |
||||
|
} else { |
||||
|
console.warn('[QRScannerComponent] No camera devices found'); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('[QRScannerComponent] Camera initialization error:', error); |
||||
|
this.hasCamera = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async startScanning(): Promise<void> { |
||||
|
if (!this.hasCamera || this.isScanning) return; |
||||
|
|
||||
|
try { |
||||
|
console.log('[QRScannerComponent] Starting QR scanning...'); |
||||
|
|
||||
|
// Get camera stream |
||||
|
const constraints = { |
||||
|
video: { |
||||
|
deviceId: this.cameras[this.currentCameraIndex]?.deviceId |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
this.stream = await navigator.mediaDevices.getUserMedia(constraints); |
||||
|
|
||||
|
// Set up video element |
||||
|
this.videoElement = this.$refs.videoElement as HTMLVideoElement; |
||||
|
if (this.videoElement) { |
||||
|
this.videoElement.srcObject = this.stream; |
||||
|
await this.videoElement.play(); |
||||
|
} |
||||
|
|
||||
|
this.isScanning = true; |
||||
|
|
||||
|
// Start QR code detection |
||||
|
this.startQRDetection(); |
||||
|
|
||||
|
console.log('[QRScannerComponent] QR scanning started'); |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('[QRScannerComponent] Failed to start scanning:', error); |
||||
|
this.hasCamera = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
stopScanning(): void { |
||||
|
if (!this.isScanning) return; |
||||
|
|
||||
|
console.log('[QRScannerComponent] Stopping QR scanning...'); |
||||
|
|
||||
|
// Stop QR detection |
||||
|
this.stopQRDetection(); |
||||
|
|
||||
|
// Stop camera stream |
||||
|
if (this.stream) { |
||||
|
this.stream.getTracks().forEach(track => track.stop()); |
||||
|
this.stream = null; |
||||
|
} |
||||
|
|
||||
|
// Clear video element |
||||
|
if (this.videoElement) { |
||||
|
this.videoElement.srcObject = null; |
||||
|
this.videoElement = null; |
||||
|
} |
||||
|
|
||||
|
this.isScanning = false; |
||||
|
console.log('[QRScannerComponent] QR scanning stopped'); |
||||
|
} |
||||
|
|
||||
|
async switchCamera(): Promise<void> { |
||||
|
if (this.cameras.length <= 1) return; |
||||
|
|
||||
|
// Stop current scanning |
||||
|
this.stopScanning(); |
||||
|
|
||||
|
// Switch to next camera |
||||
|
this.currentCameraIndex = (this.currentCameraIndex + 1) % this.cameras.length; |
||||
|
|
||||
|
// Restart scanning with new camera |
||||
|
await this.startScanning(); |
||||
|
|
||||
|
console.log('[QRScannerComponent] Switched to camera:', this.currentCameraIndex); |
||||
|
} |
||||
|
|
||||
|
private startQRDetection(): void { |
||||
|
if (!this.settings.continuousScanning) return; |
||||
|
|
||||
|
this.scanInterval = window.setInterval(() => { |
||||
|
this.detectQRCode(); |
||||
|
}, this.settings.scanInterval); |
||||
|
} |
||||
|
|
||||
|
private stopQRDetection(): void { |
||||
|
if (this.scanInterval) { |
||||
|
clearInterval(this.scanInterval); |
||||
|
this.scanInterval = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async detectQRCode(): Promise<void> { |
||||
|
if (!this.videoElement || !this.isScanning) return; |
||||
|
|
||||
|
const now = Date.now(); |
||||
|
if (now - this.lastScanTime < this.settings.scanInterval) return; |
||||
|
|
||||
|
try { |
||||
|
// Simulate QR code detection |
||||
|
// In a real implementation, you would use a QR code library like jsQR |
||||
|
const detectedQR = await this.simulateQRDetection(); |
||||
|
|
||||
|
if (detectedQR) { |
||||
|
this.addScanResult(detectedQR); |
||||
|
this.lastScanTime = now; |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('[QRScannerComponent] QR detection error:', error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async simulateQRDetection(): Promise<ScanResult | null> { |
||||
|
// Simulate QR code detection with random chance |
||||
|
if (Math.random() < 0.1) { // 10% chance of detection |
||||
|
const sampleData = [ |
||||
|
'https://example.com/qr1', |
||||
|
'WIFI:S:MyNetwork;T:WPA;P:password123;;', |
||||
|
'BEGIN:VCARD\nVERSION:3.0\nFN:John Doe\nTEL:+1234567890\nEND:VCARD', |
||||
|
'otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example' |
||||
|
]; |
||||
|
|
||||
|
const formats = ['URL', 'WiFi', 'vCard', 'TOTP']; |
||||
|
const randomIndex = Math.floor(Math.random() * sampleData.length); |
||||
|
|
||||
|
return { |
||||
|
data: sampleData[randomIndex], |
||||
|
format: formats[randomIndex], |
||||
|
timestamp: new Date() |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
private addScanResult(result: ScanResult): void { |
||||
|
// Check for duplicates |
||||
|
const isDuplicate = this.scanResults.some( |
||||
|
existing => existing.data === result.data |
||||
|
); |
||||
|
|
||||
|
if (!isDuplicate) { |
||||
|
this.scanResults.unshift(result); |
||||
|
|
||||
|
// Provide feedback |
||||
|
this.provideFeedback(); |
||||
|
|
||||
|
// Emit event |
||||
|
this.$emit('qr-detected', result.data); |
||||
|
|
||||
|
console.log('[QRScannerComponent] QR code detected:', result.data); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private provideFeedback(): void { |
||||
|
// Audio feedback |
||||
|
if (this.settings.audioFeedback) { |
||||
|
this.playBeepSound(); |
||||
|
} |
||||
|
|
||||
|
// Vibration feedback |
||||
|
if (this.settings.vibrateOnScan && 'vibrate' in navigator) { |
||||
|
navigator.vibrate(100); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private playBeepSound(): void { |
||||
|
// Create a simple beep sound |
||||
|
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); |
||||
|
const oscillator = audioContext.createOscillator(); |
||||
|
const gainNode = audioContext.createGain(); |
||||
|
|
||||
|
oscillator.connect(gainNode); |
||||
|
gainNode.connect(audioContext.destination); |
||||
|
|
||||
|
oscillator.frequency.setValueAtTime(800, audioContext.currentTime); |
||||
|
oscillator.type = 'sine'; |
||||
|
|
||||
|
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); |
||||
|
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1); |
||||
|
|
||||
|
oscillator.start(audioContext.currentTime); |
||||
|
oscillator.stop(audioContext.currentTime + 0.1); |
||||
|
} |
||||
|
|
||||
|
copyToClipboard(text: string): void { |
||||
|
navigator.clipboard.writeText(text).then(() => { |
||||
|
console.log('[QRScannerComponent] Copied to clipboard:', text); |
||||
|
}).catch(error => { |
||||
|
console.error('[QRScannerComponent] Failed to copy:', error); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
removeResult(index: number): void { |
||||
|
this.scanResults.splice(index, 1); |
||||
|
} |
||||
|
|
||||
|
clearResults(): void { |
||||
|
this.scanResults = []; |
||||
|
console.log('[QRScannerComponent] Results cleared'); |
||||
|
} |
||||
|
|
||||
|
exportResults(): void { |
||||
|
const data = JSON.stringify(this.scanResults, null, 2); |
||||
|
const blob = new Blob([data], { type: 'application/json' }); |
||||
|
const url = URL.createObjectURL(blob); |
||||
|
|
||||
|
const a = document.createElement('a'); |
||||
|
a.href = url; |
||||
|
a.download = `qr-scan-results-${new Date().toISOString().split('T')[0]}.json`; |
||||
|
a.click(); |
||||
|
|
||||
|
URL.revokeObjectURL(url); |
||||
|
console.log('[QRScannerComponent] Results exported'); |
||||
|
} |
||||
|
|
||||
|
formatTime(date: Date): string { |
||||
|
return date.toLocaleTimeString(); |
||||
|
} |
||||
|
|
||||
|
// Event emitters |
||||
|
@Emit('qr-detected') |
||||
|
emitQRDetected(data: string): string { |
||||
|
return data; |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.qr-scanner-component { |
||||
|
padding: 20px; |
||||
|
border: 1px solid #ddd; |
||||
|
border-radius: 8px; |
||||
|
background: #f9f9f9; |
||||
|
} |
||||
|
|
||||
|
.camera-controls { |
||||
|
display: flex; |
||||
|
gap: 10px; |
||||
|
margin-bottom: 20px; |
||||
|
flex-wrap: wrap; |
||||
|
} |
||||
|
|
||||
|
.camera-controls button { |
||||
|
padding: 8px 16px; |
||||
|
border: 1px solid #ccc; |
||||
|
border-radius: 4px; |
||||
|
background: #fff; |
||||
|
cursor: pointer; |
||||
|
transition: background-color 0.2s; |
||||
|
} |
||||
|
|
||||
|
.camera-controls button:hover:not(:disabled) { |
||||
|
background: #e9ecef; |
||||
|
} |
||||
|
|
||||
|
.camera-controls button:disabled { |
||||
|
opacity: 0.6; |
||||
|
cursor: not-allowed; |
||||
|
} |
||||
|
|
||||
|
.camera-status { |
||||
|
margin-bottom: 20px; |
||||
|
padding: 15px; |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
|
||||
|
.status-error { |
||||
|
background: #f8d7da; |
||||
|
border: 1px solid #f5c6cb; |
||||
|
color: #721c24; |
||||
|
} |
||||
|
|
||||
|
.status-info { |
||||
|
background: #d1ecf1; |
||||
|
border: 1px solid #bee5eb; |
||||
|
color: #0c5460; |
||||
|
} |
||||
|
|
||||
|
.status-scanning { |
||||
|
background: #d4edda; |
||||
|
border: 1px solid #c3e6cb; |
||||
|
color: #155724; |
||||
|
} |
||||
|
|
||||
|
.status-detail { |
||||
|
margin-top: 5px; |
||||
|
font-size: 0.9em; |
||||
|
opacity: 0.8; |
||||
|
} |
||||
|
|
||||
|
.camera-container { |
||||
|
position: relative; |
||||
|
width: 100%; |
||||
|
max-width: 400px; |
||||
|
height: 300px; |
||||
|
margin: 20px auto; |
||||
|
border: 2px solid #ddd; |
||||
|
border-radius: 8px; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.camera-video { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
object-fit: cover; |
||||
|
} |
||||
|
|
||||
|
.scanning-overlay { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.scan-frame { |
||||
|
width: 200px; |
||||
|
height: 200px; |
||||
|
border: 2px solid #00ff00; |
||||
|
border-radius: 8px; |
||||
|
position: relative; |
||||
|
} |
||||
|
|
||||
|
.scan-line { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
height: 2px; |
||||
|
background: #00ff00; |
||||
|
animation: scan 2s linear infinite; |
||||
|
} |
||||
|
|
||||
|
@keyframes scan { |
||||
|
0% { top: 0; } |
||||
|
100% { top: 100%; } |
||||
|
} |
||||
|
|
||||
|
.scan-results { |
||||
|
margin-top: 20px; |
||||
|
} |
||||
|
|
||||
|
.results-list { |
||||
|
max-height: 300px; |
||||
|
overflow-y: auto; |
||||
|
border: 1px solid #ddd; |
||||
|
border-radius: 4px; |
||||
|
background: #fff; |
||||
|
margin-bottom: 15px; |
||||
|
} |
||||
|
|
||||
|
.result-item { |
||||
|
padding: 12px; |
||||
|
border-bottom: 1px solid #eee; |
||||
|
} |
||||
|
|
||||
|
.result-item:last-child { |
||||
|
border-bottom: none; |
||||
|
} |
||||
|
|
||||
|
.result-header { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
margin-bottom: 8px; |
||||
|
} |
||||
|
|
||||
|
.result-number { |
||||
|
font-weight: bold; |
||||
|
color: #007bff; |
||||
|
} |
||||
|
|
||||
|
.result-time { |
||||
|
font-size: 0.85em; |
||||
|
color: #666; |
||||
|
} |
||||
|
|
||||
|
.result-content { |
||||
|
margin-bottom: 10px; |
||||
|
} |
||||
|
|
||||
|
.qr-data, |
||||
|
.qr-format { |
||||
|
margin-bottom: 5px; |
||||
|
word-break: break-all; |
||||
|
} |
||||
|
|
||||
|
.result-actions { |
||||
|
display: flex; |
||||
|
gap: 8px; |
||||
|
} |
||||
|
|
||||
|
.copy-btn, |
||||
|
.remove-btn { |
||||
|
padding: 4px 8px; |
||||
|
border: 1px solid #ccc; |
||||
|
border-radius: 3px; |
||||
|
background: #fff; |
||||
|
cursor: pointer; |
||||
|
font-size: 0.8em; |
||||
|
} |
||||
|
|
||||
|
.copy-btn:hover { |
||||
|
background: #e9ecef; |
||||
|
} |
||||
|
|
||||
|
.remove-btn:hover { |
||||
|
background: #f8d7da; |
||||
|
color: #721c24; |
||||
|
} |
||||
|
|
||||
|
.results-actions { |
||||
|
display: flex; |
||||
|
gap: 10px; |
||||
|
} |
||||
|
|
||||
|
.clear-btn, |
||||
|
.export-btn { |
||||
|
padding: 8px 16px; |
||||
|
border: 1px solid #ccc; |
||||
|
border-radius: 4px; |
||||
|
background: #fff; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
.clear-btn:hover { |
||||
|
background: #f8d7da; |
||||
|
color: #721c24; |
||||
|
} |
||||
|
|
||||
|
.export-btn:hover { |
||||
|
background: #d4edda; |
||||
|
color: #155724; |
||||
|
} |
||||
|
|
||||
|
.settings-panel { |
||||
|
margin-top: 20px; |
||||
|
padding: 15px; |
||||
|
background: #f8f9fa; |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
|
||||
|
.setting-group { |
||||
|
margin-bottom: 15px; |
||||
|
} |
||||
|
|
||||
|
.setting-group label { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
font-weight: bold; |
||||
|
margin-bottom: 5px; |
||||
|
} |
||||
|
|
||||
|
.setting-group input[type="number"] { |
||||
|
width: 100px; |
||||
|
padding: 4px 8px; |
||||
|
border: 1px solid #ccc; |
||||
|
border-radius: 3px; |
||||
|
} |
||||
|
|
||||
|
.setting-description { |
||||
|
font-size: 0.85em; |
||||
|
color: #666; |
||||
|
margin-top: 3px; |
||||
|
margin-left: 24px; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,657 @@ |
|||||
|
<template> |
||||
|
<div class="threejs-viewer"> |
||||
|
<h2>3D Model Viewer</h2> |
||||
|
|
||||
|
<!-- Viewer controls --> |
||||
|
<div class="viewer-controls"> |
||||
|
<button @click="loadModel" :disabled="isLoading || !modelUrl"> |
||||
|
{{ isLoading ? 'Loading...' : 'Load Model' }} |
||||
|
</button> |
||||
|
<button @click="resetCamera" :disabled="!isModelLoaded"> |
||||
|
Reset Camera |
||||
|
</button> |
||||
|
<button @click="toggleAnimation" :disabled="!isModelLoaded"> |
||||
|
{{ isAnimating ? 'Stop' : 'Start' }} Animation |
||||
|
</button> |
||||
|
<button @click="toggleWireframe" :disabled="!isModelLoaded"> |
||||
|
{{ showWireframe ? 'Hide' : 'Show' }} Wireframe |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Loading status --> |
||||
|
<div v-if="isLoading" class="loading-status"> |
||||
|
<div class="loading-spinner"></div> |
||||
|
<p>Loading 3D model...</p> |
||||
|
<p class="loading-detail">{{ loadingProgress }}% complete</p> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Error status --> |
||||
|
<div v-if="loadError" class="error-status"> |
||||
|
<p>Failed to load model: {{ loadError }}</p> |
||||
|
<button @click="retryLoad" class="retry-btn">Retry</button> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 3D Canvas --> |
||||
|
<div |
||||
|
ref="canvasContainer" |
||||
|
class="canvas-container" |
||||
|
:class="{ 'model-loaded': isModelLoaded }" |
||||
|
> |
||||
|
<canvas |
||||
|
ref="threeCanvas" |
||||
|
class="three-canvas" |
||||
|
></canvas> |
||||
|
|
||||
|
<!-- Overlay controls --> |
||||
|
<div v-if="isModelLoaded" class="overlay-controls"> |
||||
|
<div class="control-group"> |
||||
|
<label>Camera Distance:</label> |
||||
|
<input |
||||
|
type="range" |
||||
|
v-model.number="cameraDistance" |
||||
|
min="1" |
||||
|
max="20" |
||||
|
step="0.1" |
||||
|
@input="updateCameraDistance" |
||||
|
/> |
||||
|
<span>{{ cameraDistance.toFixed(1) }}</span> |
||||
|
</div> |
||||
|
|
||||
|
<div class="control-group"> |
||||
|
<label>Rotation Speed:</label> |
||||
|
<input |
||||
|
type="range" |
||||
|
v-model.number="rotationSpeed" |
||||
|
min="0" |
||||
|
max="2" |
||||
|
step="0.1" |
||||
|
/> |
||||
|
<span>{{ rotationSpeed.toFixed(1) }}</span> |
||||
|
</div> |
||||
|
|
||||
|
<div class="control-group"> |
||||
|
<label>Light Intensity:</label> |
||||
|
<input |
||||
|
type="range" |
||||
|
v-model.number="lightIntensity" |
||||
|
min="0" |
||||
|
max="2" |
||||
|
step="0.1" |
||||
|
@input="updateLightIntensity" |
||||
|
/> |
||||
|
<span>{{ lightIntensity.toFixed(1) }}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Model info --> |
||||
|
<div v-if="modelInfo" class="model-info"> |
||||
|
<h4>Model Information</h4> |
||||
|
<div class="info-grid"> |
||||
|
<div class="info-item"> |
||||
|
<span class="info-label">Vertices:</span> |
||||
|
<span class="info-value">{{ modelInfo.vertexCount.toLocaleString() }}</span> |
||||
|
</div> |
||||
|
<div class="info-item"> |
||||
|
<span class="info-label">Faces:</span> |
||||
|
<span class="info-value">{{ modelInfo.faceCount.toLocaleString() }}</span> |
||||
|
</div> |
||||
|
<div class="info-item"> |
||||
|
<span class="info-label">Materials:</span> |
||||
|
<span class="info-value">{{ modelInfo.materialCount }}</span> |
||||
|
</div> |
||||
|
<div class="info-item"> |
||||
|
<span class="info-label">File Size:</span> |
||||
|
<span class="info-value">{{ formatFileSize(modelInfo.fileSize) }}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Performance metrics --> |
||||
|
<div v-if="performanceMetrics" class="performance-metrics"> |
||||
|
<h4>Performance Metrics</h4> |
||||
|
<div class="metrics-grid"> |
||||
|
<div class="metric"> |
||||
|
<span class="metric-label">FPS:</span> |
||||
|
<span class="metric-value">{{ performanceMetrics.fps }}</span> |
||||
|
</div> |
||||
|
<div class="metric"> |
||||
|
<span class="metric-label">Render Time:</span> |
||||
|
<span class="metric-value">{{ performanceMetrics.renderTime }}ms</span> |
||||
|
</div> |
||||
|
<div class="metric"> |
||||
|
<span class="metric-label">Memory Usage:</span> |
||||
|
<span class="metric-value">{{ performanceMetrics.memoryUsage }}MB</span> |
||||
|
</div> |
||||
|
<div class="metric"> |
||||
|
<span class="metric-label">Draw Calls:</span> |
||||
|
<span class="metric-value">{{ performanceMetrics.drawCalls }}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue, Prop, Emit } from 'vue-facing-decorator'; |
||||
|
|
||||
|
interface ModelInfo { |
||||
|
vertexCount: number; |
||||
|
faceCount: number; |
||||
|
materialCount: number; |
||||
|
fileSize: number; |
||||
|
boundingBox: { |
||||
|
min: { x: number; y: number; z: number }; |
||||
|
max: { x: number; y: number; z: number }; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
interface PerformanceMetrics { |
||||
|
fps: number; |
||||
|
renderTime: number; |
||||
|
memoryUsage: number; |
||||
|
drawCalls: number; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* ThreeJS 3D Model Viewer Component |
||||
|
* |
||||
|
* Demonstrates lazy loading for heavy 3D rendering libraries. |
||||
|
* This component would benefit from lazy loading as ThreeJS is a large |
||||
|
* library that's only needed for 3D visualization features. |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
@Component({ |
||||
|
name: 'ThreeJSViewer' |
||||
|
}) |
||||
|
export default class ThreeJSViewer extends Vue { |
||||
|
@Prop({ required: true }) readonly modelUrl!: string; |
||||
|
|
||||
|
// Component state |
||||
|
isLoading = false; |
||||
|
isModelLoaded = false; |
||||
|
loadError: string | null = null; |
||||
|
loadingProgress = 0; |
||||
|
|
||||
|
// Animation state |
||||
|
isAnimating = false; |
||||
|
showWireframe = false; |
||||
|
|
||||
|
// Camera and lighting controls |
||||
|
cameraDistance = 5; |
||||
|
rotationSpeed = 0.5; |
||||
|
lightIntensity = 1; |
||||
|
|
||||
|
// Canvas references |
||||
|
canvasContainer: HTMLElement | null = null; |
||||
|
threeCanvas: HTMLCanvasElement | null = null; |
||||
|
|
||||
|
// Model and performance data |
||||
|
modelInfo: ModelInfo | null = null; |
||||
|
performanceMetrics: PerformanceMetrics | null = null; |
||||
|
|
||||
|
// ThreeJS objects (will be lazy loaded) |
||||
|
private three: any = null; |
||||
|
private scene: any = null; |
||||
|
private camera: any = null; |
||||
|
private renderer: any = null; |
||||
|
private model: any = null; |
||||
|
private controls: any = null; |
||||
|
private animationId: number | null = null; |
||||
|
private frameCount = 0; |
||||
|
private lastTime = 0; |
||||
|
|
||||
|
// Lifecycle hooks |
||||
|
mounted(): void { |
||||
|
console.log('[ThreeJSViewer] Component mounted'); |
||||
|
this.initializeCanvas(); |
||||
|
} |
||||
|
|
||||
|
beforeUnmount(): void { |
||||
|
this.cleanup(); |
||||
|
console.log('[ThreeJSViewer] Component unmounting'); |
||||
|
} |
||||
|
|
||||
|
// Methods |
||||
|
private initializeCanvas(): void { |
||||
|
this.canvasContainer = this.$refs.canvasContainer as HTMLElement; |
||||
|
this.threeCanvas = this.$refs.threeCanvas as HTMLCanvasElement; |
||||
|
|
||||
|
if (this.threeCanvas) { |
||||
|
this.threeCanvas.width = this.canvasContainer.clientWidth; |
||||
|
this.threeCanvas.height = this.canvasContainer.clientHeight; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async loadModel(): Promise<void> { |
||||
|
if (this.isLoading || !this.modelUrl) return; |
||||
|
|
||||
|
this.isLoading = true; |
||||
|
this.loadError = null; |
||||
|
this.loadingProgress = 0; |
||||
|
|
||||
|
try { |
||||
|
console.log('[ThreeJSViewer] Loading 3D model:', this.modelUrl); |
||||
|
|
||||
|
// Lazy load ThreeJS |
||||
|
await this.loadThreeJS(); |
||||
|
|
||||
|
// Initialize scene |
||||
|
await this.initializeScene(); |
||||
|
|
||||
|
// Load model |
||||
|
await this.loadModelFile(); |
||||
|
|
||||
|
// Start rendering |
||||
|
this.startRendering(); |
||||
|
|
||||
|
this.isModelLoaded = true; |
||||
|
this.isLoading = false; |
||||
|
|
||||
|
// Emit model loaded event |
||||
|
this.$emit('model-loaded', this.modelInfo); |
||||
|
|
||||
|
console.log('[ThreeJSViewer] Model loaded successfully'); |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('[ThreeJSViewer] Failed to load model:', error); |
||||
|
this.loadError = error instanceof Error ? error.message : 'Unknown error'; |
||||
|
this.isLoading = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async loadThreeJS(): Promise<void> { |
||||
|
// Simulate loading ThreeJS library |
||||
|
this.loadingProgress = 20; |
||||
|
await this.simulateLoading(500); |
||||
|
|
||||
|
// In a real implementation, you would import ThreeJS here |
||||
|
// this.three = await import('three'); |
||||
|
|
||||
|
this.loadingProgress = 40; |
||||
|
await this.simulateLoading(300); |
||||
|
} |
||||
|
|
||||
|
private async initializeScene(): Promise<void> { |
||||
|
this.loadingProgress = 60; |
||||
|
|
||||
|
// Simulate scene initialization |
||||
|
await this.simulateLoading(400); |
||||
|
|
||||
|
// In a real implementation, you would set up ThreeJS scene here |
||||
|
// this.scene = new this.three.Scene(); |
||||
|
// this.camera = new this.three.PerspectiveCamera(75, width / height, 0.1, 1000); |
||||
|
// this.renderer = new this.three.WebGLRenderer({ canvas: this.threeCanvas }); |
||||
|
|
||||
|
this.loadingProgress = 80; |
||||
|
} |
||||
|
|
||||
|
private async loadModelFile(): Promise<void> { |
||||
|
this.loadingProgress = 90; |
||||
|
|
||||
|
// Simulate model loading |
||||
|
await this.simulateLoading(600); |
||||
|
|
||||
|
// Simulate model info |
||||
|
this.modelInfo = { |
||||
|
vertexCount: Math.floor(Math.random() * 50000) + 1000, |
||||
|
faceCount: Math.floor(Math.random() * 25000) + 500, |
||||
|
materialCount: Math.floor(Math.random() * 5) + 1, |
||||
|
fileSize: Math.floor(Math.random() * 5000000) + 100000, |
||||
|
boundingBox: { |
||||
|
min: { x: -1, y: -1, z: -1 }, |
||||
|
max: { x: 1, y: 1, z: 1 } |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
this.loadingProgress = 100; |
||||
|
} |
||||
|
|
||||
|
private async simulateLoading(delay: number): Promise<void> { |
||||
|
return new Promise(resolve => setTimeout(resolve, delay)); |
||||
|
} |
||||
|
|
||||
|
private startRendering(): void { |
||||
|
if (!this.isModelLoaded) return; |
||||
|
|
||||
|
this.isAnimating = true; |
||||
|
this.animate(); |
||||
|
|
||||
|
// Start performance monitoring |
||||
|
this.startPerformanceMonitoring(); |
||||
|
} |
||||
|
|
||||
|
private animate(): void { |
||||
|
if (!this.isAnimating) return; |
||||
|
|
||||
|
this.animationId = requestAnimationFrame(() => this.animate()); |
||||
|
|
||||
|
// Simulate model rotation |
||||
|
if (this.model && this.rotationSpeed > 0) { |
||||
|
// this.model.rotation.y += this.rotationSpeed * 0.01; |
||||
|
} |
||||
|
|
||||
|
// Simulate rendering |
||||
|
// this.renderer.render(this.scene, this.camera); |
||||
|
|
||||
|
this.frameCount++; |
||||
|
} |
||||
|
|
||||
|
private startPerformanceMonitoring(): void { |
||||
|
const updateMetrics = () => { |
||||
|
if (!this.isAnimating) return; |
||||
|
|
||||
|
const now = performance.now(); |
||||
|
const deltaTime = now - this.lastTime; |
||||
|
|
||||
|
if (deltaTime > 0) { |
||||
|
const fps = Math.round(1000 / deltaTime); |
||||
|
|
||||
|
this.performanceMetrics = { |
||||
|
fps: Math.min(fps, 60), // Cap at 60 FPS for display |
||||
|
renderTime: Math.round(deltaTime), |
||||
|
memoryUsage: Math.round((Math.random() * 50 + 10) * 100) / 100, |
||||
|
drawCalls: Math.floor(Math.random() * 100) + 10 |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
this.lastTime = now; |
||||
|
requestAnimationFrame(updateMetrics); |
||||
|
}; |
||||
|
|
||||
|
updateMetrics(); |
||||
|
} |
||||
|
|
||||
|
resetCamera(): void { |
||||
|
if (!this.isModelLoaded) return; |
||||
|
|
||||
|
this.cameraDistance = 5; |
||||
|
this.updateCameraDistance(); |
||||
|
console.log('[ThreeJSViewer] Camera reset'); |
||||
|
} |
||||
|
|
||||
|
toggleAnimation(): void { |
||||
|
this.isAnimating = !this.isAnimating; |
||||
|
|
||||
|
if (this.isAnimating) { |
||||
|
this.animate(); |
||||
|
} else if (this.animationId) { |
||||
|
cancelAnimationFrame(this.animationId); |
||||
|
this.animationId = null; |
||||
|
} |
||||
|
|
||||
|
console.log('[ThreeJSViewer] Animation toggled:', this.isAnimating); |
||||
|
} |
||||
|
|
||||
|
toggleWireframe(): void { |
||||
|
this.showWireframe = !this.showWireframe; |
||||
|
|
||||
|
// In a real implementation, you would toggle wireframe mode |
||||
|
// this.model.traverse((child: any) => { |
||||
|
// if (child.isMesh) { |
||||
|
// child.material.wireframe = this.showWireframe; |
||||
|
// } |
||||
|
// }); |
||||
|
|
||||
|
console.log('[ThreeJSViewer] Wireframe toggled:', this.showWireframe); |
||||
|
} |
||||
|
|
||||
|
updateCameraDistance(): void { |
||||
|
if (!this.isModelLoaded) return; |
||||
|
|
||||
|
// In a real implementation, you would update camera position |
||||
|
// this.camera.position.z = this.cameraDistance; |
||||
|
// this.camera.lookAt(0, 0, 0); |
||||
|
} |
||||
|
|
||||
|
updateLightIntensity(): void { |
||||
|
if (!this.isModelLoaded) return; |
||||
|
|
||||
|
// In a real implementation, you would update light intensity |
||||
|
// this.light.intensity = this.lightIntensity; |
||||
|
} |
||||
|
|
||||
|
retryLoad(): void { |
||||
|
this.loadError = null; |
||||
|
this.loadModel(); |
||||
|
} |
||||
|
|
||||
|
private cleanup(): void { |
||||
|
if (this.animationId) { |
||||
|
cancelAnimationFrame(this.animationId); |
||||
|
this.animationId = null; |
||||
|
} |
||||
|
|
||||
|
if (this.renderer) { |
||||
|
this.renderer.dispose(); |
||||
|
} |
||||
|
|
||||
|
this.isAnimating = false; |
||||
|
this.isModelLoaded = false; |
||||
|
} |
||||
|
|
||||
|
formatFileSize(bytes: number): string { |
||||
|
const sizes = ['B', 'KB', 'MB', 'GB']; |
||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024)); |
||||
|
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`; |
||||
|
} |
||||
|
|
||||
|
// Event emitters |
||||
|
@Emit('model-loaded') |
||||
|
emitModelLoaded(info: ModelInfo): ModelInfo { |
||||
|
return info; |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.threejs-viewer { |
||||
|
padding: 20px; |
||||
|
border: 1px solid #ddd; |
||||
|
border-radius: 8px; |
||||
|
background: #f9f9f9; |
||||
|
} |
||||
|
|
||||
|
.viewer-controls { |
||||
|
display: flex; |
||||
|
gap: 10px; |
||||
|
margin-bottom: 20px; |
||||
|
flex-wrap: wrap; |
||||
|
} |
||||
|
|
||||
|
.viewer-controls button { |
||||
|
padding: 8px 16px; |
||||
|
border: 1px solid #ccc; |
||||
|
border-radius: 4px; |
||||
|
background: #fff; |
||||
|
cursor: pointer; |
||||
|
transition: background-color 0.2s; |
||||
|
} |
||||
|
|
||||
|
.viewer-controls button:hover:not(:disabled) { |
||||
|
background: #e9ecef; |
||||
|
} |
||||
|
|
||||
|
.viewer-controls button:disabled { |
||||
|
opacity: 0.6; |
||||
|
cursor: not-allowed; |
||||
|
} |
||||
|
|
||||
|
.loading-status { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
padding: 40px; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.loading-spinner { |
||||
|
width: 40px; |
||||
|
height: 40px; |
||||
|
border: 4px solid #f3f3f3; |
||||
|
border-top: 4px solid #3498db; |
||||
|
border-radius: 50%; |
||||
|
animation: spin 1s linear infinite; |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
@keyframes spin { |
||||
|
0% { transform: rotate(0deg); } |
||||
|
100% { transform: rotate(360deg); } |
||||
|
} |
||||
|
|
||||
|
.loading-detail { |
||||
|
font-size: 0.9em; |
||||
|
color: #666; |
||||
|
margin-top: 10px; |
||||
|
} |
||||
|
|
||||
|
.error-status { |
||||
|
padding: 20px; |
||||
|
background: #f8d7da; |
||||
|
border: 1px solid #f5c6cb; |
||||
|
border-radius: 4px; |
||||
|
color: #721c24; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.retry-btn { |
||||
|
margin-top: 10px; |
||||
|
padding: 8px 16px; |
||||
|
border: 1px solid #721c24; |
||||
|
border-radius: 4px; |
||||
|
background: #721c24; |
||||
|
color: #fff; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
.retry-btn:hover { |
||||
|
background: #5a1a1a; |
||||
|
} |
||||
|
|
||||
|
.canvas-container { |
||||
|
position: relative; |
||||
|
width: 100%; |
||||
|
height: 400px; |
||||
|
border: 2px solid #ddd; |
||||
|
border-radius: 8px; |
||||
|
overflow: hidden; |
||||
|
background: #000; |
||||
|
margin: 20px 0; |
||||
|
} |
||||
|
|
||||
|
.canvas-container.model-loaded { |
||||
|
border-color: #28a745; |
||||
|
} |
||||
|
|
||||
|
.three-canvas { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
display: block; |
||||
|
} |
||||
|
|
||||
|
.overlay-controls { |
||||
|
position: absolute; |
||||
|
top: 10px; |
||||
|
right: 10px; |
||||
|
background: rgba(0, 0, 0, 0.8); |
||||
|
color: #fff; |
||||
|
padding: 15px; |
||||
|
border-radius: 4px; |
||||
|
min-width: 200px; |
||||
|
} |
||||
|
|
||||
|
.control-group { |
||||
|
margin-bottom: 10px; |
||||
|
} |
||||
|
|
||||
|
.control-group:last-child { |
||||
|
margin-bottom: 0; |
||||
|
} |
||||
|
|
||||
|
.control-group label { |
||||
|
display: block; |
||||
|
font-size: 0.9em; |
||||
|
margin-bottom: 5px; |
||||
|
} |
||||
|
|
||||
|
.control-group input[type="range"] { |
||||
|
width: 100%; |
||||
|
margin-right: 10px; |
||||
|
} |
||||
|
|
||||
|
.control-group span { |
||||
|
font-size: 0.8em; |
||||
|
color: #ccc; |
||||
|
} |
||||
|
|
||||
|
.model-info { |
||||
|
position: absolute; |
||||
|
bottom: 10px; |
||||
|
left: 10px; |
||||
|
background: rgba(0, 0, 0, 0.8); |
||||
|
color: #fff; |
||||
|
padding: 15px; |
||||
|
border-radius: 4px; |
||||
|
min-width: 200px; |
||||
|
} |
||||
|
|
||||
|
.model-info h4 { |
||||
|
margin: 0 0 10px 0; |
||||
|
font-size: 1em; |
||||
|
} |
||||
|
|
||||
|
.info-grid { |
||||
|
display: grid; |
||||
|
gap: 5px; |
||||
|
} |
||||
|
|
||||
|
.info-item { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
font-size: 0.85em; |
||||
|
} |
||||
|
|
||||
|
.info-label { |
||||
|
color: #ccc; |
||||
|
} |
||||
|
|
||||
|
.info-value { |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
.performance-metrics { |
||||
|
margin-top: 20px; |
||||
|
padding: 15px; |
||||
|
background: #e8f4fd; |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
|
||||
|
.metrics-grid { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); |
||||
|
gap: 15px; |
||||
|
margin-top: 10px; |
||||
|
} |
||||
|
|
||||
|
.metric { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
padding: 8px; |
||||
|
background: #fff; |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
|
||||
|
.metric-label { |
||||
|
font-weight: bold; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.metric-value { |
||||
|
color: #007bff; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,151 @@ |
|||||
|
import { defineConfig, UserConfig } from "vite"; |
||||
|
import vue from "@vitejs/plugin-vue"; |
||||
|
import dotenv from "dotenv"; |
||||
|
import { loadAppConfig } from "./vite.config.utils.mts"; |
||||
|
import path from "path"; |
||||
|
import { fileURLToPath } from 'url'; |
||||
|
|
||||
|
// Load environment variables |
||||
|
dotenv.config(); |
||||
|
|
||||
|
const __filename = fileURLToPath(import.meta.url); |
||||
|
const __dirname = path.dirname(__filename); |
||||
|
|
||||
|
/** |
||||
|
* Optimized Vite configuration for Vue 3 + vue-facing-decorator |
||||
|
* with enhanced code splitting and lazy loading capabilities |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
export async function createOptimizedBuildConfig(mode: string): Promise<UserConfig> { |
||||
|
const appConfig = await loadAppConfig(); |
||||
|
const isCapacitor = mode === "capacitor"; |
||||
|
const isElectron = mode === "electron"; |
||||
|
const isNative = isCapacitor || isElectron; |
||||
|
|
||||
|
// Set platform and disable PWA for native platforms |
||||
|
process.env.VITE_PLATFORM = mode; |
||||
|
process.env.VITE_PWA_ENABLED = isNative ? 'false' : 'true'; |
||||
|
process.env.VITE_DISABLE_PWA = isNative ? 'true' : 'false'; |
||||
|
|
||||
|
if (isNative) { |
||||
|
process.env.VITE_PWA_ENABLED = 'false'; |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
base: "/", |
||||
|
plugins: [vue()], |
||||
|
server: { |
||||
|
port: parseInt(process.env.VITE_PORT || "8080"), |
||||
|
fs: { strict: false }, |
||||
|
// CORS headers disabled to allow images from any domain |
||||
|
// This means SharedArrayBuffer is unavailable, but absurd-sql |
||||
|
// will automatically fall back to IndexedDB mode which still works |
||||
|
}, |
||||
|
build: { |
||||
|
outDir: "dist", |
||||
|
assetsDir: 'assets', |
||||
|
chunkSizeWarningLimit: 1000, |
||||
|
rollupOptions: { |
||||
|
external: isNative |
||||
|
? ['@capacitor/app'] |
||||
|
: [], |
||||
|
output: { |
||||
|
format: 'esm', |
||||
|
generatedCode: { |
||||
|
preset: 'es2015' |
||||
|
}, |
||||
|
// Enhanced manual chunks for better code splitting |
||||
|
manualChunks: { |
||||
|
// Vendor chunks for better caching |
||||
|
'vue-vendor': ['vue', 'vue-router', 'pinia'], |
||||
|
'ui-vendor': ['@fortawesome/fontawesome-svg-core', '@fortawesome/vue-fontawesome'], |
||||
|
'crypto-vendor': ['@ethersproject/wallet', '@ethersproject/hdnode', 'ethereum-cryptography'], |
||||
|
'sql-vendor': ['@jlongster/sql.js', 'absurd-sql', 'dexie'], |
||||
|
'qr-vendor': ['qrcode', 'jsqr', 'vue-qrcode-reader'], |
||||
|
'three-vendor': ['three', '@tweenjs/tween.js'], |
||||
|
'utils-vendor': ['luxon', 'ramda', 'zod', 'axios'], |
||||
|
// Platform-specific chunks |
||||
|
...(isCapacitor && { |
||||
|
'capacitor-vendor': ['@capacitor/core', '@capacitor/app', '@capacitor/camera'] |
||||
|
}), |
||||
|
...(isElectron && { |
||||
|
'electron-vendor': ['electron'] |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
// Optimize chunk loading |
||||
|
target: 'es2015', |
||||
|
minify: 'terser', |
||||
|
terserOptions: { |
||||
|
compress: { |
||||
|
drop_console: process.env.NODE_ENV === 'production', |
||||
|
drop_debugger: true |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
worker: { |
||||
|
format: 'es', |
||||
|
plugins: () => [] |
||||
|
}, |
||||
|
define: { |
||||
|
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), |
||||
|
'process.env.VITE_PLATFORM': JSON.stringify(mode), |
||||
|
'process.env.VITE_PWA_ENABLED': JSON.stringify(!isNative), |
||||
|
'process.env.VITE_DISABLE_PWA': JSON.stringify(isNative), |
||||
|
__dirname: JSON.stringify(process.cwd()), |
||||
|
__IS_MOBILE__: JSON.stringify(isCapacitor), |
||||
|
__IS_ELECTRON__: JSON.stringify(isElectron), |
||||
|
__USE_QR_READER__: JSON.stringify(!isCapacitor), |
||||
|
'process.platform': JSON.stringify('browser'), |
||||
|
'process.version': JSON.stringify('v16.0.0'), |
||||
|
'process.env.NODE_DEBUG': JSON.stringify(false), |
||||
|
'global.process': JSON.stringify({ |
||||
|
platform: 'browser', |
||||
|
version: 'v16.0.0', |
||||
|
env: { NODE_DEBUG: false } |
||||
|
}) |
||||
|
}, |
||||
|
resolve: { |
||||
|
alias: { |
||||
|
'@': path.resolve(__dirname, 'src'), |
||||
|
'@nostr/tools': path.resolve(__dirname, 'node_modules/@nostr/tools'), |
||||
|
'@nostr/tools/nip06': path.resolve(__dirname, 'node_modules/@nostr/tools/nip06'), |
||||
|
...appConfig.aliasConfig, |
||||
|
'path': path.resolve(__dirname, './src/utils/node-modules/path.js'), |
||||
|
'fs': path.resolve(__dirname, './src/utils/node-modules/fs.js'), |
||||
|
'crypto': path.resolve(__dirname, './src/utils/node-modules/crypto.js'), |
||||
|
'dexie-export-import': path.resolve(__dirname, 'node_modules/dexie-export-import') |
||||
|
} |
||||
|
}, |
||||
|
optimizeDeps: { |
||||
|
include: [ |
||||
|
'@nostr/tools', |
||||
|
'@nostr/tools/nip06', |
||||
|
'vue', |
||||
|
'vue-router', |
||||
|
'pinia', |
||||
|
'vue-facing-decorator' |
||||
|
], |
||||
|
exclude: isNative ? [ |
||||
|
'register-service-worker', |
||||
|
'workbox-window', |
||||
|
'web-push', |
||||
|
'serviceworker-webpack-plugin', |
||||
|
'vite-plugin-pwa', |
||||
|
'@vite-pwa/vue' |
||||
|
] : [] |
||||
|
}, |
||||
|
// Enhanced performance optimizations |
||||
|
esbuild: { |
||||
|
target: 'es2015', |
||||
|
supported: { |
||||
|
'bigint': true |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export default defineConfig(async () => createOptimizedBuildConfig('web')); |
Loading…
Reference in new issue