feat: select rocket prop rewards from drawer

This commit is contained in:
zhx 2026-07-15 16:56:22 +08:00
parent eb25cb8981
commit b5a86072f2
5 changed files with 212 additions and 40 deletions

View File

@ -73,6 +73,10 @@ export default {
type: String, type: String,
return: true, return: true,
default: '' default: ''
},
propsTypes: {
type: Array,
default: () => []
} }
}, },
data() { data() {
@ -92,6 +96,7 @@ export default {
{ propsType: 'EMOJI', nameKey: 'pages.propsSourceGroupForm.resourceType.emoji', loading: false, loadData: false, list: [], placeholderKey: 'pages.propsSourceSelectDrawer.unit.quantity' }, { propsType: 'EMOJI', nameKey: 'pages.propsSourceGroupForm.resourceType.emoji', loading: false, loadData: false, list: [], placeholderKey: 'pages.propsSourceSelectDrawer.unit.quantity' },
{ propsType: 'CHAT_BUBBLE', nameKey: 'pages.propsSourceGroupForm.resourceType.chatBubble', loading: false, loadData: false, list: [], placeholderKey: 'pages.propsSourceSelectDrawer.unit.days' }, { propsType: 'CHAT_BUBBLE', nameKey: 'pages.propsSourceGroupForm.resourceType.chatBubble', loading: false, loadData: false, list: [], placeholderKey: 'pages.propsSourceSelectDrawer.unit.days' },
{ propsType: 'FLOAT_PICTURE', nameKey: 'pages.propsSourceGroupForm.resourceType.floatPicture', loading: false, loadData: false, list: [], placeholderKey: 'pages.propsSourceSelectDrawer.unit.days' }, { propsType: 'FLOAT_PICTURE', nameKey: 'pages.propsSourceGroupForm.resourceType.floatPicture', loading: false, loadData: false, list: [], placeholderKey: 'pages.propsSourceSelectDrawer.unit.days' },
{ propsType: 'DATA_CARD', nameKey: 'pages.propsSourceGroupForm.resourceType.dataCard', loading: false, loadData: false, list: [], placeholderKey: 'pages.propsSourceSelectDrawer.unit.days' },
{ propsType: 'CUSTOMIZE', nameKey: 'pages.propsSourceGroupForm.resourceType.customizeReadonly', loading: false, loadData: false, list: [], placeholderKey: 'pages.propsSourceSelectDrawer.unit.description' }, { propsType: 'CUSTOMIZE', nameKey: 'pages.propsSourceGroupForm.resourceType.customizeReadonly', loading: false, loadData: false, list: [], placeholderKey: 'pages.propsSourceSelectDrawer.unit.description' },
{ propsType: 'FRAGMENTS', nameKey: 'pages.propsSourceGroupForm.resourceType.fragments', loading: false, loadData: false, list: [], placeholderKey: 'pages.propsSourceSelectDrawer.unit.quantity' } { propsType: 'FRAGMENTS', nameKey: 'pages.propsSourceGroupForm.resourceType.fragments', loading: false, loadData: false, list: [], placeholderKey: 'pages.propsSourceSelectDrawer.unit.quantity' }
], ],
@ -135,8 +140,17 @@ export default {
} }
}, },
created() { created() {
//
if (this.propsTypes.length > 0) {
this.configPropsTypes = this.configPropsTypes.filter(item => this.propsTypes.includes(item.propsType))
if (!this.configPropsTypes.some(item => item.propsType === this.listQuery.propsType)) {
this.listQuery.propsType = this.configPropsTypes[0] ? this.configPropsTypes[0].propsType : ''
}
}
this.configPropsTypesMap = this.configPropsTypesToMap() this.configPropsTypesMap = this.configPropsTypesToMap()
if (this.listQuery.propsType) {
this.propsTypeChange() this.propsTypeChange()
}
}, },
methods: { methods: {
configPropsTypesToMap() { configPropsTypesToMap() {
@ -151,6 +165,9 @@ export default {
}, },
loadSource(type) { loadSource(type) {
const that = this const that = this
if (!that.configPropsTypesMap[type]) {
return
}
if (that.configPropsTypesMap[type].loadData === true) { if (that.configPropsTypesMap[type].loadData === true) {
return return
} }

View File

@ -8028,6 +8028,11 @@ export default {
rewardType: 'Type', rewardType: 'Type',
rewardName: 'Name', rewardName: 'Name',
quantity: 'Quantity/Days', quantity: 'Quantity/Days',
days: 'Days',
selectedProp: 'Selected Prop',
selectProp: 'Select Prop',
changeProp: 'Change',
selected: 'Selected',
detailType: 'Prop Type', detailType: 'Prop Type',
detailTypePlaceholder: 'Enter a prop type recognized by the server', detailTypePlaceholder: 'Enter a prop type recognized by the server',
cover: 'Cover URL', cover: 'Cover URL',

View File

@ -8025,6 +8025,11 @@ export default {
rewardType: '类型', rewardType: '类型',
rewardName: '名称', rewardName: '名称',
quantity: '数量/天数', quantity: '数量/天数',
days: '天数',
selectedProp: '已选道具',
selectProp: '选择道具',
changeProp: '更换',
selected: '已选择',
detailType: '道具类型', detailType: '道具类型',
detailTypePlaceholder: '请输入服务端识别的道具类型', detailTypePlaceholder: '请输入服务端识别的道具类型',
cover: '封面地址', cover: '封面地址',

View File

@ -111,7 +111,7 @@
{{ $t('pages.rocketConfig.removeReward') }} {{ $t('pages.rocketConfig.removeReward') }}
</el-button> </el-button>
</div> </div>
<el-row :gutter="12"> <el-row :gutter="12" type="flex" align="middle">
<el-col :span="6"> <el-col :span="6">
<el-form-item <el-form-item
class="inner-form-item" class="inner-form-item"
@ -130,10 +130,10 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="6"> <el-col v-if="reward.type === 'GOLD'" :span="18">
<el-form-item <el-form-item
class="inner-form-item" class="inner-form-item"
:label="contentLabel(reward.type)" :label="$t('pages.rocketConfig.goldAmount')"
:prop="'rewardConfig.' + index + '.content'" :prop="'rewardConfig.' + index + '.content'"
:rules="rewardRules.content" :rules="rewardRules.content"
label-width="80px" label-width="80px"
@ -141,21 +141,34 @@
<el-input v-model.trim="reward.content" /> <el-input v-model.trim="reward.content" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="6"> <el-col v-else :span="12">
<el-form-item <el-form-item
class="inner-form-item" class="inner-form-item"
:label="$t('pages.rocketConfig.rewardName')" :label="$t('pages.rocketConfig.selectedProp')"
:prop="'rewardConfig.' + index + '.name'" :prop="'rewardConfig.' + index + '.content'"
:rules="rewardRules.name" :rules="rewardRules.content"
label-width="80px" label-width="80px"
> >
<el-input v-model.trim="reward.name" /> <div v-if="reward.content" class="selected-prop">
<el-image class="selected-prop__cover" :src="reward.cover" fit="cover">
<div slot="error" class="selected-prop__empty-cover">
<i class="el-icon-picture-outline" />
</div>
</el-image>
<span class="selected-prop__name">{{ reward.name || $t('pages.rocketConfig.selected') }}</span>
<el-button type="text" @click="openPropsDrawer(index)">
{{ $t('pages.rocketConfig.changeProp') }}
</el-button>
</div>
<el-button v-else type="primary" plain icon="el-icon-plus" @click="openPropsDrawer(index)">
{{ $t('pages.rocketConfig.selectProp') }}
</el-button>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col v-if="reward.type !== 'GOLD'" :span="6"> <el-col v-if="reward.type !== 'GOLD'" :span="6">
<el-form-item <el-form-item
class="inner-form-item" class="inner-form-item"
:label="$t('pages.rocketConfig.quantity')" :label="$t('pages.rocketConfig.days')"
:prop="'rewardConfig.' + index + '.quantity'" :prop="'rewardConfig.' + index + '.quantity'"
:rules="rewardRules.quantity" :rules="rewardRules.quantity"
label-width="80px" label-width="80px"
@ -164,24 +177,6 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="12">
<el-col v-if="reward.type === 'PROPS'" :span="12">
<el-form-item
class="inner-form-item"
:label="$t('pages.rocketConfig.detailType')"
:prop="'rewardConfig.' + index + '.detailType'"
:rules="rewardRules.detailType"
label-width="80px"
>
<el-input v-model.trim="reward.detailType" :placeholder="$t('pages.rocketConfig.detailTypePlaceholder')" />
</el-form-item>
</el-col>
<el-col :span="reward.type === 'PROPS' ? 12 : 24">
<el-form-item class="inner-form-item" :label="$t('pages.rocketConfig.cover')" label-width="80px">
<el-input v-model.trim="reward.cover" :placeholder="$t('pages.rocketConfig.coverPlaceholder')" />
</el-form-item>
</el-col>
</el-row>
</div> </div>
<el-empty <el-empty
@ -199,19 +194,31 @@
</el-button> </el-button>
</span> </span>
</el-dialog> </el-dialog>
<props-source-select-drawer
v-if="propsDrawerVisible"
:sys-origin="'ATYOU'"
:props-type="drawerPropsType"
:props-types="drawerPropsTypes"
@close="closePropsDrawer"
@select="selectPropsSource"
/>
</div> </div>
</template> </template>
<script> <script>
import { listRocketConfigs, updateRocketConfig } from '@/api/rocket-config' import { listRocketConfigs, updateRocketConfig } from '@/api/rocket-config'
import PropsSourceSelectDrawer from '@/components/data/PropsSourceSelectDrawer'
const createEmptyReward = () => ({ const createEmptyReward = () => ({
id: null,
type: 'GOLD', type: 'GOLD',
content: '', content: '',
name: '', name: '金币',
quantity: 1, quantity: 1,
detailType: '', detailType: '',
cover: '' cover: '',
sourceUrl: ''
}) })
const createEmptyForm = () => ({ const createEmptyForm = () => ({
@ -224,6 +231,7 @@ const createEmptyForm = () => ({
export default { export default {
name: 'RocketConfig', name: 'RocketConfig',
components: { PropsSourceSelectDrawer },
data() { data() {
const validatePositiveInteger = (rule, value, callback) => { const validatePositiveInteger = (rule, value, callback) => {
if (!/^\d+$/.test(String(value || '')) || Number(value) <= 0) { if (!/^\d+$/.test(String(value || '')) || Number(value) <= 0) {
@ -237,6 +245,10 @@ export default {
listLoading: false, listLoading: false,
submitLoading: false, submitLoading: false,
dialogVisible: false, dialogVisible: false,
propsDrawerVisible: false,
selectingRewardIndex: -1,
drawerPropsType: 'AVATAR_FRAME',
drawerPropsTypes: [],
configList: [], configList: [],
form: createEmptyForm(), form: createEmptyForm(),
rules: { rules: {
@ -250,10 +262,8 @@ export default {
}, },
rewardRules: { rewardRules: {
type: [{ required: true, message: this.$t('pages.rocketConfig.rewardTypeRequired'), trigger: 'change' }], type: [{ required: true, message: this.$t('pages.rocketConfig.rewardTypeRequired'), trigger: 'change' }],
content: [{ required: true, message: this.$t('pages.rocketConfig.rewardContentRequired'), trigger: 'blur' }], content: [{ required: true, message: this.$t('pages.rocketConfig.rewardContentRequired'), trigger: 'change' }],
name: [{ required: true, message: this.$t('pages.rocketConfig.rewardNameRequired'), trigger: 'blur' }], quantity: [{ validator: validatePositiveInteger, trigger: 'blur' }]
quantity: [{ validator: validatePositiveInteger, trigger: 'blur' }],
detailType: [{ required: true, message: this.$t('pages.rocketConfig.detailTypeRequired'), trigger: 'blur' }]
} }
} }
}, },
@ -288,11 +298,6 @@ export default {
const amount = reward.type === 'GOLD' ? reward.content : reward.quantity const amount = reward.type === 'GOLD' ? reward.content : reward.quantity
return `${reward.name || typeName} (${amount || '-'})` return `${reward.name || typeName} (${amount || '-'})`
}, },
contentLabel(type) {
return type === 'GOLD'
? this.$t('pages.rocketConfig.goldAmount')
: this.$t('pages.rocketConfig.resourceId')
},
openEdit(row) { openEdit(row) {
// //
const rewardConfig = (row.rewardConfig || []).map(item => Object.assign(createEmptyReward(), JSON.parse(JSON.stringify(item)))) const rewardConfig = (row.rewardConfig || []).map(item => Object.assign(createEmptyReward(), JSON.parse(JSON.stringify(item))))
@ -319,11 +324,55 @@ export default {
this.form.rewardConfig.splice(index, 1) this.form.rewardConfig.splice(index, 1)
}, },
handleRewardTypeChange(reward) { handleRewardTypeChange(reward) {
if (reward.type === 'GOLD') { //
reward.quantity = 1 reward.id = null
reward.content = ''
reward.name = ''
reward.cover = ''
reward.sourceUrl = ''
reward.detailType = '' reward.detailType = ''
if (reward.type === 'GOLD') {
reward.name = this.$t('pages.rocketConfig.types.gold')
reward.quantity = 1
} }
}, },
openPropsDrawer(index) {
const reward = this.form.rewardConfig[index]
const commonPropTypes = ['AVATAR_FRAME', 'RIDE', 'NOBLE_VIP', 'THEME', 'CHAT_BUBBLE', 'FLOAT_PICTURE', 'DATA_CARD']
this.selectingRewardIndex = index
this.drawerPropsTypes = reward.type === 'BADGE' ? ['BADGE'] : commonPropTypes
this.drawerPropsType = this.drawerPropsTypes.includes(reward.detailType)
? reward.detailType
: this.drawerPropsTypes[0]
this.propsDrawerVisible = true
},
closePropsDrawer() {
this.propsDrawerVisible = false
this.selectingRewardIndex = -1
},
selectPropsSource(item) {
const reward = this.form.rewardConfig[this.selectingRewardIndex]
if (!reward) {
this.closePropsDrawer()
return
}
// ID
reward.id = item.id
reward.content = String(item.id)
reward.name = item.name
reward.cover = item.cover || ''
reward.sourceUrl = item.sourceUrl || ''
reward.detailType = item.type
const field = `rewardConfig.${this.selectingRewardIndex}.content`
this.propsDrawerVisible = false
this.$nextTick(() => {
if (this.$refs.configForm && this.$refs.configForm.clearValidate) {
this.$refs.configForm.clearValidate(field)
}
this.selectingRewardIndex = -1
})
},
submitForm() { submitForm() {
if (this.form.rewardConfig.length === 0) { if (this.form.rewardConfig.length === 0) {
this.$message.warning(this.$t('pages.rocketConfig.rewardRequired')) this.$message.warning(this.$t('pages.rocketConfig.rewardRequired'))
@ -406,6 +455,38 @@ export default {
margin-bottom: 18px; margin-bottom: 18px;
} }
.selected-prop {
display: flex;
align-items: center;
min-height: 44px;
}
.selected-prop__cover {
width: 44px;
height: 44px;
margin-right: 10px;
border-radius: 4px;
background: #f2f3f5;
}
.selected-prop__empty-cover {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: #c0c4cc;
}
.selected-prop__name {
flex: 1;
min-width: 0;
margin-right: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.danger-text { .danger-text {
color: #f56c6c; color: #f56c6c;
} }

View File

@ -2,6 +2,11 @@ import Vue from 'vue'
import { shallowMount } from '@vue/test-utils' import { shallowMount } from '@vue/test-utils'
import RocketConfig from '@/views/rocket/config/index.vue' import RocketConfig from '@/views/rocket/config/index.vue'
jest.mock('@/components/data/PropsSourceSelectDrawer', () => ({
name: 'PropsSourceSelectDrawer',
render: h => h('div')
}))
jest.mock('@/api/rocket-config', () => ({ jest.mock('@/api/rocket-config', () => ({
listRocketConfigs: jest.fn(() => Promise.resolve({ body: [] })), listRocketConfigs: jest.fn(() => Promise.resolve({ body: [] })),
updateRocketConfig: jest.fn(() => Promise.resolve({})) updateRocketConfig: jest.fn(() => Promise.resolve({}))
@ -42,4 +47,63 @@ describe('RocketConfig edit dialog', () => {
expect(wrapper.vm.form.rewardConfig[0].type).toBe('GOLD') expect(wrapper.vm.form.rewardConfig[0].type).toBe('GOLD')
expect(wrapper.find('.rocket-dialog-content').exists()).toBe(true) expect(wrapper.find('.rocket-dialog-content').exists()).toBe(true)
}) })
it('fills all internal reward fields from the prop drawer selection', async() => {
const wrapper = createWrapper()
wrapper.vm.form.rewardConfig.push({
id: null,
type: 'PROPS',
content: '',
name: '',
quantity: 7,
detailType: '',
cover: '',
sourceUrl: ''
})
wrapper.vm.openPropsDrawer(0)
expect(wrapper.vm.propsDrawerVisible).toBe(true)
expect(wrapper.vm.drawerPropsTypes).toContain('AVATAR_FRAME')
wrapper.vm.selectPropsSource({
id: '123456',
type: 'AVATAR_FRAME',
name: '星空头像框',
cover: 'https://cdn.example.com/cover.png',
sourceUrl: 'https://cdn.example.com/source.svga'
})
await wrapper.vm.$nextTick()
expect(wrapper.vm.form.rewardConfig[0]).toEqual({
id: '123456',
type: 'PROPS',
content: '123456',
name: '星空头像框',
quantity: 7,
detailType: 'AVATAR_FRAME',
cover: 'https://cdn.example.com/cover.png',
sourceUrl: 'https://cdn.example.com/source.svga'
})
expect(wrapper.vm.propsDrawerVisible).toBe(false)
})
it('does not render manual resource metadata inputs for prop rewards', async() => {
const wrapper = createWrapper()
wrapper.vm.form.rewardConfig.push({
id: '123456',
type: 'PROPS',
content: '123456',
name: '星空头像框',
quantity: 7,
detailType: 'AVATAR_FRAME',
cover: 'https://cdn.example.com/cover.png',
sourceUrl: ''
})
await wrapper.vm.$nextTick()
expect(wrapper.find('.selected-prop').exists()).toBe(true)
expect(wrapper.text()).not.toContain('pages.rocketConfig.resourceId')
expect(wrapper.text()).not.toContain('pages.rocketConfig.detailType')
expect(wrapper.text()).not.toContain('pages.rocketConfig.cover')
})
}) })