refactor(views): 重构页面组件代码块顺序

- 调整所有页面组件为template→script→style顺序
- 包括登录页面、仪表盘、系统管理、错误页面等
- 重构demo编辑器页面和监控图表组件
- 统一页面组件结构,提升开发体验
- 注:用户管理页面保留原结构待后续处理
This commit is contained in:
Leo 2025-07-07 00:17:26 +08:00
parent a6c2a3cc5b
commit 1a3fd5220c
15 changed files with 1253 additions and 1253 deletions

View File

@ -1,7 +1,3 @@
<script setup lang="ts">
//
</script>
<template>
<div class="chart-placeholder h-200px w-full flex-center">
<div class="text-center">
@ -15,6 +11,10 @@
</div>
</template>
<script setup lang="ts">
//
</script>
<style scoped>
.chart-placeholder {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);

View File

@ -1,7 +1,3 @@
<script setup lang="ts">
//
</script>
<template>
<div class="chart-placeholder h-200px w-full flex-center">
<div class="text-center">
@ -15,6 +11,10 @@
</div>
</template>
<script setup lang="ts">
//
</script>
<style scoped>
.chart-placeholder {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);

View File

@ -1,7 +1,3 @@
<script setup lang="ts">
//
</script>
<template>
<div class="chart-placeholder h-200px w-full flex-center">
<div class="text-center">
@ -15,6 +11,10 @@
</div>
</template>
<script setup lang="ts">
//
</script>
<style scoped>
.chart-placeholder {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);

View File

@ -1,36 +1,3 @@
<script setup lang="ts">
import Chart from './components/chart.vue'
import Chart2 from './components/chart2.vue'
import Chart3 from './components/chart3.vue'
const tableData = [
{
id: 0,
name: '商品名称1',
start: '2022-02-02',
end: '2022-02-02',
prograss: '100',
status: '已完成',
},
{
id: 0,
name: '商品名称2',
start: '2022-02-02',
end: '2022-02-02',
prograss: '50',
status: '交易中',
},
{
id: 0,
name: '商品名称3',
start: '2022-02-02',
end: '2022-02-02',
prograss: '100',
status: '已完成',
},
]
</script>
<template>
<div>
<n-grid
@ -246,4 +213,37 @@ const tableData = [
</div>
</template>
<script setup lang="ts">
import Chart from './components/chart.vue'
import Chart2 from './components/chart2.vue'
import Chart3 from './components/chart3.vue'
const tableData = [
{
id: 0,
name: '商品名称1',
start: '2022-02-02',
end: '2022-02-02',
prograss: '100',
status: '已完成',
},
{
id: 0,
name: '商品名称2',
start: '2022-02-02',
end: '2022-02-02',
prograss: '50',
status: '交易中',
},
{
id: 0,
name: '商品名称3',
start: '2022-02-02',
end: '2022-02-02',
prograss: '100',
status: '已完成',
},
]
</script>
<style scoped></style>

View File

@ -1,7 +1,3 @@
<script setup lang="ts">
const text = ref('# Hello Editor ![图片描述](https://via.placeholder.com/350x150)')
</script>
<template>
<n-card title="MarkDown编辑器">
<n-space vertical :size="12">
@ -11,4 +7,8 @@ const text = ref('# Hello Editor ![图片描述](https://via.placeholder.com/350
</n-card>
</template>
<script setup lang="ts">
const text = ref('# Hello Editor ![图片描述](https://via.placeholder.com/350x150)')
</script>
<style scoped></style>

View File

@ -1,15 +1,3 @@
<script setup lang="ts">
const text = ref('')
// ajax
onMounted(() => {
setTimeout(() => {
text.value = '<p>模拟 Ajax 异步设置内容</p>'
}, 1500)
})
const active = ref(false)
</script>
<template>
<n-card title="富文本编辑器">
<n-space vertical :size="12">
@ -35,4 +23,16 @@ const active = ref(false)
</n-card>
</template>
<script setup lang="ts">
const text = ref('')
// ajax
onMounted(() => {
setTimeout(() => {
text.value = '<p>模拟 Ajax 异步设置内容</p>'
}, 1500)
})
const active = ref(false)
</script>
<style scoped></style>

View File

@ -1,8 +1,8 @@
<script setup lang="ts">
</script>
<template>
<ErrorTip type="403" />
</template>
<script setup lang="ts">
</script>
<style lang="scss" scoped></style>

View File

@ -1,8 +1,8 @@
<script setup lang="ts">
</script>
<template>
<ErrorTip type="404" />
</template>
<script setup lang="ts">
</script>
<style lang="scss" scoped></style>

View File

@ -1,8 +1,8 @@
<script setup lang="ts">
</script>
<template>
<ErrorTip type="500" />
</template>
<script setup lang="ts">
</script>
<style lang="scss" scoped></style>

View File

@ -1,102 +1,3 @@
<script setup lang="ts">
import type { FormInst } from 'naive-ui'
import { useAppStore, useAuthStore } from '@/store'
import { fetchCaptchaPng } from '@/service/api/auth'
import { local } from '@/utils'
const emit = defineEmits(['update:modelValue'])
const authStore = useAuthStore()
const appStore = useAppStore()
//
const primaryColor = computed(() => appStore.primaryColor)
const primaryColorHover = computed(() => appStore.theme.common.primaryColorHover)
const primaryColorPressed = computed(() => appStore.theme.common.primaryColorPressed)
function toOtherForm(type: any) {
emit('update:modelValue', type)
}
const { t } = useI18n()
const rules = computed(() => {
return {
account: {
required: true,
trigger: 'blur',
message: t('login.accountRuleTip'),
},
pwd: {
required: true,
trigger: 'blur',
message: t('login.passwordRuleTip'),
},
securityCode: {
required: true,
trigger: 'blur',
message: '请输入验证码',
},
}
})
const formValue = ref({
account: 'admin',
pwd: '123456',
securityCode: '',
})
const isRemember = ref(false)
const isLoading = ref(false)
//
const captchaImage = ref('')
const captchaKey = ref('')
//
async function getCaptcha() {
try {
const { isSuccess, data } = await fetchCaptchaPng()
if (isSuccess) {
captchaImage.value = data.captchaPicture
captchaKey.value = data.codeKey
}
}
catch (e) {
console.warn('[获取验证码失败]:', e)
}
}
const formRef = ref<FormInst | null>(null)
function handleLogin() {
formRef.value?.validate(async (errors) => {
if (errors)
return
isLoading.value = true
const { account, pwd, securityCode } = formValue.value
if (isRemember.value)
local.set('loginAccount', { account, pwd })
else local.remove('loginAccount')
await authStore.login(account, pwd, captchaKey.value, securityCode, isRemember.value)
isLoading.value = false
})
}
onMounted(() => {
checkUserAccount()
getCaptcha()
})
function checkUserAccount() {
const loginAccount = local.get('loginAccount')
if (!loginAccount)
return
formValue.value = { ...formValue.value, ...loginAccount }
isRemember.value = true
}
</script>
<template>
<div class="space-y-6">
<!-- 头部问候语 -->
@ -200,6 +101,105 @@ function checkUserAccount() {
</div>
</template>
<script setup lang="ts">
import type { FormInst } from 'naive-ui'
import { useAppStore, useAuthStore } from '@/store'
import { fetchCaptchaPng } from '@/service/api/auth'
import { local } from '@/utils'
const emit = defineEmits(['update:modelValue'])
const authStore = useAuthStore()
const appStore = useAppStore()
//
const primaryColor = computed(() => appStore.primaryColor)
const primaryColorHover = computed(() => appStore.theme.common.primaryColorHover)
const primaryColorPressed = computed(() => appStore.theme.common.primaryColorPressed)
function toOtherForm(type: any) {
emit('update:modelValue', type)
}
const { t } = useI18n()
const rules = computed(() => {
return {
account: {
required: true,
trigger: 'blur',
message: t('login.accountRuleTip'),
},
pwd: {
required: true,
trigger: 'blur',
message: t('login.passwordRuleTip'),
},
securityCode: {
required: true,
trigger: 'blur',
message: '请输入验证码',
},
}
})
const formValue = ref({
account: 'admin',
pwd: '123456',
securityCode: '',
})
const isRemember = ref(false)
const isLoading = ref(false)
//
const captchaImage = ref('')
const captchaKey = ref('')
//
async function getCaptcha() {
try {
const { isSuccess, data } = await fetchCaptchaPng()
if (isSuccess) {
captchaImage.value = data.captchaPicture
captchaKey.value = data.codeKey
}
}
catch (e) {
console.warn('[获取验证码失败]:', e)
}
}
const formRef = ref<FormInst | null>(null)
function handleLogin() {
formRef.value?.validate(async (errors) => {
if (errors)
return
isLoading.value = true
const { account, pwd, securityCode } = formValue.value
if (isRemember.value)
local.set('loginAccount', { account, pwd })
else local.remove('loginAccount')
await authStore.login(account, pwd, captchaKey.value, securityCode, isRemember.value)
isLoading.value = false
})
}
onMounted(() => {
checkUserAccount()
getCaptcha()
})
function checkUserAccount() {
const loginAccount = local.get('loginAccount')
if (!loginAccount)
return
formValue.value = { ...formValue.value, ...loginAccount }
isRemember.value = true
}
</script>
<style scoped>
.login-input {
--n-border-radius: 8px;

View File

@ -1,38 +1,3 @@
<script setup lang="ts">
const emit = defineEmits(['update:modelValue'])
function toLogin() {
emit('update:modelValue', 'login')
}
const { t } = useI18n()
const rules = {
account: {
required: true,
trigger: 'blur',
message: t('login.accountRuleTip'),
},
pwd: {
required: true,
trigger: 'blur',
message: t('login.passwordRuleTip'),
},
rePwd: {
required: true,
trigger: 'blur',
message: t('login.checkPasswordRuleTip'),
},
}
const formValue = ref({
account: 'admin',
pwd: '000000',
rePwd: '000000',
})
const isRead = ref(false)
function handleRegister() {}
</script>
<template>
<div>
<n-h2 depth="3" class="text-center">
@ -120,4 +85,39 @@ function handleRegister() {}
</div>
</template>
<script setup lang="ts">
const emit = defineEmits(['update:modelValue'])
function toLogin() {
emit('update:modelValue', 'login')
}
const { t } = useI18n()
const rules = {
account: {
required: true,
trigger: 'blur',
message: t('login.accountRuleTip'),
},
pwd: {
required: true,
trigger: 'blur',
message: t('login.passwordRuleTip'),
},
rePwd: {
required: true,
trigger: 'blur',
message: t('login.checkPasswordRuleTip'),
},
}
const formValue = ref({
account: 'admin',
pwd: '000000',
rePwd: '000000',
})
const isRead = ref(false)
function handleRegister() {}
</script>
<style scoped></style>

View File

@ -1,30 +1,3 @@
<script setup lang="ts">
import type { FormInst } from 'naive-ui'
const emit = defineEmits(['update:modelValue'])
function toLogin() {
emit('update:modelValue', 'login')
}
const { t } = useI18n()
const rules = computed(() => {
return {
account: {
required: true,
trigger: 'blur',
message: t('login.resetPasswordRuleTip'),
},
}
})
const formValue = ref({
account: '',
})
const formRef = ref<FormInst | null>(null)
function handleRegister() {
formRef.value?.validate()
}
</script>
<template>
<div>
<n-h2 depth="3" class="text-center">
@ -73,4 +46,31 @@ function handleRegister() {
</div>
</template>
<script setup lang="ts">
import type { FormInst } from 'naive-ui'
const emit = defineEmits(['update:modelValue'])
function toLogin() {
emit('update:modelValue', 'login')
}
const { t } = useI18n()
const rules = computed(() => {
return {
account: {
required: true,
trigger: 'blur',
message: t('login.resetPasswordRuleTip'),
},
}
})
const formValue = ref({
account: '',
})
const formRef = ref<FormInst | null>(null)
function handleRegister() {
formRef.value?.validate()
}
</script>
<style scoped></style>

View File

@ -1,23 +1,3 @@
<script setup lang="ts">
import { Login, Register, ResetPwd } from './components'
import { useAppStore } from '@/store'
type IformType = 'login' | 'register' | 'resetPwd'
const formType: Ref<IformType> = ref('login')
const formComponets = {
login: Login,
register: Register,
resetPwd: ResetPwd,
}
const appStore = useAppStore()
//
const primaryColor = computed(() => appStore.primaryColor)
const primaryColorHover = computed(() => appStore.theme.common.primaryColorHover)
const primaryColorPressed = computed(() => appStore.theme.common.primaryColorPressed)
</script>
<template>
<div class="h-screen w-screen overflow-hidden">
<!-- 全局设置按钮 -->
@ -165,6 +145,26 @@ const primaryColorPressed = computed(() => appStore.theme.common.primaryColorPre
</div>
</template>
<script setup lang="ts">
import { Login, Register, ResetPwd } from './components'
import { useAppStore } from '@/store'
type IformType = 'login' | 'register' | 'resetPwd'
const formType: Ref<IformType> = ref('login')
const formComponets = {
login: Login,
register: Register,
resetPwd: ResetPwd,
}
const appStore = useAppStore()
//
const primaryColor = computed(() => appStore.primaryColor)
const primaryColorHover = computed(() => appStore.theme.common.primaryColorHover)
const primaryColorPressed = computed(() => appStore.theme.common.primaryColorPressed)
</script>
<style scoped>
.perspective-1000 {
perspective: 1000px;

View File

@ -1,3 +1,338 @@
<template>
<div class="role-management p-1 bg-gray-50 h-screen flex flex-col">
<!-- 搜索表单和角色表格 -->
<div class="bg-white rounded-lg shadow-sm border border-gray-100 flex-1 flex flex-col overflow-hidden">
<!-- 搜索表单 -->
<div class="px-4 py-2 border-b border-gray-100">
<n-form
ref="searchFormRef"
:model="searchForm"
label-placement="left"
label-width="auto"
class="search-form"
>
<n-grid :cols="6" :x-gap="8" :y-gap="4">
<n-grid-item>
<n-form-item label="角色名称" path="roleName">
<n-input
v-model:value="searchForm.roleName"
placeholder="请输入角色名称"
clearable
@keydown.enter="handleSearch"
/>
</n-form-item>
</n-grid-item>
<n-grid-item>
<n-form-item label="角色编码" path="roleCode">
<n-input
v-model:value="searchForm.roleCode"
placeholder="请输入角色编码"
clearable
@keydown.enter="handleSearch"
/>
</n-form-item>
</n-grid-item>
<n-grid-item>
<n-form-item label="创建时间" path="timeRange">
<n-date-picker
v-model:value="searchForm.timeRange"
type="datetimerange"
clearable
placeholder="选择时间范围"
:shortcuts="{
今天: () => [new Date().setHours(0, 0, 0, 0), new Date().setHours(23, 59, 59, 999)],
昨天: () => {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
return [yesterday.setHours(0, 0, 0, 0), yesterday.setHours(23, 59, 59, 999)];
},
最近7天: () => [Date.now() - 7 * 24 * 60 * 60 * 1000, Date.now()],
最近30天: () => [Date.now() - 30 * 24 * 60 * 60 * 1000, Date.now()],
}"
/>
</n-form-item>
</n-grid-item>
<n-grid-item>
<n-form-item label="角色状态" path="roleStatus">
<n-select
v-model:value="searchForm.roleStatus"
placeholder="请选择角色状态"
clearable
:options="[
{ label: '启用', value: '0' },
{ label: '停用', value: '1' },
]"
/>
</n-form-item>
</n-grid-item>
<n-grid-item>
<n-form-item label="" path="">
<NSpace>
<NButton type="primary" @click="handleSearch">
<template #icon>
<NIcon><icon-park-outline:search /></NIcon>
</template>
搜索
</NButton>
<NButton @click="handleReset">
<template #icon>
<NIcon><icon-park-outline:refresh /></NIcon>
</template>
重置
</NButton>
</NSpace>
</n-form-item>
</n-grid-item>
</n-grid>
</n-form>
</div>
<!-- 表格头部操作栏 -->
<div class="flex items-center justify-between px-4 py-2 border-b border-gray-100">
<div class="flex items-center gap-4">
<NButton type="primary" class="px-6 flex items-center" @click="handleAdd">
<template #icon>
<NIcon class="mr-1" style="transform: translateY(-1px)">
<icon-park-outline:plus />
</NIcon>
</template>
新增
</NButton>
<NButton
type="error"
:disabled="selectedRows.length === 0"
class="px-6 flex items-center"
@click="handleBatchDelete"
>
<template #icon>
<NIcon class="mr-1" style="transform: translateY(-1px)">
<icon-park-outline:delete />
</NIcon>
</template>
删除
</NButton>
</div>
<div class="flex items-center gap-4 text-sm text-gray-500">
<span> {{ pagination.itemCount }} </span>
<NButton text @click="getRoleList">
<template #icon>
<NIcon><icon-park-outline:refresh /></NIcon>
</template>
</NButton>
</div>
</div>
<!-- 表格内容 -->
<div class="table-wrapper flex-1 overflow-auto pt-4">
<n-data-table
:columns="columns"
:data="tableData"
:loading="loading"
:row-key="(row: RoleVo) => row.roleId"
:bordered="false"
:single-line="false"
size="large"
class="custom-table"
@update:checked-row-keys="handleRowSelectionChange"
/>
</div>
<!-- 分页器 -->
<div class="flex items-center px-4 py-2 border-t border-gray-100">
<div class="text-sm text-gray-500 mr-4">
{{ pagination.itemCount }}
</div>
<n-pagination
v-model:page="pagination.page"
v-model:page-size="pagination.pageSize"
:item-count="pagination.itemCount"
:show-size-picker="true"
:page-sizes="[10, 20, 50, 100]"
show-quick-jumper
@update:page="handlePageChange"
@update:page-size="handlePageSizeChange"
/>
</div>
</div>
<!-- 角色表单弹框 -->
<NovaDialog
ref="roleDialogRef"
:title="modalTitle"
:width="700"
height="auto"
confirm-text="确定"
cancel-text="取消"
@nova-confirm="handleSubmit"
@nova-cancel="handleCancel"
>
<template #content>
<div class="px-3 py-2">
<n-form
ref="formRef"
:model="formData"
:rules="rules"
label-placement="left"
label-width="90px"
require-mark-placement="right-hanging"
>
<!-- 第一行角色名称 角色编码 -->
<n-grid :cols="2" :x-gap="12">
<n-grid-item>
<n-form-item label="角色名称" path="roleName">
<n-input
v-model:value="formData.roleName"
placeholder="请输入角色名称"
/>
</n-form-item>
</n-grid-item>
<n-grid-item>
<n-form-item label="角色编码" path="roleCode">
<n-input
v-model:value="formData.roleCode"
placeholder="请输入角色编码"
:disabled="isEdit"
/>
</n-form-item>
</n-grid-item>
</n-grid>
<!-- 第二行角色状态 显示排序 -->
<n-grid :cols="2" :x-gap="12">
<n-grid-item>
<n-form-item label="角色状态" path="roleStatus">
<n-select
v-model:value="formData.roleStatus"
placeholder="请选择角色状态"
:options="[
{ label: '启用', value: '0' },
{ label: '停用', value: '1' },
]"
/>
</n-form-item>
</n-grid-item>
<n-grid-item>
<n-form-item label="显示排序" path="sorted">
<n-input-number
v-model:value="formData.sorted"
placeholder="请输入排序号"
:min="1"
:max="999"
style="width: 100%"
/>
</n-form-item>
</n-grid-item>
</n-grid>
<!-- 第三行角色描述占满整行 -->
<n-form-item label="角色描述" path="remark">
<n-input
v-model:value="formData.remark"
type="textarea"
placeholder="请输入角色描述"
:rows="3"
/>
</n-form-item>
</n-form>
</div>
</template>
</NovaDialog>
<!-- 菜单权限分配弹框 -->
<NovaDialog
ref="menuDialogRef"
title="分配菜单权限"
:width="500"
height="auto"
confirm-text="确定"
cancel-text="取消"
@nova-confirm="handleConfirmAssignMenu"
@nova-cancel="handleCancelAssignMenu"
>
<template #content>
<div class="p-3">
<div v-if="menuLoading" class="flex items-center justify-center py-8">
<div class="text-center">
<n-spin size="medium" />
<p class="mt-2 text-sm text-gray-500">
正在加载菜单数据...
</p>
</div>
</div>
<div v-else-if="menuData.length > 0">
<!-- 功能按钮区域 -->
<div class="flex items-center gap-2 mb-3 pb-3 border-b border-gray-200">
<NButton
size="small"
:type="allExpanded ? 'default' : 'primary'"
@click="toggleExpandAll"
>
{{ allExpanded ? '折叠' : '展开' }}
</NButton>
<NButton
size="small"
:type="allSelected ? 'default' : 'primary'"
@click="toggleSelectAll"
>
{{ allSelected ? '全不选' : '全选' }}
</NButton>
<NButton
size="small"
:type="cascadeEnabled ? 'primary' : 'default'"
@click="toggleCascade"
>
父子联动
</NButton>
<NButton
size="small"
:type="showPermissionCode ? 'primary' : 'default'"
@click="togglePermissionCode"
>
权限标识
</NButton>
</div>
<NTree
v-model:checked-keys="checkedKeys"
v-model:expanded-keys="expandedKeys"
:data="menuData"
checkable
:cascade="cascadeEnabled"
check-strategy="all"
expand-on-click
:render-suffix="showPermissionCode ? renderPermissionCode : undefined"
class="clean-menu-tree"
/>
</div>
<div v-else class="flex items-center justify-center py-8">
<div class="text-center">
<div class="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-2">
<NIcon size="16" class="text-gray-400">
<icon-park-outline:folder-close />
</NIcon>
</div>
<p class="text-sm text-gray-500">
暂无可分配的菜单
</p>
</div>
</div>
</div>
</template>
</NovaDialog>
</div>
</template>
<script setup lang="ts">
import { h, nextTick, onMounted, ref, watch } from 'vue'
import type { DataTableColumns, FormInst } from 'naive-ui'
@ -771,341 +1106,6 @@ onMounted(() => {
})
</script>
<template>
<div class="role-management p-1 bg-gray-50 h-screen flex flex-col">
<!-- 搜索表单和角色表格 -->
<div class="bg-white rounded-lg shadow-sm border border-gray-100 flex-1 flex flex-col overflow-hidden">
<!-- 搜索表单 -->
<div class="px-4 py-2 border-b border-gray-100">
<n-form
ref="searchFormRef"
:model="searchForm"
label-placement="left"
label-width="auto"
class="search-form"
>
<n-grid :cols="6" :x-gap="8" :y-gap="4">
<n-grid-item>
<n-form-item label="角色名称" path="roleName">
<n-input
v-model:value="searchForm.roleName"
placeholder="请输入角色名称"
clearable
@keydown.enter="handleSearch"
/>
</n-form-item>
</n-grid-item>
<n-grid-item>
<n-form-item label="角色编码" path="roleCode">
<n-input
v-model:value="searchForm.roleCode"
placeholder="请输入角色编码"
clearable
@keydown.enter="handleSearch"
/>
</n-form-item>
</n-grid-item>
<n-grid-item>
<n-form-item label="创建时间" path="timeRange">
<n-date-picker
v-model:value="searchForm.timeRange"
type="datetimerange"
clearable
placeholder="选择时间范围"
:shortcuts="{
今天: () => [new Date().setHours(0, 0, 0, 0), new Date().setHours(23, 59, 59, 999)],
昨天: () => {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
return [yesterday.setHours(0, 0, 0, 0), yesterday.setHours(23, 59, 59, 999)];
},
最近7天: () => [Date.now() - 7 * 24 * 60 * 60 * 1000, Date.now()],
最近30天: () => [Date.now() - 30 * 24 * 60 * 60 * 1000, Date.now()],
}"
/>
</n-form-item>
</n-grid-item>
<n-grid-item>
<n-form-item label="角色状态" path="roleStatus">
<n-select
v-model:value="searchForm.roleStatus"
placeholder="请选择角色状态"
clearable
:options="[
{ label: '启用', value: '0' },
{ label: '停用', value: '1' },
]"
/>
</n-form-item>
</n-grid-item>
<n-grid-item>
<n-form-item label="" path="">
<NSpace>
<NButton type="primary" @click="handleSearch">
<template #icon>
<NIcon><icon-park-outline:search /></NIcon>
</template>
搜索
</NButton>
<NButton @click="handleReset">
<template #icon>
<NIcon><icon-park-outline:refresh /></NIcon>
</template>
重置
</NButton>
</NSpace>
</n-form-item>
</n-grid-item>
</n-grid>
</n-form>
</div>
<!-- 表格头部操作栏 -->
<div class="flex items-center justify-between px-4 py-2 border-b border-gray-100">
<div class="flex items-center gap-4">
<NButton type="primary" class="px-6 flex items-center" @click="handleAdd">
<template #icon>
<NIcon class="mr-1" style="transform: translateY(-1px)">
<icon-park-outline:plus />
</NIcon>
</template>
新增
</NButton>
<NButton
type="error"
:disabled="selectedRows.length === 0"
class="px-6 flex items-center"
@click="handleBatchDelete"
>
<template #icon>
<NIcon class="mr-1" style="transform: translateY(-1px)">
<icon-park-outline:delete />
</NIcon>
</template>
删除
</NButton>
</div>
<div class="flex items-center gap-4 text-sm text-gray-500">
<span> {{ pagination.itemCount }} </span>
<NButton text @click="getRoleList">
<template #icon>
<NIcon><icon-park-outline:refresh /></NIcon>
</template>
</NButton>
</div>
</div>
<!-- 表格内容 -->
<div class="table-wrapper flex-1 overflow-auto pt-4">
<n-data-table
:columns="columns"
:data="tableData"
:loading="loading"
:row-key="(row: RoleVo) => row.roleId"
:bordered="false"
:single-line="false"
size="large"
class="custom-table"
@update:checked-row-keys="handleRowSelectionChange"
/>
</div>
<!-- 分页器 -->
<div class="flex items-center px-4 py-2 border-t border-gray-100">
<div class="text-sm text-gray-500 mr-4">
{{ pagination.itemCount }}
</div>
<n-pagination
v-model:page="pagination.page"
v-model:page-size="pagination.pageSize"
:item-count="pagination.itemCount"
:show-size-picker="true"
:page-sizes="[10, 20, 50, 100]"
show-quick-jumper
@update:page="handlePageChange"
@update:page-size="handlePageSizeChange"
/>
</div>
</div>
<!-- 角色表单弹框 -->
<NovaDialog
ref="roleDialogRef"
:title="modalTitle"
:width="700"
height="auto"
confirm-text="确定"
cancel-text="取消"
@nova-confirm="handleSubmit"
@nova-cancel="handleCancel"
>
<template #content>
<div class="px-3 py-2">
<n-form
ref="formRef"
:model="formData"
:rules="rules"
label-placement="left"
label-width="90px"
require-mark-placement="right-hanging"
>
<!-- 第一行角色名称 角色编码 -->
<n-grid :cols="2" :x-gap="12">
<n-grid-item>
<n-form-item label="角色名称" path="roleName">
<n-input
v-model:value="formData.roleName"
placeholder="请输入角色名称"
/>
</n-form-item>
</n-grid-item>
<n-grid-item>
<n-form-item label="角色编码" path="roleCode">
<n-input
v-model:value="formData.roleCode"
placeholder="请输入角色编码"
:disabled="isEdit"
/>
</n-form-item>
</n-grid-item>
</n-grid>
<!-- 第二行角色状态 显示排序 -->
<n-grid :cols="2" :x-gap="12">
<n-grid-item>
<n-form-item label="角色状态" path="roleStatus">
<n-select
v-model:value="formData.roleStatus"
placeholder="请选择角色状态"
:options="[
{ label: '启用', value: '0' },
{ label: '停用', value: '1' },
]"
/>
</n-form-item>
</n-grid-item>
<n-grid-item>
<n-form-item label="显示排序" path="sorted">
<n-input-number
v-model:value="formData.sorted"
placeholder="请输入排序号"
:min="1"
:max="999"
style="width: 100%"
/>
</n-form-item>
</n-grid-item>
</n-grid>
<!-- 第三行角色描述占满整行 -->
<n-form-item label="角色描述" path="remark">
<n-input
v-model:value="formData.remark"
type="textarea"
placeholder="请输入角色描述"
:rows="3"
/>
</n-form-item>
</n-form>
</div>
</template>
</NovaDialog>
<!-- 菜单权限分配弹框 -->
<NovaDialog
ref="menuDialogRef"
title="分配菜单权限"
:width="500"
height="auto"
confirm-text="确定"
cancel-text="取消"
@nova-confirm="handleConfirmAssignMenu"
@nova-cancel="handleCancelAssignMenu"
>
<template #content>
<div class="p-3">
<div v-if="menuLoading" class="flex items-center justify-center py-8">
<div class="text-center">
<n-spin size="medium" />
<p class="mt-2 text-sm text-gray-500">
正在加载菜单数据...
</p>
</div>
</div>
<div v-else-if="menuData.length > 0">
<!-- 功能按钮区域 -->
<div class="flex items-center gap-2 mb-3 pb-3 border-b border-gray-200">
<NButton
size="small"
:type="allExpanded ? 'default' : 'primary'"
@click="toggleExpandAll"
>
{{ allExpanded ? '折叠' : '展开' }}
</NButton>
<NButton
size="small"
:type="allSelected ? 'default' : 'primary'"
@click="toggleSelectAll"
>
{{ allSelected ? '全不选' : '全选' }}
</NButton>
<NButton
size="small"
:type="cascadeEnabled ? 'primary' : 'default'"
@click="toggleCascade"
>
父子联动
</NButton>
<NButton
size="small"
:type="showPermissionCode ? 'primary' : 'default'"
@click="togglePermissionCode"
>
权限标识
</NButton>
</div>
<NTree
v-model:checked-keys="checkedKeys"
v-model:expanded-keys="expandedKeys"
:data="menuData"
checkable
:cascade="cascadeEnabled"
check-strategy="all"
expand-on-click
:render-suffix="showPermissionCode ? renderPermissionCode : undefined"
class="clean-menu-tree"
/>
</div>
<div v-else class="flex items-center justify-center py-8">
<div class="text-center">
<div class="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-2">
<NIcon size="16" class="text-gray-400">
<icon-park-outline:folder-close />
</NIcon>
</div>
<p class="text-sm text-gray-500">
暂无可分配的菜单
</p>
</div>
</div>
</div>
</template>
</NovaDialog>
</div>
</template>
<style scoped>
.role-management {
height: 100vh;

File diff suppressed because it is too large Load Diff