644 lines
14 KiB
Vue
644 lines
14 KiB
Vue
<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>
|