|
@@ -6,7 +6,7 @@ import { LIBRARY_CSS } from './library-styles.js';
|
|
|
import './library-product-detail.js';
|
|
import './library-product-detail.js';
|
|
|
import { href, navigate } from '../utils/router.js';
|
|
import { href, navigate } from '../utils/router.js';
|
|
|
import { readLibrary } from '../services/library-service.js';
|
|
import { readLibrary } from '../services/library-service.js';
|
|
|
-import { LIBRARY_ENTITIES, LIBRARY_GROUPS, LIBRARY_FILTERS, FACET_COLUMNS, LIBRARY_FIELDS, libraryLabel, libraryRequest, libraryPath, libraryDate, libraryRecordInfo, libraryCatalogQuery } from '../core/library.js';
|
|
|
|
|
|
|
+import { LIBRARY_ENTITIES, LIBRARY_GROUPS, LIBRARY_FILTERS, FACET_COLUMNS, LIBRARY_FIELDS, PRODUCT_SORTS, libraryLabel, libraryRequest, libraryPath, libraryDate, libraryRecordInfo, libraryCatalogQuery } from '../core/library.js';
|
|
|
|
|
|
|
|
const number = n => Number(n || 0).toLocaleString('zh-CN');
|
|
const number = n => Number(n || 0).toLocaleString('zh-CN');
|
|
|
const safeUrl = value => {
|
|
const safeUrl = value => {
|
|
@@ -38,7 +38,7 @@ export class ViewLibrary extends LitElement {
|
|
|
db: { type: Object }, routeQuery: { type: Object }, recordId: { type: String }, recordEntity: { type: String },
|
|
db: { type: Object }, routeQuery: { type: Object }, recordId: { type: String }, recordEntity: { type: String },
|
|
|
_summary: { state: true }, _data: { state: true }, _detail: { state: true }, _loading: { state: true },
|
|
_summary: { state: true }, _data: { state: true }, _detail: { state: true }, _loading: { state: true },
|
|
|
_error: { state: true }, _notice: { state: true },
|
|
_error: { state: true }, _notice: { state: true },
|
|
|
- _advanced: { state: true }, _insights: { state: true }, _layout: { state: true },
|
|
|
|
|
|
|
+ _insights: { state: true }, _layout: { state: true },
|
|
|
_overview: { state: true }, _factsExpanded: { state: true },
|
|
_overview: { state: true }, _factsExpanded: { state: true },
|
|
|
};
|
|
};
|
|
|
constructor() {
|
|
constructor() {
|
|
@@ -46,7 +46,7 @@ export class ViewLibrary extends LitElement {
|
|
|
this.db = null; this.routeQuery = {}; this.recordId = ''; this.recordEntity = '';
|
|
this.db = null; this.routeQuery = {}; this.recordId = ''; this.recordEntity = '';
|
|
|
this._summary = null; this._data = null; this._detail = null; this._loading = true;
|
|
this._summary = null; this._data = null; this._detail = null; this._loading = true;
|
|
|
this._error = ''; this._notice = ''; this._ticket = 0;
|
|
this._error = ''; this._notice = ''; this._ticket = 0;
|
|
|
- this._advanced = false; this._insights = false; this._layout = 'cards';
|
|
|
|
|
|
|
+ this._insights = false; this._layout = 'cards';
|
|
|
this._overview = false; this._factsExpanded = false;
|
|
this._overview = false; this._factsExpanded = false;
|
|
|
this._previousCursors = new Map();
|
|
this._previousCursors = new Map();
|
|
|
}
|
|
}
|
|
@@ -57,7 +57,7 @@ export class ViewLibrary extends LitElement {
|
|
|
const sameProduct = this.recordEntity === 'products' && this._detail?.record.id === this.recordId
|
|
const sameProduct = this.recordEntity === 'products' && this._detail?.record.id === this.recordId
|
|
|
&& !changed.has('recordId') && !changed.has('recordEntity')
|
|
&& !changed.has('recordId') && !changed.has('recordEntity')
|
|
|
&& JSON.stringify(libraryCatalogQuery(changed.get('routeQuery') || {})) === JSON.stringify(libraryCatalogQuery(this.routeQuery));
|
|
&& JSON.stringify(libraryCatalogQuery(changed.get('routeQuery') || {})) === JSON.stringify(libraryCatalogQuery(this.routeQuery));
|
|
|
- if (!sameProduct || ['q', 'relatedQ'].some(key => Object.hasOwn(this.routeQuery, key))) this._load();
|
|
|
|
|
|
|
+ if (!sameProduct || ['q', 'relatedQ', 'reviewCount', 'price'].some(key => Object.hasOwn(this.routeQuery, key))) this._load();
|
|
|
}
|
|
}
|
|
|
if (changed.has('_loading') && !this._loading && this._scrollResults) {
|
|
if (changed.has('_loading') && !this._loading && this._scrollResults) {
|
|
|
this._scrollResults = false;
|
|
this._scrollResults = false;
|
|
@@ -78,12 +78,12 @@ export class ViewLibrary extends LitElement {
|
|
|
const ticket = ++this._ticket;
|
|
const ticket = ++this._ticket;
|
|
|
this._loading = true; this._error = ''; this._detail = null; this._data = null;
|
|
this._loading = true; this._error = ''; this._detail = null; this._data = null;
|
|
|
try {
|
|
try {
|
|
|
- // Saved search links become filter-only links; their cursors belong to a different query.
|
|
|
|
|
- if (['q', 'relatedQ'].some(key => Object.hasOwn(this.routeQuery, key))) {
|
|
|
|
|
|
|
+ // Remove retired keyword and interval controls, including their old cursors.
|
|
|
|
|
+ if (['q', 'relatedQ', 'reviewCount', 'price'].some(key => Object.hasOwn(this.routeQuery, key))) {
|
|
|
const query = { ...this.routeQuery };
|
|
const query = { ...this.routeQuery };
|
|
|
- if (Object.hasOwn(query, 'q')) delete query.cursor;
|
|
|
|
|
|
|
+ if (['q', 'reviewCount', 'price'].some(key => Object.hasOwn(query, key))) delete query.cursor;
|
|
|
if (Object.hasOwn(query, 'relatedQ')) delete query.relatedCursor;
|
|
if (Object.hasOwn(query, 'relatedQ')) delete query.relatedCursor;
|
|
|
- delete query.q; delete query.relatedQ;
|
|
|
|
|
|
|
+ delete query.q; delete query.relatedQ; delete query.reviewCount; delete query.price;
|
|
|
navigate(libraryPath(query, this.recordId ? { entity: this.recordEntity, id: this.recordId } : null), { replace: true });
|
|
navigate(libraryPath(query, this.recordId ? { entity: this.recordEntity, id: this.recordId } : null), { replace: true });
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
@@ -198,25 +198,21 @@ export class ViewLibrary extends LitElement {
|
|
|
}
|
|
}
|
|
|
_filter() {
|
|
_filter() {
|
|
|
const d = this._data, q = this.routeQuery;
|
|
const d = this._data, q = this.routeQuery;
|
|
|
- const primary = ['platform', 'brand', 'category', 'policyStatus'].filter(k => d.facets[k]);
|
|
|
|
|
- const more = Object.keys(d.facets).filter(k => !primary.includes(k) && k !== 'specVerification');
|
|
|
|
|
- const advancedCount = more.filter(k => q[k]).length + (q.from ? 1 : 0) + (q.to ? 1 : 0);
|
|
|
|
|
- return html`<section class="panel filter" aria-label="资料筛选">
|
|
|
|
|
|
|
+ const primary = ['platform', 'brand', 'category', 'policyStatus', 'contentScope'].filter(k => d.facets[k]);
|
|
|
|
|
+ return html`<section class=${this._type === 'products' ? 'panel filter product-filters' : 'panel filter'} aria-label="资料筛选">
|
|
|
<div class="filter-main"><div class="filter-grid primary-filters">
|
|
<div class="filter-main"><div class="filter-grid primary-filters">
|
|
|
${primary.map(k => this._facet(k, d.facets[k]))}
|
|
${primary.map(k => this._facet(k, d.facets[k]))}
|
|
|
- <label class="field">查看口径<select aria-label="查看口径" .value=${q.quality || 'analysis'} @change=${e => this._patch({ quality: e.target.value })}>
|
|
|
|
|
|
|
+ ${this._type === 'products' ? html`
|
|
|
|
|
+ <label class="field">评论数量<select aria-label="评论数量" title="按已收录评论数量排序" .value=${q.sort?.startsWith('reviews_') ? q.sort : ''} @change=${e => this._patch({ sort: e.target.value })}>
|
|
|
|
|
+ <option value="">默认排序</option>${['reviews_desc', 'reviews_asc'].map(value => html`<option value=${value} ?selected=${q.sort === value}>${PRODUCT_SORTS[value]}</option>`)}
|
|
|
|
|
+ </select></label>
|
|
|
|
|
+ <label class="field">价格<select aria-label="价格" title="按最低历史观察价排序,暂无价格的商品排在最后" .value=${q.sort?.startsWith('price_') ? q.sort : ''} @change=${e => this._patch({ sort: e.target.value })}>
|
|
|
|
|
+ <option value="">默认排序</option>${['price_asc', 'price_desc'].map(value => html`<option value=${value} ?selected=${q.sort === value}>${PRODUCT_SORTS[value]}</option>`)}
|
|
|
|
|
+ </select></label>` : ''}
|
|
|
|
|
+ ${this._type !== 'products' ? html`<label class="field">查看口径<select aria-label="查看口径" .value=${q.quality || 'analysis'} @change=${e => this._patch({ quality: e.target.value })}>
|
|
|
<option value="analysis">推荐用于分析的资料</option><option value="audit">全部资料(含待确认)</option>
|
|
<option value="analysis">推荐用于分析的资料</option><option value="audit">全部资料(含待确认)</option>
|
|
|
- </select></label>
|
|
|
|
|
- </div>
|
|
|
|
|
- <button class=${this._advanced ? 'text-button advanced-toggle active' : 'text-button advanced-toggle'} aria-expanded=${this._advanced} aria-controls="advanced-filters" @click=${() => this._advanced = !this._advanced}>
|
|
|
|
|
- ${icon('filter', 14)}${this._advanced ? '收起筛选' : '更多筛选'}${advancedCount ? html`<b>${advancedCount}</b>` : ''}${icon(this._advanced ? 'up' : 'down', 11)}
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div id="advanced-filters" class="advanced-filters filter-grid" ?hidden=${!this._advanced}>
|
|
|
|
|
- ${more.map(k => this._facet(k, d.facets[k]))}
|
|
|
|
|
- ${this._type !== 'coverage' ? html`<label class="field">${this._type === 'reviews' ? '评价' : this._type === 'social' ? '发表' : '来源'}日期起<input aria-label="日期起" type="date" .value=${q.from || ''} @change=${e => this._patch({ from: e.target.value })}></label>
|
|
|
|
|
- <label class="field">日期止<input aria-label="日期止" type="date" .value=${q.to || ''} @change=${e => this._patch({ to: e.target.value })}></label>` : ''}
|
|
|
|
|
- </div>${this._chips()}
|
|
|
|
|
|
|
+ </select></label>` : ''}
|
|
|
|
|
+ </div></div>${this._chips()}
|
|
|
</section>`;
|
|
</section>`;
|
|
|
}
|
|
}
|
|
|
_distribution() {
|
|
_distribution() {
|
|
@@ -244,7 +240,7 @@ export class ViewLibrary extends LitElement {
|
|
|
else if (r.entity === 'coverage') preview = preview ? this._label(preview) : '';
|
|
else if (r.entity === 'coverage') preview = preview ? this._label(preview) : '';
|
|
|
else if (r.entity === 'documents') preview = preview.replace(/--- PDF page \d+ ---/g, '').replace(/\s+/g, ' ').trim();
|
|
else if (r.entity === 'documents') preview = preview.replace(/--- PDF page \d+ ---/g, '').replace(/\s+/g, ' ').trim();
|
|
|
const recordDate = r.review_day ? `评价 ${r.review_day}` : r.published_day ? `发表 ${r.published_day}` : r.source_day ? `观察 ${r.source_day}` : r.policyStatusAsOf ? `状态截至 ${r.policyStatusAsOf}` : '来源日期未知';
|
|
const recordDate = r.review_day ? `评价 ${r.review_day}` : r.published_day ? `发表 ${r.published_day}` : r.source_day ? `观察 ${r.source_day}` : r.policyStatusAsOf ? `状态截至 ${r.policyStatusAsOf}` : '来源日期未知';
|
|
|
- return html`<a class="record" href=${href(libraryPath(this.routeQuery, { entity: r.entity, id: r.record_id }))} data-link data-record-id=${r.record_id}>
|
|
|
|
|
|
|
+ return html`<a class=${r.entity === 'products' ? 'record product-record' : 'record'} href=${href(libraryPath(this.routeQuery, { entity: r.entity, id: r.record_id }))} data-link data-record-id=${r.record_id}>
|
|
|
<div class="record-symbol" aria-hidden="true">${icon(TYPE_INFO[r.entity]?.[0] || 'doc', 18)}</div>
|
|
<div class="record-symbol" aria-hidden="true">${icon(TYPE_INFO[r.entity]?.[0] || 'doc', 18)}</div>
|
|
|
<div class="record-main"><div class="tags">
|
|
<div class="record-main"><div class="tags">
|
|
|
${r.platform || r.policy_status ? html`<span class="platform-tag">${this._label(r.platform || r.policy_status)}</span>` : ''}
|
|
${r.platform || r.policy_status ? html`<span class="platform-tag">${this._label(r.platform || r.policy_status)}</span>` : ''}
|
|
@@ -258,7 +254,9 @@ export class ViewLibrary extends LitElement {
|
|
|
${r.net_weight_g == null && r.pack_count == null ? html`<span>暂无详细规格</span>` : ''}
|
|
${r.net_weight_g == null && r.pack_count == null ? html`<span>暂无详细规格</span>` : ''}
|
|
|
</div>` : ''}
|
|
</div>` : ''}
|
|
|
${preview ? html`<p>${preview}</p>` : this._type === 'social' && !r.fulltext_available ? html`<p class="muted">仅有标题,暂无正文</p>` : ''}
|
|
${preview ? html`<p>${preview}</p>` : this._type === 'social' && !r.fulltext_available ? html`<p class="muted">仅有标题,暂无正文</p>` : ''}
|
|
|
- ${r.entity === 'products' ? html`<div class="product-summary"><span>${icon('chat', 11)}评价 ${number(r.relatedCounts?.reviews)}</span><span>${icon('bars', 11)}价格 ${number(r.relatedCounts?.prices)}</span><span>${icon('table', 11)}规格 ${number(r.relatedCounts?.specs)}</span></div>` : ''}
|
|
|
|
|
|
|
+ ${r.entity === 'products' ? html`
|
|
|
|
|
+ <div class="product-price">${r.priceRange ? html`<strong>¥${(r.priceRange.minMinor / 100).toFixed(2)}${r.priceRange.maxMinor !== r.priceRange.minMinor ? `–${(r.priceRange.maxMinor / 100).toFixed(2)}` : ''}</strong><span>历史观察价</span>` : html`<span>暂无价格</span>`}</div>
|
|
|
|
|
+ <div class="product-summary"><span title="已收录评价,含默认及待确认记录">${icon('chat', 11)}${r.relatedCounts?.reviews ? `已收录评价 ${number(r.relatedCounts.reviews)}` : '暂未收录评价'}</span><span>${icon('table', 11)}规格 ${number(r.relatedCounts?.specs)}</span></div>` : ''}
|
|
|
<div class="record-meta"><span>${icon('clock', 11)}${recordDate}</span><span>查看详情 ${icon('right', 11)}</span></div>
|
|
<div class="record-meta"><span>${icon('clock', 11)}${recordDate}</span><span>查看详情 ${icon('right', 11)}</span></div>
|
|
|
</div><span class="open-arrow">${icon('right', 14)}</span>
|
|
</div><span class="open-arrow">${icon('right', 14)}</span>
|
|
|
</a>`;
|
|
</a>`;
|
|
@@ -307,8 +305,11 @@ export class ViewLibrary extends LitElement {
|
|
|
const d = this._detail;
|
|
const d = this._detail;
|
|
|
if (!d) return '';
|
|
if (!d) return '';
|
|
|
const r = d.record, prose = ['content', 'full_text', 'excerpt', 'application_note'].filter(k => r[k]);
|
|
const r = d.record, prose = ['content', 'full_text', 'excerpt', 'application_note'].filter(k => r[k]);
|
|
|
- return html`<div class="detail-page"><div class="breadcrumb"><a href=${href(libraryPath(libraryCatalogQuery(this.routeQuery)))} data-link>${icon('left', 14)}返回筛选结果</a><span>${LIBRARY_ENTITIES[this.recordEntity]}</span></div>
|
|
|
|
|
- <section class="panel detail-title"><p class="eyebrow">${icon(TYPE_INFO[this.recordEntity]?.[0] || 'doc', 14)}${LIBRARY_ENTITIES[this.recordEntity]}</p>
|
|
|
|
|
|
|
+ const product = this.recordEntity === 'products';
|
|
|
|
|
+ return html`<div class=${product ? 'detail-page product-detail' : 'detail-page'}><div class="breadcrumb"><a href=${href(libraryPath(libraryCatalogQuery(this.routeQuery)))} data-link>${icon('left', 14)}返回筛选结果</a>
|
|
|
|
|
+ ${product ? html`<button class="text-button refresh" ?disabled=${this._loading} @click=${() => this._load(true)}>${icon('clock', 12)}刷新资料</button>` : html`<span>${LIBRARY_ENTITIES[this.recordEntity]}</span>`}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <section class="panel detail-title">${!product ? html`<p class="eyebrow">${icon(TYPE_INFO[this.recordEntity]?.[0] || 'doc', 14)}${LIBRARY_ENTITIES[this.recordEntity]}</p>` : ''}
|
|
|
<h2>${r.title || r.parentDocument?.title || r.parentProduct?.title || r.clause || LIBRARY_ENTITIES[this.recordEntity]}</h2>
|
|
<h2>${r.title || r.parentDocument?.title || r.parentProduct?.title || r.clause || LIBRARY_ENTITIES[this.recordEntity]}</h2>
|
|
|
<div class="detail-actions"><button class="button primary" @click=${() => this._copy(r)}>${icon('quote', 14)}复制资料引用</button>
|
|
<div class="detail-actions"><button class="button primary" @click=${() => this._copy(r)}>${icon('quote', 14)}复制资料引用</button>
|
|
|
${this._originalLink(r) ? html`<a class="button" href=${this._originalLink(r)} target="_blank" rel="noopener noreferrer">原始链接 ${icon('link', 14)}</a>` : ''}
|
|
${this._originalLink(r) ? html`<a class="button" href=${this._originalLink(r)} target="_blank" rel="noopener noreferrer">原始链接 ${icon('link', 14)}</a>` : ''}
|
|
@@ -356,7 +357,7 @@ export class ViewLibrary extends LitElement {
|
|
|
}
|
|
}
|
|
|
_skeleton() { return html`<section class="panel loading" aria-busy="true" role="status"><span class="loading-dot"></span><span>${this.recordId ? '正在读取资料原文与来源…' : '正在读取资料…'}</span></section>`; }
|
|
_skeleton() { return html`<section class="panel loading" aria-busy="true" role="status"><span class="loading-dot"></span><span>${this.recordId ? '正在读取资料原文与来源…' : '正在读取资料…'}</span></section>`; }
|
|
|
render() {
|
|
render() {
|
|
|
- return html`${head('行业资料库', '', html`${!this.recordId ? html`<button class="button overview-toggle" aria-expanded=${this._overview} aria-controls="library-overview" @click=${() => this._overview = !this._overview}>${icon('bars', 14)}资料概览${icon(this._overview ? 'up' : 'down', 11)}</button>` : ''}<button class="button refresh" ?disabled=${this._loading} @click=${() => this._load(true)}>${icon('clock', 14)}刷新资料</button>`)}
|
|
|
|
|
|
|
+ return html`${this.recordId && this.recordEntity === 'products' ? '' : head('行业资料库', '', html`${!this.recordId ? html`<button class="button overview-toggle" aria-expanded=${this._overview} aria-controls="library-overview" @click=${() => this._overview = !this._overview}>${icon('bars', 14)}资料概览${icon(this._overview ? 'up' : 'down', 11)}</button>` : ''}<button class="button refresh" ?disabled=${this._loading} @click=${() => this._load(true)}>${icon('clock', 14)}刷新资料</button>`)}
|
|
|
${this._notice ? html`<div class="notice" role="status"><span>${icon('check', 14)}${this._notice}</span><button class="text-button" aria-label="关闭提示" @click=${() => this._notice = ''}>${icon('x', 14)}</button></div>` : ''}
|
|
${this._notice ? html`<div class="notice" role="status"><span>${icon('check', 14)}${this._notice}</span><button class="text-button" aria-label="关闭提示" @click=${() => this._notice = ''}>${icon('x', 14)}</button></div>` : ''}
|
|
|
${this._error ? html`<div class="panel error" role="alert">${icon('alert', 22)}<div><h3>${this._error}</h3><p>资料暂时无法读取,请稍后重试。</p><button class="button" @click=${() => this._load(true)}>重试</button></div></div>` : ''}
|
|
${this._error ? html`<div class="panel error" role="alert">${icon('alert', 22)}<div><h3>${this._error}</h3><p>资料暂时无法读取,请稍后重试。</p><button class="button" @click=${() => this._load(true)}>重试</button></div></div>` : ''}
|
|
|
${this.recordId ? (this._loading ? this._skeleton() : this._detailView()) : html`${this._stats()}${this._tree()}<div class="library-layout"><div class="content">
|
|
${this.recordId ? (this._loading ? this._skeleton() : this._detailView()) : html`${this._stats()}${this._tree()}<div class="library-layout"><div class="content">
|