feat: 添加app模块

This commit is contained in:
neo 2024-12-03 19:47:26 +08:00
parent 39ff6f1b8d
commit 5f112390b5
11 changed files with 1099 additions and 306 deletions

View File

@ -0,0 +1,331 @@
import React, { useState, useEffect } from 'react';
import { Select, Input, Table } from 'antd';
import queryString from 'query-string';
// @ts-ignore
import SourceCode from 'dumi-theme-default/src/builtins/SourceCode';
// @ts-ignore
import Alert from 'dumi-theme-default/src/builtins/Alert';
import { ColumnsType } from 'antd/es/table';
import './CaskAndFormula/app-detail.less';
const Option = Select.Option;
const { Search } = Input;
const getQuery = () => {
return queryString.parse(window.location.search);
};
interface DataType {
arch: string;
os: string;
version: string;
url: string;
}
const columns: ColumnsType<DataType> = [
{
title: 'arch',
dataIndex: 'arch',
width: 200,
onCell: (_, index) => {
if (index === 0) {
return { rowSpan: 10 };
}
return { rowSpan: 0 };
},
},
{
title: 'os',
dataIndex: 'os',
width: 300,
},
{
title: 'version',
dataIndex: 'version',
render: (text, row) => {
return (
<a href={row.url} target="_blank" rel="noopener noreferrer">
{row.version}
</a>
);
},
},
];
const AppDetail = ({}) => {
const [caskDetail, setCaskDetail] = useState({});
const [formulaDetail, setFormulaDetail] = useState({});
const [isCask, setIsCask] = useState(true);
useEffect(() => {
const query = getQuery();
if (query.cid) {
getCaskDetail(query.cid);
setIsCask(true);
} else if (query.fid) {
getFormulaDetail(query.fid);
setIsCask(false);
}
}, []);
const getCaskDetail = id => {
fetch(
'http://localhost:8000/homebrew/cask-detail?' +
queryString.stringify({
id,
}),
)
.then(response => response.json())
.then(res => {
if (res.code === 0 && res.result) {
const result = res.result;
result.variations = JSON.parse(result.variations);
result.depends_on = JSON.parse(result.depends_on);
setCaskDetail(result);
}
});
};
const getFormulaDetail = id => {
fetch(
'http://localhost:8000/homebrew/formula-detail?' +
queryString.stringify({
id,
}),
)
.then(response => response.json())
.then(res => {
if (res.code === 0 && res.result) {
const result = res.result;
// result.versions = JSON.parse(result.versions);
// result.versioned_formulae = JSON.parse(result.versioned_formulae);
// result.dependencies = JSON.parse(result.dependencies);
// result.build_dependencies = JSON.parse(result.build_dependencies);
console.log(result);
setFormulaDetail(result);
}
});
};
const renderObjectJson = obj => {
if (!obj) return;
const list: any = [];
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const element = obj[key];
for (const condition in element) {
if (Object.prototype.hasOwnProperty.call(element, condition)) {
const version = element[condition];
list.push(
<div>
<p>
{key}
{condition}
{version}
</p>
</div>,
);
}
}
}
}
if (list.length === 0) return;
return (
<>
<h3 className="app-detail-content-title"></h3>
{list}
</>
);
};
const renderVariations = variations => {
console.log(variations);
if (!variations) return;
const keys = Object.keys(variations);
if (keys.length === 0) return;
const intelDataArray: DataType[] = [];
const appleSiliconDataArray: any = [];
keys.forEach(key => {
const value = variations[key];
if (key.includes('arm64_')) {
appleSiliconDataArray.push({
arch: 'Apple Silicon',
os: key.replace('arm64_', ''),
url: value.url,
version: value.version || caskDetail.version,
});
} else {
intelDataArray.push({
arch: 'Intel',
os: key,
url: value.url,
version: value.version || caskDetail.version,
});
}
});
return (
<>
<h3 className="app-detail-content-title"></h3>
<div className="variations-content">
{intelDataArray.length > 0 && (
<Table
columns={columns}
dataSource={intelDataArray}
showHeader={false}
pagination={false}
bordered
/>
)}
{appleSiliconDataArray.length > 0 && (
<Table
columns={columns}
dataSource={appleSiliconDataArray}
showHeader={false}
pagination={false}
bordered
/>
)}
</div>
</>
);
};
const renderCask = () => {
return (
<>
<div className="app-img">
<span>
<h1>
{caskDetail.token} {caskDetail.version}
</h1>
<h2>{caskDetail.desc}</h2>
</span>
</div>
<h3 className="app-detail-content-title"></h3>
<SourceCode
code={`brew install --cask ${caskDetail.token}`}
lang="bash"
/>
<div>{renderObjectJson(caskDetail.depends_on)}</div>
{caskDetail.caveats && <Alert type="info">{caskDetail.caveats}</Alert>}
{renderVariations(caskDetail.variations)}
</>
);
};
const renderVersionedFormulae = versionedFormulae => {
if (!versionedFormulae) return;
return (
<>
<h3 className="app-detail-content-title"></h3>
<Table
columns={[
{
dataIndex: 'name',
render: (text, row) => {
return <div>{text}</div>;
},
},
{
dataIndex: 'versions',
render: (text, row) => {
return <div>{text.stable}</div>;
},
},
]}
dataSource={versionedFormulae}
showHeader={false}
pagination={false}
bordered
/>
</>
);
};
const renderDep = (deps, title) => {
if (!deps) return;
return (
<>
<h3 className="app-detail-content-title">{title}</h3>
<Table
columns={[
{
dataIndex: 'name',
render: (text, row) => {
return <div>{text}</div>;
},
},
{
dataIndex: 'versions',
render: (text) => {
return <div>{text.stable}</div>;
},
},
]}
dataSource={deps}
showHeader={false}
pagination={false}
bordered
/>
</>
);
};
const renderFormula = () => {
return (
<>
<div className="app-img">
<span>
<h1>
{formulaDetail.name} {formulaDetail.versions?.stable}
</h1>
<h2>{formulaDetail.desc}</h2>
</span>
</div>
<h3 className="app-detail-content-title"></h3>
<SourceCode code={`brew install ${formulaDetail.name}`} lang="bash" />
{renderVersionedFormulae(formulaDetail.versioned_formulae)}
{renderDep(formulaDetail.dependencies, '依赖')}
{renderDep(formulaDetail.build_dependencies, '构建依赖')}
</>
);
};
return (
<div className="__dumi-default-app-detail">
<div className="app-detail-content">
{isCask ? renderCask() : renderFormula()}
</div>
</div>
);
};
export default AppDetail;

View File

@ -0,0 +1,205 @@
import React, { useState, useEffect } from 'react';
import { Select, Tooltip, Checkbox } from 'antd';
import { QuestionCircleOutlined } from '@ant-design/icons';
// @ts-ignore
import SourceCode from 'dumi-theme-default/src/builtins/SourceCode';
import './SourceGenerate.less';
import CaskCard from './CaskAndFormula/Cask';
import { Card, List, Input, Tabs } from 'antd';
import queryString from 'query-string';
const Option = Select.Option;
const { Search } = Input;
const CaskAndFormula = ({}) => {
const [listParams, setListParams] = useState({
page: 1,
pageSize: 12,
});
const [activeKey, setActiveKey] = useState('cask');
const [caskPageData, setCaskPageData] = useState({
data: [],
total: 0,
});
const [formulaPageData, setFormulaPageData] = useState({
data: [],
total: 0,
});
useEffect(() => {
getList();
}, [activeKey, listParams]);
const getList = () => {
if (activeKey === 'cask') {
getCaskList();
} else {
getFormula();
}
};
const onChange = (key: string) => {
setActiveKey(key);
};
const getCaskList = () => {
fetch(
'http://localhost:8000/homebrew/cask-list?' +
queryString.stringify(listParams),
)
.then(response => response.json())
.then(res => {
if (res.code === 0 && res.result) {
setCaskPageData(res.result);
}
});
};
const getFormula = () => {
fetch(
'http://localhost:8000/homebrew/formula-list?' +
queryString.stringify(listParams),
)
.then(response => response.json())
.then(res => {
if (res.code === 0 && res.result) {
const result = res.result
if (result && result.data) {
result.data.forEach(item => {
if (item.versions) {
item.versions = JSON.parse(item.versions)
}
})
}
setFormulaPageData(result);
}
});
};
const openCaskDetail = item => {
window.open('/app-detail?cid=' + item.id);
};
const openFormulaDetail = item => {
window.open('/app-detail?fid=' + item.id);
};
const onSearch = value => {
setListParams({
...listParams,
page: 1,
search: value,
});
getList();
};
const renderCask = () => {
return (
<List
grid={{ gutter: 16, column: 4 }}
dataSource={caskPageData.data}
pagination={{
onChange: page => {
console.log(page);
setListParams({
...listParams,
page,
});
},
pageSize: listParams.pageSize,
total: caskPageData.total,
}}
renderItem={item => (
<List.Item>
<Card
title={item.token}
extra={<a onClick={openCaskDetail.bind(this, item)}></a>}
>
<div>{item.desc}</div>
<div>
<div style={{ marginTop: '10px', color: '#999' }}>
{item.version}
</div>
</div>
</Card>
</List.Item>
)}
/>
);
};
const renderFormula = () => {
return (
<List
grid={{ gutter: 16, column: 4 }}
dataSource={formulaPageData.data}
pagination={{
onChange: page => {
setListParams({
...listParams,
page,
});
},
pageSize: listParams.pageSize,
total: formulaPageData.total,
}}
renderItem={item => (
<List.Item>
<Card
title={item.name}
extra={<a onClick={openFormulaDetail.bind(this, item)}></a>}
>
<div>{item.desc}</div>
<div>
<div style={{ marginTop: '10px', color: '#999' }}>
{item.versions.stable}
</div>
</div>
</Card>
</List.Item>
)}
/>
);
};
return (
<div className="__dumi-default-cask-and-formula">
<Tabs
defaultActiveKey={activeKey}
onChange={onChange}
size="large"
items={[
{
key: 'cask',
label: 'Cask',
},
{
key: 'formula',
label: 'Formula',
},
]}
/>
<div>
<Search
placeholder="搜索..."
allowClear
enterButton="Search"
size="large"
onSearch={onSearch}
/>
</div>
<div style={{ marginTop: '10px' }}>
{activeKey === 'cask' ? renderCask() : renderFormula()}
</div>
</div>
);
};
export default CaskAndFormula;

View File

@ -0,0 +1,17 @@
import React, { useState } from 'react';
import { Select, Tooltip, Checkbox } from 'antd';
const Option = Select.Option;
const CaskCard = ({ data }) => {
const [terminalType, setTerminalType] = useState<string>('zsh');
const [isJsonApi, setJsonApi] = useState<boolean>(true);
return (
<div className="__dumi-default-cask-card">
{data.token}
</div>
);
};
export default CaskCard;

View File

@ -0,0 +1,38 @@
.__dumi-default-app-detail {
.app-detail-content {
background: #fff;
max-width: 780px;
margin: 20px 0 0;
padding: 20px 30px 25px;
border-radius: 5px;
.app-img {
display: flex;
img {
width: 150px;
height: 150px;
margin-right: 40px;
}
h2 {
color: #999;
font-weight: 400;
font-size: 20px;
}
}
&-title {
font-size: 24px;
font-weight: 400;
margin-bottom: 10px;
}
.variations-content {
.ant-table-wrapper + .ant-table-wrapper {
margin-top: 20px;
}
}
}
}

137
.dumi/theme/style/flex.less Normal file
View File

@ -0,0 +1,137 @@
//from tailwindcss
.flex {
display: flex;
}
.flex-row {
flex-direction: row;
}
.flex-col {
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.flex-nowrap {
flex-wrap: nowrap;
}
.items-start {
align-items: flex-start;
}
.items-end {
align-items: flex-end;
}
.items-center {
align-items: center;
}
.items-baseline {
align-items: baseline;
}
.items-stretch {
align-items: stretch;
}
.content-center {
align-content: center;
}
.content-start {
align-content: flex-start;
}
.content-end {
align-content: flex-end;
}
.content-between {
align-content: space-between;
}
.content-around {
align-content: space-around;
}
.self-auto {
align-self: auto;
}
.self-start {
align-self: flex-start;
}
.self-end {
align-self: flex-end;
}
.self-center {
align-self: center;
}
.self-stretch {
align-self: stretch;
}
.justify-start {
justify-content: flex-start;
}
.justify-end {
justify-content: flex-end;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.justify-around {
justify-content: space-around;
}
.justify-evenly {
justify-content: space-evenly;
}
.flex-1 {
flex: 1 1 0%;
}
.flex-auto {
flex: 1 1 auto;
}
.flex-initial {
flex: 0 1 auto;
}
.flex-none {
flex: none;
}
.flex-grow-0 {
flex-grow: 0;
}
.flex-grow {
flex-grow: 1;
}
.flex-shrink-0 {
flex-shrink: 0;
}
.flex-shrink {
flex-shrink: 1;
}

View File

@ -1,5 +1,6 @@
@import './markdown.less';
@import './variables.less';
@import './flex.less';
body {
margin: 0;

8
docs/app-detail.md Normal file
View File

@ -0,0 +1,8 @@
---
nav:
hide: true
toc: false
sidemenu: false
---
<AppDetail></AppDetail>

12
docs/homebrew.md Normal file
View File

@ -0,0 +1,12 @@
---
nav:
title: 应用
toc: false
sidemenu: false
---
# Cask && Formula
如果我的工作帮助到你,您可以考虑请我喝杯咖啡。
<CaskAndFormula></CaskAndFormula>

View File

@ -47,7 +47,8 @@
},
"dependencies": {
"@ant-design/icons": "^4.5.0",
"antd": "^4.13.1"
"antd": "^4.24.16",
"query-string": "^9.1.1"
},
"pnpm": {
"patchedDependencies": {

653
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff