refactor(性能优化): 使用Promise.all并行请求核心KPI接口 style(样式调整): 修改ProblemRanking组件高度和内边距 chore: 移除调试用的console.log语句
334 lines
7.3 KiB
Vue
334 lines
7.3 KiB
Vue
<template>
|
||
<div v-if="isVisible" class="modal-overlay" @click="closeModal">
|
||
<div class="modal-container" @click.stop>
|
||
<div class="modal-header">
|
||
<h3>意见反馈</h3>
|
||
<button class="modal-close-btn" @click="closeModal">×</button>
|
||
</div>
|
||
<div class="feedback-form-container">
|
||
<h2>意见反馈</h2>
|
||
<p class="subtitle">我们期待听到您的声音,不断改进我们的产品。</p>
|
||
|
||
<!-- 提交成功或失败的提示信息 -->
|
||
<div v-if="submitStatus === 'success'" class="feedback-message success">
|
||
✓ 感谢您的反馈!我们已经收到您的宝贵意见。
|
||
</div>
|
||
<div v-if="submitStatus === 'error'" class="feedback-message error">
|
||
✗ 提交失败,请检查网络或稍后重试。
|
||
</div>
|
||
|
||
<form v-if="submitStatus !== 'success'" @submit.prevent="handleSubmit">
|
||
<!-- 反馈类型 -->
|
||
<div class="form-group">
|
||
<label for="feedback-type">反馈类型</label>
|
||
<select id="feedback-type" v-model="formData.type">
|
||
<option>功能建议</option>
|
||
<option>界面优化</option>
|
||
<option>Bug 反馈</option>
|
||
<option>其他</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- 反馈内容 -->
|
||
<div class="form-group">
|
||
<label for="feedback-content">反馈内容 (必填)</label>
|
||
<textarea
|
||
id="feedback-content"
|
||
v-model="formData.content"
|
||
placeholder="请详细描述您的问题或建议..."
|
||
rows="6"
|
||
required
|
||
></textarea>
|
||
</div>
|
||
<!-- 提交按钮 -->
|
||
<div class="form-actions">
|
||
<button type="submit" :disabled="isSubmitting">
|
||
<span v-if="isSubmitting">正在提交...</span>
|
||
<span v-else>提交反馈</span>
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import axios from 'axios';
|
||
import { ref, reactive } from 'vue';
|
||
|
||
// 定义组件的props
|
||
const props = defineProps({
|
||
isVisible: {
|
||
type: Boolean,
|
||
default: false
|
||
}
|
||
});
|
||
|
||
// 定义组件要触发的事件
|
||
const emit = defineEmits(['submit-feedback', 'close']);
|
||
|
||
// 使用 reactive 创建响应式表单数据对象
|
||
const formData = reactive({
|
||
type: '功能建议', // 默认值
|
||
content: '',
|
||
contact: '',
|
||
screenshot: null, // 存储文件对象
|
||
});
|
||
|
||
// ref 用于独立的响应式值
|
||
const isSubmitting = ref(false); // 是否正在提交
|
||
const submitStatus = ref(null); // 'success', 'error', or null
|
||
const imagePreviewUrl = ref(null); // 图片预览 URL
|
||
const fileInputRef = ref(null); // 用于引用文件输入元素
|
||
|
||
// 文件选择变化时的处理函数
|
||
const handleFileChange = (event) => {
|
||
const file = event.target.files[0];
|
||
if (file) {
|
||
formData.screenshot = file;
|
||
// 创建一个临时的 URL 用于图片预览
|
||
imagePreviewUrl.value = URL.createObjectURL(file);
|
||
}
|
||
};
|
||
|
||
// 移除已选图片
|
||
const removeImage = () => {
|
||
formData.screenshot = null;
|
||
imagePreviewUrl.value = null;
|
||
// 清空文件输入框的值,以便用户可以再次选择相同的文件
|
||
if (fileInputRef.value) {
|
||
fileInputRef.value.value = '';
|
||
}
|
||
};
|
||
|
||
// 关闭模态框
|
||
const closeModal = () => {
|
||
emit('close');
|
||
};
|
||
|
||
// 表单提交处理函数
|
||
const handleSubmit = async () => {
|
||
// 简单验证
|
||
if (!formData.content.trim()) {
|
||
alert('反馈内容不能为空!');
|
||
return;
|
||
}
|
||
|
||
isSubmitting.value = true;
|
||
submitStatus.value = null;
|
||
|
||
try {
|
||
// 创建 FormData 对象
|
||
// 获取 token (假设存储在 localStorage 中)
|
||
const token = localStorage.getItem('token') || '';
|
||
|
||
// 发送 POST 请求到后端接口
|
||
const response = await axios.post('https://feedback.api.nycjy.cn/api/v1/feedback/submit_feedback', {project:'mldash',type: formData.type, content: formData.content}, {
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`
|
||
}
|
||
});
|
||
// console.log('响应状态8888:', response.data.message);
|
||
// 提交成功
|
||
submitStatus.value = 'success';
|
||
// 触发父组件的事件,并传递数据
|
||
emit('submit-feedback', { ...formData });
|
||
} catch (error) {
|
||
console.error('提交反馈失败:', error);
|
||
// 提交失败
|
||
submitStatus.value = 'error';
|
||
} finally {
|
||
// 请求结束
|
||
isSubmitting.value = false;
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.modal-container {
|
||
background-color: #ffffff;
|
||
border-radius: 10px;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||
max-width: 600px;
|
||
width: 90%;
|
||
max-height: 90vh;
|
||
overflow-y: auto;
|
||
position: relative;
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 1rem 2rem;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.modal-header h3 {
|
||
margin: 0;
|
||
color: #1a202c;
|
||
font-size: 1.25rem;
|
||
}
|
||
|
||
.modal-close-btn {
|
||
background: none;
|
||
border: none;
|
||
font-size: 1.5rem;
|
||
cursor: pointer;
|
||
color: #718096;
|
||
padding: 0;
|
||
width: 30px;
|
||
height: 30px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.modal-close-btn:hover {
|
||
color: #1a202c;
|
||
}
|
||
|
||
.feedback-form-container {
|
||
padding: 2rem;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||
}
|
||
|
||
h2 {
|
||
text-align: center;
|
||
color: #1a202c;
|
||
margin-top: 0;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.subtitle {
|
||
text-align: center;
|
||
color: #718096;
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 0.5rem;
|
||
font-weight: 600;
|
||
color: #4a5568;
|
||
}
|
||
|
||
input[type="text"],
|
||
select,
|
||
textarea {
|
||
width: 100%;
|
||
padding: 0.75rem;
|
||
border: 1px solid #cbd5e0;
|
||
border-radius: 6px;
|
||
font-size: 1rem;
|
||
color: #2d3748;
|
||
transition: border-color 0.2s, box-shadow 0.2s;
|
||
}
|
||
|
||
input[type="text"]:focus,
|
||
select:focus,
|
||
textarea:focus {
|
||
outline: none;
|
||
border-color: #4299e1;
|
||
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
|
||
}
|
||
|
||
input[type="file"] {
|
||
width: 100%;
|
||
padding: 0.5rem;
|
||
}
|
||
|
||
.image-preview {
|
||
position: relative;
|
||
margin-top: 1rem;
|
||
max-width: 200px;
|
||
}
|
||
|
||
.image-preview img {
|
||
width: 100%;
|
||
height: auto;
|
||
border-radius: 6px;
|
||
border: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.remove-image-btn {
|
||
position: absolute;
|
||
top: -10px;
|
||
right: -10px;
|
||
background-color: #e53e3e;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 50%;
|
||
width: 24px;
|
||
height: 24px;
|
||
font-size: 16px;
|
||
line-height: 24px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.form-actions {
|
||
text-align: center;
|
||
}
|
||
|
||
button[type="submit"] {
|
||
padding: 0.75rem 2rem;
|
||
background-color: #4299e1;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
font-size: 1rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s;
|
||
width: 100%;
|
||
}
|
||
|
||
button[type="submit"]:hover:not(:disabled) {
|
||
background-color: #3182ce;
|
||
}
|
||
|
||
button[type="submit"]:disabled {
|
||
background-color: #a0aec0;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.feedback-message {
|
||
padding: 1rem;
|
||
border-radius: 6px;
|
||
text-align: center;
|
||
margin-bottom: 1.5rem;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.feedback-message.success {
|
||
background-color: #c6f6d5;
|
||
color: #2f855a;
|
||
border: 1px solid #9ae6b4;
|
||
}
|
||
|
||
.feedback-message.error {
|
||
background-color: #fed7d7;
|
||
color: #c53030;
|
||
border: 1px solid #feb2b2;
|
||
}
|
||
</style> |