This commit is contained in:
lcw
2025-10-26 12:25:50 +08:00
parent 5e18952b55
commit ea3022c3f6
617 changed files with 86322 additions and 185615 deletions

View File

@ -0,0 +1,219 @@
<template>
<el-dialog
v-model="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> 支持 JPGPNG 格式</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>
.avatar-upload-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
.avatar-preview {
width: 160px;
height: 160px;
border-radius: 50%;
overflow: hidden;
border: 2px solid #e8e8e8;
margin-bottom: 20px;
display: flex;
justify-content: center;
align-items: center;
background-color: #f5f7fa;
.preview-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.preview-placeholder {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #909399;
.placeholder-icon {
font-size: 48px;
margin-bottom: 8px;
}
span {
font-size: 14px;
}
}
}
.upload-section {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.avatar-uploader {
margin-bottom: 16px;
}
.upload-tips {
width: 100%;
padding: 12px;
background-color: #f5f7fa;
border-radius: 4px;
font-size: 12px;
color: #909399;
p {
margin: 4px 0;
line-height: 1.5;
}
}
</style>

View File

@ -0,0 +1,418 @@
<template>
<div class="dialog" v-if="dialogForm">
<div class="head_box">
<span class="title">{{ title }} </span>
<div>
<el-button size="small" @click="close">关闭</el-button>
</div>
</div>
<!-- 标题 -->
<div class="contentTitle">
<div class="content">
{{ listQuery.title }}
</div>
<div class="contentButton">
<el-button size="small" type="primary" @click="showModel = true">回复</el-button>
</div>
</div>
<div class="contentDeit" v-for="(item, index) in ListData" :key="index">
<div class="deitleft">
<div class="media_left">
<el-image style="width: 120px; height:120px"
:src="item.fbrtx ? setAddress(item.fbrtx) : setAddress(item.hfrtx)" show-progress>
<template #error>
<div class="image-slot error">
<img src="@/assets/images/mr.png" class="user-avatar" />
</div>
</template>
</el-image>
</div>
<div class="head_name">{{ item.fbrxm ? item.fbrxm : item.hfrxm }}</div>
</div>
<div class="deitright">
<div class="deitContent">
{{ item.content ? item.content : item.hfnr }}
<template v-if="item.tp && Array.isArray(item.tp) && item.tp.length > 0">
<div style="display: flex;">
<div v-for="(img, imgIndex) in item.tp" :key="imgIndex" class="image-item">
<el-image :src="setAddress(img)" show-progress style="max-width: 250px; max-height: 400px; margin: 5px;">
<template #error>
<div class="image-slot error">
<img src="@/assets/images/default_male.png" width="80" height="80" />
</div>
</template>
</el-image>
</div>
</div>
</template>
</div>
<div class="deitTime">
<div>{{ item.time ? item.time : item.hfsj }}</div>
<div class="reply" v-if="!item.replyShow" @click="sendXhf(item)">
<span v-if="listQuery.id != item.id">({{ item.xjfhList ? item.xjfhList.length : 0 }})</span> 回复
</div>
<div class="reply" v-else @click="item.replyShow = !item.replyShow">收起回复</div>
</div>
<transition name="fade-slide">
<div class="responseContent" v-if="item.replyShow">
<div class="comment" v-for="(items, indexs) in item.xjfhList" :key="indexs">
<div class="head_img">
<el-image style="width: 100%; height:100%" :src="setAddress(items.hfrtx)" show-progress>
<template #error>
<div class="image-slot error">
<img src="@/assets/images/mr.png" class="user-avatar" />
</div>
</template>
</el-image>
</div>
<div class="commentContent">
<span class="name">{{ items.hfrxm }}</span><span v-if="items.sjhfrxm">回复:{{ items.sjhfrxm }}</span>
<div class="commentContentText">{{ items.hfnr }}</div>
<div class="clickreply">
<div>{{ items.hfsj }}</div>
<div @click="openXhf(items)">回复</div>
</div>
</div>
</div>
<div class="end">
<el-input v-model="hfContent" type="textarea" placeholder="请输入内容" class="input"></el-input>
<div class="send">
<V3Emoji :options-name="optionsName" @click-emoji="onVue3Emoje" :recent="true" style="width: 40px;">
</V3Emoji>
<el-button type="primary" @click="sendHf(item)">发送</el-button>
</div>
</div>
</div>
</transition>
</div>
</div>
</div>
<WriteBack v-model="showModel" :ItemData="listQuery" @SaveReport="getbGsxtXxltHfSelectPage" title="回复帖子"
:showCancel="false" :heightNumber="436"></WriteBack>
</template>
<script setup>
import { tbGsxtXxltHfid, tbGsxtXxltHfSave, tbGsxtXxltHfSelectList } from '@/api/tbGsxtXxltHf.js'
import V3Emoji from "vue3-emoji";
import WriteBack from './writeBack.vue'
import { setAddress } from '@/utils/tools.js'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getItem } from '@/utils/storage.js'
import { useRoute, useRouter } from 'vue-router'
import {
ref,
defineExpose,
defineProps,
defineEmits,
} from "vue";
const emit = defineEmits(["updateDate"]);
const props = defineProps({
data: {
type: Object,
default: () => ({})
}
});
const optionsName = {
'Smileys & Emotion': '笑脸&表情',
'Food & Drink': '食物&饮料',
'Animals & Nature': '动物&自然',
'Travel & Places': '旅行&地点',
'People & Body': '人物&身体',
Objects: '物品',
Symbols: '符号',
Flags: '旗帜',
Activities: '活动'
};
const dialogForm = ref(false); //弹窗
const title = ref("回复帖子");
const listQuery = ref({});
const ListData = ref([])
// 初始化数据
const init = (row) => {
dialogForm.value = true;
console.log(row);
listQuery.value = {
...row,
tp: row.tp && Array.isArray(row.tp) ? row.tp : []
}
ListData.value = [row]
getbGsxtXxltHfSelectPage()
};
const showModel = ref(false)
const getbGsxtXxltHfSelectPage = () => {
tbGsxtXxltHfid(listQuery.value.id).then(res => {
let data
if (res.replyList) {
data = res.replyList.map(item => {
return {
...item,
tp: item.hftp ? item.hftp.split(',') : []
}
})
} else {
data = []
}
ListData.value = [...[listQuery.value], ...data].map(item => {
return {
...item,
replyShow: false
}
})
})
}
const hfContent = ref('')
const onVue3Emoje = (val) => {
hfContent.value += val
}
const sfwxhf = ref(true)
const xhfMsg = ref({})
// 回复
const sendHf = async (val) => {
let item
if (sfwxhf) {
item = val
} else {
item = xhfMsg.value
}
const ltmasg = getItem("ltmasg")
// 处理hfContent.value移除@用户名:前缀
let pureContent = hfContent.value;
// 检查是否以@开头并且包含冒号
if (pureContent.startsWith('@') && pureContent.includes(':')) {
// 提取冒号后面的内容作为纯文本
const colonIndex = pureContent.indexOf(':');
if (colonIndex !== -1 && colonIndex < pureContent.length - 1) {
pureContent = pureContent.substring(colonIndex + 1).trim();
}
}
if (hfContent.value === '') {
ElMessage.warning('请输入内容');
return
}
const promes = {
hfnr: pureContent, // 只使用处理后的纯内容
hfrsfzh: ltmasg.sfzh,
hfrtx: ltmasg.tx,
hfrxm: ltmasg.xm,
ltid: listQuery.value.id,
sfyjhf: 0,
sjhfid: item.id,
sjhfrxm: sfwxhf.value ? '' : item.hfrxm
}
try {
const res = await tbGsxtXxltHfSave(promes)
if (res) {
const dataxhf = await tbGsxtXxltHfSelectList({ sjhfid: item.id })
item.xjfhList = dataxhf
hfContent.value = ''
}
} catch (error) {
console.log(error);
}
sfwxhf.value = true
}
// 小回复
const sendXhf = (item) => {
if (item.id === listQuery.value.id) {
showModel.value = true
} else {
item.replyShow = !item.replyShow
}
};
// 打开回复
const openXhf = (item) => {
hfContent.value = '@' + item.hfrxm + ':'
xhfMsg.value = item
sfwxhf.value = false
}
const route = useRoute()
const router = useRouter()
// 关闭回复
const close = () => {
if (route.query.id) {
const query = { ...route.query };
delete query.id;
router.replace({ query });
}
// 关闭对话框
dialogForm.value = false;
};
defineExpose({ init });
</script>
<style lang="scss" scoped>
.contentTitle {
display: flex;
font-size: 16px;
font-weight: bold;
padding: 10px 0;
border-bottom: 1px solid #f0f0f0;
.content {
width: 80%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.contentButton {
flex: 1;
margin-left: 20px;
}
}
.contentDeit {
display: flex;
padding: 10px 0;
border-bottom: 1px solid #f0f0f0;
.deitleft {
width: 130px;
background-color: #f6f7fb;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
padding: 10px 0;
.head_img {
width: 80px;
height: 80px;
padding: 2px;
border: 1px solid #ccc;
// margin-right: 12px;
}
.head_img img {
width: 100%;
height: 100%;
}
.head_name {
height: 20px;
line-height: 20px;
font-size: 12px;
width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #000;
}
}
.deitright {
width: calc(100% - 130px);
// background-color: aqua;
padding: 15px;
.deitContent {
line-height: 24px;
font-size: 16px;
word-wrap: break-word;
overflow: hidden;
min-height: 180px;
}
.deitTime {
// width:180px;
width: 230px;
display: flex;
align-items: center;
justify-content: space-between;
margin-left: auto;
.reply {
border-bottom: 0;
color: #1D53BF;
cursor: pointer;
display: inline-block;
height: 28px;
line-height: 28px;
text-align: center;
width: 80px;
background: #f7f8fa;
}
}
/* 过渡动画样式 */
.fade-slide-enter-active,
.fade-slide-leave-active {
transition: all 0.3s ease-in-out;
}
.fade-slide-enter-from,
.fade-slide-leave-to {
max-height: 0;
opacity: 0;
overflow: hidden;
}
.fade-slide-enter-to,
.fade-slide-leave-from {
max-height: 500px;
opacity: 1;
}
.responseContent {
line-height: 24px;
font-size: 16px;
padding: 10px;
background: #f7f8fa;
border: 1px solid #f0f1f2;
margin-top: -1px;
.comment {
display: flex;
.head_img {
float: left;
width: 40px;
height: 40px;
border: 1px #ccc solid;
padding: 1px;
margin-right: 10px;
}
.commentContent {
width: calc(100% - 50px);
font-size: 14px;
.name {
color: #1D53BF;
margin-right: 5px;
}
.clickreply {
cursor: pointer;
display: flex;
justify-content: space-between;
width: 200px;
margin-left: auto;
color: #666;
font-size: 14px;
}
}
.head_img img {
width: 100%;
height: 100%;
}
}
.end {
.send {
margin-top: 10px;
margin-right: 50px;
display: flex;
align-items: center;
justify-content: right;
// margin-top:auto;
}
}
}
}
}
</style>

View File

@ -0,0 +1,130 @@
<template>
<el-dialog v-model="dialogVisible" center width="1000px" :destroy-on-close="true" :title="title" @close="close"
:close-on-click-modal="false">
<div class="cntBox">
<FormMessage :formList="formData" v-model="listQuery" ref="elform" :rules="rules" />
</div>
<template #footer>
<el-button type="primary" :loading="loding" @click="downloadWithStyles">发布</el-button>
<el-button type="primary" @click="close">取消</el-button>
</template>
</el-dialog>
</template>
<script setup>
import "@wangeditor/editor/dist/css/style.css";
import FormMessage from "@/components/aboutTable/FormMessage.vue";
import { tbGsxtXxltSave, tbGsxtXxltUpdate } from "@/api/tbGsxtXxltHf.js";
import { ref, shallowRef, onBeforeUnmount, defineEmits, defineProps, watch, reactive, computed } from "vue";
import { getItem } from "@/utils/storage";
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
title: {
type: String,
default: "报告模板"
},
heightNumber: {
type: Number,
default: 448
},
ItemData: {
type: Object,
default: () => ({})
}
});
const emits = defineEmits(["update:modelValue", "changeFn", "SaveReport"]);
const dialogVisible = computed({
get: () => props.modelValue,
set: (value) => emits('update:modelValue', value)
});
watch(() => props.modelValue, (newVal) => {
if (newVal && props.ItemData.id) {
listQuery.value = { ...props.ItemData}
} else if (newVal) {
listQuery.value = { }
}
}, { deep: true })
const listQuery = ref({tp:[]})
const elform = ref()
const formData = ref([
{ label: "帖子标题", prop: "title", type: "input", width: '100%' },
{ label: "帖子内容", prop: "content", type: "textarea", width: '100%' },
{
label: "是否公开", prop: "fbfw", type: "select", width: '100%', options: [
{ label: '公开', value: '02' },
{ label: '不公开', value: '01' }
]
},
{ label: "图片", prop: "tp", type: "upload", width: '100%', limit: 10 },
])
const rules = reactive({
title: [
{ required: true, message: "请输入帖子标题", trigger: "blur" }
],
content: [
{ required: true, message: "请输入帖子内容", trigger: "blur" }
],
fbfw: [
{ required: true, message: "请选择是否公开", trigger: "blur" }
],
})
const loding=ref(false)
const downloadWithStyles = () => {
const ltmasg= getItem('ltmasg')
elform.value.submit((valid) => {
loding.value=true
if (valid) {
const formData = {
fbrtx: ltmasg.tx,
fbrxm: ltmasg.xm,
fbrxm: ltmasg.sfzh,
...listQuery.value,
tp: listQuery.value.tp ? listQuery.value.tp.join(',') : ''
}
if (props.ItemData.id) {
tbGsxtXxltUpdate(formData).then((res) => {
emits("SaveReport");
close();
}).finally(()=>{
loding.value=false
})
} else {
tbGsxtXxltSave(formData).then((res) => {
emits("SaveReport");
close();
}).finally(()=>{
loding.value=false
});
}
}
});
}
const close = () => {
// 重置表单数据
listQuery.value.tp = null
listQuery.value = {}
dialogVisible.value = false;
};
</script>
<style lang="scss" scoped>
.cntBox {
height: 60vh;
padding: 8px;
box-sizing: border-box;
overflow: hidden;
overflow-y: auto;
border: 1px dashed #e9e9e9;
}
::v-deep .el-form-item__label {
width: 80px !important;
}
</style>

View File

@ -0,0 +1,311 @@
<template>
<div class="app-container">
<div class="user-profile-card">
<!-- <div class="profile-header">
<h3>信息</h3>
</div> -->
<div class="profile-content">
<div class="avatar-section">
<div class="avatar-wrapper" @click="showAvatarModel = true">
<el-image style="width: 120px; height:120px" :src="setAddress(userInfo.tx)"
show-progress>
<template #error>
<div class="image-slot error">
<img src="@/assets/images/mr.png" class="user-avatar" />
</div>
</template>
</el-image>
<div class="avatar-upload-overlay">
<el-icon class="upload-icon">
<Plus />
</el-icon>
</div>
</div>
<!-- <div class="avatar-hint">点击更换头像</div> -->
</div>
<div class="info-section">
<div class="info-item" @click="showEditModel = true">
<span class="info-label">昵称</span>
<span class="info-value">{{ userInfo.nc }}</span>
</div>
<div class="info-item">
<span class="info-label">姓名</span>
<span class="info-value">{{ userInfo.xm }}</span>
</div>
<div class="info-item">
<span class="info-label">部门</span>
<span class="info-value single-line-ellipsis">{{ userInfo.deptName }}</span>
</div>
</div>
</div>
</div>
<!-- 更换头像对话框 -->
<ChangeTheAvatar v-model="showAvatarModel" title="更换头像" :heightNumber="250" @avatarUpdated="handleAvatarUpdated">
</ChangeTheAvatar>
<!-- 编辑信息对话框 -->
<el-dialog v-model="showEditModel" title="编辑昵称" width="600px" center :close-on-click-modal="false">
<el-form ref="editForm" :model="editUserInfo" label-width="100px" :rules="rules">
<el-form-item label="昵称" prop="nc">
<el-input v-model="userInfo.nc" placeholder="请输入昵称"></el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showEditModel = false">取消</el-button>
<el-button type="primary" @click="handleSave">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, watch } from "vue";
import { ElMessage } from 'element-plus';
import { Plus } from '@element-plus/icons-vue';
import ChangeTheAvatar from './changeTheAvatar.vue';
import { tbGsxtXxltTxTxQueryBySfzh, tbGsxtXxltTxTxSave } from '@/api/tbGsxtXxltHf.js'
import { getItem,setItem ,removeItem} from '@/utils/storage.js'
import { setAddress } from '@/utils/tools'
// 控制显示状态的变量
const showAvatarModel = ref(false);
const showEditModel = ref(false);
// 用户信息
const userInfo = ref({});
// 编辑表单数据
const editUserInfo = reactive({});
// 表单验证规则
const rules = {
nc: [
{ required: true, message: '请输入昵称', trigger: 'blur' },
{ min: 2, max: 20, message: '昵称长度在 2 到 20 个字符', trigger: 'blur' }
]
};
// 编辑表单引用
const editForm = ref();
// 监听编辑对话框显示,复制用户信息到编辑表单
watch(() => showEditModel.value, (newVal) => {
if (newVal) {
// 深拷贝用户信息到编辑表单
Object.assign(editUserInfo, userInfo.value);
}
});
// 处理头像更新
const handleAvatarUpdated = (newAvatar) => {
userInfo.value.tx = newAvatar
tbGsxtXxltTxTxSave(userInfo.value).then(res => {
removeItem('ltmasg')
gettbGsxtXxltTxTxQueryBySfzh()
ElMessage({ message: '头像更新成功', type: 'success' });
})
};
// 处理保存编辑信息
const handleSave = () => {
editForm.value.validate((valid) => {
if (valid) {
showEditModel.value = false;
tbGsxtXxltTxTxSave(userInfo.value).then(res => {
removeItem('ltmasg')
gettbGsxtXxltTxTxQueryBySfzh()
ElMessage({ message: '信息保存成功', type: 'success' });
})
}
});
};
const gettbGsxtXxltTxTxQueryBySfzh = () => {
const sfzh = getItem('idEntityCard')
const ltmasg= getItem('ltmasg')
if (!ltmasg) {
tbGsxtXxltTxTxQueryBySfzh({
sfzh: sfzh
}).then(res => {
const deptId = getItem('deptId')[0]
userInfo.value = { ...res, deptName: deptId.deptName }
setItem('ltmasg', userInfo.value)
})
} else { userInfo.value = ltmasg
}
}
gettbGsxtXxltTxTxQueryBySfzh()
</script>
<style scoped lang="scss">
.app-container {
// height: 100%;
// padding: 20px;
background-color: #f6f7fb;
display: flex;
justify-content: center;
align-items: center;
}
.user-profile-card {
width: 100%;
max-width: 600px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 24px;
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.15);
}
.profile-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #e8e8e8;
h3 {
margin: 0;
font-size: 18px;
color: #303133;
}
}
.profile-content {
display: flex;
flex-direction: column;
align-items: center;
}
.avatar-section {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 24px;
.avatar-wrapper {
width: 120px;
height: 120px;
border-radius: 50%;
overflow: hidden;
cursor: pointer;
position: relative;
border: 3px solid #e8e8e8;
transition: border-color 0.3s ease;
&:hover {
border-color: #409eff;
}
.user-avatar {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-upload-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.3s ease;
color: white;
&:hover {
opacity: 1;
}
.upload-icon {
font-size: 24px;
}
}
}
.avatar-hint {
margin-top: 8px;
font-size: 12px;
color: #909399;
}
}
.info-section {
width: 100%;
.info-item {
display: flex;
align-items: center;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.info-label {
// width: 100px;
font-size: 14px;
color: #606266;
font-weight: 500;
}
.info-value {
flex: 1;
font-size: 14px;
color: #303133;
word-break: break-all;
}
.single-line-ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.app-container {
padding: 10px;
}
.user-profile-card {
padding: 16px;
}
.avatar-section .avatar-wrapper {
width: 100px;
height: 100px;
}
.info-section .info-item {
flex-direction: column;
align-items: flex-start;
.info-label {
width: auto;
margin-bottom: 4px;
}
}
}
</style>

View File

@ -0,0 +1,460 @@
<template>
<div class="app-container" v-show="cs">
<div class="publish-section"><el-button type="primary" @click="handleEdit()">发布帖子</el-button></div>
<div v-if="list.length > 0" v-infinite-scroll="load">
<div class="post-card" v-for="(item, index) in list" :key="index">
<div class="post-content" @click="handleOpen(item)" @mouseenter="showActions[index] = true"
@mouseleave="showActions[index] = false">
<div class="post-header">
<div class="post-title">
{{ item.title || '无标题' }}
</div>
<div class="post-meta">
<div class="post-time" style="margin-right: 20px;" title="发布人">{{ item.fbrxm || '暂无发布人' }}</div>
<div class="post-time" title="发布时间">{{ item.time || '暂无时间' }}</div>
</div>
</div>
<div class="post-body">
<div class="post-text">
{{ item.content }}
</div>
<div class="post-images">
<div class="image-list">
<div v-for="(img, imgIndex) in item.tp" :key="imgIndex" class="image-item" v-if="item.tp && item.tp.length > 0">
<el-image :src="setAddress(img)" show-progress>
<template #error>
<div class="image-slot error">
<img src="@/assets/images/default_male.png" width="80" height="80" />
</div>
</template>
</el-image>
</div>
</div>
</div>
</div>
</div>
<div style="display: flex;justify-content: flex-end;margin-bottom: 10px;" v-if="sfzh == item.fbrsfzh">
<el-button type="text" size="small" class="action-btn edit-btn" @click.stop="handleEdit(item)">
<el-icon>
<Edit />
</el-icon> 修改
</el-button>
<el-button type="text" size="small" class="action-btn delete-btn" @click.stop="handleDelete(item)">
<el-icon>
<Delete />
</el-icon> 删除
</el-button>
</div>
</div>
</div>
<!-- 空数据占位 -->
<div v-else class="empty-data">
<div class="empty-icon">
<el-icon size="64">
<Document />
</el-icon>
</div>
<div class="empty-text">暂无帖子数据</div>
<div class="empty-hint">点击上方按钮发布第一条帖子吧</div>
</div>
</div>
<Release v-model="showModel" :ItemData="ItemData" @SaveReport="SaveReport" title="帖子发布" :showCancel="false"
:heightNumber="436"></Release>
<Particulars ref="particulars"/>
</template>
<script setup>
import Release from './release.vue'
import { setAddress } from '@/utils/tools'
import { ref, reactive, onMounted } from 'vue'
import { tbGsxtXxltSelectPage, tbGsxtXxltDelete ,tbGsxtXxltHfid} from '@/api/tbGsxtXxltHf'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Edit, Delete, Document } from '@element-plus/icons-vue'
import { getItem } from '@/utils/storage.js'
import Particulars from "./particulars.vue";
import { useRouter, useRoute } from 'vue-router'
const route = useRoute()
const particulars = ref()
const showModel = ref(false)
const listQuery = reactive({
pageCurrent: 1,
pageSize: 10,
})
const total = ref(0)
const list = ref([])
const showActions = ref({})
const sfzh = ref()
const ItemData = ref()
const cs=ref(false)//需要删除
onMounted(() => {
sfzh.value = getItem('idEntityCard')
SaveReport()
if (route.query.id) {
tbGsxtXxltHfid(route.query.id).then(res => {
ItemData.value = res
particulars.value.init(ItemData.value)
cs.value=true
})
} else {
cs.value=true
}
})
const SaveReport = () => {
tbGsxtXxltSelectPage(listQuery).then(res => {
// 使用可选链操作符和箭头函数隐式返回优化代码
const data = (res.records || []).map(item => ({
...item,
tp: item.tp ? item.tp.split(',') : []
}))
total.value = res.total || 0
// 初始化所有操作按钮为隐藏状态
const actions = {}
list.value = data
list.value.forEach((_, index) => {
actions[index] = false
})
showActions.value = actions
})
}
const handleOpen = (item) => {
particulars.value.init(item);
}
// 修改帖子
const handleEdit = (item) => {
ItemData.value = item
// 这里可以添加实际的修改逻辑
showModel.value = true
}
// 删除帖子
const handleDelete = (item) => {
ElMessageBox.confirm(
`确定要删除帖子「${item.title}」吗?`,
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
tbGsxtXxltDelete(item.id).then(res => {
if (res) {
ElMessage({ message: `帖子「${item.title}」已删除`, type: 'success' })
SaveReport() // 删除后刷新列表
} else {
ElMessage({ message: `删除帖子「${item.title}」失败:${res.msg || '未知错误'}`, type: 'error' })
}
})
}).catch(() => {
ElMessage({ message: '已取消删除', type: 'info' })
})
}
const load = () => {
if (listQuery.pageCurrent * listQuery.pageSize < total.value) {
listQuery.pageCurrent++
SaveReport()
}
}
</script>
<style scoped lang="scss">
// 主容器样式
.app-container {
padding: 16px;
color: #333;
font-size: 14px;
line-height: 1.5;
background-color: #f5f7fa;
min-height: 100%;
}
// 发布按钮区域
.publish-section {
margin-bottom: 20px;
display: flex;
justify-content: flex-end;
}
// 帖子卡片样式
.post-card {
background: white;
border-radius: 8px;
margin-bottom: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
}
// 帖子内容区域
.post-content {
padding: 16px;
cursor: pointer;
}
// 帖子头部
.post-header {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
align-items: flex-start;
}
// 帖子标题
.post-title {
font-size: 18px;
font-weight: 600;
color: #409eff;
margin-right: 16px;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: color 0.3s ease;
&:hover {
color: #409eff;
}
}
// 帖子元信息
.post-meta {
flex-shrink: 0;
display: flex;
justify-content: space-between;
}
// 发布时间
.post-time {
color: #909399;
font-size: 12px;
white-space: nowrap;
}
// 帖子主体
.post-body {
position: relative;
}
// 帖子文本内容
.post-text {
color: #606266;
font-size: 14px;
line-height: 1.6;
margin-bottom: 12px;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
// 帖子图片容器
.post-images {
margin-top: 8px;
}
// 图片列表
.image-list {
display: flex;
flex-wrap: wrap;
margin: -4px;
}
// 图片项
.image-item {
margin: 4px;
width: 100px;
height: 160px;
position: relative;
overflow: hidden;
border-radius: 4px;
flex-shrink: 0;
}
// 帖子图片
.post-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
&:hover {
transform: scale(1.05);
}
}
// 更多图片提示
.image-more {
width: 80px;
height: 80px;
background: rgba(0, 0, 0, 0.6);
color: white;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
font-size: 12px;
margin: 4px;
}
// 操作按钮容器
.post-actions {
position: absolute;
right: 16px;
bottom: 16px;
background: white;
padding: 8px;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
z-index: 10;
display: flex;
gap: 8px;
transform: translateY(10px);
}
// 操作按钮显示状态
.post-actions-visible {
opacity: 1;
visibility: visible;
}
// 操作按钮基础样式
.action-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 12px;
border-radius: 4px;
transition: all 0.3s ease;
font-size: 12px;
color: #606266;
background: #f5f7fa;
&:hover {
transform: translateY(-1px);
}
}
// 修改按钮样式
.edit-btn:hover {
color: #409eff;
background: #ecf5ff;
}
// 删除按钮样式
.delete-btn:hover {
color: #f56c6c;
background: #fef0f0;
}
// 空数据样式
.empty-data {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
background: white;
border-radius: 8px;
color: #909399;
text-align: center;
margin-top: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
min-height: 300px;
}
.empty-icon {
margin-bottom: 16px;
color: #c0c4cc;
}
.empty-text {
font-size: 16px;
margin-bottom: 8px;
color: #606266;
}
.empty-hint {
font-size: 14px;
color: #909399;
}
// 移动端适配
@media (max-width: 768px) {
.app-container {
padding: 12px;
}
.post-card {
margin-bottom: 12px;
}
.post-content {
padding: 12px;
}
.post-title {
font-size: 16px;
}
.post-text {
font-size: 13px;
-webkit-line-clamp: 2;
}
.image-item {
width: 60px;
height: 60px;
}
.image-more {
width: 60px;
height: 60px;
}
.post-actions {
right: 12px;
padding: 6px;
}
.action-btn {
padding: 3px 8px;
font-size: 11px;
}
.empty-data {
padding: 40px 16px;
min-height: 250px;
}
.empty-icon {
font-size: 48px;
}
.empty-text {
font-size: 14px;
}
.empty-hint {
font-size: 12px;
}
}
</style>

View File

@ -0,0 +1,134 @@
<template>
<el-dialog v-model="dialogVisible" center width="1000px" :destroy-on-close="true" :title="title" @close="close"
:close-on-click-modal="false">
<div class="cntBox">
<FormMessage :formList="formData" v-model="hfrsfzh" ref="elform" :rules="rules" >
<template #content>
<el-input type="textarea" placeholder="请输入内容" v-model="hfrsfzh.hfnr"></el-input>
<div style="width: 100%;border-bottom: 1px solid #eee;margin-top: 10px">
<V3Emoji :options-name="optionsName" width="40px" title="表情" @click-emoji="onVue3Emoje"
:recent="true" ></V3Emoji>
</div>
</template>
</FormMessage>
</div>
<template #footer>
<el-button type="primary" @click="downloadWithStyles" :loading="loading">发布</el-button>
<el-button type="primary" @click="close">取消</el-button>
</template>
</el-dialog>
</template>
<script setup>
import "@wangeditor/editor/dist/css/style.css";
import V3Emoji from "vue3-emoji";
import FormMessage from "@/components/aboutTable/FormMessage.vue";
import { getItem } from '@/utils/storage.js'
import { tbGsxtXxltHfSave } from '@/api/tbGsxtXxltHf'
import { ElMessage } from 'element-plus';
import { ref, defineEmits, defineProps, watch, reactive ,computed} from "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
},
title: {
type: String,
default: "回复帖子"
},
heightNumber: {
type: Number,
default: 448
},ItemData: {
type: Object,
default: () => {}
}
});
const emits = defineEmits(["changeFn", "update:modelValue", "SaveReport"]);
const hfrsfzh = ref({hftp:[]})
watch(() => props.ItemData, (newVal) => {
if (newVal) {
listQuery.value = {
...newVal,
hftp:newVal.hftp?newVal.hftp.split(','):[]
}
console.log(listQuery.value);
}
}, { deep: true })
const listQuery = ref({hftp:[]})
const elform = ref()
const formData = ref([
{ label: "帖子内容", prop: "content", type: "slot", width: '100%' },
{ label: "图片", prop: "hftp", type: "upload", width: '100%', limit: 10 },
])
const loading = ref(false)
const rules = reactive({
hfnr: [
{ required: true, message: "请输入帖子内容", trigger: "blur" }
]
})
const onVue3Emoje = (val) => {
hfrsfzh.value.hfnr += val
}
const downloadWithStyles = () => {
const ltmasg = getItem('ltmasg')
const params = {
hfnr: hfrsfzh.value.hfnr,
hfrsfzh: ltmasg.sfzh,
hfrtx: ltmasg.tx,
hfrxm: ltmasg.xm,
ltid: listQuery.value.id,
sfyjhf: "1",
hftp:hfrsfzh.value.hftp?hfrsfzh.value.hftp.join(','):''
}
loading.value = true
tbGsxtXxltHfSave(params).then(res => {
close()
emits("SaveReport", res);
ElMessage.success('回复成功');
}).finally(()=>{
loading.value = false
})
}
const dialogVisible = computed({
get: () => props.modelValue,
set: (value) => emits('update:modelValue', value)
});
const close = () => {
hfrsfzh.value.hftp = []
hfrsfzh.value = {}
dialogVisible.value = false
// emits("update:modelValue", false);
}
</script>
<style lang="scss" scoped>
.cntBox {
height: 60vh;
padding: 8px;
box-sizing: border-box;
overflow: hidden;
overflow-y: auto;
border: 1px dashed #e9e9e9;
}
::v-deep .el-form-item__label {
width: 80px !important;
}
</style>

View File

@ -0,0 +1,40 @@
<template>
<div>
<div class="titleBox">
<PageTitle title="情报论坛"> </PageTitle>
</div>
<div class=" bilateral">
<div class="the-left"> <TheLeft/></div>
<div class="the-right"> <TheRight @handleOpen="handleOpen"/></div>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import PageTitle from "@/components/aboutTable/PageTitle.vue";
import TheLeft from "./components/theLeft.vue";
import TheRight from "./components/theRight.vue";
</script>
<style scoped lang="scss">
.bilateral{
background-color: #fff;
height: calc(100vh - 190px);
display: flex;
padding: 10px;
color: #000;
justify-content: space-between;
.the-left{
width: 210px;
margin-right: 15px;
}
.the-right{
flex: 1;
overflow: auto;
}
}
</style>