修复: 项目详情批量(任务列表联动刷新/工作流降级标注/中文git乱码/子目录错误行内/零工程空态/切tab保活) + 依赖图批量(节点渲染register/环高亮/防环防重/点击选中) + 销账
This commit is contained in:
@@ -46,7 +46,13 @@
|
||||
<div class="dep-graph__modal-field">
|
||||
<label>{{ $t('dependencyGraph.targetLabel') }}</label>
|
||||
<select v-model="depTo" class="dep-graph__modal-input">
|
||||
<option v-for="m in modules" :key="m.id" :value="m.id">{{ m.name }}</option>
|
||||
<!-- G6:已存在 from→to 组合或选中自身作为目标时禁用该候选 -->
|
||||
<option
|
||||
v-for="m in modules"
|
||||
:key="m.id"
|
||||
:value="m.id"
|
||||
:disabled="depFrom === m.id || (depFrom !== '' && existingDepKeys.has(`${depFrom}->${m.id}`))"
|
||||
>{{ m.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="dep-graph__modal-field">
|
||||
@@ -61,7 +67,7 @@
|
||||
</div>
|
||||
<div class="dep-graph__modal-actions">
|
||||
<button class="btn btn-ghost btn-sm" @click="showAddDep = false">{{ $t('common.cancel') }}</button>
|
||||
<button class="btn btn-primary btn-sm" :disabled="!depFrom || !depTo || depFrom === depTo" @click="onAddDep">{{ $t('dependencyGraph.confirm') }}</button>
|
||||
<button class="btn btn-primary btn-sm" :disabled="!depFrom || !depTo || depFrom === depTo || existingDepKeys.has(`${depFrom}->${depTo}`)" @click="onAddDep">{{ $t('dependencyGraph.confirm') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -74,11 +80,11 @@
|
||||
*
|
||||
* 节点用 Vue 组件(ModuleNode.vue)渲染,通过 @antv/x6-vue-shape 注册。
|
||||
*/
|
||||
import { onMounted, onBeforeUnmount, ref, watch, markRaw } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { onMounted, onBeforeUnmount, ref, watch, computed, markRaw } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Graph, Selection, Snapline, History, Scroller, MiniMap } from '@antv/x6'
|
||||
import dagre from 'dagre'
|
||||
import { register } from '@antv/x6-vue-shape'
|
||||
import '@antv/x6-vue-shape'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { moduleApi, type ProjectModuleRecord, type ModuleDependencyRecord } from '@/api/module'
|
||||
@@ -86,16 +92,16 @@ import { useConfirm } from '@/composables/useConfirm'
|
||||
import ModuleNode from './ModuleNode.vue'
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||
|
||||
// vue-shape 注册:把节点 Vue 组件绑定到 'vue-shape' 形状。
|
||||
// 库内 shapeMaps 仅经 register 填充(view.js:48 renderVueComponent 读 shapeMaps[node.shape]),
|
||||
// 单纯 import '@antv/x6-vue-shape' 只注册 shape 不绑组件 → 节点渲染空/报错(P0)。
|
||||
// 幂等:Graph.registerNode 对同名 shape 重复注册会覆盖,重复进组件也安全。
|
||||
register({ shape: 'vue-shape', component: ModuleNode })
|
||||
|
||||
const props = defineProps<{
|
||||
projectId: string
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'select-module', moduleId: string): void
|
||||
}>()
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const modules = ref<ProjectModuleRecord[]>([])
|
||||
const dependencies = ref<ModuleDependencyRecord[]>([])
|
||||
@@ -115,8 +121,23 @@ const depFrom = ref('')
|
||||
const depTo = ref('')
|
||||
const depType = ref('library')
|
||||
|
||||
// 已存在的依赖组合(from→to),用于弹窗候选禁用重复添加(G6)。
|
||||
const existingDepKeys = computed(
|
||||
() => new Set(dependencies.value.map(d => `${d.from_module_id}->${d.to_module_id}`)),
|
||||
)
|
||||
|
||||
// 当前选中高亮的节点 id(点击节点组件内高亮,不导航;G4)。
|
||||
const selectedId = ref<string | null>(null)
|
||||
|
||||
async function onAddDep() {
|
||||
if (!depFrom.value || !depTo.value || depFrom.value === depTo.value) return
|
||||
// 重复依赖拦截(G6):已存在组合,弹窗候选已禁用,此处双保险防直达。
|
||||
if (existingDepKeys.value.has(`${depFrom.value}->${depTo.value}`)) return
|
||||
// 防环(G7):本地 DFS 校验,新增 source→target 会成环则阻止。
|
||||
if (wouldCreateCycle(depFrom.value, depTo.value)) {
|
||||
Message.warning(t('dependencyGraph.cycleAddError'))
|
||||
return
|
||||
}
|
||||
try {
|
||||
await moduleApi.addModuleDependency(props.projectId, depFrom.value, depTo.value, depType.value)
|
||||
showAddDep.value = false
|
||||
@@ -127,11 +148,40 @@ async function onAddDep() {
|
||||
renderGraph()
|
||||
} catch (e) {
|
||||
console.error('[DependencyGraph] 添加依赖失败:', e)
|
||||
// G8:对齐删除依赖的失败反馈,补用户可见错误提示(common.unknownError 通用兜底)。
|
||||
Message.error(t('common.unknownError'))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地防环:在现有依赖图上,若 target 已能到达 source(DFS),则新增 source→target 会成环。
|
||||
* 复用 detectModuleCycles 的 DFS 思路的最小前端校验(纯本地,不请求后端)。
|
||||
*/
|
||||
function wouldCreateCycle(sourceId: string, targetId: string): boolean {
|
||||
const adj = new Map<string, string[]>()
|
||||
for (const d of dependencies.value) {
|
||||
const list = adj.get(d.from_module_id) ?? []
|
||||
list.push(d.to_module_id)
|
||||
adj.set(d.from_module_id, list)
|
||||
}
|
||||
const visited = new Set<string>()
|
||||
const stack = [targetId]
|
||||
while (stack.length > 0) {
|
||||
const cur = stack.pop()!
|
||||
if (cur === sourceId) return true
|
||||
if (visited.has(cur)) continue
|
||||
visited.add(cur)
|
||||
for (const next of adj.get(cur) ?? []) {
|
||||
if (!visited.has(next)) stack.push(next)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function loadModules() {
|
||||
loading.value = true
|
||||
// 数据变更后旧环高亮失效,复位待重新检测(否则残留上一批环节点红框,G5)。
|
||||
cycleNodes.value = new Set()
|
||||
try {
|
||||
const [mods, deps] = await Promise.all([
|
||||
moduleApi.listProjectModules(props.projectId),
|
||||
@@ -147,8 +197,10 @@ async function loadModules() {
|
||||
}
|
||||
}
|
||||
|
||||
function renderGraph() {
|
||||
function renderGraph(opts?: { center?: boolean }) {
|
||||
if (!graph) return
|
||||
// 点选刷新(节点点击)时跳过 centerContent,避免视图跳动;默认(加载/删除/环检测后)居中。
|
||||
const shouldCenter = opts?.center !== false
|
||||
|
||||
const nodeWidth = 200
|
||||
const nodeHeight = 80
|
||||
@@ -168,7 +220,6 @@ function renderGraph() {
|
||||
// 映射回 X6 节点格式
|
||||
const nodes = modules.value.map(m => {
|
||||
const pos = g.node(m.id)
|
||||
const isCycle = cycleNodes.value.has(m.id)
|
||||
return {
|
||||
id: m.id,
|
||||
shape: 'vue-shape',
|
||||
@@ -182,12 +233,12 @@ function renderGraph() {
|
||||
path: m.path,
|
||||
stack: m.stack,
|
||||
gitUrl: m.git_url,
|
||||
// 环节点高亮(G2):vue-shape markup 无 body 选择器,attrs.body 渲染无效,
|
||||
// 改由 isCycle 注入节点 data,ModuleNode 根元素按类描红框。
|
||||
isCycle: cycleNodes.value.has(m.id),
|
||||
// 选中高亮(G4):当前点选节点组件内高亮。
|
||||
selected: selectedId.value === m.id,
|
||||
},
|
||||
// 环节点高亮(红框);renderGraph 是画布唯一渲染入口,高亮必须在这里注入,
|
||||
// 否则会被 graph.fromJSON 整体替换抹除。
|
||||
attrs: isCycle ? {
|
||||
body: { stroke: '#e05050', strokeWidth: 3 },
|
||||
} : undefined,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -207,7 +258,7 @@ function renderGraph() {
|
||||
}))
|
||||
|
||||
graph.fromJSON({ nodes, edges })
|
||||
graph.centerContent()
|
||||
if (shouldCenter) graph.centerContent()
|
||||
}
|
||||
|
||||
function buildGraph() {
|
||||
@@ -228,8 +279,10 @@ function buildGraph() {
|
||||
graph.use(new MiniMap({ width: 200, height: 120, padding: 10 }))
|
||||
|
||||
graph.on('node:click', ({ node }) => {
|
||||
emit('select-module', String(node.id))
|
||||
router.push(`/projects/${props.projectId}`)
|
||||
// 组件内选中高亮(G4):点击节点仅高亮,不 emit 不导航(依赖图是查看/编辑关系,
|
||||
// 跳工程详情与 Tab 语义冲突,原 emit+router.push 是无意义死代码)。
|
||||
selectedId.value = String(node.id)
|
||||
renderGraph({ center: false })
|
||||
})
|
||||
|
||||
renderGraph()
|
||||
|
||||
@@ -84,9 +84,18 @@
|
||||
|
||||
<!-- 文件树视图 -->
|
||||
<div v-show="viewMode === 'tree'" class="explorer-tree">
|
||||
<div v-if="!currentModuleId" class="tree-loading">
|
||||
<!-- B4[PD-P2-8→P2-10]:区分加载中 vs 零工程——currentModuleId 为空且已加载完工程列表
|
||||
说明无工程,显示空态引导(添加工程 CTA,对齐概览模块空态),而非无限 spinner -->
|
||||
<div v-if="!currentModuleId && !mLoaded" class="tree-loading">
|
||||
<span class="spinner"></span>
|
||||
</div>
|
||||
<div v-else-if="!currentModuleId" class="module-empty">
|
||||
<div class="module-empty-icon">📦</div>
|
||||
<p class="module-empty-hint">{{ $t('projectDetail.modulesEmptyHint') }}</p>
|
||||
<div class="module-empty-actions">
|
||||
<button class="btn btn-primary btn-sm" type="button" @click="openAddModule">{{ $t('fileExplorer.addModule') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<FileTree
|
||||
v-else
|
||||
:module-id="currentModuleId"
|
||||
@@ -106,6 +115,7 @@
|
||||
<GitChanges
|
||||
v-if="currentModuleId"
|
||||
:module-id="currentModuleId"
|
||||
:refresh-key="gitStatusRefreshKey"
|
||||
@select-file="onChangeFileSelect"
|
||||
/>
|
||||
</div>
|
||||
@@ -198,7 +208,7 @@
|
||||
* 父组件(ProjectDetail)传入 projectId,本组件按 id 拉工程列表(moduleApi.listProjectModules),
|
||||
* 单工程自动选中;多工程默认选首个 + 提供下拉切换。
|
||||
*/
|
||||
import { ref, computed, watch, reactive, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, computed, watch, reactive, onMounted, onUnmounted, onActivated, onDeactivated } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
|
||||
import { moduleApi, type ProjectModuleRecord, type FileTreeEntry, type GitStatusResult } from '@/api/module'
|
||||
@@ -217,6 +227,9 @@ const modules = ref<ProjectModuleRecord[]>([])
|
||||
const currentModuleId = ref<string>('')
|
||||
const refreshing = ref(false)
|
||||
const moduleDropdownOpen = ref(false)
|
||||
// B4[PD-P2-8→P2-10]:工程列表是否已加载完(首次拉取 resolve)。currentModuleId 为空时用它
|
||||
// 区分「还在加载中(转圈)」vs「确实无工程(空态引导)」,避免无工程时无限 spinner。
|
||||
const mLoaded = ref(false)
|
||||
|
||||
// 工程 CRUD 弹窗
|
||||
const showModuleModal = ref(false)
|
||||
@@ -277,6 +290,9 @@ async function loadModules() {
|
||||
} catch (e) {
|
||||
console.error('[FileExplorer] 加载工程列表失败', e)
|
||||
modules.value = []
|
||||
} finally {
|
||||
// B4[PD-P2-8→P2-10]:标记加载完成(成功/失败都算),供空态判定
|
||||
mLoaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,6 +380,15 @@ function onDetach() {
|
||||
// ── 视图模式切换:文件树 / Git 变更 ──
|
||||
const viewMode = ref<'tree' | 'changes'>('tree')
|
||||
const gitStatusData = ref<GitStatusResult | null>(null)
|
||||
// B2[PD-P1-5]:git 状态刷新信号(自增)。AI 写文件后(下)传 GitChanges 触发重拉,
|
||||
// 替代旧的「tree→changes 切换触发 watch」hack;同时自身重拉 gitStatusData 保徽标计数。
|
||||
const gitStatusRefreshKey = ref(0)
|
||||
// 本组件是否可见(KeepAlive 保活下 onDeactivated 置 false,onActivated 置 true)。
|
||||
// 保活后组件在切走 overview 时仍存活,其 df-data-changed 监听会在后台触发;
|
||||
// 用此守卫避免「用户切到 overview 时文件 tab 后台偷偷切到 changes 视图」。
|
||||
const viewVisible = ref(true)
|
||||
onActivated(() => { viewVisible.value = true })
|
||||
onDeactivated(() => { viewVisible.value = false })
|
||||
|
||||
const changedCount = computed(() => gitStatusData.value?.changed_files.length ?? 0)
|
||||
|
||||
@@ -396,16 +421,22 @@ onUnmounted(() => document.removeEventListener('click', closeDropdown))
|
||||
// AI 工具写入文件后(df-data-changed entity=file)自动跳转变更视图 + 刷新
|
||||
let _unlistenDataChanged: UnlistenFn | null = null
|
||||
onMounted(async () => {
|
||||
_unlistenDataChanged = await listen<{ entity: string; action: string }>('df-data-changed', (e) => {
|
||||
_unlistenDataChanged = await listen<{ entity: string; action: string }>('df-data-changed', async (e) => {
|
||||
if (e.payload.entity !== 'file') return
|
||||
// 刷新 git 状态缓存,使变更计数角标即时更新
|
||||
gitStatusData.value = null
|
||||
if (viewMode.value === 'changes') {
|
||||
// 已在变更视图,触发 GitChanges 重新拉取(切一次 viewMode 触发 watch)
|
||||
viewMode.value = 'tree'
|
||||
viewMode.value = 'changes'
|
||||
} else {
|
||||
// 不在变更视图,自动切换到变更视图,让用户看到 AI 写入的内容
|
||||
if (!currentModuleId.value) return
|
||||
// B2[PD-P1-5]:重拉 git 状态而非置 null —— 置 null 会让 changedCount 徽标瞬闪为 0,
|
||||
// 且 switchToChanges 短路 !gitStatusData,事件后依赖旧缓存不再重新拉取。
|
||||
try {
|
||||
gitStatusData.value = await moduleApi.getModuleGitStatus(currentModuleId.value)
|
||||
} catch {
|
||||
gitStatusData.value = null
|
||||
}
|
||||
// 通知 GitChanges 重拉(已在 changes 视图立即生效;未挂载时挂载即拉最新)。
|
||||
// 后端 git status 有 5s TTL 缓存(module.rs GIT_STATUS_CACHE_TTL),写后立即重拉可能拿到
|
||||
// 缓存旧值,5s 内随用户操作/再次刷新自然更新;跨层绕过缓存留待设计(不强行改事件协议)。
|
||||
gitStatusRefreshKey.value++
|
||||
if (viewMode.value !== 'changes' && viewVisible.value) {
|
||||
// 不在变更视图且本组件可见:自动切换到变更视图,让用户看到 AI 写入的内容
|
||||
viewMode.value = 'changes'
|
||||
}
|
||||
})
|
||||
@@ -757,6 +788,35 @@ async function onRemoveModule() {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* B4[PD-P2-8→P2-10]:零工程空态引导(对齐概览模块空态视觉) */
|
||||
.module-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 32px 16px;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
.module-empty-icon {
|
||||
font-size: 28px;
|
||||
opacity: 0.6;
|
||||
line-height: 1;
|
||||
}
|
||||
.module-empty-hint {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--df-text-dim);
|
||||
line-height: 1.6;
|
||||
max-width: 300px;
|
||||
}
|
||||
.module-empty-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.tree-loading .spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
@@ -39,6 +39,12 @@
|
||||
<span class="expand-icon">{{ expandedPaths.has(entry.path) ? '▾' : '▸' }}</span>
|
||||
<span class="file-icon">📁</span>
|
||||
<span class="file-name">{{ entry.name }}</span>
|
||||
<!-- B3[PD-P1-6]:展开失败 → 行内 ⚠(title 显原因),点击重试;不设整树 error,不炸其他已展开节点 -->
|
||||
<span
|
||||
v-if="toggleErrors.has(entry.path)"
|
||||
class="dir-toggle-error"
|
||||
:title="$t('fileExplorer.loadFailed') + ': ' + toggleErrors.get(entry.path)"
|
||||
>⚠</span>
|
||||
</div>
|
||||
|
||||
<!-- 文件:点击通知父组件加载预览 -->
|
||||
@@ -93,7 +99,7 @@
|
||||
* 这样设计的好处:刷新(refresh)只需 FileExplorer 清空 loadedChildren 重拉根,
|
||||
* 所有展开的子树因 expandedPaths 被清而卸载,下次展开重新拉取,无脏数据。
|
||||
*/
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { ref, reactive, watch, onMounted } from 'vue'
|
||||
import { moduleApi, type FileTreeEntry } from '@/api/module'
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -126,6 +132,10 @@ const emit = defineEmits<{
|
||||
const entries = ref<FileTreeEntry[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
// B3[PD-P1-6]:目录展开失败的行内错误态(路径 → 错误信息)。之前把 toggleDir 失败设到 error.value,
|
||||
// 会让整个子树渲染分支(v-else-if="error")替换成错误文案,一次子目录失败就炸掉已展开的其他节点。
|
||||
// 改为按 path 记录,条目行内显 ⚠ + title 原因,点击重试;成功加载即清除。
|
||||
const toggleErrors = reactive(new Map<string, string>())
|
||||
|
||||
/** 拉取当前 subPath 的条目列表。 */
|
||||
async function loadEntries() {
|
||||
@@ -170,8 +180,11 @@ async function toggleDir(entry: FileTreeEntry) {
|
||||
loading.value = true
|
||||
const res = await moduleApi.getModuleFileTree(props.moduleId, entry.path)
|
||||
props.loadedChildren.set(entry.path, res.entries)
|
||||
toggleErrors.delete(entry.path)
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : String(e)
|
||||
// B3[PD-P1-6]:行内错误态(该目录 ⚠ + title 原因),不设整树 error;不展开,点击可重试
|
||||
toggleErrors.set(entry.path, e instanceof Error ? e.message : String(e))
|
||||
return
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -335,6 +348,14 @@ watch(
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* B3[PD-P1-6]:目录展开失败的行内 ⚠(flex-shrink 防挤压,危险色) */
|
||||
.dir-toggle-error {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
color: #e05050;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Git 状态徽章 */
|
||||
.git-badge {
|
||||
flex-shrink: 0;
|
||||
|
||||
@@ -183,6 +183,8 @@ const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
moduleId: string
|
||||
/** B2[PD-P1-5]:外部(FileExplorer df-data-changed)自增刷新信号,变化时重拉 git 状态。 */
|
||||
refreshKey?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -531,6 +533,13 @@ function showCommitFileDiff(path: string) {
|
||||
}
|
||||
|
||||
watch(() => props.moduleId, loadStatus, { immediate: true })
|
||||
|
||||
// B2[PD-P1-5]:外部刷新信号(AI 写文件后 FileExplorer 自增)变化 → 重拉 git 状态。
|
||||
// 需保留当前历史 Tab/详情展示(只刷 gitStatus/commits,不动 showHistory/selectedCommit)。
|
||||
watch(() => props.refreshKey, () => {
|
||||
if (props.refreshKey === undefined) return
|
||||
loadStatus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="module-node" :class="{ active: selected }">
|
||||
<div class="module-node" :class="{ active: isSelected, 'module-node--cycle': data.isCycle }">
|
||||
<div class="module-node__header">
|
||||
<span class="module-node__icon">📦</span>
|
||||
<span class="module-node__name">{{ data.name }}</span>
|
||||
@@ -16,39 +16,45 @@ import { computed } from 'vue'
|
||||
import { parseJsonArray } from '@/utils/json'
|
||||
|
||||
const props = defineProps<{
|
||||
data: {
|
||||
name: string
|
||||
path: string
|
||||
stack: string | null
|
||||
gitUrl: string | null
|
||||
}
|
||||
// x6-vue-shape 渲染组件时注入的 props 是 node + graph(view.js:48 h(component, { node, graph })),
|
||||
// 节点数据经 node.getData() 取,而非 data prop。data prop 仅作非 x6 直用场景兜底。
|
||||
node?: any
|
||||
graph?: any
|
||||
data?: Record<string, unknown>
|
||||
selected?: boolean
|
||||
}>()
|
||||
|
||||
// 数据源:优先 node.getData()(vue-shape 注入),兜底 props.data(兼容直接传数据场景)。
|
||||
// 原 data 结构 { name, path, stack, gitUrl } + 依赖图注入的 { isCycle, selected }。
|
||||
const data = computed<Record<string, any>>(() => props.node?.getData?.() ?? props.data ?? {})
|
||||
|
||||
// 选中态:data.selected(vue-shape 场景由依赖图注入)或 selected prop 直传兜底。
|
||||
const isSelected = computed(() => !!props.selected || !!data.value.selected)
|
||||
|
||||
// 路径截断:保留首段根目录(盘符/家目录锚点)+末尾目录,丢中间段。
|
||||
// 旧实现 '...' + slice(-29) 仅留末尾,丢失前缀致同末不同源目录难辨(如 C:/a 与 D:/b 同名)。
|
||||
// 现:根段(/第一段分隔符前)+ … + 末尾(容纳 32 字符上限内)。短路径直返。
|
||||
const truncatedPath = computed(() => {
|
||||
const p = props.data?.path || ''
|
||||
const p = data.value?.path || ''
|
||||
if (p.length <= 32) return p
|
||||
// 拆根段:取首个路径分隔符(/ 或 \,跨平台)前部分作锚点(盘符 C: 或家目录片段)
|
||||
const sepMatch = p.match(/[\\/]/)
|
||||
if (!sepMatch) {
|
||||
// 无分隔符的单段长名,退化为末尾
|
||||
return '...' + p.slice(-29)
|
||||
return '…' + p.slice(-29)
|
||||
}
|
||||
const sepIdx = sepMatch.index! + 1
|
||||
const head = p.slice(0, sepIdx)
|
||||
const tailBudget = 32 - head.length - 1 // -1 留给 …(此处用 …)
|
||||
const tailBudget = 32 - head.length - 1 // -1 留给 …
|
||||
if (tailBudget < 4) {
|
||||
// 根段本身就长,退化为末尾(保留原行为)
|
||||
return '...' + p.slice(-29)
|
||||
return '…' + p.slice(-29)
|
||||
}
|
||||
const tail = p.slice(-tailBudget)
|
||||
return head + '…' + tail
|
||||
})
|
||||
|
||||
const stackList = computed(() => parseJsonArray(props.data?.stack).slice(0, 3))
|
||||
const stackList = computed(() => parseJsonArray(data.value?.stack).slice(0, 3))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -70,6 +76,12 @@ const stackList = computed(() => parseJsonArray(props.data?.stack).slice(0, 3))
|
||||
border-color: #5378e8;
|
||||
box-shadow: 0 0 0 2px rgba(83, 120, 232, 0.3);
|
||||
}
|
||||
/* 环节点红框高亮(G2):vue-shape markup 无 body 选择器,原 attrs.body 渲染无效,
|
||||
改由依赖图把 isCycle 注入节点 data,组件根元素按类描边。 */
|
||||
.module-node--cycle {
|
||||
border-color: #e05050;
|
||||
box-shadow: 0 0 0 2px rgba(224, 80, 80, 0.3);
|
||||
}
|
||||
.module-node__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user