Browse Source

Merge branch 'master' of http://git.fmode.cn:3000/0235656/cloth-design

0235645 1 day ago
parent
commit
ecaf461c24

File diff suppressed because it is too large
+ 2035 - 72
cloth-design/package-lock.json


+ 1 - 0
cloth-design/package.json

@@ -40,6 +40,7 @@
     "@angular/build": "^20.0.4",
     "@angular/cli": "^20.0.4",
     "@angular/compiler-cli": "^20.0.0",
+    "@compodoc/compodoc": "^1.1.26",
     "@types/jasmine": "~5.1.0",
     "jasmine-core": "~5.7.0",
     "karma": "~6.4.0",

BIN
cloth-design/public/cloth/body.png


BIN
cloth-design/public/cloth/cloth.png


BIN
cloth-design/public/cloth/head.png


BIN
cloth-design/public/cloth/left.png


BIN
cloth-design/public/cloth/right.png


+ 743 - 0
cloth-design/public/diy.html

@@ -0,0 +1,743 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>羽绒布颜色更换系统</title>
+    <style>
+        body {
+            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+            max-width: 1000px;
+            margin: 0 auto;
+            padding: 20px;
+            background-color: #f8f9fa;
+            color: #333;
+        }
+
+        h1 {
+            text-align: center;
+            margin-bottom: 30px;
+            color: #2c3e50;
+        }
+
+      .part-selector {
+            display: flex;
+            justify-content: center;
+            gap: 10px;
+            margin-bottom: 20px;
+            flex-wrap: wrap;
+        }
+
+      .part-btn {
+            padding: 6px 15px;
+            background: white;
+            border: 1px solid #ddd;
+            border-radius: 20px;
+            cursor: pointer;
+            transition: all 0.2s;
+            font-size: 14px;
+        }
+
+      .part-btn.active {
+            background: #3498db;
+            color: white;
+            border-color: #3498db;
+        }
+
+      .color-options {
+            display: flex;
+            flex-wrap: wrap;
+            gap: 15px;
+            justify-content: center;
+            margin-bottom: 30px;
+            padding: 15px;
+            background-color: white;
+            border-radius: 10px;
+            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
+        }
+
+      .color-option {
+            width: 40px;
+            height: 40px;
+            border-radius: 50%;
+            cursor: pointer;
+            border: 2px solid #eee;
+            transition: all 0.3s ease;
+            position: relative;
+            box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+        }
+
+      .color-option:hover {
+            transform: scale(1.15);
+            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
+            z-index: 10;
+        }
+
+      .color-option.active {
+            border-color: #3498db;
+            box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.3);
+        }
+
+      .color-name {
+            position: absolute;
+            bottom: -25px;
+            left: 50%;
+            transform: translateX(-50%);
+            white-space: nowrap;
+            font-size: 12px;
+            color: #7f8c8d;
+        }
+
+      .canvas-container {
+            display: flex;
+            justify-content: center;
+            position: relative;
+            margin-bottom: 30px;
+        }
+
+      .canvas-card {
+            position: absolute;
+            background: transparent;
+            border-radius: 10px;
+            overflow: hidden;
+            box-shadow: none;
+            width: 400px;
+            height: 400px;
+        }
+
+      .canvas-header {
+            padding: 12px 20px;
+            background: #3498db;
+            color: white;
+            font-weight: bold;
+            text-align: center;
+        }
+
+        canvas {
+            display: block;
+            max-width: 100%;
+        }
+
+      .controls {
+            display: flex;
+            flex-wrap: wrap;
+            gap: 20px;
+            background: white;
+            padding: 20px;
+            border-radius: 10px;
+            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
+            margin-top: 20px;
+        }
+
+      .control-group {
+            flex: 1;
+            min-width: 250px;
+        }
+
+      .control-group h3 {
+            margin-top: 0;
+            color: #2c3e50;
+            font-size: 16px;
+            border-bottom: 1px solid #eee;
+            padding-bottom: 8px;
+        }
+
+      .slider-container {
+            margin: 15px 0;
+        }
+
+        label {
+            display: block;
+            margin-bottom: 6px;
+            font-weight: 500;
+            color: #34495e;
+        }
+
+        input[type="range"] {
+            width: 100%;
+        }
+
+      .value-display {
+            display: inline-block;
+            width: 40px;
+            text-align: right;
+            font-family: monospace;
+        }
+
+      .status-bar {
+            text-align: center;
+            padding: 15px;
+            font-size: 14px;
+            color: #7f8c8d;
+        }
+
+      .algorithm-info {
+            background: #e3f2fd;
+            padding: 15px;
+            border-radius: 8px;
+            margin-top: 20px;
+            font-size: 14px;
+        }
+
+      .algorithm-info h3 {
+            margin-top: 0;
+            color: #0d47a1;
+        }
+
+      .part-indicator {
+            text-align: center;
+            margin: 5px 0 15px;
+            font-weight: bold;
+            color: #3498db;
+        }
+    </style>
+</head>
+
+<body>
+    <h1>羽绒布颜色更换系统</h1>
+
+    <div class="part-selector" id="partSelector">
+        <button class="part-btn" data-part="all">整体</button>
+        <button class="part-btn" data-part="left">左袖</button>
+        <button class="part-btn" data-part="right">右袖</button>
+        <button class="part-btn active" data-part="body">身体</button>
+        <button class="part-btn" data-part="head">帽子</button>
+    </div>
+
+    <div class="part-indicator" id="partIndicator">当前编辑: 整体</div>
+
+    <div class="color-options" id="colorOptions"></div>
+
+    <div class="canvas-container" id="canvasContainer">
+        <!-- 整体部位的canvas -->
+        <div class="canvas-card" id="allCanvasCard">
+            <div class="canvas-header">整体</div>
+            <canvas id="allCanvas" width="750" height="650"></canvas>
+        </div>
+        <!-- 身体部位的canvas -->
+        <div class="canvas-card" id="bodyCanvasCard">
+            <div class="canvas-header">身体</div>
+            <canvas id="bodyCanvas" width="750" height="650"></canvas>
+        </div>
+        <!-- 左袖部位的canvas -->
+        <div class="canvas-card" id="leftCanvasCard">
+            <div class="canvas-header">左袖</div>
+            <canvas id="leftCanvas" width="750" height="650"></canvas>
+        </div>
+        <!-- 右袖部位的canvas -->
+        <div class="canvas-card" id="rightCanvasCard">
+            <div class="canvas-header">右袖</div>
+            <canvas id="rightCanvas" width="750" height="650"></canvas>
+        </div>
+        <!-- 帽子部位的canvas -->
+        <div class="canvas-card" id="headCanvasCard">
+            <div class="canvas-header">帽子</div>
+            <canvas id="headCanvas" width="750" height="650"></canvas>
+        </div>
+    </div>
+
+    <div class="status-bar" id="statusBar">正在加载基础图片...</div>
+
+    <div class="controls">
+        <div class="control-group">
+            <h3>色彩调整</h3>
+            <div class="slider-container">
+                <label>色相旋转: <span id="hueValue" class="value-display">0°</span></label>
+                                <input type="range" id="hueRotate" min="-180" max="180" value="0" step="1">
+            </div>
+            <div class="slider-container">
+                <label>饱和度: <span id="saturationValue" class="value-display">100%</span></label>
+                <input type="range" id="saturation" min="0" max="200" value="100" step="1">
+            </div>
+            <div class="slider-container">
+                <label>亮度: <span id="brightnessValue" class="value-display">100%</span></label>
+                <input type="range" id="brightness" min="50" max="150" value="100" step="1">
+            </div>
+        </div>
+
+        <div class="control-group">
+            <h3>高级控制</h3>
+            <div class="slider-container">
+                <label>颜色强度: <span id="intensityValue" class="value-display">100%</span></label>
+                <input type="range" id="intensity" min="0" max="100" value="100" step="1">
+            </div>
+            <div class="slider-container">
+                <label>阴影保留: <span id="shadowValue" class="value-display">80%</span></label>
+                <input type="range" id="shadowPreserve" min="0" max="100" value="80" step="1">
+            </div>
+            <button id="resetBtn" style="margin-top: 20px; padding: 8px 15px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;">重置调整参数</button>
+        </div>
+    </div>
+
+    <script>
+        // 颜色映射表
+        const colorMap = {
+            '宝蓝色': '#0033A0',
+            '浅驼色': '#D2B48C',
+            '测试色': '#FF0000',
+            '深灰绿': '#5E716A',
+            '深紫色': '#36013F',
+            '深绿色': '#013220',
+            '真朱': '#B3424A',
+            '睿智金': '#D4AF37',
+            '红色': '#FF0000',
+            '萌萌绿': '#A5D152',
+            '蒂芙尼蓝': '#81D8D0',
+            '长春花蓝': '#6667AB',
+            '青紫': '#6A0DAD',
+            '靓丽黄': '#FFD700'
+        };
+
+        // 部位信息(定义渲染顺序,确保正确叠加)
+        const parts = {
+            all: { name: '整体', image: '' }, // 整体不需要实际图片
+            body: { name: '身体', image: 'body.png' },
+            left: { name: '左袖', image: 'left.png' },
+            right: { name: '右袖', image: 'right.png' },
+            head: { name: '帽子', image: 'head.png' }
+        };
+
+        // 存储各部位的颜色设置
+        let partColors = {
+            all: colorMap['深灰绿'],
+            left: colorMap['深灰绿'],
+            right: colorMap['深灰绿'],
+            body: colorMap['深灰绿'],
+            head: colorMap['深灰绿']
+        };
+
+        // 存储各部位的图像数据
+        let partImageData = {};
+        let originalPartImageData = {}; // 存储原始图像数据
+
+        // 当前选中的部位
+        let currentPart = 'all';
+
+        // 初始化颜色选择器
+        const colorOptions = document.getElementById('colorOptions');
+        Object.entries(colorMap).forEach(([name, hex]) => {
+            const option = document.createElement('div');
+            option.className = 'color-option';
+            option.title = name;
+            option.style.backgroundColor = hex;
+            option.dataset.name = name;
+            option.dataset.hex = hex;
+
+            const nameSpan = document.createElement('span');
+            nameSpan.className = 'color-name';
+            nameSpan.textContent = name;
+
+            option.appendChild(nameSpan);
+            option.addEventListener('click', () => {
+                setPartColor(hex, name);
+            });
+
+            colorOptions.appendChild(option);
+        });
+
+        // 初始化部位选择器
+        const partSelector = document.getElementById('partSelector');
+        const partIndicator = document.getElementById('partIndicator');
+
+        partSelector.addEventListener('click', (e) => {
+            if (e.target.classList.contains('part-btn')) {
+                document.querySelectorAll('.part-btn').forEach(btn => {
+                    btn.classList.remove('active');
+                });
+                e.target.classList.add('active');
+
+                currentPart = e.target.dataset.part;
+                partIndicator.textContent = `当前编辑: ${parts[currentPart].name}`;
+
+                updateColorSelector();
+                updateAdjustmentDisplay();
+                renderAllParts(); // 切换部位时重新渲染所有
+            }
+        });
+
+        // 获取各部位的Canvas元素
+        const allCanvas = document.getElementById('allCanvas');
+        const bodyCanvas = document.getElementById('bodyCanvas');
+        const leftCanvas = document.getElementById('leftCanvas');
+        const rightCanvas = document.getElementById('rightCanvas');
+        const headCanvas = document.getElementById('headCanvas');
+        const originalCanvas = document.createElement('canvas');
+        const colorizedCanvas = document.createElement('canvas');
+        const originalCtx = originalCanvas.getContext('2d');
+        const colorizedCtx = colorizedCanvas.getContext('2d');
+        const statusBar = document.getElementById('statusBar');
+
+        // 控制元素
+        const hueRotate = document.getElementById('hueRotate');
+        const saturation = document.getElementById('saturation');
+        const brightness = document.getElementById('brightness');
+        const intensity = document.getElementById('intensity');
+        const shadowPreserve = document.getElementById('shadowPreserve');
+        const resetBtn = document.getElementById('resetBtn');
+
+        // 值显示元素
+        const hueValue = document.getElementById('hueValue');
+        const saturationValue = document.getElementById('saturationValue');
+        const brightnessValue = document.getElementById('brightnessValue');
+        const intensityValue = document.getElementById('intensityValue');
+        const shadowValue = document.getElementById('shadowValue');
+
+        // 存储各部位的调整参数
+        let partAdjustments = {
+            all: { hue: 0, saturation: 100, brightness: 100, intensity: 100, shadowPreserve: 80 },
+            left: { hue: 0, saturation: 100, brightness: 100, intensity: 100, shadowPreserve: 80 },
+            right: { hue: 0, saturation: 100, brightness: 100, intensity: 100, shadowPreserve: 80 },
+            body: { hue: 0, saturation: 100, brightness: 100, intensity: 100, shadowPreserve: 80 },
+            head: { hue: 0, saturation: 100, brightness: 100, intensity: 100, shadowPreserve: 80 }
+        };
+
+        // 加载所有图像
+        function loadAllImages() {
+            let loadedCount = 0;
+            const totalImages = Object.keys(parts).length - 1; // 减去all
+
+            // 按渲染顺序加载各部位图像
+            const renderOrder = ['body', 'left', 'right', 'head'];
+            renderOrder.forEach(part => {
+                const img = new Image();
+                img.crossOrigin = "Anonymous";
+                img.src = `./cloth/${parts[part].image}`;
+
+                img.onload = function () {
+                    // 处理着色画布
+                    const tempCanvas = document.createElement('canvas');
+                    tempCanvas.width = img.width;
+                    tempCanvas.height = img.height;
+                    const tempCtx = tempCanvas.getContext('2d');
+                    tempCtx.drawImage(img, 0, 0);
+                    partImageData[part] = tempCtx.getImageData(0, 0, img.width, img.height);
+
+                    // 处理原始画布
+                    const origTempCanvas = document.createElement('canvas');
+                    origTempCanvas.width = img.width;
+                    origTempCanvas.height = img.height;
+                    const origTempCtx = origTempCanvas.getContext('2d');
+                    origTempCtx.drawImage(img, 0, 0);
+                    originalPartImageData[part] = origTempCtx.getImageData(0, 0, img.width, img.height);
+
+                    loadedCount++;
+                    updateLoadStatus(loadedCount, totalImages);
+
+                    // 如果所有图像加载完成,设置画布大小并渲染
+                    if (loadedCount === totalImages) {
+                        // 使用第一个图像的大小作为基准
+                        const firstPart = renderOrder[0];
+                        originalCanvas.width = partImageData[firstPart].width;
+                        originalCanvas.height = partImageData[firstPart].height;
+                        colorizedCanvas.width = partImageData[firstPart].width;
+                        colorizedCanvas.height = partImageData[firstPart].height;
+
+                        // 设置所有画布大小一致
+                        [allCanvas, bodyCanvas, leftCanvas, rightCanvas, headCanvas].forEach(canvas => {
+                            canvas.width = partImageData[firstPart].width;
+                            canvas.height = partImageData[firstPart].height;
+                        });
+
+                        renderOriginalParts();
+                        renderAllParts();
+                    }
+                };
+
+                img.onerror = function () {
+                    console.error(`加载${parts[part].name}失败:./cloth/${parts[part].image}`);
+                    statusBar.textContent = `错误: 加载${parts[part].name}图像失败`;
+                };
+            });
+        }
+
+        // 更新加载状态
+        function updateLoadStatus(loaded, total) {
+            statusBar.textContent = `正在加载资源: ${loaded}/${total}`;
+            if (loaded === total) {
+                statusBar.textContent = '资源加载完成!可以开始编辑颜色';
+                updateColorSelector();
+            }
+        }
+
+        // 设置部位颜色
+        function setPartColor(hex, name) {
+            if (currentPart === 'all') {
+                // 如果是整体模式,更新所有部位的颜色
+                ['left', 'right', 'body', 'head'].forEach(part => {
+                    partColors[part] = hex;
+                });
+            } else {
+                // 否则只更新当前部位
+                partColors[currentPart] = hex;
+            }
+            
+            updateColorSelector();
+            renderAllParts();
+            
+            if (currentPart === 'all') {
+                statusBar.textContent = `所有部位已设置为: ${name}`;
+            } else {
+                statusBar.textContent = `${parts[currentPart].name}已设置为: ${name}`;
+            }
+        }
+
+        // 更新颜色选择器状态
+        function updateColorSelector() {
+            const currentColor = partColors[currentPart];
+            document.querySelectorAll('.color-option').forEach(option => {
+                option.classList.toggle('active', option.dataset.hex === currentColor);
+            });
+        }
+
+        // 更新调整参数显示
+        function updateAdjustmentDisplay() {
+            const adjustments = partAdjustments[currentPart];
+            hueRotate.value = adjustments.hue;
+            saturation.value = adjustments.saturation;
+            brightness.value = adjustments.brightness;
+            intensity.value = adjustments.intensity;
+            shadowPreserve.value = adjustments.shadowPreserve;
+
+            hueValue.textContent = `${adjustments.hue}°`;
+            saturationValue.textContent = `${adjustments.saturation}%`;
+            brightnessValue.textContent = `${adjustments.brightness}%`;
+            intensityValue.textContent = `${adjustments.intensity}%`;
+            shadowValue.textContent = `${adjustments.shadowPreserve}%`;
+        }
+
+        // 渲染原始部位(右侧画布)
+        function renderOriginalParts() {
+            originalCtx.clearRect(0, 0, originalCanvas.width, originalCanvas.height);
+
+            // 按正确顺序渲染所有部位(从下到上)
+            const renderOrder = ['body', 'left', 'right', 'head'];
+            renderOrder.forEach(part => {
+                if (originalPartImageData[part]) {
+                    originalCtx.putImageData(originalPartImageData[part], 0, 0);
+                }
+            });
+        }
+
+        // 渲染所有着色部位(左侧画布)
+        function renderAllParts() {
+            // 按正确顺序渲染所有部位(从下到上)
+            const renderOrder = ['body', 'left', 'right', 'head'];
+            
+            // 清除所有画布
+            [allCanvas, bodyCanvas, leftCanvas, rightCanvas, headCanvas].forEach(canvas => {
+                const ctx = canvas.getContext('2d');
+                ctx.clearRect(0, 0, canvas.width, canvas.height);
+            });
+            
+            // 分别渲染每个部位
+            renderOrder.forEach(part => {
+                if (partImageData[part]) {
+                    renderPart(part);
+                }
+            });
+            
+            // 最后渲染整体效果
+            renderAllCombined();
+        }
+
+        // 渲染单个部位
+        function renderPart(part) {
+            const canvas = document.getElementById(`${part}Canvas`);
+            const ctx = canvas.getContext('2d');
+            
+            // 复制原始图像数据
+            const imageData = new ImageData(
+                new Uint8ClampedArray(originalPartImageData[part].data),
+                originalPartImageData[part].width,
+                originalPartImageData[part].height
+            );
+
+            const targetColor = partColors[part];
+            const adjustments = partAdjustments[part];
+            const targetRgb = hexToRgb(targetColor);
+            const baseRgb = { r: 128, g: 128, b: 128 }; // 基础灰色
+
+            // 颜色比率
+            const rRatio = targetRgb.r / baseRgb.r;
+            const gRatio = targetRgb.g / baseRgb.g;
+            const bRatio = targetRgb.b / baseRgb.b;
+
+            // 调整参数
+            const hueAngle = adjustments.hue;
+            const satFactor = adjustments.saturation / 100;
+            const brightFactor = adjustments.brightness / 100;
+            const intensityFactor = adjustments.intensity / 100;
+            const shadowPreserveFactor = adjustments.shadowPreserve / 100;
+
+            // 处理每个像素
+            const data = imageData.data;
+            for (let i = 0; i < data.length; i += 4) {
+                const sourceAlpha = data[i + 3];
+                if (sourceAlpha === 0) continue; // 跳过透明像素
+
+                const r = data[i];
+                const g = data[i + 1];
+                const b = data[i + 2];
+                const gray = 0.299 * r + 0.587 * g + 0.114 * b; // 灰度值
+
+                // 应用颜色比率
+                let newR = r * rRatio;
+                let newG = g * gRatio;
+                let newB = b * bRatio;
+
+                // 限制范围
+                newR = Math.min(255, Math.max(0, newR));
+                newG = Math.min(255, Math.max(0, newG));
+                newB = Math.min(255, Math.max(0, newB));
+
+                // 颜色强度混合
+                newR = newR * intensityFactor + r * (1 - intensityFactor);
+                newG = newG * intensityFactor + g * (1 - intensityFactor);
+                newB = newB * intensityFactor + b * (1 - intensityFactor);
+
+                // 亮度调整(保留阴影)
+                if (gray < 30) {
+                    // 阴影区域少调整
+                    newR *= 0.3 + 0.7 * shadowPreserveFactor;
+                    newG *= 0.3 + 0.7 * shadowPreserveFactor;
+                    newB *= 0.3 + 0.7 * shadowPreserveFactor;
+                } else {
+                    newR *= brightFactor;
+                    newG *= brightFactor;
+                    newB *= brightFactor;
+                }
+
+                // HSV调整
+                let hsv = rgbToHsv(newR, newG, newB);
+                hsv.h = (hsv.h + hueAngle) % 360;
+                if (hsv.h < 0) hsv.h += 360;
+                hsv.s = Math.min(1, Math.max(0, hsv.s * satFactor));
+
+                const adjustedRgb = hsvToRgb(hsv.h, hsv.s, hsv.v);
+
+                // 设置像素值
+                data[i] = Math.min(255, Math.max(0, adjustedRgb.r));
+                data[i + 1] = Math.min(255, Math.max(0, adjustedRgb.g));
+                data[i + 2] = Math.min(255, Math.max(0, adjustedRgb.b));
+                data[i + 3] = sourceAlpha; // 保持原有的Alpha值
+            }
+
+            // 绘制到画布
+            ctx.putImageData(imageData, 0, 0);
+        }
+
+        // 渲染整体效果
+        function renderAllCombined() {
+            const ctx = allCanvas.getContext('2d');
+            ctx.clearRect(0, 0, allCanvas.width, allCanvas.height);
+            
+            // 按顺序绘制各部位
+            ['body', 'left', 'right', 'head'].forEach(part => {
+                const partCanvas = document.getElementById(`${part}Canvas`);
+                ctx.drawImage(partCanvas, 0, 0);
+            });
+        }
+
+        // 控制事件
+        hueRotate.addEventListener('input', () => {
+            partAdjustments[currentPart].hue = parseInt(hueRotate.value);
+            hueValue.textContent = `${hueRotate.value}°`;
+            renderAllParts();
+        });
+
+        saturation.addEventListener('input', () => {
+            partAdjustments[currentPart].saturation = parseInt(saturation.value);
+            saturationValue.textContent = `${saturation.value}%`;
+            renderAllParts();
+        });
+
+        brightness.addEventListener('input', () => {
+            partAdjustments[currentPart].brightness = parseInt(brightness.value);
+            brightnessValue.textContent = `${brightness.value}%`;
+            renderAllParts();
+        });
+
+        intensity.addEventListener('input', () => {
+            partAdjustments[currentPart].intensity = parseInt(intensity.value);
+            intensityValue.textContent = `${intensity.value}%`;
+            renderAllParts();
+        });
+
+        shadowPreserve.addEventListener('input', () => {
+            partAdjustments[currentPart].shadowPreserve = parseInt(shadowPreserve.value);
+            shadowValue.textContent = `${shadowPreserve.value}%`;
+            renderAllParts();
+        });
+
+        resetBtn.addEventListener('click', () => {
+            partAdjustments[currentPart] = {
+                hue: 0,
+                saturation: 100,
+                brightness: 100,
+                intensity: 100,
+                shadowPreserve: 80
+            };
+            updateAdjustmentDisplay();
+            renderAllParts();
+        });
+
+        // 工具函数:HEX转RGB
+        function hexToRgb(hex) {
+            if (hex.length === 4) {
+                hex = hex.replace(/#([a-f\d])([a-f\d])([a-f\d])/i, (m, r, g, b) => `#${r}${r}${g}${g}${b}${b}`);
+            }
+            const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+            return result? {
+                r: parseInt(result[1], 16),
+                g: parseInt(result[2], 16),
+                b: parseInt(result[3], 16)
+            } : { r: 0, g: 0, b: 0 };
+        }
+
+        // RGB转HSV
+        function rgbToHsv(r, g, b) {
+            r /= 255; g /= 255; b /= 255;
+            const max = Math.max(r, g, b);
+            const min = Math.min(r, g, b);
+            let h = 0, s = 0, v = max;
+            const d = max - min;
+            s = max? d / max : 0;
+
+            if (max!== min) {
+                switch (max) {
+                    case r: h = (g - b) / d + (g < b? 6 : 0); break;
+                    case g: h = (b - r) / d + 2; break;
+                    case b: h = (r - g) / d + 4; break;
+                }
+                h *= 60;
+            }
+            return { h, s, v };
+        }
+
+        // HSV转RGB
+        function hsvToRgb(h, s, v) {
+            let r, g, b;
+            const i = Math.floor(h / 60);
+            const f = h / 60 - i;
+            const p = v * (1 - s);
+            const q = v * (1 - f * s);
+            const t = v * (1 - (1 - f) * s);
+
+            switch (i % 6) {
+                case 0: r = v, g = t, b = p; break;
+                case 1: r = q, g = v, b = p; break;
+                case 2: r = p, g = v, b = t; break;
+                case 3: r = p, g = q, b = v; break;
+                case 4: r = t, g = p, b = v; break;
+                case 5: r = v, g = p, b = q; break;
+            }
+            return { r: r * 255, g: g * 255, b: b * 255 };
+        }
+
+        // 初始化加载
+        loadAllImages();
+    </script>
+</body>
+
+</html>

+ 176 - 29
cloth-design/src/app/modules/cloth/mobile/page-design/page-design.component.ts

@@ -1,7 +1,23 @@
-import { Component } from '@angular/core';
+// page-design.component.ts
+import { Component, ChangeDetectorRef } from '@angular/core';
 import { FormsModule } from '@angular/forms';
 import { CommonModule } from '@angular/common';
 import { ColorUsage } from '../../../../../lib/ncloud';
+import { TestMessage, TestCompletion } from '../../../../../lib/completion';
+import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
+
+// 定义类型
+interface TrendRecommendation {
+  title: string;
+  colors: string[];
+  description: string;
+}
+
+interface PresetQuestion {
+  text: string;
+  description: string;
+  icon: string;
+}
 
 @Component({
   selector: 'app-page-design',
@@ -11,6 +27,7 @@ import { ColorUsage } from '../../../../../lib/ncloud';
   styleUrls: ['./page-design.scss']
 })
 export class PageDesignComponent {
+  // 原有设计界面的变量(保持不变)
   activePart: string = 'hood';
   selectedColor: string = '#3498db';
   colors: string[] = [
@@ -19,7 +36,6 @@ export class PageDesignComponent {
     '#ecf0f1', '#e84393', '#ffeaa7','#ffb6c1',
     '#98ff98','#b399d4','#ff7f50','#89cff0',
    '#ff9aa2','#40e0d0','#f0e68c','#c8a2c8'
-
   ];
   
   partColors: { [key: string]: string } = {
@@ -29,13 +45,63 @@ export class PageDesignComponent {
     'sleeve-right': '#3498db'
   };
   
-  aiMessages: any[] = [
+  colorUsage: ColorUsage = new ColorUsage();
+
+  // 新增的AI助手相关变量
+  aiMessages: { text: string | SafeHtml; isUser: boolean; isLoading?: boolean; isTrend?: boolean }[] = [
     { text: '您好!我是您的羽绒服设计助手。我可以根据您的喜好推荐颜色搭配,或者帮您分析当前设计的流行度。有什么我可以帮您的吗?', isUser: false }
   ];
   aiInput: string = '';
+  showAIModal: boolean = false;
+  isAILoading: boolean = false;
+  
+  presetQuestions: PresetQuestion[] = [
+    { 
+      text: '推荐一些流行颜色搭配', 
+      description: '获取当前最受欢迎的配色方案',
+      icon: 'fas fa-palette'
+    },
+    { 
+      text: '分析我的设计流行度', 
+      description: '评估当前设计的市场吸引力',
+      icon: 'fas fa-chart-line'
+    },
+    { 
+      text: '生成3D预览效果', 
+      description: '创建设计的3D可视化预览',
+      icon: 'fas fa-cube'
+    },
+    { 
+      text: '提供设计灵感', 
+      description: '获取创意设计建议',
+      icon: 'fas fa-lightbulb'
+    }
+  ];
+  
+  trendRecommendations: TrendRecommendation[] = [
+    {
+      title: '海洋蓝渐变',
+      colors: ['#89CFF0', '#40E0D0', '#008080'],
+      description: '从浅蓝到深蓝的渐变效果,清新自然'
+    },
+    {
+      title: '日落橙黄',
+      colors: ['#FF7F50', '#FFD700', '#FFA500'],
+      description: '温暖活力的橙黄色系,充满活力'
+    },
+    {
+      title: '森林绿系',
+      colors: ['#98FF98', '#2E8B57', '#006400'],
+      description: '自然森林色调,环保且时尚'
+    }
+  ];
 
-  colorUsage: ColorUsage = new ColorUsage();
+  constructor(
+    private cdr: ChangeDetectorRef,
+    private sanitizer: DomSanitizer
+  ) {}
 
+  // 原有设计界面的方法(保持不变)
   selectPart(part: string) {
     this.activePart = part;
   }
@@ -64,32 +130,9 @@ export class PageDesignComponent {
   }
 
   saveDesign() {
-    // 在实际应用中,这里会调用API保存设计
     alert('设计已保存!');
   }
 
-  sendAiMessage() {
-    const text = this.aiInput.trim();
-    if (!text) return;
-    
-    this.aiMessages.push({ text, isUser: true });
-    this.aiInput = '';
-    
-    // 模拟AI回复
-    setTimeout(() => {
-      let response = '';
-      if (text.includes('推荐') || text.includes('搭配')) {
-        response = '根据您的设计,我推荐尝试海洋蓝与珊瑚橙的搭配,这种组合在近期很受欢迎,能体现活力与时尚感。';
-      } else if (text.includes('流行') || text.includes('趋势')) {
-        response = '当前最流行的羽绒服颜色搭配是深海蓝黑渐变和日落橙黄渐变。这两种搭配在本月使用率上升了35%。';
-      } else {
-        response = '我理解您的需求了。作为AI设计助手,我可以为您提供以下帮助:\n1. 推荐颜色搭配方案\n2. 分析当前设计流行度\n3. 提供设计灵感\n4. 生成3D预览效果\n请告诉我您想了解哪方面的内容?';
-      }
-      
-      this.aiMessages.push({ text: response, isUser: false });
-    }, 1000);
-  }
-
   private darkenColor(color: string, percent: number): string {
     const num = parseInt(color.replace('#', ''), 16);
     const amt = Math.round(2.55 * percent);
@@ -104,8 +147,112 @@ export class PageDesignComponent {
       (B < 255 ? B < 1 ? 0 : B : 255)
     ).toString(16).slice(1);
   }
-  // 新增颜色使用统计
+
   getColorUsage(): { [key: string]: number } {
     return this.colorUsage.getAll();
-}
+  }
+
+  // 新增的AI助手方法
+  toggleAIModal() {
+    this.showAIModal = !this.showAIModal;
+    if (this.showAIModal) {
+      this.showTrendRecommendations();
+    }
+  }
+  
+  showTrendRecommendations() {
+    if (this.aiMessages.length > 1) return;
+    
+    let trendMessage = '根据当前流行趋势,我为您推荐以下设计方向:\n\n';
+    
+    this.trendRecommendations.forEach((trend: TrendRecommendation, index: number) => {
+      trendMessage += `${index + 1}. ${trend.title}\n`;
+      trendMessage += `   ${trend.description}\n`;
+      trendMessage += `   推荐颜色: `;
+      
+      trend.colors.forEach((color: string) => {
+        trendMessage += `<span class="color-dot" style="background-color:${color}"></span> `;
+      });
+      
+      trendMessage += '\n\n';
+    });
+    
+    trendMessage += '您可以直接选择应用这些推荐,或者告诉我您的具体需求!';
+    
+    this.aiMessages.push({ 
+      text: this.sanitizer.bypassSecurityTrustHtml(trendMessage), 
+      isUser: false,
+      isTrend: true
+    });
+  }
+  
+  applyTrendRecommendation(index: number) {
+    if (index < 0 || index >= this.trendRecommendations.length) return;
+    
+    const trend = this.trendRecommendations[index];
+    
+    // 应用推荐颜色
+    this.partColors['hood'] = trend.colors[0];
+    this.partColors['body'] = trend.colors[1];
+    this.partColors['sleeve-left'] = trend.colors[0];
+    this.partColors['sleeve-right'] = trend.colors[0];
+    
+    // 更新选中颜色
+    this.selectedColor = trend.colors[0];
+    
+    this.aiMessages.push({ 
+      text: `已应用推荐:${trend.title}`, 
+      isUser: true 
+    });
+    
+    setTimeout(() => {
+      this.aiMessages.push({ 
+        text: `太棒了!您已成功应用了${trend.title}设计。这个方案在当前非常流行,预计会受到年轻群体的喜爱。您想进一步调整还是保存设计?`, 
+        isUser: false 
+      });
+    }, 500);
+  }
+
+  async sendAiMessage() {
+    const text = this.aiInput.trim();
+    if (!text) return;
+    
+    this.aiMessages.push({ text, isUser: true });
+    this.aiInput = '';
+    
+    const aiReplyIndex = this.aiMessages.length;
+    this.aiMessages.push({ text: '思考中...', isUser: false, isLoading: true });
+    
+    this.isAILoading = true;
+    this.cdr.detectChanges();
+    
+    try {
+      const messages: TestMessage[] = this.aiMessages
+        .filter(msg => !msg.isLoading && typeof msg.text === 'string')
+        .map(msg => ({
+          role: msg.isUser ? 'user' : 'assistant',
+          content: msg.text as string
+        }));
+      
+      const completion = new TestCompletion(messages);
+      
+      await completion.sendMessage(null, (content) => {
+        this.aiMessages[aiReplyIndex].text = content;
+        this.cdr.detectChanges();
+      });
+      
+    } catch (error) {
+      console.error('AI请求失败:', error);
+      this.aiMessages[aiReplyIndex].text = '抱歉,AI助手暂时无法回答,请稍后再试。';
+    } finally {
+      this.aiMessages[aiReplyIndex].isLoading = false;
+      this.isAILoading = false;
+      this.cdr.detectChanges();
+    }
+  }
+  
+  usePresetQuestion(question: string) {
+    this.aiInput = question;
+    this.sendAiMessage();
+  }
 }

+ 103 - 7
cloth-design/src/app/modules/cloth/mobile/page-design/page-design.html

@@ -1,3 +1,5 @@
+<!-- page-design.html -->
+<!-- 原有设计界面保持不变 -->
 <div class="panel">
   <h2><i class="fas fa-palette"></i> 个性定制</h2>
   <p class="section-desc">选择羽绒服的不同部位并自定义颜色,打造属于你的独特设计</p>
@@ -34,26 +36,120 @@
     </button>
   </div>
   
-  <div class="ai-recommendation">
-    <div class="ai-header">
+  <!-- 新增的AI助手部分 -->
+  <button class="ai-float-btn" (click)="toggleAIModal()">
+    <i class="fas fa-robot"></i>
+    <span>AI助手</span>
+  </button>
+  
+  <div class="ai-modal" *ngIf="showAIModal">
+    <div class="ai-modal-content">
+      <div class="ai-modal-header">
+        <div class="ai-icon">
+          <i class="fas fa-robot"></i>
+        </div>
+        <div class="ai-title">AI设计助手</div>
+        <button class="close-btn" (click)="toggleAIModal()">&times;</button>
+      </div>
+      
+      <div class="ai-messages">
+        <div *ngFor="let message of aiMessages; let i = index" class="message" 
+             [class.user-message]="message.isUser" 
+             [class.ai-message]="!message.isUser"
+             [class.trend-message]="message.isTrend">
+          <div *ngIf="message.isLoading" class="loading-dots">
+            <span></span>
+            <span></span>
+            <span></span>
+          </div>
+          
+          <div [innerHTML]="message.text"></div>
+          
+          <div *ngIf="message.isTrend" class="trend-actions">
+            <button *ngFor="let trend of trendRecommendations; let j = index" 
+                    class="trend-btn"
+                    (click)="applyTrendRecommendation(j)">
+              应用{{ trend.title }}
+            </button>
+          </div>
+        </div>
+      </div>
+      
+      <div class="preset-questions">
+        <div class="preset-title">快捷提问:</div>
+        <div class="preset-buttons">
+          <button *ngFor="let question of presetQuestions" 
+                  class="preset-btn"
+                  (click)="usePresetQuestion(question.text)">
+            <i [class]="question.icon"></i>
+            {{ question.text }}
+            <span class="preset-desc">{{ question.description }}</span>
+          </button>
+        </div>
+      </div>
+      
+      <div class="ai-input-group">
+        <input type="text" class="ai-input" placeholder="输入您的问题..." 
+               [(ngModel)]="aiInput" [disabled]="isAILoading"
+               (keyup.enter)="sendAiMessage()">
+        <button class="ai-send-btn" (click)="sendAiMessage()" [disabled]="isAILoading">
+          <i class="fas fa-paper-plane"></i>
+        </button>
+      </div>
+    </div>
+  </div>
+</div>
+<div class="ai-modal" *ngIf="showAIModal">
+  <div class="ai-modal-content">
+    <div class="ai-modal-header">
       <div class="ai-icon">
         <i class="fas fa-robot"></i>
       </div>
       <div class="ai-title">AI设计助手</div>
+      <button class="close-btn" (click)="toggleAIModal()">&times;</button>
     </div>
     
     <div class="ai-messages">
-      <div *ngFor="let message of aiMessages" class="message" 
+      <div *ngFor="let message of aiMessages; let i = index" class="message" 
            [class.user-message]="message.isUser" 
-           [class.ai-message]="!message.isUser">
-        {{ message.text }}
+           [class.ai-message]="!message.isUser"
+           [class.trend-message]="message.isTrend">
+        <div *ngIf="message.isLoading" class="loading-dots">
+          <span></span>
+          <span></span>
+          <span></span>
+        </div>
+        
+        <div [innerHTML]="message.text"></div>
+        
+        <div *ngIf="message.isTrend" class="trend-actions">
+          <button *ngFor="let trend of trendRecommendations; let j = index" 
+                  class="trend-btn"
+                  (click)="applyTrendRecommendation(j)">
+            应用{{ trend.title }}
+          </button>
+        </div>
+      </div>
+    </div>
+    
+    <div class="preset-questions">
+      <div class="preset-title">快捷提问:</div>
+      <div class="preset-buttons">
+        <button *ngFor="let question of presetQuestions" 
+                class="preset-btn"
+                (click)="usePresetQuestion(question.text)">
+          <i [class]="question.icon"></i>
+          {{ question.text }}
+          <span class="preset-desc">{{ question.description }}</span>
+        </button>
       </div>
     </div>
     
     <div class="ai-input-group">
       <input type="text" class="ai-input" placeholder="输入您的问题..." 
-             [(ngModel)]="aiInput" (keyup.enter)="sendAiMessage()">
-      <button class="ai-send-btn" (click)="sendAiMessage()">
+             [(ngModel)]="aiInput" [disabled]="isAILoading"
+             (keyup.enter)="sendAiMessage()">
+      <button class="ai-send-btn" (click)="sendAiMessage()" [disabled]="isAILoading">
         <i class="fas fa-paper-plane"></i>
       </button>
     </div>

+ 434 - 48
cloth-design/src/app/modules/cloth/mobile/page-design/page-design.scss

@@ -1,3 +1,5 @@
+/* page-design.scss */
+/* 原有设计界面样式保持不变 */
 .panel {
   background: white;
   border-radius: 25px;
@@ -6,6 +8,7 @@
   box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
   animation: fadeIn 0.6s ease;
   transform-origin: top;
+  position: relative; /* 为浮动按钮提供定位上下文 */
 }
 
 h2 {
@@ -22,6 +25,7 @@ h2 {
 h2 i {
   background: linear-gradient(135deg, #3498db, #9b59b6);
   -webkit-background-clip: text;
+  background-clip: text;
   -webkit-text-fill-color: transparent;
   width: 36px;
   height: 36px;
@@ -206,46 +210,153 @@ h2 i {
   transform: translateY(-3px);
 }
 
-.ai-recommendation {
-  background: white;
-  border-radius: 20px;
-  padding: 25px;
-  margin-top: 30px;
-  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
+@keyframes fadeIn {
+  from { opacity: 0; transform: translateY(20px); }
+  to { opacity: 1; transform: translateY(0); }
 }
 
-.ai-header {
+@keyframes pulse {
+  0% { transform: scale(1); }
+  50% { transform: scale(1.05); }
+  100% { transform: scale(1); }
+}
+
+.pulse {
+  animation: pulse 2s infinite;
+}
+
+@media (max-width: 480px) {
+  .jacket-display {
+    width: 280px;
+    height: 400px;
+  }
+  
+  .panel {
+    padding: 25px 20px;
+  }
+}
+
+/* ===== 新增的AI助手样式 ===== */
+
+/* 右下角AI助手按钮 */
+.ai-float-btn {
+  position: fixed;
+  bottom: 30px;
+  right: 30px;
+  z-index: 1000;
   display: flex;
   align-items: center;
-  gap: 12px;
-  margin-bottom: 20px;
-  color: #2c3e50;
+  gap: 10px;
+  padding: 15px 20px;
+  border-radius: 50px;
+  background: linear-gradient(135deg, #9b59b6, #3498db);
+  color: white;
+  border: none;
+  box-shadow: 0 8px 25px rgba(52, 152, 219, 0.4);
+  cursor: pointer;
+  transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
+  
+  i {
+    font-size: 1.2rem;
+  }
+  
+  span {
+    font-weight: 600;
+  }
+  
+  &:hover {
+    transform: translateY(-5px);
+    box-shadow: 0 12px 30px rgba(52, 152, 219, 0.5);
+  }
 }
 
-.ai-icon {
-  width: 40px;
-  height: 40px;
-  background: linear-gradient(135deg, #9b59b6, #3498db);
-  border-radius: 50%;
+/* AI模态框 */
+.ai-modal {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba(0, 0, 0, 0.5);
   display: flex;
   align-items: center;
   justify-content: center;
-  color: white;
-  font-size: 1.2rem;
+  z-index: 2000;
+  backdrop-filter: blur(5px);
+}
+
+.ai-modal-content {
+  width: 90%;
+  max-width: 500px;
+  max-height: 90vh;
+  background: white;
+  border-radius: 25px;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  box-shadow: 0 15px 50px rgba(0, 0, 0, 0.2);
+  animation: modalFadeIn 0.4s ease;
 }
 
-.ai-title {
-  font-size: 1.3rem;
-  font-weight: 700;
+@keyframes modalFadeIn {
+  from { opacity: 0; transform: translateY(50px); }
+  to { opacity: 1; transform: translateY(0); }
+}
+
+.ai-modal-header {
+  display: flex;
+  align-items: center;
+  padding: 20px;
+  background: linear-gradient(135deg, #9b59b6, #3498db);
+  color: white;
+  position: relative;
+  
+  .ai-icon {
+    width: 40px;
+    height: 40px;
+    background: white;
+    border-radius: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #9b59b6;
+    font-size: 1.2rem;
+  }
+  
+  .ai-title {
+    font-size: 1.3rem;
+    font-weight: 700;
+    margin-left: 15px;
+    flex: 1;
+  }
+  
+  .close-btn {
+    background: none;
+    border: none;
+    color: white;
+    font-size: 1.8rem;
+    cursor: pointer;
+    width: 40px;
+    height: 40px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: 50%;
+    transition: all 0.3s;
+    
+    &:hover {
+      background: rgba(255, 255, 255, 0.2);
+    }
+  }
 }
 
 .ai-messages {
-  height: 250px;
+  flex: 1;
+  min-height: 300px;
+  max-height: 50vh;
   overflow-y: auto;
-  padding: 15px;
+  padding: 20px;
   background: #f9f9f9;
-  border-radius: 15px;
-  margin-bottom: 20px;
 }
 
 .message {
@@ -254,6 +365,7 @@ h2 i {
   border-radius: 18px;
   max-width: 85%;
   position: relative;
+  transition: all 0.3s;
 }
 
 .user-message {
@@ -270,9 +382,134 @@ h2 i {
   border-bottom-left-radius: 5px;
 }
 
-.ai-input-group {
+/* 趋势推荐消息样式 */
+.trend-message {
+  background: #e3f2fd !important;
+  border-left: 4px solid #2196f3;
+  padding: 15px;
+}
+
+.color-dot {
+  display: inline-block;
+  width: 16px;
+  height: 16px;
+  border-radius: 50%;
+  border: 1px solid rgba(0,0,0,0.1);
+  vertical-align: middle;
+  margin: 0 2px;
+}
+
+.trend-actions {
   display: flex;
   gap: 10px;
+  margin-top: 15px;
+  flex-wrap: wrap;
+}
+
+.trend-btn {
+  padding: 8px 15px;
+  background: rgba(33, 150, 243, 0.1);
+  border: 1px solid #2196f3;
+  border-radius: 20px;
+  color: #2196f3;
+  font-size: 0.9rem;
+  cursor: pointer;
+  transition: all 0.3s;
+  
+  &:hover {
+    background: rgba(33, 150, 243, 0.2);
+    transform: translateY(-2px);
+  }
+}
+
+.loading-dots {
+  display: inline-flex;
+  gap: 5px;
+  position: absolute;
+  right: 15px;
+  top: 50%;
+  transform: translateY(-50%);
+  
+  span {
+    width: 8px;
+    height: 8px;
+    border-radius: 50%;
+    background: rgba(0, 0, 0, 0.3);
+    display: inline-block;
+    animation: dotPulse 1.5s infinite ease-in-out;
+    
+    &:nth-child(2) {
+      animation-delay: 0.2s;
+    }
+    
+    &:nth-child(3) {
+      animation-delay: 0.4s;
+    }
+  }
+}
+
+@keyframes dotPulse {
+  0%, 100% { transform: scale(0.8); opacity: 0.5; }
+  50% { transform: scale(1.2); opacity: 1; }
+}
+
+/* 预设问题模板样式 */
+.preset-questions {
+  padding: 15px;
+  background: #f8f9fa;
+  border-top: 1px solid #eee;
+}
+
+.preset-title {
+  color: #6c757d;
+  margin-bottom: 10px;
+  font-size: 0.95rem;
+  font-weight: 600;
+}
+
+.preset-buttons {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 10px;
+}
+
+.preset-btn {
+  padding: 12px 15px;
+  background: white;
+  border: 1px solid #e0e0e0;
+  border-radius: 12px;
+  color: #495057;
+  font-size: 0.95rem;
+  cursor: pointer;
+  transition: all 0.3s;
+  text-align: left;
+  display: flex;
+  flex-direction: column;
+  
+  i {
+    font-size: 1.2rem;
+    margin-bottom: 8px;
+    color: #3498db;
+  }
+  
+  &:hover {
+    border-color: #3498db;
+    box-shadow: 0 5px 15px rgba(52, 152, 219, 0.1);
+    transform: translateY(-3px);
+  }
+}
+
+.preset-desc {
+  font-size: 0.8rem;
+  color: #6c757d;
+  margin-top: 5px;
+}
+
+.ai-input-group {
+  display: flex;
+  padding: 15px;
+  background: white;
+  border-top: 1px solid #eee;
 }
 
 .ai-input {
@@ -283,10 +520,10 @@ h2 i {
   font-size: 1rem;
   outline: none;
   transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
-}
-
-.ai-input:focus {
-  border-color: #3498db;
+  
+  &:focus {
+    border-color: #3498db;
+  }
 }
 
 .ai-send-btn {
@@ -300,40 +537,189 @@ h2 i {
   align-items: center;
   justify-content: center;
   cursor: pointer;
+  margin-left: 10px;
   transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
+  
+  &:hover {
+    transform: translateY(-3px);
+    box-shadow: 0 8px 15px rgba(52, 152, 219, 0.3);
+  }
+  
+  &:disabled {
+    background: #bdc3c7;
+    cursor: not-allowed;
+    transform: none;
+    box-shadow: none;
+  }
 }
 
-.ai-send-btn:hover {
-  transform: translateY(-3px);
-  box-shadow: 0 8px 15px rgba(52, 152, 219, 0.3);
+/* 响应式调整 */
+@media (max-width: 768px) {
+  .ai-float-btn {
+    bottom: 20px;
+    right: 20px;
+    padding: 12px 15px;
+    font-size: 0.9rem;
+  }
+  
+  .ai-modal-content {
+    width: 95%;
+    max-height: 85vh;
+  }
+  
+  .ai-messages {
+    min-height: 250px;
+    max-height: 60vh;
+  }
+  
+  .preset-buttons {
+    grid-template-columns: 1fr;
+  }
 }
+/* 在 SCSS 文件的 AI 助手样式部分进行以下修改 */
 
-@keyframes fadeIn {
-  from { opacity: 0; transform: translateY(20px); }
-  to { opacity: 1; transform: translateY(0); }
+/* AI模态框 */
+.ai-modal {
+  /* 保持原有样式不变 */
 }
 
-@keyframes pulse {
-  0% { transform: scale(1); }
-  50% { transform: scale(1.05); }
-  100% { transform: scale(1); }
+.ai-modal-content {
+  width: 90%;
+  max-width: 500px;
+  max-height: 90vh;
+  background: white;
+  border-radius: 25px;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  box-shadow: 0 15px 50px rgba(0, 0, 0, 0.2);
+  animation: modalFadeIn 0.4s ease;
 }
 
-.pulse {
-  animation: pulse 2s infinite;
+/* 修改消息区域 */
+.ai-messages {
+  flex: 1;
+  min-height: 200px; /* 确保有最小高度 */
+  max-height: 40vh; /* 限制最大高度 */
+  overflow-y: auto;
+  padding: 20px;
+  background: #f9f9f9;
 }
 
-@media (max-width: 480px) {
-  .jacket-display {
-    width: 280px;
-    height: 400px;
+/* 修改预设问题区域 */
+.preset-questions {
+  padding: 15px;
+  background: #f8f9fa;
+  border-top: 1px solid #eee;
+  border-bottom: 1px solid #eee; /* 添加底部边框分隔 */
+  max-height: 30vh; /* 限制最大高度 */
+  overflow-y: auto; /* 添加滚动条 */
+}
+
+/* 修改输入框区域 */
+.ai-input-group {
+  display: flex;
+  padding: 15px;
+  background: white;
+  border-top: none; /* 移除顶部边框 */
+  position: relative; /* 确保位置正确 */
+  z-index: 10; /* 确保在顶部显示 */
+}
+
+/* 确保输入框在移动设备上正确显示 */
+.ai-input {
+  flex: 1;
+  padding: 14px 20px;
+  border: 2px solid #ecf0f1;
+  border-radius: 50px;
+  font-size: 1rem;
+  outline: none;
+  transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
+  box-sizing: border-box; /* 确保边框和内边距包含在宽度内 */
+  
+  &:focus {
+    border-color: #3498db;
+  }
+}
+
+/* 响应式调整 - 确保在较小屏幕上所有元素可见 */
+@media (max-width: 768px) {
+  .ai-modal-content {
+    width: 95%;
+    max-height: 85vh;
   }
   
-  .panel {
-    padding: 25px 20px;
+  .ai-messages {
+    min-height: 150px;
+    max-height: 35vh; /* 调整消息区域高度 */
+  }
+  
+  .preset-questions {
+    max-height: 20vh; /* 调整预设问题区域高度 */
+  }
+  
+  .preset-buttons {
+    grid-template-columns: 1fr;
+  }
+  
+  .preset-btn {
+    padding: 10px 12px;
+    font-size: 0.85rem;
+    
+    i {
+      font-size: 1rem;
+      margin-bottom: 5px;
+    }
+  }
+  
+  .preset-desc {
+    font-size: 0.75rem;
+  }
+  
+  .ai-input {
+    padding: 12px 16px;
+    font-size: 0.9rem;
+  }
+  
+  .ai-send-btn {
+    width: 45px;
+    height: 45px;
+  }
+}
+
+/* 针对小屏幕设备的额外调整 */
+@media (max-width: 480px) {
+  .ai-modal-content {
+    max-height: 90vh;
   }
   
   .ai-messages {
-    height: 200px;
+    min-height: 120px;
+    max-height: 30vh;
+    padding: 15px;
+  }
+  
+  .message {
+    padding: 10px 12px;
+    font-size: 0.9rem;
+  }
+  
+  .preset-questions {
+    padding: 10px;
+  }
+  
+  .ai-input-group {
+    padding: 10px;
+  }
+  
+  .ai-float-btn {
+    bottom: 15px;
+    right: 15px;
+    padding: 10px 15px;
+    font-size: 0.85rem;
+    
+    i {
+      font-size: 1rem;
+    }
   }
 }

+ 1 - 1
cloth-design/src/app/modules/cloth/mobile/page-design/page-design.spec.ts

@@ -8,7 +8,7 @@ describe('PageDesignComponent', () => {
 
   beforeEach(async () => {
     await TestBed.configureTestingModule({
-      imports: [PageDesignComponent]
+      imports: [PageDesignComponent]    
     })
     .compileComponents();
 

+ 170 - 0
cloth-design/src/lib/completion.ts

@@ -0,0 +1,170 @@
+
+export interface TestMessage{
+    role:string
+    content:string
+}
+
+export class TestCompletion{
+    token:string = "r:60abef69e7cd8181b146ceaba1fdbf02"
+    messageList:any = []
+    stream:boolean = true;
+    constructor(messageList:any){
+        this.messageList = messageList || this.messageList
+    }
+    async sendMessage(messageList?:null|Array<TestMessage>,onMessage?: (content: string) => void):Promise<any>{
+        
+        this.messageList = messageList || this.messageList
+        let body = {
+            "messages": this.messageList,
+            "stream": this.stream,
+            "model": "fmode-4.5-128k",
+            "temperature": 0.5,
+            "presence_penalty": 0,
+            "frequency_penalty": 0,
+            "token": "Bearer "+this.token
+        }
+
+        let response = await fetch("https://server.fmode.cn/api/apig/aigc/gpt/v1/chat/completions", {
+            "headers": {
+            },
+            "body": JSON.stringify(body),
+            "method": "POST",
+            "mode": "cors",
+            "credentials": "omit"
+        });
+
+        
+        /** 单次响应 HTTP短连接请求
+         {"choices":[{"finish_reason":"stop","index":0,"logprobs":null,"message":{"annotations":[],"content":"您好!我是一个人工智能助手,旨在帮助您回答问题、提供信息和解决各种问题。我可以处理许多主题,包括科技、历史、文化、语言学习等。如果您有任何具体的问题或需要了解的内容,请随时告诉我!","refusal":null,"role":"assistant"}}],"created":1751509370,"id":"chatcmpl-Bp3t41MP4pb2NR38n1ylrJw922SBZ","model":"gpt-4o-mini-2024-07-18","object":"chat.completion","system_fingerprint":"fp_efad92c60b","usage":{"completion_tokens":55,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":0,"rejected_prediction_tokens":0},"prompt_tokens":15,"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0},"total_tokens":70}}
+        */
+        if(this.stream == false){
+            let data = await response.json()
+            console.log(data)
+            let lastContent = data?.choices?.[0]?.message?.content
+            return lastContent
+        }
+        /**
+         * 流式加载 HTTP Event Stream 模式 长连接获取
+         */
+        
+        // Stream mode handling
+        if (!response.body) {
+            throw new Error("No response body in stream mode");
+        }
+
+        const reader = response.body.getReader();
+        const decoder = new TextDecoder("utf-8");
+        let accumulatedContent = "";
+        try {
+            while (true) {
+                const { done, value } = await reader.read();
+                if (done) break;
+
+                const chunk = decoder.decode(value, { stream: true });
+                const lines = chunk.split('\n').filter(line => line.trim() !== '');
+
+                for (const line of lines) {
+                    if (line.startsWith('data:') && !line.includes('[DONE]')) {
+                        try {
+                            const jsonStr = line.substring(5).trim();
+                            const data = JSON.parse(jsonStr);
+                            const content = data?.choices?.[0]?.delta?.content || '';
+                            
+                            if (content) {
+                                accumulatedContent += content;
+                                if (onMessage) {
+                                    onMessage(accumulatedContent);
+                                }
+                            }
+                        } catch (e) {
+                            console.error("Error parsing stream data:", e);
+                        }
+                    }
+                }
+            }
+        } finally {
+            reader.releaseLock();
+        }
+
+        return accumulatedContent;
+    }
+}
+
+/**
+ * 使用AI生成符合指定结构的JSON数据
+ * @param prompt 任务要求的整体提示词
+ * @param jsonSchema 期望的JSON结构描述(用于提示词)
+ * @param example 可选的JSON示例(帮助AI理解格式)
+ * @param onMessage 实时生成内容回调
+ * @returns 解析后的JSON对象
+ */
+export async function completionJSON(
+    prompt:string,
+  jsonSchema: string,
+  example: object | null = null,
+  onMessage?: (content: string) => void
+): Promise<any> {
+  // 1. 构建提示词
+  const JsonResultParsePrompt = `请严格按照以下要求生成JSON数据:
+- 数据结构要求:${jsonSchema}
+- 只能返回一个完整的JSON对象 确保JSON格式正确
+- 注意返回的JSON格式每个KEY都有""包裹和Value之间都有:`;
+// ${example ? `2. 参考示例格式:\n${JSON.stringify(example, null, 2)}` : ''}
+
+  // 2. 初始化消息列表
+  const messages: TestMessage[] = [
+    {
+      role: "user",
+      content: prompt+JsonResultParsePrompt
+    }
+  ];
+
+  // 3. 创建TestCompletion实例
+  const completion = new TestCompletion(messages);
+  
+  // 4. 存储累积内容和JSON对象
+  let fullContent = "";
+  let jsonObject: any = null;
+  
+  // 5. 发送请求并处理流式响应
+  const result = await completion.sendMessage(null, (content) => {
+    fullContent = content;
+  });
+  console.log("fullContent",fullContent)
+  // 6. 最终JSON提取(确保获取完整内容)
+  try {
+    // 再次尝试从完整内容中提取JSON
+      jsonObject = extractJSON(fullContent);
+  } catch (e) {
+    console.error("JSON解析失败:", e);
+    console.log("原始内容:", fullContent);
+    throw new Error("生成的响应不符合JSON格式");
+  }
+  
+  return jsonObject;
+}
+
+function extractJSON(str:string){
+  let stack = 0;
+  let startIndex = -1;
+  let result = null;
+
+  for (let i = 0; i < str.length; i++) {
+    if (str[i] === '{') {
+      if (stack === 0) startIndex = i;
+      stack++;
+    } else if (str[i] === '}') {
+      stack--;
+      if (stack === 0 && startIndex !== -1) {
+        try {
+          result = JSON.parse(str.slice(startIndex, i + 1));
+          break;
+        } catch (e) {
+          // 继续尝试下一个可能的 JSON
+          startIndex = -1;
+        }
+      }
+    }
+  }
+  return result;
+};

+ 10 - 0
cloth-design/tsconfig.doc.json

@@ -0,0 +1,10 @@
+{
+    "include": ["src/app/modules/cloth/mobile/nav-mobile-tabs/nav-mobile-tabs.component.ts",
+    "src/app/modules/cloth/mobile/page-design/page-design.component.ts",
+    "src/app/modules/cloth/mobile/page-ip/page-ip.component.ts",
+    "src/app/modules/cloth/mobile/page-mine/page-mine.component.ts",
+    "src/app/modules/cloth/mobile/page-trends/page-trends.component.ts",
+    "src/app/modules/cloth/mobile/page-home/page-home.component.ts",
+    "src/lib/ncloud.ts",]
+
+}

Some files were not shown because too many files changed in this diff