refactor: remove unused Vite configuration files and update documentation

Remove obsolete Vite configuration files that are no longer used by the build system
and update BUILDING.md to accurately reflect the current configuration structure.

**Removed Files:**
- vite.config.ts (47 lines) - Legacy configuration file
- vite.config.mts (70 lines) - Unused "main" configuration file

**Updated Documentation:**
- BUILDING.md - Corrected Vite configuration section to show actual usage

**Current Configuration Structure:**
- vite.config.web.mts - Used by build-web.sh
- vite.config.electron.mts - Used by build-electron.sh
- vite.config.capacitor.mts - Used by npm run build:capacitor
- vite.config.common.mts - Shared configuration utilities
- vite.config.utils.mts - Configuration utility functions

**Benefits:**
- Eliminates confusion about which config files to use
- Removes 117 lines of unused configuration code
- Documentation now matches actual build system usage
- Cleaner, more maintainable configuration structure

**Impact:**
- No functional changes to build process
- All platform builds continue to work correctly
- Reduced configuration complexity and maintenance overhead
This commit is contained in:
Matthew Raymer
2025-07-17 08:07:22 +00:00
parent bff36d82e4
commit e1b5367880
8 changed files with 5 additions and 2724 deletions

View File

@@ -1,554 +0,0 @@
<template>
<div class="heavy-component">
<h2>Heavy Data Processing Component</h2>
<!-- Data processing controls -->
<div class="controls">
<button :disabled="isProcessing" @click="processData">
{{ isProcessing ? "Processing..." : "Process Data" }}
</button>
<button :disabled="isProcessing" @click="clearResults">
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
:disabled="currentPage === 1"
class="page-btn"
@click="previousPage"
>
Previous
</button>
<span class="page-info">
Page {{ currentPage }} of {{ totalPages }}
</span>
<button
:disabled="currentPage === totalPages"
class="page-btn"
@click="nextPage"
>
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>

View File

@@ -1,721 +0,0 @@
<template>
<div class="qr-scanner-component">
<h2>QR Code Scanner</h2>
<!-- Camera controls -->
<div class="camera-controls">
<button :disabled="isScanning || !hasCamera" @click="startScanning">
{{ isScanning ? "Scanning..." : "Start Scanning" }}
</button>
<button :disabled="!isScanning" @click="stopScanning">
Stop Scanning
</button>
<button
:disabled="!isScanning || cameras.length <= 1"
@click="switchCamera"
>
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 class="copy-btn" @click="copyToClipboard(result.data)">
Copy
</button>
<button class="remove-btn" @click="removeResult(index)">
Remove
</button>
</div>
</div>
</div>
<div class="results-actions">
<button class="clear-btn" @click="clearResults">
Clear All Results
</button>
<button class="export-btn" @click="exportResults">
Export Results
</button>
</div>
</div>
<!-- Settings panel -->
<div class="settings-panel">
<h3>Scanner Settings</h3>
<div class="setting-group">
<label>
<input v-model="settings.continuousScanning" type="checkbox" />
Continuous Scanning
</label>
<p class="setting-description">
Automatically scan multiple QR codes without stopping
</p>
</div>
<div class="setting-group">
<label>
<input v-model="settings.audioFeedback" type="checkbox" />
Audio Feedback
</label>
<p class="setting-description">Play sound when QR code is detected</p>
</div>
<div class="setting-group">
<label>
<input v-model="settings.vibrateOnScan" type="checkbox" />
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
v-model.number="settings.scanInterval"
type="number"
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>

View File

@@ -1,667 +0,0 @@
<template>
<div class="threejs-viewer">
<h2>3D Model Viewer</h2>
<!-- Viewer controls -->
<div class="viewer-controls">
<button :disabled="isLoading || !modelUrl" @click="loadModel">
{{ isLoading ? "Loading..." : "Load Model" }}
</button>
<button :disabled="!isModelLoaded" @click="resetCamera">
Reset Camera
</button>
<button :disabled="!isModelLoaded" @click="toggleAnimation">
{{ isAnimating ? "Stop" : "Start" }} Animation
</button>
<button :disabled="!isModelLoaded" @click="toggleWireframe">
{{ 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 class="retry-btn" @click="retryLoad">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
v-model.number="cameraDistance"
type="range"
min="1"
max="20"
step="0.1"
@input="updateCameraDistance"
/>
<span>{{ cameraDistance.toFixed(1) }}</span>
</div>
<div class="control-group">
<label>Rotation Speed:</label>
<input
v-model.number="rotationSpeed"
type="range"
min="0"
max="2"
step="0.1"
/>
<span>{{ rotationSpeed.toFixed(1) }}</span>
</div>
<div class="control-group">
<label>Light Intensity:</label>
<input
v-model.number="lightIntensity"
type="range"
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>