Compare commits
2 Commits
84ff464ff3
...
414b8cdb34
| Author | SHA1 | Date |
|---|---|---|
|
|
414b8cdb34 | 1 week ago |
|
|
b4210eddce | 1 week ago |
17 changed files with 1584 additions and 0 deletions
@ -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}`); |
||||
|
}; |
||||
@ -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> |
||||
@ -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 |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
@ -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…
Reference in new issue