Merge remote-tracking branch 'origin/alpha' into alpha

This commit is contained in:
CaIon
2025-06-22 00:59:56 +08:00
8 changed files with 170 additions and 74 deletions

View File

@@ -69,8 +69,8 @@ func (a *TaskAdaptor) Init(info *relaycommon.TaskRelayInfo) {
a.ChannelType = info.ChannelType a.ChannelType = info.ChannelType
a.baseURL = info.BaseUrl a.baseURL = info.BaseUrl
// apiKey format: "access_key,secret_key" // apiKey format: "access_key|secret_key"
keyParts := strings.Split(info.ApiKey, ",") keyParts := strings.Split(info.ApiKey, "|")
if len(keyParts) == 2 { if len(keyParts) == 2 {
a.accessKey = strings.TrimSpace(keyParts[0]) a.accessKey = strings.TrimSpace(keyParts[0])
a.secretKey = strings.TrimSpace(keyParts[1]) a.secretKey = strings.TrimSpace(keyParts[1])
@@ -264,7 +264,7 @@ func (a *TaskAdaptor) createJWTToken() (string, error) {
} }
func (a *TaskAdaptor) createJWTTokenWithKey(apiKey string) (string, error) { func (a *TaskAdaptor) createJWTTokenWithKey(apiKey string) (string, error) {
parts := strings.Split(apiKey, ",") parts := strings.Split(apiKey, "|")
if len(parts) != 2 { if len(parts) != 2 {
return "", fmt.Errorf("invalid API key format, expected 'access_key,secret_key'") return "", fmt.Errorf("invalid API key format, expected 'access_key,secret_key'")
} }

View File

@@ -113,25 +113,31 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
return ( return (
<div className="max-h-[55vh] overflow-y-auto pr-2 card-content-scroll"> <div className="max-h-[55vh] overflow-y-auto pr-2 card-content-scroll">
<Timeline mode="alternate"> <Timeline mode="alternate">
{processedAnnouncements.map((item, idx) => ( {processedAnnouncements.map((item, idx) => {
<Timeline.Item const htmlContent = marked.parse(item.content || '');
key={idx} const htmlExtra = item.extra ? marked.parse(item.extra) : '';
type={item.type} return (
time={item.time} <Timeline.Item
className={item.isUnread ? '' : ''} key={idx}
> type={item.type}
<div> time={item.time}
{item.isUnread ? ( className={item.isUnread ? '' : ''}
<span className="shine-text"> >
{item.content} <div>
</span> <div
) : ( className={item.isUnread ? 'shine-text' : ''}
item.content dangerouslySetInnerHTML={{ __html: htmlContent }}
)} />
{item.extra && <div className="text-xs text-gray-500">{item.extra}</div>} {item.extra && (
</div> <div
</Timeline.Item> className="text-xs text-gray-500"
))} dangerouslySetInnerHTML={{ __html: htmlExtra }}
/>
)}
</div>
</Timeline.Item>
);
})}
</Timeline> </Timeline>
</div> </div>
); );

View File

@@ -1615,6 +1615,7 @@
"编辑公告": "Edit Notice", "编辑公告": "Edit Notice",
"公告内容": "Notice Content", "公告内容": "Notice Content",
"请输入公告内容": "Please enter the notice content", "请输入公告内容": "Please enter the notice content",
"请输入公告内容(支持 Markdown/HTML": "Please enter the notice content (supports Markdown/HTML)",
"发布日期": "Publish Date", "发布日期": "Publish Date",
"请选择发布日期": "Please select the publish date", "请选择发布日期": "Please select the publish date",
"发布时间": "Publish Time", "发布时间": "Publish Time",
@@ -1630,6 +1631,7 @@
"请输入问题标题": "Please enter the question title", "请输入问题标题": "Please enter the question title",
"回答内容": "Answer Content", "回答内容": "Answer Content",
"请输入回答内容": "Please enter the answer content", "请输入回答内容": "Please enter the answer content",
"请输入回答内容(支持 Markdown/HTML": "Please enter the answer content (supports Markdown/HTML)",
"确定要删除此问答吗?": "Are you sure you want to delete this FAQ?", "确定要删除此问答吗?": "Are you sure you want to delete this FAQ?",
"系统公告管理可以发布系统通知和重要消息最多100个前端显示最新20条": "System notice management, you can publish system notices and important messages (maximum 100, display latest 20 on the front end)", "系统公告管理可以发布系统通知和重要消息最多100个前端显示最新20条": "System notice management, you can publish system notices and important messages (maximum 100, display latest 20 on the front end)",
"常见问答管理为用户提供常见问题的答案最多50个前端显示最新20条": "FAQ management, providing answers to common questions for users (maximum 50, display latest 20 on the front end)", "常见问答管理为用户提供常见问题的答案最多50个前端显示最新20条": "FAQ management, providing answers to common questions for users (maximum 50, display latest 20 on the front end)",
@@ -1710,5 +1712,8 @@
"可信": "Reliable", "可信": "Reliable",
"所有上游数据均可信": "All upstream data is reliable", "所有上游数据均可信": "All upstream data is reliable",
"以下上游数据可能不可信:": "The following upstream data may not be reliable: ", "以下上游数据可能不可信:": "The following upstream data may not be reliable: ",
"按倍率类型筛选": "Filter by ratio type" "按倍率类型筛选": "Filter by ratio type",
"内容": "Content",
"放大编辑": "Expand editor",
"编辑公告内容": "Edit announcement content"
} }

View File

@@ -64,6 +64,8 @@ function type2secretPrompt(type) {
return '按照如下格式输入AppId|SecretId|SecretKey'; return '按照如下格式输入AppId|SecretId|SecretKey';
case 33: case 33:
return '按照如下格式输入Ak|Sk|Region'; return '按照如下格式输入Ak|Sk|Region';
case 50:
return '按照如下格式输入: AccessKey|SecretKey';
default: default:
return '请输入渠道对应的鉴权密钥'; return '请输入渠道对应的鉴权密钥';
} }

View File

@@ -2,6 +2,7 @@ import React, { useContext, useEffect, useRef, useState, useMemo, useCallback }
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme'; import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Wallet, Activity, Zap, Gauge, PieChart, Server, Bell, HelpCircle } from 'lucide-react'; import { Wallet, Activity, Zap, Gauge, PieChart, Server, Bell, HelpCircle } from 'lucide-react';
import { marked } from 'marked';
import { import {
Card, Card,
@@ -1267,10 +1268,27 @@ const Detail = (props) => {
onScroll={() => handleCardScroll(announcementScrollRef, setShowAnnouncementScrollHint)} onScroll={() => handleCardScroll(announcementScrollRef, setShowAnnouncementScrollHint)}
> >
{announcementData.length > 0 ? ( {announcementData.length > 0 ? (
<Timeline <Timeline mode="alternate">
mode="alternate" {announcementData.map((item, idx) => (
dataSource={announcementData} <Timeline.Item
/> key={idx}
type={item.type || 'default'}
time={item.time}
>
<div>
<div
dangerouslySetInnerHTML={{ __html: marked.parse(item.content || '') }}
/>
{item.extra && (
<div
className="text-xs text-gray-500"
dangerouslySetInnerHTML={{ __html: marked.parse(item.extra) }}
/>
)}
</div>
</Timeline.Item>
))}
</Timeline>
) : ( ) : (
<div className="flex justify-center items-center py-8"> <div className="flex justify-center items-center py-8">
<Empty <Empty
@@ -1321,7 +1339,9 @@ const Detail = (props) => {
header={item.question} header={item.question}
itemKey={index.toString()} itemKey={index.toString()}
> >
<p>{item.answer}</p> <div
dangerouslySetInnerHTML={{ __html: marked.parse(item.answer || '') }}
/>
</Collapse.Panel> </Collapse.Panel>
))} ))}
</Collapse> </Collapse>

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { import {
Button, Button,
Space, Space,
@@ -9,7 +9,9 @@ import {
Divider, Divider,
Modal, Modal,
Tag, Tag,
Switch Switch,
TextArea,
Tooltip
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { import {
IllustrationNoResult, IllustrationNoResult,
@@ -20,7 +22,8 @@ import {
Edit, Edit,
Trash2, Trash2,
Save, Save,
Bell Bell,
Maximize2
} from 'lucide-react'; } from 'lucide-react';
import { API, showError, showSuccess, getRelativeTime, formatDateTimeString } from '../../../helpers'; import { API, showError, showSuccess, getRelativeTime, formatDateTimeString } from '../../../helpers';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -33,6 +36,7 @@ const SettingsAnnouncements = ({ options, refresh }) => {
const [announcementsList, setAnnouncementsList] = useState([]); const [announcementsList, setAnnouncementsList] = useState([]);
const [showAnnouncementModal, setShowAnnouncementModal] = useState(false); const [showAnnouncementModal, setShowAnnouncementModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showContentModal, setShowContentModal] = useState(false);
const [deletingAnnouncement, setDeletingAnnouncement] = useState(null); const [deletingAnnouncement, setDeletingAnnouncement] = useState(null);
const [editingAnnouncement, setEditingAnnouncement] = useState(null); const [editingAnnouncement, setEditingAnnouncement] = useState(null);
const [modalLoading, setModalLoading] = useState(false); const [modalLoading, setModalLoading] = useState(false);
@@ -51,6 +55,8 @@ const SettingsAnnouncements = ({ options, refresh }) => {
// 面板启用状态 // 面板启用状态
const [panelEnabled, setPanelEnabled] = useState(true); const [panelEnabled, setPanelEnabled] = useState(true);
const formApiRef = useRef(null);
const typeOptions = [ const typeOptions = [
{ value: 'default', label: t('默认') }, { value: 'default', label: t('默认') },
{ value: 'ongoing', label: t('进行中') }, { value: 'ongoing', label: t('进行中') },
@@ -76,13 +82,16 @@ const SettingsAnnouncements = ({ options, refresh }) => {
dataIndex: 'content', dataIndex: 'content',
key: 'content', key: 'content',
render: (text) => ( render: (text) => (
<div style={{ <Tooltip content={text} position='topLeft' showArrow>
maxWidth: '300px', <div style={{
wordBreak: 'break-word', maxWidth: '300px',
whiteSpace: 'pre-wrap' overflow: 'hidden',
}}> textOverflow: 'ellipsis',
{text} whiteSpace: 'nowrap'
</div> }}>
{text}
</div>
</Tooltip>
) )
}, },
{ {
@@ -121,13 +130,17 @@ const SettingsAnnouncements = ({ options, refresh }) => {
dataIndex: 'extra', dataIndex: 'extra',
key: 'extra', key: 'extra',
render: (text) => ( render: (text) => (
<div style={{ <Tooltip content={text || '-'} showArrow>
maxWidth: '200px', <div style={{
wordBreak: 'break-word', maxWidth: '200px',
color: 'var(--semi-color-text-2)' overflow: 'hidden',
}}> textOverflow: 'ellipsis',
{text || '-'} whiteSpace: 'nowrap',
</div> color: 'var(--semi-color-text-2)'
}}>
{text || '-'}
</div>
</Tooltip>
) )
}, },
{ {
@@ -472,16 +485,31 @@ const SettingsAnnouncements = ({ options, refresh }) => {
className="rounded-xl" className="rounded-xl"
confirmLoading={modalLoading} confirmLoading={modalLoading}
> >
<Form layout='vertical' initValues={announcementForm} key={editingAnnouncement ? editingAnnouncement.id : 'new'}> <Form
layout='vertical'
initValues={announcementForm}
key={editingAnnouncement ? editingAnnouncement.id : 'new'}
getFormApi={(api) => (formApiRef.current = api)}
>
<Form.TextArea <Form.TextArea
field='content' field='content'
label={t('公告内容')} label={t('公告内容')}
placeholder={t('请输入公告内容')} placeholder={t('请输入公告内容(支持 Markdown/HTML')}
maxCount={500} maxCount={500}
rows={3} rows={3}
rules={[{ required: true, message: t('请输入公告内容') }]} rules={[{ required: true, message: t('请输入公告内容') }]}
onChange={(value) => setAnnouncementForm({ ...announcementForm, content: value })} onChange={(value) => setAnnouncementForm({ ...announcementForm, content: value })}
/> />
<Button
theme='light'
type='tertiary'
size='small'
icon={<Maximize2 size={14} />}
style={{ marginBottom: 16 }}
onClick={() => setShowContentModal(true)}
>
{t('放大编辑')}
</Button>
<Form.DatePicker <Form.DatePicker
field='publishDate' field='publishDate'
label={t('发布日期')} label={t('发布日期')}
@@ -523,6 +551,33 @@ const SettingsAnnouncements = ({ options, refresh }) => {
> >
<Text>{t('确定要删除此公告吗?')}</Text> <Text>{t('确定要删除此公告吗?')}</Text>
</Modal> </Modal>
{/* 公告内容放大编辑 Modal */}
<Modal
title={t('编辑公告内容')}
visible={showContentModal}
onOk={() => {
// 将内容同步到表单
if (formApiRef.current) {
formApiRef.current.setValue('content', announcementForm.content);
}
setShowContentModal(false);
}}
onCancel={() => setShowContentModal(false)}
okText={t('确定')}
cancelText={t('取消')}
className="rounded-xl"
width={800}
>
<TextArea
value={announcementForm.content}
placeholder={t('请输入公告内容(支持 Markdown/HTML')}
maxCount={500}
rows={15}
style={{ width: '100%' }}
onChange={(value) => setAnnouncementForm({ ...announcementForm, content: value })}
/>
</Modal>
</> </>
); );
}; };

View File

@@ -8,7 +8,8 @@ import {
Empty, Empty,
Divider, Divider,
Modal, Modal,
Switch Switch,
Tooltip
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { import {
IllustrationNoResult, IllustrationNoResult,
@@ -54,13 +55,17 @@ const SettingsFAQ = ({ options, refresh }) => {
dataIndex: 'question', dataIndex: 'question',
key: 'question', key: 'question',
render: (text) => ( render: (text) => (
<div style={{ <Tooltip content={text} showArrow>
maxWidth: '300px', <div style={{
wordBreak: 'break-word', maxWidth: '300px',
fontWeight: 'bold' overflow: 'hidden',
}}> textOverflow: 'ellipsis',
{text} whiteSpace: 'nowrap',
</div> fontWeight: 'bold'
}}>
{text}
</div>
</Tooltip>
) )
}, },
{ {
@@ -68,14 +73,17 @@ const SettingsFAQ = ({ options, refresh }) => {
dataIndex: 'answer', dataIndex: 'answer',
key: 'answer', key: 'answer',
render: (text) => ( render: (text) => (
<div style={{ <Tooltip content={text} showArrow>
maxWidth: '400px', <div style={{
wordBreak: 'break-word', maxWidth: '400px',
whiteSpace: 'pre-wrap', overflow: 'hidden',
color: 'var(--semi-color-text-1)' textOverflow: 'ellipsis',
}}> whiteSpace: 'nowrap',
{text} color: 'var(--semi-color-text-1)'
</div> }}>
{text}
</div>
</Tooltip>
) )
}, },
{ {
@@ -416,7 +424,7 @@ const SettingsFAQ = ({ options, refresh }) => {
<Form.TextArea <Form.TextArea
field='answer' field='answer'
label={t('回答内容')} label={t('回答内容')}
placeholder={t('请输入回答内容')} placeholder={t('请输入回答内容(支持 Markdown/HTML')}
maxCount={1000} maxCount={1000}
rows={6} rows={6}
rules={[{ required: true, message: t('请输入回答内容') }]} rules={[{ required: true, message: t('请输入回答内容') }]}

View File

@@ -45,6 +45,16 @@ const Setting = () => {
content: <OperationSetting />, content: <OperationSetting />,
itemKey: 'operation', itemKey: 'operation',
}); });
panes.push({
tab: (
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<LayoutDashboard size={18} />
{t('仪表盘设置')}
</span>
),
content: <DashboardSetting />,
itemKey: 'dashboard',
});
panes.push({ panes.push({
tab: ( tab: (
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}> <span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
@@ -115,16 +125,6 @@ const Setting = () => {
content: <SystemSetting />, content: <SystemSetting />,
itemKey: 'system', itemKey: 'system',
}); });
panes.push({
tab: (
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<LayoutDashboard size={18} />
{t('仪表盘设置')}
</span>
),
content: <DashboardSetting />,
itemKey: 'dashboard',
});
panes.push({ panes.push({
tab: ( tab: (
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}> <span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>