fix merge conflicts
@ -1,14 +1,15 @@
|
|||||||
{
|
{
|
||||||
'name': '宇森游戏基础数据',
|
'name': '宇森游戏基础数据',
|
||||||
'summary': '宇森游戏基础数据:图鉴 / 规则 / 论坛 / 公告',
|
'summary': '宇森游戏基础数据:图鉴 / 规则 / 论坛 / 公告 / 玩家员工',
|
||||||
'version': '1.2',
|
'version': '1.3',
|
||||||
'category': 'Game',
|
'category': 'Game',
|
||||||
'sequence': 10,
|
'sequence': 10,
|
||||||
'depends': ['base'],
|
'depends': ['base', 'hr'],
|
||||||
'data': [
|
'data': [
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'data/game_base_data.xml',
|
'data/game_base_data.xml',
|
||||||
'data/story_cron.xml',
|
'data/story_cron.xml',
|
||||||
|
'data/game_achievement_data.xml',
|
||||||
'views/codex_views.xml',
|
'views/codex_views.xml',
|
||||||
'views/rule_category_views.xml',
|
'views/rule_category_views.xml',
|
||||||
'views/rule_views.xml',
|
'views/rule_views.xml',
|
||||||
@ -16,10 +17,13 @@
|
|||||||
'views/forum_views.xml',
|
'views/forum_views.xml',
|
||||||
'views/announcement_views.xml',
|
'views/announcement_views.xml',
|
||||||
'views/story_views.xml',
|
'views/story_views.xml',
|
||||||
|
'views/game_employee_views.xml',
|
||||||
|
'views/game_achievement_views.xml',
|
||||||
|
'views/game_mail_views.xml',
|
||||||
'views/menu.xml',
|
'views/menu.xml',
|
||||||
],
|
],
|
||||||
'application': True,
|
'application': True,
|
||||||
'license': 'LGPL-3',
|
'license': 'LGPL-3',
|
||||||
'pre_init_hook': 'pre_init_hook',
|
'pre_init_hook': 'pre_init_hook',
|
||||||
'description': '基于官网内容沉淀的游戏基础数据模型,供后台维护、前端经 /game/api/* 读取;并提供与 Odoo 用户打通的登录/注册接口。',
|
'description': '基于官网内容沉淀的游戏基础数据模型,供后台维护、前端经 /game/api/* 读取;并提供与 Odoo 用户打通的登录/注册接口。注册时自动创建 hr.employee 并初始化游戏数据。',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import json
|
import json
|
||||||
from odoo import http
|
from odoo import http, fields
|
||||||
from odoo.http import request, Response
|
from odoo.http import request, Response
|
||||||
|
|
||||||
|
|
||||||
@ -309,12 +309,52 @@ class GameApiController(http.Controller):
|
|||||||
'groups_id': [(6, 0, [portal.id])],
|
'groups_id': [(6, 0, [portal.id])],
|
||||||
})
|
})
|
||||||
request.env.cr.commit() # 让新用户立即对其他游标可见,便于随后自动登录
|
request.env.cr.commit() # 让新用户立即对其他游标可见,便于随后自动登录
|
||||||
|
# === 自动创建 hr.employee 并初始化游戏数据 ===
|
||||||
|
Emp = request.env['hr.employee'].sudo()
|
||||||
|
emp = Emp.create({
|
||||||
|
'name': name or login,
|
||||||
|
'user_id': user.id,
|
||||||
|
'work_email': email or False,
|
||||||
|
'game_title': '萌新',
|
||||||
|
'game_level': 1,
|
||||||
|
'game_exp': 0,
|
||||||
|
'game_exp_max': 100,
|
||||||
|
'game_coins': 0,
|
||||||
|
'game_energy': 0,
|
||||||
|
'game_wood': 0,
|
||||||
|
'game_stone': 0,
|
||||||
|
'game_reg_date': fields.Date.today(),
|
||||||
|
})
|
||||||
|
# 生成游戏 UID(格式 10086 + 4位员工ID)
|
||||||
|
emp.game_uid = '10086%04d' % emp.id
|
||||||
|
# 给新玩家解锁第一个成就"开天辟地"
|
||||||
|
tmpl = request.env['game.achievement.template'].sudo().search(
|
||||||
|
[('code', '=', 'first_land')], limit=1)
|
||||||
|
if tmpl:
|
||||||
|
request.env['game.player.achievement'].sudo().create({
|
||||||
|
'employee_id': emp.id,
|
||||||
|
'template_id': tmpl.id,
|
||||||
|
'progress': tmpl.target,
|
||||||
|
'unlocked': True,
|
||||||
|
'unlock_date': fields.Datetime.now(),
|
||||||
|
})
|
||||||
|
# 给新玩家发一封欢迎邮件
|
||||||
|
request.env['game.player.mail'].sudo().create({
|
||||||
|
'employee_id': emp.id,
|
||||||
|
'category': 'system',
|
||||||
|
'subject': '欢迎来到宇森',
|
||||||
|
'sender': '系统',
|
||||||
|
'body': '<p>欢迎来到宇森!请查收新手礼包与世界探索指南。</p>',
|
||||||
|
})
|
||||||
|
request.env.cr.commit()
|
||||||
db = request.session.db or 'game'
|
db = request.session.db or 'game'
|
||||||
auth_info = request.session.authenticate(db, {'type': 'password', 'login': login, 'password': password})
|
auth_info = request.session.authenticate(db, {'type': 'password', 'login': login, 'password': password})
|
||||||
uid = auth_info['uid']
|
uid = auth_info['uid']
|
||||||
if uid:
|
if uid:
|
||||||
return _json({'ok': True, 'uid': uid, 'name': user.name, 'login': user.login})
|
return _json({'ok': True, 'uid': uid, 'name': user.name, 'login': user.login,
|
||||||
return _json({'ok': True, 'uid': user.id, 'name': user.name, 'login': user.login})
|
'emp_id': emp.id, 'game_uid': emp.game_uid})
|
||||||
|
return _json({'ok': True, 'uid': user.id, 'name': user.name, 'login': user.login,
|
||||||
|
'emp_id': emp.id, 'game_uid': emp.game_uid})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return _json({'ok': False, 'error': '注册失败:%s' % str(e)})
|
return _json({'ok': False, 'error': '注册失败:%s' % str(e)})
|
||||||
|
|
||||||
@ -324,9 +364,123 @@ class GameApiController(http.Controller):
|
|||||||
if uid:
|
if uid:
|
||||||
user = request.env['res.users'].sudo().browse(uid)
|
user = request.env['res.users'].sudo().browse(uid)
|
||||||
if user.exists():
|
if user.exists():
|
||||||
return _json({'uid': uid, 'name': user.name, 'login': user.login})
|
data = {'uid': uid, 'name': user.name, 'login': user.login,
|
||||||
|
'email': user.email or ''}
|
||||||
|
emp = request.env['hr.employee'].sudo().search(
|
||||||
|
[('user_id', '=', uid)], limit=1)
|
||||||
|
if emp:
|
||||||
|
data.update(emp.export_game_data())
|
||||||
|
return _json(data)
|
||||||
return _json({'uid': None})
|
return _json({'uid': None})
|
||||||
|
|
||||||
|
@http.route('/game/api/achievements', type='http', auth='public',
|
||||||
|
methods=['GET'], csrf=False)
|
||||||
|
def achievements(self, **kw):
|
||||||
|
"""返回当前登录玩家的成就列表(含模板+进度)。"""
|
||||||
|
uid = request.session.uid
|
||||||
|
if not uid:
|
||||||
|
return _json({'error': '未登录'})
|
||||||
|
emp = request.env['hr.employee'].sudo().search(
|
||||||
|
[('user_id', '=', uid)], limit=1)
|
||||||
|
if not emp:
|
||||||
|
return _json({'error': '玩家不存在', 'items': [], 'total_points': 0})
|
||||||
|
# 取所有启用的成就模板
|
||||||
|
tmpl_model = request.env['game.achievement.template'].sudo()
|
||||||
|
tier_labels = dict(tmpl_model._fields['tier'].selection)
|
||||||
|
templates = tmpl_model.search_read(
|
||||||
|
[('active', '=', True)],
|
||||||
|
['id', 'name', 'icon', 'tier', 'desc', 'points', 'target', 'sequence'],
|
||||||
|
order='sequence, id')
|
||||||
|
# 取该玩家已解锁的成就
|
||||||
|
player_recs = request.env['game.player.achievement'].sudo().search_read(
|
||||||
|
[('employee_id', '=', emp.id)],
|
||||||
|
['template_id', 'progress', 'unlocked', 'unlock_date'])
|
||||||
|
progress_map = {
|
||||||
|
r['template_id'][0]: r for r in player_recs
|
||||||
|
}
|
||||||
|
items = []
|
||||||
|
total_points = 0
|
||||||
|
unlocked_count = 0
|
||||||
|
for t in templates:
|
||||||
|
pr = progress_map.get(t['id'])
|
||||||
|
unlocked = pr and pr['unlocked']
|
||||||
|
progress = pr['progress'] if pr else 0
|
||||||
|
if unlocked:
|
||||||
|
total_points += t['points']
|
||||||
|
unlocked_count += 1
|
||||||
|
items.append({
|
||||||
|
'id': t['id'],
|
||||||
|
'name': t['name'],
|
||||||
|
'icon': t.get('icon') or '',
|
||||||
|
'tier': t['tier'],
|
||||||
|
'tier_label': tier_labels.get(t['tier'], t['tier']),
|
||||||
|
'desc': t.get('desc') or '',
|
||||||
|
'points': t['points'],
|
||||||
|
'target': t['target'],
|
||||||
|
'progress': progress,
|
||||||
|
'unlocked': bool(unlocked),
|
||||||
|
'unlock_date': str(pr['unlock_date']) if pr and pr['unlock_date'] else None,
|
||||||
|
})
|
||||||
|
return _json({
|
||||||
|
'items': items,
|
||||||
|
'total_points': total_points,
|
||||||
|
'unlocked_count': unlocked_count,
|
||||||
|
'total_count': len(items),
|
||||||
|
})
|
||||||
|
|
||||||
|
@http.route('/game/api/mails', type='http', auth='public',
|
||||||
|
methods=['GET'], csrf=False)
|
||||||
|
def mails(self, **kw):
|
||||||
|
"""返回当前登录玩家的邮件列表。"""
|
||||||
|
uid = request.session.uid
|
||||||
|
if not uid:
|
||||||
|
return _json({'error': '未登录', 'items': []})
|
||||||
|
emp = request.env['hr.employee'].sudo().search(
|
||||||
|
[('user_id', '=', uid)], limit=1)
|
||||||
|
if not emp:
|
||||||
|
return _json({'error': '玩家不存在', 'items': []})
|
||||||
|
cat_labels = dict(
|
||||||
|
request.env['game.player.mail'].sudo()._fields['category'].selection)
|
||||||
|
recs = request.env['game.player.mail'].sudo().search_read(
|
||||||
|
[('employee_id', '=', emp.id)],
|
||||||
|
['id', 'category', 'subject', 'body', 'date', 'is_read',
|
||||||
|
'is_pinned', 'sender'],
|
||||||
|
order='is_pinned desc, date desc')
|
||||||
|
items = [{
|
||||||
|
'id': r['id'],
|
||||||
|
'category': r['category'],
|
||||||
|
'category_label': cat_labels.get(r['category'], r['category']),
|
||||||
|
'subject': r['subject'],
|
||||||
|
'body': r.get('body') or '',
|
||||||
|
'date': str(r['date']) if r['date'] else '',
|
||||||
|
'is_read': r['is_read'],
|
||||||
|
'is_pinned': r['is_pinned'],
|
||||||
|
'sender': r.get('sender') or '',
|
||||||
|
} for r in recs]
|
||||||
|
unread = len([r for r in items if not r['is_read']])
|
||||||
|
return _json({'items': items, 'unread': unread})
|
||||||
|
|
||||||
|
@http.route('/game/api/mail/read', type='http', auth='public',
|
||||||
|
methods=['POST', 'OPTIONS'], csrf=False)
|
||||||
|
def mail_read(self, **kw):
|
||||||
|
"""标记邮件为已读。"""
|
||||||
|
uid = request.session.uid
|
||||||
|
if not uid:
|
||||||
|
return _json({'ok': False, 'error': '未登录'})
|
||||||
|
data = _post_body()
|
||||||
|
mail_id = data.get('mail_id')
|
||||||
|
if not mail_id:
|
||||||
|
return _json({'ok': False, 'error': '缺少 mail_id'})
|
||||||
|
emp = request.env['hr.employee'].sudo().search(
|
||||||
|
[('user_id', '=', uid)], limit=1)
|
||||||
|
if not emp:
|
||||||
|
return _json({'ok': False, 'error': '玩家不存在'})
|
||||||
|
mail = request.env['game.player.mail'].sudo().browse(mail_id)
|
||||||
|
if not mail.exists() or mail.employee_id != emp:
|
||||||
|
return _json({'ok': False, 'error': '邮件不存在或无权操作'})
|
||||||
|
mail.write({'is_read': True})
|
||||||
|
return _json({'ok': True})
|
||||||
|
|
||||||
@http.route('/game/api/logout', type='http', auth='public',
|
@http.route('/game/api/logout', type='http', auth='public',
|
||||||
methods=['POST', 'OPTIONS'], csrf=False)
|
methods=['POST', 'OPTIONS'], csrf=False)
|
||||||
def logout(self, **kw):
|
def logout(self, **kw):
|
||||||
|
|||||||
95
addons/game_base/data/game_achievement_data.xml
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
|
||||||
|
<!-- 成就模板初始数据 -->
|
||||||
|
<record model="game.achievement.template" id="ach_first_land">
|
||||||
|
<field name="code">first_land</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="name">开天辟地</field>
|
||||||
|
<field name="icon">star</field>
|
||||||
|
<field name="tier">gold</field>
|
||||||
|
<field name="points">50</field>
|
||||||
|
<field name="target">1</field>
|
||||||
|
<field name="desc">已点亮第一块领土,文明之火开始燃烧。</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record model="game.achievement.template" id="ach_civilization_cradle">
|
||||||
|
<field name="code">civilization_cradle</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="name">文明摇篮</field>
|
||||||
|
<field name="icon">sprout</field>
|
||||||
|
<field name="tier">silver</field>
|
||||||
|
<field name="points">30</field>
|
||||||
|
<field name="target">5</field>
|
||||||
|
<field name="desc">建立 5 个聚落,文明初步成形。</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record model="game.achievement.template" id="ach_sky_tower">
|
||||||
|
<field name="code">sky_tower</field>
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
<field name="name">通天塔</field>
|
||||||
|
<field name="icon">lock</field>
|
||||||
|
<field name="tier">platinum</field>
|
||||||
|
<field name="points">100</field>
|
||||||
|
<field name="target">10</field>
|
||||||
|
<field name="desc">建造 10 座奇观,文明疆域遍布穹苍。</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record model="game.achievement.template" id="ach_world_explorer">
|
||||||
|
<field name="code">world_explorer</field>
|
||||||
|
<field name="sequence">40</field>
|
||||||
|
<field name="name">世界探索者</field>
|
||||||
|
<field name="icon">compass</field>
|
||||||
|
<field name="tier">bronze</field>
|
||||||
|
<field name="points">10</field>
|
||||||
|
<field name="target">10</field>
|
||||||
|
<field name="desc">探索 10 块未知领土。</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record model="game.achievement.template" id="ach_resource_master">
|
||||||
|
<field name="code">resource_master</field>
|
||||||
|
<field name="sequence">50</field>
|
||||||
|
<field name="name">资源大亨</field>
|
||||||
|
<field name="icon">gem</field>
|
||||||
|
<field name="tier">silver</field>
|
||||||
|
<field name="points">30</field>
|
||||||
|
<field name="target">1000</field>
|
||||||
|
<field name="desc">累计获得 1000 金币。</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record model="game.achievement.template" id="ach_spirit_awakening">
|
||||||
|
<field name="code">spirit_awakening</field>
|
||||||
|
<field name="sequence">60</field>
|
||||||
|
<field name="name">灵气觉醒</field>
|
||||||
|
<field name="icon">sparkle</field>
|
||||||
|
<field name="tier">gold</field>
|
||||||
|
<field name="points">50</field>
|
||||||
|
<field name="target">1</field>
|
||||||
|
<field name="desc">世界阶段跃迁至「灵启」。</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record model="game.achievement.template" id="ach_mystic_realm">
|
||||||
|
<field name="code">mystic_realm</field>
|
||||||
|
<field name="sequence">70</field>
|
||||||
|
<field name="name">玄界之主</field>
|
||||||
|
<field name="icon">crown</field>
|
||||||
|
<field name="tier">legend</field>
|
||||||
|
<field name="points">200</field>
|
||||||
|
<field name="target">1</field>
|
||||||
|
<field name="desc">世界阶段跃迁至「玄界」。</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record model="game.achievement.template" id="ach_eternal">
|
||||||
|
<field name="code">eternal</field>
|
||||||
|
<field name="sequence">80</field>
|
||||||
|
<field name="name">永恒之锚</field>
|
||||||
|
<field name="icon">infinity</field>
|
||||||
|
<field name="tier">legend</field>
|
||||||
|
<field name="points">500</field>
|
||||||
|
<field name="target">1</field>
|
||||||
|
<field name="desc">世界阶段达到「溯元」,触及世界本源。</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
@ -6,3 +6,6 @@ from . import rule
|
|||||||
from . import page
|
from . import page
|
||||||
from . import story
|
from . import story
|
||||||
from . import ir_http
|
from . import ir_http
|
||||||
|
from . import game_employee
|
||||||
|
from . import game_achievement
|
||||||
|
from . import game_mail
|
||||||
|
|||||||
85
addons/game_base/models/game_achievement.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class GameAchievementTemplate(models.Model):
|
||||||
|
"""全局成就模板:后台定义,所有玩家共用同一套成就定义。"""
|
||||||
|
_name = 'game.achievement.template'
|
||||||
|
_description = '成就模板'
|
||||||
|
_order = 'sequence, id'
|
||||||
|
_rec_name = 'name'
|
||||||
|
|
||||||
|
TIER = [
|
||||||
|
('bronze', '铜'),
|
||||||
|
('silver', '银'),
|
||||||
|
('gold', '金'),
|
||||||
|
('platinum', '铂金'),
|
||||||
|
('legend', '传奇'),
|
||||||
|
]
|
||||||
|
|
||||||
|
sequence = fields.Integer('排序', default=10, index=True)
|
||||||
|
code = fields.Char('标识', index=True,
|
||||||
|
help='唯一标识,用于代码引用,如 first_land')
|
||||||
|
name = fields.Char('成就名称', required=True, translate=True)
|
||||||
|
icon = fields.Char('图标', help='emoji 或符号,如 star / sprout / lock')
|
||||||
|
tier = fields.Selection(TIER, '品质', default='bronze', index=True)
|
||||||
|
desc = fields.Text('描述', help='解锁条件/说明')
|
||||||
|
target = fields.Integer('目标值', default=1,
|
||||||
|
help='解锁所需进度值,如 10 表示需完成 10 次')
|
||||||
|
points = fields.Integer('成就点数', default=10)
|
||||||
|
active = fields.Boolean('启用', default=True)
|
||||||
|
player_achievement_ids = fields.One2many(
|
||||||
|
'game.player.achievement', 'template_id', '玩家解锁记录')
|
||||||
|
|
||||||
|
|
||||||
|
class GamePlayerAchievement(models.Model):
|
||||||
|
"""玩家已解锁/进行中的成就,关联到 hr.employee。"""
|
||||||
|
_name = 'game.player.achievement'
|
||||||
|
_description = '玩家成就'
|
||||||
|
_order = 'template_id, id'
|
||||||
|
_rec_name = 'name'
|
||||||
|
|
||||||
|
template_id = fields.Many2one(
|
||||||
|
'game.achievement.template', '成就模板',
|
||||||
|
required=True, ondelete='cascade', index=True)
|
||||||
|
employee_id = fields.Many2one(
|
||||||
|
'hr.employee', '所属玩家',
|
||||||
|
required=True, ondelete='cascade', index=True)
|
||||||
|
name = fields.Char('名称', related='template_id.name', store=True)
|
||||||
|
icon = fields.Char('图标', related='template_id.icon')
|
||||||
|
tier = fields.Selection(
|
||||||
|
related='template_id.tier', store=True)
|
||||||
|
desc = fields.Text('描述', related='template_id.desc')
|
||||||
|
points = fields.Integer('点数', related='template_id.points')
|
||||||
|
progress = fields.Integer('进度', default=0)
|
||||||
|
target = fields.Integer('目标值', related='template_id.target')
|
||||||
|
unlocked = fields.Boolean('已解锁', default=False)
|
||||||
|
unlock_date = fields.Datetime('解锁时间')
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _update_progress(self, employee_id, template_key, progress):
|
||||||
|
"""更新某玩家某成就的进度,达标自动解锁。"""
|
||||||
|
tmpl = self.env['game.achievement.template'].search(
|
||||||
|
[('id', '=', template_key)], limit=1)
|
||||||
|
if not tmpl:
|
||||||
|
return False
|
||||||
|
rec = self.search([
|
||||||
|
('employee_id', '=', employee_id),
|
||||||
|
('template_id', '=', tmpl.id),
|
||||||
|
], limit=1)
|
||||||
|
if not rec:
|
||||||
|
rec = self.create({
|
||||||
|
'employee_id': employee_id,
|
||||||
|
'template_id': tmpl.id,
|
||||||
|
'progress': min(progress, tmpl.target),
|
||||||
|
'unlocked': progress >= tmpl.target,
|
||||||
|
'unlock_date': fields.Datetime.now() if progress >= tmpl.target else False,
|
||||||
|
})
|
||||||
|
elif not rec.unlocked:
|
||||||
|
rec.write({
|
||||||
|
'progress': min(progress, tmpl.target),
|
||||||
|
'unlocked': progress >= tmpl.target,
|
||||||
|
})
|
||||||
|
if rec.unlocked:
|
||||||
|
rec.unlock_date = fields.Datetime.now()
|
||||||
|
return True
|
||||||
74
addons/game_base/models/game_employee.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class GameEmployee(models.Model):
|
||||||
|
"""在 hr.employee 上扩展游戏数据字段。"""
|
||||||
|
_inherit = 'hr.employee'
|
||||||
|
|
||||||
|
# === 等级系统 ===
|
||||||
|
game_level = fields.Integer('游戏等级', default=1)
|
||||||
|
game_exp = fields.Integer('经验值', default=0)
|
||||||
|
game_exp_max = fields.Integer('升级经验', default=100)
|
||||||
|
game_title = fields.Char('称号', default='萌新')
|
||||||
|
|
||||||
|
# === 资源系统 ===
|
||||||
|
game_coins = fields.Integer('金币', default=0)
|
||||||
|
game_energy = fields.Integer('灵气', default=0)
|
||||||
|
game_wood = fields.Integer('木材', default=0)
|
||||||
|
game_stone = fields.Integer('石料', default=0)
|
||||||
|
|
||||||
|
# === 世界数据 ===
|
||||||
|
game_world_name = fields.Char('世界名')
|
||||||
|
game_era = fields.Char('历元', default='星元1年·春')
|
||||||
|
game_tiles = fields.Integer('疆域地块', default=0)
|
||||||
|
game_population = fields.Integer('人口', default=0)
|
||||||
|
game_civ_level = fields.Integer('文明度', default=0)
|
||||||
|
|
||||||
|
# === 身份 ===
|
||||||
|
game_uid = fields.Char('游戏UID', index=True)
|
||||||
|
game_reg_date = fields.Date('注册日期', default=fields.Date.today)
|
||||||
|
|
||||||
|
# === 关联 ===
|
||||||
|
achievement_ids = fields.One2many('game.player.achievement', 'employee_id', '成就')
|
||||||
|
mail_ids = fields.One2many('game.player.mail', 'employee_id', '邮件')
|
||||||
|
game_achievement_count = fields.Integer(
|
||||||
|
'已解锁成就数', compute='_compute_achievement_count')
|
||||||
|
game_unread_mail = fields.Integer(
|
||||||
|
'未读邮件数', compute='_compute_unread_mail')
|
||||||
|
|
||||||
|
@api.depends('achievement_ids.unlocked')
|
||||||
|
def _compute_achievement_count(self):
|
||||||
|
for rec in self:
|
||||||
|
rec.game_achievement_count = len(
|
||||||
|
rec.achievement_ids.filtered('unlocked'))
|
||||||
|
|
||||||
|
@api.depends('mail_ids.is_read')
|
||||||
|
def _compute_unread_mail(self):
|
||||||
|
for rec in self:
|
||||||
|
rec.game_unread_mail = len(
|
||||||
|
rec.mail_ids.filtered(lambda m: not m.is_read))
|
||||||
|
|
||||||
|
def export_game_data(self):
|
||||||
|
"""将游戏数据导出为前端可用的字典。"""
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'emp_id': self.id,
|
||||||
|
'level': self.game_level,
|
||||||
|
'exp': self.game_exp,
|
||||||
|
'exp_max': self.game_exp_max,
|
||||||
|
'title': self.game_title,
|
||||||
|
'coins': self.game_coins,
|
||||||
|
'energy': self.game_energy,
|
||||||
|
'wood': self.game_wood,
|
||||||
|
'stone': self.game_stone,
|
||||||
|
'world_name': self.game_world_name or '',
|
||||||
|
'era': self.game_era or '',
|
||||||
|
'tiles': self.game_tiles,
|
||||||
|
'population': self.game_population,
|
||||||
|
'civ_level': self.game_civ_level,
|
||||||
|
'game_uid': self.game_uid or '',
|
||||||
|
'reg_date': str(self.game_reg_date) if self.game_reg_date else None,
|
||||||
|
'achievement_count': self.game_achievement_count,
|
||||||
|
'unread_mail': self.game_unread_mail,
|
||||||
|
}
|
||||||
28
addons/game_base/models/game_mail.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class GamePlayerMail(models.Model):
|
||||||
|
"""玩家邮件:后台可发送系统邮件给指定玩家(员工)。"""
|
||||||
|
_name = 'game.player.mail'
|
||||||
|
_description = '玩家邮件'
|
||||||
|
_order = 'date desc, id desc'
|
||||||
|
_rec_name = 'subject'
|
||||||
|
|
||||||
|
CATEGORY = [
|
||||||
|
('system', '系统'),
|
||||||
|
('activity', '活动'),
|
||||||
|
('reward', '奖励'),
|
||||||
|
('announcement', '公告'),
|
||||||
|
]
|
||||||
|
|
||||||
|
employee_id = fields.Many2one(
|
||||||
|
'hr.employee', '收件人',
|
||||||
|
required=True, ondelete='cascade', index=True)
|
||||||
|
category = fields.Selection(CATEGORY, '分类', default='system', index=True)
|
||||||
|
subject = fields.Char('标题', required=True)
|
||||||
|
body = fields.Html('正文')
|
||||||
|
date = fields.Datetime('发送时间', default=fields.Datetime.now)
|
||||||
|
is_read = fields.Boolean('已读', default=False)
|
||||||
|
is_pinned = fields.Boolean('置顶', default=False)
|
||||||
|
sender = fields.Char('发件人', default='系统')
|
||||||
@ -19,3 +19,9 @@ access_game_story_volume,game.story.volume public,model_game_story_volume,base.g
|
|||||||
access_game_story_volume_user,game.story.volume user,model_game_story_volume,base.group_user,1,1,1,1
|
access_game_story_volume_user,game.story.volume user,model_game_story_volume,base.group_user,1,1,1,1
|
||||||
access_game_story_chapter,game.story.chapter public,model_game_story_chapter,base.group_public,1,0,0,0
|
access_game_story_chapter,game.story.chapter public,model_game_story_chapter,base.group_public,1,0,0,0
|
||||||
access_game_story_chapter_user,game.story.chapter user,model_game_story_chapter,base.group_user,1,1,1,1
|
access_game_story_chapter_user,game.story.chapter user,model_game_story_chapter,base.group_user,1,1,1,1
|
||||||
|
access_game_achievement_template,game.achievement.template public,model_game_achievement_template,base.group_public,1,0,0,0
|
||||||
|
access_game_achievement_template_user,game.achievement.template user,model_game_achievement_template,base.group_user,1,1,1,1
|
||||||
|
access_game_player_achievement,game.player.achievement public,model_game_player_achievement,base.group_public,1,0,0,0
|
||||||
|
access_game_player_achievement_user,game.player.achievement user,model_game_player_achievement,base.group_user,1,1,1,1
|
||||||
|
access_game_player_mail,game.player.mail public,model_game_player_mail,base.group_public,1,0,0,0
|
||||||
|
access_game_player_mail_user,game.player.mail user,model_game_player_mail,base.group_user,1,1,1,1
|
||||||
|
|||||||
|
@ -28,7 +28,7 @@
|
|||||||
</group>
|
</group>
|
||||||
<field name="body_html"/>
|
<field name="body_html"/>
|
||||||
<field name="post_ids">
|
<field name="post_ids">
|
||||||
<tree><field name="author_name"/><field name="create_date"/></tree>
|
<list><field name="author_name"/><field name="create_date"/></list>
|
||||||
<form><field name="author_name"/><field name="body_html"/></form>
|
<form><field name="author_name"/><field name="body_html"/></form>
|
||||||
</field>
|
</field>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
81
addons/game_base/views/game_achievement_views.xml
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<!-- 成就模板列表 -->
|
||||||
|
<record model="ir.ui.view" id="view_game_achievement_template_tree">
|
||||||
|
<field name="name">game.achievement.template.tree</field>
|
||||||
|
<field name="model">game.achievement.template</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="sequence" widget="handle"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="icon"/>
|
||||||
|
<field name="tier"/>
|
||||||
|
<field name="points"/>
|
||||||
|
<field name="target"/>
|
||||||
|
<field name="active" widget="boolean_toggle"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 成就模板表单 -->
|
||||||
|
<record model="ir.ui.view" id="view_game_achievement_template_form">
|
||||||
|
<field name="name">game.achievement.template.form</field>
|
||||||
|
<field name="model">game.achievement.template</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="code"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="icon" placeholder="emoji 或符号"/>
|
||||||
|
<field name="tier"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="points"/>
|
||||||
|
<field name="target"/>
|
||||||
|
<field name="sequence"/>
|
||||||
|
<field name="active"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="desc" placeholder="解锁条件/说明"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 动作 -->
|
||||||
|
<record model="ir.actions.act_window" id="action_game_achievement_template">
|
||||||
|
<field name="name">成就模板</field>
|
||||||
|
<field name="res_model">game.achievement.template</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 玩家成就列表(只读查看) -->
|
||||||
|
<record model="ir.ui.view" id="view_game_player_achievement_tree">
|
||||||
|
<field name="name">game.player.achievement.tree</field>
|
||||||
|
<field name="model">game.player.achievement</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="tier"/>
|
||||||
|
<field name="points"/>
|
||||||
|
<field name="progress"/>
|
||||||
|
<field name="unlocked"/>
|
||||||
|
<field name="unlock_date"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record model="ir.actions.act_window" id="action_game_player_achievement">
|
||||||
|
<field name="name">玩家成就</field>
|
||||||
|
<field name="res_model">game.player.achievement</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
60
addons/game_base/views/game_employee_views.xml
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<!-- 继承 hr.employee 表单视图,添加「游戏数据」页签 -->
|
||||||
|
<record model="ir.ui.view" id="view_employee_game_form">
|
||||||
|
<field name="name">hr.employee.game.form</field>
|
||||||
|
<field name="model">hr.employee</field>
|
||||||
|
<field name="inherit_id" ref="hr.view_employee_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//notebook" position="inside">
|
||||||
|
<page string="游戏数据" name="game_data">
|
||||||
|
<group>
|
||||||
|
<group string="等级系统">
|
||||||
|
<field name="game_uid"/>
|
||||||
|
<field name="game_level"/>
|
||||||
|
<field name="game_exp"/>
|
||||||
|
<field name="game_exp_max"/>
|
||||||
|
<field name="game_title"/>
|
||||||
|
<field name="game_reg_date"/>
|
||||||
|
</group>
|
||||||
|
<group string="资源">
|
||||||
|
<field name="game_coins"/>
|
||||||
|
<field name="game_energy"/>
|
||||||
|
<field name="game_wood"/>
|
||||||
|
<field name="game_stone"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<group string="世界数据">
|
||||||
|
<field name="game_world_name"/>
|
||||||
|
<field name="game_era"/>
|
||||||
|
<field name="game_tiles"/>
|
||||||
|
<field name="game_population"/>
|
||||||
|
<field name="game_civ_level"/>
|
||||||
|
</group>
|
||||||
|
<group string="统计">
|
||||||
|
<field name="game_achievement_count"/>
|
||||||
|
<field name="game_unread_mail"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 员工列表:增加游戏 UID/等级/称号 列 -->
|
||||||
|
<record model="ir.ui.view" id="view_employee_game_tree">
|
||||||
|
<field name="name">hr.employee.game.tree</field>
|
||||||
|
<field name="model">hr.employee</field>
|
||||||
|
<field name="inherit_id" ref="hr.view_employee_tree"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='name']" position="after">
|
||||||
|
<field name="game_uid" string="游戏UID"/>
|
||||||
|
<field name="game_level" string="等级"/>
|
||||||
|
<field name="game_title" string="称号"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
56
addons/game_base/views/game_mail_views.xml
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<!-- 玩家邮件列表 -->
|
||||||
|
<record model="ir.ui.view" id="view_game_player_mail_tree">
|
||||||
|
<field name="name">game.player.mail.tree</field>
|
||||||
|
<field name="model">game.player.mail</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="is_pinned" widget="boolean_toggle"/>
|
||||||
|
<field name="subject"/>
|
||||||
|
<field name="employee_id" string="收件人"/>
|
||||||
|
<field name="category"/>
|
||||||
|
<field name="sender"/>
|
||||||
|
<field name="date"/>
|
||||||
|
<field name="is_read" widget="boolean_toggle"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 玩家邮件表单 -->
|
||||||
|
<record model="ir.ui.view" id="view_game_player_mail_form">
|
||||||
|
<field name="name">game.player.mail.form</field>
|
||||||
|
<field name="model">game.player.mail</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="employee_id" string="收件人"/>
|
||||||
|
<field name="category"/>
|
||||||
|
<field name="sender"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="date"/>
|
||||||
|
<field name="is_pinned"/>
|
||||||
|
<field name="is_read"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="subject"/>
|
||||||
|
<field name="body" widget="html"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- 动作 -->
|
||||||
|
<record model="ir.actions.act_window" id="action_game_player_mail">
|
||||||
|
<field name="name">玩家邮件</field>
|
||||||
|
<field name="res_model">game.player.mail</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
@ -26,6 +26,11 @@
|
|||||||
<!-- 公告 -->
|
<!-- 公告 -->
|
||||||
<menuitem id="menu_game_announcement" name="公告" parent="menu_game_root" action="action_game_announcement" sequence="40"/>
|
<menuitem id="menu_game_announcement" name="公告" parent="menu_game_root" action="action_game_announcement" sequence="40"/>
|
||||||
|
|
||||||
|
<!-- 玩家 -->
|
||||||
|
<menuitem id="menu_game_player" name="玩家" parent="menu_game_root" sequence="45"/>
|
||||||
|
<menuitem id="menu_game_player_achievement" name="玩家成就" parent="menu_game_player" action="action_game_player_achievement" sequence="46"/>
|
||||||
|
<menuitem id="menu_game_player_mail" name="玩家邮件" parent="menu_game_player" action="action_game_player_mail" sequence="47"/>
|
||||||
|
|
||||||
<!-- 小说(剧情) -->
|
<!-- 小说(剧情) -->
|
||||||
<menuitem id="menu_game_story" name="小说" parent="menu_game_root" sequence="35"/>
|
<menuitem id="menu_game_story" name="小说" parent="menu_game_root" sequence="35"/>
|
||||||
<menuitem id="menu_game_story_volume" name="卷" parent="menu_game_story" action="action_game_story_volume" sequence="36"/>
|
<menuitem id="menu_game_story_volume" name="卷" parent="menu_game_story" action="action_game_story_volume" sequence="36"/>
|
||||||
@ -37,5 +42,6 @@
|
|||||||
<field name="action" eval="False"/>
|
<field name="action" eval="False"/>
|
||||||
</record>
|
</record>
|
||||||
<menuitem id="menu_game_rule_category" name="规则类别" parent="menu_game_config" action="action_game_rule_category" sequence="51"/>
|
<menuitem id="menu_game_rule_category" name="规则类别" parent="menu_game_config" action="action_game_rule_category" sequence="51"/>
|
||||||
|
<menuitem id="menu_game_achievement_template" name="成就模板" parent="menu_game_config" action="action_game_achievement_template" sequence="52"/>
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
@ -59,6 +59,19 @@ const API = {
|
|||||||
method: 'POST', credentials: _cred,
|
method: 'POST', credentials: _cred,
|
||||||
})).json();
|
})).json();
|
||||||
},
|
},
|
||||||
|
async achievements() {
|
||||||
|
return (await fetch(_url('/game/api/achievements'), { credentials: _cred })).json();
|
||||||
|
},
|
||||||
|
async mails() {
|
||||||
|
return (await fetch(_url('/game/api/mails'), { credentials: _cred })).json();
|
||||||
|
},
|
||||||
|
async mailRead(mailId) {
|
||||||
|
return (await fetch(_url('/game/api/mail/read'), {
|
||||||
|
method: 'POST', credentials: _cred,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ mail_id: mailId }),
|
||||||
|
})).json();
|
||||||
|
},
|
||||||
async codex() {
|
async codex() {
|
||||||
return (await fetch(_url('/game/api/codex'), { credentials: _cred })).json();
|
return (await fetch(_url('/game/api/codex'), { credentials: _cred })).json();
|
||||||
},
|
},
|
||||||
@ -136,4 +149,6 @@ const API = {
|
|||||||
async codexRaces() {
|
async codexRaces() {
|
||||||
return (await fetch(_worldUrl('/yt_world/api/codex/races'), { credentials: _wcred })).json();
|
return (await fetch(_worldUrl('/yt_world/api/codex/races'), { credentials: _wcred })).json();
|
||||||
},
|
},
|
||||||
|
// 暴露后端 base,供图片等静态资源 URL 拼接(本地跨域→8069;线上同域→'')
|
||||||
|
base: API_CONFIG.base,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -53,6 +53,102 @@ async function loadRules(){
|
|||||||
} catch (e) { /* 保留静态兜底内容 */ }
|
} catch (e) { /* 保留静态兜底内容 */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 图鉴 ----------
|
||||||
|
async function loadCodex(){
|
||||||
|
const grid = document.getElementById('codexGrid');
|
||||||
|
const catsEl = document.getElementById('codexCats');
|
||||||
|
const subsEl = document.getElementById('codexSubs');
|
||||||
|
const searchEl= document.getElementById('codexSearch');
|
||||||
|
const countEl = document.getElementById('codexCount');
|
||||||
|
if (!grid) return;
|
||||||
|
|
||||||
|
let data;
|
||||||
|
try { data = await API.codex(); }
|
||||||
|
catch (e) { return; } // 失败时保留静态兜底内容
|
||||||
|
|
||||||
|
const groups = (data && data.groups) || [];
|
||||||
|
if (!groups.length){
|
||||||
|
grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;color:var(--text-dim);padding:60px 0">暂无图鉴数据</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选中状态
|
||||||
|
let curGroup = groups[0].key;
|
||||||
|
let curSub = 'all';
|
||||||
|
let curQ = '';
|
||||||
|
|
||||||
|
function renderCats(){
|
||||||
|
if (!catsEl) return;
|
||||||
|
catsEl.innerHTML = groups.map(g =>
|
||||||
|
`<button class="${g.key === curGroup ? 'active' : ''}" data-gk="${g.key}">${g.label}</button>`
|
||||||
|
).join('');
|
||||||
|
catsEl.querySelectorAll('button').forEach(b => b.addEventListener('click', () => {
|
||||||
|
curGroup = b.dataset.gk; curSub = 'all'; curQ = '';
|
||||||
|
if (searchEl) searchEl.value = '';
|
||||||
|
renderCats(); renderSubs(); renderGrid();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSubs(){
|
||||||
|
if (!subsEl) return;
|
||||||
|
const g = groups.find(x => x.key === curGroup);
|
||||||
|
const subs = (g && g.subs) || [];
|
||||||
|
subsEl.innerHTML = `<button class="${curSub === 'all' ? 'active' : ''}" data-sn="all">全部</button>` +
|
||||||
|
subs.map(s => `<button class="${curSub === s.name ? 'active' : ''}" data-sn="${s.name}">${s.name}</button>`).join('');
|
||||||
|
subsEl.querySelectorAll('button').forEach(b => b.addEventListener('click', () => {
|
||||||
|
curSub = b.dataset.sn; renderSubs(); renderGrid();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGrid(){
|
||||||
|
const g = groups.find(x => x.key === curGroup);
|
||||||
|
if (!g){ grid.innerHTML = ''; return; }
|
||||||
|
// 按子分类筛选
|
||||||
|
let items = [];
|
||||||
|
(g.subs || []).forEach(s => {
|
||||||
|
(s.items || []).forEach(it => {
|
||||||
|
if (curSub !== 'all' && it.sub !== curSub) return;
|
||||||
|
items.push(it);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// 搜索过滤
|
||||||
|
if (curQ){
|
||||||
|
const q = curQ.toLowerCase();
|
||||||
|
items = items.filter(it =>
|
||||||
|
(it.name || '').toLowerCase().includes(q) ||
|
||||||
|
(it.desc || '').replace(/<[^>]+>/g, '').toLowerCase().includes(q)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (countEl) countEl.textContent = items.length + ' 项';
|
||||||
|
if (!items.length){
|
||||||
|
grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;color:var(--text-dim);padding:60px 0">未找到匹配项</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const base = API.base || '';
|
||||||
|
grid.innerHTML = items.map(it => {
|
||||||
|
const thumb = it.image
|
||||||
|
? `<div class="thumb" style="background:url('${base}${it.image}') center/cover"></div>`
|
||||||
|
: `<div class="thumb"></div>`;
|
||||||
|
return `<div class="cx">
|
||||||
|
${thumb}
|
||||||
|
<div class="body">
|
||||||
|
<div class="nm">${it.name || ''}</div>
|
||||||
|
<div class="desc">${it.desc || ''}</div>
|
||||||
|
<div class="meta"><span class="sub">${it.sub || ''}</span><span class="tier">${it.tier || ''}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchEl){
|
||||||
|
searchEl.addEventListener('input', () => { curQ = searchEl.value.trim(); renderGrid(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCats();
|
||||||
|
renderSubs();
|
||||||
|
renderGrid();
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 论坛 ----------
|
// ---------- 论坛 ----------
|
||||||
async function loadForum(){
|
async function loadForum(){
|
||||||
const threadsEl = document.getElementById('threads');
|
const threadsEl = document.getElementById('threads');
|
||||||
@ -155,6 +251,7 @@ async function loadStory(){
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
loadPages();
|
loadPages();
|
||||||
loadRules();
|
loadRules();
|
||||||
|
loadCodex();
|
||||||
loadForum();
|
loadForum();
|
||||||
loadAnnounce();
|
loadAnnounce();
|
||||||
loadStory();
|
loadStory();
|
||||||
|
|||||||
BIN
assets/slides/01_starry_sky.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
assets/slides/02_sky.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
assets/slides/03_earth.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
assets/slides/04_underground.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
assets/slides/05_core.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
assets/slides/06_starry_sky_b.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
assets/slides/07_sky_b.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
assets/slides/08_earth_b.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
assets/slides/09_underground_b.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
assets/slides/10_core_b.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
assets/slides/login_bg.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
468
index.html
@ -15,6 +15,65 @@
|
|||||||
/* 首页特有:全屏 hero 轮播 */
|
/* 首页特有:全屏 hero 轮播 */
|
||||||
html,body{overflow:hidden;}
|
html,body{overflow:hidden;}
|
||||||
.content{flex:1;display:flex;flex-direction:column;position:relative;overflow:hidden;}
|
.content{flex:1;display:flex;flex-direction:column;position:relative;overflow:hidden;}
|
||||||
|
:root{
|
||||||
|
--bg-0:#080c1a; --bg-1:#0e1730; --bg-2:#15213f;
|
||||||
|
--violet:#8b5cf6; --magenta:#c77dff; --cyan:#4cc9f0; --gold:#e7b85c;
|
||||||
|
--text:#e9edf6; --text-dim:#9aa6c4;
|
||||||
|
--card:rgba(255,255,255,.045); --card-bd:rgba(255,255,255,.10);
|
||||||
|
--glow:0 0 22px rgba(139,92,246,.28);
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box;margin:0;padding:0;}
|
||||||
|
html,body{height:100%;width:100%;overflow:hidden;}
|
||||||
|
body{
|
||||||
|
font-family:"Noto Sans SC",system-ui,sans-serif;color:var(--text);
|
||||||
|
background:
|
||||||
|
radial-gradient(1000px 620px at 82% -8%, rgba(139,92,246,.14), transparent 60%),
|
||||||
|
radial-gradient(820px 560px at 8% 112%, rgba(76,201,240,.10), transparent 55%),
|
||||||
|
linear-gradient(160deg, var(--bg-0) 0%, var(--bg-1) 48%, var(--bg-2) 100%);
|
||||||
|
position:relative;
|
||||||
|
}
|
||||||
|
body::before,body::after{content:"";position:fixed;border-radius:50%;filter:blur(80px);z-index:0;pointer-events:none;}
|
||||||
|
body::before{width:380px;height:380px;background:rgba(139,92,246,.20);top:-110px;right:-70px;animation:fl1 20s ease-in-out infinite;}
|
||||||
|
body::after{width:340px;height:340px;background:rgba(76,201,240,.12);bottom:-110px;left:-60px;animation:fl2 24s ease-in-out infinite;}
|
||||||
|
@keyframes fl1{0%,100%{transform:translate(0,0)}50%{transform:translate(-36px,36px)}}
|
||||||
|
@keyframes fl2{0%,100%{transform:translate(0,0)}50%{transform:translate(36px,-28px)}}
|
||||||
|
|
||||||
|
.app{position:relative;z-index:2;height:100vh;display:flex;flex-direction:column;}
|
||||||
|
.topbar{height:64px;flex:0 0 auto;display:flex;align-items:center;gap:22px;padding:0 30px;border-bottom:1px solid var(--card-bd);background:rgba(8,12,26,.55);backdrop-filter:blur(10px);}
|
||||||
|
.logo{display:flex;align-items:baseline;gap:9px;cursor:pointer;}
|
||||||
|
.logo .cn{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:28px;letter-spacing:4px;color:#fff;text-shadow:0 0 16px rgba(139,92,246,.55);}
|
||||||
|
.logo .en{font-family:"Press Start 2P",monospace;font-size:9px;color:var(--magenta);letter-spacing:1px;}
|
||||||
|
.nav{display:flex;gap:6px;margin-left:12px;}
|
||||||
|
.nav button{font-family:"Noto Sans SC",sans-serif;font-size:15px;font-weight:500;color:var(--text-dim);background:transparent;border:1px solid transparent;padding:8px 15px;border-radius:10px;cursor:pointer;transition:.22s;}
|
||||||
|
.nav button:hover{color:var(--text);background:rgba(139,92,246,.12);}
|
||||||
|
.nav button.active{color:#fff;background:rgba(139,92,246,.22);border-color:var(--card-bd);box-shadow:var(--glow);}
|
||||||
|
.spacer{flex:1;}
|
||||||
|
.user-box{display:flex;align-items:center;gap:10px;font-size:14px;color:var(--text);}
|
||||||
|
.user-box .ub-name{color:#fff;font-weight:500;}
|
||||||
|
.user-box .ub-logout{font-family:inherit;font-size:12px;color:var(--text-dim);background:rgba(255,255,255,.06);border:1px solid var(--card-bd);padding:5px 12px;border-radius:8px;cursor:pointer;transition:.2s;}
|
||||||
|
.user-box .ub-logout:hover{color:#fff;background:rgba(139,92,246,.18);}
|
||||||
|
.user-box .auth-link{color:var(--text-dim);text-decoration:none;font-size:14px;transition:.2s;padding:5px 8px;}
|
||||||
|
.user-box .auth-link:hover{color:#fff;}
|
||||||
|
.user-box .auth-btn{font-family:inherit;font-size:13px;color:#0b1020;background:linear-gradient(120deg,var(--gold),#f0d49a);border:none;padding:6px 16px;border-radius:20px;cursor:pointer;font-weight:600;transition:.2s;}
|
||||||
|
.user-box .auth-btn:hover{filter:brightness(1.1);box-shadow:0 0 16px rgba(231,184,92,.4);}
|
||||||
|
.nav .auth-only{display:none;} /* 登录专属 tab,未登录时隐藏 */
|
||||||
|
.nav .auth-only.visible{display:inline-block;}
|
||||||
|
.cta:hover{filter:brightness(1.08);}
|
||||||
|
|
||||||
|
.content{flex:1;position:relative;overflow:hidden;}
|
||||||
|
.panel{position:absolute;inset:0;display:none;flex-direction:column;padding:28px 48px;overflow:hidden;}
|
||||||
|
.panel.active{display:flex;}
|
||||||
|
.panel-scroll{flex:1;min-height:0;overflow-y:auto;padding-right:8px;}
|
||||||
|
/* 隐藏内部滚动条但保留滚轮/触控滑动(规则/世界数据/公告/论坛/图鉴/剧情等) */
|
||||||
|
.rules-scroll,#worldDataContent,#announce .notes,.threads,.codex-grid,.story-reader,.panel-scroll,.toc-list{
|
||||||
|
scrollbar-width:none; -ms-overflow-style:none;
|
||||||
|
}
|
||||||
|
.rules-scroll::-webkit-scrollbar,#worldDataContent::-webkit-scrollbar,#announce .notes::-webkit-scrollbar,
|
||||||
|
.threads::-webkit-scrollbar,.codex-grid::-webkit-scrollbar,.story-reader::-webkit-scrollbar,
|
||||||
|
.panel-scroll::-webkit-scrollbar,.toc-list::-webkit-scrollbar{display:none;width:0;height:0;}
|
||||||
|
|
||||||
|
/* ===== 介绍(图片轮播主页) ===== */
|
||||||
|
#intro{padding:0;}
|
||||||
.hero{position:relative;flex:1;border-radius:0;overflow:hidden;}
|
.hero{position:relative;flex:1;border-radius:0;overflow:hidden;}
|
||||||
.hero .slideshow{position:absolute;inset:0;overflow:hidden;}
|
.hero .slideshow{position:absolute;inset:0;overflow:hidden;}
|
||||||
.hero .slide-track{position:absolute;inset:0;display:flex;flex-direction:column;transition:transform 1.25s cubic-bezier(0.4,0,0.2,1);will-change:transform;}
|
.hero .slide-track{position:absolute;inset:0;display:flex;flex-direction:column;transition:transform 1.25s cubic-bezier(0.4,0,0.2,1);will-change:transform;}
|
||||||
@ -28,6 +87,7 @@
|
|||||||
.hero .copy .tag{font-size:20px;font-weight:500;color:#e8ecf6;margin-top:4px;text-shadow:0 1px 10px rgba(0,0,0,.85);}
|
.hero .copy .tag{font-size:20px;font-weight:500;color:#e8ecf6;margin-top:4px;text-shadow:0 1px 10px rgba(0,0,0,.85);}
|
||||||
|
|
||||||
/* 轮播标签 */
|
/* 轮播标签 */
|
||||||
|
/* 轮播左下角特色文案 */
|
||||||
.slide-label{position:absolute;left:56px;bottom:72px;z-index:4;pointer-events:none;opacity:0;transform:translateY(16px);transition:opacity .8s ease, transform .8s ease;}
|
.slide-label{position:absolute;left:56px;bottom:72px;z-index:4;pointer-events:none;opacity:0;transform:translateY(16px);transition:opacity .8s ease, transform .8s ease;}
|
||||||
.slide.active .slide-label{opacity:1;transform:translateY(0);}
|
.slide.active .slide-label{opacity:1;transform:translateY(0);}
|
||||||
.slide-label .sl-en{font-family:"Press Start 2P",monospace;font-size:13px;color:var(--cyan);letter-spacing:3px;margin-bottom:10px;display:block;text-shadow:0 1px 8px rgba(0,0,0,.9);}
|
.slide-label .sl-en{font-family:"Press Start 2P",monospace;font-size:13px;color:var(--cyan);letter-spacing:3px;margin-bottom:10px;display:block;text-shadow:0 1px 8px rgba(0,0,0,.9);}
|
||||||
@ -35,6 +95,7 @@
|
|||||||
.slide-label .sl-desc{font-size:16px;color:#c8d2ec;margin-top:12px;max-width:500px;line-height:1.85;text-shadow:0 1px 8px rgba(0,0,0,.88);}
|
.slide-label .sl-desc{font-size:16px;color:#c8d2ec;margin-top:12px;max-width:500px;line-height:1.85;text-shadow:0 1px 8px rgba(0,0,0,.88);}
|
||||||
|
|
||||||
/* 指示器 */
|
/* 指示器 */
|
||||||
|
/* 轮播指示器(竖排右侧) */
|
||||||
.sl-dots{position:absolute;right:40px;bottom:96px;top:auto;transform:none;z-index:5;display:flex;flex-direction:column;gap:14px;pointer-events:auto;}
|
.sl-dots{position:absolute;right:40px;bottom:96px;top:auto;transform:none;z-index:5;display:flex;flex-direction:column;gap:14px;pointer-events:auto;}
|
||||||
.sl-dot{width:12px;height:12px;border-radius:50%;background:rgba(255,255,255,.22);border:1px solid rgba(255,255,255,.4);cursor:pointer;transition:.3s;flex:0 0 auto;}
|
.sl-dot{width:12px;height:12px;border-radius:50%;background:rgba(255,255,255,.22);border:1px solid rgba(255,255,255,.4);cursor:pointer;transition:.3s;flex:0 0 auto;}
|
||||||
.sl-dot:hover{background:rgba(255,255,255,.5);}
|
.sl-dot:hover{background:rgba(255,255,255,.5);}
|
||||||
@ -47,6 +108,7 @@
|
|||||||
.sl-arrow.next{right:16px;}
|
.sl-arrow.next{right:16px;}
|
||||||
|
|
||||||
/* 起源文案 */
|
/* 起源文案 */
|
||||||
|
/* 轮播右上角竖排「起源历史」 */
|
||||||
.slide-origin{position:absolute;right:72px;top:48px;z-index:4;pointer-events:none;writing-mode:vertical-rl;text-orientation:upright;
|
.slide-origin{position:absolute;right:72px;top:48px;z-index:4;pointer-events:none;writing-mode:vertical-rl;text-orientation:upright;
|
||||||
font-family:"Noto Sans SC",sans-serif;font-size:16px;font-weight:500;letter-spacing:3px;line-height:1.55;color:rgba(240,244,252,.94);
|
font-family:"Noto Sans SC",sans-serif;font-size:16px;font-weight:500;letter-spacing:3px;line-height:1.55;color:rgba(240,244,252,.94);
|
||||||
text-shadow:0 1px 3px rgba(0,0,0,.92);max-height:28vh;overflow:hidden;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;
|
text-shadow:0 1px 3px rgba(0,0,0,.92);max-height:28vh;overflow:hidden;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;
|
||||||
@ -59,11 +121,244 @@
|
|||||||
.hero .enter-btn{font-family:"Noto Sans SC",sans-serif;font-size:17px;font-weight:700;color:#0b1020;background:linear-gradient(120deg,var(--gold),#f0d49a);padding:13px 42px;border-radius:30px;cursor:pointer;box-shadow:0 0 26px rgba(231,184,92,.5);letter-spacing:3px;transition:.25s;border:none;}
|
.hero .enter-btn{font-family:"Noto Sans SC",sans-serif;font-size:17px;font-weight:700;color:#0b1020;background:linear-gradient(120deg,var(--gold),#f0d49a);padding:13px 42px;border-radius:30px;cursor:pointer;box-shadow:0 0 26px rgba(231,184,92,.5);letter-spacing:3px;transition:.25s;border:none;}
|
||||||
.hero .enter-btn:hover{filter:brightness(1.1);transform:translateY(-2px);box-shadow:0 0 40px rgba(231,184,92,.75);}
|
.hero .enter-btn:hover{filter:brightness(1.1);transform:translateY(-2px);box-shadow:0 0 40px rgba(231,184,92,.75);}
|
||||||
|
|
||||||
|
/* ===== 特色 ===== */
|
||||||
|
#features{align-items:center;justify-content:center;gap:22px;}
|
||||||
|
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;width:min(1060px,100%);}
|
||||||
|
.card{background:var(--card);border:1px solid var(--card-bd);border-radius:14px;padding:18px 20px;transition:.26s;min-height:132px;}
|
||||||
|
.card:hover{transform:translateY(-4px);border-color:var(--violet);box-shadow:var(--glow);background:rgba(139,92,246,.12);}
|
||||||
|
.card .ic{font-size:28px;margin-bottom:8px;filter:drop-shadow(0 0 6px rgba(199,125,255,.6));}
|
||||||
|
.card h3{font-size:17px;color:#fff;margin-bottom:7px;letter-spacing:1px;}
|
||||||
|
.card p{font-size:13px;color:var(--text-dim);line-height:1.6;}
|
||||||
|
|
||||||
|
/* ===== 背景 ===== */
|
||||||
|
#background{flex-direction:column;align-items:stretch;gap:0;overflow:hidden;padding:28px 48px;}
|
||||||
|
.bg-page{max-width:1080px;width:100%;margin:0 auto;padding:24px 0;border-top:1px solid var(--card-bd);}
|
||||||
|
.bg-page:first-child{border-top:none;padding-top:0;}
|
||||||
|
.bg-page-head{text-align:center;margin-bottom:28px;}
|
||||||
|
.bg-page-title{font-family:"Noto Serif SC",serif;font-weight:800;font-size:40px;letter-spacing:3px;line-height:1.2;margin:0 0 10px;
|
||||||
|
background:linear-gradient(120deg,#ffffff 0%,#f0d49a 48%,#c79bff 100%);
|
||||||
|
-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;}
|
||||||
|
.bg-page-sub{font-size:13px;letter-spacing:7px;color:var(--text-dim);text-transform:uppercase;}
|
||||||
|
.bg-page-body{font-size:17px;line-height:2.05;color:#e2e8f6;max-width:840px;margin:0 auto 34px;}
|
||||||
|
.bg-page-body p{margin:0 0 18px;}
|
||||||
|
.bg-page-items{display:grid;grid-template-columns:repeat(auto-fill,minmax(262px,1fr));gap:18px;}
|
||||||
|
.bg-card{display:flex;gap:15px;align-items:flex-start;background:linear-gradient(135deg,rgba(139,92,246,.10),rgba(76,201,240,.05));
|
||||||
|
border:1px solid var(--card-bd);border-radius:16px;padding:18px 20px;transition:.26s;}
|
||||||
|
.bg-card:hover{transform:translateY(-4px);border-color:var(--violet);box-shadow:var(--glow);background:rgba(139,92,246,.16);}
|
||||||
|
.bg-card-ic{font-size:27px;line-height:1.15;filter:drop-shadow(0 0 9px rgba(139,92,246,.55));flex:0 0 auto;}
|
||||||
|
.bg-card-title{font-size:17px;font-weight:700;color:#fff;margin-bottom:6px;}
|
||||||
|
.bg-card-desc{font-size:14.5px;line-height:1.66;color:var(--text-dim);}
|
||||||
|
.bg-empty{text-align:center;color:var(--text-dim);padding:70px 0;font-size:16px;}
|
||||||
|
.layers{flex:0 0 270px;display:flex;flex-direction:column;gap:8px;justify-content:center;}
|
||||||
|
.layer{border-radius:12px;padding:11px 15px;border:1px solid var(--card-bd);position:relative;}
|
||||||
|
.layer .lt{font-size:15px;color:#fff;font-weight:500;letter-spacing:1px;}
|
||||||
|
.layer .ld{font-size:12px;color:var(--text-dim);margin-top:4px;line-height:1.5;}
|
||||||
|
.layer.l1{background:linear-gradient(100deg,rgba(76,201,240,.18),rgba(139,92,246,.08));}
|
||||||
|
.layer.l2{background:linear-gradient(100deg,rgba(199,125,255,.18),rgba(139,92,246,.07));}
|
||||||
|
.layer.l3{background:linear-gradient(100deg,rgba(139,92,246,.24),rgba(76,201,240,.06));border-color:var(--violet);}
|
||||||
|
.layer.l4{background:linear-gradient(100deg,rgba(139,92,246,.14),rgba(8,12,26,.18));}
|
||||||
|
.layer.l5{background:linear-gradient(100deg,rgba(231,184,92,.14),rgba(8,12,26,.26));}
|
||||||
|
.layer .star{position:absolute;right:12px;top:9px;font-size:11px;color:var(--cyan);}
|
||||||
|
.bg-text{flex:1;display:flex;flex-direction:column;justify-content:center;overflow-y:auto;}
|
||||||
|
.bg-text h2{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:30px;letter-spacing:3px;color:#fff;text-shadow:0 0 16px rgba(139,92,246,.5);margin-bottom:16px;}
|
||||||
|
.bg-text p{font-size:14.5px;color:var(--text-dim);line-height:1.95;max-width:640px;}
|
||||||
|
|
||||||
|
/* ===== 规则 ===== */
|
||||||
|
#rules{flex-direction:column;gap:0;}
|
||||||
|
.rules-scroll{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:22px;padding-right:8px;}
|
||||||
|
.rule-block h3{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:22px;color:#fff;letter-spacing:2px;margin-bottom:6px;}
|
||||||
|
.rule-intro{font-size:13.5px;color:var(--text-dim);margin-bottom:12px;line-height:1.7;max-width:780px;}
|
||||||
|
.tier-list{display:flex;flex-direction:column;gap:8px;}
|
||||||
|
.tier-row{display:grid;grid-template-columns:96px 116px 1fr;gap:12px;align-items:center;background:var(--card);border:1px solid var(--card-bd);border-radius:10px;padding:10px 14px;transition:.2s;}
|
||||||
|
.tier-row:hover{border-color:var(--violet);background:rgba(139,92,246,.10);}
|
||||||
|
.tier-row .tn{font-family:"Press Start 2P",monospace;font-size:10px;color:var(--cyan);}
|
||||||
|
.tier-row .tname{font-size:15px;color:#fff;font-weight:500;}
|
||||||
|
.tier-row .tdesc{font-size:13px;color:var(--text-dim);line-height:1.5;}
|
||||||
|
|
||||||
|
/* ===== 图鉴 ===== */
|
||||||
|
#codex{padding:28px 48px;gap:0;}
|
||||||
|
.codex-wrap{display:flex;flex-direction:column;gap:13px;flex:1;min-height:0;}
|
||||||
|
.codex-cats{display:flex;gap:6px;flex-wrap:wrap;}
|
||||||
|
.codex-cats button{font-family:"Noto Sans SC",sans-serif;font-size:14px;color:var(--text-dim);background:var(--card);border:1px solid var(--card-bd);padding:8px 15px;border-radius:11px;cursor:pointer;transition:.2s;}
|
||||||
|
.codex-cats button:hover{color:#fff;}
|
||||||
|
.codex-cats button.active{color:#fff;background:rgba(139,92,246,.24);border-color:var(--magenta);box-shadow:var(--glow);}
|
||||||
|
.codex-subrow{display:flex;align-items:center;gap:12px;}
|
||||||
|
.codex-subs{display:flex;gap:6px;flex-wrap:wrap;flex:1;}
|
||||||
|
.codex-subs button{font-size:12px;color:var(--text-dim);background:var(--card);border:1px solid var(--card-bd);padding:5px 12px;border-radius:14px;cursor:pointer;transition:.2s;}
|
||||||
|
.codex-subs button:hover{color:#fff;}
|
||||||
|
.codex-subs button.active{color:#fff;background:rgba(76,201,240,.22);border-color:var(--cyan);}
|
||||||
|
.codex-search{background:rgba(8,12,26,.6);border:1px solid var(--card-bd);border-radius:18px;color:var(--text);padding:7px 14px;font-size:13px;outline:none;width:200px;flex:0 0 auto;}
|
||||||
|
.codex-search:focus{border-color:var(--violet);}
|
||||||
|
.codex-count{font-size:12px;color:var(--text-dim);white-space:nowrap;flex:0 0 auto;}
|
||||||
|
.codex-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;overflow-y:auto;padding:4px 6px 4px 0;align-content:start;flex:1;min-height:0;}
|
||||||
|
.cx{background:var(--card);border:1px solid var(--card-bd);border-radius:13px;overflow:hidden;transition:.24s;display:flex;flex-direction:column;cursor:default;}
|
||||||
|
.cx:hover{transform:translateY(-3px);border-color:var(--violet);box-shadow:var(--glow);}
|
||||||
|
.cx .thumb{width:100%;aspect-ratio:1/1;display:block;overflow:hidden;background:linear-gradient(135deg,rgba(139,92,246,.22),rgba(76,201,240,.10));}
|
||||||
|
.cx .thumb svg{width:100%;height:100%;display:block;}
|
||||||
|
.cx .body{padding:10px 12px;}
|
||||||
|
.cx .nm{font-size:14.5px;color:#fff;font-weight:500;}
|
||||||
|
.cx .meta{display:flex;justify-content:space-between;margin-top:5px;font-size:11px;}
|
||||||
|
.cx .meta .sub{color:var(--cyan);}
|
||||||
|
.cx .meta .tier{color:var(--gold);}
|
||||||
|
.cx .desc{font-size:12px;color:var(--text-dim);line-height:1.5;margin-top:4px;max-height:60px;overflow:hidden;}
|
||||||
|
.cx .ds{font-size:11.5px;color:var(--text-dim);line-height:1.5;margin-top:6px;}
|
||||||
|
|
||||||
|
/* ===== 论坛 ===== */
|
||||||
|
#forum{flex-direction:row;gap:28px;}
|
||||||
|
.f-cats{flex:0 0 200px;display:flex;flex-direction:column;gap:12px;}
|
||||||
|
.f-cats .ch{font-size:15.5px;color:var(--text-dim);background:var(--card);border:1px solid var(--card-bd);padding:13px 18px;border-radius:12px;cursor:pointer;transition:.22s;letter-spacing:.5px;}
|
||||||
|
.f-cats .ch:hover{color:#fff;background:rgba(139,92,246,.12);}
|
||||||
|
.f-cats .ch.active{color:#fff;background:linear-gradient(120deg,rgba(139,92,246,.28),rgba(199,125,255,.16));border-color:var(--magenta);box-shadow:var(--glow);}
|
||||||
|
.f-main{flex:1;display:flex;flex-direction:column;min-width:0;}
|
||||||
|
.threads{flex:1;overflow-y:auto;display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:16px;padding-right:8px;align-content:start;}
|
||||||
|
.th{display:flex;align-items:center;gap:16px;background:var(--card);border:1px solid var(--card-bd);border-radius:14px;padding:16px 20px;transition:.22s;cursor:pointer;}
|
||||||
|
.th:hover{border-color:var(--violet);background:rgba(139,92,246,.12);transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,.25);}
|
||||||
|
.th .pin{font-size:12px;font-weight:700;letter-spacing:1px;color:var(--gold);background:rgba(231,184,92,.16);border:1px solid rgba(231,184,92,.45);border-radius:6px;padding:2px 8px;}
|
||||||
|
.th .tt{flex:1;font-size:16.5px;color:#fff;letter-spacing:.3px;}
|
||||||
|
.th .meta{font-size:13px;color:var(--text-dim);white-space:nowrap;}
|
||||||
|
.th .rp{color:var(--cyan);font-weight:600;}
|
||||||
|
.post-form{display:none;flex-direction:column;gap:10px;background:var(--card);border:1px solid var(--magenta);border-radius:14px;padding:16px;margin-bottom:14px;}
|
||||||
|
.post-form.show{display:flex;}
|
||||||
|
.post-form input,.post-form textarea{background:rgba(8,12,26,.6);border:1px solid var(--card-bd);border-radius:10px;color:var(--text);padding:10px 14px;font-family:inherit;font-size:14px;outline:none;}
|
||||||
|
.post-form textarea{resize:vertical;min-height:72px;}
|
||||||
|
.post-form .row{display:flex;gap:10px;justify-content:flex-end;}
|
||||||
|
.post-form .row button{font-size:14px;padding:8px 18px;border-radius:10px;cursor:pointer;border:1px solid var(--card-bd);background:var(--card);color:var(--text);}
|
||||||
|
.post-form .row .ok{background:linear-gradient(120deg,var(--gold),#f0d49a);color:#0b1020;font-weight:700;border:none;}
|
||||||
|
|
||||||
|
/* ===== 公告 ===== */
|
||||||
|
#announce{gap:0;align-items:stretch;}
|
||||||
|
#announce .notes{flex:1;min-height:0;width:100%;display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:18px;overflow-y:auto;padding-right:8px;align-content:start;}
|
||||||
|
#announce .notes{grid-template-columns:repeat(auto-fill,minmax(380px,1fr));gap:20px;}
|
||||||
|
.note{display:flex;flex-direction:column;overflow:hidden;background:linear-gradient(135deg,rgba(139,92,246,.10),rgba(76,201,240,.04));border:1px solid var(--card-bd);border-left:4px solid var(--magenta);border-radius:16px;transition:.24s;}
|
||||||
|
.note:hover{transform:translateY(-3px);border-color:var(--violet);background:linear-gradient(135deg,rgba(139,92,246,.16),rgba(76,201,240,.07));box-shadow:var(--glow);}
|
||||||
|
.note.pin{border-left-color:var(--gold);background:linear-gradient(135deg,rgba(231,184,92,.12),rgba(139,92,246,.06));}
|
||||||
|
.note.pin:hover{background:linear-gradient(135deg,rgba(231,184,92,.18),rgba(139,92,246,.08));}
|
||||||
|
.note.feat{grid-column:1 / -1;flex-direction:row;align-items:stretch;}
|
||||||
|
.note.feat .note-cover{flex:0 0 44%;width:44%;min-height:260px;}
|
||||||
|
.note.feat .note-body{flex:1;justify-content:center;}
|
||||||
|
.note.feat .note-title{font-size:27px;}
|
||||||
|
.note.feat .body{font-size:16px;}
|
||||||
|
.note-cover{height:168px;flex:0 0 auto;background-size:cover;background-position:center;background-repeat:no-repeat;border-bottom:1px solid var(--card-bd);position:relative;}
|
||||||
|
.note-cover.empty{background:linear-gradient(135deg,rgba(139,92,246,.24),rgba(76,201,240,.10));}
|
||||||
|
.note-cover.empty::after{content:'封面图 · 后台配置';position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:12px;letter-spacing:2px;color:rgba(255,255,255,.5);}
|
||||||
|
.note-body{padding:18px 22px;display:flex;flex-direction:column;min-width:0;}
|
||||||
|
.note .date{font-size:13px;font-weight:600;color:var(--cyan);letter-spacing:1px;margin-bottom:8px;}
|
||||||
|
.note-title{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:21px;color:#fff;letter-spacing:1px;margin-bottom:10px;line-height:1.32;text-shadow:0 0 14px rgba(139,92,246,.4);}
|
||||||
|
.note .body{font-size:15px;color:var(--text);line-height:1.9;}
|
||||||
|
.note .body p{margin:0 0 11px;}
|
||||||
|
.note .body p:last-child{margin-bottom:0;}
|
||||||
|
|
||||||
|
/* ===== 致玩家 ===== */
|
||||||
|
#letter{align-items:center;justify-content:center;}
|
||||||
|
#letter .letter-wrap{max-width:680px;width:100%;overflow-y:auto;padding:10px 20px;}
|
||||||
|
#letter .letter-wrap h2{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:30px;letter-spacing:4px;color:#fff;text-shadow:0 0 18px rgba(139,92,246,.5);margin-bottom:6px;text-align:center;}
|
||||||
|
#letter .letter-wrap .lt-sub{text-align:center;font-family:"Press Start 2P",monospace;font-size:9px;color:var(--magenta);letter-spacing:1px;margin-bottom:28px;}
|
||||||
|
#letter .letter-wrap .lt-body{font-size:15px;color:var(--text-dim);line-height:2.1;}
|
||||||
|
#letter .letter-wrap .lt-body p{margin-bottom:16px;}
|
||||||
|
#letter .letter-wrap .lt-body p:first-child{font-size:17px;color:#fff;font-weight:500;}
|
||||||
|
#letter .letter-wrap .lt-body .hl{color:var(--gold);font-weight:500;}
|
||||||
|
#letter .letter-wrap .lt-sign{text-align:right;margin-top:28px;font-size:14px;color:var(--magenta);font-style:italic;}
|
||||||
|
|
||||||
|
/* ===== 论坛(竖排:顶部分类条 + 下方纵向帖子流) ===== */
|
||||||
|
#forum{flex-direction:column;gap:20px;}
|
||||||
|
.f-cats{flex:0 0 auto;flex-direction:row;flex-wrap:wrap;gap:10px;}
|
||||||
|
.f-cats .ch{flex:0 0 auto;}
|
||||||
|
.f-main{flex:1;min-height:0;}
|
||||||
|
.threads{width:100%;}
|
||||||
|
|
||||||
|
/* ===== 公告(内容居中) ===== */
|
||||||
|
#announce .notes{margin:0;}
|
||||||
|
|
||||||
|
/* ===== 剧情(小说阅读器:左目录 + 右正文,竖排阅读感) ===== */
|
||||||
|
#story{padding:28px 48px;}
|
||||||
|
.story-wrap{display:flex;flex-direction:row;width:100%;flex:1;min-height:0;}
|
||||||
|
.story-toc{flex:0 0 286px;display:flex;flex-direction:column;border-right:1px solid var(--card-bd);background:rgba(8,12,26,.35);min-height:0;}
|
||||||
|
.toc-head{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:24px;letter-spacing:3px;color:#fff;padding:24px 0 12px;text-shadow:0 0 16px rgba(139,92,246,.5);}
|
||||||
|
.toc-head small{display:block;font-family:"Press Start 2P",monospace;font-size:8px;color:var(--magenta);letter-spacing:1px;margin-top:6px;}
|
||||||
|
.toc-list{flex:1;overflow-y:auto;padding:0 12px 24px;}
|
||||||
|
.toc-loading{padding:30px 16px;color:var(--text-dim);font-size:14px;}
|
||||||
|
.toc-vol{margin-top:10px;}
|
||||||
|
.toc-vol .vname{font-size:14px;color:var(--gold);letter-spacing:1px;padding:8px 10px 4px;font-weight:500;}
|
||||||
|
.toc-vol .vintro{font-size:12px;color:var(--text-dim);line-height:1.6;padding:0 10px 8px;}
|
||||||
|
.toc-ch{display:block;width:100%;text-align:left;font-family:inherit;font-size:15px;color:var(--text-dim);background:transparent;border:none;border-left:2px solid transparent;padding:10px 12px;cursor:pointer;transition:.18s;border-radius:0 8px 8px 0;line-height:1.45;}
|
||||||
|
.toc-ch:hover{color:#fff;background:rgba(139,92,246,.12);}
|
||||||
|
.toc-ch.active{color:#fff;background:rgba(139,92,246,.20);border-left-color:var(--magenta);}
|
||||||
|
.story-reader{flex:1;overflow-y:auto;min-width:0;background:linear-gradient(180deg, rgba(139,92,246,.04), transparent 240px);}
|
||||||
|
.reader-loading{padding:80px 20px;text-align:center;color:var(--text-dim);font-size:15px;}
|
||||||
|
.reader-inner{max-width:840px;margin:0 auto;padding:48px 52px 100px;}
|
||||||
|
.reader-inner .r-vol{font-size:12px;color:var(--cyan);letter-spacing:2px;margin-bottom:10px;}
|
||||||
|
.reader-inner h1{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:32px;letter-spacing:2px;color:#fff;text-shadow:0 0 16px rgba(139,92,246,.5);margin-bottom:18px;line-height:1.3;}
|
||||||
|
.reader-inner .body{font-size:17.5px;color:#eef2fb;line-height:2.0;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;}
|
||||||
|
.reader-inner .body p{margin-bottom:20px;text-indent:2em;}
|
||||||
|
.reader-inner .body p:first-child{text-indent:0;}
|
||||||
|
.reader-inner .body h2,.reader-inner .body h3{color:#fff;margin:22px 0 10px;font-size:18px;}
|
||||||
|
|
||||||
|
/* ===== 世界数据报表 ===== */
|
||||||
|
#worlddata{overflow:hidden;padding:28px 48px;}
|
||||||
|
#worldDataContent{flex:1;min-height:0;overflow-y:auto;width:100%;padding-right:8px;}
|
||||||
|
.wd-tag{display:inline-block;font-size:12px;letter-spacing:2px;color:var(--gold);background:rgba(231,184,92,.12);border:1px solid rgba(231,184,92,.38);border-radius:6px;padding:3px 11px;margin-bottom:22px;}
|
||||||
|
.wd-h{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:22px;letter-spacing:3px;color:#fff;margin:38px 0 18px;text-shadow:0 0 14px rgba(139,92,246,.45);display:flex;align-items:center;gap:10px;}
|
||||||
|
.wd-h::before{content:"";width:4px;height:22px;border-radius:3px;background:linear-gradient(180deg,var(--violet),var(--cyan));box-shadow:0 0 10px rgba(139,92,246,.6);}
|
||||||
|
.wd-h small{font-family:"Press Start 2P",monospace;font-size:8px;color:var(--magenta);letter-spacing:1px;}
|
||||||
|
|
||||||
|
.wd-kpi{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:20px;}
|
||||||
|
.wd-kpi-card{position:relative;overflow:hidden;background:linear-gradient(135deg,rgba(139,92,246,.12),rgba(76,201,240,.05));border:1px solid var(--card-bd);border-radius:16px;padding:22px 26px;transition:.26s;}
|
||||||
|
.wd-kpi-card:hover{transform:translateY(-4px);border-color:var(--violet);box-shadow:var(--glow);}
|
||||||
|
.wd-kpi-card::after{content:"";position:absolute;right:-26px;top:-26px;width:88px;height:88px;border-radius:50%;background:radial-gradient(circle,rgba(199,125,255,.22),transparent 70%);}
|
||||||
|
.wd-kpi-ic{font-size:22px;filter:drop-shadow(0 0 7px rgba(139,92,246,.55));}
|
||||||
|
.wd-kpi-num{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:40px;line-height:1.1;color:#fff;margin:10px 0 4px;}
|
||||||
|
.wd-kpi-num .u{font-size:14px;color:var(--text-dim);margin-left:4px;letter-spacing:1px;}
|
||||||
|
.wd-kpi-label{font-size:13px;color:var(--text-dim);letter-spacing:1px;}
|
||||||
|
.wd-kpi-delta{position:absolute;right:14px;top:16px;font-size:11px;font-weight:700;padding:1px 7px;border-radius:5px;}
|
||||||
|
.wd-up{color:#7CFC9B;background:rgba(124,252,155,.12);}
|
||||||
|
.wd-down{color:#ff8585;background:rgba(255,133,133,.12);}
|
||||||
|
|
||||||
|
.wd-bars{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:14px 26px;}
|
||||||
|
.wd-bar-row{margin-bottom:4px;}
|
||||||
|
.wd-bar-top{display:flex;justify-content:space-between;font-size:13.5px;margin-bottom:6px;}
|
||||||
|
.wd-bar-top .k{color:var(--text);}
|
||||||
|
.wd-bar-top .v{color:var(--gold);font-weight:700;}
|
||||||
|
.wd-track{height:9px;border-radius:6px;background:rgba(255,255,255,.07);overflow:hidden;}
|
||||||
|
.wd-fill{height:100%;border-radius:6px;background:linear-gradient(90deg,var(--cyan),var(--violet));box-shadow:0 0 9px rgba(139,92,246,.55);}
|
||||||
|
|
||||||
|
.wd-table{width:100%;border-collapse:separate;border-spacing:0;font-size:14px;background:rgba(255,255,255,.02);border:1px solid var(--card-bd);border-radius:14px;overflow:hidden;}
|
||||||
|
.wd-table th{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:14px;letter-spacing:2px;color:#fff;background:linear-gradient(135deg,rgba(139,92,246,.22),rgba(76,201,240,.12));text-align:left;padding:13px 16px;border-bottom:1px solid var(--card-bd);}
|
||||||
|
.wd-table td{padding:12px 16px;border-bottom:1px solid rgba(255,255,255,.06);color:var(--text-dim);line-height:1.5;}
|
||||||
|
.wd-table tr:last-child td{border-bottom:none;}
|
||||||
|
.wd-table tr:hover td{background:rgba(139,92,246,.08);color:#eaf0ff;}
|
||||||
|
.wd-table .lead{color:#fff;font-weight:600;}
|
||||||
|
.wd-table .pill{display:inline-block;font-size:12px;padding:2px 9px;border-radius:20px;background:rgba(76,201,240,.14);color:var(--cyan);border:1px solid rgba(76,201,240,.34);}
|
||||||
|
|
||||||
|
.wd-rank{display:flex;flex-direction:column;gap:12px;}
|
||||||
|
.wd-rank-row{display:flex;align-items:center;gap:14px;background:linear-gradient(135deg,rgba(139,92,246,.08),rgba(255,255,255,.02));border:1px solid var(--card-bd);border-radius:13px;padding:12px 16px;transition:.22s;}
|
||||||
|
.wd-rank-row:hover{border-color:var(--violet);transform:translateX(4px);}
|
||||||
|
.wd-rank-no{font-family:"ZCOOL QingKe HuangYou",sans-serif;font-size:22px;width:34px;text-align:center;color:var(--gold);flex:0 0 auto;}
|
||||||
|
.wd-rank-row:nth-child(1) .wd-rank-no{color:#ffd56b;}
|
||||||
|
.wd-rank-row:nth-child(2) .wd-rank-no{color:#cfd8e8;}
|
||||||
|
.wd-rank-row:nth-child(3) .wd-rank-no{color:#e0a86b;}
|
||||||
|
.wd-rank-main{flex:1;min-width:0;}
|
||||||
|
.wd-rank-name{font-size:15px;color:#fff;margin-bottom:5px;display:flex;justify-content:space-between;}
|
||||||
|
.wd-rank-name .s{color:var(--gold);font-weight:700;font-size:13px;}
|
||||||
|
.wd-rank-track{height:8px;border-radius:5px;background:rgba(255,255,255,.07);overflow:hidden;}
|
||||||
|
.wd-rank-fill{height:100%;border-radius:5px;background:linear-gradient(90deg,var(--magenta),var(--violet));box-shadow:0 0 8px rgba(199,125,255,.5);}
|
||||||
@media (max-width:900px){
|
@media (max-width:900px){
|
||||||
.hero .copy h1{font-size:42px;}
|
.hero .copy h1{font-size:42px;}
|
||||||
}
|
}
|
||||||
@media (max-width:768px){
|
@media (max-width:768px){
|
||||||
.hero .copy{left:18px;right:18px;top:28px;}
|
.hero .copy{left:18px;right:18px;top:28px;}
|
||||||
|
html,body{height:100%;}
|
||||||
|
body{height:100dvh;}
|
||||||
|
.app{height:100dvh;}
|
||||||
|
.topbar{height:auto;min-height:54px;flex-wrap:wrap;align-items:center;padding:8px 14px;gap:10px;}
|
||||||
|
.logo .cn{font-size:22px;letter-spacing:2px;}
|
||||||
|
.logo .en{display:none;}
|
||||||
|
.spacer{display:none;}
|
||||||
|
.user-box{margin-left:auto;font-size:13px;gap:8px;}
|
||||||
|
.user-box .auth-btn{padding:6px 13px;font-size:12px;}
|
||||||
|
.nav{order:3;flex:1 0 100%;width:100%;margin-left:0;gap:4px;padding-top:6px;border-top:1px solid var(--card-bd);}
|
||||||
|
.nav button{font-size:13px;padding:7px 9px;white-space:nowrap;}
|
||||||
|
.hero .copy{left:18px;right:18px;bottom:82px;}
|
||||||
|
.hero .copy{left:18px;top:28px;}
|
||||||
.hero .copy h1{font-size:32px;letter-spacing:4px;}
|
.hero .copy h1{font-size:32px;letter-spacing:4px;}
|
||||||
.hero .copy .tag{font-size:16px;}
|
.hero .copy .tag{font-size:16px;}
|
||||||
.slide-label{left:18px;bottom:78px;}
|
.slide-label{left:18px;bottom:78px;}
|
||||||
@ -74,6 +369,10 @@
|
|||||||
.hero .enter-btn{font-size:15px;padding:11px 30px;}
|
.hero .enter-btn{font-size:15px;padding:11px 30px;}
|
||||||
}
|
}
|
||||||
@media (max-width:480px){
|
@media (max-width:480px){
|
||||||
|
.topbar{padding:8px 10px;gap:8px;}
|
||||||
|
.logo .cn{font-size:20px;}
|
||||||
|
.nav button{font-size:12px;padding:6px 7px;}
|
||||||
|
.user-box .auth-btn{padding:6px 11px;}
|
||||||
.hero .copy{left:14px;top:20px;}
|
.hero .copy{left:14px;top:20px;}
|
||||||
.hero .copy h1{font-size:26px;letter-spacing:2px;}
|
.hero .copy h1{font-size:26px;letter-spacing:2px;}
|
||||||
.hero .copy .tag{font-size:14px;}
|
.hero .copy .tag{font-size:14px;}
|
||||||
@ -81,6 +380,14 @@
|
|||||||
.slide-label .sl-cn{font-size:20px;}
|
.slide-label .sl-cn{font-size:20px;}
|
||||||
.slide-label .sl-desc{font-size:12.5px;}
|
.slide-label .sl-desc{font-size:12.5px;}
|
||||||
.sl-dots{right:12px;gap:10px;}
|
.sl-dots{right:12px;gap:10px;}
|
||||||
|
.grid{grid-template-columns:1fr;}
|
||||||
|
.panel{padding:14px 14px;}
|
||||||
|
.codex-search{width:118px;flex:1 1 100%;}
|
||||||
|
.codex-subrow{flex-wrap:wrap;}
|
||||||
|
.reader-inner{padding:22px 14px 70px;}
|
||||||
|
#story{padding:14px 14px;}
|
||||||
|
.tier-row{grid-template-columns:60px 76px 1fr;gap:6px;}
|
||||||
|
.codex-cats button,.codex-subs button{padding:7px 11px;font-size:13px;}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@ -114,6 +421,87 @@
|
|||||||
</div>
|
</div>
|
||||||
<button class="sl-arrow prev" onclick="window.slPrev && window.slPrev()">‹</button>
|
<button class="sl-arrow prev" onclick="window.slPrev && window.slPrev()">‹</button>
|
||||||
<button class="sl-arrow next" onclick="window.slNext && window.slNext()">›</button>
|
<button class="sl-arrow next" onclick="window.slNext && window.slNext()">›</button>
|
||||||
|
<!-- ===== 介绍(视频主页) ===== -->
|
||||||
|
<section class="panel active" id="intro">
|
||||||
|
<div class="hero">
|
||||||
|
<div class="slideshow" id="slideshow">
|
||||||
|
<!-- 五层世界轮播,由 JS 渲染 -->
|
||||||
|
</div>
|
||||||
|
<div class="enter-wrap">
|
||||||
|
<button class="enter-btn" id="enterGame">进入游戏</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ===== 背景(特色 + 世界背景 + 致玩家 合并平铺) ===== -->
|
||||||
|
<section class="panel" id="background">
|
||||||
|
<div class="panel-scroll" id="bgBody"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ===== 规则 ===== -->
|
||||||
|
<section class="panel" id="rules">
|
||||||
|
<div class="rules-scroll">
|
||||||
|
<!-- 静态兜底数据已注释 — 验证 API 是否取到后台数据 -->
|
||||||
|
<!-- 如页面此处为空白 = Odoo API 未返回规则数据 -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ===== 图鉴 ===== -->
|
||||||
|
<section class="panel" id="codex">
|
||||||
|
<div class="codex-wrap">
|
||||||
|
<div class="codex-cats" id="codexCats"></div>
|
||||||
|
<div class="codex-subrow">
|
||||||
|
<div class="codex-subs" id="codexSubs"></div>
|
||||||
|
<input class="codex-search" id="codexSearch" placeholder="搜索名称 / 描述…" />
|
||||||
|
<span class="codex-count" id="codexCount"></span>
|
||||||
|
</div>
|
||||||
|
<div class="codex-grid" id="codexGrid"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ===== 论坛 ===== -->
|
||||||
|
<section class="panel" id="forum">
|
||||||
|
<div class="f-cats">
|
||||||
|
<!-- 静态分类已注释 — 验证 API 是否取到后台数据 -->
|
||||||
|
<!-- 如页面此处为空白 = Odoo API 未返回论坛分类 -->
|
||||||
|
</div>
|
||||||
|
<div class="f-main">
|
||||||
|
<div class="post-form" id="postForm">
|
||||||
|
<input id="pfTitle" placeholder="标题" />
|
||||||
|
<textarea id="pfBody" placeholder="说点什么…(演示:仅本次会话内可见)"></textarea>
|
||||||
|
<div class="row"><button id="pfCancel">取消</button><button class="ok" id="pfOk">发布</button></div>
|
||||||
|
</div>
|
||||||
|
<div class="threads" id="threads">
|
||||||
|
<!-- 静态兜底数据已注释 — 验证 API 是否取到后台数据 -->
|
||||||
|
<!-- 如页面此处为空白 = Odoo API 未返回论坛数据 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ===== 公告 ===== -->
|
||||||
|
<section class="panel" id="announce">
|
||||||
|
<div class="notes">
|
||||||
|
<!-- 静态兜底数据已注释 — 验证 API 是否取到后台数据 -->
|
||||||
|
<!-- 如页面此处为空白 = Odoo API 未返回公告数据 -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ===== 剧情(小说阅读器:左目录 + 右正文) ===== -->
|
||||||
|
<section class="panel" id="story">
|
||||||
|
<div class="story-wrap">
|
||||||
|
<aside class="story-toc">
|
||||||
|
<div class="toc-head">目录<small>CONTENTS</small></div>
|
||||||
|
<div class="toc-list" id="storyTocList"><div class="toc-loading">加载中…</div></div>
|
||||||
|
</aside>
|
||||||
|
<article class="story-reader" id="storyReader">
|
||||||
|
<div class="reader-loading">从左侧目录选择章节开始阅读</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ===== 世界数据(登录后可见) ===== -->
|
||||||
|
<section class="panel" id="worlddata">
|
||||||
|
<div id="worldDataContent"></div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
@ -217,6 +605,86 @@
|
|||||||
|
|
||||||
// —— 暴露给箭头按钮 ——
|
// —— 暴露给箭头按钮 ——
|
||||||
window.slPrev=prev; window.slNext=next;
|
window.slPrev=prev; window.slNext=next;
|
||||||
|
// ============ 五层世界图片轮播 ============
|
||||||
|
(function(){
|
||||||
|
const slides = [
|
||||||
|
{ img:'assets/slides/01_starry_sky.png', en:'STARRY SKY', cn:'星空', desc:'陨石星空带悬浮于世界最外层,是文明触及维度的起点。星轨连通各浮空岛,穹鎏晶体自星尘中凝结。',
|
||||||
|
origin:'世界未形,星尘先于万物浮游于虚无。首缕维度自星隙坍缩成形,穹鎏晶体由尘凝就,文明自此仰望苍穹之始。' },
|
||||||
|
{ img:'assets/slides/02_sky.png', en:'SKY', cn:'天空', desc:'云朵地基块托起浮空岛群,灵气复苏的第一缕风拂过。各族初代聚落,在此仰望也更在此扎根。',
|
||||||
|
origin:'云基初结,浮空岛群自苍穹垂落凡尘。灵气复苏,初民循风而居,于云上筑城,仰望亦扎根,道统由此萌发。' },
|
||||||
|
{ img:'assets/slides/03_earth.png', en:'EARTH', cn:'大地', desc:'平原与森林铺展成万物生灵的摇篮,玄荒文明于斯鼎盛。城邦林立,征伐与道统在沃土上并行。',
|
||||||
|
origin:'沃野铺展为万灵摇篮,玄荒先族自林泉走出。城邦并起,征伐与道统并驰沃土之上,文明于此鼎盛绵延。' },
|
||||||
|
{ img:'assets/slides/04_underground.png',en:'UNDERGROUND',cn:'地下', desc:'溶洞晶矿与暗河在深处低语,冥玉生于幽暗玉石之中。地心工坊调度浅层资源,灯火不熄。',
|
||||||
|
origin:'溶洞深开,晶矿与暗河在幽处低语。冥玉生于幽玉深心,地心工坊取浅层之资,灯火长明,匠魂不熄。' },
|
||||||
|
{ img:'assets/slides/05_core.png', en:'CORE', cn:'地心', desc:'熔岩奔流的地心,是世界诞生的本源。元玺在此凝聚,追溯天地初开的那一瞬永恒。',
|
||||||
|
origin:'熔岩奔流处,乃世界本源所钟。元玺于亿载炽火中凝聚成形,溯天地初开一瞬,万物归处,亦是起始。' }
|
||||||
|
];
|
||||||
|
const container = document.getElementById('slideshow');
|
||||||
|
if(!container) return;
|
||||||
|
let current = 0, timer = null;
|
||||||
|
const INTERVAL = 5500;
|
||||||
|
|
||||||
|
// 竖向轨道:5 张图上下滑动
|
||||||
|
const track = document.createElement('div');
|
||||||
|
track.className='slide-track';
|
||||||
|
container.appendChild(track);
|
||||||
|
|
||||||
|
// 渲染 slide + label
|
||||||
|
slides.forEach((s,i)=>{
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className='slide'+(i===0?' active':'');
|
||||||
|
el.style.backgroundImage="url('"+s.img+"')";
|
||||||
|
el.innerHTML='<div class="slide-label"><span class="sl-en">'+s.en+'</span><div class="sl-cn">'+s.cn+'</div><div class="sl-desc">'+s.desc+'</div></div>'
|
||||||
|
+'<div class="slide-origin"><span class="so-tag">起源</span>'+s.origin+'</div>';
|
||||||
|
track.appendChild(el);
|
||||||
|
});
|
||||||
|
const all = track.querySelectorAll('.slide');
|
||||||
|
|
||||||
|
// 指示器
|
||||||
|
const dotsWrap = document.createElement('div');
|
||||||
|
dotsWrap.className='sl-dots';
|
||||||
|
slides.forEach((_,i)=>{
|
||||||
|
const d=document.createElement('div');
|
||||||
|
d.className='sl-dot'+(i===0?' active':'');
|
||||||
|
d.addEventListener('click',()=>go(i));
|
||||||
|
dotsWrap.appendChild(d);
|
||||||
|
});
|
||||||
|
document.querySelector('.hero').appendChild(dotsWrap);
|
||||||
|
const dots = dotsWrap.querySelectorAll('.sl-dot');
|
||||||
|
|
||||||
|
function go(idx){
|
||||||
|
if(idx<0)idx=slides.length-1;if(idx>=slides.length)idx=0;
|
||||||
|
if(idx===current) return;
|
||||||
|
all[current].classList.remove('active');dots[current].classList.remove('active');
|
||||||
|
current=idx;
|
||||||
|
track.style.transform='translateY('+(-current*100)+'%)';
|
||||||
|
all[current].classList.add('active');dots[current].classList.add('active');
|
||||||
|
resetTimer();
|
||||||
|
}
|
||||||
|
function next(){ go((current+1)%slides.length); }
|
||||||
|
function prev(){ go((current-1+slides.length)%slides.length); }
|
||||||
|
function resetTimer(){ clearInterval(timer); timer=setInterval(next,INTERVAL); }
|
||||||
|
|
||||||
|
// 鼠标滚轮上下滑动
|
||||||
|
let lock=false,wAccum=0;
|
||||||
|
document.querySelector('.hero').addEventListener('wheel',function(e){
|
||||||
|
wAccum+=e.deltaY;
|
||||||
|
if(lock)return;
|
||||||
|
if(Math.abs(wAccum)>50){
|
||||||
|
lock=true;
|
||||||
|
if(wAccum>0)next();else prev();
|
||||||
|
wAccum=0;
|
||||||
|
setTimeout(function(){lock=false;},1250);
|
||||||
|
}
|
||||||
|
},{passive:true});
|
||||||
|
|
||||||
|
resetTimer();
|
||||||
|
})();
|
||||||
|
|
||||||
|
// ============ 程序化矢量缩略图(零 AI 生图,不消耗积分) ============
|
||||||
|
function hashStr(s){let h=2166136261;for(let i=0;i<s.length;i++){h^=s.charCodeAt(i);h=Math.imul(h,16777619);}return h>>>0;}
|
||||||
|
function rng(seed){let s=seed>>>0;return function(){s=(Math.imul(s,1664525)+1013904223)>>>0;return s/4294967296;};}
|
||||||
|
const TIERCOL={'普通':'#9aa6c4','精良':'#5fd38a','稀有':'#4cc9f0','史诗':'#c77dff','传说':'#e7b85c'};
|
||||||
|
|
||||||
resetTimer();
|
resetTimer();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -26,6 +26,7 @@
|
|||||||
|
|
||||||
/* 登录页背景(固定单图 login_bg,原图直出,不加特效) */
|
/* 登录页背景(固定单图 login_bg,原图直出,不加特效) */
|
||||||
.bg-slides{position:fixed;inset:0;z-index:0;background:url('assets/slides/login_bg.webp') center/cover no-repeat;}
|
.bg-slides{position:fixed;inset:0;z-index:0;background:url('assets/slides/login_bg.webp') center/cover no-repeat;}
|
||||||
|
.bg-slides{position:fixed;inset:0;z-index:0;background:url('assets/slides/login_bg.png') center/cover no-repeat;}
|
||||||
|
|
||||||
.wrap{position:relative;z-index:2;width:min(420px,92vw);}
|
.wrap{position:relative;z-index:2;width:min(420px,92vw);}
|
||||||
.brand{text-align:center;margin-bottom:26px;}
|
.brand{text-align:center;margin-bottom:26px;}
|
||||||
|
|||||||
272
world.html
@ -619,51 +619,49 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ua-scroll">
|
<div class="ua-scroll">
|
||||||
<!-- 缔造者等级卡 -->
|
<!-- 缔造者等级卡(数据从 me() 动态填充) -->
|
||||||
<section class="ua-card">
|
<section class="ua-card">
|
||||||
<div class="ua-card-top">
|
<div class="ua-card-top">
|
||||||
<span class="ua-lv">Lv.<b id="uaLv">12</b></span>
|
<span class="ua-lv">Lv.<b id="uaLv">--</b></span>
|
||||||
<span class="ua-title">星界旅人</span>
|
<span class="ua-title" id="uaTitle">--</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ua-exp"><div class="ua-exp-fill" style="width:64%"></div></div>
|
<div class="ua-exp"><div class="ua-exp-fill" id="uaExpBar" style="width:0%"></div></div>
|
||||||
<div class="ua-exp-txt">EXP 640 / 1000 · 距下一级还差 360</div>
|
<div class="ua-exp-txt" id="uaExpTxt">EXP -- / --</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 我的世界 概览 -->
|
<!-- 我的世界 概览(数据从 me() 动态填充) -->
|
||||||
<section class="ua-card">
|
<section class="ua-card">
|
||||||
<div class="ua-card-h">🌍 我的世界</div>
|
<div class="ua-card-h">🌍 我的世界</div>
|
||||||
<div class="ua-world-name" id="uaWorldName">我的世界</div>
|
<div class="ua-world-name" id="uaWorldName">--</div>
|
||||||
<div class="ua-grid">
|
<div class="ua-grid">
|
||||||
<div class="ua-stat"><div class="ua-stat-v" id="uaEra">星元1年·春</div><div class="ua-stat-l">历元</div></div>
|
<div class="ua-stat"><div class="ua-stat-v" id="uaEra">--</div><div class="ua-stat-l">历元</div></div>
|
||||||
<div class="ua-stat"><div class="ua-stat-v" id="uaTiles">0</div><div class="ua-stat-l">疆域地块</div></div>
|
<div class="ua-stat"><div class="ua-stat-v" id="uaTiles">--</div><div class="ua-stat-l">疆域地块</div></div>
|
||||||
<div class="ua-stat"><div class="ua-stat-v">382</div><div class="ua-stat-l">人口</div></div>
|
<div class="ua-stat"><div class="ua-stat-v" id="uaPop">--</div><div class="ua-stat-l">人口</div></div>
|
||||||
<div class="ua-stat"><div class="ua-stat-v">68%</div><div class="ua-stat-l">文明度</div></div>
|
<div class="ua-stat"><div class="ua-stat-v" id="uaCiv">--</div><div class="ua-stat-l">文明度</div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ua-res">
|
<div class="ua-res">
|
||||||
<div class="ua-res-row"><span>🪙 金币</span><span class="ua-res-bar"><i style="width:80%"></i></span><b>1280</b></div>
|
<div class="ua-res-row"><span>🪙 金币</span><span class="ua-res-bar"><i id="uaCoinBar" style="width:0%"></i></span><b id="uaCoins">--</b></div>
|
||||||
<div class="ua-res-row"><span>✨ 灵气</span><span class="ua-res-bar"><i style="width:45%"></i></span><b>540</b></div>
|
<div class="ua-res-row"><span>✨ 灵气</span><span class="ua-res-bar"><i id="uaEnergyBar" style="width:0%"></i></span><b id="uaEnergy">--</b></div>
|
||||||
<div class="ua-res-row"><span>🪵 木材</span><span class="ua-res-bar"><i style="width:60%"></i></span><b>360</b></div>
|
<div class="ua-res-row"><span>🪵 木材</span><span class="ua-res-bar"><i id="uaWoodBar" style="width:0%"></i></span><b id="uaWood">--</b></div>
|
||||||
<div class="ua-res-row"><span>🪨 石料</span><span class="ua-res-bar"><i style="width:30%"></i></span><b>180</b></div>
|
<div class="ua-res-row"><span>🪨 石料</span><span class="ua-res-bar"><i id="uaStoneBar" style="width:0%"></i></span><b id="uaStone">--</b></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 成就 -->
|
<!-- 成就预览(数据从 API.achievements() 动态填充) -->
|
||||||
<section class="ua-card">
|
<section class="ua-card">
|
||||||
<div class="ua-card-h">🏆 成就</div>
|
<div class="ua-card-h">🏆 成就 <span id="uaAchSummary" style="font-size:11px;color:var(--text-dim);float:right;">-- / --</span></div>
|
||||||
<div class="ua-ach">
|
<div class="ua-ach" id="uaAchPreview">
|
||||||
<div class="ua-ach-item done"><span class="ua-ach-ic">🌟</span><div><div class="ua-ach-t">开天辟地</div><div class="ua-ach-d">已点亮第一块领土</div></div></div>
|
<!-- 成就预览项由 JS 动态渲染 -->
|
||||||
<div class="ua-ach-item"><span class="ua-ach-ic">🌱</span><div><div class="ua-ach-t">文明摇篮</div><div class="ua-ach-bar"><i style="width:60%"></i></div></div></div>
|
|
||||||
<div class="ua-ach-item locked"><span class="ua-ach-ic">🔒</span><div><div class="ua-ach-t">通天塔</div><div class="ua-ach-d">建造 10 座奇观解锁</div></div></div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 快捷操作 -->
|
<!-- 快捷操作 -->
|
||||||
<div class="ua-actions">
|
<div class="ua-actions">
|
||||||
<button class="ua-item" id="uaProfile" type="button">👤 个人信息</button>
|
<button class="ua-item" id="uaProfile" type="button">👤 个人</button>
|
||||||
|
<button class="ua-item" id="uaAchievements" type="button">🏆 成就</button>
|
||||||
|
<button class="ua-item" id="uaMessages" type="button">✉️ 邮箱 <span class="ua-badge" id="uaMailBadge" style="display:none;">0</span></button>
|
||||||
<button class="ua-item" id="uaSettings" type="button">⚙️ 设置</button>
|
<button class="ua-item" id="uaSettings" type="button">⚙️ 设置</button>
|
||||||
<button class="ua-item" id="uaMessages" type="button">✉️ 邮件 <span class="ua-badge">3</span></button>
|
<button class="ua-item" id="uaWebsite" type="button">🌐 官网</button>
|
||||||
<button class="ua-item" id="uaAchievements" type="button">🏆 成就墙</button>
|
|
||||||
<button class="ua-item" id="uaWebsite" type="button">🌐 官网 / 社区</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -4364,17 +4362,94 @@ if (typeof fitWorld === 'function') fitWorld();
|
|||||||
trigger.setAttribute('aria-expanded', open ? 'true' : 'false');
|
trigger.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 面板内容(游戏视角 demo):同步真实可读字段
|
// === 面板数据填充(从 me() 返回值动态渲染) ===
|
||||||
|
const cachedMe = me; // 保存供弹窗复用
|
||||||
|
function fillPanelData(d) {
|
||||||
|
const name = d.name || d.login || '';
|
||||||
const uaName2 = document.getElementById('uaName2');
|
const uaName2 = document.getElementById('uaName2');
|
||||||
|
if (uaName2) uaName2.textContent = name;
|
||||||
const uaAv = document.getElementById('uaAvatarLg');
|
const uaAv = document.getElementById('uaAvatarLg');
|
||||||
const wnEl2 = document.getElementById('worldName');
|
if (uaAv) uaAv.textContent = (name || 'M').trim().charAt(0).toUpperCase();
|
||||||
const eraEl = document.getElementById('calendarLabel');
|
const uaUid = document.getElementById('uaUid');
|
||||||
const tilesEl = document.getElementById('uaTiles');
|
if (uaUid) uaUid.textContent = d.game_uid || d.uid || '--';
|
||||||
if (uaName2) uaName2.textContent = me.name || me.login;
|
// 等级卡
|
||||||
if (uaAv) uaAv.textContent = (me.name || me.login || 'M').trim().charAt(0).toUpperCase();
|
const uaLv = document.getElementById('uaLv');
|
||||||
if (wnEl2) { const wn = document.getElementById('uaWorldName'); if (wn) wn.textContent = wnEl2.textContent; }
|
if (uaLv) uaLv.textContent = d.level || 1;
|
||||||
if (eraEl) { const er = document.getElementById('uaEra'); if (er) er.textContent = eraEl.textContent; }
|
const uaTitle = document.getElementById('uaTitle');
|
||||||
if (tilesEl && typeof tiles !== 'undefined' && Array.isArray(tiles)) tilesEl.textContent = tiles.length;
|
if (uaTitle) uaTitle.textContent = d.title || '萌新';
|
||||||
|
const expMax = d.exp_max || 100;
|
||||||
|
const expPct = expMax > 0 ? Math.min(100, (d.exp || 0) / expMax * 100) : 0;
|
||||||
|
const uaExpBar = document.getElementById('uaExpBar');
|
||||||
|
if (uaExpBar) uaExpBar.style.width = expPct + '%';
|
||||||
|
const uaExpTxt = document.getElementById('uaExpTxt');
|
||||||
|
if (uaExpTxt) uaExpTxt.textContent = 'EXP ' + (d.exp || 0) + ' / ' + expMax;
|
||||||
|
// 世界概览
|
||||||
|
const uaWorldName = document.getElementById('uaWorldName');
|
||||||
|
if (uaWorldName) uaWorldName.textContent = d.world_name || '我的世界';
|
||||||
|
const uaEra = document.getElementById('uaEra');
|
||||||
|
if (uaEra) uaEra.textContent = d.era || '--';
|
||||||
|
const uaTiles = document.getElementById('uaTiles');
|
||||||
|
if (uaTiles) uaTiles.textContent = d.tiles || 0;
|
||||||
|
const uaPop = document.getElementById('uaPop');
|
||||||
|
if (uaPop) uaPop.textContent = d.population || 0;
|
||||||
|
const uaCiv = document.getElementById('uaCiv');
|
||||||
|
if (uaCiv) uaCiv.textContent = d.civ_level || 0;
|
||||||
|
// 资源条(按上限折算宽度)
|
||||||
|
function setRes(barId, valId, val, max) {
|
||||||
|
const el = document.getElementById(valId);
|
||||||
|
if (el) el.textContent = val || 0;
|
||||||
|
const bar = document.getElementById(barId);
|
||||||
|
if (bar) bar.style.width = (max > 0 ? Math.min(100, (val || 0) / max * 100) : 0) + '%';
|
||||||
|
}
|
||||||
|
setRes('uaCoinBar', 'uaCoins', d.coins, 10000);
|
||||||
|
setRes('uaEnergyBar', 'uaEnergy', d.energy, 1000);
|
||||||
|
setRes('uaWoodBar', 'uaWood', d.wood, 1000);
|
||||||
|
setRes('uaStoneBar', 'uaStone', d.stone, 1000);
|
||||||
|
// 邮件未读角标
|
||||||
|
const uaMailBadge = document.getElementById('uaMailBadge');
|
||||||
|
if (uaMailBadge) {
|
||||||
|
const n = d.unread_mail || 0;
|
||||||
|
uaMailBadge.textContent = n;
|
||||||
|
uaMailBadge.style.display = n > 0 ? '' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fillPanelData(me);
|
||||||
|
|
||||||
|
// === 异步加载成就预览 + 邮件未读数 ===
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const ach = await API.achievements();
|
||||||
|
if (ach && ach.items) {
|
||||||
|
const summary = document.getElementById('uaAchSummary');
|
||||||
|
if (summary) summary.textContent = (ach.unlocked_count || 0) + ' / ' + (ach.total_count || 0);
|
||||||
|
const preview = document.getElementById('uaAchPreview');
|
||||||
|
if (preview) {
|
||||||
|
const top3 = ach.items.slice(0, 3);
|
||||||
|
preview.innerHTML = top3.length ? top3.map(a => {
|
||||||
|
const icon = a.unlocked ? (a.icon || '🏆') : '🔒';
|
||||||
|
const pct = a.target > 0 ? Math.min(100, (a.progress || 0) / a.target * 100) : (a.unlocked ? 100 : 0);
|
||||||
|
return `<div class="ua-ach-item${a.unlocked ? ' done' : ''}">
|
||||||
|
<span class="ua-ach-ic">${icon}</span>
|
||||||
|
<div style="flex:1;min-width:0;">
|
||||||
|
<div class="ua-ach-t">${a.name}</div>
|
||||||
|
<div class="ua-ach-bar"><i style="width:${pct}%"></i></div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}).join('') : '<div style="font-size:12px;color:#8899aa;text-align:center;padding:12px;">暂无成就数据</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { console.warn('加载成就预览失败', e); }
|
||||||
|
try {
|
||||||
|
const ml = await API.mails();
|
||||||
|
if (ml && typeof ml.unread === 'number') {
|
||||||
|
const badge = document.getElementById('uaMailBadge');
|
||||||
|
if (badge) {
|
||||||
|
badge.textContent = ml.unread;
|
||||||
|
badge.style.display = ml.unread > 0 ? '' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { console.warn('加载邮件数失败', e); }
|
||||||
|
})();
|
||||||
|
|
||||||
trigger.addEventListener('click', (ev) => {
|
trigger.addEventListener('click', (ev) => {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
@ -4418,27 +4493,40 @@ if (typeof fitWorld === 'function') fitWorld();
|
|||||||
if (e.target === appModal) closeAppModal();
|
if (e.target === appModal) closeAppModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 个人信息
|
// 个人信息(从 cachedMe 动态渲染)
|
||||||
const uaProfile = document.getElementById('uaProfile');
|
const uaProfile = document.getElementById('uaProfile');
|
||||||
if (uaProfile) uaProfile.addEventListener('click', () => {
|
if (uaProfile) uaProfile.addEventListener('click', () => {
|
||||||
|
const d = cachedMe;
|
||||||
|
const name = d.name || d.login || '';
|
||||||
|
const avatarLetter = (name || 'M').trim().charAt(0).toUpperCase();
|
||||||
|
const expMax = d.exp_max || 100;
|
||||||
|
const expPct = expMax > 0 ? Math.min(100, (d.exp || 0) / expMax * 100) : 0;
|
||||||
openAppModal('👤 个人信息', `
|
openAppModal('👤 个人信息', `
|
||||||
<div style="display:flex;align-items:center;gap:14px;margin-bottom:14px;">
|
<div style="display:flex;align-items:center;gap:14px;margin-bottom:14px;">
|
||||||
<div style="width:54px;height:54px;border-radius:50%;background:linear-gradient(135deg,#c77dff,#4af);display:flex;align-items:center;justify-content:center;font-size:22px;font-weight:700;color:#fff;box-shadow:0 0 14px rgba(199,125,255,.5);">M</div>
|
<div style="width:54px;height:54px;border-radius:50%;background:linear-gradient(135deg,#c77dff,#4af);display:flex;align-items:center;justify-content:center;font-size:22px;font-weight:700;color:#fff;box-shadow:0 0 14px rgba(199,125,255,.5);">${avatarLetter}</div>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size:16px;font-weight:700;color:#e0e0e0;">Mitchell</div>
|
<div style="font-size:16px;font-weight:700;color:#e0e0e0;">${name}</div>
|
||||||
<div style="font-size:12px;color:#8899aa;">UID: 10086001</div>
|
<div style="font-size:12px;color:#8899aa;">UID: ${d.game_uid || d.uid || '--'}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="infoRow"><span class="infoLabel">等级</span><span class="infoValue">Lv.12</span></div>
|
<div class="infoRow"><span class="infoLabel">等级</span><span class="infoValue">Lv.${d.level || 1}</span></div>
|
||||||
<div style="margin:6px 0 10px;">
|
<div style="margin:6px 0 10px;">
|
||||||
<div style="height:8px;background:rgba(255,255,255,.08);border-radius:4px;overflow:hidden;">
|
<div style="height:8px;background:rgba(255,255,255,.08);border-radius:4px;overflow:hidden;">
|
||||||
<div style="height:100%;width:64%;background:linear-gradient(90deg,#8b5cf6,#4af);border-radius:4px;"></div>
|
<div style="height:100%;width:${expPct}%;background:linear-gradient(90deg,#8b5cf6,#4af);border-radius:4px;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size:11px;color:#8899aa;text-align:right;margin-top:3px;">EXP 640 / 1000</div>
|
<div style="font-size:11px;color:#8899aa;text-align:right;margin-top:3px;">EXP ${d.exp || 0} / ${expMax}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="infoRow"><span class="infoLabel">金币</span><span class="infoValue">🪙 1280</span></div>
|
<div class="infoRow"><span class="infoLabel">称号</span><span class="infoValue">${d.title || '萌新'}</span></div>
|
||||||
<div class="infoRow"><span class="infoLabel">世界</span><span class="infoValue">星元 1年 · 春</span></div>
|
<div class="infoRow"><span class="infoLabel">金币</span><span class="infoValue">🪙 ${d.coins || 0}</span></div>
|
||||||
<div class="infoRow"><span class="infoLabel">注册</span><span class="infoValue">2026-07-20</span></div>
|
<div class="infoRow"><span class="infoLabel">灵气</span><span class="infoValue">✨ ${d.energy || 0}</span></div>
|
||||||
|
<div class="infoRow"><span class="infoLabel">木材</span><span class="infoValue">🪵 ${d.wood || 0}</span></div>
|
||||||
|
<div class="infoRow"><span class="infoLabel">石料</span><span class="infoValue">🪨 ${d.stone || 0}</span></div>
|
||||||
|
<div class="infoRow"><span class="infoLabel">世界</span><span class="infoValue">${d.world_name || '我的世界'}</span></div>
|
||||||
|
<div class="infoRow"><span class="infoLabel">历元</span><span class="infoValue">${d.era || '--'}</span></div>
|
||||||
|
<div class="infoRow"><span class="infoLabel">疆域</span><span class="infoValue">${d.tiles || 0} 块</span></div>
|
||||||
|
<div class="infoRow"><span class="infoLabel">人口</span><span class="infoValue">${d.population || 0}</span></div>
|
||||||
|
<div class="infoRow"><span class="infoLabel">文明度</span><span class="infoValue">${d.civ_level || 0}</span></div>
|
||||||
|
<div class="infoRow"><span class="infoLabel">注册</span><span class="infoValue">${d.reg_date || '--'}</span></div>
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -4475,57 +4563,81 @@ if (typeof fitWorld === 'function') fitWorld();
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// 邮件
|
// 邮件(从 API.mails() 动态取值)
|
||||||
const uaMessages = document.getElementById('uaMessages');
|
const uaMessages = document.getElementById('uaMessages');
|
||||||
if (uaMessages) uaMessages.addEventListener('click', () => {
|
if (uaMessages) uaMessages.addEventListener('click', async () => {
|
||||||
const mails = [
|
openAppModal('✉️ 邮件', '<div style="text-align:center;padding:20px;color:#8899aa;font-size:13px;">加载中…</div>');
|
||||||
{ cat: '系统', title: '欢迎来到宇森', date: '2026-07-26', body: '请查收新手礼包与世界探索指南。', unread: true },
|
let mails = [];
|
||||||
{ cat: '系统', title: '版本更新 v0.2', date: '2026-07-25', body: '新增世界导航与微观视图功能。', unread: false },
|
try {
|
||||||
{ cat: '活动', title: '夏日创作大赛', date: '2026-07-24', body: '上传你的世界截图,赢限定头像框。', unread: true }
|
const resp = await API.mails();
|
||||||
];
|
mails = (resp && resp.items) || [];
|
||||||
const cats = ['全部', '系统', '活动'];
|
} catch (e) {
|
||||||
|
if (appModalBody) appModalBody.innerHTML = '<div class="mailEmpty">邮件加载失败</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cats = ['全部', ...new Set(mails.map(m => m.category_label).filter(Boolean))];
|
||||||
function renderMail(cat) {
|
function renderMail(cat) {
|
||||||
const list = cat === '全部' ? mails : mails.filter(m => m.cat === cat);
|
const list = cat === '全部' ? mails : mails.filter(m => m.category_label === cat);
|
||||||
const tabs = cats.map(c => `<span class="mailTab${c === cat ? ' active' : ''}" data-cat="${c}">${c}</span>`).join('');
|
const tabs = cats.map(c => `<span class="mailTab${c === cat ? ' active' : ''}" data-cat="${c}">${c}</span>`).join('');
|
||||||
const items = list.length
|
const items = list.length
|
||||||
? list.map(m => `<div class="mailItem"><div class="mailTitle">${m.unread ? '● ' : ''}${m.title}</div><div class="mailMeta">${m.cat} · ${m.date}</div><div>${m.body}</div></div>`).join('')
|
? list.map(m => `<div class="mailItem" data-id="${m.id}" style="cursor:pointer;${!m.is_read ? 'border-left:3px solid #8b5cf6;' : ''}">
|
||||||
|
<div class="mailTitle">${!m.is_read ? '● ' : ''}${m.subject || '(无标题)'}</div>
|
||||||
|
<div class="mailMeta">${m.category_label || ''} · ${m.date || ''}${m.sender ? ' · ' + m.sender : ''}</div>
|
||||||
|
<div>${m.body || ''}</div>
|
||||||
|
</div>`).join('')
|
||||||
: '<div class="mailEmpty">暂无邮件</div>';
|
: '<div class="mailEmpty">暂无邮件</div>';
|
||||||
if (appModalBody) appModalBody.innerHTML = `<div class="mailTabs">${tabs}</div>${items}`;
|
if (appModalBody) appModalBody.innerHTML = `<div class="mailTabs">${tabs}</div>${items}`;
|
||||||
appModalBody.querySelectorAll('.mailTab').forEach(t => { t.onclick = () => renderMail(t.dataset.cat); });
|
appModalBody.querySelectorAll('.mailTab').forEach(t => { t.onclick = () => renderMail(t.dataset.cat); });
|
||||||
|
appModalBody.querySelectorAll('.mailItem').forEach(item => {
|
||||||
|
item.onclick = async () => {
|
||||||
|
const id = parseInt(item.dataset.id);
|
||||||
|
if (!id) return;
|
||||||
|
try { await API.mailRead(id); } catch (e) {}
|
||||||
|
const m = mails.find(x => x.id === id);
|
||||||
|
if (m) m.is_read = true;
|
||||||
|
const badge = document.getElementById('uaMailBadge');
|
||||||
|
const unread = mails.filter(x => !x.is_read).length;
|
||||||
|
if (badge) { badge.textContent = unread; badge.style.display = unread > 0 ? '' : 'none'; }
|
||||||
|
const activeTab = appModalBody.querySelector('.mailTab.active');
|
||||||
|
renderMail(activeTab ? activeTab.dataset.cat : '全部');
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
openAppModal('✉️ 邮件', '');
|
|
||||||
renderMail('全部');
|
renderMail('全部');
|
||||||
});
|
});
|
||||||
|
|
||||||
// 成就
|
// 成就(从 API.achievements() 动态取值)
|
||||||
const uaAchievements = document.getElementById('uaAchievements');
|
const uaAchievements = document.getElementById('uaAchievements');
|
||||||
if (uaAchievements) uaAchievements.addEventListener('click', () => {
|
if (uaAchievements) uaAchievements.addEventListener('click', async () => {
|
||||||
openAppModal('🏆 成就', `
|
openAppModal('🏆 成就', '<div style="text-align:center;padding:20px;color:#8899aa;font-size:13px;">加载中…</div>');
|
||||||
|
let ach = { items: [], total_points: 0, unlocked_count: 0, total_count: 0 };
|
||||||
|
try {
|
||||||
|
ach = await API.achievements();
|
||||||
|
} catch (e) {
|
||||||
|
if (appModalBody) appModalBody.innerHTML = '<div style="text-align:center;padding:20px;color:#8899aa;">成就加载失败</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const items = ach.items || [];
|
||||||
|
const html = `
|
||||||
<div style="display:flex;justify-content:space-between;margin-bottom:14px;">
|
<div style="display:flex;justify-content:space-between;margin-bottom:14px;">
|
||||||
<div><div style="font-size:22px;font-weight:700;color:#e0e0e0;">320</div><div style="font-size:11px;color:#8899aa;">成就点数</div></div>
|
<div><div style="font-size:22px;font-weight:700;color:#e0e0e0;">${ach.total_points || 0}</div><div style="font-size:11px;color:#8899aa;">成就点数</div></div>
|
||||||
<div style="text-align:right;"><div style="font-size:22px;font-weight:700;color:#e0e0e0;">8 / 20</div><div style="font-size:11px;color:#8899aa;">已解锁</div></div>
|
<div style="text-align:right;"><div style="font-size:22px;font-weight:700;color:#e0e0e0;">${ach.unlocked_count || 0} / ${ach.total_count || 0}</div><div style="font-size:11px;color:#8899aa;">已解锁</div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="achItem done">
|
${items.length ? items.map(a => {
|
||||||
<div class="achIcon">🏆</div>
|
const pct = a.target > 0 ? Math.min(100, (a.progress || 0) / a.target * 100) : (a.unlocked ? 100 : 0);
|
||||||
<div class="achMain"><div class="achName">初临世界</div><div class="achDesc">创建你的第一个世界</div></div>
|
const icon = a.unlocked ? (a.icon || '🏆') : '🔒';
|
||||||
<div class="achState">已达成</div>
|
return `<div class="achItem${a.unlocked ? ' done' : ''}">
|
||||||
|
<div class="achIcon${a.unlocked ? '' : ' locked'}">${icon}</div>
|
||||||
|
<div class="achMain">
|
||||||
|
<div class="achName">${a.name}</div>
|
||||||
|
<div class="achDesc">${a.desc || ''}${a.tier_label ? ' [' + a.tier_label + ']' : ''} · ${a.points}点</div>
|
||||||
|
${a.unlocked ? '' : `<div class="achBar"><div class="achFill" style="width:${pct}%"></div></div>`}
|
||||||
</div>
|
</div>
|
||||||
<div class="achItem done">
|
<div class="achState">${a.unlocked ? '已达成' : Math.round(pct) + '%'}</div>
|
||||||
<div class="achIcon">🌍</div>
|
</div>`;
|
||||||
<div class="achMain"><div class="achName">五界行者</div><div class="achDesc">解锁全部五层世界</div></div>
|
}).join('') : '<div style="text-align:center;padding:20px;color:#8899aa;font-size:13px;">暂无成就数据</div>'}
|
||||||
<div class="achState">已达成</div>
|
`;
|
||||||
</div>
|
if (appModalBody) appModalBody.innerHTML = html;
|
||||||
<div class="achItem">
|
|
||||||
<div class="achIcon locked">🔒</div>
|
|
||||||
<div class="achMain"><div class="achName">大地建筑师</div><div class="achDesc">在大地世界放置 50 个地块</div><div class="achBar"><div class="achFill" style="width:40%"></div></div></div>
|
|
||||||
<div class="achState">40%</div>
|
|
||||||
</div>
|
|
||||||
<div class="achItem">
|
|
||||||
<div class="achIcon locked">🔒</div>
|
|
||||||
<div class="achMain"><div class="achName">熔岩征服者</div><div class="achDesc">在地心世界停留 10 分钟</div><div class="achBar"><div class="achFill" style="width:15%"></div></div></div>
|
|
||||||
<div class="achState">15%</div>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
location.replace('login.html?next=world.html'); return;
|
location.replace('login.html?next=world.html'); return;
|
||||||
|
|||||||