feat: 添加公共设备会议创建页面

- 新增 `PublicDeviceMeetingCreate` 组件,用于创建公共设备会议
- 支持选择 ASR 模型、总结模型、总结模板等配置
- 提供参会人员、主持人、会议标签、访问密码等字段
- 实现表单验证和提交功能,支持推送到设备
dev_na
chenhao 2026-06-02 17:20:25 +08:00
parent 7c3b65624e
commit 8716608afa
1 changed files with 306 additions and 0 deletions

View File

@ -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>
);
}