Compare commits

...

2 Commits

Author SHA1 Message Date
qinhan 414b8cdb34 添加捡垃圾的功能 1 week ago
qinhan b4210eddce 添加一个捡垃圾的功能# 1 week ago
  1. 38
      Build_God_Admin_Frontend/Frontend/src/api/scrap.ts
  2. 5
      Build_God_Admin_Frontend/Frontend/src/components/Sidebar.vue
  3. 1
      Build_God_Admin_Frontend/Frontend/src/constants/theme.ts
  4. 6
      Build_God_Admin_Frontend/Frontend/src/router/index.ts
  5. 364
      Build_God_Admin_Frontend/Frontend/src/views/admin/ScrapView.vue
  6. 24
      Build_God_Api/Build_God_Api/Build_God_Api.sln
  7. 104
      Build_God_Api/Build_God_Api/Controllers/ScrapController.cs
  8. 11
      Build_God_Api/Build_God_Api/DB/CharacterScrap.cs
  9. 44
      Build_God_Api/Build_God_Api/DB/Scrap.cs
  10. 36
      Build_God_Api/Build_God_Api/Dto/ScrapDto.cs
  11. 3
      Build_God_Api/Build_God_Api/Program.cs
  12. 226
      Build_God_Api/Build_God_Api/Services/ScrapService.cs
  13. 12
      Build_God_Api/Build_God_Api/appsettings.json
  14. 76
      Build_God_Game/src/api/scrap.ts
  15. 6
      Build_God_Game/src/router/index.ts
  16. 3
      Build_God_Game/src/views/GameView.vue
  17. 625
      Build_God_Game/src/views/ScrapView.vue

38
Build_God_Admin_Frontend/Frontend/src/api/scrap.ts

@ -0,0 +1,38 @@
import http from "../api/index";
export interface Scrap {
id: number;
name: string;
description: string;
story: string;
level: number;
levelName: string;
levelColor: string;
attackBonus: number;
defenseBonus: number;
hpBonus: number;
magicBonus: number;
isActive: boolean;
}
export interface ScrapLevel {
id: number;
name: string;
displayName: string;
}
export const GetAllScraps = (): Promise<Scrap[]> => {
return http.get("scrap/all");
};
export const AddScrap = (data: Scrap): Promise<boolean> => {
return http.post("scrap", data);
};
export const UpdateScrap = (data: Scrap): Promise<boolean> => {
return http.put("scrap", data);
};
export const DeleteScrap = (id: number): Promise<boolean> => {
return http.delete(`scrap/${id}`);
};

5
Build_God_Admin_Frontend/Frontend/src/components/Sidebar.vue

@ -45,6 +45,11 @@ const menuItems = [
icon: ICONS.bag,
label: '背包管理',
path: '/admin/bags'
},
{
icon: ICONS.scrap,
label: '垃圾管理',
path: '/admin/scraps'
}
]

1
Build_God_Admin_Frontend/Frontend/src/constants/theme.ts

@ -6,6 +6,7 @@ export const ICONS = {
pill: '💊',
bag:'🎒',
mission: '📜',
scrap: '📜',
reward: {
pill: '💊',

6
Build_God_Admin_Frontend/Frontend/src/router/index.ts

@ -57,6 +57,12 @@ const routes: RouteRecordRaw[] = [
component: () => import('../views/admin/MissionView.vue'),
meta: { title: '任务管理' }
},
{
path: 'scraps',
name: 'scraps',
component: () => import('../views/admin/ScrapView.vue'),
meta: { title: '垃圾管理' }
},
{
path: 'bags',
name: 'bags',

364
Build_God_Admin_Frontend/Frontend/src/views/admin/ScrapView.vue

@ -0,0 +1,364 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Edit, Delete, Close } from '@element-plus/icons-vue'
import { GetAllScraps, AddScrap, UpdateScrap, DeleteScrap, type Scrap } from '@/api/scrap'
const scraps = ref<Scrap[]>([])
const showDialog = ref(false)
const isEditing = ref(false)
const searchQuery = ref('')
const levelFilter = ref<number | undefined>(undefined)
const scrapLevels = [
{ id: 1, name: 'White', displayName: '普通' },
{ id: 2, name: 'Green', displayName: '优秀' },
{ id: 3, name: 'Blue', displayName: '精良' },
{ id: 4, name: 'Purple', displayName: '史诗' },
{ id: 5, name: 'Orange', displayName: '传说' }
]
const levelColors: Record<number, string> = {
1: '#FFFFFF',
2: '#00FF00',
3: '#0077FF',
4: '#9932CC',
5: '#FF8C00'
}
const formData = ref<Partial<Scrap>>({
name: '',
description: '',
story: '',
level: 1,
attackBonus: 0,
defenseBonus: 0,
hpBonus: 0,
magicBonus: 0,
isActive: true
})
const filteredScraps = computed(() => {
let result = scraps.value
if (searchQuery.value) {
result = result.filter(s =>
s.name.toLowerCase().includes(searchQuery.value.toLowerCase())
)
}
if (levelFilter.value !== undefined) {
result = result.filter(s => s.level === levelFilter.value)
}
return result
})
const getLevelName = (levelId: number) => {
const level = scrapLevels.find(l => l.id === levelId)
return level ? level.displayName : '未知'
}
const getLevelColor = (levelId: number) => {
return levelColors[levelId] || '#FFFFFF'
}
const openDialog = (scrap?: Scrap) => {
if (scrap) {
isEditing.value = true
formData.value = { ...scrap }
} else {
isEditing.value = false
formData.value = {
name: '',
description: '',
story: '',
level: 1,
attackBonus: 0,
defenseBonus: 0,
hpBonus: 0,
magicBonus: 0,
isActive: true
}
}
showDialog.value = true
}
const closeDialog = () => {
showDialog.value = false
}
const saveScrap = async () => {
if (!formData.value.name?.trim()) {
ElMessage.error('请填写名称')
return
}
if (isEditing.value) {
const result = await UpdateScrap(formData.value as Scrap)
if (result) {
ElMessage.success('更新成功')
closeDialog()
await refreshScraps()
} else {
ElMessage.error('更新失败')
}
} else {
const newScrap: Scrap = {
id: 0,
name: formData.value.name || '',
description: formData.value.description || '',
story: formData.value.story || '',
level: formData.value.level || 1,
levelName: getLevelName(formData.value.level || 1),
levelColor: getLevelColor(formData.value.level || 1),
attackBonus: formData.value.attackBonus || 0,
defenseBonus: formData.value.defenseBonus || 0,
hpBonus: formData.value.hpBonus || 0,
magicBonus: formData.value.magicBonus || 0,
isActive: formData.value.isActive ?? true
}
const result = await AddScrap(newScrap)
if (result) {
ElMessage.success('添加成功')
closeDialog()
await refreshScraps()
} else {
ElMessage.error('添加失败')
}
}
}
const deleteScrapHandler = (scrap: Scrap) => {
ElMessageBox.confirm(`确定删除垃圾 "${scrap.name}" 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
const result = await DeleteScrap(scrap.id)
if (result) {
ElMessage.success('删除成功')
await refreshScraps()
} else {
ElMessage.error('删除失败')
}
}).catch(() => {})
}
onMounted(async () => {
await refreshScraps()
})
const refreshScraps = async () => {
scraps.value = await GetAllScraps()
}
</script>
<template>
<div class="scraps-container">
<div class="header">
<h2>垃圾管理</h2>
</div>
<div class="search-bar">
<el-input v-model="searchQuery" placeholder="搜索名称..." style="max-width: 300px;" clearable></el-input>
<el-select v-model="levelFilter" style="max-width: 150px;" placeholder="等级筛选" clearable>
<el-option v-for="level in scrapLevels" :key="level.id" :value="level.id" :label="level.displayName" />
</el-select>
<el-button type="primary" @click="openDialog(undefined)">
<el-icon class="el-icon--left">
<Plus />
</el-icon>
</el-button>
</div>
<el-table :data="filteredScraps" style="width: 100%;" stripe>
<el-table-column label="名称" prop="name" width="150">
<template #default="scope">
<span>{{ scope.row.name }}</span>
</template>
</el-table-column>
<el-table-column label="等级" width="100">
<template #default="scope">
<span class="level-badge" :style="{ color: getLevelColor(scope.row.level), borderColor: getLevelColor(scope.row.level) }">
{{ getLevelName(scope.row.level) }}
</span>
</template>
</el-table-column>
<el-table-column label="攻击加成" prop="attackBonus" width="100"></el-table-column>
<el-table-column label="防御加成" prop="defenseBonus" width="100"></el-table-column>
<el-table-column label="生命加成" prop="hpBonus" width="100"></el-table-column>
<el-table-column label="魔力加成" prop="magicBonus" width="100"></el-table-column>
<el-table-column label="状态" width="80">
<template #default="scope">
<el-tag :type="scope.row.isActive ? 'success' : 'info'" size="small">
{{ scope.row.isActive ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="编辑" fixed="right">
<template #default="scope">
<el-button type="primary" @click="openDialog(scope.row)" :icon="Edit" circle />
<el-button type="danger" @click="deleteScrapHandler(scope.row)" :icon="Delete" circle />
</template>
</el-table-column>
</el-table>
<!-- Dialog -->
<div v-if="showDialog" class="dialog-overlay">
<el-form :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="名称" style="width: 100%;">
<el-input v-model="formData.name" placeholder="垃圾名称" clearable />
</el-form-item>
<el-form-item label="等级" style="width: 100%;">
<el-select v-model="formData.level" style="width: 100%;">
<el-option v-for="level in scrapLevels" :key="level.id" :value="level.id" :label="level.displayName" />
</el-select>
</el-form-item>
<div class="form-row">
<el-form-item label="攻击加成">
<el-input-number v-model="formData.attackBonus" :min="0" :max="9999" controls-position="right" />
</el-form-item>
<el-form-item label="防御加成">
<el-input-number v-model="formData.defenseBonus" :min="0" :max="9999" controls-position="right" />
</el-form-item>
</div>
<div class="form-row">
<el-form-item label="生命加成">
<el-input-number v-model="formData.hpBonus" :min="0" :max="99999" controls-position="right" />
</el-form-item>
<el-form-item label="魔力加成">
<el-input-number v-model="formData.magicBonus" :min="0" :max="9999" controls-position="right" />
</el-form-item>
</div>
<el-form-item label="描述" style="width: 100%;">
<el-input v-model="formData.description" type="textarea" :rows="2" placeholder="简要描述" />
</el-form-item>
<el-form-item label="故事" style="width: 100%;">
<el-input v-model="formData.story" type="textarea" :rows="4" placeholder="物品背后的故事..." />
</el-form-item>
<el-form-item label="是否启用">
<el-switch v-model="formData.isActive" />
</el-form-item>
<div class="dialog-footer">
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="saveScrap">保存</el-button>
</div>
</el-form>
</div>
</div>
</template>
<style scoped lang="css">
.scraps-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;
}
.level-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 4px;
border: 1px solid;
font-size: 12px;
font-weight: 500;
}
.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: 600px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
padding: 10px;
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #374151;
width: 100%;
margin-bottom: 15px;
padding: 10px 10px 10px 0px;
}
.dialog-header h3 {
margin: 0;
color: #e5e7eb;
font-size: 16px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
border-top: 1px solid #374151;
width: 100%;
padding-top: 15px;
margin-top: 10px;
}
.dialog :deep(.el-form-item) {
margin-bottom: 12px;
}
.dialog :deep(.el-input),
.dialog :deep(.el-select),
.dialog :deep(.el-input-number) {
width: 100%;
}
</style>

24
Build_God_Api/Build_God_Api/Build_God_Api.sln

@ -0,0 +1,24 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Build_God_Api", "Build_God_Api.csproj", "{47C83A69-1184-B652-B317-1E0D4236A04D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{47C83A69-1184-B652-B317-1E0D4236A04D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{47C83A69-1184-B652-B317-1E0D4236A04D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{47C83A69-1184-B652-B317-1E0D4236A04D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{47C83A69-1184-B652-B317-1E0D4236A04D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E4CD997F-31FC-4563-B882-DA1CF44946AC}
EndGlobalSection
EndGlobal

104
Build_God_Api/Build_God_Api/Controllers/ScrapController.cs

@ -0,0 +1,104 @@
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 ScrapController(IScrapService scrapService, ICurrentUserService currentUserService) : ControllerBase
{
private readonly IScrapService scrapService = scrapService;
private readonly ICurrentUserService currentUserService = currentUserService;
[HttpGet("list")]
[Authorize]
public async Task<ActionResult<List<ScrapDto>>> GetScrapList()
{
return await scrapService.GetAllActiveScrapsAsync();
}
[HttpPost("scan")]
[Authorize]
public async Task<ActionResult<ScrapScanResultDto>> ScanScrap([FromBody] ScrapScanRequest request)
{
if (request.CharacterId <= 0)
{
return BadRequest("无效的角色ID");
}
var result = await scrapService.ScanAndAssignScrapAsync(request.CharacterId);
return result;
}
[HttpGet("history/{characterId}")]
[Authorize]
public async Task<ActionResult<List<ScrapHistoryDto>>> GetScrapHistory(int characterId)
{
if (characterId <= 0)
{
return BadRequest("无效的角色ID");
}
return await scrapService.GetCharacterScrapHistoryAsync(characterId);
}
[HttpGet("all")]
[Authorize(Roles = "admin")]
public async Task<ActionResult<List<Scrap>>> GetAllScraps()
{
return await scrapService.GetAllScrapsAsync();
}
[HttpPost]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> AddScrap([FromBody] ScrapDto dto)
{
if (string.IsNullOrWhiteSpace(dto.Name))
{
return BadRequest("名称不能为空");
}
var result = await scrapService.AddScrapAsync(dto);
return result;
}
[HttpPut]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> UpdateScrap([FromBody] ScrapDto dto)
{
if (dto.Id <= 0)
{
return BadRequest("无效的ID");
}
if (string.IsNullOrWhiteSpace(dto.Name))
{
return BadRequest("名称不能为空");
}
var result = await scrapService.UpdateScrapAsync(dto);
return result;
}
[HttpDelete("{id}")]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> DeleteScrap(int id)
{
if (id <= 0)
{
return BadRequest("无效的ID");
}
var result = await scrapService.DeleteScrapAsync(id);
return result;
}
}
public class ScrapScanRequest
{
public int CharacterId { get; set; }
}
}

11
Build_God_Api/Build_God_Api/DB/CharacterScrap.cs

@ -0,0 +1,11 @@
namespace Build_God_Api.DB
{
public class CharacterScrap : BaseEntity
{
public int CharacterId { get; set; }
public int ScrapId { get; set; }
public DateTime ObtainedAt { get; set; } = DateTime.UtcNow;
}
}

44
Build_God_Api/Build_God_Api/DB/Scrap.cs

@ -0,0 +1,44 @@
using SqlSugar;
using System.ComponentModel;
namespace Build_God_Api.DB
{
public class Scrap : BaseEntity
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Story { get; set; } = string.Empty;
public ScrapLevel Level { get; set; }
public int AttackBonus { get; set; }
public int DefenseBonus { get; set; }
public int HpBonus { get; set; }
public int MagicBonus { get; set; }
public bool IsActive { get; set; } = true;
}
public enum ScrapLevel
{
[Description("普通")]
White = 1,
[Description("优秀")]
Green = 2,
[Description("精良")]
Blue = 3,
[Description("史诗")]
Purple = 4,
[Description("传说")]
Orange = 5
}
}

36
Build_God_Api/Build_God_Api/Dto/ScrapDto.cs

@ -0,0 +1,36 @@
using Build_God_Api.DB;
namespace Build_God_Api.Dto
{
public class ScrapDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Story { get; set; } = string.Empty;
public int Level { get; set; }
public string LevelName { get; set; } = string.Empty;
public string LevelColor { get; set; } = string.Empty;
public int AttackBonus { get; set; }
public int DefenseBonus { get; set; }
public int HpBonus { get; set; }
public int MagicBonus { get; set; }
public bool IsActive { get; set; } = true;
}
public class ScrapScanResultDto
{
public ScrapDto Scrap { get; set; } = null!;
public int AttackGain { get; set; }
public int DefenseGain { get; set; }
public int HpGain { get; set; }
public int MagicGain { get; set; }
}
public class ScrapHistoryDto
{
public int Id { get; set; }
public ScrapDto Scrap { get; set; } = null!;
public DateTime ObtainedAt { get; set; }
}
}

3
Build_God_Api/Build_God_Api/Program.cs

@ -95,6 +95,8 @@ namespace Build_God_Api
sqlSugarClient.CodeFirst.InitTables(typeof(CharacterMissionProgress));
sqlSugarClient.CodeFirst.InitTables(typeof(ChatMessage));
sqlSugarClient.CodeFirst.InitTables(typeof(CharacterDailyMission));
sqlSugarClient.CodeFirst.InitTables(typeof(Scrap));
sqlSugarClient.CodeFirst.InitTables(typeof(CharacterScrap));
return sqlSugarClient;
});
@ -153,6 +155,7 @@ namespace Build_God_Api
builder.Services.AddScoped<IDailyMissionService, DailyMissionService>();
builder.Services.AddHostedService<DailyMissionHostedService>();
builder.Services.AddScoped<IChatService, ChatService>();
builder.Services.AddScoped<IScrapService, ScrapService>();
builder.Services.AddCors(options =>
{

226
Build_God_Api/Build_God_Api/Services/ScrapService.cs

@ -0,0 +1,226 @@
using Build_God_Api.DB;
using Build_God_Api.Dto;
using SqlSugar;
using System.ComponentModel;
namespace Build_God_Api.Services
{
public interface IScrapService
{
Task<List<ScrapDto>> GetAllActiveScrapsAsync();
Task<ScrapScanResultDto> ScanAndAssignScrapAsync(int characterId);
Task<List<ScrapHistoryDto>> GetCharacterScrapHistoryAsync(int characterId);
Task<List<Scrap>> GetAllScrapsAsync();
Task<bool> AddScrapAsync(ScrapDto dto);
Task<bool> UpdateScrapAsync(ScrapDto dto);
Task<bool> DeleteScrapAsync(int id);
}
public class ScrapService(ISqlSugarClient db) : IScrapService
{
private readonly ISqlSugarClient db = db;
private static readonly Dictionary<ScrapLevel, string> LevelColors = new()
{
{ ScrapLevel.White, "#FFFFFF" },
{ ScrapLevel.Green, "#00FF00" },
{ ScrapLevel.Blue, "#0077FF" },
{ ScrapLevel.Purple, "#9932CC" },
{ ScrapLevel.Orange, "#FF8C00" }
};
private static readonly Dictionary<ScrapLevel, int> LevelWeights = new()
{
{ ScrapLevel.White, 50 },
{ ScrapLevel.Green, 30 },
{ ScrapLevel.Blue, 15 },
{ ScrapLevel.Purple, 4 },
{ ScrapLevel.Orange, 1 }
};
public async Task<List<ScrapDto>> GetAllActiveScrapsAsync()
{
var scraps = await db.Queryable<Scrap>()
.Where(x => x.IsActive)
.OrderBy(x => x.Level)
.ToListAsync();
return scraps.Select(MapToDto).ToList();
}
public async Task<ScrapScanResultDto> ScanAndAssignScrapAsync(int characterId)
{
var character = await db.Queryable<Character>().FirstAsync(x => x.Id == characterId)
?? throw new Exception("角色不存在");
var scraps = await db.Queryable<Scrap>()
.Where(x => x.IsActive)
.ToListAsync();
if (scraps.Count == 0)
{
throw new Exception("目前没有可捡的垃圾");
}
var selectedLevel = SelectRandomLevel();
var levelScraps = scraps.Where(x => x.Level == selectedLevel).ToList();
if (levelScraps.Count == 0)
{
levelScraps = scraps;
}
var random = new Random();
var selectedScrap = levelScraps[random.Next(levelScraps.Count)];
var characterScrap = new CharacterScrap
{
CharacterId = characterId,
ScrapId = selectedScrap.Id,
CreatedBy = characterId,
UpdatedBy = characterId
};
await db.Insertable(characterScrap).ExecuteCommandAsync();
character.Attack += selectedScrap.AttackBonus;
character.MaxHP += selectedScrap.HpBonus;
await db.Updateable(character).ExecuteCommandAsync();
return new ScrapScanResultDto
{
Scrap = MapToDto(selectedScrap),
AttackGain = selectedScrap.AttackBonus,
DefenseGain = selectedScrap.DefenseBonus,
HpGain = selectedScrap.HpBonus,
MagicGain = selectedScrap.MagicBonus
};
}
public async Task<List<ScrapHistoryDto>> GetCharacterScrapHistoryAsync(int characterId)
{
var history = await db.Queryable<CharacterScrap>()
.Where(x => x.CharacterId == characterId)
.OrderByDescending(x => x.ObtainedAt)
.ToListAsync();
var result = new List<ScrapHistoryDto>();
foreach (var item in history)
{
var scrap = await db.Queryable<Scrap>().FirstAsync(x => x.Id == item.ScrapId);
if (scrap != null)
{
result.Add(new ScrapHistoryDto
{
Id = item.Id,
Scrap = MapToDto(scrap),
ObtainedAt = item.ObtainedAt
});
}
}
return result;
}
private ScrapLevel SelectRandomLevel()
{
var random = new Random();
int totalWeight = LevelWeights.Values.Sum();
int randomValue = random.Next(totalWeight);
int cumulativeWeight = 0;
foreach (var kvp in LevelWeights.OrderBy(x => x.Key))
{
cumulativeWeight += kvp.Value;
if (randomValue < cumulativeWeight)
{
return kvp.Key;
}
}
return ScrapLevel.White;
}
private static ScrapDto MapToDto(Scrap scrap)
{
var levelEnum = scrap.Level;
var description = GetEnumDescription(levelEnum);
return new ScrapDto
{
Id = scrap.Id,
Name = scrap.Name,
Description = scrap.Description,
Story = scrap.Story,
Level = (int)scrap.Level,
LevelName = description,
LevelColor = LevelColors.GetValueOrDefault(scrap.Level, "#FFFFFF"),
AttackBonus = scrap.AttackBonus,
DefenseBonus = scrap.DefenseBonus,
HpBonus = scrap.HpBonus,
MagicBonus = scrap.MagicBonus
};
}
private static string GetEnumDescription(ScrapLevel level)
{
var field = level.GetType().GetField(level.ToString());
var attribute = field?.GetCustomAttributes(typeof(DescriptionAttribute), false)
.FirstOrDefault() as DescriptionAttribute;
return attribute?.Description ?? level.ToString();
}
public async Task<List<Scrap>> GetAllScrapsAsync()
{
return await db.Queryable<Scrap>()
.OrderBy(x => x.Level)
.ToListAsync();
}
public async Task<bool> AddScrapAsync(ScrapDto dto)
{
var scrap = new Scrap
{
Name = dto.Name,
Description = dto.Description,
Story = dto.Story,
Level = (ScrapLevel)dto.Level,
AttackBonus = dto.AttackBonus,
DefenseBonus = dto.DefenseBonus,
HpBonus = dto.HpBonus,
MagicBonus = dto.MagicBonus,
IsActive = dto.IsActive
};
await db.Insertable(scrap).ExecuteCommandAsync();
return true;
}
public async Task<bool> UpdateScrapAsync(ScrapDto dto)
{
var scrap = await db.Queryable<Scrap>().FirstAsync(x => x.Id == dto.Id);
if (scrap == null)
{
throw new Exception("垃圾不存在");
}
scrap.Name = dto.Name;
scrap.Description = dto.Description;
scrap.Story = dto.Story;
scrap.Level = (ScrapLevel)dto.Level;
scrap.AttackBonus = dto.AttackBonus;
scrap.DefenseBonus = dto.DefenseBonus;
scrap.HpBonus = dto.HpBonus;
scrap.MagicBonus = dto.MagicBonus;
scrap.IsActive = dto.IsActive;
await db.Updateable(scrap).ExecuteCommandAsync();
return true;
}
public async Task<bool> DeleteScrapAsync(int id)
{
await db.Deleteable<Scrap>().Where(x => x.Id == id).ExecuteCommandAsync();
return true;
}
}
}

12
Build_God_Api/Build_God_Api/appsettings.json

@ -13,5 +13,17 @@
"Issuer": "BuildGod",
"Expires": 240,
"Audience": "ob"
},
"profiles": {
"Build_God_Api": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5091",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

76
Build_God_Game/src/api/scrap.ts

@ -0,0 +1,76 @@
import axios from 'axios'
const instance = axios.create({
baseURL: '/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
instance.interceptors.request.use(
(config) => {
const token = localStorage.getItem('auth_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => Promise.reject(error)
)
instance.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('auth_token')
localStorage.removeItem('user')
window.location.href = '/login'
}
return Promise.reject(error.response?.data || error.message)
}
)
export interface ScrapDto {
id: number
name: string
description: string
story: string
level: number
levelName: string
levelColor: string
attackBonus: number
defenseBonus: number
hpBonus: number
magicBonus: number
}
export interface ScrapScanResultDto {
scrap: ScrapDto
attackGain: number
defenseGain: number
hpGain: number
magicGain: number
}
export interface ScrapHistoryDto {
id: number
scrap: ScrapDto
obtainedAt: string
}
export const scrapApi = {
getScrapList: (): Promise<ScrapDto[]> => {
return instance.get('/scrap/list')
},
scanScrap: (characterId: number): Promise<ScrapScanResultDto> => {
return instance.post('/scrap/scan', { characterId })
},
getScrapHistory: (characterId: number): Promise<ScrapHistoryDto[]> => {
return instance.get(`/scrap/history/${characterId}`)
}
}
export default instance

6
Build_God_Game/src/router/index.ts

@ -44,6 +44,12 @@ const router = createRouter({
name: 'daily-mission',
component: () => import('@/views/DailyMissionView.vue'),
meta: { requiresAuth: true }
},
{
path: '/scrap',
name: 'scrap',
component: () => import('@/views/ScrapView.vue'),
meta: { requiresAuth: true }
}
]
})

3
Build_God_Game/src/views/GameView.vue

@ -37,6 +37,7 @@ const menuItems = computed(() => [
{ label: '背包', icon: '🎒' },
{ label: '角色', icon: '👤' },
{ label: isTraining.value ? '打坐中' : '打坐', icon: isTraining.value ? '🔥' : '🧘', isTraining: isTraining.value },
{ label: '捡垃圾', icon: '🗑️' },
])
const handleLogout = () => {
@ -54,6 +55,8 @@ const navigateTo = (item: { label: string }) => {
router.push('/training')
} else if (item.label === '任务') {
router.push('/daily-mission')
} else if (item.label === '捡垃圾') {
router.push('/scrap')
}
}

625
Build_God_Game/src/views/ScrapView.vue

@ -0,0 +1,625 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useCharacterStore } from '@/stores/character'
import { scrapApi, type ScrapScanResultDto } from '@/api/scrap'
import Particles from '@/components/Particles/Particles.vue'
import GlareHover from '@/components/GlareHover/GlareHover.vue'
const router = useRouter()
const characterStore = useCharacterStore()
const isScanning = ref(false)
const countdown = ref(0)
const countdownInterval = ref<number | null>(null)
const scanResult = ref<ScrapScanResultDto | null>(null)
const showStory = ref(false)
const isLoading = ref(false)
const errorMessage = ref('')
const characterId = computed(() => characterStore.currentCharacter?.id ?? 0)
const startScanning = async () => {
if (isScanning.value || !characterId.value) return
isLoading.value = true
errorMessage.value = ''
scanResult.value = null
showStory.value = false
const minSeconds = 5
const maxSeconds = 10
countdown.value = Math.floor(Math.random() * (maxSeconds - minSeconds + 1)) + minSeconds
isScanning.value = true
countdownInterval.value = window.setInterval(async () => {
countdown.value--
if (countdown.value <= 0) {
if (countdownInterval.value) {
clearInterval(countdownInterval.value)
countdownInterval.value = null
}
await performScan()
}
}, 1000)
}
const performScan = async () => {
try {
const result = await scrapApi.scanScrap(characterId.value)
scanResult.value = result
await characterStore.fetchCharacters()
} catch (error: unknown) {
errorMessage.value = error instanceof Error ? error.message : String(error)
} finally {
isScanning.value = false
isLoading.value = false
}
}
const cancelScanning = () => {
if (countdownInterval.value) {
clearInterval(countdownInterval.value)
countdownInterval.value = null
}
isScanning.value = false
countdown.value = 0
}
const viewStory = () => {
showStory.value = true
}
const hideStory = () => {
showStory.value = false
}
const goBack = () => {
router.push('/game')
}
const getLevelColor = (color: string) => {
const colorMap: Record<string, string> = {
'#FFFFFF': '#cccccc',
'#00FF00': '#00ff00',
'#0077FF': '#0077ff',
'#9932CC': '#9932cc',
'#FF8C00': '#ff8c00'
}
return colorMap[color] || '#ffffff'
}
</script>
<template>
<div class="scrap-page">
<Particles
:particle-count="60"
:particle-colors="['#ffffff', '#aaaaaa']"
class="particles-bg"
/>
<div class="scrap-container">
<div class="header">
<button class="back-btn" @click="goBack">
<span class="back-arrow"></span>
<span>返回</span>
</button>
<h1 class="title">捡垃圾</h1>
<div class="spacer"></div>
</div>
<div class="content">
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
<div v-if="!scanResult && !isScanning" class="scan-prompt">
<div class="scan-icon">🔍</div>
<p class="scan-text">在废墟中搜寻有价值的物品...</p>
<GlareHover
width="200px"
height="50px"
background="rgba(255,255,255,0.05)"
border-radius="25px"
border-color="rgba(255,255,255,0.15)"
glare-color="#ffffff"
:glare-opacity="0.15"
class="scan-btn"
@click="startScanning"
>
<span class="scan-btn-text">开始扫描</span>
</GlareHover>
</div>
<div v-if="isScanning" class="scanning-state">
<div class="scan-animation">🔍</div>
<p class="scanning-text">扫描中...</p>
<div class="countdown-ring">
<svg class="countdown-svg" viewBox="0 0 100 100">
<circle
class="countdown-bg"
cx="50"
cy="50"
r="45"
/>
<circle
class="countdown-progress"
cx="50"
cy="50"
r="45"
:style="{ strokeDashoffset: 283 - (283 * countdown / 10) }"
/>
</svg>
<span class="countdown-number">{{ countdown }}</span>
</div>
<GlareHover
width="150px"
height="40px"
background="rgba(255,100,100,0.1)"
border-radius="20px"
border-color="rgba(255,100,100,0.3)"
glare-color="#ff6666"
:glare-opacity="0.1"
class="cancel-btn"
@click="cancelScanning"
>
<span class="cancel-btn-text">取消</span>
</GlareHover>
</div>
<div v-if="scanResult && !showStory" class="result-card">
<div class="result-header">
<span
class="scrap-name"
:style="{ color: getLevelColor(scanResult.scrap.levelColor) }"
>
{{ scanResult.scrap.name }}
</span>
<span
class="scrap-level"
:style="{ color: getLevelColor(scanResult.scrap.levelColor) }"
>
{{ scanResult.scrap.levelName }}
</span>
</div>
<div class="result-desc">
{{ scanResult.scrap.description }}
</div>
<div class="bonus-section">
<div class="bonus-title">属性加成</div>
<div class="bonus-list">
<div v-if="scanResult.attackGain > 0" class="bonus-item attack">
<span class="bonus-icon"></span>
<span class="bonus-value">+{{ scanResult.attackGain }}</span>
<span class="bonus-label">攻击</span>
</div>
<div v-if="scanResult.defenseGain > 0" class="bonus-item defense">
<span class="bonus-icon">🛡</span>
<span class="bonus-value">+{{ scanResult.defenseGain }}</span>
<span class="bonus-label">防御</span>
</div>
<div v-if="scanResult.hpGain > 0" class="bonus-item hp">
<span class="bonus-icon"></span>
<span class="bonus-value">+{{ scanResult.hpGain }}</span>
<span class="bonus-label">生命</span>
</div>
<div v-if="scanResult.magicGain > 0" class="bonus-item magic">
<span class="bonus-icon"></span>
<span class="bonus-value">+{{ scanResult.magicGain }}</span>
<span class="bonus-label">魔力</span>
</div>
</div>
</div>
<div class="result-actions">
<GlareHover
width="100%"
height="44px"
background="rgba(255,255,255,0.05)"
border-radius="22px"
border-color="rgba(255,255,255,0.1)"
glare-color="#ffffff"
:glare-opacity="0.1"
class="story-btn"
@click="viewStory"
>
<span class="story-btn-text">查看故事</span>
</GlareHover>
<GlareHover
width="100%"
height="44px"
background="rgba(255,140,0,0.1)"
border-radius="22px"
border-color="rgba(255,140,0,0.3)"
glare-color="#ff8c00"
:glare-opacity="0.1"
class="again-btn"
@click="startScanning"
>
<span class="again-btn-text">继续捡</span>
</GlareHover>
</div>
</div>
<div v-if="showStory && scanResult" class="story-card">
<div class="story-header">
<span class="story-title">物品故事</span>
<button class="close-story" @click="hideStory">×</button>
</div>
<div class="story-content">
<p class="story-name" :style="{ color: getLevelColor(scanResult.scrap.levelColor) }">
{{ scanResult.scrap.name }}
</p>
<p class="story-text">{{ scanResult.scrap.story }}</p>
</div>
<div class="story-footer">
<GlareHover
width="100%"
height="44px"
background="rgba(255,255,255,0.05)"
border-radius="22px"
border-color="rgba(255,255,255,0.1)"
glare-color="#ffffff"
:glare-opacity="0.1"
class="back-to-result"
@click="hideStory"
>
<span class="back-btn-text">返回</span>
</GlareHover>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.scrap-page {
min-height: 100vh;
background: #000000;
padding: 20px;
position: relative;
overflow: hidden;
}
.particles-bg {
position: fixed !important;
top: 0;
left: 0;
width: 100% !important;
height: 100% !important;
}
.scrap-container {
max-width: 480px;
margin: 0 auto;
position: relative;
z-index: 10;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.back-btn {
display: flex;
align-items: center;
gap: 6px;
background: none;
border: none;
color: #888888;
font-size: 0.9rem;
cursor: pointer;
transition: color 0.2s;
}
.back-btn:hover {
color: #ffffff;
}
.back-arrow {
font-size: 1.2rem;
}
.title {
font-size: 1.3rem;
font-weight: 500;
color: #ffffff;
letter-spacing: 0.1em;
}
.spacer {
width: 60px;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
}
.error-message {
width: 100%;
padding: 16px;
background: rgba(255, 68, 68, 0.1);
border: 1px solid rgba(255, 68, 68, 0.3);
border-radius: 12px;
color: #ff6666;
font-size: 0.9rem;
text-align: center;
margin-bottom: 20px;
}
.scan-prompt {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
padding: 40px 20px;
}
.scan-icon {
font-size: 4rem;
opacity: 0.6;
}
.scan-text {
color: #888888;
font-size: 1rem;
text-align: center;
}
.scan-btn {
cursor: pointer;
}
.scan-btn-text {
color: #ffffff;
font-size: 1rem;
font-weight: 500;
}
.scanning-state {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
padding: 40px 20px;
}
.scan-animation {
font-size: 4rem;
animation: pulse 1s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 0.6; }
50% { transform: scale(1.1); opacity: 1; }
}
.scanning-text {
color: #888888;
font-size: 1rem;
}
.countdown-ring {
position: relative;
width: 120px;
height: 120px;
}
.countdown-svg {
width: 100%;
height: 100%;
transform: rotate(-90deg);
}
.countdown-bg {
fill: none;
stroke: rgba(255, 255, 255, 0.1);
stroke-width: 6;
}
.countdown-progress {
fill: none;
stroke: #ff8c00;
stroke-width: 6;
stroke-linecap: round;
stroke-dasharray: 283;
transition: stroke-dashoffset 1s linear;
}
.countdown-number {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 2.5rem;
font-weight: 300;
color: #ffffff;
}
.cancel-btn {
cursor: pointer;
}
.cancel-btn-text {
color: #ff6666;
font-size: 0.9rem;
}
.result-card {
width: 100%;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: 24px;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.scrap-name {
font-size: 1.3rem;
font-weight: 600;
}
.scrap-level {
font-size: 0.85rem;
font-weight: 500;
padding: 4px 12px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.05);
}
.result-desc {
color: #888888;
font-size: 0.9rem;
line-height: 1.6;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.bonus-section {
margin-bottom: 24px;
}
.bonus-title {
color: #666666;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 12px;
}
.bonus-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.bonus-item {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
background: rgba(255, 255, 255, 0.02);
border-radius: 8px;
}
.bonus-icon {
font-size: 1.2rem;
}
.bonus-value {
font-size: 1.1rem;
font-weight: 600;
color: #ffffff;
}
.bonus-label {
font-size: 0.8rem;
color: #666666;
}
.result-actions {
display: flex;
flex-direction: column;
gap: 12px;
}
.story-btn {
cursor: pointer;
}
.story-btn-text {
color: #cccccc;
font-size: 0.9rem;
}
.again-btn {
cursor: pointer;
}
.again-btn-text {
color: #ff8c00;
font-size: 0.9rem;
font-weight: 500;
}
.story-card {
width: 100%;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: 24px;
}
.story-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.story-title {
color: #888888;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.1em;
}
.close-story {
background: none;
border: none;
color: #666666;
font-size: 1.5rem;
cursor: pointer;
line-height: 1;
padding: 0;
}
.close-story:hover {
color: #ffffff;
}
.story-content {
margin-bottom: 24px;
}
.story-name {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 16px;
}
.story-text {
color: #aaaaaa;
font-size: 0.95rem;
line-height: 1.8;
}
.story-footer {
display: flex;
justify-content: center;
}
.back-to-result {
cursor: pointer;
}
.back-btn-text {
color: #cccccc;
font-size: 0.9rem;
}
</style>
Loading…
Cancel
Save