修复bug
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 11m14s

This commit is contained in:
sexygoat
2026-04-08 10:19:25 +08:00
parent ff087abd44
commit b510b4539f
55 changed files with 2274 additions and 2612 deletions

View File

@@ -0,0 +1,591 @@
<template>
<ArtTableFullScreen>
<div class="carrier-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
label-width="85"
:show-expand="false"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton @click="showDialog('add')" v-permission="'carrier:add'">新增运营商</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="carrierList"
:currentPage="pagination.page"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增运营商' : '编辑运营商'"
width="30%"
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
<ElFormItem label="运营商编码" prop="carrier_code">
<div style="display: flex; gap: 8px">
<ElInput
v-model="form.carrier_code"
placeholder="请输入运营商编码或点击生成"
:disabled="dialogType === 'edit'"
clearable
style="flex: 1"
/>
<CodeGeneratorButton
v-if="dialogType === 'add'"
code-type="carrier"
@generated="handleCodeGenerated"
/>
</div>
</ElFormItem>
<ElFormItem label="运营商名称" prop="carrier_name">
<ElInput v-model="form.carrier_name" placeholder="请输入运营商名称" />
</ElFormItem>
<ElFormItem label="运营商类型" prop="carrier_type">
<ElSelect
v-model="form.carrier_type"
placeholder="请选择运营商类型"
style="width: 100%"
:disabled="dialogType === 'edit'"
>
<ElOption
v-for="item in CARRIER_TYPE_OPTIONS"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="运营商描述" prop="description">
<ElInput
v-model="form.description"
type="textarea"
:rows="3"
placeholder="请输入运营商描述"
/>
</ElFormItem>
<ElFormItem label="流量重置日" prop="data_reset_day">
<ElInputNumber
v-model="form.data_reset_day"
:min="1"
:max="28"
:step="1"
controls-position="right"
placeholder="请输入流量重置日"
style="width: 100%"
/>
<div style="margin-top: 5px; font-size: 12px; color: #909399">
运营商每月清零网关计数器的日期1-28默认为1日
</div>
</ElFormItem>
<ElFormItem label="实名链接类型" prop="realname_link_type">
<ElSelect
v-model="form.realname_link_type"
placeholder="请选择实名链接类型"
style="width: 100%"
>
<ElOption label="无需实名" value="none" />
<ElOption label="模板实名" value="template" />
<ElOption label="网关实名" value="gateway" />
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="form.realname_link_type === 'template'"
label="实名链接模板"
prop="realname_link_template"
>
<ElInput
v-model="form.realname_link_template"
type="textarea"
:rows="2"
placeholder="请输入实名链接模板"
/>
<div style="margin-top: 5px; font-size: 12px; color: #909399">
支持变量{iccid}{msisdn}
</div>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)" :loading="submitLoading">
提交
</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h, watch, nextTick } from 'vue'
import { CarrierService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElTag, ElSwitch, ElButton } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type { Carrier, CarrierType } from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import CodeGeneratorButton from '@/components/business/CodeGeneratorButton.vue'
import { formatDateTime } from '@/utils/business/format'
import {
CommonStatus,
getStatusText,
CARRIER_TYPE_OPTIONS,
getCarrierTypeLabel,
getCarrierTypeColor
} from '@/config/constants'
defineOptions({ name: 'CarrierManagement' })
const { hasAuth } = useAuth()
const dialogVisible = ref(false)
const loading = ref(false)
const submitLoading = ref(false)
const tableRef = ref()
// 搜索表单初始值
const initialSearchState = {
carrier_name: '',
carrier_type: null,
status: null
}
// 搜索表单
const searchForm = reactive({ ...initialSearchState })
// 搜索表单配置
const searchFormItems: SearchFormItem[] = [
{
label: '运营商名称',
prop: 'carrier_name',
type: 'input',
config: {
clearable: true,
placeholder: '请输入运营商名称'
}
},
{
label: '运营商类型',
prop: 'carrier_type',
type: 'select',
config: {
clearable: true,
placeholder: '请选择运营商类型'
},
options: () => [
{ label: '中国移动', value: 'CMCC' },
{ label: '中国联通', value: 'CUCC' },
{ label: '中国电信', value: 'CTCC' },
{ label: '中国广电', value: 'CBN' }
]
},
{
label: '状态',
prop: 'status',
type: 'select',
config: {
clearable: true,
placeholder: '请选择状态'
},
options: () => [
{ label: '启用', value: CommonStatus.ENABLED },
{ label: '禁用', value: CommonStatus.DISABLED }
]
}
]
// 分页
const pagination = reactive({
page: 1,
pageSize: 20,
total: 0
})
// 列配置
const columnOptions = [
{ label: '运营商编码', prop: 'carrier_code' },
{ label: '运营商名称', prop: 'carrier_name' },
{ label: '运营商类型', prop: 'carrier_type' },
{ label: '运营商描述', prop: 'description' },
{ label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'created_at' }
]
const formRef = ref<FormInstance>()
const rules = reactive<FormRules>({
carrier_code: [
{ required: true, message: '请输入运营商编码', trigger: 'blur' },
{ min: 1, max: 50, message: '长度在 1 到 50 个字符', trigger: 'blur' }
],
carrier_name: [
{ required: true, message: '请输入运营商名称', trigger: 'blur' },
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' }
],
carrier_type: [{ required: true, message: '请选择运营商类型', trigger: 'change' }],
description: [{ max: 500, message: '描述不能超过500个字符', trigger: 'blur' }],
data_reset_day: [
{
validator: (rule: any, value: any, callback: any) => {
if (value === null || value === undefined || value === '') {
callback() // 允许为空
} else {
const numValue = Number(value)
if (isNaN(numValue)) {
callback(new Error('请输入有效的数字'))
} else if (!Number.isInteger(numValue)) {
callback(new Error('请输入整数'))
} else if (numValue < 1 || numValue > 28) {
callback(new Error('流量重置日必须在1-28之间'))
} else {
callback()
}
}
},
trigger: 'blur'
}
],
realname_link_type: [{ required: true, message: '请选择实名链接类型', trigger: 'change' }],
realname_link_template: [
{
required: true,
validator: (rule: any, value: any, callback: any) => {
if (form.realname_link_type === 'template' && !value) {
callback(new Error('请输入实名链接模板'))
} else {
callback()
}
},
trigger: 'blur'
}
]
})
const form = reactive<any>({
id: 0,
carrier_code: '',
carrier_name: '',
carrier_type: null,
description: '',
data_reset_day: 1,
realname_link_type: 'none',
realname_link_template: ''
})
const carrierList = ref<Carrier[]>([])
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'carrier_code',
label: '运营商编码',
minWidth: 160
},
{
prop: 'carrier_name',
label: '运营商名称',
minWidth: 150
},
{
prop: 'carrier_type',
label: '运营商类型',
width: 120,
formatter: (row: any) => {
const color = getCarrierTypeColor(row.carrier_type)
return h(
ElTag,
{ style: `background-color: ${color}; border-color: ${color}; color: #fff;` },
() => getCarrierTypeLabel(row.carrier_type)
)
}
},
{
prop: 'description',
label: '运营商描述',
minWidth: 200,
showOverflowTooltip: true
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: any) => {
return h(ElSwitch, {
modelValue: row.status,
activeValue: CommonStatus.ENABLED,
inactiveValue: CommonStatus.DISABLED,
activeText: getStatusText(CommonStatus.ENABLED),
inactiveText: getStatusText(CommonStatus.DISABLED),
inlinePrompt: true,
disabled: !hasAuth('carrier:update_status'),
'onUpdate:modelValue': (val: string | number | boolean) =>
handleStatusChange(row, val as number)
})
}
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: any) => formatDateTime(row.created_at)
},
{
prop: 'actions',
label: '操作',
width: 160,
fixed: 'right',
formatter: (row: any) => {
const buttons: any[] = []
if (hasAuth('carrier:edit')) {
buttons.push(
h(
ElButton,
{
type: 'primary',
link: true,
onClick: () => showDialog('edit', row)
},
() => '编辑'
)
)
}
if (hasAuth('carrier:delete')) {
buttons.push(
h(
ElButton,
{
type: 'danger',
link: true,
onClick: () => deleteCarrier(row)
},
() => '删除'
)
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
}
])
onMounted(() => {
getTableData()
})
// 监听对话框关闭,清除验证状态
watch(dialogVisible, (val) => {
if (!val) {
nextTick(() => {
formRef.value?.clearValidate()
})
}
})
// 获取运营商列表
const getTableData = async () => {
loading.value = true
try {
const params = {
page: pagination.page,
page_size: pagination.pageSize,
carrier_name: searchForm.carrier_name || undefined,
carrier_type: searchForm.carrier_type || undefined,
status: searchForm.status !== null ? searchForm.status : undefined
}
const res = await CarrierService.getCarriers(params)
if (res.code === 0) {
carrierList.value = res.data.items || []
pagination.total = res.data.total || 0
}
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
// 重置搜索
const handleReset = () => {
Object.assign(searchForm, { ...initialSearchState })
pagination.page = 1
getTableData()
}
// 搜索
const handleSearch = () => {
pagination.page = 1
getTableData()
}
// 刷新表格
const handleRefresh = () => {
getTableData()
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getTableData()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.page = newCurrentPage
getTableData()
}
const dialogType = ref('add')
// 显示新增/编辑对话框
const showDialog = (type: string, row?: any) => {
dialogVisible.value = true
dialogType.value = type
if (type === 'edit' && row) {
form.id = row.id
form.carrier_code = row.carrier_code
form.carrier_name = row.carrier_name
form.carrier_type = row.carrier_type
form.description = row.description
form.data_reset_day = row.data_reset_day || 1
form.realname_link_type = row.realname_link_type || 'none'
form.realname_link_template = row.realname_link_template || ''
} else {
form.id = 0
form.carrier_code = ''
form.carrier_name = ''
form.carrier_type = null
form.description = ''
form.data_reset_day = 1
form.realname_link_type = 'none'
form.realname_link_template = ''
}
// 清除表单验证状态
nextTick(() => {
formRef.value?.clearValidate()
})
}
// 处理编码生成
const handleCodeGenerated = (code: string) => {
form.carrier_code = code
// 清除该字段的验证错误提示
nextTick(() => {
formRef.value?.clearValidate('carrier_code')
})
}
// 删除运营商
const deleteCarrier = (row: any) => {
ElMessageBox.confirm(`确定删除运营商 ${row.carrier_name} 吗?`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
})
.then(async () => {
try {
await CarrierService.deleteCarrier(row.id)
ElMessage.success('删除成功')
await getTableData()
} catch (error) {
console.error(error)
}
})
.catch(() => {
// 用户取消删除
})
}
// 提交表单
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (valid) {
submitLoading.value = true
try {
const data = {
carrier_code: form.carrier_code,
carrier_name: form.carrier_name,
carrier_type: form.carrier_type as CarrierType,
description: form.description || undefined,
data_reset_day: form.data_reset_day || null
}
if (dialogType.value === 'add') {
await CarrierService.createCarrier(data)
ElMessage.success('新增成功')
} else {
const updateData = {
carrier_name: form.carrier_name,
description: form.description || undefined,
data_reset_day: form.data_reset_day || null
}
await CarrierService.updateCarrier(form.id, updateData)
ElMessage.success('修改成功')
}
dialogVisible.value = false
formEl.resetFields()
await getTableData()
} catch (error) {
console.error(error)
} finally {
submitLoading.value = false
}
}
})
}
// 状态切换
const handleStatusChange = async (row: any, newStatus: number) => {
const oldStatus = row.status
// 先更新UI
row.status = newStatus
try {
await CarrierService.updateCarrierStatus(row.id, newStatus)
ElMessage.success('状态切换成功')
} catch (error) {
// 切换失败,恢复原状态
row.status = oldStatus
console.error(error)
}
}
</script>
<style scoped lang="scss">
.carrier-page {
height: 100%;
}
</style>

View File

@@ -1,661 +0,0 @@
<template>
<ArtTableFullScreen>
<div class="menu-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
:showExpand="false"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
:showZebra="false"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<!-- 按钮权限后端控制模式使用自定义指令 -->
<ElButton v-auth="'add'" @click="showModel('menu', null, true)" v-ripple>
添加菜单
</ElButton>
<ElButton @click="toggleExpand" v-ripple>
{{ isExpanded ? '收起' : '展开' }}
</ElButton>
<!-- 按钮权限前端控制模式使用 hasAuth 方法 -->
<!-- <ElButton v-if="hasAuth('B_CODE1')" @click="showModel('menu', null, true)" v-ripple>
添加菜单
</ElButton> -->
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
rowKey="path"
ref="tableRef"
:loading="loading"
:data="filteredTableData"
:marginTop="10"
:stripe="false"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<ElDialog :title="dialogTitle" v-model="dialogVisible" width="700px" align-center>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="85px">
<ElFormItem label="菜单类型">
<ElRadioGroup v-model="labelPosition" :disabled="disableMenuType">
<ElRadioButton value="menu" label="menu">菜单</ElRadioButton>
<ElRadioButton value="button" label="button">权限</ElRadioButton>
</ElRadioGroup>
</ElFormItem>
<template v-if="labelPosition === 'menu'">
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="菜单名称" prop="name">
<ElInput v-model="form.name" placeholder="菜单名称"></ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="路由地址" prop="path">
<ElInput v-model="form.path" placeholder="路由地址"></ElInput>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="权限标识" prop="label">
<ElInput v-model="form.label" placeholder="权限标识"></ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="图标" prop="icon">
<ArtIconSelector :iconType="iconType" :defaultIcon="form.icon" width="229px" />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="菜单排序" prop="sort" style="width: 100%">
<ElInputNumber
v-model="form.sort"
style="width: 100%"
@change="handleChange"
:min="1"
controls-position="right"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="外部链接" prop="link">
<ElInput
v-model="form.link"
placeholder="外部链接/内嵌地址(https://www.baidu.com)"
></ElInput>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="5">
<ElFormItem label="是否启用" prop="isEnable">
<ElSwitch v-model="form.isEnable"></ElSwitch>
</ElFormItem>
</ElCol>
<ElCol :span="5">
<ElFormItem label="页面缓存" prop="keepAlive">
<ElSwitch v-model="form.keepAlive"></ElSwitch>
</ElFormItem>
</ElCol>
<ElCol :span="5">
<ElFormItem label="是否显示" prop="isHidden">
<ElSwitch v-model="form.isHidden"></ElSwitch>
</ElFormItem>
</ElCol>
<ElCol :span="5">
<ElFormItem label="是否内嵌" prop="isMenu">
<ElSwitch v-model="form.isIframe"></ElSwitch>
</ElFormItem>
</ElCol>
</ElRow>
</template>
<template v-if="labelPosition === 'button'">
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="权限名称" prop="authName">
<ElInput v-model="form.authName" placeholder="权限名称"></ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="权限标识" prop="authLabel">
<ElInput v-model="form.authLabel" placeholder="权限标识"></ElInput>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="权限排序" prop="authSort" style="width: 100%">
<ElInputNumber
v-model="form.authSort"
style="width: 100%"
@change="handleChange"
:min="1"
controls-position="right"
/>
</ElFormItem>
</ElCol>
</ElRow>
</template>
</ElForm>
<template #footer>
<span class="dialog-footer">
<ElButton @click="dialogVisible = false"> </ElButton>
<ElButton type="primary" @click="submitForm()"> </ElButton>
</span>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { useMenuStore } from '@/store/modules/menu'
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { IconTypeEnum } from '@/enums/appEnum'
import { formatMenuTitle } from '@/router/utils/utils'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { ElPopover, ElButton } from 'element-plus'
import { AppRouteRecord } from '@/types/router'
import { useAuth } from '@/composables/useAuth'
import { SearchFormItem } from '@/types'
defineOptions({ name: 'Menus' })
const { hasAuth } = useAuth()
const { menuList } = storeToRefs(useMenuStore())
const loading = ref(false)
// 定义表单搜索初始值
const initialSearchState = {
name: '',
route: ''
}
// 响应式表单数据
const formFilters = reactive({ ...initialSearchState })
// 增加实际应用的搜索条件状态
const appliedFilters = reactive({ ...initialSearchState })
// 重置表单
const handleReset = () => {
Object.assign(formFilters, { ...initialSearchState })
Object.assign(appliedFilters, { ...initialSearchState })
getTableData()
}
// 搜索处理
const handleSearch = () => {
// 将当前输入的筛选条件应用到实际搜索
Object.assign(appliedFilters, { ...formFilters })
getTableData()
}
// 表单配置项
const formItems: SearchFormItem[] = [
{
label: '菜单名称',
prop: 'name',
type: 'input',
config: {
clearable: true
}
},
{
label: '路由地址',
prop: 'route',
type: 'input',
config: {
clearable: true
}
}
]
// 列配置
const columnOptions = [
{ label: '勾选', type: 'selection' },
{ label: '用户名', prop: 'avatar' },
{ label: '手机号', prop: 'mobile' },
{ label: '性别', prop: 'sex' },
{ label: '部门', prop: 'dep' },
{ label: '状态', prop: 'status' },
{ label: '创建日期', prop: 'create_time' },
{ label: '操作', prop: 'operation' }
]
// 构建菜单类型标签
const buildMenuTypeTag = (row: AppRouteRecord) => {
if (row.children && row.children.length > 0) {
return 'info'
} else if (row.meta?.link && row.meta?.isIframe) {
return 'success'
} else if (row.path) {
return 'primary'
} else if (row.meta?.link) {
return 'warning'
}
}
// 构建菜单类型文本
const buildMenuTypeText = (row: AppRouteRecord) => {
if (row.children && row.children.length > 0) {
return '目录'
} else if (row.meta?.link && row.meta?.isIframe) {
return '内嵌'
} else if (row.path) {
return '菜单'
} else if (row.meta?.link) {
return '外链'
}
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'meta.title',
label: '菜单名称',
minWidth: 120,
formatter: (row: AppRouteRecord) => {
return formatMenuTitle(row.meta?.title)
}
},
{
prop: 'type',
label: '菜单类型',
formatter: (row: AppRouteRecord) => {
return h(ElTag, { type: buildMenuTypeTag(row) }, () => buildMenuTypeText(row))
}
},
{
prop: 'path',
label: '路由',
formatter: (row: AppRouteRecord) => {
return row.meta?.link || row.path || ''
}
},
{
prop: 'meta.authList',
label: '可操作权限',
formatter: (row: AppRouteRecord) => {
return h(
'div',
{},
row.meta.authList?.map((item: { title: string; auth_mark: string }, index: number) => {
return h(
ElPopover,
{
placement: 'top-start',
title: '操作',
width: 200,
trigger: 'click',
key: index
},
{
default: () =>
h('div', { style: 'margin: 0; text-align: right' }, [
h(
ElButton,
{
size: 'small',
type: 'primary',
onClick: () => showModel('button', item)
},
{ default: () => '编辑' }
),
h(
ElButton,
{
size: 'small',
type: 'danger',
onClick: () => deleteAuth()
},
{ default: () => '删除' }
)
]),
reference: () => h(ElButton, { class: 'small-btn' }, { default: () => item.title })
}
)
})
)
}
},
{
prop: 'date',
label: '编辑时间',
formatter: () => '2022-3-12 12:00:00'
},
{
prop: 'status',
label: '隐藏菜单',
formatter: (row) => {
return h(ElTag, { type: row.meta.isHide ? 'danger' : 'info' }, () =>
row.meta.isHide ? '是' : '否'
)
}
},
{
prop: 'operation',
label: '操作',
width: 180,
formatter: (row: AppRouteRecord) => {
return h('div', [
hasAuth('B_CODE1') &&
h(ArtButtonTable, {
type: 'add',
onClick: () => showModel('menu')
}),
hasAuth('B_CODE2') &&
h(ArtButtonTable, {
type: 'edit',
onClick: () => showDialog('edit', row)
}),
hasAuth('B_CODE3') &&
h(ArtButtonTable, {
type: 'delete',
onClick: () => deleteMenu()
})
])
}
}
])
const handleRefresh = () => {
getTableData()
}
const dialogVisible = ref(false)
const form = reactive({
// 菜单
name: '',
path: '',
label: '',
icon: '',
isEnable: true,
sort: 1,
isMenu: true,
keepAlive: true,
isHidden: true,
link: '',
isIframe: false,
// 权限 (修改这部分)
authName: '',
authLabel: '',
authIcon: '',
authSort: 1
})
const iconType = ref(IconTypeEnum.UNICODE)
const labelPosition = ref('menu')
const rules = reactive<FormRules>({
name: [
{ required: true, message: '请输入菜单名称', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
],
path: [{ required: true, message: '请输入路由地址', trigger: 'blur' }],
label: [{ required: true, message: '输入权限标识', trigger: 'blur' }],
// 修改这部分
authName: [{ required: true, message: '请输入权限名称', trigger: 'blur' }],
authLabel: [{ required: true, message: '请输入权限权限标识', trigger: 'blur' }]
})
const tableData = ref<AppRouteRecord[]>([])
onMounted(() => {
getTableData()
})
const getTableData = () => {
loading.value = true
setTimeout(() => {
tableData.value = menuList.value
loading.value = false
}, 500)
}
// 过滤后的表格数据
const filteredTableData = computed(() => {
// 递归搜索函数
const searchMenu = (items: AppRouteRecord[]): AppRouteRecord[] => {
return items.filter((item) => {
// 获取搜索关键词,转换为小写并去除首尾空格
const searchName = appliedFilters.name?.toLowerCase().trim() || ''
const searchRoute = appliedFilters.route?.toLowerCase().trim() || ''
// 获取菜单标题和路径,确保它们存在
const menuTitle = formatMenuTitle(item.meta?.title || '').toLowerCase()
const menuPath = (item.path || '').toLowerCase()
// 使用 includes 进行模糊匹配
const nameMatch = !searchName || menuTitle.includes(searchName)
const routeMatch = !searchRoute || menuPath.includes(searchRoute)
// 如果有子菜单,递归搜索
if (item.children && item.children.length > 0) {
const matchedChildren = searchMenu(item.children)
// 如果子菜单有匹配项,保留当前菜单
if (matchedChildren.length > 0) {
item.children = matchedChildren
return true
}
}
return nameMatch && routeMatch
})
}
return searchMenu(tableData.value)
})
const isEdit = ref(false)
const formRef = ref<FormInstance>()
const dialogTitle = computed(() => {
const type = labelPosition.value === 'menu' ? '菜单' : '权限'
return isEdit.value ? `编辑${type}` : `新建${type}`
})
const showDialog = (type: string, row: AppRouteRecord) => {
showModel('menu', row, true)
}
const handleChange = () => {}
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
try {
// const menuStore = useMenuStore()
// const params =
// labelPosition.value === 'menu'
// ? {
// title: form.name,
// path: form.path,
// name: form.label,
// icon: form.icon,
// sort: form.sort,
// isEnable: form.isEnable,
// isMenu: form.isMenu,
// keepAlive: form.keepAlive,
// isHidden: form.isHidden,
// link: form.link
// }
// : {
// title: form.authName,
// name: form.authLabel,
// icon: form.authIcon,
// sort: form.authSort
// }
if (isEdit.value) {
// await menuStore.updateMenu(params)
} else {
// await menuStore.addMenu(params)
}
ElMessage.success(`${isEdit.value ? '编辑' : '新增'}成功`)
dialogVisible.value = false
} catch (error) {
console.error(error)
}
}
})
}
const showModel = (type: string, row?: any, lock: boolean = false) => {
dialogVisible.value = true
labelPosition.value = type
isEdit.value = false
lockMenuType.value = lock
resetForm()
if (row) {
isEdit.value = true
nextTick(() => {
// 回显数据
if (type === 'menu') {
// 菜单数据回显
form.name = formatMenuTitle(row.meta.title)
form.path = row.path
form.label = row.name
form.icon = row.meta.icon
form.sort = row.meta.sort || 1
form.isMenu = row.meta.isMenu
form.keepAlive = row.meta.keepAlive
form.isHidden = row.meta.isHidden || true
form.isEnable = row.meta.isEnable || true
form.link = row.meta.link
form.isIframe = row.meta.isIframe || false
} else {
// 权限按钮数据回显
form.authName = row.title
form.authLabel = row.auth_mark
form.authIcon = row.icon || ''
form.authSort = row.sort || 1
}
})
}
}
const resetForm = () => {
formRef.value?.resetFields()
Object.assign(form, {
// 菜单
name: '',
path: '',
label: '',
icon: '',
sort: 1,
isMenu: true,
keepAlive: true,
isHidden: true,
link: '',
isIframe: false,
// 权限
authName: '',
authLabel: '',
authIcon: '',
authSort: 1
})
}
const deleteMenu = async () => {
try {
await ElMessageBox.confirm('确定要删除该菜单吗?删除后无法恢复', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
ElMessage.success('删除成功')
} catch (error) {
if (error !== 'cancel') {
console.error(error)
}
}
}
const deleteAuth = async () => {
try {
await ElMessageBox.confirm('确定要删除该权限吗?删除后无法恢复', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
ElMessage.success('删除成功')
} catch (error) {
if (error !== 'cancel') {
console.error(error)
}
}
}
// 修改计算属性,增加锁定控制参数
const disableMenuType = computed(() => {
// 编辑权限时锁定为权限类型
if (isEdit.value && labelPosition.value === 'button') return true
// 编辑菜单时锁定为菜单类型
if (isEdit.value && labelPosition.value === 'menu') return true
// 顶部添加菜单按钮时锁定为菜单类型
if (!isEdit.value && labelPosition.value === 'menu' && lockMenuType.value) return true
return false
})
// 添加一个控制变量
const lockMenuType = ref(false)
const isExpanded = ref(false)
const tableRef = ref()
const toggleExpand = () => {
isExpanded.value = !isExpanded.value
nextTick(() => {
if (tableRef.value) {
tableRef.value[isExpanded.value ? 'expandAll' : 'collapseAll']()
}
})
}
</script>
<style lang="scss" scoped>
.menu-page {
.svg-icon {
width: 1.8em;
height: 1.8em;
overflow: hidden;
vertical-align: -8px;
fill: currentcolor;
}
:deep(.small-btn) {
height: 30px !important;
padding: 0 10px !important;
font-size: 12px !important;
}
}
</style>

View File

@@ -1,5 +0,0 @@
<template>
<div class="page-content">
<h1>菜单-1</h1>
</div>
</template>

View File

@@ -1,5 +0,0 @@
<template>
<div class="page-content">
<h1>菜单-2-1</h1>
</div>
</template>

View File

@@ -1,5 +0,0 @@
<template>
<div class="page-content">
<h1>菜单-3-1</h1>
</div>
</template>

View File

@@ -1,5 +0,0 @@
<template>
<div class="page-content">
<h1>菜单-3-2-1</h1>
</div>
</template>

View File

@@ -31,27 +31,12 @@
:default-expand-all="false"
:marginTop="10"
:show-pagination="false"
:row-class-name="getRowClassName"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
@@ -134,6 +119,7 @@
ElTag,
ElRadio,
ElRadioGroup,
ElButton,
type FormInstance,
type FormRules
} from 'element-plus'
@@ -142,11 +128,6 @@
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import {
PermissionType,
PERMISSION_TYPE_OPTIONS,
@@ -222,19 +203,8 @@
const tableRef = ref()
const dialogVisible = ref(false)
const dialogType = ref('add')
const currentRow = ref<PermissionTreeNode | null>(null)
const currentPermissionId = ref<number>(0)
const submitLoading = ref(false)
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
// 表单引用和数据
const formRef = ref<FormInstance>()
@@ -342,6 +312,45 @@
prop: 'sort',
label: '排序',
width: 80
},
{
prop: 'actions',
label: '操作',
width: 160,
fixed: 'right',
formatter: (row: PermissionTreeNode) => {
const buttons: any[] = []
if (hasAuth('permission:edit')) {
buttons.push(
h(
ElButton,
{
type: 'primary',
link: true,
onClick: () => showDialog('edit', row)
},
() => '编辑'
)
)
}
if (hasAuth('permission:delete')) {
buttons.push(
h(
ElButton,
{
type: 'danger',
link: true,
onClick: () => deletePermission(row)
},
() => '删除'
)
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
}
])
@@ -444,7 +453,6 @@
}
if (type === 'edit' && row) {
currentRow.value = row
currentPermissionId.value = row.id
// 从API获取完整的权限数据包含parent_id
try {
@@ -555,47 +563,6 @@
onMounted(() => {
getPermissionList()
})
// 右键菜单项配置
const contextMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
if (hasAuth('permission:edit')) {
items.push({ key: 'edit', label: '编辑' })
}
if (hasAuth('permission:delete')) {
items.push({ key: 'delete', label: '删除' })
}
return items
})
// 处理表格行右键菜单
const handleRowContextMenu = (row: PermissionTreeNode, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'edit':
showDialog('edit', currentRow.value)
break
case 'delete':
deletePermission(currentRow.value)
break
}
}
</script>
<style scoped lang="scss">
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style>
<style scoped lang="scss"></style>

View File

@@ -32,29 +32,14 @@
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
:row-class-name="getRowClassName"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
@@ -80,8 +65,12 @@
style="width: 100%"
:disabled="dialogType === 'edit'"
>
<ElOption label="平台角色" :value="1" />
<ElOption label="客户角色" :value="2" />
<ElOption
v-for="item in ROLE_TYPE_OPTIONS"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="状态">
@@ -240,13 +229,13 @@
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format'
import { CommonStatus, getStatusText } from '@/config/constants'
import {
CommonStatus,
getStatusText,
ROLE_TYPE_OPTIONS,
getRoleTypeLabel
} from '@/config/constants'
defineOptions({ name: 'Role' })
@@ -272,17 +261,6 @@
const leftTreeFilter = ref('') // 左侧树搜索关键字
const rightTreeFilter = ref('') // 右侧树搜索关键字
const isHandlingCheck = ref(false) // 标志位:是否正在处理勾选事件
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<PlatformRole | null>(null)
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
// 搜索表单初始值
const initialSearchState = {
@@ -313,10 +291,7 @@
clearable: true,
placeholder: '请选择角色类型'
},
options: () => [
{ label: '平台角色', value: 1 },
{ label: '客户角色', value: 2 }
]
options: () => ROLE_TYPE_OPTIONS
},
{
label: '状态',
@@ -342,7 +317,6 @@
// 列配置
const columnOptions = [
{ label: 'ID', prop: 'ID' },
{ label: '角色名称', prop: 'role_name' },
{ label: '角色描述', prop: 'role_desc' },
{ label: '角色类型', prop: 'role_type' },
@@ -373,34 +347,25 @@
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'ID',
label: 'ID',
width: 80
},
{
prop: 'role_name',
label: '角色名称',
minWidth: 120
},
{
prop: 'role_desc',
label: '角色描述'
width: 180
},
{
prop: 'role_type',
label: '角色类型',
minWidth: 120,
width: 100,
formatter: (row: any) => {
return h(ElTag, { type: row.role_type === 1 ? 'primary' : 'success' }, () =>
row.role_type === 1 ? '平台角色' : '客户角色'
getRoleTypeLabel(row.role_type)
)
}
},
{
prop: 'status',
label: '状态',
minWidth: 100,
width: 100,
formatter: (row: any) => {
return h(ElSwitch, {
modelValue: row.status,
@@ -415,11 +380,68 @@
})
}
},
{
prop: 'role_desc',
label: '角色描述'
},
{
prop: 'CreatedAt',
label: '创建时间',
width: 180,
formatter: (row: any) => formatDateTime(row.CreatedAt)
},
{
prop: 'actions',
label: '操作',
width: 220,
fixed: 'right',
formatter: (row: any) => {
const buttons: any[] = []
if (hasAuth('role:permission')) {
buttons.push(
h(
ElButton,
{
type: 'primary',
link: true,
onClick: () => showPermissionDialog(row)
},
() => '分配权限'
)
)
}
if (hasAuth('role:edit')) {
buttons.push(
h(
ElButton,
{
type: 'primary',
link: true,
onClick: () => showDialog('edit', row)
},
() => '编辑'
)
)
}
if (hasAuth('role:delete')) {
buttons.push(
h(
ElButton,
{
type: 'danger',
link: true,
onClick: () => deleteRole(row)
},
() => '删除'
)
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
}
])
@@ -442,33 +464,6 @@
return data.label.toLowerCase().includes(value.toLowerCase())
}
// 获取节点的所有子节点ID(包括子菜单和按钮)
const getAllChildrenIds = (node: any): number[] => {
const ids: number[] = []
const traverse = (n: any) => {
if (n.children && n.children.length > 0) {
n.children.forEach((child: any) => {
ids.push(child.id)
traverse(child)
})
}
}
traverse(node)
return ids
}
// 在树数据中查找节点
const findNodeInTree = (treeData: any[], nodeId: number): any => {
for (const node of treeData) {
if (node.id === nodeId) return node
if (node.children && node.children.length > 0) {
const found = findNodeInTree(node.children, nodeId)
if (found) return found
}
}
return null
}
// 更新父节点的勾选状态(计算应该半选的父节点)
const updateParentCheckStatus = (checkedKeys: number[]) => {
const checkedSet = new Set(checkedKeys)
@@ -502,14 +497,19 @@
const currentNodeChecked = checkedSet.has(node.id)
// 判断父节点应该显示的状态:
// 1. 如果有子节点被勾选(someChecked=true),但不是全部子节点被勾选或当前节点未被勾选 -> 半选
// 2. 如果有子节点被勾选当前节点被勾选 ->
if (someChecked) {
if (!allChecked || !currentNodeChecked) {
// 子节点部分被勾选,或者当前节点被勾选 -> 半选
// 1. 如果当前节点被勾选但不是所有子节点被勾选 -> 半选
// 2. 如果有子节点被勾选,但当前节点被勾选 ->
// 3. 如果所有子节点都被勾选且当前节点也被勾选 -> 全选
if (currentNodeChecked) {
// 当前节点被勾选
if (!allChecked) {
// 但不是所有子节点都被勾选 -> 半选
parentIdsToHalfCheck.add(node.id)
}
// 如果 allChecked && currentNodeChecked,则为全选,不需要设置半选
// 如果 allChecked则为全选不需要设置半选
} else if (someChecked) {
// 当前节点未勾选,但有子节点被勾选 -> 半选
parentIdsToHalfCheck.add(node.id)
}
return {
@@ -524,6 +524,34 @@
return Array.from(parentIdsToHalfCheck)
}
// 判断节点是否为一级菜单没有父节点或parent_id为0/null
const isFirstLevelMenu = (nodeId: number): boolean => {
const node = allPermissionsMap.value.get(nodeId)
if (!node) return false
return !node.parent_id || node.parent_id === 0
}
// 获取节点的所有子节点ID递归
const getAllChildrenIds = (nodeId: number): number[] => {
const childrenIds: number[] = []
const collectChildren = (id: number) => {
const node = allPermissionsMap.value.get(id)
if (!node || !node.children || node.children.length === 0) return
node.children.forEach((child: any) => {
childrenIds.push(child.id)
collectChildren(child.id)
})
}
collectChildren(nodeId)
return childrenIds
}
// 上一次勾选的keys用于判断是勾选还是取消勾选
const previousCheckedKeys = ref<number[]>([])
// 处理左侧树的勾选事件
const handleLeftTreeCheck = (data: any, checked: any) => {
if (isHandlingCheck.value) return
@@ -532,7 +560,52 @@
try {
// 获取当前勾选的keys
const currentChecked = checked.checkedKeys as number[]
let currentChecked = checked.checkedKeys as number[]
// 判断是勾选还是取消勾选通过对比当前和之前的keys
const isChecking =
currentChecked.includes(data.id) && !previousCheckedKeys.value.includes(data.id)
const isUnchecking =
!currentChecked.includes(data.id) && previousCheckedKeys.value.includes(data.id)
if (isChecking) {
// 正在勾选节点
// 判断当前节点是否为一级菜单
if (isFirstLevelMenu(data.id)) {
// 一级菜单:自动勾选所有子节点(包括所有子菜单和按钮)
const childrenIds = getAllChildrenIds(data.id)
const newCheckedKeys = [...new Set([...currentChecked, ...childrenIds])]
// 设置新的勾选状态
leftTreeRef.value?.setCheckedKeys(newCheckedKeys, false)
currentChecked = newCheckedKeys
} else {
// 二级及以下菜单/按钮:只勾选自己,不勾选子节点
const childrenIds = getAllChildrenIds(data.id)
// 从当前勾选中移除所有子节点(如果有的话)
const newCheckedKeys = currentChecked.filter((id) => !childrenIds.includes(id))
// 设置新的勾选状态
leftTreeRef.value?.setCheckedKeys(newCheckedKeys, false)
currentChecked = newCheckedKeys
}
} else if (isUnchecking) {
// 正在取消勾选节点
// 判断当前节点是否为一级菜单
if (isFirstLevelMenu(data.id)) {
// 一级菜单:自动取消所有子节点(包括所有子菜单和按钮)
const childrenIds = getAllChildrenIds(data.id)
const newCheckedKeys = currentChecked.filter((id) => !childrenIds.includes(id))
// 设置新的勾选状态
leftTreeRef.value?.setCheckedKeys(newCheckedKeys, false)
currentChecked = newCheckedKeys
}
// 二级及以下菜单/按钮:直接取消勾选,不需要额外处理
}
// 更新上一次勾选的keys
previousCheckedKeys.value = [...currentChecked]
// 计算应该半选的父节点
const halfCheckedIds = updateParentCheckStatus(currentChecked)
@@ -626,23 +699,6 @@
})
}
// 获取树中所有节点的ID包括父节点和子节点
const getAllNodeIds = (treeNodes: any[]): number[] => {
const ids: number[] = []
const traverse = (nodes: any[]) => {
nodes.forEach((node) => {
ids.push(node.id)
if (node.children && node.children.length > 0) {
traverse(node.children)
}
})
}
traverse(treeNodes)
return ids
}
// 保存原始权限树数据
const originalPermissionTree = ref<PermissionTreeNode[]>([])
@@ -765,6 +821,7 @@
// 清空左侧树的勾选状态
leftTreeRef.value?.setCheckedKeys([], false)
previousCheckedKeys.value = []
ElMessage.success('权限添加成功')
} catch (error) {
@@ -842,6 +899,7 @@
}
selectedPermissions.value = []
originalPermissions.value = []
previousCheckedKeys.value = []
leftTreeFilter.value = ''
rightTreeFilter.value = ''
@@ -999,6 +1057,7 @@
const data = {
role_name: form.role_name,
role_desc: form.role_desc,
role_type: form.role_type,
status: form.status
}
await RoleService.updateRole(form.id, data)
@@ -1031,57 +1090,9 @@
console.error(error)
}
}
// 右键菜单项配置
const contextMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
if (hasAuth('role:permission')) {
items.push({ key: 'assignPermission', label: '分配权限' })
}
if (hasAuth('role:edit')) {
items.push({ key: 'edit', label: '编辑' })
}
if (hasAuth('role:delete')) {
items.push({ key: 'delete', label: '删除' })
}
return items
})
// 处理表格行右键菜单
const handleRowContextMenu = (row: any, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'assignPermission':
showPermissionDialog(currentRow.value)
break
case 'edit':
showDialog('edit', currentRow.value)
break
case 'delete':
deleteRole(currentRow.value)
break
}
}
</script>
<style scoped lang="scss">
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
.dialog-header {
display: flex;
align-items: center;