新增全局协作台账并统一工作台管理界面
新增与项目平级的协作台账,提供空白模板、独立账号授权和在线编辑。 复用共享库的访问权限与历史版本界面,拆分信息维护入口并统一工作台卡片样式。 补充在线文档字体构建支持、数据库表结构升级和回归测试。 不包含本地台账内容、账号授权记录或历史文件迁移数据。
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<CollaborationHistoryDialog :file="ledger" :target="{ scope: 'ledger' }"
|
||||
@close="emit('close')" @changed="emit('changed')" @updated="emit('updated', $event)" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import CollaborationHistoryDialog from "../collaboration/CollaborationHistoryDialog.vue";
|
||||
import type { Ledger } from "../../types/ledger";
|
||||
import type { CollaborationHistoryFile } from "../../api/collaborationHistory";
|
||||
|
||||
const { ledger } = defineProps<{ ledger: Ledger }>();
|
||||
const emit = defineEmits<{ close: []; changed: []; updated: [file: CollaborationHistoryFile] }>();
|
||||
</script>
|
||||
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<el-dialog :model-value="true" title="信息维护" width="560px" class="ledger-info-dialog"
|
||||
append-to-body align-center :close-on-click-modal="false" @close="emit('close')">
|
||||
<template #header="{ titleId }">
|
||||
<div class="ledger-info-header">
|
||||
<span class="ledger-info-header__icon" aria-hidden="true"><el-icon><EditPen /></el-icon></span>
|
||||
<h2 :id="titleId">信息维护</h2>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="error" class="ledger-error" role="alert">{{ error }} <el-button link @click="refresh">重试</el-button></div>
|
||||
<el-form id="ledger-info-form" v-loading="loading" class="ledger-info-form" label-position="top"
|
||||
:disabled="busy || loading || !current.can_manage" @submit.prevent="save">
|
||||
<el-form-item label="台账名称" required>
|
||||
<el-input v-model="form.title" maxlength="240" aria-label="台账名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用途说明">
|
||||
<template #label>用途说明<span class="ledger-field-optional">选填</span></template>
|
||||
<el-input v-model="form.description" type="textarea" maxlength="500" :rows="3"
|
||||
show-word-limit resize="vertical" aria-label="用途说明" />
|
||||
</el-form-item>
|
||||
<div class="ledger-info-details">
|
||||
<el-form-item label="所有者" class="ledger-info-owner">
|
||||
<div class="ledger-owner-identity">
|
||||
<el-avatar :size="32" class="ledger-owner-avatar" aria-hidden="true">{{ ownerInitial }}</el-avatar>
|
||||
<span class="ledger-owner-name" :title="current.owner_name">{{ current.owner_name || '—' }}</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="台账状态" class="ledger-info-status">
|
||||
<el-radio-group v-model="form.status" class="ledger-status-options" aria-label="台账状态">
|
||||
<el-radio-button value="ACTIVE">使用中</el-radio-button>
|
||||
<el-radio-button value="ARCHIVED">已归档(只读)</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button :disabled="busy" @click="emit('close')">取消</el-button>
|
||||
<el-button type="primary" :loading="busy" :disabled="loading || !current.can_manage"
|
||||
native-type="submit" form="ledger-info-form">保存信息</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { EditPen } from "@element-plus/icons-vue";
|
||||
import { fetchLedger, updateLedger } from "../../api/ledgers";
|
||||
import type { Ledger, LedgerSettings } from "../../types/ledger";
|
||||
import { getApiErrorMessage } from "../../utils/apiErrorMessage";
|
||||
|
||||
const props = defineProps<{ ledger: Ledger }>();
|
||||
const emit = defineEmits<{ close: []; changed: [] }>();
|
||||
const current = ref(props.ledger);
|
||||
const ownerInitial = computed(() => current.value.owner_name?.trim().slice(0, 1).toUpperCase() || "?");
|
||||
const toSettings = (ledger: Ledger): LedgerSettings => ({
|
||||
title: ledger.title.replace(/\.xlsx$/i, ""), description: ledger.description, status: ledger.status,
|
||||
owner_id: ledger.owner_id, allow_export: ledger.allow_export,
|
||||
allow_sheet_structure_edit: ledger.allow_sheet_structure_edit, generation: ledger.generation,
|
||||
});
|
||||
const form = ref(toSettings(props.ledger));
|
||||
const loading = ref(false);
|
||||
const busy = ref(false);
|
||||
const error = ref("");
|
||||
const refresh = async () => {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
current.value = (await fetchLedger(current.value.id)).data;
|
||||
form.value = toSettings(current.value);
|
||||
if (!current.value.can_manage) {
|
||||
emit("changed");
|
||||
emit("close");
|
||||
}
|
||||
} catch (cause) { error.value = await getApiErrorMessage(cause, "台账信息加载失败"); }
|
||||
finally { loading.value = false; }
|
||||
};
|
||||
const save = async () => {
|
||||
if (busy.value || loading.value || !current.value.can_manage) return;
|
||||
if (!form.value.title.trim()) { ElMessage.warning("请输入台账名称"); return; }
|
||||
busy.value = true;
|
||||
try {
|
||||
await ElMessageBox.confirm("此操作会使已打开的台账重新加载。请确认协作者已保存内容后继续。", "更新台账信息", {
|
||||
confirmButtonText: "继续", cancelButtonText: "取消", type: "warning",
|
||||
});
|
||||
await updateLedger(current.value.id, form.value);
|
||||
ElMessage.success("台账信息已保存");
|
||||
emit("changed");
|
||||
emit("close");
|
||||
} catch (cause) {
|
||||
if (cause !== "cancel" && cause !== "close") error.value = await getApiErrorMessage(cause, "信息保存失败,请刷新后重试");
|
||||
} finally { busy.value = false; }
|
||||
};
|
||||
onMounted(refresh);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ledger-info-header { display: flex; align-items: center; gap: 10px; padding-right: 30px; }
|
||||
.ledger-info-header__icon { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; flex-shrink: 0; border-radius: 9px; background: var(--ctms-primary-light); color: var(--ctms-primary); font-size: 17px; }
|
||||
.ledger-info-header h2 { margin: 0; color: var(--ctms-text-main); font-size: 18px; font-weight: 700; line-height: 26px; }
|
||||
.ledger-info-form :deep(.el-form-item) { margin-bottom: 18px; }
|
||||
.ledger-info-form :deep(.el-form-item__label) { height: auto; margin-bottom: 7px; color: var(--ctms-text-main); font-size: 13px; font-weight: 600; line-height: 20px; }
|
||||
.ledger-field-optional { margin-left: 7px; color: var(--ctms-text-secondary); font-size: 12px; font-weight: 400; }
|
||||
.ledger-info-form :deep(.el-input__wrapper) { min-height: 36px; padding: 0 11px; border-radius: 8px; }
|
||||
.ledger-info-form :deep(.el-input__inner) { height: 36px; color: var(--ctms-text-main); font-size: 14px; }
|
||||
.ledger-info-form :deep(.el-textarea__inner) { min-height: 88px; padding: 9px 11px 23px; border-radius: 8px; background: var(--ctms-bg-card); color: var(--ctms-text-main); font-size: 13px; line-height: 20px; }
|
||||
.ledger-info-form :deep(.el-input__count) { right: 11px; bottom: 5px; background: var(--ctms-bg-card); color: var(--ctms-text-disabled); font-size: 11px; line-height: 16px; }
|
||||
.ledger-info-details { display: grid; grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr); gap: 20px; padding-top: 16px; border-top: 1px solid var(--ctms-border-color); }
|
||||
.ledger-info-details :deep(.el-form-item) { min-width: 0; margin-bottom: 0; }
|
||||
.ledger-owner-identity { display: flex; align-items: center; gap: 9px; min-width: 0; width: 100%; min-height: 36px; }
|
||||
.ledger-owner-avatar { flex-shrink: 0; background: var(--ctms-bg-muted); color: var(--ctms-primary); font-size: 13px; font-weight: 700; }
|
||||
.ledger-owner-name { overflow: hidden; color: var(--ctms-text-regular); font-size: 13px; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ledger-status-options { display: flex; flex-wrap: nowrap; width: 100%; }
|
||||
.ledger-status-options :deep(.el-radio-button) { flex: 1; }
|
||||
.ledger-status-options :deep(.el-radio-button__inner) { display: flex; align-items: center; justify-content: center; width: 100%; height: 36px; padding: 0 10px; font-size: 12px; line-height: 18px; }
|
||||
.ledger-status-options :deep(.el-radio-button:first-child .el-radio-button__inner) { border-radius: 8px 0 0 8px; }
|
||||
.ledger-status-options :deep(.el-radio-button:last-child .el-radio-button__inner) { border-radius: 0 8px 8px 0; }
|
||||
.ledger-error { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 12px; margin-bottom: 16px; color: var(--ctms-danger); background: color-mix(in srgb, var(--ctms-danger) 8%, var(--ctms-bg-card)); border-radius: 8px; font-size: 13px; }
|
||||
:global(.ledger-info-dialog.el-dialog) { display: flex; flex-direction: column; max-width: calc(100vw - 32px); max-height: calc(100vh - 48px); max-height: calc(100dvh - 48px); padding: 0; overflow: hidden; border: 1px solid var(--ctms-border-color); border-radius: 14px; background: var(--ctms-bg-card); }
|
||||
:global(.ledger-info-dialog.el-dialog .el-dialog__header) { flex-shrink: 0; margin: 0; padding: 16px 22px; border-bottom: 1px solid var(--ctms-border-color); background: var(--ctms-bg-card); }
|
||||
:global(.ledger-info-dialog.el-dialog .el-dialog__headerbtn) { top: 16px; right: 16px; width: 32px; height: 32px; }
|
||||
:global(.ledger-info-dialog.el-dialog .el-dialog__body) { min-height: 0; padding: 20px 22px; overflow-y: auto; }
|
||||
:global(.ledger-info-dialog.el-dialog .el-dialog__footer) { display: flex; flex-shrink: 0; align-items: center; justify-content: flex-end; gap: 10px; padding: 13px 22px; border-top: 1px solid var(--ctms-border-color); background: var(--ctms-bg-muted); }
|
||||
:global(.ledger-info-dialog.el-dialog .el-dialog__footer .el-button) { height: 34px; min-width: 80px; margin-left: 0; padding: 0 16px; border-radius: 8px; font-size: 13px; }
|
||||
:global(body.is-desktop-runtime .ledger-info-dialog.el-dialog) { max-height: calc(100dvh - 96px); }
|
||||
@media (max-width: 540px) {
|
||||
.ledger-info-details { grid-template-columns: minmax(0, 1fr); gap: 16px; }
|
||||
:global(.ledger-info-dialog.el-dialog .el-dialog__header),
|
||||
:global(.ledger-info-dialog.el-dialog .el-dialog__body),
|
||||
:global(.ledger-info-dialog.el-dialog .el-dialog__footer) { padding-left: 16px; padding-right: 16px; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,63 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { flushPromises, mount } from "@vue/test-utils";
|
||||
import LedgerPanel from "./LedgerPanel.vue";
|
||||
import { fetchLedgers, initializeLedgers } from "../../api/ledgers";
|
||||
|
||||
const state = vi.hoisted(() => ({ user: { is_admin: false }, push: vi.fn() }));
|
||||
vi.mock("../../api/ledgers", () => ({ fetchLedgers: vi.fn(), initializeLedgers: vi.fn() }));
|
||||
vi.mock("../../store/auth", () => ({ useAuthStore: () => state }));
|
||||
vi.mock("vue-router", () => ({ useRouter: () => ({ push: state.push }) }));
|
||||
vi.mock("./LedgerInfoDialog.vue", () => ({ default: { template: "<div />" } }));
|
||||
vi.mock("./LedgerHistoryDialog.vue", () => ({ default: { template: "<div />" } }));
|
||||
vi.mock("../collaboration/CollaborationAccessDialog.vue", () => ({ default: { template: "<div />" } }));
|
||||
vi.mock("../../utils/apiErrorMessage", () => ({ getApiErrorMessage: async (_error: unknown, fallback: string) => fallback }));
|
||||
|
||||
const ledger = {
|
||||
id: "ledger-a", title: "医学事务部合同台账明细表.xlsx", description: "合同台账", owner_name: "负责人",
|
||||
status: "ACTIVE", can_edit: true, can_manage: false, current_revision_created_at: "2026-09-03T00:00:00Z",
|
||||
};
|
||||
const render = () => mount(LedgerPanel, { global: { stubs: {
|
||||
ElButton: { template: '<button><slot /></button>' }, ElIcon: { template: '<span><slot /></span>' },
|
||||
ElDropdown: true, ElDropdownMenu: true, ElDropdownItem: true,
|
||||
} } });
|
||||
|
||||
describe("global ledger entries", () => {
|
||||
beforeEach(() => { vi.clearAllMocks(); state.user.is_admin = false; });
|
||||
it("opens the granted ledger without needing a selected project", async () => {
|
||||
vi.mocked(fetchLedgers).mockResolvedValue({ data: [ledger] } as any);
|
||||
const wrapper = render();
|
||||
await flushPromises();
|
||||
expect(wrapper.text()).toContain("医学事务部合同台账明细表");
|
||||
expect(wrapper.text()).not.toContain("台账管理");
|
||||
await wrapper.get(".ledger-open").trigger("click");
|
||||
expect(state.push).toHaveBeenCalledWith({ name: "LedgerWorkspace", params: { fileId: "ledger-a" } });
|
||||
wrapper.unmount();
|
||||
});
|
||||
it("offers initialization only to administrators and shows an authorization empty state", async () => {
|
||||
vi.mocked(fetchLedgers).mockResolvedValue({ data: [] } as any);
|
||||
const viewer = render();
|
||||
await flushPromises();
|
||||
expect(viewer.text()).toContain("暂未获得台账访问权限");
|
||||
expect(viewer.text()).not.toContain("初始化两本台账");
|
||||
viewer.unmount();
|
||||
state.user.is_admin = true;
|
||||
vi.mocked(initializeLedgers).mockResolvedValue({ data: [ledger] } as any);
|
||||
const admin = render();
|
||||
await flushPromises();
|
||||
await admin.get("button").trigger("click");
|
||||
await flushPromises();
|
||||
expect(initializeLedgers).toHaveBeenCalledOnce();
|
||||
expect(admin.findAll(".ledger-card")).toHaveLength(1);
|
||||
admin.unmount();
|
||||
});
|
||||
it("shows a recoverable error instead of misreporting failed authorization loading as no ledgers", async () => {
|
||||
vi.mocked(fetchLedgers).mockRejectedValueOnce(new Error("offline")).mockResolvedValueOnce({ data: [ledger] } as any);
|
||||
const wrapper = render();
|
||||
await flushPromises();
|
||||
expect(wrapper.get('[role="alert"]').text()).toContain("台账加载失败");
|
||||
await wrapper.get("button").trigger("click");
|
||||
await flushPromises();
|
||||
expect(wrapper.findAll(".ledger-card")).toHaveLength(1);
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<section class="ledger-panel" aria-labelledby="ledger-heading">
|
||||
<header class="ledger-heading">
|
||||
<span class="ledger-eyebrow">Collaborative Ledgers</span>
|
||||
<div class="entry-title-row">
|
||||
<h2 id="ledger-heading">协作台账</h2>
|
||||
<EntryCountBadge v-if="ledgers.length">{{ ledgers.length }} 本可访问台账</EntryCountBadge>
|
||||
</div>
|
||||
</header>
|
||||
<div v-if="loading" class="ledger-state" role="status">正在加载台账…</div>
|
||||
<div v-else-if="error" class="ledger-state" role="alert">
|
||||
<p>{{ error }}</p><el-button @click="load">重新加载</el-button>
|
||||
</div>
|
||||
<div v-else-if="!ledgers.length" class="ledger-state">
|
||||
<p>{{ isAdmin ? '启用两本医学事务部台账,开始在线协作。' : '暂未获得台账访问权限,请联系管理员授权。' }}</p>
|
||||
<el-button v-if="isAdmin" type="primary" :loading="initializing" @click="initialize">初始化两本台账</el-button>
|
||||
</div>
|
||||
<div v-else class="ledger-grid">
|
||||
<article v-for="ledger in ledgers" :key="ledger.id" class="ledger-card">
|
||||
<div class="ledger-card-top">
|
||||
<div class="ledger-title-stack">
|
||||
<h3 :title="displayTitle(ledger.title)">{{ displayTitle(ledger.title) }}</h3>
|
||||
<p v-if="ledger.description?.trim()" class="ledger-description" :title="ledger.description">{{ ledger.description }}</p>
|
||||
</div>
|
||||
<span class="ledger-access" :class="{ 'is-readonly': !ledger.can_edit }">
|
||||
{{ ledger.status === 'ARCHIVED' ? '已归档 · 只读' : ledger.can_edit ? '可编辑' : '只读' }}
|
||||
</span>
|
||||
</div>
|
||||
<dl><div><dt>负责人</dt><dd>{{ ledger.owner_name }}</dd></div>
|
||||
<div><dt>最近保存</dt><dd>{{ formatTime(ledger.current_revision_created_at) }}</dd></div></dl>
|
||||
<footer>
|
||||
<button class="ledger-open" type="button" @click="open(ledger)">打开台账 <el-icon><ArrowRight /></el-icon></button>
|
||||
<el-dropdown v-if="ledger.can_manage" trigger="click" @command="handleManagementCommand($event, ledger)">
|
||||
<el-button link>台账管理 <el-icon><ArrowDown /></el-icon></el-button>
|
||||
<template #dropdown><el-dropdown-menu>
|
||||
<el-dropdown-item command="info">信息维护</el-dropdown-item>
|
||||
<el-dropdown-item command="access">访问与权限</el-dropdown-item>
|
||||
<el-dropdown-item command="history">历史版本</el-dropdown-item>
|
||||
</el-dropdown-menu></template>
|
||||
</el-dropdown>
|
||||
<el-button v-else link @click="openManagement('history', ledger)">历史版本</el-button>
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
<LedgerInfoDialog v-if="management?.section === 'info'" :ledger="management.ledger" @close="management = null" @changed="load" />
|
||||
<CollaborationAccessDialog v-if="management?.section === 'access'" :file="management.ledger" :target="{ scope: 'ledger' }"
|
||||
@close="management = null" @changed="load" />
|
||||
<LedgerHistoryDialog v-if="management?.section === 'history'" :ledger="management.ledger" @close="management = null" @changed="load" @updated="updateLedgerSummary" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ArrowDown, ArrowRight } from "@element-plus/icons-vue";
|
||||
import { fetchLedgers, initializeLedgers } from "../../api/ledgers";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
import { isSystemAdmin } from "../../utils/roles";
|
||||
import { getApiErrorMessage } from "../../utils/apiErrorMessage";
|
||||
import type { Ledger } from "../../types/ledger";
|
||||
import type { CollaborationHistoryFile } from "../../api/collaborationHistory";
|
||||
import LedgerInfoDialog from "./LedgerInfoDialog.vue";
|
||||
import LedgerHistoryDialog from "./LedgerHistoryDialog.vue";
|
||||
import CollaborationAccessDialog from "../collaboration/CollaborationAccessDialog.vue";
|
||||
import EntryCountBadge from "../EntryCountBadge.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
const isAdmin = computed(() => isSystemAdmin(auth.user));
|
||||
const ledgers = ref<Ledger[]>([]);
|
||||
const updateLedgerSummary = (file: CollaborationHistoryFile) => {
|
||||
ledgers.value = ledgers.value.map((ledger) => ledger.id === file.id ? { ...ledger, ...file } as Ledger : ledger);
|
||||
};
|
||||
const loading = ref(true);
|
||||
const initializing = ref(false);
|
||||
const error = ref("");
|
||||
type ManagementSection = "info" | "access" | "history";
|
||||
const management = ref<{ section: ManagementSection; ledger: Ledger } | null>(null);
|
||||
const openManagement = (section: ManagementSection, ledger: Ledger) => { management.value = { section, ledger }; };
|
||||
const handleManagementCommand = (command: string, ledger: Ledger) => {
|
||||
if (command === "info" || command === "access" || command === "history") openManagement(command, ledger);
|
||||
};
|
||||
const displayTitle = (title: string) => title.replace(/\.xlsx$/i, "");
|
||||
const formatTime = (value: string | null) => value ? new Date(value).toLocaleString("zh-CN", { hour12: false }) : "尚未保存";
|
||||
const open = (ledger: Ledger) => router.push({ name: "LedgerWorkspace", params: { fileId: ledger.id } });
|
||||
const load = async () => {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
try { ledgers.value = (await fetchLedgers()).data; }
|
||||
catch (cause) { ledgers.value = []; error.value = await getApiErrorMessage(cause, "台账加载失败"); }
|
||||
finally { loading.value = false; }
|
||||
};
|
||||
const initialize = async () => {
|
||||
if (initializing.value) return;
|
||||
initializing.value = true;
|
||||
try { ledgers.value = (await initializeLedgers()).data; }
|
||||
catch (cause) { error.value = await getApiErrorMessage(cause, "台账初始化失败"); }
|
||||
finally { initializing.value = false; }
|
||||
};
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ledger-panel { margin-top: 44px; padding-bottom: 28px; }
|
||||
.ledger-heading { margin-bottom: 26px; }
|
||||
.ledger-eyebrow { color: #387cf4; font-size: 11px; font-weight: 800; letter-spacing: 2px; text-transform: uppercase; }
|
||||
.ledger-heading h2 { margin: 0; color: #162033; font-size: 24px; }
|
||||
.ledger-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; }
|
||||
.ledger-card { display: flex; flex-direction: column; min-width: 0; background: #fff; border: 1px solid rgba(226, 232, 240, 0.85); border-radius: 12px; box-shadow: 0 4px 15px rgba(15, 23, 42, 0.01), 0 1px 2px rgba(15, 23, 42, 0.02); }
|
||||
.ledger-card-top { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 10px; margin-bottom: 16px; }
|
||||
.ledger-title-stack { display: flex; flex-direction: column; min-width: 0; }
|
||||
.ledger-access { display: inline-flex; align-items: center; justify-content: center; height: 20px; padding: 0 8px; border-radius: 4px; color: #218266; background: #edfaf5; font-size: 10px; font-weight: 700; white-space: nowrap; }
|
||||
.ledger-access.is-readonly { color: #667289; background: #f1f4f8; }
|
||||
.ledger-card h3 { margin: 0; font-size: 14.5px; line-height: 18px; font-weight: 800; color: #0f172a; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ledger-description { color: #64748b; font-size: 10.5px; line-height: 13px; font-weight: 700; letter-spacing: 0.01em; margin: 1px 0 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ledger-card dl { display: flex; flex-direction: column; gap: 5px; font-size: 11.5px; margin: 0 0 14px; }
|
||||
.ledger-card dl > div { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
|
||||
.ledger-card dt { color: #64748b; font-weight: 600; flex-shrink: 0; }
|
||||
.ledger-card dd { margin: 0; color: #0f172a; font-weight: 550; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ledger-card footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; border-top: 1px solid rgba(241, 245, 249, 0.85); margin-top: auto; padding-top: 10px; }
|
||||
.ledger-open { display: flex; align-items: center; gap: 10px; border: 0; padding: 0; background: transparent; color: #1e3a8a; font: inherit; font-size: 11px; font-weight: 800; cursor: pointer; }
|
||||
.ledger-open .el-icon { width: 20px; height: 20px; border-radius: 50%; background: rgba(59, 130, 246, 0.06); color: #2563eb; }
|
||||
.ledger-card footer .el-button { height: 20px; padding: 0; font-size: 11px; }
|
||||
.ledger-open:focus-visible { outline: 2px solid #387cf4; outline-offset: 4px; }
|
||||
.ledger-state { padding: 28px; background: #fff; border: 1px dashed #d6e1ef; border-radius: 14px; color: #738199; font-size: 14px; }
|
||||
.ledger-state p { margin: 0 0 14px; }
|
||||
:global(.desktop-entry .ledger-card) { border-radius: 8px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user