yangg vor 1 Tag
Ursprung
Commit
a8d561d12c

+ 28 - 6
package-lock.json

@@ -15,6 +15,7 @@
 				"@fast-crud/ui-element": "^1.21.2",
 				"@fast-crud/ui-interface": "^1.21.2",
 				"@iconify/vue": "^4.1.2",
+				"@types/dompurify": "^3.0.5",
 				"@types/lodash": "^4.17.7",
 				"@vitejs/plugin-vue-jsx": "^4.0.1",
 				"@vue-office/docx": "^1.6.3",
@@ -29,6 +30,7 @@
 				"china-area-data": "^5.0.1",
 				"countup.js": "^2.8.0",
 				"cropperjs": "^1.6.2",
+				"dompurify": "^3.2.7",
 				"e-icon-picker": "2.1.1",
 				"echarts": "^5.6.0",
 				"echarts-gl": "^2.0.9",
@@ -42,6 +44,7 @@
 				"jspdf": "^3.0.1",
 				"jsplumb": "^2.15.6",
 				"lodash-es": "^4.17.21",
+				"marked": "^16.3.0",
 				"mitt": "^3.0.1",
 				"nprogress": "^0.2.0",
 				"pdf-lib": "^1.17.1",
@@ -3840,6 +3843,15 @@
 			"resolved": "https://registry.npmmirror.com/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz",
 			"integrity": "sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA=="
 		},
+		"node_modules/@types/dompurify": {
+			"version": "3.0.5",
+			"resolved": "https://registry.npmmirror.com/@types/dompurify/-/dompurify-3.0.5.tgz",
+			"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
+			"license": "MIT",
+			"dependencies": {
+				"@types/trusted-types": "*"
+			}
+		},
 		"node_modules/@types/estree": {
 			"version": "1.0.6",
 			"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.6.tgz",
@@ -3911,8 +3923,7 @@
 			"version": "2.0.7",
 			"resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz",
 			"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
-			"license": "MIT",
-			"optional": true
+			"license": "MIT"
 		},
 		"node_modules/@types/web-bluetooth": {
 			"version": "0.0.16",
@@ -5821,11 +5832,10 @@
 			}
 		},
 		"node_modules/dompurify": {
-			"version": "3.2.6",
-			"resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.2.6.tgz",
-			"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
+			"version": "3.2.7",
+			"resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.2.7.tgz",
+			"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
 			"license": "(MPL-2.0 OR Apache-2.0)",
-			"optional": true,
 			"optionalDependencies": {
 				"@types/trusted-types": "^2.0.7"
 			}
@@ -7732,6 +7742,18 @@
 				"@jridgewell/sourcemap-codec": "^1.5.0"
 			}
 		},
+		"node_modules/marked": {
+			"version": "16.3.0",
+			"resolved": "https://registry.npmmirror.com/marked/-/marked-16.3.0.tgz",
+			"integrity": "sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w==",
+			"license": "MIT",
+			"bin": {
+				"marked": "bin/marked.js"
+			},
+			"engines": {
+				"node": ">= 20"
+			}
+		},
 		"node_modules/math-intrinsics": {
 			"version": "1.1.0",
 			"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",

+ 3 - 0
package.json

@@ -16,6 +16,7 @@
 		"@fast-crud/ui-element": "^1.21.2",
 		"@fast-crud/ui-interface": "^1.21.2",
 		"@iconify/vue": "^4.1.2",
+		"@types/dompurify": "^3.0.5",
 		"@types/lodash": "^4.17.7",
 		"@vitejs/plugin-vue-jsx": "^4.0.1",
 		"@vue-office/docx": "^1.6.3",
@@ -30,6 +31,7 @@
 		"china-area-data": "^5.0.1",
 		"countup.js": "^2.8.0",
 		"cropperjs": "^1.6.2",
+		"dompurify": "^3.2.7",
 		"e-icon-picker": "2.1.1",
 		"echarts": "^5.6.0",
 		"echarts-gl": "^2.0.9",
@@ -43,6 +45,7 @@
 		"jspdf": "^3.0.1",
 		"jsplumb": "^2.15.6",
 		"lodash-es": "^4.17.21",
+		"marked": "^16.3.0",
 		"mitt": "^3.0.1",
 		"nprogress": "^0.2.0",
 		"pdf-lib": "^1.17.1",

+ 2 - 0
src/views/JobApplication/list/index.vue

@@ -1024,6 +1024,8 @@ onMounted(async () => {
 		console.log(response.data)
 		if(response.data.code === 2000){
 			notificationList.value = response.data.data; // 更新通知列表
+			notificationPagination.total = response.data.total || 0;
+
 		}else{
 			ElMessage.error(response.data.message || '获取通知列表失败');
 		}

+ 147 - 0
src/views/system/ProtocolList/README.md

@@ -0,0 +1,147 @@
+# 协议管理功能说明
+
+## 功能概述
+
+协议管理模块提供了完整的协议增删改查功能,支持Markdown格式的协议内容编辑和显示。
+
+## 主要功能
+
+### 1. 协议字段
+
+- **协议标题** (title): 协议的名称
+- **协议标识** (key): 唯一标识符,如 `terms_of_service`
+- **版本号** (version): 协议版本,如 `1.0`
+- **发布日期** (publish_date): 协议发布日期
+- **是否启用** (is_active): 协议是否生效
+- **协议内容** (content): 支持Markdown格式的协议内容
+
+### 2. Markdown编辑器功能
+
+#### 编辑模式
+- 支持实时Markdown语法输入
+- 语法高亮显示
+- 自动补全和格式化
+
+#### 预览模式
+- 实时预览Markdown渲染效果
+- 支持所有标准Markdown语法
+- 美观的样式展示
+
+#### 支持的Markdown语法
+
+```markdown
+# 一级标题
+## 二级标题
+### 三级标题
+
+**粗体文本**
+*斜体文本*
+
+- 无序列表项
+- 另一个列表项
+
+1. 有序列表项
+2. 另一个有序列表项
+
+> 引用文本
+
+`行内代码`
+
+```代码块
+function example() {
+    return "Hello World";
+}
+```
+
+[链接文本](https://example.com)
+
+![图片描述](图片URL)
+```
+
+### 3. 数据流程
+
+1. **输入**: 用户在Markdown编辑器中输入Markdown格式内容
+2. **保存**: 内容以Markdown格式保存到数据库
+3. **显示**: 从数据库读取时,Markdown内容在编辑器中正确显示
+4. **预览**: 用户可以切换到预览模式查看渲染效果
+
+## API接口
+
+### 接口地址
+```
+/api/platform/agreements/
+```
+
+### 请求方法
+- `GET`: 获取协议列表
+- `POST`: 创建新协议
+- `PUT`: 更新协议
+- `DELETE`: 删除协议
+
+### 数据格式
+
+#### 创建/更新协议
+```json
+{
+    "title": "用户服务协议",
+    "key": "terms_of_service",
+    "content": "# 服务协议内容\n\n## 1. 总则...",
+    "version": "1.0",
+    "publish_date": "2025-06-28T14:53:21Z",
+    "is_active": true
+}
+```
+
+## 使用示例
+
+### 1. 创建协议
+
+1. 点击"新增"按钮
+2. 填写协议基本信息(标题、标识、版本等)
+3. 在内容编辑器中输入Markdown格式的协议内容
+4. 可以切换到"预览"标签查看渲染效果
+5. 保存协议
+
+### 2. 编辑协议
+
+1. 在协议列表中点击"编辑"按钮
+2. 修改协议信息
+3. 在Markdown编辑器中修改内容
+4. 保存更改
+
+### 3. 查看协议
+
+1. 点击"查看"按钮
+2. 在只读模式下查看协议内容
+3. 可以切换到预览模式查看渲染效果
+
+## 技术实现
+
+### 依赖库
+- `marked`: Markdown解析库
+- `dompurify`: HTML安全清理库
+- `@fast-crud/fast-crud`: CRUD框架
+- `element-plus`: UI组件库
+
+### 核心功能
+- Markdown到HTML的转换
+- HTML到Markdown的转换
+- 实时预览功能
+- 数据验证和清理
+
+## 注意事项
+
+1. 协议标识(key)必须唯一,只能包含小写字母和下划线
+2. 版本号建议使用语义化版本号(如1.0.0)
+3. Markdown内容支持所有标准语法
+4. 图片上传需要配置相应的文件上传接口
+5. 协议内容会自动进行XSS安全清理
+
+## 扩展功能
+
+可以根据需要添加以下功能:
+- 协议模板管理
+- 协议版本历史
+- 协议审批流程
+- 协议生效时间控制
+- 协议导出功能(PDF、Word等)

+ 47 - 0
src/views/system/ProtocolList/api.ts

@@ -0,0 +1,47 @@
+import {request} from '/@/utils/service';
+import {PageQuery, AddReq, DelReq, EditReq, InfoReq} from '@fast-crud/fast-crud';
+
+export const apiPrefix = '/api/platform/agreements/';
+
+export function GetList(query: PageQuery) {
+    return request({
+        url: apiPrefix,
+        method: 'get',
+        params: query,
+    });
+}
+
+export function GetObj(id: InfoReq) {
+    return request({
+        url: apiPrefix + id + '/',
+        method: 'get',
+    });
+}
+
+
+export function AddObj(obj: AddReq) {
+    return request({
+        url: apiPrefix,
+        method: 'post',
+        data: obj,
+    });
+}
+
+
+
+
+export function UpdateObj(obj: EditReq) {
+    return request({
+        url: apiPrefix + obj.id + '/',
+        method: 'put',
+        data: obj,
+    });
+}
+
+export function DelObj(id: DelReq) {
+    return request({
+        url: apiPrefix + id + '/',
+        method: 'delete',
+        data: {id},
+    });
+}

+ 258 - 0
src/views/system/ProtocolList/components/MarkdownEditor.vue

@@ -0,0 +1,258 @@
+<template>
+	<div class="markdown-editor">
+		<div class="editor-tabs">
+			<el-tabs v-model="activeTab" @tab-click="handleTabClick">
+				<el-tab-pane 
+					label="编辑" 
+					name="edit"
+					:disabled="readonly"
+				>
+					<div class="editor-container">
+						<el-input
+							v-model="markdownContent"
+							type="textarea"
+							:rows="20"
+							placeholder="请输入Markdown格式的内容..."
+							@input="handleInput"
+							class="markdown-input"
+							:disabled="readonly"
+						/>
+					</div>
+				</el-tab-pane>
+				<el-tab-pane label="预览" name="preview">
+					<div class="preview-container" v-html="previewHtml"></div>
+				</el-tab-pane>
+			</el-tabs>
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch, onMounted } from 'vue';
+import { marked } from 'marked';
+import DOMPurify from 'dompurify';
+
+const props = defineProps({
+	modelValue: {
+		type: String,
+		default: ''
+	},
+	readonly: {
+		type: Boolean,
+		default: false
+	}
+});
+
+const emit = defineEmits(['update:modelValue']);
+
+const activeTab = ref('edit');
+const markdownContent = ref('');
+
+// 配置marked选项
+marked.setOptions({
+	breaks: true,
+	gfm: true,
+});
+
+// 计算预览HTML
+const previewHtml = computed(() => {
+	if (!markdownContent.value) return '';
+	try {
+		const html = marked.parse(markdownContent.value) as string;
+		return DOMPurify.sanitize(html);
+	} catch (error) {
+		console.error('Markdown解析错误:', error);
+		return markdownContent.value;
+	}
+});
+
+// 处理输入
+const handleInput = () => {
+	emit('update:modelValue', markdownContent.value);
+};
+
+// 处理标签页切换
+const handleTabClick = (tab: any) => {
+	// 如果是只读模式,不允许切换到编辑标签页
+	if (props.readonly && tab.paneName === 'edit') {
+		return;
+	}
+	activeTab.value = tab.paneName;
+};
+
+// 监听外部值变化
+watch(() => props.modelValue, (newValue) => {
+	if (newValue !== markdownContent.value) {
+		markdownContent.value = newValue || '';
+	}
+}, { immediate: true });
+
+// 监听只读状态变化
+watch(() => props.readonly, (isReadonly) => {
+	if (isReadonly) {
+		// 只读模式下默认选中预览标签页
+		activeTab.value = 'preview';
+	} else {
+		// 编辑模式下默认选中编辑标签页
+		activeTab.value = 'edit';
+	}
+}, { immediate: true });
+
+// 组件挂载时初始化
+onMounted(() => {
+	markdownContent.value = props.modelValue || '';
+	// 根据只读状态设置默认标签页
+	if (props.readonly) {
+		activeTab.value = 'preview';
+	}
+});
+</script>
+
+<style scoped>
+.markdown-editor {
+	border: 1px solid #dcdfe6;
+	border-radius: 4px;
+	overflow: hidden;
+}
+
+.editor-tabs {
+	height: 100%;
+}
+
+.editor-container {
+	padding: 0;
+}
+
+.markdown-input {
+	height: 400px;
+}
+
+.markdown-input :deep(.el-textarea__inner) {
+	height: 100%;
+	resize: none;
+	font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+	font-size: 14px;
+	line-height: 1.5;
+	border: none;
+	border-radius: 0;
+}
+
+.markdown-input :deep(.el-textarea__inner:disabled) {
+	background-color: #f5f7fa;
+	color: #909399;
+	cursor: not-allowed;
+}
+
+.preview-container {
+	padding: 16px;
+	height: 400px;
+	overflow-y: auto;
+	border: 1px solid #f0f0f0;
+	background: #fff;
+}
+
+.preview-container :deep(h1) {
+	font-size: 24px;
+	margin: 16px 0 8px 0;
+	border-bottom: 1px solid #eaecef;
+	padding-bottom: 8px;
+}
+
+.preview-container :deep(h2) {
+	font-size: 20px;
+	margin: 14px 0 6px 0;
+	border-bottom: 1px solid #eaecef;
+	padding-bottom: 6px;
+}
+
+.preview-container :deep(h3) {
+	font-size: 18px;
+	margin: 12px 0 4px 0;
+}
+
+.preview-container :deep(h4) {
+	font-size: 16px;
+	margin: 10px 0 4px 0;
+}
+
+.preview-container :deep(h5) {
+	font-size: 14px;
+	margin: 8px 0 4px 0;
+}
+
+.preview-container :deep(h6) {
+	font-size: 12px;
+	margin: 6px 0 4px 0;
+}
+
+.preview-container :deep(p) {
+	margin: 8px 0;
+	line-height: 1.6;
+}
+
+.preview-container :deep(ul), 
+.preview-container :deep(ol) {
+	margin: 8px 0;
+	padding-left: 24px;
+}
+
+.preview-container :deep(li) {
+	margin: 4px 0;
+}
+
+.preview-container :deep(blockquote) {
+	margin: 8px 0;
+	padding: 8px 16px;
+	background: #f8f9fa;
+	border-left: 4px solid #dfe2e5;
+}
+
+.preview-container :deep(code) {
+	background: #f6f8fa;
+	padding: 2px 4px;
+	border-radius: 3px;
+	font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+	font-size: 13px;
+}
+
+.preview-container :deep(pre) {
+	background: #f6f8fa;
+	padding: 16px;
+	border-radius: 6px;
+	overflow-x: auto;
+	margin: 8px 0;
+}
+
+.preview-container :deep(pre code) {
+	background: none;
+	padding: 0;
+}
+
+.preview-container :deep(table) {
+	border-collapse: collapse;
+	width: 100%;
+	margin: 8px 0;
+}
+
+.preview-container :deep(th),
+.preview-container :deep(td) {
+	border: 1px solid #dfe2e5;
+	padding: 8px 12px;
+	text-align: left;
+}
+
+.preview-container :deep(th) {
+	background: #f6f8fa;
+	font-weight: 600;
+}
+
+.preview-container :deep(a) {
+	color: #0366d6;
+	text-decoration: none;
+}
+
+.preview-container :deep(a:hover) {
+	text-decoration: underline;
+}
+</style>
+

+ 301 - 0
src/views/system/ProtocolList/crud.tsx

@@ -0,0 +1,301 @@
+import * as api from './api';
+import { dict, useCompute, PageQuery, AddReq, DelReq, EditReq, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
+import { computed, shallowRef } from 'vue';
+import { auth } from '/@/utils/authFunction';
+import { marked } from 'marked';
+import DOMPurify from 'dompurify';
+import MarkdownEditor from './components/MarkdownEditor.vue';
+const { compute } = useCompute();
+
+// 配置marked选项
+marked.setOptions({
+	breaks: true, // 支持换行
+	gfm: true, // 支持GitHub风格的Markdown
+});
+
+// Markdown转换工具函数
+function convertMarkdownToHtml(markdown: string): string {
+	if (!markdown) return '';
+	
+	try {
+		// 使用marked解析Markdown
+		const html = marked.parse(markdown) as string;
+		// 使用DOMPurify清理HTML,防止XSS攻击
+		return DOMPurify.sanitize(html);
+	} catch (error) {
+		console.error('Markdown解析错误:', error);
+		return markdown; // 如果解析失败,返回原始内容
+	}
+}
+
+function convertHtmlToMarkdown(html: string): string {
+	if (!html) return '';
+	
+	// 简单的HTML到Markdown转换
+	let markdown = html
+		// 标题
+		.replace(/<h1[^>]*>(.*?)<\/h1>/gi, '# $1\n\n')
+		.replace(/<h2[^>]*>(.*?)<\/h2>/gi, '## $1\n\n')
+		.replace(/<h3[^>]*>(.*?)<\/h3>/gi, '### $1\n\n')
+		.replace(/<h4[^>]*>(.*?)<\/h4>/gi, '#### $1\n\n')
+		.replace(/<h5[^>]*>(.*?)<\/h5>/gi, '##### $1\n\n')
+		.replace(/<h6[^>]*>(.*?)<\/h6>/gi, '###### $1\n\n')
+		// 粗体和斜体
+		.replace(/<strong[^>]*>(.*?)<\/strong>/gi, '**$1**')
+		.replace(/<b[^>]*>(.*?)<\/b>/gi, '**$1**')
+		.replace(/<em[^>]*>(.*?)<\/em>/gi, '*$1*')
+		.replace(/<i[^>]*>(.*?)<\/i>/gi, '*$1*')
+		// 代码
+		.replace(/<pre[^>]*><code[^>]*>(.*?)<\/code><\/pre>/gi, '```\n$1\n```')
+		.replace(/<code[^>]*>(.*?)<\/code>/gi, '`$1`')
+		// 链接
+		.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, '[$2]($1)')
+		// 图片
+		.replace(/<img[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*>/gi, '![$2]($1)')
+		.replace(/<img[^>]*src="([^"]*)"[^>]*>/gi, '![]($1)')
+		// 列表
+		.replace(/<ul[^>]*>(.*?)<\/ul>/gi, '$1')
+		.replace(/<ol[^>]*>(.*?)<\/ol>/gi, '$1')
+		.replace(/<li[^>]*>(.*?)<\/li>/gi, '* $1\n')
+		// 引用
+		.replace(/<blockquote[^>]*>(.*?)<\/blockquote>/gi, '> $1\n')
+		// 水平线
+		.replace(/<hr[^>]*>/gi, '---\n')
+		// 段落和换行
+		.replace(/<p[^>]*>(.*?)<\/p>/gi, '$1\n\n')
+		.replace(/<br[^>]*>/gi, '\n')
+		.replace(/<div[^>]*>(.*?)<\/div>/gi, '$1\n')
+		// 清理其他HTML标签
+		.replace(/<[^>]+>/g, '')
+		// 清理多余的换行
+		.replace(/\n{3,}/g, '\n\n')
+		.trim();
+	
+	return markdown;
+}
+
+export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
+	const pageRequest = async (query: PageQuery) => {
+		return await api.GetList(query);
+	};
+	const editRequest = async ({ form, row }: EditReq) => {
+		form.id = row.id;
+		return await api.UpdateObj(form);
+	};
+	const delRequest = async ({ row }: DelReq) => {
+		return await api.DelObj(row.id);
+	};
+	const addRequest = async ({ form }: AddReq) => {
+		return await api.AddObj(form);
+	};
+
+	const viewRequest = async ({ row }: { row: any }) => {
+		return await api.GetObj(row.id);
+	};
+
+	return {
+		crudOptions: {
+			request: {
+				pageRequest,
+				addRequest,
+				editRequest,
+				delRequest,
+			},
+			actionbar: {
+				buttons: {
+					add: {
+						show: true,
+					},
+				},
+			},
+			rowHandle: {
+				fixed: 'right',
+				width: 200,
+				buttons: {
+					edit: {
+						text: '编辑',
+						type: 'text',
+						iconRight: 'Edit',
+						show: true,
+					},
+					view: {
+						text: '查看',
+						type: 'text',
+						iconRight: 'View',
+						show: true,
+						click({ index, row }) {
+							// 打开查看模式,传递只读标识
+							crudExpose.openView({ index, row });
+						},
+					},
+					remove: {
+						iconRight: 'Delete',
+						type: 'text',
+						show: true,
+					},
+				},
+			},
+			columns: {
+				id: {
+					title: 'ID',
+					form: {
+						show: false,
+					},
+					column: {
+						width: 80,
+					},
+				},
+				title: {
+					title: '协议标题',
+					search: {
+						show: true,
+					},
+					type: ['text', 'colspan'],
+					column: {
+						minWidth: 200,
+					},
+					form: {
+						rules: [
+							{
+								required: true,
+								message: '请输入协议标题',
+							},
+						],
+						component: { 
+							span: 24, 
+							placeholder: '请输入协议标题' 
+						},
+					},
+				},
+				key: {
+					title: '协议标识',
+					search: {
+						show: true,
+					},
+					type: 'text',
+					column: {
+						minWidth: 150,
+					},
+					form: {
+						rules: [
+							{
+								required: true,
+								message: '请输入协议标识',
+							},
+							/* {
+								pattern: /^[a-z_]+$/,
+								message: '协议标识只能包含小写字母和下划线',
+							}, */
+						],
+						component: { 
+							span: 24, 
+							placeholder: '如:terms_of_service' 
+						},
+					},
+				},
+				version: {
+					title: '版本号',
+					search: {
+						show: true,
+					},
+					type: 'text',
+					column: {
+						width: 120,
+					},
+					form: {
+						rules: [
+							{
+								required: true,
+								message: '请输入版本号',
+							},
+						],
+						component: { 
+							span: 24, 
+							placeholder: '如:1.0' 
+						},
+					},
+				},
+				publish_date: {
+					title: '发布日期',
+					type: 'datetime',
+					column: {
+						
+						width: 180,
+					},
+					form: {
+						show:false,
+						rules: [
+							{
+								required: false,
+								message: '请选择发布日期',
+							},
+						],
+						component: { 
+							span: 24,
+							valueFormat: 'YYYY-MM-DDTHH:mm:ssZ',
+						},
+					},
+				},
+				is_active: {
+					title: '是否启用',
+					type: 'dict-select',
+					column: {
+						width: 120,
+					},
+					dict: dict({
+						data: [
+							{ label: '启用', value: true, color: 'success' },
+							{ label: '禁用', value: false, color: 'danger' },
+						],
+					}),
+					form: {
+						rules: [
+							{
+								required: true,
+								message: '请选择是否启用',
+							},
+						],
+						component: { 
+							span: 24,
+						},
+					},
+				},
+				content: {
+					title: '协议内容',
+					column: {
+						width: 300,
+						show: false,
+					},
+					type: 'colspan',
+					form: {
+						rules: [
+							{
+								required: true,
+								message: '请输入协议内容',
+							},
+						],
+						component: {
+							name: shallowRef(MarkdownEditor),
+							vModel: 'modelValue',
+							readonly: compute((context) => {
+								const { mode } = context;
+								console.log('当前模式:', mode, '只读状态:', mode === 'view');
+								return mode === 'view';
+							}),
+							span: 24,
+						},
+					},
+					// 在查看模式下也使用相同的组件配置
+					view: {
+						component: {
+							name: shallowRef(MarkdownEditor),
+							vModel: 'modelValue',
+							readonly: true, // 查看模式始终只读
+							span: 24,
+						},
+					},
+				},
+			},
+		},
+	};
+}

+ 56 - 0
src/views/system/ProtocolList/example.md

@@ -0,0 +1,56 @@
+# 用户服务协议示例
+
+## 1. 总则
+
+本协议是用户与平台之间的法律协议,规定了双方的权利和义务。
+
+### 1.1 协议范围
+
+本协议适用于:
+- 所有注册用户
+- 平台提供的所有服务
+- 用户使用平台功能时的行为规范
+
+## 2. 用户权利
+
+用户享有以下权利:
+
+* **隐私保护权**:平台承诺保护用户隐私信息
+* **服务使用权**:在遵守协议的前提下使用平台服务
+* **数据控制权**:用户有权控制自己的数据
+
+## 3. 用户义务
+
+用户应当:
+
+1. 遵守国家法律法规
+2. 不得进行违法违规活动
+3. 保护账户安全
+4. 及时更新个人信息
+
+## 4. 平台责任
+
+平台承诺:
+
+> 为用户提供安全、稳定的服务环境,保护用户合法权益。
+
+## 5. 免责声明
+
+```text
+平台在以下情况下不承担责任:
+- 因不可抗力导致的服务中断
+- 用户违规操作造成的损失
+- 第三方服务的问题
+```
+
+## 6. 协议修改
+
+本协议可能会根据法律法规变化或业务需要进行调整,修改后的协议将在平台公布。
+
+---
+
+**生效日期**:2025年1月1日  
+**版本号**:1.0  
+**联系方式**:support@example.com
+
+

+ 20 - 0
src/views/system/ProtocolList/index.vue

@@ -0,0 +1,20 @@
+<template>
+	<fs-page>
+		<fs-crud ref="crudRef" v-bind="crudBinding">
+		</fs-crud>
+	</fs-page>
+</template>
+
+<script lang="ts" setup name="protocolList">
+import { onMounted } from 'vue';
+import { useFs } from '@fast-crud/fast-crud';
+import createCrudOptions from './crud';
+
+// 初始化crud配置
+const { crudRef, crudBinding, crudExpose } = useFs({ createCrudOptions });
+
+// 页面打开后获取列表数据
+onMounted(() => {
+	crudExpose.doRefresh();
+});
+</script>

+ 31 - 0
src/views/system/ProtocolList/test-data.json

@@ -0,0 +1,31 @@
+{
+  "agreements": [
+    {
+      "id": 1,
+      "title": "用户服务协议",
+      "key": "terms_of_service",
+      "content": "# 用户服务协议\n\n## 1. 总则\n\n本协议是用户与平台之间的法律协议,规定了双方的权利和义务。\n\n### 1.1 协议范围\n\n本协议适用于:\n- 所有注册用户\n- 平台提供的所有服务\n- 用户使用平台功能时的行为规范\n\n## 2. 用户权利\n\n用户享有以下权利:\n\n* **隐私保护权**:平台承诺保护用户隐私信息\n* **服务使用权**:在遵守协议的前提下使用平台服务\n* **数据控制权**:用户有权控制自己的数据\n\n## 3. 用户义务\n\n用户应当:\n\n1. 遵守国家法律法规\n2. 不得进行违法违规活动\n3. 保护账户安全\n4. 及时更新个人信息\n\n## 4. 平台责任\n\n平台承诺:\n\n> 为用户提供安全、稳定的服务环境,保护用户合法权益。\n\n## 5. 免责声明\n\n```text\n平台在以下情况下不承担责任:\n- 因不可抗力导致的服务中断\n- 用户违规操作造成的损失\n- 第三方服务的问题\n```\n\n## 6. 协议修改\n\n本协议可能会根据法律法规变化或业务需要进行调整,修改后的协议将在平台公布。\n\n---\n\n**生效日期**:2025年1月1日  \n**版本号**:1.0  \n**联系方式**:support@example.com",
+      "version": "1.0",
+      "publish_date": "2025-01-01T00:00:00Z",
+      "is_active": true
+    },
+    {
+      "id": 2,
+      "title": "隐私保护政策",
+      "key": "privacy_policy",
+      "content": "# 隐私保护政策\n\n## 概述\n\n我们非常重视您的隐私保护,本政策详细说明了我们如何收集、使用和保护您的个人信息。\n\n## 信息收集\n\n### 我们收集的信息类型\n\n- **个人身份信息**:姓名、邮箱、电话等\n- **使用数据**:访问记录、操作日志等\n- **设备信息**:IP地址、浏览器类型等\n\n### 信息收集方式\n\n1. 您主动提供的信息\n2. 自动收集的技术信息\n3. 第三方来源的信息\n\n## 信息使用\n\n我们使用收集的信息用于:\n\n* 提供和改进服务\n* 与您沟通\n* 法律合规要求\n\n## 信息保护\n\n我们采用以下措施保护您的信息:\n\n- 数据加密\n- 访问控制\n- 定期安全审计\n\n## 联系我们\n\n如有任何问题,请联系:privacy@example.com",
+      "version": "1.0",
+      "publish_date": "2025-01-01T00:00:00Z",
+      "is_active": true
+    },
+    {
+      "id": 3,
+      "title": "Cookie使用政策",
+      "key": "cookie_policy",
+      "content": "# Cookie使用政策\n\n## 什么是Cookie\n\nCookie是存储在您设备上的小型文本文件,用于改善您的浏览体验。\n\n## Cookie类型\n\n### 必要Cookie\n\n这些Cookie对于网站正常运行是必需的。\n\n### 功能Cookie\n\n这些Cookie允许网站记住您的选择。\n\n### 分析Cookie\n\n这些Cookie帮助我们了解网站的使用情况。\n\n## 管理Cookie\n\n您可以通过浏览器设置管理Cookie:\n\n1. 查看已存储的Cookie\n2. 删除不需要的Cookie\n3. 阻止特定网站的Cookie\n\n## 第三方Cookie\n\n我们可能使用第三方服务,这些服务可能设置自己的Cookie。",
+      "version": "1.0",
+      "publish_date": "2025-01-01T00:00:00Z",
+      "is_active": true
+    }
+  ]
+}

+ 1 - 1
src/views/system/UserAgreement/api.ts

@@ -44,7 +44,7 @@ export function DelObj(id: DelReq) {
 /*  */
 export function GetObjByPath(id: number) {
 	return request({
-		url:`/api/platform/agreements/${id}/`,
+		url:`/api/public/agreements/${id}/`,
 		method: 'get',
 	});
 }

+ 25 - 6
src/views/system/UserAgreement/view.vue

@@ -1,11 +1,11 @@
 <template>
     <div class="agreement-page">
-        <div class="agreement-header">
+        <!-- <div class="agreement-header">
             <h2>{{ pageTitle }}</h2>
-        </div>
+        </div> -->
         <el-scrollbar class="agreement-content" v-loading="loading">
             <div v-if="error" class="agreement-error">{{ error }}</div>
-            <div v-else v-html="htmlContent"></div>
+            <div v-else class="agreement-body" v-html="renderedHtml"></div>
         </el-scrollbar>
     </div>
 </template>
@@ -14,6 +14,8 @@
 import { onMounted, ref, computed } from 'vue';
 import { useRoute } from 'vue-router';
 import { GetObjByPath } from './api';
+import { marked } from 'marked';
+import DOMPurify from 'dompurify';
 
 const route = useRoute();
 
@@ -22,9 +24,9 @@ const error = ref('');
 const htmlContent = ref('');
 
 // 根据路径名默认映射 ID;也支持通过 ?id= 覆盖
-const defaultIdByPath: Record<string, number> = {
-    '/user-agreement': 3,
-    '/privacy-policy': 4,
+const defaultIdByPath: Record<string, string> = {
+    '/user-agreement': 'user_service_Agreement',
+    '/privacy-policy': 'privacy_Policy',
 };
 
 const currentId = computed(() => {
@@ -66,6 +68,19 @@ const fetchAgreement = async () => {
 };
 
 onMounted(fetchAgreement);
+
+// Markdown 渲染与安全处理
+marked.setOptions({
+    breaks: true,
+    gfm: true,
+});
+
+const renderedHtml = computed(() => {
+    const raw = htmlContent.value || '';
+    const containsHtmlTag = /<\/?[a-z][\s\S]*>/i.test(raw);
+    const html = containsHtmlTag ? raw : (marked.parse(raw) as string);
+    return DOMPurify.sanitize(html);
+});
 </script>
 
 <style scoped lang="scss">
@@ -84,6 +99,10 @@ onMounted(fetchAgreement);
     padding: 20px;
     height: calc(100vh - 64px);
 }
+.agreement-body {
+    max-width: 800px;
+    margin: 0 auto;
+}
 .agreement-error {
     color: #f56c6c;
 }