全局中文化
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: "",
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user