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() const appStore = useAppStore()
import { Loading } from 'vant'
// 初始化应用配置 // 初始化应用配置
onMounted(() => { onMounted(() => {
// 初始化屏幕尺寸 // 初始化屏幕尺寸
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
appStore.updateScreenSize(window.innerWidth, window.innerHeight) appStore.updateScreenSize(window.innerWidth, window.innerHeight)

View File

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

View File

@@ -1,116 +1,106 @@
<template> <template>
<div class="head"> <div class="head">
<img src="http://gree.pccsh.com/images/site/logo.png" class="logo" /><br /> <img src="http://gree.pccsh.com/images/site/logo.png" class="logo" /><br />
驰骋四海&#xB7;社区版 驰骋四海&#xB7;社区版
</div> </div>
<div> <div>
<Abar url="/home" _class="test">11111</Abar>
当前在线<strong> 139 </strong>位玩家在驰骋四海
</div>
<div class="content" style="font-size:17px">
<div>
亲爱的&nbsp;<strong style="color:red">探玩玩家</strong>欢迎来到驰骋四海&#xB7;社区版!
</div>
<div style="margin-top:5px;">
<div>
<a href="http://m.twbar.cn/Home/Index?sid=KrWxKypJuDO0zFKrTig0bG">返回探玩驿站</a>
</div> 当前在线<strong> 139 </strong>位玩家在驰骋四海
<div> </div>
<a href="http://m.twbar.cn/b/1145?sid=KrWxKypJuDO0zFKrTig0bG">游戏论坛</a>&nbsp;&nbsp;<a class="" <div class="content" style="font-size:17px">
href="/Login/LoginOut?sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">退出游戏</a>
</div>
</div>
</div>
<div class="common">
<div class="title">
=====<a class="" href="/Pallet/GameOpen/GameUser?sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">我的区服</a>=====
</div>
<div class="content">
<div class="item">
<a
href="/LoginGame/LoginOk?sid=W6Wg8iH9gY7wIBNSEdtFcQ3KbI5YiKDo">&#x2727;&#x3010;1&#x3011;新手村&#x2730;村长()</a>
</div>
</div>
</div>
<div class="common">
<div class="title">
=====其他区服=====
</div>
<div class="content">
<span>暂无区服.</span>
</div>
</div>
<div class="title">
=====<a class="" href="/Pallet/Notice/Index?sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">官方公告</a>=====
</div>
<div> <div>
<div class="item"> 亲爱的&nbsp;<strong style="color:red">探玩玩家</strong>欢迎来到驰骋四海&#xB7;社区版!
1.
<a class=""
href="/Pallet/Notice/Detail?nt=2026041901&amp;sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">[招募令]航海时代2设计专员招募</a>
</div>
<div class="item">
2.
<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>
</div>
<div class="item">
4.
<a class="" href="/Pallet/Notice/Detail?nt=2026030101&amp;sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">[推广]
3月份推广</a>
</div>
<div class="item">
5.
<a class=""
href="/Pallet/Notice/Detail?nt=2025080002&amp;sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">&#x3010;驰骋四海&#x3011;卡片&#x306E;攻略&#xFF08;2.24&#xFF09;</a>
</div>
</div> </div>
<div style="margin-top:5px;">
<div>
<a href="http://m.twbar.cn/Home/Index?sid=KrWxKypJuDO0zFKrTig0bG">返回探玩驿站</a>
</div>
<div>
<a href="http://m.twbar.cn/b/1145?sid=KrWxKypJuDO0zFKrTig0bG">游戏论坛</a>&nbsp;&nbsp;<a class=""
href="/Login/LoginOut?sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">退出游戏</a>
</div>
</div>
</div>
<div class="common">
<div class="title"> <div class="title">
=====服务导航===== =====<a class="" href="/Pallet/GameOpen/GameUser?sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">我的区服</a>=====
</div> </div>
<div class="content"> <div class="content">
<a class="" href="/Index/Kefu?sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">客服</a>.<a class="" <div class="item">
href="/Index/About?sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">关于</a>.<a class="" <a href="/LoginGame/LoginOk?sid=W6Wg8iH9gY7wIBNSEdtFcQ3KbI5YiKDo">&#x2727;&#x3010;1&#x3011;新手村&#x2730;村长()</a>
href="/Index/Cooperation?sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">合作</a> </div>
</div>
</div>
<div class="common">
<div class="title">
=====其他区服=====
</div> </div>
<div class="content"> <div class="content">
切换线路: <span>暂无区服.</span>
<span class="game_line">
<a class="" href="http://g.pccsh.com:5016/Index/Index?sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">1</a>
</span>
</div> </div>
<div class="foot"> </div>
<div class="timeService">
小G报时(18:33) <div class="title">
</div> =====<a class="" href="/Pallet/Notice/Index?sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">官方公告</a>=====
</div>
<div>
<div class="item">
1.
<a class=""
href="/Pallet/Notice/Detail?nt=2026041901&amp;sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">[招募令]航海时代2设计专员招募</a>
</div> </div>
<div class="item">
2.
<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>
</div>
<div class="item">
4.
<a class="" href="/Pallet/Notice/Detail?nt=2026030101&amp;sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">[推广] 3月份推广</a>
</div>
<div class="item">
5.
<a class=""
href="/Pallet/Notice/Detail?nt=2025080002&amp;sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">&#x3010;驰骋四海&#x3011;卡片&#x306E;攻略&#xFF08;2.24&#xFF09;</a>
</div>
</div>
<div class="title">
=====服务导航=====
</div>
<div class="content">
<a class="" href="/Index/Kefu?sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">客服</a>.<a class=""
href="/Index/About?sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">关于</a>.<a class=""
href="/Index/Cooperation?sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">合作</a>
</div>
<div class="content">
切换线路:
<span class="game_line">
<a class="" href="http://g.pccsh.com:5016/Index/Index?sid=klxy7ADn96CBYGWQ9AG4xPqFC2Ib6Ty1Kx">1</a>
</span>
</div>
<div class="foot">
<div class="timeService">
小G报时(18:33)
</div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// MessageExtend.showToast('success', '更新成功!')
MessageExtend.notifyList('primary', ['获取装备'])
definePageMeta({ definePageMeta({
layout: layout.empty layout: layout.empty
}) })
showNotify({ message: '提示' });
const req = (async () => {
try {
const test = await ApiService.ApiRequest("get", "/Login/Test", {name:"putoo",age:30});
console.log(test.data);
}
catch { }
});
//showNotify({ message: '提示' });
// await navigateTo('/auth/login', { replace: true }) // await navigateTo('/auth/login', { replace: true })
onMounted(() => { onMounted(() => {
req(); req();
alert(1); alert(1);
}) })
</script> </script>