feat(fe-editor): add CSV table editor and Markdown editor components
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b7ebbcd777
commit
ce4f474472
|
|
@ -0,0 +1,19 @@
|
|||
import api from '@/shared/api'
|
||||
import type { EditableFile, ImpactResult, ScanResult } from '@/shared/types/api'
|
||||
|
||||
export async function getFile(projectId: string, path: string): Promise<EditableFile> {
|
||||
const { data } = await api.get<EditableFile>(`/projects/${projectId}/files/${path}`)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function saveFile(projectId: string, path: string, content: string): Promise<ScanResult> {
|
||||
const { data } = await api.put<ScanResult>(`/projects/${projectId}/files/${path}`, content, {
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
export async function getFileImpact(projectId: string, path: string): Promise<ImpactResult> {
|
||||
const { data } = await api.get<ImpactResult>(`/projects/${projectId}/files/${path}/impact`)
|
||||
return data
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
<template>
|
||||
<div class="csv-editor">
|
||||
<div class="toolbar">
|
||||
<button class="primary" @click="addRow">添加行</button>
|
||||
<button class="primary" @click="$emit('save', serialize())">保存</button>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="(h, i) in headers" :key="i">{{ h }}</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, ri) in rows" :key="ri">
|
||||
<td v-for="(cell, ci) in row" :key="ci" contenteditable @blur="updateCell(ri, ci, $event)">{{ cell }}</td>
|
||||
<td><button class="danger" @click="removeRow(ri)">删除</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const props = defineProps<{ content: string }>()
|
||||
defineEmits<{ save: [content: string] }>()
|
||||
|
||||
const headers = ref<string[]>([])
|
||||
const rows = ref<string[][]>([])
|
||||
|
||||
onMounted(() => {
|
||||
const lines = props.content.trim().split('\n')
|
||||
if (lines.length > 0) {
|
||||
headers.value = lines[0].split(',')
|
||||
rows.value = lines.slice(1).map(l => l.split(','))
|
||||
}
|
||||
})
|
||||
|
||||
function updateCell(ri: number, ci: number, event: Event) {
|
||||
rows.value[ri][ci] = (event.target as HTMLElement).textContent || ''
|
||||
}
|
||||
|
||||
function addRow() {
|
||||
rows.value.push(headers.value.map(() => ''))
|
||||
}
|
||||
|
||||
function removeRow(index: number) {
|
||||
rows.value.splice(index, 1)
|
||||
}
|
||||
|
||||
function serialize(): string {
|
||||
return [headers.value.join(','), ...rows.value.map(r => r.join(','))].join('\n') + '\n'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar { margin-bottom: 12px; display: flex; gap: 8px; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
th, td { border: 1px solid #e0e0e0; padding: 6px 8px; text-align: left; }
|
||||
th { background: #f5f5f5; font-weight: 600; }
|
||||
td[contenteditable] { cursor: text; }
|
||||
td[contenteditable]:focus { outline: 2px solid #1976D2; }
|
||||
</style>
|
||||
47
frontend/src/modules/editor/components/EditorPage.vue
Normal file
47
frontend/src/modules/editor/components/EditorPage.vue
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<div class="editor-page">
|
||||
<h1>文件编辑器</h1>
|
||||
<div v-if="!currentFile" class="empty">
|
||||
<p>选择一个文件开始编辑</p>
|
||||
<input v-model="filePath" placeholder="输入相对路径,如 business-architecture/02-capability-map.csv" />
|
||||
<button class="primary" @click="load">打开</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p class="meta">{{ currentFile.path }} ({{ currentFile.format }})</p>
|
||||
<CsvEditor v-if="currentFile.format === 'csv'" :content="currentFile.content" @save="handleSave" />
|
||||
<MdEditor v-else :initial-content="currentFile.content" @save="handleSave" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useEditorStore } from '../composables/useEditor'
|
||||
import CsvEditor from './CsvEditor.vue'
|
||||
import MdEditor from './MdEditor.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const store = useEditorStore()
|
||||
const { currentFile } = storeToRefs(store)
|
||||
|
||||
const filePath = ref('')
|
||||
|
||||
function load() {
|
||||
if (filePath.value) {
|
||||
store.loadFile(route.params.id as string, filePath.value)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSave(content: string) {
|
||||
store.saveFile(route.params.id as string, currentFile.value!.path, content)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
h1 { margin-bottom: 16px; }
|
||||
.meta { font-size: 13px; color: #666; margin-bottom: 12px; }
|
||||
.empty { text-align: center; padding: 48px; }
|
||||
.empty input { margin: 12px 0; max-width: 500px; }
|
||||
</style>
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<div class="md-editor">
|
||||
<div class="toolbar">
|
||||
<button class="primary" @click="$emit('save', content)">保存</button>
|
||||
</div>
|
||||
<div class="editor-panes">
|
||||
<textarea v-model="content" class="editor-input"></textarea>
|
||||
<div class="editor-preview" v-html="preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const props = defineProps<{ initialContent: string }>()
|
||||
defineEmits<{ save: [content: string] }>()
|
||||
|
||||
const content = ref(props.initialContent)
|
||||
|
||||
const preview = computed(() => {
|
||||
return content.value
|
||||
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
||||
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
||||
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||
.replace(/`(.+?)`/g, '<code>$1</code>')
|
||||
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
||||
.replace(/\n/g, '<br>')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar { margin-bottom: 12px; }
|
||||
.editor-panes { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; height: calc(100vh - 200px); }
|
||||
.editor-input { font-family: monospace; font-size: 14px; resize: none; padding: 12px; border: 1px solid #e0e0e0; border-radius: 4px; }
|
||||
.editor-preview { padding: 12px; border: 1px solid #e0e0e0; border-radius: 4px; overflow-y: auto; }
|
||||
</style>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { EditableFile, ImpactResult } from '@/shared/types/api'
|
||||
import * as editorApi from '../api'
|
||||
|
||||
export const useEditorStore = defineStore('editor', () => {
|
||||
const currentFile = ref<EditableFile | null>(null)
|
||||
const impactResult = ref<ImpactResult | null>(null)
|
||||
const saving = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
async function loadFile(projectId: string, path: string) {
|
||||
try {
|
||||
currentFile.value = await editorApi.getFile(projectId, path)
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
}
|
||||
}
|
||||
|
||||
async function saveFile(projectId: string, path: string, content: string) {
|
||||
saving.value = true
|
||||
try {
|
||||
await editorApi.saveFile(projectId, path, content)
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function analyzeImpact(projectId: string, path: string) {
|
||||
try {
|
||||
impactResult.value = await editorApi.getFileImpact(projectId, path)
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
}
|
||||
}
|
||||
|
||||
return { currentFile, impactResult, saving, error, loadFile, saveFile, analyzeImpact }
|
||||
})
|
||||
|
|
@ -0,0 +1 @@
|
|||
export type { EditableFile, ImpactResult, ImplProgress } from '@/shared/types/api'
|
||||
Loading…
Reference in New Issue
Block a user