完成新增项目的富文本加密+修改项目的富文本加密及解密+新增项目通知的加密+编辑项目通知的加密与解密+修改项目富文本+修改编辑项目样式
This commit is contained in:
BIN
dist616 - 副本.zip
BIN
dist616 - 副本.zip
Binary file not shown.
BIN
dist616.zip
BIN
dist616.zip
Binary file not shown.
BIN
dist6月9日.zip
BIN
dist6月9日.zip
Binary file not shown.
BIN
greenOrange.zip
BIN
greenOrange.zip
Binary file not shown.
@ -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),
|
||||
{
|
||||
// 创建加密后的表单对象 - 保持原始数据结构
|
||||
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%;
|
||||
|
@ -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}`);
|
||||
}
|
||||
|
@ -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();
|
||||
};
|
||||
|
@ -110,10 +110,6 @@
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -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 v-if="previewImage || projectData.projectImage" class="preview-image">
|
||||
</div>
|
||||
|
||||
<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>
|
@ -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) {
|
||||
|
@ -1 +1 @@
|
||||
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/api/imageurl.ts","./src/api/myaxios.ts","./src/router/index.ts","./src/router/routes.ts","./src/store/index.ts","./src/store/userstore.ts","./src/types/wangeditor.d.ts","./src/app.vue","./src/layout/managelayout.vue","./src/layout/manage/manageheader.vue","./src/layout/manage/managesidebar.vue","./src/view/index.vue","./src/view/login.vue","./src/view/test.vue","./src/view/community/community.vue","./src/view/components/richtexteditor.vue","./src/view/course/linkedcourse.vue","./src/view/course/localcurriculum.vue","./src/view/project/addproject.vue","./src/view/project/addprojectnotice.vue","./src/view/project/moneydetail.vue","./src/view/project/noticedetail.vue","./src/view/project/project.vue","./src/view/project/projectdetail.vue","./src/view/project/projectnotice.vue","./src/view/project/promotioncode.vue","./src/view/settlement/applicationrecord.vue","./src/view/settlement/moneyrecord.vue","./src/view/userlist/userlist.vue","./src/view/work/workdetail.vue","./src/view/work/worklist.vue"],"version":"5.6.3"}
|
||||
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/api/imageurl.ts","./src/api/myaxios.ts","./src/router/index.ts","./src/router/routes.ts","./src/store/index.ts","./src/store/userstore.ts","./src/types/wangeditor.d.ts","./src/app.vue","./src/layout/managelayout.vue","./src/layout/manage/manageheader.vue","./src/layout/manage/managesidebar.vue","./src/view/index.vue","./src/view/login.vue","./src/view/test.vue","./src/view/community/community.vue","./src/view/components/richtexteditor.vue","./src/view/course/linkedcourse.vue","./src/view/course/localcurriculum.vue","./src/view/project/addproject.vue","./src/view/project/addprojectnotice.vue","./src/view/project/moneydetail.vue","./src/view/project/noticedetail.vue","./src/view/project/project.vue","./src/view/project/projectdetail.vue","./src/view/project/projectnotice.vue","./src/view/project/promotioncode.vue","./src/view/settlement/applicationrecord.vue","./src/view/settlement/moneyrecord.vue","./src/view/settlement/withdrawalapplicationrecord.vue","./src/view/userlist/userlist.vue","./src/view/work/workdetail.vue","./src/view/work/worklist.vue"],"version":"5.6.3"}
|
Reference in New Issue
Block a user