This commit is contained in:
Putoo
2026-04-25 18:12:07 +08:00
6 changed files with 449 additions and 135 deletions

122
AGENTS.md Normal file
View File

@@ -0,0 +1,122 @@
# AGENTS.md
Guidelines for AI agents working in this SeaTime (航海时代) codebase.
## Project Structure
Full-stack web game with two components:
- **Web/**: Nuxt 4 (Vue 3 + TypeScript) frontend
- **Service/**: .NET 10 Web API backend using Photon.Core framework
## Build Commands
### Web (Frontend)
```bash
cd Web
npm install # Install dependencies
npm run dev # Start dev server on port 5068
npm run build # Production build
npm run generate # Static site generation
npm run preview # Preview production build
npm run postinstall # Nuxt prepare (runs automatically after install)
```
### Service (Backend)
```bash
cd Service
dotnet build # Build solution
dotnet run --project Application.Web # Run web API
dotnet watch --project Application.Web # Run with hot reload
```
## Testing Commands
**No test framework configured yet.** To add tests:
- **Web**: Install Vitest or Jest for unit tests
- **Service**: Use `dotnet test` after adding xUnit/NUnit test projects
When tests are added, run them with:
```bash
# Web (Vitest example)
npm run test # Run all tests
npm run test:unit # Run unit tests only
npm run test -- <file> # Run single test file
# Service
dotnet test # Run all tests
dotnet test --filter "FullyQualifiedName~ClassName" # Run specific test class
```
## Code Style Guidelines
### TypeScript / Vue (Web)
- **Imports**: Use `~/` alias for src directory imports (e.g., `import type { IUserInfo } from '~/types/user'`)
- **Vue SFCs**: Use `<script setup lang="ts">` composition API
- **Naming Conventions**:
- Stores: `useXxxStore` in `stores/` (e.g., `useUserStore`)
- Composables: `useXxx` in `composables/` (e.g., `useAuth`)
- Services: `XxxSERVICE` class in `services/` (e.g., `UserSERVICE`)
- Extends: `xxxEXTEND` class in `extends/` (e.g., `RequestEXTEND`)
- Types: `IXxx` interface in `types/` (e.g., `IUserInfo`)
- Pages: kebab-case in `pages/` (e.g., `auth/login.vue`)
- **Auto-imports**: stores, composables, extends, services are auto-imported (see nuxt.config.ts)
- **TypeScript**: Strict mode enabled, always define return types for public functions
- **Comments**: Use Chinese for business logic documentation
- **API Pattern**: Services check `response.code !== 200 && response.code !== 0` for errors
- **State Management**: Pinia stores use `state`/`getters`/`actions` pattern with `persist` for localStorage
- **Persistence**: Only persist `token` and `userInfo` to localStorage via `piniaPluginPersistedstate.localStorage()`
### C# (Service)
- Target framework: .NET 10
- Nullable reference types enabled
- Implicit usings enabled
- Standard C# naming: PascalCase for classes/methods, camelCase for locals
- Controllers use `[ApiExplorerSettings(GroupName = "...")]` for Swagger grouping
- Route pattern: `[Route("[controller]/[action]")]`
- Return `IPoAction` / `PoAction.Ok()` from controllers
- Request params in `Application.Domain/RequestParms/` folder
- Global usings defined in `Application.Web/GlobalUsings.cs`
- Uses Photon.Core framework (DI via `Inject*`, JWT, SqlSugar, Timer)
## Error Handling
### Web
- Services: Wrap API calls in try-catch, log errors with `console.error`, re-throw or return false
- Components: Use `showToast`/`showFailToast`/`showSuccessToast` from Vant for user feedback
- Always check `typeof window !== 'undefined'` for client-side only code
- Navigation: Use `navigateTo()` for programmatic navigation
### Service
- Use standard .NET exception handling middleware
- Return consistent response models with code/message/data structure
- CORS policy "all" configured for cross-origin requests
## Key Technologies
- **Frontend**: Nuxt 4, Vue 3, Pinia, Vant UI, TypeScript
- **Backend**: .NET 10 Web API, Photon.Core framework
- **State Management**: Pinia with pinia-plugin-persistedstate (localStorage)
- **HTTP Client**: Custom RequestEXTEND class based on native fetch
- **Auth**: JWT with custom JwtHandle validator
- **Timer**: AutoJob hosted service with TimerJobManager
## Project Conventions
- Source directory: `Web/src/`
- Pages use file-based routing (Nuxt convention)
- Layouts in `Web/src/layouts/`
- Layout constants in `composables/layout.ts` as `layout` object (e.g., `layout.empty`)
- Use `definePageMeta({ layout: layout.empty })` for page layout configuration
- Store persistence: Only token and userInfo are persisted to localStorage
- No ESLint/Prettier configured yet - follow existing code patterns
## Existing Rules
No Cursor rules (.cursorrules or .cursor/rules/) or Copilot instructions (.github/copilot-instructions.md) found in this repository.

View File

@@ -14,9 +14,11 @@
// 可以在这里进行全局初始化
const appStore = useAppStore()
import { Loading } from 'vant'
// 初始化应用配置
onMounted(() => {
// 初始化屏幕尺寸
if (typeof window !== 'undefined') {
appStore.updateScreenSize(window.innerWidth, window.innerHeight)

View File

@@ -508,3 +508,126 @@ a {
max-height: 150px;
max-width: 250px;
}
/* ========== 自定义通知队列 ========== */
#custom-notify-container {
position: fixed;
top: 10px;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
pointer-events: none;
}
.custom-notify-item {
box-sizing: border-box;
max-width: 90%;
padding: 8px 16px;
margin-bottom: 8px;
border-radius: 8px;
color: #fff;
font-size: 12px;
line-height: 1.5;
text-align: center;
word-wrap: break-word;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
pointer-events: auto;
cursor: pointer;
opacity: 0;
transform: translateY(-20px);
animation: notify-slide-in 300ms ease-out forwards;
}
.custom-notify-item.leaving {
animation: notify-fade-out 200ms ease-in forwards;
}
@keyframes notify-slide-in {
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes notify-fade-out {
to {
opacity: 0;
transform: translateY(-10px);
}
}
/* ========== 顶部通告栏 ========== */
.custom-notice-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 40px;
display: flex;
align-items: center;
padding: 0 16px;
box-sizing: border-box;
z-index: 9998;
font-size: 14px;
line-height: 24px;
}
.custom-notice-bar .notice-icon {
flex-shrink: 0;
margin-right: 8px;
font-size: 16px;
position: relative;
z-index: 1;
}
.custom-notice-bar .notice-wrap {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
align-items: center;
padding: 0 40px;
box-sizing: border-box;
}
.custom-notice-bar .notice-text {
display: inline-block;
white-space: nowrap;
padding-right: 50px;
flex-shrink: 0;
}
.custom-notice-bar .notice-wrap.scrolling {
animation: notice-scroll var(--scroll-duration, 10s) linear infinite;
}
@keyframes notice-scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
.custom-notice-bar .notice-close {
flex-shrink: 0;
margin-left: 8px;
font-size: 18px;
cursor: pointer;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
position: relative;
z-index: 1;
}

View File

@@ -0,0 +1,102 @@
/**
* 消息提示工具
*/
export class MessageExtend {
// 消息通知
static notify(type: 'primary' | 'success' | 'danger' | 'warning', message: any) {
showNotify({ type, message })
}
// 提示弹窗
static dialog(
title: string,
message: string,
theme: 'round-button' | 'default',
onConfirm?: () => void,
onCancel?: () => void,
confirmButtonText: string = '确认',
) {
showConfirmDialog({
title: title,
message: message,
theme: theme || 'default',
confirmButtonText: confirmButtonText || '确认',
})
.then(() => {
onConfirm?.()
})
.catch(() => {
onCancel?.()
})
}
// 成功失败默认提示
static showToast(type: 'success' | 'fail' | 'default', text: any) {
if (type == 'success') {
showSuccessToast(text)
} else if (type == 'fail') {
showFailToast(text)
} else {
showToast(text)
}
}
// 自定义图标提示
static showIconToast(icon: any, text: any) {
showToast({
message: text,
icon: icon,
})
}
// 多条消息通知(堆叠展示)
static notifyList(type: 'primary' | 'success' | 'danger' | 'warning', messages: string[], duration: number = 3000) {
if (typeof window === 'undefined') return
if (!messages || messages.length === 0) return
const colorMap = {
primary: '#1989fa',
success: '#07c160',
danger: '#ee0a24',
warning: '#ff976a',
}
let container = document.getElementById('custom-notify-container')
if (!container) {
container = document.createElement('div')
container.id = 'custom-notify-container'
document.body.appendChild(container)
}
messages.forEach((message, index) => {
const item = document.createElement('div')
item.className = 'custom-notify-item'
item.style.backgroundColor = colorMap[type]
item.textContent = String(message)
item.style.animationDelay = `${index * 100}ms`
item.addEventListener('click', () => {
this.closeNotifyItem(item)
})
container!.appendChild(item)
setTimeout(() => {
this.closeNotifyItem(item)
}, duration)
})
}
// 关闭单条通知
private static closeNotifyItem(item: HTMLElement) {
if (!item || item.classList.contains('leaving')) return
item.classList.add('leaving')
item.addEventListener('animationend', () => {
item.remove()
const container = document.getElementById('custom-notify-container')
if (container && container.children.length === 0) {
container.remove()
}
})
}
}

View File

@@ -48,16 +48,8 @@
<label class="field-label" for="login-username">账号</label>
<div class="field-box" :class="{ 'is-error': !!errors.username }">
<span class="field-marker">A</span>
<input
id="login-username"
v-model.trim="form.username"
class="field-input"
type="text"
inputmode="text"
autocomplete="username"
placeholder="请输入账号"
@input="clearFieldError('username')"
/>
<input id="login-username" v-model.trim="form.username" class="field-input" type="text" inputmode="text"
autocomplete="username" placeholder="请输入账号" @input="clearFieldError('username')" />
</div>
<p v-if="errors.username" class="field-error">{{ errors.username }}</p>
</div>
@@ -66,15 +58,8 @@
<label class="field-label" for="login-password">密码</label>
<div class="field-box" :class="{ 'is-error': !!errors.password }">
<span class="field-marker">P</span>
<input
id="login-password"
v-model.trim="form.password"
class="field-input"
type="password"
autocomplete="current-password"
placeholder="请输入密码"
@input="clearFieldError('password')"
/>
<input id="login-password" v-model.trim="form.password" class="field-input" type="password"
autocomplete="current-password" placeholder="请输入密码" @input="clearFieldError('password')" />
</div>
<p v-if="errors.password" class="field-error">{{ errors.password }}</p>
</div>
@@ -92,12 +77,8 @@
<div class="login-agreement">
<label class="check-row">
<input
v-model="form.agreement"
class="check-input"
type="checkbox"
@change="clearFieldError('agreement')"
/>
<input v-model="form.agreement" class="check-input" type="checkbox"
@change="clearFieldError('agreement')" />
<span class="check-box"></span>
<span class="check-text">我已阅读并同意演示使用说明</span>
</label>
@@ -105,14 +86,8 @@
</div>
<div class="login-actions">
<van-button
block
round
type="primary"
native-type="submit"
:loading="isSubmitting"
:disabled="submitDisabled"
>
<van-button block round type="primary" native-type="submit" :loading="isSubmitting"
:disabled="submitDisabled">
模拟登录
</van-button>
<van-button block round plain type="primary" native-type="button" @click="clearForm">
@@ -393,7 +368,7 @@ const handleSubmit = async () => {
margin-top: 18px;
}
.field-group + .field-group {
.field-group+.field-group {
margin-top: 14px;
}
@@ -521,12 +496,12 @@ const handleSubmit = async () => {
transform: rotate(45deg);
}
.check-input:checked + .check-box {
.check-input:checked+.check-box {
border-color: #2563eb;
background: #2563eb;
}
.check-input:checked + .check-box::after {
.check-input:checked+.check-box::after {
opacity: 1;
}

View File

@@ -4,7 +4,7 @@
驰骋四海&#xB7;社区版
</div>
<div>
<Abar url="/home" _class="test">11111</Abar>
当前在线<strong> 139 </strong>位玩家在驰骋四海
</div>
<div class="content" style="font-size:17px">
@@ -28,8 +28,7 @@
</div>
<div class="content">
<div class="item">
<a
href="/LoginGame/LoginOk?sid=W6Wg8iH9gY7wIBNSEdtFcQ3KbI5YiKDo">&#x2727;&#x3010;1&#x3011;新手村&#x2730;村长()</a>
<a href="/LoginGame/LoginOk?sid=W6Wg8iH9gY7wIBNSEdtFcQ3KbI5YiKDo">&#x2727;&#x3010;1&#x3011;新手村&#x2730;村长()</a>
</div>
</div>
</div>
@@ -53,18 +52,15 @@
</div>
<div class="item">
2.
<a class=""
href="/Pallet/Notice/Detail?nt=2026041701&amp;sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">[活动]4月份活动集锦</a>
<a class="" href="/Pallet/Notice/Detail?nt=2026041701&amp;sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">[活动]4月份活动集锦</a>
</div>
<div class="item">
3.
<a class="" href="/Pallet/Notice/Detail?nt=2026040901&amp;sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">[推广]
4月份推广</a>
<a class="" href="/Pallet/Notice/Detail?nt=2026040901&amp;sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">[推广] 4月份推广</a>
</div>
<div class="item">
4.
<a class="" href="/Pallet/Notice/Detail?nt=2026030101&amp;sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">[推广]
3月份推广</a>
<a class="" href="/Pallet/Notice/Detail?nt=2026030101&amp;sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">[推广] 3月份推广</a>
</div>
<div class="item">
5.
@@ -94,20 +90,14 @@
</template>
<script setup lang="ts">
// MessageExtend.showToast('success', '更新成功!')
MessageExtend.notifyList('primary', ['获取装备'])
definePageMeta({
layout: layout.empty
})
const req = (async () => {
try {
const test = await ApiService.ApiRequest("get", "/Login/Test", {name:"putoo",age:30});
console.log(test.data);
}
catch { }
});
//showNotify({ message: '提示' });
showNotify({ message: '提示' });
// await navigateTo('/auth/login', { replace: true })
onMounted(() => {
req();