移除用户系统级角色,权限完全迁移至项目级管理
- 用户注册/创建不再需要选择角色,账号管理只管资料和状态 - 移除前后端所有系统级 role 字段的暴露,改用 is_admin 标识管理员 - PM(项目负责人)角色自动拥有全部项目权限且不可修改 - 系统管理员在项目中的成员身份不可被删除 - 管理界面全面美化 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,22 +1,59 @@
|
||||
<template>
|
||||
<el-dialog append-to=".layout-main .content-wrapper" :title="TEXT.modules.adminUsers.resetTitle" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<el-alert
|
||||
type="warning"
|
||||
show-icon
|
||||
:title="TEXT.modules.adminUsers.resetWarningTitle"
|
||||
:description="TEXT.modules.adminUsers.resetWarningDesc"
|
||||
class="mb-12"
|
||||
/>
|
||||
<div class="info">{{ TEXT.modules.adminUsers.resetTarget }}<strong>{{ user?.username }}</strong></div>
|
||||
<el-form ref="formRef" :model="form" label-width="140px" :rules="rules">
|
||||
<el-dialog
|
||||
append-to=".layout-main .content-wrapper"
|
||||
:title="TEXT.modules.adminUsers.resetTitle"
|
||||
width="540px"
|
||||
v-model="visibleProxy"
|
||||
:close-on-click-modal="false"
|
||||
class="reset-password-dialog"
|
||||
>
|
||||
<!-- 警告提示 -->
|
||||
<div class="reset-warning">
|
||||
<div class="warning-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
||||
<line x1="12" y1="9" x2="12" y2="13"/>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="warning-body">
|
||||
<span class="warning-title">{{ TEXT.modules.adminUsers.resetWarningTitle }}</span>
|
||||
<span class="warning-desc">{{ TEXT.modules.adminUsers.resetWarningDesc }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 目标用户 -->
|
||||
<div class="target-user">
|
||||
<div class="target-avatar">
|
||||
{{ getInitials(user?.full_name || user?.username || '') }}
|
||||
</div>
|
||||
<div class="target-info">
|
||||
<span class="target-name">{{ user?.full_name || user?.username }}</span>
|
||||
<span class="target-email">{{ user?.email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form ref="formRef" :model="form" label-width="100px" :rules="rules" class="reset-form">
|
||||
<el-form-item :label="TEXT.modules.adminUsers.confirmUsername" prop="confirmUsername">
|
||||
<el-input v-model="form.confirmUsername" autocomplete="off" name="ctms-reset-confirm-username" :placeholder="TEXT.modules.adminUsers.confirmUsernameHint" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="TEXT.modules.adminUsers.passwordMode" prop="mode">
|
||||
<el-radio-group v-model="form.mode">
|
||||
<el-radio label="auto">{{ TEXT.modules.adminUsers.passwordAuto }}</el-radio>
|
||||
<el-radio label="manual">{{ TEXT.modules.adminUsers.passwordManual }}</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="mode-cards">
|
||||
<div class="mode-card" :class="{ active: form.mode === 'auto' }" @click="form.mode = 'auto'">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
||||
<path d="M7 11V7a5 5 0 0 1 9.9-1"/>
|
||||
</svg>
|
||||
<span>{{ TEXT.modules.adminUsers.passwordAuto }}</span>
|
||||
</div>
|
||||
<div class="mode-card" :class="{ active: form.mode === 'manual' }" @click="form.mode = 'manual'">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
|
||||
<path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
</svg>
|
||||
<span>{{ TEXT.modules.adminUsers.passwordManual }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="form.mode === 'auto' ? TEXT.modules.adminUsers.tempPassword : TEXT.modules.adminUsers.newPassword"
|
||||
@@ -32,7 +69,13 @@
|
||||
readonly
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="regenerate">{{ TEXT.modules.adminUsers.regenerate }}</el-button>
|
||||
<el-button @click="regenerate">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14" style="margin-right:4px">
|
||||
<polyline points="23 4 23 10 17 10"/>
|
||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
||||
</svg>
|
||||
{{ TEXT.modules.adminUsers.regenerate }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-input
|
||||
@@ -44,12 +87,21 @@
|
||||
name="ctms-reset-manual-password"
|
||||
:placeholder="TEXT.modules.adminUsers.manualPasswordHint"
|
||||
/>
|
||||
<div class="hint">{{ TEXT.modules.adminUsers.passwordHint }}</div>
|
||||
<div class="password-hint">{{ TEXT.modules.adminUsers.passwordHint }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<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>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="visibleProxy = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="danger" :disabled="!canSubmit" :loading="submitting" @click="onSubmit">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14" style="margin-right:4px">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||
</svg>
|
||||
{{ TEXT.modules.adminUsers.resetConfirm }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -85,6 +137,13 @@ const form = reactive({
|
||||
manualPassword: "",
|
||||
});
|
||||
|
||||
const getInitials = (name: string) => {
|
||||
if (!name) return '?';
|
||||
const parts = name.trim().split(/\s+/);
|
||||
if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
|
||||
return name.slice(0, 2).toUpperCase();
|
||||
};
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
confirmUsername: [{ required: true, message: requiredMessage(TEXT.modules.adminUsers.confirmUsername), trigger: "blur" }],
|
||||
mode: [{ required: true, message: requiredMessage(TEXT.modules.adminUsers.passwordMode), trigger: "change" }],
|
||||
@@ -148,9 +207,7 @@ watch(
|
||||
|
||||
watch(
|
||||
() => form.mode,
|
||||
() => {
|
||||
clearPasswordValidation();
|
||||
}
|
||||
() => { clearPasswordValidation(); }
|
||||
);
|
||||
|
||||
const canSubmit = computed(() => !!props.user && form.confirmUsername === props.user.username);
|
||||
@@ -188,15 +245,129 @@ const onSubmit = async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mb-12 {
|
||||
margin-bottom: 12px;
|
||||
.reset-warning {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
background: #fef3e2;
|
||||
border: 1px solid #fde68a;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.info {
|
||||
margin-bottom: 8px;
|
||||
|
||||
.warning-icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--ctms-warning);
|
||||
margin-top: 1px;
|
||||
}
|
||||
.hint {
|
||||
|
||||
.warning-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.warning-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.warning-desc {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
color: #a16207;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.target-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
background: var(--ctms-neutral-100);
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.target-avatar {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, var(--ctms-primary), var(--ctms-primary-active));
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.target-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.target-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--ctms-text-main);
|
||||
}
|
||||
|
||||
.target-email {
|
||||
font-size: 12px;
|
||||
color: var(--ctms-text-secondary);
|
||||
}
|
||||
|
||||
.reset-form :deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mode-cards {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mode-card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
border: 2px solid var(--ctms-border-color);
|
||||
cursor: pointer;
|
||||
transition: var(--ctms-transition);
|
||||
color: var(--ctms-text-secondary);
|
||||
}
|
||||
|
||||
.mode-card:hover {
|
||||
border-color: var(--ctms-border-color-hover);
|
||||
background: var(--ctms-neutral-100);
|
||||
}
|
||||
|
||||
.mode-card.active {
|
||||
border-color: var(--ctms-primary);
|
||||
background: var(--ctms-primary-light);
|
||||
color: var(--ctms-primary);
|
||||
}
|
||||
|
||||
.mode-card span {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.password-hint {
|
||||
font-size: 12px;
|
||||
color: var(--ctms-text-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user