diff --git a/addons/game_base/__manifest__.py b/addons/game_base/__manifest__.py index c94b53d..7754e44 100644 --- a/addons/game_base/__manifest__.py +++ b/addons/game_base/__manifest__.py @@ -1,14 +1,15 @@ { 'name': '宇森游戏基础数据', - 'summary': '宇森游戏基础数据:图鉴 / 规则 / 论坛 / 公告', - 'version': '1.2', + 'summary': '宇森游戏基础数据:图鉴 / 规则 / 论坛 / 公告 / 玩家员工', + 'version': '1.3', 'category': 'Game', 'sequence': 10, - 'depends': ['base'], + 'depends': ['base', 'hr'], 'data': [ 'security/ir.model.access.csv', 'data/game_base_data.xml', 'data/story_cron.xml', + 'data/game_achievement_data.xml', 'views/codex_views.xml', 'views/rule_category_views.xml', 'views/rule_views.xml', @@ -16,10 +17,13 @@ 'views/forum_views.xml', 'views/announcement_views.xml', 'views/story_views.xml', + 'views/game_employee_views.xml', + 'views/game_achievement_views.xml', + 'views/game_mail_views.xml', 'views/menu.xml', ], 'application': True, 'license': 'LGPL-3', 'pre_init_hook': 'pre_init_hook', - 'description': '基于官网内容沉淀的游戏基础数据模型,供后台维护、前端经 /game/api/* 读取;并提供与 Odoo 用户打通的登录/注册接口。', + 'description': '基于官网内容沉淀的游戏基础数据模型,供后台维护、前端经 /game/api/* 读取;并提供与 Odoo 用户打通的登录/注册接口。注册时自动创建 hr.employee 并初始化游戏数据。', } diff --git a/addons/game_base/controllers/game_api.py b/addons/game_base/controllers/game_api.py index db230ca..452d637 100644 --- a/addons/game_base/controllers/game_api.py +++ b/addons/game_base/controllers/game_api.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- import json -from odoo import http +from odoo import http, fields from odoo.http import request, Response @@ -309,12 +309,52 @@ class GameApiController(http.Controller): 'groups_id': [(6, 0, [portal.id])], }) 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': '

欢迎来到宇森!请查收新手礼包与世界探索指南。

', + }) + request.env.cr.commit() db = request.session.db or 'game' auth_info = request.session.authenticate(db, {'type': 'password', 'login': login, 'password': password}) uid = auth_info['uid'] if uid: - 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}) + return _json({'ok': True, 'uid': uid, '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: return _json({'ok': False, 'error': '注册失败:%s' % str(e)}) @@ -324,9 +364,123 @@ class GameApiController(http.Controller): if uid: user = request.env['res.users'].sudo().browse(uid) 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}) + @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', methods=['POST', 'OPTIONS'], csrf=False) def logout(self, **kw): diff --git a/addons/game_base/data/game_achievement_data.xml b/addons/game_base/data/game_achievement_data.xml new file mode 100644 index 0000000..b1f7683 --- /dev/null +++ b/addons/game_base/data/game_achievement_data.xml @@ -0,0 +1,95 @@ + + + + + + + first_land + 10 + 开天辟地 + star + gold + 50 + 1 + 已点亮第一块领土,文明之火开始燃烧。 + + + + civilization_cradle + 20 + 文明摇篮 + sprout + silver + 30 + 5 + 建立 5 个聚落,文明初步成形。 + + + + sky_tower + 30 + 通天塔 + lock + platinum + 100 + 10 + 建造 10 座奇观,文明疆域遍布穹苍。 + + + + world_explorer + 40 + 世界探索者 + compass + bronze + 10 + 10 + 探索 10 块未知领土。 + + + + resource_master + 50 + 资源大亨 + gem + silver + 30 + 1000 + 累计获得 1000 金币。 + + + + spirit_awakening + 60 + 灵气觉醒 + sparkle + gold + 50 + 1 + 世界阶段跃迁至「灵启」。 + + + + mystic_realm + 70 + 玄界之主 + crown + legend + 200 + 1 + 世界阶段跃迁至「玄界」。 + + + + eternal + 80 + 永恒之锚 + infinity + legend + 500 + 1 + 世界阶段达到「溯元」,触及世界本源。 + + + + diff --git a/addons/game_base/models/__init__.py b/addons/game_base/models/__init__.py index 634af88..1297619 100644 --- a/addons/game_base/models/__init__.py +++ b/addons/game_base/models/__init__.py @@ -6,3 +6,6 @@ from . import rule from . import page from . import story from . import ir_http +from . import game_employee +from . import game_achievement +from . import game_mail diff --git a/addons/game_base/models/game_achievement.py b/addons/game_base/models/game_achievement.py new file mode 100644 index 0000000..588d4f7 --- /dev/null +++ b/addons/game_base/models/game_achievement.py @@ -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 diff --git a/addons/game_base/models/game_employee.py b/addons/game_base/models/game_employee.py new file mode 100644 index 0000000..f2c1961 --- /dev/null +++ b/addons/game_base/models/game_employee.py @@ -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, + } diff --git a/addons/game_base/models/game_mail.py b/addons/game_base/models/game_mail.py new file mode 100644 index 0000000..dbea734 --- /dev/null +++ b/addons/game_base/models/game_mail.py @@ -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='系统') diff --git a/addons/game_base/security/ir.model.access.csv b/addons/game_base/security/ir.model.access.csv index d0a25cc..37155c0 100644 --- a/addons/game_base/security/ir.model.access.csv +++ b/addons/game_base/security/ir.model.access.csv @@ -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_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_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 diff --git a/addons/game_base/views/forum_views.xml b/addons/game_base/views/forum_views.xml index 080c6a5..437a770 100644 --- a/addons/game_base/views/forum_views.xml +++ b/addons/game_base/views/forum_views.xml @@ -28,7 +28,7 @@ - +
diff --git a/addons/game_base/views/game_achievement_views.xml b/addons/game_base/views/game_achievement_views.xml new file mode 100644 index 0000000..8b392c0 --- /dev/null +++ b/addons/game_base/views/game_achievement_views.xml @@ -0,0 +1,81 @@ + + + + + + game.achievement.template.tree + game.achievement.template + + + + + + + + + + + + + + + + + game.achievement.template.form + game.achievement.template + +
+ + + + + + + + + + + + + + + + + + + +
+
+
+ + + + 成就模板 + game.achievement.template + tree,form + + + + + game.player.achievement.tree + game.player.achievement + + + + + + + + + + + + + + + 玩家成就 + game.player.achievement + tree,form + + +
diff --git a/addons/game_base/views/game_employee_views.xml b/addons/game_base/views/game_employee_views.xml new file mode 100644 index 0000000..bca1b93 --- /dev/null +++ b/addons/game_base/views/game_employee_views.xml @@ -0,0 +1,60 @@ + + + + + + hr.employee.game.form + hr.employee + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + hr.employee.game.tree + hr.employee + + + + + + + + + + + diff --git a/addons/game_base/views/game_mail_views.xml b/addons/game_base/views/game_mail_views.xml new file mode 100644 index 0000000..c634379 --- /dev/null +++ b/addons/game_base/views/game_mail_views.xml @@ -0,0 +1,56 @@ + + + + + + game.player.mail.tree + game.player.mail + + + + + + + + + + + + + + + + game.player.mail.form + game.player.mail + +
+ + + + + + + + + + + + + + + + + + +
+
+
+ + + + 玩家邮件 + game.player.mail + tree,form + + +
diff --git a/addons/game_base/views/menu.xml b/addons/game_base/views/menu.xml index daa2008..305054f 100644 --- a/addons/game_base/views/menu.xml +++ b/addons/game_base/views/menu.xml @@ -26,6 +26,11 @@ + + + + + @@ -37,5 +42,6 @@ + diff --git a/assets/game-api.js b/assets/game-api.js index 2d4c25f..f87e5df 100644 --- a/assets/game-api.js +++ b/assets/game-api.js @@ -59,6 +59,19 @@ const API = { method: 'POST', credentials: _cred, })).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() { return (await fetch(_url('/game/api/codex'), { credentials: _cred })).json(); }, @@ -136,4 +149,6 @@ const API = { async codexRaces() { return (await fetch(_worldUrl('/yt_world/api/codex/races'), { credentials: _wcred })).json(); }, + // 暴露后端 base,供图片等静态资源 URL 拼接(本地跨域→8069;线上同域→'') + base: API_CONFIG.base, }; diff --git a/assets/site-app.js b/assets/site-app.js index c50403e..b8e13af 100644 --- a/assets/site-app.js +++ b/assets/site-app.js @@ -53,6 +53,102 @@ async function loadRules(){ } 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 = '
暂无图鉴数据
'; + return; + } + + // 选中状态 + let curGroup = groups[0].key; + let curSub = 'all'; + let curQ = ''; + + function renderCats(){ + if (!catsEl) return; + catsEl.innerHTML = groups.map(g => + `` + ).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 = `` + + subs.map(s => ``).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 = '
未找到匹配项
'; + return; + } + const base = API.base || ''; + grid.innerHTML = items.map(it => { + const thumb = it.image + ? `
` + : `
`; + return `
+ ${thumb} +
+
${it.name || ''}
+
${it.desc || ''}
+
${it.sub || ''}${it.tier || ''}
+
+
`; + }).join(''); + } + + if (searchEl){ + searchEl.addEventListener('input', () => { curQ = searchEl.value.trim(); renderGrid(); }); + } + + renderCats(); + renderSubs(); + renderGrid(); +} + // ---------- 论坛 ---------- async function loadForum(){ const threadsEl = document.getElementById('threads'); @@ -155,6 +251,7 @@ async function loadStory(){ document.addEventListener('DOMContentLoaded', () => { loadPages(); loadRules(); + loadCodex(); loadForum(); loadAnnounce(); loadStory(); diff --git a/assets/slides/01_starry_sky.png b/assets/slides/01_starry_sky.png new file mode 100644 index 0000000..ae6faaf Binary files /dev/null and b/assets/slides/01_starry_sky.png differ diff --git a/assets/slides/02_sky.png b/assets/slides/02_sky.png new file mode 100644 index 0000000..3261a34 Binary files /dev/null and b/assets/slides/02_sky.png differ diff --git a/assets/slides/03_earth.png b/assets/slides/03_earth.png new file mode 100644 index 0000000..1b35426 Binary files /dev/null and b/assets/slides/03_earth.png differ diff --git a/assets/slides/04_underground.png b/assets/slides/04_underground.png new file mode 100644 index 0000000..d936a4e Binary files /dev/null and b/assets/slides/04_underground.png differ diff --git a/assets/slides/05_core.png b/assets/slides/05_core.png new file mode 100644 index 0000000..52f7f0f Binary files /dev/null and b/assets/slides/05_core.png differ diff --git a/assets/slides/06_starry_sky_b.png b/assets/slides/06_starry_sky_b.png new file mode 100644 index 0000000..71141a8 Binary files /dev/null and b/assets/slides/06_starry_sky_b.png differ diff --git a/assets/slides/07_sky_b.png b/assets/slides/07_sky_b.png new file mode 100644 index 0000000..1fd4e54 Binary files /dev/null and b/assets/slides/07_sky_b.png differ diff --git a/assets/slides/08_earth_b.png b/assets/slides/08_earth_b.png new file mode 100644 index 0000000..e020ee7 Binary files /dev/null and b/assets/slides/08_earth_b.png differ diff --git a/assets/slides/09_underground_b.png b/assets/slides/09_underground_b.png new file mode 100644 index 0000000..6ab21a6 Binary files /dev/null and b/assets/slides/09_underground_b.png differ diff --git a/assets/slides/10_core_b.png b/assets/slides/10_core_b.png new file mode 100644 index 0000000..c33df9c Binary files /dev/null and b/assets/slides/10_core_b.png differ diff --git a/assets/slides/login_bg.png b/assets/slides/login_bg.png new file mode 100644 index 0000000..4a8be66 Binary files /dev/null and b/assets/slides/login_bg.png differ diff --git a/index.html b/index.html index 920e968..2e5cd96 100644 --- a/index.html +++ b/index.html @@ -15,6 +15,65 @@ /* 首页特有:全屏 hero 轮播 */ html,body{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 .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;} @@ -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);} /* 轮播标签 */ + /* 轮播左下角特色文案 */ .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-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);} /* 指示器 */ + /* 轮播指示器(竖排右侧) */ .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:hover{background:rgba(255,255,255,.5);} @@ -47,6 +108,7 @@ .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; 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; @@ -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: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){ .hero .copy h1{font-size:42px;} } @media (max-width:768px){ .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 .tag{font-size:16px;} .slide-label{left:18px;bottom:78px;} @@ -74,6 +369,10 @@ .hero .enter-btn{font-size:15px;padding:11px 30px;} } @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 h1{font-size:26px;letter-spacing:2px;} .hero .copy .tag{font-size:14px;} @@ -81,6 +380,14 @@ .slide-label .sl-cn{font-size:20px;} .slide-label .sl-desc{font-size:12.5px;} .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;} } @@ -114,6 +421,87 @@ + +
+
+
+ +
+
+ +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
从左侧目录选择章节开始阅读
+
+
+
+ + +
+
@@ -217,6 +605,86 @@ // —— 暴露给箭头按钮 —— 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='
'+s.en+'
'+s.cn+'
'+s.desc+'
' + +'
起源'+s.origin+'
'; + 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>>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(); })(); diff --git a/login.html b/login.html index ac9f2ce..5ee9fab 100644 --- a/login.html +++ b/login.html @@ -26,6 +26,7 @@ /* 登录页背景(固定单图 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.png') center/cover no-repeat;} .wrap{position:relative;z-index:2;width:min(420px,92vw);} .brand{text-align:center;margin-bottom:26px;} diff --git a/world.html b/world.html index 399940d..41c1409 100644 --- a/world.html +++ b/world.html @@ -619,51 +619,49 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
- +
- Lv.12 - 星界旅人 + Lv.-- + --
-
-
EXP 640 / 1000 · 距下一级还差 360
+
+
EXP -- / --
- +
🌍 我的世界
-
我的世界
+
--
-
星元1年·春
历元
-
0
疆域地块
-
382
人口
-
68%
文明度
+
--
历元
+
--
疆域地块
+
--
人口
+
--
文明度
-
🪙 金币1280
-
✨ 灵气540
-
🪵 木材360
-
🪨 石料180
+
🪙 金币--
+
✨ 灵气--
+
🪵 木材--
+
🪨 石料--
- +
-
🏆 成就
-
-
🌟
开天辟地
已点亮第一块领土
-
🌱
文明摇篮
-
🔒
通天塔
建造 10 座奇观解锁
+
🏆 成就 -- / --
+
+
- + + + - - - +
@@ -4364,17 +4362,94 @@ if (typeof fitWorld === 'function') fitWorld(); trigger.setAttribute('aria-expanded', open ? 'true' : 'false'); }; - // 面板内容(游戏视角 demo):同步真实可读字段 - const uaName2 = document.getElementById('uaName2'); - const uaAv = document.getElementById('uaAvatarLg'); - const wnEl2 = document.getElementById('worldName'); - const eraEl = document.getElementById('calendarLabel'); - const tilesEl = document.getElementById('uaTiles'); - if (uaName2) uaName2.textContent = me.name || me.login; - if (uaAv) uaAv.textContent = (me.name || me.login || 'M').trim().charAt(0).toUpperCase(); - if (wnEl2) { const wn = document.getElementById('uaWorldName'); if (wn) wn.textContent = wnEl2.textContent; } - if (eraEl) { const er = document.getElementById('uaEra'); if (er) er.textContent = eraEl.textContent; } - if (tilesEl && typeof tiles !== 'undefined' && Array.isArray(tiles)) tilesEl.textContent = tiles.length; + // === 面板数据填充(从 me() 返回值动态渲染) === + const cachedMe = me; // 保存供弹窗复用 + function fillPanelData(d) { + const name = d.name || d.login || ''; + const uaName2 = document.getElementById('uaName2'); + if (uaName2) uaName2.textContent = name; + const uaAv = document.getElementById('uaAvatarLg'); + if (uaAv) uaAv.textContent = (name || 'M').trim().charAt(0).toUpperCase(); + const uaUid = document.getElementById('uaUid'); + if (uaUid) uaUid.textContent = d.game_uid || d.uid || '--'; + // 等级卡 + const uaLv = document.getElementById('uaLv'); + if (uaLv) uaLv.textContent = d.level || 1; + const uaTitle = document.getElementById('uaTitle'); + 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 `
+ ${icon} +
+
${a.name}
+
+
+
`; + }).join('') : '
暂无成就数据
'; + } + } + } 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) => { ev.stopPropagation(); @@ -4418,27 +4493,40 @@ if (typeof fitWorld === 'function') fitWorld(); if (e.target === appModal) closeAppModal(); }); - // 个人信息 + // 个人信息(从 cachedMe 动态渲染) const uaProfile = document.getElementById('uaProfile'); 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('👤 个人信息', `
-
M
+
${avatarLetter}
-
Mitchell
-
UID: 10086001
+
${name}
+
UID: ${d.game_uid || d.uid || '--'}
-
等级Lv.12
+
等级Lv.${d.level || 1}
-
+
-
EXP 640 / 1000
+
EXP ${d.exp || 0} / ${expMax}
-
金币🪙 1280
-
世界星元 1年 · 春
-
注册2026-07-20
+
称号${d.title || '萌新'}
+
金币🪙 ${d.coins || 0}
+
灵气✨ ${d.energy || 0}
+
木材🪵 ${d.wood || 0}
+
石料🪨 ${d.stone || 0}
+
世界${d.world_name || '我的世界'}
+
历元${d.era || '--'}
+
疆域${d.tiles || 0} 块
+
人口${d.population || 0}
+
文明度${d.civ_level || 0}
+
注册${d.reg_date || '--'}
`); }); @@ -4475,57 +4563,81 @@ if (typeof fitWorld === 'function') fitWorld(); }; }); - // 邮件 + // 邮件(从 API.mails() 动态取值) const uaMessages = document.getElementById('uaMessages'); - if (uaMessages) uaMessages.addEventListener('click', () => { - const mails = [ - { cat: '系统', title: '欢迎来到宇森', date: '2026-07-26', body: '请查收新手礼包与世界探索指南。', unread: true }, - { cat: '系统', title: '版本更新 v0.2', date: '2026-07-25', body: '新增世界导航与微观视图功能。', unread: false }, - { cat: '活动', title: '夏日创作大赛', date: '2026-07-24', body: '上传你的世界截图,赢限定头像框。', unread: true } - ]; - const cats = ['全部', '系统', '活动']; + if (uaMessages) uaMessages.addEventListener('click', async () => { + openAppModal('✉️ 邮件', '
加载中…
'); + let mails = []; + try { + const resp = await API.mails(); + mails = (resp && resp.items) || []; + } catch (e) { + if (appModalBody) appModalBody.innerHTML = '
邮件加载失败
'; + return; + } + const cats = ['全部', ...new Set(mails.map(m => m.category_label).filter(Boolean))]; 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 => `${c}`).join(''); const items = list.length - ? list.map(m => `
${m.unread ? '● ' : ''}${m.title}
${m.cat} · ${m.date}
${m.body}
`).join('') + ? list.map(m => `
+
${!m.is_read ? '● ' : ''}${m.subject || '(无标题)'}
+
${m.category_label || ''} · ${m.date || ''}${m.sender ? ' · ' + m.sender : ''}
+
${m.body || ''}
+
`).join('') : '
暂无邮件
'; if (appModalBody) appModalBody.innerHTML = `
${tabs}
${items}`; 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('全部'); }); - // 成就 + // 成就(从 API.achievements() 动态取值) const uaAchievements = document.getElementById('uaAchievements'); - if (uaAchievements) uaAchievements.addEventListener('click', () => { - openAppModal('🏆 成就', ` + if (uaAchievements) uaAchievements.addEventListener('click', async () => { + openAppModal('🏆 成就', '
加载中…
'); + let ach = { items: [], total_points: 0, unlocked_count: 0, total_count: 0 }; + try { + ach = await API.achievements(); + } catch (e) { + if (appModalBody) appModalBody.innerHTML = '
成就加载失败
'; + return; + } + const items = ach.items || []; + const html = `
-
320
成就点数
-
8 / 20
已解锁
+
${ach.total_points || 0}
成就点数
+
${ach.unlocked_count || 0} / ${ach.total_count || 0}
已解锁
-
-
🏆
-
初临世界
创建你的第一个世界
-
已达成
-
-
-
🌍
-
五界行者
解锁全部五层世界
-
已达成
-
-
-
🔒
-
大地建筑师
在大地世界放置 50 个地块
-
40%
-
-
-
🔒
-
熔岩征服者
在地心世界停留 10 分钟
-
15%
-
- `); + ${items.length ? items.map(a => { + const pct = a.target > 0 ? Math.min(100, (a.progress || 0) / a.target * 100) : (a.unlocked ? 100 : 0); + const icon = a.unlocked ? (a.icon || '🏆') : '🔒'; + return `
+
${icon}
+
+
${a.name}
+
${a.desc || ''}${a.tier_label ? ' [' + a.tier_label + ']' : ''} · ${a.points}点
+ ${a.unlocked ? '' : `
`} +
+
${a.unlocked ? '已达成' : Math.round(pct) + '%'}
+
`; + }).join('') : '
暂无成就数据
'} + `; + if (appModalBody) appModalBody.innerHTML = html; }); } catch (e) { location.replace('login.html?next=world.html'); return;