feat:新增反馈按钮和举报按钮的弹窗组件

This commit is contained in:
zheng020 2026-06-16 17:39:30 +08:00
parent a2d36cb7a0
commit 32e329b1dc
11 changed files with 3450 additions and 1698 deletions

View File

@ -210,6 +210,14 @@
</view> </view>
</template> </template>
</LikeUsersModal> </LikeUsersModal>
<!-- 举报弹窗也由 ShareReportButtons 触发asset-detail 内显式声明以便直接控制 -->
<ReportModal
:visible="showReportModal"
:assetId="assetIdParam"
@close="showReportModal = false"
@submit="handleReportSubmit"
/>
</view> </view>
</template> </template>
@ -221,6 +229,7 @@ import { getAssetCoverRealUrl } from '@/utils/assetImageHelper.js';
import LikeUsersModal from '@/pages/components/LikeUsersModal.vue'; import LikeUsersModal from '@/pages/components/LikeUsersModal.vue';
import ShareReportButtons from '@/pages/components/ShareReportButtons.vue'; import ShareReportButtons from '@/pages/components/ShareReportButtons.vue';
import ShareModal from '@/pages/components/ShareModal.vue'; import ShareModal from '@/pages/components/ShareModal.vue';
import ReportModal from '@/pages/components/ReportModal.vue';
import LenticularCard from '@/components/lenticular/LenticularCard.vue'; import LenticularCard from '@/components/lenticular/LenticularCard.vue';
import { useLenticularCraftTiltPreview } from '@/composables/useLenticularCraftTiltPreview.js'; import { useLenticularCraftTiltPreview } from '@/composables/useLenticularCraftTiltPreview.js';
import { import {
@ -429,6 +438,13 @@ const likeUsersActiveTab = ref(0);
const showShareModal = ref(false); const showShareModal = ref(false);
const shareCoverUrl = ref(''); const shareCoverUrl = ref('');
const shareQrcodeUrl = ref(''); const shareQrcodeUrl = ref('');
// asset-detail ShareReportButtons modal
const showReportModal = ref(false);
// //线
const handleReportSubmit = (payload) => {
console.log('[asset-detail] report submitted', payload);
};
// API // API
const allLikedUsers = ref([]); const allLikedUsers = ref([]);

View File

@ -0,0 +1,615 @@
<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>

View File

@ -0,0 +1,678 @@
<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>

View File

@ -20,11 +20,20 @@
:qrcodeUrl="shareQrcodeUrl" :qrcodeUrl="shareQrcodeUrl"
@close="showShareModal = false" @close="showShareModal = false"
/> />
<!-- 举报弹窗 -->
<ReportModal
:visible="showReportModal"
:assetId="assetId"
@close="showReportModal = false"
@submit="handleReportSubmit"
/>
</template> </template>
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import ShareModal from './ShareModal.vue'; import ShareModal from './ShareModal.vue';
import ReportModal from './ReportModal.vue';
const props = defineProps({ const props = defineProps({
// //
@ -68,6 +77,9 @@ const showShareModal = ref(false);
const shareCoverUrl = ref(''); const shareCoverUrl = ref('');
const shareQrcodeUrl = ref(''); const shareQrcodeUrl = ref('');
//
const showReportModal = ref(false);
// //
const handleShare = () => { const handleShare = () => {
shareCoverUrl.value = props.coverUrl; shareCoverUrl.value = props.coverUrl;
@ -76,18 +88,18 @@ const handleShare = () => {
showShareModal.value = true; showShareModal.value = true;
}; };
// // -
const handleReport = () => { const handleReport = () => {
uni.showModal({ if (!props.assetId) {
title: '举报', uni.showToast({ title: '藏品信息缺失', icon: 'none' });
content: '确定要举报该藏品吗?', return;
success: (res) => { }
if (res.confirm) { showReportModal.value = true;
// TODO: API };
uni.showToast({ title: '举报成功', icon: 'success' });
} // /
} const handleReportSubmit = (payload) => {
}); console.log('[ShareReportButtons] report submitted', payload);
}; };
</script> </script>

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

View File

@ -1018,3 +1018,30 @@ export const dashboardApi = {
getUpgradeProgress: (starId) => dashboardRequest('/upgrade-progress', { star_id: starId }), getUpgradeProgress: (starId) => dashboardRequest('/upgrade-progress', { star_id: starId }),
} }
// ==================== 举报 ====================
// TODO: 等待后端接入资产举报接口 (建议路径: POST /api/v1/assets/:assetId/reports)
// 或通用举报接口 (建议路径: POST /api/v1/reports),参数:
// { target_type: 'asset', target_id, reason, content, image_url }
export function submitReportApi(data) {
return request({
url: '/api/v1/assets/reports',
method: 'POST',
data
})
}
// ==================== 意见反馈 ====================
// 鉴权:由 gateway AuthMiddleware 强制 JWT
// 路径:POST /api/v1/feedback
// 参数:{ type, content, image_url }
// - type: 'functionality' | 'ui' | 'suggestion' | 'other'
// - content: 用户填写的具体描述(可空)
// - image_url: 截图 URL(可空,需先走 OSS 上传)
export function submitFeedbackApi(data) {
return request({
url: '/api/v1/feedback',
method: 'POST',
data
})
}