Files
autoAiWorkSys/app/views/FeedbackPage.vue
张成 e17d5610f5 1
2025-12-22 16:26:59 +08:00

644 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="page-feedback">
<h2 class="page-title">意见反馈</h2>
<!-- 反馈表单 -->
<div class="feedback-form-section">
<h3>提交反馈</h3>
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label>反馈类型 <span class="required">*</span></label>
<Dropdown
v-model="formData.type"
:options="typeOptions"
optionLabel="label"
optionValue="value"
placeholder="请选择反馈类型"
class="form-control"
required
/>
</div>
<div class="form-group">
<label>反馈内容 <span class="required">*</span></label>
<Textarea
v-model="formData.content"
rows="6"
placeholder="请详细描述您的问题或建议..."
class="form-control"
required
/>
</div>
<div class="form-group">
<label>联系方式可选</label>
<InputText
v-model="formData.contact"
placeholder="请输入您的联系方式(手机号、邮箱等)"
class="form-control"
/>
</div>
<div class="form-actions">
<Button
type="submit"
label="提交反馈"
:disabled="submitting"
:loading="submitting"
/>
<Button
label="重置"
severity="secondary"
@click="handleReset"
/>
</div>
</form>
</div>
<!-- 反馈历史 -->
<div class="feedback-history-section">
<h3>反馈历史</h3>
<ProgressSpinner v-if="loading" />
<div v-else-if="feedbacks.length === 0" class="empty">暂无反馈记录</div>
<div v-else class="feedback-table-wrapper">
<table class="feedback-table">
<thead>
<tr>
<th>反馈类型</th>
<th>反馈内容</th>
<th>状态</th>
<th>提交时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="feedback in feedbacks" :key="feedback.id">
<td>
<Tag :value="getTypeText(feedback.type)" severity="info" />
</td>
<td class="content-cell">
<div class="content-text">{{ feedback.content }}</div>
</td>
<td>
<Tag
v-if="feedback.status"
:value="getStatusText(feedback.status)"
:severity="getStatusSeverity(feedback.status)"
/>
</td>
<td class="time-cell">{{ formatTime(feedback.createTime) }}</td>
<td>
<Button
label="查看详情"
size="small"
@click="handleViewDetail(feedback)"
/>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<Paginator
v-if="total > 0"
:rows="pageSize"
:totalRecords="total"
:first="(currentPage - 1) * pageSize"
@page="onPageChange"
/>
</div>
<!-- 详情弹窗 -->
<Dialog
v-model:visible="showDetail"
modal
header="反馈详情"
:style="{ width: '600px' }"
@hide="closeDetail"
>
<div v-if="currentFeedback" class="detail-content">
<div class="detail-item">
<label>反馈类型</label>
<span>{{ getTypeText(currentFeedback.type) }}</span>
</div>
<div class="detail-item">
<label>反馈内容</label>
<div class="detail-content">{{ currentFeedback.content }}</div>
</div>
<div class="detail-item" v-if="currentFeedback.contact">
<label>联系方式</label>
<span>{{ currentFeedback.contact }}</span>
</div>
<div class="detail-item">
<label>处理状态</label>
<Tag
:value="getStatusText(currentFeedback.status)"
:severity="getStatusSeverity(currentFeedback.status)"
/>
</div>
<div class="detail-item">
<label>提交时间</label>
<span>{{ formatTime(currentFeedback.createTime) }}</span>
</div>
<div class="detail-item" v-if="currentFeedback.reply_content">
<label>回复内容</label>
<div class="detail-content">{{ currentFeedback.reply_content }}</div>
</div>
<div class="detail-item" v-if="currentFeedback.reply_time">
<label>回复时间</label>
<span>{{ formatTime(currentFeedback.reply_time) }}</span>
</div>
</div>
</Dialog>
<!-- 成功提示 -->
<div v-if="showSuccess" class="success-message">
{{ successMessage }}
</div>
</div>
</template>
<script>
import feedbackAPI from '../api/feedback.js';
import { mapState } from 'vuex';
import { Dropdown, Textarea, InputText, Button, Card, Tag, Dialog, Paginator, ProgressSpinner } from '../components/PrimeVue';
import logMixin from '../mixins/logMixin.js';
export default {
name: 'FeedbackPage',
mixins: [logMixin],
components: {
Dropdown,
Textarea,
InputText,
Button,
Card,
Tag,
Dialog,
Paginator,
ProgressSpinner
},
data() {
return {
formData: {
type: '',
content: '',
contact: ''
},
typeOptions: [
{ label: 'Bug反馈', value: 'bug' },
{ label: '功能建议', value: 'suggestion' },
{ label: '使用问题', value: 'question' },
{ label: '其他', value: 'other' }
],
submitting: false,
feedbacks: [],
loading: false,
showSuccess: false,
successMessage: '',
currentPage: 1,
pageSize: 10,
total: 0,
showDetail: false,
currentFeedback: null
};
},
computed: {
...mapState('auth', ['snCode']),
totalPages() {
return Math.ceil(this.total / this.pageSize);
}
},
mounted() {
this.loadFeedbacks();
},
methods: {
/**
* 提交反馈
*/
async handleSubmit() {
if (!this.formData.type || !this.formData.content) {
if (this.addLog) {
this.addLog('warn', '请填写完整的反馈信息');
}
return;
}
if (!this.snCode) {
if (this.addLog) {
this.addLog('error', '请先登录');
}
return;
}
this.submitting = true;
try {
const result = await feedbackAPI.submit({
...this.formData,
sn_code: this.snCode
});
if (result && result.code === 0) {
this.showSuccessMessage('反馈提交成功,感谢您的反馈!');
this.handleReset();
this.loadFeedbacks();
} else {
if (this.addLog) {
this.addLog('error', result.message || '提交失败');
}
}
} catch (error) {
console.error('提交反馈失败:', error);
if (this.addLog) {
this.addLog('error', '提交反馈失败: ' + (error.message || '未知错误'));
}
} finally {
this.submitting = false;
}
},
/**
* 重置表单
*/
handleReset() {
this.formData = {
type: '',
content: '',
contact: ''
};
},
/**
* 加载反馈列表
*/
async loadFeedbacks() {
if (!this.snCode) {
return;
}
this.loading = true;
try {
const result = await feedbackAPI.getList({
sn_code: this.snCode,
page: this.currentPage,
pageSize: this.pageSize
});
if (result && result.code === 0) {
this.feedbacks = result.data.rows || result.data.list || [];
this.total = result.data.total || result.data.count || 0;
} else {
console.warn('[反馈管理] 响应格式异常:', result);
}
} catch (error) {
console.error('加载反馈列表失败:', error);
if (this.addLog) {
this.addLog('error', '加载反馈列表失败: ' + (error.message || '未知错误'));
}
} finally {
this.loading = false;
}
},
/**
* 查看详情
*/
async handleViewDetail(feedback) {
try {
const result = await feedbackAPI.getDetail(feedback.id);
if (result && result.code === 0) {
this.currentFeedback = result.data || feedback;
this.showDetail = true;
} else {
// 如果获取详情失败,直接使用当前记录显示
this.currentFeedback = feedback;
this.showDetail = true;
}
} catch (error) {
console.error('获取详情失败:', error);
// 如果获取详情失败,直接使用当前记录显示
this.currentFeedback = feedback;
this.showDetail = true;
if (this.addLog) {
this.addLog('error', '获取详情失败: ' + (error.message || '未知错误'));
}
}
},
/**
* 关闭详情
*/
closeDetail() {
this.showDetail = false;
this.currentFeedback = null;
},
/**
* 分页切换
*/
handlePageChange(page) {
this.currentPage = page;
this.loadFeedbacks();
},
/**
* Paginator 分页事件
*/
onPageChange(event) {
this.currentPage = Math.floor(event.first / this.pageSize) + 1;
this.loadFeedbacks();
},
/**
* 获取状态严重程度(用于 Tag 组件)
*/
getStatusSeverity(status) {
const severityMap = {
'pending': 'warning',
'processing': 'info',
'completed': 'success',
'rejected': 'danger'
};
return severityMap[status] || 'secondary';
},
/**
* 获取类型文本
*/
getTypeText(type) {
const typeMap = {
'bug': 'Bug反馈',
'suggestion': '功能建议',
'question': '使用问题',
'other': '其他'
};
return typeMap[type] || type || '-';
},
/**
* 获取状态文本
*/
getStatusText(status) {
const statusMap = {
'pending': '待处理',
'processing': '处理中',
'completed': '已完成',
'rejected': '已拒绝'
};
return statusMap[status] || status || '-';
},
/**
* 格式化时间
*/
formatTime(time) {
if (!time) return '-';
const date = new Date(time);
return date.toLocaleString('zh-CN');
},
/**
* 显示成功消息
*/
showSuccessMessage(message) {
this.successMessage = message;
this.showSuccess = true;
setTimeout(() => {
this.showSuccess = false;
}, 3000);
}
}
};
</script>
<style lang="less" scoped>
.page-feedback {
padding: 20px;
height: 100%;
overflow-y: auto;
}
.page-title {
margin: 0 0 20px 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
.feedback-form-section {
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
h3 {
margin: 0 0 20px 0;
font-size: 18px;
color: #333;
}
}
.form-group {
margin-bottom: 20px;
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
.required {
color: #f44336;
}
}
}
.form-control {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
font-family: inherit;
&:focus {
outline: none;
border-color: #4CAF50;
}
}
textarea.form-control {
resize: vertical;
min-height: 120px;
}
.form-actions {
display: flex;
gap: 10px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
&.btn-primary {
background: #4CAF50;
color: #fff;
&:hover:not(:disabled) {
background: #45a049;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
&:not(.btn-primary) {
background: #f5f5f5;
color: #333;
&:hover {
background: #e0e0e0;
}
}
}
.feedback-history-section {
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
h3 {
margin: 0 0 20px 0;
font-size: 18px;
color: #333;
}
}
.loading, .empty {
padding: 40px;
text-align: center;
color: #999;
}
.feedback-table-wrapper {
overflow-x: auto;
}
.feedback-table {
width: 100%;
border-collapse: collapse;
background: #fff;
thead {
background: #f5f5f5;
th {
padding: 12px 16px;
text-align: left;
font-weight: 600;
color: #333;
border-bottom: 2px solid #e0e0e0;
white-space: nowrap;
}
}
tbody {
tr {
border-bottom: 1px solid #f0f0f0;
transition: background-color 0.2s;
&:hover {
background-color: #f9f9f9;
}
&:last-child {
border-bottom: none;
}
}
td {
padding: 12px 16px;
vertical-align: middle;
color: #666;
}
}
}
.content-cell {
max-width: 400px;
.content-text {
max-height: 60px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.5;
}
}
.time-cell {
white-space: nowrap;
font-size: 14px;
color: #999;
}
.success-message {
position: fixed;
top: 20px;
right: 20px;
background: #4CAF50;
color: #fff;
padding: 12px 20px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
z-index: 1000;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.detail-item {
margin-bottom: 15px;
label {
font-weight: 600;
color: #333;
margin-right: 8px;
display: inline-block;
min-width: 100px;
}
span {
color: #666;
}
}
.detail-content {
color: #666;
line-height: 1.6;
margin-top: 5px;
padding: 10px;
background: #f9f9f9;
border-radius: 4px;
white-space: pre-wrap;
word-break: break-word;
}
</style>