CrawlerManagementView.vue 9.06 KB
<script setup lang="ts">
import { computed } from 'vue'

import CrawlerPanel from '@/components/crawler/CrawlerPanel.vue'
import { useWorkbenchControllerContext } from '@/composables/useWorkbenchController'
import type { CrawlerRecoveryActionKey, CrawlerRecoveryHintTone } from '@/types'
import { formatTimeLabel, statusTone } from '@/utils/format'

const controller = useWorkbenchControllerContext()

const selectedPlatformLabel = computed(() => (
  controller.crawler.selectedPlatformState.value?.label || controller.crawler.form.platform || '未选择平台'
))

const primaryAction = computed(() => controller.crawler.primaryAction.value)
const recoveryHints = computed(() => controller.crawler.recoveryHints.value)
const selectedPlatformHistory = computed(() => controller.crawler.selectedPlatformHistory.value.slice(0, 6))

function statusTagType(status: string) {
  return statusTone(status)
}

function recoveryToneLabel(tone: CrawlerRecoveryHintTone) {
  if (tone === 'success') {
    return '已就绪'
  }
  if (tone === 'warning') {
    return '需注意'
  }
  if (tone === 'error') {
    return '待处理'
  }
  return '建议'
}

function recoveryActionType(actionKey: CrawlerRecoveryActionKey) {
  if (actionKey === 'start-login') {
    return 'archive-button'
  }
  if (actionKey === 'restore-last-login' || actionKey === 'check-login') {
    return 'archive-button--ghost'
  }
  return 'archive-button--quiet'
}
</script>

<template>
  <section class="crawler-page">
    <div class="crawler-header archive-shell-block">
      <div class="crawler-header__copy">
        <p class="archive-kicker">Collection Control Room</p>
        <h2 class="archive-page-title">Crawler Management</h2>
        <p class="archive-copy">
          把平台登录、采集参数、二维码状态、实时日志和排障建议放在同一个可观察界面里,避免在多个步骤之间来回切换。
        </p>
      </div>

      <div class="crawler-header__stats">
        <article class="archive-stat">
          <p>Current Platform</p>
          <strong>{{ selectedPlatformLabel }}</strong>
          <span>平台切换后,登录方式、采集模式与历史配置会同步变化。</span>
        </article>
        <article class="archive-stat">
          <p>Login Status</p>
          <strong>{{ controller.crawler.selectedPlatformState.value?.logged_in ? 'Logged In' : 'Awaiting Login' }}</strong>
          <span>{{ controller.crawler.selectedPlatformState.value?.message || '建议先检查登录状态。' }}</span>
        </article>
        <article class="archive-stat">
          <p>Recommended</p>
          <strong>{{ primaryAction.label }}</strong>
          <span>{{ primaryAction.description }}</span>
        </article>
      </div>
    </div>

    <div class="crawler-main">
      <CrawlerPanel
        :options="controller.crawler.options.value"
        :form="controller.crawler.form"
        :snapshot="controller.crawler.snapshot.value"
        :selected-platform-state="controller.crawler.selectedPlatformState.value"
        :available-login-types="controller.crawler.availableLoginTypes.value"
        :available-crawler-types="controller.crawler.availableCrawlerTypes.value"
        :capability-hint="controller.crawler.capabilityHint.value"
        :validation-issues="controller.crawler.validationIssues.value"
        :workflow-steps="controller.crawler.workflowSteps.value"
        :primary-action="controller.crawler.primaryAction.value"
        :mission-summary="controller.crawler.missionSummary.value"
        :selected-qr-code="controller.crawler.selectedQrCode.value"
        :last-login-config="controller.crawler.lastLoginConfig.value"
        :last-crawl-config="controller.crawler.lastCrawlConfig.value"
        :can-start-login="controller.crawler.canStartLogin.value"
        :can-start-crawler="controller.crawler.canStartCrawler.value"
        :loading="controller.crawler.loading.value"
        :acting="controller.crawler.acting.value"
        :collapsed="controller.crawler.collapsed.value"
        @update:form="controller.updateCrawlerForm"
        @update:collapsed="controller.updateCrawlerCollapsed"
        @check-login="controller.checkCrawlerLogin"
        @start-login="controller.startCrawlerLogin"
        @cancel-login="controller.cancelCrawlerLogin"
        @start-crawler="controller.startCrawlerTask"
        @stop-crawler="controller.stopCrawlerTask"
        @restore-last-login="controller.crawler.restoreLastLoginConfig"
        @restore-last-crawl="controller.crawler.restoreLastCrawlConfig"
      />

      <aside class="crawler-side">
        <article class="archive-panel">
          <div class="side-head">
            <div>
              <p class="archive-kicker">Troubleshooting Desk</p>
              <h3 class="archive-card-title">登录排障助手</h3>
            </div>
            <span class="archive-chip" :data-tone="statusTone(controller.crawler.selectedPlatformState.value?.status || 'pending')">
              <span class="archive-chip__dot" />
              {{ controller.crawler.selectedPlatformState.value?.status || 'pending' }}
            </span>
          </div>

          <div class="hint-list">
            <article
              v-for="hint in recoveryHints"
              :key="hint.id"
              class="hint-card"
              :data-tone="hint.tone"
            >
              <div class="hint-card__head">
                <strong>{{ hint.title }}</strong>
                <span class="archive-chip" :data-tone="hint.tone">{{ recoveryToneLabel(hint.tone) }}</span>
              </div>
              <p class="archive-copy">{{ hint.description }}</p>
              <div v-if="hint.actions.length" class="hint-card__actions">
                <button
                  v-for="action in hint.actions"
                  :key="`${hint.id}-${action.key}`"
                  :class="recoveryActionType(action.key)"
                  type="button"
                  @click="controller.applyCrawlerRecoveryAction(action.key)"
                >
                  {{ action.label }}
                </button>
              </div>
            </article>
          </div>
        </article>

        <article class="archive-panel">
          <div class="side-head">
            <div>
              <p class="archive-kicker">Archival History</p>
              <h3 class="archive-card-title">当前平台记录</h3>
            </div>
          </div>

          <div v-if="selectedPlatformHistory.length" class="history-list">
            <div
              v-for="item in selectedPlatformHistory"
              :key="item.id"
              class="history-card"
            >
              <div class="history-card__head">
                <strong>{{ item.platform_label }} · {{ item.kind }}</strong>
                <span class="archive-chip" :data-tone="statusTagType(item.status)">{{ item.status }}</span>
              </div>
              <p class="archive-copy">{{ item.message || '暂无详细说明。' }}</p>
              <time>{{ formatTimeLabel(item.updated_at) }}</time>
            </div>
          </div>

          <div v-else class="archive-empty">
            <p>当前平台暂无历史记录。</p>
          </div>
        </article>
      </aside>
    </div>
  </section>
</template>

<style scoped>
.crawler-page,
.crawler-header,
.crawler-header__stats,
.crawler-side,
.hint-list,
.history-list {
  display: grid;
  gap: 18px;
}

.crawler-page {
  max-width: 1560px;
  margin: 0 auto;
}

.crawler-header {
  grid-template-columns: minmax(0, 1fr) minmax(420px, 0.95fr);
  padding: 30px;
  border-radius: 30px;
  background:
    radial-gradient(circle at top right, rgba(141, 103, 56, 0.16), transparent 24%),
    linear-gradient(135deg, rgba(255, 251, 246, 0.94), rgba(243, 236, 226, 0.92));
}

.crawler-header__copy {
  display: grid;
  gap: 14px;
}

.crawler-header__stats {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.crawler-main {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 360px;
  gap: 18px;
  align-items: start;
}

.crawler-side {
  position: sticky;
  top: 122px;
}

.side-head,
.hint-card__head,
.history-card__head {
  display: flex;
  justify-content: space-between;
  gap: 14px;
  align-items: flex-start;
}

.side-head {
  margin-bottom: 18px;
}

.hint-card,
.history-card {
  display: grid;
  gap: 12px;
  padding: 18px;
  border-radius: 20px;
  border: 1px solid rgba(24, 35, 31, 0.08);
  background: rgba(255, 255, 255, 0.68);
}

.hint-card[data-tone='warning'] {
  border-color: rgba(163, 109, 55, 0.18);
}

.hint-card[data-tone='error'] {
  border-color: rgba(141, 58, 50, 0.18);
}

.hint-card[data-tone='success'] {
  border-color: rgba(46, 106, 88, 0.18);
}

.hint-card__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.history-card time {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-soft);
}

@media (max-width: 1360px) {
  .crawler-header,
  .crawler-main {
    grid-template-columns: 1fr;
  }

  .crawler-side {
    position: static;
  }
}

@media (max-width: 820px) {
  .crawler-header,
  .crawler-header__stats {
    grid-template-columns: 1fr;
  }

  .crawler-header {
    padding: 22px;
  }
}
</style>