feat(payment): 加 useEnvStore 共享沙箱开关状态, ProductConfig 加沙箱提示

部署级沙箱隔离方案配套 UI:
- 新建 Pinia store (apps/daxpay-admin/src/store/env.ts): 封装 sandboxEnabled
  状态 + loadSandboxEnabled(force?), 避免多个页面重复请求同一 API
- ProductConfig.vue: 复用 useEnvStore 替换局部 ref; a-card 内加 a-alert 提示
  「沙箱功能仅在测试环境可用」, 仅 sandboxEnabled=true 时显示
- 补齐 10 语种 i18n (langs/{locale}/payment/common/env.json):
  - sandboxBanner: workspace 工作台告警文案 (留给后续 ConfigCheck 改动合并后接入)
  - sandboxProductConfigTip: 产品配置页 a-alert 文案
This commit is contained in:
DaxPay Dev
2026-07-20 09:51:58 +08:00
parent abdd4dc597
commit 7138817e2c
13 changed files with 103 additions and 14 deletions

View File

@@ -0,0 +1,4 @@
{
"sandboxBanner": "You are in the sandbox environment. No real transactions will occur.",
"sandboxProductConfigTip": "Sandbox features are only available in the test environment. In production deployments, the sandbox switch is forced off and all data is guaranteed to be production data."
}

View File

@@ -0,0 +1,4 @@
{
"sandboxBanner": "Ini adalah lingkungan sandbox. Tidak ada transaksi nyata yang akan terjadi.",
"sandboxProductConfigTip": "Fitur sandbox hanya tersedia di lingkungan pengujian. Pada deployment produksi, sakelar sandbox dipaksa mati dan semua data dijamin sebagai data produksi."
}

View File

@@ -0,0 +1,4 @@
{
"sandboxBanner": "現在はサンドボックス環境です。実際の取引は発生しません。",
"sandboxProductConfigTip": "サンドボックス機能はテスト環境でのみ利用できます。本番デプロイではサンドボックススイッチが強制的にオフになり、すべてのデータは本番データであることが保証されます。"
}

View File

@@ -0,0 +1,4 @@
{
"sandboxBanner": "현재 샌드박스 환경입니다. 실제 거래가 발생하지 않습니다.",
"sandboxProductConfigTip": "샌드박스 기능은 테스트 환경에서만 사용할 수 있습니다. 프로덕션 배포에서는 샌드박스 스위치가 강제로 꺼지며 모든 데이터는 프로덕션 데이터로 보장됩니다."
}

View File

@@ -0,0 +1,4 @@
{
"sandboxBanner": "Ini adalah persekitaran sandbox. Tiada urus niaga sebenar akan berlaku.",
"sandboxProductConfigTip": "Ciri sandbox hanya tersedia di persekitaran ujian. Pada pengeluaran, suis sandbox dipaksa dimatikan dan semua data dijamin sebagai data pengeluaran."
}

View File

@@ -0,0 +1,4 @@
{
"sandboxBanner": "นี่คือสภาพแวดล้อมแซนด์บ็อกซ์ จะไม่มีธุรกรรมจริงเกิดขึ้น",
"sandboxProductConfigTip": "คุณสมบัติแซนด์บ็อกซ์ใช้ได้เฉพาะในสภาพแวดล้อมทดสอบ เมื่อปรับใช้งานจริง สวิตช์แซนด์บ็อกซ์จะถูกบังคับปิดและรับประกันว่าข้อมูลทั้งหมดเป็นข้อมูลการผลิต"
}

View File

@@ -0,0 +1,4 @@
{
"sandboxBanner": "Đây là môi trường sandbox. Không có giao dịch thực nào sẽ xảy ra.",
"sandboxProductConfigTip": "Tính năng sandbox chỉ khả dụng trong môi trường thử nghiệm. Khi triển khai sản xuất, công tắc sandbox bị tắt cưỡng bức và mọi dữ liệu được đảm bảo là dữ liệu sản xuất."
}

View File

@@ -0,0 +1,4 @@
{
"sandboxBanner": "当前为测试环境,所有交易不会真实发生",
"sandboxProductConfigTip": "沙箱功能仅在测试环境可用。生产环境部署时,沙箱开关将被强制关闭,所有数据保证为生产数据。"
}

View File

@@ -0,0 +1,4 @@
{
"sandboxBanner": "目前為測試環境,所有交易唔會真實發生",
"sandboxProductConfigTip": "沙箱功能僅在測試環境可用。生產環境部署時,沙箱開關將被強制關閉,所有資料保證為生產資料。"
}

View File

@@ -0,0 +1,4 @@
{
"sandboxBanner": "目前為測試環境,所有交易不會真實發生",
"sandboxProductConfigTip": "沙箱功能僅在測試環境可用。生產環境部署時,沙箱開關將被強制關閉,所有資料保證為生產資料。"
}

View File

@@ -0,0 +1,40 @@
import { ref } from 'vue';
import { defineStore } from 'pinia';
import { PayEnvApi } from '#/api/payment/masterdata/pay-env.api';
/**
* 平台环境状态 store
*
* 全局缓存沙箱开关等平台级配置, 供 Layout 横幅、产品配置页等共享, 避免重复请求。
* 生命周期: 用户登录后由 basic.vue 触发首次加载, 整个会话内复用。
*/
export const useEnvStore = defineStore('app-env', () => {
/** 沙箱环境全局开关 (后端 daxpay.platform.config.sandbox-enabled 控制) */
const sandboxEnabled = ref(false);
/** 是否已完成首次加载 (避免重复请求) */
const loaded = ref(false);
/**
* 加载沙箱开关状态
*
* 默认仅首次加载, 已加载后调用为 no-op; 如需刷新 (如管理员切换了全局开关) 传 force=true。
*
* @param force 是否强制刷新
*/
async function loadSandboxEnabled(force = false) {
if (loaded.value && !force) return;
try {
const { data } = await PayEnvApi.sandboxEnabled();
sandboxEnabled.value = !!data;
} catch {
// 查询失败按关闭处理, 仅展示生产环境
sandboxEnabled.value = false;
}
loaded.value = true;
}
return { sandboxEnabled, loaded, loadSandboxEnabled };
});

View File

@@ -1,2 +1,3 @@
export * from './auth';
export * from './dict';
export * from './env';

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { $t } from '@vben/locales';
@@ -7,9 +7,9 @@
import { IconifyIcon } from '@vben-core/icons';
import { PayProductConfigApi, type PayProductConfigResult } from '#/api/payment/config/pay-product-config.api';
import { PayEnvApi } from '#/api/payment/masterdata/pay-env.api';
import ChannelLogo from '#/components/channel/ChannelLogo.vue';
import { useMessage } from '#/hooks/useMessage';
import { useEnvStore } from '#/store';
defineOptions({ name: 'ProductConfig' });
@@ -19,8 +19,11 @@
const loading = ref(false);
const productList = ref<PayProductConfigResult[]>([]);
const radioRefreshKey = ref(0);
// 全局沙箱环境开关(后端控制), 关闭时不展示沙箱相关选项
const sandboxEnabled = ref(false);
// 全局沙箱环境状态(复用 store, 与 Layout 横幅共享同一份缓存)
const envStore = useEnvStore();
// 模板内仍以 sandboxEnabled 名称访问, 保持原 UI 逻辑不变
const sandboxEnabled = computed(() => envStore.sandboxEnabled);
/**
* 获取当前激活的环境值
@@ -95,17 +98,10 @@
}
/**
* 加载全局沙箱环境开关状态
* 加载全局沙箱环境开关状态(复用全局 store, 避免重复请求)
*/
function loadSandboxEnabled() {
PayEnvApi.sandboxEnabled()
.then(({ data }) => {
sandboxEnabled.value = !!data;
})
.catch(() => {
// 查询失败按关闭处理, 仅展示生产环境
sandboxEnabled.value = false;
});
envStore.loadSandboxEnabled();
}
onMounted(() => {
@@ -125,6 +121,15 @@
</div>
</template>
<!-- 沙箱模式提示: 仅在全局沙箱开关开启时显示 -->
<div v-if="sandboxEnabled" class="mb-4">
<a-alert
:message="$t('payment.common.env.sandboxProductConfigTip')"
type="warning"
show-icon
/>
</div>
<a-spin :spinning="loading">
<div v-if="productList.length === 0 && !loading" class="flex items-center justify-center empty-container">
<a-empty :description="$t('payment.constant.product.productConfig.emptyDesc')" />
@@ -177,7 +182,10 @@
>
<div
class="config-slot prod-slot"
:class="[{ 'border-r border-border': row.sandboxSupport && sandboxEnabled }, { 'config-slot-disabled': !row.isv }]"
:class="[
{ 'border-r border-border': row.sandboxSupport && sandboxEnabled },
{ 'config-slot-disabled': !row.isv },
]"
@click.stop="row.isv && openDetailPage(row, false)"
>
<div class="flex items-center gap-1.5">