10 changed files with 1055 additions and 0 deletions
@ -0,0 +1,91 @@ |
|||||
|
import http, { type EnumInfoDto } from "@/api/index"; |
||||
|
|
||||
|
export interface Monster { |
||||
|
id: number; |
||||
|
name: string; |
||||
|
description: string; |
||||
|
health: number; |
||||
|
attack: number; |
||||
|
defense: number; |
||||
|
criticalRate: number; |
||||
|
level: number; |
||||
|
type: number; |
||||
|
rewards?: MonsterReward[]; |
||||
|
} |
||||
|
|
||||
|
export interface MonsterReward { |
||||
|
id: number; |
||||
|
monsterId: number; |
||||
|
rewardType: number; |
||||
|
itemId: number; |
||||
|
itemName: string; |
||||
|
count: number; |
||||
|
} |
||||
|
|
||||
|
export interface PagedResult<T> { |
||||
|
items: T[]; |
||||
|
totalCount: number; |
||||
|
pageNumber?: number; |
||||
|
} |
||||
|
|
||||
|
interface SearchMonsterDto { |
||||
|
pageNumber: number | undefined; |
||||
|
pageSize: number | undefined; |
||||
|
monsterType: number | undefined; |
||||
|
level: number | undefined; |
||||
|
} |
||||
|
|
||||
|
export const GetMonsterList = ( |
||||
|
monsterType?: number, |
||||
|
level?: number, |
||||
|
pageNumber?: number, |
||||
|
pageSize?: number |
||||
|
): Promise<PagedResult<Monster> | Monster[]> => { |
||||
|
var dto: SearchMonsterDto = { |
||||
|
pageNumber: pageNumber, |
||||
|
pageSize: pageSize, |
||||
|
monsterType: monsterType, |
||||
|
level: level, |
||||
|
}; |
||||
|
return http.post("/monster/all", dto); |
||||
|
}; |
||||
|
|
||||
|
export const GetMonsterById = (id: number): Promise<Monster> => { |
||||
|
return http.get(`/monster/${id}`); |
||||
|
}; |
||||
|
|
||||
|
export const AddMonster = (data: Monster): Promise<boolean> => { |
||||
|
return http.post("/monster", data); |
||||
|
}; |
||||
|
|
||||
|
export const UpdateMonster = (data: Monster): Promise<boolean> => { |
||||
|
return http.put(`/monster/${data.id}`, data); |
||||
|
}; |
||||
|
|
||||
|
export const DeleteMonster = (id: number): Promise<boolean> => { |
||||
|
return http.delete(`/monster/${id}`); |
||||
|
}; |
||||
|
|
||||
|
export const GetMonsterTypes = (): Promise<EnumInfoDto[]> => { |
||||
|
return http.get("/monster/types"); |
||||
|
}; |
||||
|
|
||||
|
export const GetMonsterRewards = (monsterId: number): Promise<MonsterReward[]> => { |
||||
|
return http.get(`/monster/rewards/${monsterId}`); |
||||
|
}; |
||||
|
|
||||
|
export const GetRewardTypes = (): Promise<EnumInfoDto[]> => { |
||||
|
return http.get("/monster/reward-types"); |
||||
|
}; |
||||
|
|
||||
|
export const AddMonsterReward = (data: MonsterReward): Promise<boolean> => { |
||||
|
return http.post("/monster/reward", data); |
||||
|
}; |
||||
|
|
||||
|
export const UpdateMonsterReward = (data: MonsterReward): Promise<boolean> => { |
||||
|
return http.put(`/monster/reward/${data.id}`, data); |
||||
|
}; |
||||
|
|
||||
|
export const DeleteMonsterReward = (id: number): Promise<boolean> => { |
||||
|
return http.delete(`/monster/reward/${id}`); |
||||
|
}; |
||||
@ -0,0 +1,658 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import { ref, computed, onMounted } from 'vue' |
||||
|
import { ElMessage, ElMessageBox } from 'element-plus' |
||||
|
import { |
||||
|
AddMonster, |
||||
|
DeleteMonster, |
||||
|
GetMonsterList, |
||||
|
GetMonsterTypes, |
||||
|
UpdateMonster, |
||||
|
type Monster, |
||||
|
type MonsterReward, |
||||
|
GetRewardTypes, |
||||
|
AddMonsterReward, |
||||
|
UpdateMonsterReward, |
||||
|
DeleteMonsterReward, |
||||
|
GetMonsterRewards |
||||
|
} from '@/api/monster' |
||||
|
import type { EnumInfoDto } from '@/api' |
||||
|
import { GetPillList, type Pill } from '@/api/pill' |
||||
|
import { GetEquipmentTemplateList, type EquipmentTemplate } from '@/api/equipment' |
||||
|
import { Plus, Edit, Delete, Close } from '@element-plus/icons-vue' |
||||
|
import { ICONS } from '@/constants/theme' |
||||
|
|
||||
|
const monsters = ref<Monster[]>([]) |
||||
|
|
||||
|
const monsterTypes = ref<EnumInfoDto[]>([]) |
||||
|
const rewardTypes = ref<EnumInfoDto[]>([]) |
||||
|
const pillData = ref<Pill[]>([]) |
||||
|
const equipmentData = ref<EquipmentTemplate[]>([]) |
||||
|
|
||||
|
const currentPage = ref(1) |
||||
|
const pageSize = ref(10) |
||||
|
const totalCount = ref(0) |
||||
|
|
||||
|
const monsterTypeFilter = ref<number | undefined>(undefined) |
||||
|
const levelFilter = ref<number | undefined>(undefined) |
||||
|
|
||||
|
const showDialog = ref(false) |
||||
|
const showRewardDialog = ref(false) |
||||
|
const isEditing = ref(false) |
||||
|
const isEditingReward = ref(false) |
||||
|
const searchQuery = ref('') |
||||
|
|
||||
|
const formData = ref<Monster>({ |
||||
|
id: 0, |
||||
|
name: '', |
||||
|
description: '', |
||||
|
health: 0, |
||||
|
attack: 0, |
||||
|
defense: 0, |
||||
|
criticalRate: 0, |
||||
|
level: 1, |
||||
|
type: 1 |
||||
|
}) |
||||
|
|
||||
|
const rewardFormData = ref<MonsterReward>({ |
||||
|
id: 0, |
||||
|
monsterId: 0, |
||||
|
rewardType: 0, |
||||
|
itemId: 0, |
||||
|
itemName: '', |
||||
|
count: 0 |
||||
|
}) |
||||
|
|
||||
|
const rewardSelectData = ref<{ id: number; name: string }[]>([]) |
||||
|
|
||||
|
const filteredMonsters = computed(() => { |
||||
|
return monsters.value.filter(m => |
||||
|
m.name.toLowerCase().includes(searchQuery.value.toLowerCase()) |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
const translateMonsterType = (typeId: number) => { |
||||
|
const type = monsterTypes.value.find(t => t.id === typeId) |
||||
|
return type ? type.description : '未知类型' |
||||
|
} |
||||
|
|
||||
|
const getMonsterTypeClass = (typeId: number) => { |
||||
|
switch (typeId) { |
||||
|
case 1: |
||||
|
return 'type-normal' |
||||
|
case 2: |
||||
|
return 'type-elite' |
||||
|
case 3: |
||||
|
return 'type-boss' |
||||
|
default: |
||||
|
return '' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const openDialog = (monster?: Monster) => { |
||||
|
if (monster !== undefined) { |
||||
|
isEditing.value = true |
||||
|
formData.value = { ...monster } |
||||
|
} else { |
||||
|
isEditing.value = false |
||||
|
formData.value = { |
||||
|
id: 0, |
||||
|
name: '', |
||||
|
description: '', |
||||
|
health: 0, |
||||
|
attack: 0, |
||||
|
defense: 0, |
||||
|
criticalRate: 0, |
||||
|
level: 1, |
||||
|
type: 1 |
||||
|
} |
||||
|
} |
||||
|
showDialog.value = true |
||||
|
} |
||||
|
|
||||
|
const closeDialog = () => { |
||||
|
showDialog.value = false |
||||
|
} |
||||
|
|
||||
|
const openRewardDialog = async (monsterId?: number, reward?: MonsterReward) => { |
||||
|
if (reward !== undefined) { |
||||
|
isEditingReward.value = true |
||||
|
if (reward.rewardType) await rewardTypeChange(reward.rewardType) |
||||
|
rewardFormData.value = { ...reward } |
||||
|
rewardFormData.value.monsterId = monsterId ?? 0 |
||||
|
if (rewardFormData.value.itemId) onRewardItemChange(rewardFormData.value.itemId) |
||||
|
} else { |
||||
|
isEditingReward.value = false |
||||
|
rewardFormData.value = { |
||||
|
id: 0, |
||||
|
monsterId: monsterId ?? 0, |
||||
|
rewardType: 0, |
||||
|
itemId: 0, |
||||
|
itemName: '', |
||||
|
count: 0 |
||||
|
} |
||||
|
rewardSelectData.value = [] |
||||
|
} |
||||
|
showRewardDialog.value = true |
||||
|
} |
||||
|
|
||||
|
const closeRewardDialog = () => { |
||||
|
showRewardDialog.value = false |
||||
|
} |
||||
|
|
||||
|
const rewardTypeChange = async (id: number) => { |
||||
|
// RewardType: 1=丹药, 2=装备, 3=经验, 4=灵石 |
||||
|
if (id === 1) { |
||||
|
const pills = await GetPillList() |
||||
|
rewardSelectData.value = (Array.isArray(pills) ? pills : []).map(p => ({ id: p.id, name: p.name })) |
||||
|
rewardFormData.value.itemId = rewardSelectData.value.length ? rewardSelectData.value[0]!.id : 0 |
||||
|
if (rewardFormData.value.itemId) onRewardItemChange(rewardFormData.value.itemId) |
||||
|
} else if (id === 2) { |
||||
|
const eqs = await fetchEquipments() |
||||
|
rewardSelectData.value = (Array.isArray(eqs) ? eqs : []).map(e => ({ id: e.id, name: e.name })) |
||||
|
rewardFormData.value.itemId = rewardSelectData.value.length ? rewardSelectData.value[0]!.id : 0 |
||||
|
if (rewardFormData.value.itemId) onRewardItemChange(rewardFormData.value.itemId) |
||||
|
} else if (id === 3) { |
||||
|
rewardSelectData.value = [] |
||||
|
rewardFormData.value.itemId = 0 |
||||
|
rewardFormData.value.itemName = '经验' |
||||
|
} else if (id === 4) { |
||||
|
rewardSelectData.value = [] |
||||
|
rewardFormData.value.itemId = 0 |
||||
|
rewardFormData.value.itemName = '灵石' |
||||
|
} else { |
||||
|
rewardSelectData.value = [] |
||||
|
rewardFormData.value.itemId = 0 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const onRewardItemChange = (id: number) => { |
||||
|
const found = rewardSelectData.value.find(x => x.id === id) |
||||
|
if (found) { |
||||
|
rewardFormData.value.itemName = found.name |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const saveReward = async () => { |
||||
|
if (isEditingReward.value) { |
||||
|
var result = await UpdateMonsterReward(rewardFormData.value) |
||||
|
if (result) { |
||||
|
ElMessage.success('奖励修改成功') |
||||
|
closeRewardDialog() |
||||
|
await refreshMonsters() |
||||
|
} else { |
||||
|
ElMessage.error('修改失败') |
||||
|
} |
||||
|
} else { |
||||
|
var result = await AddMonsterReward(rewardFormData.value) |
||||
|
if (result) { |
||||
|
ElMessage.success('奖励添加成功') |
||||
|
closeRewardDialog() |
||||
|
await refreshMonsters() |
||||
|
} else { |
||||
|
ElMessage.error('添加失败') |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const deleteReward = (reward: MonsterReward) => { |
||||
|
ElMessageBox.confirm(`确定删除此奖励吗?`, '提示', { |
||||
|
confirmButtonText: '确定', |
||||
|
cancelButtonText: '取消', |
||||
|
type: 'warning', |
||||
|
}).then(async () => { |
||||
|
var result = await DeleteMonsterReward(reward.id) |
||||
|
if (result) { |
||||
|
ElMessage.success('奖励删除成功') |
||||
|
await refreshMonsters() |
||||
|
} else { |
||||
|
ElMessage.error('删除失败') |
||||
|
} |
||||
|
}).catch(() => { }) |
||||
|
} |
||||
|
|
||||
|
const getRewardTypeLabel = (id: number) => { |
||||
|
const t = rewardTypes.value.find(x => x.id === id) |
||||
|
if (t) return t.description |
||||
|
const map: Record<number, string> = { 1: '丹药', 2: '装备', 3: '经验', 4: '灵石' } |
||||
|
return map[id] || '未知' |
||||
|
} |
||||
|
|
||||
|
const getRewardEmoji = (id: number) => { |
||||
|
switch (id) { |
||||
|
case 1: |
||||
|
return ICONS.reward.pill |
||||
|
case 2: |
||||
|
return ICONS.reward.equipment |
||||
|
case 3: |
||||
|
return ICONS.reward.experience |
||||
|
case 4: |
||||
|
return ICONS.reward.money |
||||
|
default: |
||||
|
return ICONS.reward.default |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const formatRewardDisplay = (r: MonsterReward) => { |
||||
|
if (!r) return '' |
||||
|
if (r.rewardType === 3) return `${r.count} 经验` |
||||
|
if (r.rewardType === 4) return `${r.count} 灵石` |
||||
|
return r.itemName ? `${r.itemName} ×${r.count}` : `×${r.count}` |
||||
|
} |
||||
|
|
||||
|
const saveMonster = async () => { |
||||
|
if (formData.value.name == undefined |
||||
|
|| formData.value.description == undefined |
||||
|
|| formData.value.type == undefined |
||||
|
|| formData.value.level == undefined |
||||
|
) { |
||||
|
ElMessage.error('请填写必填项') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if (isEditing.value) { |
||||
|
var result = await UpdateMonster(formData.value) |
||||
|
if (result) { |
||||
|
ElMessage.success('怪兽更新成功') |
||||
|
closeDialog() |
||||
|
await refreshMonsters() |
||||
|
} else { |
||||
|
ElMessage.error('怪兽更新失败') |
||||
|
} |
||||
|
} else { |
||||
|
var result = await AddMonster(formData.value) |
||||
|
if (result) { |
||||
|
ElMessage.success('怪兽添加成功') |
||||
|
closeDialog() |
||||
|
await refreshMonsters() |
||||
|
} else { |
||||
|
ElMessage.error('添加失败') |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const deleteMonster = (monster: Monster) => { |
||||
|
ElMessageBox.confirm(`确定删除怪兽 "${monster.name}" 吗?`, '提示', { |
||||
|
confirmButtonText: '确定', |
||||
|
cancelButtonText: '取消', |
||||
|
type: 'warning', |
||||
|
}).then(async () => { |
||||
|
var result = await DeleteMonster(monster.id) |
||||
|
if (result) { |
||||
|
ElMessage.success('怪兽删除成功') |
||||
|
await refreshMonsters() |
||||
|
} else { |
||||
|
ElMessage.error('删除失败') |
||||
|
} |
||||
|
}).catch(() => { }) |
||||
|
} |
||||
|
|
||||
|
const loadMonsterRewards = async (monster: Monster) => { |
||||
|
if (!monster.rewards) { |
||||
|
const rewards = await GetMonsterRewards(monster.id) |
||||
|
monster.rewards = rewards |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const fetchEquipments = async (): Promise<EquipmentTemplate[]> => { |
||||
|
var res = await GetEquipmentTemplateList(undefined, 1, 100) |
||||
|
if (Array.isArray(res)) { |
||||
|
return res |
||||
|
} else { |
||||
|
return res.items || [] |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onMounted(async () => { |
||||
|
await refreshMonsters() |
||||
|
await fetchTypes() |
||||
|
await fetchRewardTypes() |
||||
|
}) |
||||
|
|
||||
|
const refreshMonsters = async (page?: number) => { |
||||
|
if (page !== undefined) currentPage.value = page |
||||
|
|
||||
|
const res = await GetMonsterList( |
||||
|
monsterTypeFilter.value, |
||||
|
levelFilter.value, |
||||
|
currentPage.value, |
||||
|
pageSize.value |
||||
|
) |
||||
|
|
||||
|
if (Array.isArray(res)) { |
||||
|
monsters.value = res |
||||
|
totalCount.value = res.length |
||||
|
} else { |
||||
|
monsters.value = res.items || [] |
||||
|
totalCount.value = res.totalCount || (res.items ? res.items.length : 0) |
||||
|
} |
||||
|
|
||||
|
for (const monster of monsters.value) { |
||||
|
await loadMonsterRewards(monster) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const fetchTypes = async () => { |
||||
|
var data = await GetMonsterTypes() |
||||
|
monsterTypes.value = data |
||||
|
} |
||||
|
|
||||
|
const fetchRewardTypes = async () => { |
||||
|
var data = await GetRewardTypes() |
||||
|
rewardTypes.value = data |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div class="monsters-container"> |
||||
|
<div class="header"> |
||||
|
<h2>怪兽管理</h2> |
||||
|
</div> |
||||
|
|
||||
|
<div class="search-bar"> |
||||
|
<el-input v-model="searchQuery" placeholder="搜索怪兽名称..." style="max-width: 300px;"></el-input> |
||||
|
<el-select v-model="monsterTypeFilter" style="max-width: 200px;" @change="refreshMonsters(undefined)" |
||||
|
placeholder="搜索怪兽类型..." clearable> |
||||
|
<el-option v-for="(value, index) in monsterTypes" :key="index" :value="value.id" :label="value.description" /> |
||||
|
</el-select> |
||||
|
<el-button type="primary" @click="openDialog(undefined)"> |
||||
|
<el-icon class="el-icon--left"> |
||||
|
<Plus /> |
||||
|
</el-icon> 添加怪兽 |
||||
|
</el-button> |
||||
|
</div> |
||||
|
|
||||
|
<el-table :data="filteredMonsters" style="width: 100%;" stripe :preserve-expanded-content="true"> |
||||
|
<el-table-column type="expand"> |
||||
|
<template #default="scoped"> |
||||
|
<div style="display:flex; gap:8px; align-items:center; margin-bottom:8px;"> |
||||
|
<el-button @click="openRewardDialog(scoped.row.id, undefined)">添加奖励</el-button> |
||||
|
</div> |
||||
|
|
||||
|
<div style="display:flex; gap:8px; align-items:center; margin: 16px 0 8px 0;"> |
||||
|
<h3 style="margin:0;">击杀奖励</h3> |
||||
|
</div> |
||||
|
|
||||
|
<div class="reward-cards"> |
||||
|
<div v-if="(!scoped.row.rewards || scoped.row.rewards.length === 0)" class="no-rewards">暂无奖励</div> |
||||
|
<div v-for="(r, idx) in scoped.row.rewards" :key="idx" class="reward-card"> |
||||
|
<div class="reward-icon">{{ getRewardEmoji(r.rewardType) }}</div> |
||||
|
<div class="reward-body"> |
||||
|
<div class="reward-type">{{ getRewardTypeLabel(r.rewardType) }}</div> |
||||
|
<div class="reward-name">{{ formatRewardDisplay(r) }}</div> |
||||
|
</div> |
||||
|
<div class="reward-actions"> |
||||
|
<el-button type="primary" @click="openRewardDialog(scoped.row.id, r)">编辑</el-button> |
||||
|
<el-button type="danger" @click="deleteReward(r)">删除</el-button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column label="名称" prop="name" width="150"></el-table-column> |
||||
|
<el-table-column label="等级" prop="level" width="80"></el-table-column> |
||||
|
<el-table-column label="类型"> |
||||
|
<template #default="scoped"> |
||||
|
<span class="monster-type" :class="getMonsterTypeClass(scoped.row.type)"> |
||||
|
{{ translateMonsterType(scoped.row.type) }} |
||||
|
</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column label="生命值" prop="health" width="100"></el-table-column> |
||||
|
<el-table-column label="攻击力" prop="attack" width="100"></el-table-column> |
||||
|
<el-table-column label="防御力" prop="defense" width="100"></el-table-column> |
||||
|
<el-table-column label="暴击率" width="100"> |
||||
|
<template #default="scoped"> |
||||
|
{{ scoped.row.criticalRate }}% |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column label="描述" prop="description" show-overflow-tooltip></el-table-column> |
||||
|
<el-table-column label="编辑" fixed="right"> |
||||
|
<template #default="scoped"> |
||||
|
<el-button type="primary" @click="openDialog(scoped.row)" :icon="Edit" circle /> |
||||
|
<el-button type="danger" @click="deleteMonster(scoped.row)" :icon="Delete" circle /> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
</el-table> |
||||
|
|
||||
|
<div class="pagination" v-if="totalCount > 0"> |
||||
|
<el-pagination layout="prev, pager, next" :total="totalCount" :current-page="currentPage" |
||||
|
@current-change="refreshMonsters" /> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Reward Dialog --> |
||||
|
<div v-if="showRewardDialog" class="dialog-overlay"> |
||||
|
<el-form :model="rewardFormData" class="dialog" label-position="top"> |
||||
|
<div class="dialog-header"> |
||||
|
<h3>{{ isEditingReward ? '编辑奖励' : '添加奖励' }}</h3> |
||||
|
<el-button @click="closeRewardDialog" :icon="Close" circle /> |
||||
|
</div> |
||||
|
<el-form-item label="类型"> |
||||
|
<el-select v-model="rewardFormData.rewardType" @change="rewardTypeChange" :disabled="isEditingReward"> |
||||
|
<el-option v-for="(value, index) in rewardTypes" :key="index" :value="value.id"> |
||||
|
{{ value.description }} |
||||
|
</el-option> |
||||
|
</el-select> |
||||
|
</el-form-item> |
||||
|
<el-form-item v-if="rewardFormData.rewardType === 1 || rewardFormData.rewardType === 2" label="物品"> |
||||
|
<el-select v-model="rewardFormData.itemId" @change="onRewardItemChange"> |
||||
|
<el-option v-for="(value, index) in rewardSelectData" :key="index" :value="value.id" :label="value.name" /> |
||||
|
</el-select> |
||||
|
</el-form-item> |
||||
|
<el-form-item v-else-if="rewardFormData.rewardType === 3 || rewardFormData.rewardType === 4" label="说明"> |
||||
|
<el-input v-model="rewardFormData.itemName" disabled /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="数量"> |
||||
|
<el-input-number v-model="rewardFormData.count" :min="1" /> |
||||
|
</el-form-item> |
||||
|
<div class="dialog-footer"> |
||||
|
<el-button type="info" @click="closeRewardDialog">取消</el-button> |
||||
|
<el-button type="primary" @click="saveReward">保存</el-button> |
||||
|
</div> |
||||
|
</el-form> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Monster Dialog --> |
||||
|
<div v-if="showDialog" class="dialog-overlay"> |
||||
|
<el-form :inline="true" :model="formData" class="dialog" label-position="top"> |
||||
|
<div class="dialog-header"> |
||||
|
<h3>{{ isEditing ? '编辑怪兽' : '添加怪兽' }}</h3> |
||||
|
<el-button @click="closeDialog" :icon="Close" circle /> |
||||
|
</div> |
||||
|
<el-form-item label="名称"> |
||||
|
<el-input v-model="formData.name" placeholder="怪兽名称" clearable /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="等级"> |
||||
|
<el-input-number v-model="formData.level" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="类型"> |
||||
|
<el-select v-model="formData.type"> |
||||
|
<el-option v-for="(value, index) in monsterTypes" :key="index" :value="value.id" |
||||
|
:label="value.description" /> |
||||
|
</el-select> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="生命值"> |
||||
|
<el-input-number v-model="formData.health" :min="0" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="攻击力"> |
||||
|
<el-input-number v-model="formData.attack" :min="0" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="防御力"> |
||||
|
<el-input-number v-model="formData.defense" :min="0" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="暴击率(%)"> |
||||
|
<el-input-number v-model="formData.criticalRate" :min="0" :max="100" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="描述" style="width: 100%;"> |
||||
|
<el-input v-model="formData.description" placeholder="描述" type="textarea" style="width: 100%;" /> |
||||
|
</el-form-item> |
||||
|
<div class="dialog-footer"> |
||||
|
<el-button type="info" @click="closeDialog">取消</el-button> |
||||
|
<el-button type="primary" @click="saveMonster">保存</el-button> |
||||
|
</div> |
||||
|
</el-form> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped lang="css"> |
||||
|
.monsters-container { |
||||
|
background: linear-gradient(135deg, #1f2937 0%, #111827 100%); |
||||
|
padding: 20px; |
||||
|
border-radius: 8px; |
||||
|
} |
||||
|
|
||||
|
.header { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.header h2 { |
||||
|
margin: 0; |
||||
|
color: #e5e7eb; |
||||
|
font-size: 20px; |
||||
|
} |
||||
|
|
||||
|
.search-bar { |
||||
|
display: flex; |
||||
|
margin-bottom: 20px; |
||||
|
gap: 12px; |
||||
|
} |
||||
|
|
||||
|
.monster-type { |
||||
|
display: inline-block; |
||||
|
padding: 4px 12px; |
||||
|
border-radius: 4px; |
||||
|
font-size: 12px; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
|
||||
|
.type-normal { |
||||
|
background-color: #6b7280; |
||||
|
} |
||||
|
|
||||
|
.type-elite { |
||||
|
background-color: #8b5cf6; |
||||
|
} |
||||
|
|
||||
|
.type-boss { |
||||
|
background-color: #dc2626; |
||||
|
} |
||||
|
|
||||
|
.dialog-overlay { |
||||
|
position: fixed; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
background: rgba(0, 0, 0, 0.7); |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
z-index: 1000; |
||||
|
} |
||||
|
|
||||
|
.dialog { |
||||
|
background: #1f2937; |
||||
|
border: 1px solid #374151; |
||||
|
border-radius: 8px; |
||||
|
width: 100%; |
||||
|
max-width: 500px; |
||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); |
||||
|
padding: 10px; |
||||
|
} |
||||
|
|
||||
|
.dialog .el-input, |
||||
|
.dialog .el-select, |
||||
|
.dialog .el-input-number, |
||||
|
.dialog .el-input-number .el-input { |
||||
|
width: 200px !important; |
||||
|
} |
||||
|
|
||||
|
.dialog-header { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
border-bottom: 1px solid #374151; |
||||
|
width: 100%; |
||||
|
margin-bottom: 10px; |
||||
|
padding: 10px 10px 10px 0px; |
||||
|
} |
||||
|
|
||||
|
.dialog-header h3 { |
||||
|
margin: 0; |
||||
|
color: #e5e7eb; |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
|
||||
|
.dialog-footer { |
||||
|
display: flex; |
||||
|
justify-content: flex-end; |
||||
|
gap: 12px; |
||||
|
border-top: 1px solid #374151; |
||||
|
width: 100%; |
||||
|
padding: 10px 10px 0 0; |
||||
|
} |
||||
|
|
||||
|
.pagination { |
||||
|
display: flex; |
||||
|
justify-content: flex-end; |
||||
|
margin-top: 10px; |
||||
|
} |
||||
|
|
||||
|
.reward-cards { |
||||
|
display: flex; |
||||
|
flex-wrap: wrap; |
||||
|
gap: 12px; |
||||
|
margin-top: 8px; |
||||
|
} |
||||
|
|
||||
|
.reward-card { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 12px; |
||||
|
background: linear-gradient(180deg, #111827, #0f172a); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.04); |
||||
|
padding: 12px; |
||||
|
border-radius: 8px; |
||||
|
width: 260px; |
||||
|
box-shadow: 0 6px 18px rgba(2, 6, 23, 0.6); |
||||
|
} |
||||
|
|
||||
|
.reward-icon { |
||||
|
font-size: 28px; |
||||
|
width: 42px; |
||||
|
height: 42px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
background: rgba(255, 255, 255, 0.03); |
||||
|
border-radius: 8px; |
||||
|
} |
||||
|
|
||||
|
.reward-body { |
||||
|
flex: 1 1 auto; |
||||
|
} |
||||
|
|
||||
|
.reward-type { |
||||
|
color: #9ca3af; |
||||
|
font-size: 12px; |
||||
|
margin-bottom: 4px; |
||||
|
} |
||||
|
|
||||
|
.reward-name { |
||||
|
color: #e5e7eb; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
|
||||
|
.no-rewards { |
||||
|
color: #9ca3af; |
||||
|
} |
||||
|
|
||||
|
.reward-actions { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 4px; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,100 @@ |
|||||
|
using Build_God_Api.Common; |
||||
|
using Build_God_Api.DB; |
||||
|
using Build_God_Api.Dto; |
||||
|
using Build_God_Api.Services; |
||||
|
using Microsoft.AspNetCore.Authorization; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
|
||||
|
namespace Build_God_Api.Controllers |
||||
|
{ |
||||
|
[ApiController] |
||||
|
[Route("api/god/[controller]")]
|
||||
|
public class MonsterController(IMonsterService service) : ControllerBase |
||||
|
{ |
||||
|
private readonly IMonsterService _service = service; |
||||
|
|
||||
|
[HttpPost] |
||||
|
[Authorize(Roles = "admin")] |
||||
|
public async Task<ActionResult<bool>> Add([FromBody] Monster item) |
||||
|
{ |
||||
|
return await _service.Add(item); |
||||
|
} |
||||
|
|
||||
|
[HttpPut("{id}")] |
||||
|
[Authorize(Roles = "admin")] |
||||
|
public async Task<ActionResult<bool>> Update(int id, [FromBody] Monster item) |
||||
|
{ |
||||
|
item.Id = id; |
||||
|
return await _service.Update(item); |
||||
|
} |
||||
|
|
||||
|
[HttpDelete("{id}")] |
||||
|
[Authorize(Roles = "admin")] |
||||
|
public async Task<ActionResult<bool>> Delete(int id) |
||||
|
{ |
||||
|
return await _service.Delete(id); |
||||
|
} |
||||
|
|
||||
|
[HttpGet("{id}")] |
||||
|
[Authorize] |
||||
|
public async Task<ActionResult<Monster>> GetById(int id) |
||||
|
{ |
||||
|
var monster = await _service.GetById(id); |
||||
|
if (monster == null) |
||||
|
{ |
||||
|
return NotFound(); |
||||
|
} |
||||
|
return monster; |
||||
|
} |
||||
|
|
||||
|
[HttpPost("all")] |
||||
|
[Authorize] |
||||
|
public async Task<ActionResult<PagedResult<Monster>>> GetAll([FromBody] SearchMonsterDto dto) |
||||
|
{ |
||||
|
return await _service.GetAll(dto); |
||||
|
} |
||||
|
|
||||
|
[HttpGet("types")] |
||||
|
[Authorize] |
||||
|
public ActionResult<List<EnumInfoDto>> GetMonsterTypes() |
||||
|
{ |
||||
|
return EnumHelper.GetEnumList<MonsterType>(); |
||||
|
} |
||||
|
|
||||
|
[HttpGet("rewards/{monsterId}")] |
||||
|
[Authorize] |
||||
|
public async Task<ActionResult<List<MonsterReward>>> GetRewards(int monsterId) |
||||
|
{ |
||||
|
return await _service.GetRewards(monsterId); |
||||
|
} |
||||
|
|
||||
|
[HttpPost("reward")] |
||||
|
[Authorize(Roles = "admin")] |
||||
|
public async Task<ActionResult<bool>> AddReward([FromBody] MonsterReward reward) |
||||
|
{ |
||||
|
return await _service.AddReward(reward); |
||||
|
} |
||||
|
|
||||
|
[HttpPut("reward/{id}")] |
||||
|
[Authorize(Roles = "admin")] |
||||
|
public async Task<ActionResult<bool>> UpdateReward(int id, [FromBody] MonsterReward reward) |
||||
|
{ |
||||
|
reward.Id = id; |
||||
|
return await _service.UpdateReward(reward); |
||||
|
} |
||||
|
|
||||
|
[HttpDelete("reward/{id}")] |
||||
|
[Authorize(Roles = "admin")] |
||||
|
public async Task<ActionResult<bool>> DeleteReward(int id) |
||||
|
{ |
||||
|
return await _service.DeleteReward(id); |
||||
|
} |
||||
|
|
||||
|
[HttpGet("reward-types")] |
||||
|
[Authorize] |
||||
|
public ActionResult<List<EnumInfoDto>> GetRewardTypes() |
||||
|
{ |
||||
|
return EnumHelper.GetEnumList<RewardType>(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,52 @@ |
|||||
|
using SqlSugar; |
||||
|
using System.ComponentModel; |
||||
|
|
||||
|
namespace Build_God_Api.DB |
||||
|
{ |
||||
|
public class Monster : BaseEntity |
||||
|
{ |
||||
|
[SugarColumn(Length = 100)] |
||||
|
public string Name { get; set; } = string.Empty; |
||||
|
|
||||
|
[SugarColumn(Length = 500)] |
||||
|
public string Description { get; set; } = string.Empty; |
||||
|
|
||||
|
public int Health { get; set; } |
||||
|
|
||||
|
public int Attack { get; set; } |
||||
|
|
||||
|
public int Defense { get; set; } |
||||
|
|
||||
|
public decimal CriticalRate { get; set; } |
||||
|
|
||||
|
public int Level { get; set; } |
||||
|
|
||||
|
public MonsterType Type { get; set; } |
||||
|
} |
||||
|
|
||||
|
public class MonsterReward : BaseEntity |
||||
|
{ |
||||
|
public int MonsterId { get; set; } |
||||
|
|
||||
|
public RewardType Type { get; set; } |
||||
|
|
||||
|
public int ItemId { get; set; } |
||||
|
|
||||
|
[SugarColumn(Length = 100)] |
||||
|
public string ItemName { get; set; } = string.Empty; |
||||
|
|
||||
|
public int Count { get; set; } |
||||
|
} |
||||
|
|
||||
|
public enum MonsterType |
||||
|
{ |
||||
|
[Description("普通")] |
||||
|
Normal = 1, |
||||
|
|
||||
|
[Description("精英")] |
||||
|
Elite = 2, |
||||
|
|
||||
|
[Description("首领")] |
||||
|
Boss = 3 |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
namespace Build_God_Api.Dto |
||||
|
{ |
||||
|
public class SearchMonsterDto |
||||
|
{ |
||||
|
public int PageNumber { get; set; } = 1; |
||||
|
public int PageSize { get; set; } = 10; |
||||
|
public int? MonsterType { get; set; } |
||||
|
public int? Level { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,129 @@ |
|||||
|
using Build_God_Api.DB; |
||||
|
using Build_God_Api.Dto; |
||||
|
using SqlSugar; |
||||
|
|
||||
|
namespace Build_God_Api.Services |
||||
|
{ |
||||
|
public interface IMonsterService |
||||
|
{ |
||||
|
Task<bool> Add(Monster item); |
||||
|
|
||||
|
Task<bool> Delete(int id); |
||||
|
|
||||
|
Task<bool> Update(Monster item); |
||||
|
|
||||
|
Task<Monster?> GetById(int id); |
||||
|
|
||||
|
Task<PagedResult<Monster>> GetAll(SearchMonsterDto dto); |
||||
|
|
||||
|
Task<bool> ExistsByNameAsync(string name); |
||||
|
|
||||
|
Task<bool> AddReward(MonsterReward reward); |
||||
|
|
||||
|
Task<bool> UpdateReward(MonsterReward reward); |
||||
|
|
||||
|
Task<bool> DeleteReward(int id); |
||||
|
|
||||
|
Task<List<MonsterReward>> GetRewards(int monsterId); |
||||
|
} |
||||
|
|
||||
|
public class MonsterService(ISqlSugarClient db, ICurrentUserService currentUserService) : IMonsterService |
||||
|
{ |
||||
|
private readonly ISqlSugarClient _db = db; |
||||
|
private readonly ICurrentUserService _currentUserService = currentUserService; |
||||
|
|
||||
|
public async Task<bool> Add(Monster item) |
||||
|
{ |
||||
|
var exists = await ExistsByNameAsync(item.Name); |
||||
|
if (exists) |
||||
|
{ |
||||
|
throw new Exception($"已存在名为 {item.Name} 的怪兽"); |
||||
|
} |
||||
|
item.CreatedOn = DateTime.UtcNow; |
||||
|
item.CreatedBy = _currentUserService.UserId; |
||||
|
await _db.Insertable(item).ExecuteCommandAsync(); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public async Task<bool> Delete(int id) |
||||
|
{ |
||||
|
var monster = await _db.Queryable<Monster>().FirstAsync(x => x.Id == id) |
||||
|
?? throw new Exception("没找到对应的怪兽"); |
||||
|
|
||||
|
await _db.Deleteable<MonsterReward>().Where(x => x.MonsterId == id).ExecuteCommandAsync(); |
||||
|
await _db.Deleteable(monster).ExecuteCommandAsync(); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public async Task<bool> ExistsByNameAsync(string name) |
||||
|
{ |
||||
|
return await _db.Queryable<Monster>().AnyAsync(x => x.Name == name); |
||||
|
} |
||||
|
|
||||
|
public async Task<PagedResult<Monster>> GetAll(SearchMonsterDto dto) |
||||
|
{ |
||||
|
var query = _db.Queryable<Monster>() |
||||
|
.WhereIF(dto.MonsterType != null, x => (int)x.Type == dto.MonsterType) |
||||
|
.WhereIF(dto.Level != null, x => x.Level == dto.Level) |
||||
|
.OrderBy(x => x.CreatedOn, OrderByType.Desc); |
||||
|
|
||||
|
var list = await query.Skip((dto.PageNumber - 1) * dto.PageSize).Take(dto.PageSize).ToListAsync(); |
||||
|
var total = await query.CountAsync(); |
||||
|
|
||||
|
return new PagedResult<Monster> |
||||
|
{ |
||||
|
PageNumber = dto.PageNumber, |
||||
|
TotalCount = total, |
||||
|
Items = list |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public async Task<Monster?> GetById(int id) |
||||
|
{ |
||||
|
return await _db.Queryable<Monster>().FirstAsync(x => x.Id == id); |
||||
|
} |
||||
|
|
||||
|
public async Task<bool> Update(Monster item) |
||||
|
{ |
||||
|
var monster = await _db.Queryable<Monster>().FirstAsync(x => x.Id == item.Id) |
||||
|
?? throw new Exception("没找到对应的怪兽"); |
||||
|
item.UpdatedOn = DateTime.UtcNow; |
||||
|
item.UpdatedBy = _currentUserService.UserId; |
||||
|
await _db.Updateable(item).ExecuteCommandAsync(); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public async Task<bool> AddReward(MonsterReward reward) |
||||
|
{ |
||||
|
reward.CreatedOn = DateTime.UtcNow; |
||||
|
reward.CreatedBy = _currentUserService.UserId; |
||||
|
await _db.Insertable(reward).ExecuteCommandAsync(); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public async Task<bool> UpdateReward(MonsterReward reward) |
||||
|
{ |
||||
|
var existing = await _db.Queryable<MonsterReward>().FirstAsync(x => x.Id == reward.Id) |
||||
|
?? throw new Exception("没找到对应的奖励"); |
||||
|
reward.UpdatedOn = DateTime.UtcNow; |
||||
|
reward.UpdatedBy = _currentUserService.UserId; |
||||
|
await _db.Updateable(reward).ExecuteCommandAsync(); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public async Task<bool> DeleteReward(int id) |
||||
|
{ |
||||
|
var reward = await _db.Queryable<MonsterReward>().FirstAsync(x => x.Id == id) |
||||
|
?? throw new Exception("没找到对应的奖励"); |
||||
|
await _db.Deleteable(reward).ExecuteCommandAsync(); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public async Task<List<MonsterReward>> GetRewards(int monsterId) |
||||
|
{ |
||||
|
return await _db.Queryable<MonsterReward>() |
||||
|
.Where(x => x.MonsterId == monsterId) |
||||
|
.ToListAsync(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue