Files
ai-dimension/client/src/pages/feedback/feedback.vue
T

471 lines
10 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>
<view class="feedback-page page-layout">
<!-- 状态栏占位 -->
<view class="status-bar-placeholder"></view>
<!-- 顶部导航 -->
<view class="nav-bar">
<view class="nav-back" @click="goBack">‹</view>
<text class="nav-title">意见反馈</text>
<view class="nav-spacer"></view>
</view>
<!-- 反馈类型选择 -->
<view class="form-section">
<text class="form-title">反馈类型</text>
<view class="type-grid">
<view
class="type-btn"
:class="{ active: form.type === t.value }"
v-for="t in feedbackTypes"
:key="t.value"
@click="form.type = t.value"
>
<text class="type-icon">{{ t.icon }}</text>
<text class="type-name">{{ t.name }}</text>
</view>
</view>
</view>
<!-- 标题输入 -->
<view class="form-section">
<text class="form-title">标题</text>
<input
class="form-input"
type="text"
v-model="form.title"
:placeholder="'简短描述您的问题或建议'"
placeholder-style="color: var(--text-disabled); font-size: 14px;"
maxlength="50"
/>
</view>
<!-- 内容输入 -->
<view class="form-section">
<text class="form-title">内容</text>
<textarea
class="form-textarea"
v-model="form.content"
:placeholder="'请详细描述您遇到的问题或建议(至少 10 个字)'"
placeholder-style="color: var(--text-disabled); font-size: 14px;"
maxlength="1000"
:auto-height="true"
></textarea>
</view>
<!-- 提交按钮 -->
<view class="submit-section">
<button
class="submit-btn"
:disabled="submitting || !formValid"
@click="submitFeedback"
>
<text>{{ submitting ? '提交中...' : '提交反馈' }}</text>
</button>
</view>
<!-- 我的反馈 -->
<view class="form-section">
<view class="my-header">
<text class="form-title">我的反馈</text>
<text class="my-count" v-if="myFeedbacks.length > 0">
共 {{ myFeedbacks.length }} 条
</text>
</view>
<!-- 反馈列表 -->
<scroll-view class="my-list" scroll-y>
<view
class="my-item"
v-for="fb in myFeedbacks"
:key="fb._id || fb.id"
>
<view class="my-item-top">
<text class="my-title">{{ fb.title || '无标题' }}</text>
<text class="my-status" :class="fb.status">{{ statusLabel(fb.status) }}</text>
</view>
<view class="my-item-bottom">
<text class="my-type">{{ typeLabel(fb.type) }}</text>
<text class="my-time">{{ formatTime(fb.createdAt || fb.time) }}</text>
</view>
</view>
<!-- 无反馈提示 -->
<view class="my-empty" v-if="myFeedbacks.length === 0">
<text class="my-empty-text">暂无反馈记录</text>
</view>
<view class="my-spacer"></view>
</scroll-view>
</view>
</view>
</template>
<script setup>
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
import { ref, computed, onMounted } from 'vue'
import { feedbackApi } from '@/utils/api'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 反馈类型
const feedbackTypes = [
{ value: 'bug', name: '问题报告', icon: '🐛' },
{ value: 'feature', name: '功能建议', icon: '★' },
{ value: 'content', name: '内容纠错', icon: '📝' },
{ value: 'other', name: '其他', icon: '💬' }
]
// 表单数据
const form = ref({
type: 'bug',
title: '',
content: ''
})
const submitting = ref(false)
const myFeedbacks = ref([])
// 表单验证
const formValid = computed(() => {
return form.value.title.trim().length > 0 && form.value.content.trim().length >= 10
})
// 状态标签
function statusLabel(status) {
const map = {
pending: '待处理',
processing: '处理中',
resolved: '已解决',
closed: '已关闭'
}
return map[status] || '待处理'
}
// 类型标签
function typeLabel(type) {
const map = {
bug: '🐛 问题报告',
feature: '功能建议',
content: '📝 内容纠错',
other: '💬 其他'
}
return map[type] || '💬 其他'
}
// 格式化时间
function formatTime(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
const now = new Date()
const diff = now.getTime() - date.getTime()
const hours = Math.floor(diff / 3600000)
if (hours < 1) return '刚刚'
if (hours < 24) return `${hours} 小时前`
const days = Math.floor(hours / 24)
if (days < 30) return `${days} 天前`
const months = Math.floor(days / 30)
if (months < 12) return `${months} 个月前`
return date.toLocaleDateString('zh-CN')
}
// 提交反馈
async function submitFeedback() {
if (!formValid.value) {
uni.showToast({ title: '请填写标题和至少 10 字内容', icon: 'none' })
return
}
submitting.value = true
try {
// 不传 token 可匿名提交;有 token 时带 token 提交
await feedbackApi.create(
{
type: form.value.type,
title: form.value.title.trim(),
content: form.value.content.trim()
},
userStore.isLoggedIn ? userStore.token : null
)
uni.showToast({ title: '反馈提交成功', icon: 'success' })
// 清空表单
form.value.title = ''
form.value.content = ''
form.value.type = 'bug'
// 刷新反馈列表
fetchMyFeedbacks()
} catch (err) {
console.error('[feedback] 提交失败:', err)
uni.showToast({ title: err.message || '提交失败,请重试', icon: 'none' })
} finally {
submitting.value = false
}
}
// 获取我的反馈
async function fetchMyFeedbacks() {
if (!userStore.isLoggedIn) {
myFeedbacks.value = []
return
}
try {
const data = await feedbackApi.my(userStore.token)
myFeedbacks.value = Array.isArray(data) ? data : (data.list || data.items || [])
} catch (err) {
console.error('[feedback] 获取我的反馈失败:', err)
myFeedbacks.value = []
}
}
onMounted(() => {
fetchMyFeedbacks()
})
function goBack() {
uni.navigateBack()
}
// 微信小程序分享
// #ifdef MP-WEIXIN
onShareAppMessage(() => ({
title: '帮AI维度变得更好 · 意见反馈',
path: '/pages/index/index'
}))
// #endif
</script>
<style lang="scss" scoped>
.feedback-page {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
background: var(--bg-primary);
}
// 顶部导航
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 50px 20px 12px;
flex-shrink: 0;
.nav-back {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
color: var(--text-primary);
}
.nav-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
.nav-spacer { width: 30px; }
}
// 表单区域
.form-section {
padding: 0 20px;
margin-bottom: 14px;
}
.form-title {
display: block;
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 10px;
font-weight: 500;
}
// 类型选择网格
.type-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
.type-btn {
padding: 10px 6px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 10px;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
transition: all 0.3s;
.type-icon { font-size: 18px; }
.type-name {
font-size: 11px;
color: var(--text-secondary);
}
&.active {
background: rgba(179, 136, 255, 0.12);
border-color: var(--dim1-color);
box-shadow: 0 0 12px rgba(179, 136, 255, 0.15);
.type-name { color: var(--text-primary); }
}
}
}
// 输入框
.form-input {
width: 100%;
height: 44px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 10px;
padding: 0 14px;
font-size: 14px;
color: var(--text-primary);
outline: none;
box-sizing: border-box;
}
// 文本域
.form-textarea {
width: 100%;
min-height: 100px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 10px;
padding: 12px 14px;
font-size: 14px;
color: var(--text-primary);
outline: none;
box-sizing: border-box;
resize: none;
}
// 提交区域
.submit-section {
padding: 0 20px;
margin-bottom: 16px;
.submit-btn {
width: 100%;
height: 46px;
border-radius: 12px;
border: none;
background: linear-gradient(135deg, var(--dim1-color), var(--dim3-color));
color: white;
font-size: 15px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 16px rgba(179, 136, 255, 0.3);
transition: all 0.3s;
&[disabled] {
opacity: 0.5;
box-shadow: none;
}
}
}
// 我的反馈头部
.my-header {
display: flex;
align-items: baseline;
justify-content: space-between;
.my-count {
font-size: 11px;
color: var(--text-disabled);
}
}
// 反馈列表
.my-list {
margin-top: 10px;
padding-bottom: 20px;
max-height: 300px;
overflow-y: auto;
}
.my-item {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 10px;
padding: 10px 14px;
margin-bottom: 8px;
}
.my-item-top {
display: flex;
align-items: center;
justify-content: space-between;
.my-title {
font-size: 13px;
color: var(--text-primary);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
margin-right: 8px;
}
.my-status {
font-size: 11px;
padding: 2px 8px;
border-radius: 6px;
white-space: nowrap;
}
.my-status.pending {
background: rgba(255, 213, 79, 0.12);
color: var(--dim4-color);
}
.my-status.processing {
background: rgba(77, 208, 225, 0.12);
color: var(--dim2-color);
}
.my-status.resolved {
background: rgba(77, 182, 172, 0.12);
color: var(--dim3-color);
}
.my-status.closed {
background: rgba(255, 255, 255, 0.08);
color: var(--text-disabled);
}
}
.my-item-bottom {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 6px;
.my-type { font-size: 11px; color: var(--text-secondary); }
.my-time { font-size: 11px; color: var(--text-disabled); }
}
// 无反馈
.my-empty {
text-align: center;
padding: 30px 0;
.my-empty-text {
font-size: 13px;
color: var(--text-disabled);
}
}
.my-spacer {
height: 10px;
}
</style>