diff --git a/web/plugins/com.msgbyte.genshin/manifest.json b/web/plugins/com.msgbyte.genshin/manifest.json
new file mode 100644
index 00000000..d8be0f94
--- /dev/null
+++ b/web/plugins/com.msgbyte.genshin/manifest.json
@@ -0,0 +1,9 @@
+{
+ "label": "原神工具箱插件",
+ "name": "com.msgbyte.genshin",
+ "url": "/plugins/com.msgbyte.genshin/index.js",
+ "version": "0.0.0",
+ "author": "msgbyte",
+ "description": "为Tailchat增加原神相关的娱乐能力",
+ "requireRestart": true
+}
diff --git a/web/plugins/com.msgbyte.genshin/package.json b/web/plugins/com.msgbyte.genshin/package.json
new file mode 100644
index 00000000..ef82b57e
--- /dev/null
+++ b/web/plugins/com.msgbyte.genshin/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@plugins/com.msgbyte.genshin",
+ "main": "src/index.ts",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "genshin-gacha-kit": "^1.1.0"
+ }
+}
diff --git a/web/plugins/com.msgbyte.genshin/src/GenshinPanel/GachaPool.tsx b/web/plugins/com.msgbyte.genshin/src/GenshinPanel/GachaPool.tsx
new file mode 100644
index 00000000..8a6de7ec
--- /dev/null
+++ b/web/plugins/com.msgbyte.genshin/src/GenshinPanel/GachaPool.tsx
@@ -0,0 +1,50 @@
+import { useAsync } from '@capital/common';
+import { Divider } from '@capital/component';
+import React from 'react';
+import { OfficialGachaPoolItem, util } from 'genshin-gacha-kit';
+
+const GachaPoolItem: React.FC<{
+ items: OfficialGachaPoolItem[];
+}> = React.memo((props) => {
+ return (
+
+ {props.items.map((i) => (
+
+

+
+
{i.item_name}
+
+ ))}
+
+ );
+});
+GachaPoolItem.displayName = 'GachaPoolItem';
+
+export const GachaPool: React.FC<{
+ gachaId: string;
+}> = React.memo((props) => {
+ const { value: poolData } = useAsync(() => {
+ return util.getGachaData(props.gachaId);
+ }, [props.gachaId]);
+
+ if (!poolData) {
+ return Loading...
;
+ }
+
+ return (
+
+
{poolData.banner}
+
+
{poolData.date_range}
+
+
+
+
+
+
+
+
{poolData.content}
+
+ );
+});
+GachaPool.displayName = 'GachaPool';
diff --git a/web/plugins/com.msgbyte.genshin/src/GenshinPanel/index.less b/web/plugins/com.msgbyte.genshin/src/GenshinPanel/index.less
new file mode 100644
index 00000000..1bf6459a
--- /dev/null
+++ b/web/plugins/com.msgbyte.genshin/src/GenshinPanel/index.less
@@ -0,0 +1,23 @@
+.plugin-genshin-panel {
+ width: 100%;
+ padding: 10px;
+ display: flex;
+ flex-direction: column;
+
+ .gacha-title {
+ font-weight: bold;
+ font-size: 22px;
+ margin-bottom: 10px;
+ }
+
+ .gacha-pool {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+
+ > div {
+ display: flex;
+ gap: 10px;
+ }
+ }
+}
diff --git a/web/plugins/com.msgbyte.genshin/src/GenshinPanel/index.tsx b/web/plugins/com.msgbyte.genshin/src/GenshinPanel/index.tsx
new file mode 100644
index 00000000..97c7153c
--- /dev/null
+++ b/web/plugins/com.msgbyte.genshin/src/GenshinPanel/index.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import { Translate } from '../translate';
+import { util } from 'genshin-gacha-kit';
+import { useAsync } from '@capital/common';
+import { PillTabs, PillTabPane } from '@capital/component';
+import './index.less';
+import { GachaPool } from './GachaPool';
+
+const GenshinPanel: React.FC = React.memo(() => {
+ const { value: gachaList } = useAsync(() => {
+ return util.getGachaIndex();
+ }, []);
+
+ return (
+
+
+ {Translate.genshin} - {Translate.gacha}
+
+
+
+ {(gachaList ?? []).map((item) => (
+
+
+
+ ))}
+
+
+ );
+});
+GenshinPanel.displayName = 'GenshinPanel';
+
+export default GenshinPanel;
diff --git a/web/plugins/com.msgbyte.genshin/src/index.ts b/web/plugins/com.msgbyte.genshin/src/index.ts
new file mode 100644
index 00000000..dc244f7e
--- /dev/null
+++ b/web/plugins/com.msgbyte.genshin/src/index.ts
@@ -0,0 +1,10 @@
+import { regCustomPanel, Loadable } from '@capital/common';
+import { Translate } from './translate';
+
+regCustomPanel({
+ position: 'personal',
+ icon: 'akar-icons:game-controller',
+ name: 'com.msgbyte.genshin/genshinPanel',
+ label: Translate.genshin,
+ render: Loadable(() => import('./GenshinPanel')),
+});
diff --git a/web/plugins/com.msgbyte.genshin/src/translate.ts b/web/plugins/com.msgbyte.genshin/src/translate.ts
new file mode 100644
index 00000000..4ac43768
--- /dev/null
+++ b/web/plugins/com.msgbyte.genshin/src/translate.ts
@@ -0,0 +1,6 @@
+import { localTrans } from '@capital/common';
+
+export const Translate = {
+ genshin: localTrans({ 'zh-CN': '原神', 'en-US': 'Genshin' }),
+ gacha: localTrans({ 'zh-CN': '抽卡', 'en-US': 'Gacha' }),
+};
diff --git a/web/plugins/com.msgbyte.genshin/tsconfig.json b/web/plugins/com.msgbyte.genshin/tsconfig.json
new file mode 100644
index 00000000..465a28b5
--- /dev/null
+++ b/web/plugins/com.msgbyte.genshin/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "baseUrl": "./src",
+ "esModuleInterop": true,
+ "jsx": "react",
+ "paths": {
+ "@capital/*": ["../../../src/plugin/*"],
+ }
+ }
+}
diff --git a/web/src/components/PillTabs.tsx b/web/src/components/PillTabs.tsx
index fc8b8898..514edf8c 100644
--- a/web/src/components/PillTabs.tsx
+++ b/web/src/components/PillTabs.tsx
@@ -1,4 +1,4 @@
-import { Tabs } from 'antd';
+import { Tabs, TabsProps } from 'antd';
import React from 'react';
import './PillTabs.less';
@@ -11,7 +11,7 @@ import './PillTabs.less';
*
*
*/
-export const PillTabs = React.memo((props) => {
+export const PillTabs: React.FC = React.memo((props) => {
return (
{props.children}
diff --git a/web/src/plugin/component/index.tsx b/web/src/plugin/component/index.tsx
index 9149b6b6..ae9d04b1 100644
--- a/web/src/plugin/component/index.tsx
+++ b/web/src/plugin/component/index.tsx
@@ -1,6 +1,6 @@
import { Input } from 'antd';
-export { Button, Checkbox, Input } from 'antd';
+export { Button, Checkbox, Input, Divider } from 'antd';
export const TextArea = Input.TextArea;
export { Image } from '@/components/Image';
export { Icon } from '@iconify/react';