全局中文化
This commit is contained in:
@@ -13,15 +13,15 @@
|
||||
<div class="filters">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="搜索关键词"
|
||||
:placeholder="TEXT.common.placeholders.keyword"
|
||||
clearable
|
||||
@keyup.enter="loadFaqs"
|
||||
style="width: 260px"
|
||||
/>
|
||||
<el-switch v-model="onlyActive" active-text="仅启用" @change="loadFaqs" />
|
||||
<el-switch v-model="onlyActive" :active-text="TEXT.common.labels.activeOnly" @change="loadFaqs" />
|
||||
<div class="spacer" />
|
||||
<PermissionAction action="faq.create">
|
||||
<el-button type="primary" @click="openForm()">新建咨询</el-button>
|
||||
<el-button type="primary" @click="openForm()">{{ TEXT.modules.knowledgeMedicalConsult.newItem }}</el-button>
|
||||
</PermissionAction>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -59,6 +59,7 @@ import { useAuthStore } from "../store/auth";
|
||||
import { useStudyStore } from "../store/study";
|
||||
import PermissionAction from "../components/PermissionAction.vue";
|
||||
import { usePermission } from "../utils/permission";
|
||||
import { TEXT } from "../locales";
|
||||
|
||||
const auth = useAuthStore();
|
||||
const study = useStudyStore();
|
||||
@@ -86,7 +87,7 @@ const loadCategories = async () => {
|
||||
const { data } = await fetchFaqCategories(params);
|
||||
categories.value = data.items || data || [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "分类加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -111,7 +112,7 @@ const loadFaqs = async () => {
|
||||
total.value = data.total || 0;
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "咨询加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -3,44 +3,44 @@
|
||||
<el-card v-loading="loading">
|
||||
<div class="content">
|
||||
<div class="question-header">
|
||||
<div class="question-label">问题描述</div>
|
||||
<div class="question-label">{{ TEXT.modules.knowledgeMedicalConsult.questionDesc }}</div>
|
||||
<div class="question-actions">
|
||||
<el-button v-if="canEditQuestion" type="primary" size="small" class="edit-btn" @click="openEdit">
|
||||
编辑问题
|
||||
{{ TEXT.modules.knowledgeMedicalConsult.editQuestion }}
|
||||
</el-button>
|
||||
<el-button v-if="canConfirmResolved" type="success" size="small" @click="confirmResolved">
|
||||
确认解决
|
||||
{{ TEXT.modules.knowledgeMedicalConsult.confirmResolved }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="question-meta">
|
||||
<span class="meta-pill meta-user"
|
||||
>提问人:{{ displayUser(item?.created_by, { members: memberMap, users: userMap }) }}</span
|
||||
>{{ TEXT.modules.knowledgeMedicalConsult.asker }}:{{ displayUser(item?.created_by, { members: memberMap, users: userMap }) }}</span
|
||||
>
|
||||
<span class="meta-pill meta-time">提问时间:{{ displayDateTime(item?.created_at) }}</span>
|
||||
<span class="meta-pill meta-category">分类:{{ categoryName }}</span>
|
||||
<span class="meta-pill meta-status">状态:{{ statusLabel }}</span>
|
||||
<span class="meta-pill meta-time">{{ TEXT.modules.knowledgeMedicalConsult.askedAt }}:{{ displayDateTime(item?.created_at) }}</span>
|
||||
<span class="meta-pill meta-category">{{ TEXT.common.labels.category }}:{{ categoryName }}</span>
|
||||
<span class="meta-pill meta-status">{{ TEXT.common.fields.status }}:{{ statusLabel }}</span>
|
||||
</div>
|
||||
<pre class="question-text">{{ item?.question }}</pre>
|
||||
</div>
|
||||
<div v-if="bestReply" class="best-answer">
|
||||
<div class="best-title">
|
||||
<span>最佳答案</span>
|
||||
<el-tag type="success" size="small">已采纳</el-tag>
|
||||
<span>{{ TEXT.modules.knowledgeMedicalConsult.bestAnswer }}</span>
|
||||
<el-tag type="success" size="small">{{ TEXT.modules.knowledgeMedicalConsult.adopted }}</el-tag>
|
||||
</div>
|
||||
<div class="best-meta">
|
||||
{{ displayUser(bestReply.created_by, { members: memberMap, users: userMap }) }}
|
||||
· {{ displayDateTime(bestReply.created_at) }}
|
||||
</div>
|
||||
<div class="best-content">{{ bestReply.is_deleted ? "回复已删除" : bestReply.content }}</div>
|
||||
<div class="best-content">{{ bestReply.is_deleted ? TEXT.modules.knowledgeMedicalConsult.replyDeleted : bestReply.content }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt-12" v-loading="repliesLoading">
|
||||
<template #header>
|
||||
<div class="reply-header">
|
||||
<span>回复</span>
|
||||
<span class="reply-count">共 {{ replies.length }} 条</span>
|
||||
<span>{{ TEXT.modules.knowledgeMedicalConsult.replies }}</span>
|
||||
<span class="reply-count">{{ TEXT.modules.knowledgeMedicalConsult.replyCount.replace("{count}", String(replies.length)) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="canReply" class="reply-form">
|
||||
@@ -56,7 +56,7 @@
|
||||
@clear-quote="clearQuote"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="reply-disabled">当前角色无权限回复</div>
|
||||
<div v-else class="reply-disabled">{{ TEXT.modules.knowledgeMedicalConsult.noReplyPermission }}</div>
|
||||
<el-divider v-if="replies.length" />
|
||||
<ThreadList
|
||||
v-if="replies.length"
|
||||
@@ -71,13 +71,13 @@
|
||||
@delete="removeReply"
|
||||
>
|
||||
<template #actions="{ item: r }">
|
||||
<el-tag v-if="item?.best_reply_id === r.id" type="success" size="small">最佳答案</el-tag>
|
||||
<el-tag v-if="item?.best_reply_id === r.id" type="success" size="small">{{ TEXT.modules.knowledgeMedicalConsult.bestAnswer }}</el-tag>
|
||||
<el-button v-if="canSelectBest && !r.is_deleted" type="text" size="small" @click="toggleBest(r)">
|
||||
{{ item?.best_reply_id === r.id ? "取消最佳" : "设为最佳" }}
|
||||
{{ item?.best_reply_id === r.id ? TEXT.modules.knowledgeMedicalConsult.cancelBest : TEXT.modules.knowledgeMedicalConsult.setBest }}
|
||||
</el-button>
|
||||
</template>
|
||||
</ThreadList>
|
||||
<div v-else class="reply-empty">暂无回复</div>
|
||||
<div v-else class="reply-empty">{{ TEXT.modules.knowledgeMedicalConsult.emptyReplies }}</div>
|
||||
</el-card>
|
||||
|
||||
<FaqItemForm
|
||||
@@ -111,6 +111,7 @@ import { usePermission } from "../utils/permission";
|
||||
import FaqItemForm from "../components/FaqItemForm.vue";
|
||||
import ThreadComposer from "../components/ThreadComposer.vue";
|
||||
import ThreadList from "../components/ThreadList.vue";
|
||||
import { TEXT } from "../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const auth = useAuthStore();
|
||||
@@ -163,9 +164,7 @@ const canConfirmResolved = computed(() => {
|
||||
});
|
||||
|
||||
const statusLabel = computed(() => {
|
||||
if (item.value?.status === "RESOLVED") return "已解决";
|
||||
if (item.value?.status === "PROCESSING") return "处理中";
|
||||
return "待回答";
|
||||
return TEXT.enums.faqStatus[item.value?.status as keyof typeof TEXT.enums.faqStatus] || TEXT.enums.faqStatus.PENDING;
|
||||
});
|
||||
|
||||
const bestReply = computed(() => {
|
||||
@@ -227,7 +226,7 @@ const loadReplies = async () => {
|
||||
replies.value = Array.isArray(data) ? data : data.items || [];
|
||||
await loadReplyAttachments();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "回复加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.knowledgeMedicalConsult.replyLoadFailed);
|
||||
} finally {
|
||||
repliesLoading.value = false;
|
||||
}
|
||||
@@ -247,7 +246,7 @@ const loadData = async () => {
|
||||
categories.value = catData.items || catData || [];
|
||||
await Promise.all([loadReplies(), loadMembers(faqData.study_id)]);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -263,7 +262,7 @@ const clearQuote = () => {
|
||||
|
||||
const submitReply = async () => {
|
||||
if (!replyContent.value.trim()) {
|
||||
ElMessage.warning("请输入回复内容");
|
||||
ElMessage.warning(TEXT.modules.knowledgeMedicalConsult.replyRequired);
|
||||
return;
|
||||
}
|
||||
if (!item.value) return;
|
||||
@@ -281,16 +280,16 @@ const submitReply = async () => {
|
||||
await uploadAttachment(item.value.study_id, "faq_replies", created.id, file.raw as File);
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "附件上传失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.uploadFailed);
|
||||
}
|
||||
}
|
||||
ElMessage.success("回复已提交");
|
||||
ElMessage.success(TEXT.modules.knowledgeMedicalConsult.replySubmitted);
|
||||
replyContent.value = "";
|
||||
quoteReply.value = null;
|
||||
replyFiles.value = [];
|
||||
loadReplies();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "回复失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.knowledgeMedicalConsult.replyFailed);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
@@ -298,14 +297,14 @@ const submitReply = async () => {
|
||||
|
||||
const removeReply = async (reply: any) => {
|
||||
if (!item.value) return;
|
||||
const ok = await ElMessageBox.confirm("确认删除该回复?", "提示").catch(() => null);
|
||||
const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await deleteFaqReply(item.value.id, reply.id);
|
||||
ElMessage.success("回复已删除");
|
||||
ElMessage.success(TEXT.common.messages.deleteSuccess);
|
||||
loadReplies();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "删除失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -314,14 +313,14 @@ const toggleBest = async (reply: any) => {
|
||||
try {
|
||||
const target = item.value.best_reply_id === reply.id ? null : reply.id;
|
||||
if (target) {
|
||||
const ok = await ElMessageBox.confirm("设为最佳答案并确认已解决?", "提示").catch(() => null);
|
||||
const ok = await ElMessageBox.confirm(TEXT.modules.knowledgeMedicalConsult.confirmBestReply, TEXT.common.labels.tips).catch(() => null);
|
||||
if (!ok) return;
|
||||
}
|
||||
const { data } = await setFaqBestReply(item.value.id, { best_reply_id: target });
|
||||
item.value = data;
|
||||
ElMessage.success(target ? "已设为最佳答案" : "已取消最佳答案");
|
||||
ElMessage.success(target ? TEXT.modules.knowledgeMedicalConsult.bestReplySet : TEXT.modules.knowledgeMedicalConsult.bestReplyCleared);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "操作失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.actionFailed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -330,9 +329,9 @@ const confirmResolved = async () => {
|
||||
try {
|
||||
const { data } = await setFaqStatus(item.value.id, { status: "RESOLVED" });
|
||||
item.value = data;
|
||||
ElMessage.success("已设置为已解决");
|
||||
ElMessage.success(TEXT.modules.knowledgeMedicalConsult.resolvedSuccess);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "操作失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.actionFailed);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div class="forgot-container">
|
||||
<ModulePlaceholder
|
||||
:list-title="TEXT.modules.auth.forgotTitle"
|
||||
:empty-title="TEXT.modules.auth.forgotTitle"
|
||||
:empty-description="TEXT.modules.auth.forgotDesc"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ModulePlaceholder from "../components/ModulePlaceholder.vue";
|
||||
import { TEXT } from "../locales";
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.forgot-container {
|
||||
min-height: 100vh;
|
||||
background-color: var(--ctms-bg-base);
|
||||
padding: 32px;
|
||||
}
|
||||
</style>
|
||||
@@ -2,14 +2,14 @@
|
||||
<div class="login-container">
|
||||
<div class="login-content">
|
||||
<div class="login-brand">
|
||||
<h1 class="brand-title">CTMS</h1>
|
||||
<p class="brand-subtitle">临床试验管理系统</p>
|
||||
<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">欢迎回来</h2>
|
||||
<p class="form-desc">请输入您的凭据以访问系统</p>
|
||||
<h2 class="form-title">{{ TEXT.modules.auth.loginTitle }}</h2>
|
||||
<p class="form-desc">{{ TEXT.modules.auth.loginDesc }}</p>
|
||||
</div>
|
||||
|
||||
<el-form
|
||||
@@ -20,10 +20,10 @@
|
||||
@keyup.enter.native="onSubmit"
|
||||
class="elegant-form"
|
||||
>
|
||||
<el-form-item label="电子邮箱" prop="email">
|
||||
<el-form-item :label="TEXT.modules.auth.emailLabel" prop="email">
|
||||
<el-input
|
||||
v-model="form.email"
|
||||
placeholder="name@company.com"
|
||||
:placeholder="TEXT.modules.auth.emailPlaceholder"
|
||||
autocomplete="email"
|
||||
>
|
||||
<template #prefix>
|
||||
@@ -31,11 +31,11 @@
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="登录密码" prop="password">
|
||||
<el-form-item :label="TEXT.modules.auth.passwordLabel" prop="password">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
placeholder="最小 8 位字符"
|
||||
:placeholder="TEXT.modules.auth.passwordPlaceholder"
|
||||
autocomplete="current-password"
|
||||
show-password
|
||||
>
|
||||
@@ -46,8 +46,8 @@
|
||||
</el-form-item>
|
||||
|
||||
<div class="form-options">
|
||||
<el-checkbox v-model="form.remember">记住我</el-checkbox>
|
||||
<RouterLink to="/forgot-password" class="forgot-link">忘记密码?</RouterLink>
|
||||
<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
|
||||
@@ -56,18 +56,18 @@
|
||||
class="submit-btn"
|
||||
@click="onSubmit"
|
||||
>
|
||||
登 录
|
||||
{{ TEXT.modules.auth.loginButton }}
|
||||
</el-button>
|
||||
|
||||
<div class="register-prompt">
|
||||
<span>还没有账号?</span>
|
||||
<RouterLink to="/register">立即注册</RouterLink>
|
||||
<span>{{ TEXT.modules.auth.registerPrompt }}</span>
|
||||
<RouterLink to="/register">{{ TEXT.modules.auth.registerNow }}</RouterLink>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<div class="login-footer">
|
||||
<p>© 2025 CTMS Enterprise. Powered by Professional Clinical Research.</p>
|
||||
<p>{{ TEXT.modules.auth.footer }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,6 +80,7 @@ import { ElCheckbox, ElMessage, type FormInstance, type FormRules } from "elemen
|
||||
import { Message, Lock } from "@element-plus/icons-vue";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import { getCachedCredential, setCachedCredential, clearCachedCredential } from "../utils/auth";
|
||||
import { TEXT, requiredMessage } from "../locales";
|
||||
|
||||
const auth = useAuthStore();
|
||||
const router = useRouter();
|
||||
@@ -93,10 +94,10 @@ const form = reactive({
|
||||
|
||||
const rules: FormRules<typeof form> = {
|
||||
email: [
|
||||
{ required: true, message: "请输入有效的邮箱地址", trigger: "blur" },
|
||||
{ type: "email", message: "邮箱格式不正确", trigger: "blur" },
|
||||
{ required: true, message: requiredMessage(TEXT.modules.auth.emailLabel), trigger: "blur" },
|
||||
{ type: "email", message: TEXT.common.validation.invalidEmail, trigger: "blur" },
|
||||
],
|
||||
password: [{ required: true, message: "请输入您的密码", trigger: "blur" }],
|
||||
password: [{ required: true, message: requiredMessage(TEXT.modules.auth.passwordLabel), trigger: "blur" }],
|
||||
};
|
||||
|
||||
const loading = ref(false);
|
||||
@@ -125,7 +126,7 @@ const onSubmit = async () => {
|
||||
router.push("/");
|
||||
} catch (error: any) {
|
||||
const detail = error?.response?.data?.detail || error?.response?.data?.message;
|
||||
ElMessage.error(detail || "认证失败,请检查您的凭据");
|
||||
ElMessage.error(detail || TEXT.modules.auth.authFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<el-card>
|
||||
<h3 class="title">个人设置</h3>
|
||||
<p class="subtitle">更新个人信息或修改登录密码</p>
|
||||
<h3 class="title">{{ TEXT.modules.profile.title }}</h3>
|
||||
<p class="subtitle">{{ TEXT.modules.profile.subtitle }}</p>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" class="form">
|
||||
<el-form-item label="头像">
|
||||
<el-form-item :label="TEXT.common.fields.avatar">
|
||||
<div class="avatar-row">
|
||||
<el-avatar :size="64" :src="avatarPreview" :alt="form.full_name || form.email">
|
||||
{{ form.full_name?.charAt(0)?.toUpperCase() || form.email?.charAt(0)?.toUpperCase() }}
|
||||
@@ -18,31 +18,31 @@
|
||||
:on-success="onAvatarUploaded"
|
||||
:on-error="onAvatarError"
|
||||
>
|
||||
<el-button>上传头像</el-button>
|
||||
<el-button>{{ TEXT.modules.profile.uploadAvatar }}</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱">
|
||||
<el-form-item :label="TEXT.common.fields.email">
|
||||
<el-input v-model="form.email" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名" prop="full_name">
|
||||
<el-input v-model="form.full_name" placeholder="请输入姓名" />
|
||||
<el-form-item :label="TEXT.common.fields.name" prop="full_name">
|
||||
<el-input v-model="form.full_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门" prop="department">
|
||||
<el-input v-model="form.department" placeholder="请输入部门" />
|
||||
<el-form-item :label="TEXT.common.fields.department" prop="department">
|
||||
<el-input v-model="form.department" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.department" />
|
||||
</el-form-item>
|
||||
<el-divider />
|
||||
<el-form-item label="当前密码" prop="current_password">
|
||||
<el-input v-model="form.current_password" type="password" show-password placeholder="修改密码时必填" />
|
||||
<el-form-item :label="TEXT.modules.profile.currentPassword" prop="current_password">
|
||||
<el-input v-model="form.current_password" type="password" show-password :placeholder="TEXT.modules.profile.currentPasswordHint" />
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码" prop="password">
|
||||
<el-input v-model="form.password" type="password" show-password placeholder="不修改请留空" />
|
||||
<el-form-item :label="TEXT.modules.profile.newPassword" prop="password">
|
||||
<el-input v-model="form.password" type="password" show-password :placeholder="TEXT.modules.profile.newPasswordHint" />
|
||||
</el-form-item>
|
||||
<el-form-item label="确认新密码" prop="confirmPassword">
|
||||
<el-input v-model="form.confirmPassword" type="password" show-password placeholder="再次输入新密码" />
|
||||
<el-form-item :label="TEXT.modules.profile.confirmPassword" prop="confirmPassword">
|
||||
<el-input v-model="form.confirmPassword" type="password" show-password :placeholder="TEXT.modules.profile.confirmPasswordHint" />
|
||||
</el-form-item>
|
||||
<div class="actions">
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">保存</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ TEXT.common.actions.save }}</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -56,6 +56,7 @@ import { ElMessage } from "element-plus";
|
||||
import { updateProfile, fetchMe } from "../api/auth";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import { getToken } from "../utils/auth";
|
||||
import { TEXT, requiredMessage } from "../locales";
|
||||
|
||||
const auth = useAuthStore();
|
||||
const formRef = ref<FormInstance>();
|
||||
@@ -74,13 +75,13 @@ const uploadHeaders = computed<Record<string, string>>(() => ({
|
||||
}));
|
||||
|
||||
const rules: FormRules<typeof form> = {
|
||||
full_name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
|
||||
department: [{ required: true, message: "请输入部门", trigger: "blur" }],
|
||||
full_name: [{ required: true, message: requiredMessage(TEXT.common.fields.name), trigger: "blur" }],
|
||||
department: [{ required: true, message: requiredMessage(TEXT.common.fields.department), trigger: "blur" }],
|
||||
current_password: [
|
||||
{
|
||||
validator: (_r, value, cb) => {
|
||||
if (form.password && !value) {
|
||||
cb(new Error("修改密码需输入当前密码"));
|
||||
cb(new Error(TEXT.modules.profile.passwordRequired));
|
||||
return;
|
||||
}
|
||||
cb();
|
||||
@@ -93,7 +94,7 @@ const rules: FormRules<typeof form> = {
|
||||
validator: (_r, value, cb) => {
|
||||
if (!value) return cb();
|
||||
if (value.length < 8 || !/[A-Za-z]/.test(value) || !/[0-9]/.test(value)) {
|
||||
cb(new Error("至少 8 位且包含字母与数字"));
|
||||
cb(new Error(TEXT.modules.profile.passwordRule));
|
||||
return;
|
||||
}
|
||||
cb();
|
||||
@@ -105,7 +106,7 @@ const rules: FormRules<typeof form> = {
|
||||
{
|
||||
validator: (_r, value, cb) => {
|
||||
if (form.password && value !== form.password) {
|
||||
cb(new Error("两次输入的密码不一致"));
|
||||
cb(new Error(TEXT.modules.profile.passwordMismatch));
|
||||
return;
|
||||
}
|
||||
cb();
|
||||
@@ -136,13 +137,13 @@ const onSubmit = async () => {
|
||||
password: form.password || undefined,
|
||||
});
|
||||
await auth.fetchMe();
|
||||
ElMessage.success("已更新");
|
||||
ElMessage.success(TEXT.modules.profile.updateSuccess);
|
||||
form.current_password = "";
|
||||
form.password = "";
|
||||
form.confirmPassword = "";
|
||||
avatarPreview.value = auth.user?.avatar_url || avatarPreview.value;
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || "更新失败");
|
||||
ElMessage.error(error?.response?.data?.message || TEXT.modules.profile.updateFailed);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
@@ -152,11 +153,11 @@ const onSubmit = async () => {
|
||||
const onAvatarUploaded = async () => {
|
||||
await auth.fetchMe();
|
||||
avatarPreview.value = auth.user?.avatar_url || undefined;
|
||||
ElMessage.success("头像已更新");
|
||||
ElMessage.success(TEXT.modules.profile.avatarUpdated);
|
||||
};
|
||||
|
||||
const onAvatarError = (err: any) => {
|
||||
ElMessage.error(err?.response?.data?.message || "头像上传失败");
|
||||
ElMessage.error(err?.response?.data?.message || TEXT.modules.profile.avatarUploadFailed);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<div class="register-container">
|
||||
<div class="register-card">
|
||||
<div class="header">
|
||||
<h2>创建账号</h2>
|
||||
<p>提交后需管理员审核通过方可登录</p>
|
||||
<h2>{{ TEXT.modules.auth.registerTitle }}</h2>
|
||||
<p>{{ TEXT.modules.auth.registerDesc }}</p>
|
||||
</div>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
@@ -12,36 +12,36 @@
|
||||
label-position="top"
|
||||
@keyup.enter.native="onSubmit"
|
||||
>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="form.email" placeholder="name@example.com" autocomplete="email" />
|
||||
<el-form-item :label="TEXT.modules.auth.emailLabel" prop="email">
|
||||
<el-input v-model="form.email" :placeholder="TEXT.modules.auth.emailPlaceholder" autocomplete="email" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-form-item :label="TEXT.modules.auth.passwordLabel" prop="password">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
placeholder="至少 8 位且包含字母与数字"
|
||||
:placeholder="TEXT.modules.auth.passwordRuleMix"
|
||||
show-password
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码" prop="confirmPassword">
|
||||
<el-form-item :label="TEXT.modules.auth.confirmPasswordLabel" prop="confirmPassword">
|
||||
<el-input
|
||||
v-model="form.confirmPassword"
|
||||
type="password"
|
||||
placeholder="再次输入密码"
|
||||
:placeholder="TEXT.modules.auth.confirmPasswordPlaceholder"
|
||||
show-password
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名" prop="full_name">
|
||||
<el-input v-model="form.full_name" placeholder="请输入姓名" />
|
||||
<el-form-item :label="TEXT.modules.auth.fullNameLabel" prop="full_name">
|
||||
<el-input v-model="form.full_name" :placeholder="TEXT.modules.auth.fullNamePlaceholder" />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门" prop="department">
|
||||
<el-input v-model="form.department" placeholder="请输入部门" />
|
||||
<el-form-item :label="TEXT.modules.auth.departmentLabel" prop="department">
|
||||
<el-input v-model="form.department" :placeholder="TEXT.modules.auth.departmentPlaceholder" />
|
||||
</el-form-item>
|
||||
<div class="actions">
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">提交注册</el-button>
|
||||
<RouterLink to="/login">已有账号?返回登录</RouterLink>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ TEXT.modules.auth.registerButton }}</el-button>
|
||||
<RouterLink to="/login">{{ TEXT.modules.auth.backToLogin }}</RouterLink>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
@@ -55,6 +55,7 @@ import type { FormInstance, FormRules } from "element-plus";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { register as apiRegister } from "../api/auth";
|
||||
import type { RegisterRequest } from "../types/api";
|
||||
import { TEXT, requiredMessage } from "../locales";
|
||||
|
||||
const router = useRouter();
|
||||
const defaultRole: RegisterRequest["role"] = "CRA";
|
||||
@@ -72,11 +73,11 @@ const submitting = ref(false);
|
||||
const passwordRule = {
|
||||
validator: (_: any, value: string, callback: (err?: Error) => void) => {
|
||||
if (!value || value.length < 8) {
|
||||
callback(new Error("密码至少 8 位"));
|
||||
callback(new Error(TEXT.modules.auth.passwordRuleMin));
|
||||
return;
|
||||
}
|
||||
if (!/[A-Za-z]/.test(value) || !/[0-9]/.test(value)) {
|
||||
callback(new Error("需包含字母和数字"));
|
||||
callback(new Error(TEXT.modules.auth.passwordRuleMix));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
@@ -86,19 +87,19 @@ const passwordRule = {
|
||||
|
||||
const rules: FormRules<typeof form> = {
|
||||
email: [
|
||||
{ required: true, message: "请输入邮箱", trigger: "blur" },
|
||||
{ type: "email", message: "邮箱格式不正确", trigger: "blur" },
|
||||
{ required: true, message: requiredMessage(TEXT.modules.auth.emailLabel), trigger: "blur" },
|
||||
{ type: "email", message: TEXT.common.validation.invalidEmail, trigger: "blur" },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: "请输入密码", trigger: "blur" },
|
||||
{ required: true, message: requiredMessage(TEXT.modules.auth.passwordLabel), trigger: "blur" },
|
||||
passwordRule,
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: "请确认密码", trigger: "blur" },
|
||||
{ required: true, message: requiredMessage(TEXT.modules.auth.confirmPasswordLabel), trigger: "blur" },
|
||||
{
|
||||
validator: (_: any, value: string, callback: (err?: Error) => void) => {
|
||||
if (value !== form.password) {
|
||||
callback(new Error("两次输入的密码不一致"));
|
||||
callback(new Error(TEXT.modules.auth.passwordMismatch));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
@@ -106,8 +107,8 @@ const rules: FormRules<typeof form> = {
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
full_name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
|
||||
department: [{ required: true, message: "请输入部门", trigger: "blur" }],
|
||||
full_name: [{ required: true, message: requiredMessage(TEXT.modules.auth.fullNameLabel), trigger: "blur" }],
|
||||
department: [{ required: true, message: requiredMessage(TEXT.modules.auth.departmentLabel), trigger: "blur" }],
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
@@ -123,7 +124,7 @@ const onSubmit = async () => {
|
||||
role: defaultRole,
|
||||
department: form.department,
|
||||
});
|
||||
ElMessage.success("注册成功,等待管理员审核");
|
||||
ElMessage.success(TEXT.modules.auth.registerSuccess);
|
||||
setTimeout(() => {
|
||||
router.push("/login");
|
||||
}, 3000);
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<!-- 概览头部 -->
|
||||
<div class="home-header">
|
||||
<div class="header-content">
|
||||
<h1 class="welcome-title">项目总览</h1>
|
||||
<p class="page-subtitle">当前项目运行状态与关键指标</p>
|
||||
<h1 class="welcome-title">{{ TEXT.modules.projectOverview.title }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.projectOverview.subtitle }}</p>
|
||||
<p class="study-info">
|
||||
<span class="study-name">{{ study.currentStudy.name }}</span>
|
||||
<span class="study-divider">/</span>
|
||||
@@ -12,7 +12,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="header-meta">
|
||||
<el-tag effect="plain" class="role-tag">角色: {{ auth.user?.role }}</el-tag>
|
||||
<el-tag effect="plain" class="role-tag">{{ TEXT.common.labels.role }}: {{ roleLabel }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<div class="stats-grid">
|
||||
<div class="stats-item">
|
||||
<KpiCard
|
||||
title="节点完成率"
|
||||
:title="TEXT.modules.projectOverview.kpiProgress"
|
||||
:value="progress?.milestones_done ?? 0"
|
||||
:unit="` / ${progress?.milestones_total ?? 0}`"
|
||||
:subtext="milestoneCompletionRate"
|
||||
@@ -30,19 +30,19 @@
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<KpiCard
|
||||
title="逾期 AE"
|
||||
:title="TEXT.modules.projectOverview.kpiOverdueAes"
|
||||
:value="overdueAes"
|
||||
unit="例"
|
||||
subtext="需尽快跟进处理"
|
||||
:unit="TEXT.common.units.case"
|
||||
:subtext="TEXT.modules.projectOverview.kpiOverdueAesHint"
|
||||
:loading="loading.overdueAes"
|
||||
:icon="Warning"
|
||||
/>
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<KpiCard
|
||||
title="费用总额"
|
||||
:title="TEXT.modules.projectOverview.kpiFinanceTotal"
|
||||
:value="formatAmount(financeSummary?.total_amount ?? 0)"
|
||||
:subtext="`累计已支付:¥${formatAmount(financeSummary?.paid_amount ?? 0)}`"
|
||||
:subtext="`${TEXT.modules.projectOverview.kpiFinancePaid}:¥${formatAmount(financeSummary?.paid_amount ?? 0)}`"
|
||||
:loading="loading.finance"
|
||||
:icon="Money"
|
||||
/>
|
||||
@@ -53,7 +53,7 @@
|
||||
<el-alert
|
||||
v-if="overdueAes > 0"
|
||||
type="warning"
|
||||
title="存在逾期未处理事项,请及时关注项目的合规性与进度。"
|
||||
:title="TEXT.modules.projectOverview.alertWarning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="home-alert"
|
||||
@@ -61,7 +61,7 @@
|
||||
<el-alert
|
||||
v-else
|
||||
type="success"
|
||||
title="项目运行状态良好,所有关键指标均在可控范围内。"
|
||||
:title="TEXT.modules.projectOverview.alertOk"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="home-alert"
|
||||
@@ -74,7 +74,7 @@
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-state-container">
|
||||
<StateEmpty description="请先在顶部选择一个当前项目" />
|
||||
<StateEmpty :description="TEXT.common.empty.selectProject" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -87,6 +87,8 @@ import KpiCard from "../components/KpiCard.vue";
|
||||
import QuickActions from "../components/QuickActions.vue";
|
||||
import { List, Warning, Money } from "@element-plus/icons-vue";
|
||||
import StateEmpty from "../components/StateEmpty.vue";
|
||||
import { displayEnum } from "../utils/display";
|
||||
import { TEXT } from "../locales";
|
||||
|
||||
const auth = useAuthStore();
|
||||
const study = useStudyStore();
|
||||
@@ -103,11 +105,13 @@ const loading = ref({
|
||||
|
||||
const milestoneCompletionRate = computed(() => {
|
||||
const total = progress.value?.milestones_total ?? 0;
|
||||
if (!total) return "暂无节点";
|
||||
if (!total) return TEXT.modules.projectOverview.noMilestones;
|
||||
const rate = (progress.value?.milestones_done ?? 0) / total;
|
||||
return `总体进度: ${(rate * 100).toFixed(0)}%`;
|
||||
return TEXT.modules.projectOverview.progressLabel.replace("{rate}", `${(rate * 100).toFixed(0)}%`);
|
||||
});
|
||||
|
||||
const roleLabel = computed(() => displayEnum(TEXT.enums.userRole, auth.user?.role));
|
||||
|
||||
const formatAmount = (val: number) => {
|
||||
return new Intl.NumberFormat('zh-CN', { minimumFractionDigits: 2 }).format(val);
|
||||
};
|
||||
|
||||
@@ -3,30 +3,32 @@
|
||||
<el-card>
|
||||
<div class="header">
|
||||
<div>
|
||||
<h3>注册审核</h3>
|
||||
<p>仅显示待审核账号,审核通过后即可登录</p>
|
||||
<h3>{{ TEXT.modules.adminUserApproval.title }}</h3>
|
||||
<p>{{ TEXT.modules.adminUserApproval.subtitle }}</p>
|
||||
</div>
|
||||
<el-select v-model="status" size="small" style="width: 180px" @change="loadUsers">
|
||||
<el-option label="待审核" value="PENDING" />
|
||||
<el-option label="已通过" value="ACTIVE" />
|
||||
<el-option label="已拒绝" value="REJECTED" />
|
||||
<el-option label="已禁用" value="DISABLED" />
|
||||
<el-option :label="TEXT.enums.userStatus.PENDING" value="PENDING" />
|
||||
<el-option :label="TEXT.enums.userStatus.ACTIVE" value="ACTIVE" />
|
||||
<el-option :label="TEXT.enums.userStatus.REJECTED" value="REJECTED" />
|
||||
<el-option :label="TEXT.enums.userStatus.DISABLED" value="DISABLED" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-table :data="users" v-loading="loading" style="width: 100%">
|
||||
<el-table-column prop="email" label="邮箱" min-width="200" />
|
||||
<el-table-column prop="full_name" label="姓名" min-width="140" />
|
||||
<el-table-column prop="role" label="角色" width="120" />
|
||||
<el-table-column prop="department" label="部门" min-width="160" />
|
||||
<el-table-column prop="created_at" label="注册时间" width="200">
|
||||
<el-table-column prop="email" :label="TEXT.common.fields.email" min-width="200" />
|
||||
<el-table-column prop="full_name" :label="TEXT.common.fields.name" min-width="140" />
|
||||
<el-table-column prop="role" :label="TEXT.common.labels.role" width="120">
|
||||
<template #default="scope">{{ displayEnum(TEXT.enums.userRole, scope.row.role) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="department" :label="TEXT.common.fields.department" min-width="160" />
|
||||
<el-table-column prop="created_at" :label="TEXT.modules.adminUserApproval.createdAt" width="200">
|
||||
<template #default="scope">{{ displayDateTime(scope.row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="120">
|
||||
<el-table-column prop="status" :label="TEXT.common.fields.status" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="statusType(scope.row.status)">{{ scope.row.status }}</el-tag>
|
||||
<el-tag :type="statusType(scope.row.status)">{{ displayEnum(TEXT.enums.userStatus, scope.row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="220" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-if="scope.row.status === 'PENDING'"
|
||||
@@ -35,7 +37,7 @@
|
||||
size="small"
|
||||
@click="onApprove(scope.row.id)"
|
||||
>
|
||||
审核通过
|
||||
{{ TEXT.modules.adminUserApproval.approve }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'PENDING'"
|
||||
@@ -44,9 +46,9 @@
|
||||
size="small"
|
||||
@click="onReject(scope.row.id)"
|
||||
>
|
||||
拒绝
|
||||
{{ TEXT.modules.adminUserApproval.reject }}
|
||||
</el-button>
|
||||
<span v-else>—</span>
|
||||
<span v-else>{{ TEXT.common.fallback }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -59,7 +61,8 @@ import { onMounted, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { approveUser, listPendingUsers, rejectUser } from "../../api/admin";
|
||||
import type { UserInfo, UserStatus } from "../../types/api";
|
||||
import { displayDateTime } from "../../utils/display";
|
||||
import { displayDateTime, displayEnum } from "../../utils/display";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const users = ref<UserInfo[]>([]);
|
||||
const loading = ref(false);
|
||||
@@ -84,7 +87,7 @@ const loadUsers = async () => {
|
||||
const { data } = await listPendingUsers(status.value);
|
||||
users.value = data.items || [];
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || "加载待审核用户失败");
|
||||
ElMessage.error(error?.response?.data?.message || TEXT.modules.adminUserApproval.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -93,20 +96,20 @@ const loadUsers = async () => {
|
||||
const onApprove = async (id: string) => {
|
||||
try {
|
||||
await approveUser(id);
|
||||
ElMessage.success("已通过审核");
|
||||
ElMessage.success(TEXT.modules.adminUserApproval.approveSuccess);
|
||||
loadUsers();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || "审核失败");
|
||||
ElMessage.error(error?.response?.data?.message || TEXT.modules.adminUserApproval.approveFailed);
|
||||
}
|
||||
};
|
||||
|
||||
const onReject = async (id: string) => {
|
||||
try {
|
||||
await rejectUser(id);
|
||||
ElMessage.success("已拒绝");
|
||||
ElMessage.success(TEXT.modules.adminUserApproval.rejectSuccess);
|
||||
loadUsers();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || "拒绝失败");
|
||||
ElMessage.error(error?.response?.data?.message || TEXT.modules.adminUserApproval.rejectFailed);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,22 +3,22 @@
|
||||
<el-card>
|
||||
<div class="ctms-table-toolbar">
|
||||
<div class="ctms-filter-group">
|
||||
<el-select v-model="filters.eventType" clearable placeholder="操作类型" @change="loadLogs" class="ctms-filter-item">
|
||||
<el-select v-model="filters.eventType" clearable :placeholder="TEXT.modules.adminAuditLogs.filterEvent" @change="loadLogs" class="ctms-filter-item">
|
||||
<el-option v-for="opt in eventTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
<el-select v-model="filters.operatorId" clearable placeholder="操作人" filterable @change="loadLogs" class="ctms-filter-item">
|
||||
<el-select v-model="filters.operatorId" clearable :placeholder="TEXT.modules.adminAuditLogs.filterOperator" filterable @change="loadLogs" class="ctms-filter-item">
|
||||
<el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" />
|
||||
</el-select>
|
||||
<el-select v-model="filters.result" clearable placeholder="结果" @change="loadLogs" style="width: 100px">
|
||||
<el-option label="成功" value="SUCCESS" />
|
||||
<el-option label="失败" value="FAIL" />
|
||||
<el-select v-model="filters.result" clearable :placeholder="TEXT.modules.adminAuditLogs.filterResult" @change="loadLogs" style="width: 100px">
|
||||
<el-option :label="TEXT.audit.resultSuccess" value="SUCCESS" />
|
||||
<el-option :label="TEXT.audit.resultFail" value="FAIL" />
|
||||
</el-select>
|
||||
<el-date-picker
|
||||
v-model="filters.range"
|
||||
type="daterange"
|
||||
unlink-panels
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
:start-placeholder="TEXT.modules.adminAuditLogs.rangeStart"
|
||||
:end-placeholder="TEXT.modules.adminAuditLogs.rangeEnd"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 240px"
|
||||
@@ -34,7 +34,7 @@
|
||||
:loading="exportLoading"
|
||||
@click="confirmExport('system')"
|
||||
>
|
||||
导出系统审计
|
||||
{{ TEXT.modules.adminAuditLogs.exportSystem }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canProjectExport"
|
||||
@@ -44,36 +44,36 @@
|
||||
:loading="exportLoading"
|
||||
@click="confirmExport('project')"
|
||||
>
|
||||
导出项目审计
|
||||
{{ TEXT.modules.adminAuditLogs.exportProject }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="logs" v-loading="loading" style="width: 100%">
|
||||
<el-table-column prop="timestamp" label="时间" width="180">
|
||||
<el-table-column prop="timestamp" :label="TEXT.modules.adminAuditLogs.columns.time" width="180">
|
||||
<template #default="scope">{{ displayDateTime(scope.row.timestamp) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="actorName" label="操作人" width="140" />
|
||||
<el-table-column prop="actorRoleLabel" label="角色" width="120" />
|
||||
<el-table-column prop="eventLabel" label="操作类型" min-width="160" />
|
||||
<el-table-column prop="actionText" label="操作内容" min-width="180" />
|
||||
<el-table-column label="操作对象" min-width="180">
|
||||
<el-table-column prop="actorName" :label="TEXT.modules.adminAuditLogs.columns.actor" width="140" />
|
||||
<el-table-column prop="actorRoleLabel" :label="TEXT.common.labels.role" width="120" />
|
||||
<el-table-column prop="eventLabel" :label="TEXT.modules.adminAuditLogs.columns.event" min-width="160" />
|
||||
<el-table-column prop="actionText" :label="TEXT.modules.adminAuditLogs.columns.action" min-width="180" />
|
||||
<el-table-column :label="TEXT.modules.adminAuditLogs.columns.target" min-width="180">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.targetTypeLabel">
|
||||
{{ scope.row.targetTypeLabel }}({{ scope.row.targetName || "—" }})
|
||||
{{ scope.row.targetTypeLabel }}({{ scope.row.targetName || TEXT.common.fallback }})
|
||||
</span>
|
||||
<span v-else>—</span>
|
||||
<span v-else>{{ TEXT.common.fallback }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="变更详情" min-width="220">
|
||||
<el-table-column :label="TEXT.modules.adminAuditLogs.columns.diff" min-width="220">
|
||||
<template #default="scope">
|
||||
<div v-if="scope.row.diffText?.length">
|
||||
<div v-for="(line, idx) in scope.row.diffText" :key="idx">{{ line }}</div>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
<span v-else>{{ TEXT.audit.emptyValue }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结果" width="120">
|
||||
<el-table-column :label="TEXT.modules.adminAuditLogs.columns.result" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.result === 'SUCCESS' ? 'success' : 'danger'">
|
||||
{{ scope.row.resultLabel }}
|
||||
@@ -107,6 +107,7 @@ import { roleDict, getDictLabel } from "../../dictionaries";
|
||||
import { exportAuditCsv } from "../../audit/export/auditExportService";
|
||||
import { logAudit } from "../../audit";
|
||||
import { displayDateTime } from "../../utils/display";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const buildLogKey = (log: any) => {
|
||||
if (log.id) return `id:${log.id}`;
|
||||
@@ -197,7 +198,7 @@ const loadLogs = async () => {
|
||||
total.value = (data as any).total || merged.length;
|
||||
enrichLogs();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "审计日志加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminAuditLogs.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -278,7 +279,7 @@ const fetchAllForExport = async () => {
|
||||
});
|
||||
return filtered.map((log) => normalizeAuditEvent(log, userMap));
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "导出数据加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminAuditLogs.exportLoadFailed);
|
||||
return [];
|
||||
} finally {
|
||||
exportLoading.value = false;
|
||||
@@ -287,20 +288,20 @@ const fetchAllForExport = async () => {
|
||||
|
||||
const confirmExport = async (scope: "system" | "project") => {
|
||||
const ok = await ElMessageBox.confirm(
|
||||
"该文件包含敏感审计信息,仅限授权人员使用。确认导出吗?",
|
||||
"审计导出确认",
|
||||
{ type: "warning", confirmButtonText: "确认", cancelButtonText: "取消" }
|
||||
TEXT.modules.adminAuditLogs.exportConfirm,
|
||||
TEXT.modules.adminAuditLogs.exportConfirmTitle,
|
||||
{ type: "warning", confirmButtonText: TEXT.common.actions.confirm, cancelButtonText: TEXT.common.actions.cancel }
|
||||
).catch(() => null);
|
||||
if (!ok) return;
|
||||
const events = await fetchAllForExport();
|
||||
if (!events.length) {
|
||||
ElMessage.warning("无可导出的审计记录");
|
||||
ElMessage.warning(TEXT.modules.adminAuditLogs.exportEmpty);
|
||||
return;
|
||||
}
|
||||
const fileName =
|
||||
scope === "system"
|
||||
? `系统审计_${new Date().toISOString().slice(0, 10)}`
|
||||
: `项目审计_${study.currentStudy?.code || ""}_${new Date().toISOString().slice(0, 10)}`;
|
||||
? `${TEXT.modules.adminAuditLogs.exportSystemName}_${new Date().toISOString().slice(0, 10)}`
|
||||
: `${TEXT.modules.adminAuditLogs.exportProjectName}_${study.currentStudy?.code || ""}_${new Date().toISOString().slice(0, 10)}`;
|
||||
exportAuditCsv(events, { fileName });
|
||||
logAudit(scope === "system" ? "AUDIT_EXPORT_SYSTEM" : "AUDIT_EXPORT_PROJECT", {
|
||||
targetId: study.currentStudy?.id,
|
||||
|
||||
@@ -2,27 +2,27 @@
|
||||
<div class="ctms-page">
|
||||
<div class="ctms-page-header">
|
||||
<div>
|
||||
<h1 class="ctms-page-title">项目详情</h1>
|
||||
<h1 class="ctms-page-title">{{ TEXT.modules.adminProjects.detailTitle }}</h1>
|
||||
</div>
|
||||
<div class="ctms-page-actions">
|
||||
<el-button type="primary" @click="enterProject" :disabled="!project">进入项目</el-button>
|
||||
<el-button @click="goMembers" :disabled="!project">项目成员配置</el-button>
|
||||
<el-button @click="goSites" :disabled="!project">中心管理</el-button>
|
||||
<el-button type="primary" @click="enterProject" :disabled="!project">{{ TEXT.modules.adminProjects.enter }}</el-button>
|
||||
<el-button @click="goMembers" :disabled="!project">{{ TEXT.modules.adminProjects.members }}</el-button>
|
||||
<el-button @click="goSites" :disabled="!project">{{ TEXT.modules.adminProjects.sites }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card class="ctms-section-card">
|
||||
<template #header>
|
||||
<div>
|
||||
<div class="ctms-section-title">项目基本信息</div>
|
||||
<div class="ctms-section-title">{{ TEXT.modules.adminProjects.basicInfo }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-descriptions v-if="project" :column="2" border class="detail-descriptions">
|
||||
<el-descriptions-item label="项目编号">{{ project.code }}</el-descriptions-item>
|
||||
<el-descriptions-item label="项目名称">{{ project.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="申办方">{{ project.sponsor || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">{{ statusLabel(project.status) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ displayDateTime(project.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.projectCode">{{ project.code }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.projectName">{{ project.name }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.sponsor">{{ project.sponsor || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.status">{{ statusLabel(project.status) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.modules.adminProjects.createdAt">{{ displayDateTime(project.created_at) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<StateLoading v-else :rows="4" />
|
||||
</el-card>
|
||||
@@ -30,7 +30,7 @@
|
||||
<el-card class="ctms-section-card">
|
||||
<template #header>
|
||||
<div>
|
||||
<div class="ctms-section-title">项目通用文件</div>
|
||||
<div class="ctms-section-title">{{ TEXT.modules.adminProjects.filesTitle }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<AttachmentList
|
||||
@@ -55,6 +55,7 @@ import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||
import StateLoading from "../../components/StateLoading.vue";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { displayDateTime } from "../../utils/display";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -67,16 +68,12 @@ const loadProject = async () => {
|
||||
const { data } = await fetchStudyDetail(route.params.projectId as string);
|
||||
project.value = data as Study;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "项目加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminProjects.loadFailed);
|
||||
}
|
||||
};
|
||||
|
||||
const statusLabel = (status: string) =>
|
||||
({
|
||||
DRAFT: "草稿",
|
||||
ACTIVE: "进行中",
|
||||
CLOSED: "已关闭",
|
||||
}[status] || status);
|
||||
TEXT.enums.projectStatus[status as keyof typeof TEXT.enums.projectStatus] || status;
|
||||
|
||||
const enterProject = () => {
|
||||
if (!project.value) return;
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
<template>
|
||||
<el-dialog :title="project ? '编辑项目' : '新建项目'" width="560px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<el-dialog :title="project ? TEXT.modules.adminProjects.editTitle : TEXT.modules.adminProjects.newTitle" width="560px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-form-item label="项目编号" prop="code">
|
||||
<el-input v-model="form.code" :disabled="!!project" placeholder="请输入项目编号" />
|
||||
<el-form-item :label="TEXT.common.fields.projectCode" prop="code">
|
||||
<el-input v-model="form.code" :disabled="!!project" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.projectCode" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入项目名称" />
|
||||
<el-form-item :label="TEXT.common.fields.projectName" prop="name">
|
||||
<el-input v-model="form.name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.projectName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="申办方" prop="sponsor">
|
||||
<el-input v-model="form.sponsor" placeholder="申办方(可选)" />
|
||||
<el-form-item :label="TEXT.common.fields.sponsor" prop="sponsor">
|
||||
<el-input v-model="form.sponsor" :placeholder="TEXT.modules.adminProjects.sponsorPlaceholder" />
|
||||
</el-form-item>
|
||||
<el-form-item label="方案号" prop="protocol_no">
|
||||
<el-input v-model="form.protocol_no" placeholder="方案号(可选)" />
|
||||
<el-form-item :label="TEXT.common.fields.protocolNo" prop="protocol_no">
|
||||
<el-input v-model="form.protocol_no" :placeholder="TEXT.modules.adminProjects.protocolPlaceholder" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分期" prop="phase">
|
||||
<el-input v-model="form.phase" placeholder="研究分期(可选)" />
|
||||
<el-form-item :label="TEXT.common.fields.phase" prop="phase">
|
||||
<el-input v-model="form.phase" :placeholder="TEXT.modules.adminProjects.phasePlaceholder" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="form.status" placeholder="请选择状态">
|
||||
<el-option label="草稿" value="DRAFT" />
|
||||
<el-option label="进行中" value="ACTIVE" />
|
||||
<el-option label="已关闭" value="CLOSED" />
|
||||
<el-form-item :label="TEXT.common.fields.status" prop="status">
|
||||
<el-select v-model="form.status" :placeholder="TEXT.common.placeholders.select">
|
||||
<el-option :label="TEXT.enums.projectStatus.DRAFT" value="DRAFT" />
|
||||
<el-option :label="TEXT.enums.projectStatus.ACTIVE" value="ACTIVE" />
|
||||
<el-option :label="TEXT.enums.projectStatus.CLOSED" value="CLOSED" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visibleProxy = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">保存</el-button>
|
||||
<el-button @click="visibleProxy = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ TEXT.common.actions.save }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -36,6 +36,7 @@ import { computed, reactive, ref, watch } from "vue";
|
||||
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
|
||||
import { createStudy, updateStudy } from "../../api/studies";
|
||||
import type { Study } from "../../types/api";
|
||||
import { TEXT, requiredMessage } from "../../locales";
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
@@ -64,9 +65,9 @@ const form = reactive({
|
||||
});
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
code: [{ required: true, message: "请输入项目编号", trigger: "blur" }],
|
||||
name: [{ required: true, message: "请输入项目名称", trigger: "blur" }],
|
||||
status: [{ required: true, message: "请选择项目状态", trigger: "change" }],
|
||||
code: [{ required: true, message: requiredMessage(TEXT.common.fields.projectCode), trigger: "blur" }],
|
||||
name: [{ required: true, message: requiredMessage(TEXT.common.fields.projectName), trigger: "blur" }],
|
||||
status: [{ required: true, message: requiredMessage(TEXT.common.fields.status), trigger: "change" }],
|
||||
});
|
||||
|
||||
const resetForm = () => {
|
||||
@@ -108,7 +109,7 @@ const onSubmit = async () => {
|
||||
phase: form.phase,
|
||||
status: form.status,
|
||||
});
|
||||
ElMessage.success("项目已更新");
|
||||
ElMessage.success(TEXT.modules.adminProjects.updateSuccess);
|
||||
} else {
|
||||
await createStudy({
|
||||
code: form.code,
|
||||
@@ -118,12 +119,12 @@ const onSubmit = async () => {
|
||||
phase: form.phase,
|
||||
status: form.status,
|
||||
});
|
||||
ElMessage.success("项目已创建");
|
||||
ElMessage.success(TEXT.modules.adminProjects.createSuccess);
|
||||
}
|
||||
emit("saved");
|
||||
visibleProxy.value = false;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
|
||||
@@ -3,37 +3,37 @@
|
||||
<el-card>
|
||||
<div class="header">
|
||||
<div>
|
||||
<h3>项目成员配置(角色仅在当前项目内生效)</h3>
|
||||
<div class="sub" v-if="project">项目:{{ project.code }} - {{ project.name }}</div>
|
||||
<div class="sub">同一账号在不同项目可拥有不同角色,角色授权仅在本页进行</div>
|
||||
<h3>{{ TEXT.modules.adminProjectMembers.title }}</h3>
|
||||
<div class="sub" v-if="project">{{ TEXT.modules.adminProjectMembers.projectPrefix }}{{ project.code }} - {{ project.name }}</div>
|
||||
<div class="sub">{{ TEXT.modules.adminProjectMembers.subtitle }}</div>
|
||||
</div>
|
||||
<el-button type="primary" @click="openAdd">新增成员</el-button>
|
||||
<el-button type="primary" @click="openAdd">{{ TEXT.common.actions.add }}{{ TEXT.modules.adminProjectMembers.memberLabel }}</el-button>
|
||||
</div>
|
||||
<el-table :data="memberRows" v-loading="loading" stripe>
|
||||
<el-table-column prop="username" label="用户名" min-width="160" />
|
||||
<el-table-column prop="role_in_study" label="项目角色" width="140">
|
||||
<el-table-column prop="username" :label="TEXT.modules.adminProjectMembers.username" min-width="160" />
|
||||
<el-table-column prop="role_in_study" :label="TEXT.modules.adminProjectMembers.projectRole" width="140">
|
||||
<template #default="scope">
|
||||
<el-tag>{{ scope.row.role_in_study }}</el-tag>
|
||||
<el-tag>{{ displayEnum(TEXT.enums.userRole, scope.row.role_in_study) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="160">
|
||||
<el-table-column :label="TEXT.common.fields.status" width="160">
|
||||
<template #default="scope">
|
||||
<el-tooltip
|
||||
v-if="scope.row.effectiveStatus === 'DISABLED_GLOBAL'"
|
||||
content="请前往【账号治理】启用该账号"
|
||||
:content="TEXT.modules.adminProjectMembers.disabledGlobalHint"
|
||||
placement="top"
|
||||
>
|
||||
<el-tag type="danger">全局已禁用</el-tag>
|
||||
<el-tag type="danger">{{ TEXT.modules.adminProjectMembers.disabledGlobal }}</el-tag>
|
||||
</el-tooltip>
|
||||
<el-tag v-else :type="scope.row.is_active ? 'success' : 'danger'">
|
||||
{{ scope.row.is_active ? "启用" : "已禁用" }}
|
||||
{{ scope.row.is_active ? TEXT.common.actions.enable : TEXT.modules.adminProjectMembers.disabled }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="added_at" label="加入时间" width="200">
|
||||
<el-table-column prop="added_at" :label="TEXT.modules.adminProjectMembers.addedAt" width="200">
|
||||
<template #default="scope">{{ displayDateTime(scope.row.added_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="360" fixed="right">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="360" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-select
|
||||
v-model="scope.row.role_in_study"
|
||||
@@ -42,14 +42,14 @@
|
||||
@change="(val) => updateRole(scope.row.id, val)"
|
||||
:disabled="!scope.row.is_active || scope.row.effectiveStatus === 'DISABLED_GLOBAL'"
|
||||
>
|
||||
<el-option label="PM" value="PM" />
|
||||
<el-option label="CRA" value="CRA" />
|
||||
<el-option label="PV" value="PV" />
|
||||
<el-option label="IMP" value="IMP" />
|
||||
<el-option label="ADMIN" value="ADMIN" />
|
||||
<el-option :label="TEXT.enums.userRole.PM" value="PM" />
|
||||
<el-option :label="TEXT.enums.userRole.CRA" value="CRA" />
|
||||
<el-option :label="TEXT.enums.userRole.PV" value="PV" />
|
||||
<el-option :label="TEXT.enums.userRole.IMP" value="IMP" />
|
||||
<el-option :label="TEXT.enums.userRole.ADMIN" value="ADMIN" />
|
||||
</el-select>
|
||||
<span v-if="scope.row.effectiveStatus === 'DISABLED_GLOBAL'" class="hint">该账号已在系统级被禁用,无法在任何项目中使用</span>
|
||||
<el-button link type="danger" size="small" @click="onDelete(scope.row)">删除</el-button>
|
||||
<span v-if="scope.row.effectiveStatus === 'DISABLED_GLOBAL'" class="hint">{{ TEXT.modules.adminProjectMembers.disabledGlobalDesc }}</span>
|
||||
<el-button link type="danger" size="small" @click="onDelete(scope.row)">{{ TEXT.common.actions.delete }}</el-button>
|
||||
<el-button
|
||||
link
|
||||
:type="scope.row.is_active ? 'danger' : 'primary'"
|
||||
@@ -57,17 +57,17 @@
|
||||
:disabled="scope.row.effectiveStatus === 'DISABLED_GLOBAL'"
|
||||
@click="toggleActive(scope.row)"
|
||||
>
|
||||
{{ scope.row.is_active ? "停用" : "启用" }}
|
||||
{{ scope.row.is_active ? TEXT.common.actions.disable : TEXT.common.actions.enable }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog title="新增成员" width="520px" v-model="addVisible" :close-on-click-modal="false">
|
||||
<el-dialog :title="TEXT.modules.adminProjectMembers.newTitle" width="520px" v-model="addVisible" :close-on-click-modal="false">
|
||||
<el-form :model="newMember" label-width="120px" ref="addFormRef" :rules="addRules">
|
||||
<el-form-item label="用户" prop="user_id">
|
||||
<el-select v-model="newMember.user_id" filterable placeholder="请选择用户">
|
||||
<el-form-item :label="TEXT.modules.adminProjectMembers.user" prop="user_id">
|
||||
<el-select v-model="newMember.user_id" filterable :placeholder="TEXT.modules.adminProjectMembers.userPlaceholder">
|
||||
<el-option
|
||||
v-for="user in availableUsers"
|
||||
:key="user.id"
|
||||
@@ -77,19 +77,19 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色" prop="role_in_study">
|
||||
<el-select v-model="newMember.role_in_study" placeholder="请选择角色">
|
||||
<el-option label="PM" value="PM" />
|
||||
<el-option label="CRA" value="CRA" />
|
||||
<el-option label="PV" value="PV" />
|
||||
<el-option label="IMP" value="IMP" />
|
||||
<el-option label="ADMIN" value="ADMIN" />
|
||||
<el-form-item :label="TEXT.modules.adminProjectMembers.projectRole" prop="role_in_study">
|
||||
<el-select v-model="newMember.role_in_study" :placeholder="TEXT.modules.adminProjectMembers.rolePlaceholder">
|
||||
<el-option :label="TEXT.enums.userRole.PM" value="PM" />
|
||||
<el-option :label="TEXT.enums.userRole.CRA" value="CRA" />
|
||||
<el-option :label="TEXT.enums.userRole.PV" value="PV" />
|
||||
<el-option :label="TEXT.enums.userRole.IMP" value="IMP" />
|
||||
<el-option :label="TEXT.enums.userRole.ADMIN" value="ADMIN" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="addVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="adding" @click="submitAdd">保存</el-button>
|
||||
<el-button @click="addVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" :loading="adding" @click="submitAdd">{{ TEXT.common.actions.save }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@@ -106,7 +106,8 @@ import type { Study, StudyMember, UserInfo } from "../../types/api";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
import { evaluateAction } from "../../guards/actionGuard";
|
||||
import { logAudit } from "../../audit";
|
||||
import { displayDateTime } from "../../utils/display";
|
||||
import { displayDateTime, displayEnum } from "../../utils/display";
|
||||
import { TEXT, requiredMessage } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const projectId = computed(() => route.params.projectId as string);
|
||||
@@ -125,8 +126,8 @@ const newMember = reactive({
|
||||
});
|
||||
|
||||
const addRules = reactive<FormRules>({
|
||||
user_id: [{ required: true, message: "请选择用户", trigger: "change" }],
|
||||
role_in_study: [{ required: true, message: "请选择角色", trigger: "change" }],
|
||||
user_id: [{ required: true, message: requiredMessage(TEXT.modules.adminProjectMembers.user), trigger: "change" }],
|
||||
role_in_study: [{ required: true, message: requiredMessage(TEXT.modules.adminProjectMembers.projectRole), trigger: "change" }],
|
||||
});
|
||||
|
||||
const loadProject = async () => {
|
||||
@@ -146,7 +147,7 @@ const loadMembers = async () => {
|
||||
const { data } = await listMembers(projectId.value, { limit: 500 });
|
||||
members.value = Array.isArray(data) ? data : data.items || [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "成员列表加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminProjectMembers.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -187,7 +188,7 @@ const submitAdd = async () => {
|
||||
target: { projectId: projectId.value },
|
||||
});
|
||||
if (!decision.allowed) {
|
||||
ElMessage.warning(decision.reason || "无权限执行该操作");
|
||||
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
|
||||
logAudit(decision.auditType, {
|
||||
targetId: projectId.value,
|
||||
targetName: project.value?.name,
|
||||
@@ -200,7 +201,7 @@ const submitAdd = async () => {
|
||||
adding.value = true;
|
||||
try {
|
||||
await addMember(projectId.value, newMember);
|
||||
ElMessage.success("成员已添加");
|
||||
ElMessage.success(TEXT.modules.adminProjectMembers.addSuccess);
|
||||
addVisible.value = false;
|
||||
loadMembers();
|
||||
logAudit("PROJECT_MEMBER_UPDATED", {
|
||||
@@ -210,7 +211,7 @@ const submitAdd = async () => {
|
||||
severity: "normal",
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "添加失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminProjectMembers.addFailed);
|
||||
logAudit("PROJECT_MEMBER_UPDATED", {
|
||||
targetId: projectId.value,
|
||||
targetName: project.value?.name,
|
||||
@@ -231,7 +232,7 @@ const updateRole = async (memberId: string, role: string) => {
|
||||
target: { projectId: projectId.value, memberId },
|
||||
});
|
||||
if (!decision.allowed) {
|
||||
ElMessage.warning(decision.reason || "无权限执行该操作");
|
||||
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
|
||||
logAudit(decision.auditType, {
|
||||
targetId: projectId.value,
|
||||
targetName: project.value?.name,
|
||||
@@ -242,7 +243,7 @@ const updateRole = async (memberId: string, role: string) => {
|
||||
}
|
||||
try {
|
||||
await updateMember(projectId.value, memberId, { role_in_study: role });
|
||||
ElMessage.success("角色已更新");
|
||||
ElMessage.success(TEXT.modules.adminProjectMembers.roleUpdated);
|
||||
loadMembers();
|
||||
logAudit("PROJECT_MEMBER_UPDATED", {
|
||||
targetId: projectId.value,
|
||||
@@ -251,7 +252,7 @@ const updateRole = async (memberId: string, role: string) => {
|
||||
severity: "normal",
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "更新失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.updateFailed);
|
||||
loadMembers();
|
||||
logAudit("PROJECT_MEMBER_UPDATED", {
|
||||
targetId: projectId.value,
|
||||
@@ -271,7 +272,7 @@ const toggleActive = async (row: StudyMember) => {
|
||||
target: { projectId: projectId.value, memberId: row.id },
|
||||
});
|
||||
if (!decision.allowed) {
|
||||
ElMessage.warning(decision.reason || "无权限执行该操作");
|
||||
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
|
||||
logAudit(decision.auditType, {
|
||||
targetId: projectId.value,
|
||||
targetName: project.value?.name,
|
||||
@@ -281,14 +282,14 @@ const toggleActive = async (row: StudyMember) => {
|
||||
return;
|
||||
}
|
||||
if (row.is_active) {
|
||||
const ok = await ElMessageBox.confirm("该操作将影响项目成员,请确认是否继续", "停用成员", {
|
||||
const ok = await ElMessageBox.confirm(TEXT.modules.adminProjectMembers.disableConfirm, TEXT.modules.adminProjectMembers.disableTitle, {
|
||||
type: "warning",
|
||||
confirmButtonText: "确认",
|
||||
confirmButtonText: TEXT.common.actions.confirm,
|
||||
}).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await updateMember(projectId.value, row.id, { is_active: false });
|
||||
ElMessage.success("成员已停用");
|
||||
ElMessage.success(TEXT.modules.adminProjectMembers.disableSuccess);
|
||||
loadMembers();
|
||||
logAudit("PROJECT_MEMBER_UPDATED", {
|
||||
targetId: projectId.value,
|
||||
@@ -298,7 +299,7 @@ const toggleActive = async (row: StudyMember) => {
|
||||
severity: "normal",
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "操作失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.actionFailed);
|
||||
logAudit("PROJECT_MEMBER_UPDATED", {
|
||||
targetId: projectId.value,
|
||||
targetName: project.value?.name,
|
||||
@@ -311,7 +312,7 @@ const toggleActive = async (row: StudyMember) => {
|
||||
} else {
|
||||
try {
|
||||
await updateMember(projectId.value, row.id, { is_active: true });
|
||||
ElMessage.success("成员已启用");
|
||||
ElMessage.success(TEXT.modules.adminProjectMembers.enableSuccess);
|
||||
loadMembers();
|
||||
logAudit("PROJECT_MEMBER_UPDATED", {
|
||||
targetId: projectId.value,
|
||||
@@ -321,7 +322,7 @@ const toggleActive = async (row: StudyMember) => {
|
||||
severity: "normal",
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "操作失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.actionFailed);
|
||||
logAudit("PROJECT_MEMBER_UPDATED", {
|
||||
targetId: projectId.value,
|
||||
targetName: project.value?.name,
|
||||
@@ -342,7 +343,7 @@ const onDelete = async (row: StudyMember) => {
|
||||
target: { projectId: projectId.value, memberId: row.id },
|
||||
});
|
||||
if (!decision.allowed) {
|
||||
ElMessage.warning(decision.reason || "无权限执行该操作");
|
||||
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
|
||||
logAudit(decision.auditType, {
|
||||
targetId: projectId.value,
|
||||
targetName: project.value?.name,
|
||||
@@ -351,16 +352,16 @@ const onDelete = async (row: StudyMember) => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
const ok = await ElMessageBox.confirm("确认将该账号从本项目移除?账号本身不会被删除。", "删除成员", {
|
||||
const ok = await ElMessageBox.confirm(TEXT.modules.adminProjectMembers.removeConfirm, TEXT.modules.adminProjectMembers.removeTitle, {
|
||||
type: "warning",
|
||||
confirmButtonText: "确认",
|
||||
cancelButtonText: "取消",
|
||||
confirmButtonText: TEXT.common.actions.confirm,
|
||||
cancelButtonText: TEXT.common.actions.cancel,
|
||||
}).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await removeMember(projectId.value, row.id);
|
||||
members.value = members.value.filter((m) => m.id !== row.id);
|
||||
ElMessage.success("已从项目移除");
|
||||
ElMessage.success(TEXT.modules.adminProjectMembers.removeSuccess);
|
||||
logAudit("PROJECT_MEMBER_UPDATED", {
|
||||
targetId: projectId.value,
|
||||
targetName: project.value?.name,
|
||||
@@ -369,7 +370,7 @@ const onDelete = async (row: StudyMember) => {
|
||||
severity: "normal",
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "删除失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
|
||||
logAudit("PROJECT_MEMBER_UPDATED", {
|
||||
targetId: projectId.value,
|
||||
targetName: project.value?.name,
|
||||
|
||||
@@ -3,37 +3,37 @@
|
||||
<el-card>
|
||||
<div class="header">
|
||||
<div>
|
||||
<h3>项目治理</h3>
|
||||
<p class="sub">项目是独立业务实体,账号加入项目后才成为项目成员(角色在成员配置中设置)</p>
|
||||
<h3>{{ TEXT.modules.adminProjects.title }}</h3>
|
||||
<p class="sub">{{ TEXT.modules.adminProjects.subtitle }}</p>
|
||||
</div>
|
||||
<el-button type="primary" @click="openCreate">新建项目</el-button>
|
||||
<el-button type="primary" @click="openCreate">{{ TEXT.common.actions.add }}{{ TEXT.modules.adminProjects.projectLabel }}</el-button>
|
||||
</div>
|
||||
<el-table :data="projects" v-loading="loading" stripe>
|
||||
<el-table-column prop="code" label="项目编号" width="140">
|
||||
<el-table-column prop="code" :label="TEXT.modules.adminProjects.projectCode" width="140">
|
||||
<template #default="scope">
|
||||
<el-link type="primary" @click="goDetail(scope.row)">{{ scope.row.code }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="项目名称" min-width="200">
|
||||
<el-table-column prop="name" :label="TEXT.common.fields.projectName" min-width="200">
|
||||
<template #default="scope">
|
||||
<el-link type="primary" @click="goDetail(scope.row)">{{ scope.row.name }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sponsor" label="申办方" min-width="160" />
|
||||
<el-table-column prop="status" label="状态" width="120">
|
||||
<el-table-column prop="sponsor" :label="TEXT.common.fields.sponsor" min-width="160" />
|
||||
<el-table-column prop="status" :label="TEXT.common.fields.status" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="statusTag(scope.row.status)">{{ statusLabel(scope.row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="200">
|
||||
<el-table-column prop="created_at" :label="TEXT.modules.adminUsers.createdAt" width="200">
|
||||
<template #default="scope">{{ displayDateTime(scope.row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="320" fixed="right">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="320" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="openEdit(scope.row)">编辑</el-button>
|
||||
<el-button link type="primary" size="small" @click="goMembers(scope.row)">项目账号/成员配置</el-button>
|
||||
<el-button link type="primary" size="small" @click="goSites(scope.row)">中心管理</el-button>
|
||||
<el-button link type="success" size="small" @click="enterStudy(scope.row)">进入项目</el-button>
|
||||
<el-button link type="primary" size="small" @click="openEdit(scope.row)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button link type="primary" size="small" @click="goMembers(scope.row)">{{ TEXT.modules.adminProjects.members }}</el-button>
|
||||
<el-button link type="primary" size="small" @click="goSites(scope.row)">{{ TEXT.modules.adminProjects.sites }}</el-button>
|
||||
<el-button link type="success" size="small" @click="enterStudy(scope.row)">{{ TEXT.modules.adminProjects.enter }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -51,6 +51,7 @@ import type { Study } from "../../types/api";
|
||||
import ProjectForm from "./ProjectForm.vue";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { displayDateTime } from "../../utils/display";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const projects = ref<Study[]>([]);
|
||||
const loading = ref(false);
|
||||
@@ -66,7 +67,7 @@ const loadProjects = async () => {
|
||||
const items = (data as any).items || [];
|
||||
projects.value = items;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "项目列表加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminProjects.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -100,11 +101,7 @@ const goDetail = (row: Study) => {
|
||||
};
|
||||
|
||||
const statusLabel = (status: string) =>
|
||||
({
|
||||
DRAFT: "草稿",
|
||||
ACTIVE: "进行中",
|
||||
CLOSED: "已关闭",
|
||||
}[status] || status);
|
||||
TEXT.enums.projectStatus[status as keyof typeof TEXT.enums.projectStatus] || status;
|
||||
|
||||
const statusTag = (status: string) =>
|
||||
({
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<el-dialog title="CRA 绑定" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<div class="tip">中心:{{ site?.name }}</div>
|
||||
<el-dialog :title="TEXT.modules.adminSites.craBindTitle" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<div class="tip">{{ TEXT.modules.adminSites.sitePrefix }}{{ site?.name }}</div>
|
||||
<el-form label-width="120px">
|
||||
<el-form-item label="选择 CRA">
|
||||
<el-select v-model="selectedCras" multiple filterable placeholder="请选择 CRA 账号" style="width: 100%">
|
||||
<el-form-item :label="TEXT.modules.adminSites.craSelect">
|
||||
<el-select v-model="selectedCras" multiple filterable :placeholder="TEXT.modules.adminSites.craSelectPlaceholder" style="width: 100%">
|
||||
<el-option v-for="user in craUsers" :key="user.id" :label="user.username" :value="user.id" />
|
||||
</el-select>
|
||||
<div class="hint">支持多选,保存后将以用户名写入中心联系人字段</div>
|
||||
<div class="hint">{{ TEXT.modules.adminSites.craHint }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visibleProxy = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="onSave">保存绑定</el-button>
|
||||
<el-button @click="visibleProxy = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="onSave">{{ TEXT.modules.adminSites.craSave }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -24,6 +24,7 @@ import type { Site, UserInfo } from "../../types/api";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
import { evaluateAction } from "../../guards/actionGuard";
|
||||
import { logAudit } from "../../audit";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
@@ -83,7 +84,7 @@ const onSave = async () => {
|
||||
target: { siteId: props.site.id, studyId: props.studyId },
|
||||
});
|
||||
if (!decision.allowed) {
|
||||
ElMessage.warning(decision.reason || "无权限执行该操作");
|
||||
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
|
||||
logAudit(decision.auditType, {
|
||||
targetId: props.site.id,
|
||||
targetName: props.site.name,
|
||||
@@ -98,7 +99,7 @@ const onSave = async () => {
|
||||
.map((id) => props.craUsers.find((u) => u.id === id)?.username || id)
|
||||
.filter(Boolean);
|
||||
await updateSite(props.studyId, props.site.id, { contact: names.join(",") });
|
||||
ElMessage.success("绑定已保存");
|
||||
ElMessage.success(TEXT.modules.adminSites.craSaveSuccess);
|
||||
emit("saved");
|
||||
visibleProxy.value = false;
|
||||
logAudit("SITE_CRA_BOUND", {
|
||||
@@ -109,7 +110,7 @@ const onSave = async () => {
|
||||
severity: "normal",
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
logAudit("SITE_CRA_BOUND", {
|
||||
targetId: props.site.id,
|
||||
targetName: props.site.name,
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<template>
|
||||
<el-dialog :title="site ? '编辑中心' : '新建中心'" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<el-dialog :title="site ? TEXT.modules.adminSites.editTitle : TEXT.modules.adminSites.newTitle" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-form-item label="中心名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入中心名称" />
|
||||
<el-form-item :label="TEXT.common.fields.siteName" prop="name">
|
||||
<el-input v-model="form.name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.siteName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="城市" prop="city">
|
||||
<el-input v-model="form.city" placeholder="所在城市(可选)" />
|
||||
<el-form-item :label="TEXT.common.fields.city" prop="city">
|
||||
<el-input v-model="form.city" :placeholder="TEXT.modules.adminSites.cityPlaceholder" />
|
||||
</el-form-item>
|
||||
<el-form-item label="PI" prop="pi_name">
|
||||
<el-input v-model="form.pi_name" placeholder="PI(可选)" />
|
||||
<el-form-item :label="TEXT.modules.adminSites.piLabel" prop="pi_name">
|
||||
<el-input v-model="form.pi_name" :placeholder="TEXT.modules.adminSites.piPlaceholder" />
|
||||
</el-form-item>
|
||||
<el-form-item label="电话" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="电话(可选)" />
|
||||
<el-form-item :label="TEXT.common.fields.phone" prop="phone">
|
||||
<el-input v-model="form.phone" :placeholder="TEXT.modules.adminSites.phonePlaceholder" />
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人" prop="contact">
|
||||
<el-form-item :label="TEXT.common.fields.contact" prop="contact">
|
||||
<el-select
|
||||
v-model="form.craSelections"
|
||||
multiple
|
||||
filterable
|
||||
placeholder="选择项目成员作为负责人,可多选"
|
||||
:placeholder="TEXT.modules.adminSites.contactPlaceholder"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in craOptions"
|
||||
@@ -28,13 +28,13 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="site" label="状态" prop="is_active">
|
||||
<el-switch v-model="form.is_active" active-text="启用" inactive-text="停用" />
|
||||
<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>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visibleProxy = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">保存</el-button>
|
||||
<el-button @click="visibleProxy = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ TEXT.common.actions.save }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -47,6 +47,7 @@ import type { Site } from "../../types/api";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
import { evaluateAction } from "../../guards/actionGuard";
|
||||
import { logAudit } from "../../audit";
|
||||
import { TEXT, requiredMessage } from "../../locales";
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
@@ -80,7 +81,7 @@ const form = reactive({
|
||||
});
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: "请输入中心名称", trigger: "blur" }],
|
||||
name: [{ required: true, message: requiredMessage(TEXT.common.fields.siteName), trigger: "blur" }],
|
||||
});
|
||||
|
||||
const craOptions = computed(() => {
|
||||
@@ -138,7 +139,7 @@ const onSubmit = async () => {
|
||||
target: { siteId: props.site?.id, studyId: props.studyId },
|
||||
});
|
||||
if (!decision.allowed) {
|
||||
ElMessage.warning(decision.reason || "无权限执行该操作");
|
||||
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
|
||||
logAudit(decision.auditType, {
|
||||
targetId: props.site?.id || props.studyId,
|
||||
targetName: props.site?.name,
|
||||
@@ -159,7 +160,7 @@ const onSubmit = async () => {
|
||||
contact,
|
||||
is_active: form.is_active,
|
||||
});
|
||||
ElMessage.success("中心已更新");
|
||||
ElMessage.success(TEXT.modules.adminSites.updateSuccess);
|
||||
logAudit("SITE_STATUS_CHANGED", {
|
||||
targetId: props.site.id,
|
||||
targetName: props.site.name,
|
||||
@@ -173,7 +174,7 @@ const onSubmit = async () => {
|
||||
pi_name: form.pi_name,
|
||||
contact,
|
||||
});
|
||||
ElMessage.success("中心已创建");
|
||||
ElMessage.success(TEXT.modules.adminSites.createSuccess);
|
||||
logAudit("SITE_STATUS_CHANGED", {
|
||||
targetId: props.studyId,
|
||||
targetName: form.name,
|
||||
@@ -184,7 +185,7 @@ const onSubmit = async () => {
|
||||
emit("saved");
|
||||
visibleProxy.value = false;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
logAudit("SITE_STATUS_CHANGED", {
|
||||
targetId: props.site?.id || props.studyId,
|
||||
targetName: props.site?.name || form.name,
|
||||
|
||||
@@ -3,41 +3,41 @@
|
||||
<el-card>
|
||||
<div class="header">
|
||||
<div>
|
||||
<h3>中心管理</h3>
|
||||
<div class="sub" v-if="project">项目:{{ project.code }} - {{ project.name }}</div>
|
||||
<h3>{{ TEXT.modules.adminSites.title }}</h3>
|
||||
<div class="sub" v-if="project">{{ TEXT.modules.adminSites.projectPrefix }}{{ project.code }} - {{ project.name }}</div>
|
||||
</div>
|
||||
<el-button type="primary" @click="openCreate">新建中心</el-button>
|
||||
<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>
|
||||
<el-table-column prop="name" label="中心名称" min-width="180" />
|
||||
<el-table-column prop="city" label="城市" width="140" />
|
||||
<el-table-column prop="pi_name" label="PI" width="160" />
|
||||
<el-table-column label="电话" width="160">
|
||||
<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">
|
||||
<template #default="scope">
|
||||
{{ phoneText(scope.row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="contact" label="负责人" min-width="180">
|
||||
<el-table-column prop="contact" :label="TEXT.common.fields.contact" min-width="180">
|
||||
<template #default="scope">
|
||||
{{ contactLabel(scope.row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="120">
|
||||
<el-table-column :label="TEXT.common.fields.status" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag type="success" v-if="scope.row.is_active">启用</el-tag>
|
||||
<el-tag type="danger" v-else>停用</el-tag>
|
||||
<el-tag type="success" v-if="scope.row.is_active">{{ TEXT.common.actions.enable }}</el-tag>
|
||||
<el-tag type="danger" v-else>{{ TEXT.common.actions.disable }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="220" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="openEdit(scope.row)">编辑</el-button>
|
||||
<el-button link type="primary" size="small" @click="openEdit(scope.row)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button
|
||||
link
|
||||
:type="scope.row.is_active ? 'danger' : 'primary'"
|
||||
size="small"
|
||||
@click="toggleSite(scope.row)"
|
||||
>
|
||||
{{ scope.row.is_active ? "停用" : "启用" }}
|
||||
{{ scope.row.is_active ? TEXT.common.actions.disable : TEXT.common.actions.enable }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -68,6 +68,7 @@ import type { Site, Study, UserInfo } from "../../types/api";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
import { evaluateAction } from "../../guards/actionGuard";
|
||||
import { logAudit } from "../../audit";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const projectId = computed(() => route.params.projectId as string);
|
||||
@@ -98,7 +99,7 @@ const loadSites = async () => {
|
||||
const { data } = await fetchSites(projectId.value);
|
||||
sites.value = Array.isArray(data) ? data : (data as any).items || [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "中心列表加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminSites.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -135,16 +136,16 @@ const memberNameMap = computed(() => {
|
||||
});
|
||||
|
||||
const contactLabel = (row: any) => {
|
||||
if (!row?.contact) return "—";
|
||||
if (!row?.contact) return TEXT.common.fallback;
|
||||
return String(row.contact)
|
||||
.split(",")
|
||||
.map((c) => c.trim())
|
||||
.filter(Boolean)
|
||||
.map((c) => memberNameMap.value[c] || c)
|
||||
.join("、") || "—";
|
||||
.join("、") || TEXT.common.fallback;
|
||||
};
|
||||
|
||||
const phoneText = (row: any) => row?.phone || row?.contact_phone || "—";
|
||||
const phoneText = (row: any) => row?.phone || row?.contact_phone || TEXT.common.fallback;
|
||||
|
||||
const openCreate = () => {
|
||||
loadUsers();
|
||||
@@ -168,7 +169,7 @@ const toggleSite = async (row: Site) => {
|
||||
target: { siteId: row.id, studyId: projectId.value },
|
||||
});
|
||||
if (!decision.allowed) {
|
||||
ElMessage.warning(decision.reason || "无权限执行该操作");
|
||||
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
|
||||
logAudit(decision.auditType, {
|
||||
targetId: row.id,
|
||||
targetName: row.name,
|
||||
@@ -178,15 +179,15 @@ const toggleSite = async (row: Site) => {
|
||||
return;
|
||||
}
|
||||
if (row.is_active) {
|
||||
const ok = await ElMessageBox.confirm("该操作将影响中心数据,请确认是否继续", "停用中心", {
|
||||
const ok = await ElMessageBox.confirm(TEXT.modules.adminSites.disableConfirm, TEXT.modules.adminSites.disableTitle, {
|
||||
type: "warning",
|
||||
confirmButtonText: "确认",
|
||||
confirmButtonText: TEXT.common.actions.confirm,
|
||||
}).catch(() => null);
|
||||
if (!ok) return;
|
||||
}
|
||||
try {
|
||||
await updateSite(projectId.value, row.id, { is_active: !row.is_active });
|
||||
ElMessage.success(row.is_active ? "已停用" : "已启用");
|
||||
ElMessage.success(row.is_active ? TEXT.modules.adminSites.disableSuccess : TEXT.modules.adminSites.enableSuccess);
|
||||
loadSites();
|
||||
logAudit("SITE_STATUS_CHANGED", {
|
||||
targetId: row.id,
|
||||
@@ -196,7 +197,7 @@ const toggleSite = async (row: Site) => {
|
||||
severity: "normal",
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "操作失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.actionFailed);
|
||||
logAudit("SITE_STATUS_CHANGED", {
|
||||
targetId: row.id,
|
||||
targetName: row.name,
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
<template>
|
||||
<el-dialog :title="user ? '编辑用户' : '新建用户'" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<el-dialog :title="user ? TEXT.modules.adminUsers.editTitle : TEXT.modules.adminUsers.newTitle" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="form.email" :disabled="!!user" placeholder="请输入邮箱" />
|
||||
<el-form-item :label="TEXT.common.fields.email" prop="email">
|
||||
<el-input v-model="form.email" :disabled="!!user" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.email" />
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名" prop="full_name">
|
||||
<el-input v-model="form.full_name" placeholder="请输入姓名" />
|
||||
<el-form-item :label="TEXT.common.fields.name" prop="full_name">
|
||||
<el-input v-model="form.full_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门" prop="department">
|
||||
<el-input v-model="form.department" placeholder="请输入部门" />
|
||||
<el-form-item :label="TEXT.common.fields.department" prop="department">
|
||||
<el-input v-model="form.department" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.department" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="user ? '新密码(可选)' : '初始密码'" prop="password">
|
||||
<el-input v-model="form.password" type="password" show-password :placeholder="user ? '留空则不修改密码' : '请输入初始密码'" />
|
||||
<el-form-item :label="user ? TEXT.modules.adminUsers.passwordOptional : TEXT.modules.adminUsers.passwordInitial" prop="password">
|
||||
<el-input v-model="form.password" type="password" show-password :placeholder="user ? TEXT.modules.adminUsers.passwordOptionalHint : TEXT.modules.adminUsers.passwordInitialHint" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="user" label="账号状态" prop="is_active">
|
||||
<el-switch v-model="form.is_active" active-text="启用" inactive-text="禁用" :disabled="isLastAdmin" />
|
||||
<el-form-item v-if="user" :label="TEXT.modules.adminUsers.accountStatus" prop="is_active">
|
||||
<el-switch v-model="form.is_active" :active-text="TEXT.common.actions.enable" :inactive-text="TEXT.common.actions.disable" :disabled="isLastAdmin" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visibleProxy = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">保存</el-button>
|
||||
<el-button @click="visibleProxy = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ TEXT.common.actions.save }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -29,6 +29,7 @@ import { computed, reactive, ref, watch } from "vue";
|
||||
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
|
||||
import { createUser, updateUser } from "../../api/users";
|
||||
import type { UserInfo } from "../../types/api";
|
||||
import { TEXT, requiredMessage } from "../../locales";
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
@@ -60,16 +61,16 @@ const defaultRole = "PV";
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
email: [
|
||||
{ required: true, message: "请输入邮箱", trigger: "blur" },
|
||||
{ type: "email", message: "邮箱格式不正确", trigger: "blur" },
|
||||
{ required: true, message: requiredMessage(TEXT.common.fields.email), trigger: "blur" },
|
||||
{ type: "email", message: TEXT.common.validation.invalidEmail, trigger: "blur" },
|
||||
],
|
||||
full_name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
|
||||
department: [{ required: true, message: "请输入部门", trigger: "blur" }],
|
||||
full_name: [{ required: true, message: requiredMessage(TEXT.common.fields.name), trigger: "blur" }],
|
||||
department: [{ required: true, message: requiredMessage(TEXT.common.fields.department), trigger: "blur" }],
|
||||
password: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (!props.user && !value) {
|
||||
callback(new Error("请输入初始密码"));
|
||||
callback(new Error(TEXT.modules.adminUsers.passwordInitialRequired));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
@@ -113,7 +114,7 @@ const onSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
await formRef.value.validate();
|
||||
if (props.user?.role === "ADMIN" && (props.adminCount || 0) <= 1 && !form.is_active) {
|
||||
ElMessage.warning("至少保留一个管理员账号,不能禁用该账号");
|
||||
ElMessage.warning(TEXT.modules.adminUsers.keepAdminWarning);
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
@@ -125,7 +126,7 @@ const onSubmit = async () => {
|
||||
department: form.department,
|
||||
password: form.password || undefined,
|
||||
});
|
||||
ElMessage.success("用户已更新");
|
||||
ElMessage.success(TEXT.modules.adminUsers.updateSuccess);
|
||||
} else {
|
||||
await createUser({
|
||||
email: form.email,
|
||||
@@ -134,12 +135,12 @@ const onSubmit = async () => {
|
||||
role: defaultRole,
|
||||
password: form.password,
|
||||
});
|
||||
ElMessage.success("用户已创建");
|
||||
ElMessage.success(TEXT.modules.adminUsers.createSuccess);
|
||||
}
|
||||
emit("saved");
|
||||
visibleProxy.value = false;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<template>
|
||||
<el-dialog title="重置密码" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<el-dialog :title="TEXT.modules.adminUsers.resetTitle" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<el-alert
|
||||
type="warning"
|
||||
show-icon
|
||||
title="该操作将影响用户账号,请确认是否继续"
|
||||
description="系统将生成临时密码,需再次输入用户名才能提交。"
|
||||
:title="TEXT.modules.adminUsers.resetWarningTitle"
|
||||
:description="TEXT.modules.adminUsers.resetWarningDesc"
|
||||
class="mb-12"
|
||||
/>
|
||||
<div class="info">重置对象:<strong>{{ user?.username }}</strong></div>
|
||||
<div class="info">{{ TEXT.modules.adminUsers.resetTarget }}<strong>{{ user?.username }}</strong></div>
|
||||
<el-form ref="formRef" :model="form" label-width="140px" :rules="rules">
|
||||
<el-form-item label="确认用户名" prop="confirmUsername">
|
||||
<el-input v-model="form.confirmUsername" placeholder="请输入用户名以确认" />
|
||||
<el-form-item :label="TEXT.modules.adminUsers.confirmUsername" prop="confirmUsername">
|
||||
<el-input v-model="form.confirmUsername" :placeholder="TEXT.modules.adminUsers.confirmUsernameHint" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码方式" prop="mode">
|
||||
<el-form-item :label="TEXT.modules.adminUsers.passwordMode" prop="mode">
|
||||
<el-radio-group v-model="form.mode">
|
||||
<el-radio label="auto">系统生成临时密码</el-radio>
|
||||
<el-radio label="manual">手动输入新密码</el-radio>
|
||||
<el-radio label="auto">{{ TEXT.modules.adminUsers.passwordAuto }}</el-radio>
|
||||
<el-radio label="manual">{{ TEXT.modules.adminUsers.passwordManual }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="form.mode === 'auto' ? '临时密码' : '新密码'" prop="passwordValue">
|
||||
<el-form-item :label="form.mode === 'auto' ? TEXT.modules.adminUsers.tempPassword : TEXT.modules.adminUsers.newPassword" prop="passwordValue">
|
||||
<el-input
|
||||
v-if="form.mode === 'auto'"
|
||||
v-model="form.tempPassword"
|
||||
@@ -27,7 +27,7 @@
|
||||
readonly
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="regenerate">重新生成</el-button>
|
||||
<el-button @click="regenerate">{{ TEXT.modules.adminUsers.regenerate }}</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-input
|
||||
@@ -35,14 +35,14 @@
|
||||
v-model="form.manualPassword"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入新密码"
|
||||
:placeholder="TEXT.modules.adminUsers.manualPasswordHint"
|
||||
/>
|
||||
<div class="hint">不会明文展示,请通过安全渠道告知用户</div>
|
||||
<div class="hint">{{ TEXT.modules.adminUsers.passwordHint }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visibleProxy = false">取消</el-button>
|
||||
<el-button type="danger" :disabled="!canSubmit" :loading="submitting" @click="onSubmit">确认重置</el-button>
|
||||
<el-button @click="visibleProxy = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="danger" :disabled="!canSubmit" :loading="submitting" @click="onSubmit">{{ TEXT.modules.adminUsers.resetConfirm }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -52,6 +52,7 @@ import { computed, reactive, ref, watch } from "vue";
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from "element-plus";
|
||||
import { updateUser } from "../../api/users";
|
||||
import type { UserInfo } from "../../types/api";
|
||||
import { TEXT, requiredMessage } from "../../locales";
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
@@ -78,17 +79,17 @@ const form = reactive({
|
||||
});
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
confirmUsername: [{ required: true, message: "请再次输入用户名", trigger: "blur" }],
|
||||
mode: [{ required: true, message: "请选择密码方式", trigger: "change" }],
|
||||
confirmUsername: [{ required: true, message: requiredMessage(TEXT.modules.adminUsers.confirmUsername), trigger: "blur" }],
|
||||
mode: [{ required: true, message: requiredMessage(TEXT.modules.adminUsers.passwordMode), trigger: "change" }],
|
||||
passwordValue: [
|
||||
{
|
||||
validator: (_rule, _value, callback) => {
|
||||
if (form.mode === "auto" && !form.tempPassword) {
|
||||
callback(new Error("临时密码生成异常"));
|
||||
callback(new Error(TEXT.modules.adminUsers.tempPasswordError));
|
||||
return;
|
||||
}
|
||||
if (form.mode === "manual" && !form.manualPassword) {
|
||||
callback(new Error("请输入新密码"));
|
||||
callback(new Error(TEXT.modules.adminUsers.manualPasswordRequired));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
@@ -127,16 +128,16 @@ const onSubmit = async () => {
|
||||
if (!props.user) return;
|
||||
await formRef.value?.validate();
|
||||
if (form.confirmUsername !== props.user.username) {
|
||||
ElMessage.error("确认用户名不匹配,无法提交");
|
||||
ElMessage.error(TEXT.modules.adminUsers.confirmMismatch);
|
||||
return;
|
||||
}
|
||||
const ok = await ElMessageBox.confirm(
|
||||
"该操作将影响用户账号,请确认是否继续",
|
||||
"确认重置密码",
|
||||
TEXT.modules.adminUsers.resetConfirmPrompt,
|
||||
TEXT.modules.adminUsers.resetConfirmTitle,
|
||||
{
|
||||
type: "warning",
|
||||
confirmButtonText: "确认重置",
|
||||
cancelButtonText: "取消",
|
||||
confirmButtonText: TEXT.modules.adminUsers.resetConfirm,
|
||||
cancelButtonText: TEXT.common.actions.cancel,
|
||||
}
|
||||
).catch(() => null);
|
||||
if (!ok) return;
|
||||
@@ -144,11 +145,11 @@ const onSubmit = async () => {
|
||||
try {
|
||||
const password = form.mode === "manual" ? form.manualPassword : form.tempPassword;
|
||||
await updateUser(props.user.id, { password });
|
||||
ElMessage.success("密码已重置");
|
||||
ElMessage.success(TEXT.modules.adminUsers.resetSuccess);
|
||||
emit("reset");
|
||||
visibleProxy.value = false;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "重置失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminUsers.resetFailed);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
|
||||
@@ -3,28 +3,28 @@
|
||||
<el-card>
|
||||
<div class="header">
|
||||
<div>
|
||||
<h3>账号治理(系统登录账号)</h3>
|
||||
<p class="sub">账号用于登录系统,本身不具备项目或角色权限</p>
|
||||
<h3>{{ TEXT.modules.adminUsers.title }}</h3>
|
||||
<p class="sub">{{ TEXT.modules.adminUsers.subtitle }}</p>
|
||||
</div>
|
||||
<el-button type="primary" @click="openCreate">新建用户</el-button>
|
||||
<el-button type="primary" @click="openCreate">{{ TEXT.common.actions.add }}{{ TEXT.modules.adminUsers.userLabel }}</el-button>
|
||||
</div>
|
||||
<el-table :data="users" stripe v-loading="loading" style="width: 100%">
|
||||
<el-table-column prop="email" label="邮箱" min-width="200" />
|
||||
<el-table-column prop="full_name" label="姓名" min-width="140" />
|
||||
<el-table-column prop="department" label="部门" min-width="140" />
|
||||
<el-table-column label="状态" width="140">
|
||||
<el-table-column prop="email" :label="TEXT.common.fields.email" min-width="200" />
|
||||
<el-table-column prop="full_name" :label="TEXT.common.fields.name" min-width="140" />
|
||||
<el-table-column prop="department" :label="TEXT.common.fields.department" min-width="140" />
|
||||
<el-table-column :label="TEXT.common.fields.status" width="140">
|
||||
<template #default="scope">
|
||||
<el-tag :type="statusType(scope.row.status)">
|
||||
{{ statusLabel(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="200">
|
||||
<el-table-column prop="created_at" :label="TEXT.modules.adminUsers.createdAt" width="200">
|
||||
<template #default="scope">{{ displayDateTime(scope.row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="320" fixed="right">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="320" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="openEdit(scope.row)">编辑</el-button>
|
||||
<el-button link type="primary" size="small" @click="openEdit(scope.row)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button
|
||||
link
|
||||
:type="scope.row.status === 'ACTIVE' ? 'danger' : 'primary'"
|
||||
@@ -32,9 +32,9 @@
|
||||
:disabled="isLastAdmin(scope.row)"
|
||||
@click="toggleStatus(scope.row)"
|
||||
>
|
||||
{{ scope.row.status === 'ACTIVE' ? "禁用" : "启用" }}
|
||||
{{ scope.row.status === 'ACTIVE' ? TEXT.common.actions.disable : TEXT.common.actions.enable }}
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="openReset(scope.row)">重置密码</el-button>
|
||||
<el-button link type="danger" size="small" @click="openReset(scope.row)">{{ TEXT.modules.adminUsers.resetPassword }}</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@@ -42,7 +42,7 @@
|
||||
:disabled="scope.row.id === auth.user?.id"
|
||||
@click="onDelete(scope.row)"
|
||||
>
|
||||
删除
|
||||
{{ TEXT.common.actions.delete }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -72,6 +72,7 @@ import UserForm from "./UserForm.vue";
|
||||
import UserResetPassword from "./UserResetPassword.vue";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
import { displayDateTime } from "../../utils/display";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const users = ref<UserInfo[]>([]);
|
||||
const total = ref(0);
|
||||
@@ -98,7 +99,7 @@ const loadUsers = async () => {
|
||||
total.value = (data as any).total || items.length;
|
||||
activeAdminCount.value = allItems.filter((u: UserInfo) => u.role === "ADMIN" && u.status === "ACTIVE").length;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "用户列表加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminUsers.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -116,26 +117,26 @@ const openEdit = (row: UserInfo) => {
|
||||
|
||||
const toggleStatus = async (row: UserInfo) => {
|
||||
if (isLastAdmin(row)) {
|
||||
ElMessage.warning("至少保留一个管理员账号,不能禁用该账号");
|
||||
ElMessage.warning(TEXT.modules.adminUsers.keepAdminWarning);
|
||||
return;
|
||||
}
|
||||
const disable = row.status === "ACTIVE";
|
||||
const ok = await ElMessageBox.confirm(
|
||||
disable ? "该操作将影响用户账号,请确认是否继续" : "确认启用该用户账号?",
|
||||
disable ? "禁用账号" : "启用账号",
|
||||
disable ? TEXT.modules.adminUsers.disableConfirm : TEXT.modules.adminUsers.enableConfirm,
|
||||
disable ? TEXT.modules.adminUsers.disableTitle : TEXT.modules.adminUsers.enableTitle,
|
||||
{
|
||||
type: disable ? "warning" : "info",
|
||||
confirmButtonText: "确认",
|
||||
cancelButtonText: "取消",
|
||||
confirmButtonText: TEXT.common.actions.confirm,
|
||||
cancelButtonText: TEXT.common.actions.cancel,
|
||||
}
|
||||
).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await updateUser(row.id, { status: disable ? "DISABLED" : "ACTIVE", is_active: !disable });
|
||||
ElMessage.success(disable ? "已禁用" : "已启用");
|
||||
ElMessage.success(disable ? TEXT.modules.adminUsers.disableSuccess : TEXT.modules.adminUsers.enableSuccess);
|
||||
loadUsers();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "操作失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.actionFailed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -146,28 +147,28 @@ const openReset = (row: UserInfo) => {
|
||||
|
||||
const onDelete = async (row: UserInfo) => {
|
||||
const { value, action } = await ElMessageBox.prompt(
|
||||
"删除后账号将无法登录且无法恢复,需先在各项目成员配置中移除该账号。请输入当前管理员密码确认删除。",
|
||||
"危险操作:删除账号",
|
||||
TEXT.modules.adminUsers.deleteConfirm,
|
||||
TEXT.modules.adminUsers.deleteTitle,
|
||||
{
|
||||
inputType: "password",
|
||||
inputPlaceholder: "请输入 admin 密码",
|
||||
confirmButtonText: "确认删除",
|
||||
inputPlaceholder: TEXT.modules.adminUsers.deletePasswordPlaceholder,
|
||||
confirmButtonText: TEXT.modules.adminUsers.deleteConfirmButton,
|
||||
confirmButtonClass: "el-button--danger",
|
||||
cancelButtonText: "取消",
|
||||
cancelButtonText: TEXT.common.actions.cancel,
|
||||
}
|
||||
).catch(() => ({ action: "cancel", value: "" }));
|
||||
if (action !== "confirm" || !value) return;
|
||||
try {
|
||||
await deleteUser(row.id, { admin_password: value });
|
||||
ElMessage.success("账号已删除");
|
||||
ElMessage.success(TEXT.modules.adminUsers.deleteSuccess);
|
||||
loadUsers();
|
||||
} catch (e: any) {
|
||||
const detail = e?.response?.data?.detail || e?.response?.data?.message;
|
||||
if (detail && detail.includes("项目成员")) {
|
||||
if (detail && detail.includes(TEXT.modules.adminProjectMembers.memberLabel)) {
|
||||
ElMessage.closeAll();
|
||||
ElMessage.error(detail);
|
||||
} else {
|
||||
ElMessage.error(detail || "删除失败");
|
||||
ElMessage.error(detail || TEXT.modules.adminUsers.deleteFailed);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -192,15 +193,15 @@ const statusType = (status: string) => {
|
||||
const statusLabel = (status: string) => {
|
||||
switch (status) {
|
||||
case "ACTIVE":
|
||||
return "启用";
|
||||
return TEXT.enums.userStatus.ACTIVE;
|
||||
case "PENDING":
|
||||
return "待审核";
|
||||
return TEXT.enums.userStatus.PENDING;
|
||||
case "REJECTED":
|
||||
return "已拒绝";
|
||||
return TEXT.enums.userStatus.REJECTED;
|
||||
case "DISABLED":
|
||||
return "已停用";
|
||||
return TEXT.enums.userStatus.DISABLED;
|
||||
default:
|
||||
return status || "未知";
|
||||
return status || TEXT.common.fallback;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,27 +2,27 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">运输记录详情</h1>
|
||||
<p class="page-subtitle">查看运输/流向信息与附件</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.drugShipments.detailTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.drugShipments.detailSubtitle }}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="goEdit">编辑</el-button>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button type="primary" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card v-loading="loading">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="类型">{{ detail.direction || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">{{ detail.status || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="分中心">{{ detail.site_name || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="日期">{{ displayDate(detail.ship_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="快递公司">{{ detail.carrier || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="运单号">{{ detail.tracking_no || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="发出方">{{ detail.from_party || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="接收方">{{ detail.to_party || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="物品描述" :span="2">{{ detail.drug_desc || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ detail.remark || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.direction">{{ displayEnum(TEXT.enums.shipmentDirection, detail.direction) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.status">{{ displayEnum(TEXT.enums.shipmentStatus, detail.status) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.site">{{ detail.site_name || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.shipDate">{{ displayDate(detail.ship_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.carrier">{{ detail.carrier || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.trackingNo">{{ detail.tracking_no || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.fromParty">{{ detail.from_party || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.toParty">{{ detail.to_party || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.drugDesc" :span="2">{{ detail.drug_desc || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.remark" :span="2">{{ detail.remark || TEXT.common.fallback }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
@@ -41,8 +41,9 @@ import { useRoute, useRouter } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { getDrugShipment } from "../../api/drugShipments";
|
||||
import { displayDate } from "../../utils/display";
|
||||
import { displayDate, displayEnum } from "../../utils/display";
|
||||
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -71,7 +72,7 @@ const load = async () => {
|
||||
const { data } = await getDrugShipment(studyId, shipmentId);
|
||||
Object.assign(detail, data);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -2,56 +2,56 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">{{ isEdit ? "编辑运输记录" : "新增运输记录" }}</h1>
|
||||
<p class="page-subtitle">记录寄送/回收流向</p>
|
||||
<h1 class="page-title">{{ isEdit ? TEXT.modules.drugShipments.editTitle : TEXT.modules.drugShipments.newTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.drugShipments.formSubtitle }}</p>
|
||||
</div>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card>
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="类型" required>
|
||||
<el-select v-model="form.direction" placeholder="选择类型">
|
||||
<el-option label="寄送" value="寄送" />
|
||||
<el-option label="回收" value="回收" />
|
||||
<el-form-item :label="TEXT.common.fields.direction" required>
|
||||
<el-select v-model="form.direction" :placeholder="TEXT.common.placeholders.select">
|
||||
<el-option :label="TEXT.enums.shipmentDirection.SEND" value="SEND" />
|
||||
<el-option :label="TEXT.enums.shipmentDirection.RETURN" value="RETURN" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="分中心" required>
|
||||
<el-input v-model="form.site_name" placeholder="输入分中心/机构名称" />
|
||||
<el-form-item :label="TEXT.common.fields.site" required>
|
||||
<el-input v-model="form.site_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" />
|
||||
</el-form-item>
|
||||
<el-form-item label="物品描述" required>
|
||||
<el-input v-model="form.drug_desc" type="textarea" :rows="3" placeholder="药品名称/规格/数量等" />
|
||||
<el-form-item :label="TEXT.common.fields.drugDesc" required>
|
||||
<el-input v-model="form.drug_desc" type="textarea" :rows="3" :placeholder="TEXT.common.placeholders.input" />
|
||||
</el-form-item>
|
||||
<el-form-item label="日期">
|
||||
<el-date-picker v-model="form.ship_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.shipDate">
|
||||
<el-date-picker v-model="form.ship_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="快递公司">
|
||||
<el-input v-model="form.carrier" placeholder="输入快递公司" />
|
||||
<el-form-item :label="TEXT.common.fields.carrier">
|
||||
<el-input v-model="form.carrier" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.carrier" />
|
||||
</el-form-item>
|
||||
<el-form-item label="运单号">
|
||||
<el-input v-model="form.tracking_no" placeholder="输入运单号" />
|
||||
<el-form-item :label="TEXT.common.fields.trackingNo">
|
||||
<el-input v-model="form.tracking_no" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.trackingNo" />
|
||||
</el-form-item>
|
||||
<el-form-item label="发出方">
|
||||
<el-input v-model="form.from_party" placeholder="输入发出方" />
|
||||
<el-form-item :label="TEXT.common.fields.fromParty">
|
||||
<el-input v-model="form.from_party" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.fromParty" />
|
||||
</el-form-item>
|
||||
<el-form-item label="接收方">
|
||||
<el-input v-model="form.to_party" placeholder="输入接收方" />
|
||||
<el-form-item :label="TEXT.common.fields.toParty">
|
||||
<el-input v-model="form.to_party" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.toParty" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" required>
|
||||
<el-select v-model="form.status" placeholder="选择状态">
|
||||
<el-option label="待寄出" value="待寄出" />
|
||||
<el-option label="运输中" value="运输中" />
|
||||
<el-option label="已签收" value="已签收" />
|
||||
<el-option label="已回收" value="已回收" />
|
||||
<el-option label="异常" value="异常" />
|
||||
<el-form-item :label="TEXT.common.fields.status" required>
|
||||
<el-select v-model="form.status" :placeholder="TEXT.common.placeholders.select">
|
||||
<el-option :label="TEXT.enums.shipmentStatus.PENDING" value="PENDING" />
|
||||
<el-option :label="TEXT.enums.shipmentStatus.IN_TRANSIT" value="IN_TRANSIT" />
|
||||
<el-option :label="TEXT.enums.shipmentStatus.SIGNED" value="SIGNED" />
|
||||
<el-option :label="TEXT.enums.shipmentStatus.RETURNED" value="RETURNED" />
|
||||
<el-option :label="TEXT.enums.shipmentStatus.EXCEPTION" value="EXCEPTION" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="补充说明" />
|
||||
<el-form-item :label="TEXT.common.fields.remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" :placeholder="TEXT.common.placeholders.input" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submit">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -63,7 +63,7 @@
|
||||
:entity-id="shipmentId"
|
||||
/>
|
||||
<el-card v-else class="hint-card">
|
||||
<StateEmpty description="保存后可上传交接材料" />
|
||||
<StateEmpty :description="TEXT.modules.drugShipments.uploadHint" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -76,6 +76,7 @@ import { useStudyStore } from "../../store/study";
|
||||
import { createDrugShipment, getDrugShipment, updateDrugShipment } from "../../api/drugShipments";
|
||||
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -87,7 +88,7 @@ const isEdit = computed(() => !!shipmentId.value);
|
||||
const studyId = computed(() => study.currentStudy?.id || "");
|
||||
|
||||
const form = reactive({
|
||||
direction: "寄送",
|
||||
direction: "SEND",
|
||||
site_name: "",
|
||||
drug_desc: "",
|
||||
ship_date: "",
|
||||
@@ -95,7 +96,7 @@ const form = reactive({
|
||||
tracking_no: "",
|
||||
from_party: "",
|
||||
to_party: "",
|
||||
status: "待寄出",
|
||||
status: "PENDING",
|
||||
remark: "",
|
||||
});
|
||||
|
||||
@@ -104,7 +105,7 @@ const load = async () => {
|
||||
try {
|
||||
const { data } = await getDrugShipment(studyId.value, shipmentId.value);
|
||||
Object.assign(form, {
|
||||
direction: data.direction || "寄送",
|
||||
direction: data.direction || "SEND",
|
||||
site_name: data.site_name || "",
|
||||
drug_desc: data.drug_desc || "",
|
||||
ship_date: data.ship_date || "",
|
||||
@@ -112,11 +113,11 @@ const load = async () => {
|
||||
tracking_no: data.tracking_no || "",
|
||||
from_party: data.from_party || "",
|
||||
to_party: data.to_party || "",
|
||||
status: data.status || "待寄出",
|
||||
status: data.status || "PENDING",
|
||||
remark: data.remark || "",
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -138,15 +139,15 @@ const submit = async () => {
|
||||
};
|
||||
if (isEdit.value && shipmentId.value) {
|
||||
await updateDrugShipment(studyId.value, shipmentId.value, payload);
|
||||
ElMessage.success("已保存");
|
||||
ElMessage.success(TEXT.common.messages.saveSuccess);
|
||||
router.push(`/drug/shipments/${shipmentId.value}`);
|
||||
} else {
|
||||
const { data } = await createDrugShipment(studyId.value, payload);
|
||||
ElMessage.success("已创建");
|
||||
ElMessage.success(TEXT.common.messages.createSuccess);
|
||||
router.push(`/drug/shipments/${data.id}`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
|
||||
@@ -2,24 +2,24 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">合同费用详情</h1>
|
||||
<p class="page-subtitle">查看合同费用与附件</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.financeContracts.detailTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.financeContracts.detailSubtitle }}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="goEdit">编辑</el-button>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button type="primary" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card v-loading="loading">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="分中心">{{ detail.site_name || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="合同编号">{{ detail.contract_no || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="签署日期">{{ displayDate(detail.signed_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="合同金额">
|
||||
{{ detail.amount }} {{ detail.currency || "CNY" }}
|
||||
<el-descriptions-item :label="TEXT.common.fields.site">{{ detail.site_name || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.contractNo">{{ detail.contract_no || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.signedDate">{{ displayDate(detail.signed_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.amount">
|
||||
{{ detail.amount }} {{ detail.currency || TEXT.common.fields.currencyDefault }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ detail.remark || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.remark" :span="2">{{ detail.remark || TEXT.common.fallback }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
@@ -40,6 +40,7 @@ import { useStudyStore } from "../../store/study";
|
||||
import { getFinanceContract } from "../../api/financeContracts";
|
||||
import { displayDate } from "../../utils/display";
|
||||
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -64,7 +65,7 @@ const load = async () => {
|
||||
const { data } = await getFinanceContract(studyId, contractId);
|
||||
Object.assign(detail, data);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -2,39 +2,39 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">{{ isEdit ? "编辑合同费用" : "新增合同费用" }}</h1>
|
||||
<p class="page-subtitle">填写分中心合同信息并保存</p>
|
||||
<h1 class="page-title">{{ isEdit ? TEXT.modules.financeContracts.editTitle : TEXT.modules.financeContracts.newTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.financeContracts.formSubtitle }}</p>
|
||||
</div>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card>
|
||||
<el-form ref="formRef" :model="form" label-width="110px">
|
||||
<el-form-item label="分中心" prop="site_name" required>
|
||||
<el-input v-model="form.site_name" placeholder="输入分中心/机构名称" />
|
||||
<el-form-item :label="TEXT.common.fields.site" prop="site_name" required>
|
||||
<el-input v-model="form.site_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" />
|
||||
</el-form-item>
|
||||
<el-form-item label="合同编号" prop="contract_no" required>
|
||||
<el-input v-model="form.contract_no" placeholder="输入合同编号" />
|
||||
<el-form-item :label="TEXT.common.fields.contractNo" prop="contract_no" required>
|
||||
<el-input v-model="form.contract_no" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.contractNo" />
|
||||
</el-form-item>
|
||||
<el-form-item label="签署日期">
|
||||
<el-date-picker v-model="form.signed_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.signedDate">
|
||||
<el-date-picker v-model="form.signed_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="合同金额" prop="amount" required>
|
||||
<el-form-item :label="TEXT.common.fields.amount" prop="amount" required>
|
||||
<el-input-number v-model="form.amount" :min="0" :precision="2" :step="1000" />
|
||||
</el-form-item>
|
||||
<el-form-item label="币种">
|
||||
<el-select v-model="form.currency" placeholder="选择币种">
|
||||
<el-form-item :label="TEXT.common.fields.currency">
|
||||
<el-select v-model="form.currency" :placeholder="TEXT.common.placeholders.select">
|
||||
<el-option label="CNY" value="CNY" />
|
||||
<el-option label="USD" value="USD" />
|
||||
<el-option label="EUR" value="EUR" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="补充说明" />
|
||||
<el-form-item :label="TEXT.common.fields.remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" :placeholder="TEXT.common.placeholders.input" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submit">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -46,7 +46,7 @@
|
||||
:entity-id="contractId"
|
||||
/>
|
||||
<el-card v-else class="hint-card">
|
||||
<StateEmpty description="保存后可上传合同附件" />
|
||||
<StateEmpty :description="TEXT.modules.financeContracts.uploadHint" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -59,6 +59,7 @@ import { useStudyStore } from "../../store/study";
|
||||
import { createFinanceContract, getFinanceContract, updateFinanceContract } from "../../api/financeContracts";
|
||||
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -91,7 +92,7 @@ const load = async () => {
|
||||
remark: data.remark || "",
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -109,15 +110,15 @@ const submit = async () => {
|
||||
};
|
||||
if (isEdit.value && contractId.value) {
|
||||
await updateFinanceContract(studyId.value, contractId.value, payload);
|
||||
ElMessage.success("已保存");
|
||||
ElMessage.success(TEXT.common.messages.saveSuccess);
|
||||
router.push(`/finance/contracts/${contractId.value}`);
|
||||
} else {
|
||||
const { data } = await createFinanceContract(studyId.value, payload);
|
||||
ElMessage.success("已创建");
|
||||
ElMessage.success(TEXT.common.messages.createSuccess);
|
||||
router.push(`/finance/contracts/${data.id}`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
|
||||
@@ -2,23 +2,23 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">特殊费用详情</h1>
|
||||
<p class="page-subtitle">查看费用记录与附件</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.financeSpecials.detailTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.financeSpecials.detailSubtitle }}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="goEdit">编辑</el-button>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button type="primary" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card v-loading="loading">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="分中心">{{ detail.site_name || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="费用类型">{{ detail.fee_type || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="发生日期">{{ displayDate(detail.occur_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="人员">{{ detail.staff_name || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="金额">{{ detail.amount || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ detail.remark || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.site">{{ detail.site_name || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.type">{{ displayEnum(TEXT.enums.financeSpecialType, detail.fee_type) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.occurDate">{{ displayDate(detail.occur_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.staff">{{ detail.staff_name || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.amount">{{ detail.amount || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.remark" :span="2">{{ detail.remark || TEXT.common.fallback }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
@@ -37,8 +37,9 @@ import { useRoute, useRouter } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { getFinanceSpecial } from "../../api/financeSpecials";
|
||||
import { displayDate } from "../../utils/display";
|
||||
import { displayDate, displayEnum } from "../../utils/display";
|
||||
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -63,7 +64,7 @@ const load = async () => {
|
||||
const { data } = await getFinanceSpecial(studyId, specialId);
|
||||
Object.assign(detail, data);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -2,40 +2,40 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">{{ isEdit ? "编辑特殊费用" : "新增特殊费用" }}</h1>
|
||||
<p class="page-subtitle">记录差旅、住宿等特殊费用</p>
|
||||
<h1 class="page-title">{{ isEdit ? TEXT.modules.financeSpecials.editTitle : TEXT.modules.financeSpecials.newTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.financeSpecials.subtitle }}</p>
|
||||
</div>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card>
|
||||
<el-form :model="form" label-width="110px">
|
||||
<el-form-item label="分中心" required>
|
||||
<el-input v-model="form.site_name" placeholder="输入分中心/机构名称" />
|
||||
<el-form-item :label="TEXT.common.fields.site" required>
|
||||
<el-input v-model="form.site_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" />
|
||||
</el-form-item>
|
||||
<el-form-item label="费用类型" required>
|
||||
<el-select v-model="form.fee_type" placeholder="选择类型">
|
||||
<el-option label="差旅" value="差旅" />
|
||||
<el-option label="住宿" value="住宿" />
|
||||
<el-option label="交通" value="交通" />
|
||||
<el-option label="其他" value="其他" />
|
||||
<el-form-item :label="TEXT.common.fields.type" required>
|
||||
<el-select v-model="form.fee_type" :placeholder="TEXT.common.placeholders.select">
|
||||
<el-option :label="TEXT.enums.financeSpecialType.TRAVEL" value="TRAVEL" />
|
||||
<el-option :label="TEXT.enums.financeSpecialType.HOTEL" value="HOTEL" />
|
||||
<el-option :label="TEXT.enums.financeSpecialType.TRANSPORT" value="TRANSPORT" />
|
||||
<el-option :label="TEXT.enums.financeSpecialType.OTHER" value="OTHER" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="金额" required>
|
||||
<el-form-item :label="TEXT.common.fields.amount" required>
|
||||
<el-input-number v-model="form.amount" :min="0" :precision="2" :step="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="发生日期">
|
||||
<el-date-picker v-model="form.occur_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.occurDate">
|
||||
<el-date-picker v-model="form.occur_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="人员">
|
||||
<el-input v-model="form.staff_name" placeholder="输入人员姓名" />
|
||||
<el-form-item :label="TEXT.common.fields.staff">
|
||||
<el-input v-model="form.staff_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.staff" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="补充说明" />
|
||||
<el-form-item :label="TEXT.common.fields.remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" :placeholder="TEXT.common.placeholders.input" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submit">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -47,7 +47,7 @@
|
||||
:entity-id="specialId"
|
||||
/>
|
||||
<el-card v-else class="hint-card">
|
||||
<StateEmpty description="保存后可上传凭证附件" />
|
||||
<StateEmpty :description="TEXT.modules.financeSpecials.uploadHint" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -60,6 +60,7 @@ import { useStudyStore } from "../../store/study";
|
||||
import { createFinanceSpecial, getFinanceSpecial, updateFinanceSpecial } from "../../api/financeSpecials";
|
||||
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -72,7 +73,7 @@ const studyId = computed(() => study.currentStudy?.id || "");
|
||||
|
||||
const form = reactive({
|
||||
site_name: "",
|
||||
fee_type: "差旅",
|
||||
fee_type: "TRAVEL",
|
||||
amount: 0,
|
||||
occur_date: "",
|
||||
staff_name: "",
|
||||
@@ -85,14 +86,14 @@ const load = async () => {
|
||||
const { data } = await getFinanceSpecial(studyId.value, specialId.value);
|
||||
Object.assign(form, {
|
||||
site_name: data.site_name || "",
|
||||
fee_type: data.fee_type || "差旅",
|
||||
fee_type: data.fee_type || "TRAVEL",
|
||||
amount: Number(data.amount || 0),
|
||||
occur_date: data.occur_date || "",
|
||||
staff_name: data.staff_name || "",
|
||||
remark: data.remark || "",
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -110,15 +111,15 @@ const submit = async () => {
|
||||
};
|
||||
if (isEdit.value && specialId.value) {
|
||||
await updateFinanceSpecial(studyId.value, specialId.value, payload);
|
||||
ElMessage.success("已保存");
|
||||
ElMessage.success(TEXT.common.messages.saveSuccess);
|
||||
router.push(`/finance/special/${specialId.value}`);
|
||||
} else {
|
||||
const { data } = await createFinanceSpecial(studyId.value, payload);
|
||||
ElMessage.success("已创建");
|
||||
ElMessage.success(TEXT.common.messages.createSuccess);
|
||||
router.push(`/finance/special/${data.id}`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<ModulePlaceholder
|
||||
title="稽查"
|
||||
subtitle="功能建设中"
|
||||
list-title="稽查记录列表"
|
||||
empty-description="稽查模块正在准备基础框架,敬请期待。"
|
||||
:title="TEXT.modules.audit.title"
|
||||
:subtitle="TEXT.modules.audit.subtitle"
|
||||
:list-title="TEXT.modules.audit.listTitle"
|
||||
:empty-description="TEXT.modules.audit.emptyDescription"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ModulePlaceholder from "../../components/ModulePlaceholder.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
</script>
|
||||
|
||||
@@ -2,54 +2,58 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">药品运输/流向</h1>
|
||||
<p class="page-subtitle">登记寄送与回收信息</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.drugShipments.title }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.drugShipments.subtitle }}</p>
|
||||
</div>
|
||||
<el-button type="primary" @click="goNew">新增运输记录</el-button>
|
||||
<el-button type="primary" @click="goNew">{{ TEXT.modules.drugShipments.newTitle }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card class="filter-card">
|
||||
<el-form :inline="true" :model="filters">
|
||||
<el-form-item label="分中心">
|
||||
<el-input v-model="filters.site_name" placeholder="输入分中心名称" clearable />
|
||||
<el-form-item :label="TEXT.common.fields.site">
|
||||
<el-input v-model="filters.site_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="运单号">
|
||||
<el-input v-model="filters.tracking_no" placeholder="输入运单号" clearable />
|
||||
<el-form-item :label="TEXT.common.fields.trackingNo">
|
||||
<el-input v-model="filters.tracking_no" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.trackingNo" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="filters.status" placeholder="选择状态" clearable>
|
||||
<el-option label="待寄出" value="待寄出" />
|
||||
<el-option label="运输中" value="运输中" />
|
||||
<el-option label="已签收" value="已签收" />
|
||||
<el-option label="已回收" value="已回收" />
|
||||
<el-option label="异常" value="异常" />
|
||||
<el-form-item :label="TEXT.common.fields.status">
|
||||
<el-select v-model="filters.status" :placeholder="TEXT.common.placeholders.select" clearable>
|
||||
<el-option :label="TEXT.enums.shipmentStatus.PENDING" value="PENDING" />
|
||||
<el-option :label="TEXT.enums.shipmentStatus.IN_TRANSIT" value="IN_TRANSIT" />
|
||||
<el-option :label="TEXT.enums.shipmentStatus.SIGNED" value="SIGNED" />
|
||||
<el-option :label="TEXT.enums.shipmentStatus.RETURNED" value="RETURNED" />
|
||||
<el-option :label="TEXT.enums.shipmentStatus.EXCEPTION" value="EXCEPTION" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
<el-button type="primary" @click="load">{{ TEXT.common.actions.search }}</el-button>
|
||||
<el-button @click="resetFilters">{{ TEXT.common.actions.reset }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<el-table :data="items" v-loading="loading" style="width: 100%">
|
||||
<el-table-column prop="direction" label="类型" width="100" />
|
||||
<el-table-column prop="site_name" label="分中心" min-width="160" />
|
||||
<el-table-column prop="tracking_no" label="运单号" min-width="160" />
|
||||
<el-table-column prop="ship_date" label="日期" width="140">
|
||||
<el-table-column prop="direction" :label="TEXT.common.fields.direction" width="100">
|
||||
<template #default="scope">{{ displayEnum(TEXT.enums.shipmentDirection, scope.row.direction) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="site_name" :label="TEXT.common.fields.site" min-width="160" />
|
||||
<el-table-column prop="tracking_no" :label="TEXT.common.fields.trackingNo" min-width="160" />
|
||||
<el-table-column prop="ship_date" :label="TEXT.common.fields.shipDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.ship_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="120" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column prop="status" :label="TEXT.common.fields.status" width="120">
|
||||
<template #default="scope">{{ displayEnum(TEXT.enums.shipmentStatus, scope.row.status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="goDetail(scope.row.id)">详情</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEdit(scope.row.id)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="remove(scope.row)">删除</el-button>
|
||||
<el-button link type="primary" size="small" @click="goDetail(scope.row.id)">{{ TEXT.common.actions.view }}</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEdit(scope.row.id)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button link type="danger" size="small" @click="remove(scope.row)">{{ TEXT.common.actions.delete }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loading && items.length === 0" description="暂无运输记录" />
|
||||
<StateEmpty v-if="!loading && items.length === 0" :description="TEXT.modules.drugShipments.empty" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -60,8 +64,9 @@ import { useRouter } from "vue-router";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { listDrugShipments, deleteDrugShipment } from "../../api/drugShipments";
|
||||
import { displayDate } from "../../utils/display";
|
||||
import { displayDate, displayEnum } from "../../utils/display";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const router = useRouter();
|
||||
const study = useStudyStore();
|
||||
@@ -85,7 +90,7 @@ const load = async () => {
|
||||
});
|
||||
items.value = Array.isArray(data) ? data : data.items || [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -105,14 +110,14 @@ const goEdit = (id: string) => router.push(`/drug/shipments/${id}/edit`);
|
||||
const remove = async (row: any) => {
|
||||
const studyId = study.currentStudy?.id;
|
||||
if (!studyId) return;
|
||||
const ok = await ElMessageBox.confirm("确认删除该运输记录?", "提示").catch(() => null);
|
||||
const ok = await ElMessageBox.confirm(TEXT.modules.drugShipments.confirmDelete, TEXT.common.labels.tips).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await deleteDrugShipment(studyId, row.id);
|
||||
ElMessage.success("已删除");
|
||||
ElMessage.success(TEXT.common.messages.deleteSuccess);
|
||||
load();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "删除失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,55 +2,55 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">合同费用</h1>
|
||||
<p class="page-subtitle">维护分中心合同费用与附件</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.financeContracts.title }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.financeContracts.subtitle }}</p>
|
||||
</div>
|
||||
<el-button type="primary" @click="goNew">新增合同费用</el-button>
|
||||
<el-button type="primary" @click="goNew">{{ TEXT.modules.financeContracts.newTitle }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card class="filter-card">
|
||||
<el-form :inline="true" :model="filters">
|
||||
<el-form-item label="分中心">
|
||||
<el-input v-model="filters.site_name" placeholder="输入分中心名称" clearable />
|
||||
<el-form-item :label="TEXT.common.fields.site">
|
||||
<el-input v-model="filters.site_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="合同编号">
|
||||
<el-input v-model="filters.contract_no" placeholder="输入合同编号" clearable />
|
||||
<el-form-item :label="TEXT.common.fields.contractNo">
|
||||
<el-input v-model="filters.contract_no" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.contractNo" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
<el-button type="primary" @click="load">{{ TEXT.common.actions.search }}</el-button>
|
||||
<el-button @click="resetFilters">{{ TEXT.common.actions.reset }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<el-table :data="items" v-loading="loading" style="width: 100%">
|
||||
<el-table-column prop="site_name" label="分中心" min-width="160" />
|
||||
<el-table-column prop="contract_no" label="合同编号" min-width="160" />
|
||||
<el-table-column prop="signed_date" label="签署日期" width="140">
|
||||
<el-table-column prop="site_name" :label="TEXT.common.fields.site" min-width="160" />
|
||||
<el-table-column prop="contract_no" :label="TEXT.common.fields.contractNo" min-width="160" />
|
||||
<el-table-column prop="signed_date" :label="TEXT.common.fields.signedDate" width="140">
|
||||
<template #default="scope">
|
||||
{{ displayDate(scope.row.signed_date) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" width="160">
|
||||
<el-table-column :label="TEXT.common.fields.amount" width="160">
|
||||
<template #default="scope">
|
||||
{{ scope.row.amount }} {{ scope.row.currency || "CNY" }}
|
||||
{{ scope.row.amount }} {{ scope.row.currency || TEXT.common.fields.currencyDefault }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updated_at" label="更新时间" width="180">
|
||||
<el-table-column prop="updated_at" :label="TEXT.common.labels.updatedAt" width="180">
|
||||
<template #default="scope">
|
||||
{{ displayDateTime(scope.row.updated_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="goDetail(scope.row.id)">详情</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEdit(scope.row.id)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="remove(scope.row)">删除</el-button>
|
||||
<el-button link type="primary" size="small" @click="goDetail(scope.row.id)">{{ TEXT.common.actions.view }}</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEdit(scope.row.id)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button link type="danger" size="small" @click="remove(scope.row)">{{ TEXT.common.actions.delete }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loading && items.length === 0" description="暂无合同费用记录" />
|
||||
<StateEmpty v-if="!loading && items.length === 0" :description="TEXT.modules.financeContracts.empty" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -63,6 +63,7 @@ import { useStudyStore } from "../../store/study";
|
||||
import { listFinanceContracts, deleteFinanceContract } from "../../api/financeContracts";
|
||||
import { displayDate, displayDateTime } from "../../utils/display";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const router = useRouter();
|
||||
const study = useStudyStore();
|
||||
@@ -84,7 +85,7 @@ const load = async () => {
|
||||
});
|
||||
items.value = Array.isArray(data) ? data : data.items || [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -103,14 +104,14 @@ const goEdit = (id: string) => router.push(`/finance/contracts/${id}/edit`);
|
||||
const remove = async (row: any) => {
|
||||
const studyId = study.currentStudy?.id;
|
||||
if (!studyId) return;
|
||||
const ok = await ElMessageBox.confirm("确认删除该合同费用?", "提示").catch(() => null);
|
||||
const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await deleteFinanceContract(studyId, row.id);
|
||||
ElMessage.success("已删除");
|
||||
ElMessage.success(TEXT.common.messages.deleteSuccess);
|
||||
load();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "删除失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,61 +2,65 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">特殊费用</h1>
|
||||
<p class="page-subtitle">记录差旅、住宿等特殊费用</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.financeSpecials.title }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.financeSpecials.subtitle }}</p>
|
||||
</div>
|
||||
<el-button type="primary" @click="goNew">新增特殊费用</el-button>
|
||||
<el-button type="primary" @click="goNew">{{ TEXT.modules.financeSpecials.newTitle }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card class="filter-card">
|
||||
<el-form :inline="true" :model="filters">
|
||||
<el-form-item label="分中心">
|
||||
<el-input v-model="filters.site_name" placeholder="输入分中心名称" clearable />
|
||||
<el-form-item :label="TEXT.common.fields.site">
|
||||
<el-input v-model="filters.site_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="费用类型">
|
||||
<el-select v-model="filters.fee_type" placeholder="选择类型" clearable>
|
||||
<el-option label="差旅" value="差旅" />
|
||||
<el-option label="住宿" value="住宿" />
|
||||
<el-option label="交通" value="交通" />
|
||||
<el-option label="其他" value="其他" />
|
||||
<el-form-item :label="TEXT.common.fields.type">
|
||||
<el-select v-model="filters.fee_type" :placeholder="TEXT.common.placeholders.select" clearable>
|
||||
<el-option :label="TEXT.enums.financeSpecialType.TRAVEL" value="TRAVEL" />
|
||||
<el-option :label="TEXT.enums.financeSpecialType.HOTEL" value="HOTEL" />
|
||||
<el-option :label="TEXT.enums.financeSpecialType.TRANSPORT" value="TRANSPORT" />
|
||||
<el-option :label="TEXT.enums.financeSpecialType.OTHER" value="OTHER" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
<el-button type="primary" @click="load">{{ TEXT.common.actions.search }}</el-button>
|
||||
<el-button @click="resetFilters">{{ TEXT.common.actions.reset }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<el-table :data="items" v-loading="loading" style="width: 100%">
|
||||
<el-table-column prop="site_name" label="分中心" min-width="160" />
|
||||
<el-table-column prop="fee_type" label="费用类型" width="120" />
|
||||
<el-table-column prop="occur_date" label="发生日期" width="140">
|
||||
<el-table-column prop="site_name" :label="TEXT.common.fields.site" min-width="160" />
|
||||
<el-table-column prop="fee_type" :label="TEXT.common.fields.type" width="120">
|
||||
<template #default="scope">
|
||||
{{ displayEnum(TEXT.enums.financeSpecialType, scope.row.fee_type) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="occur_date" :label="TEXT.common.fields.occurDate" width="140">
|
||||
<template #default="scope">
|
||||
{{ displayDate(scope.row.occur_date) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="staff_name" label="人员" min-width="120" />
|
||||
<el-table-column label="金额" width="140">
|
||||
<el-table-column prop="staff_name" :label="TEXT.common.fields.staff" min-width="120" />
|
||||
<el-table-column :label="TEXT.common.fields.amount" width="140">
|
||||
<template #default="scope">
|
||||
{{ scope.row.amount }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updated_at" label="更新时间" width="180">
|
||||
<el-table-column prop="updated_at" :label="TEXT.common.labels.updatedAt" width="180">
|
||||
<template #default="scope">
|
||||
{{ displayDateTime(scope.row.updated_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="goDetail(scope.row.id)">详情</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEdit(scope.row.id)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="remove(scope.row)">删除</el-button>
|
||||
<el-button link type="primary" size="small" @click="goDetail(scope.row.id)">{{ TEXT.common.actions.view }}</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEdit(scope.row.id)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button link type="danger" size="small" @click="remove(scope.row)">{{ TEXT.common.actions.delete }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loading && items.length === 0" description="暂无特殊费用记录" />
|
||||
<StateEmpty v-if="!loading && items.length === 0" :description="TEXT.modules.financeSpecials.empty" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -67,8 +71,9 @@ import { useRouter } from "vue-router";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { listFinanceSpecials, deleteFinanceSpecial } from "../../api/financeSpecials";
|
||||
import { displayDate, displayDateTime } from "../../utils/display";
|
||||
import { displayDate, displayDateTime, displayEnum } from "../../utils/display";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const router = useRouter();
|
||||
const study = useStudyStore();
|
||||
@@ -90,7 +95,7 @@ const load = async () => {
|
||||
});
|
||||
items.value = Array.isArray(data) ? data : data.items || [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -109,14 +114,14 @@ const goEdit = (id: string) => router.push(`/finance/special/${id}/edit`);
|
||||
const remove = async (row: any) => {
|
||||
const studyId = study.currentStudy?.id;
|
||||
if (!studyId) return;
|
||||
const ok = await ElMessageBox.confirm("确认删除该特殊费用?", "提示").catch(() => null);
|
||||
const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await deleteFinanceSpecial(studyId, row.id);
|
||||
ElMessage.success("已删除");
|
||||
ElMessage.success(TEXT.common.messages.deleteSuccess);
|
||||
load();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "删除失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,44 +2,44 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">注意事项</h1>
|
||||
<p class="page-subtitle">记录分中心特殊问题与附件</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.knowledgeNotes.title }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.knowledgeNotes.subtitle }}</p>
|
||||
</div>
|
||||
<el-button type="primary" @click="goNew">新增注意事项</el-button>
|
||||
<el-button type="primary" @click="goNew">{{ TEXT.modules.knowledgeNotes.newTitle }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card class="filter-card">
|
||||
<el-form :inline="true" :model="filters">
|
||||
<el-form-item label="分中心">
|
||||
<el-input v-model="filters.site_name" placeholder="输入分中心名称" clearable />
|
||||
<el-form-item :label="TEXT.common.fields.site">
|
||||
<el-input v-model="filters.site_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="filters.keyword" placeholder="标题关键词" clearable />
|
||||
<el-form-item :label="TEXT.common.fields.keyword">
|
||||
<el-input v-model="filters.keyword" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.keyword" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
<el-button type="primary" @click="load">{{ TEXT.common.actions.search }}</el-button>
|
||||
<el-button @click="resetFilters">{{ TEXT.common.actions.reset }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<el-table :data="items" v-loading="loading" style="width: 100%">
|
||||
<el-table-column prop="site_name" label="分中心" min-width="160" />
|
||||
<el-table-column prop="title" label="标题" min-width="200" />
|
||||
<el-table-column prop="level" label="重要等级" width="120" />
|
||||
<el-table-column prop="updated_at" label="更新时间" width="180">
|
||||
<el-table-column prop="site_name" :label="TEXT.common.fields.site" min-width="160" />
|
||||
<el-table-column prop="title" :label="TEXT.common.fields.title" min-width="200" />
|
||||
<el-table-column prop="level" :label="TEXT.common.fields.level" width="120" />
|
||||
<el-table-column prop="updated_at" :label="TEXT.common.labels.updatedAt" width="180">
|
||||
<template #default="scope">{{ displayDateTime(scope.row.updated_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="goDetail(scope.row.id)">详情</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEdit(scope.row.id)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="remove(scope.row)">删除</el-button>
|
||||
<el-button link type="primary" size="small" @click="goDetail(scope.row.id)">{{ TEXT.common.actions.view }}</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEdit(scope.row.id)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button link type="danger" size="small" @click="remove(scope.row)">{{ TEXT.common.actions.delete }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loading && items.length === 0" description="暂无注意事项记录" />
|
||||
<StateEmpty v-if="!loading && items.length === 0" :description="TEXT.modules.knowledgeNotes.empty" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -52,6 +52,7 @@ import { useStudyStore } from "../../store/study";
|
||||
import { listKnowledgeNotes, deleteKnowledgeNote } from "../../api/knowledgeNotes";
|
||||
import { displayDateTime } from "../../utils/display";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const router = useRouter();
|
||||
const study = useStudyStore();
|
||||
@@ -73,7 +74,7 @@ const load = async () => {
|
||||
});
|
||||
items.value = Array.isArray(data) ? data : data.items || [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -92,14 +93,14 @@ const goEdit = (id: string) => router.push(`/knowledge/notes/${id}/edit`);
|
||||
const remove = async (row: any) => {
|
||||
const studyId = study.currentStudy?.id;
|
||||
if (!studyId) return;
|
||||
const ok = await ElMessageBox.confirm("确认删除该注意事项?", "提示").catch(() => null);
|
||||
const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await deleteKnowledgeNote(studyId, row.id);
|
||||
ElMessage.success("已删除");
|
||||
ElMessage.success(TEXT.common.messages.deleteSuccess);
|
||||
load();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "删除失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<ModulePlaceholder
|
||||
title="监查"
|
||||
subtitle="功能建设中"
|
||||
list-title="监查记录列表"
|
||||
empty-description="监查功能正在搭建基础能力,敬请期待。"
|
||||
:title="TEXT.modules.monitoring.title"
|
||||
:subtitle="TEXT.modules.monitoring.subtitle"
|
||||
:list-title="TEXT.modules.monitoring.listTitle"
|
||||
:empty-description="TEXT.modules.monitoring.emptyDescription"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ModulePlaceholder from "../../components/ModulePlaceholder.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
</script>
|
||||
|
||||
@@ -2,63 +2,63 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">立项与伦理</h1>
|
||||
<p class="page-subtitle">记录立项与伦理递交信息</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.startupFeasibilityEthics.title }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.startupFeasibilityEthics.subtitle }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card>
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="立项记录" name="feasibility">
|
||||
<el-tab-pane :label="TEXT.modules.startupFeasibilityEthics.feasibilityTab" name="feasibility">
|
||||
<div class="tab-actions">
|
||||
<el-button type="primary" @click="goNewFeasibility">新增立项记录</el-button>
|
||||
<el-button type="primary" @click="goNewFeasibility">{{ TEXT.modules.startupFeasibilityEthics.feasibilityNewTitle }}</el-button>
|
||||
</div>
|
||||
<el-table :data="feasibilityItems" v-loading="loadingFeasibility" style="width: 100%">
|
||||
<el-table-column prop="submit_date" label="递交日期" width="140">
|
||||
<el-table-column prop="submit_date" :label="TEXT.common.fields.submitDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.submit_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="accept_date" label="受理日期" width="140">
|
||||
<el-table-column prop="accept_date" :label="TEXT.common.fields.acceptDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.accept_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="approved_date" label="同意日期" width="140">
|
||||
<el-table-column prop="approved_date" :label="TEXT.common.fields.approvedDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.approved_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="project_no" label="项目编号" min-width="160" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column prop="project_no" :label="TEXT.common.fields.projectNo" min-width="160" />
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="goFeasibilityDetail(scope.row.id)">详情</el-button>
|
||||
<el-button link type="primary" size="small" @click="goFeasibilityEdit(scope.row.id)">编辑</el-button>
|
||||
<el-button link type="primary" size="small" @click="goFeasibilityDetail(scope.row.id)">{{ TEXT.common.actions.view }}</el-button>
|
||||
<el-button link type="primary" size="small" @click="goFeasibilityEdit(scope.row.id)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loadingFeasibility && feasibilityItems.length === 0" description="暂无立项记录" />
|
||||
<StateEmpty v-if="!loadingFeasibility && feasibilityItems.length === 0" :description="TEXT.modules.startupFeasibilityEthics.emptyFeasibility" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="伦理记录" name="ethics">
|
||||
<el-tab-pane :label="TEXT.modules.startupFeasibilityEthics.ethicsTab" name="ethics">
|
||||
<div class="tab-actions">
|
||||
<el-button type="primary" @click="goNewEthics">新增伦理记录</el-button>
|
||||
<el-button type="primary" @click="goNewEthics">{{ TEXT.modules.startupFeasibilityEthics.ethicsNewTitle }}</el-button>
|
||||
</div>
|
||||
<el-table :data="ethicsItems" v-loading="loadingEthics" style="width: 100%">
|
||||
<el-table-column prop="submit_date" label="递交日期" width="140">
|
||||
<el-table-column prop="submit_date" :label="TEXT.common.fields.submitDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.submit_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="accept_date" label="受理日期" width="140">
|
||||
<el-table-column prop="accept_date" :label="TEXT.common.fields.acceptDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.accept_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="meeting_date" label="会议日期" width="140">
|
||||
<el-table-column prop="meeting_date" :label="TEXT.common.fields.meetingDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.meeting_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="approved_date" label="批准日期" width="140">
|
||||
<el-table-column prop="approved_date" :label="TEXT.common.fields.approvedDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.approved_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="approval_no" label="批件号" min-width="160" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column prop="approval_no" :label="TEXT.common.fields.approvalNo" min-width="160" />
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="goEthicsDetail(scope.row.id)">详情</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEthicsEdit(scope.row.id)">编辑</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEthicsDetail(scope.row.id)">{{ TEXT.common.actions.view }}</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEthicsEdit(scope.row.id)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loadingEthics && ethicsItems.length === 0" description="暂无伦理记录" />
|
||||
<StateEmpty v-if="!loadingEthics && ethicsItems.length === 0" :description="TEXT.modules.startupFeasibilityEthics.emptyEthics" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
@@ -73,6 +73,7 @@ import { useStudyStore } from "../../store/study";
|
||||
import { listFeasibility, listEthics } from "../../api/startup";
|
||||
import { displayDate } from "../../utils/display";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const router = useRouter();
|
||||
const study = useStudyStore();
|
||||
@@ -90,7 +91,7 @@ const loadFeasibility = async () => {
|
||||
const { data } = await listFeasibility(studyId);
|
||||
feasibilityItems.value = Array.isArray(data) ? data : data.items || [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loadingFeasibility.value = false;
|
||||
}
|
||||
@@ -104,7 +105,7 @@ const loadEthics = async () => {
|
||||
const { data } = await listEthics(studyId);
|
||||
ethicsItems.value = Array.isArray(data) ? data : data.items || [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loadingEthics.value = false;
|
||||
}
|
||||
|
||||
@@ -2,61 +2,61 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">启动与授权</h1>
|
||||
<p class="page-subtitle">管理启动会记录与人员培训授权</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.startupMeetingAuth.title }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.startupMeetingAuth.subtitle }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card>
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="启动会记录" name="kickoff">
|
||||
<el-tab-pane :label="TEXT.modules.startupMeetingAuth.kickoffTab" name="kickoff">
|
||||
<div class="tab-actions">
|
||||
<el-button type="primary" @click="goNewKickoff">新增启动会</el-button>
|
||||
<el-button type="primary" @click="goNewKickoff">{{ TEXT.modules.startupMeetingAuth.kickoffNewTitle }}</el-button>
|
||||
</div>
|
||||
<el-table :data="kickoffItems" v-loading="loadingKickoff" style="width: 100%">
|
||||
<el-table-column prop="kickoff_date" label="启动会日期" width="160">
|
||||
<el-table-column prop="kickoff_date" :label="TEXT.common.fields.kickoffDate" width="160">
|
||||
<template #default="scope">{{ displayDate(scope.row.kickoff_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="attendees" label="参训人员" min-width="200">
|
||||
<el-table-column prop="attendees" :label="TEXT.common.fields.attendees" min-width="200">
|
||||
<template #default="scope">
|
||||
{{ (scope.row.attendees || []).join("、") || "—" }}
|
||||
{{ (scope.row.attendees || []).join("、") || TEXT.common.fallback }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="goKickoffDetail(scope.row.id)">详情</el-button>
|
||||
<el-button link type="primary" size="small" @click="goKickoffEdit(scope.row.id)">编辑</el-button>
|
||||
<el-button link type="primary" size="small" @click="goKickoffDetail(scope.row.id)">{{ TEXT.common.actions.view }}</el-button>
|
||||
<el-button link type="primary" size="small" @click="goKickoffEdit(scope.row.id)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loadingKickoff && kickoffItems.length === 0" description="暂无启动会记录" />
|
||||
<StateEmpty v-if="!loadingKickoff && kickoffItems.length === 0" :description="TEXT.modules.startupMeetingAuth.emptyKickoff" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="培训与授权" name="training">
|
||||
<el-tab-pane :label="TEXT.modules.startupMeetingAuth.trainingTab" name="training">
|
||||
<div class="tab-actions">
|
||||
<el-button type="primary" @click="goNewTraining">新增人员</el-button>
|
||||
<el-button type="primary" @click="goNewTraining">{{ TEXT.modules.startupMeetingAuth.trainingNewTitle }}</el-button>
|
||||
</div>
|
||||
<el-table :data="trainingItems" v-loading="loadingTraining" style="width: 100%">
|
||||
<el-table-column prop="name" label="姓名" min-width="140" />
|
||||
<el-table-column prop="role" label="角色" min-width="120" />
|
||||
<el-table-column prop="site_name" label="分中心" min-width="140" />
|
||||
<el-table-column label="已培训" width="120">
|
||||
<el-table-column prop="name" :label="TEXT.common.fields.name" min-width="140" />
|
||||
<el-table-column prop="role" :label="TEXT.common.labels.role" min-width="120" />
|
||||
<el-table-column prop="site_name" :label="TEXT.common.fields.site" min-width="140" />
|
||||
<el-table-column :label="TEXT.common.fields.trained" width="120">
|
||||
<template #default="scope">
|
||||
<el-checkbox v-model="scope.row.trained" @change="toggleTraining(scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="已授权" width="120">
|
||||
<el-table-column :label="TEXT.common.fields.authorized" width="120">
|
||||
<template #default="scope">
|
||||
<el-checkbox v-model="scope.row.authorized" @change="toggleTraining(scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="goTrainingDetail(scope.row.id)">详情</el-button>
|
||||
<el-button link type="primary" size="small" @click="goTrainingEdit(scope.row.id)">编辑</el-button>
|
||||
<el-button link type="primary" size="small" @click="goTrainingDetail(scope.row.id)">{{ TEXT.common.actions.view }}</el-button>
|
||||
<el-button link type="primary" size="small" @click="goTrainingEdit(scope.row.id)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loadingTraining && trainingItems.length === 0" description="暂无培训授权人员" />
|
||||
<StateEmpty v-if="!loadingTraining && trainingItems.length === 0" :description="TEXT.modules.startupMeetingAuth.emptyTraining" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
@@ -71,6 +71,7 @@ import { useStudyStore } from "../../store/study";
|
||||
import { listKickoffs, listTrainingAuthorizations, updateTrainingAuthorization } from "../../api/startup";
|
||||
import { displayDate } from "../../utils/display";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const router = useRouter();
|
||||
const study = useStudyStore();
|
||||
@@ -88,7 +89,7 @@ const loadKickoffs = async () => {
|
||||
const { data } = await listKickoffs(studyId);
|
||||
kickoffItems.value = Array.isArray(data) ? data : data.items || [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loadingKickoff.value = false;
|
||||
}
|
||||
@@ -102,7 +103,7 @@ const loadTraining = async () => {
|
||||
const { data } = await listTrainingAuthorizations(studyId);
|
||||
trainingItems.value = Array.isArray(data) ? data : data.items || [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loadingTraining.value = false;
|
||||
}
|
||||
@@ -116,9 +117,9 @@ const toggleTraining = async (row: any) => {
|
||||
trained: row.trained,
|
||||
authorized: row.authorized,
|
||||
});
|
||||
ElMessage.success("已保存");
|
||||
ElMessage.success(TEXT.common.messages.saveSuccess);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
loadTraining();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,50 +2,52 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">受试者管理</h1>
|
||||
<p class="page-subtitle">受试者、访视与 AE 统一管理</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.subjectManagement.title }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.subjectManagement.subtitle }}</p>
|
||||
</div>
|
||||
<el-button type="primary" @click="goNew">新增受试者</el-button>
|
||||
<el-button type="primary" @click="goNew">{{ TEXT.common.actions.add }}{{ TEXT.modules.subjectManagement.subjectLabel }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card class="filter-card">
|
||||
<el-form :inline="true" :model="filters">
|
||||
<el-form-item label="编号">
|
||||
<el-input v-model="filters.subject_no" placeholder="输入受试者编号" clearable />
|
||||
<el-form-item :label="TEXT.common.fields.subjectNo">
|
||||
<el-input v-model="filters.subject_no" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.subjectNo" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="filters.status" placeholder="选择状态" clearable>
|
||||
<el-option label="筛选" value="SCREENING" />
|
||||
<el-option label="入组" value="ENROLLED" />
|
||||
<el-option label="完成" value="COMPLETED" />
|
||||
<el-option label="退出" value="DROPPED" />
|
||||
<el-form-item :label="TEXT.common.fields.status">
|
||||
<el-select v-model="filters.status" :placeholder="TEXT.common.placeholders.select" clearable>
|
||||
<el-option :label="TEXT.enums.subjectStatus.SCREENING" value="SCREENING" />
|
||||
<el-option :label="TEXT.enums.subjectStatus.ENROLLED" value="ENROLLED" />
|
||||
<el-option :label="TEXT.enums.subjectStatus.COMPLETED" value="COMPLETED" />
|
||||
<el-option :label="TEXT.enums.subjectStatus.DROPPED" value="DROPPED" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
<el-button type="primary" @click="load">{{ TEXT.common.actions.search }}</el-button>
|
||||
<el-button @click="resetFilters">{{ TEXT.common.actions.reset }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<el-table :data="items" v-loading="loading" style="width: 100%">
|
||||
<el-table-column prop="subject_no" label="受试者编号" min-width="140" />
|
||||
<el-table-column label="分中心" min-width="160">
|
||||
<template #default="scope">{{ siteMap[scope.row.site_id] || "—" }}</template>
|
||||
<el-table-column prop="subject_no" :label="TEXT.common.fields.subjectNo" min-width="140" />
|
||||
<el-table-column :label="TEXT.common.fields.site" min-width="160">
|
||||
<template #default="scope">{{ siteMap[scope.row.site_id] || TEXT.common.fallback }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="120" />
|
||||
<el-table-column prop="enrollment_date" label="入组日期" width="140">
|
||||
<el-table-column prop="status" :label="TEXT.common.fields.status" width="120">
|
||||
<template #default="scope">{{ displayEnum(TEXT.enums.subjectStatus, scope.row.status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="enrollment_date" :label="TEXT.common.fields.enrollmentDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.enrollment_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="160">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="goDetail(scope.row.id)">详情</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEdit(scope.row.id)">编辑</el-button>
|
||||
<el-button link type="primary" size="small" @click="goDetail(scope.row.id)">{{ TEXT.common.actions.view }}</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEdit(scope.row.id)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loading && items.length === 0" description="暂无受试者记录" />
|
||||
<StateEmpty v-if="!loading && items.length === 0" :description="TEXT.modules.subjectManagement.empty" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -57,8 +59,9 @@ import { ElMessage } from "element-plus";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { fetchSubjects } from "../../api/subjects";
|
||||
import { fetchSites } from "../../api/sites";
|
||||
import { displayDate } from "../../utils/display";
|
||||
import { displayDate, displayEnum } from "../../utils/display";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const router = useRouter();
|
||||
const study = useStudyStore();
|
||||
@@ -96,7 +99,7 @@ const load = async () => {
|
||||
});
|
||||
items.value = Array.isArray(data) ? data : data.items || [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">注意事项详情</h1>
|
||||
<p class="page-subtitle">查看注意事项与附件</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.knowledgeNotes.detailTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.knowledgeNotes.detailSubtitle }}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="goEdit">编辑</el-button>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button type="primary" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card v-loading="loading">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="分中心">{{ detail.site_name || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="重要等级">{{ detail.level || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="标题" :span="2">{{ detail.title || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="内容" :span="2">{{ detail.content || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.site">{{ detail.site_name || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.level">{{ detail.level || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.title" :span="2">{{ detail.title || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.content" :span="2">{{ detail.content || TEXT.common.fallback }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
@@ -36,6 +36,7 @@ import { ElMessage } from "element-plus";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { getKnowledgeNote } from "../../api/knowledgeNotes";
|
||||
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -58,7 +59,7 @@ const load = async () => {
|
||||
const { data } = await getKnowledgeNote(studyId, noteId);
|
||||
Object.assign(detail, data);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -2,29 +2,29 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">{{ isEdit ? "编辑注意事项" : "新增注意事项" }}</h1>
|
||||
<p class="page-subtitle">记录分中心特殊问题</p>
|
||||
<h1 class="page-title">{{ isEdit ? TEXT.modules.knowledgeNotes.editTitle : TEXT.modules.knowledgeNotes.newTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.knowledgeNotes.formSubtitle }}</p>
|
||||
</div>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card>
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="分中心" required>
|
||||
<el-input v-model="form.site_name" placeholder="输入分中心/机构名称" />
|
||||
<el-form-item :label="TEXT.common.fields.site" required>
|
||||
<el-input v-model="form.site_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" />
|
||||
</el-form-item>
|
||||
<el-form-item label="标题" required>
|
||||
<el-input v-model="form.title" placeholder="输入标题" />
|
||||
<el-form-item :label="TEXT.common.fields.title" required>
|
||||
<el-input v-model="form.title" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.title" />
|
||||
</el-form-item>
|
||||
<el-form-item label="重要等级">
|
||||
<el-input v-model="form.level" placeholder="例如 高/中/低" />
|
||||
<el-form-item :label="TEXT.common.fields.level">
|
||||
<el-input v-model="form.level" :placeholder="TEXT.common.placeholders.input" />
|
||||
</el-form-item>
|
||||
<el-form-item label="内容" required>
|
||||
<el-input v-model="form.content" type="textarea" :rows="4" placeholder="输入内容" />
|
||||
<el-form-item :label="TEXT.common.fields.content" required>
|
||||
<el-input v-model="form.content" type="textarea" :rows="4" :placeholder="TEXT.common.placeholders.input" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submit">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -36,7 +36,7 @@
|
||||
:entity-id="noteId"
|
||||
/>
|
||||
<el-card v-else class="hint-card">
|
||||
<StateEmpty description="保存后可上传附件" />
|
||||
<StateEmpty :description="TEXT.modules.knowledgeNotes.uploadHint" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -49,6 +49,7 @@ import { useStudyStore } from "../../store/study";
|
||||
import { createKnowledgeNote, getKnowledgeNote, updateKnowledgeNote } from "../../api/knowledgeNotes";
|
||||
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -77,7 +78,7 @@ const load = async () => {
|
||||
content: data.content || "",
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -93,15 +94,15 @@ const submit = async () => {
|
||||
};
|
||||
if (isEdit.value && noteId.value) {
|
||||
await updateKnowledgeNote(studyId.value, noteId.value, payload);
|
||||
ElMessage.success("已保存");
|
||||
ElMessage.success(TEXT.common.messages.saveSuccess);
|
||||
router.push(`/knowledge/notes/${noteId.value}`);
|
||||
} else {
|
||||
const { data } = await createKnowledgeNote(studyId.value, payload);
|
||||
ElMessage.success("已创建");
|
||||
ElMessage.success(TEXT.common.messages.createSuccess);
|
||||
router.push(`/knowledge/notes/${data.id}`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">伦理记录详情</h1>
|
||||
<p class="page-subtitle">查看伦理记录与附件</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.startupFeasibilityEthics.ethicsDetailTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.startupFeasibilityEthics.ethicsDetailSubtitle }}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="goEdit">编辑</el-button>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button type="primary" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card v-loading="loading">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="递交日期">{{ displayDate(detail.submit_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="受理日期">{{ displayDate(detail.accept_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="会议日期">{{ displayDate(detail.meeting_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="批准日期">{{ displayDate(detail.approved_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="批件号">{{ detail.approval_no || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.submitDate">{{ displayDate(detail.submit_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.acceptDate">{{ displayDate(detail.accept_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.meetingDate">{{ displayDate(detail.meeting_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.approvedDate">{{ displayDate(detail.approved_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.approvalNo">{{ detail.approval_no || TEXT.common.fallback }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
@@ -38,6 +38,7 @@ import { useStudyStore } from "../../store/study";
|
||||
import { getEthics } from "../../api/startup";
|
||||
import { displayDate } from "../../utils/display";
|
||||
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -61,7 +62,7 @@ const load = async () => {
|
||||
const { data } = await getEthics(studyId, recordId);
|
||||
Object.assign(detail, data);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -2,32 +2,32 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">{{ isEdit ? "编辑伦理记录" : "新增伦理记录" }}</h1>
|
||||
<p class="page-subtitle">维护伦理递交与审批信息</p>
|
||||
<h1 class="page-title">{{ isEdit ? TEXT.modules.startupFeasibilityEthics.ethicsEditTitle : TEXT.modules.startupFeasibilityEthics.ethicsNewTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.startupFeasibilityEthics.ethicsFormSubtitle }}</p>
|
||||
</div>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card>
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="递交日期">
|
||||
<el-date-picker v-model="form.submit_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.submitDate">
|
||||
<el-date-picker v-model="form.submit_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="受理日期">
|
||||
<el-date-picker v-model="form.accept_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.acceptDate">
|
||||
<el-date-picker v-model="form.accept_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="会议日期">
|
||||
<el-date-picker v-model="form.meeting_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.meetingDate">
|
||||
<el-date-picker v-model="form.meeting_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="批准日期">
|
||||
<el-date-picker v-model="form.approved_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.approvedDate">
|
||||
<el-date-picker v-model="form.approved_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="批件号">
|
||||
<el-input v-model="form.approval_no" placeholder="输入批件号" />
|
||||
<el-form-item :label="TEXT.common.fields.approvalNo">
|
||||
<el-input v-model="form.approval_no" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.approvalNo" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submit">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -39,7 +39,7 @@
|
||||
:entity-id="recordId"
|
||||
/>
|
||||
<el-card v-else class="hint-card">
|
||||
<StateEmpty description="保存后可上传伦理附件" />
|
||||
<StateEmpty :description="TEXT.modules.startupFeasibilityEthics.ethicsUploadHint" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -52,6 +52,7 @@ import { useStudyStore } from "../../store/study";
|
||||
import { createEthics, getEthics, updateEthics } from "../../api/startup";
|
||||
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -82,7 +83,7 @@ const load = async () => {
|
||||
approval_no: data.approval_no || "",
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -99,15 +100,15 @@ const submit = async () => {
|
||||
};
|
||||
if (isEdit.value && recordId.value) {
|
||||
await updateEthics(studyId.value, recordId.value, payload);
|
||||
ElMessage.success("已保存");
|
||||
ElMessage.success(TEXT.common.messages.saveSuccess);
|
||||
router.push(`/startup/ethics/${recordId.value}`);
|
||||
} else {
|
||||
const { data } = await createEthics(studyId.value, payload);
|
||||
ElMessage.success("已创建");
|
||||
ElMessage.success(TEXT.common.messages.createSuccess);
|
||||
router.push(`/startup/ethics/${data.id}`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">立项记录详情</h1>
|
||||
<p class="page-subtitle">查看立项记录与附件</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.startupFeasibilityEthics.feasibilityDetailTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.startupFeasibilityEthics.feasibilityDetailSubtitle }}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="goEdit">编辑</el-button>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button type="primary" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card v-loading="loading">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="递交日期">{{ displayDate(detail.submit_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="受理日期">{{ displayDate(detail.accept_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="同意日期">{{ displayDate(detail.approved_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="项目编号">{{ detail.project_no || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.submitDate">{{ displayDate(detail.submit_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.acceptDate">{{ displayDate(detail.accept_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.approvedDate">{{ displayDate(detail.approved_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.projectNo">{{ detail.project_no || TEXT.common.fallback }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
@@ -37,6 +37,7 @@ import { useStudyStore } from "../../store/study";
|
||||
import { getFeasibility } from "../../api/startup";
|
||||
import { displayDate } from "../../utils/display";
|
||||
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -59,7 +60,7 @@ const load = async () => {
|
||||
const { data } = await getFeasibility(studyId, recordId);
|
||||
Object.assign(detail, data);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -2,29 +2,29 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">{{ isEdit ? "编辑立项记录" : "新增立项记录" }}</h1>
|
||||
<p class="page-subtitle">维护立项递交与审批信息</p>
|
||||
<h1 class="page-title">{{ isEdit ? TEXT.modules.startupFeasibilityEthics.feasibilityEditTitle : TEXT.modules.startupFeasibilityEthics.feasibilityNewTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.startupFeasibilityEthics.feasibilityFormSubtitle }}</p>
|
||||
</div>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card>
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="递交日期">
|
||||
<el-date-picker v-model="form.submit_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.submitDate">
|
||||
<el-date-picker v-model="form.submit_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="受理日期">
|
||||
<el-date-picker v-model="form.accept_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.acceptDate">
|
||||
<el-date-picker v-model="form.accept_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="同意日期">
|
||||
<el-date-picker v-model="form.approved_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.approvedDate">
|
||||
<el-date-picker v-model="form.approved_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目编号">
|
||||
<el-input v-model="form.project_no" placeholder="输入项目编号" />
|
||||
<el-form-item :label="TEXT.common.fields.projectNo">
|
||||
<el-input v-model="form.project_no" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.projectNo" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submit">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -36,7 +36,7 @@
|
||||
:entity-id="recordId"
|
||||
/>
|
||||
<el-card v-else class="hint-card">
|
||||
<StateEmpty description="保存后可上传立项附件" />
|
||||
<StateEmpty :description="TEXT.modules.startupFeasibilityEthics.feasibilityUploadHint" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -49,6 +49,7 @@ import { useStudyStore } from "../../store/study";
|
||||
import { createFeasibility, getFeasibility, updateFeasibility } from "../../api/startup";
|
||||
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -77,7 +78,7 @@ const load = async () => {
|
||||
project_no: data.project_no || "",
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -93,15 +94,15 @@ const submit = async () => {
|
||||
};
|
||||
if (isEdit.value && recordId.value) {
|
||||
await updateFeasibility(studyId.value, recordId.value, payload);
|
||||
ElMessage.success("已保存");
|
||||
ElMessage.success(TEXT.common.messages.saveSuccess);
|
||||
router.push(`/startup/feasibility/${recordId.value}`);
|
||||
} else {
|
||||
const { data } = await createFeasibility(studyId.value, payload);
|
||||
ElMessage.success("已创建");
|
||||
ElMessage.success(TEXT.common.messages.createSuccess);
|
||||
router.push(`/startup/feasibility/${data.id}`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">启动会详情</h1>
|
||||
<p class="page-subtitle">查看启动会信息与附件</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.startupMeetingAuth.kickoffDetailTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.startupMeetingAuth.kickoffDetailSubtitle }}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="goEdit">编辑</el-button>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button type="primary" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card v-loading="loading">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="启动会日期">{{ displayDate(detail.kickoff_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="参训人员">
|
||||
{{ (detail.attendees || []).join("、") || "—" }}
|
||||
<el-descriptions-item :label="TEXT.common.fields.kickoffDate">{{ displayDate(detail.kickoff_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.attendees">
|
||||
{{ (detail.attendees || []).join("、") || TEXT.common.fallback }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
@@ -37,6 +37,7 @@ import { useStudyStore } from "../../store/study";
|
||||
import { getKickoff } from "../../api/startup";
|
||||
import { displayDate } from "../../utils/display";
|
||||
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -57,7 +58,7 @@ const load = async () => {
|
||||
const { data } = await getKickoff(studyId, meetingId);
|
||||
Object.assign(detail, data);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -2,28 +2,28 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">{{ isEdit ? "编辑启动会" : "新增启动会" }}</h1>
|
||||
<p class="page-subtitle">记录启动会日期与参训人员</p>
|
||||
<h1 class="page-title">{{ isEdit ? TEXT.modules.startupMeetingAuth.kickoffEditTitle : TEXT.modules.startupMeetingAuth.kickoffNewTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.startupMeetingAuth.kickoffFormSubtitle }}</p>
|
||||
</div>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card>
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="启动会日期">
|
||||
<el-date-picker v-model="form.kickoff_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.kickoffDate">
|
||||
<el-date-picker v-model="form.kickoff_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="参训人员">
|
||||
<el-form-item :label="TEXT.common.fields.attendees">
|
||||
<el-input
|
||||
v-model="form.attendees"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="每行一个姓名"
|
||||
:placeholder="TEXT.modules.startupMeetingAuth.attendeesPlaceholder"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submit">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -35,7 +35,7 @@
|
||||
:entity-id="meetingId"
|
||||
/>
|
||||
<el-card v-else class="hint-card">
|
||||
<StateEmpty description="保存后可上传会议附件" />
|
||||
<StateEmpty :description="TEXT.modules.startupMeetingAuth.kickoffUploadHint" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -48,6 +48,7 @@ import { useStudyStore } from "../../store/study";
|
||||
import { createKickoff, getKickoff, updateKickoff } from "../../api/startup";
|
||||
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -79,7 +80,7 @@ const load = async () => {
|
||||
attendees: Array.isArray(data.attendees) ? data.attendees.join("\n") : "",
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -93,15 +94,15 @@ const submit = async () => {
|
||||
};
|
||||
if (isEdit.value && meetingId.value) {
|
||||
await updateKickoff(studyId.value, meetingId.value, payload);
|
||||
ElMessage.success("已保存");
|
||||
ElMessage.success(TEXT.common.messages.saveSuccess);
|
||||
router.push(`/startup/kickoff/${meetingId.value}`);
|
||||
} else {
|
||||
const { data } = await createKickoff(studyId.value, payload);
|
||||
ElMessage.success("已创建");
|
||||
ElMessage.success(TEXT.common.messages.createSuccess);
|
||||
router.push(`/startup/kickoff/${data.id}`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
|
||||
@@ -2,25 +2,25 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">培训授权详情</h1>
|
||||
<p class="page-subtitle">查看人员培训与授权信息</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.startupMeetingAuth.trainingDetailTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.startupMeetingAuth.trainingDetailSubtitle }}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="goEdit">编辑</el-button>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button type="primary" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card v-loading="loading">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="姓名">{{ detail.name || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="角色">{{ detail.role || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="分中心">{{ detail.site_name || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="已培训">{{ detail.trained ? "是" : "否" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="培训日期">{{ displayDate(detail.trained_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="已授权">{{ detail.authorized ? "是" : "否" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="授权日期">{{ displayDate(detail.authorized_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ detail.remark || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.name">{{ detail.name || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.labels.role">{{ displayEnum(TEXT.enums.userRole, detail.role) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.site">{{ detail.site_name || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.trained">{{ detail.trained ? TEXT.common.boolean.yes : TEXT.common.boolean.no }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.trainedDate">{{ displayDate(detail.trained_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.authorized">{{ detail.authorized ? TEXT.common.boolean.yes : TEXT.common.boolean.no }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.authorizedDate">{{ displayDate(detail.authorized_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.remark" :span="2">{{ detail.remark || TEXT.common.fallback }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
@@ -39,8 +39,9 @@ import { useRoute, useRouter } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { getTrainingAuthorization } from "../../api/startup";
|
||||
import { displayDate } from "../../utils/display";
|
||||
import { displayDate, displayEnum } from "../../utils/display";
|
||||
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -67,7 +68,7 @@ const load = async () => {
|
||||
const { data } = await getTrainingAuthorization(studyId, recordId);
|
||||
Object.assign(detail, data);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -2,41 +2,41 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">{{ isEdit ? "编辑培训授权" : "新增人员" }}</h1>
|
||||
<p class="page-subtitle">维护人员培训与授权状态</p>
|
||||
<h1 class="page-title">{{ isEdit ? TEXT.modules.startupMeetingAuth.trainingEditTitle : TEXT.modules.startupMeetingAuth.trainingNewTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.startupMeetingAuth.trainingFormSubtitle }}</p>
|
||||
</div>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card>
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="姓名" required>
|
||||
<el-input v-model="form.name" placeholder="输入姓名" />
|
||||
<el-form-item :label="TEXT.common.fields.name" required>
|
||||
<el-input v-model="form.name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色">
|
||||
<el-input v-model="form.role" placeholder="输入角色" />
|
||||
<el-form-item :label="TEXT.common.labels.role">
|
||||
<el-input v-model="form.role" :placeholder="TEXT.common.placeholders.input + TEXT.common.labels.role" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分中心">
|
||||
<el-input v-model="form.site_name" placeholder="输入分中心" />
|
||||
<el-form-item :label="TEXT.common.fields.site">
|
||||
<el-input v-model="form.site_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" />
|
||||
</el-form-item>
|
||||
<el-form-item label="已培训">
|
||||
<el-form-item :label="TEXT.common.fields.trained">
|
||||
<el-switch v-model="form.trained" />
|
||||
</el-form-item>
|
||||
<el-form-item label="培训日期">
|
||||
<el-date-picker v-model="form.trained_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.trainedDate">
|
||||
<el-date-picker v-model="form.trained_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="已授权">
|
||||
<el-form-item :label="TEXT.common.fields.authorized">
|
||||
<el-switch v-model="form.authorized" />
|
||||
</el-form-item>
|
||||
<el-form-item label="授权日期">
|
||||
<el-date-picker v-model="form.authorized_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.authorizedDate">
|
||||
<el-date-picker v-model="form.authorized_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="补充说明" />
|
||||
<el-form-item :label="TEXT.common.fields.remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" :placeholder="TEXT.common.placeholders.input" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submit">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -48,7 +48,7 @@
|
||||
:entity-id="recordId"
|
||||
/>
|
||||
<el-card v-else class="hint-card">
|
||||
<StateEmpty description="保存后可上传授权附件" />
|
||||
<StateEmpty :description="TEXT.modules.startupMeetingAuth.trainingUploadHint" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -61,6 +61,7 @@ import { useStudyStore } from "../../store/study";
|
||||
import { createTrainingAuthorization, getTrainingAuthorization, updateTrainingAuthorization } from "../../api/startup";
|
||||
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -97,7 +98,7 @@ const load = async () => {
|
||||
remark: data.remark || "",
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -117,15 +118,15 @@ const submit = async () => {
|
||||
};
|
||||
if (isEdit.value && recordId.value) {
|
||||
await updateTrainingAuthorization(studyId.value, recordId.value, payload);
|
||||
ElMessage.success("已保存");
|
||||
ElMessage.success(TEXT.common.messages.saveSuccess);
|
||||
router.push(`/startup/training/${recordId.value}`);
|
||||
} else {
|
||||
const { data } = await createTrainingAuthorization(studyId.value, payload);
|
||||
ElMessage.success("已创建");
|
||||
ElMessage.success(TEXT.common.messages.createSuccess);
|
||||
router.push(`/startup/training/${data.id}`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
|
||||
@@ -2,187 +2,195 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">受试者详情</h1>
|
||||
<p class="page-subtitle">查看病史、访视与 AE 信息</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.subjectDetail.title }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.subjectDetail.subtitle }}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="goEdit">编辑</el-button>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button type="primary" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card v-loading="loading">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="受试者编号">{{ detail.subject_no || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="分中心">{{ siteMap[detail.site_id] || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">{{ detail.status || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="筛选日期">{{ displayDate(detail.screening_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="入组日期">{{ displayDate(detail.enrollment_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="完成日期">{{ displayDate(detail.completion_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="退出原因" :span="2">{{ detail.drop_reason || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.subjectNo">{{ detail.subject_no || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.site">{{ siteMap[detail.site_id] || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.status">{{ displayEnum(TEXT.enums.subjectStatus, detail.status) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.screeningDate">{{ displayDate(detail.screening_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.enrollmentDate">{{ displayDate(detail.enrollment_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.completionDate">{{ displayDate(detail.completion_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.dropReason" :span="2">{{ detail.drop_reason || TEXT.common.fallback }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="病史" name="history">
|
||||
<el-tab-pane :label="TEXT.modules.subjectDetail.tabs.history" name="history">
|
||||
<div class="tab-actions">
|
||||
<el-button type="primary" @click="openHistoryDialog()">新增病史</el-button>
|
||||
<el-button type="primary" @click="openHistoryDialog()">{{ TEXT.common.actions.newHistory }}</el-button>
|
||||
</div>
|
||||
<el-table :data="historyItems" v-loading="loadingHistory" style="width: 100%">
|
||||
<el-table-column prop="record_date" label="日期" width="140">
|
||||
<el-table-column prop="record_date" :label="TEXT.common.fields.recordDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.record_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="content" label="内容" min-width="240" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column prop="content" :label="TEXT.common.fields.content" min-width="240" />
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="openHistoryDialog(scope.row)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="removeHistory(scope.row)">删除</el-button>
|
||||
<el-button link type="primary" size="small" @click="openHistoryDialog(scope.row)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button link type="danger" size="small" @click="removeHistory(scope.row)">{{ TEXT.common.actions.delete }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loadingHistory && historyItems.length === 0" description="暂无病史记录" />
|
||||
<StateEmpty v-if="!loadingHistory && historyItems.length === 0" :description="TEXT.modules.subjectDetail.emptyHistory" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="访视" name="visits">
|
||||
<el-tab-pane :label="TEXT.modules.subjectDetail.tabs.visits" name="visits">
|
||||
<div class="tab-actions">
|
||||
<el-button type="primary" @click="openVisitDialog()">新增访视</el-button>
|
||||
<el-button type="primary" @click="openVisitDialog()">{{ TEXT.common.actions.newVisit }}</el-button>
|
||||
</div>
|
||||
<el-table :data="visitItems" v-loading="loadingVisits" style="width: 100%">
|
||||
<el-table-column prop="visit_code" label="访视编号" width="120" />
|
||||
<el-table-column prop="visit_name" label="访视名称" min-width="140" />
|
||||
<el-table-column prop="planned_date" label="计划日期" width="140">
|
||||
<el-table-column prop="visit_code" :label="TEXT.common.fields.visitCode" width="120" />
|
||||
<el-table-column prop="visit_name" :label="TEXT.common.fields.visitName" min-width="140">
|
||||
<template #default="scope">
|
||||
{{ displayText(scope.row.visit_name, TEXT.enums.visitName) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="planned_date" :label="TEXT.common.fields.plannedDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.planned_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="actual_date" label="实际日期" width="140">
|
||||
<el-table-column prop="actual_date" :label="TEXT.common.fields.actualDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.actual_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="120" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column prop="status" :label="TEXT.common.fields.status" width="120">
|
||||
<template #default="scope">{{ displayEnum(TEXT.enums.visitStatus, scope.row.status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="openVisitDialog(scope.row)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="removeVisit(scope.row)">删除</el-button>
|
||||
<el-button link type="primary" size="small" @click="openVisitDialog(scope.row)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button link type="danger" size="small" @click="removeVisit(scope.row)">{{ TEXT.common.actions.delete }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loadingVisits && visitItems.length === 0" description="暂无访视记录" />
|
||||
<StateEmpty v-if="!loadingVisits && visitItems.length === 0" :description="TEXT.modules.subjectDetail.emptyVisits" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="AE 信息" name="ae">
|
||||
<el-tab-pane :label="TEXT.modules.subjectDetail.tabs.ae" name="ae">
|
||||
<div class="tab-actions">
|
||||
<el-button type="primary" @click="openAeDialog()">新增 AE</el-button>
|
||||
<el-button type="primary" @click="openAeDialog()">{{ TEXT.common.actions.newAe }}</el-button>
|
||||
</div>
|
||||
<el-table :data="aeItems" v-loading="loadingAes" style="width: 100%">
|
||||
<el-table-column prop="onset_date" label="发生日期" width="140">
|
||||
<el-table-column prop="onset_date" :label="TEXT.common.fields.occurDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.onset_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="term" label="事件" min-width="140" />
|
||||
<el-table-column prop="seriousness" label="严重程度" width="120" />
|
||||
<el-table-column prop="causality" label="关联性" width="120" />
|
||||
<el-table-column prop="outcome" label="结局" width="120" />
|
||||
<el-table-column prop="description" label="备注" min-width="200" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column prop="term" :label="TEXT.common.fields.title" min-width="140" />
|
||||
<el-table-column prop="seriousness" :label="TEXT.common.fields.seriousness" width="120">
|
||||
<template #default="scope">{{ displayEnum(TEXT.enums.aeSeriousness, scope.row.seriousness) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="causality" :label="TEXT.common.fields.causality" width="120" />
|
||||
<el-table-column prop="outcome" :label="TEXT.common.fields.outcome" width="120" />
|
||||
<el-table-column prop="description" :label="TEXT.common.fields.remark" min-width="200" />
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="openAeDialog(scope.row)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="removeAe(scope.row)">删除</el-button>
|
||||
<el-button link type="primary" size="small" @click="openAeDialog(scope.row)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button link type="danger" size="small" @click="removeAe(scope.row)">{{ TEXT.common.actions.delete }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loadingAes && aeItems.length === 0" description="暂无 AE 记录" />
|
||||
<StateEmpty v-if="!loadingAes && aeItems.length === 0" :description="TEXT.modules.subjectDetail.emptyAe" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="historyDialogVisible" title="病史记录" width="520px">
|
||||
<el-dialog v-model="historyDialogVisible" :title="TEXT.modules.subjectDetail.dialogHistory" width="520px">
|
||||
<el-form :model="historyForm" label-width="90px">
|
||||
<el-form-item label="日期">
|
||||
<el-form-item :label="TEXT.common.fields.recordDate">
|
||||
<el-date-picker v-model="historyForm.record_date" type="date" value-format="YYYY-MM-DD" />
|
||||
</el-form-item>
|
||||
<el-form-item label="内容" required>
|
||||
<el-form-item :label="TEXT.common.fields.content" required>
|
||||
<el-input v-model="historyForm.content" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="historyDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveHistory">保存</el-button>
|
||||
<el-button @click="historyDialogVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" @click="saveHistory">{{ TEXT.common.actions.save }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="visitDialogVisible" title="访视记录" width="560px">
|
||||
<el-dialog v-model="visitDialogVisible" :title="TEXT.modules.subjectDetail.dialogVisit" width="560px">
|
||||
<el-form :model="visitForm" label-width="100px">
|
||||
<template v-if="!visitEditingId">
|
||||
<el-form-item label="访视编号" required>
|
||||
<el-form-item :label="TEXT.common.fields.visitCode" required>
|
||||
<el-input v-model="visitForm.visit_code" />
|
||||
</el-form-item>
|
||||
<el-form-item label="访视名称" required>
|
||||
<el-form-item :label="TEXT.common.fields.visitName" required>
|
||||
<el-input v-model="visitForm.visit_name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="计划日期">
|
||||
<el-form-item :label="TEXT.common.fields.plannedDate">
|
||||
<el-date-picker v-model="visitForm.planned_date" type="date" value-format="YYYY-MM-DD" />
|
||||
</el-form-item>
|
||||
<el-form-item label="窗口开始">
|
||||
<el-form-item :label="TEXT.common.fields.windowStart">
|
||||
<el-date-picker v-model="visitForm.window_start" type="date" value-format="YYYY-MM-DD" />
|
||||
</el-form-item>
|
||||
<el-form-item label="窗口结束">
|
||||
<el-form-item :label="TEXT.common.fields.windowEnd">
|
||||
<el-date-picker v-model="visitForm.window_end" type="date" value-format="YYYY-MM-DD" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-form-item label="实际日期">
|
||||
<el-form-item :label="TEXT.common.fields.actualDate">
|
||||
<el-date-picker v-model="visitForm.actual_date" type="date" value-format="YYYY-MM-DD" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-form-item :label="TEXT.common.fields.status">
|
||||
<el-select v-model="visitForm.status">
|
||||
<el-option label="计划中" value="PLANNED" />
|
||||
<el-option label="已完成" value="DONE" />
|
||||
<el-option label="未完成" value="MISSED" />
|
||||
<el-option :label="TEXT.enums.visitStatus.PLANNED" value="PLANNED" />
|
||||
<el-option :label="TEXT.enums.visitStatus.DONE" value="DONE" />
|
||||
<el-option :label="TEXT.enums.visitStatus.MISSED" value="MISSED" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-form-item :label="TEXT.common.fields.remark">
|
||||
<el-input v-model="visitForm.notes" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visitDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveVisit">保存</el-button>
|
||||
<el-button @click="visitDialogVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" @click="saveVisit">{{ TEXT.common.actions.save }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="aeDialogVisible" title="AE 记录" width="560px">
|
||||
<el-dialog v-model="aeDialogVisible" :title="TEXT.modules.subjectDetail.dialogAe" width="560px">
|
||||
<el-form :model="aeForm" label-width="100px">
|
||||
<el-form-item label="事件" required>
|
||||
<el-form-item :label="TEXT.common.fields.title" required>
|
||||
<el-input v-model="aeForm.term" />
|
||||
</el-form-item>
|
||||
<el-form-item label="发生日期">
|
||||
<el-form-item :label="TEXT.common.fields.occurDate">
|
||||
<el-date-picker v-model="aeForm.onset_date" type="date" value-format="YYYY-MM-DD" />
|
||||
</el-form-item>
|
||||
<el-form-item label="严重程度" required>
|
||||
<el-form-item :label="TEXT.common.fields.seriousness" required>
|
||||
<el-select v-model="aeForm.seriousness">
|
||||
<el-option label="严重" value="SERIOUS" />
|
||||
<el-option label="一般" value="NON_SERIOUS" />
|
||||
<el-option :label="TEXT.enums.aeSeriousness.SERIOUS" value="SERIOUS" />
|
||||
<el-option :label="TEXT.enums.aeSeriousness.NON_SERIOUS" value="NON_SERIOUS" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="严重等级" required>
|
||||
<el-form-item :label="TEXT.common.fields.severity" required>
|
||||
<el-select v-model="aeForm.severity">
|
||||
<el-option label="轻度" value="MILD" />
|
||||
<el-option label="中度" value="MODERATE" />
|
||||
<el-option label="重度" value="SEVERE" />
|
||||
<el-option :label="TEXT.enums.aeSeverity.MILD" value="MILD" />
|
||||
<el-option :label="TEXT.enums.aeSeverity.MODERATE" value="MODERATE" />
|
||||
<el-option :label="TEXT.enums.aeSeverity.SEVERE" value="SEVERE" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="关联性">
|
||||
<el-form-item :label="TEXT.common.fields.causality">
|
||||
<el-input v-model="aeForm.causality" />
|
||||
</el-form-item>
|
||||
<el-form-item label="结局">
|
||||
<el-form-item :label="TEXT.common.fields.outcome">
|
||||
<el-input v-model="aeForm.outcome" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-form-item :label="TEXT.common.fields.remark">
|
||||
<el-input v-model="aeForm.description" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="aeDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveAe">保存</el-button>
|
||||
<el-button @click="aeDialogVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" @click="saveAe">{{ TEXT.common.actions.save }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@@ -198,8 +206,9 @@ import { fetchSites } from "../../api/sites";
|
||||
import { listSubjectHistories, createSubjectHistory, updateSubjectHistory, deleteSubjectHistory } from "../../api/subjectHistories";
|
||||
import { fetchVisits, createVisit, updateVisit, deleteVisit } from "../../api/visits";
|
||||
import { fetchAes, createAe, updateAe, deleteAe } from "../../api/aes";
|
||||
import { displayDate } from "../../utils/display";
|
||||
import { displayDate, displayEnum, displayText } from "../../utils/display";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -280,7 +289,7 @@ const loadSubject = async () => {
|
||||
const { data } = await getSubject(studyId, subjectId);
|
||||
Object.assign(detail, data);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -293,7 +302,7 @@ const loadHistories = async () => {
|
||||
const { data } = await listSubjectHistories(studyId, subjectId);
|
||||
historyItems.value = Array.isArray(data) ? data : data.items || [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "病史加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loadingHistory.value = false;
|
||||
}
|
||||
@@ -322,19 +331,19 @@ const saveHistory = async () => {
|
||||
historyDialogVisible.value = false;
|
||||
loadHistories();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
}
|
||||
};
|
||||
|
||||
const removeHistory = async (row: any) => {
|
||||
if (!studyId || !subjectId) return;
|
||||
const ok = await ElMessageBox.confirm("确认删除该病史记录?", "提示").catch(() => null);
|
||||
const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await deleteSubjectHistory(studyId, subjectId, row.id);
|
||||
loadHistories();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "删除失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -345,7 +354,7 @@ const loadVisits = async () => {
|
||||
const { data } = await fetchVisits(studyId, subjectId);
|
||||
visitItems.value = Array.isArray(data) ? data : data.items || [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "访视加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loadingVisits.value = false;
|
||||
}
|
||||
@@ -394,19 +403,19 @@ const saveVisit = async () => {
|
||||
visitDialogVisible.value = false;
|
||||
loadVisits();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
}
|
||||
};
|
||||
|
||||
const removeVisit = async (row: any) => {
|
||||
if (!studyId || !subjectId) return;
|
||||
const ok = await ElMessageBox.confirm("确认删除该访视记录?", "提示").catch(() => null);
|
||||
const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await deleteVisit(studyId, subjectId, row.id);
|
||||
loadVisits();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "删除失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -417,7 +426,7 @@ const loadAes = async () => {
|
||||
const { data } = await fetchAes(studyId, { subject_id: subjectId });
|
||||
aeItems.value = Array.isArray(data) ? data : data.items || [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "AE 加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loadingAes.value = false;
|
||||
}
|
||||
@@ -459,19 +468,19 @@ const saveAe = async () => {
|
||||
aeDialogVisible.value = false;
|
||||
loadAes();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
}
|
||||
};
|
||||
|
||||
const removeAe = async (row: any) => {
|
||||
if (!studyId) return;
|
||||
const ok = await ElMessageBox.confirm("确认删除该 AE 记录?", "提示").catch(() => null);
|
||||
const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await deleteAe(studyId, row.id);
|
||||
loadAes();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "删除失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,51 +2,51 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">{{ isEdit ? "编辑受试者" : "新增受试者" }}</h1>
|
||||
<p class="page-subtitle">维护受试者基本信息</p>
|
||||
<h1 class="page-title">{{ isEdit ? TEXT.modules.subjectForm.titleEdit : TEXT.modules.subjectForm.titleNew }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.subjectForm.subtitle }}</p>
|
||||
</div>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card>
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="受试者编号" required>
|
||||
<el-input v-model="form.subject_no" :disabled="isEdit" placeholder="输入受试者编号" />
|
||||
<el-form-item :label="TEXT.common.fields.subjectNo" required>
|
||||
<el-input v-model="form.subject_no" :disabled="isEdit" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.subjectNo" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分中心" required>
|
||||
<el-select v-model="form.site_id" :disabled="isEdit" placeholder="选择分中心">
|
||||
<el-form-item :label="TEXT.common.fields.site" required>
|
||||
<el-select v-model="form.site_id" :disabled="isEdit" :placeholder="TEXT.common.placeholders.select">
|
||||
<el-option v-for="site in sites" :key="site.id" :label="site.name" :value="site.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="筛选日期">
|
||||
<el-form-item :label="TEXT.common.fields.screeningDate">
|
||||
<el-date-picker
|
||||
v-model="form.screening_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
:placeholder="TEXT.common.placeholders.select"
|
||||
:disabled="isEdit"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isEdit" label="状态">
|
||||
<el-select v-model="form.status" placeholder="选择状态">
|
||||
<el-option label="筛选" value="SCREENING" />
|
||||
<el-option label="入组" value="ENROLLED" />
|
||||
<el-option label="完成" value="COMPLETED" />
|
||||
<el-option label="退出" value="DROPPED" />
|
||||
<el-form-item v-if="isEdit" :label="TEXT.common.fields.status">
|
||||
<el-select v-model="form.status" :placeholder="TEXT.common.placeholders.select">
|
||||
<el-option :label="TEXT.enums.subjectStatus.SCREENING" value="SCREENING" />
|
||||
<el-option :label="TEXT.enums.subjectStatus.ENROLLED" value="ENROLLED" />
|
||||
<el-option :label="TEXT.enums.subjectStatus.COMPLETED" value="COMPLETED" />
|
||||
<el-option :label="TEXT.enums.subjectStatus.DROPPED" value="DROPPED" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isEdit" label="入组日期">
|
||||
<el-date-picker v-model="form.enrollment_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item v-if="isEdit" :label="TEXT.common.fields.enrollmentDate">
|
||||
<el-date-picker v-model="form.enrollment_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isEdit" label="完成日期">
|
||||
<el-date-picker v-model="form.completion_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item v-if="isEdit" :label="TEXT.common.fields.completionDate">
|
||||
<el-date-picker v-model="form.completion_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isEdit" label="退出原因">
|
||||
<el-input v-model="form.drop_reason" type="textarea" :rows="3" placeholder="输入退出原因" />
|
||||
<el-form-item v-if="isEdit" :label="TEXT.common.fields.dropReason">
|
||||
<el-input v-model="form.drop_reason" type="textarea" :rows="3" :placeholder="TEXT.common.placeholders.input" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submit">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -60,6 +60,7 @@ import { ElMessage } from "element-plus";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { createSubject, getSubject, updateSubject } from "../../api/subjects";
|
||||
import { fetchSites } from "../../api/sites";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -105,7 +106,7 @@ const load = async () => {
|
||||
drop_reason: data.drop_reason || "",
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -121,7 +122,7 @@ const submit = async () => {
|
||||
drop_reason: form.drop_reason || null,
|
||||
};
|
||||
await updateSubject(studyId.value, subjectId.value, payload);
|
||||
ElMessage.success("已保存");
|
||||
ElMessage.success(TEXT.common.messages.saveSuccess);
|
||||
router.push(`/subjects/${subjectId.value}`);
|
||||
} else {
|
||||
const payload = {
|
||||
@@ -130,11 +131,11 @@ const submit = async () => {
|
||||
screening_date: form.screening_date || null,
|
||||
};
|
||||
const { data } = await createSubject(studyId.value, payload);
|
||||
ElMessage.success("已创建");
|
||||
ElMessage.success(TEXT.common.messages.createSuccess);
|
||||
router.push(`/subjects/${data.id}`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
<!-- 头部欢迎区域 -->
|
||||
<div class="workbench-header">
|
||||
<div class="header-content">
|
||||
<h1 class="welcome-title">我的工作台</h1>
|
||||
<p class="welcome-subtitle">欢迎回来,这是基于您的角色 <strong>{{ roleLabel }}</strong> 聚合的待办事项</p>
|
||||
<h1 class="welcome-title">{{ TEXT.modules.workbench.title }}</h1>
|
||||
<p class="welcome-subtitle">
|
||||
{{ TEXT.modules.workbench.subtitle.replace("{role}", roleLabel) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-tag effect="dark" class="role-badge">{{ roleLabel }}</el-tag>
|
||||
@@ -14,7 +16,7 @@
|
||||
<!-- 提示未选择项目 -->
|
||||
<el-alert
|
||||
v-if="!currentStudyId"
|
||||
title="请先在顶部导航选择一个项目,以开启完整的工作台功能。"
|
||||
:title="TEXT.modules.workbench.selectProject"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
@@ -26,19 +28,19 @@
|
||||
<div class="stats-row">
|
||||
<div class="stats-col">
|
||||
<SectionCard
|
||||
title="今日待办"
|
||||
:title="TEXT.modules.workbench.todayTitle"
|
||||
:items="todayList"
|
||||
:loading="loading"
|
||||
empty-text="今天暂时没有需要紧急处理的业务事项"
|
||||
:empty-text="TEXT.modules.workbench.todayEmpty"
|
||||
@more="goMore(todayMorePath)"
|
||||
/>
|
||||
</div>
|
||||
<div class="stats-col">
|
||||
<SectionCard
|
||||
title="已逾期事项"
|
||||
:title="TEXT.modules.workbench.overdueTitle"
|
||||
:items="overdueList"
|
||||
:loading="loading"
|
||||
empty-text="太棒了!目前没有任何逾期待处理的事项"
|
||||
:empty-text="TEXT.modules.workbench.overdueEmpty"
|
||||
@more="goMore(overdueMorePath)"
|
||||
/>
|
||||
</div>
|
||||
@@ -48,19 +50,19 @@
|
||||
<div class="secondary-row">
|
||||
<div class="action-col">
|
||||
<SectionCard
|
||||
title="需要我处理的事项"
|
||||
:title="TEXT.modules.workbench.actionTitle"
|
||||
:items="actionList"
|
||||
:loading="loading"
|
||||
empty-text="暂无需要您处理的事项"
|
||||
:empty-text="TEXT.modules.workbench.actionEmpty"
|
||||
@more="goMore(actionMorePath)"
|
||||
/>
|
||||
</div>
|
||||
<div class="quick-col">
|
||||
<div class="quick-entry-card">
|
||||
<div class="entry-header">
|
||||
<span class="entry-title">快捷入口</span>
|
||||
<span class="entry-title">{{ TEXT.modules.workbench.quickEntryTitle }}</span>
|
||||
<el-button type="primary" link @click="goMore('/project/overview')" class="entry-more">
|
||||
进入项目 <el-icon class="el-icon--right"><Right /></el-icon>
|
||||
{{ TEXT.modules.workbench.enterProject }} <el-icon class="el-icon--right"><Right /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="quick-grid">
|
||||
@@ -98,6 +100,8 @@ import { useStudyStore } from "../../store/study";
|
||||
import { fetchAes } from "../../api/aes";
|
||||
import { listFinanceContracts } from "../../api/financeContracts";
|
||||
import { Right, Timer, UserFilled, Warning, Money, Collection } from "@element-plus/icons-vue";
|
||||
import { displayEnum } from "../../utils/display";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
interface WorkItem {
|
||||
title: string;
|
||||
@@ -121,8 +125,8 @@ const actionList = ref<WorkItem[]>([]);
|
||||
|
||||
const currentStudyId = computed(() => study.currentStudy?.id || "");
|
||||
const todayStr = computed(() => new Date().toISOString().slice(0, 10));
|
||||
const roleLabel = computed(() => auth.user?.role || "-");
|
||||
const emptyText = "暂无需要你处理的事项";
|
||||
const roleLabel = computed(() => displayEnum(TEXT.enums.userRole, auth.user?.role));
|
||||
const emptyText = TEXT.modules.workbench.quickEmpty;
|
||||
const showPersonalTodo = computed(() => role.value !== "ADMIN");
|
||||
const todoStorageKey = computed(() => `workbench_todos_${auth.user?.id || "guest"}`);
|
||||
|
||||
@@ -131,17 +135,17 @@ const quickActions = computed(() => {
|
||||
if (!currentStudyId.value) return [];
|
||||
if (role.value === "CRA")
|
||||
return [
|
||||
{ label: "立项与伦理", path: "/startup/feasibility-ethics", icon: Timer },
|
||||
{ label: "AE 列表", path: "/subjects", icon: Warning },
|
||||
{ label: TEXT.modules.workbench.feasibilityEthics, path: "/startup/feasibility-ethics", icon: Timer },
|
||||
{ label: TEXT.modules.workbench.aeList, path: "/subjects", icon: Warning },
|
||||
];
|
||||
if (role.value === "PM")
|
||||
return [
|
||||
{ label: "立项与伦理", path: "/startup/feasibility-ethics", icon: Timer },
|
||||
{ label: "合同费用", path: "/finance/contracts", icon: Money },
|
||||
{ label: "医学咨询", path: "/knowledge/medical-consult", icon: Collection },
|
||||
{ label: TEXT.modules.workbench.feasibilityEthics, path: "/startup/feasibility-ethics", icon: Timer },
|
||||
{ label: TEXT.modules.workbench.contractList, path: "/finance/contracts", icon: Money },
|
||||
{ label: TEXT.modules.workbench.medicalConsult, path: "/knowledge/medical-consult", icon: Collection },
|
||||
];
|
||||
if (role.value === "PV") return [{ label: "AE 列表", path: "/subjects", icon: Warning }];
|
||||
return [{ label: "立项与伦理", path: "/startup/feasibility-ethics", icon: Timer }];
|
||||
if (role.value === "PV") return [{ label: TEXT.modules.workbench.aeList, path: "/subjects", icon: Warning }];
|
||||
return [{ label: TEXT.modules.workbench.feasibilityEthics, path: "/startup/feasibility-ethics", icon: Timer }];
|
||||
});
|
||||
|
||||
const todayMorePath = computed(() => "/subjects");
|
||||
@@ -157,7 +161,7 @@ const isOverdue = (d?: string | null) => {
|
||||
};
|
||||
|
||||
const toAeItem = (ae: any): WorkItem => ({
|
||||
title: ae.term || "不良事件",
|
||||
title: ae.term || TEXT.modules.workbench.aeTitleFallback,
|
||||
subtitle: study.currentStudy?.name || "",
|
||||
status: ae.status,
|
||||
overdue: isOverdue(ae.expected_resolution_date || ae.updated_at),
|
||||
@@ -165,7 +169,7 @@ const toAeItem = (ae: any): WorkItem => ({
|
||||
});
|
||||
|
||||
const toContractItem = (fi: any): WorkItem => ({
|
||||
title: fi.contract_no || "合同费用",
|
||||
title: fi.contract_no || TEXT.modules.workbench.contractTitleFallback,
|
||||
subtitle: study.currentStudy?.name || "",
|
||||
status: fi.site_name,
|
||||
overdue: false,
|
||||
@@ -186,7 +190,7 @@ const loadData = async () => {
|
||||
|
||||
buildSections(aes, finances);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "工作台数据加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.workbench.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
<div class="section-header">
|
||||
<div class="header-left">
|
||||
<el-icon class="header-icon"><Notebook /></el-icon>
|
||||
<span class="header-title">个人备忘 (本地存储)</span>
|
||||
<span class="header-title">{{ TEXT.modules.workbench.todoTitle }}</span>
|
||||
</div>
|
||||
<el-tag size="small" effect="plain" type="info" class="count-tag">
|
||||
{{ todos.length }} 条记录
|
||||
{{ TEXT.modules.workbench.todoCount.replace("{count}", String(todos.length)) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="input-section">
|
||||
<el-input
|
||||
v-model="input"
|
||||
placeholder="有什么需要记录的事项吗?(回车添加)"
|
||||
:placeholder="TEXT.modules.workbench.todoPlaceholder"
|
||||
class="todo-input"
|
||||
@keyup.enter="add"
|
||||
>
|
||||
@@ -25,7 +25,7 @@
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button type="primary" :icon="Plus" @click="add" class="add-btn">
|
||||
添加
|
||||
{{ TEXT.modules.workbench.todoAdd }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
</transition-group>
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
<StateEmpty description="暂无待办事项,记录一些备忘吧" />
|
||||
<StateEmpty :description="TEXT.modules.workbench.todoEmpty" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -61,6 +61,7 @@
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
import { Notebook, EditPen, Plus, Delete } from "@element-plus/icons-vue";
|
||||
import StateEmpty from "../../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../../locales";
|
||||
|
||||
const props = defineProps<{ storageKey: string | undefined }>();
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="section-header">
|
||||
<span class="section-title">{{ title }}</span>
|
||||
<el-button v-if="!loading" type="primary" link @click="$emit('more')" class="more-btn">
|
||||
查看更多 <el-icon class="el-icon--right"><ArrowRight /></el-icon>
|
||||
{{ TEXT.common.actions.more }} <el-icon class="el-icon--right"><ArrowRight /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -29,13 +29,13 @@
|
||||
:effect="item.overdue ? 'dark' : 'light'"
|
||||
class="status-tag"
|
||||
>
|
||||
{{ item.status || "待处理" }}
|
||||
{{ item.status || TEXT.modules.workbench.todoStatusFallback }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
<StateEmpty :description="emptyText || '暂无数据'" />
|
||||
<StateEmpty :description="emptyText || TEXT.common.empty.noData" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -46,6 +46,7 @@ import { useRouter } from "vue-router";
|
||||
import { ArrowRight } from "@element-plus/icons-vue";
|
||||
import StateEmpty from "../../../components/StateEmpty.vue";
|
||||
import StateLoading from "../../../components/StateLoading.vue";
|
||||
import { TEXT } from "../../../locales";
|
||||
|
||||
defineProps<{
|
||||
title: string;
|
||||
|
||||
Reference in New Issue
Block a user