679 lines
15 KiB
Vue
679 lines
15 KiB
Vue
<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>
|