10 changed files with 1548 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