c1dd8def2c
新增与项目平级的协作台账,提供空白模板、独立账号授权和在线编辑。 复用共享库的访问权限与历史版本界面,拆分信息维护入口并统一工作台卡片样式。 补充在线文档字体构建支持、数据库表结构升级和回归测试。 不包含本地台账内容、账号授权记录或历史文件迁移数据。
129 lines
8.4 KiB
Vue
129 lines
8.4 KiB
Vue
<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>
|