feat: init commit

master
xjs 2025-04-21 14:07:24 +08:00
commit 969254663b
77 changed files with 6735 additions and 0 deletions

26
.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.env

27
ALIYUN.pem Normal file
View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA2SOM6Heb+BNGha/ucoV+GopddM7ckyGWALhGPJp/Z7P5jgw6
NSJ1G/E7CFEukaLC50eupji9mA4o8Emtrgn8y7uMIc5lafHe0IPy+WA90PZyien4
0u7dD0NrEbKH41SIEuZFbGev0CgQJsxmkS8CmOytmglyJ0JDBpJD9tQLwSuG9kad
DrYPArQdQu+ZALt4gyG7m14c4mZ2hAcIUwNYqFY9g/HIp0q9al8SW3WqzkB1U7GS
kh3i/MwtUvZWPI16aKW4/tdUX7y8PPguHQ1MWLd6DI0iGo9tqTQt35o9ax41O+0S
EY4MgU1Q3XJaMrmrYUpZ2/Y4xJbHOHgyi6afzQIDAQABAoIBAGms4ovUgkSmZOD1
MU/s5eVWx4rsje7RHqa1CAHAkxbOQTq/eqiXX3U83qT6lXZtRvu2KCpfXO4eng/r
W6pi0/P3D4j4YOTBwNWsEdkJ3KvQ9QdnpiBJ/a3K+tW/FGEvp5XDGbBbefYNOWcY
fSZVQadZMFfSFwtCNUqCbq82nY3hkoFVGiQ9EHUlvNCQM4y5VeJuCPzsl8rzAsyo
gpkHyKxU/CNg+f5UuPwostR5eTXgkp6nlpa65yDK0szsww78keE/J1tOB0d4r+Oe
12ZVzYLQrzQwt2CwIGT9KkAUv7eO7ZTMDsG8MYNnRPGXKgjSZqBSW0MCq7ksz1/P
dHTJmSECgYEA9Q8VbnLkdk5NlRCNTONfjLhNUGmAqt/qLPi7tnJa0wfSpr3tg1Aj
AxnV455fT81vbDP8V8tGDNx+/d5jBkIsdsCYOMa6dqZr1HOfSgbH8wETtU9mbKdB
Gt/frdM3rkJvyd5RjoqMex4U7x4f1OWfThHVzvi8TzqfRSXNpbXBMdUCgYEA4tVa
TBQhwgnjMHl9OYrW1urtf2fKEGOZA/uGJI55O7IL2SoYnVjFUHjlE4D94K/hdijp
l2oFydD0GrWVkWULPvTdUFzMTRPH+SUfXbVqXBMJfHJYrRuZHsJBH2jIwIzu4LaA
hliletIdJUtK8mUvwEb+4TlVUwfCYqkveu1EuhkCgYAHjK5pV6rIJkNnmznvK3YP
HMJs/sMTAJDzT7pgtYcsxynrLyC5EefyOYKIX6GqELclCzjz73Q6AzT6VzaPw8wg
4HAQF7c43oml4uX+XtUcHGViCY8rO7/atxjp/v7RJITTIEE89fG7/UJB15i9c1GE
EzKWDL2oZzLu62o5d676/QKBgQDgiCBhvmvMDs18ZkW2d+BBzTpaKvqxTmVgs9EM
zpripFNmG21SE1T9Wy4mKEEl7/NVaxoObzxbkSKQbb4ntcV0BB4uNi1k/ner/zsV
H0aw7YcuUGHGuNLQx6h+1tIhB2BNv1lposXq1aFUETuWxOKHib8yYfY7wiqATshY
/hRRwQKBgCkT2Wt/XjbXLCpX1viEJOLzYYQkrNyPvruXknCnQQ+0/cq4LoRqWZZR
4RjS0RkoOF4vM38u/hgICiUUCR4bYUy/4O7uhNu5tDle6lIeB+ZxVpRRedE0OfQT
tCpQd0yN9iONBlGyfAtpq7oNzuXgsl1OR4usZ8wCinfImChAl6wv
-----END RSA PRIVATE KEY-----

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-2024 HideInMatrix
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

31
README.md Normal file
View File

@ -0,0 +1,31 @@
# 音乐地带
免费的听歌网站(开发中)
## ✨ 特性
- 使用React开发nextjs分支使用Next.js 14版本开发(后期不维护了)。
- 🖥️ 支持 PWA可在 Chrome/Edge 里点击地址栏右边的 安装到电脑
- 无需VIP直接在线播放歌曲
- 🛠 更多特性开发中
## ⚙️ 部署至 Vercel
除了下载安装包使用,你还可以将本项目部署到 Vercel 或你的服务器上。下面是部署到 Vercel 的方法。
本项目的 Demo (https://freemusic.micromatrix.org) 就是部署在 Vercel 上的网站。
[![Powered by Vercel](https://www.datocms-assets.com/31049/1618983297-powered-by-vercel.svg)](https://vercel.com/?utm_source=ohmusic&utm_campaign=oss)
1. 点击本仓库右上角的 Fork复制本仓库到你的 GitHub 账号。
2. 打开 [Vercel.com](https://vercel.com),使用 GitHub 登录。
3. 点击 Import Git Repository 并选择你刚刚复制的仓库并点击 Import。
4. 点击 PERSONAL ACCOUNT 旁边的 Select。
## ☑️ Todo
迎提 Issue 和 Pull request。

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/index-DQMJjaAj.js","assets/.pnpm-BDdfG1pO.js","assets/index-PFueeGmc.css"])))=>i.map(i=>d[i]);
import{_ as t}from"./index-j0VtgfAk.js";import{j as r,r as a}from"./.pnpm-BDdfG1pO.js";const o=a.lazy(()=>t(()=>import("./index-DQMJjaAj.js"),__vite__mapDeps([0,1,2])));function i(){return r.jsx("div",{className:"h-full bg-[#F4F6FA]",children:r.jsx(o,{})})}export{i as default};

View File

@ -0,0 +1 @@
import{t as s,b as n,j as t,O as e}from"./.pnpm-BDdfG1pO.js";function r(...a){return s(n(a))}function c(){return t.jsx(t.Fragment,{children:t.jsx("div",{className:r("h-dvh bg-background font-sans antialiased"),children:t.jsx(e,{})})})}export{c as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
._controller_5jmgu_1{display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:16px;background-color:#fff;padding:8px 15px;padding-bottom:calc(12px + constant(safe-area-inset-bottom));padding-bottom:calc(12px + env(safe-area-inset-bottom))}._talkWrapper_5jmgu_23{--h: 16px;display:flex;align-items:center;justify-content:center;cursor:pointer;padding:8px 16px;background-color:#e3efff;border-radius:20px;-webkit-user-select:none;-moz-user-select:none;user-select:none;gap:8px;min-width:116px;color:#0078ff}._listenerDot_5jmgu_53{display:flex;align-items:center;justify-content:center;position:relative;height:var(--h);width:36px}._listenerDot_5jmgu_53 span{display:inline-block;width:4px;margin:0 1px;background-color:#4898fc;border-radius:2px;height:calc(var(--h) - var(--d) * 4px);transition:height .2s ease;opacity:calc(1 - var(--d) * .4)}._isTalking_5jmgu_93 span{animation:_soundWave_5jmgu_1 1s infinite ease-in-out}._isTalking_5jmgu_93 span:nth-child(1){animation-delay:0s}._isTalking_5jmgu_93 span:nth-child(2){animation-delay:.2s}._isTalking_5jmgu_93 span:nth-child(3){animation-delay:.4s}._isTalking_5jmgu_93 span:nth-child(4){animation-delay:.6s}._isTalking_5jmgu_93 span:nth-child(5){animation-delay:.8s}@keyframes _soundWave_5jmgu_1{0%,to{height:4px}50%{height:16px}}._microphoneWrapper_5jmgu_157{display:flex;align-items:center;background-color:#f6f6f6;padding:10px 15px;border-radius:400px}._timerWrapper_5jmgu_173{display:flex;align-items:center;justify-content:center;background-color:#f6f6f6;padding:8px 14px;border-radius:400px;color:#0078ff;font-size:14px;font-weight:500;width:100%}._headerWrapper_lrk2k_1{margin-top:65px;width:100%;padding:0 16px;box-sizing:border-box}._wrapper_lrk2k_15{height:105px;background:#b0e4ffcc;border-radius:50px 13px 13px;border:1px solid #ffffff;position:relative;z-index:0;margin-bottom:-54px}._img_lrk2k_37{width:120px;height:123px;-o-object-fit:contain;object-fit:contain;position:absolute;bottom:35px}._wrapper_lrk2k_15:before{content:"";position:absolute;width:133px;height:32px;bottom:23px;left:6px;background:radial-gradient(ellipse at top,#1580ff,#fff);z-index:2;border-radius:20px;filter:blur(10px)}._text_lrk2k_81{font-weight:500;font-size:16px;color:#000;text-align:end;margin-top:15px;margin-right:45px}._main-wrapper_lrk2k_101{background:#ffffffb3;border-radius:13px;border:1px solid #ffffff;padding:16px 17px 18px;position:relative;z-index:1;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);min-height:213px;box-sizing:border-box}._main_lrk2k_101{display:flex;justify-content:space-between}._thing_lrk2k_135{-o-object-fit:contain;object-fit:contain;position:relative;height:15px;width:91px}._circle_lrk2k_153{display:flex;align-items:center;justify-content:center;gap:2px;color:#2380e2}._circle_lrk2k_153 img{width:16px;height:16px}._rightIcon_lrk2k_179{width:10px;height:10px}._change_lrk2k_189{color:#2380e2}._tip_lrk2k_197{display:flex;align-items:center;justify-content:space-between;font-weight:400;font-size:16px;color:#000;margin-top:18px}._rotating_lrk2k_223{animation:_rotate360_lrk2k_1 1s linear}@keyframes _rotate360_lrk2k_1{0%{transform:rotate(0)}to{transform:rotate(360deg)}}._wrapper_e1j90_1{width:88px;height:88px;background:linear-gradient(180deg,#64c7ff,#0165ff);display:flex;flex-direction:column;justify-content:center;align-items:center;border-radius:50%;position:relative;margin-top:auto;margin-bottom:80px;overflow:hidden}._wrapper_e1j90_1:before{position:absolute;content:"";left:0;bottom:0;width:26px;height:26px;background:radial-gradient(farthest-corner at 100% 0%,#7bdcf0,#fff);filter:blur(14px)}._wrapper_e1j90_1:after{position:absolute;content:"";left:20%;top:0;width:26px;height:26px;background:radial-gradient(farthest-corner at 100% 0%,#7bdcf0,#a4dbe6);filter:blur(14px)}._text_e1j90_75{color:#fff;font-weight:500;font-size:13px}._call_e1j90_87{width:32px;height:32px;-o-object-fit:contain;object-fit:contain;margin-bottom:4px}._scoreWrapper_1g8jt_1{padding:15px;width:100%;box-sizing:border-box}._innerWrapper_1g8jt_13{padding:15px;width:100%;background:#ffffffb3;border-radius:13px;border:1px solid #f4f6fa;display:flex;align-items:center;justify-content:space-between;box-sizing:border-box}._detail_1g8jt_37{display:flex;align-items:center;gap:10px;margin-top:10px;font-size:16px}._right_1g8jt_53{color:#1580ff;display:flex;align-items:center}._imgIcon_1g8jt_65{width:91px;height:15px}._rightBlue_1g8jt_75{width:10px;height:10px}

View File

@ -0,0 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/App-Dh3vQfIK.js","assets/.pnpm-BDdfG1pO.js","assets/MainLayout-BWiadrJR.js"])))=>i.map(i=>d[i]);
var v=Object.defineProperty;var x=(s,t,n)=>t in s?v(s,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):s[t]=n;var m=(s,t,n)=>x(s,typeof t!="symbol"?t+"":t,n);import{r as d,c as P,j as a,a as _,R as L}from"./.pnpm-BDdfG1pO.js";(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))f(e);new MutationObserver(e=>{for(const o of e)if(o.type==="childList")for(const r of o.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&f(r)}).observe(document,{childList:!0,subtree:!0});function n(e){const o={};return e.integrity&&(o.integrity=e.integrity),e.referrerPolicy&&(o.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?o.credentials="include":e.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function f(e){if(e.ep)return;e.ep=!0;const o=n(e);fetch(e.href,o)}})();const j="modulepreload",O=function(s){return"/"+s},h={},p=function(t,n,f){let e=Promise.resolve();if(n&&n.length>0){document.getElementsByTagName("link");const r=document.querySelector("meta[property=csp-nonce]"),i=(r==null?void 0:r.nonce)||(r==null?void 0:r.getAttribute("nonce"));e=Promise.allSettled(n.map(c=>{if(c=O(c),c in h)return;h[c]=!0;const u=c.endsWith(".css"),E=u?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${c}"]${E}`))return;const l=document.createElement("link");if(l.rel=u?"stylesheet":j,u||(l.as="script"),l.crossOrigin="",l.href=c,i&&l.setAttribute("nonce",i),document.head.appendChild(l),u)return new Promise((y,g)=>{l.addEventListener("load",y),l.addEventListener("error",()=>g(new Error(`Unable to preload CSS for ${c}`)))})}))}function o(r){const i=new Event("vite:preloadError",{cancelable:!0});if(i.payload=r,window.dispatchEvent(i),!i.defaultPrevented)throw r}return e.then(r=>{for(const i of r||[])i.status==="rejected"&&o(i.reason);return t().catch(o)})},R=d.lazy(()=>p(()=>import("./App-Dh3vQfIK.js"),__vite__mapDeps([0,1]))),S=d.lazy(()=>p(()=>import("./MainLayout-BWiadrJR.js"),__vite__mapDeps([2,1])));class w extends d.Component{constructor(){super(...arguments);m(this,"state",{hasError:!1})}static getDerivedStateFromError(n){return{hasError:!0}}render(){return this.state.hasError?a.jsx("h1",{children:"出错了,请稍后再试。"}):this.props.children}}const b=P([{path:"/",element:a.jsx(S,{}),errorElement:a.jsx(w,{children:a.jsx("div",{className:"flex-auto flex flex-col p-6",children:"出错了,请稍后再试。"})}),children:[{path:"/",element:a.jsx(R,{})}]}]);_(document.getElementById("root")).render(a.jsx(d.StrictMode,{children:a.jsx(L,{router:b})}));export{p as _};

BIN
build/icons/call.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
build/icons/circle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 B

BIN
build/icons/handoff.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 B

BIN
build/icons/hello.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

BIN
build/icons/microphone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 B

BIN
build/icons/myInput.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
build/icons/right.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 B

BIN
build/icons/rightBlue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 B

BIN
build/icons/whatsThing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

33
build/index.html Normal file
View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>六维小助手</title>
<meta name="description" content="AIGC对话" />
<meta name="generator" content="React" />
<meta name="keywords" content="music, music-site" />
<meta
name="theme-color"
content="#fff"
media="(prefers-color-scheme: dark)" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no,maximum-scale=1.0, minimum-scale=1.0" />
<link rel="icon" href="https://api.static.ycymedu.com/src/images/home/app-logo.svg" />
<meta name="author" content="HideInMatrix" />
<!-- <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
// VConsole will be exported to `window.VConsole` by default.
var vConsole = new window.VConsole();
</script> -->
<script type="module" crossorigin src="/assets/index-j0VtgfAk.js"></script>
<link rel="modulepreload" crossorigin href="/assets/.pnpm-BDdfG1pO.js">
<link rel="stylesheet" crossorigin href="/assets/index-ChXXIJVe.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

20
components.json Normal file
View File

@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}

28
eslint.config.js Normal file
View File

@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

31
index.html Normal file
View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>六维小助手</title>
<meta name="description" content="AIGC对话" />
<meta name="generator" content="React" />
<meta name="keywords" content="music, music-site" />
<meta
name="theme-color"
content="#fff"
media="(prefers-color-scheme: dark)" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no,maximum-scale=1.0, minimum-scale=1.0" />
<link rel="icon" href="https://api.static.ycymedu.com/src/images/home/app-logo.svg" />
<meta name="author" content="HideInMatrix" />
<!-- <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
// VConsole will be exported to `window.VConsole` by default.
var vConsole = new window.VConsole();
</script> -->
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

54
package.json Normal file
View File

@ -0,0 +1,54 @@
{
"name": "free-music-react",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"upload": "bash ./upload.sh",
"build-and-upload": "pnpm run build && pnpm run upload"
},
"dependencies": {
"@coze/api": "1.2.0",
"@coze/realtime-api": "^1.1.1",
"@microsoft/signalr": "^8.0.7",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-toast": "^1.2.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"embla-carousel-react": "^8.2.0",
"lucide-react": "^0.437.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.1",
"react-sortablejs": "^6.1.4",
"sortablejs": "^1.15.3",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.1",
"zustand": "^4.5.5"
},
"devDependencies": {
"@eslint/js": "^9.9.0",
"@types/node": "^22.5.1",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/sortablejs": "^1.15.8",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.20",
"eslint": "^9.9.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",
"postcss": "^8.4.42",
"tailwindcss": "^3.4.10",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.1",
"vite": "^5.4.1"
}
}

4160
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
public/icons/call.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/icons/circle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 B

BIN
public/icons/handoff.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 B

BIN
public/icons/hello.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

BIN
public/icons/microphone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 B

BIN
public/icons/myInput.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
public/icons/right.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 B

BIN
public/icons/rightBlue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 B

BIN
public/icons/whatsThing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

19
src/apis/questions.ts Normal file
View File

@ -0,0 +1,19 @@
import { getRequest } from "@/lib/customFetch";
export const fetchQuestions = async ({
options,
}: {
options?: { signal?: AbortSignal,};
}) => {
const response = await getRequest(
"https://api.v3.ycymedu.com/api/zhiYuan/aigcquestionswords?",
{},
options
);
if(response.code === 200){
return {result:response.result}
}else{
return {result:[],message:response.message}
}
};

19
src/apis/user.ts Normal file
View File

@ -0,0 +1,19 @@
import { getRequest } from "@/lib/customFetch";
export const fetchUserToken = async ({
options,
}: {
options?: { signal?: AbortSignal,headers?:Record<string,string> };
}) => {
const response = await getRequest(
"https://api.v3.ycymedu.com/api/sysOnlineUser/hasitexpired",
{},
options
);
if(response.code === 200){
return {result:response.result}
}else{
return {result:[],message:response.message}
}
};

13
src/app/App.tsx Normal file
View File

@ -0,0 +1,13 @@
import { lazy } from "react";
const MainArea = lazy(() => import("@/app/MainArea/index"));
function App() {
return (
<div className="h-full bg-[#F4F6FA]">
<MainArea />
</div>
);
}
export default App;

View File

@ -0,0 +1,48 @@
import AntechamberHeader from "@/components/AntechamberHeader";
import InvokeButton from "@/components/AntechamberButton";
import AntechamberScore from "@/components/AntechamberScore";
import { useContext, useEffect, useState } from "react";
import { RealtimeClientContext } from "@/components/Provider/RealtimeClientProvider";
import { useSearchParams } from "react-router-dom";
import { fetchUserToken } from "@/apis/user";
export default function Antechamber() {
const { handleConnect } = useContext(RealtimeClientContext);
const [searchParams] = useSearchParams();
const [disable,setDisable] = useState(true);
const token = searchParams.get("token") || '';
const getUserToken = async ({ signal }: { signal: AbortSignal }) => {
const { result, message } = await fetchUserToken({ options: { signal,headers:{"Authorization":`Bearer ${token}`} } });
if (message) {
console.log(message);
} else {
const _result = result as {isExpired:boolean};
setDisable(!_result.isExpired);
}
};
useEffect(()=>{
const controller = new AbortController();
const { signal } = controller;
getUserToken({ signal });
},[token])
const toRoom = () => {
// if(disable){
// return;
// }
handleConnect();
};
return (
<div className="flex flex-col items-center h-full">
<AntechamberHeader toRoom={toRoom} />
<AntechamberScore toRoom={toRoom} />
<InvokeButton disable={disable} onClick={toRoom} />
</div>
);
}

View File

@ -0,0 +1,11 @@
import RoomConversation from "@/components/RoomConversation";
import RoomController from "@/components/RoomController";
export default function Room() {
return (
<div className="flex flex-col h-full">
<RoomConversation />
<RoomController />
</div>
);
}

View File

@ -0,0 +1,20 @@
import Room from "./Room";
import Antechamber from "./Antechamber";
import { useContext } from "react";
import {
RealtimeClientContext,
RealtimeClientProvider,
} from "@/components/Provider/RealtimeClientProvider";
function MainContent() {
const { isConnected } = useContext(RealtimeClientContext);
return isConnected ? <Room /> : <Antechamber />;
}
export default function MainArea() {
return (
<RealtimeClientProvider>
<MainContent />
</RealtimeClientProvider>
);
}

15
src/app/MainLayout.tsx Normal file
View File

@ -0,0 +1,15 @@
import { cn } from "@/lib/utils";
import { Outlet } from "react-router-dom";
export default function LocaleLayout() {
return (
<>
<div className={cn("h-dvh bg-background font-sans antialiased")}>
<Outlet />
</div>
</>
);
}

View File

@ -0,0 +1,50 @@
.wrapper {
width: 88px;
height: 88px;
background: linear-gradient(180deg, #64c7ff 0%, #0165ff 100%);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: 50%;
position: relative;
margin-top: auto;
margin-bottom: 80px;
overflow: hidden;
}
.wrapper::before {
position: absolute;
content: '';
left: 0;
bottom: 0;
width: 26px;
height: 26px;
background: radial-gradient(farthest-corner at 100% 0%, #7bdcf0, #fff);
filter: blur(14px);
}
.wrapper::after {
position: absolute;
content: '';
left: 20%;
top: 0;
width: 26px;
height: 26px;
background: radial-gradient(farthest-corner at 100% 0%, #7bdcf0, #a4dbe6);
filter: blur(14px);
}
.text {
color: #fff;
font-weight: 500;
font-size: 13px;
}
.call {
width: 32px;
height: 32px;
object-fit: contain;
margin-bottom: 4px;
}

View File

@ -0,0 +1,23 @@
import { useContext } from 'react';
import style from './index.module.css';
import CallPng from "/icons/call.png"
import { RealtimeClientContext } from '../Provider/RealtimeClientProvider';
interface IInvokeButtonProps extends React.HTMLAttributes<HTMLDivElement> {
loading?: boolean;
disable?: boolean;
}
export default function InvokeButton(props: IInvokeButtonProps) {
const {disable, loading, className, ...rest } = props;
const {isConnecting} = useContext(RealtimeClientContext);
return (
<div className={`${style.wrapper} ${className}`} {...rest}>
<img className={style.call} src={CallPng} alt="call" />
<div className={style.text}>{disable ? '暂不可用':isConnecting?'连接中':'发起通话'}</div>
</div>
);
}

View File

@ -0,0 +1,123 @@
.headerWrapper{
margin-top: 65px;
width: 100%;
padding: 0 16px;
box-sizing: border-box;
}
.wrapper {
height: 105px;
background: rgba(176, 228, 255, 0.8);
border-radius: 50px 13px 13px 13px;
border: 1px solid #ffffff;
position: relative;
z-index: 0;
margin-bottom: -54px;
}
.img {
width: 120px;
height: 123px;
object-fit: contain;
position: absolute;
bottom: 35px;
}
.wrapper::before{
content: '';
position: absolute;
width: 133px;
height: 32px;
bottom: 23px;
left: 6px;
background: radial-gradient(ellipse at top, #1580FF, #fff);
z-index: 2;
border-radius: 20px;
filter: blur(10px);
}
.text {
font-weight: 500;
font-size: 16px;
color: #000000;
text-align: end;
margin-top: 15px;
margin-right: 45px;
}
.main-wrapper {
background: rgba(255, 255, 255, 0.7);
border-radius: 13px 13px 13px 13px;
border: 1px solid #ffffff;
padding:16px 17px 18px 17px;
position: relative;
z-index: 1;
backdrop-filter:blur(10px);
min-height: 213px;
box-sizing: border-box;
}
.main {
display: flex;
justify-content: space-between;
}
.thing {
object-fit: contain;
position: relative;
height: 15px;
width: 91px;
}
.circle {
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
color:#2380e2;
}
.circle img{
width:16px;
height: 16px;
}
.rightIcon{
width: 10px;
height: 10px;
}
.change{
color:#2380e2;
}
.tip {
display: flex;
align-items: center;
justify-content: space-between;
font-weight: 400;
font-size: 16px;
color: #000000;
margin-top: 18px;
}
.rotating {
animation: rotate360 1s linear;
}
@keyframes rotate360 {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,108 @@
import { useState, useEffect, useContext } from "react";
import HelloGIF from "/icons/hello.gif";
import WhatsThing from "/icons/whatsThing.png";
import CircleIcon from "/icons/circle.png";
import RightIcon from "/icons/right.png";
import styles from "./index.module.css";
import { fetchQuestions } from "@/apis/questions";
import { RealtimeClientContext } from "../Provider/RealtimeClientProvider";
type Props = {
toRoom: () =>void;
};
export default function HeaderGroup({ toRoom }: Props) {
const [isRotating, setIsRotating] = useState(false);
const [displayQuestions, setDisplayQuestions] = useState<string[]>([]);
const [allQuestions, setAllQuestions] = useState<string[]>([]);
const { setInitMessage } = useContext(RealtimeClientContext);
// 随机获取4个问题的函数
const getRandomQuestions = () => {
const _allQuestions = Array.from(allQuestions);
const result: string[] = [];
const questionCount = Math.min(4, _allQuestions.length);
for (let i = 0; i < questionCount; i++) {
const randomIndex = Math.floor(Math.random() * _allQuestions.length);
result.push(_allQuestions.splice(randomIndex, 1)[0]);
}
return result;
};
const getQuestion = async ({ signal }: { signal: AbortSignal }) => {
const { result, message } = await fetchQuestions({ options: { signal } });
if (message) {
console.log(message);
} else {
setAllQuestions(result as string[]);
}
};
useEffect(() => {
setDisplayQuestions(getRandomQuestions());
}, [allQuestions]);
// 组件初始化时获取随机问题
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
getQuestion({ signal });
}, []);
const handleClick = () => {
setIsRotating(true);
// 更新随机问题
setDisplayQuestions(getRandomQuestions());
// 动画结束后重置状态
setTimeout(() => {
setIsRotating(false);
}, 1000);
};
const handleQuestion = async (question: string) => {
setInitMessage(question);
await toRoom();
};
return (
<div className={styles.headerWrapper}>
<div className={styles.wrapper}>
<img className={styles.img} src={HelloGIF} alt="hello" />
<div className={styles.text}>Hey,AI</div>
</div>
<div className={styles["main-wrapper"]}>
<div className={styles.main} onClick={handleClick}>
<img className={styles.thing} src={WhatsThing} alt="whatsThing" />
<div className={styles.circle}>
<img
src={CircleIcon}
className={isRotating ? styles.rotating : ""}
alt="circle"
/>
<div className={styles.change}></div>
</div>
</div>
{displayQuestions.map((item, index) => (
<div
className={styles.tip}
key={index}
onClick={() => handleQuestion(item)}
>
<div>{item}</div>
<img
src={RightIcon}
alt="right-icon"
className={styles.rightIcon}
/>
</div>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,41 @@
.scoreWrapper {
padding: 15px;
width: 100%;
box-sizing: border-box;
}
.innerWrapper {
padding: 15px;
width: 100%;
background: rgba(255, 255, 255, 0.7);
border-radius: 13px;
border: 1px solid #f4f6fa;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
}
.detail {
display: flex;
align-items: center;
gap: 10px;
margin-top: 10px;
font-size: 16px;
}
.right{
color: #1580FF;
display: flex;
align-items: center;
}
.imgIcon{
width: 91px;
height: 15px;
}
.rightBlue{
width: 10px;
height: 10px;
}

View File

@ -0,0 +1,47 @@
import { useSearchParams } from 'react-router-dom';
import MyInputIcon from '/icons/myInput.png';
import RightBlueIcon from '/icons/rightBlue.png';
import style from './index.module.css';
import { RealtimeClientContext } from '../Provider/RealtimeClientProvider';
import { useContext } from 'react';
type Props = {
toRoom: () => void;
};
export default function MyInput({ toRoom }: Props) {
const [searchParams] = useSearchParams()
const provinceName = searchParams.get('provinceName') || '山东省'
const subjectGroup = searchParams.get('subjectGroup') || '物/化/史'
const expectedScore = searchParams.get('expectedScore') || 500
const {setInitMessage} = useContext(RealtimeClientContext)
const handleQuestion = async () => {
toRoom();
setInitMessage(`我的高考地点在${provinceName},我选择的科目是${subjectGroup},我的高考分数为${expectedScore}分。我适合哪些学校和专业`);
};
return (
<div className={style.scoreWrapper}>
<div className={style.innerWrapper}>
<div className={style.left}>
<img src={MyInputIcon} className={style.imgIcon} alt="input-ico" />
<div className={style.detail}>
<div className={style.city}>{provinceName}</div>
<div className={style.subject}>{(subjectGroup as string).split(',').join('/') }</div>
<div className={style.score}>{expectedScore}</div>
</div>
</div>
<div className={style.right} onClick={handleQuestion}>
<span></span>
<img src={RightBlueIcon} alt="right" className={style.rightBlue} />
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,332 @@
import { ChatEventType, RoleType } from "@coze/api";
import {
EventNames,
RealtimeAPIError,
RealtimeClient,
RealtimeError,
RealtimeUtils,
} from "@coze/realtime-api";
import {
createContext,
ReactNode,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { useToast } from "@/hooks/use-toast";
type RoomInfo = {
appId: string;
roomId: string;
token: string;
uid: string;
};
export const RealtimeClientContext = createContext<{
client: RealtimeClient | null;
isConnecting: boolean;
isConnected: boolean;
audioEnabled: boolean;
isSupportVideo: boolean;
messageList: { content: string; role: RoleType }[];
isAiTalking: boolean;
roomInfo: RoomInfo | null;
initClient: () => void;
handleConnect: () => void;
handleInterrupt: () => void;
handleDisconnect: () => void;
toggleMicrophone: () => void;
sendUserMessageWithText: (message: string) => void;
setInitMessage: (message: string) => void;
}>({
client: null,
isConnecting: false,
isConnected: false,
audioEnabled: true,
isSupportVideo: false,
messageList: [],
isAiTalking: false,
roomInfo: null,
initClient: () => {},
handleConnect: () => {},
handleInterrupt: () => {},
handleDisconnect: () => {},
toggleMicrophone: () => {},
sendUserMessageWithText: () => {},
setInitMessage: () => {},
});
// 添加自定义hook
export const useRealtimeClient = () => {
const context = useContext(RealtimeClientContext);
if (context === undefined) {
throw new Error("useRealtimeClient必须在RealtimeClientProvider内部使用");
}
return { ...context };
};
export const RealtimeClientProvider = ({
children,
}: {
children: ReactNode;
}) => {
const token = import.meta.env.VITE_COZE_TOKEN;
const botId = import.meta.env.VITE_COZE_BOT_ID;
const voiceId = import.meta.env.VITE_COZE_VOICE_ID;
const connectorId = "1024";
const clientRef = useRef<RealtimeClient | null>(null);
// 实时语音回复消息列表
const [messageList, setMessageList] = useState<
{ content: string; role: RoleType }[]
>([]);
// 是否正在连接
const [isConnecting, setIsConnecting] = useState(false);
// 是否已连接
const [isConnected, setIsConnected] = useState(false);
// 是否开启麦克风
const [audioEnabled, setAudioEnabled] = useState(false);
// 是否支持视频
const [isSupportVideo] = useState(false);
// 是否正在说话
const [isAiTalking, setIsAiTalking] = useState(false);
const [initMessage, setInitMessage] = useState("");
const [isClientInitialized, setIsClientInitialized] = useState(false);
const [roomInfo, setRoomInfo] = useState<RoomInfo | null>(null);
const { toast } = useToast();
const initClient = async () => {
const permission = await RealtimeUtils.checkDevicePermission(false);
if (!permission.audio) {
toast({
title: "连接错误",
description: "需要麦克风访问权限",
});
throw new Error("需要麦克风访问权限");
}else{
const client = new RealtimeClient({
accessToken: token,
botId: botId,
voiceId: voiceId,
connectorId: connectorId,
allowPersonalAccessTokenInBrowser: true, // 可选:允许在浏览器中使用个人访问令牌
});
clientRef.current = client;
setupEventListeners(client);
setIsClientInitialized(true);
}
};
useEffect(() => {
if (clientRef.current) {
setupMessageEventListeners(clientRef.current);
if (initMessage) {
setupInitMessageEventListener(clientRef.current);
}
}
}, [initMessage, isClientInitialized]);
const handleConnect = async () => {
try {
if (!clientRef.current) {
await initClient();
}
await clientRef.current?.connect();
} catch (error) {
console.error(error);
if (error instanceof RealtimeAPIError) {
switch (error.code) {
case RealtimeError.CREATE_ROOM_ERROR:
console.error(`创建房间失败: ${error.message}`);
break;
case RealtimeError.CONNECTION_ERROR:
console.error(`加入房间失败: ${error.message}`);
break;
case RealtimeError.DEVICE_ACCESS_ERROR:
console.error(`获取设备失败: ${error.message}`);
break;
default:
console.error(`连接错误: ${error.message}`);
}
} else {
console.error("连接错误:" + error);
}
}
};
const handleInterrupt = () => {
try {
clientRef.current?.interrupt();
} catch (error) {
console.error("打断失败:" + error);
}
};
const handleDisconnect = () => {
try {
// 关闭客户的时候清除一些信息
setIsAiTalking(false);
setIsClientInitialized(false);
setMessageList([]);
clientRef.current?.disconnect();
clientRef.current?.clearEventHandlers();
clientRef.current = null;
setIsConnected(false);
} catch (error) {
console.error("断开失败:" + error);
}
};
const toggleMicrophone = async () => {
try {
await clientRef.current?.setAudioEnable(!audioEnabled);
setAudioEnabled(!audioEnabled);
} catch (error) {
console.error("切换麦克风状态失败:" + error);
}
};
const setupInitMessageEventListener = (client: RealtimeClient) => {
client.on(EventNames.ALL_SERVER, (eventName, _event: any) => {
if (eventName === "server.session.created") {
// 这里需要加个 server. 前缀
sendUserMessageWithText(initMessage);
}
});
};
const setupMessageEventListeners = (client: RealtimeClient) => {
let lastEvent: any;
client.on(EventNames.ALL, (_eventName, event: any) => {
// AI智能体设置
if (
event.event_type !== ChatEventType.CONVERSATION_MESSAGE_DELTA &&
event.event_type !== ChatEventType.CONVERSATION_MESSAGE_COMPLETED &&
event.event_type !== "conversation.created"
) {
return;
}
const content = event.data.content;
setMessageList((prev) => {
// 如果上一个事件是增量更新,则附加到最后一条消息
if (
lastEvent?.event_type === ChatEventType.CONVERSATION_MESSAGE_DELTA &&
(event.data.type === "answer" || event.data.type === "question")
) {
return [
...prev.slice(0, -1),
{
content: prev[prev.length - 1].content + content,
role: prev[prev.length - 1].role,
},
];
}
// 添加AI的欢迎语
if (initMessage === "" && event.event_type === "conversation.created") {
return [
...prev,
{ content: event.data.prologue, role: RoleType.Assistant },
];
}
// 否则添加新消息
if (
(content !== "" &&
event.event_type === ChatEventType.CONVERSATION_MESSAGE_DELTA) ||
(event.event_type === ChatEventType.CONVERSATION_MESSAGE_COMPLETED &&
(event.data.type === "answer" || event.data.type === "question") &&
event.data.role !== RoleType.Assistant)
) {
return [...prev, { content: content, role: event.data.role }];
}
return prev;
});
lastEvent = event;
});
};
// 设置事件监听器
const setupEventListeners = useCallback(
(client: RealtimeClient) => {
// 监听 AI 开始说话事件
client.on(EventNames.AUDIO_AGENT_SPEECH_STARTED, () => {
// console.log("AI开始说话");
setIsAiTalking(true);
setAudioEnabled(false);
});
// 监听 AI 结束说话事件
client.on(EventNames.AUDIO_AGENT_SPEECH_STOPPED, () => {
// console.log("AI结束说话");
setIsAiTalking(false);
setAudioEnabled(true);
});
// 监听连接客户端
client.on(EventNames.CONNECTING, () => {
setIsConnecting(true);
setIsConnected(false);
});
// 客户端连接成功
client.on(EventNames.CONNECTED, (_eventName: string, event: any) => {
setRoomInfo(event);
setIsConnecting(false);
setIsConnected(true);
});
},
[clientRef.current, initMessage]
);
// 发送信息
const sendUserMessageWithText = async (message: string) => {
try {
await clientRef.current?.sendMessage({
id: "",
event_type: "conversation.message.create",
data: {
role: "user",
content_type: "text",
content: message,
},
});
} catch (error) {
console.error("发送消息失败:" + error);
}
};
return (
<RealtimeClientContext.Provider
value={{
client: clientRef.current,
isConnecting,
isConnected,
audioEnabled,
isSupportVideo,
messageList,
isAiTalking,
roomInfo,
initClient,
handleConnect,
handleInterrupt,
handleDisconnect,
toggleMicrophone,
sendUserMessageWithText,
setInitMessage,
}}
>
{children}
</RealtimeClientContext.Provider>
);
};

View File

@ -0,0 +1,98 @@
.controller {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 16px;
background-color: #fff;
padding: 8px 15px;
padding-bottom: calc(12px + constant(safe-area-inset-bottom));
padding-bottom: calc(12px + env(safe-area-inset-bottom));
}
.talkWrapper {
--h: 16px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 8px 16px;
background-color: #e3efff;
border-radius: 20px;
user-select: none;
gap: 8px;
min-width: 116px;
color: #0078ff;
}
.listenerDot {
display: flex;
align-items: center;
justify-content: center;
position: relative;
height: var(--h);
width: 36px;
span {
display: inline-block;
width: 4px;
margin: 0 1px;
background-color: #4898fc;
border-radius: 2px;
height: calc(var(--h) - var(--d) * 4px);
transition: height 0.2s ease;
opacity: calc(1 - var(--d) * 0.4);
}
}
.isTalking {
span {
animation: soundWave 1s infinite ease-in-out;
}
span:nth-child(1) {
animation-delay: 0s;
}
span:nth-child(2) {
animation-delay: 0.2s;
}
span:nth-child(3) {
animation-delay: 0.4s;
}
span:nth-child(4) {
animation-delay: 0.6s;
}
span:nth-child(5) {
animation-delay: 0.8s;
}
}
@keyframes soundWave {
0%,
100% {
height: 4px;
}
50% {
height: 16px;
}
}
.microphoneWrapper {
display: flex;
align-items: center;
background-color: #f6f6f6;
padding: 10px 15px;
border-radius: 400px;
}
.timerWrapper {
display: flex;
align-items: center;
justify-content: center;
background-color: #f6f6f6;
padding: 8px 14px;
border-radius: 400px;
color: #0078ff;
font-size: 14px;
font-weight: 500;
width: 100%;
}

View File

@ -0,0 +1,55 @@
/**
* Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.
* SPDX-license-identifier: BSD-3-Clause
*/
import React, { useContext } from 'react';
import style from './index.module.css';
import LockMicroIcon from '/icons/lockmicrophone.png';
import MicroIcon from '/icons/microphone.png';
import HandleOffIcon from '/icons/handoff.png';
import { RealtimeClientContext } from '../Provider/RealtimeClientProvider';
import { useSignalRConnection } from '@/hooks/useMicrosoftSignal';
import { useSearchParams } from 'react-router-dom';
function AudioController({className, ...rest}: React.HTMLAttributes<HTMLDivElement>) {
const {handleDisconnect,toggleMicrophone,audioEnabled,isAiTalking,handleInterrupt,roomInfo} = useContext(RealtimeClientContext)
const [searchParams] = useSearchParams();
useSignalRConnection({access_token:searchParams.get('token') ||'',roomId:roomInfo?.roomId || ''})
return (
<div className={`${className} flex items-center justify-center bg-white pb-[20px] pt-[10px] gap-[10px]`} {...rest}>
<div className={`${style.microphoneWrapper}`} onClick={handleDisconnect}>
<img src={HandleOffIcon} alt="handoff" />
<div></div>
</div>
<div className={`${style.microphoneWrapper}`} onClick={toggleMicrophone}>
<img src={audioEnabled ? MicroIcon : LockMicroIcon} alt="lock" />
<div>{audioEnabled ? '关麦' : '开麦'}</div>
</div>
<div className={`${style.talkWrapper}`} onClick={handleInterrupt}>
<div className={`${isAiTalking ? style.isTalking : ''} ${style.listenerDot}`}>
<span style={{ '--d': '2' } as React.CSSProperties} />
<span style={{ '--d': '1' } as React.CSSProperties} />
<span style={{ '--d': '0' } as React.CSSProperties} />
<span style={{ '--d': '1' } as React.CSSProperties} />
<span style={{ '--d': '2' } as React.CSSProperties} />
</div>
<div>{isAiTalking ? '点击打断' : '正在听'}</div>
</div>
</div>
);
}
export default AudioController;

View File

@ -0,0 +1,46 @@
import { useRef, useEffect, useContext } from "react";
import { RealtimeClientContext } from "../Provider/RealtimeClientProvider";
import { RoleType } from "@coze/api";
export default function RoomConversation() {
const { messageList } = useContext(RealtimeClientContext);
const messagesEndRef = useRef<HTMLDivElement>(null);
// 自动滚动到最新消息
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
scrollToBottom();
}, [messageList]);
return (
<div className="flex-1 flex flex-col h-full">
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messageList.map((message: any, index: number) => (
<div
key={index}
className={`flex ${
message.role === RoleType.Assistant
? "justify-start"
: "justify-end"
}`}
>
<div
className={`max-w-3/4 p-3 rounded-lg ${
message.role === RoleType.Assistant
? "bg-white text-black rounded-tl-none"
: "bg-blue-500 text-white rounded-tr-none"
}`}
>
{message.content}
</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
</div>
);
}

View File

@ -0,0 +1,56 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

126
src/components/ui/toast.tsx Normal file
View File

@ -0,0 +1,126 @@
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Cross2Icon } from "@radix-ui/react-icons"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<Cross2Icon className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}

View File

@ -0,0 +1,33 @@
import { useToast } from "@/hooks/use-toast"
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}

0
src/entity/enum/index.ts Normal file
View File

86
src/globals.css Normal file
View File

@ -0,0 +1,86 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
@layer utilities {
/* Hide scrollbar for Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.no-scrollbar {
-ms-overflow-style: none;
/* IE and Edge */
scrollbar-width: none;
/* Firefox */
}
}

194
src/hooks/use-toast.ts Normal file
View File

@ -0,0 +1,194 @@
"use client"
// Inspired by react-hot-toast library
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }

View File

@ -0,0 +1,77 @@
import { useEffect, useRef } from "react";
import * as signalR from "@microsoft/signalr";
import { useRealtimeClient } from "@/components/Provider/RealtimeClientProvider";
import { useToast } from "@/hooks/use-toast";
export const useSignalRConnection = (params: {
access_token: string;
roomId: string;
}) => {
const connectionRef = useRef<signalR.HubConnection | null>(null);
const { handleDisconnect } = useRealtimeClient();
const { toast } = useToast();
useEffect(() => {
if (!params.access_token || !params.roomId) {
return;
}
const connection = new signalR.HubConnectionBuilder()
.withServerTimeout(30000)
.withAutomaticReconnect()
.withUrl(
`https://api.v3.ycymedu.com/hubs/weminpro?access_token=${params.access_token}&roomId=${params.roomId}`
)
.configureLogging(signalR.LogLevel.Information)
.build();
connectionRef.current = connection;
connection.on("ForceOffline", function (msg) {
// 可加逻辑:注销用户、跳转登录页等
toast({
variant: "destructive",
title: "下线提醒",
description: msg,
});
handleDisconnect();
});
connection.on("SendWarn", function (msg) {
console.warn(`下线提醒:${msg}"`);
// 也可以弹窗提醒用户保存数据
toast({
variant: "destructive",
title: "下线提醒",
description: msg,
});
});
connection
.start()
.then(() => {
console.log("SignalR连接已建立");
setInterval(() => {
connection.invoke("Ping");
}, 5000);
})
.catch((err) => {
console.error("SignalR连接失败:", err);
});
return () => {
if (connectionRef.current) {
connectionRef.current
.stop()
.then(() => {
console.log("SignalR连接已关闭");
})
.catch((err) => {
console.error("关闭SignalR连接失败:", err);
});
}
};
}, [params.access_token, params.roomId]);
return connectionRef.current;
};

7
src/hooks/useToken.ts Normal file
View File

@ -0,0 +1,7 @@
export const useTokenWithPat = () => {
// 替换成你的 PAT
const token = "pat_NhhZGW7sxkuyP4mJrPrVyZx20b3m6lymg0y2Ln9EyM0CV9q2f9t3rlGbtzppLQua"; // Access Token
return {
getToken: () => token
};
};

62
src/lib/customFetch.ts Normal file
View File

@ -0,0 +1,62 @@
/*
* @Author: HideInMatrix
* @Date: 2024-07-15
* @LastEditors: error: git config user.name & please set dead value or install git
* @LastEditTime: 2024-09-01
* @Description:
* @FilePath: /free-music-react/src/lib/customFetch.ts
*/
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
interface FetchOptions extends RequestInit {
headers?: Record<string, string>;
}
interface ApiResponse {
result?: unknown;
error?: string;
code?: number;
success?: boolean;
message?:string;
}
const apiClient = (method: HttpMethod) => {
return async (
url: string,
data?: unknown,
options: FetchOptions = {}
): Promise<ApiResponse> => {
const config: FetchOptions = {
method,
...options,
};
if (method !== "GET" && data) {
config.body = JSON.stringify(data);
} else if (method === "GET" && data) {
const _params = [];
for (const [key, value] of Object.entries(data)) {
_params.push(`${key}=${value}`);
}
url += `?${_params.join("&")}`;
}
const response = await fetch(`${url}`, config);
const result = await response.json();
if (!response.ok) {
return {
error: result.message || "Request failed",
code: response.status,
};
}
return result;
};
};
export const getRequest = apiClient("GET");
export const postRequest = apiClient("POST");
export const putRequest = apiClient("PUT");
export const deleteRequest = apiClient("DELETE");

121
src/lib/utils.ts Normal file
View File

@ -0,0 +1,121 @@
/*
* @Author: HideInMatrix
* @Date: 2024-07-16
* @LastEditors: HideInMatrix
* @LastEditTime: 2024-09-18
* @Description:
* @FilePath: /free-music-react/src/lib/utils.ts
*/
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
/**
* @Author: HideInMatrix
* @description:
* @param {array} inputs
* @return {*}
* @Date: 2024-07-17
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
/**
*
* @returns {boolean}
*/
export const isBrowser = typeof window !== "undefined";
/**
* @Author: HideInMatrix
* @description:
* @param {Function} fn
* @param {number} wait
* @return {*}
* @Date: 2024-07-28
*/
export const throttle = (fn: Function, wait: number) => {
let timer: NodeJS.Timeout | undefined;
return (...args: any) => {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = undefined;
}, wait);
}
};
};
/**
* @Author: HideInMatrix
* @description:
* @param {Function} fn
* @param {number} wait
* @return {*}
* @Date: 2024-07-28
*/
export const debounce = (fn: Function, wait: number) => {
let timer: NodeJS.Timeout | undefined;
return (...args: any) => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, args);
}, wait);
};
};
/**
* @Author: HideInMatrix
* @description:
* @seconds {number}
* @return {*}
* @Date: 2024-07-28
*/
export const formatTime = (seconds: number): string => {
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${minutes.toString().padStart(2, "0")}:${secs
.toString()
.padStart(2, "0")}`;
};
/**
* @Author: HideInMatrix
* @description:
* @return {*}
* @Date: 2024-09-08
*/
export const getNextEnumValue = <T extends Record<string, string>>(
enumObj: T,
currentValue: T[keyof T]
): T[keyof T] => {
const values = Object.values(enumObj) as T[keyof T][];
const currentIndex = values.indexOf(currentValue);
const nextIndex = (currentIndex + 1) % values.length;
return values[nextIndex];
};
// 检测容器是否需要滚动,若不需要则加载更多数据
export const checkAndLoadMore = ({
containerRef,
setPage,
toEnd,
}: {
containerRef: React.RefObject<HTMLDivElement>;
setPage: React.Dispatch<React.SetStateAction<number>>;
toEnd: boolean;
}) => {
const container = containerRef.current;
if (
container &&
container.children[0] &&
container.children[0].clientHeight <= container.clientHeight &&
!toEnd
) {
// 如果内容高度不足以滚动,且还有数据未加载,则主动触发加载
setPage((prevPage) => prevPage + 1);
}
};

13
src/main.tsx Normal file
View File

@ -0,0 +1,13 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import { router } from "@/router";
import "./globals.css";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>
);

44
src/router/index.tsx Normal file
View File

@ -0,0 +1,44 @@
import { lazy } from "react";
import { createBrowserRouter } from "react-router-dom";
import { Component } from "react";
const HomePage = lazy(() => import("@/app/App"));
const MainLayout = lazy(() => import("@/app/MainLayout"));
interface ErrorBoundaryProps {
children: React.ReactNode;
}
class ErrorBoundary extends Component<ErrorBoundaryProps> {
state = { hasError: false };
static getDerivedStateFromError(_error: any) {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <h1></h1>;
}
return this.props.children;
}
}
export const router = createBrowserRouter([
{
path: "/",
element: <MainLayout />,
errorElement: (
<ErrorBoundary>
<div className="flex-auto flex flex-col p-6"></div>
</ErrorBoundary>
),
children: [
{
path: "/",
element: <HomePage />,
},
],
},
]);

0
src/static/declare.d.ts vendored Normal file
View File

4
src/static/locales.ts Normal file
View File

@ -0,0 +1,4 @@
export const defaultLocale: string = 'zh';
export const locales: string[] = ['en', 'zh'];
export const timeZone = 'UTC';

View File

@ -0,0 +1,22 @@
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
type AccessesState = {
token: string;
setToken: (newVal: string) => void;
};
const useAccessStore = create<AccessesState>()(
persist(
(set, get) => ({
token: get()?.token || "",
setToken: (newVal: any) => set(() => ({ token: newVal })),
}),
{
name: "token",
storage: createJSONStorage(() => localStorage), // default localstorage
}
)
);
export default useAccessStore;

11
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_TOKEN: string
readonly VITE_BOT_ID: string
readonly VITE_VOICE_ID: string
// 其他环境变量...
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

57
tailwind.config.js Normal file
View File

@ -0,0 +1,57 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ["class"],
content: ["./src/**/*.{ts,tsx}"],
theme: {
extend: {
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
colors: {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
chart: {
1: "hsl(var(--chart-1))",
2: "hsl(var(--chart-2))",
3: "hsl(var(--chart-3))",
4: "hsl(var(--chart-4))",
5: "hsl(var(--chart-5))",
},
},
},
},
plugins: [require("tailwindcss-animate")],
};

35
tsconfig.app.json Normal file
View File

@ -0,0 +1,35 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": [
"src"
]
}

19
tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
},
}
}

23
tsconfig.node.json Normal file
View File

@ -0,0 +1,23 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

42
upload.sh Normal file
View File

@ -0,0 +1,42 @@
#!/bin/bash
# 服务器信息
SERVER_USER="root"
SERVER_HOST="106.14.30.150"
SERVER_PATH="/opt/1panel/apps/openresty/openresty/www/sites/chat.ycymedu.com/index"
PRIVATE_KEY="ALIYUN.pem"
BACKUP_PATH="${SERVER_PATH}-backup-$(date +%Y%m%d%H%M%S).zip"
DINGDING_WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=fca104958fea6273c9c7ef3f08b3d552645c214f929066785e8caf6e1885a5a6"
# 在上传之前备份原来的文件并压缩
ssh -i $PRIVATE_KEY $SERVER_USER@$SERVER_HOST "cd $(dirname $SERVER_PATH) && zip -r $(basename $BACKUP_PATH) $(basename $SERVER_PATH)"
# 使用 scp 上传文件
scp -i $PRIVATE_KEY -r build/* $SERVER_USER@$SERVER_HOST:$SERVER_PATH
# 提示上传完成
if [ $? -eq 0 ]; then
echo "上传成功!备份存储于 $BACKUP_PATH"
# 发送钉钉通知
curl -X POST "$DINGDING_WEBHOOK" \
-H "Content-Type: application/json" \
-d '{
"msgtype": "text",
"text": {
"content": "SixAIGC html| upload success!! backup to'"$BACKUP_PATH"'"
}
}'
else
echo "上传失败,请检查错误信息。"
# 发送钉钉通知
curl -X POST "$DINGDING_WEBHOOK" \
-H "Content-Type: application/json" \
-d '{
"msgtype": "text",
"text": {
"content": "SixAIGC html|upload failplease check error info。"
}
}'
fi

32
vite.config.ts Normal file
View File

@ -0,0 +1,32 @@
import path from "path";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
],
build: {
outDir: 'build',
reportCompressedSize: true,
sourcemap: false,
rollupOptions: {
output: {
manualChunks(id) {
// 根据需要进行手动代码分割
if (id.includes('node_modules')) {
return id.split('node_modules/')[1].split('/')[0]; // 将 node_modules 中的库分割成单独的块
}
},
},
},
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});