|
|
@@ -0,0 +1,482 @@
|
|
|
+<template>
|
|
|
+ <!-- 单根节点:AppMain 的 <transition mode="out-in"> 要求组件根为元素,
|
|
|
+ 多根 fragment 会导致切换动画无法完成、后续路由挂载卡死 -->
|
|
|
+ <div class="unified-merchant">
|
|
|
+ <el-card>
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>商户配置</span>
|
|
|
+ <!-- 只有超管能建号:判据用的是 sys_user.is_superuser,与后端
|
|
|
+ @PreAuthorize("@perm.hasRole('SUPERUSER')") 是同一个 —— 不要改用
|
|
|
+ v-hasPerm(那判的是角色授权,与端点实际校验的身份不是一回事)。
|
|
|
+ spec §9.5 明确不为 save/delete 造权限点。 -->
|
|
|
+ <el-button v-if="isSuperAdmin" type="primary" icon="Plus" @click="openCreate">
|
|
|
+ 新增配置
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <el-alert
|
|
|
+ class="mb-4"
|
|
|
+ type="info"
|
|
|
+ :closable="false"
|
|
|
+ show-icon
|
|
|
+ :title="
|
|
|
+ isSuperAdmin
|
|
|
+ ? '上游商户号与应用密钥由运营完成报备后录入;商户号全局唯一,新增时请选择归属租户。编辑时密钥留空表示不修改。'
|
|
|
+ : '这里显示的是本租户已配置的上游商户号(只读)。如需新增或修改,请联系平台运营。'
|
|
|
+ "
|
|
|
+ />
|
|
|
+
|
|
|
+ <div class="mb-4">
|
|
|
+ <el-form :inline="true" :model="searchForm">
|
|
|
+ <!-- 租户筛选只对超管有意义:非超管的查询被后端按租户钉死,只会返回自己的行 -->
|
|
|
+ <el-form-item v-if="isSuperAdmin" label="租户">
|
|
|
+ <el-select
|
|
|
+ v-model="searchForm.tenant_id"
|
|
|
+ placeholder="全部租户"
|
|
|
+ clearable
|
|
|
+ filterable
|
|
|
+ style="width: 180px"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="t in tenantList"
|
|
|
+ :key="t.id"
|
|
|
+ :label="t.name || String(t.id)"
|
|
|
+ :value="t.id"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="商户号">
|
|
|
+ <el-input
|
|
|
+ v-model="searchForm.mch_no"
|
|
|
+ placeholder="商户号"
|
|
|
+ clearable
|
|
|
+ style="width: 180px"
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="商户名称">
|
|
|
+ <el-input
|
|
|
+ v-model="searchForm.merchant_name"
|
|
|
+ placeholder="商户名称"
|
|
|
+ clearable
|
|
|
+ style="width: 180px"
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="状态">
|
|
|
+ <el-select
|
|
|
+ v-model="searchForm.status"
|
|
|
+ placeholder="全部状态"
|
|
|
+ clearable
|
|
|
+ style="width: 120px"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="s in STATUS_OPTIONS"
|
|
|
+ :key="s.value"
|
|
|
+ :label="s.label"
|
|
|
+ :value="s.value"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" @click="handleSearch">查询</el-button>
|
|
|
+ <el-button @click="handleReset">重置</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-table v-loading="loading" :data="list" border stripe>
|
|
|
+ <template #empty>
|
|
|
+ <el-empty description="暂无数据" />
|
|
|
+ </template>
|
|
|
+ <!-- 租户列只对超管有意义:非超管的查询被后端按租户钉死,整列都是同一个值 -->
|
|
|
+ <el-table-column v-if="isSuperAdmin" label="租户" min-width="140">
|
|
|
+ <template #default="{ row }">{{ tenantName(row.tenant_id) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="mch_no" label="商户号" min-width="160" />
|
|
|
+ <el-table-column label="商户名称" min-width="160">
|
|
|
+ <template #default="{ row }">{{ row.merchant_name || "-" }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="app_id" label="应用 ID" min-width="200" show-overflow-tooltip />
|
|
|
+ <el-table-column label="IfCode" width="130">
|
|
|
+ <template #default="{ row }">{{ row.channel_user_if_code || "-" }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="状态" width="90">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="row.status === '0' ? 'success' : 'info'">
|
|
|
+ {{ row.status === "0" ? "启用" : "停用" }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="更新时间" width="170">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.updated_time ? dayjs(row.updated_time).format("YYYY-MM-DD HH:mm:ss") : "-" }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="170" fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button v-if="isSuperAdmin" size="small" type="primary" @click="openEdit(row)">
|
|
|
+ 编辑
|
|
|
+ </el-button>
|
|
|
+ <el-button v-if="isSuperAdmin" size="small" type="danger" @click="handleDelete(row)">
|
|
|
+ 删除
|
|
|
+ </el-button>
|
|
|
+ <span v-if="!isSuperAdmin" class="readonly-hint">只读</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <div class="mt-4 flex justify-end">
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="pageNo"
|
|
|
+ v-model:page-size="pageSize"
|
|
|
+ :total="total"
|
|
|
+ :page-sizes="[10, 20, 50, 100]"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ @size-change="load"
|
|
|
+ @current-change="load"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <el-dialog
|
|
|
+ v-model="dialogVisible"
|
|
|
+ :title="form.id ? '编辑商户配置' : '新增商户配置'"
|
|
|
+ width="600px"
|
|
|
+ @closed="handleDialogClosed"
|
|
|
+ >
|
|
|
+ <el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
|
|
+ <!-- 只新增时显示:归属不可改(后端修改分支忽略 tenant_id),
|
|
|
+ 旧数据即使没有 tenant_id 也不该挡住改备注 -->
|
|
|
+ <el-form-item v-if="!form.id" label="租户" prop="tenant_id">
|
|
|
+ <el-select
|
|
|
+ v-model="form.tenant_id"
|
|
|
+ placeholder="选择该配置归属的租户"
|
|
|
+ filterable
|
|
|
+ style="width: 100%"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="t in tenantList"
|
|
|
+ :key="t.id"
|
|
|
+ :label="t.name || String(t.id)"
|
|
|
+ :value="t.id"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="商户号" prop="mch_no">
|
|
|
+ <el-input
|
|
|
+ v-model="form.mch_no"
|
|
|
+ placeholder="上游分配的唯一商户号"
|
|
|
+ maxlength="64"
|
|
|
+ show-word-limit
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="应用 ID" prop="app_id">
|
|
|
+ <el-input v-model="form.app_id" placeholder="上游分配的应用 ID" maxlength="64" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="应用密钥" prop="app_secret">
|
|
|
+ <!-- ★ 永不回显:响应里没有 app_secret,编辑时这里**始终是空的**。
|
|
|
+ 留空 = 保留原值(后端按 blank 判断),不是「清空密钥」。
|
|
|
+ autocomplete=new-password 防止浏览器把登录密码/旧密钥填进来。 -->
|
|
|
+ <el-input
|
|
|
+ v-model="form.app_secret"
|
|
|
+ type="password"
|
|
|
+ autocomplete="new-password"
|
|
|
+ :placeholder="secretPlaceholder"
|
|
|
+ maxlength="128"
|
|
|
+ />
|
|
|
+ <div v-if="form.id" class="form-hint">
|
|
|
+ {{ editingSecretSet ? "已配置,留空不修改" : "尚未配置密钥,请填写" }}
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="商户名称" prop="merchant_name">
|
|
|
+ <el-input v-model="form.merchant_name" maxlength="128" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="渠道 IfCode" prop="channel_user_if_code">
|
|
|
+ <el-select v-model="form.channel_user_if_code" style="width: 100%">
|
|
|
+ <el-option
|
|
|
+ v-for="o in CHANNEL_USER_IF_CODE_OPTIONS"
|
|
|
+ :key="o.value"
|
|
|
+ :label="o.label"
|
|
|
+ :value="o.value"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="状态" prop="status">
|
|
|
+ <el-radio-group v-model="form.status">
|
|
|
+ <el-radio v-for="s in STATUS_OPTIONS" :key="s.value" :value="s.value">
|
|
|
+ {{ s.label }}
|
|
|
+ </el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="备注" prop="description">
|
|
|
+ <el-input v-model="form.description" type="textarea" :rows="3" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="dialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" :loading="saving" @click="handleSubmit">保存</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+// ★ 与 sys_menu.route_name 逐字一致(`db/unified-menu.sql`):
|
|
|
+// keep-alive 的 include 匹配的是组件名,不写这一行会按文件名推成 "index",
|
|
|
+// `keep_alive=true` 静默失效(乐收赢先例 bf09016)。
|
|
|
+defineOptions({
|
|
|
+ name: "payment-unified-merchant",
|
|
|
+ inheritAttrs: false,
|
|
|
+});
|
|
|
+
|
|
|
+import { computed, onMounted, reactive, ref } from "vue";
|
|
|
+import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from "element-plus";
|
|
|
+import dayjs from "dayjs";
|
|
|
+import UnifiedAPI, {
|
|
|
+ type UnifiedMerchantSaveParams,
|
|
|
+ type UnifiedMerchantVO,
|
|
|
+} from "@/api/module_payment/unified";
|
|
|
+import TenantAPI, { type TenantTable } from "@/api/module_system/tenant";
|
|
|
+import { useUserStore } from "@/store";
|
|
|
+
|
|
|
+const userStore = useUserStore();
|
|
|
+
|
|
|
+/**
|
|
|
+ * 超管判据 = `sys_user.is_superuser`,与后端 `@perm.hasRole('SUPERUSER')` 同一个来源。
|
|
|
+ *
|
|
|
+ * ★ 不要换成 `v-hasPerm`:那读的是角色/菜单权限串,与端点实际校验的**身份**不是一回事。
|
|
|
+ * spec §9.5 也明确不建 `merchant:save` / `merchant:delete` 权限行。
|
|
|
+ */
|
|
|
+const isSuperAdmin = computed(() => userStore.basicInfo.is_superuser === true);
|
|
|
+
|
|
|
+/** 状态枚举:商户配置行是字符串 "0" 启用 / "1" 停用(DDL 默认值 "0") */
|
|
|
+const STATUS_OPTIONS = [
|
|
|
+ { value: "0", label: "启用" },
|
|
|
+ { value: "1", label: "停用" },
|
|
|
+];
|
|
|
+
|
|
|
+/** 渠道用户 ID 跳转的 ifCode(spec §4.7):服务商 / 自有公众号 / 自动 */
|
|
|
+const CHANNEL_USER_IF_CODE_OPTIONS = [
|
|
|
+ { value: "fuioupay", label: "服务商模式(fuioupay)" },
|
|
|
+ { value: "wxpay", label: "自有公众号(wxpay)" },
|
|
|
+ { value: "AUTO", label: "自动(AUTO)" },
|
|
|
+];
|
|
|
+
|
|
|
+/**
|
|
|
+ * 租户下拉数据。只有超管会用到,非超管不请求(那个接口对他们也未必有权限)。
|
|
|
+ *
|
|
|
+ * ★ 类型把 `id` 收窄成**必有**:`TenantTable.id` 是可选的 `number | undefined`,
|
|
|
+ * 而 `el-option` 的 `value` 不收 `undefined`(vue-tsc TS2322)。
|
|
|
+ * 下拉里出现一个选不中的租户在赋值那一刻就不成立(下面 onMounted 里过滤)。
|
|
|
+ */
|
|
|
+const tenantList = ref<(TenantTable & { id: number })[]>([]);
|
|
|
+
|
|
|
+const tenantName = (id?: number) => {
|
|
|
+ if (id === undefined || id === null) {
|
|
|
+ return "-";
|
|
|
+ }
|
|
|
+ const t = tenantList.value.find((x) => x.id === id);
|
|
|
+ return t ? t.name || String(id) : String(id);
|
|
|
+};
|
|
|
+
|
|
|
+const list = ref<UnifiedMerchantVO[]>([]);
|
|
|
+const total = ref(0);
|
|
|
+const pageNo = ref(1);
|
|
|
+const pageSize = ref(20);
|
|
|
+const loading = ref(false);
|
|
|
+
|
|
|
+const dialogVisible = ref(false);
|
|
|
+const saving = ref(false);
|
|
|
+const formRef = ref<FormInstance>();
|
|
|
+/** 编辑中的那一行是否已配置密钥 —— 只用于提示文案,密钥本身永远不回传/不回显 */
|
|
|
+const editingSecretSet = ref(false);
|
|
|
+
|
|
|
+const searchForm = reactive({
|
|
|
+ tenant_id: undefined as number | undefined,
|
|
|
+ mch_no: "",
|
|
|
+ merchant_name: "",
|
|
|
+ status: undefined as string | undefined,
|
|
|
+});
|
|
|
+
|
|
|
+const defaultForm = (): UnifiedMerchantSaveParams => ({
|
|
|
+ id: undefined,
|
|
|
+ tenant_id: undefined,
|
|
|
+ mch_no: "",
|
|
|
+ app_id: "",
|
|
|
+ app_secret: "",
|
|
|
+ merchant_name: "",
|
|
|
+ channel_user_if_code: "fuioupay",
|
|
|
+ status: "0",
|
|
|
+ description: "",
|
|
|
+});
|
|
|
+
|
|
|
+const form = reactive<UnifiedMerchantSaveParams>(defaultForm());
|
|
|
+
|
|
|
+/**
|
|
|
+ * 密钥提示:新增必填;编辑时「已配置,留空不修改」/「尚未配置,请填写」。
|
|
|
+ * ★ 这里只说状态,不提供查看原密钥的入口 —— 响应里根本没有这个字段。
|
|
|
+ */
|
|
|
+const secretPlaceholder = computed(() => {
|
|
|
+ if (!form.id) {
|
|
|
+ return "请输入上游应用密钥";
|
|
|
+ }
|
|
|
+ return editingSecretSet.value ? "已配置,留空不修改" : "未配置,请填写";
|
|
|
+});
|
|
|
+
|
|
|
+/**
|
|
|
+ * 租户只在**新增**时必填:后端对修改一律忽略 tenant_id(归属不可改),
|
|
|
+ * 编辑态那一格整个不渲染,校验它没有意义。
|
|
|
+ * 新增时 app_secret 必填(后端要求 tenant_id/mch_no/app_id/app_secret)。
|
|
|
+ */
|
|
|
+const rules = computed<FormRules>(() => ({
|
|
|
+ tenant_id: form.id ? [] : [{ required: true, message: "请选择租户", trigger: "change" }],
|
|
|
+ mch_no: [{ required: true, message: "请填写商户号", trigger: "blur" }],
|
|
|
+ app_id: [{ required: true, message: "请填写应用 ID", trigger: "blur" }],
|
|
|
+ app_secret: form.id ? [] : [{ required: true, message: "请填写应用密钥", trigger: "blur" }],
|
|
|
+}));
|
|
|
+
|
|
|
+async function load() {
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ const res = await UnifiedAPI.merchantPage({
|
|
|
+ tenant_id: searchForm.tenant_id || undefined,
|
|
|
+ mch_no: searchForm.mch_no || undefined,
|
|
|
+ merchant_name: searchForm.merchant_name || undefined,
|
|
|
+ status: searchForm.status || undefined,
|
|
|
+ page_no: pageNo.value,
|
|
|
+ page_size: pageSize.value,
|
|
|
+ });
|
|
|
+ // 后端序列化的分页键是 `list`,全局 TS 声明写的是 `items` —— 两个都读,
|
|
|
+ // 只读 items 会静默拿到空数组(HAR 商户页同一个坑)。
|
|
|
+ list.value = res.data.data?.list || res.data.data?.items || [];
|
|
|
+ total.value = res.data.data?.total || 0;
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function handleSearch() {
|
|
|
+ pageNo.value = 1;
|
|
|
+ load();
|
|
|
+}
|
|
|
+
|
|
|
+function handleReset() {
|
|
|
+ searchForm.tenant_id = undefined;
|
|
|
+ searchForm.mch_no = "";
|
|
|
+ searchForm.merchant_name = "";
|
|
|
+ searchForm.status = undefined;
|
|
|
+ handleSearch();
|
|
|
+}
|
|
|
+
|
|
|
+function openCreate() {
|
|
|
+ editingSecretSet.value = false;
|
|
|
+ Object.assign(form, defaultForm());
|
|
|
+ dialogVisible.value = true;
|
|
|
+}
|
|
|
+
|
|
|
+function openEdit(row: UnifiedMerchantVO) {
|
|
|
+ editingSecretSet.value = row.app_secret_set === true;
|
|
|
+ // ★ app_secret 有意不取:响应里没有它,表单里它永远是空的(留空 = 不修改)
|
|
|
+ Object.assign(form, defaultForm(), {
|
|
|
+ id: row.id,
|
|
|
+ tenant_id: row.tenant_id,
|
|
|
+ mch_no: row.mch_no,
|
|
|
+ app_id: row.app_id,
|
|
|
+ merchant_name: row.merchant_name,
|
|
|
+ channel_user_if_code: row.channel_user_if_code || "fuioupay",
|
|
|
+ status: row.status || "0",
|
|
|
+ description: row.description,
|
|
|
+ });
|
|
|
+ dialogVisible.value = true;
|
|
|
+}
|
|
|
+
|
|
|
+function handleDialogClosed() {
|
|
|
+ formRef.value?.clearValidate();
|
|
|
+ editingSecretSet.value = false;
|
|
|
+ Object.assign(form, defaultForm());
|
|
|
+}
|
|
|
+
|
|
|
+async function handleSubmit() {
|
|
|
+ const valid = await formRef.value?.validate().catch(() => false);
|
|
|
+ if (!valid) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const payload: UnifiedMerchantSaveParams = { ...form };
|
|
|
+ // 编辑时留空 = 保留原值:不发空串,语义更明确(后端两种都按 blank 处理)
|
|
|
+ if (payload.id && !payload.app_secret) {
|
|
|
+ delete payload.app_secret;
|
|
|
+ }
|
|
|
+ saving.value = true;
|
|
|
+ try {
|
|
|
+ await UnifiedAPI.merchantSave(payload);
|
|
|
+ ElMessage.success("保存成功");
|
|
|
+ dialogVisible.value = false;
|
|
|
+ load();
|
|
|
+ } finally {
|
|
|
+ saving.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function handleDelete(row: UnifiedMerchantVO) {
|
|
|
+ const owner = isSuperAdmin.value ? `(租户 ${tenantName(row.tenant_id)})` : "";
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(
|
|
|
+ `删除后该商户号将无法下单。若已有交易,后端会拒绝删除,请改用「停用」。确定删除「${row.mch_no}」${owner}?`,
|
|
|
+ "提示",
|
|
|
+ { type: "warning" }
|
|
|
+ );
|
|
|
+ } catch {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ await UnifiedAPI.merchantDelete(row.id as string);
|
|
|
+ } catch {
|
|
|
+ // 拦截器已经用后端原话弹过「已有交易,不能删除,请停用」(400),
|
|
|
+ // 这里不再叠一条 toast;列表保持不变,运营可以改为编辑 → 停用。
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ ElMessage.success("删除成功");
|
|
|
+ load();
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ load();
|
|
|
+ if (isSuperAdmin.value) {
|
|
|
+ try {
|
|
|
+ const res = await TenantAPI.listTenant({ page_no: 1, page_size: 100 });
|
|
|
+ // 后端分页键是 `list`,全局声明写的是 `items` —— 两个都读(同 load)
|
|
|
+ const page = res.data.data as unknown as {
|
|
|
+ list?: TenantTable[];
|
|
|
+ items?: TenantTable[];
|
|
|
+ };
|
|
|
+ tenantList.value = (page?.list || page?.items || []).filter(
|
|
|
+ (t): t is TenantTable & { id: number } => typeof t.id === "number"
|
|
|
+ );
|
|
|
+ } catch {
|
|
|
+ // 租户列表加载失败不阻塞列表查询(租户列退化成显示 tenant_id 数字)
|
|
|
+ }
|
|
|
+ }
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.card-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.form-hint {
|
|
|
+ margin-top: 2px;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.5;
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
+}
|
|
|
+
|
|
|
+.readonly-hint {
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
+}
|
|
|
+</style>
|