feat: init commit
commit
969254663b
|
|
@ -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
|
||||
|
|
@ -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-----
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# 音乐地带
|
||||
|
||||
免费的听歌网站(开发中)
|
||||
|
||||
## ✨ 特性
|
||||
|
||||
- 使用React开发,nextjs分支使用Next.js 14版本开发(后期不维护了)。
|
||||
- 🖥️ 支持 PWA,可在 Chrome/Edge 里点击地址栏右边的 ➕ 安装到电脑
|
||||
- 无需VIP直接在线播放歌曲
|
||||
- 🛠 更多特性开发中
|
||||
|
||||
## ⚙️ 部署至 Vercel
|
||||
|
||||
除了下载安装包使用,你还可以将本项目部署到 Vercel 或你的服务器上。下面是部署到 Vercel 的方法。
|
||||
|
||||
本项目的 Demo (https://freemusic.micromatrix.org) 就是部署在 Vercel 上的网站。
|
||||
|
||||
[](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
|
|
@ -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};
|
||||
|
|
@ -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
|
|
@ -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}
|
||||
|
|
@ -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 _};
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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 },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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}
|
||||
}
|
||||
};
|
||||
|
|
@ -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}
|
||||
}
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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%;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,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 */
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
export const useTokenWithPat = () => {
|
||||
// 替换成你的 PAT
|
||||
const token = "pat_NhhZGW7sxkuyP4mJrPrVyZx20b3m6lymg0y2Ln9EyM0CV9q2f9t3rlGbtzppLQua"; // Access Token
|
||||
return {
|
||||
getToken: () => token
|
||||
};
|
||||
};
|
||||
|
|
@ -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");
|
||||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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,0 +1,4 @@
|
|||
|
||||
export const defaultLocale: string = 'zh';
|
||||
export const locales: string[] = ['en', 'zh'];
|
||||
export const timeZone = 'UTC';
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")],
|
||||
};
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -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 fail,please check error info。"
|
||||
}
|
||||
}'
|
||||
fi
|
||||
|
|
@ -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"),
|
||||
},
|
||||
},
|
||||
});
|
||||
Loading…
Reference in New Issue