mirror of
https://gitee.com/bootx/dax-pay-ui
synced 2026-08-12 07:25:39 +08:00
feat(admin): 用户协议与隐私政策展示页接入后端接口
- 新增 AgreementContentView 公共组件,调用 findDefault 接口渲染 Markdown 正文 - agreement-terms/agreement-privacy 改为薄壳页面 - 协议路由移出 Authentication children 为独立顶级路由,全屏展示 - request.ts 增加 silentError 静默错误支持,协议未配置时不弹 toast - 10 语种 authentication.json 同步新增 agreementEmpty/agreementEffectiveTime
This commit is contained in:
@@ -93,6 +93,10 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
||||
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
|
||||
client.addResponseInterceptor(
|
||||
errorMessageResponseInterceptor((msg: string, error) => {
|
||||
// 静默错误(请求 config 中设置 silentError=true), 不弹全局 toast, 由业务自行处理
|
||||
if (error?.config?.silentError) {
|
||||
return;
|
||||
}
|
||||
const { message } = useMessage();
|
||||
// HTTP 错误(401等)响应体在 response.data; 业务错误(200但code非0)在顶层 data
|
||||
const body = error?.response?.data ?? error?.data ?? {};
|
||||
|
||||
@@ -46,9 +46,13 @@ export const UserProtocolApi = {
|
||||
clientTypeOptions(): Promise<Result<LabelValue[]>> {
|
||||
return defHttp.get({ url: '/user/protocol/client-type-options' });
|
||||
},
|
||||
/** 查询默认协议内容(对外, 各端展示用) */
|
||||
/** 查询默认协议内容(对外, 各端展示用; 协议未配置时会抛异常, 静默错误由调用方自行处理空态) */
|
||||
findDefault(type: string, clientType: string, language?: string): Promise<Result<UserProtocolContent>> {
|
||||
return defHttp.get({ url: '/user/protocol/find-default', params: { type, clientType, language } });
|
||||
return defHttp.get({
|
||||
url: '/user/protocol/find-default',
|
||||
params: { type, clientType, language },
|
||||
silentError: true,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -51,6 +51,30 @@ const socialBindCallbackRoute: RouteRecordRaw = {
|
||||
},
|
||||
};
|
||||
|
||||
/** 协议展示页(独立全屏路由,不走 AuthPageLayout,免登录预览协议正文) */
|
||||
const agreementTermsRoute: RouteRecordRaw = {
|
||||
name: 'AgreementTerms',
|
||||
path: '/auth/agreement/terms',
|
||||
component: () => import('#/views/_core/authentication/agreement-terms.vue'),
|
||||
meta: {
|
||||
// 用户协议
|
||||
title: 'authentication.termsTitle',
|
||||
hideInMenu: true,
|
||||
},
|
||||
};
|
||||
|
||||
/** 隐私政策展示页(独立全屏路由,不走 AuthPageLayout,免登录预览协议正文) */
|
||||
const agreementPrivacyRoute: RouteRecordRaw = {
|
||||
name: 'AgreementPrivacy',
|
||||
path: '/auth/agreement/privacy',
|
||||
component: () => import('#/views/_core/authentication/agreement-privacy.vue'),
|
||||
meta: {
|
||||
// 隐私政策
|
||||
title: 'authentication.privacyTitle',
|
||||
hideInMenu: true,
|
||||
},
|
||||
};
|
||||
|
||||
// 服务不可用提示页(后端不可用时的兜底,独立全屏,不走权限拦截)
|
||||
const serviceUnavailableRoute: RouteRecordRaw = {
|
||||
name: 'ServiceUnavailable',
|
||||
@@ -131,30 +155,13 @@ const coreRoutes: RouteRecordRaw[] = [
|
||||
hideInMenu: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AgreementTerms',
|
||||
path: 'agreement/terms',
|
||||
component: () => import('#/views/_core/authentication/agreement-terms.vue'),
|
||||
meta: {
|
||||
// 用户协议
|
||||
title: 'authentication.termsTitle',
|
||||
hideInMenu: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AgreementPrivacy',
|
||||
path: 'agreement/privacy',
|
||||
component: () => import('#/views/_core/authentication/agreement-privacy.vue'),
|
||||
meta: {
|
||||
// 隐私政策
|
||||
title: 'authentication.privacyTitle',
|
||||
hideInMenu: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// 第三方绑定回调(独立路由,弹窗模式)
|
||||
socialBindCallbackRoute,
|
||||
// 协议展示页(独立全屏路由,不走 AuthPageLayout)
|
||||
agreementTermsRoute,
|
||||
agreementPrivacyRoute,
|
||||
// 服务不可用提示页(后端不可用兜底,独立全屏,不走权限拦截)
|
||||
serviceUnavailableRoute,
|
||||
];
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { AuthPageCard } from './components';
|
||||
import { AgreementContentView } from './components';
|
||||
|
||||
defineOptions({ name: 'AgreementPrivacy' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 国际化:隐私政策 -->
|
||||
<AuthPageCard :title="$t('authentication.privacyTitle')">
|
||||
<!-- 占位提示,后续替换为正式协议内容 -->
|
||||
<a-alert :message="$t('authentication.agreementContentPlaceholder')" type="info" show-icon />
|
||||
</AuthPageCard>
|
||||
<AgreementContentView type="PRIVACY_POLICY" />
|
||||
</template>
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { AuthPageCard } from './components';
|
||||
import { AgreementContentView } from './components';
|
||||
|
||||
defineOptions({ name: 'AgreementTerms' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 国际化:用户协议 -->
|
||||
<AuthPageCard :title="$t('authentication.termsTitle')">
|
||||
<!-- 占位提示,后续替换为正式协议内容 -->
|
||||
<a-alert :message="$t('authentication.agreementContentPlaceholder')" type="info" show-icon />
|
||||
</AuthPageCard>
|
||||
<AgreementContentView type="USER_AGREEMENT" />
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { $t, useI18n } from '@vben/locales';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { MdPreview } from 'md-editor-v3';
|
||||
|
||||
import { UserProtocolApi, type UserProtocolContent } from '#/api/system/protocol/user-protocol.api';
|
||||
|
||||
import 'md-editor-v3/lib/preview.css';
|
||||
|
||||
defineOptions({ name: 'AgreementContentView' });
|
||||
|
||||
const props = defineProps<{
|
||||
/** 协议类型: USER_AGREEMENT(用户协议) | PRIVACY_POLICY(隐私政策) */
|
||||
type: 'PRIVACY_POLICY' | 'USER_AGREEMENT';
|
||||
}>();
|
||||
|
||||
const { locale } = useI18n();
|
||||
|
||||
// 协议内容
|
||||
const content = ref<UserProtocolContent>({});
|
||||
const loading = ref(false);
|
||||
const isEmpty = ref(false);
|
||||
|
||||
// 标题: 优先取后端返回的 title, 否则按协议类型取 i18n
|
||||
const title = computed(() => {
|
||||
return (
|
||||
content.value.title ||
|
||||
$t(props.type === 'USER_AGREEMENT' ? 'authentication.termsTitle' : 'authentication.privacyTitle')
|
||||
);
|
||||
});
|
||||
|
||||
// 版本标签与生效时间等元信息
|
||||
const metaText = computed(() => {
|
||||
const parts: string[] = [];
|
||||
if (content.value.versionLabel) {
|
||||
parts.push(content.value.versionLabel);
|
||||
} else if (content.value.versionNo) {
|
||||
// 版本号
|
||||
parts.push(`v${content.value.versionNo}`);
|
||||
}
|
||||
if (content.value.effectiveTime) {
|
||||
// 生效时间
|
||||
const formatted = dayjs(content.value.effectiveTime).format('YYYY-MM-DD');
|
||||
if (formatted && formatted !== 'Invalid Date') {
|
||||
parts.push(`${$t('authentication.agreementEffectiveTime')}: ${formatted}`);
|
||||
}
|
||||
}
|
||||
return parts.join(' \u00B7 ');
|
||||
});
|
||||
|
||||
/** 加载协议内容 */
|
||||
async function loadContent() {
|
||||
loading.value = true;
|
||||
isEmpty.value = false;
|
||||
try {
|
||||
const result = await UserProtocolApi.findDefault(props.type, 'WEB', locale.value);
|
||||
// 接口返回空或无正文内容时视为未配置
|
||||
if (!result?.data || !result.data.content) {
|
||||
isEmpty.value = true;
|
||||
content.value = {};
|
||||
} else {
|
||||
content.value = result.data;
|
||||
}
|
||||
} catch {
|
||||
// 协议未配置等情况, 显示空态(silentError 已拦截全局 toast)
|
||||
isEmpty.value = true;
|
||||
content.value = {};
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadContent);
|
||||
|
||||
// 语言切换时重新拉取对应语言的协议内容
|
||||
watch(locale, loadContent);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex min-h-screen w-full justify-center px-4 py-10">
|
||||
<div class="w-full max-w-4xl">
|
||||
<a-card class="rounded-lg shadow-lg" variant="borderless">
|
||||
<!-- 标题区 -->
|
||||
<div class="mb-2 text-center">
|
||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ title }}
|
||||
</h1>
|
||||
<p v-if="metaText" class="mt-2 text-sm text-gray-400">
|
||||
{{ metaText }}
|
||||
</p>
|
||||
</div>
|
||||
<a-divider />
|
||||
<!-- 加载态 -->
|
||||
<a-skeleton v-if="loading" active :paragraph="{ rows: 12 }" />
|
||||
<!-- 空态: 协议未配置 -->
|
||||
<a-empty v-else-if="isEmpty" :description="$t('authentication.agreementEmpty')" />
|
||||
<!-- 协议正文(Markdown 渲染) -->
|
||||
<MdPreview v-else id="agreement-content-preview" :model-value="content.content || ''" />
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,3 +1,4 @@
|
||||
export { default as AgreementContentView } from './AgreementContentView.vue';
|
||||
export { default as AuthFormActions } from './AuthFormActions.vue';
|
||||
export { default as AuthPageCard } from './AuthPageCard.vue';
|
||||
export { default as AuthPageFooterActions } from './AuthPageFooterActions.vue';
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
"termsTitle": "Terms of Service",
|
||||
"privacyTitle": "Privacy Policy",
|
||||
"agreementContentPlaceholder": "This agreement is being drafted and will be available in a future release.",
|
||||
"agreementEmpty": "No agreement content has been configured. Please contact the administrator to configure it.",
|
||||
"agreementEffectiveTime": "Effective Date",
|
||||
"layout": {
|
||||
"center": "Align Center",
|
||||
"alignLeft": "Align Left",
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
"termsTitle": "Ketentuan Layanan",
|
||||
"privacyTitle": "Kebijakan Privasi",
|
||||
"agreementContentPlaceholder": "Perjanjian ini sedang dirancang dan akan tersedia pada rilis mendatang.",
|
||||
"agreementEmpty": "Konten perjanjian belum dikonfigurasi. Silakan hubungi administrator untuk mengonfigurasinya.",
|
||||
"agreementEffectiveTime": "Tanggal Berlaku",
|
||||
"layout": {
|
||||
"center": "Sejajarkan Pusat",
|
||||
"alignLeft": "Sejajarkan ke Kiri",
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
"termsTitle": "利用規約",
|
||||
"privacyTitle": "プライバシーポリシー",
|
||||
"agreementContentPlaceholder": "この契約は現在草案中であり、将来のリリースで利用可能になる予定です。",
|
||||
"agreementEmpty": "利用規約の内容が設定されていません。管理者にご連絡のうえ設定をご依頼ください。",
|
||||
"agreementEffectiveTime": "発効日",
|
||||
"layout": {
|
||||
"center": "中央揃え",
|
||||
"alignLeft": "左揃え",
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
"termsTitle": "서비스 약관",
|
||||
"privacyTitle": "개인 정보 보호 정책",
|
||||
"agreementContentPlaceholder": "본 계약은 초안 작성 중이며 향후 릴리스에서 제공될 예정입니다.",
|
||||
"agreementEmpty": "약관 내용이 구성되지 않았습니다. 관리자에게 문의하여 구성해 주세요.",
|
||||
"agreementEffectiveTime": "시행일",
|
||||
"layout": {
|
||||
"center": "중심 정렬",
|
||||
"alignLeft": "왼쪽 정렬",
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
"termsTitle": "Syarat Perkhidmatan",
|
||||
"privacyTitle": "Dasar Privasi",
|
||||
"agreementContentPlaceholder": "Perjanjian ini sedang dirangka dan akan tersedia dalam keluaran akan datang.",
|
||||
"agreementEmpty": "Kandungan perjanjian belum dikonfigurasi. Sila hubungi pentadbir untuk mengkonfigurasikannya.",
|
||||
"agreementEffectiveTime": "Tarikh Berkuat Kuasa",
|
||||
"layout": {
|
||||
"center": "Jajarkan Tengah",
|
||||
"alignLeft": "Jajar Kiri",
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
"termsTitle": "ข้อกำหนดในการให้บริการ",
|
||||
"privacyTitle": "นโยบายความเป็นส่วนตัว",
|
||||
"agreementContentPlaceholder": "ข้อตกลงนี้อยู่ระหว่างการร่างและจะพร้อมใช้งานในรุ่นต่อๆ ไป",
|
||||
"agreementEmpty": "ยังไม่ได้กำหนดเนื้อหาข้อตกลง โปรดติดต่อผู้ดูแลระบบเพื่อกำหนด",
|
||||
"agreementEffectiveTime": "วันที่มีผลบังคับ",
|
||||
"layout": {
|
||||
"center": "จัดกึ่งกลาง",
|
||||
"alignLeft": "จัดชิดซ้าย",
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
"termsTitle": "Điều khoản dịch vụ",
|
||||
"privacyTitle": "Chính sách bảo mật",
|
||||
"agreementContentPlaceholder": "Thỏa thuận này đang được soạn thảo và sẽ có trong bản phát hành trong tương lai.",
|
||||
"agreementEmpty": "Nội dung thỏa thuận chưa được cấu hình. Vui lòng liên hệ quản trị viên để cấu hình.",
|
||||
"agreementEffectiveTime": "Ngày có hiệu lực",
|
||||
"layout": {
|
||||
"center": "Căn giữa",
|
||||
"alignLeft": "Căn trái",
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
"termsTitle": "用户协议",
|
||||
"privacyTitle": "隐私政策",
|
||||
"agreementContentPlaceholder": "协议内容正在完善中,正式版将在后续版本提供。",
|
||||
"agreementEmpty": "暂未配置协议内容,请联系管理员配置后再查看。",
|
||||
"agreementEffectiveTime": "生效时间",
|
||||
"layout": {
|
||||
"center": "居中",
|
||||
"alignLeft": "居左",
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
"termsTitle": "用户協議",
|
||||
"privacyTitle": "隱私政策",
|
||||
"agreementContentPlaceholder": "協議內容正在完善中,正式版將在後續版本提供。",
|
||||
"agreementEmpty": "暫未配置協議內容,請聯絡管理員配置後再查看。",
|
||||
"agreementEffectiveTime": "生效時間",
|
||||
"layout": {
|
||||
"center": "居中",
|
||||
"alignLeft": "居左",
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
"termsTitle": "使用者協議",
|
||||
"privacyTitle": "隱私政策",
|
||||
"agreementContentPlaceholder": "協議內容正在完善中,正式版將在後續版本提供。",
|
||||
"agreementEmpty": "暫未配置協議內容,請聯繫管理員配置後再查看。",
|
||||
"agreementEffectiveTime": "生效時間",
|
||||
"layout": {
|
||||
"center": "居中",
|
||||
"alignLeft": "居左",
|
||||
|
||||
Reference in New Issue
Block a user