feat: 接入真实登录鉴权流程
This commit is contained in:
@@ -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
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user