5 changed files with 373 additions and 0 deletions
@ -0,0 +1,27 @@ |
|||
import http from "../api/index"; |
|||
|
|||
export interface ShopItem { |
|||
id: number; |
|||
itemType: number; |
|||
itemId: number; |
|||
price: number; |
|||
dailyLimit: number; |
|||
isActive: boolean; |
|||
sortOrder: number; |
|||
} |
|||
|
|||
export const GetShopItemList = (): Promise<ShopItem[]> => { |
|||
return http.get('/shop/admin/all'); |
|||
}; |
|||
|
|||
export const CreateShopItem = (data: ShopItem): Promise<boolean> => { |
|||
return http.post('/shop/admin', data); |
|||
}; |
|||
|
|||
export const UpdateShopItem = (data: ShopItem): Promise<boolean> => { |
|||
return http.put(`/shop/admin/${data.id}`, data); |
|||
}; |
|||
|
|||
export const DeleteShopItem = (id: number): Promise<boolean> => { |
|||
return http.delete(`/shop/admin/${id}`); |
|||
}; |
|||
@ -0,0 +1,334 @@ |
|||
<script setup lang="ts"> |
|||
import { ref, computed, onMounted } from 'vue' |
|||
import { ElMessage, ElMessageBox } from 'element-plus' |
|||
import { |
|||
GetShopItemList, |
|||
CreateShopItem, |
|||
UpdateShopItem, |
|||
DeleteShopItem, |
|||
type ShopItem |
|||
} from '@/api/shop' |
|||
import { GetEquipmentTemplateList, type EquipmentTemplate } from '@/api/equipment' |
|||
import { GetPillList, type Pill } from '@/api/pill' |
|||
import { GetAllScraps, type Scrap } from '@/api/scrap' |
|||
import { GetBagItemTypes } from '@/api/bag' |
|||
import type { EnumInfoDto } from '@/api/index' |
|||
import { Plus, Edit, Delete } from '@element-plus/icons-vue' |
|||
|
|||
const shopItems = ref<ShopItem[]>([]) |
|||
const itemTypes = ref<EnumInfoDto[]>([]) |
|||
const equipmentList = ref<EquipmentTemplate[]>([]) |
|||
const pillList = ref<Pill[]>([]) |
|||
const scrapList = ref<Scrap[]>([]) |
|||
|
|||
const showDialog = ref(false) |
|||
const isEditing = ref(false) |
|||
const searchQuery = ref('') |
|||
|
|||
const formData = ref<Partial<ShopItem>>({ |
|||
itemType: 1, |
|||
itemId: 0, |
|||
price: 0, |
|||
dailyLimit: 0, |
|||
isActive: true, |
|||
sortOrder: 0 |
|||
}) |
|||
|
|||
const filteredItems = computed(() => { |
|||
return shopItems.value.filter(item => { |
|||
const name = getItemName(item.itemType, item.itemId) |
|||
return name.toLowerCase().includes(searchQuery.value.toLowerCase()) |
|||
}) |
|||
}) |
|||
|
|||
const getItemName = (itemType: number, itemId: number): string => { |
|||
if (itemType === 1) { |
|||
const eq = equipmentList.value.find(e => e.id === itemId) |
|||
return eq?.name || `装备-${itemId}` |
|||
} else if (itemType === 2) { |
|||
const pill = pillList.value.find(p => p.id === itemId) |
|||
return pill?.name || `丹药-${itemId}` |
|||
} else if (itemType === 3) { |
|||
const scrap = scrapList.value.find(s => s.id === itemId) |
|||
return scrap?.name || `垃圾-${itemId}` |
|||
} |
|||
return `未知-${itemId}` |
|||
} |
|||
|
|||
const translateItemType = (typeId: number) => { |
|||
const type = itemTypes.value.find((t: EnumInfoDto) => t.id === typeId) |
|||
return type ? type.description : '未知类型' |
|||
} |
|||
|
|||
const getCurrentTypeItems = computed(() => { |
|||
const type = formData.value.itemType |
|||
if (type === 1) { |
|||
return equipmentList.value.map(e => ({ value: e.id, label: e.name })) |
|||
} else if (type === 2) { |
|||
return pillList.value.map(p => ({ value: p.id, label: p.name })) |
|||
} else if (type === 3) { |
|||
return scrapList.value.map(s => ({ value: s.id, label: s.name })) |
|||
} |
|||
return [] |
|||
}) |
|||
|
|||
const refreshShopItems = async () => { |
|||
shopItems.value = await GetShopItemList() |
|||
} |
|||
|
|||
const loadItemTypes = async () => { |
|||
itemTypes.value = await GetBagItemTypes() |
|||
} |
|||
|
|||
const loadEquipmentList = async () => { |
|||
const res = await GetEquipmentTemplateList() |
|||
if (Array.isArray(res)) { |
|||
equipmentList.value = res |
|||
} else { |
|||
equipmentList.value = res.items || [] |
|||
} |
|||
} |
|||
|
|||
const loadPillList = async () => { |
|||
pillList.value = await GetPillList() |
|||
} |
|||
|
|||
const loadScrapList = async () => { |
|||
scrapList.value = await GetAllScraps() |
|||
} |
|||
|
|||
const loadAllData = async () => { |
|||
await Promise.all([ |
|||
refreshShopItems(), |
|||
loadItemTypes(), |
|||
loadEquipmentList(), |
|||
loadPillList(), |
|||
loadScrapList() |
|||
]) |
|||
} |
|||
|
|||
const openDialog = (item?: ShopItem) => { |
|||
if (item) { |
|||
isEditing.value = true |
|||
formData.value = { ...item } |
|||
} else { |
|||
isEditing.value = false |
|||
formData.value = { |
|||
itemType: 1, |
|||
itemId: 0, |
|||
price: 0, |
|||
dailyLimit: 0, |
|||
isActive: true, |
|||
sortOrder: 0 |
|||
} |
|||
} |
|||
showDialog.value = true |
|||
} |
|||
|
|||
const closeDialog = () => { |
|||
showDialog.value = false |
|||
} |
|||
|
|||
const saveShopItem = async () => { |
|||
if (formData.value.itemType === undefined || formData.value.itemId === undefined || formData.value.itemId === 0) { |
|||
ElMessage.error('请选择物品') |
|||
return |
|||
} |
|||
if (formData.value.price === undefined || formData.value.price <= 0) { |
|||
ElMessage.error('请填写价格') |
|||
return |
|||
} |
|||
|
|||
if (isEditing.value) { |
|||
const result = await UpdateShopItem(formData.value as ShopItem) |
|||
if (result) { |
|||
ElMessage.success('商店物品更新成功') |
|||
closeDialog() |
|||
await refreshShopItems() |
|||
} else { |
|||
ElMessage.error('商店物品更新失败') |
|||
} |
|||
} else { |
|||
const newItem: ShopItem = { |
|||
id: 0, |
|||
itemType: formData.value.itemType || 1, |
|||
itemId: formData.value.itemId || 0, |
|||
price: formData.value.price || 0, |
|||
dailyLimit: formData.value.dailyLimit || 0, |
|||
isActive: formData.value.isActive ?? true, |
|||
sortOrder: formData.value.sortOrder || 0 |
|||
} |
|||
const result = await CreateShopItem(newItem) |
|||
if (result) { |
|||
ElMessage.success('商店物品添加成功') |
|||
closeDialog() |
|||
await refreshShopItems() |
|||
} else { |
|||
ElMessage.error('添加失败') |
|||
} |
|||
} |
|||
} |
|||
|
|||
const deleteShopItem = (item: ShopItem) => { |
|||
ElMessageBox.confirm(`确定删除商店物品 "${getItemName(item.itemType, item.itemId)}" 吗?`, '提示', { |
|||
confirmButtonText: '确定', |
|||
cancelButtonText: '取消', |
|||
type: 'warning', |
|||
}).then(async () => { |
|||
const result = await DeleteShopItem(item.id) |
|||
if (result) { |
|||
ElMessage.success('商店物品删除成功') |
|||
await refreshShopItems() |
|||
} else { |
|||
ElMessage.error('删除失败') |
|||
} |
|||
}).catch(() => {}) |
|||
} |
|||
|
|||
const onItemTypeChange = () => { |
|||
formData.value.itemId = 0 |
|||
} |
|||
|
|||
onMounted(() => { |
|||
loadAllData() |
|||
}) |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="shop-container"> |
|||
<div class="header"> |
|||
<h2>商店管理</h2> |
|||
<div class="header-actions"> |
|||
<el-input |
|||
v-model="searchQuery" |
|||
placeholder="搜索物品名称" |
|||
class="search-input" |
|||
clearable |
|||
/> |
|||
<el-button type="primary" :icon="Plus" @click="openDialog()"> |
|||
添加商店物品 |
|||
</el-button> |
|||
</div> |
|||
</div> |
|||
|
|||
<el-table :data="filteredItems" border stripe style="width: 100%"> |
|||
<el-table-column prop="id" label="ID" width="80" /> |
|||
<el-table-column label="物品类型" width="120"> |
|||
<template #default="{ row }"> |
|||
{{ translateItemType(row.itemType) }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="物品名称" min-width="150"> |
|||
<template #default="{ row }"> |
|||
{{ getItemName(row.itemType, row.itemId) }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="price" label="价格(灵石)" width="120" /> |
|||
<el-table-column prop="dailyLimit" label="每日限购" width="100"> |
|||
<template #default="{ row }"> |
|||
{{ row.dailyLimit === 0 ? '不限' : row.dailyLimit }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="sortOrder" label="排序" width="80" /> |
|||
<el-table-column label="状态" width="80"> |
|||
<template #default="{ row }"> |
|||
<el-tag :type="row.isActive ? 'success' : 'info'"> |
|||
{{ row.isActive ? '启用' : '禁用' }} |
|||
</el-tag> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="操作" width="150" fixed="right"> |
|||
<template #default="{ row }"> |
|||
<el-button type="primary" :icon="Edit" circle @click="openDialog(row)" /> |
|||
<el-button type="danger" :icon="Delete" circle @click="deleteShopItem(row)" /> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
|
|||
<el-dialog |
|||
v-model="showDialog" |
|||
:title="isEditing ? '编辑商店物品' : '添加商店物品'" |
|||
width="500px" |
|||
:close-on-click-modal="false" |
|||
> |
|||
<el-form :model="formData" label-width="100px"> |
|||
<el-form-item label="物品类型"> |
|||
<el-select v-model="formData.itemType" placeholder="选择物品类型" @change="onItemTypeChange"> |
|||
<el-option |
|||
v-for="type in itemTypes" |
|||
:key="type.id" |
|||
:label="type.description" |
|||
:value="type.id" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="选择物品"> |
|||
<el-select |
|||
v-model="formData.itemId" |
|||
placeholder="选择物品" |
|||
filterable |
|||
:disabled="!formData.itemType" |
|||
> |
|||
<el-option |
|||
v-for="item in getCurrentTypeItems" |
|||
:key="item.value" |
|||
:label="item.label" |
|||
:value="item.value" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="价格(灵石)"> |
|||
<el-input-number v-model="formData.price" :min="0" :step="100" /> |
|||
</el-form-item> |
|||
<el-form-item label="每日限购"> |
|||
<el-input-number v-model="formData.dailyLimit" :min="0" :step="1" /> |
|||
<span class="form-tip">0表示不限购</span> |
|||
</el-form-item> |
|||
<el-form-item label="排序权重"> |
|||
<el-input-number v-model="formData.sortOrder" :min="0" :step="1" /> |
|||
<span class="form-tip">越大越优先出现</span> |
|||
</el-form-item> |
|||
<el-form-item label="是否启用"> |
|||
<el-switch v-model="formData.isActive" /> |
|||
</el-form-item> |
|||
</el-form> |
|||
<template #footer> |
|||
<el-button @click="closeDialog">取消</el-button> |
|||
<el-button type="primary" @click="saveShopItem">确定</el-button> |
|||
</template> |
|||
</el-dialog> |
|||
</div> |
|||
</template> |
|||
|
|||
<style scoped lang="css"> |
|||
.shop-container { |
|||
padding: 20px; |
|||
} |
|||
|
|||
.header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
.header h2 { |
|||
margin: 0; |
|||
} |
|||
|
|||
.header-actions { |
|||
display: flex; |
|||
gap: 10px; |
|||
align-items: center; |
|||
} |
|||
|
|||
.search-input { |
|||
width: 200px; |
|||
} |
|||
|
|||
.form-tip { |
|||
margin-left: 10px; |
|||
color: #909399; |
|||
font-size: 12px; |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue