| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564 |
- <main class="data-center" *ngIf="vm$ | async as vm">
- <app-page-header
- title="数据中台"
- eyebrow="数据资产"
- [crumbs]="crumbs">
- </app-page-header>
- <app-page-toolbar [fields]="toolbarFields(vm.dataset)">
- <div pageToolbarActions class="command-actions">
- <button class="icon-button" type="button" title="刷新数据" aria-label="刷新数据" [disabled]="refreshing" (click)="refreshAll()">
- <lucide-icon [img]="icons.refresh" [size]="17" [class.spinning]="refreshing"></lucide-icon>
- </button>
- <button class="secondary-button" type="button" (click)="configureSource()">
- <lucide-icon [img]="icons.settings" [size]="16"></lucide-icon>
- 接入配置
- </button>
- <button class="primary-button" type="button" (click)="openSync(vm.dataset)">
- <lucide-icon [img]="icons.play" [size]="16"></lucide-icon>
- 新建同步
- </button>
- </div>
- </app-page-toolbar>
- <section class="health-band" [class.health-green]="vm.statusTone === 'green'" [class.health-amber]="vm.statusTone === 'amber'" [class.health-red]="vm.statusTone === 'red'">
- <div class="health-main">
- <lucide-icon [img]="vm.statusTone === 'green' ? icons.check : icons.alert" [size]="20"></lucide-icon>
- <div>
- <strong>{{ vm.statusTitle }}</strong>
- <p>{{ vm.statusMessage }}</p>
- </div>
- </div>
- <div class="health-facts">
- <span><b>{{ vm.readyDomains }}</b>/{{ vm.coverage.length }} 业务域就绪</span>
- <span><b>{{ vm.qualityFailed }}</b> 项阻断</span>
- <span><b>{{ vm.activeJobs }}</b> 个运行任务</span>
- <span>快照 {{ vm.sourceAgeLabel }}</span>
- </div>
- </section>
- <div class="warning-row" *ngIf="vm.warnings.length">
- <lucide-icon [img]="icons.alert" [size]="16"></lucide-icon>
- <span>{{ vm.warnings.join(';') }},当前保留可用数据并显示接口异常。</span>
- </div>
- <div class="warning-row" *ngIf="actionError">
- <lucide-icon [img]="icons.failed" [size]="16"></lucide-icon>
- <span>{{ actionError }}</span>
- </div>
- <nav class="view-tabs" role="tablist" aria-label="数据中台视图">
- <button
- *ngFor="let tab of tabs"
- type="button"
- role="tab"
- [attr.aria-selected]="activeView === tab.id"
- [class.active]="activeView === tab.id"
- (click)="setView(tab.id)"
- >
- {{ tab.label }}
- </button>
- </nav>
- <section class="view-content" *ngIf="activeView === 'overview'">
- <div class="metric-strip" aria-label="数据总览指标">
- <div class="metric-item">
- <span>平台商品</span>
- <strong>{{ number(vm.ownProducts + vm.competitorProducts) }}</strong>
- <small>本品 {{ number(vm.ownProducts) }} · 竞品 {{ number(vm.competitorProducts) }}</small>
- </div>
- <div class="metric-item">
- <span>商品日明细</span>
- <strong>{{ number(vm.dataset.summary.metricRows) }}</strong>
- <small>{{ vm.dateDays }} 天经营周期</small>
- </div>
- <div class="metric-item metric-risk">
- <span>评价证据</span>
- <strong>{{ number(vm.ownReviews + vm.competitorReviews) }}</strong>
- <small>本品 {{ number(vm.ownReviews) }} · 竞品 {{ number(vm.competitorReviews) }}</small>
- </div>
- <div class="metric-item">
- <span>最近导入成功率</span>
- <strong>{{ vm.latestImport ? percent(vm.importSuccessRate) : '--' }}</strong>
- <small>{{ vm.latestImport ? number(vm.latestImport.successRows) + ' / ' + number(vm.latestImport.totalRows) + ' 行' : '暂无导入记录' }}</small>
- </div>
- </div>
- <section class="workspace-section">
- <div class="section-heading">
- <div>
- <h2>国内电商业务对象覆盖</h2>
- <p>按业务对象与数据粒度审视能力,聚合指标不替代订单、售后或履约明细。</p>
- </div>
- <span class="section-count">{{ vm.readyDomains }}/{{ vm.coverage.length }} 已就绪</span>
- </div>
- <div class="table-scroll">
- <table class="data-table coverage-table">
- <thead>
- <tr>
- <th>业务域</th>
- <th>核心对象</th>
- <th>标准粒度</th>
- <th>接入状态</th>
- <th>覆盖率</th>
- <th>当前证据</th>
- <th>下一缺口</th>
- </tr>
- </thead>
- <tbody>
- <tr *ngFor="let domain of vm.coverage">
- <td><strong>{{ domain.label }}</strong></td>
- <td>{{ domain.objectLabel }}</td>
- <td><code>{{ domain.grain }}</code></td>
- <td><span class="status-tag" [class]="'status-tag ' + domain.state">{{ coverageStateLabel(domain.state) }}</span></td>
- <td>
- <div class="coverage-cell">
- <span class="coverage-track" aria-hidden="true"><i [style.width.%]="domain.coverage * 100"></i></span>
- <b>{{ percent(domain.coverage) }}</b>
- </div>
- </td>
- <td>{{ domain.evidence }}</td>
- <td class="gap-cell">{{ domain.gap }}</td>
- </tr>
- </tbody>
- </table>
- </div>
- </section>
- <div class="overview-lower">
- <section class="workspace-section priority-section">
- <div class="section-heading compact-heading">
- <div>
- <h2>治理优先队列</h2>
- <p>先处理阻断洞察可信度的缺口,再扩展新业务域。</p>
- </div>
- <button class="text-button" type="button" (click)="setView('quality')">查看全部规则</button>
- </div>
- <div class="priority-list">
- <div class="priority-row" *ngFor="let rule of vm.rules">
- <span class="severity-mark" [class]="rule.state"></span>
- <div class="priority-copy">
- <strong>{{ rule.name }}</strong>
- <span>{{ rule.result }} · 门槛 {{ rule.threshold }}</span>
- </div>
- <span class="affected">影响 {{ number(rule.affected) }}</span>
- <button class="row-action" *ngIf="rule.action" type="button" (click)="runRuleAction(rule)">处理</button>
- </div>
- </div>
- </section>
- <section class="workspace-section run-section">
- <div class="section-heading compact-heading">
- <div>
- <h2>最近运行</h2>
- <p>数据源、导入与同步任务的最新状态。</p>
- </div>
- <button class="text-button" type="button" (click)="setView('sources')">进入运行中心</button>
- </div>
- <dl class="run-facts">
- <div>
- <dt>数据源</dt>
- <dd>{{ vm.sources.length ? sourceStatusLabel(vm.sources[0].status) : '未读取' }}</dd>
- <span>{{ vm.sources.length ? sourceKindLabel(vm.sources[0].kind) : '等待接入配置' }}</span>
- </div>
- <div>
- <dt>标准化导入</dt>
- <dd>{{ vm.latestImport ? importStatusLabel(vm.latestImport.status) : '无记录' }}</dd>
- <span>{{ vm.latestImport ? dateTime(vm.latestImport.completedAt) : '尚未完成导入' }}</span>
- </div>
- <div>
- <dt>同步队列</dt>
- <dd>{{ vm.jobs.length }} 个任务</dd>
- <span>{{ vm.activeJobs ? vm.activeJobs + ' 个正在运行' : '当前队列空闲' }}</span>
- </div>
- <div>
- <dt>质量门禁</dt>
- <dd>{{ vm.qualityPassed }} 通过 / {{ vm.qualityFailed }} 阻断</dd>
- <span>{{ vm.qualityWarnings }} 项需要关注</span>
- </div>
- </dl>
- </section>
- </div>
- </section>
- <section class="view-content" *ngIf="activeView === 'sources'">
- <section class="workspace-section">
- <div class="section-heading">
- <div>
- <h2>数据源</h2>
- <p>统一管理平台连接、凭证托管方式和最近检查时间。</p>
- </div>
- <button class="secondary-button compact" type="button" (click)="configureSource()">
- <lucide-icon [img]="icons.plug" [size]="15"></lucide-icon>
- 管理接入
- </button>
- </div>
- <div class="table-scroll">
- <table class="data-table source-table">
- <thead><tr><th>数据源</th><th>平台</th><th>连接状态</th><th>凭证托管</th><th>最近检查</th><th>操作</th></tr></thead>
- <tbody>
- <tr class="interactive-row" *ngFor="let source of vm.sources" tabindex="0" (click)="openSourceDetails(source)" (keydown.enter)="openSourceDetails(source)">
- <td><strong>{{ sourceKindLabel(source.kind) }}</strong><small class="cell-meta">{{ source.id }}</small></td>
- <td>{{ source.platform | uppercase }}</td>
- <td><span class="status-tag" [class.ready]="source.status === 'active' || source.status === 'configured'" [class.missing]="source.status === 'error'">{{ sourceStatusLabel(source.status) }}</span></td>
- <td>{{ credentialStorageLabel(source.credentialStorage) }}</td>
- <td>{{ dateTime(source.lastCheckedAt) }}</td>
- <td><button class="row-action" type="button" (click)="$event.stopPropagation(); openSourceDetails(source)">查看详情</button></td>
- </tr>
- <tr *ngIf="!vm.sources.length"><td colspan="6" class="empty-cell">尚未读取到数据源。</td></tr>
- </tbody>
- </table>
- </div>
- </section>
- <section class="workspace-section">
- <div class="section-heading">
- <div>
- <h2>同步任务</h2>
- <p>按平台商品 ID 拉取商品与评价,失败任务可重试,待执行任务可取消。</p>
- </div>
- <div class="section-tools">
- <select [(ngModel)]="jobStatus" aria-label="筛选同步任务状态">
- <option *ngFor="let status of jobStatuses" [ngValue]="status.value">{{ status.label }}</option>
- </select>
- <button class="primary-button compact" type="button" (click)="openSync(vm.dataset)">
- <lucide-icon [img]="icons.play" [size]="15"></lucide-icon>
- 新建同步
- </button>
- </div>
- </div>
- <div class="table-scroll">
- <table class="data-table jobs-table">
- <thead><tr><th>任务</th><th>范围</th><th>商品数</th><th>状态</th><th>进度</th><th>请求时间</th><th>操作</th></tr></thead>
- <tbody>
- <tr class="interactive-row" *ngFor="let job of filteredJobs(vm.jobs)" tabindex="0" (click)="openJobDetails(job)" (keydown.enter)="openJobDetails(job)">
- <td><strong>{{ job.id }}</strong><small class="cell-meta">尝试 {{ job.attempts }}/{{ job.maxAttempts }}</small></td>
- <td>{{ job.scopes.join(' + ') }}</td>
- <td>{{ job.productIds.length }}</td>
- <td><span class="status-tag" [class.ready]="job.status === 'completed'" [class.partial]="job.status === 'partial' || job.status === 'pending' || job.status === 'processing'" [class.missing]="job.status === 'failed'">{{ jobStatusLabel(job.status) }}</span></td>
- <td>
- <div class="coverage-cell compact-progress">
- <span class="coverage-track"><i [style.width.%]="job.progress"></i></span>
- <b>{{ job.progress }}%</b>
- </div>
- <small class="error-summary" *ngIf="job.errorSummary">{{ job.errorSummary }}</small>
- </td>
- <td>{{ dateTime(job.requestedAt) }}</td>
- <td>
- <div class="row-actions">
- <button class="icon-button small" type="button" title="查看任务详情" aria-label="查看任务详情" (click)="$event.stopPropagation(); openJobDetails(job)">
- <lucide-icon [img]="icons.history" [size]="15"></lucide-icon>
- </button>
- <button class="icon-button small" *ngIf="job.status === 'failed' || job.status === 'partial'" type="button" title="重试任务" aria-label="重试任务" [disabled]="busyJobs.has(job.id)" (click)="$event.stopPropagation(); retryJob(job)">
- <lucide-icon [img]="icons.retry" [size]="15"></lucide-icon>
- </button>
- <button class="icon-button small danger" *ngIf="job.status === 'pending'" type="button" title="取消任务" aria-label="取消任务" [disabled]="busyJobs.has(job.id)" (click)="$event.stopPropagation(); cancelJob(job)">
- <lucide-icon [img]="icons.close" [size]="15"></lucide-icon>
- </button>
- </div>
- </td>
- </tr>
- <tr *ngIf="!filteredJobs(vm.jobs).length"><td colspan="7" class="empty-cell">当前筛选下没有同步任务。</td></tr>
- </tbody>
- </table>
- </div>
- </section>
- <section class="workspace-section">
- <div class="section-heading">
- <div>
- <h2>导入批次</h2>
- <p>记录标准化快照来源、行级处理结果和完成时间。</p>
- </div>
- </div>
- <div class="table-scroll">
- <table class="data-table">
- <thead><tr><th>批次</th><th>来源</th><th>文件</th><th>处理结果</th><th>状态</th><th>完成时间</th></tr></thead>
- <tbody>
- <tr *ngFor="let batch of vm.imports">
- <td><strong>{{ batch.id }}</strong></td>
- <td>{{ sourceKindLabel(batch.sourceKind) }}</td>
- <td>{{ batch.sourceFile || '--' }}</td>
- <td>{{ number(batch.successRows) }} 成功 / {{ number(batch.failedRows) }} 失败</td>
- <td><span class="status-tag" [class.ready]="batch.status === 'completed'" [class.partial]="batch.status === 'partial' || batch.status === 'processing'" [class.missing]="batch.status === 'failed'">{{ importStatusLabel(batch.status) }}</span></td>
- <td>{{ dateTime(batch.completedAt) }}</td>
- </tr>
- <tr *ngIf="!vm.imports.length"><td colspan="6" class="empty-cell">暂无导入批次。</td></tr>
- </tbody>
- </table>
- </div>
- </section>
- </section>
- <section class="view-content" *ngIf="activeView === 'quality'">
- <div class="quality-summary">
- <div><strong>{{ vm.qualityPassed }}</strong><span>通过</span></div>
- <div class="quality-warning"><strong>{{ vm.qualityWarnings }}</strong><span>关注</span></div>
- <div class="quality-failed"><strong>{{ vm.qualityFailed }}</strong><span>阻断</span></div>
- <p>阻断项直接影响洞察可信度;关注项表示数据可用,但粒度、覆盖或时效尚未达到运营标准。</p>
- </div>
- <section class="workspace-section">
- <div class="section-heading">
- <div>
- <h2>质量门禁</h2>
- <p>覆盖完整性、结构一致性、新鲜度和业务可追溯性。</p>
- </div>
- </div>
- <div class="table-scroll">
- <table class="data-table quality-table">
- <thead><tr><th>领域</th><th>规则</th><th>当前结果</th><th>门槛</th><th>影响对象</th><th>结论</th><th>操作</th></tr></thead>
- <tbody>
- <tr *ngFor="let rule of vm.rules">
- <td>{{ rule.domain }}</td>
- <td><strong>{{ rule.name }}</strong></td>
- <td>{{ rule.result }}</td>
- <td>{{ rule.threshold }}</td>
- <td>{{ number(rule.affected) }}</td>
- <td><span class="status-tag" [class]="'status-tag ' + rule.state">{{ ruleStateLabel(rule.state) }}</span></td>
- <td><button class="row-action" type="button" *ngIf="rule.action" (click)="runRuleAction(rule)">处理</button><span *ngIf="!rule.action">--</span></td>
- </tr>
- </tbody>
- </table>
- </div>
- </section>
- <section class="semantic-band">
- <div>
- <lucide-icon [img]="icons.layers" [size]="19"></lucide-icon>
- <strong>聚合指标</strong>
- <p>商品日 GMV、订单数、退款额用于趋势与经营对比,不能回溯单笔交易。</p>
- </div>
- <div>
- <lucide-icon [img]="icons.shopping" [size]="19"></lucide-icon>
- <strong>完整业务实体</strong>
- <p>订单、子单、支付、发货、签收、售后单、库存流水需要独立主键与状态历史。</p>
- </div>
- <div>
- <lucide-icon [img]="icons.star" [size]="19"></lucide-icon>
- <strong>VOC 证据</strong>
- <p>评价、追评、商家回复和售后原因应保留来源、时间、商品归属与可回溯证据。</p>
- </div>
- </section>
- </section>
- <section class="view-content" *ngIf="activeView === 'dictionary'">
- <section class="workspace-section">
- <div class="section-heading dictionary-heading">
- <div>
- <h2>国内电商指标口径</h2>
- <p>统一指标定义、计算公式、数据粒度和来源可用性。</p>
- </div>
- <div class="section-tools dictionary-tools">
- <label class="search-field">
- <lucide-icon [img]="icons.search" [size]="15"></lucide-icon>
- <input [(ngModel)]="dictionarySearch" type="search" placeholder="搜索指标、定义或公式" aria-label="搜索指标口径">
- </label>
- <select [(ngModel)]="dictionaryDomain" aria-label="筛选指标领域">
- <option value="">全部领域</option>
- <option *ngFor="let domain of dictionaryDomains()" [value]="domain">{{ domain }}</option>
- </select>
- </div>
- </div>
- <div class="table-scroll">
- <table class="data-table dictionary-table">
- <thead><tr><th>领域</th><th>指标</th><th>业务定义</th><th>计算口径</th><th>粒度</th><th>来源</th><th>可用性</th></tr></thead>
- <tbody>
- <tr *ngFor="let metric of filteredDictionary()">
- <td>{{ metric.domain }}</td>
- <td><strong>{{ metric.name }}</strong><code class="metric-key">{{ metric.key }}</code></td>
- <td>{{ metric.definition }}</td>
- <td>{{ metric.formula }}</td>
- <td><code>{{ metric.grain }}</code></td>
- <td>{{ metric.source }}</td>
- <td><span class="status-tag" [class.ready]="metric.availability === 'available'" [class.partial]="metric.availability === 'partial'" [class.missing]="metric.availability === 'missing'">{{ availabilityLabel(metric.availability) }}</span></td>
- </tr>
- <tr *ngIf="!filteredDictionary().length"><td colspan="7" class="empty-cell">没有匹配的指标口径。</td></tr>
- </tbody>
- </table>
- </div>
- </section>
- </section>
- <button class="drawer-backdrop" type="button" aria-label="关闭侧栏" *ngIf="drawerOpen()" (click)="closeActiveDrawer()"></button>
- <aside *ngIf="selectedJob" class="detail-drawer open" role="dialog" aria-modal="true" aria-labelledby="job-detail-title">
- <ng-container *ngIf="selectedJob as job">
- <div class="drawer-header">
- <div>
- <h2 id="job-detail-title">同步任务详情</h2>
- <p>{{ job.id }}</p>
- </div>
- <button class="icon-button" type="button" title="关闭" aria-label="关闭任务详情" (click)="closeJobDetails()">
- <lucide-icon [img]="icons.close" [size]="18"></lucide-icon>
- </button>
- </div>
- <div class="drawer-body detail-body">
- <div class="detail-status-line">
- <span class="status-tag" [class.ready]="job.status === 'completed'" [class.partial]="job.status === 'partial' || job.status === 'pending' || job.status === 'processing'" [class.missing]="job.status === 'failed'">{{ jobStatusLabel(job.status) }}</span>
- <span>尝试 {{ job.attempts }}/{{ job.maxAttempts }}</span>
- </div>
- <section class="drawer-section progress-section" aria-label="任务进度">
- <div class="drawer-section-heading"><h3>任务进度</h3><strong>{{ job.progress }}%</strong></div>
- <span class="detail-progress-track"><i [style.width.%]="job.progress"></i></span>
- <p>{{ job.productIds.length }} 个商品 · {{ job.scopes.length }} 个同步范围</p>
- </section>
- <div class="detail-error" *ngIf="job.errorSummary">
- <lucide-icon [img]="icons.alert" [size]="17"></lucide-icon>
- <div><strong>错误摘要</strong><p>{{ job.errorSummary }}</p></div>
- </div>
- <section class="drawer-section">
- <div class="drawer-section-heading"><h3>运行信息</h3></div>
- <dl class="detail-list">
- <div><dt>平台</dt><dd>{{ job.platform | uppercase }}</dd></div>
- <div><dt>请求时间</dt><dd>{{ dateTime(job.requestedAt) }}</dd></div>
- <div><dt>开始时间</dt><dd>{{ dateTime(job.startedAt) }}</dd></div>
- <div><dt>完成时间</dt><dd>{{ dateTime(job.completedAt) }}</dd></div>
- </dl>
- <div class="detail-subsection">
- <span class="detail-label">同步范围</span>
- <div class="detail-chips"><span *ngFor="let scope of job.scopes">{{ scopeLabel(scope) }}</span></div>
- </div>
- <div class="detail-subsection">
- <span class="detail-label">平台商品 ID</span>
- <div class="product-id-list"><code *ngFor="let productId of job.productIds">{{ productId }}</code></div>
- </div>
- </section>
- <section class="drawer-section event-section">
- <div class="drawer-section-heading">
- <div><h3>事件时间线</h3><p>按服务端记录顺序展示。</p></div>
- <button class="icon-button small" type="button" title="刷新事件" aria-label="刷新任务事件" [disabled]="jobEventsLoading" (click)="reloadJobEvents()">
- <lucide-icon [img]="icons.refresh" [size]="15" [class.spinning]="jobEventsLoading"></lucide-icon>
- </button>
- </div>
- <div class="drawer-loading" *ngIf="jobEventsLoading"><span></span>正在读取事件</div>
- <div class="drawer-inline-error" *ngIf="!jobEventsLoading && jobEventsError">
- <p>{{ jobEventsError }}</p>
- <button class="text-button" type="button" (click)="reloadJobEvents()">重新加载</button>
- </div>
- <ol class="event-timeline" *ngIf="!jobEventsLoading && !jobEventsError && jobEvents.length">
- <li *ngFor="let event of jobEvents" [class]="'event-' + event.level">
- <span class="event-marker">
- <lucide-icon *ngIf="event.level === 'info'" [img]="icons.check" [size]="13"></lucide-icon>
- <lucide-icon *ngIf="event.level !== 'info'" [img]="icons.alert" [size]="13"></lucide-icon>
- </span>
- <div class="event-content">
- <div class="event-heading"><strong>{{ eventTypeLabel(event.type) }}</strong><time>{{ dateTime(event.createdAt) }}</time></div>
- <p>{{ event.message }}</p>
- <div class="event-details" *ngIf="eventDetailEntries(event) as details">
- <span *ngFor="let detail of details"><b>{{ detail.label }}</b>{{ detail.value }}</span>
- </div>
- <small>{{ eventLevelLabel(event.level) }} · {{ event.type }}</small>
- </div>
- </li>
- </ol>
- <div class="drawer-empty" *ngIf="!jobEventsLoading && !jobEventsError && !jobEvents.length">
- <lucide-icon [img]="icons.history" [size]="19"></lucide-icon>
- <p>任务尚未记录事件。</p>
- </div>
- </section>
- </div>
- <div class="drawer-footer">
- <button class="secondary-button" type="button" (click)="closeJobDetails()">关闭</button>
- <button class="secondary-button danger-text" *ngIf="job.status === 'pending'" type="button" [disabled]="busyJobs.has(job.id)" (click)="cancelJob(job)">取消任务</button>
- <button class="primary-button" *ngIf="job.status === 'failed' || job.status === 'partial'" type="button" [disabled]="busyJobs.has(job.id)" (click)="retryJob(job)">
- <lucide-icon [img]="icons.retry" [size]="16"></lucide-icon>重试任务
- </button>
- </div>
- </ng-container>
- </aside>
- <aside *ngIf="selectedSource" class="detail-drawer open" role="dialog" aria-modal="true" aria-labelledby="source-detail-title">
- <ng-container *ngIf="selectedSource as source">
- <div class="drawer-header">
- <div>
- <h2 id="source-detail-title">数据源详情</h2>
- <p>{{ sourceKindLabel(source.kind) }} · {{ source.id }}</p>
- </div>
- <button class="icon-button" type="button" title="关闭" aria-label="关闭数据源详情" (click)="closeSourceDetails()">
- <lucide-icon [img]="icons.close" [size]="18"></lucide-icon>
- </button>
- </div>
- <div class="drawer-body detail-body">
- <div class="detail-status-line">
- <span class="status-tag" [class.ready]="source.status === 'active' || source.status === 'configured'" [class.missing]="source.status === 'error'">{{ sourceStatusLabel(source.status) }}</span>
- <span>{{ source.platform | uppercase }} · {{ workspaceId }}</span>
- </div>
- <section class="drawer-section">
- <div class="drawer-section-heading"><h3>授权与连接</h3></div>
- <dl class="detail-list source-detail-list">
- <div><dt>授权托管</dt><dd>{{ credentialStorageLabel(source.credentialStorage) }}</dd></div>
- <div><dt>最近检查</dt><dd>{{ dateTime(source.lastCheckedAt) }}</dd></div>
- <div><dt>授权账号</dt><dd><span class="building-label">待建设</span></dd></div>
- <div><dt>授权范围</dt><dd><span class="building-label">待建设</span></dd></div>
- <div><dt>授权到期时间</dt><dd><span class="building-label">待建设</span></dd></div>
- <div><dt>店铺绑定</dt><dd><span class="building-label">待建设</span></dd></div>
- </dl>
- </section>
- <section class="drawer-section">
- <div class="drawer-section-heading"><div><h3>连接器数据域</h3><p>以当前真实同步范围为准。</p></div></div>
- <div class="domain-list">
- <div *ngFor="let domain of connectorDomains">
- <div><strong>{{ domain.label }}</strong><p>{{ domain.description }}</p></div>
- <span class="status-tag" [class.ready]="domain.state === 'supported'" [class.missing]="domain.state === 'building'">{{ domain.state === 'supported' ? '已支持' : '待建设' }}</span>
- </div>
- </div>
- </section>
- <section class="next-action-band">
- <lucide-icon [img]="source.status === 'error' ? icons.alert : icons.check" [size]="18"></lucide-icon>
- <div><span>下一步动作</span><strong>{{ sourceNextAction(source).title }}</strong><p>{{ sourceNextAction(source).description }}</p></div>
- </section>
- </div>
- <div class="drawer-footer">
- <button class="secondary-button" type="button" (click)="configureSource()">
- <lucide-icon [img]="icons.settings" [size]="16"></lucide-icon>管理接入
- </button>
- <button class="primary-button" *ngIf="source.status === 'active' || source.status === 'configured'" type="button" (click)="openSyncFromSource(vm.dataset)">
- <lucide-icon [img]="icons.play" [size]="16"></lucide-icon>新建同步
- </button>
- </div>
- </ng-container>
- </aside>
- <aside *ngIf="syncOpen" class="sync-drawer open" role="dialog" aria-modal="true" aria-labelledby="sync-drawer-title">
- <div class="drawer-header">
- <div>
- <h2 id="sync-drawer-title">新建平台同步</h2>
- <p>{{ platform | uppercase }} · {{ workspaceId }}</p>
- </div>
- <button class="icon-button" type="button" title="关闭" aria-label="关闭" [disabled]="syncing" (click)="closeSync()">
- <lucide-icon [img]="icons.close" [size]="18"></lucide-icon>
- </button>
- </div>
- <div class="drawer-body">
- <label class="field-label" for="sync-products">平台商品 ID</label>
- <textarea id="sync-products" [(ngModel)]="syncProductIds" rows="8" placeholder="每行一个商品 ID,最多 100 个"></textarea>
- <small class="field-help">支持换行、逗号或空格分隔;系统会自动去重。</small>
- <fieldset class="scope-fieldset">
- <legend>同步范围</legend>
- <label><input type="checkbox" [(ngModel)]="syncProductScope"><span>商品主数据</span></label>
- <label><input type="checkbox" [(ngModel)]="syncReviewScope"><span>商品评价</span></label>
- </fieldset>
- <div class="sync-note">
- <lucide-icon [img]="icons.clock" [size]="17"></lucide-icon>
- <p>任务进入后台队列后可在“接入与同步”查看进度。商品与评价会分别记录结果,部分成功的任务可重试。</p>
- </div>
- <p class="form-error" *ngIf="syncError">{{ syncError }}</p>
- </div>
- <div class="drawer-footer">
- <button class="secondary-button" type="button" [disabled]="syncing" (click)="closeSync()">取消</button>
- <button class="primary-button" type="button" [disabled]="syncing" (click)="submitSync()">
- <lucide-icon [img]="icons.play" [size]="16"></lucide-icon>
- {{ syncing ? '正在创建' : '创建任务' }}
- </button>
- </div>
- </aside>
- </main>
|