import { Injectable } from '@angular/core'; import { Observable, BehaviorSubject, of } from 'rxjs'; import { map, tap, catchError } from 'rxjs/operators'; import { DataStoreService } from '../../../app/core/services/data-store.service'; // ── Interfaces ────────────────────────────────────────────────────────────── export interface SizeRow { id?: string; sizeName: string; usaSize: string; bust: string; waist: string; hip: string; torso: string; cup: string; } export interface SizeTip { type: 'warn' | 'info'; text: string; } export interface SizeChart { id?: string; name: string; category: string; unit: string; note: string; rows: SizeRow[]; tips: SizeTip[]; } // ── Built-in default ───────────────────────────────────────────────────────── export const DEFAULT_SIZE_CHART: SizeChart = { name: 'US 女装标准尺码(泳装/瑜伽裤/运动上衣)', category: 'swimwear', unit: 'in', note: '美国站标准尺码对照表(泳装/瑜伽裤/运动上衣,单位:英寸)。当某 SKU 尺码不合退货占比 > 25% 时,请核对实测三围与下表偏差。', rows: [ { sizeName: 'XS', usaSize: '0-2', bust: '32-34', waist: '25-26.5', hip: '35-36.5', torso: '57.5-59.5', cup: '30A/B, 32A' }, { sizeName: 'S', usaSize: '4-6', bust: '34.5-36', waist: '27-28.5', hip: '37-38.5', torso: '59.5-61.5', cup: '32B/C, 34B' }, { sizeName: 'M', usaSize: '8-10', bust: '36.5-38', waist: '29-31', hip: '39-41', torso: '61.5-63.5', cup: '34C/D, 36B/C'}, { sizeName: 'L', usaSize: '12-14', bust: '38.5-40.5',waist: '32-34', hip: '42-44', torso: '63.5-65.5', cup: '36D, 38C/D' }, { sizeName: 'XL', usaSize: '16-18', bust: '41-43', waist: '35-37', hip: '45-47', torso: '65.5-67.5', cup: '38DD, 40C/D' }, { sizeName: 'XXL', usaSize: '20-22', bust: '43.5-45.5',waist: '37.5-39', hip: '47.5-49.5',torso: '67.5-69.5', cup: '40DD, 42C/D' } ], tips: [ { type: 'warn', text: '退货预警:若「尺码偏小」退货 > 25%,优先检查胸围实测 vs 标准偏差是否超 0.5 in' }, { type: 'info', text: '版型选型:高腰全包(安全感+防透)→ 对照腰围标准;常规下装 → 对照臀围标准' }, { type: 'info', text: '新品迭代:每批新品上市后 30 天追踪尺码退货率,偏差 > 0.5 in 触发改版' } ] }; const PARSE_CLASS = 'SizeChartKnowledge'; // ── Service ────────────────────────────────────────────────────────────────── /** * @description 尺码表知识库服务(可供全局任意页面注入使用) * * 数据存储在 Parse `SizeChartKnowledge` 表;无数据时自动降级到内置默认表。 * * 典型用法: * constructor(private sizeChartService: SizeChartService) {} * this.sizeChartService.getCharts().subscribe(charts => ...); * this.sizeChartService.saveChart(chart).subscribe(saved => ...); */ @Injectable({ providedIn: 'root' }) export class SizeChartService { private cache$ = new BehaviorSubject([]); constructor(private dataStore: DataStoreService) {} // ── 订阅缓存(实时更新)───────────────────────────── get charts$(): Observable { return this.cache$.asObservable(); } // ── 查询全部尺码表(加载后自动写入缓存)───────────── getCharts(): Observable { return this.dataStore.query(PARSE_CLASS, {}, { limit: 100 }).pipe( map(rows => rows.length ? rows.map(r => this.fromRow(r)) : [DEFAULT_SIZE_CHART]), tap(charts => this.cache$.next(charts)), catchError(() => { const fallback = [DEFAULT_SIZE_CHART]; this.cache$.next(fallback); return of(fallback); }) ); } // ── 取单张表 ────────────────────────────────────── getChart(id: string): Observable { return this.dataStore.query(PARSE_CLASS, { objectId: id }, { limit: 1 }).pipe( map(rows => rows.length ? this.fromRow(rows[0]) : null), catchError(() => of(null)) ); } // ── 新建或更新 ──────────────────────────────────── saveChart(chart: SizeChart): Observable { const payload: Record = { name: chart.name, category: chart.category, unit: chart.unit, note: chart.note, rows: JSON.stringify(chart.rows), tips: JSON.stringify(chart.tips) }; if (chart.id) payload['objectId'] = chart.id; return this.dataStore.save(PARSE_CLASS, payload, chart.id ? 'objectId' : undefined).pipe( map((saved: any) => { const result: SizeChart = { ...chart, id: saved?.objectId ?? chart.id }; this.updateCache(result); return result; }), catchError(() => of(chart)) ); } // ── 删除 ───────────────────────────────────────── deleteChart(id: string): Observable { return this.dataStore.delete(PARSE_CLASS, id).pipe( tap(() => this.cache$.next(this.cache$.value.filter(c => c.id !== id))), map(() => void 0), catchError(() => of(void 0)) ); } // ── CSV 导入(返回解析后的行数组,由调用方决定写入哪张表)─ parseCsv(csvText: string): SizeRow[] { const lines = csvText.trim().split('\n').filter(l => l.trim()); if (lines.length < 2) return []; const headers = lines[0].split(',').map(h => h.trim().toLowerCase()); return lines.slice(1).map(line => { const cols = line.split(',').map(c => c.trim().replace(/^"|"$/g, '')); const cell = (key: string[]) => { for (const k of key) { const i = headers.indexOf(k); if (i >= 0 && cols[i]) return cols[i]; } return ''; }; return { sizeName: cell(['size', 'sizename', 'size_name', '尺码']), usaSize: cell(['usa', 'usasize', 'usa_size', 'usa码']), bust: cell(['bust', '胸围']), waist: cell(['waist', '腰围']), hip: cell(['hip', '臀围']), torso: cell(['torso', '躯干']), cup: cell(['cup', '罩杯']) }; }).filter(r => r.sizeName); } // ── 获取默认表(其他页面可直接取同步引用)─────────── getDefaultChart(): SizeChart { return { ...DEFAULT_SIZE_CHART }; } // ── 工具:生成空行 ──────────────────────────────── newEmptyRow(): SizeRow { return { sizeName: '', usaSize: '', bust: '', waist: '', hip: '', torso: '', cup: '' }; } // ── 深克隆一张表(用于编辑前创建副本)──────────────── cloneChart(chart: SizeChart): SizeChart { return JSON.parse(JSON.stringify(chart)); } // ── 生成 AI 可读的尺码知识库上下文块 ──────────────── /** * 将尺码表数组序列化为 Markdown 格式的知识块,可直接嵌入 AI Prompt。 * 如果传入空数组,自动降级到内置默认表。 */ formatForAI(charts: SizeChart[]): string { const list = charts.length ? charts : [DEFAULT_SIZE_CHART]; const sections = list.map(chart => { const unit = chart.unit || 'in'; const header = `### 【${chart.name}】(单位:${unit},品类:${chart.category || '通用'})`; const note = chart.note ? `> ${chart.note}\n` : ''; // 表格 const tableHeader = `| 尺码 | 美码 | 胸围 | 腰围 | 臀围 | 躯干长 | 罩杯 |`; const tableSep = `|------|------|------|------|------|--------|------|`; const tableRows = (chart.rows || []).map(r => `| ${r.sizeName} | ${r.usaSize} | ${r.bust} | ${r.waist} | ${r.hip} | ${r.torso} | ${r.cup} |` ).join('\n'); // 使用提示 const tips = (chart.tips || []).map(t => `- ${t.type === 'warn' ? '⚠️' : 'ℹ️'} ${t.text}` ).join('\n'); return [header, note, tableHeader, tableSep, tableRows, tips ? '\n**使用提示:**\n' + tips : ''] .filter(s => s.trim()).join('\n'); }); return [ '---', '## 📐 尺码知识库(请在涉及尺码、版型、退货相关优化时优先参照此标准)', ...sections, '---' ].join('\n\n'); } // ── 内部 ───────────────────────────────────────── private fromRow(r: any): SizeChart { return { id: r.objectId, name: r.name ?? '', category: r.category ?? '', unit: r.unit ?? 'in', note: r.note ?? '', rows: this.safeJson(r.rows, []), tips: this.safeJson(r.tips, []) }; } private safeJson(val: any, fallback: any): any { if (!val) return fallback; if (typeof val === 'object') return val; try { return JSON.parse(val); } catch { return fallback; } } private updateCache(chart: SizeChart): void { const list = [...this.cache$.value]; const idx = list.findIndex(c => c.id === chart.id); idx >= 0 ? (list[idx] = chart) : list.push(chart); this.cache$.next(list); } }