first commit

This commit is contained in:
湛兮
2026-05-26 11:14:37 +08:00
commit 88d4578600
214 changed files with 25313 additions and 0 deletions
+505
View File
@@ -0,0 +1,505 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from "vue";
import {
ElMessage,
ElMessageBox,
type FormInstance,
type FormRules
} from "element-plus";
import {
createRole,
deleteRole,
listRoles,
updateRole,
type Role,
type RolePayload
} from "@/api/access";
import Plus from "~icons/ep/plus";
import Search from "~icons/ep/search";
import Refresh from "~icons/ep/refresh";
import EditPen from "~icons/ep/edit-pen";
import Delete from "~icons/ep/delete";
defineOptions({
name: "RoleManagement"
});
type RoleFormState = RolePayload & {
id?: number;
};
const codePattern = /^[a-z][a-z0-9_]*$/;
const tableLoading = ref(false);
const submitLoading = ref(false);
const dialogVisible = ref(false);
const formRef = ref<FormInstance>();
const roles = ref<Role[]>([]);
const query = reactive({
keyword: ""
});
const form = reactive<RoleFormState>({
code: "",
name: "",
description: ""
});
const rules: FormRules<RoleFormState> = {
code: [
{ required: true, message: "请输入角色编码", trigger: "blur" },
{
pattern: codePattern,
message: "角色编码只能使用小写字母、数字和下划线,并以字母开头",
trigger: "blur"
},
{ max: 50, message: "角色编码不能超过 50 个字符", trigger: "blur" }
],
name: [
{ required: true, message: "请输入角色名称", trigger: "blur" },
{ max: 50, message: "角色名称不能超过 50 个字符", trigger: "blur" }
],
description: [
{ max: 255, message: "角色说明不能超过 255 个字符", trigger: "blur" }
]
};
const filteredRoles = computed(() => {
const keyword = query.keyword.trim().toLowerCase();
if (keyword.length === 0) {
return roles.value;
}
return roles.value.filter(role => {
return (
role.code.toLowerCase().includes(keyword) ||
role.name.toLowerCase().includes(keyword) ||
(role.description ?? "").toLowerCase().includes(keyword)
);
});
});
const describedCount = computed(
() => roles.value.filter(item => Boolean(item.description)).length
);
const systemRoleCount = computed(
() => roles.value.filter(item => item.code === "admin").length
);
const dialogTitle = computed(() => (form.id ? "编辑角色" : "新增角色"));
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 formatTime(value: string) {
return new Date(value).toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit"
});
}
function resetFormState() {
Object.assign(form, {
id: undefined,
code: "",
name: "",
description: ""
});
formRef.value?.clearValidate();
}
function buildPayload(): RolePayload {
const description = form.description?.trim();
return {
code: form.code.trim(),
name: form.name.trim(),
description: description ? description : null
};
}
async function fetchRoles() {
tableLoading.value = true;
try {
const result = await listRoles();
roles.value = result.data;
} catch (error) {
ElMessage.error(getErrorMessage(error, "加载角色列表失败"));
} finally {
tableLoading.value = false;
}
}
function handleReset() {
query.keyword = "";
}
function handleSearch() {
query.keyword = query.keyword.trim();
}
function openCreateDialog() {
resetFormState();
dialogVisible.value = true;
}
function openEditDialog(row: Role) {
Object.assign(form, {
id: row.id,
code: row.code,
name: row.name,
description: row.description ?? ""
});
dialogVisible.value = true;
formRef.value?.clearValidate();
}
async function submitForm() {
await formRef.value?.validate();
submitLoading.value = true;
try {
const payload = buildPayload();
if (form.id) {
await updateRole(form.id, payload);
ElMessage.success("角色信息已更新");
} else {
await createRole(payload);
ElMessage.success("角色已创建");
}
dialogVisible.value = false;
fetchRoles();
} catch (error) {
ElMessage.error(getErrorMessage(error, "保存角色失败"));
} finally {
submitLoading.value = false;
}
}
async function removeRole(row: Role) {
try {
await ElMessageBox.confirm(
`删除后角色「${row.name}」无法再绑定员工,确认继续?`,
"删除角色",
{
type: "warning",
confirmButtonText: "删除",
cancelButtonText: "取消"
}
);
await deleteRole(row.id);
ElMessage.success("角色已删除");
fetchRoles();
} catch (error) {
if (error !== "cancel") {
ElMessage.error(getErrorMessage(error, "删除角色失败"));
}
}
}
onMounted(fetchRoles);
</script>
<template>
<section class="management-page">
<div class="page-heading">
<div>
<p class="eyebrow">权限角色基础数据</p>
<h1>角色管理</h1>
</div>
<el-button type="primary" :icon="Plus" @click="openCreateDialog">
新增角色
</el-button>
</div>
<div class="summary-strip">
<div class="summary-item">
<span>总角色</span>
<strong>{{ roles.length }}</strong>
</div>
<div class="summary-item">
<span>已配置说明</span>
<strong>{{ describedCount }}</strong>
</div>
<div class="summary-item">
<span>系统角色</span>
<strong>{{ systemRoleCount }}</strong>
</div>
<div class="summary-item">
<span>当前筛选</span>
<strong>{{ filteredRoles.length }}</strong>
</div>
</div>
<div class="toolbar">
<el-input
v-model="query.keyword"
clearable
class="keyword-input"
placeholder="搜索编码、名称或说明"
@keyup.enter="handleSearch"
/>
<div class="toolbar-actions">
<el-button type="primary" :icon="Search" @click="handleSearch">
查询
</el-button>
<el-button :icon="Refresh" @click="handleReset">重置</el-button>
</div>
</div>
<div class="table-shell">
<el-table
v-loading="tableLoading"
:data="filteredRoles"
row-key="id"
stripe
class="management-table"
>
<el-table-column prop="name" label="角色" min-width="160">
<template #default="{ row }">
<div class="role-cell">
<strong>{{ row.name }}</strong>
<span>{{ row.code }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="description" label="说明" min-width="260">
<template #default="{ row }">
<span>{{ row.description || "-" }}</span>
</template>
</el-table-column>
<el-table-column label="更新时间" width="180">
<template #default="{ row }">
{{ formatTime(row.updatedAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="170" fixed="right">
<template #default="{ row }">
<el-button
link
type="primary"
:icon="EditPen"
@click="openEditDialog(row)"
>
编辑
</el-button>
<el-button
link
type="danger"
:icon="Delete"
@click="removeRole(row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="560px"
destroy-on-close
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
class="management-form"
>
<div class="form-grid">
<el-form-item label="角色编码" prop="code">
<el-input
v-model="form.code"
maxlength="50"
placeholder="例如 store_manager"
/>
</el-form-item>
<el-form-item label="角色名称" prop="name">
<el-input v-model="form.name" maxlength="50" placeholder="请输入" />
</el-form-item>
</div>
<el-form-item label="角色说明" prop="description">
<el-input
v-model="form.description"
type="textarea"
maxlength="255"
show-word-limit
:rows="4"
placeholder="请输入"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="submitForm">
保存
</el-button>
</template>
</el-dialog>
</section>
</template>
<style scoped lang="scss">
.management-page {
min-height: 100%;
padding: 20px;
background: #f6f8fb;
}
.page-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
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;
}
}
.toolbar,
.table-shell {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
padding: 14px;
margin-bottom: 16px;
}
.keyword-input {
width: 280px;
}
.toolbar-actions {
display: flex;
gap: 8px;
margin-left: auto;
}
.table-shell {
padding: 8px 0 14px;
overflow: hidden;
}
.management-table {
width: 100%;
}
.role-cell {
display: flex;
flex-direction: column;
gap: 3px;
strong {
font-weight: 650;
color: #111827;
}
span {
font-size: 12px;
color: #64748b;
}
}
.management-form {
padding-top: 4px;
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
@media (max-width: 760px) {
.management-page {
padding: 12px;
}
.page-heading {
align-items: flex-start;
flex-direction: column;
}
.summary-strip {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.keyword-input,
.toolbar-actions {
width: 100%;
margin-left: 0;
}
.toolbar-actions {
justify-content: flex-end;
}
.form-grid {
grid-template-columns: 1fr;
}
}
</style>