Compare commits

13 Commits

Author SHA1 Message Date
5ae5353baa Merge pull request 'main_beta' (#4) from main_beta into main
Reviewed-on: #4
2026-02-11 10:04:32 +08:00
mll
09a300e98a Merge branch 'main' of http://8.130.49.250:3000/admin/gr_report_web 2026-02-10 22:17:30 +08:00
mll
7a496e6407 加隐藏列 2026-02-10 22:17:26 +08:00
gesilong
1cdabba4da commit:更换首页获取应用接口 2026-02-10 16:44:58 +08:00
gesilong
d7def83ee4 commit:应用权限功能 2026-02-10 16:28:33 +08:00
chy
e69cffbdb6 Merge branch 'main_beta'
# Conflicts:
#	src/views/Home/Index20.vue
2026-02-10 09:52:20 +08:00
gesilong
34f7c68041 commit:首页重构;应用管理优化 2026-02-10 09:30:20 +08:00
mll
bcac3326a3 维度隐藏列 2026-02-09 20:16:36 +08:00
chy
f9393dfca8 1 2026-02-09 18:11:10 +08:00
chy
67fe2e22fc 修改国际化 2026-02-09 13:36:16 +08:00
chy
9e210114b6 添加工作台 2026-02-09 11:34:46 +08:00
chy
8185399f8f 1 2026-02-09 11:34:22 +08:00
7f62a5b191 Merge pull request 'zh' (#1) from zh into main
Reviewed-on: #1
2026-02-09 00:39:20 +08:00
13 changed files with 804 additions and 986 deletions

View File

@@ -17,6 +17,21 @@ export interface UpdateStatusReqVO {
status: number status: number
} }
// 获取登陆用户的应用
export const getMyPage = async (params: PageParam) => {
return await request.get({ url: '/system/oauth2-client/myPage', params })
}
// 获取角色下的应用id
export const getRoleAppIds = async (roleId: number) => {
return await request.get({ url: '/system/permission/list-role-clients?roleId=' + roleId })
}
// 新增角色
export const saveApp = async (data: any) => {
return await request.post({ url: '/system/permission/assign-role-client', data })
}
// 查询角色列表 // 查询角色列表
export const getRolePage = async (params: PageParam) => { export const getRolePage = async (params: PageParam) => {
return await request.get({ url: '/system/role/page', params }) return await request.get({ url: '/system/role/page', params })

View File

@@ -128,6 +128,7 @@ const tableInfo = ref<any>({})
const timerObj = ref<any>({}) const timerObj = ref<any>({})
const numberRange = ref<string[]>([]) const numberRange = ref<string[]>([])
const dateRange=ref<string[]>([]) const dateRange=ref<string[]>([])
const hideColumns=ref<string[]>([])
const dateTimeRange=ref<string[]>([]) const dateTimeRange=ref<string[]>([])
const timeRange=ref<string[]>([]) const timeRange=ref<string[]>([])
const amountFieds=ref<any>({}) const amountFieds=ref<any>({})
@@ -136,6 +137,7 @@ const crudRef = ref()
const dimensionFields=ref<any>({}) const dimensionFields=ref<any>({})
const exportLoading = ref(false) const exportLoading = ref(false)
const fieldList = ref<any[]>([]) // 添加fieldList引用 const fieldList = ref<any[]>([]) // 添加fieldList引用
const hideFeilds= ref<any>({})
const permissions = const permissions =
wsCache.get(CACHE_KEY.USER).lideeYunjipermissions?.[route.meta.menuDataId as string] || false wsCache.get(CACHE_KEY.USER).lideeYunjipermissions?.[route.meta.menuDataId as string] || false
const selectIds = computed(() => { const selectIds = computed(() => {
@@ -170,12 +172,14 @@ const initTable = async () => {
loading.value = true loading.value = true
const { fieldList: apiFieldList, reportVo } = await ReportApi.getWebConfig(props.reportCode) const { fieldList: apiFieldList, reportVo } = await ReportApi.getWebConfig(props.reportCode)
// 存储字段列表到响应式引用
fieldList.value = apiFieldList fieldList.value = apiFieldList
const isHeight = reportVo.tableConfig?.includes('height') const isHeight = reportVo.tableConfig?.includes('height')
const isPage = reportVo.dataConfig?.includes('page') const isPage = reportVo.dataConfig?.includes('page')
const isPermi = reportVo.dataConfig?.includes('authTrue') const isPermi = reportVo.dataConfig?.includes('authTrue')
const isHideExport = reportVo.tableConfig?.includes('hideExport') const isHideExport = reportVo.tableConfig?.includes('hideExport')
hideColumns.value=[]
tableInfo.value = { ...reportVo, isPage, isHeight, isPermi, isHideExport } tableInfo.value = { ...reportVo, isPage, isHeight, isPermi, isHideExport }
tableOption.value = { tableOption.value = {
selection: !isHideExport, selection: !isHideExport,
@@ -188,46 +192,40 @@ const initTable = async () => {
border: reportVo.tableConfig.includes('border'), border: reportVo.tableConfig.includes('border'),
stripe: reportVo.tableConfig.includes('stripe'), stripe: reportVo.tableConfig.includes('stripe'),
showSummary:false, showSummary:false,
column: {} column: {}
} }
//国际化处理
const fieldLengObj = assembleLengObj(apiFieldList, 'labelI18n', 'fieldCode', 'fieldName') const fieldLengObj = assembleLengObj(apiFieldList, 'labelI18n', 'fieldCode', 'fieldName')
for (const key in fieldLengObj) { for (const key in fieldLengObj) {
mergeLocaleMessage(key, { [props.reportCode]: fieldLengObj[key] }) mergeLocaleMessage(key, { [props.reportCode]: fieldLengObj[key] })
} }
// 分离固定列和动态列 // 根据parentFieldCode判断表头结构
const fixedFields = apiFieldList.filter(f => f.isFixedColumn === 'Y') const hasSubFields = Array.isArray(apiFieldList) && apiFieldList.some(field => field.parentFieldCode && field.parentFieldCode !== '')
const dynamicFields = apiFieldList.filter(f => f.isFixedColumn !== 'Y')
// 处理固定列 // 构建父子关系映射
fixedFields.forEach((item) => { const parentChildMap = new Map()
const config: any = { const childParentMap = new Map()
prop: item.fieldCode,
label: t(`${props.reportCode}.${item.fieldCode}`),
type: 'input',
overHidden: true,
isExport: item.isExport == 'Y',
search: item.queryIsWeb == 'Y',
fixed: true
}
tableOption.value.column[item.fieldCode] = config
})
// 构建父子关系映射(仅用于识别子字段配置) // 建立映射关系
const childFieldConfigs = new Map() apiFieldList.forEach(field => {
dynamicFields.forEach(field => {
if (field.parentFieldCode && field.parentFieldCode !== '') { if (field.parentFieldCode && field.parentFieldCode !== '') {
if (!childFieldConfigs.has(field.parentFieldCode)) { // 这是子字段
childFieldConfigs.set(field.parentFieldCode, []) childParentMap.set(field.fieldCode, field.parentFieldCode)
if (!parentChildMap.has(field.parentFieldCode)) {
parentChildMap.set(field.parentFieldCode, [])
} }
childFieldConfigs.get(field.parentFieldCode).push(field) parentChildMap.get(field.parentFieldCode).push(field.fieldCode)
} }
}) })
// 处理动态列暂不构建children等数据返回后动态生成 //字段处理 - 构建正确的表头结构
dynamicFields.forEach((item, index) => { apiFieldList.forEach((item, index) => {
if (item.parentFieldCode && item.parentFieldCode !== '') return // 跳过子字段,子字段会在父字段中处理
if (item.parentFieldCode && item.parentFieldCode !== '') {
return
}
const config: any = { const config: any = {
prop: item.fieldCode, prop: item.fieldCode,
@@ -237,8 +235,23 @@ const initTable = async () => {
isExport: item.isExport == 'Y', isExport: item.isExport == 'Y',
sortable: item.isShowSort == 'Y' ? 'custom' : false, sortable: item.isShowSort == 'Y' ? 'custom' : false,
search: item.queryIsWeb == 'Y', search: item.queryIsWeb == 'Y',
_hasChildConfig: childFieldConfigs.has(item.fieldCode), }
_childConfigs: childFieldConfigs.get(item.fieldCode) || []
// 如果该字段有子字段,添加子列配置
if (parentChildMap.has(item.fieldCode)) {
const childFields = parentChildMap.get(item.fieldCode)
config.children = childFields.map(childFieldCode => {
const childField = apiFieldList.find(f => f.fieldCode === childFieldCode)
return {
prop: childField.fieldCode,
label: t(`${props.reportCode}.${childField.fieldCode}`),
type: 'input',
overHidden: true,
isExport: childField.isExport == 'Y',
sortable: childField.isShowSort == 'Y' ? 'custom' : false,
search: childField.queryIsWeb == 'Y'
}
})
} }
if(!!item.isAmount){ if(!!item.isAmount){
@@ -248,25 +261,37 @@ const initTable = async () => {
} }
if(item.isDimension=='Y'){ if(item.isDimension=='Y'){
dimensionFields.value[config.prop]=config dimensionFields.value[config.prop]=config
hideFeilds.value[config.prop]=item.isHideDimension
}
if(item.isHideCol == 'Y') {
config.hide = true
hideColumns.value.push( config.prop )
} }
if (item.queryMode == 'RANGE') config.searchRange = true if (item.queryMode == 'RANGE') config.searchRange = true
if (['Integer', 'BigInt', 'BigDecimal'].includes(item.fieldType)) config.type = 'number' if (['Integer', 'BigInt', 'BigDecimal'].includes(item.fieldType)) config.type = 'number'
else if (item.fieldType == 'Date') { else if (item.fieldType == 'Date') {
config.type = 'date' config.type = 'date'
config.format = 'YYYY-MM-DD' config.format = 'YYYY-MM-DD'
config.valueFormat = 'YYYY-MM-DD' config.valueFormat = 'YYYY-MM-DD'
if(config.searchRange) dateRange.value.push(config.prop) if(config.searchRange){
dateRange.value.push(config.prop)
}
} else if (item.fieldType == 'Time') { } else if (item.fieldType == 'Time') {
config.type = 'time' config.type = 'time'
config.format = 'HH:mm:ss' config.format = 'HH:mm:ss'
config.valueFormat = 'HH:mm:ss' config.valueFormat = 'HH:mm:ss'
if(config.searchRange) timeRange.value.push(config.prop)
if(config.searchRange){
timeRange.value.push(config.prop)
}
} else if (item.fieldType == 'DateTime') { } else if (item.fieldType == 'DateTime') {
config.type = 'datetime' config.type = 'datetime'
config.format = 'YYYY-MM-DD HH:mm:ss' config.format = 'YYYY-MM-DD HH:mm:ss'
config.valueFormat = 'YYYY-MM-DD HH:mm:ss' config.valueFormat = 'YYYY-MM-DD HH:mm:ss'
if(config.searchRange) dateTimeRange.value.push(config.prop) if(config.searchRange){
dateTimeRange.value.push(config.prop)
}
} }
if (config.type == 'number' && config.searchRange) numberRange.value.push(config.prop) if (config.type == 'number' && config.searchRange) numberRange.value.push(config.prop)
@@ -277,11 +302,13 @@ const initTable = async () => {
tableOption.value.column[item.fieldCode] = config tableOption.value.column[item.fieldCode] = config
}) })
isInit.value = true isInit.value = true
searchChange() searchChange()
initTableLayout() initTableLayout()
} }
const searchDimension=()=>{ const searchDimension=()=>{
searchChange() searchChange()
} }
const initTableLayout = () => { const initTableLayout = () => {
@@ -347,158 +374,28 @@ const getTableData = async (isLoading = true) => {
if (isLoading) loading.value = true if (isLoading) loading.value = true
const searchObj = await getSearchData() const searchObj = await getSearchData()
try { try {
// 获取数据 const data = await ReportApi.getTableList(props.reportCode, searchObj)
let data = await ReportApi.getTableList(props.reportCode, searchObj)
// 功能测试CS_DTBT报表使用模拟数据
if (props.reportCode === 'CS_DTBT') {
data = {
records: [
{ "yuefen": "一月", "benyue": 100, "leiji": 110 },
{ "yuefen": "一月", "benyue": 120, "leiji": 130 },
{ "yuefen": "二月", "benyue": 140, "leiji": 150 },
{ "yuefen": "二月", "benyue": 160, "leiji": 170 },
{ "yuefen": "三月", "benyue": 180, "leiji": 190 },
{ "yuefen": "三月", "benyue": 200, "leiji": 210 },
{ "yuefen": "四月", "benyue": null, "leiji": null },
{ "yuefen": "四月", "benyue": 220, "leiji": 230 },
{ "yuefen": "五月", "benyue": 333, "leiji": 444 },
{ "yuefen": "六月", "benyue": 555, "leiji": 555 },
{ "yuefen": "六月", "benyue": 666, "leiji": 666 },
],
total: 6
}
}
if (tablePage.value) tablePage.value['total'] = data.total if (tablePage.value) tablePage.value['total'] = data.total
// 查找动态分组字段(用于生成一级表头) // 处理包含子字段的数据
const groupField = fieldList.value.find(f => f.isDynamicGroup === 'Y') let processedData = data.records
// 根据字段配置判断是否需要处理子字段
const hasSubFields = Array.isArray(fieldList.value) && fieldList.value.length > 0 &&
fieldList.value.some(field => field.parentFieldCode && field.parentFieldCode !== '')
// 动态生成二维表头 if (hasSubFields) {
if (data.records && data.records.length > 0) { processedData = data.records.map(record => {
const newColumns = {}
// 保留固定列
for (const colKey in tableOption.value.column) {
const colConfig = tableOption.value.column[colKey]
if (colConfig.fixed) {
newColumns[colKey] = colConfig
}
}
if (groupField) {
// 有分组字段,从数据中提取所有不重复的分组值(一级表头)
const groupValues = [...new Set(data.records.map(r => r[groupField.fieldCode]))].filter(Boolean)
// 找到所有子字段配置按sortNum排序
const childFields = fieldList.value
.filter(f =>
f.parentFieldCode &&
f.parentFieldCode !== '' &&
f.isFixedColumn !== 'Y'
)
.sort((a, b) => (a.sortNum || 0) - (b.sortNum || 0))
// 为每个分组值生成列
groupValues.forEach(groupValue => {
const parentProp = `group_${groupValue}`
newColumns[parentProp] = {
label: groupValue,
children: childFields.map(childField => ({
prop: `${parentProp}_${childField.fieldCode}`,
label: t(`${props.reportCode}.${childField.fieldCode}`),
type: ['Integer', 'BigInt', 'BigDecimal'].includes(childField.fieldType) ? 'number' : 'input',
overHidden: true,
isExport: childField.isExport == 'Y'
}))
}
})
} else {
// 没有分组字段,使用原有的逻辑
for (const colKey in tableOption.value.column) {
const colConfig = tableOption.value.column[colKey]
if (!colConfig.fixed) {
newColumns[colKey] = colConfig
}
}
}
tableOption.value.column = newColumns
}
// 将扁平数据按分组字段转换为二维表头数据
let processedData = []
if (groupField && data.records.length > 0) {
// 有分组字段,根据分组字段值进行分组
const fixedFields = fieldList.value.filter(f => f.isFixedColumn === 'Y')
const childFields = fieldList.value
.filter(f => f.parentFieldCode && f.parentFieldCode !== '' && f.isFixedColumn !== 'Y')
.sort((a, b) => (a.sortNum || 0) - (b.sortNum || 0))
// 获取所有分组值
const groupValues = [...new Set(data.records.map(r => r[groupField.fieldCode]))].filter(Boolean)
// 按分组字段值对数据进行分组
const groupedData = {}
data.records.forEach(record => {
const groupValue = record[groupField.fieldCode]
if (!groupedData[groupValue]) {
groupedData[groupValue] = []
}
groupedData[groupValue].push(record)
})
// 计算需要生成的行数(根据固定列配置或分组内最大数据量)
let rowCount = 1
if (fixedFields.length > 0 && fixedFields[0].fixedColumnValue) {
rowCount = fixedFields[0].fixedColumnValue.split(',').length
} else {
// 如果没有固定列,取每个分组中数据最多的数量
rowCount = Math.max(...Object.values(groupedData).map((arr: any[]) => arr.length))
}
// 按行处理数据
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
const flatRecord = {}
// 处理固定列
fixedFields.forEach(field => {
if (field.fixedColumnValue) {
const fixedValues = field.fixedColumnValue.split(',')
flatRecord[field.fieldCode] = fixedValues[rowIndex] || ''
}
})
// 处理动态列数据(根据分组字段值获取对应数据)
groupValues.forEach(groupValue => {
const groupRecords = groupedData[groupValue] || []
const record = groupRecords[rowIndex]
if (record) {
const parentProp = `group_${groupValue}`
childFields.forEach(childField => {
flatRecord[`${parentProp}_${childField.fieldCode}`] = record[childField.fieldCode]
})
}
})
processedData.push(flatRecord)
}
} else {
// 没有分组字段,直接处理数据
processedData = data.records.map((record, recordIndex) => {
const flatRecord = { ...record } const flatRecord = { ...record }
// 处理子字段数据
// 处理固定列
fieldList.value.forEach(field => { fieldList.value.forEach(field => {
if (field.isFixedColumn === 'Y' && field.fixedColumnValue) { if (field.parentFieldCode && field.parentFieldCode !== '') {
const fixedValues = field.fixedColumnValue.split(',') // 这是子字段,从父字段中提取数据
flatRecord[field.fieldCode] = fixedValues[recordIndex] || '' const parentData = record[field.parentFieldCode]
if (parentData && typeof parentData === 'object') {
flatRecord[field.fieldCode] = parentData[field.fieldCode]
}
} }
}) })
return flatRecord return flatRecord
}) })
} }
@@ -578,6 +475,15 @@ const clearSearch = () => {
const searchChange = (params?, done?) => { const searchChange = (params?, done?) => {
if (tablePage.value) tablePage.value['currentPage'] = 1 if (tablePage.value) tablePage.value['currentPage'] = 1
getTableData().finally(() => { getTableData().finally(() => {
let field=tableSearch.value['Group by']
let hides=[]
if(field.length){
hides=Object.keys(hideFeilds.value).length?hideFeilds.value[field].split(','):[]
}
Object.keys(tableOption.value.column).forEach(key=>{
let item=tableOption.value.column[key]
item.hide=hides.includes(item.prop)||hideColumns.value.includes(item.prop)
})
if (done) done() if (done) done()
}) })
} }

View File

@@ -69,7 +69,7 @@ export default {
noPermission: `抱歉,您无权访问此页面。`, noPermission: `抱歉,您无权访问此页面。`,
pageError: '抱歉,您访问的页面不存在。', pageError: '抱歉,您访问的页面不存在。',
networkError: '抱歉,服务器报告错误。', networkError: '抱歉,服务器报告错误。',
returnToHome: '返回首页' returnToHome: '返回工作台'
}, },
permission: { permission: {
hasPermission: `请设置操作权限标签值`, hasPermission: `请设置操作权限标签值`,
@@ -157,7 +157,7 @@ export default {
router: { router: {
login: '登录', login: '登录',
socialLogin: '社交登录', socialLogin: '社交登录',
home: '首页', home: '工作台',
analysis: '分析页', analysis: '分析页',
workplace: '工作台' workplace: '工作台'
}, },
@@ -275,7 +275,7 @@ export default {
}, },
exception: { exception: {
backLogin: '返回登录', backLogin: '返回登录',
backHome: '返回首页', backHome: '返回工作台',
subTitle403: '抱歉,您无权访问此页面。', subTitle403: '抱歉,您无权访问此页面。',
subTitle404: '抱歉,您访问的页面不存在。', subTitle404: '抱歉,您访问的页面不存在。',
subTitle500: '抱歉,服务器报告错误。', subTitle500: '抱歉,服务器报告错误。',

View File

@@ -59,7 +59,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
children: [ children: [
{ {
path: 'index', path: 'index',
component: () => import('@/views/Home/Index5.vue'), component: () => import('@/views/Home/Index20.vue'),
name: 'Index', name: 'Index',
meta: { meta: {
title: t('router.home'), title: t('router.home'),

View File

@@ -103,6 +103,8 @@ export const getDictLabel = (dictType: string, value: any): string => {
export enum DICT_TYPE { export enum DICT_TYPE {
USER_TYPE = 'user_type', USER_TYPE = 'user_type',
COMMON_STATUS = 'common_status', COMMON_STATUS = 'common_status',
APP_CATEGORY = 'app_category',
APP_QX = 'app_qx',
TERMINAL = 'terminal', // 终端 TERMINAL = 'terminal', // 终端
// ========== SYSTEM 模块 ========== // ========== SYSTEM 模块 ==========
@@ -120,6 +122,7 @@ export enum DICT_TYPE {
SYSTEM_SMS_RECEIVE_STATUS = 'system_sms_receive_status', SYSTEM_SMS_RECEIVE_STATUS = 'system_sms_receive_status',
SYSTEM_ERROR_CODE_TYPE = 'system_error_code_type', SYSTEM_ERROR_CODE_TYPE = 'system_error_code_type',
SYSTEM_OAUTH2_GRANT_TYPE = 'system_oauth2_grant_type', SYSTEM_OAUTH2_GRANT_TYPE = 'system_oauth2_grant_type',
SYSTEM_OAUTH2_SCOPE='system_oauth2_scope',
SYSTEM_MAIL_SEND_STATUS = 'system_mail_send_status', SYSTEM_MAIL_SEND_STATUS = 'system_mail_send_status',
SYSTEM_NOTIFY_TEMPLATE_TYPE = 'system_notify_template_type', SYSTEM_NOTIFY_TEMPLATE_TYPE = 'system_notify_template_type',
SYSTEM_SOCIAL_TYPE = 'system_social_type', SYSTEM_SOCIAL_TYPE = 'system_social_type',

View File

@@ -1,631 +1,371 @@
<template> <template>
<ContentWrap> <div class="app-container">
<!-- 头部区域 -->
<!-- <header class="app-header">
<h1 class="title">
<i class="el-icon-menu"></i>
</h1>
</header> -->
<el-table <!-- 搜索区域 -->
:data="processedTableData" :span-method="objectSpanMethod" border stripe <!-- <div class="search-container">
height="calc(100vh - 300px)" style="width: 100%" v-loading="loading" :summary-method="getSummaries" <el-input
show-summary :summary-text="summaryText"> v-model="searchQuery"
<!-- 动态生成表头 --> placeholder="搜索应用..."
<template v-for="dim in selectedDimensions" :key="dim"> clearable
<el-table-column prefix-icon="el-icon-search"
:prop="dim" :label="getDimensionName(dim)" :min-width="getColumnWidth(dim)" class="search-input"
show-overflow-tooltip> ></el-input>
<template #default="{ row }"> </div> -->
<span>{{ row[dim] || '-' }}</span>
</template>
</el-table-column>
</template>
<!-- 统计指标列 --> <!-- 空状态 -->
<el-table-column prop="salesAmount" label="销售额(万)" width="120" sortable> <div v-if="Object.keys(filteredCategories).length === 0" class="empty-state">
<template #default="{ row }"> <i class="el-icon-warning empty-icon"></i>
{{ formatNumber(row.salesAmount) }} <h3>未找到匹配的应用</h3>
</template> <p>请尝试调整搜索关键词</p>
</el-table-column> </div>
<el-table-column prop="salesQuantity" label="销售数量" width="120" sortable> <!-- 分类应用展示 -->
<template #default="{ row }"> <div
{{ formatNumber(row.salesQuantity) }} v-for="(category, categoryName) in filteredCategories"
</template> :key="categoryName"
</el-table-column> class="category-section">
<div class="category-header">
<h2 class="category-title">{{ categoryName }}</h2>
<el-tag size="small" class="app-count">{{ category.apps.length }} 个应用</el-tag>
</div>
<el-table-column prop="growthRate" label="同比增长" width="120" sortable> <div class="apps-grid">
<template #default="{ row }"> <div
<span :class="getGrowthClass(row.growthRate)"> v-for="app in category.apps"
{{ formatPercent(row.growthRate) }} :key="app.id"
class="app-card"
@click="handleAppClick(app)">
<div class="app-icon-wrapper">
<img :src="app.logo" class="app-icon-img" />
</div>
<div class="app-name">{{ app.name }}</div>
</div>
</div>
</div>
<!-- 应用详情对话框 -->
<el-dialog
v-model:visible="dialogVisible"
title="应用详情"
width="30%"
custom-class="app-dialog">
<div v-if="selectedApp" class="dialog-content">
<div class="dialog-icon-wrapper">
<img :src="selectedApp.logo" class="dialog-app-icon-img" />
</div>
<h3 class="dialog-app-name">{{ selectedApp.name }}</h3>
<p class="dialog-app-id">ID: {{ selectedApp.id }}</p>
<p class="dialog-app-category" v-if="selectedApp.category">分类: {{ selectedApp.category }}</p>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmLaunch">启动</el-button>
</span> </span>
</template> </template>
</el-table-column> </el-dialog>
</el-table> </div>
</ContentWrap>
</template> </template>
<script setup> <script>
import { ref, computed, watch, onMounted } from 'vue' import * as RoleApi from '@/api/system/role'
import { ElMessage } from 'element-plus' import { DICT_TYPE, getDictLabel } from '@/utils/dict'
import { Check } from '@element-plus/icons-vue' export default {
import * as echarts from 'echarts' name: 'AppManager',
data() {
// 维度定义 - 7个维度 return {
const dimensions = ref([ searchQuery: '',
{ id: 'salesType', name: '销售类型' }, dialogVisible: false,
{ id: 'customer', name: '客户' }, selectedApp: null,
{ id: 'dosageForm', name: '剂型' }, appsData: []
{ id: 'product', name: '产品' }, };
{ id: 'administrativeArea', name: '行政区域' }, },
{ id: 'salesman', name: '业务员' }, mounted() {
{ id: 'specification', name: '品规' } this.fetchApps()
]) },
computed: {
// 状态管理 - 默认选中所有维度 categorizedApps() {
const selectedDimensions = ref(dimensions.value.map(d => d.id)) const result = {};
const primaryDimension = ref('product') // 默认主维度为产品 this.appsData.forEach(app => {
const tableData = ref([]) const categoryLabel = app.categoryLabel || '未分类'
const loading = ref(false) if (!result[categoryLabel]) {
const lastUpdate = ref('') result[categoryLabel] = { apps: [] }
const showChart = ref(true)
const activeChartTab = ref('bar')
// 合计行相关状态
const showSummaryRow = ref(true)
const totalStats = ref(null)
const summaryText = computed(() => showSummaryRow.value ? '合计' : '')
// 计算属性
const processedTableData = computed(() => {
return tableData.value.map(item => ({
...item,
formattedAmount: formatNumber(item.salesAmount),
formattedQuantity: formatNumber(item.salesQuantity),
formattedGrowth: formatPercent(item.growthRate)
}))
})
// 方法定义
const toggleDimension = (dimensionId) => {
const index = selectedDimensions.value.indexOf(dimensionId)
if (index > -1) {
// 如果当前维度是主维度,且只有一个维度被选中,不能移除
if (selectedDimensions.value.length === 1) {
ElMessage.warning('至少需要保留一个维度')
return
}
// 如果要移除的是当前主维度,则自动切换到下一个维度
if (dimensionId === primaryDimension.value) {
// 找下一个可用的维度作为主维度
const nextDim = selectedDimensions.value.find(d => d !== dimensionId)
if (nextDim) {
primaryDimension.value = nextDim
}
}
selectedDimensions.value.splice(index, 1)
} else {
selectedDimensions.value.push(dimensionId)
}
}
const getDimensionName = (dimensionId) => {
const dim = dimensions.value.find(d => d.id === dimensionId)
return dim ? dim.name : dimensionId
}
// 单元格合并逻辑
const objectSpanMethod = ({ row, column, rowIndex, columnIndex }) => {
if (!primaryDimension.value) return { rowspan: 1, colspan: 1 }
const prop = column.property
if (prop !== primaryDimension.value) return { rowspan: 1, colspan: 1 }
// 计算相同值的行数
let spanCount = 1
for (let i = rowIndex + 1; i < tableData.value.length; i++) {
if (tableData.value[i][prop] === row[prop]) {
spanCount++
} else {
break
}
}
// 如果是第一行,返回合并
let isFirstRow = true
for (let i = rowIndex - 1; i >= 0; i--) {
if (tableData.value[i][prop] === row[prop]) {
isFirstRow = false
break
}
}
if (isFirstRow && spanCount > 1) {
return { rowspan: spanCount, colspan: 1 }
} else {
return { rowspan: 0, colspan: 0 }
}
}
// 自定义合计方法 - 包含同比增长的加权平均计算
const getSummaries = (param) => {
const { columns, data } = param
const sums = []
columns.forEach((column, index) => {
if (index === 0) {
sums[index] = showSummaryRow.value ? '合计' : ''
return
}
// 如果不是数值列,不显示合计
if (!['salesAmount', 'salesQuantity', 'growthRate'].includes(column.property)) {
sums[index] = ''
return
}
// 计算数值列的总和或加权平均
const values = data.map(item => Number(item[column.property]))
if (!values.every(value => isNaN(value))) {
if (column.property === 'salesAmount' || column.property === 'salesQuantity') {
// 销售额和销售数量直接求和
const sum = values.reduce((prev, curr) => {
const value = Number(curr)
if (!isNaN(value)) {
return prev + curr
} else {
return prev
}
}, 0)
if (column.property === 'salesAmount') {
sums[index] = formatNumber(sum)
} else if (column.property === 'salesQuantity') {
sums[index] = formatNumber(sum)
}
} else if (column.property === 'growthRate') {
// 同比增长率需要计算加权平均值(按销售额加权)
let totalAmount = 0
let weightedGrowthSum = 0
data.forEach(item => {
const amount = Number(item.salesAmount) || 0
const growth = Number(item.growthRate) || 0
if (!isNaN(amount) && !isNaN(growth)) {
totalAmount += amount
weightedGrowthSum += growth * amount
} }
result[categoryLabel].apps.push(app)
}) })
return result;
const avgGrowth = totalAmount > 0 ? weightedGrowthSum / totalAmount : 0 },
sums[index] = formatPercent(avgGrowth) filteredCategories() {
if (!this.searchQuery.trim()) {
return this.categorizedApps;
}
const query = this.searchQuery.toLowerCase();
const result = {};
for (const [categoryName, categoryData] of Object.entries(this.categorizedApps)) {
const filteredApps = categoryData.apps.filter(app =>
app.name.toLowerCase().includes(query) ||
String(app.categoryLabel || '').toLowerCase().includes(query)
);
if (filteredApps.length > 0) {
result[categoryName] = {...categoryData, apps: filteredApps};
} }
} else {
sums[index] = '-'
} }
})
return sums return result;
} },
totalApps() {
// 数据获取 return this.appsData.length;
const fetchData = async () => { },
loading.value = true categoriesCount() {
return Object.keys(this.categorizedApps).length;
},
visibleCategories() {
return Object.keys(this.filteredCategories).length;
}
},
methods: {
async fetchApps() {
try { try {
// 使用模拟数据 const data = await RoleApi.getMyPage({ pageNo: 1, pageSize: 1000 })
setTimeout(() => { const list = data?.list || []
tableData.value = generateMockData() this.appsData = list.map(item => ({
id: item.id,
// 计算并保存总统计数据 name: item.name,
calculateTotalStats() logo: item.logo,
category: item.category,
lastUpdate.value = new Date().toLocaleString() categoryLabel: getDictLabel(DICT_TYPE.APP_CATEGORY, item.category),
loading.value = false redirectUris: item.redirectUris
}))
// 更新图表 } catch (e) {
if (showChart.value) { this.$message.error('应用数据加载失败')
updateCharts()
} }
}, 1000)
} catch (error) {
console.error('获取数据失败:', error)
loading.value = false
ElMessage.error('获取数据失败')
}
}
// 计算总统计数据
const calculateTotalStats = () => {
const totalAmount = tableData.value.reduce((sum, row) => sum + (row.salesAmount || 0), 0)
const totalQuantity = tableData.value.reduce((sum, row) => sum + (row.salesQuantity || 0), 0)
// 计算加权平均增长率(按销售额加权)
const weightedGrowthSum = tableData.value.reduce(
(sum, row) => sum + (row.growthRate || 0) * (row.salesAmount || 0), 0
)
const avgGrowth = totalAmount > 0 ? weightedGrowthSum / totalAmount : 0
totalStats.value = {
totalAmount,
totalQuantity,
avgGrowth
}
}
// 生成模拟数据
const generateMockData = () => {
const data = []
// 定义各维度的取值范围
const dimValues = {
salesType: ['零售', '批发', '直销'],
customer: ['客户1', '客户2', '客户3', '客户4', '客户5'],
dosageForm: ['片剂', '胶囊', '颗粒', '口服液'],
product: ['产品A', '产品B', '产品C', '产品D'],
administrativeArea: ['上海', '北京', '广州', '深圳', '杭州'],
salesman: ['张三', '李四', '王五', '赵六'],
specification: ['10mg*20片', '20mg*30片', '50mg*10片', '100mg*5片']
}
// 为每个产品生成一些数据
const products = dimValues.product
const rowCount = 100 // 生成100行数据
for (let i = 0; i < rowCount; i++) {
const row = {}
// 为每个维度生成随机值
dimensions.value.forEach(dim => {
const values = dimValues[dim.id] || ['默认值']
row[dim.id] = values[Math.floor(Math.random() * values.length)]
})
// 添加统计指标
row.salesAmount = Math.random() * 200 + 50
row.salesQuantity = Math.floor(Math.random() * 5000) + 1000
row.growthRate = (Math.random() * 0.3 - 0.1) // -10% 到 20%
data.push(row)
}
// 按主维度排序
return data.sort((a, b) => {
const aValue = a[primaryDimension.value] || ''
const bValue = b[primaryDimension.value] || ''
return aValue.localeCompare(bValue)
})
}
// 辅助方法
const formatNumber = (num) => {
if (num === undefined || num === null) return '-'
return Number(num).toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
}
const formatPercent = (num) => {
if (num === undefined || num === null) return '-'
return (num * 100).toFixed(2) + '%'
}
const getGrowthClass = (rate) => {
if (rate > 0) return 'positive'
if (rate < 0) return 'negative'
return 'neutral'
}
const getColumnWidth = (dimensionId) => {
const widthMap = {
salesType: 100,
customer: 120,
dosageForm: 80,
product: 100,
administrativeArea: 100,
salesman: 100,
specification: 120
}
return widthMap[dimensionId] || 100
}
// 图表相关
let barChartInstance = null
let pieChartInstance = null
const updateCharts = () => {
if (barChartInstance) {
barChartInstance.dispose()
}
if (pieChartInstance) {
pieChartInstance.dispose()
}
// 更新柱状图
const barElement = document.querySelector('.chart-container .el-tab-pane:first-child div')
if (barElement) {
barChartInstance = echarts.init(barElement)
// 按主维度分组数据
const groups = {}
tableData.value.forEach(d => {
const key = d[primaryDimension.value] || '其他'
if (!groups[key]) {
groups[key] = 0
}
groups[key] += d.salesAmount || 0
})
const barOption = {
title: { text: `${getDimensionName(primaryDimension.value)}销售额分布`, left: 'center' },
tooltip: {
trigger: 'axis',
formatter: '{b}: {c}万元'
}, },
xAxis: { handleAppClick(app) {
type: 'category', const uri = Array.isArray(app.redirectUris) ? app.redirectUris[0] : app.redirectUris
data: Object.keys(groups) if (uri) {
window.location.href = uri
} else {
this.$message.warning('未配置重定向地址')
}
}, },
yAxis: { confirmLaunch() {
type: 'value', this.$message.success(`正在启动 ${this.selectedApp.name}...`);
name: '销售额(万)' this.dialogVisible = false;
},
series: [{
data: Object.values(groups),
type: 'bar',
itemStyle: {
color: '#409eff'
}
}]
}
barChartInstance.setOption(barOption)
}
// 更新饼图
const pieElement = document.querySelector('.chart-container .el-tab-pane:last-child div')
if (pieElement) {
pieChartInstance = echarts.init(pieElement)
// 按第一个维度分组数据
const firstDim = selectedDimensions.value[0]
if (firstDim) {
const groups = {}
tableData.value.forEach(d => {
const key = d[firstDim] || '其他'
if (!groups[key]) {
groups[key] = 0
}
groups[key] += d.salesAmount || 0
})
const pieOption = {
title: { text: `${getDimensionName(firstDim)}占比`, left: 'center' },
tooltip: {
trigger: 'item',
formatter: '{a}<br/>{b}: {c}万元 ({d}%)'
},
series: [{
name: '销售额',
type: 'pie',
radius: '50%',
data: Object.keys(groups).map(key => ({
name: key,
value: groups[key]
})),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
} }
} }
}] };
}
pieChartInstance.setOption(pieOption)
}
}
}
const reset = () => {
selectedDimensions.value = dimensions.value.map(d => d.id)
primaryDimension.value = 'product'
tableData.value = []
showSummaryRow.value = true
totalStats.value = null
}
// 监听维度变化
watch(() => [...selectedDimensions.value], (newVal) => {
// 如果当前主维度不在选中维度中,则设置为第一个选中的维度
if (!newVal.includes(primaryDimension.value) && newVal.length > 0) {
primaryDimension.value = newVal[0]
}
})
// 监听主维度变化
watch(primaryDimension, () => {
if (tableData.value.length > 0) {
fetchData()
}
})
// 初始化
onMounted(() => {
fetchData()
// 监听窗口变化调整图表
window.addEventListener('resize', () => {
if (barChartInstance) barChartInstance.resize()
if (pieChartInstance) pieChartInstance.resize()
})
})
</script> </script>
<style scoped> <style scoped>
.multi-dimension-report {
display: flex;
height: 100vh; /* 头部样式 */
background: #f5f7fa; .app-header {
flex-direction: column; padding: 30px 0;
color: white;
text-align: center;
} }
.dimension-controls { .title {
margin-bottom: 10px;
font-size: 2.5rem;
text-shadow: 0 2px 4px rgb(0 0 0 / 10%);
}
.subtitle {
font-size: 1.1rem;
opacity: 0.9;
}
/* 统计信息样式 */
.stats-container {
display: flex;
padding: 20px; padding: 20px;
background: white; margin-bottom: 30px;
border-bottom: 1px solid #e6e6e6; background: rgb(255 255 255 / 20%);
border-radius: 15px;
justify-content: space-around;
backdrop-filter: blur(10px);
} }
.header { .stat-card {
color: white;
text-align: center;
}
.stat-value {
margin-bottom: 5px;
font-size: 2rem;
font-weight: bold;
}
.stat-label {
font-size: 0.9rem;
opacity: 0.9;
}
/* 搜索区域样式 */
.search-container {
max-width: 500px;
margin: 0 auto 30px;
}
.search-input ::v-deep .el-input__inner {
padding-left: 40px;
border-radius: 25px;
}
/* 空状态样式 */
.empty-state {
padding: 60px 20px;
margin-bottom: 30px;
color: rgb(255 255 255 / 80%);
text-align: center;
background: rgb(255 255 255 / 10%);
border-radius: 15px;
}
.empty-icon {
display: block;
margin-bottom: 15px;
font-size: 3rem;
}
/* 分类区域样式 */
.category-section {
padding: 25px;
margin-bottom: 30px;
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgb(0 0 0 / 10%);
animation: fadeIn 0.5s ease-out;
}
.category-header {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
justify-content: space-between;
margin-bottom: 20px; margin-bottom: 20px;
} }
.dimension-selection { .category-title {
margin-top: 10px; padding-left: 15px;
margin: 0;
font-size: 1.5rem;
color: #303133;
border-left: 5px solid #409EFF;
} }
.dimension-selection h3 { .app-count {
margin: 0 0 15px;
font-size: 16px;
color: #606266;
}
.dimension-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin-bottom: 10px;
}
.dimension-item {
position: relative;
padding: 12px;
font-weight: 500;
color: #409eff; color: #409eff;
background: #ecf5ff;
}
/* 应用网格样式 */
.apps-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 25px;
}
.app-card {
padding: 20px 15px;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
background-color: #ecf5ff; background: #f8f9fa;
border: 2px solid #409eff; border-radius: 12px;
border-radius: 4px; box-shadow: 0 2px 10px rgb(0 0 0 / 5%);
transition: all 0.3s; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
user-select: none;
} }
.dimension-item:hover { .app-card:hover {
background-color: #d9ecff; color: white;
transform: translateY(-2px); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
box-shadow: 0 2px 8px rgb(64 158 255 / 20%); transform: translateY(-5px);
box-shadow: 0 10px 25px rgb(64 158 255 / 20%);
} }
.dimension-item.inactive { .app-icon-wrapper {
font-weight: normal;
color: #606266;
background-color: #f5f7fa;
border-color: #dcdfe6;
}
.dimension-item.inactive:hover {
background-color: #ebeef5;
transform: translateY(-2px);
box-shadow: 0 2px 8px rgb(0 0 0 / 10%);
}
.dimension-item .check-icon {
position: absolute;
top: 5px;
right: 5px;
font-size: 12px;
color: #409eff;
}
.dimension-item.inactive .check-icon {
display: none;
}
.report-container {
flex: 1;
padding: 20px;
overflow: hidden;
}
.report-header {
display: flex; display: flex;
justify-content: space-between; width: 60px;
height: 60px;
margin: 0 auto 12px;
font-size: 28px;
line-height: 60px;
color: white;
border-radius: 50%;
align-items: center; align-items: center;
margin-bottom: 20px; justify-content: center;
} }
.statistics-info { .app-icon-img {
padding: 8px 15px; width: 60px;
height: 60px;
border-radius: 50%;
object-fit: cover;
}
.app-name {
overflow: hidden;
font-size: 14px; font-size: 14px;
color: #666; font-weight: 500;
background: #f5f7fa; text-overflow: ellipsis;
border: 1px solid #e6e6e6; white-space: nowrap;
border-radius: 4px;
} }
.table-container { /* 对话框样式 */
padding: 20px; ::v-deep .app-dialog .el-dialog__body {
background: white; padding: 30px 20px;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
} }
.positive { .dialog-content {
color: #67c23a;
}
.negative {
color: #f56c6c;
}
.neutral {
color: #909399;
}
.chart-container {
padding: 20px;
margin-top: 20px;
background: white;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
}
:deep(.el-table .cell) {
padding: 4px 8px;
}
:deep(.el-table th) {
font-weight: 600;
background-color: #f8f9fa;
}
:deep(.el-table__footer-wrapper .el-table__cell) {
font-weight: bold;
background-color: #f0f9ff;
}
:deep(.el-table__footer-wrapper .positive) {
color: #67c23a;
}
:deep(.el-table__footer-wrapper .negative) {
color: #f56c6c;
}
:deep(.el-table__footer-wrapper .neutral) {
color: #909399;
}
/* 维度选择提示 */
.dimension-hint {
margin-top: 10px;
font-size: 12px;
color: #909399;
text-align: center; text-align: center;
} }
.dialog-icon-wrapper {
display: flex;
width: 80px;
height: 80px;
margin: 0 auto 20px;
font-size: 36px;
line-height: 80px;
color: white;
border-radius: 50%;
align-items: center;
justify-content: center;
}
.dialog-app-icon-img {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
}
.dialog-app-name {
margin: 0 0 10px;
font-size: 1.5rem;
}
.dialog-app-id,
.dialog-app-category {
margin: 5px 0;
color: #909399;
}
.dialog-footer {
text-align: right;
}
</style> </style>

View File

@@ -183,6 +183,43 @@ export const useRenderVxeColumn = (useType = 'table') => {
) )
} }
}, },
LowSelectMultiple: {
default: (renderOpts, { row, column }, isStop = false) => {
const { dicData } = renderOpts
const value = row[column.field]
const valStr=dicData.filter(item=>value.includes(item.value)).map(item=>item.label).join('')
return <span>{valStr}</span>
},
edit: (renderOpts, { row, rowIndex, column }) => {
const { multiple, filterable, allowCreate, typeKey ,dicData} = renderOpts
interface DictItem {
label: string;
value: string | number;
[key: string]: any; // 兼容其他可能的字段
}
return (
<el-select
popper-class="vxe-table--ignore-clear"
v-model={row[column.field]}
placeholder={'请选择 ' + column.title}
multiple={multiple}
filterable={filterable}
collapseTags={true}
collapseTagsTooltip={true}
allowCreate={allowCreate}
clearable={true}
>
{dicData.map((item: DictItem, index: number) => (
<el-option
key={index} // 建议用 item.value 作为 key更稳定
label={item.label}
value={item.value}
/>
))}
</el-select>
)
}
},
LowSummaryBottomSql: { LowSummaryBottomSql: {
default: (renderOpts, { row, column }, isStop = false) => { default: (renderOpts, { row, column }, isStop = false) => {
const { dicObj } = renderOpts const { dicObj } = renderOpts
@@ -266,32 +303,6 @@ export const useRenderVxeColumn = (useType = 'table') => {
</el-popover> </el-popover>
) )
} }
},
LowButton: {
default: (renderOpts, { row, column }) => {
const { buttonText, disabled } = renderOpts
const isDisabled = typeof disabled === 'function' ? disabled(row) : disabled
if (isDisabled) return <span style="color: #c0c4cc;">-</span>
return <span style="color: #409eff; cursor: pointer;">{buttonText || '操作'}</span>
},
edit: (renderOpts, { row, column }) => {
const { buttonText, buttonType, buttonSize, disabled } = renderOpts
const isDisabled = typeof disabled === 'function' ? disabled(row) : disabled
return (
<el-button
type={buttonType || 'primary'}
size={buttonSize || 'small'}
disabled={isDisabled}
onClick={() => {
if (renderOpts.events && renderOpts.events.click) {
renderOpts.events.click(row)
}
}}
>
{buttonText || '操作'}
</el-button>
)
}
} }
} }
for (const key in lowControl) { for (const key in lowControl) {

View File

@@ -40,65 +40,6 @@
</template> </template>
</template> </template>
</DesignPopup> </DesignPopup>
<!-- 固定列行配置弹窗 -->
<DesignPopup
v-model="fixedColumnDialog.visible"
title="配置固定列内容"
width="600px"
:is-footer="true"
:handleClose="handleFixedColumnClose"
>
<template #default>
<div style="padding: 20px;">
<el-alert
title="提示"
type="info"
:closable="false"
style="margin-bottom: 20px;"
>
为该固定列的每一行配置不同的显示内容点击"添加行"按钮增加新行输入内容后保存
</el-alert>
<div style="margin-bottom: 15px;">
<ElButton type="primary" @click="addFixedColumnRow" size="small">
<Icon icon="ep:plus" />
添加行
</ElButton>
</div>
<div v-if="fixedColumnDialog.rows.length === 0" style="text-align: center; padding: 40px; color: #909399;">
暂无配置请点击"添加行"按钮添加内容
</div>
<div v-else style="max-height: 400px; overflow-y: auto;">
<div
v-for="(row, index) in fixedColumnDialog.rows"
:key="index"
style="display: flex; align-items: center; margin-bottom: 10px;"
>
<span style="width: 80px; color: #606266;"> {{ index + 1 }} </span>
<ElInput
v-model="row.value"
placeholder="请输入该行显示的内容"
style="flex: 1; margin-right: 10px;"
/>
<ElButton
type="danger"
size="small"
@click="removeFixedColumnRow(index)"
:icon="Delete"
>
删除
</ElButton>
</div>
</div>
</div>
</template>
<template #footer>
<ElButton @click="handleFixedColumnCancel"> </ElButton>
<ElButton type="primary" @click="handleFixedColumnSave"> </ElButton>
</template>
</DesignPopup>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -108,7 +49,6 @@ import { tableInfoOption, dicObj } from '../designData'
import { formattingLengStr } from '@/utils/lowDesign' import { formattingLengStr } from '@/utils/lowDesign'
import { cloneDeep } from 'lodash-es' import { cloneDeep } from 'lodash-es'
import * as DictDataApi from '@/api/system/dict/dict.type' import * as DictDataApi from '@/api/system/dict/dict.type'
import { Delete } from '@element-plus/icons-vue'
defineOptions({ name: 'TableInfo' }) defineOptions({ name: 'TableInfo' })
const message = useMessage() const message = useMessage()
@@ -146,17 +86,10 @@ const tabsRef = ref()
// 父字段引用 // 父字段引用
const parentFieldMap = ref(new Map()) const parentFieldMap = ref(new Map())
// 固定列行配置弹窗
const fixedColumnDialog = ref({
visible: false,
currentRow: null as any,
rows: [] as Array<{ value: string }>,
originalValue: '' // 保存原始值,用于取消时恢复
})
const fieldList = computed(() => { const fieldList = computed(() => {
let dicData: Array<{ label: string; value: string; type: string }> = [] let dicData: Array<{ label: string; value: string; type: string }> = []
infoData.value.basics.forEach((item) => { infoData.value.basics.forEach((item) => {
if (item.fieldCode && item.isDb == 'Y') if (item.fieldCode && item.isDb == 'Y')
dicData.push({ dicData.push({
label: `${item.fieldCode}${item.fieldName ? '' + item.fieldName + '' : ''}`, label: `${item.fieldCode}${item.fieldName ? '' + item.fieldName + '' : ''}`,
@@ -291,61 +224,6 @@ const addSubFieldRow = (parentField, parentRowIndex) => {
}) })
} }
// 打开固定列行配置弹窗
const openFixedColumnDialog = (row) => {
fixedColumnDialog.value.currentRow = row
// 保存原始值
fixedColumnDialog.value.originalValue = row.fixedColumnValue || ''
// 将字符串转换为数组:按逗号分割
let rowsArray = []
if (row.fixedColumnValue && typeof row.fixedColumnValue === 'string' && row.fixedColumnValue.trim() !== '') {
rowsArray = row.fixedColumnValue.split(',').map(value => ({ value: value }))
}
fixedColumnDialog.value.rows = rowsArray
fixedColumnDialog.value.visible = true
}
// 添加固定列行
const addFixedColumnRow = () => {
fixedColumnDialog.value.rows.push({ value: '' })
}
// 删除固定列行
const removeFixedColumnRow = (index) => {
fixedColumnDialog.value.rows.splice(index, 1)
}
// 取消固定列配置
const handleFixedColumnCancel = () => {
// 恢复原始值
if (fixedColumnDialog.value.currentRow) {
fixedColumnDialog.value.currentRow.fixedColumnValue = fixedColumnDialog.value.originalValue
}
fixedColumnDialog.value.visible = false
}
// 处理弹窗关闭X按钮或遮罩层
const handleFixedColumnClose = (done: () => void) => {
// 恢复原始值
if (fixedColumnDialog.value.currentRow) {
fixedColumnDialog.value.currentRow.fixedColumnValue = fixedColumnDialog.value.originalValue
}
done()
}
// 保存固定列配置
const handleFixedColumnSave = () => {
if (fixedColumnDialog.value.currentRow) {
// 将数组转换为字符串:使用逗号拼接
const rowValues = fixedColumnDialog.value.rows.map(row => row.value)
fixedColumnDialog.value.currentRow.fixedColumnValue = rowValues.join(',')
message.success(`已保存固定列"${fixedColumnDialog.value.currentRow.fieldName}"的行配置(共${fixedColumnDialog.value.rows.length}行)`)
}
fixedColumnDialog.value.visible = false
}
const initEditInfoData = () => { const initEditInfoData = () => {
const data = tableInfoOption.formattingInitData(props.editInfoData) const data = tableInfoOption.formattingInitData(props.editInfoData)
const fieldList: any[] = [] const fieldList: any[] = []
@@ -380,7 +258,9 @@ const initEditInfoData = () => {
if (fieldItem.hasChildren === 'Y') { if (fieldItem.hasChildren === 'Y') {
fieldItem.hasChildren = 'Y' fieldItem.hasChildren = 'Y'
} }
if (!!fieldItem.isHideDimension&&Object.prototype.toString.call(fieldItem.isHideDimension) == '[object String]') {
fieldItem.isHideDimension=fieldItem.isHideDimension.split(',')
}
fieldList.push(fieldItem) fieldList.push(fieldItem)
}) })
@@ -405,12 +285,14 @@ onMounted(() => {
}) })
} }
} }
tableInfoOption.infoColumn.fieldColumn.isHideDimension.editRender.dicData = infoData.value.basics.map(({fieldCode,fieldName})=>{
return {label:fieldName,value:fieldCode}
})
// 设置父字段下拉选项 // 设置父字段下拉选项
const updateParentFieldOptions = () => { const updateParentFieldOptions = () => {
// 获取所有parentFieldCode为空且非固定列的字段作为可选父字段 // 获取所有parentFieldCode为空的字段作为可选父字段
const parentFieldOptions = infoData.value.basics const parentFieldOptions = infoData.value.basics
.filter(field => !field.parentFieldCode && field.fieldCode && field.fieldName && field.isFixedColumn !== 'Y') .filter(field => !field.parentFieldCode && field.fieldCode && field.fieldName)
.map(field => ({ .map(field => ({
label: `${field.fieldName}`, label: `${field.fieldName}`,
value: field.fieldCode value: field.fieldCode
@@ -584,41 +466,6 @@ onMounted(() => {
// 初始化父字段选项 // 初始化父字段选项
updateParentFieldOptions() updateParentFieldOptions()
// 添加固定列选项的change事件处理
tableInfoOption.infoColumn.fieldColumn.isFixedColumn.editRender.events = {
change: (row) => {
if (row.isFixedColumn === 'Y') {
// 固定列不能有父字段
if (row.parentFieldCode) {
handleParentFieldClear(row)
}
// 固定列不能有子字段
if (row.hasChildren === 'Y') {
row.hasChildren = 'N'
// 清除所有子字段的父子关系
infoData.value.basics.forEach(field => {
if (field.parentFieldCode === row.fieldCode) {
handleParentFieldClear(field)
}
})
}
} else {
// 取消固定列时清空固定列内容
row.fixedColumnValue = ''
}
updateParentFieldOptions()
}
}
// 添加固定列内容配置按钮的点击事件
tableInfoOption.infoColumn.fieldColumn.fixedColumnValue.editRender.events = {
click: (row) => {
if (row.isFixedColumn === 'Y') {
openFixedColumnDialog(row)
}
}
}
// 设置字典Code的下拉选项 // 设置字典Code的下拉选项
DictDataApi.getSimpleDictTypeList().then((dicData) => { DictDataApi.getSimpleDictTypeList().then((dicData) => {
const dicObj = {} const dicObj = {}

View File

@@ -152,18 +152,17 @@ const infoColumn = {
parentFieldName: { title: '父字段名称', width: 120, editRender: { name: 'LowSelect', dicData: [], filterable: true, clearable: true } }, parentFieldName: { title: '父字段名称', width: 120, editRender: { name: 'LowSelect', dicData: [], filterable: true, clearable: true } },
labelI18n: { title: '国际化配置', width: 140, editRender: { name: 'LowMonacoEditorInput', events: {} } }, labelI18n: { title: '国际化配置', width: 140, editRender: { name: 'LowMonacoEditorInput', events: {} } },
fieldType: { title: '字段类型', minWidth: 100, editRender: { name: 'LowSelect', verifyEdit: true, dicData: dicObj.fieldType, dicObj: getDicObj('fieldType') } }, fieldType: { title: '字段类型', minWidth: 100, editRender: { name: 'LowSelect', verifyEdit: true, dicData: dicObj.fieldType, dicObj: getDicObj('fieldType') } },
isFixedColumn: { title: '固定列', width: 75, align: "center", editRender: { name: 'LowCheckbox' } },
fixedColumnValue: { title: '固定列内容', width: 110, align: "center", editRender: { name: 'LowButton', disabled: (row) => row.isFixedColumn !== 'Y', buttonText: '配置内容', buttonType: 'primary', buttonSize: 'small', events: {} } },
queryIsDb: { title: '接口查询', width: 75, align: "center", editRender: { name: 'LowCheckbox' } }, queryIsDb: { title: '接口查询', width: 75, align: "center", editRender: { name: 'LowCheckbox' } },
queryIsWeb: { title: '查询控件', width: 75, align: "center", editRender: { name: 'LowCheckbox' } }, queryIsWeb: { title: '查询控件', width: 75, align: "center", editRender: { name: 'LowCheckbox' } },
queryMode: { title: '查询模式', width: 130, editRender: { name: 'LowSelect', verifyEdit: true, dicData: dicObj.queryMode, dicObj: getDicObj('queryMode') } }, queryMode: { title: '查询模式', width: 130, editRender: { name: 'LowSelect', verifyEdit: true, dicData: dicObj.queryMode, dicObj: getDicObj('queryMode') } },
dictCode: { title: '字典Code', width: 180, editRender: { name: 'LowSelect', verifyEdit: true, filterable: true, noStop: true, dicData: [] } }, dictCode: { title: '字典Code', width: 180, editRender: { name: 'LowSelect', verifyEdit: true, filterable: true, noStop: true, dicData: [] } },
isExport: { title: '是否可导出', width: 90, align: "center", editRender: { name: 'LowCheckbox' } }, isExport: { title: '是否可导出', width: 90, align: "center", editRender: { name: 'LowCheckbox' } },
isHideCol: { title: '隐藏列', width: 90, align: "center", editRender: { name: 'LowCheckbox' } },
isAmount: { title: '是否合计', width: 75, align: "center", editRender: { name: 'LowCheckboxSum' } }, isAmount: { title: '是否合计', width: 75, align: "center", editRender: { name: 'LowCheckboxSum' } },
isDimension: { title: '是否维度', width: 75, align: "center", editRender: { name: 'LowCheckbox' } }, isDimension: { title: '是否维度', width: 75, align: "center", editRender: { name: 'LowCheckbox' } },
isHideDimension: { title: '维度隐藏列', width: 180, editRender: { name: 'LowSelectMultiple', verifyEdit: true, filterable: true, multiple:true,dicData: [] } },
isShowSort: { title: '是否排序', width: 75, align: "center", editRender: { name: 'LowCheckbox' } }, isShowSort: { title: '是否排序', width: 75, align: "center", editRender: { name: 'LowCheckbox' } },
isDynamicGroup: { title: '动态分组', width: 75, align: "center", editRender: { name: 'LowCheckbox' } },
hasChildren: { title: '子字段', width: 90, align: "center", editRender: { name: 'LowButton', disabled: (row) => row.isSubField === true, buttonText: '添加子字段', buttonType: 'primary', buttonSize: 'small' } }, hasChildren: { title: '子字段', width: 90, align: "center", editRender: { name: 'LowButton', disabled: (row) => row.isSubField === true, buttonText: '添加子字段', buttonType: 'primary', buttonSize: 'small' } },
}, },
} }
@@ -182,10 +181,6 @@ for (const key in infoColumn) {
if (!keys.includes('hasChildren')) { if (!keys.includes('hasChildren')) {
keys.push('hasChildren') keys.push('hasChildren')
} }
// 确保固定列内容字段被包含
if (!keys.includes('fixedColumnValue')) {
keys.push('fixedColumnValue')
}
} }
infoApiKey[apiKey[key]] = keys infoApiKey[apiKey[key]] = keys
} }
@@ -194,7 +189,7 @@ for (const key in infoColumn) {
//默认值 //默认值
const infoDefaultData = { const infoDefaultData = {
basics: { basics: {
fieldCode: '', fieldName: '', parentFieldName: '', labelI18n: '', fieldType: 'String', isFixedColumn: 'N', fixedColumnValue: '', queryIsDb: 'N', queryIsWeb: 'N', queryMode: 'LIKE', dictCode: '', isExport: 'Y', isShowSort: 'N', isAmount: '', isDimension: '', isDynamicGroup: 'N', hasChildren: 'N', isSubField: false, parentFieldId: '', parentFieldCode: '', fieldCode: '', fieldName: '', parentFieldName: '', labelI18n: '', fieldType: 'String', queryIsDb: 'N', queryIsWeb: 'N', queryMode: 'LIKE', dictCode: '',isHideCol:'N', isExport: 'Y', isShowSort: 'N', isAmount: '', isDimension: '',isHideDimension:'', hasChildren: 'N', isSubField: false, parentFieldId: '', parentFieldCode: '',
}, },
} }

View File

@@ -349,7 +349,7 @@ const tableFormVerify = (type) => {
const index = Number(i) const index = Number(i)
const item = filedData[index] const item = filedData[index]
item.sortNum = index + 1 item.sortNum = index + 1
item.isHideDimension?item.isHideDimension=item.isHideDimension.join(','):''
let messageText = '' let messageText = ''
let tabKey = 'mysql' let tabKey = 'mysql'
// 子字段不能再包含子字段 // 子字段不能再包含子字段

View File

@@ -32,7 +32,46 @@
</el-tag> </el-tag>
</template> </template>
<!-- 表单 --> <!-- 表单 -->
<template #scopes-form="scope"> <template #category-form>
<el-select
v-model="tableForm.category"
filterable
placeholder="请输入应用分类"
style="width: 100%"
>
<el-option
v-for="item in getIntDictOptions(DICT_TYPE.APP_CATEGORY)"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
<template #authorizedGrantTypes-form>
<el-select
v-model="tableForm.authorizedGrantTypes"
filterable
multiple
allow-create
placeholder="请输入授权类型"
style="width: 100%"
>
<el-option v-for="item in getDictOptions(DICT_TYPE.SYSTEM_OAUTH2_GRANT_TYPE)" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</template>
<template #authorities-form>
<el-select
v-model="tableForm.authorities"
filterable
multiple
allow-create
placeholder="请输入权限"
style="width: 100%"
>
<el-option v-for="item in getDictOptions(DICT_TYPE.APP_QX)" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</template>
<template #scopes-form>
<el-select <el-select
v-model="tableForm.scopes" v-model="tableForm.scopes"
filterable filterable
@@ -41,20 +80,21 @@
placeholder="请输入授权范围" placeholder="请输入授权范围"
style="width: 100%" style="width: 100%"
> >
<el-option v-for="item in scope.value" :key="item" :label="item" :value="item" /> <el-option v-for="item in [{
label: 'user.read',
value: 'user.read'
},{
label: 'user.write',
value: 'user.write'
}]" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
</template> </template>
<template #redirectUris-form="scope"> <template #category="scope">
<el-select <dict-tag
v-model="tableForm.redirectUris" v-if="scope.row.category !== undefined"
filterable :type="DICT_TYPE.APP_CATEGORY"
multiple :value="scope.row.category"
allow-create />
placeholder="请输入可重定向的 URI 地址"
style="width: 100%"
>
<el-option v-for="item in scope.value" :key="item" :label="item" :value="item" />
</el-select>
</template> </template>
<template #status="scope"> <template #status="scope">
<dict-tag <dict-tag
@@ -105,9 +145,14 @@ const tableOption = reactive({
name: { name: {
label: '应用名', label: '应用名',
search: true, search: true,
minWidth: 100, span: 12,
rules: [{ required: true, message: '应用名不能为空', trigger: 'blur' }] rules: [{ required: true, message: '应用名不能为空', trigger: 'blur' }]
}, },
category: {
label: '应用分类',
span: 12,
rules: [{ required: true, message: '应用分类不能为空', trigger: 'blur' }]
},
logo: { logo: {
label: '应用图标', label: '应用图标',
span: 24, span: 24,
@@ -173,7 +218,7 @@ const tableOption = reactive({
span: 12, span: 12,
hide: true, hide: true,
control: (val) => { control: (val) => {
let dicData = [] let dicData = ["user.read","user.write"]
if (val?.length) { if (val?.length) {
dicData = val.map((item) => { dicData = val.map((item) => {
return { label: item, value: item } return { label: item, value: item }
@@ -190,12 +235,10 @@ const tableOption = reactive({
span: 12, span: 12,
multiple: true, multiple: true,
hide: true, hide: true,
dicData: [] dicData: ["user.read","user.write"]
}, },
redirectUris: { redirectUris: {
label: '可重定向的 URI 地址', label: '可重定向的 URI 地址',
type: 'select',
multiple: true,
span: 12, span: 12,
hide: true, hide: true,
rules: [{ required: true, message: '可重定向的 URI 地址不能为空', trigger: 'blur' }] rules: [{ required: true, message: '可重定向的 URI 地址不能为空', trigger: 'blur' }]
@@ -203,12 +246,20 @@ const tableOption = reactive({
authorities: { authorities: {
label: '权限', label: '权限',
span: 12, span: 12,
multiple: true,
hide: true,
type: 'select'
},
callbackUris: {
label: '回调地址',
span: 12,
hide: true hide: true
}, },
resourceIds: { resourceIds: {
label: '资源', label: '资源',
span: 12, span: 12,
hide: true hide: true,
type: 'array'
}, },
additionalInformation: { additionalInformation: {
label: '附加信息', label: '附加信息',

View File

@@ -0,0 +1,233 @@
<template>
<DesignPopup v-model="dialogVisible" title="应用权限" :is-footer="true" width="40%">
<div class="p-20px">
<el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
<el-form-item label="角色名称">
<el-tag>{{ formData.name }}</el-tag>
</el-form-item>
<el-form-item label="角色标识">
<el-tag>{{ formData.code }}</el-tag>
</el-form-item>
<el-form-item label="应用权限">
<el-card class="w-full h-400px !overflow-y-scroll" shadow="never">
<template #header>
全选/全不选:
<el-switch
v-model="treeNodeAll"
active-text=""
inactive-text=""
inline-prompt
@change="handleCheckedTreeNodeAll"
/>
</template>
<!-- 修复移除未使用的node变量优化无子集Tree展示 -->
<el-tree
ref="treeRef"
:data="appOptions"
:props="{ label: 'name', children: 'children' }"
empty-text="暂无应用数据"
node-key="id"
show-checkbox
check-strictly
v-loading="tableLoading"
class="app-tree-list"
:indent="0"
>
<!-- 修复只解构使用到的data删除未使用的node -->
<template #default="{ data }">
<div class="app-tree-node">
<span class="node-name">{{ data.name }}</span>
<span class="node-clientId">客户端ID{{ data.clientId }}</span>
<span class="node-status">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="data.status" />
</span>
</div>
</template>
</el-tree>
</el-card>
</el-form-item>
</el-form>
</div>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</DesignPopup>
</template>
<script lang="ts" setup>
import { DICT_TYPE } from '@/utils/dict'
import * as RoleApi from '@/api/system/role'
defineOptions({ name: 'SystemRoleAssignAppForm' })
const { t } = useI18n()
const message = useMessage()
const dialogVisible = ref(false)
const formLoading = ref(false)
const tableLoading = ref(false)
const formData = ref({
id: 0,
name: '',
code: '',
clientIds: [] as number[]
})
const formRef = ref()
const treeRef = ref()
const appOptions = ref<any[]>([])
const treeNodeAll = ref(false)
// 优化Tree配置明确指定无children纯列表展示
const treeProps = {
label: 'name',
children: () => [] // 强制返回空数组,彻底避免树形层级
}
/** 打开弹窗 */
const open = async (row: RoleApi.RoleVO) => {
dialogVisible.value = true
formLoading.value = true
resetForm()
// 设置角色信息
formData.value.id = row.id
formData.value.name = row.name
formData.value.code = row.code
try {
// 加载应用列表pageSize=99模拟不分页
await getAppList()
// 获取当前角色已有的应用ID
const roleAppIds = await RoleApi.getRoleAppIds(row.id)
formData.value.clientIds = roleAppIds
// 设置Tree选中状态
await nextTick()
treeRef.value?.setCheckedNodes([])
formData.value.clientIds.forEach(id => {
treeRef.value?.setChecked(id, true, false)
})
} finally {
formLoading.value = false
}
}
defineExpose({ open })
/** 获取应用列表固定pageSize=99不分页 */
const getAppList = async () => {
tableLoading.value = true
try {
const data = await RoleApi.getMyPage({
pageNo: 1,
pageSize: 99 // 设为99覆盖大部分场景的数量模拟不分页
})
// 优化确保返回数据绝对没有children字段避免树形展示
appOptions.value = data.list.map(item => {
const { children, ...rest } = item // 移除可能存在的children字段
return { ...rest, children: [] } // 强制设置空children
})
} finally {
tableLoading.value = false
}
}
/** 全选/全不选适配Tree组件 */
const handleCheckedTreeNodeAll = () => {
if (treeNodeAll.value) {
// 全选:选中所有节点
treeRef.value?.setCheckedNodes(appOptions.value)
formData.value.clientIds = appOptions.value.map(item => item.id)
} else {
// 全不选:清空选中
treeRef.value?.setCheckedNodes([])
formData.value.clientIds = []
}
}
/** 提交表单 */
const emit = defineEmits(['success'])
const submitForm = async () => {
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
formLoading.value = true
try {
// 获取Tree选中的节点ID
formData.value.clientIds = treeRef.value?.getCheckedKeys(false) as number[]
const data = {
roleId: formData.value.id,
clientIds: formData.value.clientIds
}
await RoleApi.saveApp(data)
message.success(t('common.updateSuccess'))
dialogVisible.value = false
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
treeNodeAll.value = false
formData.value = {
id: 0,
name: '',
code: '',
clientIds: []
}
appOptions.value = []
treeRef.value?.setCheckedNodes([])
formRef.value?.resetFields()
}
</script>
<style lang="scss" scoped>
// 自定义Tree节点样式模拟表格列布局
.app-tree-list {
::v-deep(.el-tree-node) {
height: 40px;
line-height: 40px;
border-bottom: 1px solid #f5f7fa;
}
::v-deep(.el-tree-node__content) {
padding: 0 10px;
height: 40px !important;
// 移除树形节点的默认图标
.el-tree-node__expand-icon {
display: none;
}
}
::v-deep(.el-tree-node__children) {
padding-left: 0 !important; // 彻底去掉子节点缩进
}
}
// 节点内容布局
.app-tree-node {
display: flex;
align-items: center;
width: 100%;
.node-name {
flex: 0 0 120px;
font-weight: 500;
}
.node-clientId {
flex: 0 0 150px;
color: #666;
}
.node-status {
flex: 0 0 80px;
text-align: center;
}
}
// 卡片样式优化
::v-deep(.el-card__header) {
padding: 10px 20px;
}
::v-deep(.el-card__body) {
padding: 10px;
}
</style>

View File

@@ -58,6 +58,15 @@
<span>菜单权限</span> <span>菜单权限</span>
</div> </div>
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item
:command="{ type: 'app', row }"
v-if="checkPermi(['system:permission:assign-role-app'])"
>
<div class="flex items-center">
<Icon icon="ep:menu" />
<span>应用权限</span>
</div>
</el-dropdown-item>
<el-dropdown-item <el-dropdown-item
:command="{ type: 'data', row }" :command="{ type: 'data', row }"
v-if="checkPermi(['system:permission:assign-role-data-scope'])" v-if="checkPermi(['system:permission:assign-role-data-scope'])"
@@ -86,6 +95,8 @@
<RoleAssignMenuForm ref="assignMenuFormRef" @success="getTableData" /> <RoleAssignMenuForm ref="assignMenuFormRef" @success="getTableData" />
<!-- 表单弹窗数据权限 --> <!-- 表单弹窗数据权限 -->
<RoleDataPermissionForm ref="dataPermissionFormRef" @success="getTableData" /> <RoleDataPermissionForm ref="dataPermissionFormRef" @success="getTableData" />
<!-- 表单弹窗应用权限 -->
<RoleAssignAppForm ref="assignAppFormRef" @success="getTableData" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
@@ -96,6 +107,7 @@ import * as RoleApi from '@/api/system/role'
import { CommonStatusEnum } from '@/utils/constants' import { CommonStatusEnum } from '@/utils/constants'
import RoleAssignMenuForm from './RoleAssignMenuForm.vue' import RoleAssignMenuForm from './RoleAssignMenuForm.vue'
import RoleDataPermissionForm from './RoleDataPermissionForm.vue' import RoleDataPermissionForm from './RoleDataPermissionForm.vue'
import RoleAssignAppForm from './RoleAssignAppForm.vue'
defineOptions({ name: 'SystemRole' }) defineOptions({ name: 'SystemRole' })
@@ -186,7 +198,7 @@ const tablePage = ref({
total: 0 total: 0
}) })
const permission = getCurrPermi(['system:role']) const permission = getCurrPermi(['system:role'])
const assignAppFormRef = ref()
const crudRef = ref() const crudRef = ref()
useCrudHeight(crudRef) useCrudHeight(crudRef)
@@ -197,6 +209,11 @@ const menuHandle = ({ row, type }) => {
if (type == 'menu') openAssignMenuForm(row) if (type == 'menu') openAssignMenuForm(row)
else if (type == 'data') openDataPermissionForm(row) else if (type == 'data') openDataPermissionForm(row)
else if (type == 'del') rowDel(row) else if (type == 'del') rowDel(row)
else if (type == 'app') openAssignAppForm(row)
}
const openAssignAppForm = (row) => {
assignAppFormRef.value.open(row)
} }
/** 查询列表 */ /** 查询列表 */