This commit is contained in:
lcw
2026-03-29 19:46:50 +08:00
parent 60de16032f
commit af838854fa
49 changed files with 9947 additions and 1512 deletions

BIN
gsxt.zip Normal file

Binary file not shown.

View File

@ -7,9 +7,17 @@
@close="closed"
>
<div>
<div class="flex" style="margin-bottom: 10px;">
<el-button :type="bqLb === '01' ? 'success' : 'info'" @click="qihuan('01')">标签大类</el-button>
<el-button :type="bqLb === '02' ? 'success' : 'info'" @click="qihuan('02')"> 标签小类 </el-button>
<div class="mark-tabs">
<span class="label">标签类型</span>
<el-radio-group v-model="bqLx" @change="changeBqLx">
<el-radio-button label="02">行为标签</el-radio-button>
<el-radio-button label="01">身份标签</el-radio-button>
</el-radio-group>
<span class="label" style="margin-left: 20px;">类别</span>
<el-radio-group v-model="bqLb" @change="qihuan">
<el-radio-button label="01">大类</el-radio-button>
<el-radio-button label="02">小类</el-radio-button>
</el-radio-group>
</div>
<el-form :model="listQuery" class="mosty-from-wrap" :inline="true">
<el-form-item label="标签名称">
@ -83,7 +91,7 @@
<script setup>
import { qcckGet } from "@/api/qcckApi.js";
import { defineProps, ref, getCurrentInstance, watch } from "vue";
import { defineProps, ref, getCurrentInstance, watch, nextTick } from "vue";
const { proxy } = getCurrentInstance();
const { D_GS_BQ_DJ, D_GS_SSYJ } = proxy.$dict("D_GS_BQ_DJ", "D_GS_SSYJ"); //获取字典数据
const props = defineProps({
@ -107,6 +115,11 @@ const props = defineProps({
roleIds: {
type: Array,
default: []
},
// 标签类型02-行为标签01-身份标签
bqLx: {
type: String,
default: "02"
}
});
const loading = ref(false);
@ -120,6 +133,8 @@ const keyVal = ref();
const multipleUserRef = ref(null);
const multipleSelectionUser = ref([]);
const tableData = ref([]);
const bqLx = ref(props.bqLx);
const bqLb = ref("01");
const emits = defineEmits(["update:modelValue", "choosed"]);
const keyid = (row) => {
return row.id;
@ -148,9 +163,9 @@ const onComfirm = () => {
closed();
};
const qihuan = (val) => {
bqLb.value = val
getListData()
}
listQuery.value.pageCurrent = 1;
getListData();
};
/**
* pageSize 改变触发
*/
@ -168,7 +183,7 @@ const handleCurrentChange = (currentPage) => {
const getListData = () => {
keyVal.value++;
loading.value = true;
const params = { ...listQuery.value, bqLx: "02",bqLb:bqLb.value };
const params = { ...listQuery.value, bqLx: bqLx.value, bqLb: bqLb.value };
qcckGet(params, "/mosty-gsxt/tbGsxtBqgl/selectPage")
.then((res) => {
loading.value = false;
@ -183,6 +198,7 @@ const getListData = () => {
//列表回显 - 优化版,确保已选择数据正确回显
function multipleUser() {
nextTick(() => {
if (!multipleUserRef.value || !tableData.value || tableData.value.length === 0) {
return;
}
@ -200,6 +216,7 @@ function multipleUser() {
}
});
}
});
}
const handleFilter = () => {
@ -218,7 +235,11 @@ const handleSelectionChange = (val) => {
multipleSelectionUser.value = val;
}
};
const bqLb=ref('01')
const changeBqLx = (val) => {
listQuery.value.pageCurrent = 1;
multipleUserRef.value?.clearSelection();
getListData();
};
// 监听弹窗打开状态,打开时重新加载数据
watch(
() => props.modelValue,
@ -233,19 +254,40 @@ watch(
// 监听roleIds变化确保数据回显正确
watch(
() => props.roleIds,
(newRoleIds) => {
// 使用setTimeout确保在表格数据加载完成后再进行选择
setTimeout(() => {
() => {
nextTick(() => {
multipleUser();
}, 100);
});
},
{ deep: true }
);
// 监听外部传入的 bqLx 变化
watch(
() => props.bqLx,
(newVal) => {
bqLx.value = newVal;
}
);
</script>
<style lang="scss" scoped>
@import "@/assets/css/layout.scss";
@import "@/assets/css/element-plus.scss";
.mark-tabs {
display: flex;
align-items: center;
margin-bottom: 15px;
padding: 10px 15px;
background: #f5f7fa;
border-radius: 6px;
.label {
font-weight: 500;
color: #606266;
}
}
</style>
<style>
.tabBoxRadio .el-checkbox__inner {

View File

@ -1,5 +1,9 @@
<template>
<div class="form-item-box" :class="props.showBtn ? 'showBtn-upload' : ''" :style="{ width: width }">
<div
class="form-item-box"
:class="props.showBtn ? 'showBtn-upload' : ''"
:style="{ width: width }"
>
<el-upload
v-bind="$attrs"
:headers="headers"
@ -13,19 +17,33 @@
:before-remove="beforeRemove"
:on-exceed="handleExceed"
:on-success="handlerSuccess"
:before-upload="beforeImgUpload">
:before-upload="beforeImgUpload"
>
<template #default>
<el-button v-if="props.showBtn" size="small" type="primary">上传文件</el-button>
<el-button v-if="props.showBtn" size="small" type="primary"
>上传文件</el-button
>
<el-icon v-else><Plus /></el-icon>
</template>
<template #file="{ file }" v-if="!props.showBtn">
<div v-if="props.isImg">
<img class="el-upload-list__item-thumbnail" :src="file.url || ''" alt="" />
<img
class="el-upload-list__item-thumbnail"
:src="file.url || ''"
alt=""
/>
<span class="el-upload-list__item-actions">
<span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
<span
class="el-upload-list__item-preview"
@click="handlePictureCardPreview(file)"
>
<el-icon> <zoom-in /></el-icon>
</span>
<span v-if="!disabled" class="el-upload-list__item-delete" @click="handleRemove(file, fileList)">
<span
v-if="!disabled"
class="el-upload-list__item-delete"
@click="handleRemove(file, fileList)"
>
<el-icon><Delete /></el-icon>
</span>
</span>
@ -36,10 +54,18 @@
<span class="file-name">{{ file.name }}</span>
</div>
<span class="el-upload-list__item-actions">
<span v-if="!disabled" class="el-upload-list__item-delete" @click="handleDownload(file)">
<span
v-if="!disabled"
class="el-upload-list__item-delete"
@click="handleDownload(file)"
>
<el-icon><Download /></el-icon>
</span>
<span v-if="!disabled" class="el-upload-list__item-delete" @click="handleRemove(file, fileList)">
<span
v-if="!disabled"
class="el-upload-list__item-delete"
@click="handleRemove(file, fileList)"
>
<el-icon><Delete /></el-icon>
</span>
</span>
@ -54,7 +80,15 @@
<script setup>
import { COMPONENT_WIDTH } from "@/constant";
import { ref, defineProps, defineEmits, computed, watch, onMounted, onUnmounted } from "vue";
import {
ref,
defineProps,
defineEmits,
computed,
watch,
onMounted,
onUnmounted
} from "vue";
import { ElMessage } from "element-plus";
import { useStore } from "vuex";
const props = defineProps({
@ -98,35 +132,41 @@ const headers = ref({
});
const fileList = ref([]);
watch(() => props.modelValue,(val) => {
watch(
() => props.modelValue,
(val) => {
let arr = val ? (Array.isArray(val) ? val : [val]) : [];
if(arr.length == 0 ) return fileList.value = [];
if (arr.length == 0) return (fileList.value = []);
fileList.value = arr.map((el) => {
if (Object.prototype.toString.call(el) === "[object Object]") {
// 确保file.url始终是字符串URL
const fileUrl = props.isAll ? `/mosty-api/mosty-base/minio/image/download/` + el.id : el.url;
const fileUrl = props.isAll
? `/mosty-api/mosty-base/minio/image/download/` + el.id
: el.url;
return {
...el,
url: String(fileUrl || ''),
name: el.name || '',
url: String(fileUrl || ""),
name: el.name || ""
};
} else {
return {
url: String(`/mosty-api/mosty-base/minio/image/download/` + el || ''),
url: String(`/mosty-api/mosty-base/minio/image/download/` + el || ""),
id: el
};
}
});
console.log(fileList.value, "fileList.value");
},{ immediate: true,deep:true });
},
{ immediate: true, deep: true }
);
const actionUrl = computed(() => {
if (props.isAll) {
return "/mosty-api/mosty-base/minio/image/upload/id";
} else {
return props.isImg ? "/mosty-api/mosty-base/minio/image/upload/id": "/mosty-api/mosty-base/minio/file/uploadObj";
return props.isImg
? "/mosty-api/mosty-base/minio/image/upload/id"
: "/mosty-api/mosty-base/minio/file/uploadObj";
}
});
@ -158,7 +198,17 @@ const getSuffix = (fileName) => {
//pdf
if (suffix === "pdf") return "PDF";
//视频 音频
var videolist = ["mp4","m2v","mkv","rmvb","wmv","avi","flv","mov","m4v"];
var videolist = [
"mp4",
"m2v",
"mkv",
"rmvb",
"wmv",
"avi",
"flv",
"mov",
"m4v"
];
if (videolist.includes(suffix)) return "VIDEO";
var musiclist = ["mp3", "wav", "wmv"];
if (musiclist.includes(suffix)) return "MUSIC";
@ -177,19 +227,19 @@ const handlerSuccess = (res, file) => {
file.url = `/mosty-api/mosty-base/minio/image/download/` + res.data;
file.id = res.data;
fileList.value.push(file);
let arr = []
let arr = [];
if (props.isImg) {
arr = fileList.value.map((el) => el.id)
arr = fileList.value.map((el) => el.id);
} else {
console.log(fileList, "测试");
arr = fileList.value.map((el) => {
console.log(el,'xunhuan');
console.log(el, "xunhuan");
return {
id: el.id, name: el.name
}
})
id: el.id,
name: el.name
};
});
console.log(arr, "测试2222");
}
emits("update:modelValue", arr);
};
@ -212,7 +262,7 @@ const beforeImgUpload = (file) => {
};
//查询图片
const handlePictureCardPreview = (file) => {
dialogImageUrl.value = file.url || '';
dialogImageUrl.value = file.url || "";
dialogVisible.value = true;
};
function downloadFile(url, filename) {
@ -247,7 +297,7 @@ const beforeRemove = (file) => {
});
props.modelValue.splice(index, 1);
emits("update:modelValue", props.modelValue);
}
};
const handleRemove = (file) => {
let index = fileList.value.findIndex(function (item) {
@ -257,7 +307,6 @@ const handleRemove = (file) => {
props.modelValue.splice(index, 1);
emits("update:modelValue", props.modelValue);
};
</script>
<style lang="scss" scoped>

View File

@ -1,11 +1,11 @@
import { ref, toRefs, isRef } from 'vue';
import { getSysDictByCode, fzdict } from '@/api/sysDict' //引入封装数字字典接口
import { ref, toRefs, isRef } from "vue";
import { getSysDictByCode, fzdict } from "@/api/sysDict"; //引入封装数字字典接口
import { getLocalDic } from "@/utils/localDic/index.js"
import { getLocalDic } from "@/utils/localDic/index.js";
/**
* 获取字典数据
*/
let list = []
let list = [];
/** 是否取本地字典 (需要本地加载就加这里,不需要就删除) */
export function isLocalDict(dictCode) {
let localDicObj = {
@ -15,9 +15,9 @@ export function isLocalDict(dictCode) {
D_GS_SSYJ: true, // "岗哨系统四色预警"
D_BZ_SF: true, // "是否"
BD_BK_CLYJBQ: true, // "车辆预警标签"
D_YJXX_CZCSLX: true, //常控处置措施类型
}
return localDicObj[dictCode]
D_YJXX_CZCSLX: true //常控处置措施类型
};
return localDicObj[dictCode];
}
export function getDict(...args) {
const res = ref({});
@ -26,32 +26,32 @@ export function getDict(...args) {
res.value[d] = [];
// 本地字典拦截,如果本地字典存在,则使用本地字典,否则使用远程字典
if (isLocalDict(d) && getLocalDic(d)) {
res.value[d] = getLocalDic(d)
res.value[d] = getLocalDic(d);
} else {
getSysDictByCode({
dictCode: d
}).then(result => {
result = result || {}
result.itemList = Array.isArray(result.itemList) ? result.itemList : []
result.itemList.forEach(p => {
p.label = p.zdmc
p.value = p.dm
p.id = p.dm
p.elTagType = p.dictType
}).then((result) => {
result = result || {};
result.itemList = Array.isArray(result.itemList)
? result.itemList
: [];
result.itemList.forEach((p) => {
p.label = p.zdmc;
p.value = p.dm;
p.id = p.dm;
p.elTagType = p.dictType;
if (p?.itemList && p.itemList?.length > 0) {
getChildren(p)
getChildren(p);
}
p.children = p.itemList
})
res.value[d] = result.itemList
p.children = p.itemList;
});
res.value[d] = result.itemList;
//
})
});
}
})
});
return toRefs(res.value);
})()
})();
}
export function getFzDict(...args) {
const res = ref({});
@ -64,7 +64,7 @@ export function getFzDict(...args) {
} else {
fzdict({
dictLabel: d
}).then(result => {
}).then((result) => {
result = result || {};
// result.itemList = Array.isArray(result.itemList) ? result.itemList : [];
// result.itemList.forEach(p => {
@ -79,26 +79,25 @@ export function getFzDict(...args) {
// });
// console.log(res.value);
res.value[d] = result
res.value[d] = result;
});
}
});
// 使用toRefs确保返回的是响应式对象
return toRefs(res.value);
})()
})();
}
export function getChildren(item) {
item.label = item.zdmc
item.value = item.dm
item.id = item.dm
item.label = item.zdmc;
item.value = item.dm;
item.id = item.dm;
if (item.itemList && item.itemList.length > 0) {
item.itemList.forEach(v => {
getChildren(v)
})
item.itemList.forEach((v) => {
getChildren(v);
});
}
item.children = item.itemList
item.children = item.itemList;
}
/**
* 设置级联选择器回显
@ -106,17 +105,17 @@ export function getChildren(item) {
* @param {*} array 级联数据树
* @param {*} childDeptList 子集变量
*/
export function setCascader(id, array, childDeptList = 'childDeptList', fun) {
export function setCascader(id, array, childDeptList = "childDeptList", fun) {
if (array) {
array.forEach(item => {
array.forEach((item) => {
if (item.childDeptList && item.id != id) {
setCascader(id, item.childDeptList, childDeptList, fun)
setCascader(id, item.childDeptList, childDeptList, fun);
} else if (item.childDeptList && item.id == id) {
fun(item)
fun(item);
} else if (!item.childDeptList && item.id == id) {
fun(item)
fun(item);
}
})
});
}
}
/**
@ -127,58 +126,64 @@ export function setCascader(id, array, childDeptList = 'childDeptList', fun) {
*/
export function IdCard(IdCard, type) {
let user = {
birthday: '',
sex: '',
age: ''
}
if (type === 1 || type == 'all') {
birthday: "",
sex: "",
age: ""
};
if (type === 1 || type == "all") {
//获取出生日期
let birthday = IdCard.substring(6, 10) + "-" + IdCard.substring(10, 12) + "-" + IdCard.substring(12, 14)
if (type == 'all') {
user.birthday = birthday
let birthday =
IdCard.substring(6, 10) +
"-" +
IdCard.substring(10, 12) +
"-" +
IdCard.substring(12, 14);
if (type == "all") {
user.birthday = birthday;
} else {
return birthday
return birthday;
}
}
if (type === 2 || type == 'all') {
if (type === 2 || type == "all") {
//获取性别
if (parseInt(IdCard.substr(16, 1)) % 2 === 1) {
if (type == 'all') {
user.sex = '男'
if (type == "all") {
user.sex = "男";
} else {
return "男"
return "男";
}
} else {
if (type == 'all') {
user.sex = '女'
if (type == "all") {
user.sex = "女";
} else {
return "女"
return "女";
}
}
}
if (type === 3 || type == 'all') {
if (type === 3 || type == "all") {
//获取年龄
var ageDate = new Date()
var month = ageDate.getMonth() + 1
var day = ageDate.getDate()
var age = ageDate.getFullYear() - IdCard.substring(6, 10) - 1
if (IdCard.substring(10, 12) < month || IdCard.substring(10, 12) === month && IdCard.substring(12, 14) <= day) {
age++
var ageDate = new Date();
var month = ageDate.getMonth() + 1;
var day = ageDate.getDate();
var age = ageDate.getFullYear() - IdCard.substring(6, 10) - 1;
if (
IdCard.substring(10, 12) < month ||
(IdCard.substring(10, 12) === month && IdCard.substring(12, 14) <= day)
) {
age++;
}
if (age <= 0) {
age = 1
age = 1;
}
if (type == 'all') {
user.age = age
if (type == "all") {
user.age = age;
} else {
return age
return age;
}
}
return user
return user;
}
/**
*翻译字典数据
* @export
@ -186,14 +191,14 @@ export function IdCard(IdCard, type) {
* @param {*} array
*/
export function getDictValue(dm, array) {
let item = array.value.find(item => {
let item = array.value.find((item) => {
if (item.value) {
return item.value == dm;
} else if (item.dm) {
return item.dm == dm;
}
})
return item ? item.label : ""
});
return item ? item.label : "";
}
/** 获取多个字典值(一个值也可以) 字典内容 value-label
@ -201,15 +206,15 @@ export function getDictValue(dm, array) {
* @param {Array} dict 字典内容
*/
export function getMultiDictVal(values, dict) {
if (typeof values === 'string' && values?.length) values = values.split(',')
if (!Array.isArray(values)) return ''
if (isRef(dict)) dict = dict.value
if (!Array.isArray(dict)) return ''
if (typeof values === "string" && values?.length) values = values.split(",");
if (!Array.isArray(values)) return "";
if (isRef(dict)) dict = dict.value;
if (!Array.isArray(dict)) return "";
return values.map(v => {
const item = dict.find(item => item.value === v);
return values
.map((v) => {
const item = dict.find((item) => item.value === v);
return item ? item.label : v;
}).join(',');
})
.join(",");
}

View File

@ -13,13 +13,24 @@
</div>
<!-- 表格 -->
<div class="margTop" style="padding: 0;">
<WarnDataTable :data="pageData.tableData" :columns="pageData.tableColumn" :tableHeight="pageData.tableHeight"
:loading="pageData.tableConfiger.loading" @selection-change="chooseData">
<div class="margTop" style="padding: 0">
<WarnDataTable
:data="pageData.tableData"
:columns="pageData.tableColumn"
:tableHeight="pageData.tableHeight"
:loading="pageData.tableConfiger.loading"
@selection-change="chooseData"
>
<template #bqList="{ row }">
<ul>
<li class="one_text_detail marks mb4" :key="index" v-for="(item, index) in row.bqList">{{ item.bqMc }}({{
item.bqFz || 0 }} ) </li>
<li
:style="{ background: Bqys(item.bqYs) }"
class="one_text_detail marks mb4"
:key="index"
v-for="(item, index) in row.bqList"
>
{{ item.bqMc }}({{ item.bqFz || 0 }} )
</li>
</ul>
</template>
<template #ryXb="{ row }">
@ -48,35 +59,93 @@
</template>
<template #xtSjzt="{ row }">
<div> {{ row.xtSjzt == 0 ? "注销" : row.xtSjzt == 1 ? "正常" : "封存" }}</div>
<div>
{{ row.xtSjzt == 0 ? "注销" : row.xtSjzt == 1 ? "正常" : "封存" }}
</div>
</template>
<!-- 操作 -->
<template #controls="{ row }">
<!-- <el-link size="small" type="success" @click="handleremove(row.id)">移至关注库</el-link> -->
<el-link size="small" type="success" @click="handleMoveToFocus(row.id)">移入关注库</el-link>
<el-link size="small" type="success" @click="handleremove(row.id)">移入基础库</el-link>
<el-link size="small" type="success" v-if="row.zdrZt == '01' || row.zdrZt == '03'"
@click="handleSend(row.id)">送审</el-link>
<el-link size="small" type="primary" v-if="row.zdrZt == '01' || row.zdrZt == '03'"
@click="addEdit('edit', row)">编辑</el-link>
<el-link size="small" type="primary" @click="addEdit('detail', row)">详情</el-link>
<el-link size="small" type="danger" @click="deleteRow(row.id)">删除</el-link>
<el-link
size="small"
type="success"
@click="handleMoveToFocus(row.id)"
>移入关注库</el-link
>
<el-link size="small" type="success" @click="handleremove(row.id)"
>移入基础库</el-link
>
<el-link
size="small"
type="success"
v-if="row.zdrZt == '01' || row.zdrZt == '03'"
@click="handleSend(row.id)"
>送审</el-link
>
<el-link
size="small"
type="primary"
v-if="row.zdrZt == '01' || row.zdrZt == '03'"
@click="addEdit('edit', row)"
>编辑</el-link
>
<el-link size="small" type="primary" @click="addEdit('detail', row)"
>详情</el-link
>
<el-link size="small" type="danger" @click="deleteRow(row.id)"
>删除</el-link
>
</template>
</WarnDataTable>
<Pages @changeNo="changeNo" @changeSize="changeSize" :tableHeight="pageData.tableHeight" :pageConfiger="{
<Pages
@changeNo="changeNo"
@changeSize="changeSize"
:tableHeight="pageData.tableHeight"
:pageConfiger="{
...pageData.pageConfiger,
total: pageData.total
}"></Pages>
}"
></Pages>
</div>
<!-- 详情 -->
<AddForm ref="addFormDiloag" @updateDate="getList"
:dic="{ D_GS_ZDR_RYJB, D_BZ_XB, D_BZ_MZ, D_BZ_XZQHDM, D_ZDRGK_GKZT, D_GS_ZDR_CZZT, D_GS_BQ_ZL, D_GS_BQ_LB, D_GS_BQ_LX, D_GS_ZDR_YJDJ, D_GS_BK_SSJZ }" />
<AddForm
ref="addFormDiloag"
@updateDate="getList"
:dic="{
D_GS_ZDR_RYJB,
D_BZ_XB,
D_BZ_MZ,
D_BZ_XZQHDM,
D_ZDRGK_GKZT,
D_GS_ZDR_CZZT,
D_GS_BQ_ZL,
D_GS_BQ_LB,
D_GS_BQ_LX,
D_GS_ZDR_YJDJ,
D_GS_BK_SSJZ
}"
/>
<!-- 选择用户 -->
<ChooseUser v-model="chooseUserVisible" @choosedUsers="handleUserSelected" :roleIds="roleIds" />
<ChooseUser
v-model="chooseUserVisible"
@choosedUsers="handleUserSelected"
:roleIds="roleIds"
/>
<!-- 转线索 -->
<ZxsForm v-if="showzxs" ref="zxsDilof" @change="getList"
:dic="{ D_BZ_SF, D_BZ_XB, D_GS_XS_LY, D_BZ_SSZT, D_GS_XS_LX, D_GS_XS_QTLX }"></ZxsForm>
<ZxsForm
v-if="showzxs"
ref="zxsDilof"
@change="getList"
:dic="{
D_BZ_SF,
D_BZ_XB,
D_GS_XS_LY,
D_BZ_SSZT,
D_GS_XS_LX,
D_GS_XS_QTLX
}"
></ZxsForm>
</div>
</template>
@ -91,13 +160,52 @@ import Search from "@/components/aboutTable/Search.vue";
import AddForm from "./components/addForm.vue";
import { qcckGet, qcckPost, qcckDelete } from "@/api/qcckApi.js";
import { reactive, ref, onMounted, getCurrentInstance, nextTick } from "vue";
import { useRouter, useRoute } from 'vue-router'
import { useRouter, useRoute } from "vue-router";
import { getItem } from "@/utils/storage.js";
const router = useRouter()
const route = useRoute()
const router = useRouter();
const route = useRoute();
const { proxy } = getCurrentInstance();
const { D_GS_ZDQT_ZT, D_GS_ZDR_RYJB, D_BZ_XB, D_BZ_MZ, D_BZ_RCBKZT, D_BZ_XZQHDM, D_ZDRGK_GKZT, D_GS_ZDR_CZZT, D_GS_BQ_ZL, D_GS_BQ_LB, D_GS_BQ_LX, D_GS_ZDR_YJDJ, D_GS_BK_SSJZ, D_GS_BK_SQLX, D_BZ_SF, D_GS_XS_LY, D_BZ_SSZT, D_GS_XS_LX, D_GS_XS_QTLX } =
proxy.$dict("D_GS_ZDQT_ZT", "D_BZ_RCBKZT", "D_GS_ZDR_RYJB", "D_BZ_XB", "D_BZ_MZ", "D_BZ_XZQHDM", "D_ZDRGK_GKZT", "D_GS_ZDR_CZZT", "D_GS_BQ_ZL", "D_GS_BQ_LB", "D_GS_BQ_LX", "D_GS_ZDR_YJDJ", "D_GS_BK_SSJZ", "D_GS_BK_SQLX", "D_BZ_SF", "D_GS_XS_LY", "D_BZ_SSZT", "D_GS_XS_LX", "D_GS_XS_QTLX");
const {
D_GS_ZDQT_ZT,
D_GS_ZDR_RYJB,
D_BZ_XB,
D_BZ_MZ,
D_BZ_RCBKZT,
D_BZ_XZQHDM,
D_ZDRGK_GKZT,
D_GS_ZDR_CZZT,
D_GS_BQ_ZL,
D_GS_BQ_LB,
D_GS_BQ_LX,
D_GS_ZDR_YJDJ,
D_GS_BK_SSJZ,
D_GS_BK_SQLX,
D_BZ_SF,
D_GS_XS_LY,
D_BZ_SSZT,
D_GS_XS_LX,
D_GS_XS_QTLX
} = proxy.$dict(
"D_GS_ZDQT_ZT",
"D_BZ_RCBKZT",
"D_GS_ZDR_RYJB",
"D_BZ_XB",
"D_BZ_MZ",
"D_BZ_XZQHDM",
"D_ZDRGK_GKZT",
"D_GS_ZDR_CZZT",
"D_GS_BQ_ZL",
"D_GS_BQ_LB",
"D_GS_BQ_LX",
"D_GS_ZDR_YJDJ",
"D_GS_BK_SSJZ",
"D_GS_BK_SQLX",
"D_BZ_SF",
"D_GS_XS_LY",
"D_BZ_SSZT",
"D_GS_XS_LX",
"D_GS_XS_QTLX"
);
const obj = ref({});
const showzxs = ref(false);
const zxsDilof = ref();
@ -134,7 +242,7 @@ const searchConfiger = ref([
placeholder: "请输入人员级别",
showType: "select",
options: D_GS_ZDR_RYJB
},
}
]);
const queryFrom = ref({});
const pageData = reactive({
@ -150,30 +258,48 @@ const pageData = reactive({
pageSize: 20,
pageCurrent: 1
},
controlsWidth: 250,
tableColumn: [
{ label: "姓名", prop: "ryXm", width: 100 },
{ label: "性别", prop: "ryXb", slotName: "ryXb", width: 80 },
{ label: "身份证", prop: "rySfzh", width: 170 },
{ label: "民族", prop: "ryMz", slotName: "ryMz", width: 80 },
{ label: "户籍派出所", prop: "hjdPcsmc" },
{ label: "标签", prop: "bqList", slotName: "bqList", showOverflowTooltip: true },
{
label: "标签",
prop: "bqList",
slotName: "bqList",
showOverflowTooltip: true
},
{ label: "协管单位", prop: "gxSsbmmc" },
{ label: "管控状态", prop: "zdrBkZt", showOverflowTooltip: true, slotName: "zdrBkZt", width: 100 },
{
label: "管控状态",
prop: "zdrBkZt",
showOverflowTooltip: true,
slotName: "zdrBkZt",
width: 100
},
{ label: "审核状态", prop: "zdrZt", slotName: "zdrZt", width: 100 },
{ label: "入库时间", prop: "zdrRkkssj", },
{ label: "操作", prop: "controls", slotName: "controls", width: 250 },
{ label: "入库时间", prop: "zdrRkkssj" },
{
label: "操作",
prop: "controls",
slotName: "controls",
align: "center",
width: 350
}
]
});
const isShiQzDelet = ref(false)
const isShiQzDelet = ref(false);
onMounted(() => {
tabHeightFn();
const isShiQz = getItem('roleList').find(item => item.roleCode == 'JS_777777') != undefined
if (isShiQz) isShiQzDelet.value = true
const isShiQz =
getItem("roleList").find((item) => item.roleCode == "JS_777777") !=
undefined;
if (isShiQz) isShiQzDelet.value = true;
if (route.query.id) {
addEdit('x', {
addEdit("x", {
id: route.query.id
})
});
} else {
getList();
}
@ -200,12 +326,14 @@ const getList = () => {
pageData.tableConfiger.loading = true;
// 人员类型D_ZDRY_RYLX(01 重点 02 普通〉
// rylx: '01',
let data = { ...pageData.pageConfiger, ...queryFrom.value, rylx: '01' };
qcckGet(data, "/mosty-gsxt/tbGsxtZdry/selectPage").then((res) => {
let data = { ...pageData.pageConfiger, ...queryFrom.value, rylx: "01" };
qcckGet(data, "/mosty-gsxt/tbGsxtZdry/selectPage")
.then((res) => {
pageData.tableData = res.records || [];
pageData.total = res.total;
pageData.tableConfiger.loading = false;
}).catch(() => {
})
.catch(() => {
pageData.tableConfiger.loading = false;
});
};
@ -217,25 +345,27 @@ const handleSend = (id) => {
proxy.$message({ type: "success", message: "送审成功" });
getList();
});
})
});
};
// 移除
const handleremove = (id) => {
proxy.$confirm("确定要移除此重点人员?", "警告", { type: "warning" }).then(() => {
qcckPost({ id, rylx: '02' }, "/mosty-gsxt/tbGsxtZdry/update").then(() => {
proxy
.$confirm("确定要移除此重点人员?", "警告", { type: "warning" })
.then(() => {
qcckPost({ id, rylx: "02" }, "/mosty-gsxt/tbGsxtZdry/update").then(() => {
proxy.$message({ type: "success", message: "移除成功" });
getList();
});
})
}
});
};
const handleMoveToFocus = (id) => {
proxy.$confirm("确定要移至关注库?", "警告", { type: "warning" }).then(() => {
qcckPost({ id, rylx: '03' }, "/mosty-gsxt/tbGsxtZdry/rylxyd").then(() => {
qcckPost({ id, rylx: "03" }, "/mosty-gsxt/tbGsxtZdry/rylxyd").then(() => {
proxy.$message({ type: "success", message: "移除成功" });
getList();
});
})
}
});
};
const chooseData = (data) => {
ids.value = Array.isArray(data) ? data.map((item) => item.id) : [];
@ -243,12 +373,15 @@ const chooseData = (data) => {
};
// 选择申请数据数据
const handleApplication = () => {
if (ids.value.length === 0) return ElMessage.error("请先选择需要布控的重点人");
qcckPost({ ids: ids.value }, "/mosty-gsxt/tbGsxtZdry/addBksq").then(() => {
if (ids.value.length === 0)
return ElMessage.error("请先选择需要布控的重点人");
qcckPost({ ids: ids.value }, "/mosty-gsxt/tbGsxtZdry/addBksq")
.then(() => {
ElMessage.success("申请成功");
visible.value = false;
getList();
}).catch(() => {
})
.catch(() => {
ElMessage.error("布控申请失败");
});
};
@ -260,25 +393,34 @@ const handleUserSelected = (val) => {
// 处理分配
const handlefp = () => {
if (ids.value.length === 0) return ElMessage.error("请先选择需要布控的重点人");
qcckPost({ ids: ids.value, uid: obj.value.fpid }, "/mosty-gsxt/tbGsxtZdry/addGkmj").then(() => {
if (ids.value.length === 0)
return ElMessage.error("请先选择需要布控的重点人");
qcckPost(
{ ids: ids.value, uid: obj.value.fpid },
"/mosty-gsxt/tbGsxtZdry/addGkmj"
)
.then(() => {
ElMessage.success("分配成功");
visible.value = false;
visiblefp.value = false;
getList();
}).catch(() => {
})
.catch(() => {
ElMessage.error("分配失败");
});
};
// 移交管控
const handleMove = () => {
if (ids.value.length === 0) return ElMessage.error("请先选择需要移交管控的重点群体");
if (ids.value.length === 0)
return ElMessage.error("请先选择需要移交管控的重点群体");
proxy.$confirm("是否确定移交?", "警告", { type: "warning" }).then(() => {
qcckPost({ ids: ids.value }, "/mosty-gsxt/tbGsxtZdry/addSfyj").then(() => {
qcckPost({ ids: ids.value }, "/mosty-gsxt/tbGsxtZdry/addSfyj")
.then(() => {
ElMessage.success("移交管控成功");
getList();
}).catch(() => {
})
.catch(() => {
ElMessage.error("移交管控失败");
});
});
@ -286,7 +428,8 @@ const handleMove = () => {
// 转线索
const handleZxs = () => {
if (ids.value.length === 0) return ElMessage.error("请先选择需要转线索的重点群体");
if (ids.value.length === 0)
return ElMessage.error("请先选择需要转线索的重点群体");
showzxs.value = true;
nextTick(() => {
zxsDilof.value.init(choosList.value);
@ -303,22 +446,38 @@ const deleteRow = (id) => {
});
};
//新增编辑
const addEdit = (type, row) => {
show.value = true;
nextTick(() => {
addFormDiloag.value.init(type, row);
})
});
};
// 表格高度计算
const tabHeightFn = () => {
pageData.tableHeight = window.innerHeight - searchBox.value.offsetHeight - 220;
pageData.tableHeight =
window.innerHeight - searchBox.value.offsetHeight - 220;
window.onresize = function () {
tabHeightFn();
};
};
// 标签颜色
const Bqys = (ys) => {
switch (ys) {
case "01":
return "#fd4343";
case "02":
return "#c26e09";
case "03":
return "#ffd208ff";
case "04":
return "#01abee";
default:
break;
}
};
</script>
<style lang="scss" scoped>

View File

@ -3,35 +3,60 @@
<div class="headClass">
<h3>人员信息</h3>
<!-- @click="gettbGsxtZdqtUpdate" -->
<el-button type="primary" v-if="showBut" :disabled="disabled" @click="submit">保存</el-button>
<el-button
type="primary"
v-if="showBut"
:disabled="disabled"
@click="submit"
>保存</el-button
>
</div>
<div>
<FormMessage :disabled="disabled" v-model="listQuery" :formList="formData" labelWidth="130px" ref="elform"
:rules="rules">
<FormMessage
:disabled="disabled"
v-model="listQuery"
:formList="formData"
labelWidth="130px"
ref="elform"
:rules="rules"
>
<template #ryzp>
<div style="width: 100%; padding-left: 50px">
<MOSTY.Upload :showBtn="false" :limit="1" v-model="listQuery.ryzp" />
<MOSTY.Upload
:showBtn="false"
:limit="1"
v-model="listQuery.ryzp"
/>
</div>
</template>
<template #gkMjXm>
<div>
<el-input v-model="listQuery.gkMjXm" class="group" placeholder="请输入管控民警姓名" readonly
@click="chooseMarksVisible = true" />
<el-input
v-model="listQuery.gkMjXm"
class="group"
placeholder="请输入管控民警姓名"
readonly
@click="chooseMarksVisible = true"
/>
</div>
</template>
<!-- <template #gkmjsfzh>
<!-- <template #gkMjSfzh>
<div>
<el-input v-model="listQuery.gkmjsfzh" class="group" placeholder="请输入管控民警身份证号" readonly
<el-input v-model="listQuery.gkMjSfzh" class="group" placeholder="请输入管控民警身份证号" readonly
@click="chooseMarksVisible = true" />
</div>
</template> -->
<!-- { label: "管控民警", prop: "gkMjXm", type: "slot" }, -->
<!-- { label: "管控民警身份证号", prop: "gkmjsfzh", type: "slot" }, -->
<!-- { label: "管控民警身份证号", prop: "gkMjSfzh", type: "slot" }, -->
</FormMessage>
</div>
</div>
<!-- <ChooseMarks v-model="chooseMarksVisible" @choosed="choosed" :roleIds="roleIds" /> -->
<ChooseUser v-model="chooseMarksVisible" @choosedUsers="choosed" :roleIds="roleIds" />
<ChooseUser
v-model="chooseMarksVisible"
@choosedUsers="choosed"
:roleIds="roleIds"
/>
</template>
<script setup>
@ -43,24 +68,52 @@ import ChooseUser from "@/components/ChooseList/ChooseUser/index.vue";
import { ref, reactive, onMounted, getCurrentInstance, watch } from "vue";
import { tbGsxtZdryUpdate } from "@/api/zdr.js";
const { proxy } = getCurrentInstance();
const { D_BZ_XB, D_BZ_ZZMM, D_BZ_HYZK, D_BZ_MZ, D_BZ_XZQHDM, D_ZDRY_RYLX, D_BZ_RCBKZT, D_GS_ZDR_RYJB, D_GS_ZDR_YJDJ, D_GS_BK_SSJZ, D_GS_ZDR_CZZT, D_BZ_WHCD, D_ZDRY_ZYLB } =
proxy.$dict('D_BZ_XB', 'D_BZ_ZZMM', 'D_BZ_HYZK', 'D_BZ_MZ', "D_ZDRY_RYLX", 'D_BZ_XZQHDM', 'D_BZ_RCBKZT', 'D_GS_ZDR_RYJB', 'D_GS_ZDR_YJDJ', 'D_GS_BK_SSJZ', 'D_GS_ZDR_CZZT', 'D_BZ_WHCD', 'D_ZDRY_ZYLB')
const {
D_BZ_XB,
D_BZ_ZZMM,
D_BZ_HYZK,
D_BZ_MZ,
D_BZ_XZQHDM,
D_ZDRY_RYLX,
D_BZ_RCBKZT,
D_GS_ZDR_RYJB,
D_GS_ZDR_YJDJ,
D_GS_BK_SSJZ,
D_GS_ZDR_CZZT,
D_BZ_WHCD,
D_ZDRY_ZYLB
} = proxy.$dict(
"D_BZ_XB",
"D_BZ_ZZMM",
"D_BZ_HYZK",
"D_BZ_MZ",
"D_ZDRY_RYLX",
"D_BZ_XZQHDM",
"D_BZ_RCBKZT",
"D_GS_ZDR_RYJB",
"D_GS_ZDR_YJDJ",
"D_GS_BK_SSJZ",
"D_GS_ZDR_CZZT",
"D_BZ_WHCD",
"D_ZDRY_ZYLB"
);
const props = defineProps({
dataList: {
type: Object,
default: () => { },
}, disabled: {
default: () => {}
},
disabled: {
type: Boolean,
default: false
},
showBut: {
type: Boolean,
default: false
},
})
}
});
const rules = reactive({
ryXm: [{ required: true, message: "请输入姓名", trigger: "blur" }],
...rule.identityCardRule({ validator: true }, 'rySfzh'), //身份证校验
...rule.identityCardRule({ validator: true }, "rySfzh"), //身份证校验
...rule.phoneRule({ validator: true }, "gkMjLxfs"), // 是否必填 是否进行校验,
rySfzh: [{ required: true, message: "请输入身份证号", trigger: "blur" }],
ryXb: [{ required: true, message: "请选择性别", trigger: "change" }],
@ -68,11 +121,15 @@ const rules = reactive({
ryCsrq: [{ required: true, message: "请选择出生日期", trigger: "change" }],
zdrYjdj: [{ required: true, message: "请选择预警等级", trigger: "change" }],
gxSsbmdm: [{ required: true, message: "请选择协管单位", trigger: "change" }],
zdrRkkssj: [{ required: true, message: "请选择入库开始时间", trigger: "change" }],
zdrRkjssj: [{ required: true, message: "请选择入库结束时间", trigger: "change" }],
gkMjLxfs: [{ required: true, message: "请输入民警联系方式", trigger: "blur" }],
zdrRkkssj: [
{ required: true, message: "请选择入库开始时间", trigger: "change" }
],
zdrRkjssj: [
{ required: true, message: "请选择入库结束时间", trigger: "change" }
],
gkMjLxfs: [{ required: true, message: "请输入民警联系方式", trigger: "blur" }]
// gkMjXm: [{ required: true, message: "请选择管控民警", trigger: "change" }],
// gkmjsfzh: [{ required: true, message: "请选择管控民警身份证号", trigger: "change" }],
// gkMjSfzh: [{ required: true, message: "请选择管控民警身份证号", trigger: "change" }],
// rylx: [{ required: true, message: "请选择人员类型", trigger: "change" }]
});
const listQuery = ref({}); //表单
@ -82,11 +139,35 @@ const formData = ref([
{ label: "人员照片", prop: "ryzp", type: "slot", width: "100%" },
{ label: "姓名", prop: "ryXm", type: "input", width: "30%" },
{ label: "身份证号", prop: "rySfzh", type: "input", width: "30%" },
{ label: "性别", prop: "ryXb", type: "select", options: D_BZ_XB, width: "30%" },
{
label: "性别",
prop: "ryXb",
type: "select",
options: D_BZ_XB,
width: "30%"
},
{ label: "出生日期", prop: "ryCsrq", type: "date", width: "30%" },
{ label: "民族", prop: "ryMz", type: "select", options: D_BZ_MZ, width: "30%" },
{ label: "协管单位", prop: "gxSsbmdm", depMc: 'gxSsbmmc', type: "department", width: "30%" },
{ label: "预警等级", prop: "zdrYjdj", type: "select", options: D_GS_ZDR_YJDJ, width: "30%" },
{
label: "民族",
prop: "ryMz",
type: "select",
options: D_BZ_MZ,
width: "30%"
},
{
label: "协管单位",
prop: "gxSsbmdm",
depMc: "gxSsbmmc",
type: "department",
width: "30%"
},
{
label: "预警等级",
prop: "zdrYjdj",
type: "select",
options: D_GS_ZDR_YJDJ,
width: "30%"
},
{ label: "管控民警", prop: "gkMjXm", type: "slot", width: "30%" },
{ label: "民警联系方式", prop: "gkMjLxfs", type: "input", width: "30%" },
{ label: "入库开始时间", prop: "zdrRkkssj", type: "datetime", width: "30%" },
@ -94,30 +175,85 @@ const formData = ref([
{ label: "重点人联系电话", prop: "ryLxdh", type: "input", width: "30%" },
{ label: "籍贯", prop: "ryJg", type: "input", width: "30%" },
{ label: "曾用名", prop: "cym", type: "input", width: "30%" },
{ label: "文化程度", prop: "whcdBm", type: "select", options: D_BZ_WHCD, width: "30%" },
{ label: "政治面貌", prop: "zzmm", type: "select", options: D_BZ_ZZMM, width: "30%" },
{
label: "文化程度",
prop: "whcdBm",
type: "select",
options: D_BZ_WHCD,
width: "30%"
},
{
label: "政治面貌",
prop: "zzmm",
type: "select",
options: D_BZ_ZZMM,
width: "30%"
},
{ label: "职业", prop: "zyBm", type: "input", width: "30%" },
{ label: "人员级别", prop: "zdrRyjb", type: "select", options: D_GS_ZDR_RYJB, width: "30%" },
{
label: "人员级别",
prop: "zdrRyjb",
type: "select",
options: D_GS_ZDR_RYJB,
width: "30%"
},
{ label: "户籍地区划", prop: "hjdQh", type: "input", width: "30%" },
{ label: "户籍地详址", prop: "hjdXz", type: "input", width: "30%" },
{ label: "户籍地派出所", prop: "hjdPcsmc", type: "input", width: "30%" },
{ label: "现住地区划", prop: "xzdQh", type: "input", width: "30%" },
{ label: "现住地详址", prop: "xzdXz", type: "input", width: "30%" },
{ label: "现住地派出所", prop: "xzdPcsdm", depMc: "xzdPcsmc", type: "department", width: "30%" },
{
label: "现住地派出所",
prop: "xzdPcsdm",
depMc: "xzdPcsmc",
type: "department",
width: "30%"
},
// { label: "民警身份证", prop: "gkmjsfzh", type: "slot" },
// { label: "民警身份证", prop: "gkMjSfzh", type: "slot" },
{ label: "诉求单位", prop: "sqSsbmmc", type: "input", width: "30%" },
{ label: "责任单位", prop: "zrSsbmmc", type: "input", width: "30%" },
{ label: "所属警种", prop: "zdrSsjz", type: "select", options: D_GS_BK_SSJZ, width: "30%" },
{ label: "涉及警种", prop: "zdrSjjz", type: "select", options: D_GS_BK_SSJZ, multiple: true, width: "30%" },
{ label: "婚姻状态", prop: "hyzk", type: "select", options: D_BZ_HYZK, width: "30%" },
{ label: "处置状态", prop: "zdrCzzt", type: "select", options: D_GS_ZDR_CZZT, width: "30%" },
{ label: "布控状态", prop: "zdrBkZt", type: "select", options: D_BZ_RCBKZT, width: "30%" },
{
label: "所属警种",
prop: "zdrSsjz",
type: "select",
options: D_GS_BK_SSJZ,
width: "30%"
},
{
label: "涉及警种",
prop: "zdrSjjz",
type: "select",
options: D_GS_BK_SSJZ,
multiple: true,
width: "30%"
},
{
label: "婚姻状态",
prop: "hyzk",
type: "select",
options: D_BZ_HYZK,
width: "30%"
},
{
label: "处置状态",
prop: "zdrCzzt",
type: "select",
options: D_GS_ZDR_CZZT,
width: "30%"
},
{
label: "布控状态",
prop: "zdrBkZt",
type: "select",
options: D_BZ_RCBKZT,
width: "30%"
},
// { label: "人员类型", prop: "rylx", type: "select", options: D_ZDRY_RYLX },
{ label: "Mac地址", prop: "macDz", type: "input", width: "30%" },
// { label: "标签选择", prop: "tags", type: "slot", width: "100%" },
{ label: "管控原因", prop: "zdrLkyy", type: "textarea", width: "100%" },
{ label: "管控原因", prop: "zdrLkyy", type: "textarea", width: "100%" }
]);
const loading = ref(false);
const elform = ref();
@ -125,14 +261,14 @@ const disabled = ref(false);
// phoneList已重构为listQuery.value.ryLxdh
// 创建一个工具函数进行深拷贝
const deepClone = (obj) => {
if (obj === null || typeof obj !== 'object') {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (obj instanceof Date) {
return new Date(obj.getTime());
}
if (obj instanceof Array) {
return obj.map(item => deepClone(item));
return obj.map((item) => deepClone(item));
}
const clonedObj = {};
for (const key in obj) {
@ -143,64 +279,79 @@ const deepClone = (obj) => {
return clonedObj;
};
// 监听身份证号变化,自动填充性别、出生日期和民族
watch(() => listQuery.value.rySfzh, (val) => {
watch(
() => listQuery.value.rySfzh,
(val) => {
if (val && val.length === 18) {
// 使用IdCard方法提取出生日期
listQuery.value.ryCsrq = IdCard(val, 1);
// 使用IdCard方法提取性别
const genderText = IdCard(val, 2);
listQuery.value.ryXb = D_BZ_XB.value.find(item => item.zdmc === genderText).dm;
listQuery.value.ryXb = D_BZ_XB.value.find(
(item) => item.zdmc === genderText
).dm;
}
});
}
);
// 监听props.dataList变化处理初始化数据
watch(() => props.dataList, (val) => {
watch(
() => props.dataList,
(val) => {
if (val) {
console.log(val);
// 使用深拷贝避免直接引用同一个对象
listQuery.value = deepClone(val);
// 处理照片数据
listQuery.value.ryzp = val.ryzp == null || val.ryzp == '' ? [] : [val.ryzp];
listQuery.value.zdrSjjz = val.zdrSjjz == null || val.zdrSjjz == '' ? [] : JSON.parse(val.zdrSjjz);
listQuery.value.ryzp =
val.ryzp == null || val.ryzp == "" ? [] : [val.ryzp];
listQuery.value.zdrSjjz =
val.zdrSjjz == null || val.zdrSjjz == "" ? [] : JSON.parse(val.zdrSjjz);
// 处理标签ID数据确保数据回显
if (val.tagIds && Array.isArray(val.tagIds) && val.tagIds.length > 0) {
roleIds.value = [...val.tagIds];
} else if (val.bqIds && Array.isArray(val.bqIds) && val.bqIds.length > 0) {
} else if (
val.bqIds &&
Array.isArray(val.bqIds) &&
val.bqIds.length > 0
) {
roleIds.value = [...val.bqIds];
} else {
roleIds.value = [];
}
}
}, { deep: true })
},
{ deep: true }
);
// 提交
const submit = () => {
loading.value = true
gettbGsxtZdryUpdate()
loading.value = true;
gettbGsxtZdryUpdate();
};
//
const gettbGsxtZdryUpdate = () => {
const promes = {
...listQuery.value,
ryzp: listQuery.value.ryzp.length > 0 ? listQuery.value.ryzp.toString() : "",
zdrSjjz: JSON.stringify(listQuery.value.zdrSjjz),
}
ryzp:
listQuery.value.ryzp.length > 0 ? listQuery.value.ryzp.toString() : "",
zdrSjjz: JSON.stringify(listQuery.value.zdrSjjz)
};
elform.value.submit((data) => {
tbGsxtZdryUpdate(promes).then((res) => {
listQuery.value.ryzp = []
tbGsxtZdryUpdate(promes)
.then((res) => {
listQuery.value.ryzp = [];
proxy.$message({
message: '更新成功',
type: 'success',
})
}).catch((err) => {
}).finally(() => {
loading.value = false
message: "更新成功",
type: "success"
});
})
}
.catch((err) => {})
.finally(() => {
loading.value = false;
});
});
};
const throwData = () => {
return new Promise((resolve, reject) => {
@ -209,37 +360,40 @@ const throwData = () => {
// 过滤掉空的电话号码
resolve({
...listQuery.value,
ryzp: listQuery.value.ryzp && listQuery.value.ryzp.length > 0 ? listQuery.value.ryzp.toString() : '',
zdrSjjz: JSON.stringify(listQuery.value.zdrSjjz),
ryzp:
listQuery.value.ryzp && listQuery.value.ryzp.length > 0
? listQuery.value.ryzp.toString()
: "",
zdrSjjz: JSON.stringify(listQuery.value.zdrSjjz)
});
});
})
} else {
elform.value.submit((data) => {
// 如果没有验证方法,直接返回数据
resolve({
...listQuery.value,
ryzp: listQuery.value.ryzp && listQuery.value.ryzp.length > 0 ? listQuery.value.ryzp.toString() : '',
zdrSjjz: JSON.stringify(listQuery.value.zdrSjjz),
ryzp:
listQuery.value.ryzp && listQuery.value.ryzp.length > 0
? listQuery.value.ryzp.toString()
: "",
zdrSjjz: JSON.stringify(listQuery.value.zdrSjjz)
});
});
})
}
});
};
const choosed = (val) => {
roleIds.value = [val[0].id]
listQuery.value.gkMjXm = val[0].userName
listQuery.value.gkmjsfzh = val[0].idEntityCard
listQuery.value.gkMjLxfs = val[0].mobile
roleIds.value = [val[0].id];
listQuery.value.gkMjXm = val[0].userName;
listQuery.value.gkMjSfzh = val[0].idEntityCard;
listQuery.value.gkMjLxfs = val[0].mobile;
listQuery.value.gkMjJh = val[0].inDustRialId;
console.log(listQuery.value);
};
defineExpose({
throwData,
throwData
});
</script>
<style lang="scss" scoped>
@ -283,7 +437,7 @@ defineExpose({
}
.headClass::after {
content: '';
content: "";
position: absolute;
left: 0;
bottom: -2px;

View File

@ -2,10 +2,21 @@
<div>
<div class="headClass" style="">
<h3>人员标签</h3>
<el-button type="primary" :disabled="disabled" @click="chooseMarksVisible = true">选择</el-button>
<el-button
type="primary"
:disabled="disabled"
@click="chooseMarksVisible = true"
>选择</el-button
>
</div>
<MyTable :tableData="pageData.tableData" :tableColumn="pageData.tableColumn" :tableHeight="pageData.tableHeight"
:key="pageData.keyCount" :tableConfiger="pageData.tableConfiger" :controlsWidth="pageData.controlsWidth">
<MyTable
:tableData="pageData.tableData"
:tableColumn="pageData.tableColumn"
:tableHeight="pageData.tableHeight"
:key="pageData.keyCount"
:tableConfiger="pageData.tableConfiger"
:controlsWidth="pageData.controlsWidth"
>
<template #bqLx="{ row }">
<DictTag :tag="false" :value="row.bqLx" :options="D_GS_BQ_DJ" />
</template>
@ -18,143 +29,181 @@
</template>
</MyTable>
</div>
<ChooseMarks v-model="chooseMarksVisible" @choosed="addMarks" :roleIds="roleIds" />
<ChooseMarks
v-model="chooseMarksVisible"
@choosed="addMarks"
:roleIds="roleIds"
:bqLx="defaultBqLx"
/>
</template>
<script setup>
import { ref, reactive, watch, toRaw, getCurrentInstance, onMounted, onUnmounted } from "vue";
import {
ref,
reactive,
watch,
toRaw,
computed,
getCurrentInstance,
onMounted,
onUnmounted
} from "vue";
import MyTable from "@/components/aboutTable/MyTable.vue";
import ChooseMarks from "@/components/ChooseList/ChooseMarks/index.vue";
import { tbGsxtZdryUpdate } from '@/api/zdr.js'
import { tbGsxtZdryUpdate } from "@/api/zdr.js";
import { ElMessage, ElMessageBox } from "element-plus";
const { proxy } = getCurrentInstance();
const { D_GS_BQ_DJ, D_GS_SSYJ } = proxy.$dict("D_GS_BQ_DJ", "D_GS_SSYJ"); //获取字典数据
const chooseMarksVisible = ref(false)
const chooseMarksVisible = ref(false);
const props = defineProps({
dataList: {
type: Object,
default: () => { },
}, disabled: {
default: () => {}
},
disabled: {
type: Boolean,
default: false
},
showBut: {
type: Boolean,
default: false
},
})
const listData = ref({})
watch(() => props.dataList, (val) => {
if (val) {
listData.value = val
pageData.tableData = val.bqList
roleIds.value = val.bqList.map(v => v.bqId)
console.log(roleIds.value);
}
}, { deep: true })
const roleIds = ref([])
});
const listData = ref({});
const roleIds = ref([]);
watch(
() => props.dataList,
(val) => {
if (val) {
listData.value = val;
pageData.tableData = val.bqList;
roleIds.value = val.bqList.map((v) => v.bqId);
console.log(roleIds.value);
}
},
{ deep: true }
);
// 表格数据
const pageData = reactive({
tableData: [],
tableColumn: [{
prop: 'bqMc',
label: '标签名称',
tableColumn: [
{
prop: "bqMc",
label: "标签名称",
showOverflowTooltip: true
}, {
prop: 'bqDm',
label: '标签代码',
}, {
},
{
prop: "bqDm",
label: "标签代码"
},
{
showSolt: true,
prop: 'bqLx',
label: '标签类型',
}, {
prop: "bqLx",
label: "标签类型"
},
{
showSolt: true,
prop: 'bqLb',
label: '标签类别',
}],
tableHeight: '200px',
prop: "bqLb",
label: "标签类别"
}
],
tableHeight: "200px",
keyCount: 0,
tableConfiger: {
border: true,
stripe: true,
showHeader: true,
showIndex: true,
indexLabel: '序号',
indexLabel: "序号",
indexWidth: 60,
align: 'center',
align: "center",
showOverflowTooltip: true,
haveControls: !props.disabled
},
controlsWidth: 200,
})
controlsWidth: 200
});
// 计算默认的标签类型:如果已选数据中有身份标签则默认显示身份标签,否则默认显示行为标签
const defaultBqLx = computed(() => {
if (roleIds.value && roleIds.value.length > 0) {
const selectedItem = pageData.tableData.find((item) => roleIds.value.includes(item.bqId));
if (selectedItem && selectedItem.bqLx) {
return selectedItem.bqLx;
}
}
return "02";
});
// 修改数据接口
const zdqtUpdate = (val) => {
const params = {
id: listData.value.id,
bqList: pageData.tableData,
rySfzh: listData.value.rySfzh,
}
tbGsxtZdryUpdate(params).then(res => {
rySfzh: listData.value.rySfzh
};
tbGsxtZdryUpdate(params).then((res) => {
proxy.$message({
message: val,
type: 'success'
})
})
}
type: "success"
});
});
};
// 新增标签
const addMarks = (val) => {
pageData.tableData = val.map(v => {
return { bqDm: v.bqDm, bqId: v.id, bqLb: v.bqLb, bqLx: v.bqLx, bqMc: v.bqMc }
pageData.tableData = val.map((v) => {
return {
bqDm: v.bqDm,
bqId: v.id,
bqLb: v.bqLb,
bqLx: v.bqLx,
bqMc: v.bqMc,
bqYs: v.bqYs
};
});
roleIds.value = val.map(v => v.id)
roleIds.value = val.map((v) => v.id);
if (!props.disabled && props.showBut) {
zdqtUpdate("标签添加成功")
}
zdqtUpdate("标签添加成功");
}
};
// 删除标签
const delDictItem = (val) => {
if (!props.disabled && props.showBut) {
ElMessageBox.confirm(
'是否删除标签',
'提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
)
ElMessageBox.confirm("是否删除标签", "提示", {
confirmButtonText: "确认",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
pageData.tableData = pageData.tableData.filter(v => v.bqId != val)
roleIds.value = roleIds.value.filter(v => v != val)
zdqtUpdate("标签删除成功")
pageData.tableData = pageData.tableData.filter((v) => v.bqId != val);
roleIds.value = roleIds.value.filter((v) => v != val);
zdqtUpdate("标签删除成功");
})
.catch(() => {
ElMessage({
type: 'info',
message: '取消删除',
})
})
type: "info",
message: "取消删除"
});
});
} else {
pageData.tableData = pageData.tableData.filter(v => v.bqId != val)
roleIds.value = roleIds.value.filter(v => v != val)
}
pageData.tableData = pageData.tableData.filter((v) => v.bqId != val);
roleIds.value = roleIds.value.filter((v) => v != val);
}
};
// 抛出数据并验证标签列表不为空
const throwData = () => {
return new Promise((resolve) => {
// 验证:确保标签列表不为空
if (!pageData.tableData || pageData.tableData.length === 0) {
throw new Error('请选择群体标签');
throw new Error("请选择群体标签");
}
resolve(pageData.tableData);
});
}
};
defineExpose({
throwData
})
});
</script>
<style lang="scss" scoped>
@ -196,7 +245,7 @@ defineExpose({
}
.headClass::after {
content: '';
content: "";
position: absolute;
left: 0;
bottom: -2px;

View File

@ -4,34 +4,95 @@
<div class="query-grid">
<div v-for="field in renderFields" :key="field.key" class="query-cell">
<div class="cell-label">{{ field.label }}</div>
<div class="cell-control" :class="{ 'is-checkbox': field.type === 'checkbox' }">
<el-input clearable v-if="field.type === 'input'" v-model="formState[field.key]" class="control-input"
:placeholder="field.placeholder || ''" />
<el-input clearable v-else-if="field.type === 'number'" v-model="formState[field.key]" class="control-input"
type="number" :placeholder="field.placeholder || ''" />
<el-select clearable v-else-if="field.type === 'select'" v-model="formState[field.key]" class="control-select"
:placeholder="field.placeholder || '请选择'" :multiple="field.multiple || false" collapse-tags
collapse-tags-tooltip>
<el-option v-for="item in field.options || []" :key="item.value ?? item" :label="item.label ?? item"
:value="item.value ?? item" />
<div
class="cell-control"
:class="{ 'is-checkbox': field.type === 'checkbox' }"
>
<el-input
clearable
v-if="field.type === 'input'"
v-model="formState[field.key]"
class="control-input"
:placeholder="field.placeholder || ''"
/>
<el-input
clearable
v-else-if="field.type === 'number'"
v-model="formState[field.key]"
class="control-input"
type="number"
:placeholder="field.placeholder || ''"
/>
<el-select
clearable
v-else-if="field.type === 'select'"
v-model="formState[field.key]"
class="control-select"
:placeholder="field.placeholder || '请选择'"
:multiple="field.multiple || false"
collapse-tags
collapse-tags-tooltip
>
<el-option
v-for="item in field.options || []"
:key="item.value ?? item"
:label="item.label ?? item"
:value="item.value ?? item"
/>
</el-select>
<el-date-picker clearable v-else-if="field.type === 'date'" v-model="formState[field.key]"
class="control-date" type="date" :placeholder="field.placeholder || '请选择日期'" value-format="YYYY-MM-DD" />
<el-date-picker clearable v-else-if="field.type === 'datetime'" v-model="formState[field.key]"
class="control-date" type="datetime" :placeholder="field.placeholder || '请选择时间'"
value-format="YYYY-MM-DD HH:mm:ss" />
<el-date-picker clearable v-else-if="field.type === 'daterange'" v-model="formState[field.key]"
class="control-date" type="daterange" range-separator="" start-placeholder="开始日期" end-placeholder="结束日期"
value-format="YYYY-MM-DD" />
<el-date-picker clearable v-else-if="field.type === 'datetimerange'" v-model="formState[field.key]"
class="control-date" type="datetimerange" range-separator="" start-placeholder="开始时间"
end-placeholder="结束时间" value-format="YYYY-MM-DD HH:mm:ss" />
<el-date-picker
clearable
v-else-if="field.type === 'date'"
v-model="formState[field.key]"
class="control-date"
type="date"
:placeholder="field.placeholder || '请选择日期'"
value-format="YYYY-MM-DD"
/>
<el-date-picker
clearable
v-else-if="field.type === 'datetime'"
v-model="formState[field.key]"
class="control-date"
type="datetime"
:placeholder="field.placeholder || '请选择时间'"
value-format="YYYY-MM-DD HH:mm:ss"
/>
<el-date-picker
clearable
v-else-if="field.type === 'daterange'"
v-model="formState[field.key]"
class="control-date"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
/>
<el-date-picker
clearable
v-else-if="field.type === 'datetimerange'"
v-model="formState[field.key]"
class="control-date"
type="datetimerange"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD HH:mm:ss"
/>
<template v-else-if="field.type === 'department'">
<MOSTY.Department clearable v-model="formState[field.key]" class="control-select" />
<MOSTY.Department
clearable
v-model="formState[field.key]"
class="control-select"
/>
</template>
<div v-else-if="field.type === 'checkbox'" class="checkbox-wrap">
<el-checkbox v-model="formState[field.key]" />
</div>
<div v-else-if="field.type === 'slot'" class="checkbox-wrap">
<slot :name="field.key" />
</div>
</div>
</div>
</div>
@ -40,24 +101,28 @@
<slot name="but"></slot>
</div>
<div>
<el-button size="small" type="primary" @click="handleSearch">{{ searchText }}</el-button>
<el-button size="small" type="button" @click="handleReset">重置 </el-button>
<el-button size="small" type="primary" @click="handleSearch">{{
searchText
}}</el-button>
<el-button size="small" type="button" @click="handleReset"
>重置
</el-button>
</div>
</div>
</section>
</template>
<script setup>
import { computed, reactive, watch } from 'vue'
import { computed, reactive, watch } from "vue";
import * as MOSTY from "@/components/MyComponents/index";
const props = defineProps({
title: {
type: String,
default: '查询条件'
default: "查询条件"
},
searchText: {
type: String,
default: '查询'
default: "查询"
},
fields: {
type: Array,
@ -67,100 +132,106 @@ const props = defineProps({
type: Array,
default: () => []
}
})
});
const emit = defineEmits(['search', 'submit', 'reset'])
const formState = reactive({})
const emit = defineEmits(["search", "submit", "reset"]);
const formState = reactive({});
const renderFields = computed(() => {
const source = props.searchArr.length ? props.searchArr : props.fields
return source.map((field) => ({
const source = props.searchArr.length ? props.searchArr : props.fields;
return source
.map((field) => ({
...field,
key: field.key || field.prop,
type: field.type || field.showType || 'input'
})).filter((field) => field.key)
})
type: field.type || field.showType || "input"
}))
.filter((field) => field.key);
});
const getResetValue = (field) => {
if (field.defaultVal !== undefined) {
return Array.isArray(field.defaultVal) ? [...field.defaultVal] : field.defaultVal
return Array.isArray(field.defaultVal)
? [...field.defaultVal]
: field.defaultVal;
}
if (field.type === 'checkbox') {
return false
if (field.type === "checkbox") {
return false;
}
if (field.type === 'daterange' || field.type === 'datetimerange') {
return []
if (field.type === "daterange" || field.type === "datetimerange") {
return [];
}
if (field.type === 'select') {
if (field.type === "select") {
if (field.multiple) {
return []
return [];
}
return null
}
return ''
return null;
}
return "";
};
const buildResetPayload = () => {
const payload = {}
const payload = {};
renderFields.value.forEach((field) => {
payload[field.key] = getResetValue(field)
})
return payload
}
payload[field.key] = getResetValue(field);
});
return payload;
};
const setFormState = (value = {}) => {
Object.keys(formState).forEach((key) => {
delete formState[key]
})
delete formState[key];
});
renderFields.value.forEach((field) => {
if (value[field.key] !== undefined) {
formState[field.key] = Array.isArray(value[field.key]) ? [...value[field.key]] : value[field.key]
return
}
formState[field.key] = getResetValue(field)
})
formState[field.key] = Array.isArray(value[field.key])
? [...value[field.key]]
: value[field.key];
return;
}
formState[field.key] = getResetValue(field);
});
};
const getFormSnapshot = () => {
const snapshot = {}
const snapshot = {};
renderFields.value.forEach((field) => {
const value = formState[field.key]
const value = formState[field.key];
if (Array.isArray(value)) {
snapshot[field.key] = [...value]
return
}
snapshot[field.key] = value
})
return snapshot
snapshot[field.key] = [...value];
return;
}
snapshot[field.key] = value;
});
return snapshot;
};
const handleSearch = () => {
const payload = getFormSnapshot()
emit('search', payload)
emit('submit', payload)
}
const payload = getFormSnapshot();
emit("search", payload);
emit("submit", payload);
};
const handleReset = () => {
const payload = buildResetPayload()
setFormState(payload)
emit('reset', true)
emit('search', payload)
emit('submit', payload)
}
const payload = buildResetPayload();
setFormState(payload);
emit("reset", true);
emit("search", payload);
emit("submit", payload);
};
watch(
renderFields,
() => {
setFormState(buildResetPayload())
setFormState(buildResetPayload());
},
{ immediate: true }
)
);
defineExpose({
formState,
handleSearch,
handleReset
})
});
</script>
<style scoped lang="scss">

View File

@ -1,32 +1,70 @@
<template>
<div>
<PageTitle :malginLeft="10" :height="35" backgroundColor="#ffff" :marginBottom="5" :marginTop="5">
<PageTitle
:malginLeft="10"
:height="35"
backgroundColor="#ffff"
:marginBottom="5"
:marginTop="5"
>
<template #left>
<!-- -->
<template v-for="(item, index) in butList" :key="index">
<el-popover placement="right" :width="240" style='height: 300px;' trigger="click" v-if="item == '布控预警'">
<!-- <el-popover
placement="right"
:width="240"
style="height: 300px"
trigger="click"
v-if="item == '布控预警'"
>
<template #reference>
<el-button :type="butStylChange(qh) ? 'primary' : 'default'" size="small">{{ item }}</el-button>
<el-button
:type="butStylChange(qh) ? 'primary' : 'default'"
size="small"
>{{ item }}</el-button
>
</template>
<el-select v-model="value" placeholder="请选择预警类型" @change="qh = value">
<el-select
v-model="value"
placeholder="请选择预警类型"
@change="qh = value"
>
<el-option label="人像预警" value="人像预警" />
<el-option label="车辆预警" value="车辆预警" />
<el-option label="区域预警" value="区域预警" />
<el-option label="布控预警" value="布控预警" />
</el-select>
</el-popover>
<el-popover placement="right" :width="240" style='height: 300px;' trigger="click" v-else-if="item == '标签预警'">
</el-popover> -->
<!-- <el-popover
placement="right"
:width="240"
style="height: 300px"
trigger="click"
v-if="item == '标签预警'"
>
<template #reference>
<el-button :type="BqbutStylChange(qh) ? 'primary' : 'default'" size="small">{{ item }}</el-button>
<el-button
:type="BqbutStylChange(qh) ? 'primary' : 'default'"
size="small"
>{{ item }}</el-button
>
</template>
<el-select v-model="Bqvalue" placeholder="请选择预警类型" @change="qh = Bqvalue">
<el-select
v-model="Bqvalue"
placeholder="请选择预警类型"
@change="qh = Bqvalue"
>
<el-option label="身份预警" value="身份预警" />
<el-option label="行为预警" value="行为预警" />
<el-option label="组合预警" value="组合预警" />
</el-select>
</el-popover>
<el-button :type="qh == item ? 'primary' : 'default'" @click="qh = item" size="small" v-else>{{ item
}}</el-button>
</el-popover> -->
<el-button
:type="qh == item ? 'primary' : 'default'"
@click="qh = item"
size="small"
>{{ item }}</el-button
>
</template>
</template>
</PageTitle>
@ -41,57 +79,63 @@
<IdentityWarning v-if="qh == '身份预警'" />
<BehaviorWarning v-if="qh == '行为预警'" />
<CombinedWarning v-if="qh == '组合预警'" />
<LabelWarning v-if="qh == '标签预警'" />
<!-- <Cs v-if="qh == '测试'" /> -->
</div>
</template>
<script setup>
import { getItem } from '@//utils/storage.js'
import { getItem } from "@//utils/storage.js";
import PageTitle from "@/components/aboutTable/PageTitle.vue";
import FouColorWarning from "@/views/backOfficeSystem/fourColorManage/warningControl/fouColorWarning/index.vue"
import SevenWarning from "@/views/backOfficeSystem/fourColorManage/warningControl/sevenWarning/index.vue"
import IdentityWarning from "@/views/backOfficeSystem/fourColorManage/warningControl/identityWarning/index.vue"
import BehaviorWarning from "@/views/backOfficeSystem/fourColorManage/warningControl/behaviorWarning/index.vue"
import CombinedWarning from "@/views/backOfficeSystem/fourColorManage/warningControl/combinedWarning/index.vue"
import PortraitWarning from "@/views/backOfficeSystem/fourColorManage/warningList/portraitWarning/index.vue"
import VehicleWarning from "@/views/backOfficeSystem/fourColorManage/warningList/vehicleWarning/index.vue"
import ControlWarning from "@/views/backOfficeSystem/fourColorManage/warningControl/controlWarning/index.vue"
import RegionalControl from "@/views/backOfficeSystem/fourColorManage/warningControl/regionalControl/index.vue"
import WrjWarning from "@/views/backOfficeSystem/fourColorManage/warningControl/wrjWarning/index.vue"
import PoliticalSecurityWarning from "@/views/backOfficeSystem/fourColorManage/warningControl/politicalSecurity/index.vue"
import Cs from '@/views/backOfficeSystem/ces/index.vue'
import FouColorWarning from "@/views/backOfficeSystem/fourColorManage/warningControl/fouColorWarning/index.vue";
import SevenWarning from "@/views/backOfficeSystem/fourColorManage/warningControl/sevenWarning/index.vue";
import IdentityWarning from "@/views/backOfficeSystem/fourColorManage/warningControl/identityWarning/index.vue";
import BehaviorWarning from "@/views/backOfficeSystem/fourColorManage/warningControl/behaviorWarning/index.vue";
import CombinedWarning from "@/views/backOfficeSystem/fourColorManage/warningControl/combinedWarning/index.vue";
import PortraitWarning from "@/views/backOfficeSystem/fourColorManage/warningList/portraitWarning/index.vue";
import VehicleWarning from "@/views/backOfficeSystem/fourColorManage/warningList/vehicleWarning/index.vue";
// import ControlWarning from "@/views/backOfficeSystem/fourColorManage/warningControl/controlWarning/index.vue";
import RegionalControl from "@/views/backOfficeSystem/fourColorManage/warningControl/regionalControl/index.vue";
import WrjWarning from "@/views/backOfficeSystem/fourColorManage/warningControl/wrjWarning/index.vue";
import PoliticalSecurityWarning from "@/views/backOfficeSystem/fourColorManage/warningControl/politicalSecurity/index.vue";
import ControlWarning from "@/views/backOfficeSystem/fourColorManage/warningControl/deploymentIntegration/index.vue";
import LabelWarning from "@/views/backOfficeSystem/fourColorManage/warningControl/iabelWarning/index.vue";
import { onMounted, ref } from "vue";
// "人像预警", "车辆预警",, "区域预警","无人机预警"
const butList = ref(["七类重点", '政保预警', "布控预警", "预警整合"])
const qh = ref('七类重点')
const value = ref('人像预警')
const Bqvalue = ref('身份预警')
const butStyle = ref()
const hasPermissin = ref(false)
const butList = ref(["七类重点", "政保预警", "布控预警", "预警整合"]);
const qh = ref("七类重点");
const value = ref("人像预警");
const Bqvalue = ref("身份预警");
const butStyle = ref();
const hasPermissin = ref(false);
const butStylChange = (val) => {
return ["人像预警", "车辆预警", "区域预警", "布控预警"].includes(val)
}
return ["人像预警", "车辆预警", "区域预警", "布控预警"].includes(val);
};
const BqbutStylChange = (val) => {
return ["身份预警", "行为预警", "组合预警"].includes(val)
}
return ["身份预警", "行为预警", "组合预警"].includes(val);
};
onMounted(() => {
// rolCode : 市情指领导(JS_666666)、市情指权限(JS_777777)、县情指权限(JS_888888)、县情指领导权限(JS_999999);
// depCode : 市情指领导(513030199509084123 )、市情指(340827200404141028)、县情指领导(540421196805217650)、朗县公安局指挥中心(县情指)(540422200010197030)、朗县公安局县城派出所(部门)(513425199305205211)
let rolCode = ['JS_666666', 'JS_777777', 'JS_888888', 'JS_999999']
let depCode = ['513030199509084123', '340827200404141028', '540421196805217650', '540422200010197030', '513425199305205211']
let roleData = getItem('roleList');
let deptData = getItem('deptId');
roleData.forEach(item => {
if (rolCode.includes(item.roleCode)) hasPermissin.value = true
})
deptData.forEach(item => {
if (depCode.includes(item.deptCode)) hasPermissin.value = true
})
let rolCode = ["JS_666666", "JS_777777", "JS_888888", "JS_999999"];
// let depCode = [
// "513030199509084123",
// "340827200404141028",
// "540421196805217650",
// "540422200010197030",
// "513425199305205211"
// ];
let roleData = getItem("roleList");
// let deptData = getItem("deptId");
roleData.forEach((item) => {
if (rolCode.includes(item.roleCode)) hasPermissin.value = true;
});
// deptData.forEach((item) => {
// if (depCode.includes(item.deptCode)) hasPermissin.value = true;
// });
if (hasPermissin.value) {
const data = butList.value.filter(item => item !== "预警整合")
const data = butList.value.filter((item) => item !== "预警整合");
// "身份预警", "行为预警", "组合预警"
butList.value = [...data, ...["标签预警", "预警整合"]]
butList.value = [...data, ...["标签预警", "预警整合"]];
}
})
});
</script>

View File

@ -0,0 +1,79 @@
<template>
<div class="dialog" v-if="dialogForm">
<div class="head_box">
<span class="title">身份预警{{ title }} </span>
<div>
<!-- <el-button type="primary" size="small" :loading="loading" @click="submit">保存</el-button> -->
<el-button size="small" @click="close">关闭</el-button>
</div>
</div>
<div class="form_cnt">
<FormMessage :formList="formData" disabled v-model="listQuery" ref="elform">
</FormMessage>
</div>
</div>
</template>
<script setup>
import FormMessage from "@/components/aboutTable/FormMessage.vue";
import { qcckGet, qcckPost } from "@/api/qcckApi.js";
import { ref, defineExpose, reactive, defineEmits, getCurrentInstance, watch } from "vue";
const emit = defineEmits(["updateDate"]);
const props = defineProps({
dict: {
type: Object,
default: () => { }
}
});
const { proxy } = getCurrentInstance();
const dialogForm = ref(false); //弹窗
const formData = ref([])
watch(() => props.dict, (res) => {
if (res) {
formData.value = [
{ label: "预警人员姓名", prop: "yjRyxm", type: "input" },
{ label: "预警人员身份证号码", prop: "yjRysfzh", type: "input" },
{ label: "电话", prop: "dh", type: "input" },
{ label: "预警标签", prop: "yjbqmc", type: "input" },
{ label: "今日预警次数", prop: "yjJrcs", type: "input",lx:"number" },
{ label: "标签颜色", prop: "yjJb", type: "select", options: props.dict.D_GS_SSYJ },
{ label: "预警标题", prop: "yjBt", type: "input" },
{ label: "预警内容", prop: "yjNr", type: "input" },
{ label: "预警时间", prop: "yjFssj", type: "input" },
{ label: "处置状态", prop: "czzt", type: "select", options: props.dict.D_GSXT_YJXX_CZZT },
{ label: "所属部门", prop: "ssbm", type: "input" },
{ label: "所属县局", prop: "ssxgaj", type: "input" },
{ label: "所属市局", prop: "sssgaj", type: "input" },
]
}
}, { deep: true, immediate: true })
const listQuery = ref({}); //表单
const loading = ref(false);
const title = ref("详情");
const init = (type, row) => {
dialogForm.value = true;
qcckGet({},'/mosty-gsxt/tbYjxx/getInfo/'+ row.id).then((res) => {
listQuery.value = res || {}
})
};
// 关闭
const close = () => {
listQuery.value = {};
loading.value = false;
dialogForm.value = false;
listQuery.value = {}
};
defineExpose({ init });
</script>
<style lang="scss" scoped>
@import "~@/assets/css/layout.scss";
@import "~@/assets/css/element-plus.scss";
::v-deep {
.el-form-item__content {
align-items: normal;
}
}
</style>

View File

@ -0,0 +1,50 @@
<template>
<el-dialog title="处置建议" v-model="visible" width="50%" v-if="visible" @close="closeHndle">
<el-form :model="form" ref="formRef" :rules="rules" label-width="120px" >
<el-form-item label="处置建议" prop="jynr">
<el-input v-model="form.jynr" placeholder="请输入处置建议" type="textarea"></el-input>
</el-form-item>
<div class="flex just-center">
<el-button type="primary" @click="okSubmit">确定</el-button>
<el-button @click="closeHndle">返回</el-button>
</div>
</el-form>
</el-dialog>
</template>
<script setup>
import { qcckPost } from "@/api/qcckApi.js";
import { ref , defineExpose} from 'vue'
const emit = defineEmits(['okSubmit'])
const visible = ref(false)
const formRef = ref()
const form = ref({})
const rules = ref({
jynr: [
{ required: true, message: '请输入处置建议', trigger: 'blur' }
]
})
const init = (row) => {
visible.value = true;
form.value.yjid = row.id;
}
const closeHndle = () => {
visible.value = false;
form.value = {};
}
const okSubmit = async () => {
await formRef.value.validate((valid) => {
if (!valid) return;
let params = {...form.value , lylx:'01'}
qcckPost(params,'/mosty-gsxt/yjxx/czjy/insert').then((res) => {
emit('okSubmit', {...form.value})
closeHndle()
})
})
}
defineExpose({
init
})
</script>

View File

@ -0,0 +1,478 @@
<!--预警指派展示组件 -->
<template>
<el-dialog :draggable="true" :model-value="modelValue" :title="title" :width="width" @close="close" append-to-body>
<div class="archive-container">
<div class="three-column-layout">
<!-- 重点人员基本信息页 -->
<div class="column">
<div class="column-header">重点人员基本信息</div>
<div class="info-section">
<div class="info-row">
<div class="info-item">
<span class="info-label">人员姓名</span>
<span class="info-value">{{ dataForm.yjRyxm }}</span>
</div>
<div class="info-item">
<span class="info-label">性别</span>
<span class="info-value">
<DictTag :value="dataForm.xbdm" :tag="false" :options="dict.D_BZ_XB" />
</span>
</div>
<div class="info-item">
<span class="info-label">年龄</span>
<span class="info-value">{{ dataForm.nl }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item full-width">
<span class="info-label">身份证号码</span>
<span class="info-value">{{ dataForm.yjRysfzh }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item full-width">
<span class="info-label">户籍地</span>
<span class="info-value">{{ dataForm.hjdXz }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item full-width">
<span class="info-label">现住地址</span>
<span class="info-value">{{ dataForm.xzdXz }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item full-width">
<span class="info-label">处置要求</span>
<span class="info-value">
<DictTag :value="dataForm.bkczyq" :tag="false" :options="dict.D_GS_BK_CZYQ" />
</span>
</div>
</div>
<div class="info-row">
<div class="info-item full-width">
<span class="info-label">预警级别</span>
<span class="info-value warning-level" :class="ys()">
<DictTag :value="dataForm.yjJb" :tag="false" :options="dict.D_BZ_YJJB" />
</span>
</div>
</div>
<div class="info-row">
<div class="info-item full-width">
<span class="info-label">布控起始时间</span>
<span class="info-value">{{ dataForm.bkkssj }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item full-width">
<span class="info-label">布控结束时间</span>
<span class="info-value">{{ dataForm.bkjssj }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item full-width">
<span class="info-label">所属单位</span>
<span class="info-value">{{ dataForm.ssbm }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item full-width">
<span class="info-label">布控单位</span>
<span class="info-value">{{ dataForm.gkbmmc }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item full-width">
<span class="info-label">布控原因</span>
<span class="info-value text-area"></span>
</div>
</div>
<div class="info-row">
<div class="info-item full-width">
<span class="info-label">一标三实</span>
<span class="info-value text-area"></span>
</div>
</div>
</div>
</div>
<!-- 重点人员活动信息页 -->
<div class="column">
<div class="column-header">重点人员活动信息</div>
<div class="info-section blue-bg">
<div class="info-row">
<div class="info-item full-width">
<span class="info-label">轨迹类别</span>
<span class="info-value">
<DictTag :value="dataForm.yjLylx" :tag="false" :options="dict.D_GS_ZDR_GJLB" />
</span>
</div>
</div>
<div class="info-row">
<div class="info-item full-width">
<span class="info-label">核查时间</span>
<span v-if="dataForm.fkList && dataForm.fkList.length > 0">{{ dataForm.fkList[0].czsj }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item full-width">
<span class="info-label">比对时间</span>
<span class="info-value">{{ dataForm.yjSj }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item full-width">
<span class="info-label">预警内容</span>
<span class="info-value text-area">{{ dataForm.yjNr }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item full-width">
<span class="info-label">预警信息提供单位</span>
<span class="info-value">{{ dataForm.jczmc }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item full-width">
<span class="info-label">预警信息接收单位</span>
<span class="info-value">{{ dataForm.ssbm }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item full-width">
<span class="info-label">二次指派单位</span>
<span class="info-value">
<div v-if="dataForm.zpList && dataForm.zpList.length > 0">
<span v-for="(item, index) in dataForm.zpList" :key="item.id">
{{ item.zpbm }}<span v-if="index < dataForm.zpList.length - 1">,</span>
</span>
</div>
</span>
</div>
</div>
<div class="info-row">
<div class="info-item full-width">
<span class="info-label">签收时限</span>
<span class="info-value">{{ dataForm.qssj }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item full-width">
<span class="info-label">反馈时限</span>
<span class="info-value">{{ dataForm.fksj }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item full-width">
<span class="info-label">处置建议</span>
<span class="info-value text-area">
<div v-if="dataForm.czjyList && dataForm.czjyList.length > 0">
<div v-for="(item, index) in dataForm.czjyList" :key="item.id">
<span>{{ `${index + 1}` + item.jynr }}</span>
</div>
</div>
</span>
</div>
</div>
<div class="info-row">
<div class="info-item full-width">
<span class="info-label">反馈内容</span>
<span class="info-value text-area">
<div v-if="dataForm.fkList && dataForm.fkList.length > 0">
<div v-for="(item, index) in dataForm.fkList" :key="item.id">
<span v-if="item.ckczbcxx">{{ `${index + 1}` + item.ckczbcxx }}</span>
</div>
</div>
</span>
</div>
</div>
</div>
</div>
<!-- 历史预警信息页 -->
<div class="column">
<div class="column-header">历史预警信息</div>
<div class="info-section">
<div class="history-item" v-for="(item, index) in dataForm.yjgjList" :key="item.id">
<span class="history-index">{{ `${index + 1}` }}</span>
<span class="history-content">{{ item.yjNr }}</span>
</div>
</div>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer" style="text-align: center;">
<el-button @click="close">关闭</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { defineProps, getCurrentInstance, watch, ref } from 'vue';
import { qcckGet } from '@/api/qcckApi.js'
const { proxy } = getCurrentInstance();
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
title: {
type: String,
default: '预警详情'
},
width: {
type: String,
default: '90%'
}, dataList: {
type: Object,
default: () => ({})
},
dict: {
type: Object,
default: () => ({})
}
});
// 定义事件
const emit = defineEmits(['update:modelValue']);
const loading = ref(false)
let abortController = null
const close = () => {
if (abortController) {
abortController.abort()
abortController = null
}
loading.value = false
emit('update:modelValue', false);
};
const dataForm = ref({});
watch(() => props.modelValue, (newVal) => {
if (newVal) {
getPart(props.dataList.id)
}
}, { deep: true });
const getPart = (id) => {
if (abortController) {
abortController.abort()
}
abortController = new AbortController()
loading.value = true
qcckGet({}, `/mosty-gsxt/tbYjxx/getInfo/${id}`, { signal: abortController.signal }).then(res => {
if (res) {
dataForm.value = res
} else {
dataForm.value = {}
}
}).catch(err => {
if (err.name !== 'AbortError') {
console.error('请求失败:', err)
}
}).finally(() => {
loading.value = false
})
}
const ys = () => {
switch (dataForm.value.yjJb) {
case '01':
return 'red';
case '02':
return 'orange';
case '03':
return 'yellow';
case '04':
return 'blue';
default:
return '';
}
}
</script>
<style scoped>
.archive-container {
padding: 0;
max-height: 500px;
overflow-y: auto;
}
.three-column-layout {
display: flex;
height: 100%;
}
.column {
flex: 1;
display: flex;
flex-direction: column;
min-width: 300px;
border: 1px solid #dcdfe6;
}
.column:not(:last-child) {
border-right: 1px solid #dcdfe6;
}
.column-header {
font-size: 16px;
font-weight: bold;
padding: 10px;
background: #f5f7fa;
color: #303133;
text-align: center;
border-bottom: 1px solid #dcdfe6;
}
.column .info-section {
flex: 1;
overflow-y: auto;
padding: 15px;
}
.info-section {
background: #ffffff;
}
.info-section.blue-bg {
background: #e6f7ff;
border: none;
}
.info-row {
display: flex;
flex-wrap: wrap;
margin-bottom: 12px;
}
.info-item {
flex: 1;
min-width: 300px;
display: flex;
align-items: center;
margin-bottom: 10px;
}
.info-item.full-width {
flex: 100%;
min-width: 100%;
}
.info-label {
font-weight: 600;
margin-right: 10px;
white-space: nowrap;
color: #303133;
font-size: 14px;
width: 100px;
flex-shrink: 0;
}
.info-value {
flex: 1;
padding: 4px 8px;
border: 1px solid #dcdfe6;
background: #ffffff;
min-height: 28px;
display: flex;
align-items: center;
font-size: 14px;
color: #606266;
}
.info-value.text-area {
min-height: 80px;
align-items: flex-start;
padding: 8px;
resize: vertical;
line-height: 1.5;
}
.info-value.warning-level {
background: #fffbe6;
border-color: #ffe58f;
color: #d48806;
font-weight: 600;
padding: 2px 6px;
font-size: 12px;
width: fit-content;
}
.info-value.warning-level.red {
background: #fef0f0;
border-color: #ffccc7;
color: #f56c6c;
}
.info-value.warning-level.orange {
background: #fffbe6;
border-color: #ffe58f;
color: #d48806;
}
.info-value.warning-level.yellow {
background: #fdf6ec;
border-color: #faecd8;
color: #e6a23c;
}
.info-value.warning-level.blue {
background: #ecf5ff;
border-color: #d9ecff;
color: #409eff;
}
.history-item {
margin-bottom: 8px;
padding: 6px 10px;
border: none;
background: #ffffff;
display: flex;
}
.history-index {
font-weight: bold;
margin-right: 8px;
color: #303133;
font-size: 14px;
min-width: 40px;
}
.history-content {
flex: 1;
color: #606266;
font-size: 14px;
line-height: 1.4;
}
/* 滚动条样式 */
.archive-container::-webkit-scrollbar,
.info-section::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.archive-container::-webkit-scrollbar-track,
.info-section::-webkit-scrollbar-track {
background: #f1f1f1;
}
.archive-container::-webkit-scrollbar-thumb,
.info-section::-webkit-scrollbar-thumb {
background: #c1c1c1;
}
.archive-container::-webkit-scrollbar-thumb:hover,
.info-section::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 按钮样式 */
.dialog-footer {
padding: 15px;
background: #f9f9f9;
border-top: 1px solid #e4e7ed;
text-align: center;
}
</style>

View File

@ -0,0 +1,88 @@
<template>
<div class="dialog" v-if="dialogForm">
<div class="head_box">
<span class="title">身份预警{{ title }} </span>
<div>
<!-- <el-button type="primary" size="small" :loading="loading" @click="submit">保存</el-button> -->
<el-button size="small" @click="close">关闭</el-button>
</div>
</div>
<div class="form_cnt">
<FormMessage :formList="formData" v-model="listQuery" ref="elform">
</FormMessage>
</div>
</div>
</template>
<script setup>
import FormMessage from "@/components/aboutTable/FormMessage.vue";
import { tbYjxxGetInfo,yjzxyjzxSfyjSelectList } from "@/api/yj.js";
import { IdCard } from '@/utils/validate.js'
import { ref, defineExpose, reactive, defineEmits, getCurrentInstance, watch } from "vue";
const emit = defineEmits(["updateDate"]);
const props = defineProps({
dict: {
type: Object,
default: () => { }
}
});
const { proxy } = getCurrentInstance();
const dialogForm = ref(false); //弹窗
const formData = ref([])
watch(() => props.dict, (res) => {
if (res) {
formData.value = [
{ label: "预警人姓名", prop: "xm", type: "input" },
{ label: "身份证号", prop: "sfzh", type: "input" },
{ label: "电话", prop: "dh", type: "input" },
{ label: "组合大类", prop: "sfdlmc", type: "input" },
{ label: "组合小类", prop: "sfzlmc", type: "input" },
{ label: "组合预警次数", prop: "sfcs", type: "input",lx:"number" },
{ label: "标签颜色", prop: "bqys", type: "select", options: props.dict.D_GS_SSYJ },
{ label: "预警时间", prop: "yjsj", type: "input" },
{ label: "预警分值", prop: "sffz", type: "input",lx:"number" },
{ label: "处置状态", prop: "czzt", type: "select", options: props.dict.D_GSXT_YJXX_CZZT },
{ label: "所属部门", prop: "ssbm", type: "input" },
{ label: "所属县局", prop: "ssxgaj", type: "input" },
{ label: "所属市局", prop: "sssgaj", type: "input" },
{ label: "接警员姓名", prop: "jjyxm", type: "input" },
]
}
}, { deep: true, immediate: true })
const listQuery = ref({}); //表单
const loading = ref(false);
const elform = ref();
const title = ref("详情");
const init = (type, row) => {
dialogForm.value = true;
yjzxyjzxSfyjSelectList(row.id).then(res => {
listQuery.value = {
...res,
nl: IdCard(res.yjRysfzh, 3) || "",
xb: IdCard(res.yjRysfzh, 2) || "",
xsd: res.xsd + '%'
}
})
};
// 关闭
const close = () => {
listQuery.value = {};
loading.value = false;
dialogForm.value = false;
listQuery.value = {}
};
defineExpose({ init });
</script>
<style lang="scss" scoped>
@import "~@/assets/css/layout.scss";
@import "~@/assets/css/element-plus.scss";
::v-deep {
.el-form-item__content {
align-items: normal;
}
}
</style>

View File

@ -0,0 +1,88 @@
<template>
<div class="dialog" v-if="dialogForm">
<div class="head_box">
<span class="title">行为预警{{ title }} </span>
<div>
<!-- <el-button type="primary" size="small" :loading="loading" @click="submit">保存</el-button> -->
<el-button size="small" @click="close">关闭</el-button>
</div>
</div>
<div class="form_cnt">
<FormMessage :formList="formData" v-model="listQuery" ref="elform">
</FormMessage>
</div>
</div>
</template>
<script setup>
import FormMessage from "@/components/aboutTable/FormMessage.vue";
import { tbYjxxGetInfo,yjzxXwyjSelectList } from "@/api/yj.js";
import { IdCard } from '@/utils/validate.js'
import { ref, defineExpose, reactive, defineEmits, getCurrentInstance, watch } from "vue";
const emit = defineEmits(["updateDate"]);
const props = defineProps({
dict: {
type: Object,
default: () => { }
}
});
const { proxy } = getCurrentInstance();
const dialogForm = ref(false); //弹窗
const formData = ref([])
watch(() => props.dict, (res) => {
if (res) {
formData.value = [
{ label: "预警人姓名", prop: "xm", type: "input" },
{ label: "身份证号", prop: "sfzh", type: "input" },
{ label: "电话", prop: "dh", type: "input" },
{ label: "行为大类", prop: "xldlmc", type: "input" },
{ label: "行为子类", prop: "xwzlmc", type: "input" },
{ label: "行为次数", prop: "xwcs", type: "input",lx:"number" },
{ label: "标签颜色", prop: "bqys", type: "select", options: props.dict.D_GS_SSYJ },
{ label: "预警时间", prop: "yjsj", type: "input" },
{ label: "行为分值", prop: "xwfz", type: "input",lx:"number" },
{ label: "处置状态", prop: "czzt", type: "select", options: props.dict.D_GSXT_YJXX_CZZT },
{ label: "所属部门", prop: "ssbm", type: "input" },
{ label: "所属县局", prop: "ssxgaj", type: "input" },
{ label: "所属市局", prop: "sssgaj", type: "input" },
{ label: "接警员姓名", prop: "jjyxm", type: "input" },
{ label: "行为描述", prop: "xwms", type: "textarea", width: "100%" },
]
}
}, { deep: true, immediate: true })
const listQuery = ref({}); //表单
const loading = ref(false);
const elform = ref();
const title = ref("详情");
const init = (row) => {
dialogForm.value = true;
yjzxXwyjSelectList(row.id).then(res => {
listQuery.value = {
...res,
nl: IdCard(res.yjRysfzh, 3) || "",
xb: IdCard(res.yjRysfzh, 2) || "",
xsd: res.xsd + '%'
}
})
};
// 关闭
const close = () => {
listQuery.value = {};
loading.value = false;
dialogForm.value = false;
listQuery.value = {}
};
defineExpose({ init });
</script>
<style lang="scss" scoped>
@import "~@/assets/css/layout.scss";
@import "~@/assets/css/element-plus.scss";
::v-deep {
.el-form-item__content {
align-items: normal;
}
}
</style>

View File

@ -0,0 +1,547 @@
<template>
<div>
<!-- 搜索 -->
<div ref="searchBox" class="mt10">
<QueryFormPanel
v-model="listQuery"
:fields="searchConfiger"
@search="onSearch"
>
<template #but>
<el-button type="primary" @click="exportExl" size="small"
>导出</el-button
>
<el-button type="primary" size="small" @click="handleQs"
>签收</el-button
>
</template>
</QueryFormPanel>
</div>
<!-- 表格 -->
<div
class="tabBox_zdy"
:style="{ height: pageData.tableHeight + 40 + 'px' }"
>
<WarnDataTable
:loading="pageData.tableConfiger.loading"
:tableHeight="pageData.tableHeight"
:data="pageData.tableData"
:columns="pageData.tableColumn"
table-class="warn-table"
@selectionChange="handleChooseData"
>
<template #czzt="{ row }">
<DictTag
:value="row.czzt"
:color="row.czzt === '01' ? '#ff2424' : '#1d72e8'"
:tag="false"
:options="D_GSXT_YJXX_CZZT"
/>
</template>
<template #xbdm="{ row }">
<DictTag :value="row.xbdm" :tag="false" :options="D_BZ_XB" />
</template>
<template #yjJb="{ row }">
<div :style="{ 'background-color': bqYs(row.yjJb) }">
<DictTag
:value="row.yjJb"
color="#fff"
:tag="false"
:options="D_BZ_YJJB"
/>
</div>
</template>
<template #qblyjb="{ row }">
<DictTag :value="row.qblyjb" :tag="false" :options="D_BZ_QBLYJB" />
</template>
<template #bksj="{ row }">
{{ row.bkkssj && row.bkjssj ? `${row.bkkssj} - ${row.bkjssj}` : "" }}
</template>
<template #xsd="{ row }"> {{ row.xsd }}% </template>
<template #cszt="{ row }">
<DictTag :value="row.cszt" :tag="false" :options="D_GS_CSZT" />
</template>
<template #bkyjlx="{ row }">
<DictTag :value="row.bkyjlx" :tag="false" :options="D_BZ_BKYJLX" />
</template>
<template #operation="{ row }">
<div style="display: flex; justify-content: space-between">
<span class="warning" @click="pushAssess(row)">全息档案</span>
<span class="primary" @click="handleCzjy(row)" v-if="roleCode"
>处置建议</span
>
<!-- <span type="primary" @click="showDetail(row)">转合成</span> -->
<!-- <span type="danger" @click="delDictItem(row.id)">转会商</span> -->
<span
class="success"
@click="handleQsFk(row, '签收')"
v-if="row.czzt == '01' && permission_sfqs"
>签收</span
>
<span
class="success"
@click="handleQsFk(row, '反馈')"
v-else-if="row.czzt == '02' && permission_sfqs"
>反馈</span
>
<!-- <span type="success" @click="handleQsFk(row, '查看反馈')" v-else>查看反馈</span> -->
<span class="primary" @click="openBox(row)">详情</span>
<span class="primary" @click="pushWarning(row)">指派</span>
</div>
</template>
</WarnDataTable>
<Pages
@changeNo="changeNo"
@changeSize="changeSize"
:tableHeight="pageData.tableHeight"
:pageConfiger="{
...pageData.pageConfiger,
total: pageData.total
}"
></Pages>
</div>
</div>
<FkDialog @change="getList" lx="05" />
<AddFrom
ref="addModelRef"
:dict="{ D_GSXT_YJXX_CZZT, D_BZ_YJJB, D_GS_SSYJ }"
/>
<!-- 处置建议 -->
<Czjy ref="czjyRef" @okSubmit="getList"></Czjy>
<ZpForm v-model="warningShow" :dataList="dataList" />
<!-- <Pagination v-model="paginationOpen" /> -->
<Pagination
v-model="paginationOpen"
:dataList="dataPres"
:dict="{
D_BZ_XB,
D_BZ_YJJB,
D_GS_QLZDRLX,
D_GS_ZDR_RYJB,
D_GS_ZDR_GJLB,
D_GS_BK_CZYQ
}"
/>
</template>
<script setup>
import { IdCard } from "@/utils/validate.js";
import Czjy from "./components/czjy.vue";
import { getItem, setItem } from "@/utils/storage";
import WarnDataTable from "@/views/backOfficeSystem/ces/components/WarnDataTable.vue";
import QueryFormPanel from "@/views/backOfficeSystem/ces/components/QueryFormPanel.vue";
import Pages from "@/components/aboutTable/Pages.vue";
import AddFrom from "./components/addFrom.vue";
import FkDialog from "@/views/backOfficeSystem/fourColorManage/warningControl/centerHome/components/fkDialog.vue";
import ZpForm from "@/views/backOfficeSystem/fourColorManage/warningControl/sevenWarning/zpForm.vue";
import { reactive, ref, onMounted, getCurrentInstance, nextTick } from "vue";
import { qcckGet, qcckPost } from "@/api/qcckApi.js";
import emitter from "@/utils/eventBus.js";
import { holographicProfileJump } from "@/utils/tools.js";
import { exportExlByObj } from "@/utils/exportExcel.js";
import { getMultiDictVal } from "@/utils/dict.js";
import Pagination from "./components/particulars.vue";
const czjyRef = ref();
const { proxy } = getCurrentInstance();
const searchBox = ref();
const {
D_GS_QLZDRLX,
D_BZ_YJLY,
D_GSXT_YJXX_CZZT,
D_GS_SSYJ,
D_BZ_YJJB,
D_BZ_BKLYS,
D_BZ_XB,
D_BZ_SF,
D_GS_CSZT,
D_GS_BKZT,
D_GS_ZDR_RYJB,
D_GS_ZDR_GJLB,
D_GS_BK_CZYQ,
D_BZ_SJLY,
D_BZ_QBLYJB,
D_BZ_BKYJLX
} = proxy.$dict(
"D_GS_QLZDRLX",
"D_BZ_YJLY",
"D_GSXT_YJXX_CZZT",
"D_GS_SSYJ",
"D_BZ_YJJB",
"D_BZ_BKLYS",
"D_BZ_XB",
"D_BZ_SF",
"D_GS_CSZT",
"D_GS_BKZT",
"D_GS_ZDR_RYJB",
"D_GS_ZDR_GJLB",
"D_GS_BK_CZYQ",
"D_BZ_SJLY",
"D_BZ_QBLYJB",
"D_BZ_BKYJLX"
);
const dict = reactive({ D_GSXT_YJXX_CZZT, D_GS_SSYJ });
// 搜索配置
const searchConfiger = ref([
{
label: "处置状态",
prop: "czzt",
showType: "select",
options: D_GSXT_YJXX_CZZT
},
{
label: "预警时间",
prop: "startTime",
showType: "datetimerange",
placeholder: "请选择预警时间"
},
{
label: "姓名",
prop: "yjRyxm",
showType: "input",
placeholder: "请输入姓名"
},
{
label: "身份证号",
prop: "yjRysfzh",
showType: "input",
placeholder: "请输入身份证号"
},
{
label: "管控级别",
prop: "qblyjb",
showType: "select",
options: D_BZ_QBLYJB
},
{
label: "预警级别",
prop: "yjJb",
showType: "select",
options: D_BZ_YJJB,
placeholder: "请选择预警级别",
multiple: true
},
{
label: "布控时间",
prop: "bksj",
showType: "daterange",
placeholder: "请选择布控开始时间"
},
{
label: "接收单位",
prop: "ssbmdm",
showType: "department",
placeholder: "请选择接收单位"
},
{
label: "布控单位",
prop: "gkbmdm",
showType: "department",
placeholder: "请选择布控单位"
},
{
label: "比对源",
prop: "bkyjlx",
showType: "select",
options: D_BZ_BKYJLX,
placeholder: "请选择比对源"
},
{
label: "超时状态",
prop: "cszt",
placeholder: "请选择超时状态",
showType: "select",
options: D_GS_CSZT
},
{
label: "在控状态",
prop: "zkzt",
showType: "select",
options: D_GS_BKZT,
placeholder: "请选择在控状态"
}
]);
const ORDIMG = "https://89.40.7.122:38496/image";
const IMGYM = "https://sg.lz.dsj.xz/dhimage";
const permission_sfqs = ref(false);
const roleCode = ref(false);
const queryFrom = ref({});
// 页面数据
const pageData = reactive({
tableData: [],
keyCount: 0,
tableConfiger: {
rowHieght: 61,
showSelectType: "checkBox",
loading: false,
haveControls: true
},
total: 0,
pageConfiger: {
pageSize: 20,
pageCurrent: 1
},
controlsWidth: 180, //操作栏宽度
tableColumn: [
{ type: "index", label: "序号", width: 55, align: "center" },
{ label: "处置状态", align: "center", width: 70, slotName: "czzt" },
{ prop: "yjSj", label: "预警时间", width: 150 },
{ prop: "yjRyxm", label: "人员姓名", width: 70, align: "center" },
{ label: "性别", width: 50, align: "center", slotName: "xbdm" },
{ prop: "nl", label: "年龄", width: 50, align: "center" },
{ prop: "yjRysfzh", label: "身份证号", width: 150, align: "center" },
{ label: "管控级别", width: 80, align: "center", slotName: "qblyjb" },
{ label: "预警级别", width: 80, align: "center", slotName: "yjJb" },
{ label: "布控时间", width: 80, align: "center", slotName: "bksj" },
{ prop: "ssbm", label: "接收单位", width: 100, align: "center" },
{ label: "布控单位", prop: "gkbmmc", align: "center", width: 100 },
{ prop: "bkyjlx", label: "比对源", align: "center", width: 80 },
{ label: "相似度", slotName: "xsd", align: "center", width: 50 },
{ label: "预警内容", prop: "yjNr", align: "center" },
{ label: "操作", width: 180, slotName: "operation" },
{ label: "超时状态", width: 80, align: "center", slotName: "cszt" },
{ label: "在控状态", width: 70, align: "center", slotName: "zkzt" }
// { prop: "bkkssj", label: "布控开始时间", align: "center" },
// { prop: "bkjssj", label: "布控结束时间", align: "center" }
//
// { label: "预警级别", width: 80, align: "center", slotName: "yjJb" },
//
// { label: "布控来源", align: "center", slotName: "bkly", width: 70 },
// { prop: "bkfw", label: "布控范围", align: "center", width: 70 },
// { label: "处置要求", width: 70, slotName: "bkczyq", align: "center" },
// { label: "预警内容", prop: "yjNr", align: "center" },
// { label: "相似度", slotName: "xsd", align: "center", width: 50 },
// { label: "所属部门", prop: "ssbm", align: "center" },
// { label: "数据来源", slotName: "yjLylx", align: "center" },
]
});
onMounted(() => {
let str = getItem("deptId") ? getItem("deptId")[0].deptLevel : "";
permission_sfqs.value = str.startsWith("2" || "3") ? false : true;
let rols = getItem("roleList") ? getItem("roleList") : [];
let obj = rols.find((item) => {
return ["JS_666666", "JS_777777", "JS_888888"].includes(item.roleCode);
});
roleCode.value = obj ? true : false;
tabHeightFn();
getList();
});
const onSearch = (val) => {
queryFrom.value = {
...queryFrom.value,
...val,
startTime: val.startTime ? val.startTime[0] : "",
endTime: val.startTime ? val.startTime[1] : "",
bkkssj: val.bksj ? val.bksj[0] : "",
bkjssj: val.bksj ? val.bksj[1] : "",
yjJb: val.yjJb?.join(",") || ""
};
pageData.pageConfiger.pageCurrent = 1;
getList();
};
const reset = () => {
delete queryFrom.value.ksfz;
delete queryFrom.value.jsfz;
};
const changeNo = (val) => {
pageData.pageConfiger.pageCurrent = val;
getList();
};
const changeSize = (val) => {
pageData.pageConfiger.pageSize = val;
getList();
};
const getList = () => {
pageData.tableConfiger.loading = true;
let params = {
...queryFrom.value,
pageCurrent: pageData.pageConfiger.pageCurrent,
pageSize: pageData.pageConfiger.pageSize,
yjlb: "01"
};
qcckPost(params, "/mosty-gsxt/tbYjxx/getPageList")
.then((res) => {
pageData.tableData = res?.records || [];
pageData.total = res?.total || 0;
pageData.tableConfiger.loading = false;
})
.catch(() => {
pageData.tableConfiger.loading = false;
});
};
// 处理签收
const handleQsFk = (val, type) => {
switch (type) {
case "签收":
proxy
.$confirm("是否确定要签收?", "警告", { type: "warning" })
.then(() => {
qcckPost({ id: val.id }, "/mosty-gsxt/mosty-gsxt/tbYjxx/yjqs").then(
() => {
val.czzt = "02";
getList();
proxy.$message({ type: "success", message: "签收成功" });
}
);
});
break;
case "反馈":
case "查看反馈":
emitter.emit("openFkDialog", { id: val.id, type });
break;
}
};
const pushAssess = (val) => {
return holographicProfileJump(val?.yjLx, val); // 全息档案跳转
};
const bqYs = (val) => {
switch (val) {
case "01":
return "#ff0202";
case "02":
return "#ff8c00";
case "03":
return "#bdbd00";
case "04":
return "#0000ff";
default:
return "";
}
};
// 新增
const addModelRef = ref(null);
const openAddFrom = (row) => {
addModelRef.value.init("add", row);
};
const handleCzjy = (row) => {
czjyRef.value.init(row);
};
// 表格高度计算
const tabHeightFn = () => {
pageData.tableHeight =
window.innerHeight - searchBox.value.offsetHeight - 230;
window.onresize = function () {
tabHeightFn();
};
};
// 指派
const dataList = ref(null);
const warningShow = ref(false);
const pushWarning = (val) => {
warningShow.value = true;
dataList.value = val;
};
/** 选中项 */
const selectRows = ref([]);
const handleChooseData = (val) => {
selectRows.value = val;
};
const exportExl = () => {
const titleObj = {
czzt_cname: "处置状态",
yjSj: "预警时间",
yjRyxm: "姓名",
nl_cname: "年龄", // IdCard(row.yjRysfzh, 3)
yjLylx: "数据来源",
xb_cname: "性别",
yjJb_cname: "预警级别",
xsd_cname: "相似度",
yjDz: "预警地点",
yjCs: "预警次数",
yjRysjh: "布控手机号",
yjClcph: "布控车牌号",
yjRysfzh: "身份证"
};
/** 导出【选中】的数据 (没有就全部)*/
const needArr =
selectRows.value?.length > 0 ? selectRows.value : pageData.tableData;
const data = needArr.map((item) => {
return {
...item,
nl_cname: IdCard(item.yjRysfzh, 3),
xb_cname: IdCard(item.yjRysfzh, 2),
xsd_cname: (item.xsd > 0 ? item.xsd : 0) + "%",
yjJb_name: getMultiDictVal(item.yjJb, D_GS_SSYJ),
czzt_name: getMultiDictVal(item.czzt, D_GSXT_YJXX_CZZT),
yjJb_cname: getMultiDictVal(item.yjJb, D_BZ_YJJB)
};
});
exportExlByObj(titleObj, data, "预警布控");
};
const handleQs = () => {
if (selectRows.value?.length === 0)
return proxy.$message({ type: "warning", message: "请选择要签收的预警" });
let wqs = selectRows.value.filter((item) => item.czzt == "01");
if (wqs.length == 0)
return proxy.$message({
type: "warning",
message: "数据都已签收,请选择未签收的数据"
});
let yqs = selectRows.value.filter((item) => item.czzt == "02");
let texy =
yqs.length > 0
? `${yqs.length}条已签收预警数据,确认要签收${wqs.length}条未签收预警数据吗?`
: "确认要签收所有预警数据吗?";
proxy
.$confirm(texy, "警告", { type: "warning" })
.then(() => {
let ids = wqs.map((item) => item.id);
qcckPost({ ids }, "/mosty-gsxt/tbYjxx/batchQs")
.then(() => {
proxy.$message({ type: "success", message: "成功" });
getList();
})
.catch(() => {
proxy.$message({ type: "error", message: "失败" });
});
})
.catch(() => {});
};
// 详情
const paginationOpen = ref(false);
const dataPres = ref({});
const openBox = (val) => {
paginationOpen.value = true;
dataPres.value = val;
};
</script>
<style lang="scss" scoped>
.el-loading-mask {
background: rgba(0, 0, 0, 0.5) !important;
}
.tabBox_zdy {
background-color: #fff;
}
::v-deep .el-table .cell {
padding: 0;
}
::v-deep .el-table .el-table__cell {
padding: 4px 0;
}
</style>

View File

@ -0,0 +1,105 @@
<template>
<div class="warning-item" >
<el-divider content-position="left">预警内容</el-divider>
<div class="item-row" style="border: none;"> {{ props.row.yjNr }} </div>
<el-empty v-if="!props.row.yjNr" :image-size="0.5" description="暂无数据" />
<el-divider content-position="left">处置建议</el-divider>
<div class="item-row" v-for="(it,idx) in list" :key="idx">
<div class="info-item">
<span class="text">预警人姓名{{ it.jryXm }}</span>
<span class="text">建议时间{{ it.jysj }}</span>
<span class="text">所属部门{{ it.ssbm }}</span>
</div>
<div class="info-item">建议内容<span>{{ it.jynr || '暂无' }}</span></div>
</div>
<div>
<el-empty v-if="list.length === 0" :image-size="0.5" description="暂无数据" />
</div>
<el-divider content-position="left">反馈内容</el-divider>
<div class="item-row" v-for="(it,idx) in Fklist" :key="idx">
<div class="info-item">
<span class="text">处置地址{{ it.czdz }}</span>
<span class="text">处置时间{{ it.czsj }}</span>
</div>
<div class="info-item">
<span class="text">常控不尿检理由<span>{{ it.ckbnjly || '暂无' }}</span></span>
<span class="text">常控处置反馈补充信息<span>{{ it.ckczbcxx || '暂无' }}</span></span>
<span class="text">常控立线侦察评估<span>{{ it.cklxzcpg || '暂无' }}</span></span>
<span class="text">常控立线侦察评估依据<span>{{ it.cklxzcpgyj || '暂无' }}</span></span>
</div>
</div>
<div>
<el-empty v-if="Fklist.length === 0" :image-size="0.5" description="暂无数据" />
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { qcckPost,qcckGet } from "@/api/qcckApi.js";
const props = defineProps({
/** 表格行数据 */
row: {
type: Object,
default: () => ({})
},
dict: {
type: Object,
default: () => ({
D_GS_SSYJ: [],
D_GSXT_YJXX_CZZT: []
})
},
})
const list = ref([])
const Fklist = ref([])
onMounted(() => {
if(!props.row.id) return;
qcckPost({yjid: props.row.id},'/mosty-gsxt/yjxx/czjy/getPageList').then((res) => {
list.value = res.records || []
})
qcckGet({},'/mosty-gsxt/tbYjxx/getInfo/'+ props.row.id).then((res) => {
Fklist.value = res.fkList || []
})
})
</script>
<style lang="scss" scoped>
.warning-item {
width: 100%;
padding: 15px;
box-sizing: border-box;
border-radius: 8px;
background-color: #fafafa;
}
.item-row{
border-bottom: 1px dashed #e8e8e8;
line-height: 36px;
padding-bottom: 10px;
margin-bottom: 10px;
padding-left: 2rem;
box-sizing: border-box;
&:nth-last-child(1){
border-bottom: none;
}
}
.info-item{
line-height: 36px;
width: 100%;
.text{
display: inline-block;
width: 25%;
margin-right: 10px;
}
}
::v-deep .el-empty{
--el-empty-padding: 0px;
margin-bottom: 18px;
--el-empty-description-margin-top: 10px;
}
</style>

View File

@ -0,0 +1,50 @@
<template>
<el-dialog title="处置建议" v-model="visible" width="50%" v-if="visible" @close="closeHndle">
<el-form :model="form" ref="formRef" :rules="rules" label-width="120px" >
<el-form-item label="处置建议" prop="jynr">
<el-input v-model="form.jynr" placeholder="请输入处置建议" type="textarea"></el-input>
</el-form-item>
<div class="flex just-center">
<el-button type="primary" @click="okSubmit">确定</el-button>
<el-button @click="closeHndle">返回</el-button>
</div>
</el-form>
</el-dialog>
</template>
<script setup>
import { qcckPost } from "@/api/qcckApi.js";
import { ref , defineExpose} from 'vue'
const emit = defineEmits(['okSubmit'])
const visible = ref(false)
const formRef = ref()
const form = ref({})
const rules = ref({
jynr: [
{ required: true, message: '请输入处置建议', trigger: 'blur' }
]
})
const init = (row) => {
visible.value = true;
form.value.yjid = row.id;
}
const closeHndle = () => {
visible.value = false;
form.value = {};
}
const okSubmit = async () => {
await formRef.value.validate((valid) => {
if (!valid) return;
let params = {...form.value , lylx:'02'}
qcckPost(params,'/mosty-gsxt/yjxx/czjy/insert').then((res) => {
emit('okSubmit', {...form.value})
closeHndle()
})
})
}
defineExpose({
init
})
</script>

View File

@ -0,0 +1,57 @@
<template>
<el-dialog :title="`组合预警次数详情(${pageData.tableData.length}`" v-model="dialogVisible" width="60%">
<MyTable
:tableData="pageData.tableData"
:tableColumn="pageData.tableColumn"
:tableHeight="pageData.tableHeight"
:key="pageData.keyCount"
:tableConfiger="pageData.tableConfiger"
>
</MyTable>
</el-dialog>
</template>
<script setup>
import { qcckGet } from "@/api/qcckApi.js";
import MyTable from "@/components/aboutTable/MyTable.vue";
import { ref , reactive , defineExpose} from 'vue'
const dialogVisible = ref(false)
const pageData = reactive({
tableData: [{jqbh:'JQBH-43',bjr:'张三',bjrdh:'15665255545',bjrsfzh:'510156565656525565',yjnr:'xxxxxxxxx',jsj:'2023-08-24 15:00:00'}], //表格数据
keyCount: 0,
tableConfiger: {
rowHieght: 61,
loading: false,
haveControls: false,
},
tableHeight:600,
total: 0,
pageConfiger: {
pageSize: 20,
pageCurrent: 1
}, //分页
tableColumn: [
{ label: "警情编号", prop: "jjdbh"},
{ label: "报警人", prop: "bjrmc" },
{ label: "报警人电话", prop: "bjdh" },
{ label: "报警人身份证", prop: "bjrzjhm"},
{ label: "预警内容", prop: "bjnr", showOverflowTooltip: true },
{ label: "报警时间", prop: "bjsj", showOverflowTooltip: true },
]
});
const init = (row) => {
dialogVisible.value = true;
pageData.tableConfiger.loading = true;
pageData.tableData = []
qcckGet({yjid:row.id},'/mosty-gsxt/yjzxSfyjxq/selectList').then((res)=>{
pageData.tableData = res || [];
pageData.tableConfiger.loading = false;
}).catch(()=>{
pageData.tableConfiger.loading = false;
})
}
defineExpose({
init
})
</script>

View File

@ -0,0 +1,88 @@
<template>
<div class="dialog" v-if="dialogForm">
<div class="head_box">
<span class="title">身份预警{{ title }} </span>
<div>
<!-- <el-button type="primary" size="small" :loading="loading" @click="submit">保存</el-button> -->
<el-button size="small" @click="close">关闭</el-button>
</div>
</div>
<div class="form_cnt">
<FormMessage :formList="formData" v-model="listQuery" ref="elform">
</FormMessage>
</div>
</div>
</template>
<script setup>
import FormMessage from "@/components/aboutTable/FormMessage.vue";
import { tbYjxxGetInfo,yjzxyjzxSfyjSelectList } from "@/api/yj.js";
import { IdCard } from '@/utils/validate.js'
import { ref, defineExpose, reactive, defineEmits, getCurrentInstance, watch } from "vue";
const emit = defineEmits(["updateDate"]);
const props = defineProps({
dict: {
type: Object,
default: () => { }
}
});
const { proxy } = getCurrentInstance();
const dialogForm = ref(false); //弹窗
const formData = ref([])
watch(() => props.dict, (res) => {
if (res) {
formData.value = [
{ label: "预警人姓名", prop: "xm", type: "input" },
{ label: "身份证号", prop: "sfzh", type: "input" },
{ label: "电话", prop: "dh", type: "input" },
{ label: "组合大类", prop: "sfdlmc", type: "input" },
{ label: "组合小类", prop: "sfzlmc", type: "input" },
{ label: "组合预警次数", prop: "sfcs", type: "input",lx:"number" },
{ label: "标签颜色", prop: "bqys", type: "select", options: props.dict.D_GS_SSYJ },
{ label: "预警时间", prop: "yjsj", type: "input" },
{ label: "预警分值", prop: "sffz", type: "input",lx:"number" },
{ label: "处置状态", prop: "czzt", type: "select", options: props.dict.D_GSXT_YJXX_CZZT },
{ label: "所属部门", prop: "ssbm", type: "input" },
{ label: "所属县局", prop: "ssxgaj", type: "input" },
{ label: "所属市局", prop: "sssgaj", type: "input" },
{ label: "接警员姓名", prop: "jjyxm", type: "input" },
]
}
}, { deep: true, immediate: true })
const listQuery = ref({}); //表单
const loading = ref(false);
const elform = ref();
const title = ref("详情");
const init = (type, row) => {
dialogForm.value = true;
yjzxyjzxSfyjSelectList(row.id).then(res => {
listQuery.value = {
...res,
nl: IdCard(res.yjRysfzh, 3) || "",
xb: IdCard(res.yjRysfzh, 2) || "",
xsd: res.xsd + '%'
}
})
};
// 关闭
const close = () => {
listQuery.value = {};
loading.value = false;
dialogForm.value = false;
listQuery.value = {}
};
defineExpose({ init });
</script>
<style lang="scss" scoped>
@import "~@/assets/css/layout.scss";
@import "~@/assets/css/element-plus.scss";
::v-deep {
.el-form-item__content {
align-items: normal;
}
}
</style>

View File

@ -0,0 +1,88 @@
<template>
<div class="dialog" v-if="dialogForm">
<div class="head_box">
<span class="title">行为预警{{ title }} </span>
<div>
<!-- <el-button type="primary" size="small" :loading="loading" @click="submit">保存</el-button> -->
<el-button size="small" @click="close">关闭</el-button>
</div>
</div>
<div class="form_cnt">
<FormMessage :formList="formData" v-model="listQuery" ref="elform">
</FormMessage>
</div>
</div>
</template>
<script setup>
import FormMessage from "@/components/aboutTable/FormMessage.vue";
import { tbYjxxGetInfo,yjzxXwyjSelectList } from "@/api/yj.js";
import { IdCard } from '@/utils/validate.js'
import { ref, defineExpose, reactive, defineEmits, getCurrentInstance, watch } from "vue";
const emit = defineEmits(["updateDate"]);
const props = defineProps({
dict: {
type: Object,
default: () => { }
}
});
const { proxy } = getCurrentInstance();
const dialogForm = ref(false); //弹窗
const formData = ref([])
watch(() => props.dict, (res) => {
if (res) {
formData.value = [
{ label: "预警人姓名", prop: "xm", type: "input" },
{ label: "身份证号", prop: "sfzh", type: "input" },
{ label: "电话", prop: "dh", type: "input" },
{ label: "行为大类", prop: "xldlmc", type: "input" },
{ label: "行为子类", prop: "xwzlmc", type: "input" },
{ label: "行为次数", prop: "xwcs", type: "input",lx:"number" },
{ label: "标签颜色", prop: "bqys", type: "select", options: props.dict.D_GS_SSYJ },
{ label: "预警时间", prop: "yjsj", type: "input" },
{ label: "行为分值", prop: "xwfz", type: "input",lx:"number" },
{ label: "处置状态", prop: "czzt", type: "select", options: props.dict.D_GSXT_YJXX_CZZT },
{ label: "所属部门", prop: "ssbm", type: "input" },
{ label: "所属县局", prop: "ssxgaj", type: "input" },
{ label: "所属市局", prop: "sssgaj", type: "input" },
{ label: "接警员姓名", prop: "jjyxm", type: "input" },
{ label: "行为描述", prop: "xwms", type: "textarea", width: "100%" },
]
}
}, { deep: true, immediate: true })
const listQuery = ref({}); //表单
const loading = ref(false);
const elform = ref();
const title = ref("详情");
const init = (row) => {
dialogForm.value = true;
yjzxXwyjSelectList(row.id).then(res => {
listQuery.value = {
...res,
nl: IdCard(res.yjRysfzh, 3) || "",
xb: IdCard(res.yjRysfzh, 2) || "",
xsd: res.xsd + '%'
}
})
};
// 关闭
const close = () => {
listQuery.value = {};
loading.value = false;
dialogForm.value = false;
listQuery.value = {}
};
defineExpose({ init });
</script>
<style lang="scss" scoped>
@import "~@/assets/css/layout.scss";
@import "~@/assets/css/element-plus.scss";
::v-deep {
.el-form-item__content {
align-items: normal;
}
}
</style>

View File

@ -0,0 +1,109 @@
<template>
<div class="dialog" v-if="dialogForm">
<div class="head_box">
<span class="title">组合预警{{ title }} </span>
<div>
<!-- <el-button type="primary" size="small" :loading="loading" @click="submit">保存</el-button> -->
<el-button size="small" @click="close">关闭</el-button>
</div>
</div>
<div class="form_cnt">
<FormMessage :formList="formData" v-model="listQuery" ref="elform">
</FormMessage>
</div>
</div>
</template>
<script setup>
import FormMessage from "@/components/aboutTable/FormMessage.vue";
import { tbYjxxGetInfo, yjzxZhyjSelectList } from "@/api/yj.js";
import { IdCard } from "@/utils/validate.js";
import {
ref,
defineExpose,
reactive,
defineEmits,
getCurrentInstance,
watch
} from "vue";
const emit = defineEmits(["updateDate"]);
const props = defineProps({
dict: {
type: Object,
default: () => {}
}
});
const { proxy } = getCurrentInstance();
const dialogForm = ref(false); //弹窗
const formData = ref([]);
watch(
() => props.dict,
(res) => {
if (res) {
formData.value = [
{ label: "预警人姓名", prop: "xm", type: "input" },
{ label: "身份证号", prop: "sfzh", type: "input" },
{ label: "电话", prop: "dh", type: "input" },
{ label: "组合大类", prop: "sfdlmc", type: "input" },
{ label: "组合小类", prop: "sfzlmc", type: "input" },
{ label: "组合预警次数", prop: "sfcs", type: "input", lx: "number" },
{
label: "标签颜色",
prop: "bqys",
type: "select",
options: props.dict.D_GS_SSYJ
},
{ label: "预警时间", prop: "yjsj", type: "input" },
{ label: "预警分值", prop: "sffz", type: "input", lx: "number" },
{
label: "处置状态",
prop: "czzt",
type: "select",
options: props.dict.D_GSXT_YJXX_CZZT
},
{ label: "所属部门", prop: "ssbm", type: "input" },
{ label: "所属县局", prop: "ssxgaj", type: "input" },
{ label: "所属市局", prop: "sssgaj", type: "input" },
{ label: "接警员姓名", prop: "jjyxm", type: "input" }
// { label: "预警内容", prop: "yjNr", type: "textarea", width: "100%" },
];
}
},
{ deep: true, immediate: true }
);
const listQuery = ref({}); //表单
const loading = ref(false);
const elform = ref();
const title = ref("详情");
const init = (row) => {
dialogForm.value = true;
yjzxZhyjSelectList(row.id).then((res) => {
listQuery.value = {
...res,
nl: IdCard(res.yjRysfzh, 3) || "",
xb: IdCard(res.yjRysfzh, 2) || "",
xsd: res.xsd + "%"
};
});
};
// 关闭
const close = () => {
listQuery.value = {};
loading.value = false;
dialogForm.value = false;
listQuery.value = {};
};
defineExpose({ init });
</script>
<style lang="scss" scoped>
@import "~@/assets/css/layout.scss";
@import "~@/assets/css/element-plus.scss";
::v-deep {
.el-form-item__content {
align-items: normal;
}
}
</style>

View File

@ -0,0 +1,402 @@
<template>
<div>
<!-- 搜索 -->
<div ref="searchBox" class="mt10 mb10">
<Searchs :searchArr="searchConfiger" @submit="onSearch" @reset="reset" :key="pageData.keyCount">
<template #jfd>
<div>
<el-input v-model="queryFrom.ksfz" type="number" placeholder="开始身份分值" style="width: 130px"></el-input>
<span style="color: #333;margin: 0 4px;"></span>
<el-input v-model="queryFrom.jsfz" type="number" placeholder="结束身份分值" style="width: 130px"></el-input>
</div>
</template>
</Searchs>
</div>
<PageTitle :malginLeft="10" :height="35" backgroundColor="#ffff" :marginBottom="5" :marginTop="5">
<template #left>
<el-button type="primary" size="small" @click="exportExl">导出</el-button>
<el-button type="primary" size="small" @click="handleQs">签收</el-button>
</template>
</PageTitle>
<!-- 表格 -->
<div class="tabBox tabBox_zdy heightBox" :style="{ height: (pageData.tableHeight + 40) + 'px' }">
<MyTable :tableData="pageData.tableData" :tableColumn="pageData.tableColumn" :tableHeight="pageData.tableHeight"
:key="pageData.keyCount" :tableConfiger="pageData.tableConfiger" :controlsWidth="pageData.controlsWidth" expand
@chooseData="handleChooseData">
<template #expand="{ props }">
<div style="max-width: 100%">
<Items :row="props || {}" :dict="dict" />
</div>
</template>
<template #sfcs="{ row }">
<span style="color: #0072ff;" @click="handleClick(row)">{{ row.sfcs }}</span>
</template>
<template #czzt="{ row }">
<DictTag :value="row.czzt" :options="D_GSXT_YJXX_CZZT" />
</template>
<template #sffz>
<el-table-column prop="bqfz" width="80" align="center" label="标签分值" />
<el-table-column prop="pzxs" width="60" align="center" label="系数" />
<el-table-column prop="sffz" width="90" align="center" label="计算分值"/>
</template>
<template #controls="{ row }">
<el-link type="warning" @click="pushAssess(row)">全息档案</el-link>
<el-link type="primary" @click="handleCzjy(row)" v-if="roleCode">处置建议</el-link>
<el-link type="primary" @click="chooseJfFun(row)">配置系统</el-link>
<!-- <el-link type="primary" @click="showDetail(row)">转合成</el-link>
<el-link type="danger" @click="handleQsFk(row)">转会商</el-link> -->
<el-link type="success" @click="handleQsFk(row, '签收')" v-if="row.czzt == '01' && permission_sfqs">签收</el-link>
<el-link type="success" @click="handleQsFk(row, '反馈')"
v-else-if="row.czzt == '02' && permission_sfqs">反馈</el-link>
<!-- <el-link type="success" @click="handleQsFk(row, '查看反馈')" v-else>查看反馈</el-link> -->
<el-link type="primary" @click="openAddFrom(row)">详情</el-link>
</template>
</MyTable>
<Pages @changeNo="changeNo" @changeSize="changeSize" :tableHeight="pageData.tableHeight" :pageConfiger="{
...pageData.pageConfiger,
total: pageData.total
}"></Pages>
</div>
</div>
<Detail ref="detailRef" />
<HolographicArchive v-model="assessShow" :dataList="dataList" />
<FkDialog @change="getList" lx="03" />
<Information v-model="showDialog" title="发送指令" @submit='submit' @close='closeFszl'>
<SemdFqzl ref="semdFqzlRef" :itemData="itemData" @handleClose="handleClose" identification="yj"
:tacitly="tacitly" />
</Information>
<AddFrom ref="addModelRef" :dict="{ D_GSXT_YJXX_CZZT, D_BZ_YJJB, D_GS_SSYJ }" />
<!-- 处置建议 -->
<Czjy ref="czjyRef" @okSubmit="getList"></Czjy>
<ChooseJf v-model="chooseJfShow" titleValue="选择系数" :Single="false" :chooseJfBh="chooseJfBh" url="/yjzxSfyj/sjxspz"
:roleIds="roleIds" />
</template>
<script setup>
import Czjy from './components/czjy.vue'
import { getItem, setItem } from '@/utils/storage'
import PageTitle from "@/components/aboutTable/PageTitle.vue";
import Searchs from "@/components/aboutTable/Search.vue";
import MyTable from "@/components/aboutTable/MyTable.vue";
import Pages from "@/components/aboutTable/Pages.vue";
import HolographicArchive from '@/views/home/components/holographicArchive.vue'
import Information from "@/views/home/model/information.vue";
import SemdFqzl from '@/components/instructionHasBeen/sendFqzl.vue'
import AddFrom from "./components/addFrom.vue";
import ChooseJf from '@/components/ChooseList/ChooseJf/index.vue'
import FkDialog from "@/views/backOfficeSystem/fourColorManage/warningControl/centerHome/components/fkDialog.vue";
import { reactive, ref, onMounted, getCurrentInstance, nextTick } from "vue";
import { qcckGet, qcckPost } from "@/api/qcckApi.js";
import { yjzxSfyjSelectList, yjzxyjzxSfyjSelectList } from "@/api/yj.js";
import { tbGsxtBqglSelectList } from '@/api/zdr'
import Detail from './components/detail.vue'
import { watch } from "vue";
import emitter from "@/utils/eventBus.js";
import { holographicProfileJump } from "@/utils/tools.js"
import Items from "./item/items.vue"
import { exportExlByObj } from "@/utils/exportExcel.js"
import { getMultiDictVal } from "@/utils/dict.js"
const czjyRef = ref()
const { proxy } = getCurrentInstance();
const searchBox = ref();
const { D_GSXT_YJXX_CZZT, D_GS_SSYJ, D_BZ_YJJB } = proxy.$dict("D_GSXT_YJXX_CZZT", "D_GS_SSYJ", 'D_BZ_YJJB')
const dict = reactive({ D_GSXT_YJXX_CZZT, D_GS_SSYJ })
// 搜索配置
const searchConfiger = ref([
{ label: "姓名", prop: 'xm', placeholder: "请输入姓名", showType: "input" },
{ label: "身份证号码", prop: 'sfzh', placeholder: "请输入身份证号码", showType: "input" },
{ label: "预警标签", prop: 'yjbqmc', placeholder: "请输入预警标签", showType: "input" },
{ label: "部门", prop: 'ssbmdm', placeholder: "请选择部门", showType: "department" },
{ label: "级别", prop: 'bqys', placeholder: "请选择级别", showType: "select", options: D_BZ_YJJB },
{ label: "积分段", prop: 'jfd', placeholder: "请选择积分段", showType: "Slot" },
{ label: "预警时间", prop: 'times', showType: "datetimerange" },
]);
const permission_sfqs = ref(false)
const roleCode = ref(false)
const queryFrom = ref({});
// 页面数据
const pageData = reactive({
tableData: [],
keyCount: 0,
tableConfiger: {
rowHieght: 61,
showSelectType: "checkBox",
loading: false,
haveControls: true,
},
total: 0,
pageConfiger: {
pageSize: 20,
pageCurrent: 1
},
controlsWidth: 300, //操作栏宽度
tableColumn: [
{ label: "状态", prop: "czzt", showSolt: true },
{ label: "预警时间", prop: "yjsj" ,width: 180},
{ label: "姓名", prop: "xm" },
{ label: "身份证号", prop: "sfzh" ,width: 180},
{ label: "标签", prop: "yjbqmc" },
{ label: "接收单位", prop: "ssbm" },
{ label: "活动频次", prop: "sfcs",width: 90 },
{ label: "预警分值", prop: "sffz",showSolt: true },
]
});
onMounted(() => {
let str = getItem('deptId') ? getItem('deptId')[0].deptLevel : ''
permission_sfqs.value = str.startsWith('2' || '3') ? false : true;
let rols = getItem('roleList') ? getItem('roleList') : []
let obj = rols.find(item => {
return ['JS_666666', 'JS_777777', 'JS_888888'].includes(item.roleCode)
})
roleCode.value = obj ? true : false;
gettbGsxtBqglSelectList()
tabHeightFn();
getList();
});
const onSearch = (val) => {
queryFrom.value = { ...queryFrom.value, ...val };
queryFrom.value.startTime = val.times ? val.times[0] : ''
queryFrom.value.endTime = val.times ? val.times[1] : ''
pageData.pageConfiger.pageCurrent = 1;
getList();
};
const reset = () => {
delete queryFrom.value.ksfz
delete queryFrom.value.jsfz
}
const changeNo = (val) => {
pageData.pageConfiger.pageCurrent = val;
getList();
};
const changeSize = (val) => {
pageData.pageConfiger.pageSize = val;
getList();
};
const getList = () => {
pageData.tableConfiger.loading = true;
// TODO: 替换为实际的身份预警API接口
let params = {
...queryFrom.value,
pageCurrent: pageData.pageConfiger.pageCurrent,
pageSize: pageData.pageConfiger.pageSize,
}
delete params.times;
yjzxSfyjSelectList(params).then((res) => {
pageData.tableData = Array.isArray(res?.records) ? res.records : [];
pageData.tableData = pageData.tableData.map(item => {
return {
...item,
bqys_cname: getMultiDictVal(item.bqys, D_GS_SSYJ),
czzt_cname: getMultiDictVal(item.czzt, D_GSXT_YJXX_CZZT),
}
})
pageData.total = res?.total || 0;
pageData.tableConfiger.loading = false;
}).catch(() => {
pageData.tableConfiger.loading = false;
});
};
// 标签
const bqLbData = ref({
bqDl: [],
bqXl: []
})
const gettbGsxtBqglSelectList = (val) => {
const promes = {
bqLx: '01',
bqLb: val ? '02' : "01",
bqDlId: val ? bqLbData.value.bqDl.find(item => item.value == val)?.id : ""
}
tbGsxtBqglSelectList(promes).then((res) => {
if (val) {
queryFrom.value.bqxl = ''
bqLbData.value.bqXl = res.data ? res.data.map(item => {
return {
label: item.bqMc,
value: item.bqDm,
}
}) : []
} else {
bqLbData.value.bqDl = res ? res.map(item => {
return {
label: item.bqMc,
value: item.bqDm,
id: item.id
}
}) : []
}
})
}
watch(() => bqLbData.value.bqXl, (res) => {
bqLbData.value.bqXl = res
}, { deep: true })
// 查看详情
const detailRef = ref()
const handleClick = (row) => {
detailRef.value.init(row)
}
// 处理签收
const handleQsFk = (val, type) => {
switch (type) {
case '签收':
proxy.$confirm("是否确定要签收?", "警告", { type: "warning" }).then(() => {
qcckPost({ id: val.id }, "/mosty-gsxt//yjzxSfyj/yjqs").then(() => {
val.czzt = '02'
getList()
proxy.$message({ type: "success", message: "签收成功" });
});
})
break;
case '反馈':
case '查看反馈':
emitter.emit("openFkDialog", { id: val.id, type });
break;
}
}
// 全息档案
const assessShow = ref(false)
const dataList = ref()
const pushAssess = (val) => {
return holographicProfileJump(val.yjlb, val) // 全息档案跳转
}
// 发送指令
const showDialog = ref(false)
const itemData = ref()
const showDetail = (item) => {
showDialog.value = true;
itemData.value = item
}
const handleClose = () => {
showDialog.value = false;
}
const semdFqzlRef = ref()
const tacitly = {
title: 'yjbt',
instructionContent: 'yjnr'
}
const submit = () => {
semdFqzlRef.value.getsendFqzl()
}
const closeFszl = () => {
semdFqzlRef.value.close()
}
const bqYs = (val) => {
if (val == '01') {
return '#ff0202'
} else if (val == '02') {
return '#ff8c00'
} else if (val == '03') {
return '#ffff00'
} else if (val == '04') {
return '#0000ff'
}
}
// 新增
const addModelRef = ref(null)
const openAddFrom = (row) => {
addModelRef.value.init('add', row)
}
// 选择系数
const chooseJfShow = ref(false)
const chooseJfBh = ref()
const roleIds = ref()
const chooseJfFun = (val) => {
chooseJfBh.value = val.id
yjzxyjzxSfyjSelectList(val.id).then(res => {
roleIds.value = res.sjxspzList.map(item => item.xsid)
chooseJfShow.value = true
})
}
const handleCzjy = (row) => {
czjyRef.value.init(row)
}
// 表格高度计算
const tabHeightFn = () => {
pageData.tableHeight = window.innerHeight - searchBox.value.offsetHeight - 280;
window.onresize = function () {
tabHeightFn();
};
};
/** 选中项 */
const selectRows = ref([])
const handleChooseData = (val) => {
selectRows.value = val
}
const exportExl = () => {
const titleObj = {
czzt_cname: "状态",
yjsj: "预警时间",
xm: "姓名",
sfzh: "身份证号",
yjbqmc: "标签",
ssbm: "接收单位",
sfcs: "活动频次",
sffz: "预警分值",
}
/** 导出【选中】的数据 (没有就全部)*/
const needArr = selectRows.value?.length > 0 ? selectRows.value : pageData.tableData
const data = needArr.map(item => {
return {
...item,
bqys_cname: getMultiDictVal(item.bqys, D_GS_SSYJ),
czzt_cname: getMultiDictVal(item.czzt, D_GSXT_YJXX_CZZT),
}
})
exportExlByObj(titleObj, data, '身份预警')
}
const handleQs = () => {
if (selectRows.value?.length === 0) return proxy.$message({ type: "warning", message: "请选择要签收的预警" });
let wqs = selectRows.value.filter(item => item.czzt == '01');
if (wqs.length == 0) return proxy.$message({ type: "warning", message: "数据都已签收,请选择未签收的数据" });
let yqs = selectRows.value.filter(item => item.czzt == '02');
let texy = yqs.length > 0 ? `${yqs.length}条已签收预警数据,确认要签收${wqs.length}条未签收预警数据吗?` : '确认要签收所有预警数据吗?'
proxy.$confirm(texy, "警告", { type: "warning" }).then(() => {
let ids = wqs.map(item => item.id)
qcckPost({ids}, '/mosty-gsxt/yjzxSfyj/batchQs').then(() => {
proxy.$message({ type: "success", message: "成功" });
getList();
}).catch(() => {
proxy.$message({ type: "error", message: "失败" });
});
}).catch(() => { });
}
</script>
<style scoped></style>
<style lang="scss">
.el-loading-mask {
background: rgba(0, 0, 0, 0.5) !important;
}
.tabBox_zdy {
.el-table--fit {
overflow: unset !important;
}
}
</style>

View File

@ -0,0 +1,564 @@
<template>
<div>
<!-- 搜索 -->
<div ref="searchBox" class="mt10">
<QueryFormPanel :fields="searchConfiger" @search="onSearch">
<template #yjfz>
<div>
<el-input
v-model="queryFrom.yjksfz"
type="number"
placeholder="开始分值"
style="width: 130px"
clearable
></el-input>
<el-input
v-model="queryFrom.yjjsfz"
type="number"
placeholder="结束分值"
style="width: 130px"
clearable
></el-input>
</div>
</template>
<template #but>
<el-button type="primary" @click="exportExl" size="small"
>导出</el-button
>
<el-button type="primary" size="small" @click="handleQs"
>签收</el-button
>
</template>
</QueryFormPanel>
</div>
<!-- 表格 -->
<div :style="{ height: pageData.tableHeight + 40 + 'px' }" class="bgTable">
<WarnDataTable
:loading="pageData.tableConfiger.loading"
:tableHeight="pageData.tableHeight"
:data="pageData.tableData"
:columns="pageData.tableColumn"
table-class="warn-table"
@selectionChange="handleChooseData"
>
<template #sfcs="{ row }">
<span style="color: #0072ff" @click="handleClick(row)">{{
row.sfcs
}}</span>
</template>
<template #czzt="{ row }">
<DictTag
:value="row.czzt"
:color="row.czzt === '01' ? '#ff2424' : '#1d72e8'"
:tag="false"
:options="D_GSXT_YJXX_CZZT"
/>
</template>
<template #yjLylx="{ row }">
<DictTag :value="row.yjLylx" :tag="false" :options="D_BZ_YJBQLX" />
</template>
<template #yjjb="{ row }">
<div :style="{ 'background-color': ys(row) }">
<DictTag
:value="row.yjjb"
color="#fff"
:tag="false"
:options="D_BZ_YJJB"
/>
</div>
</template>
<template #nl="{ row }">
<div>{{ IdCard(row.rysfzh, 3) }}</div>
</template>
<template #xb="{ row }">
<div>{{ IdCard(row.rysfzh, 2) }}</div>
</template>
<template #operation="{ row }">
<div style="display: flex; justify-content: space-between">
<span class="warning" @click="pushAssess(row)">全息档案</span>
<span class="primary" @click="handleCzjy(row)" v-if="roleCode"
>处置建议</span
>
<span class="primary" @click="chooseJfFun(row)">配置系统</span>
<span
class="success"
@click="handleQsFk(row, '签收')"
v-if="row.czzt == '01' && permission_sfqs"
>签收</span
>
<span
class="success"
@click="handleQsFk(row, '反馈')"
v-else-if="row.czzt == '02' && permission_sfqs"
>反馈</span
>
<span class="primary" @click="openAddFrom(row)">详情</span>
</div>
</template>
</WarnDataTable>
<Pages
@changeNo="changeNo"
@changeSize="changeSize"
:tableHeight="pageData.tableHeight"
:pageConfiger="{
...pageData.pageConfiger,
total: pageData.total
}"
></Pages>
</div>
</div>
<Detail ref="detailRef" />
<HolographicArchive v-model="assessShow" :dataList="dataList" />
<FkDialog @change="getList" lx="03" />
<Information
v-model="showDialog"
title="发送指令"
@submit="submit"
@close="closeFszl"
>
<SemdFqzl
ref="semdFqzlRef"
:itemData="itemData"
@handleClose="handleClose"
identification="yj"
:tacitly="tacitly"
/>
</Information>
<AddFrom
ref="addModelRef"
:dict="{ D_GSXT_YJXX_CZZT, D_BZ_YJJB, D_GS_SSYJ }"
/>
<Xwbq ref="xwbqRef" :dict="{ D_GSXT_YJXX_CZZT, D_BZ_YJJB, D_GS_SSYJ }" />
<Sfbq ref="sfbqRef" :dict="{ D_GSXT_YJXX_CZZT, D_BZ_YJJB, D_GS_SSYJ }" />
<!-- 处置建议 -->
<Czjy ref="czjyRef" @okSubmit="getList"></Czjy>
<ChooseJf
v-model="chooseJfShow"
titleValue="选择系数"
:Single="false"
:chooseJfBh="chooseJfBh"
url="/yjzxSfyj/sjxspz"
:roleIds="roleIds"
/>
</template>
<script setup>
import Czjy from "./components/czjy.vue";
import { getItem } from "@/utils/storage";
import Pages from "@/components/aboutTable/Pages.vue";
import HolographicArchive from "@/views/home/components/holographicArchive.vue";
import Information from "@/views/home/model/information.vue";
import SemdFqzl from "@/components/instructionHasBeen/sendFqzl.vue";
import AddFrom from "./components/zhbq.vue";
import Xwbq from "./components/xwbq.vue";
import Sfbq from "./components/sfbq.vue";
import ChooseJf from "@/components/ChooseList/ChooseJf/index.vue";
import FkDialog from "@/views/backOfficeSystem/fourColorManage/warningControl/centerHome/components/fkDialog.vue";
import { reactive, ref, onMounted, getCurrentInstance } from "vue";
import { qcckPost, qcckGet } from "@/api/qcckApi.js";
import { yjzxSfyjSelectList, yjzxyjzxSfyjSelectList } from "@/api/yj.js";
import { tbGsxtBqglSelectList } from "@/api/zdr";
import Detail from "./components/detail.vue";
import { watch } from "vue";
import emitter from "@/utils/eventBus.js";
import { holographicProfileJump, bqYs } from "@/utils/tools.js";
import { exportExlByObj } from "@/utils/exportExcel.js";
import { getMultiDictVal } from "@/utils/dict.js";
import WarnDataTable from "@/views/backOfficeSystem/ces/components/WarnDataTable.vue";
import QueryFormPanel from "@/views/backOfficeSystem/ces/components/QueryFormPanel.vue";
import { IdCard } from "@/utils/dict.js";
const czjyRef = ref();
const { proxy } = getCurrentInstance();
const searchBox = ref();
const { D_GSXT_YJXX_CZZT, D_GS_SSYJ, D_BZ_YJJB, D_BZ_YJBQLX } = proxy.$dict(
"D_GSXT_YJXX_CZZT",
"D_GS_SSYJ",
"D_BZ_YJJB",
"D_BZ_YJBQLX"
);
const dict = reactive({ D_GSXT_YJXX_CZZT, D_GS_SSYJ });
// 搜索配置
const searchConfiger = ref([
{
key: "czzt",
label: "处置状态",
type: "select",
options: D_GSXT_YJXX_CZZT,
placeholder: "请选择级别"
},
{
key: "startTime",
label: "预警时间",
type: "datetimerange",
placeholder: "请选择预警时间"
},
{ key: "ryxm", label: "姓名", type: "input", placeholder: "请输入姓名" },
{
key: "rysfzh",
label: "身份证号码",
type: "input",
placeholder: "请输入身份证号码"
},
{
key: "yjLylx",
label: "标签类型",
type: "select",
options: D_BZ_YJBQLX,
placeholder: "请选择预警标签"
},
{
key: "yjjb",
label: "标签级别",
type: "select",
options: D_BZ_YJJB,
placeholder: "请选择预警标签"
},
{
key: "ssbmdm",
label: "接收部门",
type: "department",
placeholder: "请选择接收部门"
},
{
key: "yjcs",
label: "活动频次",
type: "input",
placeholder: "请输入活动频次"
},
{
key: "yjfz",
label: "标签分值",
type: "slot",
placeholder: "请输入标签分值"
}
]);
const permission_sfqs = ref(false);
const roleCode = ref(false);
const queryFrom = ref({});
// 页面数据
const pageData = reactive({
tableData: [],
keyCount: 0,
tableConfiger: {
rowHieght: 61,
showSelectType: "checkBox",
loading: false,
haveControls: true
},
total: 0,
pageConfiger: {
pageSize: 20,
pageCurrent: 1
},
controlsWidth: 300, //操作栏宽度
tableColumn: [
{ type: "index", label: "序号", width: 55, align: "center" },
{ label: "处置状态", align: "center", slotName: "czzt" },
{ prop: "yjsj", label: "预警时间", align: "center", width: 200 },
{ prop: "ryxm", label: "姓名", align: "center" },
{ label: "性别", align: "center", slotName: "xb", align: "center" },
{ prop: "rysfzh", label: "身份证号", align: "center", width: 200 },
{ label: "年龄", align: "center", slotName: "nl", align: "center" },
{ label: "标签类型", align: "center", slotName: "yjLylx" },
{ label: "标签级别", align: "center", slotName: "yjjb" },
{ prop: "ssbm", label: "接收单位", align: "center", width: 200 },
{ prop: "yjcs", label: "活动频次", align: "center" },
{ prop: "yjfz", label: "标签分值", align: "center" },
{ label: "操作", width: 200, slotName: "operation" }
]
});
onMounted(() => {
let str = getItem("deptId") ? getItem("deptId")[0].deptLevel : "";
permission_sfqs.value = str.startsWith("2" || "3") ? false : true;
let rols = getItem("roleList") ? getItem("roleList") : [];
let obj = rols.find((item) => {
return ["JS_666666", "JS_777777", "JS_888888"].includes(item.roleCode);
});
roleCode.value = obj ? true : false;
gettbGsxtBqglSelectList();
tabHeightFn();
getList();
});
const onSearch = (val) => {
queryFrom.value = { ...queryFrom.value, ...val };
if (val.startTime && Array.isArray(val.startTime)) {
queryFrom.value.startTime = val.startTime[0];
queryFrom.value.endTime = val.startTime[1];
}
pageData.pageConfiger.pageCurrent = 1;
getList();
};
const changeNo = (val) => {
pageData.pageConfiger.pageCurrent = val;
getList();
};
const changeSize = (val) => {
pageData.pageConfiger.pageSize = val;
getList();
};
const getList = () => {
pageData.tableConfiger.loading = true;
let params = {
...queryFrom.value,
pageCurrent: pageData.pageConfiger.pageCurrent,
pageSize: pageData.pageConfiger.pageSize
};
qcckPost(params, "/mosty-gsxt/tbYjxx/getPageBqyjList")
.then((res) => {
pageData.tableData = Array.isArray(res?.records) ? res.records : [];
pageData.total = res?.total || 0;
pageData.tableConfiger.loading = false;
})
.catch(() => {
pageData.tableConfiger.loading = false;
});
};
// 标签
const bqLbData = ref({
bqDl: [],
bqXl: []
});
const gettbGsxtBqglSelectList = (val) => {
const promes = {
bqLx: "01",
bqLb: val ? "02" : "01",
bqDlId: val ? bqLbData.value.bqDl.find((item) => item.value == val)?.id : ""
};
tbGsxtBqglSelectList(promes).then((res) => {
if (val) {
queryFrom.value.bqxl = "";
bqLbData.value.bqXl = res.data
? res.data.map((item) => {
return {
label: item.bqMc,
value: item.bqDm
};
})
: [];
} else {
bqLbData.value.bqDl = res
? res.map((item) => {
return {
label: item.bqMc,
value: item.bqDm,
id: item.id
};
})
: [];
}
});
};
watch(
() => bqLbData.value.bqXl,
(res) => {
bqLbData.value.bqXl = res;
},
{ deep: true }
);
// 查看详情
const detailRef = ref();
const handleClick = (row) => {
detailRef.value.init(row);
};
// 处理签收
const handleQsFk = (val, type) => {
switch (type) {
case "签收":
proxy
.$confirm("是否确定要签收?", "警告", { type: "warning" })
.then(() => {
qcckPost({ id: val.id }, "/mosty-gsxt//yjzxSfyj/yjqs").then(() => {
val.czzt = "02";
getList();
proxy.$message({ type: "success", message: "签收成功" });
});
});
break;
case "反馈":
case "查看反馈":
emitter.emit("openFkDialog", { id: val.id, type });
break;
}
};
// 全息档案
const assessShow = ref(false);
const dataList = ref();
const pushAssess = (val) => {
return holographicProfileJump(val.yjlb, val); // 全息档案跳转
};
// 发送指令
const showDialog = ref(false);
const itemData = ref();
const showDetail = (item) => {
showDialog.value = true;
itemData.value = item;
};
const handleClose = () => {
showDialog.value = false;
};
const semdFqzlRef = ref();
const tacitly = {
title: "yjbt",
instructionContent: "yjnr"
};
const submit = () => {
semdFqzlRef.value.getsendFqzl();
};
const closeFszl = () => {
semdFqzlRef.value.close();
};
// 新增
const addModelRef = ref(null);
const xwbqRef = ref(null);
const sfbqRef = ref(null);
// 21 身份预警 22 行为预警 23 组合预警
const openAddFrom = (row) => {
switch (row.yjLylx) {
case "22":
xwbqRef.value.init(row);
break;
case "21":
sfbqRef.value.init("add", row);
break;
case "23":
addModelRef.value.init(row);
break;
default:
break;
}
};
// 选择系数
const chooseJfShow = ref(false);
const chooseJfBh = ref();
const roleIds = ref();
const chooseJfFun = (val) => {
chooseJfBh.value = val.id;
yjzxyjzxSfyjSelectList(val.id).then((res) => {
roleIds.value = res.sjxspzList.map((item) => item.xsid);
chooseJfShow.value = true;
});
};
const handleCzjy = (row) => {
czjyRef.value.init(row);
};
// 表格高度计算
const tabHeightFn = () => {
pageData.tableHeight =
window.innerHeight - searchBox.value.offsetHeight - 230;
window.onresize = function () {
tabHeightFn();
};
};
/** 选中项 */
const selectRows = ref([]);
const handleChooseData = (val) => {
selectRows.value = val;
};
const exportExl = () => {
const titleObj = {
czzt_cname: "状态",
yjsj: "预警时间",
xm: "姓名",
sfzh: "身份证号",
yjbqmc: "标签",
bqys_cname: "级别",
ssbm: "接收单位",
sfcs: "活动频次",
bqfz: "标签分值",
pzxs: "系数",
sffz: "计算分值"
};
/** 导出【选中】的数据 (没有就全部)*/
const needArr =
selectRows.value?.length > 0 ? selectRows.value : pageData.tableData;
const data = needArr.map((item) => {
return {
...item,
bqys_cname: getMultiDictVal(item.bqys, D_GS_SSYJ),
czzt_cname: getMultiDictVal(item.czzt, D_GSXT_YJXX_CZZT)
};
});
exportExlByObj(titleObj, data, "身份预警");
};
const handleQs = () => {
if (selectRows.value?.length === 0)
return proxy.$message({ type: "warning", message: "请选择要签收的预警" });
let wqs = selectRows.value.filter((item) => item.czzt == "01");
if (wqs.length == 0)
return proxy.$message({
type: "warning",
message: "数据都已签收,请选择未签收的数据"
});
let yqs = selectRows.value.filter((item) => item.czzt == "02");
let texy =
yqs.length > 0
? `${yqs.length}条已签收预警数据,确认要签收${wqs.length}条未签收预警数据吗?`
: "确认要签收所有预警数据吗?";
proxy
.$confirm(texy, "警告", { type: "warning" })
.then(() => {
let ids = wqs.map((item) => item.id);
qcckPost({ ids }, "/mosty-gsxt/yjzxSfyj/batchQs")
.then(() => {
proxy.$message({ type: "success", message: "成功" });
getList();
})
.catch(() => {
proxy.$message({ type: "error", message: "失败" });
});
})
.catch(() => {});
};
const ys = (item) => {
switch (item.yjjb) {
case "01":
return "red";
case "02":
return "orange";
case "03":
return "yellow";
case "04":
return "blue";
default:
return "";
}
};
</script>
<style lang="scss" scoped>
.el-loading-mask {
background: rgba(0, 0, 0, 0.5) !important;
}
::v-deep .el-table .cell {
padding: 0;
}
::v-deep .el-table .el-table__cell {
padding: 4px 0;
}
.bgTable {
background-color: #fff;
}
</style>

View File

@ -0,0 +1,91 @@
<template>
<div class="warning-item" >
<el-divider content-position="left">预警内容</el-divider>
<div class="item-row" style="border: none;"> {{ props.row.yjnr }} </div>
<el-empty v-if="!props.row.yjnr" :image-size="0.5" description="暂无数据" />
<el-divider content-position="left">处置建议</el-divider>
<div class="item-row" v-for="(it,idx) in list" :key="idx">
<div class="info-item">
<span class="text">预警人姓名{{ it.jryXm }}</span>
<span class="text">建议时间{{ it.jysj }}</span>
<span class="text">所属部门{{ it.ssbm }}</span>
</div>
<div class="info-item">建议内容<span>{{ it.jynr || '暂无' }}</span></div>
</div>
<el-empty v-if="list.length === 0" :image-size="0.5" description="暂无数据" />
<el-divider content-position="left">反馈内容</el-divider>
<div class="item-row" v-for="(it,idx) in Fklist" :key="idx">
<div class="info-item">
<span class="text">处置地址{{ it.czdz }}</span>
<span class="text">处置时间{{ it.czsj }}</span>
</div>
<div class="info-item">
<span class="text">常控不尿检理由<span>{{ it.ckbnjly || '暂无' }}</span></span>
<span class="text">常控处置反馈补充信息<span>{{ it.ckczbcxx || '暂无' }}</span></span>
<span class="text">常控立线侦察评估<span>{{ it.cklxzcpg || '暂无' }}</span></span>
<span class="text">常控立线侦察评估依据<span>{{ it.cklxzcpgyj || '暂无' }}</span></span>
</div>
</div>
<el-empty :image-size="0.5" v-if="Fklist.length === 0" description="暂无数据" />
</div>
</template>
<script setup>
import { ref, onMounted ,defineProps} from 'vue'
import { qcckPost,qcckGet } from "@/api/qcckApi.js";
const props = defineProps({
row: {
type: Object,
default: () => ({})
},
})
const list = ref([])
const Fklist = ref([])
onMounted(() => {
if(props.row.id){
qcckPost({yjid: props.row.id},'/mosty-gsxt/yjxx/czjy/getPageList').then((res) => {
list.value = res.records || []
})
qcckGet({},'/mosty-gsxt/yjzxSfyj/'+ props.row.id).then((res) => {
Fklist.value = res.fkList || []
})
}
})
</script>
<style lang="scss" scoped>
.warning-item {
width: 100%;
padding: 15px;
box-sizing: border-box;
border-radius: 8px;
background-color: #fafafa;
}
.item-row{
border-bottom: 1px dashed #e8e8e8;
line-height: 36px;
padding-left: 2rem;
box-sizing: border-box;
padding-bottom: 10px;
margin-bottom: 10px;
&:nth-last-child(1){
border-bottom: none;
}
}
.info-item{
line-height: 36px;
width: 100%;
.text{
display: inline-block;
width: 25%;
margin-right: 10px;
}
}
::v-deep .el-empty{
--el-empty-padding: 0px;
margin-bottom: 18px;
--el-empty-description-margin-top: 10px;
}
</style>

View File

@ -4,12 +4,25 @@
<div ref="searchBox" class="mt10 mb10">
<!-- <Search :searchArr="searchConfiger" @submit="onSearch" ref="searchDom" :key="pageData.keyCount">
</Search> -->
<QueryFormPanel v-model="queryFrom" :fields="searchConfiger" ref="searchDom" @search='onSearch'>
<QueryFormPanel
v-model="queryFrom"
:fields="searchConfiger"
ref="searchDom"
@search="onSearch"
>
<template #but>
<el-button type="primary" size="small" @click="exportExl">批量导出</el-button>
<el-button type="primary" size="small" @click="handleQs">批量签收</el-button>
<el-button type="primary" size="small" @click="handleQs">批量分析</el-button>
<el-button type="primary" size="small" @click="countPeople">计算人数</el-button>
<el-button type="primary" size="small" @click="exportExl"
>批量导出</el-button
>
<el-button type="primary" size="small" @click="handleQs"
>批量签收</el-button
>
<el-button type="primary" size="small" @click="handleQs"
>批量分析</el-button
>
<el-button type="primary" size="small" @click="countPeople"
>计算人数</el-button
>
</template>
</QueryFormPanel>
</div>
@ -22,20 +35,34 @@
</template>
</PageTitle> -->
<!-- 表格 -->
<div style="background-color: #fff;">
<WarnDataTable :loading="pageData.tableConfiger.loading" :tableHeight="pageData.tableHeight"
:data="pageData.tableData" :columns="pageData.tableColumn" table-class="warn-table"
@selectionChange="handleChooseData">
<div style="background-color: #fff">
<WarnDataTable
:loading="pageData.tableConfiger.loading"
:tableHeight="pageData.tableHeight"
:data="pageData.tableData"
:columns="pageData.tableColumn"
table-class="warn-table"
@selectionChange="handleChooseData"
>
<template #status="{ row }">
<DictTag :value="row.czzt" :color="row.czzt === '01' ? '#ff2424' : '#1d72e8'" :tag="false"
:options="D_GSXT_YJXX_CZZT" />
<DictTag
:value="row.czzt"
:color="row.czzt === '01' ? '#ff2424' : '#1d72e8'"
:tag="false"
:options="D_GSXT_YJXX_CZZT"
/>
</template>
<template #xbdm="{ row }">
<DictTag :value="row.xbdm" :tag="false" :options="D_BZ_XB" />
</template>
<template #yjJb="{ row }">
<div :style="{ 'background-color': bqYs(row.yjJb) }">
<DictTag :value="row.yjJb" color="#fff" :tag="false" :options="D_BZ_YJJB" />
<DictTag
:value="row.yjJb"
color="#fff"
:tag="false"
:options="D_BZ_YJJB"
/>
</div>
</template>
<template #bqdl="{ row }">
@ -47,69 +74,210 @@
<template #cszt="{ row }">
<DictTag :value="row.cszt" :tag="false" :options="D_GS_CSZT" />
</template>
<template #qbly="{ row }">
<DictTag :value="row.qbly" :tag="false" :options="D_BZ_QBLY" />
</template>
<template #qblyjb="{ row }">
<DictTag :value="row.qblyjb" :tag="false" :options="D_BZ_QBLYJB" />
</template>
<template #operation="{ row }">
<div style="display: flex;justify-content: space-between;">
<div style="display: flex; justify-content: space-between">
<span class="primary" @click="handleQsSingle(row)">签收</span>
<span class="primary" @click="particularsOpen(row)">详情</span>
<span class="warning" @click="pushWarning(row)">指派</span>
<span class="warning" v-if="row.sfbc != '1'" @click="failWarning(row)">报错</span>
<span
class="warning"
v-if="row.sfbc != '1'"
@click="failWarning(row)"
>报错</span
>
<span class="primary" @click="payAttention(row)">关注</span>
</div>
</template>
</WarnDataTable>
<Pages @changeNo="changeNo" @changeSize="changeSize" :tableHeight="pageData.tableHeight" :pageConfiger="{
<Pages
@changeNo="changeNo"
@changeSize="changeSize"
:tableHeight="pageData.tableHeight"
:pageConfiger="{
...pageData.pageConfiger,
total: pageData.total
}"></Pages>
}"
></Pages>
</div>
</div>
<ZpForm v-model="warningShow" :dataList="dataList" />
<Particulars v-model="particularsShow" :dataList="dataPres"
:dict="{ D_BZ_XB, D_BZ_YJJB, D_GS_QLZDRLX, D_GS_ZDR_RYJB, D_GS_ZDR_GJLB }" />
<Particulars
v-model="particularsShow"
:dataList="dataPres"
:dict="{ D_BZ_XB, D_BZ_YJJB, D_GS_QLZDRLX, D_GS_ZDR_RYJB, D_GS_ZDR_GJLB }"
/>
<peopleConut v-model="searchOpen" :dataConut="dataConut" />
</template>
<script setup>
import { getMultiDictVal } from "@/utils/dict.js"
import { exportExlByObj } from "@/utils/exportExcel.js"
import { getMultiDictVal } from "@/utils/dict.js";
import { exportExlByObj } from "@/utils/exportExcel.js";
import ZpForm from "./zpForm.vue";
import { bqYs } from '@/utils/tools.js'
import { bqYs } from "@/utils/tools.js";
import Particulars from "./particulars.vue";
import Search from "@/components/aboutTable/Search.vue";
import WarnDataTable from '@/views/backOfficeSystem/ces/components/WarnDataTable.vue'
import WarnDataTable from "@/views/backOfficeSystem/ces/components/WarnDataTable.vue";
import QueryFormPanel from "@/views/backOfficeSystem/ces/components/QueryFormPanel.vue";
import PageTitle from "@/components/aboutTable/PageTitle.vue";
// import MyTable from "@/components/aboutTable/MyTable.vue";
import Pages from "@/components/aboutTable/Pages.vue";
import { qcckGet, qcckPost } from "@/api/qcckApi.js";
import { reactive, ref, onMounted, getCurrentInstance, } from "vue";
import { reactive, ref, onMounted, getCurrentInstance } from "vue";
import peopleConut from "./peopleConut.vue";
const { proxy } = getCurrentInstance();
const { D_BZ_YJLY, D_GS_QLZDRLX, D_GS_ZDR_GJLB, D_BZ_YJJB, D_GS_CSZT, D_GS_QLZDRYXX, D_BZ_XB, D_GSXT_YJXX_CZZT, D_GS_ZDR_RYJB, D_BZ_SF } = proxy.$dict('D_BZ_YJLY', 'D_GS_QLZDRLX', "D_BZ_YJJB", "D_GS_QLZDRYXX", "D_BZ_XB", "D_GSXT_YJXX_CZZT", "D_GS_ZDR_RYJB", 'D_GS_ZDR_GJLB', 'D_GS_CSZT', "D_BZ_SF"); //获取字典数据
const {
D_BZ_YJLY,
D_GS_QLZDRLX,
D_GS_ZDR_GJLB,
D_BZ_YJJB,
D_GS_CSZT,
D_GS_QLZDRYXX,
D_BZ_XB,
D_GSXT_YJXX_CZZT,
D_GS_ZDR_RYJB,
D_BZ_SF,
D_BZ_QBLY,
D_BZ_QBLYJB
} = proxy.$dict(
"D_BZ_YJLY",
"D_GS_QLZDRLX",
"D_BZ_YJJB",
"D_GS_QLZDRYXX",
"D_BZ_XB",
"D_GSXT_YJXX_CZZT",
"D_GS_ZDR_RYJB",
"D_GS_ZDR_GJLB",
"D_GS_CSZT",
"D_BZ_SF",
"D_BZ_QBLY",
"D_BZ_QBLYJB"
); //获取字典数据
const searchBox = ref(); //搜索框
const warningShow = ref(false);
const dataList = ref([]);
const searchConfiger = ref(
[
{ key: 'startTime', label: '预警时间', type: 'datetimerange', placeholder: '请选择预警时间' },
{ key: 'yjJb', label: '预警级别', type: 'select', options: D_BZ_YJJB, multiple: true, placeholder: '请选择预警级别' },
{ key: 'ssbmdm', label: '接收单位', type: 'department', placeholder: '请选择接收单位' },
{ key: 'sfglyj', label: '关联预警', type: 'select', options: D_BZ_SF, placeholder: '请选择关联预警' },
{ key: 'yjRyxm', label: '姓名', type: 'input', placeholder: '请输入姓名' },
{ key: 'xbdm', label: '性别', type: 'select', options: D_BZ_XB, placeholder: '请选择性别' },
{ key: 'cszt', label: '超时状态', type: 'select', options: D_GS_CSZT, placeholder: '请选择超时状态' },
{ key: 'bqdl', label: '人员类别', type: 'select', options: D_GS_QLZDRLX, placeholder: '请选择人员类别' },
{ key: 'sfgz', label: '重点关注', type: 'select', options: D_BZ_SF, placeholder: '请选择重点关注' },
{ key: 'sfzp', label: '二次指派', type: 'select', options: D_BZ_SF, placeholder: '请选择二次指派' },
{ key: 'yjRysfzh', label: '身份证号码', type: 'input', placeholder: '请输入身份证号码' },
{ key: 'ksnl', label: '开始年龄', type: 'input', placeholder: '请输入开始年龄' },
{ key: 'jsnl', label: '结束年龄', type: 'input', placeholder: '请输入结束年龄' },
{ key: 'yjCs', label: '预警次数', type: 'input', placeholder: '请输入预警次数' },
{ key: 'bqdl', label: '人员级别', type: 'select', options: D_GS_ZDR_RYJB },
{ key: 'yjLylx', label: '轨迹类别', type: 'select', options: D_GS_ZDR_GJLB },
{ key: 'yjDz', label: '活动发生地', type: 'input', placeholder: '请输入活动发生地' },
{ key: 'yjbqmc', label: '人员细类', type: 'input', placeholder: '请输入人员细类' }
])
const searchConfiger = ref([
{
key: "startTime",
label: "预警时间",
type: "datetimerange",
placeholder: "请选择预警时间"
},
{
key: "yjJb",
label: "预警级别",
type: "select",
options: D_BZ_YJJB,
multiple: true,
placeholder: "请选择预警级别"
},
{
key: "ssbmdm",
label: "接收单位",
type: "department",
placeholder: "请选择接收单位"
},
{
key: "sfglyj",
label: "关联预警",
type: "select",
options: D_BZ_SF,
placeholder: "请选择关联预警"
},
{ key: "yjRyxm", label: "姓名", type: "input", placeholder: "请输入姓名" },
{
key: "xbdm",
label: "性别",
type: "select",
options: D_BZ_XB,
placeholder: "请选择性别"
},
{
key: "cszt",
label: "超时状态",
type: "select",
options: D_GS_CSZT,
placeholder: "请选择超时状态"
},
{
key: "bqdl",
label: "人员类别",
type: "select",
options: D_GS_QLZDRLX,
placeholder: "请选择人员类别"
},
{
key: "sfgz",
label: "重点关注",
type: "select",
options: D_BZ_SF,
placeholder: "请选择重点关注"
},
{
key: "sfzp",
label: "二次指派",
type: "select",
options: D_BZ_SF,
placeholder: "请选择二次指派"
},
{
key: "yjRysfzh",
label: "身份证号码",
type: "input",
placeholder: "请输入身份证号码"
},
{
key: "ksnl",
label: "开始年龄",
type: "input",
placeholder: "请输入开始年龄"
},
{
key: "jsnl",
label: "结束年龄",
type: "input",
placeholder: "请输入结束年龄"
},
{
key: "yjCs",
label: "预警次数",
type: "input",
placeholder: "请输入预警次数"
},
{ key: "bqdl", label: "人员级别", type: "select", options: D_GS_ZDR_RYJB },
{ key: "yjLylx", label: "轨迹类别", type: "select", options: D_GS_ZDR_GJLB },
{
key: "yjDz",
label: "活动发生地",
type: "input",
placeholder: "请输入活动发生地"
},
{
key: "yjbqmc",
label: "人员细类",
type: "input",
placeholder: "请输入人员细类"
},
{
key: "qbly",
label: "情报来源",
type: "select",
options: D_BZ_QBLY,
placeholder: "请选择情报来源"
},
{
key: "qblyjb",
label: "情报来源级别",
type: "select",
options: D_BZ_QBLYJB,
placeholder: "请选择情报来源级别"
}
]);
const queryFrom = ref({});
const pageData = reactive({
@ -129,91 +297,92 @@ const pageData = reactive({
}, //分页
controlsWidth: 200, //操作栏宽度
tableColumn: [
{ type: 'index', label: '序号', width: 55, align: 'center' },
{ label: '预警状态', width: 86, align: 'center', slotName: 'status' },
{ prop: 'yjSj', label: '预警时间', width: 150 },
{ prop: 'yjRyxm', label: '人员姓名', width: 80 },
{ prop: 'yjRysfzh', label: '身份证号', width: 158 },
{ label: '性别', width: 56, align: 'center', slotName: 'xbdm' },
{ prop: 'nl', label: '年龄', width: 56, align: 'center' },
{ label: '预警级别', width: 88, align: 'center', slotName: 'yjJb' },
{ label: '人员类别', width: 90, align: 'center', slotName: 'bqdl' },
{ prop: 'yjbqmc', label: '人员细类', width: 92 },
{ label: '轨迹类别', width: 92, align: 'center', slotName: 'yjLylx' },
{ prop: 'yjDz', label: '活动发生地' },
{ prop: 'ssbm', label: '接收单位' },
{ prop: 'yjCs', label: '次数', width: 60, align: 'center' },
{ label: '操作', width: 180, slotName: 'operation' },
{ label: '超时状态', width: 80, align: 'center', slotName: 'cszt' }
{ type: "index", label: "序号", width: 55, align: "center" },
{ label: "预警状态", width: 80, align: "center", slotName: "status" },
{ prop: "yjSj", label: "预警时间", width: 145 },
{ prop: "yjRyxm", label: "人员姓名", width: 80 },
{ prop: "yjRysfzh", label: "身份证号", width: 158 },
{ label: "性别", width: 55, align: "center", slotName: "xbdm" },
{ prop: "nl", label: "年龄", width: 55, align: "center" },
{ label: "预警级别", width: 80, align: "center", slotName: "yjJb" },
{ label: "人员类别", width: 80, align: "center", slotName: "bqdl" },
{ prop: "yjbqmc", label: "人员细类", width: 80 },
{ label: "轨迹类别", width: 80, align: "center", slotName: "yjLylx" },
{ prop: "yjDz", label: "活动发生地", width: 100 },
{ prop: "ssbm", label: "接收单位" },
{ label: "情报来源", width: 80, align: "center", slotName: "qbly" },
{ label: "来源级别", width: 80, align: "center", slotName: "qblyjb" },
{ prop: "yjCs", label: "次数", width: 55, align: "center" },
{ label: "操作", width: 180, slotName: "operation" },
{ label: "超时状态", width: 80, align: "center", slotName: "cszt" }
]
});
onMounted(() => {
tabHeightFn();
getList()
getList();
});
// 搜索
const onSearch = (val) => {
queryFrom.value = { ...val, yjJb: val.yjJb?.join(',') || '' }
queryFrom.value.startTime = val.startTime ? val.startTime[0] : ''
queryFrom.value.endTime = val.startTime ? val.startTime[1] : ''
queryFrom.value = { ...val, yjJb: val.yjJb?.join(",") || "" };
queryFrom.value.startTime = val.startTime ? val.startTime[0] : "";
queryFrom.value.endTime = val.startTime ? val.startTime[1] : "";
// queryFrom.value.sfglyj = val.sfglyj?.join(',') || ''
// queryFrom.value.sfgz = val.sfgz?.join(',') || ''
// queryFrom.value.sfzp = val.sfzp?.join(',') || ''
pageData.pageConfiger.pageCurrent = 1;
getList()
}
getList();
};
const changeNo = (val) => {
pageData.pageConfiger.pageCurrent = val;
getList()
}
getList();
};
const changeSize = (val) => {
pageData.pageConfiger.pageSize = val;
getList()
}
getList();
};
const getList = () => {
pageData.tableConfiger.loading = true;
const promes = {
...queryFrom.value,
pageCurrent: pageData.pageConfiger.pageCurrent,
pageSize: pageData.pageConfiger.pageSize,
}
pageSize: pageData.pageConfiger.pageSize
};
delete promes.times;
qcckPost(promes, '/mosty-gsxt/tbYjxx/getQlzdrPageList').then((res) => {
qcckPost(promes, "/mosty-gsxt/tbYjxx/getQlzdrPageList")
.then((res) => {
pageData.total = res.total || 0;
pageData.tableConfiger.loading = false;
pageData.tableData = res.records || []
}).catch(() => {
pageData.tableConfiger.loading = false;
pageData.tableData = res.records || [];
})
}
.catch(() => {
pageData.tableConfiger.loading = false;
});
};
const pushWarning = (val) => {
warningShow.value = true;
dataList.value = val;
}
};
const failWarning = (val) => {
let ids = [val.id]
qcckPost({ ids }, '/mosty-gsxt/tbYjxx/yjbc').then((res) => {
let ids = [val.id];
qcckPost({ ids }, "/mosty-gsxt/tbYjxx/yjbc")
.then((res) => {
proxy.$message({ type: "success", message: "成功" });
getList();
}).catch(() => {
})
.catch(() => {
proxy.$message({ type: "error", message: "失败" });
});
}
};
/** 选中项 */
const selectRows = ref([])
const selectRows = ref([]);
const handleChooseData = (val) => {
selectRows.value = val
}
selectRows.value = val;
};
const exportExl = () => {
const titleObj = {
czzt_name: "状态",
@ -225,102 +394,138 @@ const exportExl = () => {
yjbqmc: "细类",
yjDz: "活动发生地",
ssbm: "接收单位",
yjCs: "预警次数",
}
yjCs: "预警次数"
};
/** 导出【选中】的数据 (没有就全部)*/
const needArr = selectRows.value?.length > 0 ? selectRows.value : pageData.tableData
const data = needArr.map(item => {
const needArr =
selectRows.value?.length > 0 ? selectRows.value : pageData.tableData;
const data = needArr.map((item) => {
return {
...item,
czzt_name: getMultiDictVal(item.czzt, D_GSXT_YJXX_CZZT),
yjJb_name: getMultiDictVal(item.yjJb, D_BZ_YJJB),
bqdl_name: getMultiDictVal(item.bqdl, D_GS_QLZDRLX),
}
})
exportExlByObj(titleObj, data, '七类重点')
}
bqdl_name: getMultiDictVal(item.bqdl, D_GS_QLZDRLX)
};
});
exportExlByObj(titleObj, data, "七类重点");
};
// 批量签收
const handleQs = () => {
if (selectRows.value?.length === 0) return proxy.$message({ type: "warning", message: "请选择要签收的预警" });
let wqs = selectRows.value.filter(item => item.czzt == '01');
if (wqs.length == 0) return proxy.$message({ type: "warning", message: "数据都已签收,请选择未签收的数据" });
let yqs = selectRows.value.filter(item => item.czzt == '02');
let texy = yqs.length > 0 ? `${yqs.length}条已签收预警数据,确认要签收${wqs.length}条未签收预警数据吗?` : '确认要签收所有预警数据吗?'
proxy.$confirm(texy, "警告", { type: "warning" }).then(() => {
let ids = wqs.map(item => item.id)
qcckPost({ ids }, '/mosty-gsxt/tbYjxx/batchQs').then(() => {
if (selectRows.value?.length === 0)
return proxy.$message({ type: "warning", message: "请选择要签收的预警" });
let wqs = selectRows.value.filter((item) => item.czzt == "01");
if (wqs.length == 0)
return proxy.$message({
type: "warning",
message: "数据都已签收,请选择未签收的数据"
});
let yqs = selectRows.value.filter((item) => item.czzt == "02");
let texy =
yqs.length > 0
? `${yqs.length}条已签收预警数据,确认要签收${wqs.length}条未签收预警数据吗?`
: "确认要签收所有预警数据吗?";
proxy
.$confirm(texy, "警告", { type: "warning" })
.then(() => {
let ids = wqs.map((item) => item.id);
qcckPost({ ids }, "/mosty-gsxt/tbYjxx/batchQs")
.then(() => {
proxy.$message({ type: "success", message: "成功" });
getList();
}).catch(() => {
})
.catch(() => {
proxy.$message({ type: "error", message: "失败" });
});
}).catch(() => { });
}
})
.catch(() => {});
};
// 详情
const dataPres = ref({})
const dataPres = ref({});
const particularsShow = ref(false);
const particularsOpen = (row) => {
dataPres.value = row
dataPres.value = row;
particularsShow.value = true;
}
};
// 单条签收
const handleQsSingle = (row) => {
if (row.czzt == '02') return proxy.$message({ type: "warning", message: "数据已签收,无需重复签收" });
proxy.$confirm('确认要签收该条预警数据吗?', "警告", { type: "warning" }).then(() => {
qcckPost({ ids: [row.id] }, '/mosty-gsxt/tbYjxx/batchQs').then(() => {
if (row.czzt == "02")
return proxy.$message({
type: "warning",
message: "数据已签收,无需重复签收"
});
proxy
.$confirm("确认要签收该条预警数据吗?", "警告", { type: "warning" })
.then(() => {
qcckPost({ ids: [row.id] }, "/mosty-gsxt/tbYjxx/batchQs")
.then(() => {
proxy.$message({ type: "success", message: "成功" });
getList();
}).catch(() => {
})
.catch(() => {
proxy.$message({ type: "error", message: "失败" });
});
}).catch(() => { });
}
})
.catch(() => {});
};
// 关注
const payAttention = (row) => {
let promes = {}
if (row.sfgz == '1') {
promes.sfgz = '0'
promes.msg = '取消关注'
let promes = {};
if (row.sfgz == "1") {
promes.sfgz = "0";
promes.msg = "取消关注";
} else {
promes.sfgz = '1'
promes.msg = '关注'
promes.sfgz = "1";
promes.msg = "关注";
}
proxy.$confirm('确认要关注该条预警数据吗?', "警告", { type: "warning" }).then(() => {
qcckPost({ sfgz: promes.sfgz, id: row.id }, '/mosty-gsxt/tbYjxx/yjgz').then(() => {
proxy
.$confirm("确认要关注该条预警数据吗?", "警告", { type: "warning" })
.then(() => {
qcckPost({ sfgz: promes.sfgz, id: row.id }, "/mosty-gsxt/tbYjxx/yjgz")
.then(() => {
proxy.$message({ type: "success", message: `${promes.msg}成功` });
getList();
}).catch(() => {
})
.catch(() => {
proxy.$message({ type: "error", message: `${promes.msg}失败` });
});
}).catch(() => { });
}
})
.catch(() => {});
};
// 人数计算
const searchDom = ref(null)
const dataConut = ref(0)
const searchOpen = ref(false)
const searchDom = ref(null);
const dataConut = ref(0);
const searchOpen = ref(false);
const countPeople = () => {
const promes = {
...searchDom.value.formState,
yjJb: searchDom.value.formState.yjJb?.join(',') || '',
startTime: searchDom.value.formState.startTime ? searchDom.value.formState.startTime[0] : '',
endTime: searchDom.value.formState.endTime ? searchDom.value.formState.endTime[1] : '',
}
qcckPost(promes, '/mosty-gsxt/tbYjxx/bkyjQctj').then((res) => {
dataConut.value = res || 0
searchOpen.value = true
}).catch(() => {
yjJb: searchDom.value.formState.yjJb?.join(",") || "",
startTime: searchDom.value.formState.startTime
? searchDom.value.formState.startTime[0]
: "",
endTime: searchDom.value.formState.endTime
? searchDom.value.formState.endTime[1]
: ""
};
qcckPost(promes, "/mosty-gsxt/tbYjxx/bkyjQctj")
.then((res) => {
dataConut.value = res || 0;
searchOpen.value = true;
})
.catch(() => {
// proxy.$message({ type: "error", message: `${promes.msg}失败` });
});
}
};
// 表格高度计算
const tabHeightFn = () => {
console.log("xxxxxxx");
pageData.tableHeight = window.innerHeight - searchBox.value.offsetHeight - 230;
window.onresize = function () { tabHeightFn(); };
pageData.tableHeight =
window.innerHeight - searchBox.value.offsetHeight - 230;
window.onresize = function () {
tabHeightFn();
};
};
</script>

View File

@ -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> 支持 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>
@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>

View File

@ -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>

View 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>

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View 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>

View 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>
<!-- scopedel-image-viewer 通过 Teleport 挂到 body需全局选择器 -->
<style lang="scss">
@import "./styles/luntan-tech.scss";
</style>

View File

@ -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);
}
}

View 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;
}
}

View File

@ -0,0 +1,64 @@
@import './luntan-tech.scss';
// vue3-emojidist/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;
}
}

View File

@ -1,6 +1,14 @@
<template>
<el-dialog :model-value="modelValue" center width="500px" :destroy-on-close="true" :title="title" @close="close"
:close-on-click-modal="false">
<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">
@ -133,11 +141,13 @@ const close = () => {
</script>
<style lang="scss" scoped>
@import '../styles/luntan-tech.scss';
.avatar-upload-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
padding: 12px 0 8px;
}
.avatar-preview {
@ -145,12 +155,13 @@ const close = () => {
height: 160px;
border-radius: 50%;
overflow: hidden;
border: 2px solid #e8e8e8;
border: 2px solid rgba(0, 227, 255, 0.4);
margin-bottom: 20px;
display: flex;
justify-content: center;
align-items: center;
background-color: #f5f7fa;
background: rgba(10, 30, 60, 0.6);
box-shadow: 0 0 20px rgba(0, 120, 200, 0.2);
.preview-image {
width: 100%;
@ -163,11 +174,12 @@ const close = () => {
flex-direction: column;
justify-content: center;
align-items: center;
color: #909399;
color: $lt-text-muted;
.placeholder-icon {
font-size: 48px;
margin-bottom: 8px;
color: rgba(0, 227, 255, 0.45);
}
span {
@ -185,15 +197,22 @@ const close = () => {
.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;
background-color: #f5f7fa;
border-radius: 4px;
font-size: 12px;
color: #909399;
color: $lt-text-muted;
background: rgba(10, 30, 60, 0.65);
border: 1px solid $lt-border-dim;
p {
margin: 4px 0;
@ -201,3 +220,7 @@ const close = () => {
}
}
</style>
<style lang="scss">
@import '../styles/luntan-dialog-tech.scss';
</style>

View File

@ -28,12 +28,17 @@
</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}条回复` }}
<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>
@ -45,13 +50,27 @@
<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" />
<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;" />
<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
size="small"
type="primary"
@click="submitReply(comment)"
>
回复
</el-button>
</div>
@ -60,9 +79,24 @@
</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">
<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>
@ -90,7 +124,10 @@
</el-icon>
<span v-if="reply.likeCount">{{ reply.likeCount }}</span>
</div> -->
<div class="action-btn" @click="handleReplyToReply(reply, comment)">
<div
class="action-btn"
@click="handleReplyToReply(reply, comment)"
>
回复
</div>
</div>
@ -98,16 +135,34 @@
<!-- 二级回复输入框 -->
<transition name="slide-fade">
<div v-if="activeReplyId === reply.id" class="reply-input-box">
<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" />
<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;" />
<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 size="small" @click="cancelReply"
>取消</el-button
>
<el-button
size="small"
type="primary"
@click="submitReply(comment, reply)"
>
回复
</el-button>
</div>
@ -124,122 +179,128 @@
</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'
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: '活动'
}
"Smileys & Emotion": "笑脸&表情",
"Food & Drink": "食物&饮料",
"Animals & Nature": "动物&自然",
"Travel & Places": "旅行&地点",
"People & Body": "人物&身体",
Objects: "物品",
Symbols: "符号",
Flags: "旗帜",
Activities: "活动"
};
const props = defineProps({
comments: {
type: Array,
default: () => []
}, replyTo: {
},
replyTo: {
type: Object,
default: () => ({})
}
})
});
const emit = defineEmits(['reply', 'like'])
const emit = defineEmits(["reply", "like"]);
const activeReplyId = ref(null)
const replyContent = ref('')
const activeReplyId = ref(null);
const replyContent = ref("");
const getTagType = (tag) => {
const tagMap = {
'户外活动部': 'success',
'校长': 'warning',
'校本部': 'info'
}
return tagMap[tag] || 'info'
}
户外活动部: "success",
校长: "warning",
校本部: "info"
};
return tagMap[tag] || "info";
};
const handleLike = (comment) => {
emit('like', comment)
}
emit("like", comment);
};
const handleReply = (comment) => {
emit('reply')
activeReplyId.value = comment.id
replyContent.value = ''
}
emit("reply");
activeReplyId.value = comment.id;
replyContent.value = "";
};
const handleReplyToReply = (reply, parentComment) => {
emit('reply')
activeReplyId.value = reply.id
replyContent.value = `@${reply.userName}:`
}
emit("reply");
activeReplyId.value = reply.id;
replyContent.value = `@${reply.userName}:`;
};
const onEmojiClick = (emoji) => {
replyContent.value += emoji
}
replyContent.value += emoji;
};
const cancelReply = () => {
activeReplyId.value = null
replyContent.value = ''
}
activeReplyId.value = null;
replyContent.value = "";
};
// 切换回复列表的展开/收起
const toggleReplies = (comment) => {
if (!comment.showReplies) {
comment.showReplies = true
comment.showReplies = true;
} else {
comment.showReplies = false
}
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 || '',
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 || ''
}
}
ssbm: reply.ssbm || "",
replyToUser: reply.replyToUser || reply.sjhfrxm || ""
};
};
const submitReply = async (parentComment, replyToComment = null) => {
console.log(parentComment);
if (!replyContent.value.trim()) {
ElMessage.warning('请输入回复内容')
return
ElMessage.warning("请输入回复内容");
return;
}
try {
const ltmasg = getItem("ltmasg")
let pureContent = replyContent.value
if (pureContent.startsWith('@') && pureContent.includes(':')) {
const colonIndex = pureContent.indexOf(':')
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()
pureContent = pureContent.substring(colonIndex + 1).trim();
}
}
if (!pureContent) {
ElMessage.warning('请输入回复内容')
return
ElMessage.warning("请输入回复内容");
return;
}
const targetReply = replyToComment || null
const targetReply = replyToComment || null;
const newReply = {
hfnr: pureContent,
hfrsfzh: ltmasg.sfzh,
@ -248,14 +309,18 @@ const submitReply = async (parentComment, replyToComment = null) => {
ltid: props.replyTo.id,
sfyjhf: 0,
sjhfid: parentComment.id,
sjhfrxm: targetReply ? targetReply.userName : ''
}
sjhfrxm: targetReply ? targetReply.userName : ""
};
try {
const res = await tbGsxtXxltHfSave(newReply)
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)
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);
@ -266,24 +331,32 @@ const submitReply = async (parentComment, replyToComment = null) => {
// parentComment.replies.push(newReply)
// 自动展开回复列表
parentComment.showReplies = true
ElMessage.success('回复成功')
cancelReply()
parentComment.showReplies = true;
ElMessage.success("回复成功");
cancelReply();
} catch (error) {
console.log(error);
ElMessage.error('回复失败')
}
ElMessage.error("回复失败");
}
};
</script>
<style scoped lang="scss">
@import "../styles/luntan-tech.scss";
.comment-list {
background: #ffffff;
.comment-item {
margin-bottom: 20px;
margin-bottom: 18px;
padding-bottom: 18px;
border-bottom: 1px solid $lt-border-dim;
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
}
}
@ -296,6 +369,8 @@ const submitReply = async (parentComment, replyToComment = null) => {
.comment-avatar {
flex-shrink: 0;
border: 1px solid lt-blue(0.28);
box-shadow: 0 0 8px lt-blue(0.12);
}
.comment-content {
@ -312,18 +387,19 @@ const submitReply = async (parentComment, replyToComment = null) => {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.user-name {
font-size: 14px;
font-weight: 500;
color: #303133;
font-weight: 600;
color: $lt-text;
}
.comment-text {
font-size: 14px;
line-height: 1.6;
color: #606266;
line-height: 1.65;
color: $lt-text-dim;
margin-bottom: 8px;
word-break: break-word;
}
@ -336,7 +412,7 @@ const submitReply = async (parentComment, replyToComment = null) => {
.comment-time {
font-size: 12px;
color: #909399;
color: $lt-text-muted;
}
.comment-actions {
@ -349,16 +425,17 @@ const submitReply = async (parentComment, replyToComment = null) => {
align-items: center;
gap: 4px;
font-size: 12px;
color: #909399;
color: $lt-text-muted;
cursor: pointer;
transition: all 0.3s ease;
transition: color 0.2s ease;
&:hover {
color: #409eff;
color: $lt-bar-blue-end;
text-shadow: none;
}
&.active {
color: #409eff;
color: $lt-bar-blue-end;
}
&.toggle-btn {
@ -375,18 +452,30 @@ const submitReply = async (parentComment, replyToComment = null) => {
.reply-input-box {
margin-top: 12px;
padding: 12px;
background: #f5f7fa;
border-radius: 8px;
border-radius: 4px;
@include lt-panel-soft-bg;
}
.reply-label {
font-size: 12px;
color: #606266;
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 {
@ -398,12 +487,29 @@ const submitReply = async (parentComment, replyToComment = null) => {
.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: 16px;
margin-top: 14px;
padding-left: 12px;
border-left: 2px solid #f0f0f0;
border-left: 2px solid lt-blue(0.22);
box-shadow: -2px 0 12px lt-blue(0.06);
}
.reply-item {
@ -419,6 +525,7 @@ const submitReply = async (parentComment, replyToComment = null) => {
.reply-avatar {
flex-shrink: 0;
border: 1px solid lt-blue(0.22);
}
.reply-content {
@ -432,18 +539,19 @@ const submitReply = async (parentComment, replyToComment = null) => {
align-items: center;
gap: 8px;
margin-bottom: 6px;
flex-wrap: wrap;
}
.reply-text {
font-size: 13px;
line-height: 1.6;
color: #606266;
color: $lt-text-dim;
margin-bottom: 6px;
word-break: break-word;
}
.reply-to {
color: #409eff;
color: $lt-bar-btn-text;
margin-right: 4px;
}
@ -455,7 +563,7 @@ const submitReply = async (parentComment, replyToComment = null) => {
.reply-time {
font-size: 12px;
color: #909399;
color: $lt-text-muted;
}
.reply-actions {
@ -480,9 +588,15 @@ const submitReply = async (parentComment, replyToComment = null) => {
.author-tag {
padding: 2px 8px;
border-radius: 4px;
background-color: #409EFF;
color: #fff;
font-size: 12px;
background: lt-blue(0.1);
color: $lt-bar-btn-text;
font-size: 11px;
font-weight: 600;
line-height: 18px;
border: 1px solid lt-blue(0.25);
}
</style>
<style lang="scss">
@import "../styles/luntan-v3emoji-tech.scss";
</style>

View File

@ -1,8 +1,8 @@
<template>
<div class="post-detail">
<div class="post-detail luntan-tech-detail">
<!-- 头部 -->
<div class="detail-header">
<el-button text @click="handleBack">
<el-button class="detail-back-btn" @click="handleBack">
<el-icon>
<ArrowLeft />
</el-icon>
@ -13,15 +13,18 @@
<!-- 帖子内容 -->
<div class="post-main">
<div class="premium-badge" v-if="postData.isPremium">精品</div>
<div class="premium-badge" v-if="postData.isPremium">置顶</div>
<div class="post-author">
<el-avatar :size="50" :src="postData.userAvatar">
<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>
@ -33,9 +36,22 @@
<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 :src="img" fit="cover" :preview-src-list="postData.images" :initial-index="index">
<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>
@ -68,7 +84,11 @@
<div class="comment-section">
<!-- Tab切换 -->
<div class="comment-tabs">
<div class="tab-item" :class="{ active: activeTab === 'all' }" @click="activeTab = 'all'">
<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'">
@ -89,225 +109,272 @@
<!-- 评论列表 -->
<CommentList :comments="filteredComments" @reply="handleReply" :replyTo="replyTo" @like="handleCommentLike" />
<CommentList
:comments="filteredComments"
@reply="handleReply"
:replyTo="replyTo"
@like="handleCommentLike"
/>
</div>
<!-- 回复弹窗 -->
<ReplyDialog v-model="showReplyDialog" :reply-to="replyTo" @success="handleReplySuccess" />
<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'
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 emit = defineEmits(["back"]);
const activeTab = ref('all')
const showReplyDialog = ref(false)
const replyTo = ref(null)
const loading = ref(false)
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: '',
userName: "",
userAvatar: "",
userTag: "",
publishTime: "",
content: "",
images: [],
commentCount: 0,
likeCount: 0,
isPremium: false,
isLiked: false
})
});
// 评论数据
const comments = ref([])
const comments = ref([]);
onMounted(() => {
loadPostDetail()
})
loadPostDetail();
});
const loadPostDetail = async () => {
loading.value = true
loading.value = true;
try {
const res = await tbGsxtXxltHfid(props.postId)
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)) : [],
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,
}
ssbm: res.ssbm
};
// 设置评论数据
if (res.replyList && res.replyList.length > 0) {
comments.value = res.replyList.map(item => ({
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 || '',
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 => ({
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 || '',
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,
})) : []
replyToUser: reply.sjhfrxm || "",
ssbm: reply.ssbm
}))
: []
}));
}
} catch (error) {
console.error('加载详情失败', error)
ElMessage.error('加载详情失败')
console.error("加载详情失败", error);
ElMessage.error("加载详情失败");
} finally {
loading.value = false
}
loading.value = false;
}
};
const filteredComments = computed(() => {
if (activeTab.value === 'author') {
return comments.value.filter(c => c.userName === postData.value.userName)
if (activeTab.value === "author") {
return comments.value.filter((c) => c.userName === postData.value.userName);
}
console.log(comments.value);
return comments.value
})
return comments.value;
});
const getTagType = (tag) => {
const tagMap = {
'户外活动部': 'success',
'校长': 'warning',
'校本部': 'info'
}
return tagMap[tag] || 'info'
}
户外活动部: "success",
校长: "warning",
校本部: "info"
};
return tagMap[tag] || "info";
};
const handleBack = () => {
emit('back')
}
emit("back");
};
const handleLike = () => {
postData.value.isLiked = !postData.value.isLiked
postData.value.likeCount += postData.value.isLiked ? 1 : -1
ElMessage.success(postData.value.isLiked ? '点赞成功' : '取消点赞')
}
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,
}
...postData.value
};
// showReplyDialog.value = true
}
};
const replyToData = () => {
replyTo.value = {
...postData.value,
}
showReplyDialog.value = true
}
...postData.value
};
showReplyDialog.value = true;
};
const handleCommentLike = (comment) => {
comment.isLiked = !comment.isLiked
comment.likeCount += comment.isLiked ? 1 : -1
}
comment.isLiked = !comment.isLiked;
comment.likeCount += comment.isLiked ? 1 : -1;
};
const handleReplySuccess = () => {
ElMessage.success('回复成功')
loadPostDetail()
}
ElMessage.success("回复成功");
loadPostDetail();
};
</script>
<style scoped lang="scss">
.post-detail {
background: #f5f7fa;
@import "../styles/luntan-tech.scss";
.luntan-tech-detail {
background: transparent;
height: 100%;
display: flex;
flex-direction: column;
}
.detail-header {
background: #fff;
padding: 16px 20px;
padding: 14px 18px;
display: flex;
align-items: center;
border-bottom: 1px solid #f0f0f0;
flex-shrink: 0;
border-bottom: 1px solid lt-blue(0.12);
background: linear-gradient(
135deg,
lt-blue-light(0.14) 0%,
lt-blue(0.07) 48%,
transparent 100%
);
:deep(.detail-back-btn.el-button) {
height: 36px;
padding: 0 14px;
font-weight: 500;
border-radius: 8px;
background: #ffffff !important;
border: 1px solid lt-blue(0.22) !important;
color: $lt-bar-btn-text !important;
box-shadow: 0 1px 4px lt-blue(0.12);
&:hover,
&:focus {
color: $lt-bar-blue-end !important;
border-color: lt-blue(0.4) !important;
background: #f8fafc !important;
box-shadow: 0 2px 10px lt-blue(0.18);
}
}
: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: 500;
color: #303133;
font-weight: 600;
color: $lt-text;
margin-right: 60px;
letter-spacing: 0.08em;
text-shadow: 0 0 14px lt-blue(0.18);
}
}
.post-main {
background: #fff;
padding: 20px;
margin-bottom: 12px;
padding: 20px 18px 18px;
margin: 12px 12px 0;
position: relative;
flex-shrink: 0;
border-radius: 4px;
@include lt-panel-soft-bg;
}
.comment-section {
background: #fff;
padding: 16px 20px 40px 20px;
padding: 8px 12px 32px;
flex: 1;
overflow-y: auto;
}
.post-main {
background: #fff;
padding: 20px;
margin-bottom: 12px;
position: relative;
flex-shrink: 0;
min-height: 0;
}
.premium-badge {
position: absolute;
top: 20px;
right: 20px;
background: linear-gradient(135deg, #ff9a56 0%, #ff6b6b 100%);
top: 16px;
right: 16px;
background: linear-gradient(
135deg,
$lt-bar-blue-start 0%,
$lt-bar-blue-end 100%
);
color: #fff;
padding: 4px 12px;
border-radius: 4px;
border-radius: 2px;
font-size: 12px;
font-weight: 500;
font-weight: 600;
border: 1px solid rgba(255, 255, 255, 0.35);
box-shadow: 0 2px 10px lt-blue(0.3);
}
.post-author {
@ -317,6 +384,11 @@ const handleReplySuccess = () => {
align-items: flex-start;
}
.author-avatar {
border: 2px solid lt-blue(0.35);
box-shadow: 0 0 12px lt-blue(0.22);
}
.author-info {
flex: 1;
padding-top: 2px;
@ -325,34 +397,46 @@ const handleReplySuccess = () => {
.author-name-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 4px;
}
.author-name {
font-size: 16px;
font-weight: 500;
color: #303133;
font-weight: 700;
color: $lt-text;
}
.level-badge {
font-size: 11px;
padding: 2px 8px;
border-radius: 2px;
background: lt-blue(0.1);
color: $lt-bar-blue-end;
font-weight: 600;
border: 1px solid lt-blue(0.28);
}
.author-tag {
padding: 2px 8px;
border-radius: 4px;
background-color: #409EFF;
color: #fff;
border-radius: 2px;
background: lt-blue(0.08);
color: $lt-bar-btn-text;
font-size: 12px;
line-height: 18px;
border: 1px solid lt-blue(0.25);
}
.publish-time {
font-size: 12px;
color: #909399;
color: $lt-text-muted;
}
.post-content-text {
font-size: 14px;
line-height: 1.8;
color: #303133;
line-height: 1.85;
color: $lt-text-dim;
margin-bottom: 16px;
word-break: break-word;
}
@ -367,8 +451,10 @@ const handleReplySuccess = () => {
.image-item {
width: 200px;
height: 140px;
border-radius: 8px;
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%;
@ -382,8 +468,8 @@ const handleReplySuccess = () => {
justify-content: center;
width: 100%;
height: 100%;
background: #f5f7fa;
color: #c0c4cc;
background: rgba(10, 30, 60, 0.6);
color: $lt-text-muted;
font-size: 24px;
}
@ -391,24 +477,20 @@ const handleReplySuccess = () => {
display: flex;
gap: 24px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
border-top: 1px solid $lt-border-dim;
}
.stat-item {
display: flex;
align-items: center;
gap: 6px;
color: #909399;
color: $lt-text-muted;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
transition: color 0.2s ease;
&:hover {
color: #409eff;
}
&.active {
color: #409eff;
color: $lt-cyan;
}
.el-icon {
@ -416,40 +498,34 @@ const handleReplySuccess = () => {
}
}
.comment-section {
background: #fff;
padding: 16px 20px 40px 20px;
flex: 1;
overflow-y: auto;
}
.comment-tabs {
display: flex;
align-items: center;
margin-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 14px;
border-bottom: 1px solid $lt-border-dim;
}
.tab-item {
padding: 12px 16px;
padding: 12px 14px;
font-size: 14px;
color: #606266;
color: $lt-text-muted;
cursor: pointer;
position: relative;
transition: all 0.3s ease;
transition: color 0.2s ease;
&.active {
color: #409eff;
font-weight: 500;
color: $lt-cyan;
font-weight: 600;
&::after {
content: '';
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: #409eff;
background: linear-gradient(90deg, transparent, $lt-cyan, transparent);
box-shadow: 0 0 10px rgba(0, 227, 255, 0.6);
}
}
}
@ -461,11 +537,28 @@ const handleReplySuccess = () => {
}
.top-input {
margin-bottom: 20px;
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>

View File

@ -1,9 +1,8 @@
<template>
<div class="post-item" @click="handleClick">
<!-- 精品标签 -->
<div class="premium-badge" v-if="post.isPremium">精品</div>
<div class="premium-badge" v-if="post.isPremium">置顶</div>
<div class="post-main-content">
<el-avatar :size="50" :src="post.userAvatar" class="post-avatar">
<el-avatar :size="44" :src="post.userAvatar" class="post-avatar">
<img src="@/assets/images/mr.png" />
</el-avatar>
@ -11,6 +10,9 @@
<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>
@ -18,13 +20,37 @@
<div class="post-time">{{ post.publishTime }}</div>
</div>
<div class="post-content">
<div class="post-text">{{ post.content }}</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">
<div v-for="(img, index) in post.images" :key="index" class="image-item" @click.stop>
<el-image :src="img" fit="cover" :preview-src-list="post.images" :initial-index="index">
<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>
@ -44,12 +70,15 @@
</el-icon>
<span>{{ post.commentCount || 0 }}</span>
</div>
<!-- <div class="action-item" :class="{ active: post.isLiked }" @click.stop="handleLike">
<el-icon>
<Promotion />
</el-icon>
<span>{{ post.likeCount || 0 }}</span>
</div> -->
<button
v-if="!post.isPremium"
type="button"
class="post-pin-btn"
:disabled="pinning"
@click.stop="handlePinTop"
>
置顶
</button>
</div>
</div>
</div>
@ -57,179 +86,311 @@
</template>
<script setup>
import { ChatDotRound, Promotion, Picture } from '@element-plus/icons-vue'
import { computed, ref } from "vue";
import { ChatDotRound, Picture } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import { tbGsxtXxltHfid, tbGsxtXxltUpdate } from "@/api/tbGsxtXxltHf.js";
const props = defineProps({
post: {
type: Object,
required: true
}
})
});
const emit = defineEmits(['like', 'click'])
const emit = defineEmits(["like", "click", "pin"]);
const getTagType = (tag) => {
const tagMap = {
'已动态': 'success',
'校长': 'warning',
'校本部': 'info'
}
return 'warning'
}
const pinning = ref(false);
const handleLike = () => {
emit('like', props.post)
const handlePinTop = async () => {
if (pinning.value) return;
pinning.value = true;
try {
const detail = await tbGsxtXxltHfid(props.post.id);
await tbGsxtXxltUpdate({ ...detail, sfzd: 1 });
ElMessage.success("置顶成功");
emit("pin");
} catch (e) {
console.error(e);
ElMessage.error("置顶失败");
} finally {
pinning.value = false;
}
};
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)
}
emit("click", props.post);
};
</script>
<style scoped lang="scss">
@import "../styles/luntan-tech.scss";
.post-item {
background: #fff !important;
border-radius: 8px !important;
padding: 20px !important;
margin-bottom: 16px !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08) !important;
position: relative !important;
transition: all 0.3s ease !important;
cursor: pointer !important;
position: relative;
display: flex;
align-items: flex-start;
gap: 12px;
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 {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12) !important;
border-color: lt-blue(0.35);
box-shadow: $lt-glow-strong, inset 0 1px 0 lt-blue(0.12);
}
}
.post-pin-btn {
flex-shrink: 0;
padding: 3px 10px;
font-size: 12px;
font-weight: 600;
color: #fff;
cursor: pointer;
border: none;
border-radius: 3px;
background: linear-gradient(
135deg,
$lt-bar-blue-start 0%,
$lt-bar-blue-end 100%
);
box-shadow: 0 1px 5px lt-blue(0.2);
transition: opacity 0.2s ease, transform 0.15s ease;
&:hover:not(:disabled) {
opacity: 0.9;
transform: translateY(-1px);
}
&:disabled {
opacity: 0.65;
cursor: not-allowed;
}
}
.premium-badge {
position: absolute !important;
top: 16px !important;
right: 16px !important;
background: linear-gradient(135deg, #ff9a56 0%, #ff6b6b 100%) !important;
color: #fff !important;
padding: 4px 12px !important;
border-radius: 4px !important;
font-size: 12px !important;
font-weight: 500 !important;
box-shadow: 0 2px 4px rgba(255, 107, 107, 0.3) !important;
position: absolute;
top: 14px;
right: 14px;
background: linear-gradient(
135deg,
$lt-bar-blue-start 0%,
$lt-bar-blue-end 100%
);
color: #fff;
padding: 3px 10px;
border-radius: 2px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.04em;
border: 1px solid rgba(255, 255, 255, 0.35);
box-shadow: 0 2px 10px lt-blue(0.28);
}
.post-main-content {
display: flex !important;
gap: 12px !important;
align-items: flex-start !important;
flex: 1;
min-width: 0;
display: flex;
gap: 12px;
align-items: flex-start;
}
.post-avatar {
flex-shrink: 0 !important;
flex-shrink: 0;
border: 2px solid lt-blue(0.35);
box-shadow: 0 0 10px lt-blue(0.2);
}
.post-right {
flex: 1 !important;
min-width: 0 !important;
padding-top: 2px !important;
flex: 1;
min-width: 0;
padding-top: 0;
}
.post-header {
margin-bottom: 12px !important;
margin-bottom: 10px;
}
.user-name-row {
display: flex !important;
align-items: center !important;
gap: 8px !important;
margin-bottom: 4px !important;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px 8px;
margin-bottom: 4px;
}
.user-name {
font-size: 16px !important;
font-weight: 500 !important;
color: #303133 !important;
font-size: 16px;
font-weight: 700;
color: $lt-text;
}
.level-badge {
font-size: 11px;
padding: 2px 8px;
border-radius: 2px;
background: lt-blue(0.1);
color: $lt-bar-blue-end;
font-weight: 600;
line-height: 1.3;
border: 1px solid lt-blue(0.28);
}
.post-time {
font-size: 12px !important;
color: #909399 !important;
font-size: 12px;
color: $lt-text-muted;
}
.post-content {
margin-bottom: 16px !important;
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 {
font-size: 14px !important;
line-height: 1.6 !important;
color: #606266 !important;
margin-bottom: 12px !important;
word-break: break-word !important;
display: inline;
}
.full-text-link {
margin-left: 4px;
color: $lt-bar-blue-end;
font-weight: 500;
cursor: pointer;
&:hover {
text-shadow: 0 0 8px lt-blue(0.35);
}
}
.post-images {
display: flex !important;
gap: 8px !important;
flex-wrap: wrap !important;
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: 200px !important;
height: 140px !important;
border-radius: 8px !important;
overflow: hidden !important;
cursor: pointer !important;
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% !important;
height: 100% !important;
width: 100%;
height: 100%;
}
}
.post-images.is-three .image-item {
max-width: 200px;
}
.image-error {
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 100% !important;
height: 100% !important;
background: #f5f7fa !important;
color: #c0c4cc !important;
font-size: 24px !important;
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 !important;
gap: 24px !important;
padding-top: 12px !important;
border-top: 1px solid #f0f0f0 !important;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px 20px;
padding-top: 10px;
border-top: 1px solid $lt-border-dim;
}
.action-item {
display: flex !important;
align-items: center !important;
gap: 6px !important;
color: #909399 !important;
font-size: 14px !important;
cursor: pointer !important;
transition: all 0.3s ease !important;
display: flex;
align-items: center;
gap: 6px;
color: $lt-text-muted;
font-size: 13px;
cursor: pointer;
transition: color 0.2s ease;
&:hover {
color: #409eff !important;
}
&.active {
color: #409eff !important;
color: $lt-bar-blue-end;
}
.el-icon {
font-size: 18px !important;
font-size: 17px;
}
}
.author-tag {
padding: 2px 8px;
border-radius: 4px;
background-color: #409EFF;
color: #fff;
font-size: 12px;
border-radius: 2px;
background: lt-blue(0.08);
color: $lt-bar-btn-text;
font-size: 11px;
line-height: 18px;
font-weight: 600;
border: 1px solid lt-blue(0.25);
}
</style>

View File

@ -1,20 +1,71 @@
<template>
<div class="post-list">
<!-- 发布按钮 -->
<div class="publish-section">
<el-button type="primary" @click="showPublishDialog = true">
<el-icon>
<Edit />
</el-icon>
发布帖子
</el-button>
<!-- 贴吧风格顶栏左信息 + 右操作页面背景仍为外层网格不变 -->
<div class="bar-head">
<div class="bar-logo">
<el-icon><ChatDotRound /></el-icon>
</div>
<div class="bar-text-block">
<div class="bar-title">信息论坛</div>
<div class="bar-meta-row">
<span class="bar-meta-item">帖子 {{ replyCountStub.fts }}</span>
<span class="bar-meta-sep">·</span>
<span class="bar-meta-item bar-meta-reply"
>回复 {{ replyCountStub.hfs }}</span
>
</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">
<PostItem v-for="post in postList" :key="post.id" :post="post" @like="handleLike"
@click="handlePostClick(post)" />
<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"
@pin="handlePostPinned"
@click="handlePostClick(post)"
/>
<!-- 加载更多提示 -->
<div v-if="loadingMore" class="loading-more">
@ -25,71 +76,64 @@
</div>
<!-- 没有更多数据提示 -->
<div v-if="noMore && postList.length > 0" class="no-more">
<div v-if="noMore && normalList.length > 0" class="no-more">
没有更多数据了
</div>
<!-- 空状态 -->
<el-empty v-if="!loading && postList.length === 0" description="暂无帖子" />
<el-empty
v-if="!loading && postList.length === 0"
class="post-empty"
description="暂无帖子"
/>
</div>
<!-- 发布对话框 -->
<PublishDialog v-model="showPublishDialog" @success="handlePublishSuccess" />
<PublishDialog
v-model="showPublishDialog"
@success="handlePublishSuccess"
/>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { Edit, Loading } 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)
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";
import { qcckGet } from "@/api/qcckApi.js";
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 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 => ({
const total = ref(0);
// 获取置顶数据
const pinnedList = ref([]);
const getpinnedList = () => {
const params = {
pageCurrent: 1,
pageSize: 10,
sfzd: 1
};
tbGsxtXxltSelectPage(params).then((res) => {
pinnedList.value = (res.records || []).map((item) => ({
id: item.id,
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)) : [],
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,
@ -97,70 +141,394 @@ const loadPosts = async (isLoadMore = false) => {
ssbm: item.ssbm,
// 保存原始数据
rawData: item
}))
}));
});
};
const normalList = computed(() => postList.value);
/** 置顶行展示标题:优先接口 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(() => {
getpinnedList();
loadPosts();
getStatistics();
});
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]
postList.value = [...postList.value, ...data];
} else {
postList.value = data
postList.value = data;
}
total.value = res.total || 0
total.value = res.total || 0;
} catch (error) {
console.error('加载失败', error)
ElMessage.error('加载失败')
console.error("加载失败", error);
ElMessage.error("加载失败");
} finally {
loading.value = false
loadingMore.value = false
}
loading.value = false;
loadingMore.value = false;
}
};
// 加载更多
const loadMore = () => {
if (postList.value.length >= total.value) {
return
return;
}
listQuery.value.pageCurrent++
loadPosts(true)
}
listQuery.value.pageCurrent++;
loadPosts(true);
};
const handleLike = (post) => {
post.isLiked = !post.isLiked
post.likeCount += post.isLiked ? 1 : -1
ElMessage.success(post.isLiked ? '点赞成功' : '取消点赞')
}
post.isLiked = !post.isLiked;
post.likeCount += post.isLiked ? 1 : -1;
ElMessage.success(post.isLiked ? "点赞成功" : "取消点赞");
};
const handlePublishSuccess = () => {
// 重置分页并重新加载
listQuery.value.pageCurrent = 1
loadPosts()
}
listQuery.value.pageCurrent = 1;
loadPosts();
getStatistics();
};
const emit = defineEmits(['openDetail'])
const handlePostPinned = () => {
listQuery.value.pageCurrent = 1;
loadPosts(false);
getpinnedList();
getStatistics();
};
const replyCountStub = ref({
fts: 0,
hfs: 0
});
const totalDisplay = (n) => {
if (n >= 10000) return `${(n / 10000).toFixed(1)}W`;
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
return String(n);
};
// 获取统计数量
const getStatistics = () => {
qcckGet({}, "/mosty-gsxt/tbGsxtXxlt/lttj").then((res) => {
console.log(res);
replyCountStub.value = {
fts: totalDisplay(res.fts),
hfs: totalDisplay(res.hfs)
};
});
};
const emit = defineEmits(["openDetail"]);
const handlePostClick = (post) => {
emit('openDetail', post)
}
emit("openDetail", post);
};
</script>
<style scoped lang="scss">
@import "../styles/luntan-tech.scss";
.post-list {
background: transparent;
}
.publish-section {
margin-bottom: 20px;
.bar-head {
--bar-logo-size: 72px;
--bar-logo-gap: 18px;
display: grid;
grid-template-columns: auto 1fr auto;
grid-template-rows: auto;
column-gap: var(--bar-logo-gap);
align-items: center;
margin-bottom: 18px;
padding: 22px 22px 20px;
border-radius: 12px;
/* 深蓝科技横幅:左亮右暗,贴近深色企业顶栏 */
background-color: #0a0f18;
background-image: linear-gradient(
92deg,
rgba(147, 197, 253, 0.09) 0%,
rgba(96, 165, 250, 0.04) 6%,
transparent 14%
),
linear-gradient(100deg, rgba(255, 255, 255, 0.05) 0%, transparent 12%),
linear-gradient(
110deg,
#2a4f8c 0%,
#1a3358 26%,
#121c2e 58%,
#101828 78%,
#080d14 100%
);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.38),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
.bar-pill {
color: $lt-bar-btn-text;
background: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.95);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
&:hover {
opacity: 1;
background: #f8fafc;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
}
}
}
.bar-logo {
grid-column: 1;
grid-row: 1;
align-self: center;
flex-shrink: 0;
width: var(--bar-logo-size);
height: var(--bar-logo-size);
border-radius: 16px;
display: flex;
justify-content: flex-end;
align-items: center;
justify-content: center;
background: #fff;
padding: 16px 20px;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.2);
.el-icon {
font-size: 38px;
color: $lt-cyan-mid;
}
}
.bar-text-block {
grid-column: 2;
grid-row: 1;
align-self: center;
min-width: 0;
display: flex;
flex-direction: column;
gap: 10px;
justify-content: center;
}
.bar-title {
font-size: 24px;
font-weight: 800;
color: #ffffff;
letter-spacing: 0.04em;
line-height: 1.2;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
}
.bar-meta-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px 10px;
font-size: 15px;
font-weight: 600;
color: rgba(255, 255, 255, 0.95);
line-height: 1.4;
letter-spacing: 0.02em;
}
.bar-meta-sep {
opacity: 0.65;
font-weight: 500;
}
.bar-meta-reply {
font-weight: 700;
font-size: 16px;
color: #fff;
text-shadow: 0 0 12px rgba(255, 255, 255, 0.35);
}
.bar-actions {
grid-column: 3;
grid-row: 1;
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
align-self: center;
}
.bar-pill {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 10px 22px;
border-radius: 999px;
font-size: 14px;
font-weight: 600;
color: #fff;
border: 1px solid $lt-cyan-mid;
background: $lt-cyan-mid;
cursor: pointer;
transition: opacity 0.2s ease, box-shadow 0.2s ease;
.el-icon {
font-size: 16px;
}
&:hover {
opacity: 0.85;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.35);
}
}
.bar-pill-primary {
&:hover {
opacity: 0.85;
}
}
.bar-head .bar-pill-primary:hover {
opacity: 1;
}
// 置顶缩略区
.pinned-block {
margin-bottom: 4px;
}
.pinned-list {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
padding: 10px 12px;
background: $lt-panel-soft;
border: 1px solid $lt-border-dim;
}
.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 $lt-border-dim;
}
&:hover {
background: rgba(64, 158, 255, 0.06);
}
}
.pinned-badge {
flex-shrink: 0;
padding: 2px 8px;
font-size: 11px;
font-weight: 600;
color: $lt-cyan-mid;
border: 1px solid rgba(64, 158, 255, 0.4);
border-radius: 4px;
background: rgba(64, 158, 255, 0.06);
}
.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: $lt-border-dim;
}
.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(245, 247, 250, 0.7);
}
:deep(.el-loading-spinner .path) {
stroke: $lt-cyan-mid;
}
}
.post-empty {
padding: 48px 0;
:deep(.el-empty__description) {
color: $lt-text-muted;
}
}
.loading-more {
@ -168,19 +536,20 @@ const handlePostClick = (post) => {
align-items: center;
justify-content: center;
padding: 20px;
color: #909399;
color: $lt-text-muted;
font-size: 14px;
gap: 8px;
.el-icon {
font-size: 16px;
color: $lt-cyan-mid;
}
}
.no-more {
text-align: center;
padding: 20px;
color: #c0c4cc;
color: $lt-text-muted;
font-size: 14px;
}
</style>

View File

@ -1,13 +1,31 @@
<template>
<el-dialog v-model="dialogVisible" title="发布帖子" width="600px" :before-close="handleClose">
<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-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-input
v-model="form.content"
type="textarea"
:rows="6"
placeholder="请输入帖子内容"
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item label="表情">
@ -22,6 +40,7 @@
<Upload v-model="imageIds" :limit="9" :isImg="true" :isAll="true" />
</el-form-item>
</el-form>
</div>
<template #footer>
<el-button @click="handleClose">取消</el-button>
@ -33,115 +52,121 @@
</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'
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: '活动'
}
"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 emit = defineEmits(["update:modelValue", "success"]);
const dialogVisible = ref(false)
const formRef = ref()
const submitting = ref(false)
const imageIds = ref([])
const dialogVisible = ref(false);
const formRef = ref();
const submitting = ref(false);
const imageIds = ref([]);
const form = ref({
title: '',
content: ''
})
title: "",
content: ""
});
const rules = {
title: [
{ required: true, message: '请输入标题', trigger: 'blur' }
],
content: [
{ required: true, message: '请输入内容', trigger: 'blur' }
]
}
title: [{ required: true, message: "请输入标题", trigger: "blur" }],
content: [{ required: true, message: "请输入内容", trigger: "blur" }]
};
watch(() => props.modelValue, (val) => {
dialogVisible.value = val
})
watch(
() => props.modelValue,
(val) => {
dialogVisible.value = val;
}
);
watch(dialogVisible, (val) => {
emit('update:modelValue', val)
emit("update:modelValue", val);
if (!val) {
resetForm()
resetForm();
}
})
});
const onEmojiClick = (emoji) => {
form.value.content += emoji
}
form.value.content += emoji;
};
const handleClose = () => {
dialogVisible.value = false
}
dialogVisible.value = false;
};
const handleSubmit = async () => {
if (!formRef.value) return
if (!formRef.value) return;
await formRef.value.validate(async (valid) => {
if (valid) {
submitting.value = true
submitting.value = true;
try {
const ltmasg = getItem('ltmasg')
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 || ''
}
tp: imageIds.value.join(","),
fbrsfzh: ltmasg?.sfzh || "",
fbrxm: ltmasg?.xm || "",
fbrtx: ltmasg?.tx || ""
};
await tbGsxtXxltSave(postData)
await tbGsxtXxltSave(postData);
ElMessage.success('发布成功')
emit('success')
handleClose()
ElMessage.success("发布成功");
emit("success");
handleClose();
} catch (error) {
console.error('发布失败', error)
ElMessage.error('发布失败')
console.error("发布失败", error);
ElMessage.error("发布失败");
} finally {
submitting.value = false
submitting.value = false;
}
}
})
}
});
};
const resetForm = () => {
form.value = {
title: '',
content: ''
}
imageIds.value = []
formRef.value?.resetFields()
}
title: "",
content: ""
};
imageIds.value = [];
formRef.value?.resetFields();
};
</script>
<style scoped lang="scss">
// 样式已由Upload组件内部处理</style>
// Upload 等子组件样式在各自内部
::v-deep .form-item-box {
width: 100% !important;
}
</style>
<style lang="scss">
@import "../styles/luntan-dialog-tech.scss";
</style>

View File

@ -1,6 +1,7 @@
<template>
<el-dialog
v-model="dialogVisible"
class="luntan-tech-dialog"
title="发表回复"
width="600px"
:before-close="handleClose"
@ -155,3 +156,7 @@ const resetForm = () => {
gap: 12px;
}
</style>
<style lang="scss">
@import '../styles/luntan-dialog-tech.scss';
</style>

View File

@ -1,8 +1,9 @@
<template>
<div class="user-card">
<div class="user-card-head">
<div class="user-avatar">
<div class="avatar-wrapper" @click="showAvatarDialog = true">
<el-avatar :size="80" :src="avatarUrl">
<el-avatar :size="56" :src="avatarUrl">
<img src="@/assets/images/mr.png" />
</el-avatar>
<div class="avatar-overlay">
@ -12,38 +13,66 @@
</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>
<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>
<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>
<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>
<span class="label">部门</span>
<span class="value">{{ userInfo.department || "-" }}</span>
</div>
</div>
</div>
<!-- 更换头像对话框 -->
<ChangeAvatar v-model="showAvatarDialog" title="更换头像" @avatarUpdated="handleAvatarUpdated" />
<ChangeAvatar
v-model="showAvatarDialog"
title="更换头像"
@avatarUpdated="handleAvatarUpdated"
/>
<!-- 编辑昵称对话框 -->
<el-dialog v-model="showNicknameDialog" title="编辑昵称" width="400px" center :close-on-click-modal="false">
<el-form ref="nicknameFormRef" :model="nicknameForm" :rules="nicknameRules" label-width="80px">
<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-input
v-model="nicknameForm.nickname"
placeholder="请输入昵称"
maxlength="20"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
@ -54,168 +83,186 @@
</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'
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 showAvatarDialog = ref(false);
const showNicknameDialog = ref(false);
const nicknameFormRef = ref();
const userInfo = ref({
avatar: '',
account: '',
name: '',
department: '',
nickname: ''
})
avatar: "",
account: "",
name: "",
department: "",
nickname: ""
});
const nicknameForm = reactive({
nickname: ''
})
nickname: ""
});
const nicknameRules = {
nickname: [
{ required: true, message: '请输入昵称', trigger: 'blur' },
{ min: 2, max: 20, message: '昵称长度在 2 到 20 个字符', trigger: 'blur' }
{ 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) : ''
})
return userInfo.value.avatar ? setAddress(userInfo.value.avatar) : "";
});
// 加载用户信息
const loadUserInfo = async () => {
const sfzh = getItem('idEntityCard')
let ltmasg = getItem('ltmasg')
const sfzh = getItem("idEntityCard");
let ltmasg = getItem("ltmasg");
if (!ltmasg) {
try {
const res = await tbGsxtXxltTxTxQueryBySfzh({ sfzh: sfzh })
const res = await tbGsxtXxltTxTxQueryBySfzh({ sfzh: sfzh });
console.log(res);
const deptId = getItem('deptId')?.[0]
const deptId = getItem("deptId")?.[0];
ltmasg = {
...res,
deptName: deptId?.deptName || ''
}
setItem('ltmasg', ltmasg)
deptName: deptId?.deptName || ""
};
setItem("ltmasg", ltmasg);
} catch (error) {
console.error('加载用户信息失败:', 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 || ''
}
}
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 ltmasg = getItem("ltmasg");
const updateData = {
...ltmasg,
tx: newAvatar
}
};
await tbGsxtXxltTxTxSave(updateData)
removeItem('ltmasg')
await loadUserInfo()
ElMessage.success('头像更新成功')
await tbGsxtXxltTxTxSave(updateData);
removeItem("ltmasg");
await loadUserInfo();
ElMessage.success("头像更新成功");
} catch (error) {
console.error('更新头像失败:', error)
ElMessage.error('头像更新失败,请重试')
}
console.error("更新头像失败:", error);
ElMessage.error("头像更新失败,请重试");
}
};
// 处理保存昵称
const handleSaveNickname = async () => {
if (!nicknameFormRef.value) return
if (!nicknameFormRef.value) return;
await nicknameFormRef.value.validate(async (valid) => {
if (valid) {
try {
const ltmasg = getItem('ltmasg')
const ltmasg = getItem("ltmasg");
const updateData = {
...ltmasg,
nc: nicknameForm.nickname
}
};
await tbGsxtXxltTxTxSave(updateData)
removeItem('ltmasg')
await loadUserInfo()
showNicknameDialog.value = false
ElMessage.success('昵称保存成功')
await tbGsxtXxltTxTxSave(updateData);
removeItem("ltmasg");
await loadUserInfo();
showNicknameDialog.value = false;
ElMessage.success("昵称保存成功");
} catch (error) {
console.error('保存昵称失败:', error)
ElMessage.error('昵称保存失败,请重试')
console.error("保存昵称失败:", error);
ElMessage.error("昵称保存失败,请重试");
}
}
})
}
});
};
// 监听昵称对话框打开,初始化表单
const openNicknameDialog = () => {
nicknameForm.nickname = userInfo.value.nickname
}
nicknameForm.nickname = userInfo.value.nickname;
};
// 监听对话框显示状态
const unwatchNickname = () => {
if (showNicknameDialog.value) {
openNicknameDialog()
}
openNicknameDialog();
}
};
onMounted(() => {
loadUserInfo()
})
loadUserInfo();
});
// 监听昵称对话框
const stopWatch = () => {
if (showNicknameDialog.value) {
nicknameForm.nickname = userInfo.value.nickname
}
nicknameForm.nickname = userInfo.value.nickname;
}
};
// 使用 watch 监听对话框状态
import { watch } from 'vue'
import { watch } from "vue";
watch(showNicknameDialog, (newVal) => {
if (newVal) {
nicknameForm.nickname = userInfo.value.nickname
nicknameForm.nickname = userInfo.value.nickname;
}
})
});
</script>
<style scoped lang="scss">
@import "../styles/luntan-tech.scss";
.user-card {
background: #fff;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
padding: 18px 16px 16px;
background: linear-gradient(
135deg,
$lt-bar-blue-start 0%,
$lt-bar-blue-end 100%
);
border: 1px solid rgba(255, 255, 255, 0.22);
box-shadow: 0 4px 18px lt-blue(0.22);
}
.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.2);
}
.user-avatar {
display: flex;
justify-content: center;
margin-bottom: 20px;
flex-shrink: 0;
.avatar-wrapper {
position: relative;
cursor: pointer;
border-radius: 50%;
overflow: hidden;
border: 2px solid rgba(255, 255, 255, 0.5);
&:hover .avatar-overlay {
opacity: 1;
@ -227,7 +274,7 @@ watch(showNicknameDialog, (newVal) => {
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
background: rgba(0, 0, 0, 0.55);
display: flex;
justify-content: center;
align-items: center;
@ -236,19 +283,41 @@ watch(showNicknameDialog, (newVal) => {
border-radius: 50%;
.upload-icon {
font-size: 24px;
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: #ffffff;
word-break: break-all;
}
.sub-stats {
font-size: 12px;
color: rgba(255, 255, 255, 0.82);
line-height: 1.4;
}
.user-info {
.info-item {
display: flex;
align-items: center;
margin-bottom: 12px;
font-size: 14px;
align-items: flex-start;
margin-bottom: 10px;
font-size: 13px;
position: relative;
&:last-child {
@ -257,14 +326,14 @@ watch(showNicknameDialog, (newVal) => {
&.clickable {
cursor: pointer;
padding: 4px 8px;
padding: 6px 8px;
margin-left: -8px;
margin-right: -8px;
border-radius: 4px;
transition: background-color 0.3s ease;
border-radius: 6px;
transition: background-color 0.2s ease;
&:hover {
background-color: #f5f7fa;
background-color: rgba(255, 255, 255, 0.12);
.edit-icon {
opacity: 1;
@ -273,23 +342,28 @@ watch(showNicknameDialog, (newVal) => {
}
.label {
color: #909399;
min-width: 50px;
color: rgba(255, 255, 255, 0.72);
min-width: 40px;
flex-shrink: 0;
}
.value {
color: #303133;
color: #ffffff;
flex: 1;
word-break: break-all;
}
.edit-icon {
margin-left: 8px;
color: #409eff;
margin-left: 6px;
color: rgba(255, 255, 255, 0.95);
font-size: 14px;
opacity: 0;
transition: opacity 0.3s ease;
transition: opacity 0.2s ease;
}
}
}
</style>
<style lang="scss">
@import "../styles/luntan-dialog-tech.scss";
</style>

View File

@ -1,82 +1,304 @@
<template>
<div class="luntan-container">
<!-- 列表页 -->
<!-- 列表页主列帖子 + 右侧信息栏 -->
<template v-if="!showDetail">
<div class="luntan-left">
<UserCard />
</div>
<div class="luntan-center">
<div class="luntan-main">
<PostList @openDetail="handleOpenDetail" />
</div>
<div class="luntan-right">
<!-- 右侧留白区域可以后续添加其他内容 -->
<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-left"></div>
<div class="luntan-detail-wrap">
<div class="luntan-detail">
<PostDetail :post-id="currentPostId" @back="handleBack" />
</div>
<div class="luntan-right"></div>
</div>
</template>
</div>
</template>
<script setup>
import { ref } from 'vue'
import UserCard from './components/UserCard.vue'
import PostList from './components/PostList.vue'
import PostDetail from './components/PostDetail.vue'
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";
import { qcckGet } from "@/api/qcckApi.js";
const showDetail = ref(false);
const currentPostId = ref(null);
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 = () => {
hotLoading.value = true;
qcckGet({}, "/mosty-gsxt/tbGsxtXxlt/hotList")
.then((res) => {
console.log(res);
hotList.value = res.map(mapHotPost);
hotLoading.value = false;
})
.catch((err) => {})
.finally(() => {
hotLoading.value = false;
});
};
onMounted(() => {
loadHotNews();
});
const handleOpenDetail = (post) => {
currentPostId.value = post.id
showDetail.value = true
}
currentPostId.value = post.id;
showDetail.value = true;
};
const handleBack = () => {
showDetail.value = false
currentPostId.value = null
}
showDetail.value = false;
currentPostId.value = null;
};
</script>
<style scoped lang="scss">
@import "./styles/luntan-tech.scss";
.luntan-container {
position: relative;
display: flex;
gap: 20px;
padding: 20px;
background-color: #f5f7fa;
min-height: calc(100vh - 60px);
gap: 24px;
padding: 20px 24px;
box-sizing: border-box;
/* 固定可视高度 + 隐藏溢出让主列在内部滚动flex 子项需 min-height:0 */
height: calc(100vh - 60px);
max-height: calc(100vh - 60px);
min-height: 0;
overflow: hidden;
background: #ffffff;
}
.luntan-left {
width: 240px;
flex-shrink: 0;
}
.luntan-center {
.luntan-main {
position: relative;
z-index: 1;
flex: 1;
min-width: 0;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
}
.luntan-right {
width: 240px;
.luntan-sidebar {
position: relative;
z-index: 1;
width: 400px;
flex-shrink: 0;
min-height: 0;
align-self: stretch;
display: flex;
flex-direction: column;
gap: 16px;
overflow: hidden;
}
.hot-news-card {
position: relative;
flex: 0 1 auto;
display: flex;
flex-direction: column;
border-radius: 12px;
background: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
overflow: hidden;
:deep(.el-loading-mask) {
background-color: rgba(245, 247, 250, 0.7);
}
:deep(.el-loading-spinner .path) {
stroke: #409eff;
}
}
.hot-news-head {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 16px 18px 14px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.hot-news-title {
font-size: 16px;
font-weight: 700;
color: #1a1a1a;
letter-spacing: 0.06em;
}
.hot-news-badge {
flex-shrink: 0;
padding: 3px 10px;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.12em;
color: #fff;
border-radius: 4px;
background: linear-gradient(180deg, #ff7a3c 0%, #ff5722 100%);
}
.hot-news-list {
margin: 0;
padding: 8px 12px 12px;
list-style: none;
overflow-y: auto;
}
.hot-news-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 8px;
border-radius: 6px;
cursor: pointer;
transition: background 0.15s ease;
& + .hot-news-item {
border-top: 1px solid rgba(0, 0, 0, 0.05);
}
&:hover {
background: rgba(64, 158, 255, 0.08);
}
}
.hot-rank {
flex-shrink: 0;
width: 22px;
height: 22px;
line-height: 22px;
text-align: center;
font-size: 12px;
font-weight: 700;
color: #4b5563;
border-radius: 4px;
background: #f3f4f6;
border: 1px solid #e5e7eb;
&.is-top {
color: #fff;
background: linear-gradient(180deg, #ff7a3c 0%, #ff5722 100%);
border-color: rgba(255, 87, 34, 0.4);
}
}
.hot-item-title {
flex: 1;
min-width: 0;
font-size: 13px;
line-height: 1.55;
color: $lt-text;
font-weight: 500;
word-break: break-word;
}
.hot-news-item:hover .hot-item-title {
color: $lt-bar-blue-end;
}
.hot-news-empty {
padding: 28px 12px;
text-align: center;
font-size: 13px;
color: $lt-text-muted;
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;
background: #fff;
border-radius: 8px;
border-radius: 4px;
overflow-y: auto;
overflow-x: hidden;
background: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
}
</style>
<!-- scopedel-image-viewer 通过 Teleport 挂到 body需全局选择器 -->
<style lang="scss">
@import "./styles/luntan-tech.scss";
</style>

View File

@ -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);
}
}

View File

@ -0,0 +1,115 @@
// 论坛模块 — 白色清新主题变量
$lt-bg: #f5f7fa;
$lt-bg-soft: #ffffff;
$lt-cyan: #4a9eff;
$lt-cyan-mid: #409eff;
$lt-panel: #ffffff;
$lt-panel-soft: #f8fafc;
$lt-border: rgba(0, 0, 0, 0.08);
$lt-border-dim: rgba(0, 0, 0, 0.04);
$lt-text: #333333;
$lt-text-dim: #666666;
$lt-text-muted: #999999;
$lt-glow: 0 1px 4px rgba(0, 0, 0, 0.06);
$lt-glow-strong: 0 2px 12px rgba(0, 0, 0, 0.08);
// 顶栏:与浅色页面、白字/白按钮协调的蓝色
$lt-bar-blue-start: #3d7dd6;
$lt-bar-blue-end: #2563eb;
$lt-bar-btn-text: #1d4ed8;
// 主题蓝 rgba与顶栏渐变一致便于详情页等复用
@function lt-blue($alpha) {
@return rgba(37, 99, 235, $alpha);
}
@function lt-blue-light($alpha) {
@return rgba(61, 125, 214, $alpha);
}
@mixin lt-panel-frame {
background: $lt-panel;
border: 1px solid $lt-border;
box-shadow: $lt-glow;
border-radius: 8px;
}
@mixin lt-panel-soft-bg {
background: $lt-panel-soft;
border: 1px solid $lt-border-dim;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
border-radius: 8px;
}
@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.5;
}
&::before {
top: -1px;
left: -1px;
border-width: 2px 0 0 2px;
box-shadow: -1px -1px 6px rgba(64, 158, 255, 0.2);
}
&::after {
bottom: -1px;
right: -1px;
border-width: 0 2px 2px 0;
box-shadow: 1px 1px 6px rgba(64, 158, 255, 0.2);
}
}
// el-image-viewer 全屏预览覆盖
.el-image-viewer__wrapper {
background: rgba(0, 0, 0, 0.85) !important;
.el-image-viewer__mask {
background: rgba(0, 0, 0, 0.6) !important;
}
.el-image-viewer__btn {
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.3);
background: rgba(80, 80, 80, 0.7);
&:hover {
background: rgba(100, 100, 100, 0.85);
border-color: rgba(255, 255, 255, 0.5);
}
&.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;
}
}

View File

@ -0,0 +1,64 @@
@import './luntan-tech.scss';
// vue3-emojidist/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;
}
}