feat: add employee workspace operations admin
This commit is contained in:
+101
-81
@@ -3,6 +3,7 @@ import { computed, onMounted, reactive, ref } from "vue";
|
||||
import {
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElNotification,
|
||||
type FormInstance,
|
||||
type FormRules
|
||||
} from "element-plus";
|
||||
@@ -15,7 +16,6 @@ import {
|
||||
listRoles,
|
||||
resetEmployeePassword,
|
||||
updateEmployee,
|
||||
updateEmployeePassword,
|
||||
updateEmployeeStatus,
|
||||
type Employee,
|
||||
type EmployeePayload,
|
||||
@@ -34,7 +34,7 @@ import Delete from "~icons/ep/delete";
|
||||
import CircleCheck from "~icons/ep/circle-check";
|
||||
import CircleClose from "~icons/ep/circle-close";
|
||||
import Key from "~icons/ep/key";
|
||||
import RefreshLeft from "~icons/ep/refresh-left";
|
||||
import CopyDocument from "~icons/ep/copy-document";
|
||||
|
||||
defineOptions({
|
||||
name: "EmployeeManagement"
|
||||
@@ -48,8 +48,7 @@ type EmployeeFormState = EmployeePayload & {
|
||||
type PasswordFormState = {
|
||||
employeeId?: number;
|
||||
employeeName: string;
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
/** 员工手机号按中国大陆手机号做前端第一层校验,最终唯一性仍由后端保证。 */
|
||||
@@ -68,12 +67,14 @@ const submitLoading = ref(false);
|
||||
const passwordSubmitLoading = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const passwordDialogVisible = ref(false);
|
||||
const temporaryPasswordVisible = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
const passwordFormRef = ref<FormInstance>();
|
||||
const employees = ref<Employee[]>([]);
|
||||
const stores = ref<Store[]>([]);
|
||||
const roles = ref<RoleOption[]>([]);
|
||||
const originalStoreId = ref<number>();
|
||||
const temporaryPassword = ref("");
|
||||
|
||||
const query = reactive({
|
||||
storeId: undefined as number | undefined,
|
||||
@@ -101,8 +102,7 @@ const form = reactive<EmployeeFormState>({
|
||||
const passwordForm = reactive<PasswordFormState>({
|
||||
employeeId: undefined,
|
||||
employeeName: "",
|
||||
oldPassword: "",
|
||||
newPassword: ""
|
||||
reason: ""
|
||||
});
|
||||
|
||||
const rules: FormRules<EmployeeFormState> = {
|
||||
@@ -125,21 +125,12 @@ const rules: FormRules<EmployeeFormState> = {
|
||||
};
|
||||
|
||||
const passwordRules: FormRules<PasswordFormState> = {
|
||||
oldPassword: [
|
||||
{ required: true, message: "请输入旧密码", trigger: "blur" },
|
||||
reason: [
|
||||
{ required: true, message: "请填写重置原因", trigger: "blur" },
|
||||
{
|
||||
min: 8,
|
||||
max: 128,
|
||||
message: "密码长度需要在 8-128 个字符之间",
|
||||
trigger: "blur"
|
||||
}
|
||||
],
|
||||
newPassword: [
|
||||
{ required: true, message: "请输入新密码", trigger: "blur" },
|
||||
{
|
||||
min: 8,
|
||||
max: 128,
|
||||
message: "密码长度需要在 8-128 个字符之间",
|
||||
min: 4,
|
||||
max: 255,
|
||||
message: "原因需要在 4-255 个字符之间",
|
||||
trigger: "blur"
|
||||
}
|
||||
]
|
||||
@@ -155,8 +146,12 @@ const dialogTitle = computed(() => (form.id ? "编辑员工" : "新增员工"));
|
||||
const canCreateEmployee = computed(() => hasMenuAction("employees", "create"));
|
||||
const canUpdateEmployee = computed(() => hasMenuAction("employees", "update"));
|
||||
const canDeleteEmployee = computed(() => hasMenuAction("employees", "delete"));
|
||||
const canResetCredential = computed(() => hasPerms("credential:reset"));
|
||||
const canOperateEmployee = computed(
|
||||
() => canUpdateEmployee.value || canDeleteEmployee.value
|
||||
() =>
|
||||
canUpdateEmployee.value ||
|
||||
canDeleteEmployee.value ||
|
||||
canResetCredential.value
|
||||
);
|
||||
const canViewAllEmployees = computed(() => hasPerms("employee:view:all"));
|
||||
|
||||
@@ -223,11 +218,11 @@ function resetFormState() {
|
||||
}
|
||||
|
||||
function resetPasswordFormState() {
|
||||
temporaryPassword.value = "";
|
||||
Object.assign(passwordForm, {
|
||||
employeeId: undefined,
|
||||
employeeName: "",
|
||||
oldPassword: "",
|
||||
newPassword: ""
|
||||
reason: ""
|
||||
});
|
||||
passwordFormRef.value?.clearValidate();
|
||||
}
|
||||
@@ -397,56 +392,51 @@ async function submitForm() {
|
||||
}
|
||||
|
||||
function openPasswordDialog(row: Employee) {
|
||||
if (!canUpdateEmployee.value) return;
|
||||
if (!canResetCredential.value) return;
|
||||
resetPasswordFormState();
|
||||
Object.assign(passwordForm, {
|
||||
employeeId: row.id,
|
||||
employeeName: row.name,
|
||||
oldPassword: "",
|
||||
newPassword: ""
|
||||
reason: ""
|
||||
});
|
||||
passwordDialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function submitPasswordForm() {
|
||||
if (!canUpdateEmployee.value || !passwordForm.employeeId) return;
|
||||
if (!canResetCredential.value || !passwordForm.employeeId) return;
|
||||
await passwordFormRef.value?.validate();
|
||||
passwordSubmitLoading.value = true;
|
||||
|
||||
try {
|
||||
await updateEmployeePassword(passwordForm.employeeId, {
|
||||
oldPassword: passwordForm.oldPassword,
|
||||
newPassword: passwordForm.newPassword
|
||||
const result = await resetEmployeePassword(passwordForm.employeeId, {
|
||||
reason: passwordForm.reason.trim()
|
||||
});
|
||||
ElMessage.success("员工密码已修改");
|
||||
passwordDialogVisible.value = false;
|
||||
temporaryPassword.value = result.data.temporaryPassword;
|
||||
temporaryPasswordVisible.value = true;
|
||||
fetchEmployees();
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, "修改密码失败"));
|
||||
ElMessage.error(getErrorMessage(error, "重置密码失败"));
|
||||
} finally {
|
||||
passwordSubmitLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function resetPassword(row: Employee) {
|
||||
if (!canUpdateEmployee.value) return;
|
||||
async function copyTemporaryPassword() {
|
||||
if (!temporaryPassword.value) return;
|
||||
await navigator.clipboard.writeText(temporaryPassword.value);
|
||||
ElMessage.success("临时密码已复制");
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认将员工「${row.name}」的密码重置为初始密码 pw111111?`,
|
||||
"重置密码",
|
||||
{
|
||||
type: "warning",
|
||||
confirmButtonText: "重置",
|
||||
cancelButtonText: "取消"
|
||||
}
|
||||
);
|
||||
await resetEmployeePassword(row.id);
|
||||
ElMessage.success("员工密码已重置为初始密码");
|
||||
} catch (error) {
|
||||
if (error !== "cancel") {
|
||||
ElMessage.error(getErrorMessage(error, "重置密码失败"));
|
||||
}
|
||||
}
|
||||
function clearTemporaryPassword() {
|
||||
temporaryPassword.value = "";
|
||||
}
|
||||
|
||||
function showPasswordResetBoundary() {
|
||||
ElNotification.warning({
|
||||
title: "安全边界",
|
||||
message: "正式版不支持查看当前明文密码,只能重置并一次性查看临时密码。"
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleStatus(row: Employee) {
|
||||
@@ -684,20 +674,11 @@ onMounted(async () => {
|
||||
{{ row.status === "ACTIVE" ? "停用" : "启用" }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canUpdateEmployee"
|
||||
link
|
||||
type="primary"
|
||||
:icon="Key"
|
||||
@click="openPasswordDialog(row)"
|
||||
>
|
||||
改密
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canUpdateEmployee"
|
||||
v-if="canResetCredential"
|
||||
link
|
||||
type="warning"
|
||||
:icon="RefreshLeft"
|
||||
@click="resetPassword(row)"
|
||||
:icon="Key"
|
||||
@click="openPasswordDialog(row)"
|
||||
>
|
||||
重置密码
|
||||
</el-button>
|
||||
@@ -823,8 +804,8 @@ onMounted(async () => {
|
||||
|
||||
<el-dialog
|
||||
v-model="passwordDialogVisible"
|
||||
title="修改员工密码"
|
||||
width="420px"
|
||||
title="重置员工密码"
|
||||
width="460px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form
|
||||
@@ -836,22 +817,21 @@ onMounted(async () => {
|
||||
<el-form-item label="员工">
|
||||
<el-input v-model="passwordForm.employeeName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="旧密码" prop="oldPassword">
|
||||
<el-alert
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="password-alert"
|
||||
title="不会展示员工当前密码。提交后仅在下一步弹窗显示一次临时密码。"
|
||||
/>
|
||||
<el-form-item label="重置原因" prop="reason">
|
||||
<el-input
|
||||
v-model="passwordForm.oldPassword"
|
||||
type="password"
|
||||
maxlength="128"
|
||||
show-password
|
||||
placeholder="请输入旧密码"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码" prop="newPassword">
|
||||
<el-input
|
||||
v-model="passwordForm.newPassword"
|
||||
type="password"
|
||||
maxlength="128"
|
||||
show-password
|
||||
placeholder="请输入 8-128 位新密码"
|
||||
v-model="passwordForm.reason"
|
||||
type="textarea"
|
||||
maxlength="255"
|
||||
show-word-limit
|
||||
:rows="4"
|
||||
placeholder="请填写重置原因,用于凭据审计"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -863,7 +843,39 @@ onMounted(async () => {
|
||||
:loading="passwordSubmitLoading"
|
||||
@click="submitPasswordForm"
|
||||
>
|
||||
保存
|
||||
重置并生成临时密码
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="temporaryPasswordVisible"
|
||||
title="一次性临时密码"
|
||||
width="460px"
|
||||
destroy-on-close
|
||||
@closed="clearTemporaryPassword"
|
||||
>
|
||||
<el-alert
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="password-alert"
|
||||
title="关闭弹窗后前端会清空临时密码,请立即复制并通过安全渠道交付员工。"
|
||||
/>
|
||||
<el-input
|
||||
:model-value="temporaryPassword"
|
||||
readonly
|
||||
class="temporary-password"
|
||||
@focus="showPasswordResetBoundary"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="temporaryPasswordVisible = false">关闭</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:icon="CopyDocument"
|
||||
@click="copyTemporaryPassword"
|
||||
>
|
||||
复制临时密码
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -1024,6 +1036,14 @@ onMounted(async () => {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.password-alert {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.temporary-password {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
}
|
||||
|
||||
@media (width <= 760px) {
|
||||
.employee-page {
|
||||
padding: 12px;
|
||||
|
||||
Reference in New Issue
Block a user