Files
rating/pages/release/index.vue
2026-06-10 01:37:27 +08:00

887 lines
20 KiB
Vue
Raw Permalink 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="page-container">
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-content">
<view class="nav-side left" @tap="goBack">
<text class="back-icon"></text>
</view>
<text class="nav-title">发起评分</text>
<view class="nav-side right" @tap="handlePreview">
<text class="preview-text">预览</text>
</view>
</view>
</view>
<view :style="{ height: statusBarHeight + 44 + 'px' }"></view>
<view class="content-wrap">
<view class="topic-editor card-block">
<view class="editor-badge">创建新话题</view>
<textarea
v-model="formData.title"
class="topic-input"
maxlength="40"
placeholder="输入一个有趣的话题..."
placeholder-class="topic-placeholder"
auto-height
/>
<text class="topic-helper">标题越明确越容易吸引大家参与评分和评论</text>
</view>
<!-- <view class="cover-card" :class="{ 'has-cover': formData.coverUrl }" @tap="chooseCover">
<image
v-if="formData.coverUrl"
:src="FILE_BASE_URL + formData.coverUrl"
class="cover-image"
mode="aspectFill"
/>
<view v-else class="cover-placeholder">
<view class="cover-icon"></view>
<text class="cover-text">上传封面图</text>
</view>
</view> -->
<view class="category-group card-block">
<view class="section-meta">
<text class="category-label">话题分类</text>
<text class="section-helper">选择一个更贴近内容的圈层</text>
</view>
<view class="category-list">
<view
v-for="category in categories"
:key="category.id"
class="category-chip"
:class="{ active: formData.categoryId === category.id }"
@tap="formData.categoryId = category.id"
>
{{ category.title }}
</view>
</view>
</view>
<view class="section-header">
<view class="section-title-wrap">
<text class="section-title">评分对象</text>
<text class="section-subtitle">至少保留 1 最多添加 20 </text>
</view>
<text class="section-count">{{ participants.length }}/20</text>
</view>
<view class="participant-list">
<view
v-for="(item, index) in participants"
:key="item.id"
class="participant-card"
>
<view class="participant-main">
<view class="participant-avatar-wrap" @tap="chooseParticipantAvatar(index)">
<image
v-if="item.avatar"
:src="FILE_BASE_URL + item.avatar"
class="participant-avatar"
mode="aspectFill"
/>
<view v-else class="participant-avatar empty">
<view class="camera-small"></view>
</view>
</view>
<view class="participant-info">
<input
v-model="item.name"
class="participant-name"
maxlength="12"
placeholder="输入评价对象"
placeholder-class="participant-placeholder"
/>
<!-- <input
v-model="item.desc"
class="participant-desc"
maxlength="24"
placeholder="一句话简介"
placeholder-class="participant-placeholder"
/> -->
</view>
</view>
<view class="participant-actions">
<!-- <text class="drag-icon"></text> -->
<image class="delete-icon-img" src="/static/images/icon/remove.png" @tap="removeParticipant(item.id)" mode="aspectFit"></image>
</view>
</view>
<view class="add-participant" @tap="addParticipant">
<view class="add-plus-wrap">
<text class="add-plus">+</text>
</view>
<text class="add-text">添加评分对象</text>
</view>
</view>
<!-- <view class="settings-card card-block">
<view class="setting-row">
<view class="setting-info">
<text class="setting-title">允许用户锐评</text>
<text class="setting-desc">开启后用户可以对评分对象发表评论</text>
</view>
<switch
:checked="formData.allowComment"
color="#4d44f1"
@change="formData.allowComment = $event.detail.value"
/>
</view>
<view class="setting-row">
<view class="setting-info">
<text class="setting-title">允许开启PK</text>
<text class="setting-desc">启用后系统会自动生成对象间的对比PK</text>
</view>
<switch
:checked="formData.allowPk"
color="#4d44f1"
@change="formData.allowPk = $event.detail.value"
/>
</view>
<view class="setting-row last">
<view class="setting-info">
<text class="setting-title">匿名评分</text>
<text class="setting-desc">用户可以隐藏个人身份进行打分</text>
</view>
<switch
:checked="formData.anonymous"
color="#4d44f1"
@change="formData.anonymous = $event.detail.value"
/>
</view>
</view> -->
</view>
<view class="footer-bar">
<button class="submit-btn" @tap="handleSubmit">发布夯拉评分</button>
</view>
<!-- 成功特效弹窗 -->
<SuccessPopup ref="successPopupRef" />
<LoginPopup ref="loginPopupRef" @logind="handleLoginSuccess" />
</view>
</template>
<script setup>
import { computed, reactive, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { getStatusBarHeight } from "@/utils/system";
import { releaseTopic, fetchTopicCategories } from "@/api/topic";
import { smartNavigateBack } from "@/utils/page";
import {
uploadImage,
chooseAndUploadImage,
} from "@/utils/common.js";
import SuccessPopup from "@/components/SuccessPopup/SuccessPopup.vue";
import LoginPopup from "@/components/LoginPopup/LoginPopup.vue";
import { FILE_BASE_URL } from "@/utils/constants";
import { useUserStore } from "@/stores/user";
const userStore = useUserStore();
const successPopupRef = ref(null);
const loginPopupRef = ref(null);
const statusBarHeight = ref(getStatusBarHeight() || 0);
const isLoggedIn = computed(() => !!userStore.token || !!userStore.userInfo?.nickName);
const categories = ref([]);
const loadCategories = async () => {
try {
const res = await fetchTopicCategories();
// 假设返回结构为 { data: { list: [{ id: 1, title: '历史' }, ...] } }
// 或者直接是一个数组 [{ id: 1, title: '历史' }, ...]
// 根据实际返回结构调整
const list = res?.data?.list || res?.list || res || [];
categories.value = list;
// 如果存在分类且未选择,则默认选中第一个
if (categories.value.length > 0 && !formData.categoryId) {
formData.categoryId = categories.value[0].id;
}
} catch (error) {
console.error("获取分类失败:", error);
}
};
onLoad(() => {
loadCategories();
});
const formData = reactive({
title: "",
description: "",
coverUrl: "",
categoryId: "",
allowComment: true,
allowPk: true,
anonymous: false,
});
const tagInput = ref("");
const participants = ref([
{
id: 1,
name: "",
desc: "一句话简介",
avatar: "",
},
]);
const goBack = () => {
smartNavigateBack();
};
const openLoginPopup = (message = "请先登录后再操作") => {
loginPopupRef.value?.open();
if (message) {
uni.showToast({
title: message,
icon: "none",
});
}
};
const handlePreview = () => {
if (!isLoggedIn.value) {
openLoginPopup("请先登录后再预览");
return;
}
uni.showToast({
title: "预览功能待接入",
icon: "none",
});
};
const chooseCover = async () => {
try {
const urls = await chooseAndUploadImage({ count: 1 });
if (urls && urls.length > 0) {
formData.coverUrl = urls[0];
}
} catch (e) {
console.error("选择封面失败", e);
}
};
const chooseParticipantAvatar = async (index) => {
if (!isLoggedIn.value) {
openLoginPopup("请先登录后再上传头像");
return;
}
try {
const urls = await chooseAndUploadImage({ count: 1 });
if (urls && urls.length > 0) {
participants.value[index].avatar = urls[0];
}
} catch (e) {
console.error("选择评分对象头像失败", e);
}
};
const handleAddTag = () => {
const value = tagInput.value.trim();
if (!value) return;
if (formData.tags.includes(value)) {
uni.showToast({
title: "标签已存在",
icon: "none",
});
return;
}
formData.tags.push(value);
tagInput.value = "";
};
const removeTag = (index) => {
formData.tags.splice(index, 1);
};
const addParticipant = () => {
if (participants.value.length >= 20) {
uni.showToast({
title: "最多添加20个对象",
icon: "none",
});
return;
}
participants.value.push({
id: Date.now(),
name: "",
desc: "",
avatar: "",
});
};
const removeParticipant = (id) => {
if (participants.value.length <= 1) {
uni.showToast({
title: "至少保留1个对象",
icon: "none",
});
return;
}
participants.value = participants.value.filter((item) => item.id !== id);
};
const handleSubmit = async () => {
if (!isLoggedIn.value) {
openLoginPopup("请先登录后再发起评分");
return;
}
if (!formData.title.trim()) {
uni.showToast({ title: '请输入话题', icon: 'none' });
return;
}
// if (!formData.coverUrl) {
// uni.showToast({ title: '请上传封面图', icon: 'none' });
// return;
// }
if (!formData.categoryId) {
uni.showToast({ title: '请选择话题分类', icon: 'none' });
return;
}
// 检查是否有空的对象名称
const hasEmptyName = participants.value.some(p => !p.name.trim());
if (hasEmptyName) {
uni.showToast({ title: '请完善评分对象名称', icon: 'none' });
return;
}
uni.showLoading({ title: '发布中...' });
try {
const payload = {
...formData,
participants: participants.value,
};
await releaseTopic(payload);
// 记录发布事件
uni.$trackRecord({
eventName: 'release_topic',
eventType: 'release',
elementContent: `话题_${payload.title}`,
customParams: {
title: payload.title,
categoryId: payload.categoryId,
}
});
uni.hideLoading();
if (successPopupRef.value) {
successPopupRef.value.show({
emoji: '🎉',
label: '请耐心等待管理员审核'
}, 'release');
}
// 初始化表单内容
Object.assign(formData, {
title: "",
description: "",
coverUrl: "",
categoryId: "",
});
// 初始化评分对象列表为默认的一个空项,并且赋予唯一的 ID
participants.value = [
{ id: Date.now(), name: "", desc: "", avatar: "" }
];
// 延迟跳转,等待弹窗展示完成
setTimeout(() => {
smartNavigateBack();
}, 2000);
} catch (error) {
uni.hideLoading();
uni.showToast({
title: "发布失败,请重试",
icon: "none",
});
console.error("发布失败:", error);
}
};
const handleLoginSuccess = () => {
if (categories.value.length === 0) {
loadCategories();
}
};
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background:
radial-gradient(circle at top, rgba(111, 90, 255, 0.1), transparent 34%),
linear-gradient(180deg, #f7f8ff 0%, #f6f7fb 28%, #ffffff 100%);
padding-bottom: 180rpx;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Segoe UI, Arial, Roboto, "PingFang SC", "miui", "Hiragino Sans GB", "Microsoft Yahei", sans-serif;
}
.nav-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: rgba(247, 248, 255, 0.82);
backdrop-filter: blur(24rpx);
border-bottom: 1rpx solid rgba(133, 142, 194, 0.08);
}
.nav-content {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 28rpx;
}
.nav-side {
width: 120rpx;
display: flex;
align-items: center;
}
.nav-side.right {
justify-content: flex-end;
}
.back-icon {
width: 36rpx;
height: 36rpx;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23222' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='15 18 9 12 15 6'%3E%3C/polyline%3E%3C/svg%3E");
background-size: cover;
}
.nav-title {
font-size: 34rpx;
font-weight: 800;
color: #16161d;
letter-spacing: 1rpx;
}
.preview-text {
font-size: 28rpx;
color: #5c43f5;
font-weight: 600;
}
.content-wrap {
padding: 28rpx 24rpx 0;
}
.card-block {
background: #ffffff;
border-radius: 34rpx;
box-shadow: 0 18rpx 40rpx rgba(84, 88, 132, 0.08);
margin-bottom: 28rpx;
border: 1rpx solid rgba(222, 227, 246, 0.9);
}
.topic-editor {
padding: 28rpx 28rpx 30rpx;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(245, 246, 255, 0.92));
}
.editor-badge {
display: inline-flex;
align-items: center;
height: 44rpx;
padding: 0 18rpx;
border-radius: 999rpx;
margin-bottom: 22rpx;
background: rgba(92, 67, 245, 0.08);
color: #5c43f5;
font-size: 22rpx;
font-weight: 700;
}
.topic-input {
width: 100%;
min-height: 76rpx;
color: #191a22;
font-size: 48rpx;
font-weight: 800;
line-height: 1.3;
}
.topic-placeholder {
font-size: 48rpx;
font-weight: 800;
color: #c6cadc;
}
.topic-helper {
margin-top: 18rpx;
display: block;
font-size: 24rpx;
line-height: 1.6;
color: #8a8fa7;
}
.cover-card {
height: 340rpx;
border-radius: 36rpx;
background: #e9ecf5;
overflow: hidden;
margin-bottom: 28rpx;
border: 2rpx dashed #d1d5db;
}
.cover-card.has-cover {
border: none; /* 有图片时去掉虚线框 */
}
.cover-image {
width: 100%;
height: 100%;
}
.cover-placeholder {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #77728b;
}
.cover-icon {
width: 80rpx; /* 图标稍微放大 */
height: 80rpx;
margin-bottom: 24rpx;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2377728b' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='3' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Ccircle cx='8.5' cy='8.5' r='1.5'%3E%3C/circle%3E%3Cpolyline points='21 15 16 10 5 21'%3E%3C/polyline%3E%3C/svg%3E"); /* 替换为更像图片的占位图标 */
background-size: cover;
}
.cover-text {
font-size: 30rpx;
}
.category-group {
padding: 28rpx;
}
.section-meta {
margin-bottom: 20rpx;
}
.category-label {
display: block;
font-size: 30rpx;
font-weight: 700;
color: #1c1d24;
}
.section-helper {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #9196ac;
}
.category-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.category-chip {
min-width: 88rpx;
padding: 16rpx 30rpx;
border-radius: 999rpx;
background: #f3f5fc;
color: #5f647d;
font-size: 28rpx;
font-weight: 600;
text-align: center;
border: 1rpx solid transparent;
transition: all 0.22s ease;
}
.category-chip.active {
background: linear-gradient(135deg, #5e43f4, #7d4dff);
color: #fff;
font-weight: 700;
border-color: rgba(116, 85, 255, 0.24);
box-shadow: 0 12rpx 26rpx rgba(101, 74, 245, 0.25);
transform: translateY(-2rpx);
}
.tag-box {
padding: 24rpx 32rpx; /* 增加内边距 */
margin-bottom: 32rpx;
}
.selected-tags {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12rpx;
}
.tag-chip {
padding: 8rpx 20rpx;
background-color: #f0f4ff; /* 浅蓝色背景 */
border-radius: 8rpx;
color: #4e46e5;
font-size: 26rpx;
display: flex;
align-items: center;
}
.tag-chip text {
margin-right: 8rpx;
}
.tag-chip::after {
content: "×";
font-size: 28rpx;
color: #8c89e8;
margin-left: 4rpx;
}
.tag-input {
flex: 1;
min-width: 180rpx;
height: 52rpx;
font-size: 26rpx;
color: #555;
}
.tag-input-placeholder {
color: #b9b7c7;
}
.section-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 20rpx;
padding: 0 8rpx;
}
.section-title-wrap {
display: flex;
flex-direction: column;
}
.section-title {
font-size: 34rpx;
font-weight: 800;
color: #1b1c24;
}
.section-subtitle {
margin-top: 8rpx;
font-size: 24rpx;
color: #9297ad;
}
.section-count {
min-width: 78rpx;
height: 46rpx;
line-height: 46rpx;
text-align: center;
border-radius: 999rpx;
background: rgba(92, 67, 245, 0.1);
color: #5c43f5;
font-size: 26rpx;
font-weight: 700;
}
.participant-list {
margin-bottom: 32rpx;
}
.participant-card {
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
border-radius: 32rpx;
padding: 28rpx 24rpx;
margin-bottom: 24rpx;
border: 1rpx solid #edf0fb;
box-shadow: 0 12rpx 28rpx rgba(72, 82, 130, 0.05);
}
.participant-card:active {
transform: scale(0.995);
}
.participant-main {
display: flex;
align-items: center;
flex: 1;
}
.participant-avatar-wrap {
margin-right: 20rpx;
position: relative;
}
.participant-avatar {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
background: #eceffc;
box-shadow: 0 8rpx 18rpx rgba(95, 99, 255, 0.12);
}
.participant-avatar.empty {
display: flex;
align-items: center;
justify-content: center;
border: 2rpx dashed #c8cde1;
background: linear-gradient(180deg, #fafbff, #f3f5fc);
}
.camera-small {
width: 38rpx;
height: 38rpx;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238a90a6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z'%3E%3C/path%3E%3Ccircle cx='12' cy='13' r='4'%3E%3C/circle%3E%3C/svg%3E");
background-size: cover;
}
.participant-info {
flex: 1;
}
.participant-name,
.participant-desc {
width: 100%;
}
.participant-name {
height: 44rpx;
font-size: 30rpx;
font-weight: 700;
color: #1c1d24;
}
.participant-placeholder {
color: #b7bbcd;
}
.participant-actions {
width: 72rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-left: 16rpx;
}
.delete-icon-img {
width: 40rpx;
height: 40rpx;
padding: 12rpx;
border-radius: 50%;
background: #fff5f5;
}
.add-participant {
height: 120rpx;
border: 2rpx dashed #cfd5e8;
border-radius: 32rpx;
display: flex;
align-items: center;
justify-content: center;
color: #737993;
gap: 16rpx;
background: rgba(255, 255, 255, 0.72);
}
.add-plus-wrap {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(92, 67, 245, 0.1);
}
.add-plus {
font-size: 34rpx;
line-height: 1;
color: #5c43f5;
}
.add-text {
font-size: 28rpx;
font-weight: 600;
}
.settings-card {
padding: 16rpx 32rpx; /* 增加内边距 */
}
.setting-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 0; /* 增加行间距 */
border-bottom: 2rpx solid #f0f1f7;
}
.setting-row.last {
border-bottom: none;
}
.setting-info {
flex: 1;
padding-right: 20rpx;
}
.setting-title {
display: block;
font-size: 30rpx; /* 字体稍微调小 */
font-weight: 700; /* 加粗 */
color: #181818;
margin-bottom: 12rpx; /* 增加与描述的间距 */
}
.setting-desc {
display: block;
font-size: 24rpx; /* 描述字体稍微调大 */
color: #8c92a4; /* 颜色调浅 */
line-height: 1.5;
}
.footer-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 24rpx 24rpx calc(28rpx + env(safe-area-inset-bottom));
background: linear-gradient(180deg, rgba(247, 248, 255, 0) 0%, rgba(247, 248, 255, 0.94) 26%, rgba(247, 248, 255, 1) 100%);
}
.submit-btn {
height: 104rpx;
line-height: 104rpx;
border-radius: 999rpx;
background: linear-gradient(135deg, #5c43f5, #6f55ff 58%, #8b66ff);
color: #fff;
font-size: 34rpx;
font-weight: 800;
letter-spacing: 1rpx;
box-shadow: 0 18rpx 36rpx rgba(98, 69, 246, 0.34);
}
.submit-btn::after {
border: none;
}
</style>