feat: 接入真实登录鉴权流程

This commit is contained in:
湛兮
2026-05-26 14:45:15 +08:00
parent a6c9f5dee3
commit 5003628017
21 changed files with 572 additions and 305 deletions
+39 -5
View File
@@ -21,6 +21,7 @@ import {
type RoleOption,
type StoreOption
} from "@/api/access";
import { hasPerms } from "@/utils/auth";
import Plus from "~icons/ep/plus";
import Search from "~icons/ep/search";
@@ -41,6 +42,13 @@ type EmployeeFormState = EmployeePayload & {
/** 员工手机号按中国大陆手机号做前端第一层校验,最终唯一性仍由后端保证。 */
const phonePattern = /^1[3-9]\d{9}$/;
const bindableRoleCodes = new Set([
"store_manager",
"cashier",
"kitchen",
"part_time",
"admin"
]);
const tableLoading = ref(false);
const catalogLoading = ref(false);
const submitLoading = ref(false);
@@ -99,6 +107,8 @@ const inactiveCount = computed(
() => employees.value.filter(item => item.status === "INACTIVE").length
);
const dialogTitle = computed(() => (form.id ? "编辑员工" : "新增员工"));
const canManageEmployees = computed(() => hasPerms("employee:manage"));
const canViewAllEmployees = computed(() => hasPerms("employee:view:all"));
function getErrorMessage(error: unknown, fallback: string) {
const message = (
@@ -147,14 +157,22 @@ function buildPayload(): EmployeePayload {
/** 门店和角色是员工表单的基础字典,打开弹窗前必须先加载。 */
async function fetchCatalog() {
const shouldLoadStores =
canViewAllEmployees.value || canManageEmployees.value;
const shouldLoadRoles = canManageEmployees.value;
if (!shouldLoadStores && !shouldLoadRoles) return;
catalogLoading.value = true;
try {
const [storeResult, roleResult] = await Promise.all([
listStores(),
listRoles()
shouldLoadStores ? listStores() : Promise.resolve({ data: [] }),
shouldLoadRoles ? listRoles() : Promise.resolve({ data: [] })
]);
stores.value = storeResult.data;
roles.value = roleResult.data;
roles.value = roleResult.data.filter(role =>
bindableRoleCodes.has(role.code)
);
} catch (error) {
ElMessage.error(getErrorMessage(error, "加载门店和角色选项失败"));
} finally {
@@ -209,12 +227,14 @@ function handleSizeChange(pageSize: number) {
}
function openCreateDialog() {
if (!canManageEmployees.value) return;
resetFormState();
dialogVisible.value = true;
}
/** 编辑前重新拉详情,确保角色绑定不是来自列表摘要的过期数据。 */
async function openEditDialog(row: Employee) {
if (!canManageEmployees.value) return;
try {
const result = await getEmployee(row.id);
const employee = result.data;
@@ -235,6 +255,7 @@ async function openEditDialog(row: Employee) {
}
async function submitForm() {
if (!canManageEmployees.value) return;
await formRef.value?.validate();
submitLoading.value = true;
@@ -259,6 +280,7 @@ async function submitForm() {
}
async function toggleStatus(row: Employee) {
if (!canManageEmployees.value) return;
const nextStatus: EmployeeStatus =
row.status === "ACTIVE" ? "INACTIVE" : "ACTIVE";
const action = nextStatus === "ACTIVE" ? "启用" : "停用";
@@ -284,6 +306,7 @@ async function toggleStatus(row: Employee) {
}
async function removeEmployee(row: Employee) {
if (!canManageEmployees.value) return;
try {
await ElMessageBox.confirm(
`删除后员工「${row.name}」会被软删除并停用,确认继续?`,
@@ -321,7 +344,12 @@ onMounted(async () => {
<p class="eyebrow">门店员工权限管理</p>
<h1>员工管理</h1>
</div>
<el-button type="primary" :icon="Plus" @click="openCreateDialog">
<el-button
v-if="canManageEmployees"
type="primary"
:icon="Plus"
@click="openCreateDialog"
>
新增员工
</el-button>
</div>
@@ -347,6 +375,7 @@ onMounted(async () => {
<div class="toolbar">
<el-select
v-if="canViewAllEmployees"
v-model="query.storeId"
clearable
filterable
@@ -440,7 +469,12 @@ onMounted(async () => {
{{ formatTime(row.updatedAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="260" fixed="right">
<el-table-column
v-if="canManageEmployees"
label="操作"
width="260"
fixed="right"
>
<template #default="{ row }">
<el-button
link
+39 -28
View File
@@ -10,7 +10,7 @@ import { useEventListener } from "@vueuse/core";
import type { FormInstance } from "element-plus";
import { useLayout } from "@/layout/hooks/useLayout";
import { useUserStoreHook } from "@/store/modules/user";
import { initRouter, getTopMenu } from "@/router/utils";
import { getTopMenu } from "@/router/utils";
import { bg, avatar, illustration } from "./utils/static";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
@@ -38,38 +38,49 @@ const { title } = useNav();
const ruleForm = reactive({
username: "admin",
password: "admin123"
password: "Admin@123456"
});
function getErrorMessage(error: unknown, fallback: string) {
const message = (
error as { response?: { data?: { error?: { message?: string } } } }
)?.response?.data?.error?.message;
if (message) return message;
if (error instanceof Error && error.message) return error.message;
return fallback;
}
const onLogin = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate(valid => {
if (valid) {
loading.value = true;
useUserStoreHook()
.loginByUsername({
username: ruleForm.username,
password: ruleForm.password
})
.then(res => {
if (res.success) {
// 获取后端路由
return initRouter().then(() => {
disabled.value = true;
router
.push(getTopMenu(true).path)
.then(() => {
message("登录成功", { type: "success" });
})
.finally(() => (disabled.value = false));
});
} else {
message("登录失败", { type: "error" });
}
})
.finally(() => (loading.value = false));
try {
await formEl.validate();
} catch {
return;
}
loading.value = true;
try {
const res = await useUserStoreHook().loginByUsername({
username: ruleForm.username,
password: ruleForm.password
});
if (res.success) {
disabled.value = true;
const redirect = router.currentRoute.value.query.redirect as
| string
| undefined;
await router.push(redirect || getTopMenu(true)?.path || "/access-denied");
message("登录成功", { type: "success" });
}
});
} catch (error) {
message(getErrorMessage(error, "登录失败"), { type: "error" });
} finally {
disabled.value = false;
loading.value = false;
}
};
const immediateDebounce: any = debounce(
+3 -6
View File
@@ -1,9 +1,8 @@
import { reactive } from "vue";
import type { FormRules } from "element-plus";
/** 密码正则(密码格式应为8-18位数字、字母、符号的任意两种组合) */
export const REGEXP_PWD =
/^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[()])+$)(?!^.*[\u4E00-\u9FA5].*$)([^(0-9a-zA-Z)]|[()]|[a-z]|[A-Z]|[0-9]){8,18}$/;
/** 后端登录密码约束:8-128 个字符。 */
export const REGEXP_PWD = /^.{8,128}$/;
/** 登录校验 */
const loginRules = reactive<FormRules>({
@@ -13,9 +12,7 @@ const loginRules = reactive<FormRules>({
if (value === "") {
callback(new Error("请输入密码"));
} else if (!REGEXP_PWD.test(value)) {
callback(
new Error("密码格式应为8-18位数字、字母、符号的任意两种组合")
);
callback(new Error("密码长度应为 8-128 个字符"));
} else {
callback();
}
+26 -5
View File
@@ -14,6 +14,7 @@ import {
type Role,
type RolePayload
} from "@/api/access";
import { hasPerms } from "@/utils/auth";
import Plus from "~icons/ep/plus";
import Search from "~icons/ep/search";
@@ -74,6 +75,7 @@ const systemRoleCount = computed(
() => roles.value.filter(item => item.code === "admin").length
);
const dialogTitle = computed(() => (form.id ? "编辑角色" : "新增角色"));
const canManageRoles = computed(() => hasPerms("role:manage"));
function applyRoleQuery(items: Role[]) {
const keyword = query.keyword.trim().toLowerCase();
@@ -136,9 +138,7 @@ function buildPayload(): RolePayload {
async function fetchRoles() {
tableLoading.value = true;
try {
const result = await listRoles({
keyword: query.keyword.trim() || undefined
});
const result = await listRoles();
roles.value = applyRoleQuery(result.data);
} catch (error) {
ElMessage.error(getErrorMessage(error, "加载角色列表失败"));
@@ -158,11 +158,13 @@ function handleSearch() {
}
function openCreateDialog() {
if (!canManageRoles.value) return;
resetFormState();
dialogVisible.value = true;
}
function openEditDialog(row: Role) {
if (!canManageRoles.value || row.isSystem) return;
Object.assign(form, {
id: row.id,
code: row.code,
@@ -174,6 +176,7 @@ function openEditDialog(row: Role) {
}
async function submitForm() {
if (!canManageRoles.value) return;
await formRef.value?.validate();
submitLoading.value = true;
@@ -198,6 +201,7 @@ async function submitForm() {
}
async function removeRole(row: Role) {
if (!canManageRoles.value || row.isSystem) return;
try {
await ElMessageBox.confirm(
`删除后角色「${row.name}」无法再绑定员工,确认继续?`,
@@ -228,7 +232,12 @@ onMounted(fetchRoles);
<p class="eyebrow">权限角色基础数据</p>
<h1>角色管理</h1>
</div>
<el-button type="primary" :icon="Plus" @click="openCreateDialog">
<el-button
v-if="canManageRoles"
type="primary"
:icon="Plus"
@click="openCreateDialog"
>
新增角色
</el-button>
</div>
@@ -295,9 +304,15 @@ onMounted(fetchRoles);
{{ formatTime(row.updatedAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="170" fixed="right">
<el-table-column
v-if="canManageRoles"
label="操作"
width="170"
fixed="right"
>
<template #default="{ row }">
<el-button
v-if="!row.isSystem"
link
type="primary"
:icon="EditPen"
@@ -306,6 +321,7 @@ onMounted(fetchRoles);
编辑
</el-button>
<el-button
v-if="!row.isSystem"
link
type="danger"
:icon="Delete"
@@ -313,6 +329,7 @@ onMounted(fetchRoles);
>
删除
</el-button>
<span v-if="row.isSystem" class="muted">系统内置</span>
</template>
</el-table-column>
</el-table>
@@ -473,6 +490,10 @@ onMounted(fetchRoles);
}
}
.muted {
color: #94a3b8;
}
.management-form {
padding-top: 4px;
}
+20 -5
View File
@@ -15,6 +15,7 @@ import {
type StorePayload,
type StoreStatus
} from "@/api/access";
import { hasPerms } from "@/utils/auth";
import Plus from "~icons/ep/plus";
import Search from "~icons/ep/search";
@@ -70,6 +71,7 @@ const inactiveCount = computed(
() => stores.value.filter(item => item.status === "INACTIVE").length
);
const dialogTitle = computed(() => (form.id ? "编辑门店" : "新增门店"));
const canManageStores = computed(() => hasPerms("store:manage"));
function applyStoreQuery(items: Store[]) {
const keyword = query.keyword.trim().toLowerCase();
@@ -136,9 +138,7 @@ async function fetchStores() {
tableLoading.value = true;
try {
const result = await listStores({
includeInactive: true,
status: query.status,
keyword: query.keyword.trim() || undefined
includeInactive: true
});
stores.value = applyStoreQuery(result.data);
} catch (error) {
@@ -160,11 +160,13 @@ function handleSearch() {
}
function openCreateDialog() {
if (!canManageStores.value) return;
resetFormState();
dialogVisible.value = true;
}
function openEditDialog(row: Store) {
if (!canManageStores.value) return;
Object.assign(form, {
id: row.id,
name: row.name,
@@ -177,6 +179,7 @@ function openEditDialog(row: Store) {
}
async function submitForm() {
if (!canManageStores.value) return;
await formRef.value?.validate();
submitLoading.value = true;
@@ -201,6 +204,7 @@ async function submitForm() {
}
async function toggleStatus(row: Store) {
if (!canManageStores.value) return;
const nextStatus: StoreStatus =
row.status === "ACTIVE" ? "INACTIVE" : "ACTIVE";
const action = nextStatus === "ACTIVE" ? "启用" : "停用";
@@ -226,6 +230,7 @@ async function toggleStatus(row: Store) {
}
async function removeStore(row: Store) {
if (!canManageStores.value) return;
try {
await ElMessageBox.confirm(
`删除后门店「${row.name}」不会再出现在员工新增下拉里,确认继续?`,
@@ -256,7 +261,12 @@ onMounted(fetchStores);
<p class="eyebrow">门店基础数据</p>
<h1>门店管理</h1>
</div>
<el-button type="primary" :icon="Plus" @click="openCreateDialog">
<el-button
v-if="canManageStores"
type="primary"
:icon="Plus"
@click="openCreateDialog"
>
新增门店
</el-button>
</div>
@@ -345,7 +355,12 @@ onMounted(fetchStores);
{{ formatTime(row.updatedAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="260" fixed="right">
<el-table-column
v-if="canManageStores"
label="操作"
width="260"
fixed="right"
>
<template #default="{ row }">
<el-button
link