<template>
<div class="operation-container">
<div class="header">
<h2>{{ $t('config.title') }}</h2>
<div>
<el-button type="success" size="default" @click="showAddDialog" style="margin-bottom: 10px">
{{ $t('config.addConfig') }}
</el-button>
</div>
</div>
<div class="filter-container">
<el-form :model="filter" class="filter-form" label-width="150px">
<el-row :gutter="20" style="margin-bottom: 20px">
<el-col :span="6">
<!-- 主机应用选择 -->
<el-form-item :label="$t('config.hostApp')">
<el-select
v-model="filter.hostApp"
:placeholder="$t('config.selectHostApp')"
clearable
filterable
style="width: 300px"
@change="handleFilter"
>
<el-option
v-for="app in hostAppOptions"
:key="app"
:label="getHostAppLabel(app)"
:value="app"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<!-- 卡位置选择 -->
<el-form-item :label="$t('config.cardPosition')">
<el-select
v-model="filter.cardPosition"
:placeholder="$t('config.selectCardPosition')"
clearable
filterable
style="width: 300px"
@change="handleFilter"
>
<el-option
v-for="position in cardPositionOptions"
:key="position"
:label="getCardPositionLabel(position)"
:value="position"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<!-- 设备类型选择 -->
<el-form-item :label="$t('config.deviceType')">
<el-select
v-model="filter.deviceType"
:placeholder="$t('config.selectDeviceType')"
clearable
filterable
style="width: 300px"
@change="handleFilter"
>
<el-option
v-for="item in deviceTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<!-- 卡片名称查询 -->
<el-form-item :label="$t('management.cardName')" prop="cardName">
<el-select
v-model="filter.cardName"
:placeholder="$t('common.selectCardName')"
clearable
filterable
@change="handleFilter"
style="width: 300px"
>
<el-option
v-for="item in cardAddOptions"
:key="item.cardName"
:label="cardTitleMap[item.cardName] + '-' + item.cardName"
:value="item.cardName"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-row type="flex" justify="end">
<el-button @click="resetFields">{{ $t('common.reset') }}</el-button>
<el-button type="primary" @click="handleFilter">{{ $t('common.search') }}</el-button>
</el-row>
</el-col>
</el-row>
</el-form>
</div>
<!-- 批量操作工具栏 -->
<div class="batch-toolbar">
<el-button
type="warning"
size="small"
@click="betaOrStopOrFullPublish('batchRuleSet')"
icon="CircleCheck"
>
{{ $t('config.batchConfigCardRules') }}
</el-button>
<el-button
type="success"
size="small"
@click="betaOrStopOrFullPublish('full')"
icon="CircleCheck"
>
{{ $t('config.batchFullPublish') }}
</el-button>
<el-button
type="danger"
size="small"
@click="betaOrStopOrFullPublish('stop')"
icon="CircleCheck"
>
{{ $t('config.batchStopPublish') }}
</el-button>
</div>
<el-table
:data="tableData"
:fit="true"
border
style="width: 100%"
:empty-text="$t('common.noData')"
v-loading="loading"
class="github-table"
ref="tableRef"
@expand-change="handleExpandChange"
:expand-row-keys="defaultExpandedRows"
row-key="id"
>
<!-- 展开/收起列 -->
<el-table-column type="expand" width="60">
<template #header>
<el-icon :size="13" class="expand-icon" @click.stop="toggleExpandAll">
<component :is="isAllExpanded ? 'ArrowDown' : 'ArrowRight'" />
</el-icon>
</template>
<template #default="{ row }">
<div class="version-container">
<el-table
:data="versionTablesData[row.id]?.tableData || []"
:fit="true"
border
stripe
style="width: 100%"
:empty-text="$t('common.noData')"
v-loading="versionTablesData[row.id]?.loading"
class="github-table"
:ref="`versionTable_${row.id}`"
:row-key="(row) => row.cardVer"
>
<el-table-column width="40" type="selection" reserve-selection />
<el-table-column prop="cardVer" :label="$t('version.version')" align="center" />
<el-table-column
prop="status"
:label="$t('management.cardPackageStatus')"
align="center"
>
<template #default="{ row }">
<el-tag :type="row.status === 0 ? 'success' : 'danger'">
{{ row.status === 0 ? $t('management.listed') : $t('management.unlisted') }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="cardTags" :label="$t('version.tags')" align="center">
<template #default="{ row }">
<div :class="{ 'tag-cell': row.cardTags?.slice(0, 3)?.length > 1 }">
<!-- 显示前N个可见标签 -->
<div v-for="tag in row.cardTags?.slice(0, 3)" :key="tag">
<el-tag v-if="tag.length > 10" class="table-tag" @click="handleTagClick(tag)">
{{ truncateText(tag, 10) }}
</el-tag>
<el-tag v-else>
{{ tag }}
</el-tag>
</div>
<!-- 折叠剩余标签 -->
<el-popover
v-if="row.cardTags?.slice(3)?.length > 0"
placement="bottom"
trigger="hover"
:width="300"
>
<template #reference>
<el-tag class="more-tag">+{{ row.cardTags?.slice(3)?.length }}</el-tag>
</template>
<div class="hidden-tags-container">
<el-tag v-for="tag in row.cardTags?.slice(3)" :key="tag" class="hidden-tag">
{{ tag }}
</el-tag>
</div>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column
prop="deviceTypes"
:label="$t('version.supportedDevices')"
align="center"
>
<template #default="{ row }">
<div class="vertical-tags-container">
<el-tag
v-for="type in row.deviceTypes.join(',').split(',')"
:key="type"
class="content-fit-tag"
>
{{ deviceTypeFilter(type) }}
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column prop="minorsMode" :label="$t('version.minorsMode')" align="center">
<template #default="{ row }">
<el-tag :type="row.minorsMode === 1 ? 'success' : 'warning'">
{{ row.minorsMode === 1 ? $t('version.available') : $t('version.unavailable') }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('version.multiLangDesc')" width="150" align="center">
<template #default="{ row }">
<div>
<el-button
size="small"
@click="handleShowJson(row.cardMultiLang, $t('version.multiLangDesc'))"
>
{{ $t('version.viewJson') }}
</el-button>
</div>
</template>
</el-table-column>
<el-table-column :label="$t('version.capabilities')" width="150" align="center">
<template #default="{ row }">
<div>
<el-button
size="small"
@click="handleShowJson(row.services, $t('version.capabilities'))"
>
{{ $t('version.viewJson') }}
</el-button>
</div>
</template>
</el-table-column>
<el-table-column
prop="publishStatus"
:label="$t('config.releaseStatus')"
align="center"
>
<template #default="{ row }">
<div v-if="row.publishStatus === 0">
{{ $t('config.releaseStatusEnum.toBeReleased') }}
</div>
<div v-if="row.publishStatus === 1">
{{ $t('config.releaseStatusEnum.crowdtestingRelease') }}
</div>
<div v-if="row.publishStatus === 2">
{{ $t('config.releaseStatusEnum.fullRelease') }}
</div>
</template>
</el-table-column>
<el-table-column
:label="$t('common.actions')"
width="300"
fixed="right"
align="center"
>
<template #default="{ row: version }">
<div style="flex-wrap: wrap; align-items: center; gap: 4px; min-height: 32px">
<el-button size="small" @click="actionsTo(row.id, 'edit', version)"
>{{ $t('common.edit') }}
</el-button>
<el-button
type="danger"
style="margin-left: 9px"
size="small"
@click="actionsTo(row.id, 'stop', version)"
v-if="version.publishStatus !== 0"
>{{ $t('crowdTesting.stopPublish') }}
</el-button>
<el-button
type="success"
style="margin-left: 9px"
size="small"
@click="actionsTo(row.id, 'full', version)"
v-if="version.publishStatus === 1"
>{{ $t('crowdTesting.fullPublish') }}
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div>
<el-pagination
size="default"
@size-change="(val) => handleVersionSizeChange(row.id, val)"
@current-change="(val) => handleVersionCurrentChange(row.id, val)"
:current-page="versionCurrentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="versionPageSize"
layout="total, prev, pager, next,sizes, jumper"
:total="versionTablesData[row.id]?.total"
/>
</div>
</div>
</template>
</el-table-column>
<!-- 选择框列 -->
<el-table-column prop="hostApp" :label="$t('config.hostApp')" align="center" width="300px">
<template #default="scope">
<el-tooltip :content="scope.row.hostApp">
<span class="text-ellipsis">{{ getHostAppLabel(scope.row.hostApp) }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="cardPosition" :label="$t('config.cardPosition')" align="center">
<template #default="scope">
<el-tooltip :content="scope.row.cardPosition">
<span class="text-ellipsis">{{ getCardPositionLabel(scope.row.cardPosition) }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column
prop="deviceType"
:label="$t('config.deviceType')"
align="center"
width="150"
>
<template #default="scope">
{{ getDeviceType(scope.row.deviceType) }}
</template>
</el-table-column>
<el-table-column prop="cardName" :label="$t('management.cardName')" align="center">
<template #default="scope">
<el-tooltip :content="scope.row.cardName">
<span class="text-ellipsis">{{
cardTitleMap[scope.row.cardName] || scope.row.cardName
}}</span>
</el-tooltip>
</template>
</el-table-column>
<!-- 索引列 -->
<el-table-column prop="index" :label="$t('config.sortIndex')" align="center" width="150">
<template #default="scope">
{{ basicCardObj[scope.row.cardName]?.cardType === 2 ? '/' : scope.row.index }}
</template>
</el-table-column>
<el-table-column :label="$t('common.actions')" fixed="right" align="center">
<template #default="{ row, $index }">
<div class="action-buttons">
<el-button
type="primary"
size="small"
@click="batchUpdateIndex($index, row)"
v-if="basicCardObj[row.cardName]?.cardType !== 2"
>
{{ $t('config.modifyCardOrder') }}
</el-button>
<el-button size="small" type="danger" @click="handleDelete($index, row)"
>{{ $t('common.delete') }}
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div>
<el-pagination
size="default"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
layout="total, prev, pager, next,sizes, jumper"
:total="total"
>
</el-pagination>
</div>
<AddCardConfig
:dialogVisible="dialogVisible"
:title="dialogTitle"
:form-data="form"
:host-app-options="hostAppOptions"
:card-position-options="cardPositionOptions"
:device-type-options="deviceTypeOptions"
:card-options="cardAddOptions"
:card-obj="basicCardObj"
:is-edit="currentIndex !== -1"
:cancel="handleCancel"
v-if="dialogVisible"
:cardTitleMap="cardTitleMap"
:getHostAppLabel="getHostAppLabel"
:getCardPositionLabel="getCardPositionLabel"
/>
<!-- JSON查看对话框 -->
<JsonViewerDialog
:jsonViewerVisible="jsonViewerVisible"
:jsonViewerTitle="jsonViewerTitle"
:formattedJson="formattedJson"
:close="closeShowJson"
/>
<EditCardIndex
:config="rowData"
:dialogVisible="batchUpdateIndexDialogVisible"
v-if="batchUpdateIndexDialogVisible"
:cancel="cancel"
:cardTitleMap="cardTitleMap"
:getHostAppLabel="getHostAppLabel"
:getCardPositionLabel="getCardPositionLabel"
/>
<BatchSetRule
:dialogVisible="batchRuleSetDialogVisible"
:cancel="cancel"
:cardOptions="cardAddOptions"
:cardTitleMap="cardTitleMap"
:ruleDtoList="ruleDtoList"
/>
<EditCardVersionCrowdTesting
:visible="showDialog"
v-model:visible="showDialog"
:testRowData="testRowData"
:getHostAppLabel="getHostAppLabel"
:getCardPositionLabel="getCardPositionLabel"
/>
</div>
</template>
<script setup>
import { useGlobalMaps } from '@/utils/util.ts'
const { maps, initGlobalMaps } = useGlobalMaps()
const getHostAppLabel = (app) => maps.value.hostAppMap[app] || app
const getCardPositionLabel = (app) => maps.value.cardPositionMap[app] || app
</script>
<script>
import { ElMessage, ElMessageBox } from 'element-plus'
import enumInfo from '@/constants/EnumInfo.ts'
import { requestHandler } from '@/utils/request'
import axios from 'axios'
import EditCardVersionCrowdTesting from './components/EditCardVersionCrowdTesting.vue'
import AddCardConfig from './components/AddCardConfig.vue'
import EditCardIndex from './components/EditCardIndex.vue'
import BatchSetRule from './components/BatchSetRule.vue'
import JsonViewerDialog from '@/components/JsonViewerDialog.vue'
import { initGlobalMaps, useGlobalMaps } from '@/utils/util.ts';
import { useStore } from '@/store'
import { getCardOptions } from '@/service'
const store = useStore();
export default {
components: {
EditCardVersionCrowdTesting,
EditCardIndex,
AddCardConfig,
BatchSetRule,
JsonViewerDialog
},
data() {
return {
loading: false,
dialogVisible: false,
batchRuleSetDialogVisible: false,
batchUpdateIndexDialogVisible: false,
rowData: {},
dialogTitle: this.$t('config.addOperationConfig'),
currentIndex: -1,
tableData: [],
versionTablesData: {}, // Changed from versionTables to store version data by card ID
cardOptions: [],
cardAddOptions: [],
defaultExpandedRows: [],
basicCardObj: {},
currentPage: 1,
pageSize: 10,
total: 0,
basicCardObject: {},
jsonViewerVisible: false,
jsonViewerTitle: '',
formattedJson: '',
filter: {
hostApp: '',
cardPosition: '',
deviceType: '',
cardName: '',
publishStatus: ''
},
form: {
hostApp: '',
cardPosition: '',
deviceType: 'phone',
cardName: '',
index: 1
},
versionLoading: false,
versionCurrentPage: 1,
versionPageSize: 10,
versionTotal: 0,
tagVisible: false,
showDialog: false,
tagContent: '',
testRowData: {},
basic: {
cardName: '',
provider: '',
dimension: '',
disable: null
},
isAllExpanded: false,
cardTitleMap: new Map(),
ruleDtoList: []
}
},
computed: {
hostAppOptions() {
return [...new Set(this.cardOptions.map((item) => item.hostApp))]
},
cardPositionOptions() {
return [...new Set(this.cardOptions.map((item) => item.cardPosition))]
},
deviceTypeOptions() {
return enumInfo.deviceType()
}
},
async mounted() {
await initGlobalMaps()
this.queryCardOptions()
this.fetchCardList()
this.cardAddOptions = await getCardOptions()
this.basicCardObj = this.cardAddOptions.reduce((map, obj) => {
map[obj.cardName] = obj
return map
}, {})
const targetLang = store.language
this.cardTitleMap = this.cardAddOptions.reduce((map, card) => {
// 先设置一个作为兜底
map[card.cardName] = card.cardName
if (card.cardMultiLang && card.cardMultiLang.length > 0) {
// 将cardMultiLang数组转换为{cardName: cardTitle}的对象
for (const langItem of card.cardMultiLang) {
if (langItem.language === targetLang) {
map[card.cardName] = langItem.cardTitle || card.cardName
}
}
}
return map
}, {})
},
methods: {
fetchCardVersionConfig(cardConfigId, config) {
if (!this.versionTablesData[cardConfigId]) {
this.versionTablesData[cardConfigId] = {
loading: false,
tableData: [],
currentPage: 1,
pageSize: 10,
total: 0
}
}
this.versionTablesData[cardConfigId].loading = true
const queryData = {
cardName: config.cardName,
hostApp: config.hostApp,
deviceType: config.deviceType,
cardOpId: cardConfigId,
pageNumber: this.versionTablesData[cardConfigId].currentPage || 1,
pageSize: this.versionTablesData[cardConfigId].pageSize || 10,
pageInfo: {
pageSize: this.versionTablesData[cardConfigId].pageSize || 10,
offset: this.versionTablesData[cardConfigId].currentPage || 1
}
}
axios
.post('/v1/card/version-op/page-query', queryData)
.then((response) => {
if (response?.data?.code === 0) {
this.versionTablesData[cardConfigId]['tableData'] =
response.data.opList?.map((item) => ({
...item,
hostApp: config.hostApp,
deviceType: config.deviceType,
cardPosition: config.cardPosition
})) || []
this.versionTablesData[cardConfigId]['total'] = response.data.count || 0
this.versionTablesData[cardConfigId]['config'] = config
} else {
ElMessage.error(response?.data?.message || this.$t('common.getListFailed'))
}
})
.finally(() => {
this.versionTablesData[cardConfigId]['loading'] = false
})
},
cancel() {
this.batchUpdateIndexDialogVisible = false
this.batchRuleSetDialogVisible = false
},
deviceTypeFilter(input) {
if (!input) return null
const found = enumInfo.deviceType().find((ent) => ent.value === input)
return found ? found.label : ''
},
truncateText(text, maxLength = 6) {
return text.length > maxLength ? `${text.slice(0, maxLength)}...` : text
},
handleTagClick(tag) {
this.tagContent = tag
this.tagVisible = true
},
actionsTo(cardConfigId, type, row) {
if (type === 'beta' && !row.testRules) {
ElMessage.error(this.$t('config.checkRules'))
} else {
let tempRow = JSON.parse(JSON.stringify(row))
this.testRowData = {
action: type,
hostApp: this.versionTablesData[cardConfigId].config.hostApp,
cardName: this.versionTablesData[cardConfigId].config.cardName,
cardVer: tempRow.cardVer,
testRules: tempRow.testRules,
cardOpId: cardConfigId,
publishStatus: tempRow.publishStatus
}
this.showDialog = true
}
},
handleVersionSizeChange(cardConfigId, val) {
this.versionTablesData[cardConfigId].pageSize = val
this.versionTablesData[cardConfigId].currentPage = 1
this.versionPageSize = val
this.fetchCardVersionConfig(cardConfigId, this.versionTablesData[cardConfigId].config)
},
handleVersionCurrentChange(cardConfigId, val) {
this.versionTablesData[cardConfigId].currentPage = val
this.versionCurrentPage = val
this.fetchCardVersionConfig(cardConfigId, this.versionTablesData[cardConfigId].config)
},
toggleExpandAll() {
this.isAllExpanded = !this.isAllExpanded
this.tableData.forEach((row) => {
this.$refs.tableRef.toggleRowExpansion(row, this.isAllExpanded)
})
},
handleExpandChange(row, expandedRows) {
if (expandedRows.includes(row) && !row.versionDataLoaded) {
row.versionDataLoaded = true
this.versionTablesData[row.id] = {
...this.versionTablesData[row.id],
config: row
}
this.fetchCardVersionConfig(row.id, row)
}
},
betaOrStopOrFullPublish(type) {
const ruleDtoList = []
Object.keys(this.versionTablesData).forEach((cardConfigId) => {
const table = this.$refs[`versionTable_${cardConfigId}`]
if (table && table.getSelectionRows) {
ruleDtoList.push(
...table.getSelectionRows().map((item) => ({
cardOpId: cardConfigId,
hostApp: item.hostApp,
cardPosition: item.cardPosition,
deviceType: item.deviceType,
cardName: item.cardName,
cardTitle: this.cardTitleMap[item.cardName] || item.cardName,
cardVer: item.cardVer,
status: item.publishStatus,
testRules: item.testRules
}))
)
}
})
if (ruleDtoList.length === 0) {
ElMessage.error(this.$t('config.selectAtLeastOneRecord'))
return
}
if (ruleDtoList.length > 20) {
ElMessage.error(this.$t('config.selectMaxRecords'))
return
}
// 众测发布
if (type === 'batchRuleSet') {
const firstRecord = ruleDtoList[0]
const inconsistentFields = []
if (ruleDtoList.some((item) => item.hostApp !== firstRecord.hostApp)) {
inconsistentFields.push(this.$t('config.hostApp'))
}
if (ruleDtoList.some((item) => item.cardPosition !== firstRecord.cardPosition)) {
inconsistentFields.push(this.$t('config.cardPosition'))
}
if (ruleDtoList.some((item) => item.deviceType !== firstRecord.deviceType)) {
inconsistentFields.push(this.$t('config.deviceType'))
}
if (inconsistentFields.length > 0) {
const fieldsText = inconsistentFields.join('、')
ElMessage.error(
this.$t('config.inconsistentFieldsError', {
fields: fieldsText
})
)
return
}
// 筛选既不是众测发布也不是待发布状态的条数
const invalidRecords = ruleDtoList.filter((item) => item.status !== 0 && item.status !== 1)
if (invalidRecords.length) {
// 提取不符合要求的卡片名和版本号
const invalidItems = invalidRecords
.map((item) => `${item.cardTitle}(${item.cardVer})`)
.join('、')
ElMessage.error(
this.$t('config.invalidRecordsBatchConfigCardRule', {
count: invalidRecords.length,
items: invalidItems,
requiredStatus: `${this.$t('config.betaRelease')}${
store.isEnglish ? ' ' : ''
}${this.$t('common.or')}${store.isEnglish ? ' ' : ''}${this.$t(
'config.pendingRelease'
)}`
})
)
return
}
this.batchRuleSetDialogVisible = true
this.ruleDtoList = ruleDtoList
return
}
if (type === 'stop') {
// 获取状态为0(不符合要求)的记录
const invalidRecords = ruleDtoList.filter((item) => item.status === 0)
if (invalidRecords.length) {
// 提取不符合要求的卡片名和版本号
const invalidItems = invalidRecords
.map((item) => `${item.cardTitle}(${item.cardVer})`)
.join('、')
ElMessage.error(
this.$t('config.invalidRecordsForStop', {
count: invalidRecords.length,
items: invalidItems,
requiredStatus: `${this.$t('config.betaRelease')}${
store.isEnglish ? ' ' : ''
}${this.$t('common.or')}${store.isEnglish ? ' ' : ''}${this.$t(
'config.fullRelease'
)}`
})
)
return
}
}
if (type === 'full') {
// 获取状态不为1(不符合全量发布要求)的记录
const invalidRecords = ruleDtoList.filter((item) => item.status !== 1)
if (invalidRecords.length) {
// 提取不符合要求的卡片名和版本号
const invalidItems = invalidRecords
.map((item) => `${item.cardTitle}(${item.cardVer})`)
.join('、')
ElMessage.error(
this.$t('config.invalidRecordsForFullRelease', {
count: invalidRecords.length,
items: invalidItems,
requiredStatus: this.$t('config.betaRelease')
})
)
return
}
}
let status
if (type === 'beta') {
status = 1
} else if (type === 'full') {
status = 2
} else {
status = 0
}
requestHandler({
url: '/v1/card/version-op/batch-update',
data: { ruleDtoList: ruleDtoList, status: status }
})
},
resetFields() {
Object.assign(this.filter, {
hostApp: '',
cardPosition: '',
deviceType: '',
cardName: '',
publishStatus: ''
})
this.handleFilter()
},
getDeviceType(type) {
if (type === '') {
return null
}
const _deviceType = this.deviceTypeOptions.find((ent) => {
return ent.value === type
})
if (_deviceType) {
return _deviceType.label
}
return ''
},
fetchCardList() {
this.tableData = []
this.loading = true
const queryData = {
hostApp: this.filter.hostApp || null,
cardPosition: this.filter.cardPosition || null,
deviceType: this.filter.deviceType || null,
cardName: this.filter.cardName || null,
publishStatus: this.filter.publishStatus,
pageNumber: this.currentPage,
pageSize: this.pageSize
}
axios
.post('/v1/card/operation/page-query', queryData)
.then((response) => {
if (response && response.data && response.data.code === 0) {
this.tableData = response.data.cardOpConfigs || []
this.total = response.data.count || 0
// 默认展开第一行(取第一行的row-key值)
if (this.tableData.length > 0) {
this.defaultExpandedRows = [this.tableData[0].id]
this.fetchCardVersionConfig(this.tableData[0].id, this.tableData[0])
}
} else {
ElMessage.error(response.data.message || this.$t('common.getListFailed'))
}
})
.finally(() => {
this.loading = false
})
},
queryCardOptions() {
const queryData = {
pageNumber: 1,
pageSize: 1000
}
axios.post('/v1/card/operation/page-query', queryData).then((response) => {
if (response && response.data && response.data.code === 0) {
this.cardOptions = response.data.cardOpConfigs
}
})
},
showAddDialog() {
this.dialogTitle = this.$t('config.addOperationConfig')
this.dialogVisible = true
this.currentIndex = -1
this.form = {}
},
handleEdit(index, row) {
this.dialogTitle = this.$t('config.editOperationConfig')
Object.assign(this.form, row)
this.currentIndex = index
this.dialogVisible = true
},
handleCancel() {
this.dialogVisible = false
},
batchUpdateIndex(index, row) {
Object.assign(this.form, row)
this.currentIndex = index
this.batchUpdateIndexDialogVisible = true
this.rowData = row
},
handleDelete(index, row) {
ElMessageBox.confirm(this.$t('common.confirmDelete'), this.$t('common.warning'), {
confirmButtonText: this.$t('common.confirm'),
cancelButtonText: this.$t('common.cancel'),
type: 'warning'
}).then(() => {
requestHandler({
url: '/v1/card/operation/delete',
data: { id: row.id }
})
})
},
handleSizeChange(val) {
this.pageSize = val
this.currentPage = 1
this.fetchCardList()
},
handleCurrentChange(val) {
this.currentPage = val
this.fetchCardList()
},
handleFilter() {
this.currentPage = 1
this.fetchCardList()
this.isAllExpanded = false
},
handleShowJson(jsonStr, title) {
try {
this.formattedJson = jsonStr
this.jsonViewerTitle = title
this.jsonViewerVisible = true
} catch (e) {
ElMessage.error(this.$t('common.jsonFormatError') + e)
}
},
closeShowJson() {
this.jsonViewerVisible = false
}
}
}
</script>
<style scoped>
.operation-container {
padding: 20px;
background-color: #fff;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.filter-container {
margin-bottom: 20px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.github-table {
--el-table-border-color: #e1e4e8;
--el-table-header-bg-color: #f6f8fa;
--el-table-row-hover-bg-color: #f6f8fa;
}
.github-table :deep(.el-table__header th) {
background-color: #f6f8fa;
color: #24292e;
font-weight: 600;
}
.github-table :deep(.el-table__body td) {
padding: 12px 0;
}
.filter-container {
padding: 20px;
margin-bottom: 20px;
border-radius: 4px;
}
.batch-toolbar {
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.batch-toolbar {
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 12px;
}
/* 修改行的样式 */
:deep(.el-table .modified-row) {
--el-table-tr-bg-color: rgba(var(--el-color-success-rgb), 0.08);
}
.expand-icon {
vertical-align: middle;
cursor: pointer;
color: rgb(96, 98, 102);
}
/* 垂直排列容器 - 完全居中方案 */
.vertical-tags-container {
display: grid;
justify-items: center; /* 水平居中 */
gap: 6px;
padding: 2px 0;
width: 100%;
}
/* 自适应宽度标签 */
.content-fit-tag {
width: max-content; /* 宽度严格包裹内容 */
margin: 0 auto !important; /* 双重居中保障 */
padding: 0 10px;
display: block; /* 使margin:auto生效 */
}
.version-container {
padding: 20px;
background-color: #fff;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.github-table {
--el-table-border-color: #e1e4e8;
--el-table-header-bg-color: #f6f8fa;
--el-table-row-hover-bg-color: #f6f8fa;
}
.github-table :deep(.el-table__header th) {
background-color: #f6f8fa;
color: #24292e;
font-weight: 600;
}
.github-table :deep(.el-table__body td) {
padding: 12px 0;
}
/* 表格标签专用样式 */
.tag-cell {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
min-height: 32px;
}
.table-tag {
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
transition: all 0.2s;
}
.more-tag {
background: var(--el-color-info-light-8);
border: none;
color: var(--el-color-info);
}
.hidden-tags-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
max-height: 200px;
overflow-y: auto;
padding: 8px;
}
.tag-cell {
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
flex-wrap: wrap; /* 允许标签换行 */
gap: 4px; /* 标签间距 */
width: 100%; /* 确保占据全部宽度 */
}
</style>
这个是我的CardConfig
<template>
<el-dialog v-model="visible" :title="title" width="60%" :before-close="resetForm">
<el-steps :active="activeStep" simple style="margin-bottom: 20px" v-if="!isEdit">
<el-step :title="title" />
<el-step :title="$t('config.crowdTestRules')" />
</el-steps>
<!-- Step 1: Card Configuration -->
<el-form
v-show="activeStep === 0"
ref="cardFormRef"
:model="form"
:rules="rules"
label-width="150px"
label-position="left"
>
<el-form-item :label="$t('config.hostApp')" prop="hostApp">
<el-select
filterable
v-model="form.hostApp"
:placeholder="$t('config.selectHostApp')"
style="width: 100%"
allow-create
:disabled="isEdit"
>
<el-option
v-for="app in hostAppOptions"
:key="app"
:label="getHostAppLabel(app)"
:value="app"
/>
</el-select>
</el-form-item>
<el-form-item :label="$t('config.cardPosition')" prop="cardPosition">
<el-select
filterable
v-model="form.cardPosition"
:placeholder="$t('config.selectCardPosition')"
style="width: 100%"
:disabled="isEdit"
allow-create
>
<el-option
v-for="position in cardPositionOptions"
:key="position"
:label="getCardPositionLabel(position)"
:value="position"
/>
</el-select>
</el-form-item>
<el-form-item :label="$t('management.cardName')" prop="cardName">
<el-select
v-model="form.cardName"
:placeholder="$t('common.selectCardName')"
style="width: 100%"
filterable
:disabled="isEdit"
@change="selectCard"
popper-class="wider-select-popper"
>
<el-option
v-for="item in cardOptions"
:key="item.cardName"
:label="`${cardTitleMap[item.cardName]}-${item.cardName}`"
:value="item.cardName"
style="width: 800px"
/>
</el-select>
</el-form-item>
<el-form-item :label="$t('config.deviceType')" prop="deviceType">
<el-select
filterable
v-model="form.deviceType"
:placeholder="$t('config.selectDeviceType')"
:disabled="isEdit"
style="width: 100%"
>
<el-option
v-for="item in deviceTypeOptions.filter(
(item) => availableDeviceTypeOptions.indexOf(item.value) !== -1
)"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item :label="$t('config.sortIndex')" prop="index" v-if="form.cardType !== 2">
<el-input-number
v-model="form.index"
:min="1"
:max="100"
size="default"
style="width: 100%"
/>
</el-form-item>
</el-form>
<!-- Step 2: Crowd Testing Rules -->
<el-form
v-show="activeStep === 1"
v-if="!isEdit"
ref="testRulesFormRef"
:model="testRules"
:rules="testRulesRules"
label-width="150px"
label-position="left"
>
<el-form-item :label="$t('version.cardVer')" prop="cardVer" required>
<el-select
v-model="testRules.cardVer"
:placeholder="$t('version.cardVer')"
style="width: 100%"
filterable
>
<el-option
v-for="version in cardVersionOptions"
:key="version"
:label="version"
:value="version"
/>
</el-select>
</el-form-item>
<el-form-item :label="$t('config.configMethod')" prop="configMethod" required>
<el-radio-group v-model="testRules.configMethod" @change="changeConfigMethod">
<el-radio value="template">{{ $t('config.useTemplate') }}</el-radio>
<el-radio value="manual">{{ $t('config.manualSetup') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="testRules.configMethod === 'template'"
:label="$t('config.ruleTemplate')"
prop="templateId"
>
<el-select
v-model="testRules.templateId"
:placeholder="$t('config.selectTemplate')"
style="width: 100%"
filterable
@change="handleTemplateChange"
>
<el-option
v-for="template in ruleTemplateOptions"
:key="template.id"
:label="template.ruleName"
:value="template.id"
/>
</el-select>
</el-form-item>
<CardRules
:action="'edit'"
:formData="group"
ref="cardRulesFormRef"
@rules-changed="handleRulesChanged"
/>
</el-form>
<template #footer>
<el-button @click="resetForm()">{{ $t('common.cancel') }}</el-button>
<el-button v-if="activeStep === 0" @click="submitForm" type="primary">
{{ $t('common.save') }}
</el-button>
<el-button v-if="activeStep > 0" @click="prevStep">{{ $t('common.prev') }}</el-button>
<el-button v-if="activeStep < 1 && !isEdit" type="primary" @click="nextStep">
{{ $t('common.next') }}
</el-button>
<el-button v-if="activeStep === 1" @click="saveAndPublish(1)" type="primary">
{{ $t('common.saveAndBetaPublish') }}
</el-button>
</template>
</el-dialog>
</template>
<script>
import { ElMessage } from 'element-plus'
import validator from '@/utils/ValidateUtil.ts'
import { requestHandler } from '@/utils/request'
import axios from 'axios'
import CardRules from '../../testrule/components/CardRules.vue'
import { useGlobalMaps } from '@/utils/util.ts'
import { reactive } from 'vue'
export default {
setup() {
const maps = reactive({
hostAppMap: {},
cardPositionMap: {},
providerMap: {}
})
const getHostAppLabel = (app) => maps.hostAppMap[app] || app
const getCardPositionLabel = (app) => maps.cardPositionMap[app] || app
return { getHostAppLabel, getCardPositionLabel, maps }
},
components: {
CardRules
},
props: {
cancel: Function,
dialogVisible: {
type: Boolean,
default: false
},
title: {
type: String,
default: ''
},
formData: Object,
hostAppOptions: {
type: Array,
default: () => []
},
cardPositionOptions: {
type: Array,
default: () => []
},
deviceTypeOptions: {
type: Array,
default: () => []
},
cardOptions: {
type: Array,
default: () => []
},
isEdit: {
type: Boolean,
default: false
},
getHostAppLabel: Function,
getCardPositionLabel: Function,
cardTitleMap: Object
},
emits: ['update:visible', 'success'],
computed: {
visible: function () {
return this.dialogVisible
}
},
data() {
return {
formRef: {},
testRulesFormRef: {},
activeStep: 0,
form: {
hostApp: this.formData.hostApp,
cardPosition: this.formData.cardPosition,
deviceType: this.formData.deviceType,
cardName: this.formData.cardName,
id: this.formData.id,
cardType: 0,
index: this.formData.index || 1
},
group: {
testRules: [
{
id: Date.now(),
items: [
{
op: 'equal',
key: '',
values: []
}
]
}
]
},
testRules: {
cardVer: '',
configMethod: 'template',
templateId: ''
},
rules: {
hostApp: [
{ required: true, message: this.$t('config.selectHostApp'), trigger: 'change' },
{
pattern: validator.hostApp,
message: this.$t('config.validHostApp'),
trigger: ['blur', 'change', 'input']
}
],
cardPosition: [
{ required: true, message: this.$t('config.selectCardPosition'), trigger: 'change' },
{
pattern: validator.cardPosition,
message: this.$t('config.validPosition'),
trigger: ['blur', 'change']
}
],
deviceType: [
{ required: true, message: this.$t('config.selectDeviceType'), trigger: 'change' }
],
cardName: [
{ required: true, message: this.$t('common.selectCardName'), trigger: 'change' }
],
index: [
{ required: true, message: this.$t('config.enterCardIndex'), trigger: 'blur' },
{ pattern: validator.integer, message: this.$t('common.validNumber'), trigger: 'blur' },
{ validator: (_, val) => val <= 2147483647, message: this.$t('common.validNumber') }
]
},
testRulesRules: {
cardVer: [{ required: true, message: this.$t('version.cardVer'), trigger: 'change' }],
templateId: [
{
required: true,
message: this.$t('config.selectTemplate'),
trigger: 'change',
when: () => this.testRules.configMethod === 'template'
}
]
},
cardVersionOptions: [],
availableDeviceTypeOptions: [],
ruleTemplateOptions: []
}
},
watch: {
formData() {
this.form = JSON.parse(JSON.stringify(this.formData))
},
isEdit() {},
'form.cardName'(newVal) {
if (newVal && this.activeStep === 1) {
this.loadCardVersions()
}
}
},
created() {
this.initMaps() // 组件创建时初始化
},
methods: {
selectCard() {
// 设置卡片类型
const item = this.cardOptions.find((ent) => {
return ent.cardName === this.form.cardName
})
this.form.cardType = item.cardType
// 查询版本
const queryData = {
pageNumber: 1,
pageSize: 1000,
cardName: this.form.cardName
}
axios.post('/v2/card/version/page-query', queryData).then((response) => {
if (response && response.data && response.data.code === 0) {
this.availableDeviceTypeOptions = [
...new Set(response.data.cardVersions.flatMap((item) => item.deviceTypes))
]
this.form.deviceType = null
}
})
},
// 新增处理规则变化的方法
handleRulesChanged(newRules) {
this.group.testRules = newRules
},
async nextStep() {
try {
await this.$refs.cardFormRef.validate()
this.activeStep++
this.loadCardVersions()
this.loadRuleTemplates()
} catch (error) {
console.error('Validation failed:', error)
}
},
prevStep() {
this.activeStep--
},
handleTemplateChange(selectedId) {
// 从 ruleTemplateOptions 中找到选中的 template
const selectedTemplate = this.ruleTemplateOptions.find(
(template) => template.id === selectedId
)
if (selectedTemplate) {
this.group = { testRules: selectedTemplate.testRules }
}
},
changeConfigMethod(configMethod) {
if (configMethod === 'manual') {
this.group = {
testRules: [
{
id: Date.now(),
items: [
{
op: 'equal',
key: '',
values: []
}
]
}
]
}
} else {
this.handleTemplateChange(this.testRules.templateId)
}
},
loadCardVersions() {
const queryData = {
pageNumber: 1,
pageSize: 1000,
cardName: this.form.cardName,
deviceTypes: [this.form.deviceType]
}
axios.post('/v2/card/version/page-query', queryData).then((response) => {
if (response && response.data && response.data.code === 0) {
this.cardVersionOptions = [
...new Set(response.data.cardVersions.map((item) => item.cardVer))
]
}
})
},
loadRuleTemplates() {
const queryData = {
pageNumber: 1,
pageSize: 1000,
hostApp: this.form.hostApp
}
axios.post('/v1/card/test-rule/page-query', queryData).then((response) => {
if (response && response.data && response.data.code === 0) {
this.ruleTemplateOptions = response.data.testRuleDtos
}
})
},
async submitForm() {
try {
await Promise.all([this.$refs.cardFormRef.validate()])
const cardNameToIndex = {}
const index = this.form.cardType === 2 ? 0 : this.form.index || 1
this.form.index = index
cardNameToIndex[this.form.cardName] = index
const requestData = {
...this.form,
testRules: this.testRules,
cardIndexMap: cardNameToIndex
}
const url = this.isEdit
? '/v1/card/operation/batch-update-index'
: '/v1/card/operation/insert'
await requestHandler({
url,
data: requestData
})
this.$emit('success')
this.cancel()
} catch (error) {
// 处理后端返回的重复错误 (code 100001)
if (error.response?.data.code === 100001) {
ElMessage.error(this.$t('config.cardDuplicateIndexError'))
return
}
if (error.message) {
ElMessage.error(error.message || this.$t('common.operationFailed'))
}
}
},
async saveAndPublish(status) {
try {
await Promise.all([
this.$refs.cardFormRef.validate(),
this.$refs.testRulesFormRef.validate(),
this.$refs.cardRulesFormRef.validate()
])
this.form.index = this.form.cardType === 2 ? 0 : this.form.index || 1
const requestData = {
...this.form,
next: {
cardName: this.form.cardName,
cardVer: this.testRules.cardVer,
status: status,
testRules: this.group.testRules
}
}
const url = '/v1/card/operation/insert'
await requestHandler({
url,
data: requestData
})
this.$emit('success')
this.cancel()
} catch (error) {
console.error('Submit failed:', error)
}
},
resetForm() {
this.cancel()
this.activeStep = 0
if (this.$refs.cardFormRef) {
this.$refs.cardFormRef.resetFields()
}
if (this.$refs.testRulesFormRef) {
this.$refs.testRulesFormRef.resetFields()
}
this.form = {
hostApp: '',
cardPosition: '',
deviceType: 'phone',
cardName: '',
index: 1
}
this.testRules = {
cardVer: '',
configMethod: 'template',
templateId: ''
}
this.group = null
},
async initMaps() {
const { maps, initGlobalMaps } = useGlobalMaps()
await initGlobalMaps()
// 手动将 ref 值赋给 data(失去响应性,需注意)
this.maps = maps.value
}
}
}
</script>
<style scoped>
/* 扩大 el-select 下拉框的宽度 */
.wider-select-popper .el-select-dropdown__list {
min-width: 600px !important;
width: 100% !important;
max-width: 800px;
}
</style>
这个是我的addCardConfig
我怎么修改才能修改下拉框背景宽度?
最新发布