From 9c6b728704ca0b802b9ddc670fcfeef3f6a98277 Mon Sep 17 00:00:00 2001 From: matevip Date: Fri, 1 May 2026 09:52:04 +0800 Subject: [PATCH] feat(ui): MateClaw skill detail drawer + shared McPagination / McConfirm components --- mateclaw-ui/src/App.vue | 4 + .../src/components/common/McConfirmHost.vue | 247 ++++ .../src/components/common/McPagination.vue | 322 +++++ .../src/components/common/useConfirm.ts | 62 + mateclaw-ui/src/i18n/locales/en-US.ts | 29 + mateclaw-ui/src/i18n/locales/zh-CN.ts | 29 + mateclaw-ui/src/views/ChatConsole.vue | 16 +- .../src/views/Security/Activity/index.vue | 34 +- mateclaw-ui/src/views/SkillMarket.vue | 1139 ++++++++++++++--- 9 files changed, 1638 insertions(+), 244 deletions(-) create mode 100644 mateclaw-ui/src/components/common/McConfirmHost.vue create mode 100644 mateclaw-ui/src/components/common/McPagination.vue create mode 100644 mateclaw-ui/src/components/common/useConfirm.ts diff --git a/mateclaw-ui/src/App.vue b/mateclaw-ui/src/App.vue index e208928d..5a197b96 100644 --- a/mateclaw-ui/src/App.vue +++ b/mateclaw-ui/src/App.vue @@ -1,6 +1,9 @@ @@ -11,6 +14,7 @@ import en from 'element-plus/es/locale/lang/en' import zhCn from 'element-plus/es/locale/lang/zh-cn' import { currentLocale } from '@/i18n' import { useThemeStore } from '@/stores/useThemeStore' +import McConfirmHost from '@/components/common/McConfirmHost.vue' // Initialize theme — applies .dark class to immediately useThemeStore() diff --git a/mateclaw-ui/src/components/common/McConfirmHost.vue b/mateclaw-ui/src/components/common/McConfirmHost.vue new file mode 100644 index 00000000..8869fa18 --- /dev/null +++ b/mateclaw-ui/src/components/common/McConfirmHost.vue @@ -0,0 +1,247 @@ + + + + + diff --git a/mateclaw-ui/src/components/common/McPagination.vue b/mateclaw-ui/src/components/common/McPagination.vue new file mode 100644 index 00000000..30c8dcad --- /dev/null +++ b/mateclaw-ui/src/components/common/McPagination.vue @@ -0,0 +1,322 @@ + + + + + diff --git a/mateclaw-ui/src/components/common/useConfirm.ts b/mateclaw-ui/src/components/common/useConfirm.ts new file mode 100644 index 00000000..3119abf4 --- /dev/null +++ b/mateclaw-ui/src/components/common/useConfirm.ts @@ -0,0 +1,62 @@ +import { shallowRef } from 'vue' + +/** + * MateClaw confirm dialog — imperative API. + * + * Usage: + * const ok = await mcConfirm({ + * title: '确认', + * message: '确定删除这个会话?此操作不可恢复。', + * tone: 'danger', + * }) + * if (!ok) return + * + * Resolves with `true` on confirm, `false` on cancel / overlay click / + * Esc. Never rejects — that pattern (which `ElMessageBox.confirm` uses) + * forces every caller to add a noop `.catch`, and turns ordinary user + * cancellation into a console-looking "error". + */ +export type ConfirmTone = 'default' | 'primary' | 'danger' + +export interface ConfirmOptions { + /** Header text. Defaults to a generic "Confirm" string. */ + title?: string + /** Body copy. Required — this is the question being asked. */ + message: string + /** Confirm button label. Defaults to common.confirm. */ + confirmText?: string + /** Cancel button label. Defaults to common.cancel. */ + cancelText?: string + /** Visual tone of the icon + confirm button. */ + tone?: ConfirmTone +} + +export interface ActiveConfirm extends ConfirmOptions { + resolve: (ok: boolean) => void +} + +/** + * Singleton slot. The host component watches this and renders one dialog + * at a time. We use `shallowRef` because the resolve fn is non-reactive + * and the options blob is replaced wholesale, never mutated. + */ +export const activeConfirm = shallowRef(null) + +export function mcConfirm(opts: ConfirmOptions): Promise { + return new Promise((resolve) => { + // If a previous prompt is somehow still open (unlikely — clicks + // close it before resolving), cancel it so we don't leak a never- + // settled promise. + if (activeConfirm.value) { + activeConfirm.value.resolve(false) + } + activeConfirm.value = { ...opts, resolve } + }) +} + +export function resolveConfirm(ok: boolean) { + const current = activeConfirm.value + if (!current) return + activeConfirm.value = null + current.resolve(ok) +} diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index 39d7494e..fbbff619 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -37,6 +37,14 @@ export default { collapse: 'Collapse', enable: 'Enable', disable: 'Disable', + pager: { + total: '{n} total', + perPage: '{n}/page', + prev: 'Previous', + next: 'Next', + jumpTo: 'Go to', + pageSuffix: '', + }, }, auth: { changePassword: 'Change Password', @@ -2012,6 +2020,7 @@ export default { modal: { configureTitle: 'Configure Skill', newTitle: 'New Skill', + newSimpleHint: 'Just the bones — keep configuring icon, body, and tools in the detail panel after this.', }, fields: { name: 'Name', @@ -2062,6 +2071,9 @@ export default { delete: 'Delete', saveChanges: 'Save Changes', createSkill: 'Create Skill', + edit: 'Edit', + save: 'Save', + cancel: 'Cancel', }, preflight: { title: 'Pre-flight check', @@ -2079,6 +2091,8 @@ export default { }, detail: { title: 'Skill detail', + overview: 'Overview', + body: 'Body', manifest: 'Manifest', tools: 'Tools', features: 'Features', @@ -2090,6 +2104,20 @@ export default { runtimeError: 'Runtime error', path: 'Resolved path', synthesized: 'Source', + identitySection: 'Identity', + manifestProjectedSection: 'From SKILL.md', + manifestProjectedHint: 'These fields mirror the SKILL.md frontmatter — editing the row directly does not stick (the next resolve overwrites it). Edit the SKILL.md body below to change them.', + displayOverridesSection: 'Display & tags', + displayOverridesHint: 'DB-only — these are not overwritten by the resolver.', + bodyHint: 'SKILL.md body — injected into the LLM system prompt at load time.', + sourceCodeHint: 'Executable body (dynamic skills only). Re-runs on next call after save.', + viewRawManifest: 'View parsed runtime manifest', + noBody: 'No SKILL.md body has been provided yet.', + builtinReadonly: 'Built-in skill: only nameZh / nameEn / tags / description / body are editable.', + virtualReadonly: 'Virtual MCP-derived skill — edit it on the MCP Connections page instead.', + editSource: 'Edit SKILL.md', + editingSource: 'Editing SKILL.md', + sourceSavedReprojection: 'Saved. The next resolve will sync icon/version/author and friends from the new frontmatter.', noManifest: 'This skill does not declare a v3 manifest. Legacy fields apply.', noTools: 'No allowed-tools declared by this skill. The LLM will fall back to the global tool set when this skill is bound to an agent.', noFeatures: 'No features[] matrix declared. The skill is treated as a single default feature.', @@ -2116,6 +2144,7 @@ export default { }, messages: { saveFailed: 'Failed to save skill', + saveSuccess: 'Saved', deleteConfirm: 'Are you sure you want to delete this skill? This cannot be undone.', deleteTitle: 'Confirm Delete', deleteFailed: 'Failed to delete skill', diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index 1317f3ba..94bfe1e8 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -37,6 +37,14 @@ export default { disable: '停用', refresh: '刷新', collapse: '收起', + pager: { + total: '共 {n} 条', + perPage: '{n} 条/页', + prev: '上一页', + next: '下一页', + jumpTo: '前往', + pageSuffix: '页', + }, }, auth: { changePassword: '修改密码', @@ -2014,6 +2022,7 @@ export default { modal: { configureTitle: '配置技能', newTitle: '新建技能', + newSimpleHint: '先建好骨架,下一步在详情面板里继续配置图标、正文和工具。', }, fields: { name: '名称', @@ -2064,6 +2073,9 @@ export default { delete: '删除', saveChanges: '保存更改', createSkill: '创建技能', + edit: '编辑', + save: '保存', + cancel: '取消', }, preflight: { title: '装前检查', @@ -2081,6 +2093,8 @@ export default { }, detail: { title: '技能详情', + overview: '概览', + body: '正文', manifest: 'Manifest', tools: '工具', features: '特性', @@ -2092,6 +2106,20 @@ export default { runtimeError: '运行时错误', path: '解析路径', synthesized: '来源', + identitySection: '身份', + manifestProjectedSection: '来自 SKILL.md', + manifestProjectedHint: '这些字段是 SKILL.md frontmatter 的镜像 — 直接改 row 不持久(下次解析时被覆盖)。要改请编辑下方 SKILL.md 正文。', + displayOverridesSection: '显示与标签', + displayOverridesHint: '仅存于数据库,不会被解析器覆盖。', + bodyHint: 'SKILL.md 正文 — 部署后注入到 LLM 的系统提示。', + sourceCodeHint: '执行体(仅 dynamic 类型)。修改后下次调用立即生效。', + viewRawManifest: '查看解析后的运行时 manifest', + noBody: '尚未提供 SKILL.md 正文。', + builtinReadonly: '内置技能:仅 nameZh / nameEn / 标签 / description / 正文可改。', + virtualReadonly: 'MCP 派生的虚拟技能不可在此编辑,请去 MCP 连接页修改。', + editSource: '编辑 SKILL.md', + editingSource: '编辑 SKILL.md', + sourceSavedReprojection: '已保存。下次解析将根据新 frontmatter 同步 icon/version/author 等字段。', noManifest: '该技能未声明 v3 manifest,使用旧字段。', noTools: '该 skill 未声明 allowed-tools。绑定到 agent 时 LLM 将回退到全局工具集。', noFeatures: '未声明 features[] 矩阵,技能被视为单一默认特性。', @@ -2118,6 +2146,7 @@ export default { }, messages: { saveFailed: '保存技能失败', + saveSuccess: '已保存', deleteConfirm: '确定要删除这个技能吗?此操作不可撤销。', deleteTitle: '确认删除', deleteFailed: '删除技能失败', diff --git a/mateclaw-ui/src/views/ChatConsole.vue b/mateclaw-ui/src/views/ChatConsole.vue index c26bca16..7e590814 100644 --- a/mateclaw-ui/src/views/ChatConsole.vue +++ b/mateclaw-ui/src/views/ChatConsole.vue @@ -300,7 +300,8 @@ import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useI18n } from 'vue-i18n' -import { ElMessage, ElMessageBox } from 'element-plus' +import { ElMessage } from 'element-plus' +import { mcConfirm } from '@/components/common/useConfirm' import { ChatDotRound, Delete, Plus, Setting, UploadFilled } from '@element-plus/icons-vue' import { conversationApi, agentApi, modelApi, chatApi, cronJobApi } from '@/api/index' import { channelIconUrl } from '@/utils/channelSource' @@ -463,12 +464,13 @@ function cancelRename() { } // Delete with confirmation -function confirmDeleteConversation(conversationId: string) { - ElMessageBox.confirm( - t('chat.deleteConfirm') || 'Delete this conversation?', - t('common.confirm'), - { type: 'warning', confirmButtonText: t('common.confirm'), cancelButtonText: t('common.cancel') } - ).then(() => deleteConversation(conversationId)).catch(() => {}) +async function confirmDeleteConversation(conversationId: string) { + const ok = await mcConfirm({ + title: t('common.confirm'), + message: t('chat.deleteConfirm') || 'Delete this conversation?', + tone: 'danger', + }) + if (ok) deleteConversation(conversationId) } // 拖拽上传 diff --git a/mateclaw-ui/src/views/Security/Activity/index.vue b/mateclaw-ui/src/views/Security/Activity/index.vue index 8931ed7f..0cd191ee 100644 --- a/mateclaw-ui/src/views/Security/Activity/index.vue +++ b/mateclaw-ui/src/views/Security/Activity/index.vue @@ -98,21 +98,16 @@ - + @@ -209,6 +204,7 @@ import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue' import { useI18n } from 'vue-i18n' import { activityApi } from '@/api' +import McPagination from '@/components/common/McPagination.vue' const { t } = useI18n() @@ -241,9 +237,6 @@ function syncMobile(e: MediaQueryListEvent | MediaQueryList) { } const drawerSize = computed(() => isMobile.value ? '100%' : '720px') -const paginationLayout = computed(() => - isMobile.value ? 'prev, pager, next' : 'total, sizes, prev, pager, next, jumper', -) const events = ref([]) const loading = ref(false) @@ -400,15 +393,6 @@ async function loadEvents() { } } -function onPageSizeChange(newSize: number) { - pageSize.value = newSize - // Switching page size resets to page 1 — Element Plus emits both - // size-change AND current-change, but order isn't guaranteed; we - // pin page=1 here so the request never fires with a stale offset. - page.value = 1 - loadEvents() -} - function filterEventsLocally() { /* trigger recompute via reactive filter */ } function openDetail(event: ActivityEvent) { diff --git a/mateclaw-ui/src/views/SkillMarket.vue b/mateclaw-ui/src/views/SkillMarket.vue index ab873254..caf4859a 100644 --- a/mateclaw-ui/src/views/SkillMarket.vue +++ b/mateclaw-ui/src/views/SkillMarket.vue @@ -120,7 +120,7 @@ + + + +
+
+ SKILL.md +
+ + +
+
+ + +
+ +
+
+ +
- -
-

{{ t('skills.detail.noManifest') }}

-
{{ detailManifestPretty }}
+ + +
+ +
+
+

{{ t('skills.detail.manifestProjectedSection') }}

+
+

{{ t('skills.detail.manifestProjectedHint') }}

+
+ slug{{ detailSkill.name }} + {{ t('skills.fields.type') }}{{ detailSkill.skillType || '—' }} + {{ t('skills.fields.icon') }}{{ detailSkill.icon }} + {{ t('skills.fields.version') }}v{{ detailSkill.version }} + {{ t('skills.fields.author') }}{{ detailSkill.author }} +
+
+ + +
+
+

{{ t('skills.detail.displayOverridesSection') }}

+ +
+ + +
+
+

{{ t('skills.detail.virtualReadonly') }}

+

{{ t('skills.detail.displayOverridesHint') }}

+ +
+
{{ t('skills.fields.nameZh') }}
{{ detailSkill.nameZh || '—' }}
+
{{ t('skills.fields.nameEn') }}
{{ detailSkill.nameEn || '—' }}
+
{{ t('skills.fields.tags') }}
{{ detailSkill.tags || '—' }}
+
{{ t('skills.fields.description') }}
{{ detailSkill.description || '—' }}
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ {{ t('skills.detail.viewRawManifest') }} +

{{ t('skills.detail.noManifest') }}

+
{{ detailManifestPretty }}
+
+
+ + +
+
+
+

{{ t('skills.fields.skillContent') }}

+ +
+

{{ t('skills.detail.bodyHint') }}

+

{{ t('skills.detail.noBody') }}

+
{{ detailSkill.skillContent }}
+ + +
@@ -354,14 +523,20 @@

{{ t('skills.detail.noLessons') }}

{{ detailLessonsRaw }}
-
- +
+ + + + - -