feat: add employee workspace operations admin

This commit is contained in:
湛兮
2026-06-02 14:23:31 +08:00
parent 835b52709f
commit ab73565d37
10 changed files with 3523 additions and 147 deletions
+101 -81
View File
@@ -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;