t18:前端代录与导入——管理员视图两个新动作接进同一条 submit 管线
- Fill.vue 加代录态:?proxy=1 且该表单在 managed 清单里才画「被记录人」选择器(复用 UserCheckModal + GetSchoolAll 本校组织树),提交体带 subjectId。三条口径写在注释里:proxy 只是界面开关、判权在后端 requireSubject(非 ALL 档带字段即 403);subjectId 是被写入数据的字段而不是操作者身份(V17 不变); 代录态不取也不显示"我的提交"段(那是本人视角的历史,混进来会让人以为已经给这个人记过)。 - AppContext 管理员视图每行加「添加(代录)」与「导入」;导入弹窗组件 components/admin/ImportPanel.vue: 下载模板 → 选文件 → 逐行结果表(行号/结果/答卷ID/原因)+ 成功失败计数。整批拒(不是这张表的模板、 label 重复、超行数上限)与坏行原因一律按后端 msg 原样亮出,前端不复制规则(同 AdminListPanel 口径)。 - api/module/data.ts 加 import(multipart,formUuid 走 query)与 importTemplateBlob(原生 fetch: 文件流不能过 alova 的 response.json() 拦截器,理由同导出那条)。types/data.ts 加 ImportResult/ImportRowResult。 - api/module/anon.ts 加开发期会话开关:仅 conf.isMock(build 产物恒 false)时,地址带 mockSession=0 即判"无会话"——否则 mock 的 GetUserInfo 永远返回 code=0,匿名通道在 mock 面演不出来(V67 前半句要实拍)。 门禁:npx vue-tsc --noEmit 0 错(s7/tsc-2.log);npm run build ✓ built in 20.99s(s7/build-1.log)。
This commit is contained in:
+10
-1
@@ -48,8 +48,17 @@ async function probe<T>(path: string): Promise<Envelope<T>> {
|
||||
}
|
||||
}
|
||||
|
||||
/** 探针 1(C15 第一跳):当前浏览器有没有 CAS 会话。非 0(含 302/网络错)一律按"无会话"回答 */
|
||||
/**
|
||||
* 探针 1(C15 第一跳):当前浏览器有没有 CAS 会话。非 0(含 302/网络错)一律按"无会话"回答。
|
||||
*
|
||||
* mock 面的开发期开关(`conf.isMock` 在 build 产物里恒 false,生产链路一个字都不走这支):
|
||||
* 地址带 `mockSession=0` 即视为无会话——否则 mock 的 GetUserInfo 永远返回 code=0,
|
||||
* 匿名通道在 mock 面就演不出来(V67 要的"匿名表单 page=直接可填"需要能在本机实拍)。
|
||||
*/
|
||||
export async function probeSession(): Promise<SessionProbe> {
|
||||
if (conf.isMock && window.location.href.includes("mockSession=0")) {
|
||||
return { logged: false, user: null };
|
||||
}
|
||||
const envelope = await probe("/api/base/GetUserInfo");
|
||||
return { logged: envelope.code === 0, user: envelope.code === 0 ? envelope.data ?? null : null };
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
AnswerDetail,
|
||||
BatchItemResult,
|
||||
FilterField,
|
||||
ImportResult,
|
||||
ListRequest,
|
||||
ListView,
|
||||
QueryRequest,
|
||||
@@ -93,10 +94,53 @@ class DataApi {
|
||||
params: paramsOf({ ...scope })
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量导入(契约 v2 C14,T18):multipart xlsx,逐行走提交管线,响应给逐行结果(坏行不静默)。
|
||||
* 守卫与导出同一把(该表单 ALL 档),`formUuid` 走 query 而不是 body——它是请求的寻址件不是数据。
|
||||
*/
|
||||
import(formUuid: string, file: File, scope: Scope = {}) {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
return alovaInstance.Post<ImportResult>(`${BASE}/import`, formData, {
|
||||
params: paramsOf({ ...scope, formUuid }),
|
||||
headers: { "Content-Type": "multipart/form-data" }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const dataApi = new DataApi();
|
||||
|
||||
/**
|
||||
* 导入模板下载(C14,T18)。与 {@link exportBlob} 同一支原生 fetch:这两个端点都直接写
|
||||
* OutputStream、不套 JSON 信封,本仓的响应拦截器对每个响应都 `response.json()`,文件流过它必报错。
|
||||
*/
|
||||
export async function importTemplateBlob(formUuid: string, scope: Scope = {}): Promise<Blob> {
|
||||
const query = new URLSearchParams({ formUuid });
|
||||
if (scope.schId !== undefined && scope.schId !== null) query.set("schId", String(scope.schId));
|
||||
const url = `${conf.serviceHost}${BASE}/import-template?${query.toString()}`;
|
||||
const headers: Record<string, string> = {
|
||||
yschapirequest: "1",
|
||||
yschreferer: Utils.strToHexCharCode(window.location.href)
|
||||
};
|
||||
const currentRole = localStorage.getItem("currentRole");
|
||||
if (currentRole) headers.currentRole = currentRole;
|
||||
|
||||
const response = await fetch(url, { method: "GET", headers, credentials: "include" });
|
||||
const type = response.headers.get("content-type") || "";
|
||||
if (!response.ok || type.includes("application/json")) {
|
||||
let msg = "模板下载失败";
|
||||
try {
|
||||
const env = await response.json();
|
||||
msg = env?.msg || msg;
|
||||
} catch {
|
||||
/* 响应体不是 JSON 时保持默认文案 */
|
||||
}
|
||||
throw new Error(msg);
|
||||
}
|
||||
return await response.blob();
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出(xlsx 流式同步下载,方案 §5.5 铁律④:不设行数上限、不做异步任务与下载中心)。
|
||||
*
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 批量导入面板(契约 v2 §5.1/C14,T18):通用壳管理员视图「导入」按钮背后的弹窗。
|
||||
*
|
||||
* 三件事:下载模板 → 选文件 → 逐行结果。四处分明:
|
||||
* 1. **导入入口只在管理员视图**(§5.1):能不能进来由该表单 ALL 档决定,守卫在后端
|
||||
* (`requireData`),这里只负责不把这个按钮画给填报者;
|
||||
* 2. **坏行不静默**:响应是逐行结果,表格里 ok/error 全摊开;整体"成功 N 行失败 M 行"
|
||||
* 不等于"文件里有 N+M 行"这件事也由行号列自证;
|
||||
* 3. **重复导入同一个文件是逐行 200+原 answerId**(C13 服务端确定性幂等键),
|
||||
* 所以"手抖点两次导入"不会产生重复数据,也不会报错——提示语里写清楚,否则用户以为没执行;
|
||||
* 4. 模板与解析共用后端那一份映射(`AnswerImportService`),前端不重述字段规则;
|
||||
* 行上限 1000 超限是整批拒(不是截断),文案按后端 msg 原样给。
|
||||
*/
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { dataApi, downloadBlob, importTemplateBlob } from "@/api/module/data";
|
||||
import type { ImportRowResult } from "@/types/data";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
/** 导入目标:数据面按 uuid 寻址(code 只作填报入口,契约 §1) */
|
||||
form: { formUuid: string; code: string; name: string; appCode?: string | null };
|
||||
}>();
|
||||
const emit = defineEmits<{ (e: "update:modelValue", value: boolean): void }>();
|
||||
|
||||
const file = ref<File | null>(null);
|
||||
const inputRef = ref<HTMLInputElement | null>(null);
|
||||
const busy = ref(false);
|
||||
const rows = ref<ImportRowResult[]>([]);
|
||||
const counts = ref<{ ok: number; fail: number } | null>(null);
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (next: boolean) => emit("update:modelValue", next)
|
||||
});
|
||||
|
||||
const columns = computed(() => [
|
||||
{ title: "行号", key: "rowNo", width: 70 },
|
||||
{
|
||||
title: "结果",
|
||||
key: "ok",
|
||||
width: 80,
|
||||
render: (h: any, p: any) =>
|
||||
h("Tag", { color: p.row.ok ? "green" : "red" }, p.row.ok ? "已写入" : "未写入")
|
||||
},
|
||||
{ title: "答卷ID", key: "answerId", minWidth: 150, ellipsis: true, tooltip: true },
|
||||
{ title: "原因", key: "error", minWidth: 220 }
|
||||
]);
|
||||
|
||||
function pick(next: Event) {
|
||||
const picked = (next.target as HTMLInputElement).files?.[0] || null;
|
||||
file.value = picked;
|
||||
rows.value = [];
|
||||
counts.value = null;
|
||||
}
|
||||
|
||||
async function downloadTemplate() {
|
||||
busy.value = true;
|
||||
try {
|
||||
const blob = await importTemplateBlob(props.form.formUuid);
|
||||
downloadBlob(blob, `import-template-${props.form.code}.xlsx`);
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || "模板下载失败");
|
||||
} finally {
|
||||
busy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function runImport() {
|
||||
if (!file.value) {
|
||||
Message.warning("先选择按模板填好的 xlsx 文件");
|
||||
return;
|
||||
}
|
||||
busy.value = true;
|
||||
try {
|
||||
const result = await dataApi.import(props.form.formUuid, file.value).send();
|
||||
rows.value = result?.rows || [];
|
||||
counts.value = { ok: result?.okCount ?? 0, fail: result?.failCount ?? 0 };
|
||||
if (counts.value.fail === 0) {
|
||||
Message.success(`导入完成:${counts.value.ok} 行已写入`);
|
||||
} else {
|
||||
Message.warning(`导入完成:${counts.value.ok} 行已写入,${counts.value.fail} 行未写入(见下表原因)`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
// 整批被拒的情形都在这里(不是这张表的模板、label 重复、行数超上限、解析失败):
|
||||
// 后端给的是可诊断文案,原样亮出来,不在前端改写
|
||||
Message.error(e?.message || "导入失败");
|
||||
} finally {
|
||||
busy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(visible, (open) => {
|
||||
if (!open) return;
|
||||
file.value = null;
|
||||
rows.value = [];
|
||||
counts.value = null;
|
||||
if (inputRef.value) inputRef.value.value = "";
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal v-model="visible" :title="`批量导入|${form.name}`" :width="760" :mask-closable="false">
|
||||
<div class="import-bar">
|
||||
<Button :loading="busy" icon="md-download" @click="downloadTemplate">下载模板</Button>
|
||||
<input ref="inputRef" type="file" accept=".xlsx" @change="pick" />
|
||||
<Button type="primary" :loading="busy" :disabled="!file" icon="md-cloud-upload" @click="runImport">
|
||||
导入
|
||||
</Button>
|
||||
</div>
|
||||
<Alert show-icon type="info" class="import-tip">
|
||||
每一行都走与手工填报<b>同一条提交管线</b>(值校验、限次、截止、幂等、索引),所以导入的数据与
|
||||
手工填的完全可比;重复导入同一个文件不会落重复行,而是逐行返回原答卷ID。
|
||||
第一列「被记录人」留空即以你本人为被记录人;填他人 ID 需要该表单的 ALL 档。
|
||||
</Alert>
|
||||
<p v-if="counts" class="import-count">
|
||||
共 {{ rows.length }} 行:已写入 {{ counts.ok }},未写入 {{ counts.fail }}
|
||||
</p>
|
||||
<Table :columns="columns" :data="rows" size="small" :border="false" max-height="320" />
|
||||
<template #footer>
|
||||
<Button @click="visible = false">关闭</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.import-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.import-tip {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.import-count {
|
||||
margin: 0 0 8px;
|
||||
font-size: 13px;
|
||||
color: #515a6e;
|
||||
}
|
||||
</style>
|
||||
@@ -191,3 +191,24 @@ export interface AppRow {
|
||||
export function scopeLabel(name: string): string {
|
||||
return DATA_SCOPE_LABELS[name] ?? name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量导入的逐行结果(契约 v2 C14,T18,`POST /api/form/admin/data/import`)。
|
||||
*
|
||||
* 失败原因直接是提交管线那句话(导入侧不改写),所以"这一行为什么没进去"在界面上与手工填报报错同文;
|
||||
* 重复导入同一个文件时该行仍是 ok 且带<b>原</b> answerId(C13 的确定性幂等键)。
|
||||
*/
|
||||
export interface ImportRowResult {
|
||||
/** xlsx 里的物理行号(与用户在 Excel 里看到的行号一致,可直接指到那一格) */
|
||||
rowNo: number;
|
||||
ok: boolean;
|
||||
answerId?: string | null;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
/** 导入响应(C14 的闭集:rows + 两个计数;行上限 1000 由服务端整批拒) */
|
||||
export interface ImportResult {
|
||||
rows: ImportRowResult[];
|
||||
okCount: number;
|
||||
failCount: number;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useRoute, useRouter } from "vue-router";
|
||||
import { listFillableForms, listManagedForms } from "@/api/module/fill";
|
||||
import { fillRouteOf } from "@/utils/fillLink";
|
||||
import Data from "@/views/admin/Data.vue";
|
||||
import ImportPanel from "@/components/admin/ImportPanel.vue";
|
||||
import type { FillableForm, ManagedForm } from "@/types/data";
|
||||
import { Message } from "view-ui-plus";
|
||||
|
||||
@@ -85,6 +86,25 @@ function goFill(row: FillableForm) {
|
||||
router.push(fillRouteOf(row));
|
||||
}
|
||||
|
||||
/**
|
||||
* 代录入口(T18):进同一张填报页,带 {@code proxy=1} 让它把"被记录人"选择器画出来。
|
||||
* 这个 query 只是界面开关——<b>能不能真的挂到别人名下完全由后端判</b>(该表单 ALL 档,
|
||||
* {@code AnswerService#requireSubject}),非档者即使手搓请求体也是 403。
|
||||
*/
|
||||
function goProxy(row: ManagedForm) {
|
||||
const target = fillRouteOf(row);
|
||||
router.push({ path: target.path, query: { ...target.query, proxy: "1" } });
|
||||
}
|
||||
|
||||
/** 导入弹窗的目标行(§5.1:导入入口只存在于管理员视图) */
|
||||
const importTarget = ref<ManagedForm | null>(null);
|
||||
const importVisible = ref(false);
|
||||
|
||||
function openImport(row: ManagedForm) {
|
||||
importTarget.value = row;
|
||||
importVisible.value = true;
|
||||
}
|
||||
|
||||
function fillPathText(row: FillableForm): string {
|
||||
const target = fillRouteOf(row);
|
||||
return `${location.origin}${location.pathname}#${target.path}${target.query.appCode ? `?appCode=${target.query.appCode}` : ""}`;
|
||||
@@ -135,22 +155,33 @@ watch(appCode, () => loadAll());
|
||||
|
||||
<h5 class="section-title">{{ currentTitle }}</h5>
|
||||
|
||||
<!-- 管理员视图:数据列表(内嵌 Data,候选=managed 的 code)+ 复制填报路径。
|
||||
「添加(代录)」与「导入」在 T18 接进这一档(同一条 submit 管线、同一 ALL 档守卫)。 -->
|
||||
<!-- 管理员视图:数据列表(内嵌 Data,候选=managed 的 code)+ 复制填报路径
|
||||
+ 添加(代录)+ 导入(T18 接进这一档:同一条 submit 管线、同一 ALL 档守卫)。 -->
|
||||
<template v-if="activeView === 'data'">
|
||||
<div class="manage-rows">
|
||||
<div v-for="row in managedRows" :key="row.code" class="manage-row">
|
||||
<div class="manage-main">
|
||||
<div class="manage-name">{{ row.name }}</div>
|
||||
<div class="manage-name">
|
||||
{{ row.name }}
|
||||
<Tag v-if="row.status !== 1" color="warning" size="small">
|
||||
{{ row.status === 0 ? "草稿" : "已停用" }}
|
||||
</Tag>
|
||||
</div>
|
||||
<code class="manage-path">{{ fillPathText(row) }}</code>
|
||||
<span class="toolbar-note">
|
||||
来源:{{ (row.role || []).map((r) => (r === 'creator' ? '创建人' : '表单管理员')).join('、') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="manage-ops">
|
||||
<Button size="small" icon="md-copy" @click="copyFillPath(row)">复制填报路径</Button>
|
||||
<Button size="small" icon="md-person-add" @click="goProxy(row)">添加(代录)</Button>
|
||||
<Button size="small" icon="md-cloud-upload" :disabled="row.status !== 1"
|
||||
@click="openImport(row)">导入</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Data v-if="adminCodes.length" context="app" :app-code="appCode" :codes="adminCodes" embedded />
|
||||
<ImportPanel v-if="importTarget" v-model="importVisible" :form="importTarget" />
|
||||
</template>
|
||||
<template v-else-if="activeView === 'fill'">
|
||||
<!-- 单表单直落:只有一个可填项时不叠列表,内容区直接给这张表的填报入口 -->
|
||||
@@ -277,6 +308,12 @@ watch(appCode, () => loadAll());
|
||||
font-weight: 600;
|
||||
color: #17233d;
|
||||
}
|
||||
.manage-ops {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.manage-path {
|
||||
display: inline-block;
|
||||
margin: 2px 8px 2px 0;
|
||||
|
||||
+54
-1
@@ -19,11 +19,15 @@ import type { DesignField } from "@/types/formSchema";
|
||||
import type { ResolvedForm, MyAnswerItem, SubmitRequest } from "@/types/answer";
|
||||
import { resolveForm, submitAnswer, listMyAnswers, deleteMyAnswer, editMyAnswer, getMyAnswerDetail } from "@/api/module/fill";
|
||||
import { probeSession, probeAnonForm, submitAnonAnswer, anonUploadPath } from "@/api/module/anon";
|
||||
import { baseApi } from "@/api/module/base";
|
||||
import UserCheckModal from "@/components/UserCheckModal.vue";
|
||||
import { CENTER_APP_CODE, managesForm, useDefaultStore } from "@/store";
|
||||
import { startHeightReporting, reportSubmitResult, reportNeedLogin } from "@/utils/embed";
|
||||
import Utils from "@/utils";
|
||||
import { Message, Modal } from "view-ui-plus";
|
||||
|
||||
const route = useRoute();
|
||||
const store = useDefaultStore();
|
||||
|
||||
// ==================== mode 闭集(契约 §7) ====================
|
||||
|
||||
@@ -80,7 +84,10 @@ async function loadForm() {
|
||||
if ((await probeSession()).logged) {
|
||||
channel.value = "session";
|
||||
form.value = await resolveForm(code.value, queryAppCode);
|
||||
await loadMyAnswers();
|
||||
// 代录态不取"我的提交":那一段是本人视角的历史,代录的是别人的数据,混在一起会让人以为
|
||||
// 自己已经给这个人记过一次(后端限次键按 coalesce(subject, submitter) 判,也不能靠界面猜)
|
||||
if (String(route.query.proxy ?? "") === "1") myAnswers.value = [];
|
||||
else await loadMyAnswers();
|
||||
return;
|
||||
}
|
||||
const anonymous = await probeAnonForm(code.value, queryAppCode);
|
||||
@@ -114,6 +121,29 @@ function guideToLogin() {
|
||||
window.location.href = "/cas/login?service=" + encodeURIComponent(window.location.href);
|
||||
}
|
||||
|
||||
// ==================== 代录(T18,§5.1 管理员视图「添加」的落点) ====================
|
||||
|
||||
/**
|
||||
* {@code ?proxy=1} 只是<b>界面开关</b>:它让这一页画出"被记录人"选择器,并在提交体里带 {@code subjectId}。
|
||||
* 真正的把关完全在后端——{@code AnswerService#requireSubject} 按"该表单 ALL 档"判,非档者带了也是 403;
|
||||
* 而 {@code subjectId} 从来不是"操作者身份"(操作者身份仍由会话推导,V17 不变),它是被写入数据的一个字段。
|
||||
*/
|
||||
const proxyMode = computed(() => String(route.query.proxy ?? "") === "1" && channel.value === "session");
|
||||
const canProxy = computed(() =>
|
||||
proxyMode.value && managesForm(store.managed, appCode.value || CENTER_APP_CODE, code.value)
|
||||
);
|
||||
|
||||
const subject = ref<{ id: number; name: string } | null>(null);
|
||||
const checkRef = ref<InstanceType<typeof UserCheckModal> | null>(null);
|
||||
/** 选人数据源=本校组织树(与管理名单同一件,跨校的人根本选不出来,P33 同一条) */
|
||||
const subjectSources = [{ name: "本校组织", loader: () => baseApi.getSchoolAll().send() }];
|
||||
|
||||
function onSubjectPicked(users: any[]) {
|
||||
const first = (users || []).find((u) => u && u.id != null);
|
||||
if (!first) return;
|
||||
subject.value = { id: Number(first.id), name: String(first.name || "").trim() || `用户 ${first.id}` };
|
||||
}
|
||||
|
||||
// ==================== 字段渲染与校验 ====================
|
||||
|
||||
const formValues = ref<Record<string, any>>({});
|
||||
@@ -190,6 +220,9 @@ async function handleSubmit() {
|
||||
formCode: code.value,
|
||||
appCode: appCode.value || undefined,
|
||||
idempotencyKey,
|
||||
// 代录:subject=被记录人(后端按该表单 ALL 档把关,非档者带这个字段就是 403);
|
||||
// 没选人就不带——"以我自己为被记录人"与手工填报同一条路径
|
||||
subjectId: canProxy.value ? subject.value?.id : undefined,
|
||||
values: formValues.value
|
||||
};
|
||||
// 两条通道同一条请求形状(匿名体里没有 subjectId,服务端也不读身份字段——C8/V17)
|
||||
@@ -376,6 +409,20 @@ const readonlyReason = computed(() => {
|
||||
<div v-else class="form-content">
|
||||
<h2 class="form-title">{{ form.name }}</h2>
|
||||
|
||||
<!-- 代录条(?proxy=1 且这张表在你名下是 ALL 档时才画;判权在后端,这里只是界面开关) -->
|
||||
<Alert v-if="canProxy" show-icon type="warning" class="readonly-tip">
|
||||
代录模式:这条记录挂到「被记录人」名下,提交人仍是你本人(不选人即以你自己为被记录人)。
|
||||
<Button size="small" type="primary" ghost @click="checkRef?.show()">
|
||||
{{ subject ? "重新选人" : "选择被记录人" }}
|
||||
</Button>
|
||||
<span v-if="subject" class="proxy-subject">
|
||||
{{ subject.name }}
|
||||
<a href="javascript:;" @click="subject = null">清除</a>
|
||||
</span>
|
||||
</Alert>
|
||||
<UserCheckModal ref="checkRef" :sources="subjectSources" :userids="subject ? [subject.id] : []"
|
||||
@on-submit="onSubjectPicked" />
|
||||
|
||||
<!-- 只读态提示 -->
|
||||
<Alert v-if="isReadonly" show-icon type="info" class="readonly-tip">
|
||||
{{ readonlyReason }},当前为只读模式
|
||||
@@ -496,6 +543,12 @@ const readonlyReason = computed(() => {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.proxy-subject {
|
||||
margin-left: 10px;
|
||||
font-weight: 600;
|
||||
color: #17233d;
|
||||
}
|
||||
|
||||
.success-state {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user