From 3ae1ea4c6deb7f5253e0eaa84aeadc6dfe435d6c Mon Sep 17 00:00:00 2001 From: selfrelease Date: Tue, 28 Jul 2026 15:34:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AE=BE=E7=BD=AE=E9=A1=B5=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=AF=BC=E5=87=BA=E6=96=B0=E5=A2=9E=E9=97=AE=E5=8D=B7?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=E5=88=86=E6=9E=90=E5=AF=BC=E5=87=BA=EF=BC=8C?= =?UTF-8?q?=E7=94=9F=E6=88=90MD=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/Settings.tsx | 122 +++++++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 6cd74c5..46c606d 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -1,13 +1,14 @@ import { useState, useEffect } from 'react' import { toast } from 'sonner' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { Building2, Users, CreditCard, Plus, Bell, Download, Upload, FileSpreadsheet, Clock, CheckCircle, AlertCircle } from 'lucide-react' +import { Building2, Users, CreditCard, Plus, Bell, Download, Upload, FileSpreadsheet, Clock, CheckCircle, AlertCircle, ClipboardList } from 'lucide-react' import api from '../lib/api' import { useAuthStore } from '../store/authStore' import Card from '../components/ui/Card' import Button from '../components/ui/Button' import { Input, Label, Select } from '../components/ui/Input' import Modal from '../components/ui/Modal' +import { surveyPages, totalFeatures } from '../data/surveyData' export default function Settings() { const queryClient = useQueryClient() @@ -527,10 +528,129 @@ function ExportSettings() { + + {/* 问卷结果分析导出 */} +
+
+ +

问卷结果分析导出

+
+

将功能调查问卷的填写结果导出为 Markdown 文档,包含每项功能的评分、有用性和备注

+ +
) } +/** 导出问卷结果为 Markdown 文档 */ +function handleSurveyExport() { + try { + const raw = localStorage.getItem('survey-results') + if (!raw) { + toast.info('暂无已提交的问卷结果,请先在功能调查问卷中提交') + return + } + const data = JSON.parse(raw) + const items: Array<{ featureId: string; score: number; useful: string; remark: string }> = data.items || [] + if (items.length === 0) { + toast.info('问卷结果为空') + return + } + + const scoreMap = new Map(items.map(i => [i.featureId, i])) + const usefulLabels: Record = { yes: '有用', no: '无用', maybe: '待定', '': '' } + const submittedAt = data.submittedAt ? new Date(data.submittedAt).toLocaleString('zh-CN') : '' + + let md = `# 功能调查问卷结果分析\n\n` + md += `> **导出时间:** ${new Date().toLocaleString('zh-CN')}\n` + if (submittedAt) md += `> **提交时间:** ${submittedAt}\n` + md += `> **总功能数:** ${totalFeatures}\n` + md += `> **已评功能数:** ${items.length}\n\n` + + const rated = items.filter(i => i.score > 0) + const avgScore = rated.length > 0 ? (rated.reduce((s, i) => s + i.score, 0) / rated.length).toFixed(2) : '0' + const usefulCount = items.filter(i => i.useful === 'yes').length + const notUsefulCount = items.filter(i => i.useful === 'no').length + const maybeCount = items.filter(i => i.useful === 'maybe').length + + md += `## 统计概览\n\n` + md += `| 指标 | 数值 |\n|------|------|\n` + md += `| 已评功能数 | ${items.length} / ${totalFeatures} |\n` + md += `| 平均评分 | ${avgScore} / 5 |\n` + md += `| 标记有用 | ${usefulCount} |\n` + md += `| 标记无用 | ${notUsefulCount} |\n` + md += `| 标记待定 | ${maybeCount} |\n\n` + + md += `## 评分分布\n\n` + for (let s = 5; s >= 1; s--) { + const count = rated.filter(i => i.score === s).length + const pct = rated.length > 0 ? ((count / rated.length) * 100).toFixed(1) : '0' + md += `- **${s}星**:${count} 项(${pct}%)\n` + } + md += `\n` + + md += `## 详细评分\n\n` + + for (const page of surveyPages) { + const pageItems = page.features.filter(f => scoreMap.has(f.id)) + if (pageItems.length === 0) continue + + md += `### ${page.id}. ${page.name}\n\n` + md += `> 📍 ${page.menu}` + if (page.tab) md += ` | Tab: ${page.tab}` + md += `\n\n` + + md += `| # | 功能 | 评分 | 有用性 | 备注 |\n` + md += `|---|------|:----:|:------:|------|\n` + for (const f of pageItems) { + const s = scoreMap.get(f.id)! + const stars = '★'.repeat(s.score) + '☆'.repeat(5 - s.score) + md += `| ${f.id} | ${f.name} | ${stars} (${s.score}) | ${usefulLabels[s.useful] || ''} | ${s.remark || ''} |\n` + } + md += `\n` + } + + md += `## 低分功能(≤2分)\n\n` + const lowScore = rated.filter(i => i.score <= 2).sort((a, b) => a.score - b.score) + if (lowScore.length > 0) { + md += `| # | 功能 | 评分 | 备注 |\n|---|------|:----:|------|\n` + for (const item of lowScore) { + const feat = surveyPages.flatMap(p => p.features).find(f => f.id === item.featureId) + md += `| ${item.featureId} | ${feat?.name || item.featureId} | ${item.score} | ${item.remark || ''} |\n` + } + } else { + md += `无低分功能\n` + } + md += `\n` + + md += `## 高分功能(≥4分)\n\n` + const highScore = rated.filter(i => i.score >= 4).sort((a, b) => b.score - a.score) + if (highScore.length > 0) { + md += `| # | 功能 | 评分 | 备注 |\n|---|------|:----:|------|\n` + for (const item of highScore) { + const feat = surveyPages.flatMap(p => p.features).find(f => f.id === item.featureId) + md += `| ${item.featureId} | ${feat?.name || item.featureId} | ${item.score} | ${item.remark || ''} |\n` + } + } else { + md += `无高分功能\n` + } + md += `\n---\n*由企业用工专家系统自动生成*\n` + + const blob = new Blob([md], { type: 'text/markdown;charset=utf-8' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `survey-results-${new Date().toISOString().slice(0, 10)}.md` + a.click() + URL.revokeObjectURL(url) + toast.success('问卷结果已导出为 Markdown 文档') + } catch { + toast.error('导出问卷结果失败') + } +} + function PlanSettings({ orgData }: { orgData: any }) { const queryClient = useQueryClient() const plan = orgData?.plan || 'FREE'