Compare commits
1 Commits
bcac3326a3
...
zh
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b40163b2e7 |
@@ -68,7 +68,7 @@
|
||||
></InputTimeRange>
|
||||
</template>
|
||||
<template #header v-if="Object.keys(dimensionFields)?.length">
|
||||
<div style="display:flex;margin-left:40px">
|
||||
<div style="display:flex;margin-left:40px">
|
||||
<el-checkbox-group @change="searchDimension" v-model="tableSearch['Group by']"
|
||||
placeholder="请选择内容">
|
||||
<template v-for="(item,key) in dimensionFields">
|
||||
@@ -136,7 +136,6 @@ const crudRef = ref()
|
||||
const dimensionFields=ref<any>({})
|
||||
const exportLoading = ref(false)
|
||||
const fieldList = ref<any[]>([]) // 添加fieldList引用
|
||||
const hideFeilds= ref<any>({})
|
||||
const permissions =
|
||||
wsCache.get(CACHE_KEY.USER).lideeYunjipermissions?.[route.meta.menuDataId as string] || false
|
||||
const selectIds = computed(() => {
|
||||
@@ -171,7 +170,6 @@ const initTable = async () => {
|
||||
loading.value = true
|
||||
const { fieldList: apiFieldList, reportVo } = await ReportApi.getWebConfig(props.reportCode)
|
||||
|
||||
// 存储字段列表到响应式引用
|
||||
fieldList.value = apiFieldList
|
||||
|
||||
const isHeight = reportVo.tableConfig?.includes('height')
|
||||
@@ -190,40 +188,46 @@ const initTable = async () => {
|
||||
border: reportVo.tableConfig.includes('border'),
|
||||
stripe: reportVo.tableConfig.includes('stripe'),
|
||||
showSummary:false,
|
||||
|
||||
column: {}
|
||||
}
|
||||
//国际化处理
|
||||
|
||||
const fieldLengObj = assembleLengObj(apiFieldList, 'labelI18n', 'fieldCode', 'fieldName')
|
||||
for (const key in fieldLengObj) {
|
||||
mergeLocaleMessage(key, { [props.reportCode]: fieldLengObj[key] })
|
||||
}
|
||||
|
||||
// 根据parentFieldCode判断表头结构
|
||||
const hasSubFields = Array.isArray(apiFieldList) && apiFieldList.some(field => field.parentFieldCode && field.parentFieldCode !== '')
|
||||
// 分离固定列和动态列
|
||||
const fixedFields = apiFieldList.filter(f => f.isFixedColumn === 'Y')
|
||||
const dynamicFields = apiFieldList.filter(f => f.isFixedColumn !== 'Y')
|
||||
|
||||
// 构建父子关系映射
|
||||
const parentChildMap = new Map()
|
||||
const childParentMap = new Map()
|
||||
|
||||
// 建立映射关系
|
||||
apiFieldList.forEach(field => {
|
||||
// 处理固定列
|
||||
fixedFields.forEach((item) => {
|
||||
const config: any = {
|
||||
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()
|
||||
dynamicFields.forEach(field => {
|
||||
if (field.parentFieldCode && field.parentFieldCode !== '') {
|
||||
// 这是子字段
|
||||
childParentMap.set(field.fieldCode, field.parentFieldCode)
|
||||
if (!parentChildMap.has(field.parentFieldCode)) {
|
||||
parentChildMap.set(field.parentFieldCode, [])
|
||||
if (!childFieldConfigs.has(field.parentFieldCode)) {
|
||||
childFieldConfigs.set(field.parentFieldCode, [])
|
||||
}
|
||||
parentChildMap.get(field.parentFieldCode).push(field.fieldCode)
|
||||
childFieldConfigs.get(field.parentFieldCode).push(field)
|
||||
}
|
||||
})
|
||||
|
||||
//字段处理 - 构建正确的表头结构
|
||||
apiFieldList.forEach((item, index) => {
|
||||
// 跳过子字段,子字段会在父字段中处理
|
||||
if (item.parentFieldCode && item.parentFieldCode !== '') {
|
||||
return
|
||||
}
|
||||
// 处理动态列(暂不构建children,等数据返回后动态生成)
|
||||
dynamicFields.forEach((item, index) => {
|
||||
if (item.parentFieldCode && item.parentFieldCode !== '') return
|
||||
|
||||
const config: any = {
|
||||
prop: item.fieldCode,
|
||||
@@ -233,23 +237,8 @@ const initTable = async () => {
|
||||
isExport: item.isExport == 'Y',
|
||||
sortable: item.isShowSort == 'Y' ? 'custom' : false,
|
||||
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'
|
||||
}
|
||||
})
|
||||
_hasChildConfig: childFieldConfigs.has(item.fieldCode),
|
||||
_childConfigs: childFieldConfigs.get(item.fieldCode) || []
|
||||
}
|
||||
|
||||
if(!!item.isAmount){
|
||||
@@ -259,8 +248,6 @@ const initTable = async () => {
|
||||
}
|
||||
if(item.isDimension=='Y'){
|
||||
dimensionFields.value[config.prop]=config
|
||||
hideFeilds.value[config.prop]=item.isHideDimension
|
||||
|
||||
}
|
||||
|
||||
if (item.queryMode == 'RANGE') config.searchRange = true
|
||||
@@ -269,25 +256,17 @@ const initTable = async () => {
|
||||
config.type = 'date'
|
||||
config.format = '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') {
|
||||
config.type = 'time'
|
||||
config.format = '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') {
|
||||
config.type = 'datetime'
|
||||
config.format = '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)
|
||||
@@ -297,14 +276,12 @@ const initTable = async () => {
|
||||
if (item.queryMode == 'NE') config.searchLabel = `${config.label} !=`
|
||||
tableOption.value.column[item.fieldCode] = config
|
||||
})
|
||||
|
||||
|
||||
isInit.value = true
|
||||
searchChange()
|
||||
initTableLayout()
|
||||
}
|
||||
const searchDimension=()=>{
|
||||
|
||||
searchChange()
|
||||
}
|
||||
const initTableLayout = () => {
|
||||
@@ -370,28 +347,158 @@ const getTableData = async (isLoading = true) => {
|
||||
if (isLoading) loading.value = true
|
||||
const searchObj = await getSearchData()
|
||||
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
|
||||
|
||||
// 处理包含子字段的数据
|
||||
let processedData = data.records
|
||||
// 根据字段配置判断是否需要处理子字段
|
||||
const hasSubFields = Array.isArray(fieldList.value) && fieldList.value.length > 0 &&
|
||||
fieldList.value.some(field => field.parentFieldCode && field.parentFieldCode !== '')
|
||||
// 查找动态分组字段(用于生成一级表头)
|
||||
const groupField = fieldList.value.find(f => f.isDynamicGroup === 'Y')
|
||||
|
||||
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]
|
||||
}
|
||||
// 动态生成二维表头
|
||||
if (data.records && data.records.length > 0) {
|
||||
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 }
|
||||
|
||||
// 处理固定列
|
||||
fieldList.value.forEach(field => {
|
||||
if (field.isFixedColumn === 'Y' && field.fixedColumnValue) {
|
||||
const fixedValues = field.fixedColumnValue.split(',')
|
||||
flatRecord[field.fieldCode] = fixedValues[recordIndex] || ''
|
||||
}
|
||||
})
|
||||
|
||||
return flatRecord
|
||||
})
|
||||
}
|
||||
@@ -471,15 +578,6 @@ const clearSearch = () => {
|
||||
const searchChange = (params?, done?) => {
|
||||
if (tablePage.value) tablePage.value['currentPage'] = 1
|
||||
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)
|
||||
})
|
||||
if (done) done()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ export default {
|
||||
noPermission: `抱歉,您无权访问此页面。`,
|
||||
pageError: '抱歉,您访问的页面不存在。',
|
||||
networkError: '抱歉,服务器报告错误。',
|
||||
returnToHome: '返回工作台'
|
||||
returnToHome: '返回首页'
|
||||
},
|
||||
permission: {
|
||||
hasPermission: `请设置操作权限标签值`,
|
||||
@@ -157,7 +157,7 @@ export default {
|
||||
router: {
|
||||
login: '登录',
|
||||
socialLogin: '社交登录',
|
||||
home: '工作台',
|
||||
home: '首页',
|
||||
analysis: '分析页',
|
||||
workplace: '工作台'
|
||||
},
|
||||
@@ -275,7 +275,7 @@ export default {
|
||||
},
|
||||
exception: {
|
||||
backLogin: '返回登录',
|
||||
backHome: '返回工作台',
|
||||
backHome: '返回首页',
|
||||
subTitle403: '抱歉,您无权访问此页面。',
|
||||
subTitle404: '抱歉,您访问的页面不存在。',
|
||||
subTitle500: '抱歉,服务器报告错误。',
|
||||
|
||||
@@ -59,7 +59,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
component: () => import('@/views/Home/Index20.vue'),
|
||||
component: () => import('@/views/Home/Index5.vue'),
|
||||
name: 'Index',
|
||||
meta: {
|
||||
title: t('router.home'),
|
||||
|
||||
@@ -120,7 +120,6 @@ export enum DICT_TYPE {
|
||||
SYSTEM_SMS_RECEIVE_STATUS = 'system_sms_receive_status',
|
||||
SYSTEM_ERROR_CODE_TYPE = 'system_error_code_type',
|
||||
SYSTEM_OAUTH2_GRANT_TYPE = 'system_oauth2_grant_type',
|
||||
SYSTEM_OAUTH2_SCOPE='system_oauth2_scope',
|
||||
SYSTEM_MAIL_SEND_STATUS = 'system_mail_send_status',
|
||||
SYSTEM_NOTIFY_TEMPLATE_TYPE = 'system_notify_template_type',
|
||||
SYSTEM_SOCIAL_TYPE = 'system_social_type',
|
||||
|
||||
@@ -1,353 +1,631 @@
|
||||
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 头部区域 -->
|
||||
<!-- <header class="app-header">
|
||||
<h1 class="title">
|
||||
<i class="el-icon-menu"></i>
|
||||
|
||||
</h1>
|
||||
</header> -->
|
||||
<ContentWrap>
|
||||
|
||||
|
||||
<el-table
|
||||
:data="processedTableData" :span-method="objectSpanMethod" border stripe
|
||||
height="calc(100vh - 300px)" style="width: 100%" v-loading="loading" :summary-method="getSummaries"
|
||||
show-summary :summary-text="summaryText">
|
||||
<!-- 动态生成表头 -->
|
||||
<template v-for="dim in selectedDimensions" :key="dim">
|
||||
<el-table-column
|
||||
:prop="dim" :label="getDimensionName(dim)" :min-width="getColumnWidth(dim)"
|
||||
show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span>{{ row[dim] || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<!-- <div class="search-container">
|
||||
<el-input
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索应用..."
|
||||
clearable
|
||||
prefix-icon="el-icon-search"
|
||||
class="search-input"
|
||||
></el-input>
|
||||
</div> -->
|
||||
<!-- 统计指标列 -->
|
||||
<el-table-column prop="salesAmount" label="销售额(万)" width="120" sortable>
|
||||
<template #default="{ row }">
|
||||
{{ formatNumber(row.salesAmount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="Object.keys(filteredCategories).length === 0" class="empty-state">
|
||||
<i class="el-icon-warning empty-icon"></i>
|
||||
<h3>未找到匹配的应用</h3>
|
||||
<p>请尝试调整搜索关键词</p>
|
||||
</div>
|
||||
<el-table-column prop="salesQuantity" label="销售数量" width="120" sortable>
|
||||
<template #default="{ row }">
|
||||
{{ formatNumber(row.salesQuantity) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 分类应用展示 -->
|
||||
<div
|
||||
v-for="(category, categoryName) in filteredCategories"
|
||||
:key="categoryName"
|
||||
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>
|
||||
|
||||
<div class="apps-grid">
|
||||
<div
|
||||
v-for="app in category.apps"
|
||||
:key="app.id"
|
||||
class="app-card"
|
||||
@click="handleAppClick(app)">
|
||||
<div class="app-icon-wrapper">
|
||||
<i :class="app.iconClass" class="app-icon"></i>
|
||||
</div>
|
||||
<div class="app-name">{{ app.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-table-column prop="growthRate" label="同比增长" width="120" sortable>
|
||||
<template #default="{ row }">
|
||||
<span :class="getGrowthClass(row.growthRate)">
|
||||
{{ formatPercent(row.growthRate) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 应用详情对话框 -->
|
||||
<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">
|
||||
<i :class="selectedApp.iconClass" class="dialog-app-icon"></i>
|
||||
</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>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AppManager',
|
||||
data() {
|
||||
return {
|
||||
searchQuery: '',
|
||||
dialogVisible: false,
|
||||
selectedApp: null,
|
||||
appsData: {
|
||||
统计报表: [
|
||||
{id: 'office1', name: '驾驶舱总屏幕', iconClass: 'el-icon-document'},
|
||||
{id: 'office2', name: '销售报表', iconClass: 'el-icon-tickets'},
|
||||
{id: 'office3', name: '生产报表', iconClass: 'el-icon-present'}
|
||||
],
|
||||
工业互联网: [
|
||||
{id: 'dev1', name: '产线组态', iconClass: 'el-icon-setting'}
|
||||
],
|
||||
数据中台: [
|
||||
{id: 'fun1', name: '数据采集', iconClass: 'el-icon-headset'},
|
||||
// {id: 'fun2', name: '视频观看', iconClass: 'el-icon-video-play'},
|
||||
// {id: 'fun3', name: '游戏中心', iconClass: 'el-icon-game'},
|
||||
// {id: 'fun4', name: '阅读器', iconClass: 'el-icon-reading'}
|
||||
]
|
||||
// 系统工具: [
|
||||
// {id: 'sys1', name: '系统监控', iconClass: 'el-icon-monitor'},
|
||||
// {id: 'sys2', name: '磁盘清理', iconClass: 'el-icon-delete-solid'},
|
||||
// {id: 'sys3', name: '网络诊断', iconClass: 'el-icon-mobile-phone'},
|
||||
// {id: 'sys4', name: '安全防护', iconClass: 'el-icon-lock'}
|
||||
// ]
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
categorizedApps() {
|
||||
const result = {};
|
||||
for (const [category, apps] of Object.entries(this.appsData)) {
|
||||
result[category] = {
|
||||
apps: apps.map(app => ({...app, category}))
|
||||
};
|
||||
}
|
||||
return result;
|
||||
},
|
||||
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) ||
|
||||
app.category.toLowerCase().includes(query)
|
||||
);
|
||||
|
||||
if (filteredApps.length > 0) {
|
||||
result[categoryName] = {...categoryData, apps: filteredApps};
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
totalApps() {
|
||||
return Object.values(this.appsData).reduce((total, apps) => total + apps.length, 0);
|
||||
},
|
||||
categoriesCount() {
|
||||
return Object.keys(this.appsData).length;
|
||||
},
|
||||
visibleCategories() {
|
||||
return Object.keys(this.filteredCategories).length;
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Check } from '@element-plus/icons-vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
// 维度定义 - 7个维度
|
||||
const dimensions = ref([
|
||||
{ id: 'salesType', name: '销售类型' },
|
||||
{ id: 'customer', name: '客户' },
|
||||
{ id: 'dosageForm', name: '剂型' },
|
||||
{ id: 'product', name: '产品' },
|
||||
{ id: 'administrativeArea', name: '行政区域' },
|
||||
{ id: 'salesman', name: '业务员' },
|
||||
{ id: 'specification', name: '品规' }
|
||||
])
|
||||
|
||||
// 状态管理 - 默认选中所有维度
|
||||
const selectedDimensions = ref(dimensions.value.map(d => d.id))
|
||||
const primaryDimension = ref('product') // 默认主维度为产品
|
||||
const tableData = ref([])
|
||||
const loading = ref(false)
|
||||
const lastUpdate = ref('')
|
||||
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
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleAppClick(app) {
|
||||
this.selectedApp = app;
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
confirmLaunch() {
|
||||
this.$message.success(`正在启动 ${this.selectedApp.name}...`);
|
||||
this.dialogVisible = false;
|
||||
|
||||
// 如果要移除的是当前主维度,则自动切换到下一个维度
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
const avgGrowth = totalAmount > 0 ? weightedGrowthSum / totalAmount : 0
|
||||
sums[index] = formatPercent(avgGrowth)
|
||||
}
|
||||
} else {
|
||||
sums[index] = '-'
|
||||
}
|
||||
})
|
||||
|
||||
return sums
|
||||
}
|
||||
|
||||
// 数据获取
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 使用模拟数据
|
||||
setTimeout(() => {
|
||||
tableData.value = generateMockData()
|
||||
|
||||
// 计算并保存总统计数据
|
||||
calculateTotalStats()
|
||||
|
||||
lastUpdate.value = new Date().toLocaleString()
|
||||
loading.value = false
|
||||
|
||||
// 更新图表
|
||||
if (showChart.value) {
|
||||
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: {
|
||||
type: 'category',
|
||||
data: Object.keys(groups)
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '销售额(万)'
|
||||
},
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
||||
|
||||
/* 头部样式 */
|
||||
.app-header {
|
||||
padding: 30px 0;
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.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 {
|
||||
.multi-dimension-report {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
background: #f5f7fa;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dimension-controls {
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
background: rgb(255 255 255 / 20%);
|
||||
border-radius: 15px;
|
||||
justify-content: space-around;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.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;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.category-header {
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
padding-left: 15px;
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
color: #303133;
|
||||
border-left: 5px solid #409EFF;
|
||||
.dimension-selection {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.app-count {
|
||||
color: #409eff;
|
||||
background: #ecf5ff;
|
||||
.dimension-selection h3 {
|
||||
margin: 0 0 15px;
|
||||
font-size: 16px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
/* 应用网格样式 */
|
||||
.apps-grid {
|
||||
.dimension-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 25px;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.app-card {
|
||||
padding: 20px 15px;
|
||||
.dimension-item {
|
||||
position: relative;
|
||||
padding: 12px;
|
||||
font-weight: 500;
|
||||
color: #409eff;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 10px rgb(0 0 0 / 5%);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background-color: #ecf5ff;
|
||||
border: 2px solid #409eff;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.app-card:hover {
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 25px rgb(64 158 255 / 20%);
|
||||
.dimension-item:hover {
|
||||
background-color: #d9ecff;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 2px 8px rgb(64 158 255 / 20%);
|
||||
}
|
||||
|
||||
.app-icon-wrapper {
|
||||
display: flex;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin: 0 auto 12px;
|
||||
font-size: 28px;
|
||||
line-height: 60px;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 50%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.dimension-item.inactive {
|
||||
font-weight: normal;
|
||||
color: #606266;
|
||||
background-color: #f5f7fa;
|
||||
border-color: #dcdfe6;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
.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;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 对话框样式 */
|
||||
::v-deep .app-dialog .el-dialog__body {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dialog-icon-wrapper {
|
||||
.report-header {
|
||||
display: flex;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 20px;
|
||||
font-size: 36px;
|
||||
line-height: 80px;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 50%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.dialog-app-name {
|
||||
margin: 0 0 10px;
|
||||
font-size: 1.5rem;
|
||||
.statistics-info {
|
||||
padding: 8px 15px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
background: #f5f7fa;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dialog-app-id,
|
||||
.dialog-app-category {
|
||||
margin: 5px 0;
|
||||
.table-container {
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
|
||||
}
|
||||
|
||||
.positive {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.negative {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.neutral {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
.chart-container {
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
|
||||
}
|
||||
</style>
|
||||
|
||||
: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;
|
||||
}
|
||||
</style>
|
||||
@@ -183,43 +183,6 @@ 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: {
|
||||
default: (renderOpts, { row, column }, isStop = false) => {
|
||||
const { dicObj } = renderOpts
|
||||
@@ -303,6 +266,32 @@ export const useRenderVxeColumn = (useType = 'table') => {
|
||||
</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) {
|
||||
|
||||
@@ -40,6 +40,65 @@
|
||||
</template>
|
||||
</template>
|
||||
</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>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -49,6 +108,7 @@ import { tableInfoOption, dicObj } from '../designData'
|
||||
import { formattingLengStr } from '@/utils/lowDesign'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import * as DictDataApi from '@/api/system/dict/dict.type'
|
||||
import { Delete } from '@element-plus/icons-vue'
|
||||
|
||||
defineOptions({ name: 'TableInfo' })
|
||||
const message = useMessage()
|
||||
@@ -86,10 +146,17 @@ const tabsRef = ref()
|
||||
// 父字段引用
|
||||
const parentFieldMap = ref(new Map())
|
||||
|
||||
// 固定列行配置弹窗
|
||||
const fixedColumnDialog = ref({
|
||||
visible: false,
|
||||
currentRow: null as any,
|
||||
rows: [] as Array<{ value: string }>,
|
||||
originalValue: '' // 保存原始值,用于取消时恢复
|
||||
})
|
||||
|
||||
const fieldList = computed(() => {
|
||||
let dicData: Array<{ label: string; value: string; type: string }> = []
|
||||
infoData.value.basics.forEach((item) => {
|
||||
|
||||
if (item.fieldCode && item.isDb == 'Y')
|
||||
dicData.push({
|
||||
label: `${item.fieldCode}${item.fieldName ? '(' + item.fieldName + ')' : ''}`,
|
||||
@@ -224,6 +291,61 @@ 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 data = tableInfoOption.formattingInitData(props.editInfoData)
|
||||
const fieldList: any[] = []
|
||||
@@ -258,9 +380,7 @@ const initEditInfoData = () => {
|
||||
if (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)
|
||||
})
|
||||
|
||||
@@ -285,14 +405,12 @@ onMounted(() => {
|
||||
})
|
||||
}
|
||||
}
|
||||
tableInfoOption.infoColumn.fieldColumn.isHideDimension.editRender.dicData = infoData.value.basics.map(({fieldCode,fieldName})=>{
|
||||
return {label:fieldName,value:fieldCode}
|
||||
})
|
||||
|
||||
// 设置父字段下拉选项
|
||||
const updateParentFieldOptions = () => {
|
||||
// 获取所有parentFieldCode为空的字段作为可选父字段
|
||||
// 获取所有parentFieldCode为空且非固定列的字段作为可选父字段
|
||||
const parentFieldOptions = infoData.value.basics
|
||||
.filter(field => !field.parentFieldCode && field.fieldCode && field.fieldName)
|
||||
.filter(field => !field.parentFieldCode && field.fieldCode && field.fieldName && field.isFixedColumn !== 'Y')
|
||||
.map(field => ({
|
||||
label: `${field.fieldName}`,
|
||||
value: field.fieldCode
|
||||
@@ -466,6 +584,41 @@ onMounted(() => {
|
||||
// 初始化父字段选项
|
||||
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的下拉选项
|
||||
DictDataApi.getSimpleDictTypeList().then((dicData) => {
|
||||
const dicObj = {}
|
||||
|
||||
@@ -152,6 +152,8 @@ const infoColumn = {
|
||||
parentFieldName: { title: '父字段名称', width: 120, editRender: { name: 'LowSelect', dicData: [], filterable: true, clearable: true } },
|
||||
labelI18n: { title: '国际化配置', width: 140, editRender: { name: 'LowMonacoEditorInput', events: {} } },
|
||||
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' } },
|
||||
queryIsWeb: { title: '查询控件', width: 75, align: "center", editRender: { name: 'LowCheckbox' } },
|
||||
queryMode: { title: '查询模式', width: 130, editRender: { name: 'LowSelect', verifyEdit: true, dicData: dicObj.queryMode, dicObj: getDicObj('queryMode') } },
|
||||
@@ -159,9 +161,9 @@ const infoColumn = {
|
||||
isExport: { title: '是否可导出', width: 90, align: "center", editRender: { name: 'LowCheckbox' } },
|
||||
isAmount: { title: '是否合计', width: 75, align: "center", editRender: { name: 'LowCheckboxSum' } },
|
||||
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' } },
|
||||
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' } },
|
||||
},
|
||||
}
|
||||
@@ -180,6 +182,10 @@ for (const key in infoColumn) {
|
||||
if (!keys.includes('hasChildren')) {
|
||||
keys.push('hasChildren')
|
||||
}
|
||||
// 确保固定列内容字段被包含
|
||||
if (!keys.includes('fixedColumnValue')) {
|
||||
keys.push('fixedColumnValue')
|
||||
}
|
||||
}
|
||||
infoApiKey[apiKey[key]] = keys
|
||||
}
|
||||
@@ -188,7 +194,7 @@ for (const key in infoColumn) {
|
||||
//默认值
|
||||
const infoDefaultData = {
|
||||
basics: {
|
||||
fieldCode: '', fieldName: '', parentFieldName: '', labelI18n: '', fieldType: 'String', queryIsDb: 'N', queryIsWeb: 'N', queryMode: 'LIKE', dictCode: '', isExport: 'Y', isShowSort: 'N', isAmount: '', isDimension: '',isHideDimension:'', hasChildren: 'N', isSubField: false, parentFieldId: '', parentFieldCode: '',
|
||||
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: '',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -349,7 +349,7 @@ const tableFormVerify = (type) => {
|
||||
const index = Number(i)
|
||||
const item = filedData[index]
|
||||
item.sortNum = index + 1
|
||||
item.isHideDimension?item.isHideDimension=item.isHideDimension.join(','):''
|
||||
|
||||
let messageText = ''
|
||||
let tabKey = 'mysql'
|
||||
// 子字段不能再包含子字段
|
||||
|
||||
@@ -173,8 +173,7 @@ const tableOption = reactive({
|
||||
span: 12,
|
||||
hide: true,
|
||||
control: (val) => {
|
||||
let dicData = ["user.read","user.write"]
|
||||
debugger
|
||||
let dicData = []
|
||||
if (val?.length) {
|
||||
dicData = val.map((item) => {
|
||||
return { label: item, value: item }
|
||||
@@ -191,11 +190,11 @@ const tableOption = reactive({
|
||||
span: 12,
|
||||
multiple: true,
|
||||
hide: true,
|
||||
dicData: ["user.read","user.write"]
|
||||
dicData: []
|
||||
},
|
||||
redirectUris: {
|
||||
label: '可重定向的 URI 地址',
|
||||
// type: 'select',
|
||||
type: 'select',
|
||||
multiple: true,
|
||||
span: 12,
|
||||
hide: true,
|
||||
|
||||
Reference in New Issue
Block a user