topfans/frontend/pages/components/FeedbackModal.vue

616 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
v-if="visible"
class="modal-wrapper"
@touchmove.stop.prevent
@click.stop
>
<transition name="fade">
<view
v-if="visible"
class="modal-mask"
@touchstart.stop="handleMaskTouchStart"
@click.stop="handleMaskClick"
></view>
</transition>
<transition name="slide-up">
<view v-if="visible" class="modal-container" @click.stop>
<!-- 背景图片(紫色渐变 + 装饰) -->
<image
class="modal-background"
src="/static/assetDetail/Vector.png"
mode="aspectFill"
></image>
<image
class="title-background"
src="/static/assetDetail/topfans-fk.png"
mode="aspectFill"
></image>
<!-- 蒙层(增强对比) -->
<view class="modal-overlay"></view>
<!-- 内容区域 -->
<view class="modal-content" @touchstart.stop @touchend.stop @click.stop>
<!-- 标题:意见反馈 -->
<view class="title-row">
<!-- <text class="modal-title">意见反馈</text> -->
</view>
<!-- 4 个反馈类型 - 单行 -->
<view class="options-grid">
<view
v-for="opt in feedbackOptions"
:key="opt.value"
class="option-item"
:class="{ 'option-item--active': selectedType === opt.value }"
@tap="handleSelectOption(opt)"
>
<text
class="option-text"
:class="{ 'option-text--active': selectedType === opt.value }"
>
{{ opt.label }}
</text>
</view>
</view>
<!-- 上传图片 -->
<view class="upload-area" @tap="handleChooseImage">
<view v-if="!imagePath" class="upload-placeholder">
<view class="upload-icon-circle">
<text class="upload-plus">+</text>
</view>
<text class="upload-label">上传图片</text>
</view>
<view v-else class="upload-preview">
<image
class="upload-image"
:src="imagePath"
mode="aspectFill"
></image>
<view class="upload-remove" @tap.stop="handleRemoveImage">
<text class="upload-remove-text">×</text>
</view>
</view>
</view>
<!-- 文字输入 -->
<view class="input-area">
<textarea
class="input-field"
v-model="content"
placeholder="请输入具体内容"
placeholder-class="input-placeholder"
:maxlength="200"
auto-height
></textarea>
</view>
<!-- 按钮 -->
<view class="button-row">
<view class="action-button back-button" @tap="handleCloseClick">
<text class="action-text">返回</text>
</view>
<view
class="action-button confirm-button"
:class="{ 'action-button--disabled': !canSubmit }"
@tap="handleConfirm"
>
<text class="action-text">{{
submitting ? "提交中" : "确定"
}}</text>
</view>
</view>
</view>
</view>
</transition>
</view>
</template>
<script setup>
import { ref, computed } from "vue";
import { submitFeedbackApi, uploadLocalFileToOss } from "@/utils/api.js";
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["close", "submit"]);
// 反馈类型选项(与 Figma 设计一致)
const feedbackOptions = [
{ value: "functionality", label: "功能异常" },
{ value: "ui", label: "界面问题" },
{ value: "suggestion", label: "产品建议" },
{ value: "other", label: "其他" },
];
// 状态
const selectedType = ref("");
const content = ref("");
const imagePath = ref("");
const imageUrl = ref("");
const submitting = ref(false);
// 是否可提交:必须选择反馈类型
const canSubmit = computed(() => {
return !!selectedType.value && !submitting.value;
});
// 关闭相关:与 ReportModal 保持一致的触摸处理,防止误触穿透
let maskTouchStartTime = 0;
const handleMaskTouchStart = (e) => {
maskTouchStartTime = Date.now();
e.preventDefault();
e.stopPropagation();
};
const handleMaskClick = () => {
emit("close");
};
const handleCloseClick = (e) => {
if (e && e.preventDefault) {
e.preventDefault();
e.stopPropagation();
}
handleResetAndClose();
};
const handleResetAndClose = () => {
// 关闭前重置表单
selectedType.value = "";
content.value = "";
imagePath.value = "";
imageUrl.value = "";
submitting.value = false;
emit("close");
};
// 选择反馈类型
const handleSelectOption = (opt) => {
selectedType.value = opt.value;
};
// 选择图片
const handleChooseImage = () => {
uni.chooseImage({
count: 1,
sizeType: ["compressed"],
sourceType: ["album", "camera"],
success: (res) => {
const tempFile = res.tempFilePaths?.[0];
if (tempFile) {
imagePath.value = tempFile;
}
},
fail: (err) => {
console.warn("[FeedbackModal] chooseImage fail", err);
},
});
};
// 移除图片
const handleRemoveImage = () => {
imagePath.value = "";
imageUrl.value = "";
};
// 确认提交
const handleConfirm = async () => {
if (!canSubmit.value) {
if (!selectedType.value) {
uni.showToast({ title: "请选择反馈类型", icon: "none" });
}
return;
}
submitting.value = true;
try {
// 如果选择了本地图片,先上传到 OSS
let uploadedUrl = "";
if (imagePath.value && !imageUrl.value) {
try {
const upRes = await uploadLocalFileToOss(imagePath.value, {
type: "feedback",
});
uploadedUrl = upRes?.imageUrl || "";
imageUrl.value = uploadedUrl;
} catch (e) {
console.error("[FeedbackModal] upload image fail", e);
uni.showToast({ title: "图片上传失败", icon: "none" });
submitting.value = false;
return;
}
}
// 调用反馈 API
const res = await submitFeedbackApi({
type: selectedType.value,
content: content.value || "",
image_url: uploadedUrl || imageUrl.value || "",
});
if (res && res.code === 0) {
uni.showToast({ title: "反馈已提交", icon: "success" });
emit("submit", { type: selectedType.value });
handleResetAndClose();
} else {
uni.showToast({
title: (res && res.message) || "提交失败",
icon: "none",
});
}
} catch (e) {
console.error("[FeedbackModal] submit fail", e);
uni.showToast({ title: e?.message || "提交失败", icon: "none" });
} finally {
submitting.value = false;
}
};
</script>
<style scoped>
.modal-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
.modal-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 1;
}
.modal-container {
position: relative;
width: 680rpx;
max-height: 85vh;
border-radius: 32rpx;
overflow: hidden;
z-index: 2;
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.5);
}
.modal-background {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 0;
object-fit: cover;
}
.title-background {
position: absolute;
top: -224rpx;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 110%;
z-index: 2;
object-fit: cover;
}
.modal-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 1;
background: linear-gradient(
180deg,
rgba(75, 30, 110, 0.35) 0%,
rgba(131, 75, 158, 0.55) 100%
);
}
/* 内容区域 */
.modal-content {
position: relative;
z-index: 2;
padding: 248rpx 36rpx 36rpx;
display: flex;
flex-direction: column;
}
/* 标题 */
.title-row {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24rpx;
}
.modal-title {
font-size: 36rpx;
font-weight: bold;
color: #fff9e7;
font-family: "yt", sans-serif;
text-shadow: -1rpx 1rpx 4rpx rgba(0, 0, 0, 0.84);
letter-spacing: 4rpx;
}
/* 4 个反馈类型 - 单行 */
.options-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 16rpx 14rpx;
margin-bottom: 24rpx;
}
.option-item {
/* 单行 4 个:扣除 3 个 gap 后均分 */
flex: 0 0 calc((100% - 42rpx) / 4);
height: 64rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
background: url("/static/assetDetail/text-bj.png") center no-repeat;
transition: all 0.2s ease;
box-shadow: 0 4px 4px 0 rgba(182, 48, 48, 0.25), 0 2px 0px 0 rgba(212, 39, 39, 0.31);
}
.option-item:active {
transform: scale(0.96);
opacity: 0.85;
}
.option-item--active {
background: linear-gradient(
165deg,
#f0e4b1 0%,
#f08399 50%,
#b94e73 90%,
#834b9e 100%
);
box-shadow:
0 4rpx 12rpx rgba(255, 143, 158, 0.4),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.45);
transform: scale(1.04);
}
.option-text {
font-size: 26rpx;
color: #fff9e7;
font-family: "yt", sans-serif;
text-shadow: -1rpx 1rpx 4rpx rgba(0, 0, 0, 0.84);
white-space: nowrap;
}
.option-text--active {
color: #ffffff;
font-weight: bold;
}
/* 上传图片区域 */
.upload-area {
height: 120rpx;
border-radius: 16rpx;
margin-bottom: 20rpx;
background: linear-gradient(
168.9deg,
rgba(154, 146, 255, 0.48) 4.58%,
rgba(255, 202, 229, 0.48) 43.91%,
rgba(255, 250, 253, 0.475) 76.13%,
rgba(211, 209, 255, 0.48) 91.61%
);
box-shadow: 2rpx 4rpx 1.8rpx rgba(214, 41, 41, 0.12);
backdrop-filter: blur(0.6px);
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.upload-area:active {
opacity: 0.85;
}
.upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8rpx;
}
.upload-icon-circle {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.25);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.25);
}
.upload-plus {
font-size: 36rpx;
color: #fff9e7;
line-height: 1;
font-weight: 300;
margin-top: -4rpx;
text-shadow: -1rpx 1rpx 4rpx rgba(0, 0, 0, 0.84);
}
.upload-label {
font-size: 24rpx;
color: #fff9e7;
font-family: "yt", sans-serif;
opacity: 0.85;
text-shadow: -1rpx 1rpx 4rpx rgba(0, 0, 0, 0.84);
}
.upload-preview {
position: relative;
width: 100%;
height: 100%;
}
.upload-image {
width: 100%;
height: 100%;
}
.upload-remove {
position: absolute;
top: 6rpx;
right: 6rpx;
width: 40rpx;
height: 40rpx;
border-radius: 50%;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
}
.upload-remove-text {
font-size: 32rpx;
color: #fff;
line-height: 1;
margin-top: -4rpx;
}
/* 文字输入 */
.input-area {
height: 100rpx;
border-radius: 16rpx;
margin-bottom: 32rpx;
background: linear-gradient(
168.9deg,
rgba(154, 146, 255, 0.48) 4.58%,
rgba(255, 202, 229, 0.48) 43.91%,
rgba(255, 250, 253, 0.475) 76.13%,
rgba(211, 209, 255, 0.48) 91.61%
);
box-shadow: 2rpx 4rpx 1.8rpx rgba(214, 41, 41, 0.12);
backdrop-filter: blur(0.6px);
padding: 16rpx 24rpx;
}
.input-field {
width: 100%;
height: 100%;
font-size: 26rpx;
color: #fff9e7;
font-family: "yt", sans-serif;
line-height: 1.5;
text-shadow: -1rpx 1rpx 4rpx rgba(0, 0, 0, 0.84);
}
.input-placeholder {
color: rgba(255, 249, 231, 0.6) !important;
font-size: 26rpx;
}
/* 按钮 */
.button-row {
display: flex;
align-items: center;
justify-content: center;
gap: 24rpx;
padding: 0 12rpx;
}
.action-button {
width: 192rpx;
height: 72rpx;
border-radius: 36rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 1rpx 2rpx 5.1rpx rgba(225, 33, 33, 0.32);
transition: all 0.2s ease;
}
.action-button:active {
transform: scale(0.96);
opacity: 0.85;
}
.action-button--disabled {
opacity: 0.55;
}
.back-button {
opacity: 0.82;
background: linear-gradient(
116deg,
rgba(246, 233, 180, 0.2) -19.66%,
rgba(240, 131, 153, 0.2) 70.92%,
rgba(213, 107, 109, 0.17) 138.79%,
rgba(105, 209, 230, 0.14) 198.61%
);
box-shadow: 0 1px 4px 0 rgba(255, 255, 255, 0.5);
}
.confirm-button {
opacity: 0.82;
background: linear-gradient(
116deg,
#f6e9b4 -19.66%,
#f08399 70.92%,
rgba(213, 107, 109, 0.84) 138.79%,
rgba(105, 209, 230, 0.69) 198.61%
);
box-shadow: 1px 2px 5.1px 0 rgba(225, 33, 33, 0.32);
}
.action-text {
font-size: 30rpx;
color: #fff9e7;
font-family: "yt", sans-serif;
font-weight: bold;
text-shadow: -1rpx 1rpx 4rpx rgba(191, 61, 61, 0.84);
letter-spacing: 2rpx;
}
/* 动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.slide-up-enter-active,
.slide-up-leave-active {
transition: all 0.3s ease;
}
.slide-up-enter-from,
.slide-up-leave-to {
opacity: 0;
transform: translateY(80rpx);
}
</style>