view.vue 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. <template>
  2. <div class="agreement-page">
  3. <!-- <div class="agreement-header">
  4. <h2>{{ pageTitle }}</h2>
  5. </div> -->
  6. <el-scrollbar class="agreement-content" v-loading="loading">
  7. <div v-if="error" class="agreement-error">{{ error }}</div>
  8. <div v-else class="agreement-body" v-html="renderedHtml"></div>
  9. </el-scrollbar>
  10. </div>
  11. </template>
  12. <script setup lang="ts">
  13. import { onMounted, ref, computed } from 'vue';
  14. import { useRoute } from 'vue-router';
  15. import { GetObjByPath } from './api';
  16. import { marked } from 'marked';
  17. import DOMPurify from 'dompurify';
  18. const route = useRoute();
  19. const loading = ref(false);
  20. const error = ref('');
  21. const htmlContent = ref('');
  22. // 根据路径名默认映射 ID;也支持通过 ?id= 覆盖
  23. const defaultIdByPath: Record<string, string> = {
  24. '/user-agreement': 'user_service_Agreement',
  25. '/privacy-policy': 'privacy_Policy',
  26. };
  27. const currentId = computed(() => {
  28. const qid = Number(route.query.id);
  29. if (!Number.isNaN(qid) && qid > 0) return qid;
  30. return defaultIdByPath[route.path] ?? 3;
  31. });
  32. const pageTitle = computed(() => {
  33. if (route.path === '/privacy-policy') return '隐私协议';
  34. return '用户服务协议';
  35. });
  36. const parseContent = (data: any): string => {
  37. // 兼容不同字段命名
  38. return (
  39. data?.content_html ||
  40. data?.content ||
  41. data?.detail ||
  42. data?.description ||
  43. ''
  44. );
  45. };
  46. const fetchAgreement = async () => {
  47. loading.value = true;
  48. error.value = '';
  49. htmlContent.value = '';
  50. try {
  51. const res: any = await GetObjByPath(currentId.value as any);
  52. // 兼容 service 里 code=200 / 2000 的返回
  53. const payload = res?.data ?? res;
  54. htmlContent.value = parseContent(payload) || '<p>暂无内容</p>';
  55. } catch (e: any) {
  56. error.value = e?.msg || '内容加载失败';
  57. } finally {
  58. loading.value = false;
  59. }
  60. };
  61. onMounted(fetchAgreement);
  62. // Markdown 渲染与安全处理
  63. marked.setOptions({
  64. breaks: true,
  65. gfm: true,
  66. });
  67. const renderedHtml = computed(() => {
  68. const raw = htmlContent.value || '';
  69. const containsHtmlTag = /<\/?[a-z][\s\S]*>/i.test(raw);
  70. const html = containsHtmlTag ? raw : (marked.parse(raw) as string);
  71. return DOMPurify.sanitize(html);
  72. });
  73. </script>
  74. <style scoped lang="scss">
  75. .agreement-page {
  76. display: flex;
  77. flex-direction: column;
  78. width: 100%;
  79. height: 100vh;
  80. background: #fff;
  81. }
  82. .agreement-header {
  83. padding: 16px 20px;
  84. border-bottom: 1px solid #f0f0f0;
  85. }
  86. .agreement-content {
  87. padding: 20px;
  88. height: calc(100vh - 64px);
  89. }
  90. .agreement-body {
  91. max-width: 800px;
  92. margin: 0 auto;
  93. }
  94. .agreement-error {
  95. color: #f56c6c;
  96. }
  97. </style>