| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211 |
- <template>
- <header class="codex-topbar">
- <div class="codex-topbar__title">
- Codex 数据分析
- <span v-if="sessionTitle" class="codex-topbar__meta">{{ sessionTitle }}</span>
- </div>
- <span class="codex-topbar__spacer"></span>
- <!-- 运行时状态 -->
- <div class="codex-topbar__status" :title="runtimeTitle">
- <span class="codex-dot" :class="`codex-dot--${dotTone}`"></span>
- <span class="codex-topbar__meta">{{ statusText }}</span>
- <span v-if="version" class="codex-topbar__meta">v{{ version }}</span>
- </div>
- <!-- 工作目录 -->
- <div v-if="cwd" class="codex-topbar__status" :title="cwd">
- <Icon icon="mdi:folder-outline" :size="14" class="codex-topbar__icon" />
- <span class="codex-topbar__meta codex-topbar__path">{{ cwd }}</span>
- <button class="codex-btn" type="button" @click="onCopyCwd">复制</button>
- </div>
- <div class="codex-topbar__actions">
- <span v-if="runBadge" class="codex-badge" :class="runBadge.tone" :title="runBadge.title">
- {{ runBadge.text }}
- </span>
- <span v-if="runningCount" class="codex-badge codex-badge--running" :title="`${runningCount} 个会话正在执行`">
- {{ runningCount }} 个会话运行中
- </span>
- <a-button v-if="needsStart" size="small" type="primary" :loading="busy" @click="onStart">
- <template #icon><Icon icon="mdi:play" :size="14" /></template>
- 启动运行时
- </a-button>
- <a-button v-else size="small" :loading="busy" @click="onRestart">
- <template #icon><Icon icon="mdi:restart" :size="14" /></template>
- 重启
- </a-button>
- <a-dropdown :trigger="['click']" placement="bottomRight">
- <a-button size="small" :disabled="!hasMessages">
- <template #icon><Icon icon="mdi:download-outline" :size="14" /></template>
- 导出
- </a-button>
- <template #overlay>
- <a-menu @click="onExportClick">
- <a-menu-item key="md">Markdown 报告</a-menu-item>
- <a-menu-item key="html">HTML 报告</a-menu-item>
- <a-menu-item key="xlsx">Excel(表格合集)</a-menu-item>
- <a-menu-item key="png">对话长图</a-menu-item>
- </a-menu>
- </template>
- </a-dropdown>
- <a-button size="small" @click="emit('diagnostics')">
- <template #icon><Icon icon="mdi:bug-outline" :size="14" /></template>
- 诊断
- </a-button>
- <a-button size="small" @click="emit('create')">
- <template #icon><Icon icon="mdi:plus" :size="14" /></template>
- 新建会话
- </a-button>
- </div>
- </header>
- </template>
- <script lang="ts" setup>
- /**
- * 顶部状态条:运行时状态 / 工作目录 / 导出 / 诊断 / 新建会话。
- *
- * 状态直接读 store(这里没有第二份状态),只有需要 DOM 的动作(导出长图)才上抛给页面。
- */
- import { computed, ref } from 'vue';
- import { Button, Dropdown, Menu, MenuItem } from 'ant-design-vue';
- import Icon from '@/core/components/Icon/src/Icon.vue';
- import { useMessage } from '@/core/hooks/web/useMessage';
- import { useCodexChatStore } from '../store/codexChatStore';
- import type { CodexReportFormat } from '../types/codexTypes';
- import { copyCodexText } from '../utils/codexFormat';
- const emit = defineEmits<{
- (e: 'create'): void;
- (e: 'diagnostics'): void;
- (e: 'export', format: CodexReportFormat): void;
- }>();
- const AButton = Button;
- const ADropdown = Dropdown;
- const AMenu = Menu;
- const AMenuItem = MenuItem;
- const store = useCodexChatStore();
- const { showMessage } = useMessage();
- const busy = ref(false);
- const sessionTitle = computed(() => store.activeSession?.title || '');
- const cwd = computed(() => store.activeCwd);
- const version = computed(() => store.runtime?.version || '');
- const hasMessages = computed(() => store.hasMessages);
- const runningCount = computed(() => store.runningCount);
- /**
- * 非默认运行参数的徽标。
- * 把「只读 / 全权 / 免审批 / 每步确认」摆在顶栏,是为了让用户随时看得见当前会话的安全姿态。
- */
- const runBadge = computed<{ text: string; tone: string; title: string } | null>(() => {
- const options = store.activeRunOptions;
- const parts: string[] = [];
- let tone = 'codex-badge--muted';
- if (options.sandbox === 'read-only') parts.push('只读');
- if (options.sandbox === 'danger-full-access') {
- parts.push('全权访问');
- tone = 'codex-badge--error';
- }
- if (options.approvalPolicy === 'never') {
- parts.push('免审批');
- tone = 'codex-badge--error';
- } else if (options.approvalPolicy === 'untrusted') {
- parts.push('每步确认');
- }
- if (!parts.length) return null;
- return { text: parts.join(' · '), tone, title: '当前运行参数,可在输入区的「参数」里修改' };
- });
- const needsStart = computed(() => (store.runtime?.state ?? 'stopped') !== 'ready');
- const statusText = computed(() => {
- const status = store.runtime;
- if (!status) return '未检测';
- if (status.state === 'ready') return status.degraded ? '就绪(模型降级)' : '就绪';
- if (status.state === 'starting') return '启动中';
- if (status.state === 'error') return '运行时异常';
- return '未启动';
- });
- const dotTone = computed(() => {
- const status = store.runtime;
- if (!status) return 'idle';
- if (status.state === 'ready') return 'ready';
- if (status.state === 'starting') return 'busy';
- if (status.state === 'error') return 'error';
- return 'idle';
- });
- const runtimeTitle = computed(() => {
- const status = store.runtime;
- const parts: string[] = [];
- if (status?.binaryPath) parts.push(`二进制:${status.binaryPath}`);
- if (status?.applied) parts.push(`已应用模型:${status.applied.model}`);
- if (status?.error) parts.push(`错误:${status.error}`);
- if (store.runtimeError && !status?.error) parts.push(store.runtimeError);
- return parts.join('\n') || 'Codex 运行时状态';
- });
- async function onStart() {
- busy.value = true;
- try {
- await store.startRuntime();
- showMessage('Codex 运行时已启动', 'success');
- } catch (error: any) {
- showMessage(error?.message || '启动失败', 'error');
- } finally {
- busy.value = false;
- }
- }
- async function onRestart() {
- busy.value = true;
- try {
- await store.restartRuntime();
- showMessage('Codex 运行时已重启', 'success');
- } catch (error: any) {
- showMessage(error?.message || '重启失败', 'error');
- } finally {
- busy.value = false;
- }
- }
- async function onCopyCwd() {
- const ok = await copyCodexText(cwd.value);
- showMessage(ok ? '已复制工作目录' : '复制失败', ok ? 'success' : 'warning');
- }
- function onExportClick(info: { key: string | number }) {
- emit('export', String(info.key) as CodexReportFormat);
- }
- </script>
- <style lang="less">
- .codex-topbar {
- &__icon {
- flex: 0 0 auto;
- color: var(--codex-text-2);
- }
- &__path {
- display: inline-block;
- max-width: 220px;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- direction: rtl;
- text-align: left;
- }
- }
- </style>
|