完善后台管理与测试支撑
1、为后台用户列表增加关键词和状态筛选,后端同步支持过滤与总数统计。 2、创建用户时要求填写初始密码,并补充前端校验和自动填充隔离。 3、优化项目管理列表角色展示、项目详情抽屉脏数据保护和审计详情抽屉关闭体验。 4、清理旧费用附件关联、统一测试模型注册,并更新迁移端点和注册筛选测试。
This commit is contained in:
@@ -34,18 +34,18 @@
|
||||
</el-form>
|
||||
</section>
|
||||
<section class="unified-section">
|
||||
<div v-if="isEdit && meetingId" class="attachment-grid">
|
||||
<AttachmentList
|
||||
v-for="group in kickoffAttachmentGroups"
|
||||
:key="group.entityType"
|
||||
:study-id="studyId"
|
||||
:entity-type="group.entityType"
|
||||
:entity-id="meetingId"
|
||||
:title="group.title"
|
||||
:readonly="isReadOnly"
|
||||
/>
|
||||
<div class="ctms-section-header">
|
||||
<div class="ctms-section-title">{{ TEXT.common.labels.attachments }}</div>
|
||||
</div>
|
||||
<StateEmpty v-else :description="TEXT.modules.startupMeetingAuth.kickoffUploadHint" />
|
||||
<AttachmentList
|
||||
ref="attachmentPanelRef"
|
||||
:study-id="studyId"
|
||||
entity-type="startup_kickoff"
|
||||
:entity-id="meetingId || ''"
|
||||
:entity-groups="kickoffAttachmentGroups"
|
||||
:readonly="isReadOnly"
|
||||
:mode="'upload'"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,14 +62,16 @@ import { fetchSites } from "../../api/sites";
|
||||
import { listMembers } from "../../api/members";
|
||||
import { fetchUsers } from "../../api/users";
|
||||
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
import { usePermission } from "../../utils/permission";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
const study = useStudyStore();
|
||||
const { can } = usePermission();
|
||||
const saving = ref(false);
|
||||
const attachmentPanelRef = ref<InstanceType<typeof AttachmentList> | null>(null);
|
||||
|
||||
const meetingId = computed(() => route.params.meetingId as string | undefined);
|
||||
const isEdit = computed(() => !!meetingId.value);
|
||||
@@ -79,6 +81,10 @@ const siteId = ref("");
|
||||
const users = ref<any[]>([]);
|
||||
const members = ref<any[]>([]);
|
||||
const isAdmin = computed(() => auth.user?.is_admin);
|
||||
const canReadMembers = computed(() => can("project.members.list"));
|
||||
const canCreateAuth = computed(() => can("startup.auth.create"));
|
||||
const canUpdateAuth = computed(() => can("startup.auth.update"));
|
||||
const canSaveAuth = computed(() => (isEdit.value ? canUpdateAuth.value : canCreateAuth.value));
|
||||
|
||||
const memberNameMap = computed(() => {
|
||||
const map: Record<string, string> = {};
|
||||
@@ -103,12 +109,13 @@ const ownerLabel = computed(() => {
|
||||
.map((c) => memberNameMap.value[c] || c)
|
||||
.join("、") || TEXT.common.fallback;
|
||||
});
|
||||
const isReadOnly = computed(() => siteInfo.is_active === false);
|
||||
const isInactiveSite = computed(() => siteInfo.is_active === false);
|
||||
const isReadOnly = computed(() => !canSaveAuth.value || isInactiveSite.value);
|
||||
const kickoffAttachmentGroups = [
|
||||
{ entityType: "startup_kickoff_minutes", title: TEXT.modules.startupMeetingAuth.kickoffMinutesLabel },
|
||||
{ entityType: "startup_kickoff_signin", title: TEXT.modules.startupMeetingAuth.kickoffSignInLabel },
|
||||
{ entityType: "startup_kickoff_ppt", title: TEXT.modules.startupMeetingAuth.kickoffPptLabel },
|
||||
{ entityType: "startup_kickoff", title: TEXT.modules.startupMeetingAuth.kickoffOtherLabel },
|
||||
{ entityType: "startup_kickoff_minutes", label: TEXT.modules.startupMeetingAuth.kickoffMinutesLabel },
|
||||
{ entityType: "startup_kickoff_signin", label: TEXT.modules.startupMeetingAuth.kickoffSignInLabel },
|
||||
{ entityType: "startup_kickoff_ppt", label: TEXT.modules.startupMeetingAuth.kickoffPptLabel },
|
||||
{ entityType: "startup_kickoff", label: TEXT.modules.startupMeetingAuth.kickoffOtherLabel },
|
||||
];
|
||||
|
||||
const form = reactive({
|
||||
@@ -149,7 +156,11 @@ const load = async () => {
|
||||
|
||||
const submit = async () => {
|
||||
if (!studyId.value) return;
|
||||
if (isReadOnly.value) {
|
||||
if (!canSaveAuth.value) {
|
||||
ElMessage.warning("权限不足");
|
||||
return;
|
||||
}
|
||||
if (isInactiveSite.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
@@ -159,15 +170,16 @@ const submit = async () => {
|
||||
kickoff_date: form.kickoff_date || null,
|
||||
attendees: parseAttendees(form.attendees),
|
||||
};
|
||||
let savedId = meetingId.value || "";
|
||||
if (isEdit.value && meetingId.value) {
|
||||
await updateKickoff(studyId.value, meetingId.value, payload);
|
||||
ElMessage.success(TEXT.common.messages.saveSuccess);
|
||||
router.push(`/startup/kickoff/${meetingId.value}`);
|
||||
} else {
|
||||
const { data } = await createKickoff(studyId.value, { ...payload, site_id: siteId.value });
|
||||
ElMessage.success(TEXT.common.messages.createSuccess);
|
||||
router.push(`/startup/kickoff/${data.id}`);
|
||||
savedId = data.id;
|
||||
}
|
||||
await attachmentPanelRef.value?.uploadPending(savedId);
|
||||
ElMessage.success(isEdit.value ? TEXT.common.messages.saveSuccess : TEXT.common.messages.createSuccess);
|
||||
router.push(`/startup/kickoff/${savedId}`);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
} finally {
|
||||
@@ -179,8 +191,11 @@ const goBack = () => router.push("/startup/meeting-auth");
|
||||
|
||||
onMounted(async () => {
|
||||
if (studyId.value) {
|
||||
const membersReq = listMembers(studyId.value, { limit: 500 });
|
||||
const usersReq = isAdmin.value ? fetchUsers({ limit: 500 }) : Promise.resolve(null);
|
||||
let membersReq: Promise<any> = Promise.resolve(null);
|
||||
if (canReadMembers.value) {
|
||||
membersReq = listMembers(studyId.value, { limit: 500 });
|
||||
}
|
||||
const [membersResp, usersResp] = await Promise.all([membersReq, usersReq]).catch(() => [null, null] as const);
|
||||
if (membersResp?.data) {
|
||||
members.value = Array.isArray(membersResp.data) ? membersResp.data : membersResp.data.items || [];
|
||||
@@ -200,9 +215,4 @@ onMounted(async () => {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.attachment-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -40,14 +40,17 @@
|
||||
</el-form>
|
||||
</section>
|
||||
<section class="unified-section">
|
||||
<div class="ctms-section-header">
|
||||
<div class="ctms-section-title">{{ TEXT.common.labels.attachments }}</div>
|
||||
</div>
|
||||
<AttachmentList
|
||||
v-if="isEdit && recordId"
|
||||
ref="attachmentPanelRef"
|
||||
:study-id="studyId"
|
||||
entity-type="training_authorization"
|
||||
:entity-id="recordId"
|
||||
:entity-id="recordId || ''"
|
||||
:readonly="isReadOnly"
|
||||
:mode="'upload'"
|
||||
/>
|
||||
<StateEmpty v-else :description="TEXT.modules.startupMeetingAuth.trainingUploadHint" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,13 +64,15 @@ import { useStudyStore } from "../../store/study";
|
||||
import { createTrainingAuthorization, getTrainingAuthorization, updateTrainingAuthorization } from "../../api/startup";
|
||||
import { fetchSites } from "../../api/sites";
|
||||
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
import { usePermission } from "../../utils/permission";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const study = useStudyStore();
|
||||
const { can } = usePermission();
|
||||
const saving = ref(false);
|
||||
const attachmentPanelRef = ref<InstanceType<typeof AttachmentList> | null>(null);
|
||||
const sites = ref<any[]>([]);
|
||||
|
||||
const recordId = computed(() => route.params.recordId as string | undefined);
|
||||
@@ -92,7 +97,11 @@ const siteActiveMap = computed(() => {
|
||||
});
|
||||
return map;
|
||||
});
|
||||
const isReadOnly = computed(() => isEdit.value && !!form.site_name && siteActiveMap.value[form.site_name] === false);
|
||||
const canCreateAuth = computed(() => can("startup.auth.create"));
|
||||
const canUpdateAuth = computed(() => can("startup.auth.update"));
|
||||
const canSaveAuth = computed(() => (isEdit.value ? canUpdateAuth.value : canCreateAuth.value));
|
||||
const isInactiveSite = computed(() => isEdit.value && !!form.site_name && siteActiveMap.value[form.site_name] === false);
|
||||
const isReadOnly = computed(() => !canSaveAuth.value || isInactiveSite.value);
|
||||
|
||||
const loadSites = async () => {
|
||||
if (!studyId.value) return;
|
||||
@@ -125,7 +134,11 @@ const load = async () => {
|
||||
|
||||
const submit = async () => {
|
||||
if (!studyId.value) return;
|
||||
if (isReadOnly.value) {
|
||||
if (!canSaveAuth.value) {
|
||||
ElMessage.warning("权限不足");
|
||||
return;
|
||||
}
|
||||
if (isInactiveSite.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
@@ -145,15 +158,16 @@ const submit = async () => {
|
||||
authorized_date: form.authorized_date || null,
|
||||
remark: form.remark || null,
|
||||
};
|
||||
let savedId = recordId.value || "";
|
||||
if (isEdit.value && recordId.value) {
|
||||
await updateTrainingAuthorization(studyId.value, recordId.value, payload);
|
||||
ElMessage.success(TEXT.common.messages.saveSuccess);
|
||||
router.push(`/startup/training/${recordId.value}`);
|
||||
} else {
|
||||
const { data } = await createTrainingAuthorization(studyId.value, payload);
|
||||
ElMessage.success(TEXT.common.messages.createSuccess);
|
||||
router.push(`/startup/training/${data.id}`);
|
||||
savedId = data.id;
|
||||
}
|
||||
await attachmentPanelRef.value?.uploadPending(savedId);
|
||||
ElMessage.success(isEdit.value ? TEXT.common.messages.saveSuccess : TEXT.common.messages.createSuccess);
|
||||
router.push(`/startup/training/${savedId}`);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user