feat: 添加公共设备会议创建页面
- 新增 `PublicDeviceMeetingCreate` 组件,用于创建公共设备会议 - 支持选择 ASR 模型、总结模型、总结模板等配置 - 提供参会人员、主持人、会议标签、访问密码等字段 - 实现表单验证和提交功能,支持推送到设备dev_na
parent
7c3b65624e
commit
8716608afa
|
|
@ -0,0 +1,306 @@
|
|||
import { App, Button, Card, Col, DatePicker, Form, Input, Radio, Row, Select, Space, Spin, Typography } from "antd";
|
||||
import { AudioOutlined, CheckCircleOutlined, QrcodeOutlined } from "@ant-design/icons";
|
||||
import dayjs from "dayjs";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import { listUsers } from "@/api";
|
||||
import { getAiModelDefault, getAiModelPage, type AiModelVO } from "@/api/business/aimodel";
|
||||
import { getHotWordPage, type HotWordVO } from "@/api/business/hotword";
|
||||
import { getHotWordGroupOptions, type HotWordGroupVO } from "@/api/business/hotwordGroup";
|
||||
import {
|
||||
createPublicDeviceMeetingBySession,
|
||||
type PublicDeviceMeetingCreateCommand,
|
||||
type SummaryDetailLevel,
|
||||
} from "@/api/business/meeting";
|
||||
import { getPromptPage, type PromptTemplateVO } from "@/api/business/prompt";
|
||||
import type { SysUser } from "@/types";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
type FormValues = {
|
||||
title: string;
|
||||
meetingTime: dayjs.Dayjs;
|
||||
participants?: number[];
|
||||
tags?: string[];
|
||||
hostUserId?: number;
|
||||
asrModelId: number;
|
||||
summaryModelId: number;
|
||||
promptId: number;
|
||||
hotWordGroupId?: number;
|
||||
summaryDetailLevel: SummaryDetailLevel;
|
||||
useSpkId?: boolean;
|
||||
enableTextRefine?: boolean;
|
||||
userPrompt?: string;
|
||||
accessPassword?: string;
|
||||
};
|
||||
|
||||
export default function PublicDeviceMeetingCreate() {
|
||||
const { message } = App.useApp();
|
||||
const navigate = useNavigate();
|
||||
const { sessionId } = useParams<{ sessionId: string }>();
|
||||
const [form] = Form.useForm<FormValues>();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [asrModels, setAsrModels] = useState<AiModelVO[]>([]);
|
||||
const [llmModels, setLlmModels] = useState<AiModelVO[]>([]);
|
||||
const [prompts, setPrompts] = useState<PromptTemplateVO[]>([]);
|
||||
const [hotwordList, setHotwordList] = useState<HotWordVO[]>([]);
|
||||
const [hotWordGroups, setHotWordGroups] = useState<HotWordGroupVO[]>([]);
|
||||
const [userList, setUserList] = useState<SysUser[]>([]);
|
||||
|
||||
const watchedPromptId = Form.useWatch("promptId", form);
|
||||
const watchedHotWordGroupId = Form.useWatch("hotWordGroupId", form);
|
||||
|
||||
const selectedPrompt = useMemo(
|
||||
() => prompts.find((item) => item.id === watchedPromptId) || null,
|
||||
[prompts, watchedPromptId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem("accessToken");
|
||||
if (!token) {
|
||||
const redirect = encodeURIComponent(window.location.pathname + window.location.search);
|
||||
navigate(`/login?redirect=${redirect}`, { replace: true });
|
||||
return;
|
||||
}
|
||||
void loadInitialData();
|
||||
}, [navigate]);
|
||||
|
||||
const loadInitialData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [asrRes, llmRes, promptRes, hotwordRes, hotWordGroupRes, users, defaultAsr, defaultLlm] = await Promise.all([
|
||||
getAiModelPage({ current: 1, size: 100, type: "ASR" }),
|
||||
getAiModelPage({ current: 1, size: 100, type: "LLM" }),
|
||||
getPromptPage({ current: 1, size: 100 }),
|
||||
getHotWordPage({ current: 1, size: 1000 }),
|
||||
getHotWordGroupOptions(),
|
||||
listUsers(),
|
||||
getAiModelDefault("ASR"),
|
||||
getAiModelDefault("LLM"),
|
||||
]);
|
||||
const activePrompts = promptRes.data.data.records.filter((item: PromptTemplateVO) => item.status === 1);
|
||||
const defaultPrompt = activePrompts[0];
|
||||
setAsrModels(asrRes.data.data.records.filter((item: AiModelVO) => item.status === 1));
|
||||
setLlmModels(llmRes.data.data.records.filter((item: AiModelVO) => item.status === 1));
|
||||
setPrompts(activePrompts);
|
||||
setHotwordList(hotwordRes.data.data.records.filter((item: HotWordVO) => item.status === 1));
|
||||
setHotWordGroups((hotWordGroupRes.data.data || []).filter((item: HotWordGroupVO) => item.status === 1));
|
||||
setUserList(users || []);
|
||||
form.setFieldsValue({
|
||||
title: `设备会议 ${dayjs().format("MM-DD HH:mm")}`,
|
||||
meetingTime: dayjs(),
|
||||
asrModelId: defaultAsr.data.data?.id,
|
||||
summaryModelId: defaultLlm.data.data?.id,
|
||||
promptId: defaultPrompt?.id,
|
||||
hotWordGroupId: defaultPrompt?.hotWordGroupId ?? 0,
|
||||
summaryDetailLevel: "STANDARD",
|
||||
useSpkId: true,
|
||||
enableTextRefine: false,
|
||||
});
|
||||
} catch {
|
||||
message.error("加载建会配置失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!sessionId) {
|
||||
message.error("扫码会话不存在");
|
||||
return;
|
||||
}
|
||||
const values = await form.validateFields();
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const selectedHotWords = values.hotWordGroupId == null || values.hotWordGroupId === 0
|
||||
? undefined
|
||||
: hotwordList
|
||||
.filter((item) => item.hotWordGroupId === values.hotWordGroupId)
|
||||
.map((item) => item.word)
|
||||
.filter((word) => !!word?.trim());
|
||||
|
||||
const payload: PublicDeviceMeetingCreateCommand = {
|
||||
title: values.title,
|
||||
meetingTime: values.meetingTime.format("YYYY-MM-DD HH:mm:ss"),
|
||||
participants: values.participants?.join(",") || "",
|
||||
tags: values.tags?.join(",") || "",
|
||||
hostUserId: values.hostUserId,
|
||||
asrModelId: values.asrModelId,
|
||||
summaryModelId: values.summaryModelId,
|
||||
promptId: values.promptId,
|
||||
hotWordGroupId: values.hotWordGroupId,
|
||||
summaryDetailLevel: values.summaryDetailLevel,
|
||||
useSpkId: values.useSpkId ? 1 : 0,
|
||||
enableTextRefine: !!values.enableTextRefine,
|
||||
userPrompt: values.userPrompt,
|
||||
hotWords: selectedHotWords,
|
||||
accessPassword: values.accessPassword?.trim() || undefined,
|
||||
};
|
||||
await createPublicDeviceMeetingBySession(sessionId, payload);
|
||||
message.success("会议已创建,已推送到设备");
|
||||
form.resetFields();
|
||||
navigate("/meetings");
|
||||
} catch {
|
||||
message.error("创建设备会议失败");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: "100vh", background: "linear-gradient(180deg, #f7fbff 0%, #eef4f8 100%)", padding: "48px 16px" }}>
|
||||
<div style={{ maxWidth: 980, margin: "0 auto" }}>
|
||||
<Card style={{ borderRadius: 20, boxShadow: "0 24px 64px rgba(15, 49, 86, 0.08)", border: "1px solid #d9e6f2" }}>
|
||||
<Space direction="vertical" size={8} style={{ width: "100%", marginBottom: 32 }}>
|
||||
<Space size={12}>
|
||||
<div style={{ width: 52, height: 52, borderRadius: 14, background: "#e6f4ff", display: "flex", alignItems: "center", justifyContent: "center", color: "#1677ff", fontSize: 24 }}>
|
||||
<QrcodeOutlined />
|
||||
</div>
|
||||
<div>
|
||||
<Title level={3} style={{ margin: 0 }}>扫码设备发起会议</Title>
|
||||
<Text type="secondary">登录后填写会议基础信息,系统会把待开始会议推送到设备端。</Text>
|
||||
</div>
|
||||
</Space>
|
||||
<Space size={16} wrap>
|
||||
<Text type="secondary"><AudioOutlined /> 设备录音仍在设备端执行</Text>
|
||||
<Text type="secondary"><CheckCircleOutlined /> 创建成功后会自动推送到扫码设备</Text>
|
||||
</Space>
|
||||
</Space>
|
||||
|
||||
{loading ? (
|
||||
<div style={{ padding: "64px 0", textAlign: "center" }}>
|
||||
<Spin />
|
||||
</div>
|
||||
) : (
|
||||
<Form form={form} layout="vertical">
|
||||
<Row gutter={24}>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="title" label="会议标题" rules={[{ required: true, message: "请输入会议标题" }]}>
|
||||
<Input size="large" placeholder="请输入会议标题" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="meetingTime" label="会议时间" rules={[{ required: true, message: "请选择会议时间" }]}>
|
||||
<DatePicker showTime style={{ width: "100%" }} size="large" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={24}>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="participants" label="参会人员">
|
||||
<Select mode="multiple" placeholder="选择参会人员" showSearch optionFilterProp="children" size="large">
|
||||
{userList.map((u) => (
|
||||
<Option key={u.userId} value={u.userId}>{u.displayName || u.username}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="hostUserId" label="主持人">
|
||||
<Select allowClear placeholder="默认当前登录人" showSearch optionFilterProp="children" size="large">
|
||||
{userList.map((u) => (
|
||||
<Option key={u.userId} value={u.userId}>{u.displayName || u.username}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={24}>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="tags" label="会议标签">
|
||||
<Select mode="tags" placeholder="输入标签" size="large" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="accessPassword" label="访问密码">
|
||||
<Input size="large" placeholder="可为空" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={24}>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="asrModelId" label="语音识别模型" rules={[{ required: true, message: "请选择 ASR 模型" }]}>
|
||||
<Select placeholder="选择 ASR 模型" size="large">
|
||||
{asrModels.map((m) => (
|
||||
<Option key={m.id} value={m.id}>{m.modelName}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="summaryModelId" label="总结模型" rules={[{ required: true, message: "请选择总结模型" }]}>
|
||||
<Select placeholder="选择总结模型" size="large">
|
||||
{llmModels.map((m) => (
|
||||
<Option key={m.id} value={m.id}>{m.modelName}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={24}>
|
||||
<Col span={24}>
|
||||
<Form.Item name="promptId" label="总结模板" rules={[{ required: true, message: "请选择总结模板" }]}>
|
||||
<Select placeholder="请选择总结模板" showSearch optionFilterProp="children" size="large">
|
||||
{prompts.map((p) => (
|
||||
<Option key={p.id} value={p.id}>{p.templateName}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={24}>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item
|
||||
name="hotWordGroupId"
|
||||
label="热词组"
|
||||
extra={watchedHotWordGroupId != null ? "创建会议时优先使用这里选择的热词组" : undefined}
|
||||
>
|
||||
<Select
|
||||
placeholder={selectedPrompt?.hotWordGroupId ? "默认已带出模板热词组,可按需修改" : "请选择热词组"}
|
||||
size="large"
|
||||
options={[{ label: "不使用热词组", value: 0 }, ...hotWordGroups.map((item) => ({ label: `${item.groupName} (${item.hotWordCount}/200)`, value: item.id }))]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="summaryDetailLevel" label="总结详细程度" rules={[{ required: true, message: "请选择总结详细程度" }]}>
|
||||
<Radio.Group>
|
||||
<Radio value="DETAILED">详细</Radio>
|
||||
<Radio value="STANDARD">标准</Radio>
|
||||
<Radio value="BRIEF">简洁</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={24}>
|
||||
<Col span={24}>
|
||||
<Form.Item name="userPrompt" label="用户提示词">
|
||||
<Input.TextArea autoSize={{ minRows: 3, maxRows: 6 }} maxLength={1000} showCount placeholder="例如:请重点关注待办事项与负责人" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "flex-end", marginTop: 24 }}>
|
||||
<Space>
|
||||
<Button size="large" onClick={() => navigate(-1)}>返回</Button>
|
||||
<Button type="primary" size="large" loading={submitting} onClick={() => void handleSubmit()}>
|
||||
创建并推送到设备
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue