完成新增项目的富文本加密+修改项目的富文本加密及解密+新增项目通知的加密+编辑项目通知的加密与解密+修改项目富文本+修改编辑项目样式

This commit is contained in:
2025-06-19 20:09:38 +08:00
parent e64541bf18
commit ee3a0c7853
12 changed files with 459 additions and 216 deletions

View File

@ -10,7 +10,7 @@ const myAxios = axios.create({
// baseURL:'http://1.94.237.210:3457'
//baseURL:'http://1.94.237.210:8088'
// baseURL:'http://27.30.77.229:9091/'
baseURL:'http://27.30.77.229:9092/'
baseURL:'http://27.30.77.229:9092/'
});

View File

@ -198,6 +198,7 @@
</form>
</template>
<script setup lang="ts">
import { reactive, onMounted, nextTick,ref } from 'vue';
import RichTextEditor from '../components/RichTextEditor.vue';
@ -220,6 +221,7 @@ interface ProjectForm {
}
const fileInput = ref<HTMLInputElement | null>(null);
const fileName = ref('');
const handleFileUpload = async (event: Event) => {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
@ -227,7 +229,6 @@ const handleFileUpload = async (event: Event) => {
if (!file) return;
try {
// 重命名局部变量避免冲突(修复错误❷)
const uploadFormData = new FormData();
uploadFormData.append('biz', 'project');
uploadFormData.append('file', file);
@ -241,7 +242,6 @@ const handleFileUpload = async (event: Event) => {
});
if (res.code === 1) {
// 正确访问响应式对象
formData.projectImage = res.data;
fileName.value = file.name;
}
@ -251,11 +251,6 @@ const handleFileUpload = async (event: Event) => {
}
};
// 添加文件名响应式变量
const fileName = ref('');
// 响应式表单数据
const formData = reactive<ProjectForm>({
projectName: '',
@ -270,39 +265,90 @@ const formData = reactive<ProjectForm>({
projectImage: ''
});
// Base64编码函数
const encode64 = (text: string): string => {
return btoa(
encodeURIComponent(text).replace(
/%([0-9A-F]{2})/g,
(_, p1) => String.fromCharCode(parseInt(p1, 16)))
)
};
// 生命周期钩子
onMounted(() => {
// 初始处理
nextTick(() => {
// 为手机预览添加初始样式
const phoneScreen = document.querySelector('.phone-screen');
if (phoneScreen) {
// 添加阴影和动画效果
phoneScreen.classList.add('animate-fade-in');
}
});
});
const encryptValue = (value: any, key: string): any => {
// 特定字段不加密
if (key === 'projectStatus') {
return value;
}
// 数值型字段直接返回,不加密
if (key === 'projectSettlementCycle' || key === 'maxPromoterCount' || key === 'projectImage' || key === 'projectName') {
return value;
}
try {
// 将值转换为字符串
const valueString = typeof value === 'object'
? JSON.stringify(value)
: String(value);
// 使用Base64编码数据
return encode64(valueString);
} catch (error) {
console.error('Base64编码失败:', error);
return value; // 编码失败返回原始值
}
};
const handleSubmit = async () => {
try {
// 表单验证
if (!validateForm()) {
alert('请填写所有必填字段');
return;
}
const storedToken = localStorage.getItem('token');
const res:any = await myAxios.post(`/project/add`,
JSON.stringify(formData),
{
headers: {
'Content-Type': 'application/json',
'Authorization': storedToken
}
}
);
// 创建加密后的表单对象 - 保持原始数据结构
const encryptedFormData: Record<string, any> = {};
console.log("===== 字段Base64编码结果 =====");
Object.entries(formData).forEach(([key, value]) => {
// 跳过空值字段
if (value === null || value === undefined || value === '') return;
// 对字段值进行Base64编码
encryptedFormData[key] = encryptValue(value, key);
// 打印编码结果
console.log(`字段: ${key}`);
console.log(`原始值: ${value}`);
console.log(`编码值: ${encryptedFormData[key]}`);
console.log(`类型: ${typeof encryptedFormData[key]}`);
console.log('----------------------');
});
console.log("=======================");
// 发送Base64编码后的数据
const res: any = await myAxios.post(`/project/add`, encryptedFormData, {
headers: {
'Content-Type': 'application/json',
'Authorization': storedToken
}
});
if (res.code === 1) {
alert('项目创建成功!');
// router.back();
router.push('/project')
// 重置表单
Object.assign(formData, {
projectName: '',
projectSettlementCycle: 2,
@ -315,16 +361,40 @@ const handleSubmit = async () => {
applyPromoCodeDesc: '',
projectImage: ''
});
fileName.value = ''; // 重置文件名
router.push('/project');
} else {
alert(`创建失败:${res.message}`);
}
} catch (error) {
console.error('请求失败:', error);
alert('提交失败,请检查控制台获取详细信息');
}
};
const validateForm = (): boolean => {
const requiredFields: (keyof ProjectForm)[] = [
'projectName',
'projectImage',
'projectSettlementCycle',
'maxPromoterCount',
'projectStatus',
'projectDescription',
'settlementDesc',
'projectDesc',
'projectFlow',
'applyPromoCodeDesc'
];
return requiredFields.every(field => {
const value = formData[field];
return value !== '' && value !== null && value !== undefined;
});
};
</script>
<style scoped>
.modern-form {
max-width: 90%;

View File

@ -86,6 +86,13 @@ import myAxios from "../../api/myAxios.ts";
import RichTextEditor from '../components/RichTextEditor.vue';
import {useRoute} from "vue-router";
function encode64(text: string): string {
// 使用_代替未使用的match参数
return btoa(encodeURIComponent(text).replace(/%([0-9A-F]{2})/g, (_, p1) => {
return String.fromCharCode(parseInt(p1, 16));
}));
}
interface NotificationForm {
notificationTitle: string;
notificationContent: string;
@ -110,9 +117,16 @@ onMounted(() => {
const handleSubmit = async () => {
try {
const storedToken = localStorage.getItem('token');
// 创建要发送的数据副本
const payload = {
...formData,
notificationContent: encode64(formData.notificationContent)
};
const res: any = await myAxios.post(
'/projectNotification/add',
JSON.stringify(formData),
JSON.stringify(payload),
{
headers: {
'Content-Type': 'application/json',
@ -124,11 +138,10 @@ const handleSubmit = async () => {
if (res.code === 1) {
alert('通知发布成功!');
router.back();
Object.assign(formData, {
notificationTitle: '',
notificationContent: '',
projectId: ''
});
// 重置表单
formData.notificationTitle = '';
formData.notificationContent = '';
formData.projectId = '';
} else {
alert(`发布失败:${res.message}`);
}

View File

@ -55,7 +55,24 @@ import myAxios from "../../api/myAxios.ts";
import { NotificationOutlined } from '@ant-design/icons-vue';
import RichTextEditor from '../components/RichTextEditor.vue'; // 引入富文本编辑器
import '@vueup/vue-quill/dist/vue-quill.snow.css'; // 引入富文本编辑器样式
function encode64(text: string): string {
return btoa(encodeURIComponent(text).replace(/%([0-9A-F]{2})/g, (_, p1) => {
return String.fromCharCode(parseInt(p1, 16));
}));
}
function decode64(text: string): string {
try {
return decodeURIComponent(
Array.from(atob(text), char =>
'%' + ('00' + char.charCodeAt(0).toString(16)).slice(-2)
).join('')
);
} catch (error) {
console.error('Base64解码失败:', error);
return text; // 解码失败时返回原始文本
}
}
interface NotificationDetail {
id: string; // 改为 string 类型
notificationTitle: string;
@ -94,44 +111,45 @@ const cancelEdit = () => {
isEditing.value = false;
};
// 保存编辑 - 根据接口要求修改
// 修改保存函数
const saveNotice = async () => {
try {
loading.value = true;
const storedToken = localStorage.getItem('token');
// 创建符合接口要求的JSON对象
// 对通知内容进行Base64编码
const encodedContent = encode64(editableNotice.value.notificationContent || '');
// 创建请求数据
const requestData = {
id: editableNotice.value.id,
notificationTitle: editableNotice.value.notificationTitle,
notificationContent: editableNotice.value.notificationContent,
notificationContent: encodedContent, // 使用编码后的内容
projectId: editableNotice.value.projectId
};
// 发送JSON格式的数据
const res:any = await myAxios.post(
"/projectNotification/update",
requestData, // 直接发送JSON对象
requestData,
{
headers: {
Authorization: storedToken,
'Content-Type': 'application/json' // 关键修改使用JSON格式
'Content-Type': 'application/json'
}
}
);
if (res.code === 1) {
message.success('更新成功');
// 更新本地数据并退出编辑模式
projectNotice.value = { ...editableNotice.value };
// 更新本地数据并退出编辑模式 - 使用解码后的内容
projectNotice.value = {
...editableNotice.value, // 这里包含的是用户编辑的原始内容(未编码)
};
isEditing.value = false;
} else {
message.error(res.message || '更新失败');
}
} catch (error: any) {
console.error('更新通知失败:', error);
// 改进错误处理
const errMsg = error.response?.data?.message ||
error.message ||
'更新通知失败,请检查网络';
@ -140,9 +158,6 @@ const saveNotice = async () => {
loading.value = false;
}
};
const fetchNotificationDetail = async (id: number) => {
try {
loading.value = true;
@ -161,10 +176,13 @@ const fetchNotificationDetail = async (id: number) => {
);
if (res.code === 1 && res.data) {
// 对通知内容进行Base64解码
const decodedContent = decode64(res.data.notificationContent);
projectNotice.value = {
id: res.data.id,
notificationTitle: res.data.notificationTitle,
notificationContent: res.data.notificationContent,
notificationContent: decodedContent,
projectId: res.data.projectId,
createTime: res.data.createTime
};
@ -183,8 +201,6 @@ const fetchNotificationDetail = async (id: number) => {
}
};
const goBack = () => {
router.back();
};

View File

@ -110,10 +110,6 @@
</template>
</template>
</a-table>
</template>
<script lang="ts" setup>

View File

@ -3,7 +3,7 @@ import {useRoute, useRouter} from "vue-router";
import { onMounted, ref } from "vue";
import myAxios from "../../api/myAxios";
import { message } from "ant-design-vue";
import { QuillEditor } from '@vueup/vue-quill';
import RichTextEditor from '../components/RichTextEditor.vue';
import {Form} from 'ant-design-vue'
import {downLoadImage} from "../../api/ImageUrl.ts";
@ -36,10 +36,53 @@ const projectData = ref<ProjectDetail>({
projectFlow: '',
applyPromoCodeDesc: ''
});
const editData = ref<ProjectDetail | null>(null);
const projectId = ref<string | null>(null);
const isEditing = ref(false);
const fileInput = ref<HTMLInputElement | null>(null);
function encode64(text: string): string {
return btoa(encodeURIComponent(text).replace(/%([0-9A-F]{2})/g, (_, p1) => {
return String.fromCharCode(parseInt(p1, 16));
}));
}
function decode64(text: string): string {
return decodeURIComponent(
Array.from(atob(text), char =>
'%' + ('00' + char.charCodeAt(0).toString(16)).slice(-2)
).join('')
);
}
const decryptProjectData = (data: ProjectDetail): ProjectDetail => {
const decryptedData: ProjectDetail = { ...data };
const fieldsToDecrypt: (keyof ProjectDetail)[] = [
'projectName',
'projectDescription',
'settlementDesc',
'projectDesc',
'projectFlow',
'applyPromoCodeDesc'
];
fieldsToDecrypt.forEach(field => {
const value = decryptedData[field];
if (typeof value === 'string' && value) {
try {
(decryptedData as Record<keyof ProjectDetail, any>)[field] = decode64(value);
} catch (error) {
console.error(`Base64解码失败 (${field}):`, error);
(decryptedData as Record<keyof ProjectDetail, any>)[field] = value;
}
}
});
return decryptedData;
};
// 图片上传处理逻辑
const handleFileUpload = async (event: Event) => {
@ -52,7 +95,6 @@ const handleFileUpload = async (event: Event) => {
// 本地预览
const reader = new FileReader();
reader.onload = (e) => {
// 仅设置临时预览,不覆盖实际值
previewImage.value = e.target?.result as string;
};
reader.readAsDataURL(file);
@ -71,8 +113,9 @@ const handleFileUpload = async (event: Event) => {
});
if (res.code === 1 && res.data) {
// 核心修改将返回的文件标识符赋值给projectImage
projectData.value.projectImage = res.data;
if (editData.value) {
editData.value.projectImage = res.data;
}
message.success('图片上传成功');
} else {
throw new Error(res.message || '上传失败');
@ -81,12 +124,15 @@ const handleFileUpload = async (event: Event) => {
console.error('上传失败:', error);
message.error('文件上传失败');
previewImage.value = '';
} finally {
// 重置文件输入,允许重复上传同一文件
if (input) input.value = '';
}
};
const previewImage = ref('');
const formRef = ref<typeof Form>(); // 添加表单引用
const formRef = ref<typeof Form>();
// 添加数字输入校验方法
const handleNumberInput = (e: KeyboardEvent) => {
@ -105,24 +151,38 @@ const handlePaste = (e: ClipboardEvent) => {
}
};
// 更新项目时提交处理
const finishEditing = async () => {
//isEditing.value = false;
try {
if (!editData.value) {
message.error('编辑数据不存在');
return;
}
const storedToken = localStorage.getItem('token');
if (!storedToken) throw new Error('未找到登录信息');
if (!storedToken) {
message.error('未找到登录信息');
return;
}
// 转换富文本内容为字符串
const payload = {
...projectData.value,
settlementDesc: String(projectData.value.settlementDesc),
projectDesc: String(projectData.value.projectDesc),
projectFlow: String(projectData.value.projectFlow),
applyPromoCodeDesc: String(projectData.value.applyPromoCodeDesc)
};
const payload: Record<string, any> = { ...editData.value };
const res:any = await myAxios.post("/project/update",
const fieldsToEncode: (keyof ProjectDetail)[] = [
'projectDescription',
'settlementDesc',
'projectDesc',
'projectFlow',
'applyPromoCodeDesc'
];
fieldsToEncode.forEach(field => {
const value = payload[field];
if (typeof value === 'string' && value) {
payload[field] = encode64(value);
}
});
const res: any = await myAxios.post(
"/project/update",
JSON.stringify(payload),
{
headers: {
@ -131,25 +191,24 @@ const finishEditing = async () => {
}
}
);
console.log(res)
if (res.code === 1) {
message.success('项目更新成功');
projectData.value = editData.value;
editData.value = null;
router.push('/project')
}else {
} else {
message.error(res.message);
}
} catch (error) {
console.error("更新失败:", error);
message.error('项目更新失败');
return;
} finally {
isEditing.value = false;
previewImage.value = ''; // 清空预览图
}
};
if (typeof route.query.id === 'string') {
projectId.value = route.query.id;
}
@ -160,24 +219,34 @@ onMounted(() => {
}
});
const handleIdSearch = async (id: string) => {
if (!id) return message.warning("请输入项目ID");
if (!/^\d+$/.test(id)) return message.warning("ID必须为数字");
if (!id) {
message.warning("请输入项目ID");
return;
}
if (!/^\d+$/.test(id)) {
message.warning("ID必须为数字");
return;
}
loading.value = true;
try {
const storedToken = localStorage.getItem('token');
if (!storedToken) throw new Error('未找到登录信息');
if (!storedToken) {
message.error('未找到登录信息');
return;
}
const res:any = await myAxios.post("/project/queryById",
const res: any = await myAxios.post(
"/project/queryById",
{ id: parseInt(id) },
{ headers: { Authorization: storedToken } }
);
if (res.code === 1 && res.data) {
console.log('Received project data:', res.data);
projectData.value = res.data;
const decryptedData = decryptProjectData(res.data);
projectData.value = decryptedData;
} else {
message.error(res.message || '查询失败');
}
@ -194,6 +263,9 @@ const updateProject = () => {
message.warning('项目数据尚未加载完成,请稍后再试');
return;
}
// 创建编辑数据的深拷贝副本
editData.value = JSON.parse(JSON.stringify(projectData.value));
previewImage.value = ''; // 进入编辑模式时重置预览图
isEditing.value = true;
};
@ -202,12 +274,17 @@ const router = useRouter();
const goBack = () => {
router.push('/project')
};
</script>
// 触发文件选择
const triggerFileInput = () => {
if (fileInput.value) {
fileInput.value.click();
}
};
</script>
<template>
<div class="project-detail-container">
<!-- 加载中 -->
<div v-if="loading" class="loading">加载中...</div>
@ -265,9 +342,9 @@ const goBack = () => {
</div>
</div>
<!-- 编辑页 - 使用 addProject.vue 结构 -->
<div v-if="isEditing && projectData" class="add-project-container">
<a-form ref="formRef" :model="projectData" layout="vertical" @submit.prevent="finishEditing">
<!-- 编辑页 -->
<div v-if="isEditing && editData" class="add-project-container">
<a-form ref="formRef" :model="editData" layout="vertical" @submit.prevent="finishEditing">
<div class="form-section">
<div class="form-header">
<h2>项目基本信息</h2>
@ -276,12 +353,12 @@ const goBack = () => {
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="项目名称">
<a-input v-model:value="projectData.projectName" placeholder="请输入项目名称" />
<a-input v-model:value="editData.projectName" placeholder="请输入项目名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="项目状态">
<a-select v-model:value="projectData.projectStatus" placeholder="请选择状态">
<a-select v-model:value="editData.projectStatus" placeholder="请选择状态">
<a-select-option value="running">运行中</a-select-option>
<a-select-option value="full">人数已满</a-select-option>
<a-select-option value="paused">已暂停</a-select-option>
@ -303,7 +380,7 @@ const goBack = () => {
}]"
>
<a-input-number
v-model:value="projectData.projectSettlementCycle"
v-model:value="editData.projectSettlementCycle"
style="width: 100%"
:min="0"
@keypress="handleNumberInput"
@ -323,7 +400,7 @@ const goBack = () => {
}]"
>
<a-input-number
v-model:value="projectData.maxPromoterCount"
v-model:value="editData.maxPromoterCount"
style="width: 100%"
:min="1"
@keypress="handleNumberInput"
@ -342,12 +419,14 @@ const goBack = () => {
@change="handleFileUpload"
ref="fileInput"
>
<div class="upload-button" @click="fileInput?.click()">
<span v-if="!projectData.projectImage && !previewImage">点击上传图片</span>
<span v-else class="file-name">已选择图片</span>
<div class="upload-area" @click="triggerFileInput">
<div class="upload-button">
<span>点击上传图片</span>
<div class="file-name">支持 JPG/PNG 格式大小不超过 5MB</div>
</div>
</div>
<!-- 修改优先显示预览图没有预览图时显示服务器图片 -->
<div v-if="previewImage || projectData.projectImage" class="preview-image">
<div v-if="previewImage || editData.projectImage" class="preview-image">
<img
v-if="previewImage"
:src="previewImage"
@ -355,7 +434,7 @@ const goBack = () => {
>
<img
v-else
:src="downLoadImage + projectData.projectImage"
:src="downLoadImage + editData.projectImage"
alt="项目封面预览"
>
</div>
@ -363,28 +442,60 @@ const goBack = () => {
</a-form-item>
<a-form-item label="项目描述">
<a-textarea v-model:value="projectData.projectDescription" :rows="4" />
<textarea
v-model="editData.projectDescription"
class="textarea-field"
placeholder="请输入项目简介..."
maxlength="30"
></textarea>
<div class="absolute right-2 bottom-2 text-xs text-gray-500">
{{ editData.projectDescription.length }}/30
</div>
</a-form-item>
</div>
<div class="form-section">
<h2>富文本配置</h2>
<a-form-item label="结算说明">
<QuillEditor v-model:content="projectData.settlementDesc" content-type="html" />
</a-form-item>
<div class="rich-text-container">
<div class="rich-text-columns">
<div class="rich-text-group">
<span class="label-text">结算说明</span>
<RichTextEditor
v-model="editData.settlementDesc"
:disable="false"
@content-change="(html: string) => { if (editData) editData.settlementDesc = html }"
/>
</div>
<a-form-item label="项目说明">
<QuillEditor v-model:content="projectData.projectDesc" content-type="html" />
</a-form-item>
<div class="rich-text-group">
<span class="label-text">项目说明</span>
<RichTextEditor
v-model="editData.projectDesc"
:disable="false"
@content-change="(html: string) => { if (editData) editData.projectDesc = html }"
/>
</div>
<a-form-item label="项目流程">
<QuillEditor v-model:content="projectData.projectFlow" content-type="html" />
</a-form-item>
<div class="rich-text-group">
<span class="label-text">项目流程</span>
<RichTextEditor
v-model="editData.projectFlow"
:disable="false"
@content-change="(html: string) => { if (editData) editData.projectFlow = html }"
/>
</div>
<a-form-item label="申请推广码说明">
<QuillEditor v-model:content="projectData.applyPromoCodeDesc" content-type="html" />
</a-form-item>
<div class="rich-text-group">
<span class="label-text">申请推广码说明</span>
<RichTextEditor
v-model="editData.applyPromoCodeDesc"
:disable="false"
@content-change="(html: string) => { if (editData) editData.applyPromoCodeDesc = html }"
/>
</div>
</div>
</div>
</div>
<div class="form-actions">
@ -403,10 +514,8 @@ const goBack = () => {
margin-bottom: 24px;
}
/* 原有样式保持不变 */
.project-detail-container {
max-width: 1200px;
width: 100%;
margin: 2rem auto;
padding: 0 1rem;
}
@ -436,13 +545,7 @@ const goBack = () => {
font-size: 2rem;
color: #1a1a1a;
margin: 0;
}
.status-tag {
padding: 4px 12px;
border-radius: 16px;
color: white;
font-size: 0.9rem;
flex-grow: 1;
}
.basic-info {
@ -540,31 +643,16 @@ const goBack = () => {
.project-title {
font-size: 1.6rem;
}
}
.form-group label {
display: block;
font-weight: bold;
margin-bottom: 0.5rem;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 0.6rem;
font-size: 1rem;
border: 1px solid #ccc;
border-radius: 6px;
resize: vertical;
}
.form-group textarea {
height: 100px;
.header-section {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
}
.add-project-container {
max-width: 960px;
width: 100%;
margin: 2rem auto;
padding: 2rem;
background: #fff;
@ -586,25 +674,16 @@ const goBack = () => {
.form-actions {
text-align: right;
margin-top: 2rem;
}
.ql-container {
min-height: 120px;
}
.preview-image {
margin-top: 1rem;
}
.preview-image img {
max-width: 200px;
height: auto;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
/* 保持原有上传样式 */
.file-upload {
position: relative;
margin-top: 1rem;
}
.file-input {
@ -614,16 +693,22 @@ const goBack = () => {
height: 1px;
}
.upload-area {
cursor: pointer;
}
.upload-button {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0.8rem 1.5rem;
padding: 1.5rem;
background: #f8fafc;
border: 2px dashed #cbd5e1;
border-radius: 0.75rem;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
}
.upload-button:hover {
@ -631,23 +716,41 @@ const goBack = () => {
border-color: #94a3b8;
}
.file-name {
color: #64748b;
font-size: 0.9rem;
.upload-button span {
font-weight: 500;
color: #334155;
}
/*橙色按钮*/
.file-name {
color: #64748b;
font-size: 0.85rem;
margin-top: 0.5rem;
}
.preview-image {
margin-top: 1rem;
text-align: center;
}
.preview-image img {
max-width: 100%;
max-height: 300px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
border: 1px solid #eee;
}
.custom-button {
background-color: #ffa940;
border-color: #ffa940;
color: #fff;
font-weight: 500;
}
.custom-button:hover,
.custom-button:focus {
background-color: #ffa940;
border-color: #ffa940;
background-color: #fa8c16;
border-color: #fa8c16;
color: #fff;
}
@ -658,7 +761,6 @@ const goBack = () => {
color: #fff;
}
/* 危险按钮样式 */
.custom-button.ant-btn-dangerous {
background-color: #ff4d4f;
border-color: #ff4d4f;
@ -670,23 +772,89 @@ const goBack = () => {
border-color: #ff7875;
}
.custom-search :deep(.ant-input-search-button) {
background-color: #ffa940;
border-color: #ffa940;
.rich-text-container {
margin-top: 1.5rem;
}
.custom-search :deep(.ant-input-search-button:hover),
.custom-search :deep(.ant-input-search-button:focus) {
background-color: #fa8c16;
border-color: #fa8c16;
.rich-text-columns {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 1rem;
align-items: start;
}
/* 保持输入框原有样式不变 */
.custom-search :deep(.ant-input) {
border-right-color: #ffa940;
.rich-text-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
height: 100%;
border: 2px solid #e2e8f0;
border-radius: 0.75rem;
overflow: hidden;
transition: all 0.3s ease;
min-height: 320px;
flex-grow: 1;
padding: 10px;
background: white;
}
.rich-text-group:focus-within {
border-color: #6366f1;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
.label-text {
display: block;
font-size: 0.95rem;
font-weight: 600;
color: #3b4151;
padding-left: 0.2rem;
transition: color 0.3s ease;
}
.rich-text-group:focus-within .label-text {
color: #6366f1;
}
@media (max-width: 1280px) {
.rich-text-columns {
grid-template-columns: repeat(2, 1fr);
gap: 1.2rem;
}
}
@media (max-width: 768px) {
.rich-text-columns {
grid-template-columns: 1fr;
gap: 1.5rem;
}
}
:deep(.ql-container) {
height: 280px;
border: none !important;
}
.textarea-field {
min-height: 100px;
resize: vertical;
}
.textarea-field:focus {
border-color: #6366f1;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
outline: none;
}
.textarea-field {
width: 100%;
padding: 0.8rem 1.2rem;
border: 2px solid #e2e8f0;
border-radius: 0.75rem;
transition: all 0.3s ease;
font-size: 1rem;
background: white;
}
</style>

View File

@ -4,6 +4,19 @@ import {useRoute, useRouter} from "vue-router";
import myAxios from "../../api/myAxios";
import {message} from "ant-design-vue";
function decode64(text: string): string {
try {
return decodeURIComponent(
Array.from(atob(text), char =>
'%' + ('00' + char.charCodeAt(0).toString(16)).slice(-2)
).join('')
);
} catch (error) {
console.error('Base64解码失败:', error);
return text; // 解码失败时返回原始文本
}
}
const columns = [
{
title: '项目通知ID',
@ -112,6 +125,7 @@ onMounted(() => {
getNotifications(projectId.value);
}
});
const getNotifications = async (id: string | number) => {
const storedToken = localStorage.getItem('token');
try {
@ -126,10 +140,13 @@ const getNotifications = async (id: string | number) => {
}
}
);
console.log(response)
if (response.code === 1) {
originalTableData.value = response.data;
displayData.value = response.data; // 初始显示所有数据
// 对通知内容进行Base64解码
originalTableData.value = response.data.map((item:any) => ({
...item,
notificationContent: decode64(item.notificationContent)
}));
displayData.value = originalTableData.value;
} else {
error.value = "获取通知列表失败";
originalTableData.value = [];
@ -144,6 +161,7 @@ const getNotifications = async (id: string | number) => {
}
};
onMounted(() => {
if (projectId.value) {
getNotifications(projectId.value);
@ -232,6 +250,7 @@ const goBack = () => {
</a-form-item>
<a-button class="custom-button" @click="goAddProjectNotice">新增项目通知</a-button>
<a-button class="custom-button" @click="reset">重置搜索</a-button>
<a-button @click="goBack" class="custom-button">返回</a-button>
</a-space>
</a-form>
</div>
@ -256,11 +275,6 @@ const goBack = () => {
</a-space>
</template>
</template>
<template #footer>
<div class="table-footer">
<a-button @click="goBack" class="back-button">返回</a-button>
</div>
</template>
</a-table>
</template>
@ -274,29 +288,6 @@ const goBack = () => {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.error-alert {
padding: 1rem;
background: #ffe3e3;
color: #ff4444;
border-radius: 6px;
display: flex;
align-items: center;
gap: 0.8rem;
margin-top: 1rem;
}
.error-icon {
display: inline-block;
width: 1.2rem;
height: 1.2rem;
border-radius: 50%;
background: #ff4444;
color: white;
text-align: center;
line-height: 1.2rem;
font-weight: bold;
}
:deep(.ant-table-thead) {
background-color: #fafafa !important;
font-weight: 600;
@ -362,17 +353,6 @@ const goBack = () => {
border-right-color: #ffa940;
}
/* 新增表格页脚样式 */
.table-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
}
.back-button {
margin-right: 16px;
}
/* 调整分页器位置 */
:deep(.ant-table-pagination.ant-pagination) {