发布 v1.10 #5
@@ -15,12 +15,28 @@ describe('skill list contract', () => {
|
||||
expect(listSource).toContain("text: '新建技能'");
|
||||
});
|
||||
|
||||
it('shows only the agreed business columns and omits technical source data', () => {
|
||||
for (const label of ['技能', '用途', '创建人', '范围', '状态', '操作']) {
|
||||
it('shows only the agreed business columns and omits status and technical source data', () => {
|
||||
for (const label of ['技能', '用途', '创建人', '范围', '操作']) {
|
||||
expect(listSource).toContain(`label="${label}"`);
|
||||
}
|
||||
expect(listSource).not.toContain('label="状态"');
|
||||
expect(listSource).not.toContain('label="来源"');
|
||||
expect(listSource).not.toContain('label="能力"');
|
||||
expect(listSource).not.toContain('<h1>技能库</h1>');
|
||||
});
|
||||
|
||||
it('supports publishing, republishing, and taking published skills off shelf from the list', () => {
|
||||
expect(listSource).toContain('canPublishRow(row)');
|
||||
expect(listSource).toContain("? '重新发布'");
|
||||
expect(listSource).toContain(':type="rowPublishButtonType(row)"');
|
||||
expect(listSource).toContain("? 'warning'");
|
||||
expect(listSource).toContain(": 'success'");
|
||||
expect(listSource).toContain('command="offline"');
|
||||
expect(listSource).toContain('skill-list-page__danger-action');
|
||||
expect(listSource).toContain('color: hsl(var(--destructive))');
|
||||
expect(listSource).toContain('validateSkillForPublish(row.id)');
|
||||
expect(listSource).toContain(
|
||||
'submitSkillPublishApproval(target.id, reason)',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,7 +49,8 @@ import { withListReturnTo } from '#/router/list-return-context';
|
||||
import {
|
||||
canAiResourceDelete,
|
||||
canAiResourceOffline,
|
||||
resolveAiResourceDisplayStatus,
|
||||
canAiResourcePublish,
|
||||
canAiResourceRepublish,
|
||||
} from '#/views/ai/shared/publish-status';
|
||||
|
||||
import {
|
||||
@@ -59,6 +60,8 @@ import {
|
||||
getSkillCategories,
|
||||
submitSkillDeleteApproval,
|
||||
submitSkillOfflineApproval,
|
||||
submitSkillPublishApproval,
|
||||
validateSkillForPublish,
|
||||
} from './api';
|
||||
import {
|
||||
isSkillAccessDeniedError,
|
||||
@@ -91,6 +94,9 @@ const editingCategory = ref<SkillCategory>();
|
||||
const categoryAction = ref('');
|
||||
const copyDialogOpen = ref(false);
|
||||
const copyLoading = ref(false);
|
||||
const publishDialogOpen = ref(false);
|
||||
const publishReason = ref('');
|
||||
const publishTarget = ref<SkillInfo>();
|
||||
const rowAction = ref('');
|
||||
const copyForm = reactive({
|
||||
categoryId: '' as number | string,
|
||||
@@ -105,6 +111,9 @@ const canView = computed(() => hasPermission(['/api/v1/skill/getDetail']));
|
||||
const canUpdate = computed(() => hasPermission(['/api/v1/skill/update']));
|
||||
const canExport = computed(() => hasPermission(['/api/v1/skill/export']));
|
||||
const canCopy = computed(() => canCreate.value);
|
||||
const canPublish = computed(() =>
|
||||
hasPermission(['/api/v1/skill/submitPublishApproval']),
|
||||
);
|
||||
const canOffline = computed(() =>
|
||||
hasPermission(['/api/v1/skill/submitOfflineApproval']),
|
||||
);
|
||||
@@ -291,21 +300,6 @@ function handlePageStateChange(state: {
|
||||
if (target.fullPath !== route.fullPath) void router.replace(target);
|
||||
}
|
||||
|
||||
function statusMeta(row: SkillInfo) {
|
||||
const status = resolveAiResourceDisplayStatus(
|
||||
row.displayPublishStatus,
|
||||
row.publishStatus,
|
||||
);
|
||||
return {
|
||||
DELETE_PENDING: { label: '删除中', type: 'danger' as const },
|
||||
DRAFT: { label: '草稿', type: 'info' as const },
|
||||
OFFLINE: { label: '已下线', type: 'info' as const },
|
||||
OFFLINE_PENDING: { label: '下线中', type: 'warning' as const },
|
||||
PUBLISHED: { label: '已发布', type: 'success' as const },
|
||||
PUBLISH_PENDING: { label: '待审核', type: 'warning' as const },
|
||||
}[status];
|
||||
}
|
||||
|
||||
function scopeMeta(scope?: string) {
|
||||
return {
|
||||
DEPT: { label: '部门', type: 'primary' as const },
|
||||
@@ -318,6 +312,27 @@ function rowPrimaryLabel(row: SkillInfo) {
|
||||
return row.manageable !== false && canUpdate.value ? '编辑' : '查看';
|
||||
}
|
||||
|
||||
function canPublishRow(row: SkillInfo) {
|
||||
return (
|
||||
row.manageable !== false &&
|
||||
canPublish.value &&
|
||||
(canAiResourcePublish(row.displayPublishStatus, row.publishStatus) ||
|
||||
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus))
|
||||
);
|
||||
}
|
||||
|
||||
function rowPublishLabel(row: SkillInfo) {
|
||||
return canAiResourceRepublish(row.displayPublishStatus, row.publishStatus)
|
||||
? '重新发布'
|
||||
: '发布';
|
||||
}
|
||||
|
||||
function rowPublishButtonType(row: SkillInfo): 'success' | 'warning' {
|
||||
return canAiResourceRepublish(row.displayPublishStatus, row.publishStatus)
|
||||
? 'warning'
|
||||
: 'success';
|
||||
}
|
||||
|
||||
function canOfflineRow(row: SkillInfo) {
|
||||
return (
|
||||
row.manageable !== false &&
|
||||
@@ -334,8 +349,62 @@ function canDeleteRow(row: SkillInfo) {
|
||||
);
|
||||
}
|
||||
|
||||
function isRowBusy(row: SkillInfo) {
|
||||
return rowAction.value.endsWith(`:${row.id}`);
|
||||
function isPublishRowBusy(row: SkillInfo) {
|
||||
return (
|
||||
rowAction.value === `publish:${row.id}` ||
|
||||
rowAction.value === `validate:${row.id}`
|
||||
);
|
||||
}
|
||||
|
||||
function isMoreActionBusy(row: SkillInfo) {
|
||||
return !isPublishRowBusy(row) && rowAction.value.endsWith(`:${row.id}`);
|
||||
}
|
||||
|
||||
async function openPublishDialog(row: SkillInfo) {
|
||||
if (!row.id || !canPublishRow(row) || rowAction.value) return;
|
||||
rowAction.value = `validate:${row.id}`;
|
||||
try {
|
||||
const response = await validateSkillForPublish(row.id);
|
||||
if (response.errorCode !== 0) return;
|
||||
if (!response.data.valid) {
|
||||
const issue = response.data.issues?.find(
|
||||
(item) => item.severity === 'ERROR',
|
||||
);
|
||||
ElMessage.warning(
|
||||
issue?.message
|
||||
? `发布校验未通过:${issue.message}`
|
||||
: '发布校验未通过,请编辑技能后重试',
|
||||
);
|
||||
return;
|
||||
}
|
||||
publishTarget.value = row;
|
||||
publishReason.value = '';
|
||||
publishDialogOpen.value = true;
|
||||
} finally {
|
||||
rowAction.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmPublish() {
|
||||
const target = publishTarget.value;
|
||||
const reason = publishReason.value.trim();
|
||||
if (!target?.id || rowAction.value) return;
|
||||
if (!reason) {
|
||||
ElMessage.warning('请输入发布说明');
|
||||
return;
|
||||
}
|
||||
if (reason.length > 500) return;
|
||||
rowAction.value = `publish:${target.id}`;
|
||||
try {
|
||||
const response = await submitSkillPublishApproval(target.id, reason);
|
||||
if (response.errorCode !== 0) return;
|
||||
publishDialogOpen.value = false;
|
||||
publishTarget.value = undefined;
|
||||
pageDataRef.value?.reload?.();
|
||||
ElMessage.success(response.message || '已提交发布');
|
||||
} finally {
|
||||
rowAction.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRowCommand(command: string, row: SkillInfo) {
|
||||
@@ -396,9 +465,9 @@ async function exportOne(row: SkillInfo) {
|
||||
async function offline(row: SkillInfo) {
|
||||
if (!row.id || rowAction.value) return;
|
||||
try {
|
||||
await ElMessageBox.confirm('下线后将不可继续使用,确认下线?', '下线技能', {
|
||||
await ElMessageBox.confirm('下架后将不可继续使用,确认下架?', '下架技能', {
|
||||
cancelButtonText: '取消',
|
||||
confirmButtonText: '下线',
|
||||
confirmButtonText: '下架',
|
||||
type: 'warning',
|
||||
});
|
||||
} catch {
|
||||
@@ -597,19 +666,22 @@ async function handleCategorySaved() {
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" width="96">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="statusMeta(row).type" effect="plain">
|
||||
{{ statusMeta(row).label }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="112" fixed="right">
|
||||
<ElTableColumn label="操作" width="208" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="skill-list-page__actions">
|
||||
<ElButton text type="primary" @click="openDetail(row)">
|
||||
{{ rowPrimaryLabel(row) }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="canPublishRow(row)"
|
||||
text
|
||||
:type="rowPublishButtonType(row)"
|
||||
:disabled="Boolean(rowAction)"
|
||||
:loading="isPublishRowBusy(row)"
|
||||
@click="openPublishDialog(row)"
|
||||
>
|
||||
{{ rowPublishLabel(row) }}
|
||||
</ElButton>
|
||||
<ElDropdown
|
||||
v-if="
|
||||
canExport ||
|
||||
@@ -624,7 +696,7 @@ async function handleCategorySaved() {
|
||||
text
|
||||
:icon="MoreFilled"
|
||||
aria-label="更多操作"
|
||||
:loading="isRowBusy(row)"
|
||||
:loading="isMoreActionBusy(row)"
|
||||
/>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
@@ -646,7 +718,9 @@ async function handleCategorySaved() {
|
||||
v-if="canOfflineRow(row)"
|
||||
command="offline"
|
||||
>
|
||||
下线
|
||||
<span class="skill-list-page__danger-action">
|
||||
下架
|
||||
</span>
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem
|
||||
v-if="canDeleteRow(row)"
|
||||
@@ -749,6 +823,47 @@ async function handleCategorySaved() {
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<ElDialog
|
||||
v-model="publishDialogOpen"
|
||||
width="min(520px, calc(100vw - 32px))"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="!rowAction.startsWith('publish:')"
|
||||
:show-close="!rowAction.startsWith('publish:')"
|
||||
:title="
|
||||
publishTarget ? `${rowPublishLabel(publishTarget)}技能` : '发布技能'
|
||||
"
|
||||
>
|
||||
<ElForm label-position="top">
|
||||
<ElFormItem label="发布说明" required>
|
||||
<ElInput
|
||||
v-model="publishReason"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="填写本次发布内容"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton
|
||||
:disabled="rowAction.startsWith('publish:')"
|
||||
@click="publishDialogOpen = false"
|
||||
>
|
||||
取消
|
||||
</ElButton>
|
||||
<ElButton
|
||||
:type="
|
||||
publishTarget ? rowPublishButtonType(publishTarget) : 'success'
|
||||
"
|
||||
:loading="rowAction.startsWith('publish:')"
|
||||
@click="confirmPublish"
|
||||
>
|
||||
{{ publishTarget ? rowPublishLabel(publishTarget) : '发布' }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -848,6 +963,10 @@ async function handleCategorySaved() {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.skill-list-page__danger-action {
|
||||
color: hsl(var(--destructive));
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.skill-list-page {
|
||||
padding: var(--space-4);
|
||||
|
||||
@@ -326,6 +326,58 @@ describe('skill resource workbench validation', () => {
|
||||
|
||||
expect(valid).toBe(true);
|
||||
expect(apiMocks.validateSkillForPublish).toHaveBeenCalledWith(101);
|
||||
expect(wrapper.find('.document-editor-workbench__inspector').exists()).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps validation issues collapsed until the user opens them', async () => {
|
||||
apiMocks.validateSkillForPublish.mockResolvedValue({
|
||||
data: {
|
||||
issues: [
|
||||
{
|
||||
line: 3,
|
||||
message: '缺少必填字段',
|
||||
path: 'SKILL.md',
|
||||
severity: 'ERROR',
|
||||
},
|
||||
],
|
||||
valid: false,
|
||||
},
|
||||
errorCode: 0,
|
||||
});
|
||||
wrapper = mount(SkillResourceWorkbench, {
|
||||
global: {
|
||||
directives: { loading: {} },
|
||||
stubs: {
|
||||
MarkdownLiveEditor: {
|
||||
template: '<div data-testid="markdown-live-editor"></div>',
|
||||
},
|
||||
},
|
||||
},
|
||||
props: { skillId: 101 },
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
const valid = await (
|
||||
wrapper.vm as unknown as {
|
||||
validateAll: () => Promise<boolean>;
|
||||
}
|
||||
).validateAll();
|
||||
await flushPromises();
|
||||
|
||||
expect(valid).toBe(false);
|
||||
expect(wrapper.find('.document-editor-workbench__inspector').exists()).toBe(
|
||||
false,
|
||||
);
|
||||
const issueButton = wrapper.get('button[aria-label="查看校验问题"]');
|
||||
expect(issueButton.text()).toContain('校验问题 1');
|
||||
|
||||
await issueButton.trigger('click');
|
||||
|
||||
expect(wrapper.find('.document-editor-workbench__inspector').exists()).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -540,7 +540,6 @@ async function validateAll() {
|
||||
if (res.errorCode !== 0) return false;
|
||||
issues.value = res.data.issues || [];
|
||||
emit('issues', issues.value);
|
||||
inspector.value = 'validation';
|
||||
if (res.data.valid) ElMessage.success('发布级校验通过');
|
||||
else ElMessage.warning('发现需要处理的校验问题');
|
||||
return res.data.valid;
|
||||
@@ -1246,6 +1245,16 @@ defineExpose({
|
||||
size="small"
|
||||
/>
|
||||
<div class="skill-resource-workbench__toolbar-actions">
|
||||
<ElButton
|
||||
v-if="issues.length > 0"
|
||||
text
|
||||
type="warning"
|
||||
aria-label="查看校验问题"
|
||||
title="查看校验问题"
|
||||
@click="inspector = 'validation'"
|
||||
>
|
||||
校验问题 {{ issues.length }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="currentBuffer?.conflict"
|
||||
text
|
||||
|
||||
Reference in New Issue
Block a user