新增用户注册功能
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
<el-card class="login-card">
|
||||
<div class="login-header">
|
||||
<h2 class="form-title">用户登录</h2>
|
||||
<p class="form-desc">请输入您的账号与密码以访问系统</p>
|
||||
<p class="form-desc">请输入邮箱与密码登录,未激活账号将无法登录</p>
|
||||
</div>
|
||||
|
||||
<el-form
|
||||
@@ -19,11 +19,11 @@
|
||||
label-position="top"
|
||||
@keyup.enter.native="onSubmit"
|
||||
>
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input
|
||||
v-model="form.username"
|
||||
placeholder="请输入用户名"
|
||||
autocomplete="username"
|
||||
v-model="form.email"
|
||||
placeholder="请输入邮箱"
|
||||
autocomplete="email"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password">
|
||||
@@ -37,6 +37,7 @@
|
||||
</el-form-item>
|
||||
|
||||
<div class="form-footer">
|
||||
<el-checkbox v-model="form.remember">记住账号密码</el-checkbox>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
@@ -45,6 +46,9 @@
|
||||
>
|
||||
登 录
|
||||
</el-button>
|
||||
<div class="link-row">
|
||||
<RouterLink to="/register">没有账号?注册</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -57,40 +61,60 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from "vue";
|
||||
import { reactive, ref, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import type { FormInstance, FormRules } from "element-plus";
|
||||
import { ElCheckbox, ElMessage, type FormInstance, type FormRules } from "element-plus";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import { getCachedCredential, setCachedCredential, clearCachedCredential } from "../utils/auth";
|
||||
|
||||
const auth = useAuthStore();
|
||||
const router = useRouter();
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const form = reactive({
|
||||
username: "",
|
||||
email: "",
|
||||
password: "",
|
||||
remember: false,
|
||||
});
|
||||
|
||||
const rules: FormRules<typeof form> = {
|
||||
username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
|
||||
email: [
|
||||
{ required: true, message: "请输入邮箱", trigger: "blur" },
|
||||
{ type: "email", message: "邮箱格式不正确", trigger: "blur" },
|
||||
],
|
||||
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
|
||||
};
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
const cached = getCachedCredential();
|
||||
if (cached) {
|
||||
form.email = cached.email;
|
||||
form.password = cached.password;
|
||||
form.remember = true;
|
||||
}
|
||||
});
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
await auth.login(form.username, form.password);
|
||||
await auth.fetchMe();
|
||||
router.push("/");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
const valid = await formRef.value.validate();
|
||||
if (!valid) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
await auth.login(form.email, form.password);
|
||||
if (form.remember) {
|
||||
setCachedCredential(form.email, form.password);
|
||||
} else {
|
||||
clearCachedCredential();
|
||||
}
|
||||
});
|
||||
router.push("/");
|
||||
} catch (error: any) {
|
||||
const detail = error?.response?.data?.detail || error?.response?.data?.message;
|
||||
ElMessage.error(detail || "邮箱或密码错误");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -169,6 +193,14 @@ const onSubmit = async () => {
|
||||
font-size: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.link-row {
|
||||
margin-top: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.link-row a {
|
||||
color: var(--ctms-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
|
||||
@@ -102,7 +102,7 @@ const loadMilestones = async () => {
|
||||
const { data } = await fetchMilestones(study.currentStudy.id);
|
||||
milestones.value = data.items || data;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "里程碑加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || "节点加载失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -150,7 +150,13 @@ const statusColor = (v: string) => getMilestoneStatusColor(v);
|
||||
|
||||
const memberMap = computed(() =>
|
||||
members.value.reduce<Record<string, string>>((acc, cur) => {
|
||||
const username = cur?.user?.display_name || cur?.user?.username || cur?.username;
|
||||
const username =
|
||||
cur?.user?.full_name ||
|
||||
cur?.user?.display_name ||
|
||||
cur?.user?.username ||
|
||||
cur?.user?.email ||
|
||||
cur?.username ||
|
||||
cur?.email;
|
||||
if (cur?.user_id) acc[cur.user_id] = username || cur.user_id;
|
||||
return acc;
|
||||
}, {})
|
||||
@@ -166,8 +172,10 @@ const siteName = (row: any) => {
|
||||
const ownerName = (ownerId: string) => {
|
||||
const fromPayload = milestones.value.find((m) => m.owner_id === ownerId)?.owner;
|
||||
return (
|
||||
fromPayload?.full_name ||
|
||||
fromPayload?.display_name ||
|
||||
fromPayload?.username ||
|
||||
fromPayload?.email ||
|
||||
displayUser(ownerId, { members: memberMap.value })
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<el-card>
|
||||
<h3 class="title">个人设置</h3>
|
||||
<p class="subtitle">更新个人信息或修改登录密码</p>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" class="form">
|
||||
<el-form-item label="头像">
|
||||
<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() }}
|
||||
</el-avatar>
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
:show-file-list="false"
|
||||
action="/api/v1/auth/me/avatar"
|
||||
name="file"
|
||||
:headers="uploadHeaders"
|
||||
:on-success="onAvatarUploaded"
|
||||
:on-error="onAvatarError"
|
||||
>
|
||||
<el-button>上传头像</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱">
|
||||
<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>
|
||||
<el-form-item label="部门" prop="department">
|
||||
<el-input v-model="form.department" placeholder="请输入部门" />
|
||||
</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>
|
||||
<el-form-item label="新密码" prop="password">
|
||||
<el-input v-model="form.password" type="password" show-password placeholder="不修改请留空" />
|
||||
</el-form-item>
|
||||
<el-form-item label="确认新密码" prop="confirmPassword">
|
||||
<el-input v-model="form.confirmPassword" type="password" show-password placeholder="再次输入新密码" />
|
||||
</el-form-item>
|
||||
<div class="actions">
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">保存</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import type { FormInstance, FormRules } from "element-plus";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { updateProfile, fetchMe } from "../api/auth";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import { getToken } from "../utils/auth";
|
||||
|
||||
const auth = useAuthStore();
|
||||
const formRef = ref<FormInstance>();
|
||||
const submitting = ref(false);
|
||||
const form = reactive({
|
||||
email: "",
|
||||
full_name: "",
|
||||
department: "",
|
||||
current_password: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
const avatarPreview = ref<string | undefined>();
|
||||
const uploadHeaders = computed<Record<string, string>>(() => ({
|
||||
Authorization: `Bearer ${getToken() || ""}`,
|
||||
}));
|
||||
|
||||
const rules: FormRules<typeof form> = {
|
||||
full_name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
|
||||
department: [{ required: true, message: "请输入部门", trigger: "blur" }],
|
||||
current_password: [
|
||||
{
|
||||
validator: (_r, value, cb) => {
|
||||
if (form.password && !value) {
|
||||
cb(new Error("修改密码需输入当前密码"));
|
||||
return;
|
||||
}
|
||||
cb();
|
||||
},
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
password: [
|
||||
{
|
||||
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 位且包含字母与数字"));
|
||||
return;
|
||||
}
|
||||
cb();
|
||||
},
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
confirmPassword: [
|
||||
{
|
||||
validator: (_r, value, cb) => {
|
||||
if (form.password && value !== form.password) {
|
||||
cb(new Error("两次输入的密码不一致"));
|
||||
return;
|
||||
}
|
||||
cb();
|
||||
},
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const loadProfile = async () => {
|
||||
const { data } = await fetchMe();
|
||||
form.email = data.email;
|
||||
form.full_name = data.full_name;
|
||||
form.department = data.department;
|
||||
avatarPreview.value = data.avatar_url || undefined;
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
submitting.value = true;
|
||||
try {
|
||||
await updateProfile({
|
||||
full_name: form.full_name,
|
||||
department: form.department,
|
||||
current_password: form.password ? form.current_password : undefined,
|
||||
password: form.password || undefined,
|
||||
});
|
||||
await auth.fetchMe();
|
||||
ElMessage.success("已更新");
|
||||
form.current_password = "";
|
||||
form.password = "";
|
||||
form.confirmPassword = "";
|
||||
avatarPreview.value = auth.user?.avatar_url || avatarPreview.value;
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || "更新失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onAvatarUploaded = async () => {
|
||||
await auth.fetchMe();
|
||||
avatarPreview.value = auth.user?.avatar_url || undefined;
|
||||
ElMessage.success("头像已更新");
|
||||
};
|
||||
|
||||
const onAvatarError = (err: any) => {
|
||||
ElMessage.error(err?.response?.data?.message || "头像上传失败");
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadProfile();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
.title {
|
||||
margin: 0;
|
||||
}
|
||||
.subtitle {
|
||||
margin: 6px 0 16px;
|
||||
color: #666;
|
||||
}
|
||||
.form {
|
||||
max-width: 520px;
|
||||
}
|
||||
.actions {
|
||||
margin-top: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<div class="register-container">
|
||||
<div class="register-card">
|
||||
<div class="header">
|
||||
<h2>创建账号</h2>
|
||||
<p>提交后需管理员审核通过方可登录</p>
|
||||
</div>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
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>
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
placeholder="至少 8 位且包含字母与数字"
|
||||
show-password
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码" prop="confirmPassword">
|
||||
<el-input
|
||||
v-model="form.confirmPassword"
|
||||
type="password"
|
||||
placeholder="再次输入密码"
|
||||
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>
|
||||
<el-form-item label="部门" prop="department">
|
||||
<el-input v-model="form.department" placeholder="请输入部门" />
|
||||
</el-form-item>
|
||||
<div class="actions">
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">提交注册</el-button>
|
||||
<RouterLink to="/login">已有账号?返回登录</RouterLink>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
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";
|
||||
|
||||
const router = useRouter();
|
||||
const defaultRole: RegisterRequest["role"] = "CRA";
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const form = reactive({
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
full_name: "",
|
||||
department: "",
|
||||
});
|
||||
const submitting = ref(false);
|
||||
|
||||
const passwordRule = {
|
||||
validator: (_: any, value: string, callback: (err?: Error) => void) => {
|
||||
if (!value || value.length < 8) {
|
||||
callback(new Error("密码至少 8 位"));
|
||||
return;
|
||||
}
|
||||
if (!/[A-Za-z]/.test(value) || !/[0-9]/.test(value)) {
|
||||
callback(new Error("需包含字母和数字"));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: "blur",
|
||||
};
|
||||
|
||||
const rules: FormRules<typeof form> = {
|
||||
email: [
|
||||
{ required: true, message: "请输入邮箱", trigger: "blur" },
|
||||
{ type: "email", message: "邮箱格式不正确", trigger: "blur" },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: "请输入密码", trigger: "blur" },
|
||||
passwordRule,
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: "请确认密码", trigger: "blur" },
|
||||
{
|
||||
validator: (_: any, value: string, callback: (err?: Error) => void) => {
|
||||
if (value !== form.password) {
|
||||
callback(new Error("两次输入的密码不一致"));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
full_name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
|
||||
department: [{ required: true, message: "请输入部门", trigger: "blur" }],
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
submitting.value = true;
|
||||
try {
|
||||
await apiRegister({
|
||||
email: form.email,
|
||||
password: form.password,
|
||||
full_name: form.full_name,
|
||||
role: defaultRole,
|
||||
department: form.department,
|
||||
});
|
||||
ElMessage.success("注册成功,等待管理员审核");
|
||||
setTimeout(() => {
|
||||
router.push("/login");
|
||||
}, 3000);
|
||||
Object.assign(form, {
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
full_name: "",
|
||||
department: "",
|
||||
});
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.register-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #f7f9fb 0%, #eef2f7 100%);
|
||||
padding: 24px;
|
||||
}
|
||||
.register-card {
|
||||
width: 420px;
|
||||
background: #fff;
|
||||
padding: 24px 28px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #f0f2f5;
|
||||
}
|
||||
.header h2 {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
color: #1f2d3d;
|
||||
}
|
||||
.header p {
|
||||
margin: 6px 0 16px;
|
||||
color: #6c7a89;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.actions a {
|
||||
color: var(--ctms-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<el-card>
|
||||
<div class="header">
|
||||
<div>
|
||||
<h3>注册审核</h3>
|
||||
<p>仅显示待审核账号,审核通过后即可登录</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-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">
|
||||
<template #default="scope">{{ displayDateTime(scope.row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="statusType(scope.row.status)">{{ scope.row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-if="scope.row.status === 'PENDING'"
|
||||
type="success"
|
||||
link
|
||||
size="small"
|
||||
@click="onApprove(scope.row.id)"
|
||||
>
|
||||
审核通过
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'PENDING'"
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
@click="onReject(scope.row.id)"
|
||||
>
|
||||
拒绝
|
||||
</el-button>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
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";
|
||||
|
||||
const users = ref<UserInfo[]>([]);
|
||||
const loading = ref(false);
|
||||
const status = ref<UserStatus>("PENDING");
|
||||
|
||||
const statusType = (s: UserStatus) => {
|
||||
switch (s) {
|
||||
case "ACTIVE":
|
||||
return "success";
|
||||
case "PENDING":
|
||||
return "warning";
|
||||
case "REJECTED":
|
||||
return "danger";
|
||||
default:
|
||||
return "info";
|
||||
}
|
||||
};
|
||||
|
||||
const loadUsers = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await listPendingUsers(status.value);
|
||||
users.value = data.items || [];
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || "加载待审核用户失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onApprove = async (id: string) => {
|
||||
try {
|
||||
await approveUser(id);
|
||||
ElMessage.success("已通过审核");
|
||||
loadUsers();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || "审核失败");
|
||||
}
|
||||
};
|
||||
|
||||
const onReject = async (id: string) => {
|
||||
try {
|
||||
await rejectUser(id);
|
||||
ElMessage.success("已拒绝");
|
||||
loadUsers();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || "拒绝失败");
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadUsers();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.header h3 {
|
||||
margin: 0;
|
||||
}
|
||||
.header p {
|
||||
margin: 4px 0 0;
|
||||
color: #6c7a89;
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,14 @@
|
||||
<template>
|
||||
<el-dialog :title="user ? '编辑用户' : '新建用户'" 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="username">
|
||||
<el-input v-model="form.username" :disabled="!!user" placeholder="请输入用户名" />
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="form.email" :disabled="!!user" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名" prop="full_name">
|
||||
<el-input v-model="form.full_name" placeholder="请输入姓名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门" prop="department">
|
||||
<el-input v-model="form.department" placeholder="请输入部门" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="user ? '新密码(可选)' : '初始密码'" prop="password">
|
||||
<el-input v-model="form.password" type="password" show-password :placeholder="user ? '留空则不修改密码' : '请输入初始密码'" />
|
||||
@@ -42,13 +48,22 @@ const visibleProxy = computed({
|
||||
const formRef = ref<FormInstance>();
|
||||
const submitting = ref(false);
|
||||
const form = reactive({
|
||||
username: "",
|
||||
email: "",
|
||||
full_name: "",
|
||||
department: "",
|
||||
password: "",
|
||||
is_active: true,
|
||||
});
|
||||
|
||||
const defaultRole = "PV";
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
|
||||
email: [
|
||||
{ required: true, message: "请输入邮箱", trigger: "blur" },
|
||||
{ type: "email", message: "邮箱格式不正确", trigger: "blur" },
|
||||
],
|
||||
full_name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
|
||||
department: [{ required: true, message: "请输入部门", trigger: "blur" }],
|
||||
password: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
@@ -64,7 +79,9 @@ const rules = reactive<FormRules>({
|
||||
});
|
||||
|
||||
const resetForm = () => {
|
||||
form.username = "";
|
||||
form.email = "";
|
||||
form.full_name = "";
|
||||
form.department = "";
|
||||
form.password = "";
|
||||
form.is_active = true;
|
||||
};
|
||||
@@ -75,7 +92,9 @@ watch(
|
||||
if (val) {
|
||||
resetForm();
|
||||
if (props.user) {
|
||||
form.username = props.user.username;
|
||||
form.email = props.user.email;
|
||||
form.full_name = props.user.full_name;
|
||||
form.department = props.user.department;
|
||||
form.is_active = props.user.is_active;
|
||||
}
|
||||
}
|
||||
@@ -90,13 +109,17 @@ const onSubmit = async () => {
|
||||
if (props.user) {
|
||||
await updateUser(props.user.id, {
|
||||
is_active: form.is_active,
|
||||
full_name: form.full_name,
|
||||
department: form.department,
|
||||
password: form.password || undefined,
|
||||
});
|
||||
ElMessage.success("用户已更新");
|
||||
} else {
|
||||
await createUser({
|
||||
username: form.username,
|
||||
role: "PV",
|
||||
email: form.email,
|
||||
full_name: form.full_name,
|
||||
department: form.department,
|
||||
role: defaultRole,
|
||||
password: form.password,
|
||||
});
|
||||
ElMessage.success("用户已创建");
|
||||
|
||||
@@ -9,11 +9,14 @@
|
||||
<el-button type="primary" @click="openCreate">新建用户</el-button>
|
||||
</div>
|
||||
<el-table :data="users" stripe v-loading="loading" style="width: 100%">
|
||||
<el-table-column prop="username" label="用户名" min-width="140" />
|
||||
<el-table-column label="状态" width="120">
|
||||
<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">
|
||||
<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="statusType(scope.row.status)">
|
||||
{{ statusLabel(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="200">
|
||||
@@ -24,11 +27,11 @@
|
||||
<el-button link type="primary" size="small" @click="openEdit(scope.row)">编辑</el-button>
|
||||
<el-button
|
||||
link
|
||||
:type="scope.row.is_active ? 'danger' : 'primary'"
|
||||
:type="scope.row.status === 'ACTIVE' ? 'danger' : 'primary'"
|
||||
size="small"
|
||||
@click="toggleStatus(scope.row)"
|
||||
>
|
||||
{{ scope.row.is_active ? "禁用" : "启用" }}
|
||||
{{ scope.row.status === 'ACTIVE' ? "禁用" : "启用" }}
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="openReset(scope.row)">重置密码</el-button>
|
||||
<el-button
|
||||
@@ -105,7 +108,7 @@ const openEdit = (row: UserInfo) => {
|
||||
};
|
||||
|
||||
const toggleStatus = async (row: UserInfo) => {
|
||||
const disable = row.is_active;
|
||||
const disable = row.status === "ACTIVE";
|
||||
const ok = await ElMessageBox.confirm(
|
||||
disable ? "该操作将影响用户账号,请确认是否继续" : "确认启用该用户账号?",
|
||||
disable ? "禁用账号" : "启用账号",
|
||||
@@ -117,7 +120,7 @@ const toggleStatus = async (row: UserInfo) => {
|
||||
).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await updateUser(row.id, { is_active: !row.is_active });
|
||||
await updateUser(row.id, { status: disable ? "DISABLED" : "ACTIVE", is_active: !disable });
|
||||
ElMessage.success(disable ? "已禁用" : "已启用");
|
||||
loadUsers();
|
||||
} catch (e: any) {
|
||||
@@ -161,6 +164,34 @@ const onDelete = async (row: UserInfo) => {
|
||||
onMounted(() => {
|
||||
loadUsers();
|
||||
});
|
||||
|
||||
const statusType = (status: string) => {
|
||||
switch (status) {
|
||||
case "ACTIVE":
|
||||
return "success";
|
||||
case "PENDING":
|
||||
return "warning";
|
||||
case "REJECTED":
|
||||
return "danger";
|
||||
default:
|
||||
return "info";
|
||||
}
|
||||
};
|
||||
|
||||
const statusLabel = (status: string) => {
|
||||
switch (status) {
|
||||
case "ACTIVE":
|
||||
return "启用";
|
||||
case "PENDING":
|
||||
return "待审核";
|
||||
case "REJECTED":
|
||||
return "已拒绝";
|
||||
case "DISABLED":
|
||||
return "已停用";
|
||||
default:
|
||||
return status || "未知";
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user