Compare commits
8 Commits
cd2b745336
...
main_beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cdabba4da | ||
|
|
d7def83ee4 | ||
|
|
34f7c68041 | ||
| 9e210114b6 | |||
| 8185399f8f | |||
| 7f62a5b191 | |||
|
|
59c51ca360 | ||
|
|
7a7df06d66 |
@@ -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 })
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ const amountObj=ref<any>({})
|
|||||||
const crudRef = ref()
|
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 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(() => {
|
||||||
@@ -167,7 +168,11 @@ const summaryMethod1=({columns,data})=>{
|
|||||||
const initTable = async () => {
|
const initTable = async () => {
|
||||||
isInit.value = false
|
isInit.value = false
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const { fieldList, reportVo } = await ReportApi.getWebConfig(props.reportCode)
|
const { fieldList: apiFieldList, reportVo } = await ReportApi.getWebConfig(props.reportCode)
|
||||||
|
|
||||||
|
// 存储字段列表到响应式引用
|
||||||
|
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')
|
||||||
@@ -188,12 +193,37 @@ const initTable = async () => {
|
|||||||
column: {}
|
column: {}
|
||||||
}
|
}
|
||||||
//国际化处理
|
//国际化处理
|
||||||
const fieldLengObj = assembleLengObj(fieldList, '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] })
|
||||||
}
|
}
|
||||||
//字段处理
|
|
||||||
fieldList.forEach((item,index) => {
|
// 根据parentFieldCode判断表头结构
|
||||||
|
const hasSubFields = Array.isArray(apiFieldList) && apiFieldList.some(field => field.parentFieldCode && field.parentFieldCode !== '')
|
||||||
|
|
||||||
|
// 构建父子关系映射
|
||||||
|
const parentChildMap = new Map()
|
||||||
|
const childParentMap = new Map()
|
||||||
|
|
||||||
|
// 建立映射关系
|
||||||
|
apiFieldList.forEach(field => {
|
||||||
|
if (field.parentFieldCode && field.parentFieldCode !== '') {
|
||||||
|
// 这是子字段
|
||||||
|
childParentMap.set(field.fieldCode, field.parentFieldCode)
|
||||||
|
if (!parentChildMap.has(field.parentFieldCode)) {
|
||||||
|
parentChildMap.set(field.parentFieldCode, [])
|
||||||
|
}
|
||||||
|
parentChildMap.get(field.parentFieldCode).push(field.fieldCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
//字段处理 - 构建正确的表头结构
|
||||||
|
apiFieldList.forEach((item, index) => {
|
||||||
|
// 跳过子字段,子字段会在父字段中处理
|
||||||
|
if (item.parentFieldCode && item.parentFieldCode !== '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const config: any = {
|
const config: any = {
|
||||||
prop: item.fieldCode,
|
prop: item.fieldCode,
|
||||||
label: t(`${props.reportCode}.${item.fieldCode}`),
|
label: t(`${props.reportCode}.${item.fieldCode}`),
|
||||||
@@ -203,6 +233,24 @@ const initTable = async () => {
|
|||||||
sortable: item.isShowSort == 'Y' ? 'custom' : false,
|
sortable: item.isShowSort == 'Y' ? 'custom' : false,
|
||||||
search: item.queryIsWeb == 'Y',
|
search: item.queryIsWeb == 'Y',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果该字段有子字段,添加子列配置
|
||||||
|
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){
|
||||||
index==0?amountFieds.value.fistField=config.prop:''
|
index==0?amountFieds.value.fistField=config.prop:''
|
||||||
amountFieds.value[item.isAmount]=config
|
amountFieds.value[item.isAmount]=config
|
||||||
@@ -320,7 +368,31 @@ const getTableData = async (isLoading = true) => {
|
|||||||
try {
|
try {
|
||||||
const data = await ReportApi.getTableList(props.reportCode, searchObj)
|
const data = await ReportApi.getTableList(props.reportCode, searchObj)
|
||||||
if (tablePage.value) tablePage.value['total'] = data.total
|
if (tablePage.value) tablePage.value['total'] = data.total
|
||||||
tableData.value = data.records
|
|
||||||
|
// 处理包含子字段的数据
|
||||||
|
let processedData = data.records
|
||||||
|
// 根据字段配置判断是否需要处理子字段
|
||||||
|
const hasSubFields = Array.isArray(fieldList.value) && fieldList.value.length > 0 &&
|
||||||
|
fieldList.value.some(field => field.parentFieldCode && field.parentFieldCode !== '')
|
||||||
|
|
||||||
|
if (hasSubFields) {
|
||||||
|
processedData = data.records.map(record => {
|
||||||
|
const flatRecord = { ...record }
|
||||||
|
// 处理子字段数据
|
||||||
|
fieldList.value.forEach(field => {
|
||||||
|
if (field.parentFieldCode && field.parentFieldCode !== '') {
|
||||||
|
// 这是子字段,从父字段中提取数据
|
||||||
|
const parentData = record[field.parentFieldCode]
|
||||||
|
if (parentData && typeof parentData === 'object') {
|
||||||
|
flatRecord[field.fieldCode] = parentData[field.fieldCode]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return flatRecord
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
tableData.value = processedData
|
||||||
// amountFieds.value={
|
// amountFieds.value={
|
||||||
// 'thissaqty_s':{
|
// 'thissaqty_s':{
|
||||||
// prop:'thissaqty',
|
// prop:'thissaqty',
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -164,7 +164,21 @@ export const useRenderVxeColumn = (useType = 'table') => {
|
|||||||
multiple={multiple}
|
multiple={multiple}
|
||||||
filterable={filterable}
|
filterable={filterable}
|
||||||
allowCreate={allowCreate}
|
allowCreate={allowCreate}
|
||||||
onChange={() => renderOpts.events ? renderOpts.events.change(row, column.field, rowIndex) : ''}
|
clearable={true}
|
||||||
|
onChange={(value) => {
|
||||||
|
// 先更新行数据
|
||||||
|
row[column.field] = value
|
||||||
|
// 再触发自定义事件
|
||||||
|
if (renderOpts.events && renderOpts.events.change) {
|
||||||
|
renderOpts.events.change(row, column.field, rowIndex)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClear={() => {
|
||||||
|
// 触发清空事件
|
||||||
|
if (renderOpts.events && renderOpts.events.clear) {
|
||||||
|
renderOpts.events.clear(row, column.field, rowIndex)
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SqlOption, InfoVxeTable, InfoVxeTopBtn } from '../../tableDesign/components'
|
import { SqlOption, InfoVxeTable, InfoVxeTopBtn } from '../../tableDesign/components'
|
||||||
import { MonacoEditor } from '@/components/MonacoEditor/index'
|
import { MonacoEditor } from '@/components/MonacoEditor/index'
|
||||||
import { tableInfoOption } from '../designData'
|
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'
|
||||||
@@ -83,6 +83,9 @@ const optionComponents = markRaw({
|
|||||||
const optionRef = ref({})
|
const optionRef = ref({})
|
||||||
const tabsRef = ref()
|
const tabsRef = ref()
|
||||||
|
|
||||||
|
// 父字段引用
|
||||||
|
const parentFieldMap = ref(new Map())
|
||||||
|
|
||||||
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) => {
|
||||||
@@ -113,6 +116,7 @@ const dorpdownHandleCommand = (command) => {
|
|||||||
nextTick(() => infoData.value[dataKey].splice(type == 'up' ? index - 1 : index + 1, 0, delItem))
|
nextTick(() => infoData.value[dataKey].splice(type == 'up' ? index - 1 : index + 1, 0, delItem))
|
||||||
} else if (type == 'add') cellAddData(index + 1)
|
} else if (type == 'add') cellAddData(index + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const setInfoOrder = () => {
|
const setInfoOrder = () => {
|
||||||
let { dataKey } = tabsValue.value
|
let { dataKey } = tabsValue.value
|
||||||
if (!dataKey) return
|
if (!dataKey) return
|
||||||
@@ -182,16 +186,82 @@ const tableScrollIndex = (key, index, addIndex?) => {
|
|||||||
|
|
||||||
const cellClick = ({ rowIndex }) => {
|
const cellClick = ({ rowIndex }) => {
|
||||||
let { prop } = tabsValue.value
|
let { prop } = tabsValue.value
|
||||||
|
// 普通行点击进入编辑状态
|
||||||
tableRefObj.value[prop].vxeTableRef.setEditRow(
|
tableRefObj.value[prop].vxeTableRef.setEditRow(
|
||||||
tableRefObj.value[prop].vxeTableRef.getData(rowIndex)
|
tableRefObj.value[prop].vxeTableRef.getData(rowIndex)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加子字段行
|
||||||
|
const addSubFieldRow = (parentField, parentRowIndex) => {
|
||||||
|
let { key, dataKey } = tabsValue.value
|
||||||
|
if (!dataKey) return
|
||||||
|
|
||||||
|
// 创建子字段数据
|
||||||
|
let subFieldData = cloneDeep(tableInfoOption.infoDefaultData[dataKey]) || {}
|
||||||
|
subFieldData.parentFieldId = parentField._X_ROW_KEY
|
||||||
|
subFieldData.isSubField = true
|
||||||
|
subFieldData.parentFieldName = parentField.fieldName
|
||||||
|
|
||||||
|
parentField.hasChildren = 'Y'
|
||||||
|
// 确保子字段不能包含子字段
|
||||||
|
subFieldData.hasChildren = 'N'
|
||||||
|
|
||||||
|
// 在父字段下方插入子字段行
|
||||||
|
let insertIndex = parentRowIndex + 1
|
||||||
|
infoData.value[dataKey].splice(insertIndex, 0, subFieldData)
|
||||||
|
|
||||||
|
// 记录父子关系
|
||||||
|
parentFieldMap.value.set(subFieldData._X_ROW_KEY, parentField._X_ROW_KEY)
|
||||||
|
|
||||||
|
// 滚动到新添加的行并设置为编辑状态
|
||||||
|
tableScrollIndex(key, insertIndex, insertIndex)
|
||||||
|
|
||||||
|
// 给出成功提示
|
||||||
|
nextTick(() => {
|
||||||
|
message.success(`已为字段 "${parentField.fieldName}" 添加子字段`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const initEditInfoData = () => {
|
const initEditInfoData = () => {
|
||||||
const data = tableInfoOption.formattingInitData(props.editInfoData)
|
const data = tableInfoOption.formattingInitData(props.editInfoData)
|
||||||
const fieldList: any[] = []
|
const fieldList: any[] = []
|
||||||
|
|
||||||
|
// 重建父子字段关系
|
||||||
|
const fieldCodeMap = new Map()
|
||||||
|
|
||||||
|
// 第一遍遍历:建立字段编码映射
|
||||||
|
data.infoData.forEach((item, index) => {
|
||||||
|
if (item.fieldCode) {
|
||||||
|
fieldCodeMap.set(item.fieldCode, { ...item, originalIndex: index })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 第二遍遍历:处理子字段关系
|
||||||
|
data.infoData.forEach((item) => {
|
||||||
|
const fieldItem = cloneDeep(item)
|
||||||
|
|
||||||
|
// 如果是子字段且有父字段编码
|
||||||
|
if (fieldItem.parentFieldCode && fieldCodeMap.has(fieldItem.parentFieldCode)) {
|
||||||
|
fieldItem.isSubField = true
|
||||||
|
fieldItem.parentFieldId = fieldCodeMap.get(fieldItem.parentFieldCode)._X_ROW_KEY
|
||||||
|
// 设置父字段显示名称(只显示字段名称)
|
||||||
|
const parentField = fieldCodeMap.get(fieldItem.parentFieldCode)
|
||||||
|
fieldItem.parentFieldName = parentField.fieldName
|
||||||
|
|
||||||
|
// 记录父子关系
|
||||||
|
parentFieldMap.value.set(fieldItem._X_ROW_KEY, fieldItem.parentFieldId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果字段包含子字段,设置相应标识
|
||||||
|
if (fieldItem.hasChildren === 'Y') {
|
||||||
|
fieldItem.hasChildren = 'Y'
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldList.push(fieldItem)
|
||||||
|
})
|
||||||
|
|
||||||
tableInfoDefault.value = data.infoData.filter((item) => {
|
tableInfoDefault.value = data.infoData.filter((item) => {
|
||||||
fieldList.push(cloneDeep(item))
|
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
infoData.value.basics = fieldList
|
infoData.value.basics = fieldList
|
||||||
@@ -201,8 +271,9 @@ onMounted(() => {
|
|||||||
tableInfoDefault.value = []
|
tableInfoDefault.value = []
|
||||||
infoData.value.basics = []
|
infoData.value.basics = []
|
||||||
if (props.formType != 'add') initEditInfoData()
|
if (props.formType != 'add') initEditInfoData()
|
||||||
const { fieldColumn } = tableInfoOption.infoColumn
|
|
||||||
fieldColumn.labelI18n.editRender.events = {
|
// 设置字段国际化配置的点击事件
|
||||||
|
tableInfoOption.infoColumn.fieldColumn.labelI18n.editRender.events = {
|
||||||
click: (row) => {
|
click: (row) => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
tableRefObj.value['tab_field'].vxeTableRef.setRow(row, {
|
tableRefObj.value['tab_field'].vxeTableRef.setRow(row, {
|
||||||
@@ -211,14 +282,216 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置父字段下拉选项
|
||||||
|
const updateParentFieldOptions = () => {
|
||||||
|
// 获取所有parentFieldCode为空的字段作为可选父字段
|
||||||
|
const parentFieldOptions = infoData.value.basics
|
||||||
|
.filter(field => !field.parentFieldCode && field.fieldCode && field.fieldName)
|
||||||
|
.map(field => ({
|
||||||
|
label: `${field.fieldName}`,
|
||||||
|
value: field.fieldCode
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 更新父字段列的下拉选项
|
||||||
|
tableInfoOption.infoColumn.fieldColumn.parentFieldName.editRender.dicData = parentFieldOptions
|
||||||
|
}
|
||||||
|
// 用于跟踪上次选中的值,避免重复触发
|
||||||
|
let _lastSelectedValue = ''
|
||||||
|
|
||||||
|
// 父字段选择事件处理
|
||||||
|
tableInfoOption.infoColumn.fieldColumn.parentFieldName.editRender.events = {
|
||||||
|
// 简化的change事件处理
|
||||||
|
change: (row) => {
|
||||||
|
console.log('父字段选择change事件触发:', row.parentFieldName);
|
||||||
|
const selectdItem = row.parentFieldName.item;
|
||||||
|
|
||||||
|
if(!selectdItem) return;
|
||||||
|
|
||||||
|
// 获取选中的字段编码
|
||||||
|
let selectedParentCode = selectdItem.value;
|
||||||
|
console.log('选中的值:', selectedParentCode);
|
||||||
|
|
||||||
|
|
||||||
|
_lastSelectedValue = selectedParentCode;
|
||||||
|
|
||||||
|
if (selectedParentCode && selectedParentCode.trim() !== '') {
|
||||||
|
// 检查是否在有效选项中
|
||||||
|
const isValidOption = infoData.value.basics.some(field =>
|
||||||
|
field.fieldCode === selectedParentCode && !field.parentFieldCode
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isValidOption) {
|
||||||
|
handleParentFieldSelection(row, selectedParentCode);
|
||||||
|
} else {
|
||||||
|
console.log('无效的选项:', selectedParentCode);
|
||||||
|
// 重置无效选择
|
||||||
|
row.parentFieldName = '';
|
||||||
|
row.parentFieldCode = '';
|
||||||
|
row.parentFieldId = '';
|
||||||
|
row.isSubField = false;
|
||||||
|
_lastSelectedValue = '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 清空选择
|
||||||
|
handleParentFieldClear(row);
|
||||||
|
_lastSelectedValue = '';
|
||||||
|
}
|
||||||
|
// 更新父字段选项
|
||||||
|
updateParentFieldOptions();
|
||||||
|
},
|
||||||
|
// 添加clear事件专门处理清空操作
|
||||||
|
clear: (row) => {
|
||||||
|
console.log('父字段清空事件触发');
|
||||||
|
handleParentFieldClear(row);
|
||||||
|
_lastSelectedValue = '';
|
||||||
|
updateParentFieldOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理父字段选择
|
||||||
|
const handleParentFieldSelection = (row, selectedParentCode) => {
|
||||||
|
// 找到选中的父字段
|
||||||
|
const parentField = infoData.value.basics.find(field => field.fieldCode === selectedParentCode);
|
||||||
|
if (!parentField) return;
|
||||||
|
|
||||||
|
// 检查是否形成循环引用
|
||||||
|
if (checkCircularReference(row, parentField)) {
|
||||||
|
message.warning('不能形成循环引用!');
|
||||||
|
// 先清空显示值,再x用清空处理函数
|
||||||
|
handleParentFieldClear(row);
|
||||||
|
// 强制刷新表格以确保UI更新
|
||||||
|
nextTick(() => {
|
||||||
|
if (tableRefObj.value && tableRefObj.value['tab_field'] && tableRefObj.value['tab_field'].vxeTableRef) {
|
||||||
|
tableRefObj.value['tab_field'].vxeTableRef.refreshColumn();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存旧的父字段信息用于清理
|
||||||
|
const oldParentFieldCode = row.parentFieldCode;
|
||||||
|
|
||||||
|
// 设置当前字段的父子关系
|
||||||
|
row.parentFieldCode = selectedParentCode;
|
||||||
|
row.parentFieldId = parentField._X_ROW_KEY;
|
||||||
|
row.isSubField = true;
|
||||||
|
row.parentFieldName = parentField.fieldName; // 显示选中的字段名称
|
||||||
|
|
||||||
|
// 设置父字段的hasChildren为'Y'
|
||||||
|
parentField.hasChildren = 'Y';
|
||||||
|
|
||||||
|
// 清理旧父字段的关系
|
||||||
|
if (oldParentFieldCode && oldParentFieldCode !== selectedParentCode) {
|
||||||
|
const oldParentField = infoData.value.basics.find(field => field.fieldCode === oldParentFieldCode);
|
||||||
|
if (oldParentField) {
|
||||||
|
const hasOtherChildren = infoData.value.basics.some(child =>
|
||||||
|
child.parentFieldCode === oldParentFieldCode && child !== row
|
||||||
|
);
|
||||||
|
if (!hasOtherChildren) {
|
||||||
|
oldParentField.hasChildren = 'N';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message.success(`已将 "${row.fieldName}" 设置为 "${parentField.fieldName}" 的子字段`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理父字段清空
|
||||||
|
const handleParentFieldClear = (row) => {
|
||||||
|
const oldParentFieldCode = row.parentFieldCode;
|
||||||
|
|
||||||
|
// 清空所有父子关系相关字段
|
||||||
|
row.parentFieldCode = '';
|
||||||
|
row.parentFieldId = '';
|
||||||
|
row.parentFieldName = '';
|
||||||
|
row.isSubField = false;
|
||||||
|
|
||||||
|
// 清理旧父字段的关系
|
||||||
|
if (oldParentFieldCode) {
|
||||||
|
const oldParentField = infoData.value.basics.find(field => field.fieldCode === oldParentFieldCode);
|
||||||
|
if (oldParentField) {
|
||||||
|
const hasOtherChildren = infoData.value.basics.some(child =>
|
||||||
|
child.parentFieldCode === oldParentFieldCode && child !== row
|
||||||
|
);
|
||||||
|
if (!hasOtherChildren) {
|
||||||
|
oldParentField.hasChildren = 'N';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查循环引用
|
||||||
|
const checkCircularReference = (childField, parentField) => {
|
||||||
|
// 如果子字段本身就是父字段,则不允许
|
||||||
|
if (childField.fieldCode === parentField.fieldCode) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查父字段是否已经是该子字段的子字段
|
||||||
|
let currentParent = parentField
|
||||||
|
while (currentParent && currentParent.parentFieldCode) {
|
||||||
|
if (currentParent.parentFieldCode === childField.fieldCode) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
currentParent = infoData.value.basics.find(field => field.fieldCode === currentParent.parentFieldCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听数据变化,更新父字段选项
|
||||||
|
watch(() => infoData.value.basics, updateParentFieldOptions, { deep: true })
|
||||||
|
|
||||||
|
// 监听字段名称变化,同步更新父字段显示
|
||||||
|
watch(() => infoData.value.basics, (newBasics) => {
|
||||||
|
newBasics.forEach(field => {
|
||||||
|
if (field.parentFieldCode) {
|
||||||
|
const parentField = newBasics.find(f => f.fieldCode === field.parentFieldCode)
|
||||||
|
if (parentField) {
|
||||||
|
// 更新为被选择字段的fieldName
|
||||||
|
field.parentFieldName = parentField.fieldName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
updateParentFieldOptions()
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
// 初始化父字段选项
|
||||||
|
updateParentFieldOptions()
|
||||||
|
|
||||||
|
// 设置字典Code的下拉选项
|
||||||
DictDataApi.getSimpleDictTypeList().then((dicData) => {
|
DictDataApi.getSimpleDictTypeList().then((dicData) => {
|
||||||
const dicObj = {}
|
const dicObj = {}
|
||||||
dicData = dicData.map(({ type, name }) => {
|
dicData = dicData.map(({ type, name }) => {
|
||||||
dicObj[type] = `${type}(${name})`
|
dicObj[type] = `${type}(${name})`
|
||||||
return { label: dicObj[type], value: type }
|
return { label: dicObj[type], value: type }
|
||||||
})
|
})
|
||||||
Object.assign(fieldColumn.dictCode.editRender, { dicData, dicObj })
|
Object.assign(tableInfoOption.infoColumn.fieldColumn.dictCode.editRender, { dicData, dicObj })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 为主字段列添加点击事件处理添加子字段按钮
|
||||||
|
tableInfoOption.infoColumn.fieldColumn.hasChildren.editRender.events = {
|
||||||
|
click: (row) => {
|
||||||
|
// 子字段不显示按钮,这个检查主要是保险
|
||||||
|
if (row.isSubField === true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前行的索引
|
||||||
|
const currentIndex = infoData.value.basics.findIndex(item => item._X_ROW_KEY === row._X_ROW_KEY)
|
||||||
|
if (currentIndex !== -1) {
|
||||||
|
addSubFieldRow(row, currentIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态控制包含子字段选项的禁用状态
|
||||||
|
tableInfoOption.infoColumn.fieldColumn.hasChildren.editRender.disabled = (row) => {
|
||||||
|
// 子字段不能包含子字段
|
||||||
|
return row.isSubField === true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
@@ -228,7 +501,8 @@ defineExpose({
|
|||||||
tableRefObj,
|
tableRefObj,
|
||||||
setTabsValue,
|
setTabsValue,
|
||||||
tableScrollIndex,
|
tableScrollIndex,
|
||||||
initEditInfoData
|
initEditInfoData,
|
||||||
|
parentFieldMap
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -238,6 +512,22 @@ defineExpose({
|
|||||||
left: 20px !important;
|
left: 20px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 为子字段的子字段列添加特殊样式
|
||||||
|
::v-deep(.field-vxe-table) {
|
||||||
|
.vxe-body--column {
|
||||||
|
&[field="hasChildren"] {
|
||||||
|
.vxe-cell {
|
||||||
|
// 为子字段添加视觉提示
|
||||||
|
.is-sub-field {
|
||||||
|
text-align: center;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
::v-deep(.virtual-hide-row) {
|
::v-deep(.virtual-hide-row) {
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ const infoColumn = {
|
|||||||
fieldColumn: {
|
fieldColumn: {
|
||||||
fieldCode: { title: '字段编码', minWidth: 120, editRender: { name: 'LowInput', verifyEdit: true } },
|
fieldCode: { title: '字段编码', minWidth: 120, editRender: { name: 'LowInput', verifyEdit: true } },
|
||||||
fieldName: { title: '字段名称', minWidth: 120, editRender: { name: 'LowInput' } },
|
fieldName: { title: '字段名称', minWidth: 120, editRender: { name: 'LowInput' } },
|
||||||
|
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') } },
|
||||||
queryIsDb: { title: '接口查询', width: 75, align: "center", editRender: { name: 'LowCheckbox' } },
|
queryIsDb: { title: '接口查询', width: 75, align: "center", editRender: { name: 'LowCheckbox' } },
|
||||||
@@ -160,6 +161,7 @@ const infoColumn = {
|
|||||||
isDimension: { title: '是否维度', width: 75, align: "center", editRender: { name: 'LowCheckbox' } },
|
isDimension: { title: '是否维度', width: 75, align: "center", editRender: { name: 'LowCheckbox' } },
|
||||||
|
|
||||||
isShowSort: { title: '是否排序', width: 75, align: "center", editRender: { name: 'LowCheckbox' } },
|
isShowSort: { 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' } },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +170,16 @@ const apiKey = { fieldColumn: 'fieldList' }
|
|||||||
for (const key in infoColumn) {
|
for (const key in infoColumn) {
|
||||||
if (apiKey[key]) {
|
if (apiKey[key]) {
|
||||||
const keys = Object.keys(infoColumn[key])
|
const keys = Object.keys(infoColumn[key])
|
||||||
if (key == 'fieldColumn') keys.push('sortNum')
|
if (key == 'fieldColumn') {
|
||||||
|
keys.push('sortNum')
|
||||||
|
// 确保父子关系字段被包含
|
||||||
|
if (!keys.includes('parentFieldCode')) {
|
||||||
|
keys.push('parentFieldCode')
|
||||||
|
}
|
||||||
|
if (!keys.includes('hasChildren')) {
|
||||||
|
keys.push('hasChildren')
|
||||||
|
}
|
||||||
|
}
|
||||||
infoApiKey[apiKey[key]] = keys
|
infoApiKey[apiKey[key]] = keys
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,7 +187,7 @@ for (const key in infoColumn) {
|
|||||||
//默认值
|
//默认值
|
||||||
const infoDefaultData = {
|
const infoDefaultData = {
|
||||||
basics: {
|
basics: {
|
||||||
fieldCode: '', fieldName: '', labelI18n: '', fieldType: 'String', queryIsDb: 'N', queryIsWeb: 'N', queryMode: 'LIKE', dictCode: '', isExport: 'Y', isShowSort: 'N',isAmount:'',isDimension:''
|
fieldCode: '', fieldName: '', parentFieldName: '', labelI18n: '', fieldType: 'String', queryIsDb: 'N', queryIsWeb: 'N', queryMode: 'LIKE', dictCode: '', isExport: 'Y', isShowSort: 'N', isAmount: '', isDimension: '', hasChildren: 'N', isSubField: false, parentFieldId: '', parentFieldCode: '',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -330,6 +330,21 @@ const tableFormVerify = (type) => {
|
|||||||
filedData.splice(item.sortNum || 999, 0, item)
|
filedData.splice(item.sortNum || 999, 0, item)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 先进行hasChildren一致性检查
|
||||||
|
filedData.forEach(currentItem => {
|
||||||
|
if (currentItem.hasChildren === 'Y') {
|
||||||
|
// 检查是否存在子字段(parentFieldCode等于当前字段的fieldCode)
|
||||||
|
const hasChildFields = filedData.some(childItem =>
|
||||||
|
childItem.parentFieldCode === currentItem.fieldCode
|
||||||
|
)
|
||||||
|
|
||||||
|
// 如果没有子字段,则将hasChildren设为'N'
|
||||||
|
if (!hasChildFields) {
|
||||||
|
currentItem.hasChildren = 'N'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
for (const i in filedData) {
|
for (const i in filedData) {
|
||||||
const index = Number(i)
|
const index = Number(i)
|
||||||
const item = filedData[index]
|
const item = filedData[index]
|
||||||
@@ -337,12 +352,31 @@ const tableFormVerify = (type) => {
|
|||||||
|
|
||||||
let messageText = ''
|
let messageText = ''
|
||||||
let tabKey = 'mysql'
|
let tabKey = 'mysql'
|
||||||
|
// 子字段不能再包含子字段
|
||||||
|
if (item.isSubField && item.hasChildren === 'Y') {
|
||||||
|
debugger
|
||||||
|
messageText = `<div style="line-height:24px">
|
||||||
|
<div>子字段不能包含子字段</div>
|
||||||
|
<div>序号:${index + 1}</div>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
// 当不包含子字段时,字段编码和字段名称必须填写
|
||||||
|
else if (item.hasChildren !== 'Y') {
|
||||||
if (!item.fieldCode || !item.fieldName) {
|
if (!item.fieldCode || !item.fieldName) {
|
||||||
messageText = `<div style="line-height:24px">
|
messageText = `<div style="line-height:24px">
|
||||||
<div>${!item.fieldCode ? '字段编码' : '字段名称'}必须填写</div>
|
<div>${!item.fieldCode ? '字段编码' : '字段名称'}必须填写</div>
|
||||||
<div>序号:${index + 1}</div>
|
<div>序号:${index + 1}</div>
|
||||||
</div>`
|
</div>`
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 包含子字段时,字段名称必须填写
|
||||||
|
if (!item.fieldName) {
|
||||||
|
messageText = `<div style="line-height:24px">
|
||||||
|
<div>字段名称必须填写</div>
|
||||||
|
<div>序号:${index + 1}</div>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
}
|
||||||
if (fieldCodeArr.includes(item.fieldCode)) {
|
if (fieldCodeArr.includes(item.fieldCode)) {
|
||||||
messageText = `<div style="line-height:24px">
|
messageText = `<div style="line-height:24px">
|
||||||
<div>
|
<div>
|
||||||
@@ -353,6 +387,8 @@ const tableFormVerify = (type) => {
|
|||||||
</div>`
|
</div>`
|
||||||
}
|
}
|
||||||
fieldCodeArr.push(item.fieldCode)
|
fieldCodeArr.push(item.fieldCode)
|
||||||
|
// 当不包含子字段时才验证字段编码格式
|
||||||
|
if (item.hasChildren !== 'Y' && item.fieldCode) {
|
||||||
if (!/(^[a-zA-Z]{2}(_?[a-zA-Z0-9])*_?$)/.test(item.fieldCode)) {
|
if (!/(^[a-zA-Z]{2}(_?[a-zA-Z0-9])*_?$)/.test(item.fieldCode)) {
|
||||||
messageText = `<div style="line-height:24px">
|
messageText = `<div style="line-height:24px">
|
||||||
<div>
|
<div>
|
||||||
@@ -363,6 +399,7 @@ const tableFormVerify = (type) => {
|
|||||||
<div>序号:${index + 1}</div>
|
<div>序号:${index + 1}</div>
|
||||||
</div>`
|
</div>`
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (messageText) {
|
if (messageText) {
|
||||||
handleVerifyError(tabKey, item._X_ROW_KEY, index)
|
handleVerifyError(tabKey, item._X_ROW_KEY, index)
|
||||||
@@ -379,6 +416,20 @@ const tableFormVerify = (type) => {
|
|||||||
})
|
})
|
||||||
if (key == 'fieldList') {
|
if (key == 'fieldList') {
|
||||||
itemObj.labelI18n = formattingLengStr(itemObj.labelI18n, itemObj.fieldName)
|
itemObj.labelI18n = formattingLengStr(itemObj.labelI18n, itemObj.fieldName)
|
||||||
|
// 为子字段添加父字段编码信息
|
||||||
|
if (item.isSubField && item.parentFieldId) {
|
||||||
|
// 查找父字段的编码
|
||||||
|
const parentField = filedData.find(field => field._X_ROW_KEY === item.parentFieldId)
|
||||||
|
if (parentField && parentField.fieldCode) {
|
||||||
|
itemObj.parentFieldCode = parentField.fieldCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 为父字段添加是否包含子字段的标识
|
||||||
|
if (item.hasChildren === 'Y') {
|
||||||
|
itemObj.hasChildren = 'Y'
|
||||||
|
} else {
|
||||||
|
itemObj.hasChildren = 'N'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (type == 'edit' && item[`${key}_id`]) itemObj['id'] = item[`${key}_id`]
|
if (type == 'edit' && item[`${key}_id`]) itemObj['id'] = item[`${key}_id`]
|
||||||
infoData[key].push(itemObj)
|
infoData[key].push(itemObj)
|
||||||
|
|||||||
@@ -46,7 +46,22 @@
|
|||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
</template>
|
</template>
|
||||||
</vxe-column>
|
</vxe-column>
|
||||||
<vxe-column v-for="(item, key) in column" :key="key" :field="key" v-bind="item"></vxe-column>
|
<vxe-column v-for="(item, key) in column" :key="key" :field="key" v-bind="item">
|
||||||
|
<template #default="{ row }" v-if="key === 'hasChildren'">
|
||||||
|
<div :class="{ 'is-sub-field': row.isSubField }">
|
||||||
|
<el-button
|
||||||
|
v-if="!row.isSubField"
|
||||||
|
:type="item.editRender?.buttonType || 'primary'"
|
||||||
|
:size="item.editRender?.buttonSize || 'small'"
|
||||||
|
:disabled="item.editRender?.disabled ? item.editRender.disabled(row) : false"
|
||||||
|
@click.stop="handleButtonClick(item, row)"
|
||||||
|
>
|
||||||
|
{{ item.editRender?.buttonText || '添加子字段' }}
|
||||||
|
</el-button>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</vxe-column>
|
||||||
</vxe-table>
|
</vxe-table>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -65,6 +80,33 @@ const emit = defineEmits(['selection-change', 'cell-click', 'dropdown-command'])
|
|||||||
|
|
||||||
const vxeTableRef = ref()
|
const vxeTableRef = ref()
|
||||||
|
|
||||||
|
// 处理按钮点击事件
|
||||||
|
const handleButtonClick = (item, row) => {
|
||||||
|
// 检查editRender是否存在
|
||||||
|
if (!item.editRender) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查events是否存在
|
||||||
|
if (!item.editRender.events) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查click事件是否存在
|
||||||
|
if (!item.editRender.events.click) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行自定义点击事件
|
||||||
|
item.editRender.events.click(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试点击方法
|
||||||
|
const testClick = (row) => {
|
||||||
|
console.log('测试按钮被点击:', row)
|
||||||
|
alert('测试按钮工作正常!')
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({ vxeTableRef })
|
defineExpose({ vxeTableRef })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -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: '附加信息',
|
||||||
|
|||||||
233
src/views/system/role/RoleAssignAppForm.vue
Normal file
233
src/views/system/role/RoleAssignAppForm.vue
Normal 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>
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询列表 */
|
/** 查询列表 */
|
||||||
|
|||||||
Reference in New Issue
Block a user