feat: 移除mock并接入真实权限控制
This commit is contained in:
@@ -12,7 +12,7 @@ import {
|
||||
getEmployee,
|
||||
listEmployees,
|
||||
listRoles,
|
||||
listStores,
|
||||
listStoreOptions,
|
||||
updateEmployee,
|
||||
updateEmployeeStatus,
|
||||
type Employee,
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
type RoleOption,
|
||||
type StoreOption
|
||||
} from "@/api/access";
|
||||
import { hasPerms } from "@/utils/auth";
|
||||
import { hasMenuAction, hasPerms } from "@/utils/auth";
|
||||
|
||||
import Plus from "~icons/ep/plus";
|
||||
import Search from "~icons/ep/search";
|
||||
@@ -107,7 +107,12 @@ const inactiveCount = computed(
|
||||
() => employees.value.filter(item => item.status === "INACTIVE").length
|
||||
);
|
||||
const dialogTitle = computed(() => (form.id ? "编辑员工" : "新增员工"));
|
||||
const canManageEmployees = computed(() => hasPerms("employee:manage"));
|
||||
const canCreateEmployee = computed(() => hasMenuAction("employees", "create"));
|
||||
const canUpdateEmployee = computed(() => hasMenuAction("employees", "update"));
|
||||
const canDeleteEmployee = computed(() => hasMenuAction("employees", "delete"));
|
||||
const canOperateEmployee = computed(
|
||||
() => canUpdateEmployee.value || canDeleteEmployee.value
|
||||
);
|
||||
const canViewAllEmployees = computed(() => hasPerms("employee:view:all"));
|
||||
|
||||
function getErrorMessage(error: unknown, fallback: string) {
|
||||
@@ -158,15 +163,17 @@ function buildPayload(): EmployeePayload {
|
||||
/** 门店和角色是员工表单的基础字典,打开弹窗前必须先加载。 */
|
||||
async function fetchCatalog() {
|
||||
const shouldLoadStores =
|
||||
canViewAllEmployees.value || canManageEmployees.value;
|
||||
const shouldLoadRoles = canManageEmployees.value;
|
||||
canViewAllEmployees.value ||
|
||||
canCreateEmployee.value ||
|
||||
canUpdateEmployee.value;
|
||||
const shouldLoadRoles = canCreateEmployee.value || canUpdateEmployee.value;
|
||||
|
||||
if (!shouldLoadStores && !shouldLoadRoles) return;
|
||||
|
||||
catalogLoading.value = true;
|
||||
try {
|
||||
const [storeResult, roleResult] = await Promise.all([
|
||||
shouldLoadStores ? listStores() : Promise.resolve({ data: [] }),
|
||||
shouldLoadStores ? listStoreOptions() : Promise.resolve({ data: [] }),
|
||||
shouldLoadRoles ? listRoles() : Promise.resolve({ data: [] })
|
||||
]);
|
||||
stores.value = storeResult.data;
|
||||
@@ -227,14 +234,14 @@ function handleSizeChange(pageSize: number) {
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
if (!canManageEmployees.value) return;
|
||||
if (!canCreateEmployee.value) return;
|
||||
resetFormState();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
/** 编辑前重新拉详情,确保角色绑定不是来自列表摘要的过期数据。 */
|
||||
async function openEditDialog(row: Employee) {
|
||||
if (!canManageEmployees.value) return;
|
||||
if (!canUpdateEmployee.value) return;
|
||||
try {
|
||||
const result = await getEmployee(row.id);
|
||||
const employee = result.data;
|
||||
@@ -255,7 +262,7 @@ async function openEditDialog(row: Employee) {
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
if (!canManageEmployees.value) return;
|
||||
if (form.id ? !canUpdateEmployee.value : !canCreateEmployee.value) return;
|
||||
await formRef.value?.validate();
|
||||
submitLoading.value = true;
|
||||
|
||||
@@ -280,7 +287,7 @@ async function submitForm() {
|
||||
}
|
||||
|
||||
async function toggleStatus(row: Employee) {
|
||||
if (!canManageEmployees.value) return;
|
||||
if (!canUpdateEmployee.value) return;
|
||||
const nextStatus: EmployeeStatus =
|
||||
row.status === "ACTIVE" ? "INACTIVE" : "ACTIVE";
|
||||
const action = nextStatus === "ACTIVE" ? "启用" : "停用";
|
||||
@@ -306,7 +313,7 @@ async function toggleStatus(row: Employee) {
|
||||
}
|
||||
|
||||
async function removeEmployee(row: Employee) {
|
||||
if (!canManageEmployees.value) return;
|
||||
if (!canDeleteEmployee.value) return;
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`删除后员工「${row.name}」会被软删除并停用,确认继续?`,
|
||||
@@ -345,7 +352,7 @@ onMounted(async () => {
|
||||
<h1>员工管理</h1>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="canManageEmployees"
|
||||
v-if="canCreateEmployee"
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
@click="openCreateDialog"
|
||||
@@ -470,13 +477,14 @@ onMounted(async () => {
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="canManageEmployees"
|
||||
v-if="canOperateEmployee"
|
||||
label="操作"
|
||||
width="260"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="canUpdateEmployee"
|
||||
link
|
||||
type="primary"
|
||||
:icon="EditPen"
|
||||
@@ -485,6 +493,7 @@ onMounted(async () => {
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canUpdateEmployee"
|
||||
link
|
||||
:type="row.status === 'ACTIVE' ? 'warning' : 'success'"
|
||||
:icon="row.status === 'ACTIVE' ? CircleClose : CircleCheck"
|
||||
@@ -493,6 +502,7 @@ onMounted(async () => {
|
||||
{{ row.status === "ACTIVE" ? "停用" : "启用" }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canDeleteEmployee"
|
||||
link
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
@@ -618,9 +628,9 @@ onMounted(async () => {
|
||||
|
||||
.page-heading {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h1 {
|
||||
@@ -733,9 +743,9 @@ onMounted(async () => {
|
||||
|
||||
.pagination-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 14px 16px 0;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
@@ -761,8 +771,8 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.summary-strip {
|
||||
@@ -781,8 +791,8 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.pagination-row {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { hasAuth, getAuths } from "@/router/utils";
|
||||
|
||||
defineOptions({
|
||||
name: "PermissionButtonRouter"
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p class="mb-2!">当前拥有的code列表:{{ getAuths() }}</p>
|
||||
|
||||
<el-card shadow="never" class="mb-2">
|
||||
<template #header>
|
||||
<div class="card-header">组件方式判断权限</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<Auth value="permission:btn:add">
|
||||
<el-button plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
</Auth>
|
||||
<Auth :value="['permission:btn:edit']">
|
||||
<el-button plain type="primary">
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
</Auth>
|
||||
<Auth
|
||||
:value="[
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
]"
|
||||
>
|
||||
<el-button plain type="danger">
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</Auth>
|
||||
</el-space>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="mb-2">
|
||||
<template #header>
|
||||
<div class="card-header">函数方式判断权限</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<el-button v-if="hasAuth('permission:btn:add')" plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
<el-button v-if="hasAuth(['permission:btn:edit'])" plain type="primary">
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="
|
||||
hasAuth([
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
])
|
||||
"
|
||||
plain
|
||||
type="danger"
|
||||
>
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</el-space>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
指令方式判断权限(该方式不能动态修改权限)
|
||||
</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<el-button v-auth="'permission:btn:add'" plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
<el-button v-auth="['permission:btn:edit']" plain type="primary">
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
<el-button
|
||||
v-auth="[
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
]"
|
||||
plain
|
||||
type="danger"
|
||||
>
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</el-space>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,109 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { hasPerms } from "@/utils/auth";
|
||||
import { useUserStoreHook } from "@/store/modules/user";
|
||||
|
||||
const { permissions } = useUserStoreHook();
|
||||
|
||||
defineOptions({
|
||||
name: "PermissionButtonLogin"
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p class="mb-2!">当前拥有的code列表:{{ permissions }}</p>
|
||||
<p v-show="permissions?.[0] === '*:*:*'" class="mb-2!">
|
||||
*:*:* 代表拥有全部按钮级别权限
|
||||
</p>
|
||||
|
||||
<el-card shadow="never" class="mb-2">
|
||||
<template #header>
|
||||
<div class="card-header">组件方式判断权限</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<Perms value="permission:btn:add">
|
||||
<el-button plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
</Perms>
|
||||
<Perms :value="['permission:btn:edit']">
|
||||
<el-button plain type="primary">
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
</Perms>
|
||||
<Perms
|
||||
:value="[
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
]"
|
||||
>
|
||||
<el-button plain type="danger">
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</Perms>
|
||||
</el-space>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="mb-2">
|
||||
<template #header>
|
||||
<div class="card-header">函数方式判断权限</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<el-button v-if="hasPerms('permission:btn:add')" plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasPerms(['permission:btn:edit'])"
|
||||
plain
|
||||
type="primary"
|
||||
>
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="
|
||||
hasPerms([
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
])
|
||||
"
|
||||
plain
|
||||
type="danger"
|
||||
>
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</el-space>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
指令方式判断权限(该方式不能动态修改权限)
|
||||
</div>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<el-button v-perms="'permission:btn:add'" plain type="warning">
|
||||
拥有code:'permission:btn:add' 权限可见
|
||||
</el-button>
|
||||
<el-button v-perms="['permission:btn:edit']" plain type="primary">
|
||||
拥有code:['permission:btn:edit'] 权限可见
|
||||
</el-button>
|
||||
<el-button
|
||||
v-perms="[
|
||||
'permission:btn:add',
|
||||
'permission:btn:edit',
|
||||
'permission:btn:delete'
|
||||
]"
|
||||
plain
|
||||
type="danger"
|
||||
>
|
||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
||||
'permission:btn:delete'] 权限可见
|
||||
</el-button>
|
||||
</el-space>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,66 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { initRouter } from "@/router/utils";
|
||||
import { storageLocal } from "@pureadmin/utils";
|
||||
import { type CSSProperties, ref, computed } from "vue";
|
||||
import { useUserStoreHook } from "@/store/modules/user";
|
||||
import { usePermissionStoreHook } from "@/store/modules/permission";
|
||||
|
||||
defineOptions({
|
||||
name: "PermissionPage"
|
||||
});
|
||||
|
||||
const elStyle = computed((): CSSProperties => {
|
||||
return {
|
||||
width: "85vw",
|
||||
justifyContent: "start"
|
||||
};
|
||||
});
|
||||
|
||||
const username = ref(useUserStoreHook()?.username);
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: "admin",
|
||||
label: "管理员角色"
|
||||
},
|
||||
{
|
||||
value: "common",
|
||||
label: "普通角色"
|
||||
}
|
||||
];
|
||||
|
||||
function onChange() {
|
||||
useUserStoreHook()
|
||||
.loginByUsername({ username: username.value, password: "admin123" })
|
||||
.then(res => {
|
||||
if (res.success) {
|
||||
storageLocal().removeItem("async-routes");
|
||||
usePermissionStoreHook().clearAllCachePage();
|
||||
initRouter();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p class="mb-2!">
|
||||
模拟后台根据不同角色返回对应路由,观察左侧菜单变化(管理员角色可查看系统管理菜单、普通角色不可查看系统管理菜单)
|
||||
</p>
|
||||
<el-card shadow="never" :style="elStyle">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>当前角色:{{ username }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-select v-model="username" class="w-[160px]!" @change="onChange">
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,533 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import {
|
||||
getPermissionDefinitions,
|
||||
getPermissionPolicies,
|
||||
updateRolePermissions,
|
||||
type PermissionDefinitionGroup,
|
||||
type PermissionPolicy
|
||||
} from "@/api/access";
|
||||
import { useUserStoreHook } from "@/store/modules/user";
|
||||
import { hasMenuAction } from "@/utils/auth";
|
||||
|
||||
import Check from "~icons/ep/check";
|
||||
import Edit from "~icons/ep/edit";
|
||||
import Refresh from "~icons/ep/refresh";
|
||||
|
||||
defineOptions({
|
||||
name: "PermissionPolicies"
|
||||
});
|
||||
|
||||
const tableLoading = ref(false);
|
||||
const saving = ref(false);
|
||||
const drawerVisible = ref(false);
|
||||
const policies = ref<PermissionPolicy[]>([]);
|
||||
const definitionGroups = ref<PermissionDefinitionGroup[]>([]);
|
||||
const editingPolicy = ref<PermissionPolicy | null>(null);
|
||||
const checkedPermissions = ref<string[]>([]);
|
||||
|
||||
const canUpdatePermissions = computed(() =>
|
||||
hasMenuAction("permissions", "update")
|
||||
);
|
||||
const editableRoleCount = computed(
|
||||
() => policies.value.filter(item => item.editable).length
|
||||
);
|
||||
const menuTotal = computed(() =>
|
||||
policies.value.reduce((total, item) => total + item.menus.length, 0)
|
||||
);
|
||||
const permissionTotal = computed(() =>
|
||||
policies.value.reduce(
|
||||
(total, item) =>
|
||||
total + item.permissions.filter(permission => permission !== "*").length,
|
||||
0
|
||||
)
|
||||
);
|
||||
const drawerTitle = computed(() =>
|
||||
editingPolicy.value ? `分配权限:${editingPolicy.value.roleName}` : "分配权限"
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function formatActions(actions: string[]) {
|
||||
const actionMap: Record<string, string> = {
|
||||
view: "查看",
|
||||
create: "新增",
|
||||
update: "编辑",
|
||||
delete: "删除"
|
||||
};
|
||||
|
||||
return actions.map(action => actionMap[action] ?? action).join(" / ");
|
||||
}
|
||||
|
||||
function formatPermissionTitle(code: string) {
|
||||
for (const group of definitionGroups.value) {
|
||||
const found = group.permissions.find(permission => permission.code === code);
|
||||
if (found) return found.title;
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
function isGroupChecked(group: PermissionDefinitionGroup) {
|
||||
const codes = group.permissions.map(permission => permission.code);
|
||||
return codes.every(code => checkedPermissions.value.includes(code));
|
||||
}
|
||||
|
||||
function isGroupIndeterminate(group: PermissionDefinitionGroup) {
|
||||
const codes = group.permissions.map(permission => permission.code);
|
||||
const checkedCount = codes.filter(code =>
|
||||
checkedPermissions.value.includes(code)
|
||||
).length;
|
||||
|
||||
return checkedCount > 0 && checkedCount < codes.length;
|
||||
}
|
||||
|
||||
function toggleGroup(group: PermissionDefinitionGroup, checked: boolean) {
|
||||
const nextPermissions = new Set(checkedPermissions.value);
|
||||
|
||||
for (const permission of group.permissions) {
|
||||
if (checked) {
|
||||
nextPermissions.add(permission.code);
|
||||
} else {
|
||||
nextPermissions.delete(permission.code);
|
||||
}
|
||||
}
|
||||
|
||||
checkedPermissions.value = [...nextPermissions];
|
||||
}
|
||||
|
||||
function openEditor(policy: PermissionPolicy) {
|
||||
if (!policy.editable || !canUpdatePermissions.value) return;
|
||||
|
||||
editingPolicy.value = policy;
|
||||
checkedPermissions.value = policy.permissions.filter(
|
||||
permission => permission !== "*"
|
||||
);
|
||||
drawerVisible.value = true;
|
||||
}
|
||||
|
||||
async function fetchPermissionData() {
|
||||
tableLoading.value = true;
|
||||
try {
|
||||
const [policyResult, definitionResult] = await Promise.all([
|
||||
getPermissionPolicies(),
|
||||
getPermissionDefinitions()
|
||||
]);
|
||||
|
||||
policies.value = policyResult.data;
|
||||
definitionGroups.value = definitionResult.data.groups;
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, "加载权限数据失败"));
|
||||
} finally {
|
||||
tableLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function savePermissions() {
|
||||
if (!editingPolicy.value) return;
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
await updateRolePermissions(
|
||||
editingPolicy.value.roleId,
|
||||
checkedPermissions.value
|
||||
);
|
||||
await fetchPermissionData();
|
||||
await useUserStoreHook().loadAuthContext(true);
|
||||
drawerVisible.value = false;
|
||||
ElMessage.success("权限已更新");
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, "保存权限失败"));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchPermissionData);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="permission-page">
|
||||
<div class="page-heading">
|
||||
<div>
|
||||
<p class="eyebrow">动态权限分配</p>
|
||||
<h1>权限策略</h1>
|
||||
</div>
|
||||
<el-button :icon="Refresh" @click="fetchPermissionData">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<div class="summary-strip">
|
||||
<div class="summary-item">
|
||||
<span>角色策略</span>
|
||||
<strong>{{ policies.length }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>可分配角色</span>
|
||||
<strong>{{ editableRoleCount }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>权限码</span>
|
||||
<strong>{{ permissionTotal }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>菜单授权</span>
|
||||
<strong>{{ menuTotal }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-shell">
|
||||
<el-table
|
||||
v-loading="tableLoading"
|
||||
:data="policies"
|
||||
row-key="roleCode"
|
||||
stripe
|
||||
class="permission-table"
|
||||
>
|
||||
<el-table-column label="角色" min-width="190">
|
||||
<template #default="{ row }">
|
||||
<div class="role-cell">
|
||||
<strong>{{ row.roleName }}</strong>
|
||||
<span>{{ row.roleCode }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="scope" label="作用范围" min-width="140" />
|
||||
<el-table-column label="权限码" min-width="300">
|
||||
<template #default="{ row }">
|
||||
<div class="tag-list">
|
||||
<el-tag
|
||||
v-for="permission in row.permissions"
|
||||
:key="permission"
|
||||
effect="plain"
|
||||
size="small"
|
||||
>
|
||||
{{ permission === "*" ? "全部权限" : formatPermissionTitle(permission) }}
|
||||
</el-tag>
|
||||
<span v-if="row.permissions.length === 0" class="muted-text">
|
||||
未分配
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="菜单与动作" min-width="360">
|
||||
<template #default="{ row }">
|
||||
<div class="menu-list">
|
||||
<div v-for="menu in row.menus" :key="menu.key" class="menu-item">
|
||||
<strong>{{ menu.title }}</strong>
|
||||
<span>{{ formatActions(menu.actions) }}</span>
|
||||
</div>
|
||||
<span v-if="row.menus.length === 0" class="muted-text">
|
||||
无后台菜单
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="132" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip
|
||||
:content="row.editable ? '分配权限' : '系统最高权限不可编辑'"
|
||||
placement="top"
|
||||
>
|
||||
<span>
|
||||
<el-button
|
||||
:icon="Edit"
|
||||
:disabled="!row.editable || !canUpdatePermissions"
|
||||
text
|
||||
type="primary"
|
||||
@click="openEditor(row)"
|
||||
>
|
||||
分配
|
||||
</el-button>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-drawer
|
||||
v-model="drawerVisible"
|
||||
:title="drawerTitle"
|
||||
size="520px"
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="drawer-body">
|
||||
<section
|
||||
v-for="group in definitionGroups"
|
||||
:key="group.key"
|
||||
class="permission-group"
|
||||
>
|
||||
<div class="group-heading">
|
||||
<strong>{{ group.title }}</strong>
|
||||
<el-checkbox
|
||||
:model-value="isGroupChecked(group)"
|
||||
:indeterminate="isGroupIndeterminate(group)"
|
||||
@change="value => toggleGroup(group, Boolean(value))"
|
||||
>
|
||||
全选
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<div class="permission-options">
|
||||
<el-checkbox-group v-model="checkedPermissions">
|
||||
<el-checkbox
|
||||
v-for="permission in group.permissions"
|
||||
:key="permission.code"
|
||||
:label="permission.code"
|
||||
border
|
||||
>
|
||||
<span class="permission-option">
|
||||
<strong>{{ permission.title }}</strong>
|
||||
<small>{{ permission.code }}</small>
|
||||
</span>
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="drawer-footer">
|
||||
<el-button @click="drawerVisible = false">取消</el-button>
|
||||
<el-button
|
||||
:icon="Check"
|
||||
:loading="saving"
|
||||
type="primary"
|
||||
@click="savePermissions"
|
||||
>
|
||||
保存
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.permission-page {
|
||||
min-height: 100%;
|
||||
padding: 20px;
|
||||
background: #f6f8fb;
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h1 {
|
||||
margin: 4px 0 0;
|
||||
font-size: 24px;
|
||||
font-weight: 650;
|
||||
color: #111827;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.summary-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
padding: 14px 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.table-shell {
|
||||
padding: 8px 0 14px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.permission-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.role-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
|
||||
strong {
|
||||
font-weight: 650;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 10px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
|
||||
strong {
|
||||
font-weight: 650;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.muted-text {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.drawer-body {
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.permission-group {
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.permission-group:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.permission-group:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.group-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
|
||||
strong {
|
||||
font-size: 15px;
|
||||
color: #111827;
|
||||
}
|
||||
}
|
||||
|
||||
.permission-options {
|
||||
:deep(.el-checkbox-group) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox.is-bordered) {
|
||||
height: auto;
|
||||
margin-right: 0;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__label) {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.permission-option {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
|
||||
strong {
|
||||
font-size: 14px;
|
||||
font-weight: 650;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
small {
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.permission-page {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.summary-strip {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.permission-options :deep(.el-checkbox-group) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+107
-37
@@ -9,12 +9,12 @@ import {
|
||||
import {
|
||||
createRole,
|
||||
deleteRole,
|
||||
listRoles,
|
||||
listRolePage,
|
||||
updateRole,
|
||||
type Role,
|
||||
type RolePayload
|
||||
} from "@/api/access";
|
||||
import { hasPerms } from "@/utils/auth";
|
||||
import { hasMenuAction } from "@/utils/auth";
|
||||
|
||||
import Plus from "~icons/ep/plus";
|
||||
import Search from "~icons/ep/search";
|
||||
@@ -40,7 +40,16 @@ const formRef = ref<FormInstance>();
|
||||
const roles = ref<Role[]>([]);
|
||||
|
||||
const query = reactive({
|
||||
keyword: ""
|
||||
keyword: "",
|
||||
isSystem: undefined as boolean | undefined,
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
/** 角色管理列表由后端分页,这里只记录当前筛选结果的分页摘要。 */
|
||||
const pagination = reactive({
|
||||
total: 0,
|
||||
totalPages: 0
|
||||
});
|
||||
|
||||
const form = reactive<RoleFormState>({
|
||||
@@ -72,26 +81,15 @@ const describedCount = computed(
|
||||
() => roles.value.filter(item => Boolean(item.description)).length
|
||||
);
|
||||
const systemRoleCount = computed(
|
||||
() => roles.value.filter(item => item.code === "admin").length
|
||||
() => roles.value.filter(item => item.isSystem).length
|
||||
);
|
||||
const dialogTitle = computed(() => (form.id ? "编辑角色" : "新增角色"));
|
||||
const canManageRoles = computed(() => hasPerms("role:manage"));
|
||||
|
||||
function applyRoleQuery(items: Role[]) {
|
||||
const keyword = query.keyword.trim().toLowerCase();
|
||||
|
||||
if (keyword.length === 0) {
|
||||
return items;
|
||||
}
|
||||
|
||||
return items.filter(role => {
|
||||
return (
|
||||
role.code.toLowerCase().includes(keyword) ||
|
||||
role.name.toLowerCase().includes(keyword) ||
|
||||
(role.description ?? "").toLowerCase().includes(keyword)
|
||||
);
|
||||
});
|
||||
}
|
||||
const canCreateRole = computed(() => hasMenuAction("roles", "create"));
|
||||
const canUpdateRole = computed(() => hasMenuAction("roles", "update"));
|
||||
const canDeleteRole = computed(() => hasMenuAction("roles", "delete"));
|
||||
const canOperateRole = computed(
|
||||
() => canUpdateRole.value || canDeleteRole.value
|
||||
);
|
||||
|
||||
function getErrorMessage(error: unknown, fallback: string) {
|
||||
const message = (
|
||||
@@ -134,12 +132,19 @@ function buildPayload(): RolePayload {
|
||||
};
|
||||
}
|
||||
|
||||
/** 角色查询必须走接口,避免搜索条件只在前端过滤当前缓存。 */
|
||||
/** 角色筛选、分页必须走接口,避免查询条件只在前端过滤当前缓存。 */
|
||||
async function fetchRoles() {
|
||||
tableLoading.value = true;
|
||||
try {
|
||||
const result = await listRoles();
|
||||
roles.value = applyRoleQuery(result.data);
|
||||
const result = await listRolePage({
|
||||
keyword: query.keyword.trim() || undefined,
|
||||
isSystem: query.isSystem,
|
||||
page: query.page,
|
||||
pageSize: query.pageSize
|
||||
});
|
||||
roles.value = result.data.items;
|
||||
pagination.total = result.data.pagination.total;
|
||||
pagination.totalPages = result.data.pagination.totalPages;
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, "加载角色列表失败"));
|
||||
} finally {
|
||||
@@ -149,22 +154,37 @@ async function fetchRoles() {
|
||||
|
||||
function handleReset() {
|
||||
query.keyword = "";
|
||||
query.isSystem = undefined;
|
||||
query.page = 1;
|
||||
query.pageSize = 20;
|
||||
fetchRoles();
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
query.keyword = query.keyword.trim();
|
||||
query.page = 1;
|
||||
fetchRoles();
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
query.page = page;
|
||||
fetchRoles();
|
||||
}
|
||||
|
||||
function handleSizeChange(pageSize: number) {
|
||||
query.page = 1;
|
||||
query.pageSize = pageSize;
|
||||
fetchRoles();
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
if (!canManageRoles.value) return;
|
||||
if (!canCreateRole.value) return;
|
||||
resetFormState();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEditDialog(row: Role) {
|
||||
if (!canManageRoles.value || row.isSystem) return;
|
||||
if (!canUpdateRole.value || row.isSystem) return;
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
code: row.code,
|
||||
@@ -176,7 +196,7 @@ function openEditDialog(row: Role) {
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
if (!canManageRoles.value) return;
|
||||
if (form.id ? !canUpdateRole.value : !canCreateRole.value) return;
|
||||
await formRef.value?.validate();
|
||||
submitLoading.value = true;
|
||||
|
||||
@@ -201,7 +221,7 @@ async function submitForm() {
|
||||
}
|
||||
|
||||
async function removeRole(row: Role) {
|
||||
if (!canManageRoles.value || row.isSystem) return;
|
||||
if (!canDeleteRole.value || row.isSystem) return;
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`删除后角色「${row.name}」无法再绑定员工,确认继续?`,
|
||||
@@ -214,6 +234,10 @@ async function removeRole(row: Role) {
|
||||
);
|
||||
await deleteRole(row.id);
|
||||
ElMessage.success("角色已删除");
|
||||
|
||||
if (roles.value.length === 1 && query.page > 1) {
|
||||
query.page -= 1;
|
||||
}
|
||||
fetchRoles();
|
||||
} catch (error) {
|
||||
if (error !== "cancel") {
|
||||
@@ -233,7 +257,7 @@ onMounted(fetchRoles);
|
||||
<h1>角色管理</h1>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="canManageRoles"
|
||||
v-if="canCreateRole"
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
@click="openCreateDialog"
|
||||
@@ -245,14 +269,14 @@ onMounted(fetchRoles);
|
||||
<div class="summary-strip">
|
||||
<div class="summary-item">
|
||||
<span>总角色</span>
|
||||
<strong>{{ roles.length }}</strong>
|
||||
<strong>{{ pagination.total }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>已配置说明</span>
|
||||
<span>当前页有说明</span>
|
||||
<strong>{{ describedCount }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>系统角色</span>
|
||||
<span>当前页系统角色</span>
|
||||
<strong>{{ systemRoleCount }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
@@ -262,6 +286,16 @@ onMounted(fetchRoles);
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<el-select
|
||||
v-model="query.isSystem"
|
||||
clearable
|
||||
placeholder="全部类型"
|
||||
class="toolbar-control"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="系统内置" :value="true" />
|
||||
<el-option label="自定义角色" :value="false" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="query.keyword"
|
||||
clearable
|
||||
@@ -305,14 +339,14 @@ onMounted(fetchRoles);
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="canManageRoles"
|
||||
v-if="canOperateRole"
|
||||
label="操作"
|
||||
width="170"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="!row.isSystem"
|
||||
v-if="canUpdateRole && !row.isSystem"
|
||||
link
|
||||
type="primary"
|
||||
:icon="EditPen"
|
||||
@@ -321,7 +355,7 @@ onMounted(fetchRoles);
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!row.isSystem"
|
||||
v-if="canDeleteRole && !row.isSystem"
|
||||
link
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
@@ -333,6 +367,22 @@ onMounted(fetchRoles);
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-row">
|
||||
<span
|
||||
>共 {{ pagination.total }} 条,{{ pagination.totalPages }} 页</span
|
||||
>
|
||||
<el-pagination
|
||||
v-model:current-page="query.page"
|
||||
v-model:page-size="query.pageSize"
|
||||
background
|
||||
layout="sizes, prev, pager, next"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
@@ -391,9 +441,9 @@ onMounted(fetchRoles);
|
||||
|
||||
.page-heading {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h1 {
|
||||
@@ -459,6 +509,10 @@ onMounted(fetchRoles);
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.toolbar-control {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -474,6 +528,16 @@ onMounted(fetchRoles);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pagination-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px 0;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.role-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -510,8 +574,8 @@ onMounted(fetchRoles);
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.summary-strip {
|
||||
@@ -519,6 +583,7 @@ onMounted(fetchRoles);
|
||||
}
|
||||
|
||||
.keyword-input,
|
||||
.toolbar-control,
|
||||
.toolbar-actions {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
@@ -528,6 +593,11 @@ onMounted(fetchRoles);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.pagination-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
+88
-34
@@ -15,7 +15,7 @@ import {
|
||||
type StorePayload,
|
||||
type StoreStatus
|
||||
} from "@/api/access";
|
||||
import { hasPerms } from "@/utils/auth";
|
||||
import { hasMenuAction } from "@/utils/auth";
|
||||
|
||||
import Plus from "~icons/ep/plus";
|
||||
import Search from "~icons/ep/search";
|
||||
@@ -42,7 +42,15 @@ const stores = ref<Store[]>([]);
|
||||
|
||||
const query = reactive({
|
||||
status: undefined as StoreStatus | undefined,
|
||||
keyword: ""
|
||||
keyword: "",
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
/** 门店管理列表由后端分页,这里只记录当前筛选结果的分页摘要。 */
|
||||
const pagination = reactive({
|
||||
total: 0,
|
||||
totalPages: 0
|
||||
});
|
||||
|
||||
const form = reactive<StoreFormState>({
|
||||
@@ -71,23 +79,12 @@ 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();
|
||||
|
||||
return items.filter(store => {
|
||||
const matchedStatus =
|
||||
query.status === undefined || store.status === query.status;
|
||||
const matchedKeyword =
|
||||
keyword.length === 0 ||
|
||||
store.name.toLowerCase().includes(keyword) ||
|
||||
(store.address ?? "").toLowerCase().includes(keyword) ||
|
||||
(store.phone ?? "").toLowerCase().includes(keyword);
|
||||
|
||||
return matchedStatus && matchedKeyword;
|
||||
});
|
||||
}
|
||||
const canCreateStore = computed(() => hasMenuAction("stores", "create"));
|
||||
const canUpdateStore = computed(() => hasMenuAction("stores", "update"));
|
||||
const canDeleteStore = computed(() => hasMenuAction("stores", "delete"));
|
||||
const canOperateStore = computed(
|
||||
() => canUpdateStore.value || canDeleteStore.value
|
||||
);
|
||||
|
||||
function getErrorMessage(error: unknown, fallback: string) {
|
||||
const message = (
|
||||
@@ -133,14 +130,19 @@ function buildPayload(): StorePayload {
|
||||
};
|
||||
}
|
||||
|
||||
/** 门店筛选必须走接口,避免查询条件只停留在当前页面内存里。 */
|
||||
/** 门店筛选、分页必须走接口,避免查询条件只停留在当前页面内存里。 */
|
||||
async function fetchStores() {
|
||||
tableLoading.value = true;
|
||||
try {
|
||||
const result = await listStores({
|
||||
includeInactive: true
|
||||
status: query.status,
|
||||
keyword: query.keyword.trim() || undefined,
|
||||
page: query.page,
|
||||
pageSize: query.pageSize
|
||||
});
|
||||
stores.value = applyStoreQuery(result.data);
|
||||
stores.value = result.data.items;
|
||||
pagination.total = result.data.pagination.total;
|
||||
pagination.totalPages = result.data.pagination.totalPages;
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, "加载门店列表失败"));
|
||||
} finally {
|
||||
@@ -151,22 +153,36 @@ async function fetchStores() {
|
||||
function handleReset() {
|
||||
query.status = undefined;
|
||||
query.keyword = "";
|
||||
query.page = 1;
|
||||
query.pageSize = 20;
|
||||
fetchStores();
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
query.keyword = query.keyword.trim();
|
||||
query.page = 1;
|
||||
fetchStores();
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
query.page = page;
|
||||
fetchStores();
|
||||
}
|
||||
|
||||
function handleSizeChange(pageSize: number) {
|
||||
query.page = 1;
|
||||
query.pageSize = pageSize;
|
||||
fetchStores();
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
if (!canManageStores.value) return;
|
||||
if (!canCreateStore.value) return;
|
||||
resetFormState();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEditDialog(row: Store) {
|
||||
if (!canManageStores.value) return;
|
||||
if (!canUpdateStore.value) return;
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
@@ -179,7 +195,7 @@ function openEditDialog(row: Store) {
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
if (!canManageStores.value) return;
|
||||
if (form.id ? !canUpdateStore.value : !canCreateStore.value) return;
|
||||
await formRef.value?.validate();
|
||||
submitLoading.value = true;
|
||||
|
||||
@@ -204,7 +220,7 @@ async function submitForm() {
|
||||
}
|
||||
|
||||
async function toggleStatus(row: Store) {
|
||||
if (!canManageStores.value) return;
|
||||
if (!canUpdateStore.value) return;
|
||||
const nextStatus: StoreStatus =
|
||||
row.status === "ACTIVE" ? "INACTIVE" : "ACTIVE";
|
||||
const action = nextStatus === "ACTIVE" ? "启用" : "停用";
|
||||
@@ -230,7 +246,7 @@ async function toggleStatus(row: Store) {
|
||||
}
|
||||
|
||||
async function removeStore(row: Store) {
|
||||
if (!canManageStores.value) return;
|
||||
if (!canDeleteStore.value) return;
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`删除后门店「${row.name}」不会再出现在员工新增下拉里,确认继续?`,
|
||||
@@ -243,6 +259,10 @@ async function removeStore(row: Store) {
|
||||
);
|
||||
await deleteStore(row.id);
|
||||
ElMessage.success("门店已删除");
|
||||
|
||||
if (stores.value.length === 1 && query.page > 1) {
|
||||
query.page -= 1;
|
||||
}
|
||||
fetchStores();
|
||||
} catch (error) {
|
||||
if (error !== "cancel") {
|
||||
@@ -262,7 +282,7 @@ onMounted(fetchStores);
|
||||
<h1>门店管理</h1>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="canManageStores"
|
||||
v-if="canCreateStore"
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
@click="openCreateDialog"
|
||||
@@ -274,14 +294,14 @@ onMounted(fetchStores);
|
||||
<div class="summary-strip">
|
||||
<div class="summary-item">
|
||||
<span>总门店</span>
|
||||
<strong>{{ stores.length }}</strong>
|
||||
<strong>{{ pagination.total }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>启用门店</span>
|
||||
<span>当前页启用</span>
|
||||
<strong>{{ activeCount }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>停用门店</span>
|
||||
<span>当前页停用</span>
|
||||
<strong>{{ inactiveCount }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
@@ -356,13 +376,14 @@ onMounted(fetchStores);
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="canManageStores"
|
||||
v-if="canOperateStore"
|
||||
label="操作"
|
||||
width="260"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="canUpdateStore"
|
||||
link
|
||||
type="primary"
|
||||
:icon="EditPen"
|
||||
@@ -371,6 +392,7 @@ onMounted(fetchStores);
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canUpdateStore"
|
||||
link
|
||||
:type="row.status === 'ACTIVE' ? 'warning' : 'success'"
|
||||
:icon="row.status === 'ACTIVE' ? CircleClose : CircleCheck"
|
||||
@@ -379,6 +401,7 @@ onMounted(fetchStores);
|
||||
{{ row.status === "ACTIVE" ? "停用" : "启用" }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canDeleteStore"
|
||||
link
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
@@ -389,6 +412,22 @@ onMounted(fetchStores);
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-row">
|
||||
<span
|
||||
>共 {{ pagination.total }} 条,{{ pagination.totalPages }} 页</span
|
||||
>
|
||||
<el-pagination
|
||||
v-model:current-page="query.page"
|
||||
v-model:page-size="query.pageSize"
|
||||
background
|
||||
layout="sizes, prev, pager, next"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
@@ -453,9 +492,9 @@ onMounted(fetchStores);
|
||||
|
||||
.page-heading {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h1 {
|
||||
@@ -540,6 +579,16 @@ onMounted(fetchStores);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pagination-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px 0;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.primary-text {
|
||||
font-weight: 650;
|
||||
color: #111827;
|
||||
@@ -561,8 +610,8 @@ onMounted(fetchStores);
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.summary-strip {
|
||||
@@ -580,6 +629,11 @@ onMounted(fetchStores);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.pagination-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user