chore: stop tracking frontend node_modules

This commit is contained in:
Cheng Zhou
2026-01-20 08:52:54 +08:00
parent 7fdcfdaadd
commit 8e258d21a7
17405 changed files with 1643 additions and 2086724 deletions
+13 -4
View File
@@ -50,7 +50,6 @@ const rules: FormRules = {
const isEdit = computed(() => !!props.category);
const reset = () => {
form.study_id = study.currentStudy?.id || "";
form.name = "";
form.description = "";
form.sort_order = 0;
@@ -61,7 +60,6 @@ watch(
() => props.category,
(val) => {
if (val) {
form.study_id = val.study_id || "";
form.name = val.name;
form.description = val.description || "";
form.sort_order = val.sort_order || 0;
@@ -86,16 +84,26 @@ const close = () => emit("update:modelValue", false);
const onSubmit = async () => {
if (!formRef.value) return;
// 验证当前项目存在
if (!study.currentStudy?.id) {
ElMessage.warning(TEXT.common.empty.selectProject);
return;
}
await formRef.value.validate();
submitting.value = true;
try {
const payload: Record<string, any> = {
study_id: study.currentStudy?.id || null,
study_id: study.currentStudy.id,
name: form.name,
description: form.description || null,
sort_order: form.sort_order,
is_active: form.is_active,
};
console.log('[FaqCategoryForm] payload:', payload);
console.log('[FaqCategoryForm] isEdit:', isEdit.value);
console.log('[FaqCategoryForm] study.currentStudy:', study.currentStudy);
if (isEdit.value && props.category) {
await updateFaqCategory(props.category.id, payload);
} else {
@@ -105,7 +113,8 @@ const onSubmit = async () => {
emit("success");
close();
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
const errorMsg = e?.response?.data?.message || e?.response?.data?.detail || TEXT.common.messages.saveFailed;
ElMessage.error(typeof errorMsg === 'string' ? errorMsg : TEXT.common.messages.saveFailed);
} finally {
submitting.value = false;
}
+1
View File
@@ -91,6 +91,7 @@ export interface Site {
city?: string | null;
pi_name?: string | null;
contact?: string | null;
enrollment_target?: number | null;
is_active: boolean;
created_at?: string;
}
-4
View File
@@ -1,4 +1,3 @@
export type DocumentStatus = "DRAFT" | "ACTIVE" | "ARCHIVED";
export type DocumentScopeType = "GLOBAL" | "SITE" | "DERIVED";
export type DocumentVersionStatus =
| "DRAFT"
@@ -13,19 +12,16 @@ export interface DocumentVersionSummary {
id: string;
version_no: string;
status: DocumentVersionStatus;
effective_at?: string | null;
}
export interface DocumentSummary {
id: string;
trial_id: string;
site_id?: string | null;
doc_no: string;
doc_type: string;
title: string;
scope_type: DocumentScopeType;
owner_id?: string | null;
status: DocumentStatus;
current_effective_version_id?: string | null;
current_effective_version?: DocumentVersionSummary | null;
created_at: string;
+467 -152
View File
@@ -1,83 +1,119 @@
<template>
<div class="login-container">
<div class="login-content">
<div class="login-brand">
<h1 class="brand-title">{{ TEXT.common.appName }}</h1>
<p class="brand-subtitle">{{ TEXT.modules.auth.brandSubtitle }}</p>
</div>
<el-card class="login-card">
<div class="login-header">
<h2 class="form-title">{{ TEXT.modules.auth.loginTitle }}</h2>
<p class="form-desc">{{ TEXT.modules.auth.loginDesc }}</p>
<!-- Vanta.js 网络背景 -->
<div ref="vantaCanvas" id="vanta-canvas" class="vanta-background"></div>
<!-- 登录卡片 -->
<div class="login-wrapper">
<div class="max-w-md w-full card-container">
<div class="card-content">
<!-- 顶部装饰区域 -->
<div class="login-header" id="vanta-header">
<!-- 左上角安全访问 -->
<div class="header-left">
<span class="secure-badge">安全访问</span>
</div>
<!-- 中间CTMS -->
<div class="header-center">
<div class="ctms-logo">CTMS</div>
<div class="header-divider"></div>
</div>
</div>
<!-- 登录表单内容 -->
<div class="login-form-section">
<div class="form-label-section">
<span class="auth-badge">身份验证</span>
<h3 class="form-title">账户登录</h3>
</div>
<el-form
ref="formRef"
:model="form"
:rules="rules"
@keyup.enter="onSubmit"
class="login-form"
>
<!-- 邮箱字段 -->
<div class="form-group">
<label for="email" class="input-label">邮箱</label>
<el-form-item prop="email">
<el-input
id="email"
v-model="form.email"
type="email"
placeholder="your@email.com"
autocomplete="email"
class="custom-input"
/>
</el-form-item>
</div>
<!-- 密码字段 -->
<div class="form-group">
<div class="password-label-row">
<label for="password" class="input-label">密码</label>
<a href="#" class="forgot-link">忘记密码</a>
</div>
<el-form-item prop="password">
<el-input
id="password"
v-model="form.password"
type="password"
placeholder="••••••••"
autocomplete="current-password"
show-password
class="custom-input"
/>
</el-form-item>
</div>
<!-- 记住设备 -->
<div class="remember-row">
<el-checkbox id="remember" v-model="form.remember" class="custom-checkbox">
<span class="remember-text">记住此设备</span>
</el-checkbox>
</div>
<!-- 登录按钮 -->
<div class="button-group">
<el-button
type="primary"
:loading="loading"
@click="onSubmit"
class="login-btn"
>
<svg xmlns="http://www.w3.org/2000/svg" class="btn-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"></path>
</svg>
登录
</el-button>
</div>
</el-form>
<!-- 底部信息 -->
<div class="footer-section">
<div class="footer-divider"></div>
<p class="footer-text">
还没有账户 <RouterLink to="/register" class="footer-link">立即注册</RouterLink>
</p>
<div class="status-row">
<span class="status-dot"></span>
<span class="status-text">系统状态正常运行</span>
</div>
</div>
</div>
</div>
<el-form
:model="form"
ref="formRef"
:rules="rules"
label-position="top"
@keyup.enter.native="onSubmit"
class="elegant-form"
>
<el-form-item :label="TEXT.modules.auth.emailLabel" prop="email">
<el-input
v-model="form.email"
:placeholder="TEXT.modules.auth.emailPlaceholder"
autocomplete="email"
>
<template #prefix>
<el-icon><Message /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item :label="TEXT.modules.auth.passwordLabel" prop="password">
<el-input
v-model="form.password"
type="password"
:placeholder="TEXT.modules.auth.passwordPlaceholder"
autocomplete="current-password"
show-password
>
<template #prefix>
<el-icon><Lock /></el-icon>
</template>
</el-input>
</el-form-item>
<div class="form-options">
<el-checkbox v-model="form.remember">{{ TEXT.modules.auth.remember }}</el-checkbox>
<RouterLink to="/forgot-password" class="forgot-link">{{ TEXT.modules.auth.forgot }}</RouterLink>
</div>
<el-button
type="primary"
:loading="loading"
class="submit-btn"
@click="onSubmit"
>
{{ TEXT.modules.auth.loginButton }}
</el-button>
<div class="register-prompt">
<span>{{ TEXT.modules.auth.registerPrompt }}</span>
<RouterLink to="/register">{{ TEXT.modules.auth.registerNow }}</RouterLink>
</div>
</el-form>
</el-card>
<div class="login-footer">
<p>{{ TEXT.modules.auth.footer }}</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted } from "vue";
import { reactive, ref, onMounted, onUnmounted } from "vue";
import { useRouter } from "vue-router";
import { ElCheckbox, ElMessage, type FormInstance, type FormRules } from "element-plus";
import { Message, Lock } from "@element-plus/icons-vue";
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
import { useAuthStore } from "../store/auth";
import { getCachedCredential, setCachedCredential, clearCachedCredential } from "../utils/auth";
import { TEXT, requiredMessage } from "../locales";
@@ -86,6 +122,7 @@ const auth = useAuthStore();
const router = useRouter();
const formRef = ref<FormInstance>();
const vantaCanvas = ref<HTMLDivElement>();
const form = reactive({
email: "",
password: "",
@@ -101,7 +138,9 @@ const rules: FormRules<typeof form> = {
};
const loading = ref(false);
let vantaEffect: any = null;
// 初始化 Vanta.js 效果
onMounted(() => {
const cached = getCachedCredential();
if (cached) {
@@ -109,8 +148,61 @@ onMounted(() => {
form.password = cached.password;
form.remember = true;
}
// 动态加载 Three.js 和 Vanta.js
loadVantaEffect();
});
onUnmounted(() => {
if (vantaEffect) {
vantaEffect.destroy();
}
});
// 加载 Vanta.js 网络效果
const loadVantaEffect = () => {
// 检查是否已加载
if ((window as any).VANTA && (window as any).THREE) {
initVanta();
return;
}
// 加载 Three.js
const threeScript = document.createElement('script');
threeScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js';
threeScript.onload = () => {
// 加载 Vanta.js NET 效果
const vantaScript = document.createElement('script');
vantaScript.src = 'https://cdn.jsdelivr.net/npm/vanta@latest/dist/vanta.net.min.js';
vantaScript.onload = () => {
initVanta();
};
document.head.appendChild(vantaScript);
};
document.head.appendChild(threeScript);
};
const initVanta = () => {
if (vantaCanvas.value && (window as any).VANTA) {
vantaEffect = (window as any).VANTA.NET({
el: vantaCanvas.value,
mouseControls: true,
touchControls: true,
gyroControls: false,
minHeight: 200.00,
minWidth: 200.00,
scale: 1.00,
scaleMobile: 1.00,
color: 0xd1d5db,
backgroundColor: 0x171717,
points: 8,
maxDistance: 20.00,
spacing: 18.00,
showDots: true
});
}
};
const onSubmit = async () => {
if (!formRef.value) return;
const valid = await formRef.value.validate();
@@ -134,141 +226,364 @@ const onSubmit = async () => {
</script>
<style scoped>
/* 导入 Inter 字体 */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
/* 主容器 */
.login-container {
position: fixed;
inset: 0;
background: #0a0a0a;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background-color: var(--ctms-bg-base);
padding: 1rem;
font-family: 'Inter', sans-serif;
}
/* Vanta 背景 */
.vanta-background {
position: fixed;
inset: 0;
overflow: hidden;
width: 100%;
height: 100%;
z-index: 0;
}
/* 登录包装器 */
.login-wrapper {
position: relative;
z-index: 10;
width: 100%;
max-width: 28rem;
}
/* 卡片容器 - 带渐变边框效果 */
.card-container {
position: relative;
z-index: 0;
}
.card-container::before {
content: "";
position: absolute;
inset: -1px;
background: linear-gradient(to bottom right, #525252, transparent, #262626);
border-radius: 0.75rem;
z-index: -1;
}
/* 卡片内容 */
.card-content {
border-radius: 0.75rem;
overflow: hidden;
background: #171717;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
/* 登录头部 */
.login-header {
height: 140px;
position: relative;
background: #171717;
overflow: hidden;
}
.login-content {
width: 420px;
z-index: 1;
position: relative;
/* 左上角 */
.header-left {
position: absolute;
top: 1rem;
left: 1rem;
z-index: 10;
}
.login-brand {
text-align: center;
margin-bottom: 32px;
/* 中间区域 */
.header-center {
position: absolute;
top: 70%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
}
.brand-title {
font-size: 48px;
font-weight: 700;
color: var(--ctms-text-main);
letter-spacing: -1px;
margin: 0;
/* CTMS Logo */
.ctms-logo {
font-size: 3.5rem;
font-weight: 800;
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: 0.1em;
margin-bottom: 0.5rem;
text-shadow: 0 0 30px rgba(102, 126, 234, 0.3);
animation: glow 3s ease-in-out infinite;
}
.brand-subtitle {
font-size: 14px;
color: var(--ctms-text-secondary);
margin-top: 8px;
letter-spacing: 2px;
text-transform: uppercase;
font-weight: 500;
@keyframes glow {
0%, 100% {
filter: drop-shadow(0 0 8px rgba(102, 126, 234, 0.4));
}
50% {
filter: drop-shadow(0 0 16px rgba(102, 126, 234, 0.6));
}
}
.login-card {
border: 1px solid var(--ctms-border-color);
background: #ffffff;
border-radius: 16px;
box-shadow: none;
padding: 12px;
.secure-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
background: rgba(38, 38, 38, 0.8);
border-radius: 9999px;
font-size: 0.75rem;
color: #a3a3a3;
}
.login-header {
text-align: center;
margin-bottom: 32px;
.header-divider {
height: 0.25rem;
width: 3rem;
background: linear-gradient(90deg, #667eea, #764ba2, #f093fb);
margin-top: 0.5rem;
border-radius: 9999px;
}
/* 登录表单区域 */
.login-form-section {
padding: 1.5rem 1.5rem 1.5rem;
display: flex;
flex-direction: column;
background: #171717;
}
.form-label-section {
margin-bottom: 1.5rem;
}
.auth-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
background: #262626;
border-radius: 9999px;
font-size: 0.75rem;
color: #a3a3a3;
margin-bottom: 0.5rem;
}
.form-title {
font-size: 24px;
font-weight: 700;
color: var(--ctms-text-main);
font-size: 1.25rem;
font-weight: 600;
color: #e5e5e5;
margin: 0;
}
.form-desc {
font-size: 14px;
color: var(--ctms-text-secondary);
margin-top: 8px;
/* 表单样式 */
.login-form {
margin-bottom: 1.5rem;
}
.elegant-form :deep(.el-form-item__label) {
font-size: 13px;
font-weight: 600;
color: var(--ctms-text-regular);
padding-bottom: 8px;
.login-form :deep(.el-form-item) {
margin-bottom: 1rem;
}
.elegant-form :deep(.el-input__wrapper) {
background-color: #ffffff;
box-shadow: 0 0 0 1px var(--ctms-border-color) inset;
padding: 8px 12px;
border-radius: 10px;
transition: var(--ctms-transition);
.login-form :deep(.el-form-item__error) {
color: #f87171;
font-size: 0.75rem;
margin-top: 0.25rem;
}
.elegant-form :deep(.el-input__wrapper.is-focus) {
box-shadow: 0 0 0 1px var(--ctms-primary) inset !important;
background-color: #ffffff;
.form-group {
margin-bottom: 1rem;
}
.form-options {
.input-label {
display: block;
color: #d4d4d4;
font-size: 0.75rem;
font-weight: 500;
margin-bottom: 0.25rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.password-label-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
margin-bottom: 0.25rem;
}
.forgot-link {
font-size: 13px;
color: var(--ctms-primary);
color: #a3a3a3;
font-size: 0.75rem;
text-decoration: none;
font-weight: 500;
transition: color 0.2s;
}
.submit-btn {
width: 100%;
height: 48px;
font-size: 16px;
font-weight: 600;
border-radius: 12px;
background: var(--ctms-primary);
border: none;
.forgot-link:hover {
color: #d4d4d4;
}
/* 自定义输入框样式 */
.custom-input :deep(.el-input__wrapper) {
background: #262626;
border: 1px solid #404040;
border-radius: 0.5rem;
padding: 0.5rem 1rem;
box-shadow: none;
transition: var(--ctms-transition);
transition: all 0.2s;
}
.submit-btn:hover {
transform: translateY(-1px);
.custom-input :deep(.el-input__wrapper:hover) {
background: #262626;
border-color: #525252;
}
.register-prompt {
margin-top: 24px;
.custom-input :deep(.el-input__wrapper.is-focus) {
background: #262626;
border-color: #525252;
box-shadow: 0 0 0 2px rgba(82, 82, 82, 0.2);
}
.custom-input :deep(.el-input__inner) {
color: #e5e5e5;
font-size: 0.875rem;
}
.custom-input :deep(.el-input__inner::placeholder) {
color: #737373;
}
.custom-input :deep(.el-icon) {
color: #a3a3a3;
}
/* 记住设备行 */
.remember-row {
display: flex;
align-items: center;
margin-bottom: 1rem;
}
.custom-checkbox :deep(.el-checkbox__inner) {
width: 1rem;
height: 1rem;
background: #262626;
border-color: #404040;
}
.custom-checkbox :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
background: #262626;
border-color: #404040;
}
.custom-checkbox :deep(.el-checkbox__inner::after) {
border-color: #e5e5e5;
}
.remember-text {
color: #a3a3a3;
font-size: 0.75rem;
margin-left: 0.5rem;
}
/* 按钮组 */
.button-group {
display: flex;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.btn-icon {
width: 1rem;
height: 1rem;
margin-right: 0.5rem;
}
.login-btn {
width: 100%;
height: 2.75rem;
background: #262626;
border: none;
border-radius: 0.5rem;
color: #e5e5e5;
font-weight: 500;
font-size: 0.875rem;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
cursor: pointer;
}
.login-btn:hover {
background: #404040;
}
/* 底部区域 */
.footer-section {
padding-top: 1rem;
text-align: center;
font-size: 14px;
color: var(--ctms-text-secondary);
}
.register-prompt a {
color: var(--ctms-primary);
font-weight: 600;
.footer-divider {
height: 1px;
background: linear-gradient(to right, transparent, #525252, transparent);
margin-bottom: 1rem;
}
.footer-text {
color: #a3a3a3;
font-size: 0.75rem;
margin: 0 0 1rem 0;
}
.footer-link {
color: #d4d4d4;
text-decoration: none;
margin-left: 4px;
transition: text-decoration 0.2s;
}
.login-footer {
text-align: center;
margin-top: 40px;
.footer-link:hover {
text-decoration: underline;
}
.login-footer p {
font-size: 12px;
color: var(--ctms-text-disabled);
.status-row {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.status-dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background: #22c55e;
}
.status-text {
color: #a3a3a3;
font-size: 0.75rem;
}
/* 响应式设计 */
@media (max-width: 640px) {
.login-wrapper {
padding: 0.5rem;
}
.login-header {
height: 120px;
}
.login-form-section {
padding: 1.25rem;
}
}
</style>
@@ -28,7 +28,7 @@
<el-tag :type="statusType(scope.row.status)">{{ displayEnum(TEXT.enums.userStatus, scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="220" fixed="right">
<el-table-column :label="TEXT.common.labels.actions" width="220" align="center">
<template #default="scope">
<el-button
v-if="scope.row.status === 'PENDING'"
+15 -4
View File
@@ -10,12 +10,12 @@
</div>
<el-table :data="memberRows" v-loading="loading" stripe>
<el-table-column prop="full_name" :label="TEXT.modules.adminProjectMembers.username" min-width="160" />
<el-table-column prop="role_in_study" :label="TEXT.modules.adminProjectMembers.projectRole" width="140">
<el-table-column prop="role_in_study" :label="TEXT.modules.adminProjectMembers.projectRole" min-width="140">
<template #default="scope">
<el-tag>{{ displayEnum(TEXT.enums.userRole, scope.row.role_in_study) }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.fields.status" width="160">
<el-table-column :label="TEXT.common.fields.status" width="100">
<template #default="scope">
<el-tooltip
v-if="scope.row.effectiveStatus === 'DISABLED_GLOBAL'"
@@ -29,10 +29,10 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="added_at" :label="TEXT.modules.adminProjectMembers.addedAt" width="200">
<el-table-column prop="added_at" :label="TEXT.modules.adminProjectMembers.addedAt" min-width="180">
<template #default="scope">{{ displayDateTime(scope.row.added_at) }}</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="360" fixed="right">
<el-table-column :label="TEXT.common.labels.actions" width="300" align="center" class-name="action-column">
<template #default="scope">
<el-select
v-model="scope.row.role_in_study"
@@ -412,3 +412,14 @@ onMounted(async () => {
font-size: 12px;
}
</style>
<style>
.el-table .action-column .cell {
padding-left: 8px;
padding-right: 8px;
}
.action-column .el-select {
margin-right: 8px;
}
</style>
+7 -2
View File
@@ -28,8 +28,8 @@
/>
</el-select>
</el-form-item>
<el-form-item v-if="site" :label="TEXT.common.fields.status" prop="is_active">
<el-switch v-model="form.is_active" :active-text="TEXT.common.actions.enable" :inactive-text="TEXT.common.actions.disable" />
<el-form-item label="入组目标" prop="enrollment_target">
<el-input-number v-model="form.enrollment_target" :min="0" :step="1" :placeholder="'请输入入组目标人数'" />
</el-form-item>
</el-form>
<template #footer>
@@ -77,6 +77,7 @@ const form = reactive({
phone: "",
contact: "",
craSelections: [] as string[],
enrollment_target: 0,
is_active: true,
});
@@ -102,6 +103,7 @@ const resetForm = () => {
form.phone = "";
form.contact = "";
form.craSelections = [];
form.enrollment_target = 0;
form.is_active = true;
};
@@ -125,6 +127,7 @@ watch(
optionByLabel[opt.label] = opt.value;
});
form.craSelections = rawSelections.map((s) => optionByLabel[s] || s);
form.enrollment_target = (props.site as any)?.enrollment_target || 0;
form.is_active = props.site.is_active;
}
}
@@ -158,6 +161,7 @@ const onSubmit = async () => {
city: form.city,
pi_name: form.pi_name,
contact,
enrollment_target: form.enrollment_target,
is_active: form.is_active,
});
ElMessage.success(TEXT.modules.adminSites.updateSuccess);
@@ -173,6 +177,7 @@ const onSubmit = async () => {
city: form.city,
pi_name: form.pi_name,
contact,
enrollment_target: form.enrollment_target,
});
ElMessage.success(TEXT.modules.adminSites.createSuccess);
logAudit("SITE_STATUS_CHANGED", {
+12 -7
View File
@@ -9,26 +9,26 @@
<el-button type="primary" @click="openCreate">{{ TEXT.common.actions.add }}{{ TEXT.modules.adminSites.siteLabel }}</el-button>
</div>
<el-table :data="sites" v-loading="loading" stripe :row-class-name="siteRowClass">
<el-table-column prop="name" :label="TEXT.common.fields.siteName" min-width="180" />
<el-table-column prop="city" :label="TEXT.common.fields.city" width="140" />
<el-table-column prop="pi_name" :label="TEXT.modules.adminSites.piLabel" width="160" />
<el-table-column :label="TEXT.common.fields.phone" width="160">
<el-table-column prop="name" :label="TEXT.common.fields.siteName" width="180" />
<el-table-column prop="city" :label="TEXT.common.fields.city" width="100" />
<el-table-column prop="pi_name" :label="TEXT.modules.adminSites.piLabel" width="120" />
<el-table-column :label="TEXT.common.fields.phone" width="140">
<template #default="scope">
{{ phoneText(scope.row) }}
</template>
</el-table-column>
<el-table-column prop="contact" :label="TEXT.common.fields.contact" min-width="180">
<el-table-column prop="contact" :label="TEXT.common.fields.contact" width="160">
<template #default="scope">
{{ contactLabel(scope.row) }}
</template>
</el-table-column>
<el-table-column :label="TEXT.common.fields.status" width="120">
<el-table-column :label="TEXT.common.fields.status" width="100">
<template #default="scope">
<el-tag type="success" v-if="scope.row.is_active">已解锁</el-tag>
<el-tag type="danger" v-else>已锁定</el-tag>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="260" fixed="right">
<el-table-column :label="TEXT.common.labels.actions" width="200" align="center" class-name="action-column">
<template #default="scope">
<el-button link type="primary" size="small" @click="openEdit(scope.row)">{{ TEXT.common.actions.edit }}</el-button>
<el-button
@@ -290,4 +290,9 @@ onMounted(async () => {
.row-inactive .el-tag {
opacity: 0.6;
}
.el-table .action-column .cell {
padding-left: 8px;
padding-right: 8px;
}
</style>
@@ -3,8 +3,6 @@
<div class="ctms-page-header">
<div>
<h1 class="ctms-page-title">
{{ detail.doc_no || TEXT.common.fallback }}
<span class="text-secondary mx-2">/</span>
{{ detail.title || TEXT.modules.fileVersionManagement.title }}
</h1>
<p class="ctms-page-subtitle">
@@ -27,14 +25,9 @@
<el-card class="ctms-section-card" v-loading="loading">
<el-descriptions :column="3" border class="ctms-descriptions">
<el-descriptions-item :label="TEXT.modules.fileVersionManagement.columns.docNo">{{ detail.doc_no || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.modules.fileVersionManagement.fields.docType">{{ displayText(detail.doc_type, TEXT.enums.documentType) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.modules.fileVersionManagement.fields.site">{{ displaySite(detail.site_id) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.modules.fileVersionManagement.columns.status">
<el-tag :type="getStatusType(detail.status)" effect="light" size="small">{{ displayEnum(TEXT.enums.documentStatus, detail.status) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item :label="TEXT.modules.fileVersionManagement.labels.currentEffective">{{ detail.current_effective_version?.version_no || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.modules.fileVersionManagement.columns.effectiveAt">{{ formatDate(detail.current_effective_version?.effective_at) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.modules.fileVersionManagement.columns.updatedAt">{{ formatDate(detail.updated_at) }}</el-descriptions-item>
</el-descriptions>
</el-card>
@@ -83,7 +76,7 @@
</div>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.actions" width="280" fixed="right">
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.actions" width="280" align="center">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="downloadVersion(row)" v-if="canAction('view')">
{{ TEXT.modules.fileVersionManagement.actions.download }}
@@ -144,7 +137,7 @@
<span :class="{ 'text-danger': row.stats?.overdue }">{{ formatDate(row.due_at) }}</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.fileVersionManagement.labels.ackAction" width="140" fixed="right">
<el-table-column :label="TEXT.modules.fileVersionManagement.labels.ackAction" width="140" align="center">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="ack(row, 'RECEIVED')" v-if="canAction('ack')">
{{ TEXT.modules.fileVersionManagement.actions.ackReceived }}
@@ -311,21 +304,6 @@ import type { UserInfo } from "../../types/api";
import { TEXT } from "../../locales";
import { displayEnum, displayText, getUserDisplayName } from "../../utils/display";
const getStatusType = (status: string) => {
switch (status) {
case "ACTIVE":
return "success";
case "DRAFT":
return "info";
case "ARCHIVED":
return "info";
case "OBSOLETE":
return "danger";
default:
return "";
}
};
const getVersionStatusType = (status: string) => {
switch (status) {
case "APPROVED":
@@ -353,11 +331,9 @@ const detail = reactive<DocumentDetail>({
id: "",
trial_id: "",
site_id: null,
doc_no: "",
doc_type: "",
title: "",
scope_type: "GLOBAL",
status: "ACTIVE",
current_effective_version_id: null,
current_effective_version: null,
created_at: "",
+1 -47
View File
@@ -32,12 +32,6 @@
<el-option v-for="item in docTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<div class="ctms-filter-item">
<span class="ctms-filter-label">{{ TEXT.modules.fileVersionManagement.filters.status }}</span>
<el-select v-model="filters.status" :placeholder="TEXT.modules.fileVersionManagement.filters.all" clearable>
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<div class="ctms-filter-spacer"></div>
<div class="ctms-filter-actions">
<el-button type="primary" @click="load">{{ TEXT.common.actions.search }}</el-button>
@@ -54,11 +48,6 @@
:row-class-name="documentRowClass"
class="ctms-table"
>
<el-table-column prop="doc_no" :label="TEXT.modules.fileVersionManagement.columns.docNo" width="160">
<template #default="{ row }">
<span class="font-mono font-medium">{{ row.doc_no }}</span>
</template>
</el-table-column>
<el-table-column prop="title" :label="TEXT.modules.fileVersionManagement.columns.title" min-width="200">
<template #default="{ row }">
<span class="font-semibold">{{ row.title }}</span>
@@ -90,24 +79,12 @@
<span v-else class="text-secondary">-</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.status" width="120">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" effect="light">
{{ displayEnum(TEXT.enums.documentStatus, row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.effectiveAt" width="180">
<template #default="{ row }">
<span>{{ formatDate(row.current_effective_version?.effective_at) }}</span>
</template>
</el-table-column>
<el-table-column prop="updated_at" :label="TEXT.modules.fileVersionManagement.columns.updatedAt" width="180">
<template #default="{ row }">
<span class="text-secondary">{{ formatDate(row.updated_at) }}</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.actions" width="100" fixed="right">
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.actions" width="100" align="center">
<template #default="{ row }">
<el-button link type="danger" :disabled="isInactiveSite(row.site_id)" @click.stop="confirmDelete(row)">
{{ TEXT.common.actions.delete }}
@@ -165,22 +142,6 @@ import type { Site } from "../../types/api";
import { displayEnum, displayText } from "../../utils/display";
import { TEXT } from "../../locales";
const getStatusType = (status: string) => {
switch (status) {
case "ACTIVE":
return "success";
case "DRAFT":
return "info";
case "ARCHIVED":
return "info";
case "OBSOLETE":
return "danger";
default:
return "";
}
};
const route = useRoute();
const router = useRouter();
const study = useStudyStore();
@@ -197,17 +158,12 @@ const filters = reactive({
scope_type: "",
site_id: "",
doc_type: "",
status: "",
});
const docTypeOptions = Object.keys(TEXT.enums.documentType).map((value) => ({
value,
label: TEXT.enums.documentType[value as keyof typeof TEXT.enums.documentType],
}));
const statusOptions = Object.keys(TEXT.enums.documentStatus).map((value) => ({
value,
label: TEXT.enums.documentStatus[value as keyof typeof TEXT.enums.documentStatus],
}));
const scopeOptions = [
{ value: "GLOBAL", label: TEXT.enums.scopeType.GLOBAL },
{ value: "SITE", label: TEXT.enums.scopeType.SITE },
@@ -289,7 +245,6 @@ const load = async () => {
scope_type: filters.scope_type || undefined,
site_id: filters.site_id || undefined,
doc_type: filters.doc_type || undefined,
status: filters.status || undefined,
});
items.value = data.items || [];
} catch (e: any) {
@@ -303,7 +258,6 @@ const resetFilters = () => {
filters.scope_type = "";
filters.site_id = "";
filters.doc_type = "";
filters.status = "";
load();
};
+1 -1
View File
@@ -81,7 +81,7 @@
<el-table-column prop="remark" :label="TEXT.common.fields.remark" min-width="160" show-overflow-tooltip>
<template #default="scope">{{ scope.row.remark || TEXT.common.fallback }}</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="120" fixed="right">
<el-table-column :label="TEXT.common.labels.actions" width="120" align="center">
<template #default="scope">
<el-button
link
@@ -52,7 +52,7 @@
<span class="font-mono text-main">{{ row.project_no }}</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="120" fixed="right">
<el-table-column :label="TEXT.common.labels.actions" width="120" align="center">
<template #default="scope">
<el-button
link
@@ -115,7 +115,7 @@
<span class="font-mono text-main">{{ row.approval_no }}</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="120" fixed="right">
<el-table-column :label="TEXT.common.labels.actions" width="120" align="center">
<template #default="scope">
<el-button
link