topfans/frontend/pages/components/ReportModal.vue

679 lines
15 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-jb.png"
mode="aspectFill"
></image>
<!-- 蒙层(增强对比) -->
<view class="modal-overlay"></view>
<!-- 关闭按钮 -->
<!-- <view class="close-button" @touchstart.stop="handleCloseTouchStart" @touchend.stop="handleCloseTouchEnd"
@click="handleCloseClick">
<image class="back-icon" src="/static/starbookcontent/tuichu.png" mode="aspectFit"></image>
</view> -->
<!-- 内容区域 -->
<view class="modal-content" @touchstart.stop @touchend.stop @click.stop>
<!-- 标题 -->
<view class="title-row">
<!-- <text class="modal-title">举报</text> -->
</view>
<!-- 举报类型选项 3x3 网格 -->
<view class="options-grid">
<view
v-for="(opt, idx) in reportOptions"
:key="opt.value"
class="option-item"
:class="{ 'option-item--active': selectedReason === opt.value }"
@tap="handleSelectOption(opt)"
>
<text
class="option-text"
:class="{ 'option-text--active': selectedReason === 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 { submitReportApi, uploadLocalFileToOss } from "@/utils/api.js";
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
assetId: {
type: [String, Number],
default: "",
},
});
const emit = defineEmits(["close", "submit"]);
// 举报类型选项(与 Figma 设计一致)
const reportOptions = [
{ value: "malicious_distortion", label: "恶意丑化" },
{ value: "baiting", label: "拉踩引战" },
{ value: "plagiarism", label: "抄袭盗图" },
{ value: "slander", label: "抹黑造谣" },
{ value: "bad_guidance", label: "不良导向" },
{ value: "legal_red_line", label: "法规红线" },
{ value: "illegal_ads", label: "违规广告" },
{ value: "personal_dislike", label: "个人反感" },
{ value: "other", label: "其他" },
];
// 状态
const selectedReason = ref("");
const content = ref("");
const imagePath = ref("");
const imageUrl = ref("");
const submitting = ref(false);
// 是否可提交:必须选择举报类型
const canSubmit = computed(() => {
return !!selectedReason.value && !submitting.value;
});
// 关闭相关:与 LikeUsersModal 保持一致的触摸处理,防止误触穿透
let maskTouchStartTime = 0;
const handleMaskTouchStart = (e) => {
maskTouchStartTime = Date.now();
e.preventDefault();
e.stopPropagation();
};
const handleMaskClick = () => {
emit("close");
};
let closeTouchStartTime = 0;
let closeTouchLocked = false;
const handleCloseTouchStart = (e) => {
closeTouchStartTime = Date.now();
closeTouchLocked = false;
};
const handleCloseTouchEnd = (e) => {
if (closeTouchLocked) return;
closeTouchLocked = true;
const touchDuration = Date.now() - closeTouchStartTime;
if (touchDuration < 300) {
e.preventDefault();
e.stopPropagation();
handleResetAndClose();
}
};
const handleCloseClick = (e) => {
if (e && e.preventDefault) {
e.preventDefault();
e.stopPropagation();
}
handleResetAndClose();
};
const handleResetAndClose = () => {
// 关闭前重置表单
selectedReason.value = "";
content.value = "";
imagePath.value = "";
imageUrl.value = "";
submitting.value = false;
emit("close");
};
// 选择举报类型
const handleSelectOption = (opt) => {
selectedReason.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("[ReportModal] chooseImage fail", err);
},
});
};
// 移除图片
const handleRemoveImage = () => {
imagePath.value = "";
imageUrl.value = "";
};
// 确认提交
const handleConfirm = async () => {
if (!canSubmit.value) {
if (!selectedReason.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: "asset",
});
uploadedUrl = upRes?.imageUrl || "";
imageUrl.value = uploadedUrl;
} catch (e) {
console.error("[ReportModal] upload image fail", e);
uni.showToast({ title: "图片上传失败", icon: "none" });
submitting.value = false;
return;
}
}
// 调用举报 API
const res = await submitReportApi({
asset_id: props.assetId,
reason: selectedReason.value,
content: content.value || "",
image_url: uploadedUrl || imageUrl.value || "",
});
if (res && res.code === 0) {
uni.showToast({ title: "举报成功", icon: "success" });
emit("submit", { reason: selectedReason.value });
handleResetAndClose();
} else {
uni.showToast({
title: (res && res.message) || "举报失败",
icon: "none",
});
}
} catch (e) {
console.error("[ReportModal] 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: 80%;
z-index: 0;
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%
);
}
/* 关闭按钮 */
.close-button {
position: absolute;
top: 16rpx;
right: 16rpx;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.back-icon {
width: 36rpx;
height: 36rpx;
}
/* 内容区域 */
.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;
}
/* 9 个举报类型 - 3 列网格 */
.options-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 16rpx 14rpx;
margin-bottom: 24rpx;
}
.option-item {
flex: 0 0 calc((100% - 28rpx) / 3);
height: 64rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
/* 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:
0 4rpx 4rpx rgba(182, 48, 48, 0.25),
0 2rpx 3rpx rgba(212, 39, 39, 0.31);
backdrop-filter: blur(0.6px); */
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 {
/* flex: 1; */
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>