feat(personal): export reviewed outlines to pptx

This commit is contained in:
2026-07-12 23:23:04 +08:00
parent 0e81fe66ee
commit 661980f43f
11 changed files with 469 additions and 15 deletions
+81 -2
View File
@@ -82,15 +82,22 @@
<button class="secondary-button" :disabled="outlineBusy" @click="saveOutline">保存修改</button>
<button class="primary-button" :disabled="outlineBusy" @click="confirmOutline">确认大纲</button>
</view>
<view v-else class="outline-actions export-actions">
<button v-if="outline.status === 'CONFIRMED' || outline.status === 'FAILED'" class="primary-button" :disabled="outlineBusy" @click="generatePpt">
{{ outline.status === 'FAILED' ? '重新生成 PPT' : '生成 PPT' }}
</button>
<button v-if="outline.status === 'PENDING' || outline.status === 'RUNNING'" class="secondary-button" disabled>正在生成 PPT</button>
<button v-if="outline.status === 'READY'" class="primary-button" :disabled="outlineBusy" @click="downloadPpt">下载 PPT</button>
</view>
<text v-if="outlineMessage" class="status-message">{{ outlineMessage }}</text>
</view>
</view>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { computed, onUnmounted, ref } from 'vue';
import type { PersonalAskResponse, PersonalCitation, PersonalExportOutline } from '@/types/api';
import { askPersonalAssistant, confirmPersonalOutline, createPersonalOutline, updatePersonalOutline, type KnowledgeScope } from '@/services/personal-assistant';
import { askPersonalAssistant, confirmPersonalOutline, createPersonalOutline, downloadPersonalExport, enqueuePersonalPpt, loadPersonalExport, updatePersonalOutline, type KnowledgeScope } from '@/services/personal-assistant';
import { personalItemRoute } from '@/services/personal-assistant-id';
const scopes = ref<KnowledgeScope[]>(['PERSONAL']);
@@ -102,6 +109,7 @@ const outline = ref<PersonalExportOutline>();
const outlineBusy = ref(false);
const outlineMessage = ref('');
let requestGeneration = 0;
let exportPollTimer: ReturnType<typeof setTimeout> | undefined;
const enterpriseEnabled = computed(() => scopes.value.includes('ENTERPRISE'));
const scopeKey = (value: KnowledgeScope[]) => value.join(',');
@@ -110,6 +118,7 @@ const changeScope = () => {
requestGeneration += 1;
answer.value = undefined;
outline.value = undefined;
stopExportPolling();
message.value = '';
scopes.value = enterpriseEnabled.value ? ['PERSONAL'] : ['PERSONAL', 'ENTERPRISE'];
};
@@ -122,6 +131,7 @@ const submitQuestion = async () => {
asking.value = true;
answer.value = undefined;
outline.value = undefined;
stopExportPolling();
message.value = '';
try {
const response = await askPersonalAssistant({ queryText: question, scope: scopeSnapshot, outputFormat: 'ACTION_PLAN' });
@@ -206,6 +216,74 @@ const confirmOutline = async () => {
}
};
const stopExportPolling = () => {
if (exportPollTimer) clearTimeout(exportPollTimer);
exportPollTimer = undefined;
};
const pollExport = async (id: string, attempts = 0) => {
stopExportPolling();
try {
const latest = await loadPersonalExport(id);
if (!outline.value || outline.value.id !== id) return;
outline.value = latest;
if (latest.status === 'READY') {
outlineMessage.value = 'PPT 已生成,可下载 5 分钟短链';
return;
}
if (latest.status === 'FAILED') {
outlineMessage.value = 'PPT 生成失败,可重新生成';
return;
}
if (attempts >= 40) {
outlineMessage.value = 'PPT 仍在后台生成,请稍后重试';
return;
}
exportPollTimer = setTimeout(() => void pollExport(id, attempts + 1), 1500);
} catch (error) {
outlineMessage.value = error instanceof Error ? error.message : 'PPT 状态查询失败';
}
};
const generatePpt = async () => {
if (!outline.value?.confirmed || outlineBusy.value) return;
outlineBusy.value = true;
outlineMessage.value = '';
try {
await enqueuePersonalPpt(outline.value.id);
outline.value.status = 'PENDING';
outlineMessage.value = 'PPT 已进入后台生成队列';
void pollExport(outline.value.id);
} catch (error) {
outlineMessage.value = error instanceof Error ? error.message : 'PPT 生成请求失败';
} finally {
outlineBusy.value = false;
}
};
const downloadPpt = async () => {
if (!outline.value || outline.value.status !== 'READY' || outlineBusy.value) return;
outlineBusy.value = true;
try {
const download = await downloadPersonalExport(outline.value.id);
uni.downloadFile({
url: download.url,
success: (result) => {
if (result.statusCode >= 200 && result.statusCode < 300) {
uni.openDocument({ filePath: result.tempFilePath, showMenu: true });
} else {
outlineMessage.value = 'PPT 下载失败,请重试';
}
},
fail: () => { outlineMessage.value = 'PPT 下载失败,请重试'; }
});
} catch (error) {
outlineMessage.value = error instanceof Error ? error.message : 'PPT 下载失败';
} finally {
outlineBusy.value = false;
}
};
const addOutlineSection = () => outline.value?.sections.push({ title: '', bullets: [''] });
const addOutlineBullet = (sectionIndex: number) => outline.value?.sections[sectionIndex]?.bullets.push('');
@@ -223,6 +301,7 @@ const goBack = () => uni.navigateBack();
const openLibrary = () => uni.navigateTo({ url: '/pages/user/assistant/library' });
const openCapture = () => uni.navigateTo({ url: '/pages/user/assistant/capture' });
const openSessions = () => uni.navigateTo({ url: '/pages/user/assistant/sessions' });
onUnmounted(stopExportPolling);
</script>
<style scoped>
@@ -187,3 +187,19 @@ export const confirmPersonalOutline = (id: string) => apiRequest<void>({
url: `/api/aihr/personal-assistant/exports/${encodeURIComponent(id)}/confirm`,
method: 'POST'
});
export const loadPersonalExport = (id: string) => apiRequest<PersonalExportOutline>({
url: `/api/aihr/personal-assistant/exports/${encodeURIComponent(id)}`,
method: 'GET'
});
export const enqueuePersonalPpt = (id: string) => apiRequest<void>({
url: `/api/aihr/personal-assistant/exports/${encodeURIComponent(id)}/pptx`,
method: 'POST',
data: { templateCode: 'standard' }
});
export const downloadPersonalExport = (id: string) => apiRequest<{ url: string; expiresAt?: string }>({
url: `/api/aihr/personal-assistant/exports/${encodeURIComponent(id)}/download-url`,
method: 'GET'
});
@@ -61,6 +61,11 @@ test('个人助理先生成可编辑大纲并在确认后冻结来源', async ()
assert.match(client, /createPersonalOutline/);
assert.match(client, /updatePersonalOutline/);
assert.match(client, /confirmPersonalOutline/);
assert.match(client, /enqueuePersonalPpt/);
assert.match(client, /loadPersonalExport/);
assert.match(client, /downloadPersonalExport/);
assert.match(page, /生成 PPT/);
assert.match(page, /下载 PPT/);
assert.match(page, /生成汇报提纲/);
assert.match(page, /确认大纲/);
assert.match(page, /资料来源不可编辑/);