Files
MrleeAdmin/frontend/scanapp/pages/settings/feedback.vue
T
2026-08-31 11:33:59 +08:00

648 lines
13 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" :class="`theme-${themeMode}`">
<view class="hero-card">
<view>
<text class="hero-tag">意见收集</text>
<text class="hero-title">问题反馈</text>
<text class="hero-subtitle">提交使用建议、问题描述或异常截图,我们会尽快跟进</text>
</view>
<view class="hero-icon">
<u-icon name="email" color="#ffffff" size="44" />
</view>
</view>
<view class="section-card">
<view class="section-header">
<text class="section-title">反馈内容</text>
</view>
<view class="field-card">
<text class="field-label">反馈类型</text>
<view class="chip-row">
<view
v-for="item in typeOptions"
:key="item.value"
class="chip"
:class="{ 'is-active': form.type === item.value }"
@tap="form.type = item.value"
>
{{ item.label }}
</view>
</view>
</view>
<view class="field-card">
<text class="field-label">问题描述</text>
<view class="textarea-wrap">
<textarea v-model="form.content" placeholder="请描述遇到的问题、操作步骤或建议" placeholder-class="textarea-placeholder" maxlength="500" />
</view>
</view>
<view class="field-card">
<text class="field-label">联系方式(选填)</text>
<u-input v-model="form.contact" placeholder="手机号 / 微信 / 邮箱" :border="false" />
</view>
<view class="field-card">
<text class="field-label">截图附件</text>
<view class="upload-row">
<view v-if="form.images.length < 3" class="upload-box" @tap="pickImage" :class="{ 'is-disabled': uploading || form.images.length >= 3 }">
<u-icon name="plus" color="#ff9d42" size="34" />
<text>{{ uploading ? '上传中' : '添加截图' }}</text>
</view>
<view v-for="(img, index) in form.images" :key="img" class="upload-preview">
<image :src="img" mode="aspectFill" />
<text class="upload-label">图片{{ index + 1 }}</text>
<view class="upload-delete" @tap.stop="removeImage(index)">
<u-icon name="close" color="#ffffff" size="20" />
</view>
</view>
</view>
</view>
</view>
<view class="tips-card">
<text class="tips-title">提交说明</text>
<text class="tips-text">1. 请优先说明页面名称和操作路径;2. 如果是扫码异常,请附上单号或二维码截图;3. 反馈会保存在本机历史记录中。</text>
</view>
<view class="bottom-actions">
<view class="secondary-button" @tap="resetForm">清空</view>
<view class="primary-button" :class="{ 'is-disabled': submitting }" @tap="submitFeedback">{{ submitting ? '提交中' : '提交反馈' }}</view>
</view>
<view class="history-card">
<view class="history-header">
<view>
<text class="section-title">我的反馈</text>
<text class="history-subtitle">仅展示当前账号已提交的问题</text>
</view>
<view class="refresh-button" :class="{ 'is-disabled': historyLoading }" @tap="loadFeedbackList">
<u-icon name="reload" color="#ff8a3d" size="28" />
<text>刷新</text>
</view>
</view>
<view v-if="historyLoading" class="history-loading">正在加载反馈记录…</view>
<view v-else-if="feedbackList.length" class="feedback-list">
<view v-for="item in feedbackList" :key="item.id" class="feedback-record" @tap="goFeedbackDetail(item.id)">
<view class="record-main">
<view class="record-title-row">
<text class="record-type">{{ getTypeLabel(item.type) }}</text>
<text class="record-time">{{ item.created_date || '时间未知' }}</text>
</view>
<text class="record-content">{{ item.content }}</text>
</view>
<u-icon name="arrow-right" color="#9aa5b5" size="28" />
</view>
</view>
<u-empty v-else mode="list" text="暂无已提交的反馈" icon="list" />
</view>
</view>
</template>
<script>
import { getMyFeedbackList, submitFeedback as createFeedback } from '@/api/feedback.js'
import { uploadImage } from '@/api/upload.js'
export default {
data() {
return {
uploading: false,
submitting: false,
historyLoading: false,
feedbackList: [],
form: {
type: 'bug',
content: '',
contact: '',
images: []
},
typeOptions: [
{ label: '功能异常', value: 'bug' },
{ label: '体验建议', value: 'suggestion' },
{ label: '内容纠错', value: 'correction' }
]
}
},
onShow() {
this.loadFeedbackList()
},
methods: {
getTypeLabel(type) {
return this.typeOptions.find((item) => item.value === type)?.label || '问题反馈'
},
async loadFeedbackList() {
if (this.historyLoading) {
return
}
this.historyLoading = true
try {
const result = await getMyFeedbackList({ page: 1, limit: 10 })
this.feedbackList = Array.isArray(result?.items) ? result.items : []
} catch (error) {
console.error('[scanapp] 获取反馈记录失败', error)
this.feedbackList = []
} finally {
this.historyLoading = false
}
},
goFeedbackDetail(id) {
if (!id) {
return
}
uni.navigateTo({
url: `/pages/settings/feedback-detail?id=${encodeURIComponent(id)}`
})
},
pickImage() {
if (this.uploading || this.submitting || this.form.images.length >= 3) {
return
}
uni.chooseImage({
count: 3 - this.form.images.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: ({ tempFilePaths = [] }) => {
this.form.images = [...this.form.images, ...tempFilePaths].slice(0, 3)
}
})
},
removeImage(index) {
if (this.uploading || this.submitting) return
this.form.images.splice(index, 1)
},
resetForm() {
if (this.submitting || this.uploading) {
return
}
this.form.content = ''
this.form.contact = ''
this.form.images = []
this.form.type = 'bug'
},
async submitFeedback() {
if (this.submitting || this.uploading) {
return
}
if (!this.form.content.trim()) {
uni.showToast({ title: '请输入问题描述', icon: 'none' })
return
}
this.submitting = true
try {
this.uploading = true
const uploadedImages = []
for (const filePath of this.form.images) {
uploadedImages.push(await uploadImage(filePath, 'feedback'))
}
await createFeedback({
type: this.form.type,
content: this.form.content.trim(),
contact: this.form.contact.trim(),
images: uploadedImages,
source: 'app'
})
uni.showToast({ title: '反馈已提交', icon: 'none' })
this.form.content = ''
this.form.contact = ''
this.form.images = []
this.form.type = 'bug'
await this.loadFeedbackList()
} catch (error) {
console.error('[scanapp] 提交反馈失败', error)
} finally {
this.uploading = false
this.submitting = false
}
}
}
}
</script>
<style lang="scss" scoped>
.feedback-page {
min-height: 100vh;
padding: 28rpx 24rpx 48rpx;
box-sizing: border-box;
background: linear-gradient(180deg, #eef6ff 0%, #f4f6fb 36%, #f4f6fb 100%);
}
.hero-card,
.section-card,
.tips-card {
padding: 34rpx;
border-radius: 28rpx;
border: 1rpx solid rgba(255, 255, 255, 0.75);
background: #ffffff;
box-shadow: 0 16rpx 40rpx rgba(23, 35, 61, 0.06);
}
.section-card,
.tips-card {
margin-top: 28rpx;
}
.hero-card {
display: flex;
align-items: center;
justify-content: space-between;
}
.hero-tag,
.section-subtitle,
.tips-text {
display: block;
}
.hero-tag {
color: #ff8a3d;
font-size: 20rpx;
font-weight: 700;
}
.hero-title,
.section-title {
display: block;
margin-top: 10rpx;
color: #17233d;
font-size: 28rpx;
font-weight: 800;
}
.hero-subtitle,
.section-subtitle {
margin-top: 10rpx;
color: #7d8798;
font-size: 20rpx;
line-height: 1.5;
}
.hero-icon {
display: flex;
align-items: center;
justify-content: center;
width: 88rpx;
height: 88rpx;
border-radius: 24rpx;
background: linear-gradient(135deg, #ff9d42 0%, #ff7048 100%);
}
.section-header {
margin-bottom: 20rpx;
}
.field-card + .field-card {
margin-top: 22rpx;
}
.field-card {
padding: 24rpx;
border-radius: 22rpx;
background: #f8fafc;
}
.field-label {
display: block;
margin-bottom: 16rpx;
color: #17233d;
font-size: 24rpx;
font-weight: 700;
}
.chip-row {
display: flex;
flex-wrap: wrap;
}
.chip-row .chip {
display: flex;
flex: 0 0 auto;
margin-right: 24rpx;
margin-bottom: 16rpx;
}
.chip {
padding: 16rpx 22rpx;
border-radius: 18rpx;
background: #e9edf4;
color: #5f6b7a;
font-size: 22rpx;
font-weight: 600;
}
.chip.is-active {
background: linear-gradient(135deg, #ff9d42 0%, #ff7d47 100%);
color: #ffffff;
}
.textarea-wrap {
padding: 18rpx;
border-radius: 20rpx;
background: #ffffff;
}
textarea {
width: 100%;
min-height: 220rpx;
color: #17233d;
font-size: 24rpx;
line-height: 1.6;
}
.upload-row {
display: flex;
flex-wrap: nowrap;
align-items: flex-start;
overflow-x: auto;
padding-bottom: 4rpx;
}
.upload-row > * {
margin-right: 14rpx;
margin-bottom: 14rpx;
}
.upload-box,
.upload-preview {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 144rpx;
height: 144rpx;
border-radius: 20rpx;
overflow: hidden;
}
.upload-box {
flex-direction: column;
border: 2rpx dashed #ffb47e;
background: #fff8f2;
color: #ff8a3d;
font-size: 20rpx;
}
.upload-box text {
margin-top: 8rpx;
}
.upload-preview image {
width: 100%;
height: 100%;
}
.upload-label {
position: absolute;
right: 8rpx;
bottom: 8rpx;
padding: 4rpx 8rpx;
border-radius: 8rpx;
color: #ffffff;
background: rgba(0, 0, 0, 0.45);
font-size: 18rpx;
line-height: 1.2;
}
.upload-delete {
position: absolute;
top: 8rpx;
right: 8rpx;
display: flex;
align-items: center;
justify-content: center;
width: 34rpx;
height: 34rpx;
border-radius: 50%;
background: rgba(0, 0, 0, 0.55);
}
.tips-title {
display: block;
color: #17233d;
font-size: 26rpx;
font-weight: 800;
}
.tips-text {
margin-top: 12rpx;
color: #7d8798;
font-size: 22rpx;
line-height: 1.6;
}
.bottom-actions {
display: flex;
margin-top: 28rpx;
}
.bottom-actions .primary-button {
margin-left: 14rpx;
}
.bottom-actions .secondary-button {
flex-shrink: 0;
}
.secondary-button,
.primary-button {
display: flex;
align-items: center;
justify-content: center;
height: 84rpx;
border-radius: 20rpx;
font-size: 24rpx;
font-weight: 700;
}
.secondary-button {
width: 32%;
background: #f0f3f8;
color: #56647a;
}
.primary-button {
flex: 1;
background: linear-gradient(135deg, #ff9d42 0%, #ff7048 100%);
color: #ffffff;
}
.upload-box.is-disabled,
.primary-button.is-disabled,
.refresh-button.is-disabled {
opacity: 0.6;
}
.history-card {
margin-top: 28rpx;
padding: 34rpx;
border-radius: 28rpx;
background: #ffffff;
box-shadow: 0 16rpx 40rpx rgba(23, 35, 61, 0.06);
}
.history-header,
.feedback-record,
.record-title-row,
.refresh-button {
display: flex;
align-items: center;
}
.history-header {
justify-content: space-between;
}
.history-subtitle {
display: block;
margin-top: 10rpx;
color: #7d8798;
font-size: 20rpx;
}
.refresh-button {
padding: 12rpx 16rpx;
border-radius: 16rpx;
background: #fff8f2;
color: #ff8a3d;
font-size: 20rpx;
}
.refresh-button text {
margin-left: 8rpx;
}
.history-loading {
padding: 40rpx 0;
color: #7d8798;
font-size: 22rpx;
text-align: center;
}
.feedback-list {
margin-top: 20rpx;
}
.feedback-record {
justify-content: space-between;
padding: 22rpx 0;
border-bottom: 1rpx solid #edf0f5;
}
.feedback-record:last-child {
border-bottom: 0;
}
.record-main {
min-width: 0;
flex: 1;
}
.record-title-row {
justify-content: space-between;
}
.record-type {
color: #17233d;
font-size: 24rpx;
font-weight: 700;
}
.record-time {
margin-left: 20rpx;
color: #7d8798;
font-size: 18rpx;
flex-shrink: 0;
}
.record-content {
display: block;
margin-top: 10rpx;
margin-right: 20rpx;
color: #5f6b7a;
font-size: 22rpx;
line-height: 1.5;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.theme-dark.feedback-page {
background: #151923;
}
.theme-dark .hero-card,
.theme-dark .section-card,
.theme-dark .tips-card,
.theme-dark .history-card {
border-color: #364152;
background: #222936;
box-shadow: none;
}
.theme-dark .field-card,
.theme-dark .textarea-wrap,
.theme-dark .upload-box {
border-color: #364152;
background: #1c2431;
}
.theme-dark .hero-title,
.theme-dark .section-title,
.theme-dark .field-label,
.theme-dark .tips-title,
.theme-dark textarea {
color: #f2f5fa;
}
.theme-dark .hero-subtitle,
.theme-dark .section-subtitle,
.theme-dark .tips-text,
.theme-dark .history-subtitle,
.theme-dark .history-loading,
.theme-dark .record-time,
.theme-dark .record-content,
.theme-dark .chip,
.theme-dark .upload-box {
color: #aeb9ca;
}
.theme-dark .record-type {
color: #f2f5fa;
}
.theme-dark .feedback-record {
border-color: #364152;
}
.theme-dark .refresh-button {
background: #34291f;
}
.theme-dark .chip {
background: #1a2230;
}
.theme-dark .chip.is-active {
background: linear-gradient(135deg, #a85d2f 0%, #8b4d35 100%);
color: #ffffff;
}
.theme-dark textarea::placeholder,
.theme-dark :deep(.u-input__input)::placeholder {
color: #8996aa;
}
.theme-dark .secondary-button {
background: #293344;
color: #d7deea;
}
.theme-dark .primary-button {
background: linear-gradient(135deg, #ff9d42 0%, #ff7048 100%);
}
</style>