lcw
This commit is contained in:
@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
class="luntan-tech-dialog"
|
||||
:model-value="modelValue"
|
||||
center
|
||||
width="500px"
|
||||
:destroy-on-close="true"
|
||||
:title="title"
|
||||
@close="close"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="avatar-upload-container">
|
||||
<div class="avatar-preview">
|
||||
<img v-if="avatarUrl" :src="avatarUrl" alt="预览头像" class="preview-image">
|
||||
<div v-else class="preview-placeholder">
|
||||
<el-icon class="placeholder-icon">
|
||||
<User />
|
||||
</el-icon>
|
||||
<span>请上传头像</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="upload-section">
|
||||
<el-upload class="avatar-uploader" :auto-upload="false" :show-file-list="false"
|
||||
:on-change="handleAvatarChange" :before-upload="beforeAvatarUpload" accept="image/*">
|
||||
<el-button size="small" type="primary">选择图片</el-button>
|
||||
</el-upload>
|
||||
<div class="upload-tips">
|
||||
<p>• 支持 JPG、PNG 格式</p>
|
||||
<p>• 图片大小不超过 2MB</p>
|
||||
<p>• 建议尺寸为 200x200 像素</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="close">取消</el-button>
|
||||
<el-button type="primary" @click="confirmUpload" :loading="uploading" :disabled="!avatarUrl || uploading">
|
||||
{{ uploading ? '上传中...' : '确认更换' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { User } from '@element-plus/icons-vue';
|
||||
import { upImageUploadId } from '@/api/commit';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "更换头像"
|
||||
},
|
||||
heightNumber: {
|
||||
type: Number,
|
||||
default: 250
|
||||
}
|
||||
});
|
||||
|
||||
const emits = defineEmits(["update:modelValue", "avatarUpdated"]);
|
||||
|
||||
// 头像相关状态
|
||||
const avatarUrl = ref('');
|
||||
const uploading = ref(false);
|
||||
|
||||
// 监听对话框显示状态,重置头像预览
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
if (!newVal) {
|
||||
// 对话框关闭时重置头像预览
|
||||
avatarUrl.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// 当前选择的文件
|
||||
const selectedFile = ref(null);
|
||||
|
||||
// 处理头像选择
|
||||
const handleAvatarChange = (file) => {
|
||||
selectedFile.value = file.raw;
|
||||
// 创建临时预览URL
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
avatarUrl.value = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file.raw);
|
||||
};
|
||||
|
||||
// 上传前检查
|
||||
const beforeAvatarUpload = (file) => {
|
||||
const isJPG = file.type === 'image/jpeg';
|
||||
const isPNG = file.type === 'image/png';
|
||||
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||
|
||||
if (!isJPG && !isPNG) {
|
||||
ElMessage.error('请上传 JPG 或 PNG 格式的图片!');
|
||||
return false;
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error('图片大小不能超过 2MB!');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 确认上传头像
|
||||
const confirmUpload = async () => {
|
||||
if (!avatarUrl.value || !selectedFile.value) {
|
||||
ElMessage.warning('请先选择头像图片');
|
||||
return;
|
||||
}
|
||||
|
||||
uploading.value = true;
|
||||
try {
|
||||
// 创建FormData对象
|
||||
const formData = new FormData();
|
||||
formData.append('file', selectedFile.value);
|
||||
// 调用实际的上传接口
|
||||
const response = await upImageUploadId(formData);
|
||||
console.log(response);
|
||||
emits('avatarUpdated', response);
|
||||
// 关闭对话框
|
||||
close();
|
||||
|
||||
} catch (error) {
|
||||
console.error('上传头像失败:', error);
|
||||
ElMessage.error('上传头像失败,请重试');
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭对话框
|
||||
const close = () => {
|
||||
emits("update:modelValue", false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../styles/luntan-tech.scss';
|
||||
|
||||
.avatar-upload-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 12px 0 8px;
|
||||
}
|
||||
|
||||
.avatar-preview {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 2px solid rgba(0, 227, 255, 0.4);
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(10, 30, 60, 0.6);
|
||||
box-shadow: 0 0 20px rgba(0, 120, 200, 0.2);
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.preview-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: $lt-text-muted;
|
||||
|
||||
.placeholder-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 8px;
|
||||
color: rgba(0, 227, 255, 0.45);
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatar-uploader {
|
||||
margin-bottom: 16px;
|
||||
|
||||
:deep(.el-button--primary) {
|
||||
background: linear-gradient(180deg, #00a3ff 0%, #0066bb 100%);
|
||||
border-color: rgba(0, 227, 255, 0.5);
|
||||
box-shadow: 0 0 12px rgba(0, 163, 255, 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
.upload-tips {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: $lt-text-muted;
|
||||
background: rgba(10, 30, 60, 0.65);
|
||||
border: 1px solid $lt-border-dim;
|
||||
|
||||
p {
|
||||
margin: 4px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../styles/luntan-dialog-tech.scss';
|
||||
</style>
|
||||
@ -0,0 +1,534 @@
|
||||
<template>
|
||||
<div class="comment-list">
|
||||
<div v-for="comment in comments" :key="comment.id" class="comment-item">
|
||||
<!-- 一级评论 -->
|
||||
<div class="comment-main">
|
||||
<el-avatar :size="40" :src="comment.userAvatar" class="comment-avatar">
|
||||
<img src="@/assets/images/mr.png" />
|
||||
</el-avatar>
|
||||
|
||||
<div class="comment-content">
|
||||
<div class="comment-header">
|
||||
<div class="user-info">
|
||||
<span class="user-name">{{ comment.userName }}</span>
|
||||
<div v-if="comment.ssbm" class="author-tag">
|
||||
{{ comment.ssbm }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comment-text">{{ comment.content }}</div>
|
||||
|
||||
<div class="comment-footer">
|
||||
<span class="comment-time">{{ comment.publishTime }}</span>
|
||||
<div class="comment-actions">
|
||||
<!-- <div class="action-btn" :class="{ active: comment.isLiked }" @click="handleLike(comment)">
|
||||
<el-icon>
|
||||
<Promotion />
|
||||
</el-icon>
|
||||
<span>{{ comment.likeCount || 0 }}</span>
|
||||
</div> -->
|
||||
<div class="action-btn" @click="handleReply(comment)">
|
||||
回复
|
||||
</div>
|
||||
<div v-if="comment.replies && comment.replies.length > 0" class="action-btn toggle-btn"
|
||||
@click="toggleReplies(comment)">
|
||||
{{ comment.showReplies ? '收起' : `展开${comment.replies.length}条回复` }}
|
||||
<el-icon :class="{ 'rotate-icon': comment.showReplies }">
|
||||
<ArrowDown />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 回复输入框 -->
|
||||
<transition name="slide-fade">
|
||||
<div v-if="activeReplyId === comment.id" class="reply-input-box">
|
||||
<div class="reply-label">回复 {{ comment.userName }}</div>
|
||||
<el-input v-model="replyContent" type="textarea" :rows="3" placeholder="输入回复内容..."
|
||||
class="reply-textarea" />
|
||||
<div class="reply-actions">
|
||||
<V3Emoji :options-name="optionsName" @click-emoji="onEmojiClick" :recent="true" style="width: 40px;" />
|
||||
<div class="reply-buttons">
|
||||
<el-button size="small" @click="cancelReply">取消</el-button>
|
||||
<el-button size="small" type="primary" @click="submitReply(comment)">
|
||||
回复
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<!-- 二级评论列表 -->
|
||||
<div v-if="comment.replies && comment.replies.length > 0 && comment.showReplies" class="replies-list">
|
||||
<div v-for="reply in comment.replies" :key="reply.id" class="reply-item">
|
||||
<el-avatar :size="32" :src="reply.userAvatar" class="reply-avatar">
|
||||
<img src="@/assets/images/mr.png" />
|
||||
</el-avatar>
|
||||
|
||||
<div class="reply-content">
|
||||
<div class="reply-header">
|
||||
<span class="user-name">{{ reply.userName }}</span>
|
||||
<div v-if="reply.ssbm" class="author-tag">
|
||||
{{ reply.ssbm }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="reply-text">
|
||||
<span v-if="reply.replyToUser" class="reply-to">
|
||||
回复 {{ reply.replyToUser }}:
|
||||
</span>
|
||||
{{ reply.content }}
|
||||
</div>
|
||||
|
||||
<div class="reply-footer">
|
||||
<span class="reply-time">{{ reply.publishTime }}</span>
|
||||
<div class="reply-actions">
|
||||
<!-- <div class="action-btn" :class="{ active: reply.isLiked }" @click="handleLike(reply)">
|
||||
<el-icon>
|
||||
<Promotion />
|
||||
</el-icon>
|
||||
<span v-if="reply.likeCount">{{ reply.likeCount }}</span>
|
||||
</div> -->
|
||||
<div class="action-btn" @click="handleReplyToReply(reply, comment)">
|
||||
回复
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 二级回复输入框 -->
|
||||
<transition name="slide-fade">
|
||||
<div v-if="activeReplyId === reply.id" class="reply-input-box">
|
||||
<div class="reply-label">回复 {{ reply.userName }}</div>
|
||||
<el-input v-model="replyContent" type="textarea" :rows="3" placeholder="输入回复内容..."
|
||||
class="reply-textarea" />
|
||||
<div class="reply-actions">
|
||||
<V3Emoji :options-name="optionsName" @click-emoji="onEmojiClick" :recent="true"
|
||||
style="width: 40px;" />
|
||||
<div class="reply-buttons">
|
||||
<el-button size="small" @click="cancelReply">取消</el-button>
|
||||
<el-button size="small" type="primary" @click="submitReply(comment, reply)">
|
||||
回复
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { Promotion, ArrowDown } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import V3Emoji from 'vue3-emoji'
|
||||
import { tbGsxtXxltHfid, tbGsxtXxltHfSave, tbGsxtXxltHfSelectList } from '@/api/tbGsxtXxltHf.js'
|
||||
import { getItem } from '@/utils/storage.js'
|
||||
import { setAddress } from '@/utils/tools'
|
||||
|
||||
const optionsName = {
|
||||
'Smileys & Emotion': '笑脸&表情',
|
||||
'Food & Drink': '食物&饮料',
|
||||
'Animals & Nature': '动物&自然',
|
||||
'Travel & Places': '旅行&地点',
|
||||
'People & Body': '人物&身体',
|
||||
Objects: '物品',
|
||||
Symbols: '符号',
|
||||
Flags: '旗帜',
|
||||
Activities: '活动'
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
comments: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}, replyTo: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['reply', 'like'])
|
||||
|
||||
const activeReplyId = ref(null)
|
||||
const replyContent = ref('')
|
||||
|
||||
const getTagType = (tag) => {
|
||||
const tagMap = {
|
||||
'户外活动部': 'success',
|
||||
'校长': 'warning',
|
||||
'校本部': 'info'
|
||||
}
|
||||
return tagMap[tag] || 'info'
|
||||
}
|
||||
|
||||
const handleLike = (comment) => {
|
||||
emit('like', comment)
|
||||
}
|
||||
|
||||
const handleReply = (comment) => {
|
||||
emit('reply')
|
||||
activeReplyId.value = comment.id
|
||||
replyContent.value = ''
|
||||
}
|
||||
|
||||
const handleReplyToReply = (reply, parentComment) => {
|
||||
emit('reply')
|
||||
activeReplyId.value = reply.id
|
||||
replyContent.value = `@${reply.userName}:`
|
||||
}
|
||||
|
||||
const onEmojiClick = (emoji) => {
|
||||
replyContent.value += emoji
|
||||
}
|
||||
|
||||
const cancelReply = () => {
|
||||
activeReplyId.value = null
|
||||
replyContent.value = ''
|
||||
}
|
||||
|
||||
// 切换回复列表的展开/收起
|
||||
const toggleReplies = (comment) => {
|
||||
if (!comment.showReplies) {
|
||||
comment.showReplies = true
|
||||
} else {
|
||||
comment.showReplies = false
|
||||
}
|
||||
}
|
||||
|
||||
const formatReplyItem = (reply) => {
|
||||
return {
|
||||
...reply,
|
||||
id: reply.id,
|
||||
userName: reply.hfrxm || '匿名用户',
|
||||
userAvatar: reply.userAvatar || (reply.hfrtx ? setAddress(reply.hfrtx) : ''),
|
||||
userTag: reply.userTag || '',
|
||||
content: reply.content || reply.hfnr || '',
|
||||
publishTime: reply.publishTime || reply.hfsj || '',
|
||||
likeCount: reply.likeCount || 0,
|
||||
isLiked: reply.isLiked || false,
|
||||
ssbm: reply.ssbm || '',
|
||||
replyToUser: reply.replyToUser || reply.sjhfrxm || ''
|
||||
}
|
||||
}
|
||||
|
||||
const submitReply = async (parentComment, replyToComment = null) => {
|
||||
console.log(parentComment);
|
||||
|
||||
if (!replyContent.value.trim()) {
|
||||
ElMessage.warning('请输入回复内容')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const ltmasg = getItem("ltmasg")
|
||||
let pureContent = replyContent.value
|
||||
if (pureContent.startsWith('@') && pureContent.includes(':')) {
|
||||
const colonIndex = pureContent.indexOf(':')
|
||||
if (colonIndex !== -1 && colonIndex < pureContent.length - 1) {
|
||||
pureContent = pureContent.substring(colonIndex + 1).trim()
|
||||
}
|
||||
}
|
||||
if (!pureContent) {
|
||||
ElMessage.warning('请输入回复内容')
|
||||
return
|
||||
}
|
||||
const targetReply = replyToComment || null
|
||||
const newReply = {
|
||||
hfnr: pureContent,
|
||||
hfrsfzh: ltmasg.sfzh,
|
||||
hfrtx: ltmasg.tx,
|
||||
hfrxm: ltmasg.xm,
|
||||
ltid: props.replyTo.id,
|
||||
sfyjhf: 0,
|
||||
sjhfid: parentComment.id,
|
||||
sjhfrxm: targetReply ? targetReply.userName : ''
|
||||
}
|
||||
try {
|
||||
const res = await tbGsxtXxltHfSave(newReply)
|
||||
if (res) {
|
||||
const dataxhf = await tbGsxtXxltHfSelectList({ sjhfid: parentComment.id })
|
||||
const replyList = Array.isArray(dataxhf) ? dataxhf : (dataxhf?.records || [])
|
||||
parentComment.replies = replyList.map(formatReplyItem)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
// if (!parentComment.replies) {
|
||||
// parentComment.replies = []
|
||||
// }
|
||||
// parentComment.replies.push(newReply)
|
||||
|
||||
// 自动展开回复列表
|
||||
parentComment.showReplies = true
|
||||
ElMessage.success('回复成功')
|
||||
cancelReply()
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
ElMessage.error('回复失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '../styles/luntan-tech.scss';
|
||||
|
||||
.comment-list {
|
||||
.comment-item {
|
||||
margin-bottom: 18px;
|
||||
padding-bottom: 18px;
|
||||
border-bottom: 1px solid $lt-border-dim;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-main {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.comment-avatar {
|
||||
flex-shrink: 0;
|
||||
border: 1px solid rgba(0, 227, 255, 0.3);
|
||||
box-shadow: 0 0 8px rgba(0, 120, 180, 0.2);
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: $lt-text;
|
||||
}
|
||||
|
||||
.comment-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.65;
|
||||
color: $lt-text-dim;
|
||||
margin-bottom: 8px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.comment-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.comment-time {
|
||||
font-size: 12px;
|
||||
color: $lt-text-muted;
|
||||
}
|
||||
|
||||
.comment-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: $lt-text-muted;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: $lt-cyan;
|
||||
text-shadow: 0 0 8px rgba(0, 227, 255, 0.35);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: $lt-cyan;
|
||||
}
|
||||
|
||||
&.toggle-btn {
|
||||
.el-icon {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.rotate-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reply-input-box {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
@include lt-panel-soft-bg;
|
||||
}
|
||||
|
||||
.reply-label {
|
||||
font-size: 12px;
|
||||
color: $lt-text-muted;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.reply-textarea {
|
||||
margin-bottom: 8px;
|
||||
|
||||
:deep(.el-textarea__inner) {
|
||||
background: rgba(10, 28, 58, 0.88) !important;
|
||||
color: $lt-text-dim;
|
||||
border: 1px solid rgba(0, 227, 255, 0.28);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 12px rgba(0, 80, 140, 0.15) inset;
|
||||
|
||||
&::placeholder {
|
||||
color: $lt-text-muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reply-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.reply-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
:deep(.el-button) {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
:deep(.el-button--default) {
|
||||
background: rgba(0, 40, 70, 0.5);
|
||||
border-color: rgba(0, 163, 255, 0.35);
|
||||
color: $lt-text-dim;
|
||||
}
|
||||
|
||||
:deep(.el-button--primary) {
|
||||
background: linear-gradient(180deg, #00a3ff 0%, #0066bb 100%);
|
||||
border-color: rgba(0, 227, 255, 0.45);
|
||||
box-shadow: 0 0 12px rgba(0, 163, 255, 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
.replies-list {
|
||||
margin-top: 14px;
|
||||
padding-left: 12px;
|
||||
border-left: 2px solid rgba(0, 227, 255, 0.25);
|
||||
box-shadow: -2px 0 12px rgba(0, 100, 160, 0.08);
|
||||
}
|
||||
|
||||
.reply-item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
align-items: flex-start;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-avatar {
|
||||
flex-shrink: 0;
|
||||
border: 1px solid rgba(0, 227, 255, 0.22);
|
||||
}
|
||||
|
||||
.reply-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.reply-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.reply-text {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: $lt-text-dim;
|
||||
margin-bottom: 6px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.reply-to {
|
||||
color: $lt-cyan;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.reply-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.reply-time {
|
||||
font-size: 12px;
|
||||
color: $lt-text-muted;
|
||||
}
|
||||
|
||||
.reply-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.slide-fade-enter-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.slide-fade-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.slide-fade-enter-from,
|
||||
.slide-fade-leave-to {
|
||||
transform: translateY(-10px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.author-tag {
|
||||
padding: 2px 8px;
|
||||
border-radius: 2px;
|
||||
background: rgba(0, 163, 255, 0.22);
|
||||
color: #b8ecff;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
border: 1px solid rgba(0, 227, 255, 0.35);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../styles/luntan-v3emoji-tech.scss';
|
||||
</style>
|
||||
565
src/views/backOfficeSystem/luntan copy/components/PostDetail.vue
Normal file
565
src/views/backOfficeSystem/luntan copy/components/PostDetail.vue
Normal file
@ -0,0 +1,565 @@
|
||||
<template>
|
||||
<div class="post-detail luntan-tech-detail">
|
||||
<!-- 头部 -->
|
||||
<div class="detail-header">
|
||||
<el-button class="detail-back-btn" @click="handleBack">
|
||||
<el-icon>
|
||||
<ArrowLeft />
|
||||
</el-icon>
|
||||
返回
|
||||
</el-button>
|
||||
<div class="header-title">帖子详情</div>
|
||||
</div>
|
||||
|
||||
<!-- 帖子内容 -->
|
||||
<div class="post-main">
|
||||
<div class="premium-badge" v-if="postData.isPremium">置顶</div>
|
||||
|
||||
<div class="post-author">
|
||||
<el-avatar :size="50" :src="postData.userAvatar" class="author-avatar">
|
||||
<img src="@/assets/images/mr.png" />
|
||||
</el-avatar>
|
||||
<div class="author-info">
|
||||
<div class="author-name-row">
|
||||
<span class="author-name">{{ postData.userName }}</span>
|
||||
<span v-if="postData.userTag" class="level-badge">{{
|
||||
postData.userTag
|
||||
}}</span>
|
||||
<div v-if="postData.ssbm" class="author-tag">
|
||||
{{ postData.ssbm }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="publish-time">{{ postData.publishTime }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="post-content-text">{{ postData.content }}</div>
|
||||
|
||||
<!-- 图片展示 -->
|
||||
<div
|
||||
class="post-images"
|
||||
v-if="postData.images && postData.images.length > 0"
|
||||
>
|
||||
<div
|
||||
v-for="(img, index) in postData.images"
|
||||
:key="index"
|
||||
class="image-item"
|
||||
>
|
||||
<el-image
|
||||
:preview-teleported="true"
|
||||
:src="img"
|
||||
fit="cover"
|
||||
:preview-src-list="postData.images"
|
||||
:initial-index="index"
|
||||
>
|
||||
<template #error>
|
||||
<div class="image-error">
|
||||
<el-icon>
|
||||
<Picture />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="post-stats">
|
||||
<div class="stat-item">
|
||||
<el-icon>
|
||||
<ChatDotRound />
|
||||
</el-icon>
|
||||
<span>{{ postData.commentCount || 0 }}</span>
|
||||
</div>
|
||||
<!-- <div class="stat-item" :class="{ active: postData.isLiked }" @click="handleLike">
|
||||
<el-icon>
|
||||
<Promotion />
|
||||
</el-icon>
|
||||
<span>{{ postData.likeCount || 0 }}</span>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 评论区 -->
|
||||
<div class="comment-section">
|
||||
<!-- Tab切换 -->
|
||||
<div class="comment-tabs">
|
||||
<div
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'all' }"
|
||||
@click="activeTab = 'all'"
|
||||
>
|
||||
全部回复({{ comments.length }})
|
||||
</div>
|
||||
<!-- <div class="tab-item" :class="{ active: activeTab === 'author' }" @click="activeTab = 'author'">
|
||||
只看楼主
|
||||
</div> -->
|
||||
|
||||
<!-- <div class="sort-buttons">
|
||||
<el-button text size="small">热门</el-button>
|
||||
<el-button text size="small">正序</el-button>
|
||||
<el-button text size="small">倒序</el-button>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- 顶部输入框 - 点击打开弹窗 -->
|
||||
<div class="top-input" @click="replyToData">
|
||||
<el-input placeholder="发点干货 文明第一步" readonly />
|
||||
</div>
|
||||
|
||||
<!-- 评论列表 -->
|
||||
|
||||
<CommentList
|
||||
:comments="filteredComments"
|
||||
@reply="handleReply"
|
||||
:replyTo="replyTo"
|
||||
@like="handleCommentLike"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 回复弹窗 -->
|
||||
<ReplyDialog
|
||||
v-model="showReplyDialog"
|
||||
:reply-to="replyTo"
|
||||
@success="handleReplySuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import {
|
||||
ArrowLeft,
|
||||
ChatDotRound,
|
||||
Promotion,
|
||||
Picture
|
||||
} from "@element-plus/icons-vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import CommentList from "./CommentList.vue";
|
||||
import ReplyDialog from "./ReplyDialog.vue";
|
||||
import { tbGsxtXxltHfid } from "@/api/tbGsxtXxltHf";
|
||||
import { setAddress } from "@/utils/tools";
|
||||
|
||||
const props = defineProps({
|
||||
postId: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["back"]);
|
||||
|
||||
const activeTab = ref("all");
|
||||
const showReplyDialog = ref(false);
|
||||
const replyTo = ref(null);
|
||||
const loading = ref(false);
|
||||
|
||||
// 帖子数据
|
||||
const postData = ref({
|
||||
id: null,
|
||||
userName: "",
|
||||
userAvatar: "",
|
||||
userTag: "",
|
||||
publishTime: "",
|
||||
content: "",
|
||||
images: [],
|
||||
commentCount: 0,
|
||||
likeCount: 0,
|
||||
isPremium: false,
|
||||
isLiked: false
|
||||
});
|
||||
|
||||
// 评论数据
|
||||
const comments = ref([]);
|
||||
|
||||
onMounted(() => {
|
||||
loadPostDetail();
|
||||
});
|
||||
|
||||
const loadPostDetail = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await tbGsxtXxltHfid(props.postId);
|
||||
|
||||
// 设置帖子数据
|
||||
postData.value = {
|
||||
id: res.id,
|
||||
userName: res.fbrxm || "匿名用户",
|
||||
userAvatar: res.fbrtx ? setAddress(res.fbrtx) : "",
|
||||
userTag: res.userTag || "",
|
||||
publishTime: res.time || "",
|
||||
content: res.content || "",
|
||||
images: res.tp ? res.tp.split(",").map((img) => setAddress(img)) : [],
|
||||
commentCount: res.commentCount || 0,
|
||||
likeCount: res.likeCount || 0,
|
||||
isPremium: res.sfzd === 1,
|
||||
isLiked: false,
|
||||
ssbm: res.ssbm
|
||||
};
|
||||
|
||||
// 设置评论数据
|
||||
if (res.replyList && res.replyList.length > 0) {
|
||||
comments.value = res.replyList.map((item) => ({
|
||||
id: item.id,
|
||||
userName: item.hfrxm || "匿名用户",
|
||||
userAvatar: item.hfrtx ? setAddress(item.hfrtx) : "",
|
||||
userTag: item.userTag || "",
|
||||
content: item.hfnr || "",
|
||||
publishTime: item.hfsj || "",
|
||||
likeCount: item.likeCount || 0,
|
||||
isLiked: false,
|
||||
showReplies: false,
|
||||
ssbm: item.ssbm,
|
||||
replies: item.xjfhList
|
||||
? item.xjfhList.map((reply) => ({
|
||||
id: reply.id,
|
||||
userName: reply.hfrxm || "匿名用户",
|
||||
userAvatar: reply.hfrtx ? setAddress(reply.hfrtx) : "",
|
||||
userTag: reply.userTag || "",
|
||||
content: reply.hfnr || "",
|
||||
publishTime: reply.hfsj || "",
|
||||
likeCount: reply.likeCount || 0,
|
||||
isLiked: false,
|
||||
replyToUser: reply.sjhfrxm || "",
|
||||
ssbm: reply.ssbm
|
||||
}))
|
||||
: []
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载详情失败", error);
|
||||
ElMessage.error("加载详情失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const filteredComments = computed(() => {
|
||||
if (activeTab.value === "author") {
|
||||
return comments.value.filter((c) => c.userName === postData.value.userName);
|
||||
}
|
||||
return comments.value;
|
||||
});
|
||||
|
||||
const getTagType = (tag) => {
|
||||
const tagMap = {
|
||||
户外活动部: "success",
|
||||
校长: "warning",
|
||||
校本部: "info"
|
||||
};
|
||||
return tagMap[tag] || "info";
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
emit("back");
|
||||
};
|
||||
|
||||
const handleLike = () => {
|
||||
postData.value.isLiked = !postData.value.isLiked;
|
||||
postData.value.likeCount += postData.value.isLiked ? 1 : -1;
|
||||
ElMessage.success(postData.value.isLiked ? "点赞成功" : "取消点赞");
|
||||
};
|
||||
|
||||
const handleReply = () => {
|
||||
replyTo.value = {
|
||||
...postData.value
|
||||
};
|
||||
// showReplyDialog.value = true
|
||||
};
|
||||
const replyToData = () => {
|
||||
replyTo.value = {
|
||||
...postData.value
|
||||
};
|
||||
showReplyDialog.value = true;
|
||||
};
|
||||
const handleCommentLike = (comment) => {
|
||||
comment.isLiked = !comment.isLiked;
|
||||
comment.likeCount += comment.isLiked ? 1 : -1;
|
||||
};
|
||||
|
||||
const handleReplySuccess = () => {
|
||||
ElMessage.success("回复成功");
|
||||
loadPostDetail();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../styles/luntan-tech.scss";
|
||||
|
||||
.luntan-tech-detail {
|
||||
background: transparent;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
padding: 14px 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid $lt-border-dim;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 163, 255, 0.08) 0%,
|
||||
transparent 55%
|
||||
);
|
||||
|
||||
:deep(.detail-back-btn.el-button) {
|
||||
height: 36px;
|
||||
padding: 0 14px;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
background: rgba(10, 30, 60, 0.75) !important;
|
||||
border: 1px solid $lt-border-dim !important;
|
||||
color: $lt-text-dim !important;
|
||||
box-shadow: inset 0 1px 0 rgba(0, 220, 255, 0.08),
|
||||
0 0 12px rgba(0, 80, 140, 0.2);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: $lt-cyan !important;
|
||||
border-color: rgba(0, 227, 255, 0.45) !important;
|
||||
background: rgba(12, 40, 75, 0.92) !important;
|
||||
box-shadow: 0 0 16px rgba(0, 163, 255, 0.28),
|
||||
inset 0 1px 0 rgba(0, 220, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.detail-back-btn .el-icon) {
|
||||
margin-right: 4px;
|
||||
font-size: 16px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: $lt-text;
|
||||
margin-right: 60px;
|
||||
letter-spacing: 0.08em;
|
||||
text-shadow: 0 0 16px rgba(0, 227, 255, 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
.post-main {
|
||||
padding: 20px 18px 18px;
|
||||
margin: 12px 12px 0;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
@include lt-panel-soft-bg;
|
||||
}
|
||||
|
||||
.comment-section {
|
||||
padding: 8px 12px 32px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.premium-badge {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 227, 255, 0.35) 0%,
|
||||
rgba(0, 100, 180, 0.5) 100%
|
||||
);
|
||||
color: #fff;
|
||||
padding: 4px 12px;
|
||||
border-radius: 2px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
border: 1px solid rgba(0, 227, 255, 0.55);
|
||||
box-shadow: 0 0 14px rgba(0, 227, 255, 0.35);
|
||||
}
|
||||
|
||||
.post-author {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.author-avatar {
|
||||
border: 2px solid rgba(0, 227, 255, 0.35);
|
||||
box-shadow: 0 0 12px rgba(0, 163, 255, 0.25);
|
||||
}
|
||||
|
||||
.author-info {
|
||||
flex: 1;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.author-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.author-name {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: $lt-text;
|
||||
}
|
||||
|
||||
.level-badge {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 2px;
|
||||
background: rgba(0, 227, 255, 0.15);
|
||||
color: $lt-cyan;
|
||||
font-weight: 600;
|
||||
border: 1px solid rgba(0, 227, 255, 0.35);
|
||||
}
|
||||
|
||||
.author-tag {
|
||||
padding: 2px 8px;
|
||||
border-radius: 2px;
|
||||
background: rgba(0, 163, 255, 0.25);
|
||||
color: #b8ecff;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
border: 1px solid rgba(0, 227, 255, 0.35);
|
||||
}
|
||||
|
||||
.publish-time {
|
||||
font-size: 12px;
|
||||
color: $lt-text-muted;
|
||||
}
|
||||
|
||||
.post-content-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.85;
|
||||
color: $lt-text-dim;
|
||||
margin-bottom: 16px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.post-images {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.image-item {
|
||||
width: 200px;
|
||||
height: 140px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
border: 1px solid $lt-border-dim;
|
||||
box-shadow: 0 0 12px rgba(0, 100, 160, 0.2);
|
||||
|
||||
:deep(.el-image) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.image-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(10, 30, 60, 0.6);
|
||||
color: $lt-text-muted;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.post-stats {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid $lt-border-dim;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: $lt-text-muted;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: $lt-cyan;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
border-bottom: 1px solid $lt-border-dim;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
padding: 12px 14px;
|
||||
font-size: 14px;
|
||||
color: $lt-text-muted;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&.active {
|
||||
color: $lt-cyan;
|
||||
font-weight: 600;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, $lt-cyan, transparent);
|
||||
box-shadow: 0 0 10px rgba(0, 227, 255, 0.6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sort-buttons {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.top-input {
|
||||
margin-bottom: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
cursor: pointer;
|
||||
background: rgba(10, 30, 60, 0.65) !important;
|
||||
border-radius: 4px !important;
|
||||
box-shadow: 0 0 0 1px rgba(0, 227, 255, 0.28) inset,
|
||||
0 0 16px rgba(0, 100, 180, 0.15) !important;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px rgba(0, 227, 255, 0.45) inset,
|
||||
0 0 20px rgba(0, 163, 255, 0.25) !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
color: $lt-text-dim;
|
||||
|
||||
&::placeholder {
|
||||
color: $lt-text-muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
332
src/views/backOfficeSystem/luntan copy/components/PostItem.vue
Normal file
332
src/views/backOfficeSystem/luntan copy/components/PostItem.vue
Normal file
@ -0,0 +1,332 @@
|
||||
<template>
|
||||
<div class="post-item" @click="handleClick">
|
||||
<div class="premium-badge" v-if="post.isPremium">置顶</div>
|
||||
<div class="post-main-content">
|
||||
<el-avatar :size="44" :src="post.userAvatar" class="post-avatar">
|
||||
<img src="@/assets/images/mr.png" />
|
||||
</el-avatar>
|
||||
|
||||
<div class="post-right">
|
||||
<div class="post-header">
|
||||
<div class="user-name-row">
|
||||
<span class="user-name">{{ post.userName }}</span>
|
||||
<span v-if="post.userTag" class="level-badge">{{
|
||||
post.userTag
|
||||
}}</span>
|
||||
<div v-if="post.ssbm" class="author-tag">
|
||||
{{ post.ssbm }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="post-time">{{ post.publishTime }}</div>
|
||||
</div>
|
||||
|
||||
<div class="post-content" v-if="postTitle || bodySource">
|
||||
<div v-if="postTitle" class="post-title">{{ postTitle }}</div>
|
||||
<div v-if="bodySource" class="post-text-wrap">
|
||||
<div class="post-text">{{ displayBody }}</div>
|
||||
<span
|
||||
v-if="showFullLink"
|
||||
class="full-text-link"
|
||||
@click.stop="handleClick"
|
||||
>全文</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 图片展示 -->
|
||||
<div
|
||||
class="post-images"
|
||||
v-if="post.images && post.images.length > 0"
|
||||
:class="{ 'is-three': post.images.length >= 3 }"
|
||||
>
|
||||
<div
|
||||
v-for="(img, index) in post.images"
|
||||
:key="index"
|
||||
class="image-item"
|
||||
@click.stop
|
||||
>
|
||||
<el-image
|
||||
:preview-teleported="true"
|
||||
:src="img"
|
||||
fit="cover"
|
||||
:preview-src-list="post.images"
|
||||
:initial-index="index"
|
||||
>
|
||||
<template #error>
|
||||
<div class="image-error">
|
||||
<el-icon>
|
||||
<Picture />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="post-footer">
|
||||
<div class="action-item">
|
||||
<el-icon>
|
||||
<ChatDotRound />
|
||||
</el-icon>
|
||||
<span>{{ post.commentCount || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { ChatDotRound, Picture } from "@element-plus/icons-vue";
|
||||
|
||||
const props = defineProps({
|
||||
post: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["like", "click"]);
|
||||
|
||||
const EXCERPT_LEN = 160;
|
||||
|
||||
const rawContent = computed(() => (props.post.content || "").trim());
|
||||
|
||||
const postTitle = computed(() => {
|
||||
const t = rawContent.value;
|
||||
if (!t) return "";
|
||||
const idx = t.indexOf("\n");
|
||||
if (idx === -1) return "";
|
||||
const first = t.slice(0, idx).trim();
|
||||
return first.length > 0 ? first : "";
|
||||
});
|
||||
|
||||
const bodySource = computed(() => {
|
||||
const t = rawContent.value;
|
||||
if (!t) return "";
|
||||
const idx = t.indexOf("\n");
|
||||
if (idx === -1) return t;
|
||||
const rest = t.slice(idx + 1).trim();
|
||||
return rest;
|
||||
});
|
||||
|
||||
const showFullLink = computed(() => bodySource.value.length > EXCERPT_LEN);
|
||||
|
||||
const displayBody = computed(() => {
|
||||
const b = bodySource.value;
|
||||
if (!b) return "";
|
||||
if (b.length <= EXCERPT_LEN) return b;
|
||||
return `${b.slice(0, EXCERPT_LEN)}…`;
|
||||
});
|
||||
|
||||
const handleClick = () => {
|
||||
emit("click", props.post);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../styles/luntan-tech.scss";
|
||||
|
||||
.post-item {
|
||||
position: relative;
|
||||
padding: 18px 18px 14px;
|
||||
margin-bottom: 14px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
@include lt-panel-frame;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(0, 227, 255, 0.55);
|
||||
box-shadow: $lt-glow-strong, inset 0 1px 0 rgba(0, 220, 255, 0.18);
|
||||
}
|
||||
}
|
||||
|
||||
.premium-badge {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 14px;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 227, 255, 0.35) 0%,
|
||||
rgba(0, 100, 180, 0.45) 100%
|
||||
);
|
||||
color: #fff;
|
||||
padding: 3px 10px;
|
||||
border-radius: 2px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
border: 1px solid rgba(0, 227, 255, 0.55);
|
||||
box-shadow: 0 0 12px rgba(0, 227, 255, 0.35);
|
||||
}
|
||||
|
||||
.post-main-content {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.post-avatar {
|
||||
flex-shrink: 0;
|
||||
border: 2px solid rgba(0, 227, 255, 0.35);
|
||||
box-shadow: 0 0 10px rgba(0, 163, 255, 0.2);
|
||||
}
|
||||
|
||||
.post-right {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.post-header {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.user-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: $lt-text;
|
||||
}
|
||||
|
||||
.level-badge {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 2px;
|
||||
background: rgba(0, 227, 255, 0.12);
|
||||
color: $lt-cyan;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
border: 1px solid rgba(0, 227, 255, 0.35);
|
||||
}
|
||||
|
||||
.post-time {
|
||||
font-size: 12px;
|
||||
color: $lt-text-muted;
|
||||
}
|
||||
|
||||
.post-content {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: $lt-text;
|
||||
line-height: 1.45;
|
||||
margin-bottom: 8px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.post-text-wrap {
|
||||
font-size: 14px;
|
||||
line-height: 1.65;
|
||||
color: $lt-text-dim;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.post-text {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.full-text-link {
|
||||
margin-left: 4px;
|
||||
color: $lt-cyan;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-shadow: 0 0 8px rgba(0, 227, 255, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.post-images {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 12px;
|
||||
|
||||
&.is-three .image-item {
|
||||
width: calc((100% - 16px) / 3);
|
||||
min-width: 0;
|
||||
aspect-ratio: 4 / 3;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.image-item {
|
||||
width: 160px;
|
||||
height: 112px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid $lt-border-dim;
|
||||
box-shadow: 0 0 10px rgba(0, 80, 140, 0.25);
|
||||
|
||||
:deep(.el-image) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.post-images.is-three .image-item {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.image-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(10, 30, 60, 0.65);
|
||||
color: $lt-text-muted;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.post-footer {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid $lt-border-dim;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: $lt-text-muted;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: $lt-cyan;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
.author-tag {
|
||||
padding: 2px 8px;
|
||||
border-radius: 2px;
|
||||
background: rgba(0, 200, 180, 0.18);
|
||||
color: #9ff;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
font-weight: 600;
|
||||
border: 1px solid rgba(0, 227, 255, 0.35);
|
||||
}
|
||||
</style>
|
||||
463
src/views/backOfficeSystem/luntan copy/components/PostList.vue
Normal file
463
src/views/backOfficeSystem/luntan copy/components/PostList.vue
Normal file
@ -0,0 +1,463 @@
|
||||
<template>
|
||||
<div class="post-list">
|
||||
<!-- 贴吧风格顶栏:左信息 + 右操作(页面背景仍为外层网格,不变) -->
|
||||
<div class="bar-head">
|
||||
<div class="bar-head-left">
|
||||
<div class="bar-logo">
|
||||
<el-icon><ChatDotRound /></el-icon>
|
||||
</div>
|
||||
<div class="bar-head-text">
|
||||
<div class="bar-title">信息论坛</div>
|
||||
<div class="bar-meta">帖子 {{ totalDisplay }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bar-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="bar-pill bar-pill-primary"
|
||||
@click="showPublishDialog = true"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
发帖
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 帖子列表 -->
|
||||
<div
|
||||
class="posts-container"
|
||||
v-loading="loading"
|
||||
v-infinite-scroll="loadMore"
|
||||
:infinite-scroll-disabled="scrollDisabled"
|
||||
:infinite-scroll-distance="100"
|
||||
>
|
||||
<!-- 置顶:仅标题缩略列表 -->
|
||||
<div v-if="pinnedList.length" class="pinned-block">
|
||||
<div class="pinned-list">
|
||||
<div
|
||||
v-for="post in pinnedList"
|
||||
:key="'zd-' + post.id"
|
||||
class="pinned-row"
|
||||
@click="handlePostClick(post)"
|
||||
>
|
||||
<span class="pinned-badge">置顶</span>
|
||||
<span class="pinned-title">{{ displayTitle(post) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分界线 -->
|
||||
<div v-if="pinnedList.length && normalList.length" class="list-divider">
|
||||
<span class="divider-line" />
|
||||
<span class="divider-text">全部帖子</span>
|
||||
<span class="divider-line" />
|
||||
</div>
|
||||
|
||||
<!-- 普通帖子 -->
|
||||
<PostItem
|
||||
v-for="post in normalList"
|
||||
:key="post.id"
|
||||
:post="post"
|
||||
@like="handleLike"
|
||||
@click="handlePostClick(post)"
|
||||
/>
|
||||
|
||||
<!-- 加载更多提示 -->
|
||||
<div v-if="loadingMore" class="loading-more">
|
||||
<el-icon class="is-loading">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
|
||||
<!-- 没有更多数据提示 -->
|
||||
<div v-if="noMore && normalList.length > 0" class="no-more">
|
||||
没有更多数据了
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<el-empty
|
||||
v-if="!loading && postList.length === 0"
|
||||
class="post-empty"
|
||||
description="暂无帖子"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 发布对话框 -->
|
||||
<PublishDialog
|
||||
v-model="showPublishDialog"
|
||||
@success="handlePublishSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import { ChatDotRound, Loading, Plus } from "@element-plus/icons-vue";
|
||||
import PostItem from "./PostItem.vue";
|
||||
import PublishDialog from "./PublishDialog.vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { tbGsxtXxltSelectPage } from "@/api/tbGsxtXxltHf";
|
||||
import { setAddress } from "@/utils/tools";
|
||||
|
||||
const loading = ref(false);
|
||||
const loadingMore = ref(false);
|
||||
const postList = ref([]);
|
||||
const showPublishDialog = ref(false);
|
||||
|
||||
const listQuery = ref({
|
||||
pageCurrent: 1,
|
||||
pageSize: 10
|
||||
});
|
||||
|
||||
const total = ref(0);
|
||||
|
||||
const totalDisplay = computed(() => {
|
||||
const n = total.value;
|
||||
if (n >= 10000) return `${(n / 10000).toFixed(1)}W`;
|
||||
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
||||
return String(n);
|
||||
});
|
||||
|
||||
const pinnedList = computed(() => postList.value.filter((p) => p.isPremium));
|
||||
|
||||
const normalList = computed(() => postList.value.filter((p) => !p.isPremium));
|
||||
|
||||
/** 置顶行展示标题:优先接口 title,否则从正文首行截取 */
|
||||
function displayTitle(post) {
|
||||
const tit = (post.title || "").trim();
|
||||
if (tit) {
|
||||
return tit.length <= 56 ? tit : `${tit.slice(0, 56)}…`;
|
||||
}
|
||||
const t = (post.content || "").trim();
|
||||
if (!t) return "无标题";
|
||||
const idx = t.indexOf("\n");
|
||||
const firstLine = idx === -1 ? t : t.slice(0, idx).trim();
|
||||
const line = firstLine || t.slice(0, 80);
|
||||
if (line.length <= 56) return line;
|
||||
return `${line.slice(0, 56)}…`;
|
||||
}
|
||||
|
||||
// 计算是否禁用滚动加载
|
||||
const scrollDisabled = computed(() => {
|
||||
return loadingMore.value || noMore.value;
|
||||
});
|
||||
|
||||
// 计算是否没有更多数据
|
||||
const noMore = computed(() => {
|
||||
return postList.value.length >= total.value && total.value > 0;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
loadPosts();
|
||||
});
|
||||
|
||||
const loadPosts = async (isLoadMore = false) => {
|
||||
if (isLoadMore) {
|
||||
loadingMore.value = true;
|
||||
} else {
|
||||
loading.value = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await tbGsxtXxltSelectPage(listQuery.value);
|
||||
const data = (res.records || []).map((item) => ({
|
||||
id: item.id,
|
||||
title: item.title || "",
|
||||
userName: item.fbrxm || "匿名用户",
|
||||
userAvatar: item.fbrtx ? setAddress(item.fbrtx) : "",
|
||||
userTag: item.userTag || "",
|
||||
publishTime: item.time || "",
|
||||
content: item.content || "",
|
||||
images: item.tp ? item.tp.split(",").map((img) => setAddress(img)) : [],
|
||||
commentCount: item.hfsl || 0,
|
||||
likeCount: item.likeCount || 0,
|
||||
isPremium: item.sfzd === 1,
|
||||
isLiked: false,
|
||||
ssbm: item.ssbm,
|
||||
// 保存原始数据
|
||||
rawData: item
|
||||
}));
|
||||
|
||||
if (isLoadMore) {
|
||||
postList.value = [...postList.value, ...data];
|
||||
} else {
|
||||
postList.value = data;
|
||||
}
|
||||
|
||||
total.value = res.total || 0;
|
||||
} catch (error) {
|
||||
console.error("加载失败", error);
|
||||
ElMessage.error("加载失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
loadingMore.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 加载更多
|
||||
const loadMore = () => {
|
||||
if (postList.value.length >= total.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
listQuery.value.pageCurrent++;
|
||||
loadPosts(true);
|
||||
};
|
||||
|
||||
const handleLike = (post) => {
|
||||
post.isLiked = !post.isLiked;
|
||||
post.likeCount += post.isLiked ? 1 : -1;
|
||||
ElMessage.success(post.isLiked ? "点赞成功" : "取消点赞");
|
||||
};
|
||||
|
||||
const handlePublishSuccess = () => {
|
||||
// 重置分页并重新加载
|
||||
listQuery.value.pageCurrent = 1;
|
||||
loadPosts();
|
||||
};
|
||||
|
||||
const emit = defineEmits(["openDetail"]);
|
||||
|
||||
const handlePostClick = (post) => {
|
||||
emit("openDetail", post);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../styles/luntan-tech.scss";
|
||||
|
||||
.post-list {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
// 贴吧式顶栏:圆角条 + 左图标标题统计 + 右胶囊按钮(外层页面仍为原深蓝网格背景)
|
||||
.bar-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 18px;
|
||||
padding: 14px 18px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(
|
||||
160deg,
|
||||
rgba(16, 46, 96, 0.88) 0%,
|
||||
rgba(12, 34, 76, 0.82) 100%
|
||||
);
|
||||
border: 1px solid rgba(0, 163, 255, 0.28);
|
||||
box-shadow: inset 0 1px 0 rgba(0, 220, 255, 0.1),
|
||||
0 4px 20px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.bar-head-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bar-logo {
|
||||
flex-shrink: 0;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
box-shadow: 0 2px 10px rgba(0, 100, 180, 0.2);
|
||||
|
||||
.el-icon {
|
||||
font-size: 26px;
|
||||
color: #00a3ff;
|
||||
}
|
||||
}
|
||||
|
||||
.bar-head-text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bar-title {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: $lt-text;
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.bar-meta {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: $lt-text-muted;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.bar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bar-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 8px 18px;
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: rgba(230, 240, 255, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
|
||||
.el-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(0, 227, 255, 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
.bar-pill-primary {
|
||||
border-color: rgba(0, 227, 255, 0.4);
|
||||
background: rgba(0, 60, 120, 0.35);
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 100, 180, 0.45);
|
||||
box-shadow: 0 0 14px rgba(0, 163, 255, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
// 置顶缩略区(仅标题)
|
||||
.pinned-block {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.pinned-list {
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
background: rgba(220, 235, 255, 0.12);
|
||||
border: 1px solid rgba(0, 163, 255, 0.22);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.pinned-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 8px 4px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
& + .pinned-row {
|
||||
border-top: 1px solid rgba(0, 163, 255, 0.12);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 163, 255, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.pinned-badge {
|
||||
flex-shrink: 0;
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: $lt-cyan-mid;
|
||||
border: 1px solid rgba(0, 163, 255, 0.45);
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 40, 90, 0.35);
|
||||
}
|
||||
|
||||
.pinned-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: $lt-text;
|
||||
line-height: 1.45;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
// 置顶与普通帖之间的分界
|
||||
.list-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 16px 0 14px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.divider-line {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(0, 163, 255, 0.45),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.divider-text {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
color: $lt-text-muted;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.posts-container {
|
||||
min-height: 400px;
|
||||
|
||||
:deep(.el-loading-mask) {
|
||||
background-color: rgba(0, 5, 16, 0.65);
|
||||
}
|
||||
|
||||
:deep(.el-loading-spinner .path) {
|
||||
stroke: #3db8ff;
|
||||
}
|
||||
}
|
||||
|
||||
.post-empty {
|
||||
padding: 48px 0;
|
||||
|
||||
:deep(.el-empty__description) {
|
||||
color: rgba(180, 200, 230, 0.65);
|
||||
}
|
||||
|
||||
:deep(.el-empty__image) {
|
||||
opacity: 0.85;
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
color: rgba(160, 185, 215, 0.75);
|
||||
font-size: 14px;
|
||||
gap: 8px;
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
color: #3db8ff;
|
||||
}
|
||||
}
|
||||
|
||||
.no-more {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: rgba(140, 165, 200, 0.55);
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
class="luntan-tech-dialog"
|
||||
title="发布帖子"
|
||||
width="60%"
|
||||
:before-close="handleClose"
|
||||
>
|
||||
<div style="overflow: auto; height: 60vh">
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input
|
||||
v-model="form.title"
|
||||
placeholder="请输入帖子标题"
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="内容" prop="content">
|
||||
<el-input
|
||||
v-model="form.content"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入帖子内容"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="表情">
|
||||
<V3Emoji
|
||||
:options-name="optionsName"
|
||||
@click-emoji="onEmojiClick"
|
||||
:recent="true"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="图片">
|
||||
<Upload v-model="imageIds" :limit="9" :isImg="true" :isAll="true" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
发布
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import V3Emoji from "vue3-emoji";
|
||||
import { tbGsxtXxltSave } from "@/api/tbGsxtXxltHf";
|
||||
import { getItem } from "@/utils/storage.js";
|
||||
import Upload from "@/components/MyComponents/Upload/index.vue";
|
||||
|
||||
const optionsName = {
|
||||
"Smileys & Emotion": "笑脸&表情",
|
||||
"Food & Drink": "食物&饮料",
|
||||
"Animals & Nature": "动物&自然",
|
||||
"Travel & Places": "旅行&地点",
|
||||
"People & Body": "人物&身体",
|
||||
Objects: "物品",
|
||||
Symbols: "符号",
|
||||
Flags: "旗帜",
|
||||
Activities: "活动"
|
||||
};
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "success"]);
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const formRef = ref();
|
||||
const submitting = ref(false);
|
||||
const imageIds = ref([]);
|
||||
|
||||
const form = ref({
|
||||
title: "",
|
||||
content: ""
|
||||
});
|
||||
|
||||
const rules = {
|
||||
title: [{ required: true, message: "请输入标题", trigger: "blur" }],
|
||||
content: [{ required: true, message: "请输入内容", trigger: "blur" }]
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
dialogVisible.value = val;
|
||||
}
|
||||
);
|
||||
|
||||
watch(dialogVisible, (val) => {
|
||||
emit("update:modelValue", val);
|
||||
if (!val) {
|
||||
resetForm();
|
||||
}
|
||||
});
|
||||
|
||||
const onEmojiClick = (emoji) => {
|
||||
form.value.content += emoji;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitting.value = true;
|
||||
try {
|
||||
const ltmasg = getItem("ltmasg");
|
||||
|
||||
const postData = {
|
||||
title: form.value.title,
|
||||
content: form.value.content,
|
||||
tp: imageIds.value.join(","),
|
||||
fbrsfzh: ltmasg?.sfzh || "",
|
||||
fbrxm: ltmasg?.xm || "",
|
||||
fbrtx: ltmasg?.tx || ""
|
||||
};
|
||||
|
||||
await tbGsxtXxltSave(postData);
|
||||
|
||||
ElMessage.success("发布成功");
|
||||
emit("success");
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
console.error("发布失败", error);
|
||||
ElMessage.error("发布失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
form.value = {
|
||||
title: "",
|
||||
content: ""
|
||||
};
|
||||
imageIds.value = [];
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// Upload 等子组件样式在各自内部
|
||||
::v-deep .form-item-box {
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../styles/luntan-dialog-tech.scss";
|
||||
</style>
|
||||
@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
class="luntan-tech-dialog"
|
||||
title="发表回复"
|
||||
width="600px"
|
||||
:before-close="handleClose"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef">
|
||||
<el-form-item prop="content">
|
||||
<el-input
|
||||
v-model="form.content"
|
||||
type="textarea"
|
||||
:rows="8"
|
||||
placeholder="发点干货 文明第一步"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<div class="emoji-row">
|
||||
<V3Emoji
|
||||
:options-name="optionsName"
|
||||
@click-emoji="onEmojiClick"
|
||||
:recent="true"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
回复
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import V3Emoji from "vue3-emoji";
|
||||
import { getItem } from "@/utils/storage.js";
|
||||
import {
|
||||
tbGsxtXxltHfid,
|
||||
tbGsxtXxltHfSave,
|
||||
tbGsxtXxltHfSelectList
|
||||
} from "@/api/tbGsxtXxltHf.js";
|
||||
|
||||
const optionsName = {
|
||||
"Smileys & Emotion": "笑脸&表情",
|
||||
"Food & Drink": "食物&饮料",
|
||||
"Animals & Nature": "动物&自然",
|
||||
"Travel & Places": "旅行&地点",
|
||||
"People & Body": "人物&身体",
|
||||
Objects: "物品",
|
||||
Symbols: "符号",
|
||||
Flags: "旗帜",
|
||||
Activities: "活动"
|
||||
};
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
replyTo: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "success"]);
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const formRef = ref();
|
||||
const submitting = ref(false);
|
||||
|
||||
const form = ref({
|
||||
content: ""
|
||||
});
|
||||
|
||||
const rules = {
|
||||
content: [{ required: true, message: "请输入回复内容", trigger: "blur" }]
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
dialogVisible.value = val;
|
||||
}
|
||||
);
|
||||
|
||||
watch(dialogVisible, (val) => {
|
||||
emit("update:modelValue", val);
|
||||
if (!val) {
|
||||
resetForm();
|
||||
}
|
||||
});
|
||||
|
||||
const onEmojiClick = (emoji) => {
|
||||
form.value.content += emoji;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitting.value = true;
|
||||
try {
|
||||
const ltmasg = getItem("ltmasg");
|
||||
const promes = {
|
||||
hfnr: form.value.content,
|
||||
hfrsfzh: ltmasg.sfzh,
|
||||
hfrtx: ltmasg.tx,
|
||||
hfrxm: ltmasg.xm,
|
||||
ltid: props.replyTo.id,
|
||||
sfyjhf: "1"
|
||||
// hftp: hfrsfzh.value.hftp ? hfrsfzh.value.hftp.join(',') : ''
|
||||
};
|
||||
|
||||
// 这里替换为实际的API调用
|
||||
await tbGsxtXxltHfSave(promes);
|
||||
// await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
ElMessage.success("回复成功");
|
||||
emit("success", form.value);
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
ElMessage.error("回复失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
form.value = {
|
||||
content: ""
|
||||
};
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.emoji-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../styles/luntan-dialog-tech.scss';
|
||||
</style>
|
||||
346
src/views/backOfficeSystem/luntan copy/components/UserCard.vue
Normal file
346
src/views/backOfficeSystem/luntan copy/components/UserCard.vue
Normal file
@ -0,0 +1,346 @@
|
||||
<template>
|
||||
<div class="user-card">
|
||||
<div class="user-card-head">
|
||||
<div class="user-avatar">
|
||||
<div class="avatar-wrapper" @click="showAvatarDialog = true">
|
||||
<el-avatar :size="56" :src="avatarUrl">
|
||||
<img src="@/assets/images/mr.png" />
|
||||
</el-avatar>
|
||||
<div class="avatar-overlay">
|
||||
<el-icon class="upload-icon">
|
||||
<Camera />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-card-head-text">
|
||||
<div class="name-row">
|
||||
<span class="nickname">{{ userInfo.nickname || '用户信息' }}</span>
|
||||
</div>
|
||||
<div class="sub-stats">内部论坛 · 已登录</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-info">
|
||||
<div class="info-item clickable" @click="showNicknameDialog = true">
|
||||
<span class="label">昵称</span>
|
||||
<span class="value">{{ userInfo.nickname || '-' }}</span>
|
||||
<el-icon class="edit-icon">
|
||||
<Edit />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">账号</span>
|
||||
<span class="value">{{ userInfo.account || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">姓名</span>
|
||||
<span class="value">{{ userInfo.name || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">部门</span>
|
||||
<span class="value">{{ userInfo.department || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 更换头像对话框 -->
|
||||
<ChangeAvatar v-model="showAvatarDialog" title="更换头像" @avatarUpdated="handleAvatarUpdated" />
|
||||
|
||||
<!-- 编辑昵称对话框 -->
|
||||
<el-dialog
|
||||
v-model="showNicknameDialog"
|
||||
class="luntan-tech-dialog"
|
||||
title="编辑昵称"
|
||||
width="400px"
|
||||
center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form ref="nicknameFormRef" :model="nicknameForm" :rules="nicknameRules" label-width="80px">
|
||||
<el-form-item label="昵称" prop="nickname">
|
||||
<el-input v-model="nicknameForm.nickname" placeholder="请输入昵称" maxlength="20" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showNicknameDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSaveNickname">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Camera, Edit } from '@element-plus/icons-vue'
|
||||
import { getItem, setItem, removeItem } from '@/utils/storage.js'
|
||||
import { setAddress } from '@/utils/tools'
|
||||
import { tbGsxtXxltTxTxQueryBySfzh, tbGsxtXxltTxTxSave } from '@/api/tbGsxtXxltHf.js'
|
||||
import ChangeAvatar from './ChangeAvatar.vue'
|
||||
|
||||
const showAvatarDialog = ref(false)
|
||||
const showNicknameDialog = ref(false)
|
||||
const nicknameFormRef = ref()
|
||||
|
||||
const userInfo = ref({
|
||||
avatar: '',
|
||||
account: '',
|
||||
name: '',
|
||||
department: '',
|
||||
nickname: ''
|
||||
})
|
||||
|
||||
const nicknameForm = reactive({
|
||||
nickname: ''
|
||||
})
|
||||
|
||||
const nicknameRules = {
|
||||
nickname: [
|
||||
{ required: true, message: '请输入昵称', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '昵称长度在 2 到 20 个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const avatarUrl = computed(() => {
|
||||
return userInfo.value.avatar ? setAddress(userInfo.value.avatar) : ''
|
||||
})
|
||||
|
||||
// 加载用户信息
|
||||
const loadUserInfo = async () => {
|
||||
const sfzh = getItem('idEntityCard')
|
||||
let ltmasg = getItem('ltmasg')
|
||||
|
||||
if (!ltmasg) {
|
||||
try {
|
||||
const res = await tbGsxtXxltTxTxQueryBySfzh({ sfzh: sfzh })
|
||||
console.log(res);
|
||||
|
||||
const deptId = getItem('deptId')?.[0]
|
||||
ltmasg = {
|
||||
...res,
|
||||
deptName: deptId?.deptName || ''
|
||||
}
|
||||
setItem('ltmasg', ltmasg)
|
||||
} catch (error) {
|
||||
console.error('加载用户信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
if (ltmasg) {
|
||||
userInfo.value = {
|
||||
avatar: ltmasg.tx || '',
|
||||
account: ltmasg.sfzh || '',
|
||||
name: ltmasg.xm || '',
|
||||
department: ltmasg.deptName || ltmasg.bm || '',
|
||||
nickname: ltmasg.nc || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理头像更新
|
||||
const handleAvatarUpdated = async (newAvatar) => {
|
||||
try {
|
||||
const ltmasg = getItem('ltmasg')
|
||||
const updateData = {
|
||||
...ltmasg,
|
||||
tx: newAvatar
|
||||
}
|
||||
|
||||
await tbGsxtXxltTxTxSave(updateData)
|
||||
removeItem('ltmasg')
|
||||
await loadUserInfo()
|
||||
ElMessage.success('头像更新成功')
|
||||
} catch (error) {
|
||||
console.error('更新头像失败:', error)
|
||||
ElMessage.error('头像更新失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理保存昵称
|
||||
const handleSaveNickname = async () => {
|
||||
if (!nicknameFormRef.value) return
|
||||
|
||||
await nicknameFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const ltmasg = getItem('ltmasg')
|
||||
const updateData = {
|
||||
...ltmasg,
|
||||
nc: nicknameForm.nickname
|
||||
}
|
||||
|
||||
await tbGsxtXxltTxTxSave(updateData)
|
||||
removeItem('ltmasg')
|
||||
await loadUserInfo()
|
||||
showNicknameDialog.value = false
|
||||
ElMessage.success('昵称保存成功')
|
||||
} catch (error) {
|
||||
console.error('保存昵称失败:', error)
|
||||
ElMessage.error('昵称保存失败,请重试')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 监听昵称对话框打开,初始化表单
|
||||
const openNicknameDialog = () => {
|
||||
nicknameForm.nickname = userInfo.value.nickname
|
||||
}
|
||||
|
||||
// 监听对话框显示状态
|
||||
const unwatchNickname = () => {
|
||||
if (showNicknameDialog.value) {
|
||||
openNicknameDialog()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadUserInfo()
|
||||
})
|
||||
|
||||
// 监听昵称对话框
|
||||
const stopWatch = () => {
|
||||
if (showNicknameDialog.value) {
|
||||
nicknameForm.nickname = userInfo.value.nickname
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 watch 监听对话框状态
|
||||
import { watch } from 'vue'
|
||||
watch(showNicknameDialog, (newVal) => {
|
||||
if (newVal) {
|
||||
nicknameForm.nickname = userInfo.value.nickname
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '../styles/luntan-tech.scss';
|
||||
|
||||
.user-card {
|
||||
border-radius: 4px;
|
||||
padding: 18px 16px 16px;
|
||||
@include lt-panel-frame;
|
||||
}
|
||||
|
||||
.user-card-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
flex-shrink: 0;
|
||||
|
||||
.avatar-wrapper {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 2px solid rgba(100, 180, 255, 0.35);
|
||||
|
||||
&:hover .avatar-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.avatar-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
border-radius: 50%;
|
||||
|
||||
.upload-icon {
|
||||
font-size: 22px;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-card-head-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.name-row {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #f0f6ff;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.sub-stats {
|
||||
font-size: 12px;
|
||||
color: rgba(180, 200, 230, 0.55);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
position: relative;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
padding: 6px 8px;
|
||||
margin-left: -8px;
|
||||
margin-right: -8px;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.06);
|
||||
|
||||
.edit-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
color: rgba(160, 185, 215, 0.55);
|
||||
min-width: 40px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: rgba(220, 230, 245, 0.88);
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
margin-left: 6px;
|
||||
color: #5eb8ff;
|
||||
font-size: 14px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../styles/luntan-dialog-tech.scss';
|
||||
</style>
|
||||
333
src/views/backOfficeSystem/luntan copy/index.vue
Normal file
333
src/views/backOfficeSystem/luntan copy/index.vue
Normal file
@ -0,0 +1,333 @@
|
||||
<template>
|
||||
<div class="luntan-container">
|
||||
<!-- 列表页:主列帖子 + 右侧信息栏 -->
|
||||
<template v-if="!showDetail">
|
||||
<div class="luntan-main">
|
||||
<PostList @openDetail="handleOpenDetail" />
|
||||
</div>
|
||||
<aside class="luntan-sidebar">
|
||||
<UserCard />
|
||||
<div class="hot-news-card" v-loading="hotLoading">
|
||||
<div class="hot-news-head">
|
||||
<span class="hot-news-title">热度消息</span>
|
||||
<span class="hot-news-badge">HOT</span>
|
||||
</div>
|
||||
<ul class="hot-news-list">
|
||||
<li
|
||||
v-for="(item, index) in hotList"
|
||||
:key="item.id"
|
||||
class="hot-news-item"
|
||||
@click="handleOpenDetail(item)"
|
||||
>
|
||||
<span class="hot-rank" :class="{ 'is-top': index < 3 }">{{
|
||||
index + 1
|
||||
}}</span>
|
||||
<span class="hot-item-title">{{ item.lineTitle }}</span>
|
||||
</li>
|
||||
<li
|
||||
v-if="!hotLoading && hotList.length === 0"
|
||||
class="hot-news-empty"
|
||||
>
|
||||
暂无热度内容
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<!-- 详情页 -->
|
||||
<template v-else>
|
||||
<div class="luntan-detail-wrap">
|
||||
<div class="luntan-detail">
|
||||
<PostDetail :post-id="currentPostId" @back="handleBack" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import UserCard from "./components/UserCard.vue";
|
||||
import PostList from "./components/PostList.vue";
|
||||
import PostDetail from "./components/PostDetail.vue";
|
||||
import { tbGsxtXxltSelectPage } from "@/api/tbGsxtXxltHf";
|
||||
import { setAddress } from "@/utils/tools";
|
||||
|
||||
const showDetail = ref(false);
|
||||
const currentPostId = ref(null);
|
||||
|
||||
const hotList = ref([]);
|
||||
const hotLoading = ref(false);
|
||||
|
||||
function lineTitleFromRecord(item) {
|
||||
const tit = (item.title || "").trim();
|
||||
if (tit) {
|
||||
return tit.length > 40 ? `${tit.slice(0, 40)}…` : tit;
|
||||
}
|
||||
const t = (item.content || "").trim();
|
||||
if (!t) return "无标题";
|
||||
const idx = t.indexOf("\n");
|
||||
const first = idx === -1 ? t : t.slice(0, idx).trim();
|
||||
const line = first || t.slice(0, 60);
|
||||
return line.length > 40 ? `${line.slice(0, 40)}…` : line;
|
||||
}
|
||||
|
||||
function mapHotPost(item) {
|
||||
return {
|
||||
id: item.id,
|
||||
lineTitle: lineTitleFromRecord(item),
|
||||
userName: item.fbrxm || "匿名用户",
|
||||
userAvatar: item.fbrtx ? setAddress(item.fbrtx) : "",
|
||||
userTag: item.userTag || "",
|
||||
publishTime: item.time || "",
|
||||
content: item.content || "",
|
||||
images: item.tp ? item.tp.split(",").map((img) => setAddress(img)) : [],
|
||||
commentCount: item.hfsl || 0,
|
||||
likeCount: item.likeCount || 0,
|
||||
isPremium: item.sfzd === 1,
|
||||
isLiked: false,
|
||||
ssbm: item.ssbm,
|
||||
rawData: item
|
||||
};
|
||||
}
|
||||
|
||||
const loadHotNews = async () => {
|
||||
hotLoading.value = true;
|
||||
try {
|
||||
const res = await tbGsxtXxltSelectPage({
|
||||
pageCurrent: 1,
|
||||
pageSize: 12
|
||||
});
|
||||
const records = res.records || [];
|
||||
hotList.value = records.map(mapHotPost);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
hotList.value = [];
|
||||
} finally {
|
||||
hotLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadHotNews();
|
||||
});
|
||||
|
||||
const handleOpenDetail = (post) => {
|
||||
currentPostId.value = post.id;
|
||||
showDetail.value = true;
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
showDetail.value = false;
|
||||
currentPostId.value = null;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./styles/luntan-tech.scss";
|
||||
|
||||
.luntan-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
padding: 20px 24px;
|
||||
min-height: calc(100vh - 60px);
|
||||
max-height: calc(100vh - 60px);
|
||||
overflow: hidden;
|
||||
background: linear-gradient(165deg, $lt-bg 0%, $lt-bg-soft 42%, #0a1830 100%);
|
||||
box-sizing: border-box;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: linear-gradient(
|
||||
rgba(0, 163, 255, 0.04) 1px,
|
||||
transparent 1px
|
||||
),
|
||||
linear-gradient(90deg, rgba(0, 163, 255, 0.04) 1px, transparent 1px);
|
||||
background-size: 32px 32px;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.luntan-main {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.luntan-sidebar {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 300px;
|
||||
flex-shrink: 0;
|
||||
height: calc(100vh - 100px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hot-news-card {
|
||||
position: relative;
|
||||
// flex: 1;
|
||||
height: calc(80% - 100px);
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
border-radius: 6px;
|
||||
@include lt-panel-frame;
|
||||
background: linear-gradient(
|
||||
165deg,
|
||||
rgba(14, 40, 82, 0.9) 0%,
|
||||
rgba(10, 28, 58, 0.88) 100%
|
||||
) !important;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.el-loading-mask) {
|
||||
background-color: rgba(4, 12, 28, 0.55);
|
||||
}
|
||||
|
||||
:deep(.el-loading-spinner .path) {
|
||||
stroke: $lt-cyan-mid;
|
||||
}
|
||||
}
|
||||
|
||||
.hot-news-head {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 14px 16px 12px;
|
||||
border-bottom: 1px solid rgba(0, 163, 255, 0.22);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 163, 255, 0.1) 0%,
|
||||
transparent 70%
|
||||
);
|
||||
}
|
||||
|
||||
.hot-news-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: $lt-text;
|
||||
letter-spacing: 0.08em;
|
||||
text-shadow: 0 0 14px rgba(0, 227, 255, 0.35);
|
||||
}
|
||||
|
||||
.hot-news-badge {
|
||||
flex-shrink: 0;
|
||||
padding: 3px 10px;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.12em;
|
||||
color: #1a0a00;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(180deg, #ffb347 0%, #ff9900 100%);
|
||||
border: 1px solid rgba(255, 200, 120, 0.5);
|
||||
box-shadow: 0 0 12px rgba(255, 153, 0, 0.35);
|
||||
}
|
||||
|
||||
.hot-news-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
padding: 8px 10px 12px;
|
||||
list-style: none;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.hot-news-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 10px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, box-shadow 0.15s ease;
|
||||
|
||||
& + .hot-news-item {
|
||||
border-top: 1px solid rgba(0, 163, 255, 0.1);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 163, 255, 0.1);
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 227, 255, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.hot-rank {
|
||||
flex-shrink: 0;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: rgba(210, 225, 245, 0.92);
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 40, 80, 0.45);
|
||||
border: 1px solid rgba(0, 163, 255, 0.2);
|
||||
|
||||
&.is-top {
|
||||
color: #1a0a00;
|
||||
background: linear-gradient(180deg, #ffd699 0%, #ff9900 100%);
|
||||
border-color: rgba(255, 180, 80, 0.55);
|
||||
box-shadow: 0 0 8px rgba(255, 153, 0, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
.hot-item-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: rgba(236, 245, 255, 0.94);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hot-news-item:hover .hot-item-title {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.hot-news-empty {
|
||||
padding: 28px 12px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: rgba(200, 218, 240, 0.82);
|
||||
list-style: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.luntan-detail-wrap {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.luntan-detail {
|
||||
height: 83vh;
|
||||
flex: 1;
|
||||
border-radius: 4px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
@include lt-panel-frame;
|
||||
box-shadow: $lt-glow-strong, 0 12px 40px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 非 scoped:el-image-viewer 通过 Teleport 挂到 body,需全局选择器 -->
|
||||
<style lang="scss">
|
||||
@import "./styles/luntan-tech.scss";
|
||||
</style>
|
||||
@ -0,0 +1,104 @@
|
||||
@import "./luntan-tech.scss";
|
||||
@import "./luntan-v3emoji-tech.scss";
|
||||
|
||||
.luntan-tech-dialog.el-dialog {
|
||||
background: rgba(8, 20, 48, 0.98) !important;
|
||||
border: 1px solid $lt-border;
|
||||
border-radius: 4px;
|
||||
box-shadow: $lt-glow-strong, 0 16px 48px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.luntan-tech-dialog .el-dialog__header {
|
||||
border-bottom: 1px solid $lt-border-dim;
|
||||
padding: 14px 18px;
|
||||
margin: 0;
|
||||
background: rgba(8, 20, 48, 0.98) !important;
|
||||
}
|
||||
|
||||
.luntan-tech-dialog .el-dialog__title {
|
||||
color: $lt-text;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.luntan-tech-dialog .el-dialog__headerbtn .el-dialog__close {
|
||||
color: $lt-text-muted;
|
||||
}
|
||||
|
||||
.luntan-tech-dialog .el-dialog__headerbtn:hover .el-dialog__close {
|
||||
color: $lt-cyan;
|
||||
}
|
||||
|
||||
.luntan-tech-dialog .el-dialog__body {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.luntan-tech-dialog .el-dialog__footer {
|
||||
border-top: 1px solid $lt-border-dim;
|
||||
padding: 12px 18px;
|
||||
background: rgba(8, 20, 48, 0.98) !important;
|
||||
}
|
||||
|
||||
.luntan-tech-dialog .el-form-item__label {
|
||||
color: $lt-text-muted;
|
||||
}
|
||||
|
||||
.luntan-tech-dialog .el-input__wrapper {
|
||||
background: rgba(10, 28, 58, 0.85) !important;
|
||||
box-shadow: 0 0 0 1px rgba(0, 227, 255, 0.28) inset !important;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px rgba(0, 227, 255, 0.4) inset !important;
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
box-shadow: 0 0 0 1px rgba(0, 227, 255, 0.55) inset,
|
||||
0 0 14px rgba(0, 163, 255, 0.22) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.luntan-tech-dialog .el-input__inner {
|
||||
color: $lt-text-dim;
|
||||
}
|
||||
|
||||
.luntan-tech-dialog .el-input__count,
|
||||
.luntan-tech-dialog .el-input__count .el-input__count-inner {
|
||||
background: transparent !important;
|
||||
color: $lt-text-muted;
|
||||
}
|
||||
|
||||
.luntan-tech-dialog .el-textarea__inner {
|
||||
background: rgba(10, 28, 58, 0.9) !important;
|
||||
color: $lt-text-dim;
|
||||
border: 1px solid rgba(0, 227, 255, 0.3);
|
||||
border-radius: 4px;
|
||||
box-shadow: inset 0 0 20px rgba(20, 80, 140, 0.2);
|
||||
}
|
||||
|
||||
.luntan-tech-dialog .el-button--default {
|
||||
background: rgba(15, 40, 75, 0.65);
|
||||
border-color: rgba(0, 163, 255, 0.35);
|
||||
color: $lt-text-dim;
|
||||
}
|
||||
|
||||
.luntan-tech-dialog .el-button--primary {
|
||||
background: linear-gradient(180deg, #00a3ff 0%, #0066bb 100%);
|
||||
border-color: rgba(0, 227, 255, 0.5);
|
||||
box-shadow: 0 0 16px rgba(0, 163, 255, 0.4);
|
||||
}
|
||||
|
||||
.luntan-tech-dialog .el-upload--picture-card {
|
||||
background: rgba(10, 28, 58, 0.75) !important;
|
||||
border-color: rgba(0, 227, 255, 0.35) !important;
|
||||
|
||||
.el-icon {
|
||||
color: $lt-cyan;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(0, 227, 255, 0.55) !important;
|
||||
box-shadow: 0 0 14px rgba(0, 163, 255, 0.25);
|
||||
}
|
||||
}
|
||||
103
src/views/backOfficeSystem/luntan copy/styles/luntan-tech.scss
Normal file
103
src/views/backOfficeSystem/luntan copy/styles/luntan-tech.scss
Normal file
@ -0,0 +1,103 @@
|
||||
// 论坛模块 — 大屏科技风主题变量
|
||||
$lt-bg: #0a1628;
|
||||
$lt-bg-soft: #0d1f3c;
|
||||
$lt-cyan: #00e5ff;
|
||||
$lt-cyan-mid: #00a3ff;
|
||||
$lt-panel: rgba(10, 30, 60, 0.85);
|
||||
$lt-panel-soft: rgba(8, 24, 52, 0.65);
|
||||
$lt-border: rgba(0, 227, 255, 0.38);
|
||||
$lt-border-dim: rgba(0, 163, 255, 0.2);
|
||||
$lt-text: #e8f4ff;
|
||||
$lt-text-dim: rgba(140, 200, 230, 0.78);
|
||||
$lt-text-muted: rgba(120, 170, 210, 0.55);
|
||||
$lt-glow: 0 0 18px rgba(0, 163, 255, 0.35);
|
||||
$lt-glow-strong: 0 0 28px rgba(0, 227, 255, 0.28);
|
||||
|
||||
@mixin lt-panel-frame {
|
||||
background: $lt-panel;
|
||||
border: 1px solid $lt-border;
|
||||
box-shadow: $lt-glow, inset 0 1px 0 rgba(0, 220, 255, 0.12);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
@mixin lt-panel-soft-bg {
|
||||
background: $lt-panel-soft;
|
||||
border: 1px solid $lt-border-dim;
|
||||
box-shadow: inset 0 1px 0 rgba(0, 220, 255, 0.08);
|
||||
}
|
||||
|
||||
@mixin lt-corner-brackets {
|
||||
position: relative;
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-color: $lt-cyan-mid;
|
||||
border-style: solid;
|
||||
pointer-events: none;
|
||||
opacity: 0.85;
|
||||
}
|
||||
&::before {
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
border-width: 2px 0 0 2px;
|
||||
box-shadow: -1px -1px 8px rgba(0, 227, 255, 0.4);
|
||||
}
|
||||
&::after {
|
||||
bottom: -1px;
|
||||
right: -1px;
|
||||
border-width: 0 2px 2px 0;
|
||||
box-shadow: 1px 1px 8px rgba(0, 227, 255, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
// el-image-viewer 全屏预览覆盖 — Teleport 到 body,全局生效
|
||||
.el-image-viewer__wrapper {
|
||||
background: rgba(4, 10, 24, 0.95) !important;
|
||||
backdrop-filter: blur(4px);
|
||||
|
||||
.el-image-viewer__mask {
|
||||
background: rgba(0, 10, 30, 0.78) !important;
|
||||
}
|
||||
|
||||
.el-image-viewer__btn {
|
||||
color: $lt-cyan;
|
||||
border: 1px solid rgba(0, 227, 255, 0.3);
|
||||
background: rgba(10, 28, 58, 0.65);
|
||||
box-shadow: 0 0 10px rgba(0, 120, 200, 0.2);
|
||||
|
||||
&:hover {
|
||||
background: rgba(15, 45, 80, 0.88);
|
||||
border-color: rgba(0, 227, 255, 0.55);
|
||||
}
|
||||
|
||||
&.el-image-viewer__close {
|
||||
top: 24px;
|
||||
right: 28px;
|
||||
}
|
||||
|
||||
&.el-image-viewer__prev {
|
||||
left: 24px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
&.el-image-viewer__next {
|
||||
right: 24px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.el-image-viewer__canvas {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
@import './luntan-tech.scss';
|
||||
|
||||
// vue3-emoji(dist/style.css)默认浅色/白底,与论坛科技风统一
|
||||
@mixin lt-v3-emoji-vars {
|
||||
--V3Emoji-backgroundColor: rgba(10, 28, 58, 0.96);
|
||||
--V3Emoji-hoverColor: rgba(0, 100, 160, 0.22);
|
||||
--V3Emoji-activeColor: rgba(0, 130, 200, 0.3);
|
||||
--V3Emoji-fontColor: #{$lt-text};
|
||||
--V3Emoji-borderColor: #{rgba(0, 227, 255, 0.38)};
|
||||
--V3Emoji-borderFocusColor: #{$lt-cyan};
|
||||
--V3Emoji-shadowColor: rgba(0, 40, 90, 0.45);
|
||||
}
|
||||
|
||||
.luntan-tech-dialog,
|
||||
.comment-list .reply-input-box,
|
||||
.luntan-tech-dialog .emoji-row {
|
||||
@include lt-v3-emoji-vars;
|
||||
}
|
||||
|
||||
// 组件内未使用 CSS 变量的原生 input / textarea(仍为白底)
|
||||
.luntan-tech-dialog [class*='emojiInput'] input,
|
||||
.comment-list .reply-input-box [class*='emojiInput'] input {
|
||||
background: rgba(10, 28, 58, 0.92) !important;
|
||||
color: $lt-text-dim !important;
|
||||
border-color: rgba(0, 227, 255, 0.35) !important;
|
||||
box-shadow: inset 0 0 12px rgba(0, 50, 100, 0.25) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: $lt-text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
.luntan-tech-dialog [class*='emojiTextarea'] textarea,
|
||||
.comment-list .reply-input-box [class*='emojiTextarea'] textarea {
|
||||
background: rgba(10, 28, 58, 0.92) !important;
|
||||
color: $lt-text-dim !important;
|
||||
border-color: rgba(0, 227, 255, 0.35) !important;
|
||||
box-shadow: inset 0 0 12px rgba(0, 50, 100, 0.25) !important;
|
||||
}
|
||||
|
||||
.luntan-tech-dialog [class*='emojiContainerOpenBtn'],
|
||||
.luntan-tech-dialog [class*='emojiTextareaOpenBtn'],
|
||||
.comment-list .reply-input-box [class*='emojiContainerOpenBtn'],
|
||||
.comment-list .reply-input-box [class*='emojiTextareaOpenBtn'] {
|
||||
color: $lt-cyan !important;
|
||||
filter: drop-shadow(0 0 6px rgba(0, 227, 255, 0.35));
|
||||
}
|
||||
|
||||
// 表情面板可能 teleport / 挂到 body,需单独写选择器
|
||||
[class*='V3Emoji-vue'][class*='__pollup___'],
|
||||
[class*='PollUp-vue'][class*='__pollup___'] {
|
||||
@include lt-v3-emoji-vars;
|
||||
}
|
||||
|
||||
[id='EmojiItem'],
|
||||
[id*='EmojiItem'] {
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 163, 255, 0.45) !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(8, 20, 48, 0.88) !important;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user