← All Blocks Workbench 工作台

DB Workbench 数据库工作台

左侧 Schema 树 + 右侧 SQL 编辑器 + 下方结果 / 历史 / 消息三 tab。Splitter 双向分隔,Postgres / MySQL 风格。

db-workbench source
DbWorkbench.vue vue
<script setup lang="ts">
import { ref } from 'vue';
import {
  CfSplitter,
  CfTreeView,
  CfCodeEditor,
  CfDataGrid,
  CfTabs,
  CfTabPanel,
  CfButton,
  CfStatusCodeBadge,
} from '@chufix-design/vue';
import type { TreeNode } from '@chufix-design/vue';

const schema: TreeNode[] = [
  { id: 'public', label: 'public', children: [
    { id: 'users', label: 'users · 12,400 行' },
    { id: 'orders', label: 'orders · 184,200 行' },
    { id: 'payments', label: 'payments · 62,810 行' },
  ]},
  { id: 'analytics', label: 'analytics', children: [
    { id: 'events', label: 'events · 4.2M 行' },
    { id: 'sessions', label: 'sessions · 920k 行' },
  ]},
];

const sql = ref(`SELECT
  o.id,
  o.amount,
  u.email,
  p.status
FROM orders o
JOIN users u ON u.id = o.user_id
JOIN payments p ON p.order_id = o.id
WHERE o.created_at > now() - interval '7 days'
ORDER BY o.created_at DESC
LIMIT 20;`);

const cols = [
  { key: 'id', title: 'id', dataIndex: 'id', width: 70 },
  { key: 'amount', title: 'amount', dataIndex: 'amount', width: 100 },
  { key: 'email', title: 'email', dataIndex: 'email' },
  { key: 'status', title: 'status', dataIndex: 'status', width: 110 },
];
const rows = [
  { id: '1842', amount: '¥ 480.00', email: '[email protected]', status: 200 },
  { id: '1841', amount: '¥ 1,200.00', email: '[email protected]', status: 200 },
  { id: '1840', amount: '¥ 88.00', email: '[email protected]', status: 500 },
  { id: '1839', amount: '¥ 240.00', email: '[email protected]', status: 200 },
  { id: '1838', amount: '¥ 3,180.00', email: '[email protected]', status: 200 },
  { id: '1837', amount: '¥ 96.00', email: '[email protected]', status: 304 },
];

const history = [
  { id: '1', sql: 'SELECT count(*) FROM orders;', when: '2m 前' },
  { id: '2', sql: 'UPDATE users SET status=...', when: '14m 前' },
];
</script>

<template>
  <div class="db">
    <CfSplitter orientation="horizontal" :first-size="220">
      <template #first>
        <div class="db__pane">
          <h3>Schema</h3>
          <CfTreeView :nodes="schema" />
        </div>
      </template>
      <template #second>
        <CfSplitter orientation="vertical" :first-size="160">
          <template #first>
            <div class="db__editor">
              <div class="db__bar">
                <CfButton variant="primary" size="sm">▶ 运行</CfButton>
                <CfButton variant="tertiary" size="sm">保存</CfButton>
                <span class="db__hint">Ctrl + Enter 运行</span>
              </div>
              <CfCodeEditor v-model="sql" language="sql" :rows="6" />
            </div>
          </template>
          <template #second>
            <CfTabs default-value="result">
              <CfTabPanel value="result" label="结果 · 6 行">
                <CfDataGrid :columns="cols" :rows="rows">
                  <template #cell-status="{ row }">
                    <CfStatusCodeBadge :code="row.status" size="sm" />
                  </template>
                </CfDataGrid>
              </CfTabPanel>
              <CfTabPanel value="history" label="历史">
                <ul class="db__hist">
                  <li v-for="h in history" :key="h.id">
                    <code>{{ h.sql }}</code>
                    <span class="db__hist-time">{{ h.when }}</span>
                  </li>
                </ul>
              </CfTabPanel>
              <CfTabPanel value="messages" label="消息">
                <pre class="db__msg">[14:32:01] Query OK · 6 rows · 38ms</pre>
              </CfTabPanel>
            </CfTabs>
          </template>
        </CfSplitter>
      </template>
    </CfSplitter>
  </div>
</template>

<style scoped>
.db {
  height: 560px;
  border: 1px solid var(--line-1);
  border-radius: var(--r-6);
  overflow: hidden;
  font-family: var(--font-sans);
}
.db__pane {
  padding: 12px 14px;
  height: 100%;
  overflow: auto;
}
.db__pane h3 {
  margin: 0 0 8px;
  font-size: var(--t-11);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--fg-3);
  font-weight: var(--w-medium);
}
.db__editor {
  display: flex;
  flex-direction: column;
  height: 100%;
}
.db__bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  background: var(--bg-2);
  border-bottom: 1px solid var(--line-1);
}
.db__hint {
  font-family: var(--font-mono);
  font-size: var(--t-11);
  color: var(--fg-3);
  margin-left: auto;
}
.db__hist {
  list-style: none;
  margin: 0;
  padding: 8px;
  font-size: var(--t-12);
}
.db__hist > li {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 6px 8px;
  border-radius: var(--r-3);
}
.db__hist > li:hover {
  background: var(--bg-2);
}
.db__hist code {
  font-family: var(--font-mono);
  color: var(--fg-1);
  background: transparent;
}
.db__hist-time {
  color: var(--fg-3);
  font-family: var(--font-mono);
  font-size: var(--t-11);
}
.db__msg {
  margin: 0;
  padding: 12px 16px;
  font-family: var(--font-mono);
  font-size: var(--t-12);
  color: var(--fg-2);
}
</style>
DbWorkbench.tsx tsx
import { useState } from 'react';
import {
  CfSplitter,
  CfTreeView,
  CfCodeEditor,
  CfDataGrid,
  CfTabs,
  CfTabPanel,
  CfButton,
  CfStatusCodeBadge,
} from '@chufix-design/react';
import type { TreeNode } from '@chufix-design/react';

const schema: TreeNode[] = [
  {
    id: 'public',
    label: 'public',
    children: [
      { id: 'users', label: 'users · 12,400 行' },
      { id: 'orders', label: 'orders · 184,200 行' },
      { id: 'payments', label: 'payments · 62,810 行' },
    ],
  },
  {
    id: 'analytics',
    label: 'analytics',
    children: [
      { id: 'events', label: 'events · 4.2M 行' },
      { id: 'sessions', label: 'sessions · 920k 行' },
    ],
  },
];

const cols = [
  { key: 'id', title: 'id', dataIndex: 'id', width: 70 },
  { key: 'amount', title: 'amount', dataIndex: 'amount', width: 100 },
  { key: 'email', title: 'email', dataIndex: 'email' },
  {
    key: 'status',
    title: 'status',
    dataIndex: 'status',
    width: 110,
    render: (_v: unknown, row: any) => <CfStatusCodeBadge code={row.status} size="sm" />,
  },
];
const rows = [
  { id: '1842', amount: '¥ 480.00', email: '[email protected]', status: 200 },
  { id: '1841', amount: '¥ 1,200.00', email: '[email protected]', status: 200 },
  { id: '1840', amount: '¥ 88.00', email: '[email protected]', status: 500 },
  { id: '1839', amount: '¥ 240.00', email: '[email protected]', status: 200 },
  { id: '1838', amount: '¥ 3,180.00', email: '[email protected]', status: 200 },
  { id: '1837', amount: '¥ 96.00', email: '[email protected]', status: 304 },
];

const history = [
  { id: '1', sql: 'SELECT count(*) FROM orders;', when: '2m 前' },
  { id: '2', sql: 'UPDATE users SET status=...', when: '14m 前' },
];

export function DbWorkbench() {
  const [sql, setSql] = useState(`SELECT
  o.id,
  o.amount,
  u.email,
  p.status
FROM orders o
JOIN users u ON u.id = o.user_id
JOIN payments p ON p.order_id = o.id
WHERE o.created_at > now() - interval '7 days'
ORDER BY o.created_at DESC
LIMIT 20;`);

  const editor = (
    <div className="db__editor">
      <div className="db__bar">
        <CfButton variant="primary" size="sm">▶ 运行</CfButton>
        <CfButton variant="tertiary" size="sm">保存</CfButton>
        <span className="db__hint">Ctrl + Enter 运行</span>
      </div>
      <CfCodeEditor value={sql} onChange={setSql} language="sql" rows={6} />
    </div>
  );

  const result = (
    <CfTabs
      defaultValue="result"
      items={[
        { value: 'result', label: '结果 · 6 行' },
        { value: 'history', label: '历史' },
        { value: 'messages', label: '消息' },
      ]}
    >
      {({ active }) => (
        <>
          <CfTabPanel value="result" active={active}>
            <CfDataGrid columns={cols} rows={rows} />
          </CfTabPanel>
          <CfTabPanel value="history" active={active}>
            <ul className="db__hist">
              {history.map((h) => (
                <li key={h.id}>
                  <code>{h.sql}</code>
                  <span className="db__hist-time">{h.when}</span>
                </li>
              ))}
            </ul>
          </CfTabPanel>
          <CfTabPanel value="messages" active={active}>
            <pre className="db__msg">[14:32:01] Query OK · 6 rows · 38ms</pre>
          </CfTabPanel>
        </>
      )}
    </CfTabs>
  );

  const sidebar = (
    <div className="db__pane">
      <h3>Schema</h3>
      <CfTreeView nodes={schema} />
    </div>
  );

  const main = (
    <CfSplitter
      orientation="vertical"
      defaultValue={160}
      unit="px"
      start={editor}
      end={result}
    />
  );

  return (
    <div className="db">
      <CfSplitter
        orientation="horizontal"
        defaultValue={220}
        unit="px"
        start={sidebar}
        end={main}
      />
    </div>
  );
}