diff --git a/addons/game_base/__init__.py b/addons/game_base/__init__.py index f7209b1..6ecf639 100644 --- a/addons/game_base/__init__.py +++ b/addons/game_base/__init__.py @@ -1,2 +1,10 @@ from . import models from . import controllers + + +def pre_init_hook(cr): + """模块升级前清理旧的 game.currency 记录,避免模型删除时产生冲突。""" + cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name='game_currency'") + if cr.fetchone(): + cr.execute("DELETE FROM game_currency") + cr.execute("DELETE FROM ir_model_data WHERE model='game.currency'") diff --git a/addons/game_base/__manifest__.py b/addons/game_base/__manifest__.py index 6fd9f7c..8fd8aba 100644 --- a/addons/game_base/__manifest__.py +++ b/addons/game_base/__manifest__.py @@ -1,16 +1,24 @@ { - 'name': 'Yusen Game Base', + 'name': '宇森游戏基础数据', 'summary': '宇森游戏基础数据:图鉴 / 规则 / 论坛 / 公告', - 'version': '1.1', + 'version': '1.2', 'category': 'Game', 'sequence': 10, 'depends': ['base'], 'data': [ 'security/ir.model.access.csv', 'data/game_base_data.xml', - 'views/game_base_views.xml', + 'views/codex_views.xml', + 'views/rule_category_views.xml', + 'views/rule_views.xml', + 'views/page_views.xml', + 'views/forum_views.xml', + 'views/announcement_views.xml', + 'views/story_views.xml', + 'views/menu.xml', ], 'application': True, 'license': 'LGPL-3', + 'pre_init_hook': 'pre_init_hook', 'description': '基于官网内容沉淀的游戏基础数据模型,供后台维护、前端经 /game/api/* 读取;并提供与 Odoo 用户打通的登录/注册接口。', } diff --git a/addons/game_base/controllers/game_api.py b/addons/game_base/controllers/game_api.py index f57e62f..02ecff3 100644 --- a/addons/game_base/controllers/game_api.py +++ b/addons/game_base/controllers/game_api.py @@ -63,42 +63,86 @@ class GameApiController(http.Controller): @http.route('/game/api/rules', type='http', auth='public', methods=['GET'], csrf=False) def rules(self, **kw): - """规则 / 等级制度:按类别(type)分组。""" - model = request.env['game.rule'].sudo() - type_labels = dict(model._fields['type'].selection) - recs = model.search_read([], ['id', 'type', 'level', 'name', 'desc']) - by_type = {} + """规则 / 等级制度 / 货币:按类别分组。""" + Rule = request.env['game.rule'].sudo() + Category = request.env['game.rule.category'].sudo() + cats = Category.search_read( + [('active', '=', True)], + ['id', 'key', 'name', 'sequence'], + order='sequence, id' + ) + cat_map = {c['id']: c for c in cats} + recs = Rule.search_read( + [], + ['id', 'category_id', 'level', 'name', 'desc', 'rate', 'note'], + order='category_id, level' + ) + by_cat = {} for r in recs: - by_type.setdefault(r['type'], []).append(r) + cid = r['category_id'][0] if r['category_id'] else None + if cid not in cat_map: + continue + by_cat.setdefault(cid, []).append(r) groups = [] - for t in by_type: + for c in cats: + items = by_cat.get(c['id'], []) + if not items: + continue groups.append({ - 'key': t, - 'label': type_labels.get(t, t), + 'key': c['key'], + 'label': c['name'], 'items': [{ - 'id': r['id'], - 'level': r['level'], - 'name': r['name'], - 'desc': r['desc'] or '', - } for r in by_type[t]], + 'id': it['id'], + 'level': it['level'], + 'name': it['name'], + 'desc': it['desc'] or '', + 'rate': it.get('rate') or 1, + 'note': it.get('note') or '', + } for it in items], }) return _json({'groups': groups}) @http.route('/game/api/currencies', type='http', auth='public', methods=['GET'], csrf=False) def currencies(self, **kw): - """货币等级:按 level 排序返回。""" - recs = request.env['game.currency'].sudo().search_read( - [], ['id', 'level', 'name', 'desc', 'rate'], order='level asc' + """货币等级:兼容旧接口,实际从 game.rule 按类别过滤。""" + Rule = request.env['game.rule'].sudo() + Category = request.env['game.rule.category'].sudo() + currency_keys = ['currency', 'material', 'token', 'honor'] + cats = Category.search_read( + [('active', '=', True), ('key', 'in', currency_keys)], + ['id', 'key', 'name', 'sequence'], + order='sequence, id' ) - return _json({ - 'items': [{ - 'id': r['id'], - 'level': r['level'], - 'name': r['name'], - 'desc': r['desc'] or '', - 'rate': r['rate'] or 1, - } for r in recs] - }) + cat_map = {c['id']: c for c in cats} + recs = Rule.search_read( + [('category_id.key', 'in', currency_keys)], + ['id', 'category_id', 'level', 'name', 'desc', 'note', 'rate'], + order='category_id, level' + ) + by_type = {} + for r in recs: + cid = r['category_id'][0] if r['category_id'] else None + if cid not in cat_map: + continue + by_type.setdefault(cid, []).append(r) + groups = [] + for c in cats: + items = by_type.get(c['id'], []) + if not items: + continue + groups.append({ + 'key': c['key'], + 'label': c['name'], + 'items': [{ + 'id': it['id'], + 'level': it['level'], + 'name': it['name'], + 'desc': it['desc'] or '', + 'note': it.get('note') or '', + 'rate': it.get('rate') or 1, + } for it in items], + }) + return _json({'groups': groups}) @http.route('/game/api/pages', type='http', auth='public', methods=['GET'], csrf=False) def pages(self, **kw): diff --git a/addons/game_base/data/game_base_data.xml b/addons/game_base/data/game_base_data.xml index 5c74301..df6d5f5 100644 --- a/addons/game_base/data/game_base_data.xml +++ b/addons/game_base/data/game_base_data.xml @@ -1,93 +1,127 @@ + + + world + 世界等级 + 10 + + + faction + 势力等级 + 20 + + + character + 人物角色等级 + 30 + + + currency + 货币等级 + 40 + + - world1凡墟 + 1凡墟 - world2灵启 + 2灵启 - world3玄界 + 3玄界 - world4穹冥 + 4穹冥 - world5溯元 + 5溯元 - faction1墟族 + 1墟族 - faction2灵聚 + 2灵聚 - faction3玄邑 + 3玄邑 - faction4冥朝 + 4冥朝 - faction5穹统 + 5穹统 - faction6溯盟 + 6溯盟 - character1萌新 - character2学徒 - character3熟手 - character4好手 - character5精英 - character6大师 - character7宗匠 - character8传奇 - character9神话 - character10半神 - character11圣者 - character12永恒 + 1萌新 + 2学徒 + 3熟手 + 4好手 + 5精英 + 6大师 + 7宗匠 + 8传奇 + 9神话 + 10半神 + 11圣者 + 12永恒 - - - 1墟铢100 + + + 1墟铢 + 100 + - - 2灵币100 + + 2灵币 + 100 + - - 3玄锭100 + + 3玄锭 + 100 + - - 4冥玉100 + + 4冥玉 + 100 + - - 5穹鎏100 + + 5穹鎏 + 100 + - - 6元玺1 + + 6元玺 + 1 + @@ -183,6 +217,32 @@ 每个 NPC 都有独立的天赋、性格与成长轨迹。英雄在历练中觉醒,传奇在时间中自发生成,无人是棋子。 + + + intro + 关于宇森 + ABOUT YUSEN + 15 + 宇森,是一颗会自己呼吸的种子。

+

你播种的不是一个角色,而是一个世界——从地心熔岩到陨石星空,五层垂直切面层层相托,文明在其中自发地生长、分裂、兼并、飞升。没有脚本替你演绎剧情,没有关卡替你定义胜利,每一次开局都是一部独一无二的编年史。

+

我们相信,最好的模拟不在于你操控了多少,而在于你见证了多少。所以宇森把「玩家」重新定义为园丁:你不能命令谁,却可以改变土壤、调节灵气、在关键节点轻轻推一把。世界的走向,由无数个体的选择汇聚而成,而你只是其中一股温柔的力。

+

往下翻,看这颗种子如何长成一片森林。

]]>
+
+ + + 10 + 🌍 + 一颗会呼吸的世界 + 五层垂直世界,文明自发演化,没有固定剧情与通关条件。 + + + + 20 + 🧑‍🌾 + 你是园丁,不是上帝 + 改变土壤与灵气,在关键节点轻推一把,而非发号施令。 + + background @@ -233,6 +293,48 @@ 本源之地 · 元玺凝聚 + + + notes + 制度说明 + HOW THE WORLD WORKS + 25 + 宇森的世界由三套相互独立的等级体系共同驱动,它们彼此加成,又彼此制约:

+

世界等级是舞台的上限——它决定这片土地上「能发生什么」。从凡墟到溯元,每一级跃迁都会解锁更深的内容层(更深的地下、更远的星空)与更厚的事件池。

+

势力等级是群体的进度——它决定文明「能调动什么」。聚落、联盟到多维联合体,等级越高,可调度资源与外交权重越大。

+

角色等级是个体的成长——它决定一个生命「能做成什么」。从无名萌新到超越生死的永恒,英雄在历练中觉醒。

+

而贯穿三者的,是六档货币:从凡俗墟铢到本源元玺,百进制兑换构成完整经济闭环,决定文明「能交换什么」。

+

园丁的核心策略,便是在这四方之间分配有限的引导资源,让文明自洽地向上演化——每一级提升,世界都因此越活越厚。

]]>
+
+ + + 10 + 🌐 + 世界等级 + 舞台上限:决定能发生什么。凡墟 → 灵启 → 玄界 → 穹冥 → 溯元。 + + + + 20 + 🏛️ + 势力等级 + 群体进度:决定能调动什么。墟族 → 灵聚 → 玄邑 → 冥朝 → 穹统 → 溯盟。 + + + + 30 + ⚔️ + 角色等级 + 个体成长:决定能做成什么。萌新 → …… → 永恒,十二阶序。 + + + + 40 + 💰 + 六档货币 + 经济闭环:决定能交换什么。墟铢 → 灵币 → 玄锭 → 冥玉 → 穹鎏 → 元玺。 + + letter diff --git a/addons/game_base/migrations/1.2/pre-migrate.py b/addons/game_base/migrations/1.2/pre-migrate.py new file mode 100644 index 0000000..f1d6c87 --- /dev/null +++ b/addons/game_base/migrations/1.2/pre-migrate.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +def migrate(cr, version): + """升级到 1.2:清理旧的 game.currency 记录与外部 ID,避免合并到 game.rule 时冲突。""" + # 先删数据表记录,再删对应 ir.model.data 行 + cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name='game_currency'") + if cr.fetchone(): + cr.execute("DELETE FROM game_currency") + cr.execute("DELETE FROM ir_model_data WHERE model='game.currency'") diff --git a/addons/game_base/models/__init__.py b/addons/game_base/models/__init__.py index 5ecebe0..42dfeb2 100644 --- a/addons/game_base/models/__init__.py +++ b/addons/game_base/models/__init__.py @@ -1,7 +1,7 @@ from . import codex from . import forum from . import announcement +from . import rule_category from . import rule -from . import currency from . import page from . import story diff --git a/addons/game_base/models/currency.py b/addons/game_base/models/currency.py deleted file mode 100644 index 4e4e0fd..0000000 --- a/addons/game_base/models/currency.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- -from odoo import models, fields, api - - -class GameCurrency(models.Model): - _name = 'game.currency' - _description = '游戏货币等级' - _order = 'level asc' - - level = fields.Integer(string='等级', required=True) - name = fields.Char(string='名称', required=True) - desc = fields.Text(string='描述') - rate = fields.Integer( - string='兑换比例', - default=1, - help='多少单位本货币可兑换 1 单位下一等级货币。例如 100 墟铢 = 1 灵币,则墟铢的 rate 为 100。' - ) - - @api.model - def convert(self, amount, from_level, to_level): - """按等级汇率将 amount 从 from_level 换算为 to_level 货币。 - - 汇率方向:低等级 → 高等级 为除以 rate,高等级 → 低等级 为乘以 rate。 - 最高等级货币(元玺)的 rate 固定为 1,仅作占位。 - """ - if from_level == to_level: - return amount - currencies = self.search_read([], ['level', 'rate'], order='level asc') - rate_map = {c['level']: c['rate'] or 1 for c in currencies} - - if from_level < to_level: - # 低等级 → 高等级:逐级除以兑换比例 - result = amount - for lv in range(from_level, to_level): - result = result / rate_map.get(lv, 1) - return result - else: - # 高等级 → 低等级:逐级乘以兑换比例 - result = amount - for lv in range(to_level, from_level): - result = result * rate_map.get(lv, 1) - return result diff --git a/addons/game_base/models/rule.py b/addons/game_base/models/rule.py index cdf9ca1..0b6c76f 100644 --- a/addons/game_base/models/rule.py +++ b/addons/game_base/models/rule.py @@ -1,19 +1,64 @@ -from odoo import models, fields +# -*- coding: utf-8 -*- +from odoo import models, fields, api class GameRule(models.Model): _name = 'game.rule' - _description = '规则 / 等级制度' + _description = '规则 / 等级制度 / 货币' _rec_name = 'name' - _order = 'type, level' + _order = 'category_id, level' - TYPE = [ - ('world', '世界等级'), - ('faction', '势力等级'), - ('character', '人物角色等级'), - ] - - type = fields.Selection(TYPE, string='类别', required=True, index=True) - level = fields.Integer(string='等级', index=True) + category_id = fields.Many2one( + 'game.rule.category', + string='类别', + required=True, + index=True, + ondelete='restrict', + ) + level = fields.Integer( + string='等级', + index=True, + aggregator=False, # 分组时不累计 + ) name = fields.Char(string='名称', required=True) desc = fields.Html(string='说明') + rate = fields.Integer( + string='兑换比例', + default=1, + help='多少单位本货币可兑换 1 单位下一等级货币。例如 100 墟铢 = 1 灵币,则墟铢的 rate 为 100。仅货币类有效。', + ) + note = fields.Text( + string='备注', + help='额外说明,仅后台可见(如发行背景、流通范围等)。', + ) + + @api.model + def convert(self, amount, from_level, to_level, category_key='currency'): + """按等级汇率将 amount 从 from_level 换算为 to_level 货币。 + + 默认只在「货币」类别内换算。汇率方向:低等级 → 高等级 为除以 rate, + 高等级 → 低等级 为乘以 rate。最高等级货币(元玺)的 rate 固定为 1。 + """ + if from_level == to_level: + return amount + category = self.env['game.rule.category'].sudo().search( + [('key', '=', category_key)], limit=1 + ) + if not category: + return amount + recs = self.search( + [('category_id', '=', category.id)], + order='level asc', + ) + rate_map = {r.level: r.rate or 1 for r in recs} + + if from_level < to_level: + result = amount + for lv in range(from_level, to_level): + result = result / rate_map.get(lv, 1) + return result + else: + result = amount + for lv in range(to_level, from_level): + result = result * rate_map.get(lv, 1) + return result diff --git a/addons/game_base/models/rule_category.py b/addons/game_base/models/rule_category.py new file mode 100644 index 0000000..8c2b79b --- /dev/null +++ b/addons/game_base/models/rule_category.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields + + +class GameRuleCategory(models.Model): + _name = 'game.rule.category' + _description = '规则类别' + _order = 'sequence, id' + _rec_name = 'name' + + _sql_constraints = [ + ('key_uniq', 'unique(key)', '规则类别标识必须唯一'), + ] + + key = fields.Char(string='标识', required=True, index=True) + name = fields.Char(string='名称', required=True) + sequence = fields.Integer(string='排序', default=10) + active = fields.Boolean(string='启用', default=True) diff --git a/addons/game_base/security/ir.model.access.csv b/addons/game_base/security/ir.model.access.csv index d588c72..d0a25cc 100644 --- a/addons/game_base/security/ir.model.access.csv +++ b/addons/game_base/security/ir.model.access.csv @@ -7,10 +7,10 @@ access_game_forum_post,game.forum.post public,model_game_forum_post,base.group_p access_game_forum_post_user,game.forum.post user,model_game_forum_post,base.group_user,1,1,1,1 access_game_announcement,game.announcement public,model_game_announcement,base.group_public,1,0,0,0 access_game_announcement_user,game.announcement user,model_game_announcement,base.group_user,1,1,1,1 +access_game_rule_category,game.rule.category public,model_game_rule_category,base.group_public,1,0,0,0 +access_game_rule_category_user,game.rule.category user,model_game_rule_category,base.group_user,1,1,1,1 access_game_rule,game.rule public,model_game_rule,base.group_public,1,0,0,0 access_game_rule_user,game.rule user,model_game_rule,base.group_user,1,1,1,1 -access_game_currency,game.currency public,model_game_currency,base.group_public,1,0,0,0 -access_game_currency_user,game.currency user,model_game_currency,base.group_user,1,1,1,1 access_game_page,game.page public,model_game_page,base.group_public,1,0,0,0 access_game_page_user,game.page user,model_game_page,base.group_user,1,1,1,1 access_game_page_item,game.page.item public,model_game_page_item,base.group_public,1,0,0,0 diff --git a/addons/game_base/views/announcement_views.xml b/addons/game_base/views/announcement_views.xml new file mode 100644 index 0000000..1b8213a --- /dev/null +++ b/addons/game_base/views/announcement_views.xml @@ -0,0 +1,30 @@ + + + + + + 公告 + game.announcement + list,form + + + + game.announcement.list + game.announcement + + + + + + + game.announcement.form + game.announcement + +
+ + + +
+
+ +
diff --git a/addons/game_base/views/codex_views.xml b/addons/game_base/views/codex_views.xml new file mode 100644 index 0000000..daafb14 --- /dev/null +++ b/addons/game_base/views/codex_views.xml @@ -0,0 +1,39 @@ + + + + + + 图鉴 + game.codex.entry + list,form + {'group_by': 'cat'} + + + + game.codex.entry.list + game.codex.entry + + + + + + + + + game.codex.entry.form + game.codex.entry + +
+ + + + + + + + + + +
+ +
diff --git a/addons/game_base/views/forum_views.xml b/addons/game_base/views/forum_views.xml new file mode 100644 index 0000000..080c6a5 --- /dev/null +++ b/addons/game_base/views/forum_views.xml @@ -0,0 +1,61 @@ + + + + + + 主题 + game.forum.thread + list,form + + + + game.forum.thread.list + game.forum.thread + + + + + + + + + game.forum.thread.form + game.forum.thread + +
+ + + + + + + + + + +
+ + + + 回复 + game.forum.post + list,form + + + + game.forum.post.list + game.forum.post + + + + + + + game.forum.post.form + game.forum.post + +
+ +
+ +
diff --git a/addons/game_base/views/game_base_views.xml b/addons/game_base/views/game_base_views.xml deleted file mode 100644 index e9c4add..0000000 --- a/addons/game_base/views/game_base_views.xml +++ /dev/null @@ -1,261 +0,0 @@ - - - - - - - - - 图鉴 - game.codex.entry - list,form - - - - - game.codex.entry.list - game.codex.entry - - - - - - - - game.codex.entry.form - game.codex.entry - -
- - - - - - - - - - -
- - - - 规则 - game.rule - list,form - - - - game.rule.list - game.rule - - - - - - game.rule.form - game.rule - -
-
-
- - - - 货币等级 - game.currency - list,form - - - - game.currency.list - game.currency - - - - - - game.currency.form - game.currency - -
-
-
- - - - 页面板块 - game.page - list,form - - - - game.page.list - game.page - - - - - - - - game.page.form - game.page - -
- - - - - - - - - - - - - - - -
-
-
- - - - - 主题 - game.forum.thread - list,form - - - - 回复 - game.forum.post - list,form - - - - - game.forum.thread.list - game.forum.thread - - - - - - - - game.forum.thread.form - game.forum.thread - -
- - - - - - - - - - -
- - game.forum.post.list - game.forum.post - - - - - - game.forum.post.form - game.forum.post - -
- -
- - - - 公告 - game.announcement - list,form - - - - game.announcement.list - game.announcement - - - - - - game.announcement.form - game.announcement - -
- - - -
-
- - - - - - game.story.volume - list,form - - - - 章节 - game.story.chapter - list,form - - - - - game.story.volume.list - game.story.volume - - - - - - game.story.volume.form - game.story.volume - -
- - - - - - - - -
-
- - game.story.chapter.list - game.story.chapter - - - - - - game.story.chapter.form - game.story.chapter - -
- - - -
-
- -
diff --git a/addons/game_base/views/menu.xml b/addons/game_base/views/menu.xml new file mode 100644 index 0000000..daa2008 --- /dev/null +++ b/addons/game_base/views/menu.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/game_base/views/page_views.xml b/addons/game_base/views/page_views.xml new file mode 100644 index 0000000..ac0ff40 --- /dev/null +++ b/addons/game_base/views/page_views.xml @@ -0,0 +1,45 @@ + + + + + + 页面板块 + game.page + list,form + + + + game.page.list + game.page + + + + + + + + + game.page.form + game.page + +
+ + + + + + + + + + + + + + + +
+
+
+ +
diff --git a/addons/game_base/views/rule_category_views.xml b/addons/game_base/views/rule_category_views.xml new file mode 100644 index 0000000..8df06ff --- /dev/null +++ b/addons/game_base/views/rule_category_views.xml @@ -0,0 +1,39 @@ + + + + + + 规则类别 + game.rule.category + list,form + + + + game.rule.category.list + game.rule.category + + + + + + + + + + + + game.rule.category.form + game.rule.category + +
+ + + + + + +
+
+
+ +
diff --git a/addons/game_base/views/rule_views.xml b/addons/game_base/views/rule_views.xml new file mode 100644 index 0000000..76f6c47 --- /dev/null +++ b/addons/game_base/views/rule_views.xml @@ -0,0 +1,43 @@ + + + + + + 规则 + game.rule + list,form + {'group_by': 'category_id'} + + + + game.rule.list + game.rule + + + + + + + + + + + game.rule.form + game.rule + +
+ + + + + + + + + + + +
+
+ +
diff --git a/addons/game_base/views/story_views.xml b/addons/game_base/views/story_views.xml new file mode 100644 index 0000000..5105b75 --- /dev/null +++ b/addons/game_base/views/story_views.xml @@ -0,0 +1,61 @@ + + + + + + + game.story.volume + list,form + + + + game.story.volume.list + game.story.volume + + + + + + + game.story.volume.form + game.story.volume + +
+ + + + + + + + +
+
+ + + + 章节 + game.story.chapter + list,form + + + + game.story.chapter.list + game.story.chapter + + + + + + + game.story.chapter.form + game.story.chapter + +
+ + + +
+
+ +
diff --git a/addons/yt_world/views/yt_world_views.xml b/addons/yt_world/views/yt_world_views.xml index 1890677..ce8e500 100644 --- a/addons/yt_world/views/yt_world_views.xml +++ b/addons/yt_world/views/yt_world_views.xml @@ -15,8 +15,6 @@ - - @@ -31,8 +29,6 @@ - - diff --git a/assets/art/transitions/transition_adventure.png b/assets/art/transitions/transition_adventure.png new file mode 100644 index 0000000..a176842 Binary files /dev/null and b/assets/art/transitions/transition_adventure.png differ diff --git a/assets/art/transitions/transition_coast.png b/assets/art/transitions/transition_coast.png new file mode 100644 index 0000000..539910d Binary files /dev/null and b/assets/art/transitions/transition_coast.png differ diff --git a/assets/art/transitions/transition_dawn.png b/assets/art/transitions/transition_dawn.png new file mode 100644 index 0000000..16b0f38 Binary files /dev/null and b/assets/art/transitions/transition_dawn.png differ diff --git a/assets/art/transitions/transition_dungeon.png b/assets/art/transitions/transition_dungeon.png new file mode 100644 index 0000000..bc45cbc Binary files /dev/null and b/assets/art/transitions/transition_dungeon.png differ diff --git a/assets/art/transitions/transition_forest.png b/assets/art/transitions/transition_forest.png new file mode 100644 index 0000000..0cd4e6d Binary files /dev/null and b/assets/art/transitions/transition_forest.png differ diff --git a/assets/art/transitions/transition_night.png b/assets/art/transitions/transition_night.png new file mode 100644 index 0000000..ba0ff4a Binary files /dev/null and b/assets/art/transitions/transition_night.png differ diff --git a/assets/art/transitions/transition_sky.png b/assets/art/transitions/transition_sky.png new file mode 100644 index 0000000..1448d27 Binary files /dev/null and b/assets/art/transitions/transition_sky.png differ diff --git a/assets/art/transitions/transition_skycity.png b/assets/art/transitions/transition_skycity.png new file mode 100644 index 0000000..64e1c5a Binary files /dev/null and b/assets/art/transitions/transition_skycity.png differ diff --git a/index.html b/index.html index b0ac6e8..ac8424a 100644 --- a/index.html +++ b/index.html @@ -36,7 +36,7 @@ .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;} + .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;} @@ -87,7 +87,24 @@ .card p{font-size:13px;color:var(--text-dim);line-height:1.6;} /* ===== 背景 ===== */ - #background{flex-direction:row;gap:38px;align-items:stretch;} + #background{flex-direction:column;align-items:stretch;gap:0;overflow-y:auto;padding:48px 6% 90px;} + .bg-page{max-width:1060px;width:100%;margin:0 auto;padding:30px 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;} @@ -155,7 +172,7 @@ .threads{flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:10px;padding-right:6px;} .th{display:flex;align-items:center;gap:14px;background:var(--card);border:1px solid var(--card-bd);border-radius:11px;padding:13px 16px;transition:.2s;cursor:pointer;} .th:hover{border-color:var(--violet);background:rgba(139,92,246,.10);} - .th .pin{font-size:10px;font-family:"Press Start 2P",monospace;color:var(--gold);} + .th .pin{font-size:11px;font-weight:700;letter-spacing:1px;color:var(--gold);background:rgba(231,184,92,.14);border:1px solid rgba(231,184,92,.42);border-radius:5px;padding:1px 7px;} .th .tt{flex:1;font-size:15px;color:#fff;} .th .meta{font-size:12px;color:var(--text-dim);white-space:nowrap;} .th .rp{color:var(--cyan);font-weight:500;} @@ -221,6 +238,55 @@ .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-y:auto;padding:46px 6% 90px;} + #worldDataContent{max-width:1080px;width:100%;margin:0 auto;} + .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(190px,1fr));gap:16px;} + .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:18px 20px;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:34px;line-height:1.1;color:#fff;margin:8px 0 2px;} + .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){ .grid{grid-template-columns:repeat(2,1fr);} #background,#forum{flex-direction:column;overflow-y:auto;} @@ -238,9 +304,8 @@
- +
-
PIN【公告】宇森官网第一阶段上线,欢迎园丁们入驻128 回复 · 官方
+
置顶【公告】宇森官网第一阶段上线,欢迎园丁们入驻128 回复 · 官方
新手向:园丁视角的三种开局思路42 回复 · 行者
你们更喜欢哪种族?来投个票76 回复 · 雾岛
手绘了我的主世界浮空岛,求轻喷31 回复 · 青柠
@@ -415,10 +480,7 @@
世界数据WORLD DATA
-
-
🌍
- 加载中… -
+
@@ -434,6 +496,13 @@ document.getElementById(b.dataset.tab).classList.add('active'); })); + // logo 点击回主页(intro 面板) + document.getElementById('logoHome').addEventListener('click',()=>{ + navBtns.forEach(x=>x.classList.remove('active')); + panels.forEach(p=>p.classList.remove('active')); + document.getElementById('intro').classList.add('active'); + }); + // ============ 程序化矢量缩略图(零 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;};} @@ -668,6 +737,128 @@ } refreshUserBox(); + // ============ 背景页:从 odoo 拉取页面板块渲染 ============ + async function renderBackground(){ + const box = document.getElementById('background'); + if(!box) return; + try{ + const r = await fetch('/game/api/pages?db=game', {credentials:'same-origin'}); + const d = await r.json(); + const pages = (d && d.items) || []; + if(!pages.length){ box.innerHTML = '
板块内容即将上线…
'; return; } + box.innerHTML = pages.map(p=>{ + const cards = (p.items||[]).map(it=>` +
+
${it.icon||'✦'}
+
+
${it.title||''}
+
${it.desc||''}
+
+
`).join(''); + return ` +
+
+

${p.title||''}

+ ${p.subtitle?`
${p.subtitle}
`:''} +
+ ${p.body_html?`
${p.body_html}
`:''} + ${cards?`
${cards}
`:''} +
`; + }).join(''); + }catch(e){ + box.innerHTML = '
板块加载失败,请刷新重试。
'; + } + } + renderBackground(); + + // ============ 世界数据:大报表(演示数据,后端接口待接入) ============ + async function renderWorldData(){ + const box = document.getElementById('worldDataContent'); + if(!box) return; + // TODO: 后端接入后改为 await API.worldData() 取真实数据 + const data = worldDataMock(); + const kpis = data.kpi.map(k=>` +
+ ${k.delta>=0?'▲':'▼'} ${Math.abs(k.delta)}% +
${k.icon}
+
${k.num}${k.unit||''}
+
${k.label}
+
`).join(''); + const bars = data.bars.map(b=>` +
+
${b.k}${b.v}%
+
+
`).join(''); + const rows = data.layers.map(l=>` + + ${l.lv} + ${l.band} + ${l.eco} + ${l.pop} + ${l.terr} + `).join(''); + const ranks = data.factions.map((f,i)=>` +
+
${i+1}
+
+
${f.name}${f.score} 影响力
+
+
+
`).join(''); + box.innerHTML = ` + 演示数据 · 后端接口待接入 +
${kpis}
+ +
核心指标进度 CORE METRICS
+
${bars}
+ +
世界五层结构 WORLD LAYERS
+ + + ${rows} +
层级高度带地表生态人口占比代表地形
+ +
势力影响力排行 FACTION RANKING
+
${ranks}
+ `; + } + function worldDataMock(){ + return { + kpi:[ + {icon:'👥',num:'12,840',unit:'人',label:'世界总人口',delta:3}, + {icon:'🌍',num:'Lv.7',unit:'',label:'当前世界等级',delta:1}, + {icon:'🏛️',num:'38',unit:'个',label:'活跃势力',delta:5}, + {icon:'⛪',num:'9.6M',unit:'',label:'信仰值总量',delta:8}, + {icon:'⚔️',num:'214',unit:'',label:'在线角色',delta:-2}, + {icon:'💎',num:'1.3M',unit:'',label:'流通晶币',delta:4} + ], + bars:[ + {k:'世界安定度',v:78}, + {k:'生态健康度',v:64}, + {k:'信仰传播度',v:88}, + {k:'文明探索度',v:52}, + {k:'势力平衡度',v:71}, + {k:'资源充裕度',v:60} + ], + layers:[ + {lv:'第一层 · 地表',band:'0–80m',eco:'平原 / 森林 / 河流',pop:'58%',terr:'草原城邦'}, + {lv:'第二层 · 丘陵',band:'80–260m',eco:'梯田 / 矿脉 / 林地',pop:'22%',terr:'山道驿站'}, + {lv:'第三层 · 高地',band:'260–600m',eco:'苔原 / 雪线 / 圣峰',pop:'12%',terr:'云端神殿'}, + {lv:'第四层 · 地下',band:'-30–-200m',eco:'溶洞 / 晶矿 / 暗河',pop:'6%',terr:'地心工坊'}, + {lv:'第五层 · 秘境',band:'位面裂隙',eco:'虚空 / 混沌 / 星界',pop:'2%',terr:'裂隙哨站'} + ], + factions:[ + {name:'曦光同盟',score:9420,pct:100}, + {name:'玄铁工盟',score:8730,pct:93}, + {name:'碧波联邦',score:7610,pct:81}, + {name:'夜枭议会',score:6880,pct:73}, + {name:'荒原游商',score:5240,pct:56}, + {name:'星语祭司团',score:4310,pct:46} + ] + }; + } + renderWorldData(); + // ============ 进入游戏:已登录→世界页;未登录→登录页 ============ const enterBtn = document.getElementById('enterGame'); if(enterBtn) enterBtn.addEventListener('click', async ()=>{ diff --git a/website/announce_preview.html b/website/announce_preview.html new file mode 100644 index 0000000..7a93f9f --- /dev/null +++ b/website/announce_preview.html @@ -0,0 +1,144 @@ + + + + + +公告 · 新版预览 + + + + + + +
+ +
+

公 告

+

官方通知 · ANNOUNCEMENTS

+
+
+
+ +
⚡ 新版公告预览 · 点击卡片可弹窗
+ + + + + + + + + + + \ No newline at end of file diff --git a/website/assets/site-app.js b/website/assets/site-app.js index 2fc071b..cfbc77e 100644 --- a/website/assets/site-app.js +++ b/website/assets/site-app.js @@ -49,17 +49,111 @@ async function loadForum(){ } catch (e) { /* 保留静态兜底内容 */ } } -// ---------- 公告 ---------- +// ---------- 公告(卡片 + 弹窗) ---------- +function annEscapeHtml(s) { + return String(s == null ? '' : s) + .replace(/&/g, '&').replace(//g, '>') + .replace(/"/g, '"').replace(/'/g, '''); +} +function annFormatDate(d) { + // "2026-07-09" -> "2026.07.09" + return d ? String(d).replace(/-/g, '.') : ''; +} +function annStripHtml(html) { + const tmp = document.createElement('div'); + tmp.innerHTML = html || ''; + return (tmp.textContent || '').replace(/\s+/g, ' ').trim(); +} + async function loadAnnounce(){ - const el = document.querySelector('#announce .notes'); - if (!el) return; + const root = document.getElementById('announce'); + if (!root) return; + root.innerHTML = '
加载中…
'; + let list = []; try { - const list = await API.announcements(); - if (!list || !list.length){ el.innerHTML = '
暂无公告
'; return; } - el.innerHTML = list.map(n => - `
${n.date}
${n.body_html || ''}
` - ).join(''); - } catch (e) { /* 保留静态兜底内容 */ } + list = await API.announcements(); + } catch (e) { /* 落到空态 */ } + if (!list || !list.length) { + root.innerHTML = '
暂无公告
'; + return; + } + root.innerHTML = list.map((n, i) => { + const pri = n.priority || 'normal'; + const preview = annStripHtml(n.body_html).slice(0, 160); + return ` +
+
+ ${annEscapeHtml(annFormatDate(n.date))} + + ${annEscapeHtml(n.priority_label || pri)} +
+
${annEscapeHtml(n.name)}
+
${annEscapeHtml(preview)}
+
阅读全文
+
`; + }).join(''); + + root.querySelectorAll('.announce-card').forEach(card => { + const idx = +card.dataset.idx; + const open = () => openAnnounceModal(list[idx]); + card.addEventListener('click', open); + card.addEventListener('keydown', e => { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); } + }); + }); +} + +// ---- 弹窗(懒创建一次) ---- +let _annModal = null; +let _annLastFocus = null; +function ensureAnnounceModal() { + if (_annModal) return _annModal; + _annModal = document.createElement('div'); + _annModal.className = 'announce-modal'; + _annModal.setAttribute('role', 'presentation'); + _annModal.innerHTML = ` +
+ `; + document.body.appendChild(_annModal); + _annModal.addEventListener('click', e => { + if (e.target.closest('[data-close]')) closeAnnounceModal(); + }); + document.addEventListener('keydown', e => { + if (e.key === 'Escape' && _annModal.classList.contains('open')) closeAnnounceModal(); + }); + return _annModal; +} +function openAnnounceModal(item) { + if (!item) return; + const m = ensureAnnounceModal(); + m.querySelector('[data-title]').textContent = item.name || ''; + m.querySelector('[data-meta]').textContent = + [annFormatDate(item.date), item.priority_label].filter(Boolean).join(' · '); + // body_html 来自后台管理员撰写,可信;非管理员输入路径不存在,OK 直接渲染 + m.querySelector('[data-body]').innerHTML = item.body_html || '

(无正文)

'; + _annLastFocus = document.activeElement; + m.classList.add('open'); + document.body.style.overflow = 'hidden'; + // 焦点移入弹窗(关闭按钮) + const closeBtn = m.querySelector('.close'); + if (closeBtn) closeBtn.focus({ preventScroll: true }); +} +function closeAnnounceModal() { + if (!_annModal || !_annModal.classList.contains('open')) return; + _annModal.classList.remove('open'); + document.body.style.overflow = ''; + if (_annLastFocus && _annLastFocus.focus) _annLastFocus.focus({ preventScroll: true }); } document.addEventListener('DOMContentLoaded', () => { diff --git a/website/auth.css b/website/auth.css index 60085fa..a33f533 100644 --- a/website/auth.css +++ b/website/auth.css @@ -144,3 +144,282 @@ .login-footer a:hover { text-decoration: underline; } + +/* ================================================== + 公告页 · 大气版(卡片 + 弹窗) + ================================================== */ + +/* ---- 页面头 ---- */ +#panel-announce h1 { + font-size: 44px; + letter-spacing: 8px; + margin-bottom: 10px; + text-align: center; +} +#panel-announce .page-sub { + text-align: center; + font-size: 13px; + letter-spacing: 5px; + color: #6b8aa8; + display: block; + margin: 0 auto 36px; + position: relative; + padding: 0 32px; + width: max-content; +} +#panel-announce .page-sub::before, +#panel-announce .page-sub::after { + content: ""; + position: absolute; + top: 50%; + width: 36px; + height: 1px; + background: linear-gradient(90deg, transparent, #4af, transparent); + opacity: .7; +} +#panel-announce .page-sub::before { right: 100%; } +#panel-announce .page-sub::after { left: 100%; } + +/* ---- 卡片列表 ---- */ +.announce-list { + display: flex; + flex-direction: column; + gap: 14px; +} +.announce-card { + position: relative; + background: linear-gradient(135deg, rgba(20, 28, 48, 0.72), rgba(12, 18, 32, 0.72)); + border: 1px solid rgba(120, 140, 200, 0.14); + border-left: 3px solid #a78bfa; + border-radius: 12px; + padding: 22px 28px 22px 26px; + cursor: pointer; + overflow: hidden; + transition: transform .22s ease, border-color .22s, box-shadow .22s, background .22s; + outline: none; +} +.announce-card::before { + content: ""; + position: absolute; + inset: 0; + background: radial-gradient(600px circle at 0% 0%, rgba(167, 139, 250, 0.10), transparent 45%); + pointer-events: none; + opacity: 0; + transition: opacity .25s; +} +.announce-card:hover, +.announce-card:focus-visible { + transform: translateY(-2px); + border-color: rgba(167, 139, 250, 0.4); + box-shadow: 0 14px 38px -18px rgba(167, 139, 250, 0.55), + 0 0 0 1px rgba(167, 139, 250, 0.12); +} +.announce-card:hover::before, +.announce-card:focus-visible::before { opacity: 1; } + +.announce-card.priority-important { border-left-color: #fbbf24; } +.announce-card.priority-important:hover, +.announce-card.priority-important:focus-visible { + box-shadow: 0 14px 38px -18px rgba(251, 191, 36, 0.5), + 0 0 0 1px rgba(251, 191, 36, 0.14); +} +.announce-card.priority-urgent { + border-left-color: #f87171; + background: linear-gradient(135deg, rgba(42, 20, 30, 0.72), rgba(22, 12, 18, 0.72)); +} +.announce-card.priority-urgent:hover, +.announce-card.priority-urgent:focus-visible { + box-shadow: 0 14px 38px -18px rgba(248, 113, 113, 0.55), + 0 0 0 1px rgba(248, 113, 113, 0.18); +} + +.announce-card .meta-row { + display: flex; + align-items: center; + gap: 10px; + font-size: 12px; + color: #6b7d92; + letter-spacing: 1px; + margin-bottom: 10px; + position: relative; +} +.announce-card .date { font-variant-numeric: tabular-nums; } +.announce-card .dot { + width: 3px; height: 3px; border-radius: 50%; + background: #4a5568; +} +.announce-card .badge { + font-size: 11px; + padding: 2px 8px; + border-radius: 4px; + letter-spacing: 1.5px; + border: 1px solid currentColor; +} +.announce-card .badge.normal { color: #6b7d92; } +.announce-card .badge.important { color: #fbbf24; background: rgba(251, 191, 36, 0.08); } +.announce-card .badge.urgent { color: #f87171; background: rgba(248, 113, 113, 0.10); } + +.announce-card .title { + font-size: 18px; + font-weight: 600; + color: #e8eef7; + line-height: 1.55; + margin-bottom: 8px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} +.announce-card .preview { + font-size: 14px; + color: #8b9bb2; + line-height: 1.75; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + margin-bottom: 12px; +} +.announce-card .more { + font-size: 13px; + color: #a78bfa; + letter-spacing: 1.5px; + display: inline-flex; + align-items: center; + gap: 4px; + transition: gap .2s; +} +.announce-card:hover .more, +.announce-card:focus-visible .more { gap: 8px; } +.announce-card .more .arrow { transition: transform .2s; display: inline-block; } +.announce-card:hover .more .arrow, +.announce-card:focus-visible .more .arrow { transform: translateX(3px); } + +.announce-empty { + text-align: center; + padding: 60px 20px; + color: #556677; + font-size: 15px; +} + +/* ---- 弹窗 ---- */ +.announce-modal { + position: fixed; + inset: 0; + z-index: 2000; + display: none; + align-items: center; + justify-content: center; + padding: 40px 20px; +} +.announce-modal.open { + display: flex; + animation: annFade .2s ease; +} +@keyframes annFade { from { opacity: 0; } to { opacity: 1; } } + +.announce-modal .backdrop { + position: absolute; + inset: 0; + background: rgba(4, 7, 14, 0.72); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + cursor: pointer; +} +.announce-modal .dialog { + position: relative; + width: 100%; + max-width: 720px; + max-height: calc(100vh - 80px); + background: linear-gradient(160deg, #0e1626, #0a1020); + border: 1px solid rgba(167, 139, 250, 0.22); + border-radius: 16px; + box-shadow: 0 30px 80px -20px rgba(0, 0, 0, 0.7), + 0 0 60px -20px rgba(167, 139, 250, 0.3); + display: flex; + flex-direction: column; + animation: annPop .28s cubic-bezier(0.2, 0.9, 0.3, 1.2); + overflow: hidden; +} +@keyframes annPop { + from { opacity: 0; transform: translateY(20px) scale(0.96); } + to { opacity: 1; transform: none; } +} +.announce-modal .head { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: 28px 32px 20px; + border-bottom: 1px solid rgba(120, 140, 200, 0.12); + flex-shrink: 0; + gap: 16px; +} +.announce-modal .head .title { + font-size: 22px; + font-weight: 600; + color: #f0f4fb; + line-height: 1.5; + margin-bottom: 8px; + padding-right: 8px; +} +.announce-modal .head .meta { + font-size: 12px; + color: #6b7d92; + letter-spacing: 1.5px; +} +.announce-modal .close { + width: 34px; + height: 34px; + border-radius: 8px; + background: rgba(120, 140, 200, 0.08); + border: none; + color: #8b9bb2; + font-size: 20px; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: background .2s, color .2s; +} +.announce-modal .close:hover { + background: rgba(167, 139, 250, 0.18); + color: #a78bfa; +} +.announce-modal .body { + padding: 24px 32px 32px; + overflow-y: auto; + font-size: 15px; + line-height: 1.9; + color: #c0cad9; +} +.announce-modal .body p { color: #c0cad9; font-size: 15px; margin-bottom: 12px; line-height: 1.9; } +.announce-modal .body p:last-child { margin-bottom: 0; } +.announce-modal .body strong { color: #e8eef7; } +.announce-modal .body a { color: #a78bfa; } +.announce-modal .body h1, +.announce-modal .body h2, +.announce-modal .body h3 { color: #e8eef7; margin: 18px 0 10px; } +.announce-modal .body ul, +.announce-modal .body ol { padding-left: 22px; margin-bottom: 12px; } +.announce-modal .body li { margin-bottom: 4px; } +.announce-modal .body code { + background: rgba(120, 140, 200, 0.12); + padding: 1px 6px; + border-radius: 4px; + font-size: 13px; +} + +/* ---- 响应式 ---- */ +@media (max-width: 768px) { + #panel-announce h1 { font-size: 34px; letter-spacing: 5px; } + #panel-announce .page-sub { font-size: 12px; letter-spacing: 4px; margin-bottom: 28px; } + .announce-card { padding: 18px 18px 18px 20px; } + .announce-card .title { font-size: 16px; } + .announce-card .preview { font-size: 13px; } + .announce-modal { padding: 16px 12px; } + .announce-modal .head { padding: 20px 20px 14px; } + .announce-modal .head .title { font-size: 18px; } + .announce-modal .body { padding: 18px 20px 24px; font-size: 14px; } +} diff --git a/website/intro.html b/website/intro.html index c0e59aa..1d8e374 100644 --- a/website/intro.html +++ b/website/intro.html @@ -259,8 +259,8 @@ p { color:#99aabb; font-size:15px; margin-bottom:14px; line-height:1.9; }

公 告

-

官方通知与更新日志

-
加载中...
+

官方通知 · ANNOUNCEMENTS

+
diff --git a/world.html b/world.html index 0a81dd2..297b520 100644 --- a/world.html +++ b/world.html @@ -214,13 +214,26 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa .status-moving { color: #4af; } /* 飞入遮罩 —— 底部提示药丸 + 全宽进度条 */ +/* ===== 飞入过渡:随机动漫插画作背景 + 进度条/提示在前景 ===== */ #flyInOverlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; - background: #0a0e17; z-index: 300; + background: transparent; z-index: 300; display: flex; flex-direction: column; justify-content: flex-end; padding: 0 24px 48px 24px; opacity: 0; visibility: hidden; pointer-events: none; } +/* 背景插画层:铺满 + 放大遮水印,置于内容之下 */ +#flyInOverlay .flyBg { + position: absolute; inset: 0; width: 100%; height: 100%; + object-fit: cover; transform: scale(1.08); + z-index: 0; +} +/* 深色压暗层,保证前景文字/进度条可读 */ +#flyInOverlay::before { + content: ''; position: absolute; inset: 0; z-index: 1; + background: linear-gradient(180deg, rgba(10,14,23,.35) 0%, rgba(10,14,23,.65) 100%); +} +#flyInOverlay > *:not(.flyBg) { position: relative; z-index: 2; } #flyInOverlay.active { opacity: 1; visibility: visible; pointer-events: auto; } #flyInOverlay .flyLine { display: flex; align-items: center; gap: 14px; @@ -397,16 +410,25 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa #navControls .nc-down{grid-column:2;grid-row:3;} #navControls .nc-se{grid-column:3;grid-row:3;} -/* ===== 登录闸门 ===== */ +/* ===== 登录闸门(小型居中转圈,不遮挡主屏幕) ===== */ #authSplash{position:fixed;inset:0;z-index:9999;display:flex;align-items:center;justify-content:center; - background:linear-gradient(160deg,#080c1a,#0e1730 48%,#15213f);color:#9aa6c4; - font-family:"Noto Sans SC",system-ui,sans-serif;font-size:15px;letter-spacing:3px;} + background:rgba(8,12,26,.45);backdrop-filter:blur(4px);color:#cdd8e6; + font-family:"Noto Sans SC",system-ui,sans-serif;font-size:13px;letter-spacing:2px;} +#authSplash .auth-spinner{ + display:flex;flex-direction:column;align-items:center;gap:14px; + padding:28px 36px;border-radius:16px; + background:rgba(14,23,48,.75);border:1px solid rgba(139,92,246,.25); + box-shadow:0 8px 32px rgba(0,0,0,.5);} +#authSpinner{width:32px;height:32px;border:3px solid rgba(139,92,246,.25); + border-top-color:#a78bfa;border-radius:50%; + animation:authSpin .7s linear infinite;} +@keyframes authSpin{to{transform:rotate(360deg);}} /* #authBar 已融合进 #topBar .userArea */ -
登录校验中…
+
登录校验中…
@@ -422,7 +444,6 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
宇森大地 -
坐标: (0, 0)