全局中文化

This commit is contained in:
Cheng Zhou
2026-01-12 11:14:04 +08:00
parent 9021c7fe2b
commit 1338fa7e2b
103 changed files with 2443 additions and 1533 deletions
+11 -10
View File
@@ -1,22 +1,22 @@
<template>
<el-dialog :model-value="modelValue" :title="isEdit ? '编辑分类' : '新增分类'" width="480px" @close="close">
<el-dialog :model-value="modelValue" :title="isEdit ? TEXT.modules.knowledgeMedicalConsult.editCategory : TEXT.modules.knowledgeMedicalConsult.newCategory" width="480px" @close="close">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="名称" prop="name">
<el-form-item :label="TEXT.common.fields.name" prop="name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="描述">
<el-form-item :label="TEXT.common.fields.description">
<el-input v-model="form.description" type="textarea" :rows="2" />
</el-form-item>
<el-form-item label="排序">
<el-form-item :label="TEXT.common.fields.sortOrder">
<el-input-number v-model="form.sort_order" :min="0" :step="1" />
</el-form-item>
<el-form-item label="启用">
<el-form-item :label="TEXT.common.fields.enabled">
<el-switch v-model="form.is_active" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="close">取消</el-button>
<el-button type="primary" :loading="submitting" @click="onSubmit">保存</el-button>
<el-button @click="close">{{ TEXT.common.actions.cancel }}</el-button>
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ TEXT.common.actions.save }}</el-button>
</template>
</el-dialog>
</template>
@@ -26,6 +26,7 @@ import { computed, reactive, ref, watch } from "vue";
import { ElMessage, FormInstance, FormRules } from "element-plus";
import { createFaqCategory, updateFaqCategory } from "../api/faqs";
import { useStudyStore } from "../store/study";
import { TEXT, requiredMessage } from "../locales";
const props = defineProps<{ modelValue: boolean; category?: any; isAdmin: boolean }>();
const emit = defineEmits(["update:modelValue", "success"]);
@@ -43,7 +44,7 @@ const form = reactive({
});
const rules: FormRules = {
name: [{ required: true, message: "请输入名称", trigger: "blur" }],
name: [{ required: true, message: requiredMessage(TEXT.common.fields.name), trigger: "blur" }],
};
const isEdit = computed(() => !!props.category);
@@ -100,11 +101,11 @@ const onSubmit = async () => {
} else {
await createFaqCategory(payload);
}
ElMessage.success("保存成功");
ElMessage.success(TEXT.common.messages.saveSuccess);
emit("success");
close();
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "保存失败");
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
} finally {
submitting.value = false;
}
+12 -8
View File
@@ -1,23 +1,23 @@
<template>
<el-card class="panel">
<div class="header">
<span>分类</span>
<span>{{ TEXT.common.labels.category }}</span>
<div v-if="canMaintain">
<PermissionAction action="faq.edit">
<el-button type="primary" size="small" @click="openForm()">新建</el-button>
<el-button type="primary" size="small" @click="openForm()">{{ TEXT.modules.knowledgeMedicalConsult.newCategory }}</el-button>
</PermissionAction>
</div>
</div>
<el-menu :default-active="activeCategory" @select="onSelect" class="menu">
<el-menu-item index="">全部</el-menu-item>
<el-menu-item index="">{{ TEXT.common.labels.all }}</el-menu-item>
<el-menu-item v-for="c in sortedCategories" :key="c.id" :index="c.id">
<span class="name" :title="c.name">{{ c.name }}</span>
<div v-if="canEdit(c) || canDelete" class="actions">
<PermissionAction v-if="canEdit(c)" action="faq.edit">
<el-button type="text" size="small" @click.stop="openForm(c)">编辑</el-button>
<el-button type="text" size="small" @click.stop="openForm(c)">{{ TEXT.common.actions.edit }}</el-button>
</PermissionAction>
<PermissionAction v-if="canDelete" action="faq.edit">
<el-button type="text" size="small" class="danger" @click.stop="onDelete(c)">删除</el-button>
<el-button type="text" size="small" class="danger" @click.stop="onDelete(c)">{{ TEXT.common.actions.delete }}</el-button>
</PermissionAction>
</div>
</el-menu-item>
@@ -36,6 +36,7 @@ import { useAuthStore } from "../store/auth";
import { useStudyStore } from "../store/study";
import PermissionAction from "./PermissionAction.vue";
import { usePermission } from "../utils/permission";
import { TEXT } from "../locales";
const props = defineProps<{
categories: FaqCategory[];
@@ -76,14 +77,17 @@ const openForm = (cat?: FaqCategory) => {
};
const onDelete = async (cat: FaqCategory) => {
const ok = await ElMessageBox.confirm(`确认删除分类「${cat.name}」?`, "提示").catch(() => null);
const ok = await ElMessageBox.confirm(
TEXT.modules.knowledgeMedicalConsult.confirmDeleteCategory.replace("{name}", cat.name),
TEXT.common.labels.tips
).catch(() => null);
if (!ok) return;
try {
await deleteFaqCategory(cat.id);
ElMessage.success("删除成功");
ElMessage.success(TEXT.common.messages.deleteSuccess);
emit("refresh");
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "删除失败");
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
}
};
</script>
+12 -11
View File
@@ -1,21 +1,21 @@
<template>
<el-dialog :model-value="modelValue" :title="isEdit ? '编辑咨询' : '新建咨询'" width="640px" @close="close">
<el-dialog :model-value="modelValue" :title="isEdit ? TEXT.modules.knowledgeMedicalConsult.editItem : TEXT.modules.knowledgeMedicalConsult.newItem" width="640px" @close="close">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="分类" prop="category_id">
<el-select v-model="form.category_id" placeholder="请选择分类">
<el-form-item :label="TEXT.common.labels.category" prop="category_id">
<el-select v-model="form.category_id" :placeholder="TEXT.common.placeholders.select">
<el-option v-for="c in categories" :key="c.id" :label="c.name" :value="c.id" />
</el-select>
</el-form-item>
<el-form-item label="问题" prop="question">
<el-form-item :label="TEXT.common.fields.question" prop="question">
<el-input v-model="form.question" />
</el-form-item>
<el-form-item label="启用">
<el-form-item :label="TEXT.common.fields.enabled">
<el-switch v-model="form.is_active" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="close">取消</el-button>
<el-button type="primary" :loading="submitting" @click="onSubmit">提交</el-button>
<el-button @click="close">{{ TEXT.common.actions.cancel }}</el-button>
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ TEXT.common.actions.submit }}</el-button>
</template>
</el-dialog>
</template>
@@ -25,6 +25,7 @@ import { computed, reactive, ref, watch } from "vue";
import { ElMessage, FormInstance, FormRules } from "element-plus";
import { createFaqItem, updateFaqItem } from "../api/faqs";
import { useStudyStore } from "../store/study";
import { TEXT, requiredMessage } from "../locales";
const props = defineProps<{ modelValue: boolean; item?: any; categories: any[] }>();
const emit = defineEmits(["update:modelValue", "success"]);
@@ -41,8 +42,8 @@ const form = reactive({
});
const rules: FormRules = {
category_id: [{ required: true, message: "请选择分类", trigger: "change" }],
question: [{ required: true, message: "请输入问题", trigger: "blur" }],
category_id: [{ required: true, message: requiredMessage(TEXT.common.labels.category), trigger: "change" }],
question: [{ required: true, message: requiredMessage(TEXT.common.fields.question), trigger: "blur" }],
};
const isEdit = computed(() => !!props.item);
@@ -87,11 +88,11 @@ const onSubmit = async () => {
} else {
await createFaqItem(payload);
}
ElMessage.success("保存成功");
ElMessage.success(TEXT.common.messages.saveSuccess);
emit("success");
close();
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "保存失败");
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
} finally {
submitting.value = false;
}
+26 -19
View File
@@ -1,34 +1,36 @@
<template>
<el-table :data="items" v-loading="loading" style="width: 100%" @row-click="onRowClick">
<el-table-column prop="question" label="问题" min-width="200">
<el-table-column prop="question" :label="TEXT.common.fields.question" min-width="200">
<template #default="scope">
<el-link type="primary" :underline="false" class="question-link">
{{ scope.row.question }}
</el-link>
</template>
</el-table-column>
<el-table-column prop="category_id" label="分类" width="140">
<el-table-column prop="category_id" :label="TEXT.common.labels.category" width="140">
<template #default="scope">
{{ categoryMap[scope.row.category_id] || scope.row.category_id }}
</template>
</el-table-column>
<el-table-column prop="is_active" label="启用" width="80">
<el-table-column prop="is_active" :label="TEXT.common.fields.enabled" width="80">
<template #default="scope">
<el-tag :type="scope.row.is_active ? 'success' : 'info'">{{ scope.row.is_active ? "是" : "否" }}</el-tag>
<el-tag :type="scope.row.is_active ? 'success' : 'info'">
{{ scope.row.is_active ? TEXT.common.boolean.yes : TEXT.common.boolean.no }}
</el-tag>
</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 prop="status" label="状态" width="120">
<el-table-column prop="status" :label="TEXT.common.fields.status" width="120">
<template #default="scope">
<el-tag :type="statusTagType(scope.row.status)" size="small">{{ statusLabel(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<el-table-column :label="TEXT.common.labels.actions" width="180">
<template #default="scope">
<el-button v-if="canDelete(scope.row)" type="text" size="small" class="danger" @click="remove(scope.row)">
删除
{{ TEXT.common.actions.delete }}
</el-button>
<PermissionAction v-if="canEdit" action="faq.edit">
<el-button
@@ -37,7 +39,7 @@
@click="toggle(scope.row)"
:style="{ color: scope.row.is_active ? '#f56c6c' : '#67c23a' }"
>
{{ scope.row.is_active ? "停用" : "启用" }}
{{ scope.row.is_active ? TEXT.common.actions.disable : TEXT.common.actions.enable }}
</el-button>
</PermissionAction>
</template>
@@ -52,8 +54,9 @@ import { useRouter } from "vue-router";
import { deleteFaqItem, updateFaqItem } from "../api/faqs";
import type { FaqItem, FaqCategory } from "../api/faqs";
import PermissionAction from "./PermissionAction.vue";
import { displayDateTime } from "../utils/display";
import { displayDateTime, displayEnum } from "../utils/display";
import { useAuthStore } from "../store/auth";
import { TEXT } from "../locales";
const props = defineProps<{
items: FaqItem[];
@@ -83,9 +86,7 @@ const onRowClick = (row: FaqItem, column: any, event: MouseEvent) => {
};
const statusLabel = (status?: string) => {
if (status === "RESOLVED") return "已解决";
if (status === "PROCESSING") return "处理中";
return "待回答";
return displayEnum(TEXT.enums.faqStatus, status || "");
};
const statusTagType = (status?: string) => {
@@ -99,26 +100,32 @@ const canDelete = (row: FaqItem) => props.canEdit || row.created_by === auth.use
const toggle = async (row: FaqItem) => {
const target = !row.is_active;
const ok = await ElMessageBox.confirm(`确认${target ? "启用" : "停用"}此咨询?`, "提示").catch(() => null);
const ok = await ElMessageBox.confirm(
TEXT.modules.knowledgeMedicalConsult.confirmToggleItem.replace("{action}", target ? TEXT.common.actions.enable : TEXT.common.actions.disable),
TEXT.common.labels.tips
).catch(() => null);
if (!ok) return;
try {
await updateFaqItem(row.id, { is_active: target });
ElMessage.success("操作成功");
ElMessage.success(TEXT.common.messages.updateSuccess);
emit("refresh");
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "操作失败");
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.actionFailed);
}
};
const remove = async (row: FaqItem) => {
const ok = await ElMessageBox.confirm(`确认删除咨询「${row.question}」?`, "提示").catch(() => null);
const ok = await ElMessageBox.confirm(
TEXT.modules.knowledgeMedicalConsult.confirmDeleteItem.replace("{name}", row.question),
TEXT.common.labels.tips
).catch(() => null);
if (!ok) return;
try {
await deleteFaqItem(row.id);
ElMessage.success("问题已删除");
ElMessage.success(TEXT.common.messages.deleteSuccess);
emit("refresh");
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "删除失败");
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
}
};
</script>
+27 -26
View File
@@ -3,7 +3,7 @@
<el-aside :width="isCollapsed ? '72px' : '240px'" class="layout-aside" :class="{ collapsed: isCollapsed }" v-if="isAdmin || study.currentStudy">
<div class="aside-logo">
<div class="logo-icon"></div>
<span class="logo-text">CTMS</span>
<span class="logo-text">{{ TEXT.common.appName }}</span>
</div>
<el-menu
router
@@ -14,73 +14,73 @@
>
<el-menu-item v-if="!isAdmin" index="/workbench">
<el-icon><Monitor /></el-icon>
<span>我的工作台</span>
<span>{{ TEXT.menu.workbench }}</span>
</el-menu-item>
<template v-if="isAdmin">
<div class="menu-divider">管理后台</div>
<div class="menu-divider">{{ TEXT.menu.admin }}</div>
<el-menu-item index="/admin/users">
<el-icon><User /></el-icon>
<span>账号管理</span>
<span>{{ TEXT.menu.accountManagement }}</span>
</el-menu-item>
<el-menu-item index="/admin/projects">
<el-icon><Suitcase /></el-icon>
<span>项目管理</span>
<span>{{ TEXT.menu.projectManagement }}</span>
</el-menu-item>
<el-menu-item v-if="isAdmin" index="/admin/audit-logs">
<el-icon><Document /></el-icon>
<span>审计日志</span>
<span>{{ TEXT.menu.auditLogs }}</span>
</el-menu-item>
</template>
<template v-if="study.currentStudy">
<div class="menu-divider">当前项目</div>
<div class="menu-divider">{{ TEXT.menu.currentProject }}</div>
<el-menu-item index="/project/overview">
<el-icon><House /></el-icon>
<span>项目总览</span>
<span>{{ TEXT.menu.projectOverview }}</span>
</el-menu-item>
<el-sub-menu index="finance">
<template #title>
<el-icon><Coin /></el-icon>
<span>费用管理</span>
<span>{{ TEXT.menu.finance }}</span>
</template>
<el-menu-item index="/finance/contracts">合同费用</el-menu-item>
<el-menu-item index="/finance/special">特殊费用</el-menu-item>
<el-menu-item index="/finance/contracts">{{ TEXT.menu.financeContracts }}</el-menu-item>
<el-menu-item index="/finance/special">{{ TEXT.menu.financeSpecials }}</el-menu-item>
</el-sub-menu>
<el-sub-menu index="drug">
<template #title>
<el-icon><Box /></el-icon>
<span>药品管理</span>
<span>{{ TEXT.menu.drug }}</span>
</template>
<el-menu-item index="/drug/shipments">运输/流向</el-menu-item>
<el-menu-item index="/drug/shipments">{{ TEXT.menu.drugShipments }}</el-menu-item>
</el-sub-menu>
<el-menu-item index="/startup/feasibility-ethics">
<el-icon><Calendar /></el-icon>
<span>立项与伦理</span>
<span>{{ TEXT.menu.startupFeasibilityEthics }}</span>
</el-menu-item>
<el-menu-item index="/startup/meeting-auth">
<el-icon><CircleCheck /></el-icon>
<span>启动与授权</span>
<span>{{ TEXT.menu.startupMeetingAuth }}</span>
</el-menu-item>
<el-menu-item index="/subjects">
<el-icon><User /></el-icon>
<span>受试者管理</span>
<span>{{ TEXT.menu.subjects }}</span>
</el-menu-item>
<el-menu-item index="/monitoring">
<el-icon><ChatDotRound /></el-icon>
<span>监查</span>
<span>{{ TEXT.menu.monitoring }}</span>
</el-menu-item>
<el-menu-item index="/audit">
<el-icon><Flag /></el-icon>
<span>稽查</span>
<span>{{ TEXT.menu.audit }}</span>
</el-menu-item>
<el-sub-menu index="knowledge">
<template #title>
<el-icon><Notebook /></el-icon>
<span>知识库</span>
<span>{{ TEXT.menu.knowledge }}</span>
</template>
<el-menu-item index="/knowledge/medical-consult">医学咨询</el-menu-item>
<el-menu-item index="/knowledge/notes">注意事项</el-menu-item>
<el-menu-item index="/knowledge/medical-consult">{{ TEXT.menu.knowledgeMedicalConsult }}</el-menu-item>
<el-menu-item index="/knowledge/notes">{{ TEXT.menu.knowledgeNotes }}</el-menu-item>
</el-sub-menu>
</template>
</el-menu>
@@ -89,7 +89,7 @@
<el-container class="main-container">
<el-header class="layout-header">
<div class="header-left">
<el-button class="collapse-btn" @click="toggleCollapse" aria-label="收缩侧边栏">
<el-button class="collapse-btn" @click="toggleCollapse" :aria-label="TEXT.common.labels.collapseSidebar">
<el-icon class="collapse-icon">
<svg viewBox="0 0 24 24" aria-hidden="true">
<rect x="4" y="5" width="16" height="14" rx="2" fill="none" stroke="currentColor" stroke-width="1.8" />
@@ -108,20 +108,20 @@
shape="square"
class="user-avatar-comp"
>
{{ (auth.user?.username || auth.user?.email || "U").charAt(0).toUpperCase() }}
{{ (auth.user?.username || auth.user?.email || TEXT.common.labels.userInitialFallback).charAt(0).toUpperCase() }}
</el-avatar>
<span class="username">{{ auth.user?.username || auth.user?.email || "用户" }}</span>
<span class="username">{{ auth.user?.username || auth.user?.email || TEXT.common.labels.userFallback }}</span>
<el-icon><ArrowDown /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu class="user-dropdown-menu">
<el-dropdown-item command="profile">
<el-icon><User /></el-icon>
<span>个人中心</span>
<span>{{ TEXT.menu.profile }}</span>
</el-dropdown-item>
<el-dropdown-item command="logout">
<el-icon><SwitchButton /></el-icon>
<span>退出登录</span>
<span>{{ TEXT.menu.logout }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
@@ -148,6 +148,7 @@ import { useRoute, useRouter } from "vue-router";
import { useAuthStore } from "../store/auth";
import { useStudyStore } from "../store/study";
import StudySelector from "./StudySelector.vue";
import { TEXT } from "../locales";
import {
Monitor, User, Suitcase, House, Calendar, Flag, ChatDotRound,
CircleCheck, Box, Coin, Notebook, Document, ArrowDown, SwitchButton
@@ -11,7 +11,7 @@
<div class="module-list">
<div class="module-list-header">
<span class="list-title">{{ listTitle }}</span>
<span class="list-note">列表区域预留</span>
<span class="list-note">{{ TEXT.common.empty.listPlaceholder }}</span>
</div>
<StateEmpty :title="emptyTitle" :description="emptyDescription" />
</div>
@@ -21,6 +21,7 @@
<script setup lang="ts">
import StateEmpty from "./StateEmpty.vue";
import { TEXT } from "../locales";
withDefaults(
defineProps<{
@@ -32,9 +33,9 @@ withDefaults(
}>(),
{
subtitle: "",
listTitle: "列表",
emptyTitle: "功能建设中",
emptyDescription: "该模块基础骨架已建立,后续将补充完整交互与数据能力。",
listTitle: TEXT.common.empty.listPlaceholder,
emptyTitle: TEXT.common.empty.building,
emptyDescription: TEXT.common.empty.buildingDesc,
}
);
</script>
+8 -7
View File
@@ -2,7 +2,7 @@
<div class="quick-actions-section">
<div class="section-header">
<el-icon class="header-icon"><Pointer /></el-icon>
<span class="header-title">通往业务模块的快捷入口</span>
<span class="header-title">{{ TEXT.common.labels.quickActions }}</span>
</div>
<div class="actions-grid">
<div
@@ -27,16 +27,17 @@ import {
Pointer, ArrowRight, Timer, UserFilled,
Management, Money, Collection
} from "@element-plus/icons-vue";
import { TEXT } from "../locales";
const router = useRouter();
const actions = [
{ label: "立项与伦理", path: "/startup/feasibility-ethics", icon: Timer },
{ label: "启动与授权", path: "/startup/meeting-auth", icon: Timer },
{ label: "受试者管理", path: "/subjects", icon: UserFilled },
{ label: "药品流向", path: "/drug/shipments", icon: Management },
{ label: "费用管理", path: "/finance/contracts", icon: Money },
{ label: "知识库", path: "/knowledge/medical-consult", icon: Collection },
{ label: TEXT.menu.startupFeasibilityEthics, path: "/startup/feasibility-ethics", icon: Timer },
{ label: TEXT.menu.startupMeetingAuth, path: "/startup/meeting-auth", icon: Timer },
{ label: TEXT.menu.subjects, path: "/subjects", icon: UserFilled },
{ label: TEXT.menu.drugShipments, path: "/drug/shipments", icon: Management },
{ label: TEXT.menu.finance, path: "/finance/contracts", icon: Money },
{ label: TEXT.menu.knowledge, path: "/knowledge/medical-consult", icon: Collection },
];
const go = (path: string) => router.push(path);
+2 -1
View File
@@ -11,6 +11,7 @@
<script setup lang="ts">
import { Document } from "@element-plus/icons-vue";
import { TEXT } from "../locales";
withDefaults(
defineProps<{
@@ -18,7 +19,7 @@ withDefaults(
description?: string;
}>(),
{
title: "暂无数据",
title: TEXT.common.empty.noData,
description: "",
}
);
+3 -2
View File
@@ -11,6 +11,7 @@
<script setup lang="ts">
import { CircleCloseFilled } from "@element-plus/icons-vue";
import { TEXT } from "../locales";
withDefaults(
defineProps<{
@@ -18,8 +19,8 @@ withDefaults(
description?: string;
}>(),
{
title: "加载失败",
description: "请稍后重试",
title: TEXT.common.messages.loadFailed,
description: TEXT.common.messages.requestFailed,
}
);
</script>
+6 -5
View File
@@ -2,15 +2,15 @@
<el-dropdown trigger="click" class="study-selector-dropdown" @command="onCommand">
<div class="selector-trigger">
<el-icon class="trigger-icon"><OfficeBuilding /></el-icon>
<span class="current-study-name">{{ study.currentStudy?.name || "选择项目" }}</span>
<span class="current-study-name">{{ study.currentStudy?.name || TEXT.common.empty.selectProject }}</span>
<el-icon class="arrow-icon"><ArrowDown /></el-icon>
</div>
<template #dropdown>
<el-dropdown-menu class="study-dropdown">
<el-dropdown-item v-if="loading" disabled>
<el-icon><Loading /></el-icon>加载项目中...
<el-icon><Loading /></el-icon>{{ TEXT.common.loading }}
</el-dropdown-item>
<el-dropdown-item v-else-if="!studies.length" disabled>暂无可用项目</el-dropdown-item>
<el-dropdown-item v-else-if="!studies.length" disabled>{{ TEXT.common.empty.noData }}</el-dropdown-item>
<el-dropdown-item
v-for="item in studies"
:key="item.id"
@@ -23,7 +23,7 @@
</div>
</el-dropdown-item>
<el-dropdown-item divided command="clear" class="danger-item">
<el-icon><Close /></el-icon>退出当前项目
<el-icon><Close /></el-icon>{{ TEXT.common.actions.exitProject }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
@@ -38,6 +38,7 @@ import { useStudyStore } from "../store/study";
import { fetchStudies } from "../api/studies";
import type { Study } from "../types/api";
import { OfficeBuilding, ArrowDown, Close, Loading } from "@element-plus/icons-vue";
import { TEXT } from "../locales";
const router = useRouter();
const study = useStudyStore();
@@ -50,7 +51,7 @@ const loadStudies = async () => {
const { data } = await fetchStudies();
studies.value = 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;
}
+9 -8
View File
@@ -2,22 +2,22 @@
<div class="thread-composer">
<div v-if="quoteItem" class="quote-box">
<div class="quote-meta">
引用 {{ displayUser(quoteItem.created_by, { users: userMap, members: memberMap }) }}
{{ TEXT.common.actions.quote }} {{ displayUser(quoteItem.created_by, { users: userMap, members: memberMap }) }}
· {{ displayDateTime(quoteItem.created_at) }}
<el-button type="text" size="small" @click="$emit('clear-quote')">取消引用</el-button>
<el-button type="text" size="small" @click="$emit('clear-quote')">{{ TEXT.modules.knowledgeMedicalConsult.clearQuote }}</el-button>
</div>
<div class="quote-content">{{ quoteContent(quoteItem) }}</div>
</div>
<el-input v-model="contentProxy" type="textarea" rows="3" placeholder="输入内容" />
<el-input v-model="contentProxy" type="textarea" rows="3" :placeholder="TEXT.common.placeholders.input" />
<div v-if="allowAttachments" class="upload-row">
<div class="upload-label">附件</div>
<div class="upload-label">{{ TEXT.common.labels.attachments }}</div>
<el-upload v-model:file-list="fileListProxy" :auto-upload="false" multiple list-type="picture" class="thread-upload">
<el-button size="small" class="upload-button">上传附件</el-button>
<el-button size="small" class="upload-button">{{ TEXT.common.actions.upload }}</el-button>
</el-upload>
</div>
<div class="actions">
<el-button v-if="showCancel" size="small" @click="$emit('cancel')">取消</el-button>
<el-button size="small" type="primary" :loading="submitting" @click="$emit('submit')">提交</el-button>
<el-button v-if="showCancel" size="small" @click="$emit('cancel')">{{ TEXT.common.actions.cancel }}</el-button>
<el-button size="small" type="primary" :loading="submitting" @click="$emit('submit')">{{ TEXT.common.actions.submit }}</el-button>
</div>
</div>
</template>
@@ -26,6 +26,7 @@
import { computed } from "vue";
import type { UploadUserFile } from "element-plus";
import { displayDateTime, displayUser } from "../utils/display";
import { TEXT } from "../locales";
const props = defineProps<{
modelValue: string;
@@ -56,7 +57,7 @@ const fileListProxy = computed({
set: (val: UploadUserFile[]) => emit("update:fileList", val),
});
const quoteContent = (item: any) => (item?.is_deleted ? "内容已删除" : item?.content || "—");
const quoteContent = (item: any) => (item?.is_deleted ? TEXT.modules.knowledgeMedicalConsult.quoteDeleted : item?.content || TEXT.common.fallback);
</script>
<style scoped>
+8 -5
View File
@@ -5,9 +5,9 @@
<div class="thread-meta">
<strong>{{ displayUser(item.created_by, { users: userMap, members: memberMap }) }}</strong>
<div class="thread-actions">
<el-button v-if="canQuote" type="text" size="small" @click="$emit('quote', item)">引用</el-button>
<el-button v-if="canQuote" type="text" size="small" @click="$emit('quote', item)">{{ TEXT.common.actions.quote }}</el-button>
<el-button v-if="canDelete?.(item)" type="text" size="small" class="danger" @click="$emit('delete', item)">
删除
{{ TEXT.common.actions.delete }}
</el-button>
<slot name="actions" :item="item" />
</div>
@@ -33,7 +33,7 @@
{{ file.filename }}
</div>
<el-link v-else :underline="false" class="file-link" @click="download(file.id)">
<span class="file-badge">文件</span>
<span class="file-badge">{{ TEXT.common.labels.file }}</span>
<span class="file-name">{{ file.filename }}</span>
</el-link>
</div>
@@ -46,6 +46,7 @@
<script setup lang="ts">
import { displayDateTime, displayUser } from "../utils/display";
import { TEXT } from "../locales";
const props = defineProps<{
items: any[];
@@ -62,8 +63,10 @@ defineEmits<{
(e: "delete", item: any): void;
}>();
const quoteContent = (item: any) => (item?.is_deleted ? "内容已删除" : item?.content || "—");
const contentText = (item: any) => (item?.is_deleted ? "内容已删除" : item?.content || "—");
const quoteContent = (item: any) =>
item?.is_deleted ? TEXT.modules.knowledgeMedicalConsult.quoteDeleted : item?.content || TEXT.common.fallback;
const contentText = (item: any) =>
item?.is_deleted ? TEXT.modules.knowledgeMedicalConsult.quoteDeleted : item?.content || TEXT.common.fallback;
const attachmentUrl = (id: string) => {
const token = localStorage.getItem("ctms_token");
@@ -1,7 +1,7 @@
<template>
<el-card>
<div class="header">
<span>附件</span>
<span>{{ TEXT.common.labels.attachments }}</span>
<AttachmentUploader
:study-id="studyId"
:entity-type="entityType"
@@ -10,23 +10,23 @@
/>
</div>
<el-table :data="attachments" v-loading="loading" style="width: 100%">
<el-table-column prop="filename" label="文件名" min-width="200" />
<el-table-column label="大小" width="120">
<el-table-column prop="filename" :label="TEXT.common.labels.filename" min-width="200" />
<el-table-column :label="TEXT.common.labels.size" width="120">
<template #default="scope">{{ formatFileSize(scope.row.file_size ?? scope.row.size) }}</template>
</el-table-column>
<el-table-column label="上传人" width="180">
<el-table-column :label="TEXT.common.labels.uploader" width="180">
<template #default="scope">
{{ uploaderLabel(scope.row) }}
</template>
</el-table-column>
<el-table-column prop="uploaded_at" label="上传时间" width="180">
<el-table-column prop="uploaded_at" :label="TEXT.common.labels.uploadedAt" width="180">
<template #default="scope">
{{ displayDateTime(scope.row.uploaded_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<el-table-column :label="TEXT.common.labels.actions" width="180">
<template #default="scope">
<el-button link type="primary" size="small" @click="download(scope.row)">下载</el-button>
<el-button link type="primary" size="small" @click="download(scope.row)">{{ TEXT.common.actions.download }}</el-button>
<el-button
v-if="canDelete(scope.row)"
link
@@ -34,7 +34,7 @@
size="small"
@click="remove(scope.row)"
>
删除
{{ TEXT.common.actions.delete }}
</el-button>
</template>
</el-table-column>
@@ -52,6 +52,7 @@ import { useAuthStore } from "../../store/auth";
import { useStudyStore } from "../../store/study";
import { listMembers } from "../../api/members";
import { displayDateTime, displayUser, getMemberDisplayName, getUserDisplayName } from "../../utils/display";
import { TEXT } from "../../locales";
const props = defineProps<{
studyId: string;
@@ -72,7 +73,7 @@ const load = async () => {
const { data } = await fetchAttachments(props.studyId, props.entityType, props.entityId);
attachments.value = data.items || data || [];
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "附件加载失败");
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
} finally {
loading.value = false;
}
@@ -95,7 +96,7 @@ const uploaderLabel = (row: any) => {
return acc;
}, {});
if (row?.uploaded_by && typeof row.uploaded_by === "object") {
return getUserDisplayName(row.uploaded_by) || row.uploaded_by.id || "—";
return getUserDisplayName(row.uploaded_by) || row.uploaded_by.id || TEXT.common.fallback;
}
return displayUser(row.uploaded_by_id || row.uploaded_by, { members: memberMap });
};
@@ -116,14 +117,14 @@ const canDelete = (row: any) => {
};
const remove = async (row: any) => {
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 deleteAttachment(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);
}
};
@@ -7,7 +7,7 @@
:disabled="loading"
:auto-upload="true"
>
<el-button type="primary" :loading="loading">上传附件</el-button>
<el-button type="primary" :loading="loading">{{ TEXT.common.actions.upload }}</el-button>
</el-upload>
<el-progress v-if="progress > 0 && progress < 100" :percentage="progress" :stroke-width="6" />
</div>
@@ -17,6 +17,7 @@
import { ref } from "vue";
import { ElMessage } from "element-plus";
import { uploadAttachment } from "../../api/attachments";
import { TEXT } from "../../locales";
const props = defineProps<{
studyId: string;
@@ -34,7 +35,7 @@ const doUpload = async (options: any) => {
const file: File = options.file;
if (!file) return;
if (file.size > maxSize * 1024 * 1024) {
ElMessage.error(`文件大小不能超过 ${maxSize}MB`);
ElMessage.error(`${TEXT.common.messages.fileTooLarge} ${maxSize}MB`);
return;
}
progress.value = 0;
@@ -47,10 +48,10 @@ const doUpload = async (options: any) => {
}
},
});
ElMessage.success("上传成功");
ElMessage.success(TEXT.common.messages.uploadSuccess);
emit("uploaded");
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "上传失败");
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.uploadFailed);
} finally {
progress.value = 0;
loading.value = false;
@@ -0,0 +1,49 @@
<template>
<el-select
:model-value="modelValue"
:multiple="multiple"
:placeholder="placeholder || TEXT.common.placeholders.select"
:filterable="filterable"
:clearable="clearable"
:loading="loading"
style="width: 100%"
@update:model-value="onUpdate"
>
<el-option v-for="opt in options" :key="String(opt.value)" :label="opt.label" :value="opt.value" />
<slot />
</el-select>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { TEXT } from "../../locales";
type OptionItem = {
label: string;
value: string | number;
};
const props = defineProps<{
modelValue?: string | number | Array<string | number> | null;
options?: OptionItem[];
multiple?: boolean;
placeholder?: string;
filterable?: boolean;
clearable?: boolean;
loading?: boolean;
}>();
const emit = defineEmits<{
(event: "update:modelValue", value: string | number | Array<string | number> | null): void;
}>();
const onUpdate = (value: string | number | Array<string | number> | null) => {
emit("update:modelValue", value);
};
const options = computed(() => props.options ?? []);
const multiple = computed(() => props.multiple ?? false);
const filterable = computed(() => props.filterable ?? true);
const clearable = computed(() => props.clearable ?? true);
const loading = computed(() => props.loading ?? false);
</script>