| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235 |
- 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<SizeChart[]>([]);
- constructor(private dataStore: DataStoreService) {}
- // ── 订阅缓存(实时更新)─────────────────────────────
- get charts$(): Observable<SizeChart[]> {
- return this.cache$.asObservable();
- }
- // ── 查询全部尺码表(加载后自动写入缓存)─────────────
- getCharts(): Observable<SizeChart[]> {
- 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<SizeChart | null> {
- 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<SizeChart> {
- const payload: Record<string, any> = {
- 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<void> {
- 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);
- }
- }
|