| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263 |
- <script setup lang="ts">
- import {
- NForm,
- NFormItem,
- NInput,
- NSelect,
- NRadioGroup,
- NRadio,
- NButton,
- NUpload,
- useMessage,
- } from 'naive-ui'
- import { ref, reactive } from 'vue'
- import { submitFeedback, uploadFeedbackAssets, deleteFeedbackAsset } from '@/service/feedback'
- import type { FeedbackPriority, FeatureCategory, FeedbackUploadResult } from '@/service/feedback/interface'
- import type { UploadFileInfo } from 'naive-ui'
- defineOptions({
- name: 'FeatureForm',
- })
- const emit = defineEmits<{
- submitted: []
- }>()
- const message = useMessage()
- const loading = ref(false)
- const formData = reactive({
- title: '',
- category: 'new' as FeatureCategory,
- priority: 'medium' as FeedbackPriority,
- description: '',
- scenario: '',
- relatedPage: '',
- })
- // 暂存的本地文件列表(提交时才上传)
- interface PendingFile {
- id: string
- file: File
- name: string
- }
- const pendingFiles = ref<PendingFile[]>([])
- const categoryOptions = [
- { label: '新功能', value: 'new' },
- { label: '功能改进', value: 'improvement' },
- { label: '体验优化', value: 'experience' },
- ]
- const rules = {
- title: { required: true, message: '请输入标题', trigger: 'blur' },
- description: { required: true, message: '请描述需求', trigger: 'blur' },
- }
- const formRef = ref()
- // 选择文件 → 暂存
- function handleFileSelect({ file }: { file: UploadFileInfo }) {
- if (!file.file) return
- if (file.file.size > 10 * 1024 * 1024) {
- message.warning('文件大小不能超过 10MB')
- return
- }
- pendingFiles.value.push({
- id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
- file: file.file,
- name: file.name || file.file.name,
- })
- }
- // 粘贴截图 → 暂存
- function handlePaste(event: ClipboardEvent) {
- const items = event.clipboardData?.items
- if (!items) return
- for (const item of items) {
- if (item.type.startsWith('image/')) {
- const file = item.getAsFile()
- if (file) {
- event.preventDefault()
- const fileName = `screenshot_${Date.now()}.png`
- const renamedFile = new File([file], fileName, { type: file.type })
- pendingFiles.value.push({
- id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
- file: renamedFile,
- name: fileName,
- })
- message.info('截图已添加')
- }
- break
- }
- }
- }
- function removePendingFile(index: number) {
- pendingFiles.value.splice(index, 1)
- }
- async function handleSubmit() {
- try {
- await formRef.value?.validate()
- } catch {
- return
- }
- loading.value = true
- let uploadedResults: FeedbackUploadResult[] = []
- try {
- // 1. 提交时才上传文件
- if (pendingFiles.value.length > 0) {
- const files = pendingFiles.value.map((p) => p.file)
- uploadedResults = await uploadFeedbackAssets(files)
- }
- // 2. 提交需求(附带已上传的 assetId 列表)
- await submitFeedback({
- title: formData.title,
- type: 'feature',
- priority: formData.priority,
- category: formData.category,
- description: formData.description,
- scenario: formData.scenario || undefined,
- relatedPage: formData.relatedPage || undefined,
- assetIds: uploadedResults.length > 0
- ? uploadedResults.map((r) => r.assetId)
- : undefined,
- })
- message.success('需求已提交,感谢建议!')
- resetForm()
- emit('submitted')
- } catch (err: any) {
- // 提交失败 → 清理已上传的文件
- if (uploadedResults.length > 0) {
- for (const result of uploadedResults) {
- try {
- await deleteFeedbackAsset(result.assetId)
- } catch {
- // 忽略
- }
- }
- }
- message.error(err?.message || '提交失败,请稍后重试')
- } finally {
- loading.value = false
- }
- }
- function resetForm() {
- formData.title = ''
- formData.category = 'new'
- formData.priority = 'medium'
- formData.description = ''
- formData.scenario = ''
- formData.relatedPage = ''
- pendingFiles.value = []
- }
- </script>
- <template>
- <NForm ref="formRef" :model="formData" :rules="rules" label-placement="top" label-width="auto">
- <NFormItem label="标题" path="title">
- <NInput v-model:value="formData.title" placeholder="简要描述需求" maxlength="100" show-count />
- </NFormItem>
- <div class="grid grid-cols-1 gap-x-4 md:grid-cols-2">
- <NFormItem label="需求类型" path="category">
- <NSelect v-model:value="formData.category" :options="categoryOptions" />
- </NFormItem>
- <NFormItem label="优先级" path="priority">
- <NRadioGroup v-model:value="formData.priority">
- <NRadio value="urgent">紧急</NRadio>
- <NRadio value="high">高</NRadio>
- <NRadio value="medium">中</NRadio>
- <NRadio value="low">低</NRadio>
- </NRadioGroup>
- </NFormItem>
- </div>
- <NFormItem label="需求描述" path="description">
- <NInput
- v-model:value="formData.description"
- type="textarea"
- placeholder="详细描述你期望的功能或改进"
- :rows="5"
- />
- </NFormItem>
- <NFormItem label="使用场景">
- <NInput
- v-model:value="formData.scenario"
- type="textarea"
- placeholder="描述什么情况下需要这个功能(可选)"
- :rows="3"
- />
- </NFormItem>
- <NFormItem label="相关页面">
- <NInput v-model:value="formData.relatedPage" placeholder="可选,如:站点监控 / 批量下载" />
- </NFormItem>
- <!-- 截图/附件 -->
- <NFormItem label="截图 / 附件">
- <div class="w-full" @paste="handlePaste">
- <NUpload
- :show-file-list="false"
- :custom-request="({ file }) => handleFileSelect({ file: file as UploadFileInfo })"
- accept="image/*,.pdf,.log,.txt,.xlsx,.xls,.csv,.zip"
- :multiple="true"
- >
- <NButton dashed class="w-full">
- <template #icon>
- <span class="i-ph:upload-simple" />
- </template>
- 点击选择文件或粘贴截图(Ctrl+V)
- </NButton>
- </NUpload>
- <!-- 暂存文件列表 -->
- <div v-if="pendingFiles.length > 0" class="mt-3 space-y-2">
- <div
- v-for="(pf, index) in pendingFiles"
- :key="pf.id"
- class="flex items-center justify-between rounded bg-neutral-50 px-3 py-2 dark:bg-neutral-800"
- >
- <span class="text-sm text-neutral-600 dark:text-neutral-400">
- 📎 {{ pf.name }}
- <span class="ml-1 text-xs text-neutral-400">
- ({{ (pf.file.size / 1024).toFixed(1) }}KB)
- </span>
- </span>
- <NButton size="tiny" quaternary type="error" @click="removePendingFile(index)">
- <template #icon>
- <span class="i-ph:x" />
- </template>
- </NButton>
- </div>
- </div>
- <p class="mt-1 text-xs text-neutral-400">
- 支持图片、PDF、日志文件等,单文件最大 10MB。文件将在提交时上传。
- </p>
- </div>
- </NFormItem>
- <div class="flex justify-end gap-3">
- <NButton @click="resetForm">重置</NButton>
- <NButton type="primary" :loading="loading" @click="handleSubmit">
- <template #icon>
- <span class="i-ph:paper-plane-tilt" />
- </template>
- 提交需求
- </NButton>
- </div>
- </NForm>
- </template>
|