From 7671f94fcd6068e48c60a231ef3c1fd05f479a94 Mon Sep 17 00:00:00 2001
From: puz <13060209078@163.com>
Date: Wed, 8 Jul 2026 16:31:28 +0800
Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=20=E7=95=8C=E9=9D=A2=E4=BC=98?=
=?UTF-8?q?=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
frontend/src/pages/business/Meetings.css | 10 +-
.../src/pages/business/PromptTemplates.css | 34 +++-
.../src/pages/business/PromptTemplates.tsx | 31 +--
.../pages/business/ScreenSaverManagement.css | 63 ++++++
.../pages/business/ScreenSaverManagement.tsx | 50 +++--
.../src/pages/system/sys-params/index.less | 79 ++++++++
.../src/pages/system/sys-params/index.tsx | 186 ++++++++++++++++--
7 files changed, 405 insertions(+), 48 deletions(-)
diff --git a/frontend/src/pages/business/Meetings.css b/frontend/src/pages/business/Meetings.css
index 1b6c8e6..1543219 100644
--- a/frontend/src/pages/business/Meetings.css
+++ b/frontend/src/pages/business/Meetings.css
@@ -119,21 +119,23 @@
}
.meetings-card-scroll .ant-list-items {
- flex: 1 1 auto;
- min-height: 100%;
+ flex: 0 0 auto;
+ min-height: 0;
height: auto;
display: grid;
grid-template-columns: repeat(var(--meeting-card-columns), minmax(0, 1fr));
- grid-auto-rows: minmax(var(--meeting-card-grid-min-height), 1fr);
+ grid-auto-rows: var(--meeting-card-grid-min-height);
gap: var(--meeting-card-grid-gap);
align-items: stretch;
- align-content: stretch;
+ align-content: start;
}
.meetings-card-scroll--full-page .ant-list-items {
+ flex: 1 1 auto;
height: 100%;
min-height: 0;
grid-template-rows: repeat(var(--meeting-card-rows), minmax(var(--meeting-card-grid-min-height), 1fr));
+ align-content: stretch;
}
.meeting-card-list-item.ant-list-item {
diff --git a/frontend/src/pages/business/PromptTemplates.css b/frontend/src/pages/business/PromptTemplates.css
index e00e87a..149b835 100644
--- a/frontend/src/pages/business/PromptTemplates.css
+++ b/frontend/src/pages/business/PromptTemplates.css
@@ -111,6 +111,23 @@
min-width: 0;
}
+.prompt-template-editor-header {
+ align-items: center;
+ margin: 24px 0 20px;
+}
+
+.prompt-template-editor-header__col {
+ display: flex;
+ align-items: center;
+ min-height: 25px;
+}
+
+.prompt-template-editor-title {
+ min-width: 0;
+ font-weight: 600;
+ line-height: 22px;
+}
+
.prompt-template-editor {
height: calc(100vh - 400px);
}
@@ -142,10 +159,25 @@
}
.prompt-template-editor__preview-meta {
- margin-bottom: 12px;
+ display: flex;
+ align-items: center;
+ min-width: 0;
+ max-width: min(420px, 40vw);
+}
+
+.prompt-template-editor__preview-meta .ant-tag {
+ max-width: 100%;
+ margin-inline-end: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
@media (max-width: 1200px) {
+ .prompt-template-editor__preview-meta {
+ max-width: 100%;
+ }
+
.prompt-template-editor {
height: auto;
}
diff --git a/frontend/src/pages/business/PromptTemplates.tsx b/frontend/src/pages/business/PromptTemplates.tsx
index 8873aa0..b2d783a 100644
--- a/frontend/src/pages/business/PromptTemplates.tsx
+++ b/frontend/src/pages/business/PromptTemplates.tsx
@@ -3,7 +3,6 @@ import {
App,
Button,
Col,
- Divider,
Empty,
Form,
Input,
@@ -562,7 +561,25 @@ const PromptTemplates: React.FC = () => {
- 提示词编辑器 (Markdown 实时预览)
+
+
+
+ 提示词编辑器 (Markdown 实时预览)
+
+
+
+
+ {selectedHotWordGroupId ? (
+
+ 绑定热词组:
+ {groupOptions.find((item) => item.id === selectedHotWordGroupId)?.groupName || '已选择'}
+
+ ) : (
+ 未绑定热词组
+ )}
+
+
+
@@ -574,16 +591,6 @@ const PromptTemplates: React.FC = () => {
-
- {selectedHotWordGroupId ? (
-
- 绑定热词组:
- {groupOptions.find((item) => item.id === selectedHotWordGroupId)?.groupName || '已选择'}
-
- ) : (
- 未绑定热词组
- )}
-
{previewContent}
diff --git a/frontend/src/pages/business/ScreenSaverManagement.css b/frontend/src/pages/business/ScreenSaverManagement.css
index 31cfe2b..e67267d 100644
--- a/frontend/src/pages/business/ScreenSaverManagement.css
+++ b/frontend/src/pages/business/ScreenSaverManagement.css
@@ -76,6 +76,12 @@
background: #f9fafe;
}
+.screen-saver-page .screen-saver-table-thumb .ant-image {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+
.screen-saver-page .screen-saver-table-thumb img {
display: block;
width: 100%;
@@ -83,6 +89,19 @@
object-fit: cover;
}
+.screen-saver-page .screen-saver-table-thumb__image {
+ cursor: zoom-in;
+ transition: transform 0.2s ease;
+}
+
+.screen-saver-page .screen-saver-table-thumb:hover .screen-saver-table-thumb__image {
+ transform: scale(1.03);
+}
+
+.screen-saver-page .screen-saver-table-thumb .ant-image-mask {
+ border-radius: 4px;
+}
+
.screen-saver-page .screen-saver-table-thumb__empty {
width: 100%;
height: 100%;
@@ -124,15 +143,33 @@
max-width: 420px;
margin: 0 auto;
aspect-ratio: 8 / 5;
+ display: block;
+ padding: 0;
border-radius: 4px;
background: #f5f6fa;
border: 1px solid #e6e6e6;
+ cursor: pointer;
+ color: inherit;
}
.screen-saver-drawer .screen-saver-preview-stage::after {
content: none;
}
+.screen-saver-drawer .screen-saver-preview-stage:hover,
+.screen-saver-drawer .screen-saver-preview-stage:focus-visible {
+ border-color: #3c70f5;
+}
+
+.screen-saver-drawer .screen-saver-preview-stage:focus-visible {
+ outline: 2px solid rgba(60, 112, 245, 0.28);
+ outline-offset: 2px;
+}
+
+.screen-saver-drawer .screen-saver-preview-stage:disabled {
+ cursor: wait;
+}
+
.screen-saver-drawer .screen-saver-preview-stage img {
display: block;
width: 100%;
@@ -147,6 +184,32 @@
color: #8c9bb3;
}
+.screen-saver-drawer .screen-saver-preview-stage__action {
+ position: absolute;
+ inset: auto 12px 12px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ min-height: 32px;
+ padding: 6px 10px;
+ border-radius: 4px;
+ background: rgba(16, 35, 63, 0.74);
+ color: #ffffff;
+ font-size: 13px;
+ font-weight: 600;
+ opacity: 0;
+ transform: translateY(4px);
+ transition: opacity 0.2s ease, transform 0.2s ease;
+}
+
+.screen-saver-drawer .screen-saver-preview-stage:hover .screen-saver-preview-stage__action,
+.screen-saver-drawer .screen-saver-preview-stage:focus-visible .screen-saver-preview-stage__action,
+.screen-saver-drawer .screen-saver-preview-stage__empty + .screen-saver-preview-stage__action {
+ opacity: 1;
+ transform: translateY(0);
+}
+
.screen-saver-crop-modal .ant-modal-content {
overflow: hidden;
border-radius: 4px;
diff --git a/frontend/src/pages/business/ScreenSaverManagement.tsx b/frontend/src/pages/business/ScreenSaverManagement.tsx
index 15496d8..632b062 100644
--- a/frontend/src/pages/business/ScreenSaverManagement.tsx
+++ b/frontend/src/pages/business/ScreenSaverManagement.tsx
@@ -8,6 +8,7 @@ import {
Drawer,
Empty,
Form,
+ Image as AntImage,
Input,
InputNumber,
Modal,
@@ -618,7 +619,12 @@ export default function ScreenSaverManagement() {
render: (_, record) => (
{record.imageUrl ? (
-

+
) : (
@@ -876,24 +882,30 @@ export default function ScreenSaverManagement() {
屏保成片预览
固定 8:5 构图,根据设定尺寸导出。上传后后端只做校验与存储。
-
- } loading={uploading}>
- 选择图片并裁剪
-
-
-
- {currentImageUrl ? (
-

- ) : (
-
-
-
- 请选择图片并完成裁剪后上传
-
-
- )}
-
+
+
+
输出规格 {currentWidth} × {currentHeight}
当前作用域 {currentScopeType === "USER" ? "用户级" : "平台级"}
@@ -924,7 +936,7 @@ export default function ScreenSaverManagement() {
-
+
diff --git a/frontend/src/pages/system/sys-params/index.less b/frontend/src/pages/system/sys-params/index.less
index bd1c202..32cb1b2 100644
--- a/frontend/src/pages/system/sys-params/index.less
+++ b/frontend/src/pages/system/sys-params/index.less
@@ -32,6 +32,85 @@
border-radius: 4px;
}
+.param-value-template-preview {
+ display: block;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ cursor: default;
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
+ font-size: 12px;
+}
+
+.param-value-template-token {
+ display: inline-flex;
+ align-items: center;
+ max-width: 100%;
+ margin: 0 2px;
+ padding: 0 5px;
+ border: 1px solid #91caff;
+ border-radius: 4px;
+ background: #e6f4ff;
+ color: #1677ff;
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
+ font-size: 12px;
+ line-height: 18px;
+ vertical-align: baseline;
+}
+
+.param-value-template-popover {
+ max-width: min(720px, 70vw);
+}
+
+.param-value-template-popover__content {
+ max-width: min(680px, 66vw);
+ max-height: 360px;
+ margin: 0 !important;
+ overflow: auto;
+ white-space: pre-wrap;
+ word-break: break-word;
+ user-select: text;
+ line-height: 1.6;
+}
+
+.param-value-rich-editor {
+ min-height: var(--param-value-editor-min-height);
+ max-height: 260px;
+ padding: 6px 11px;
+ overflow: auto;
+ border: 1px solid #d9d9d9;
+ border-radius: 6px;
+ background: #fff;
+ color: rgba(0, 0, 0, 0.88);
+ font-family: 'SFMono-Regular', Consolas, Menlo, monospace;
+ font-size: 14px;
+ line-height: 1.5715;
+ outline: none;
+ white-space: pre-wrap;
+ word-break: break-word;
+ transition: border-color 0.2s, box-shadow 0.2s;
+}
+
+.param-value-rich-editor:hover {
+ border-color: #4096ff;
+}
+
+.param-value-rich-editor:focus {
+ border-color: #1677ff;
+ box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1);
+}
+
+.param-value-rich-editor:empty::before {
+ color: rgba(0, 0, 0, 0.25);
+ content: attr(data-placeholder);
+}
+
+.param-value-rich-editor .param-value-template-token {
+ margin: 0 2px;
+ vertical-align: 1px;
+}
+
.param-boolean-segmented {
width: 176px;
padding: 2px;
diff --git a/frontend/src/pages/system/sys-params/index.tsx b/frontend/src/pages/system/sys-params/index.tsx
index 7f6be60..cd2334c 100644
--- a/frontend/src/pages/system/sys-params/index.tsx
+++ b/frontend/src/pages/system/sys-params/index.tsx
@@ -1,5 +1,5 @@
-import { Button, Col, Drawer, Form, Input, Popconfirm, Row, Segmented, Select, Space, Switch, Tag, Tooltip, Typography, message } from "antd";
-import { useCallback, useEffect, useState } from "react";
+import { Button, Col, Drawer, Form, Input, Popconfirm, Popover, Row, Segmented, Select, Space, Switch, Tag, Tooltip, Typography, message } from "antd";
+import { type CSSProperties, type ReactNode, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { DeleteOutlined, EditOutlined, InfoCircleOutlined, PlusOutlined, SearchOutlined, SettingOutlined } from "@ant-design/icons";
import { createParam, deleteParam, pageParams, updateParam } from "@/api";
@@ -14,12 +14,176 @@ import LongTextPreview from "@/components/shared/LongTextPreview";
import type { SysParamQuery, SysParamVO } from "@/types";
import "./index.less";
-const { Text } = Typography;
+const { Paragraph, Text } = Typography;
+const TEMPLATE_PARAM_PATTERN = /\{\{[^{}\s]+\}\}/g;
/** Normalize paramType to title-case for case-insensitive comparison */
const isType = (actual: string | undefined, expected: string) =>
(actual || "").toLowerCase() === expected.toLowerCase();
+const hasTemplateParam = (text: string) => {
+ TEMPLATE_PARAM_PATTERN.lastIndex = 0;
+ const matched = TEMPLATE_PARAM_PATTERN.test(text);
+ TEMPLATE_PARAM_PATTERN.lastIndex = 0;
+ return matched;
+};
+
+const renderTemplateParamSegments = (text: string) => {
+ const segments: ReactNode[] = [];
+ let lastIndex = 0;
+
+ TEMPLATE_PARAM_PATTERN.lastIndex = 0;
+ text.replace(TEMPLATE_PARAM_PATTERN, (match: string, offset: number) => {
+ if (offset > lastIndex) {
+ segments.push(text.slice(lastIndex, offset));
+ }
+ segments.push(
+
+ {match}
+
+ );
+ lastIndex = offset + match.length;
+ return match;
+ });
+ TEMPLATE_PARAM_PATTERN.lastIndex = 0;
+
+ if (lastIndex < text.length) {
+ segments.push(text.slice(lastIndex));
+ }
+
+ return segments;
+};
+
+const readEditorText = (element: HTMLElement) => element.innerText.replace(/\r\n/g, "\n");
+
+const getCaretTextOffset = (element: HTMLElement) => {
+ const selection = window.getSelection();
+ if (!selection || selection.rangeCount === 0 || !selection.anchorNode || !element.contains(selection.anchorNode)) {
+ return null;
+ }
+
+ const range = selection.getRangeAt(0);
+ const prefixRange = range.cloneRange();
+ prefixRange.selectNodeContents(element);
+ prefixRange.setEnd(range.endContainer, range.endOffset);
+ return prefixRange.toString().length;
+};
+
+const restoreCaretTextOffset = (element: HTMLElement, offset: number) => {
+ const selection = window.getSelection();
+ if (!selection) return;
+
+ const range = document.createRange();
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
+ let remaining = offset;
+ let node = walker.nextNode();
+
+ while (node) {
+ const textLength = node.textContent?.length ?? 0;
+ if (remaining <= textLength) {
+ range.setStart(node, remaining);
+ range.collapse(true);
+ selection.removeAllRanges();
+ selection.addRange(range);
+ return;
+ }
+ remaining -= textLength;
+ node = walker.nextNode();
+ }
+
+ range.selectNodeContents(element);
+ range.collapse(false);
+ selection.removeAllRanges();
+ selection.addRange(range);
+};
+
+function TemplateParamValueEditor({
+ value,
+ onChange,
+ onFocus,
+ onTextChange,
+ placeholder,
+ rows = 8
+}: {
+ value?: string | number | null;
+ onChange?: (value: string) => void;
+ onFocus?: () => void;
+ onTextChange?: (value: string) => void;
+ placeholder?: string;
+ rows?: number;
+}) {
+ const editorRef = useRef(null);
+ const pendingCaretOffsetRef = useRef(null);
+ const text = value === undefined || value === null ? "" : String(value);
+ const editorStyle = { "--param-value-editor-min-height": `${rows * 22 + 20}px` } as CSSProperties;
+
+ useLayoutEffect(() => {
+ const editor = editorRef.current;
+ const caretOffset = pendingCaretOffsetRef.current;
+ if (!editor || caretOffset === null || document.activeElement !== editor) {
+ return;
+ }
+
+ restoreCaretTextOffset(editor, caretOffset);
+ pendingCaretOffsetRef.current = null;
+ }, [text]);
+
+ const handleInput = () => {
+ const editor = editorRef.current;
+ if (!editor) return;
+
+ pendingCaretOffsetRef.current = getCaretTextOffset(editor);
+ const nextValue = readEditorText(editor);
+ onChange?.(nextValue);
+ onTextChange?.(nextValue);
+ };
+
+ return (
+ {
+ event.preventDefault();
+ document.execCommand("insertText", false, event.clipboardData.getData("text/plain"));
+ }}
+ >
+ {text ? renderTemplateParamSegments(text) : null}
+
+ );
+}
+
+function TemplateParamValuePreview({ value }: { value?: string | number | null }) {
+ const text = value === undefined || value === null || value === "" ? "-" : String(value);
+
+ if (!hasTemplateParam(text)) {
+ return ;
+ }
+
+ return (
+
+ {renderTemplateParamSegments(text)}
+
+ }
+ >
+ {renderTemplateParamSegments(text)}
+
+ );
+}
+
export default function SysParams() {
const { t } = useTranslation();
const { can } = usePermission();
@@ -173,7 +337,7 @@ export default function SysParams() {
title: t("sysParams.paramValue"),
dataIndex: "paramValue",
key: "paramValue",
- width: 360,
+ width: 520,
ellipsis: { showTitle: false },
render: (text: string, record: SysParamVO) => {
const type = record.paramType;
@@ -184,10 +348,10 @@ export default function SysParams() {
}
// JSON: 等宽字体,以纯字符串显示
if (isType(type, "JSON")) {
- return ;
+ return ;
}
// 数字和字符串:等宽代码文本,悬浮查看完整内容。
- return ;
+ return ;
}
},
{
@@ -271,7 +435,7 @@ export default function SysParams() {
columns={columns}
dataSource={data}
loading={loading}
- scroll={{ x: "max(100%, 1350px)", y: "100%" }}
+ scroll={{ x: "max(100%, 1510px)", y: "100%" }}
pagination={false}
/>
@@ -338,16 +502,14 @@ export default function SysParams() {
]}
/>
) : isType(paramType, "JSON") ? (
- {
const v = drawerForm.getFieldValue("paramValue");
if (!v) drawerForm.setFieldValue("paramValue", JSON.stringify({ key: "value" }));
}}
- onChange={(e) => {
- const v = e.target.value;
+ onTextChange={(v) => {
if (!v) {
setJsonError(null);
return;
@@ -361,7 +523,7 @@ export default function SysParams() {
}}
/>
) : (
-
+
)}