diff --git a/app/admin/controller/game/ZiHuaDictionary.php b/app/admin/controller/game/ZiHuaDictionary.php new file mode 100644 index 0000000..0db50a7 --- /dev/null +++ b/app/admin/controller/game/ZiHuaDictionary.php @@ -0,0 +1,97 @@ +initializeBackend($request); + if ($response !== null) { + return $response; + } + if ($request->method() !== 'GET') { + return $this->error(__('Parameter error')); + } + $row = Db::name('game_config')->where('config_key', ZiHuaDictionaryLib::CONFIG_KEY)->find(); + $items = ZiHuaDictionaryLib::parseFromConfigValue($row['config_value'] ?? null); + return $this->success('', [ + 'items' => $items, + 'categories' => ZiHuaDictionaryLib::CATEGORIES, + ]); + } + + /** + * 保存 JSON 数组(仅 value_type=json) + */ + public function save(WebmanRequest $request): Response + { + $response = $this->initializeBackend($request); + if ($response !== null) { + return $response; + } + if ($request->method() !== 'POST') { + return $this->error(__('Parameter error')); + } + $payload = $request->post(); + if (!is_array($payload)) { + return $this->error(__('Parameter %s can not be empty', [''])); + } + $items = $payload['items'] ?? null; + if (!is_array($items)) { + return $this->error('items 必须为数组'); + } + try { + $clean = ZiHuaDictionaryLib::prepareItemsForSave($items); + $json = ZiHuaDictionaryLib::encodeForDb($clean); + } catch (InvalidArgumentException $e) { + return $this->error($e->getMessage()); + } + + $now = time(); + try { + $exists = Db::name('game_config')->where('config_key', ZiHuaDictionaryLib::CONFIG_KEY)->find(); + if ($exists) { + Db::name('game_config')->where('config_key', ZiHuaDictionaryLib::CONFIG_KEY)->update([ + 'config_value' => $json, + 'value_type' => 'json', + 'update_time' => $now, + ]); + } else { + Db::name('game_config')->insert([ + 'config_key' => ZiHuaDictionaryLib::CONFIG_KEY, + 'config_value' => $json, + 'value_type' => 'json', + 'remark' => '36字花字典 JSON 数组(独立表单维护)', + 'create_time' => $now, + 'update_time' => $now, + ]); + } + } catch (Throwable $e) { + return $this->error($e->getMessage()); + } + + return $this->success(__('Saved successfully')); + } +} diff --git a/app/common/library/game/ZiHuaDictionary.php b/app/common/library/game/ZiHuaDictionary.php new file mode 100644 index 0000000..5d9da1d --- /dev/null +++ b/app/common/library/game/ZiHuaDictionary.php @@ -0,0 +1,226 @@ + 'zodiac', + '猛兽' => 'beast', + '飞禽' => 'fowl', + '虫蛇' => 'vermin', + '神兽' => 'divine', + 'zodiac' => 'zodiac', + 'beast' => 'beast', + 'fowl' => 'fowl', + 'vermin' => 'vermin', + 'divine' => 'divine', + ]; + + /** + * 默认 36 条(与 PRD 一致,category 为英文) + * + * @return list + */ + public static function defaultItems(): array + { + return [ + ['no' => 1, 'name' => '鼠', 'category' => 'zodiac'], + ['no' => 2, 'name' => '牛', 'category' => 'zodiac'], + ['no' => 3, 'name' => '虎', 'category' => 'zodiac'], + ['no' => 4, 'name' => '兔', 'category' => 'zodiac'], + ['no' => 5, 'name' => '龙', 'category' => 'zodiac'], + ['no' => 6, 'name' => '蛇', 'category' => 'zodiac'], + ['no' => 7, 'name' => '马', 'category' => 'zodiac'], + ['no' => 8, 'name' => '羊', 'category' => 'zodiac'], + ['no' => 9, 'name' => '猴', 'category' => 'zodiac'], + ['no' => 10, 'name' => '鸡', 'category' => 'zodiac'], + ['no' => 11, 'name' => '狗', 'category' => 'zodiac'], + ['no' => 12, 'name' => '猪', 'category' => 'zodiac'], + ['no' => 13, 'name' => '狮', 'category' => 'beast'], + ['no' => 14, 'name' => '象', 'category' => 'beast'], + ['no' => 15, 'name' => '鹿', 'category' => 'beast'], + ['no' => 16, 'name' => '熊', 'category' => 'beast'], + ['no' => 17, 'name' => '狼', 'category' => 'beast'], + ['no' => 18, 'name' => '豹', 'category' => 'beast'], + ['no' => 19, 'name' => '鹰', 'category' => 'fowl'], + ['no' => 20, 'name' => '鹤', 'category' => 'fowl'], + ['no' => 21, 'name' => '孔雀', 'category' => 'fowl'], + ['no' => 22, 'name' => '鸳鸯', 'category' => 'fowl'], + ['no' => 23, 'name' => '鸵鸟', 'category' => 'fowl'], + ['no' => 24, 'name' => '天鹅', 'category' => 'fowl'], + ['no' => 25, 'name' => '蟾蜍', 'category' => 'vermin'], + ['no' => 26, 'name' => '蜘蛛', 'category' => 'vermin'], + ['no' => 27, 'name' => '蝙蝠', 'category' => 'vermin'], + ['no' => 28, 'name' => '蜈蚣', 'category' => 'vermin'], + ['no' => 29, 'name' => '蝎', 'category' => 'vermin'], + ['no' => 30, 'name' => '蛇蜥', 'category' => 'vermin'], + ['no' => 31, 'name' => '麒麟', 'category' => 'divine'], + ['no' => 32, 'name' => '凤凰', 'category' => 'divine'], + ['no' => 33, 'name' => '青龙', 'category' => 'divine'], + ['no' => 34, 'name' => '白虎', 'category' => 'divine'], + ['no' => 35, 'name' => '朱雀', 'category' => 'divine'], + ['no' => 36, 'name' => '玄武', 'category' => 'divine'], + ]; + } + + /** + * @return list + */ + public static function parseFromConfigValue(?string $configValue): array + { + if ($configValue === null || trim($configValue) === '') { + return self::defaultItems(); + } + $decoded = json_decode($configValue, true); + if (!is_array($decoded)) { + return self::defaultItems(); + } + if (isset($decoded['items']) && is_array($decoded['items'])) { + return self::normalizeItemsList($decoded['items']); + } + if ($decoded !== [] && array_is_list($decoded)) { + return self::normalizeItemsList($decoded); + } + return self::defaultItems(); + } + + /** + * @param list $items + * @return list + */ + public static function normalizeItemsList(array $items): array + { + $defaults = self::defaultItems(); + $defaultByNo = []; + foreach ($defaults as $row) { + $defaultByNo[$row['no']] = $row; + } + + $out = []; + foreach ($items as $row) { + if (!is_array($row)) { + continue; + } + $no = $row['no'] ?? null; + if (!is_numeric($no)) { + continue; + } + $n = intval($no, 10); + if ($n < 1 || $n > 36) { + continue; + } + $name = isset($row['name']) && is_string($row['name']) ? trim($row['name']) : ''; + if ($name === '') { + $name = $defaultByNo[$n]['name'] ?? ''; + } + $catRaw = $row['category'] ?? 'zodiac'; + $catStr = is_string($catRaw) ? trim($catRaw) : ''; + $category = self::LEGACY_CATEGORY_MAP[$catStr] ?? (in_array($catStr, self::CATEGORIES, true) ? $catStr : 'zodiac'); + $out[] = ['no' => $n, 'name' => $name, 'category' => $category]; + } + + if (count($out) !== 36) { + return self::defaultItems(); + } + + usort($out, static fn (array $a, array $b): int => $a['no'] <=> $b['no']); + + $seen = []; + foreach ($out as $row) { + if (isset($seen[$row['no']])) { + return self::defaultItems(); + } + $seen[$row['no']] = true; + } + + for ($i = 1; $i <= 36; $i++) { + if (!isset($seen[$i])) { + return self::defaultItems(); + } + } + + return $out; + } + + /** + * 校验 POST 数据并输出入库用的 36 条(no 升序) + * + * @param list $items + * + * @return list + * + * @throws InvalidArgumentException + */ + public static function prepareItemsForSave(array $items): array + { + if (count($items) !== 36) { + throw new InvalidArgumentException('必须恰好 36 条'); + } + $nos = []; + $out = []; + foreach ($items as $idx => $row) { + if (!is_array($row)) { + throw new InvalidArgumentException('第 ' . ($idx + 1) . ' 行格式错误'); + } + $no = $row['no'] ?? null; + if (!is_numeric($no)) { + throw new InvalidArgumentException('编号必须为数字'); + } + $n = intval($no, 10); + if ($n < 1 || $n > 36) { + throw new InvalidArgumentException('编号必须在 1–36'); + } + if (isset($nos[$n])) { + throw new InvalidArgumentException('编号重复:' . $n); + } + $nos[$n] = true; + + $name = $row['name'] ?? ''; + if (!is_string($name) || trim($name) === '') { + throw new InvalidArgumentException('编号 ' . $n . ' 名称不能为空'); + } + + $cat = $row['category'] ?? ''; + $catTrim = is_string($cat) ? trim($cat) : ''; + if (!in_array($catTrim, self::CATEGORIES, true)) { + throw new InvalidArgumentException('编号 ' . $n . ' 类型无效'); + } + + $out[] = ['no' => $n, 'name' => trim($name), 'category' => $catTrim]; + } + for ($i = 1; $i <= 36; $i++) { + if (!isset($nos[$i])) { + throw new InvalidArgumentException('缺少编号:' . $i); + } + } + usort($out, static fn (array $a, array $b): int => $a['no'] <=> $b['no']); + return $out; + } + + /** + * @param list $items + */ + public static function encodeForDb(array $items): string + { + usort($items, static fn (array $a, array $b): int => $a['no'] <=> $b['no']); + $encoded = json_encode($items, JSON_UNESCAPED_UNICODE); + if ($encoded === false) { + throw new InvalidArgumentException('JSON 编码失败'); + } + return $encoded; + } +} diff --git a/web/src/lang/backend/en/game/ziHuaDictionary.ts b/web/src/lang/backend/en/game/ziHuaDictionary.ts new file mode 100644 index 0000000..100f6b7 --- /dev/null +++ b/web/src/lang/backend/en/game/ziHuaDictionary.ts @@ -0,0 +1,16 @@ +export default { + title: '36 Zi Hua dictionary', + desc: 'Maintain the display names and grouping of the 36 entries. Changes affect player selection display, related filters/statistics, and admin mapping references. Please review before saving.', + no: 'No.', + name: 'Name', + category: 'Category', + btn_save: 'Save', + category_label: { + zodiac: 'Zodiac', + beast: 'Beast', + fowl: 'Fowl', + vermin: 'Vermin', + divine: 'Divine', + }, +} + diff --git a/web/src/lang/backend/zh-cn/game/ziHuaDictionary.ts b/web/src/lang/backend/zh-cn/game/ziHuaDictionary.ts new file mode 100644 index 0000000..49fb3b8 --- /dev/null +++ b/web/src/lang/backend/zh-cn/game/ziHuaDictionary.ts @@ -0,0 +1,16 @@ +export default { + title: '36字花字典', + desc: '用于维护 36 字花的展示名称与归类。修改后会影响前台选号展示、相关筛选统计及后台对照信息,请确认无误后再保存。', + no: '编号', + name: '名称', + category: '类型', + btn_save: '保存', + category_label: { + zodiac: '生肖', + beast: '猛兽', + fowl: '飞禽', + vermin: '虫蛇', + divine: '神兽', + }, +} + diff --git a/web/src/views/backend/game/ziHuaDictionary/index.vue b/web/src/views/backend/game/ziHuaDictionary/index.vue new file mode 100644 index 0000000..2538d74 --- /dev/null +++ b/web/src/views/backend/game/ziHuaDictionary/index.vue @@ -0,0 +1,102 @@ + + + + +