Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion app/web/components/skills/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,12 @@
align-items: center;
justify-content: center;
flex: 0 0 auto;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 0%, rgba(99, 102, 241, 0.12) 100%);
background:
linear-gradient(
135deg,
rgba(59, 130, 246, 0.08) 0%,
rgba(99, 102, 241, 0.12) 100%
);
border: 1px solid rgba(219, 234, 254, 0.5);
.is-package-icon {
color: #EC4899;
Expand Down
68 changes: 23 additions & 45 deletions app/web/pages/skills/detail/components/SkillInstallPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,6 @@ interface SkillInstallPanelProps {
contributor?: string;
}

const renderInlineCommand = (command: string, copyMessage: string, compact = false) => (
<div className={`command-surface is-light ${compact ? 'is-compact' : ''}`.trim()}>
<div className="command-surface-inline">
<div className="command-inline-code-wrap">
<code>{command || '暂无可复制命令'}</code>
</div>
<Button
type="text"
className="command-copy-btn is-inline-copy"
icon={<DetailIcon src={copyDarkIcon} className="is-copy-dark" />}
onClick={() => copyToClipboard(command, copyMessage)}
disabled={!command}
/>
</div>
</div>
);

const renderTerminalCommand = (command: string, copyMessage: string) => (
<div className="command-surface is-dark is-terminal">
<div className="terminal-head">
Expand Down Expand Up @@ -97,10 +80,7 @@ export const SkillInstallPanel: React.FC<SkillInstallPanelProps> = ({
}) => (
<aside className="detail-right-sidebar">
<section className="install-panel">
<div className="sidebar-section-title">
安装方式
<span className="install-soon-badge">SOON</span>
</div>
<div className="sidebar-section-title">安装方式</div>

<div
className={`install-option-card ${
Expand Down Expand Up @@ -184,37 +164,35 @@ export const SkillInstallPanel: React.FC<SkillInstallPanelProps> = ({
}`.trim()}
>
<div className="install-option-body-inner">
<div className="human-command-card">
<div className="human-command-title">先安装 Doraemon CLI</div>
{renderInlineCommand('', 'CLI 安装命令已复制到剪贴板', false)}
</div>
<div className="human-command-card">
<div className="human-command-title">再安装当前技能</div>
{renderInlineCommand(
isInstallable ? skillInstallCommand : downloadCommand,
isInstallable
? '技能安装命令已复制到剪贴板'
: '下载命令已复制到剪贴板',
false
)}
</div>
{renderTerminalCommand(
isInstallable ? skillInstallCommand : downloadCommand,
'命令已复制到剪贴板'
)}
</div>
</div>
</div>
</section>

<section className="download-panel">
<div className="sidebar-section-title">手动下载</div>
<Button
type="default"
block
className="download-btn"
onClick={() => safeOpenUrl(manualDownloadUrl)}
>
<DetailIcon src={downloadIcon} className="is-download" />
下载 .zip
</Button>
{renderInlineCommand(downloadCommand, '下载命令已复制到剪贴板', true)}
<div className="download-btn-row">
<Button
type="default"
className="download-btn"
onClick={() => safeOpenUrl(manualDownloadUrl)}
>
<DetailIcon src={downloadIcon} className="is-download" />
下载 .zip
</Button>
<Button
type="text"
className="command-copy-btn is-download-copy"
icon={<DetailIcon src={copyDarkIcon} className="is-copy-dark" />}
onClick={() => copyToClipboard(manualDownloadUrl, '下载链接已复制到剪贴板')}
disabled={!manualDownloadUrl}
/>
</div>
{renderTerminalCommand(downloadCommand, '下载命令已复制到剪贴板')}
</section>

<section className="related-panel">
Expand Down
33 changes: 10 additions & 23 deletions app/web/pages/skills/detail/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -584,17 +584,6 @@
align-items: center;
gap: 6px;
}
.install-soon-badge {
display: inline-flex;
align-items: center;
padding: 2px 6px;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.05em;
color: #C2410C;
background: #FED7AA;
border-radius: 4px;
}
.install-option-card {
border: 1px solid #DBE5F0;
border-radius: 8px;
Expand Down Expand Up @@ -685,25 +674,13 @@
flex-direction: column;
gap: 12px;
}
.human-command-card {
display: flex;
flex-direction: column;
gap: 8px;
}
.human-command-title {
color: #566166;
font-size: 12px;
font-weight: 600;
line-height: 16px;
}
.download-panel,
.related-panel,
.meta-panel {
padding-top: 4px;
}
.download-btn {
height: 28px;
margin-bottom: 12px;
padding: 0 12px;
border-radius: 4px;
font-size: 12px;
Expand All @@ -715,6 +692,7 @@
align-items: center;
justify-content: center;
gap: 8px;
flex: 1;
&:hover,
&:focus {
background: #1B2438;
Expand All @@ -727,6 +705,15 @@
color: #FFF;
}
}
.download-btn-row {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 12px;
.is-download-copy {
flex: 0 0 auto;
}
}
.install-panel,
.download-panel,
.related-panel,
Expand Down
5 changes: 4 additions & 1 deletion app/web/pages/skills/detail/utils/skillDetailUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,5 +152,8 @@ export const formatCompactDate = (value?: string) => {
if (!value) return '-';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '-';
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
const pad = (n: number) => String(n).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(
date.getHours()
)}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
};
40 changes: 34 additions & 6 deletions app/web/pages/skills/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
CopyOutlined,
DeleteOutlined,
Expand All @@ -24,6 +24,7 @@ import {
Typography,
Upload,
} from 'antd';
import debounce from 'lodash/debounce';

import { API } from '@/api';
import { SkillCard } from '@/components/skills/SkillCard';
Expand Down Expand Up @@ -66,7 +67,10 @@ const SkillsMarket: React.FC<any> = ({ history }) => {
const [importForm] = Form.useForm();
const [editForm] = Form.useForm();
const [query, setQuery] = useState(INITIAL_QUERY);
const [keywordInput, setKeywordInput] = useState('');
const [selectedSlugs, setSelectedSlugs] = useState<Set<string>>(new Set());
const queryRef = useRef(query);
queryRef.current = query;

const fetchSkills = useCallback(async (nextQuery) => {
setLoading(true);
Expand All @@ -90,14 +94,29 @@ const SkillsMarket: React.FC<any> = ({ history }) => {

useEffect(() => {
fetchSkills(query);
}, [fetchSkills, query]);
}, []);

useEffect(() => {
return () => {
debouncedFetchRef.current.cancel();
};
}, []);

const updateQueryAndFetch = (patch: Partial<typeof query>) => {
const next = { ...query, ...patch };
const next = { ...queryRef.current, ...patch };
setQuery(next);
setSelectedSlugs(new Set());
fetchSkills(next);
};

const debouncedFetchRef = useRef(
debounce((keyword: string) => {
const next = { ...queryRef.current, keyword, pageNum: 1 };
setQuery(next);
fetchSkills(next);
}, 300)
);

const handleSelect = (skill: SkillItem, selected: boolean) => {
setSelectedSlugs((prev) => {
const next = new Set(prev);
Expand Down Expand Up @@ -295,12 +314,21 @@ const SkillsMarket: React.FC<any> = ({ history }) => {
<div className="search-filter-row">
<Search
allowClear
value={query.keyword}
value={keywordInput}
className="keyword-search"
placeholder="搜索名称、描述、标签或贡献者..."
enterButton={<SearchOutlined />}
onChange={(e) => setQuery({ ...query, keyword: e.target.value })}
onSearch={(value) => updateQueryAndFetch({ keyword: value, pageNum: 1 })}
onChange={(e) => {
setKeywordInput(e.target.value);
debouncedFetchRef.current(e.target.value);
}}
onSearch={(value) => {
debouncedFetchRef.current.cancel();
setKeywordInput(value);
const next = { ...queryRef.current, keyword: value, pageNum: 1 };
setQuery(next);
fetchSkills(next);
}}
/>
<Space size={12}>
<Button
Expand Down
1 change: 1 addition & 0 deletions config/config.default.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ module.exports = (app) => {
mode: 'file', // 使用文件模式,直接保存到临时文件
tmpdir: path.join(app.baseDir, 'cache/uploads'), // 临时文件目录
fields: 100, // 允许的最多字段数量
files: 50, // 允许的最多文件数量(默认10,Skills上传需要更多)
cleanSchedule: {
// 清理上传的临时文件
cron: '0 30 4 * * *', // 每天4:30清理
Expand Down
Loading