feat(web): app share
This commit is contained in:
@@ -1,13 +1,23 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-06 21:11:51
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-13 17:11:14
|
||||
*/
|
||||
import { type FC, useRef, useState } from 'react'
|
||||
import RecordRTC from 'recordrtc'
|
||||
|
||||
import { fileUploadUrlWithoutApiPrefix } from '@/api/fileStorage'
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
/** Props for the AudioRecorder component */
|
||||
interface AudioRecorderProps {
|
||||
/** Callback fired when recording is complete, receives uploaded file info and raw blob */
|
||||
onRecordingComplete?: (file: { file_id: string; file_key: string; url: string; type?: string; }, blob?: Blob) => void
|
||||
className?: string;
|
||||
/** Upload endpoint URL, defaults to fileUploadUrlWithoutApiPrefix */
|
||||
action?: string;
|
||||
/** Additional config passed to the upload request */
|
||||
requestConfig?: Record<string, any>;
|
||||
}
|
||||
|
||||
@@ -17,9 +27,12 @@ const AudioRecorder: FC<AudioRecorderProps> = ({
|
||||
action = fileUploadUrlWithoutApiPrefix,
|
||||
requestConfig = {}
|
||||
}) => {
|
||||
// Whether the recorder is currently capturing audio
|
||||
const [isRecording, setIsRecording] = useState(false)
|
||||
// Holds the RecordRTC instance across renders
|
||||
const recorderRef = useRef<RecordRTC | null>(null)
|
||||
|
||||
/** Request microphone access and start recording */
|
||||
const startRecording = async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
||||
@@ -34,6 +47,7 @@ const AudioRecorder: FC<AudioRecorderProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
/** Stop recording, upload the audio blob, then invoke the completion callback */
|
||||
const stopRecording = () => {
|
||||
if (recorderRef.current) {
|
||||
recorderRef.current.stopRecording(() => {
|
||||
@@ -49,6 +63,7 @@ const AudioRecorder: FC<AudioRecorderProps> = ({
|
||||
type: blob.type,
|
||||
url
|
||||
}, blob)
|
||||
// Release recorder resources after upload
|
||||
recorderRef.current?.destroy()
|
||||
recorderRef.current = null
|
||||
})
|
||||
@@ -57,12 +72,14 @@ const AudioRecorder: FC<AudioRecorderProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle between recording/idle states on click;
|
||||
// swap background image to reflect current state
|
||||
return (
|
||||
<div
|
||||
className={`rb:size-5.5 rb:cursor-pointer rb:bg-cover ${className} ${
|
||||
isRecording
|
||||
? `rb:bg-[url('@/assets/images/conversation/audio_ing.gif')]`
|
||||
: `rb:bg-[url('@/assets/images/conversation/audio.svg')] rb:hover:bg-[url('@/assets/images/conversation/audio_hover.svg')]`
|
||||
: `rb:bg-[url('@/assets/images/conversation/audio.svg')]`
|
||||
}`}
|
||||
onClick={isRecording ? stopRecording : startRecording}
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 15:01:59
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-02 15:46:05
|
||||
* @Last Modified time: 2026-03-12 14:59:38
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { type FC, type ReactNode, useEffect } from 'react';
|
||||
import { type RadioGroupProps } from 'antd';
|
||||
import { type RadioGroupProps, Flex } from 'antd';
|
||||
import clsx from 'clsx'
|
||||
|
||||
// Button checkbox component props
|
||||
@@ -32,6 +32,7 @@ interface ButtonCheckboxProps extends Omit<RadioGroupProps, 'onChange'> {
|
||||
checkedIcon?: string;
|
||||
/** Button content */
|
||||
children?: ReactNode
|
||||
cicle?: boolean;
|
||||
}
|
||||
|
||||
const ButtonCheckbox: FC<ButtonCheckboxProps> = ({
|
||||
@@ -41,6 +42,7 @@ const ButtonCheckbox: FC<ButtonCheckboxProps> = ({
|
||||
icon,
|
||||
checkedIcon,
|
||||
children,
|
||||
cicle = false
|
||||
}) => {
|
||||
// Listen to value changes and trigger side effects via onValueChange callback
|
||||
useEffect(() => {
|
||||
@@ -57,21 +59,26 @@ const ButtonCheckbox: FC<ButtonCheckboxProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx("rb:flex rb:items-center rb:border rb:rounded-lg rb:px-2 rb:text-[12px] rb:h-6 rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
|
||||
<Flex
|
||||
align="center"
|
||||
justify={cicle ? 'center' : 'start'}
|
||||
gap={4}
|
||||
className={clsx("rb:flex rb:items-center rb:cursor-pointer rb:border rb:hover:bg-[#F6F6F6]", {
|
||||
'rb:size-7 rb:rounded-[14px] rb:border-[0.5px] rb:border-[#EBEBEB]': cicle,
|
||||
'rb:rounded-lg rb:px-2 rb:text-[12px] rb:h-6': !cicle,
|
||||
// Checked state: blue background and border
|
||||
"rb:bg-[rgba(21,94,239,0.06)] rb:border-[#155EEF] rb:text-[#155EEF]": checked,
|
||||
"rb:bg-[rgba(21,94,239,0.06)] rb:border-[rgba(21,94,239,0.25)] rb:hover:bg-[rgba(21,94,239,0.06)] rb:text-[#155EEF]": checked,
|
||||
// Unchecked state: gray border and dark text
|
||||
"rb:border-[#DFE4ED] rb:text-[#212332]": !checked,
|
||||
})}
|
||||
onClick={handleChange}
|
||||
>
|
||||
{/* Display unchecked icon when not checked */}
|
||||
{icon && !checked && <img src={icon} className="rb:w-4 rb:h-4 rb:mr-1" />}
|
||||
{icon && !checked && <img src={icon} className="rb:size-4" />}
|
||||
{/* Display checked icon when checked */}
|
||||
{checkedIcon && checked && <img src={checkedIcon} className="rb:w-4 rb:h-4 rb:mr-1" />}
|
||||
{children}
|
||||
</div>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2025-12-10 16:46:09
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-06 21:05:09
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-12 13:57:49
|
||||
*/
|
||||
import { type FC } from 'react'
|
||||
import ChatInput from './ChatInput'
|
||||
@@ -25,7 +25,8 @@ const Chat: FC<ChatProps> = ({
|
||||
labelFormat,
|
||||
errorDesc,
|
||||
fileList,
|
||||
fileChange
|
||||
fileChange,
|
||||
renderRuntime
|
||||
}) => {
|
||||
return (
|
||||
<div className="rb:h-full rb:relative rb:pt-2">
|
||||
@@ -37,6 +38,7 @@ const Chat: FC<ChatProps> = ({
|
||||
empty={empty}
|
||||
labelFormat={labelFormat}
|
||||
errorDesc={errorDesc}
|
||||
renderRuntime={renderRuntime}
|
||||
/>
|
||||
|
||||
{/* Chat input area */}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2025-12-10 16:45:54
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-06 21:05:09
|
||||
* @Last Modified time: 2026-03-12 13:57:51
|
||||
*/
|
||||
import { type ReactNode } from 'react'
|
||||
|
||||
@@ -53,6 +53,7 @@ export interface ChatProps {
|
||||
fileList?: any[];
|
||||
/** Attachment update */
|
||||
fileChange?: (fileList: any[]) => void;
|
||||
renderRuntime?: (item: ChatItem, index: number) => ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user