From 0dc5c3ca55411e31c39e1683c9779ae044c61794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Mon, 27 Jul 2026 18:54:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=20Skill=20=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E4=B8=8E=E7=BC=96=E8=BE=91=E5=B7=A5=E4=BD=9C=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 统一列表、分类、新建与详情页的产品交互 - 提供 Markdown 实时编辑、源码与脚本资源工作台 - 实现能力自动保存、导入导出及完整状态反馈 --- easyflow-ui-admin/app/package.json | 18 +- .../components/headerSearch/HeaderSearch.vue | 5 + .../app/src/components/page/PageData.test.ts | 28 + .../app/src/components/page/PageData.vue | 55 +- .../app/src/components/page/PageSide.test.ts | 141 ++ .../app/src/components/page/PageSide.vue | 567 +++-- .../ai/skill/SkillCapabilityPanel.test.ts | 457 ++++ .../views/ai/skill/SkillCapabilityPanel.vue | 1359 +++++++++++ .../ai/skill/SkillCategoryFormDialog.test.ts | 132 + .../ai/skill/SkillCategoryFormDialog.vue | 245 ++ .../views/ai/skill/SkillCreateDialog.test.ts | 106 + .../src/views/ai/skill/SkillCreateDialog.vue | 235 ++ .../src/views/ai/skill/SkillDetail.test.ts | 449 ++++ .../app/src/views/ai/skill/SkillDetail.vue | 1022 +++++--- .../app/src/views/ai/skill/SkillList.test.ts | 216 ++ .../app/src/views/ai/skill/SkillList.vue | 2156 +++++++++++++++-- .../ai/skill/SkillResourceWorkbench.test.ts | 380 +++ .../views/ai/skill/SkillResourceWorkbench.vue | 1881 ++++++++++++++ .../ai/skill/SkillSettingsDialog.test.ts | 107 + .../views/ai/skill/SkillSettingsDialog.vue | 241 ++ .../app/src/views/ai/skill/api.ts | 262 +- .../views/ai/skill/skill-api-error.test.ts | 55 + .../app/src/views/ai/skill/skill-api-error.ts | 65 + .../views/ai/skill/skill-capability.test.ts | 113 + .../src/views/ai/skill/skill-capability.ts | 113 + .../src/views/ai/skill/skill-category.test.ts | 90 + .../app/src/views/ai/skill/skill-category.ts | 98 + .../src/views/ai/skill/skill-create.test.ts | 40 + .../app/src/views/ai/skill/skill-create.ts | 52 + .../src/views/ai/skill/skill-draft.test.ts | 96 + .../app/src/views/ai/skill/skill-draft.ts | 65 + .../ai/skill/skill-file-download.test.ts | 34 + .../src/views/ai/skill/skill-file-download.ts | 16 + .../skill/skill-file-representation.test.ts | 16 + .../ai/skill/skill-file-representation.ts | 49 + .../src/views/ai/skill/skill-import.test.ts | 116 + .../app/src/views/ai/skill/skill-import.ts | 59 + .../src/views/ai/skill/skill-markdown.test.ts | 162 ++ .../app/src/views/ai/skill/skill-markdown.ts | 165 ++ .../app/src/views/ai/skill/types.ts | 234 +- .../ai/skill/use-skill-file-buffers.test.ts | 91 + .../views/ai/skill/use-skill-file-buffers.ts | 133 + .../views/system/approval/ApprovalDetail.vue | 23 +- .../views/system/approval/ApprovalManage.vue | 29 + .../system/approval/approval-format.test.ts | 14 + .../views/system/approval/approval-format.ts | 24 + easyflow-ui-admin/cspell.json | 1 + .../base/design/src/design-tokens/default.css | 21 + .../packages/@core/base/icons/src/lucide.ts | 9 +- .../@core/ui-kit/editor-ui/build.config.ts | 21 + .../@core/ui-kit/editor-ui/package.json | 58 + .../ui-kit/editor-ui/src/CodeEditor.test.ts | 85 + .../@core/ui-kit/editor-ui/src/CodeEditor.vue | 419 ++++ .../ui-kit/editor-ui/src/CodeViewer.test.ts | 118 + .../@core/ui-kit/editor-ui/src/CodeViewer.vue | 189 ++ .../src/MarkdownLiveEditor.security.test.ts | 123 + .../editor-ui/src/MarkdownLiveEditor.test.ts | 410 ++++ .../editor-ui/src/MarkdownLiveEditor.vue | 630 +++++ .../editor-ui/src/MarkdownSourceEditor.vue | 43 + .../@core/ui-kit/editor-ui/src/index.ts | 6 + .../editor-ui/src/markdown-fidelity.test.ts | 67 + .../ui-kit/editor-ui/src/markdown-fidelity.ts | 43 + .../editor-ui/src/markdown-security.test.ts | 53 + .../ui-kit/editor-ui/src/markdown-security.ts | 47 + .../editor-ui/src/milkdown-frontmatter.ts | 132 + .../@core/ui-kit/editor-ui/src/types.ts | 20 + .../@core/ui-kit/editor-ui/tsconfig.json | 6 + .../packages/effects/common-ui/package.json | 1 + .../DocumentEditorWorkbench.vue | 358 +++ .../document-editor/EditorStatusBar.vue | 91 + .../document-editor/FileTreeNode.vue | 475 ++++ .../document-editor/FileTreePanel.vue | 263 ++ .../document-editor/ValidationPanel.vue | 151 ++ .../__tests__/FileTreePanel.test.ts | 200 ++ .../src/components/document-editor/index.ts | 5 + .../src/components/document-editor/types.ts | 17 + .../effects/common-ui/src/components/index.ts | 1 + easyflow-ui-admin/pnpm-lock.yaml | 1135 ++++++++- 78 files changed, 16413 insertions(+), 799 deletions(-) create mode 100644 easyflow-ui-admin/app/src/components/page/PageData.test.ts create mode 100644 easyflow-ui-admin/app/src/components/page/PageSide.test.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/SkillCapabilityPanel.test.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/SkillCapabilityPanel.vue create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/SkillCategoryFormDialog.test.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/SkillCategoryFormDialog.vue create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/SkillCreateDialog.test.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/SkillCreateDialog.vue create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/SkillDetail.test.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/SkillList.test.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/SkillResourceWorkbench.test.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/SkillResourceWorkbench.vue create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/SkillSettingsDialog.test.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/SkillSettingsDialog.vue create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/skill-api-error.test.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/skill-api-error.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/skill-capability.test.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/skill-capability.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/skill-category.test.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/skill-category.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/skill-create.test.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/skill-create.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/skill-draft.test.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/skill-draft.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/skill-file-download.test.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/skill-file-download.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/skill-file-representation.test.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/skill-file-representation.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/skill-import.test.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/skill-import.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/skill-markdown.test.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/skill-markdown.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/use-skill-file-buffers.test.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/skill/use-skill-file-buffers.ts create mode 100644 easyflow-ui-admin/app/src/views/system/approval/approval-format.test.ts create mode 100644 easyflow-ui-admin/app/src/views/system/approval/approval-format.ts create mode 100644 easyflow-ui-admin/packages/@core/ui-kit/editor-ui/build.config.ts create mode 100644 easyflow-ui-admin/packages/@core/ui-kit/editor-ui/package.json create mode 100644 easyflow-ui-admin/packages/@core/ui-kit/editor-ui/src/CodeEditor.test.ts create mode 100644 easyflow-ui-admin/packages/@core/ui-kit/editor-ui/src/CodeEditor.vue create mode 100644 easyflow-ui-admin/packages/@core/ui-kit/editor-ui/src/CodeViewer.test.ts create mode 100644 easyflow-ui-admin/packages/@core/ui-kit/editor-ui/src/CodeViewer.vue create mode 100644 easyflow-ui-admin/packages/@core/ui-kit/editor-ui/src/MarkdownLiveEditor.security.test.ts create mode 100644 easyflow-ui-admin/packages/@core/ui-kit/editor-ui/src/MarkdownLiveEditor.test.ts create mode 100644 easyflow-ui-admin/packages/@core/ui-kit/editor-ui/src/MarkdownLiveEditor.vue create mode 100644 easyflow-ui-admin/packages/@core/ui-kit/editor-ui/src/MarkdownSourceEditor.vue create mode 100644 easyflow-ui-admin/packages/@core/ui-kit/editor-ui/src/index.ts create mode 100644 easyflow-ui-admin/packages/@core/ui-kit/editor-ui/src/markdown-fidelity.test.ts create mode 100644 easyflow-ui-admin/packages/@core/ui-kit/editor-ui/src/markdown-fidelity.ts create mode 100644 easyflow-ui-admin/packages/@core/ui-kit/editor-ui/src/markdown-security.test.ts create mode 100644 easyflow-ui-admin/packages/@core/ui-kit/editor-ui/src/markdown-security.ts create mode 100644 easyflow-ui-admin/packages/@core/ui-kit/editor-ui/src/milkdown-frontmatter.ts create mode 100644 easyflow-ui-admin/packages/@core/ui-kit/editor-ui/src/types.ts create mode 100644 easyflow-ui-admin/packages/@core/ui-kit/editor-ui/tsconfig.json create mode 100644 easyflow-ui-admin/packages/effects/common-ui/src/components/document-editor/DocumentEditorWorkbench.vue create mode 100644 easyflow-ui-admin/packages/effects/common-ui/src/components/document-editor/EditorStatusBar.vue create mode 100644 easyflow-ui-admin/packages/effects/common-ui/src/components/document-editor/FileTreeNode.vue create mode 100644 easyflow-ui-admin/packages/effects/common-ui/src/components/document-editor/FileTreePanel.vue create mode 100644 easyflow-ui-admin/packages/effects/common-ui/src/components/document-editor/ValidationPanel.vue create mode 100644 easyflow-ui-admin/packages/effects/common-ui/src/components/document-editor/__tests__/FileTreePanel.test.ts create mode 100644 easyflow-ui-admin/packages/effects/common-ui/src/components/document-editor/index.ts create mode 100644 easyflow-ui-admin/packages/effects/common-ui/src/components/document-editor/types.ts diff --git a/easyflow-ui-admin/app/package.json b/easyflow-ui-admin/app/package.json index 91f278c7..9391616a 100644 --- a/easyflow-ui-admin/app/package.json +++ b/easyflow-ui-admin/app/package.json @@ -13,6 +13,14 @@ "#/*": "./src/*" }, "dependencies": { + "@codemirror/commands": "^6.10.2", + "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-python": "^6.2.1", + "@codemirror/language": "^6.12.2", + "@codemirror/legacy-modes": "^6.5.1", + "@codemirror/state": "^6.5.4", + "@codemirror/view": "^6.39.15", + "@easyflow-core/editor-ui": "workspace:*", "@easyflow-core/shadcn-ui": "workspace:*", "@easyflow/access": "workspace:*", "@easyflow/common-ui": "workspace:*", @@ -28,13 +36,6 @@ "@easyflow/styles": "workspace:*", "@easyflow/types": "workspace:*", "@easyflow/utils": "workspace:*", - "@codemirror/commands": "^6.10.2", - "@codemirror/lang-javascript": "^6.2.4", - "@codemirror/lang-python": "^6.2.1", - "@codemirror/language": "^6.12.2", - "@codemirror/legacy-modes": "^6.5.1", - "@codemirror/state": "^6.5.4", - "@codemirror/view": "^6.39.15", "@element-plus/icons-vue": "^2.3.2", "@tinyflow-ai/vue": "workspace:*", "@ungap/structured-clone": "1.3.0", @@ -55,7 +56,8 @@ "vue-element-plus-x": "catalog:", "vue-router": "catalog:", "vue3-json-viewer": "^2.4.1", - "wicg-inert": "3.1.3" + "wicg-inert": "3.1.3", + "yaml": "^2.8.1" }, "devDependencies": { "@types/node-forge": "^1.3.14", diff --git a/easyflow-ui-admin/app/src/components/headerSearch/HeaderSearch.vue b/easyflow-ui-admin/app/src/components/headerSearch/HeaderSearch.vue index a9610035..d23f6786 100644 --- a/easyflow-ui-admin/app/src/components/headerSearch/HeaderSearch.vue +++ b/easyflow-ui-admin/app/src/components/headerSearch/HeaderSearch.vue @@ -73,6 +73,10 @@ const handleReset = () => { emit('search', ''); }; +defineExpose({ + reset: handleReset, +}); + const handleButtonClick = (button) => { emitButtonEvent({ type: 'button', @@ -133,6 +137,7 @@ const handleDropdownClick = (button) => { :type="button.type || 'default'" :icon="button.icon" :disabled="button.disabled" + :loading="button.loading" v-access:code="button.permission" @click="handleButtonClick(button)" > diff --git a/easyflow-ui-admin/app/src/components/page/PageData.test.ts b/easyflow-ui-admin/app/src/components/page/PageData.test.ts new file mode 100644 index 00000000..7707beb3 --- /dev/null +++ b/easyflow-ui-admin/app/src/components/page/PageData.test.ts @@ -0,0 +1,28 @@ +import { flushPromises, mount } from '@vue/test-utils'; + +import { describe, expect, it, vi } from 'vitest'; + +import PageData from './PageData.vue'; + +describe('page data recovery', () => { + it('shows a retry action after a page request fails', async () => { + const get = vi + .fn() + .mockRejectedValueOnce(new Error('network unavailable')) + .mockResolvedValueOnce({ data: { records: [], totalRow: 0 } }); + const wrapper = mount(PageData, { + global: { + directives: { loading: {} }, + }, + props: { pageUrl: '/page', requestClient: { get } }, + }); + + await flushPromises(); + expect(wrapper.text()).toContain('数据加载失败,请重试'); + + await wrapper.get('button').trigger('click'); + await flushPromises(); + expect(get).toHaveBeenCalledTimes(2); + expect(wrapper.text()).not.toContain('数据加载失败,请重试'); + }); +}); diff --git a/easyflow-ui-admin/app/src/components/page/PageData.vue b/easyflow-ui-admin/app/src/components/page/PageData.vue index a0c84663..35a0b78b 100644 --- a/easyflow-ui-admin/app/src/components/page/PageData.vue +++ b/easyflow-ui-admin/app/src/components/page/PageData.vue @@ -3,7 +3,7 @@ import { onMounted, reactive, ref, watch } from 'vue'; import { preferences } from '@easyflow/preferences'; -import { ElEmpty, ElPagination } from 'element-plus'; +import { ElButton, ElEmpty, ElPagination } from 'element-plus'; import { api } from '#/api/request'; import { getEmptyStateImageUrl } from '#/utils/assets'; @@ -28,7 +28,9 @@ const props = withDefaults(defineProps(), { // 响应式数据 const pageList = ref([]); const loading = ref(false); +const loadError = ref(); const queryParams = ref>({}); +let pageRequest = 0; const pageInfo = reactive({ pageNumber: 1, @@ -38,22 +40,20 @@ const pageInfo = reactive({ // 模拟 API 调用 - 这里需要根据你的实际 API 调用方式调整 const doGet = async (params: Record) => { - loading.value = true; - try { - // 这里替换为你的实际 API 调用 - // 例如:return await api.get(props.pageUrl, { params }) - const response = await props.requestClient.get(`${props.pageUrl}`, { - params, - }); - const data = await response.data; - return { data }; - } finally { - loading.value = false; - } + // 这里替换为你的实际 API 调用 + // 例如:return await api.get(props.pageUrl, { params }) + const response = await props.requestClient.get(`${props.pageUrl}`, { + params, + }); + const data = await response.data; + return { data }; }; // 获取页面数据 const getPageList = async () => { + const request = ++pageRequest; + loading.value = true; + loadError.value = undefined; try { const res = await doGet({ pageNumber: pageInfo.pageNumber, @@ -61,12 +61,18 @@ const getPageList = async () => { ...props.extraQueryParams, ...queryParams.value, }); - pageList.value = res.data?.records || []; - pageInfo.total = res.data?.totalRow || 0; + if (request === pageRequest) { + pageList.value = res.data?.records || []; + pageInfo.total = res.data?.totalRow || 0; + } } catch (error) { - console.error('get data error:', error); - pageList.value = []; - pageInfo.total = 0; + if (request === pageRequest) { + loadError.value = error; + pageList.value = []; + pageInfo.total = 0; + } + } finally { + if (request === pageRequest) loading.value = false; } }; @@ -104,7 +110,6 @@ const setQuery = (newQueryParams: Record) => { pageInfo.pageNumber = 1; pageInfo.pageSize = props.pageSize; queryParams.value = newQueryParams; - getPageList(); }; // 暴露方法给父组件 @@ -116,7 +121,7 @@ defineExpose({ // 监听器 watch( - [() => pageInfo.pageNumber, () => pageInfo.pageSize], + [() => pageInfo.pageNumber, () => pageInfo.pageSize, () => queryParams.value], () => { getPageList(); }, @@ -150,6 +155,16 @@ onMounted(() => { /> + + + 重新加载 + + diff --git a/easyflow-ui-admin/app/src/components/page/PageSide.test.ts b/easyflow-ui-admin/app/src/components/page/PageSide.test.ts new file mode 100644 index 00000000..6e0eee71 --- /dev/null +++ b/easyflow-ui-admin/app/src/components/page/PageSide.test.ts @@ -0,0 +1,141 @@ +import { flushPromises, mount } from '@vue/test-utils'; +import { defineComponent, h } from 'vue'; + +import { describe, expect, it, vi } from 'vitest'; + +import PageSide from './PageSide.vue'; + +/* eslint-disable vue/one-component-per-file -- Inline dropdown stubs keep this component test focused. */ + +const DropdownStub = defineComponent({ + setup(_, { slots }) { + return () => + h('div', { class: 'dropdown-stub' }, [ + slots.default?.(), + slots.dropdown?.(), + ]); + }, +}); +const DropdownMenuStub = defineComponent({ + setup(_, { slots }) { + return () => h('div', { class: 'dropdown-menu-stub' }, slots.default?.()); + }, +}); +const DropdownItemStub = defineComponent({ + props: { disabled: Boolean }, + emits: ['click'], + setup(props, { emit, slots }) { + return () => + h( + 'div', + { + class: 'dropdown-item-stub', + 'data-disabled': String(props.disabled), + onClick: () => { + if (!props.disabled) emit('click'); + }, + }, + slots.default?.(), + ); + }, +}); + +describe('page side', () => { + it('keeps the existing flat menu selection and footer behavior', async () => { + const add = vi.fn(); + const wrapper = mount(PageSide, { + props: { + defaultSelected: '', + footerButton: { label: '添加', onClick: add }, + labelKey: 'name', + menus: [ + { id: '', name: '全部' }, + { id: 'docs', name: '文档' }, + ], + valueKey: 'id', + }, + }); + + expect(wrapper.findAll('.page-side__item')).toHaveLength(2); + expect(wrapper.findAll('.page-side__item')[0]?.classes()).toContain( + 'is-selected', + ); + + await wrapper.findAll('.page-side__item')[1]?.trigger('click'); + expect(wrapper.emitted('change')?.[0]?.[0]).toMatchObject({ id: 'docs' }); + + await wrapper.get('.page-side__footer').trigger('click'); + expect(add).toHaveBeenCalledOnce(); + }); + + it('renders nested menus and emits the selected child', async () => { + const wrapper = mount(PageSide, { + props: { + childrenKey: 'children', + defaultSelected: '', + labelKey: 'name', + menus: [ + { id: '', name: '全部' }, + { + children: [{ id: 2, name: '平台' }], + id: 1, + name: '研发', + }, + ], + valueKey: 'id', + }, + }); + await flushPromises(); + + expect(wrapper.findAll('.el-tree-node__content')).toHaveLength(3); + const all = wrapper + .findAll('.el-tree-node__content') + .find((node) => node.text().includes('全部')); + expect(all?.element.parentElement?.classList).toContain('is-current'); + const child = wrapper + .findAll('.el-tree-node__content') + .find((node) => node.text().includes('平台')); + expect(child).toBeTruthy(); + await child?.trigger('click'); + expect(wrapper.emitted('change')?.at(-1)?.[0]).toMatchObject({ id: 2 }); + }); + + it('supports per-node disabled action states', async () => { + const edit = vi.fn(); + const remove = vi.fn(); + const wrapper = mount(PageSide, { + global: { + stubs: { + ElDropdown: DropdownStub, + ElDropdownItem: DropdownItemStub, + ElDropdownMenu: DropdownMenuStub, + }, + }, + props: { + controlBtns: [ + { + disabled: () => true, + label: '编辑', + onClick: edit, + }, + { label: '删除', onClick: remove }, + ], + labelKey: 'name', + menus: [ + { id: '', name: '全部' }, + { id: 1, name: '研发' }, + ], + valueKey: 'id', + }, + }); + + const actions = wrapper.findAll('.dropdown-item-stub'); + expect(actions).toHaveLength(2); + expect(actions[0]?.attributes('data-disabled')).toBe('true'); + await actions[0]?.trigger('click'); + expect(edit).not.toHaveBeenCalled(); + + await actions[1]?.trigger('click'); + expect(remove).toHaveBeenCalledOnce(); + }); +}); diff --git a/easyflow-ui-admin/app/src/components/page/PageSide.vue b/easyflow-ui-admin/app/src/components/page/PageSide.vue index 0bd88ee6..fe22fe97 100644 --- a/easyflow-ui-admin/app/src/components/page/PageSide.vue +++ b/easyflow-ui-admin/app/src/components/page/PageSide.vue @@ -1,10 +1,9 @@