feat: 完善前端功能,添加爬虫页面和项目分页

- 新增 CrawlerView 爬虫页面
- 完善 HomeView 分页展示(9个/页)
- 更新 ProjectCard 组件图标
- 优化 API 客户端和类型定义
- 重构样式文件结构到独立目录

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Developer
2026-03-18 10:45:32 +08:00
parent 68453cead8
commit a1342b7634
14 changed files with 471 additions and 34 deletions

View File

@@ -10,6 +10,7 @@
"dependencies": {
"@element-plus/icons-vue": "^2.3.0",
"@vueuse/core": "^11.0.0",
"ant-design-vue": "^4.2.6",
"axios": "^1.7.0",
"element-plus": "^2.8.0",
"pinia": "^2.2.0",
@@ -26,6 +27,43 @@
"vue-tsc": "^3.2.5"
}
},
"node_modules/@ant-design/colors": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/@ant-design/colors/-/colors-6.0.0.tgz",
"integrity": "sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==",
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "^3.4.0"
}
},
"node_modules/@ant-design/colors/node_modules/@ctrl/tinycolor": {
"version": "3.6.1",
"resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
"integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/@ant-design/icons-svg": {
"version": "4.4.2",
"resolved": "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz",
"integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==",
"license": "MIT"
},
"node_modules/@ant-design/icons-vue": {
"version": "7.0.1",
"resolved": "https://registry.npmmirror.com/@ant-design/icons-vue/-/icons-vue-7.0.1.tgz",
"integrity": "sha512-eCqY2unfZK6Fe02AwFlDHLfoyEFreP6rBwAZMIJ1LugmfMiVgwWDYlp1YsRugaPtICYOabV1iWxXdP12u9U43Q==",
"license": "MIT",
"dependencies": {
"@ant-design/colors": "^6.0.0",
"@ant-design/icons-svg": "^4.2.1"
},
"peerDependencies": {
"vue": ">=3.0.3"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
@@ -59,6 +97,15 @@
"node": ">=6.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.29.2",
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.2.tgz",
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
"version": "7.29.0",
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz",
@@ -97,6 +144,18 @@
"vue": "^3.2.0"
}
},
"node_modules/@emotion/hash": {
"version": "0.9.2",
"resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.9.2.tgz",
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
"license": "MIT"
},
"node_modules/@emotion/unitless": {
"version": "0.8.1",
"resolved": "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.8.1.tgz",
"integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
@@ -1241,6 +1300,16 @@
"win32"
]
},
"node_modules/@simonwep/pickr": {
"version": "1.8.2",
"resolved": "https://registry.npmmirror.com/@simonwep/pickr/-/pickr-1.8.2.tgz",
"integrity": "sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==",
"license": "MIT",
"dependencies": {
"core-js": "^3.15.1",
"nanopop": "^2.1.0"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
@@ -1486,6 +1555,61 @@
"dev": true,
"license": "MIT"
},
"node_modules/ant-design-vue": {
"version": "4.2.6",
"resolved": "https://registry.npmmirror.com/ant-design-vue/-/ant-design-vue-4.2.6.tgz",
"integrity": "sha512-t7eX13Yj3i9+i5g9lqFyYneoIb3OzTvQjq9Tts1i+eiOd3Eva/6GagxBSXM1fOCjqemIu0FYVE1ByZ/38epR3Q==",
"license": "MIT",
"dependencies": {
"@ant-design/colors": "^6.0.0",
"@ant-design/icons-vue": "^7.0.0",
"@babel/runtime": "^7.10.5",
"@ctrl/tinycolor": "^3.5.0",
"@emotion/hash": "^0.9.0",
"@emotion/unitless": "^0.8.0",
"@simonwep/pickr": "~1.8.0",
"array-tree-filter": "^2.1.0",
"async-validator": "^4.0.0",
"csstype": "^3.1.1",
"dayjs": "^1.10.5",
"dom-align": "^1.12.1",
"dom-scroll-into-view": "^2.0.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.15",
"resize-observer-polyfill": "^1.5.1",
"scroll-into-view-if-needed": "^2.2.25",
"shallow-equal": "^1.0.0",
"stylis": "^4.1.3",
"throttle-debounce": "^5.0.0",
"vue-types": "^3.0.0",
"warning": "^4.0.0"
},
"engines": {
"node": ">=12.22.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ant-design-vue"
},
"peerDependencies": {
"vue": ">=3.2.0"
}
},
"node_modules/ant-design-vue/node_modules/@ctrl/tinycolor": {
"version": "3.6.1",
"resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
"integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/array-tree-filter": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/array-tree-filter/-/array-tree-filter-2.1.0.tgz",
"integrity": "sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==",
"license": "MIT"
},
"node_modules/async-validator": {
"version": "4.2.5",
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
@@ -1579,6 +1703,23 @@
"node": ">= 0.8"
}
},
"node_modules/compute-scroll-into-view": {
"version": "1.0.20",
"resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
"integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==",
"license": "MIT"
},
"node_modules/core-js": {
"version": "3.49.0",
"resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.49.0.tgz",
"integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz",
@@ -1623,6 +1764,18 @@
"node": ">=8"
}
},
"node_modules/dom-align": {
"version": "1.12.4",
"resolved": "https://registry.npmmirror.com/dom-align/-/dom-align-1.12.4.tgz",
"integrity": "sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==",
"license": "MIT"
},
"node_modules/dom-scroll-into-view": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/dom-scroll-into-view/-/dom-scroll-into-view-2.0.1.tgz",
"integrity": "sha512-bvVTQe1lfaUr1oFzZX80ce9KLDlZ3iU+XGNE/bz9HnGdklTieqsbmsLHe+rT2XWqopvL0PckkYqN7ksmm5pe3w==",
"license": "MIT"
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -2020,6 +2173,21 @@
"node": ">=0.10.0"
}
},
"node_modules/is-plain-object": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-3.0.1.tgz",
"integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT"
},
"node_modules/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.23.tgz",
@@ -2043,6 +2211,18 @@
"lodash-es": "*"
}
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
@@ -2113,6 +2293,12 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/nanopop": {
"version": "2.4.2",
"resolved": "https://registry.npmmirror.com/nanopop/-/nanopop-2.4.2.tgz",
"integrity": "sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==",
"license": "MIT"
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz",
@@ -2223,6 +2409,12 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
"license": "MIT"
},
"node_modules/rollup": {
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.59.0.tgz",
@@ -2647,6 +2839,21 @@
"node": ">=14.0.0"
}
},
"node_modules/scroll-into-view-if-needed": {
"version": "2.2.31",
"resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
"integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==",
"license": "MIT",
"dependencies": {
"compute-scroll-into-view": "^1.0.20"
}
},
"node_modules/shallow-equal": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/shallow-equal/-/shallow-equal-1.2.1.tgz",
"integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==",
"license": "MIT"
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -2668,6 +2875,12 @@
"node": ">=0.8"
}
},
"node_modules/stylis": {
"version": "4.3.6",
"resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.3.6.tgz",
"integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
"license": "MIT"
},
"node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz",
@@ -2707,6 +2920,15 @@
"node": ">=16.0.0"
}
},
"node_modules/throttle-debounce": {
"version": "5.0.2",
"resolved": "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
"integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==",
"license": "MIT",
"engines": {
"node": ">=12.22"
}
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -2913,6 +3135,30 @@
"typescript": ">=5.0.0"
}
},
"node_modules/vue-types": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/vue-types/-/vue-types-3.0.2.tgz",
"integrity": "sha512-IwUC0Aq2zwaXqy74h4WCvFCUtoV0iSWr0snWnE9TnU18S66GAQyqQbRf2qfJtUuiFsBf6qp0MEwdonlwznlcrw==",
"license": "MIT",
"dependencies": {
"is-plain-object": "3.0.1"
},
"engines": {
"node": ">=10.15.0"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmmirror.com/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz",

View File

@@ -10,6 +10,7 @@
"dependencies": {
"@element-plus/icons-vue": "^2.3.0",
"@vueuse/core": "^11.0.0",
"ant-design-vue": "^4.2.6",
"axios": "^1.7.0",
"element-plus": "^2.8.0",
"pinia": "^2.2.0",

View File

@@ -248,6 +248,36 @@ html, body, #app {
background: var(--bg-tertiary);
}
/* Ant Design Vue Select Dropdown */
.ant-select-dropdown {
background: var(--bg-elevated) !important;
border: 1px solid var(--border-subtle) !important;
border-radius: 8px !important;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3) !important;
}
.ant-select-item {
color: var(--text-primary) !important;
border-radius: 6px !important;
}
.ant-select-item-option-active:not(.ant-select-item-option-disabled) {
background: var(--bg-hover) !important;
}
.ant-select-item-option-selected:not(.ant-select-item-option-disabled) {
background: var(--accent-primary-muted) !important;
color: var(--accent-primary) !important;
}
.ant-select-selection-item {
color: var(--text-primary) !important;
}
.ant-select-selection-placeholder {
color: var(--text-secondary) !important;
}
.el-textarea__inner {
background: var(--bg-tertiary) !important;
border: 1px solid var(--border-subtle) !important;

View File

@@ -26,6 +26,17 @@ request.interceptors.response.use(
// Handle new ApiResponse format
if (data.success !== undefined) {
if (data.success) {
// Check if this is a paginated response by checking for pagination field
if (data.pagination) {
// Return full response with pagination for paginated endpoints
return {
items: data.data,
total: data.pagination.total,
page: data.pagination.page,
page_size: data.pagination.page_size,
total_pages: data.pagination.total_pages
}
}
return data.data // Return the actual data
} else {
return Promise.reject(new Error(data.message || data.error || '请求失败'))
@@ -41,9 +52,10 @@ request.interceptors.response.use(
)
export const projectApi = {
list: () => request.get<Project[]>('/projects/'),
list: (params?: { page?: number; page_size?: number }) =>
request.get<{ items: Project[]; pagination: { total: number } }>('/projects', { params }),
get: (id: string) => request.get<Project>(`/projects/${id}`),
create: (data: ProjectCreate) => request.post<{ id: string }>('/projects/', data),
create: (data: ProjectCreate) => request.post<{ id: string }>('/projects', data),
update: (id: string, data: ProjectUpdate) => request.put<Project>(`/projects/${id}`, data),
delete: (id: string) => request.delete(`/projects/${id}`)
}
@@ -53,14 +65,14 @@ export const fileApi = {
request.post(`/projects/${projectId}/files/upload`, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
}),
list: (projectId: string) => request.get(`/projects/${projectId}/files/`),
list: (projectId: string) => request.get(`/projects/${projectId}/files`),
get: (projectId: string, fileId: string) => request.get(`/projects/${projectId}/files/${fileId}`),
delete: (projectId: string, fileId: string) => request.delete(`/projects/${projectId}/files/${fileId}`)
}
export const chunkApi = {
split: (projectId: string, data: any) => request.post(`/projects/${projectId}/chunks/split`, data),
list: (projectId: string, params?: any) => request.get(`/projects/${projectId}/chunks/`, { params }),
list: (projectId: string, params?: any) => request.get(`/projects/${projectId}/chunks`, { params }),
get: (projectId: string, chunkId: string) => request.get(`/projects/${projectId}/chunks/${chunkId}`),
update: (projectId: string, chunkId: string, data: any) => request.put(`/projects/${projectId}/chunks/${chunkId}`, data),
delete: (projectId: string, chunkId: string) => request.delete(`/projects/${projectId}/chunks/${chunkId}`)
@@ -74,8 +86,8 @@ export const questionApi = {
}
export const datasetApi = {
list: (projectId: string) => request.get(`/projects/${projectId}/datasets/`),
create: (projectId: string, data: any) => request.post(`/projects/${projectId}/datasets/`, data),
list: (projectId: string) => request.get(`/projects/${projectId}/datasets`),
create: (projectId: string, data: any) => request.post(`/projects/${projectId}/datasets`, data),
get: (projectId: string, datasetId: string) => request.get(`/projects/${projectId}/datasets/${datasetId}`),
delete: (projectId: string, datasetId: string) => request.delete(`/projects/${projectId}/datasets/${datasetId}`),
export: (projectId: string, datasetId: string, data: any) =>

View File

@@ -58,24 +58,35 @@
<div class="templates-section">
<span class="templates-label">快速开始模板</span>
<div class="templates-grid">
<div class="template-card" @click="useTemplate('qa')">
<div
class="template-card"
:class="{ active: formData.type === 'qa' }"
@click="useTemplate('qa')"
>
<el-icon><ChatDotRound /></el-icon>
<span>问答对</span>
</div>
<div class="template-card" @click="useTemplate('conversation')">
<el-icon><ChatLineRound /></el-icon>
<span>对话</span>
<div
class="template-card"
:class="{ active: formData.type === 'table' }"
@click="useTemplate('table')"
>
<el-icon><Document /></el-icon>
<span>表格</span>
</div>
<div class="template-card" @click="useTemplate('instruction')">
<el-icon><Promotion /></el-icon>
<span>指令</span>
<div
class="template-card"
:class="{ active: formData.type === 'database' }"
@click="useTemplate('database')"
>
<el-icon><Connection /></el-icon>
<span>数据库</span>
</div>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose" class="btn-cancel">取消</el-button>
<el-button
type="primary"
:loading="loading"
@@ -108,19 +119,18 @@ const emit = defineEmits(['update:visible', 'submit'])
const formData = reactive({
name: '',
description: ''
description: '',
type: ''
})
const templates = {
qa: { name: '问答数据集', description: '基于文档生成问答对训练数据' },
conversation: { name: '对话数据集', description: '创建多轮对话训练数据' },
instruction: { name: '指令数据集', description: '构建指令跟随训练数据' }
table: { name: '表格数据集', description: '从表格数据生成结构化训练数据' },
database: { name: '数据库数据集', description: '从数据库导出数据生成训练数据' }
}
const useTemplate = (type) => {
const t = templates[type]
formData.name = t.name
formData.description = t.description
formData.type = type
}
const handleClose = () => {
@@ -136,6 +146,7 @@ watch(() => props.visible, (newVal) => {
if (newVal) {
formData.name = ''
formData.description = ''
formData.type = ''
}
})
</script>
@@ -319,6 +330,10 @@ watch(() => props.visible, (newVal) => {
transition: all 0.25s ease;
}
.custom-input :deep(.el-textarea__inner::placeholder) {
color: var(--text-muted);
}
.custom-input :deep(.el-textarea__inner:hover) {
border-color: rgba(0, 212, 255, 0.3);
}
@@ -368,6 +383,12 @@ watch(() => props.visible, (newVal) => {
transform: translateY(-2px);
}
.template-card.active {
background: rgba(0, 212, 255, 0.12);
border-color: var(--accent-primary);
box-shadow: 0 0 15px rgba(0, 212, 255, 0.2);
}
.template-card .el-icon {
font-size: 22px;
color: var(--accent-primary);
@@ -385,8 +406,7 @@ watch(() => props.visible, (newVal) => {
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
justify-content: center;
padding: 20px 28px;
background: rgba(0, 0, 0, 0.2);
border-top: 1px solid rgba(255, 255, 255, 0.05);
@@ -408,11 +428,13 @@ watch(() => props.visible, (newVal) => {
}
.btn-create {
padding: 10px 24px;
width: 100%;
padding: 14px 32px;
background: linear-gradient(135deg, var(--accent-primary) 0%, #0891b2 100%);
border: none;
border-radius: 10px;
font-weight: 500;
font-size: 15px;
transition: all 0.25s ease;
}

View File

@@ -14,7 +14,7 @@
</button>
<div class="card-header">
<div class="card-avatar">
<el-icon><Folder /></el-icon>
<el-icon><component :is="projectIcon" /></el-icon>
</div>
</div>
<h3 class="card-title">{{ project.name }}</h3>
@@ -34,6 +34,7 @@
<script setup>
import { computed } from 'vue'
import { Folder, ChatDotRound, Document, Connection } from '@element-plus/icons-vue'
const props = defineProps({
project: {
@@ -58,6 +59,14 @@ defineEmits(['click', 'delete'])
const delay = computed(() => `${props.index * 0.1}s`)
const projectIcon = computed(() => {
const type = props.project.type
if (type === 'qa') return ChatDotRound
if (type === 'table') return Document
if (type === 'database') return Connection
return Folder
})
const formattedDate = computed(() => {
if (!props.project.created_at) return ''
const d = new Date(props.project.created_at)

View File

@@ -2,6 +2,8 @@ import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/reset.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'
@@ -17,5 +19,6 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.use(createPinia())
app.use(router)
app.use(ElementPlus)
app.use(Antd)
app.mount('#app')

View File

@@ -51,6 +51,11 @@ const routes = [
path: '/models',
name: 'ModelSettings',
component: () => import('@/views/ModelSettingsView.vue')
},
{
path: '/crawler',
name: 'Crawler',
component: () => import('@/views/CrawlerView.vue')
}
]

View File

@@ -6,16 +6,19 @@ export interface Project {
id: string
name: string
description?: string
type: string
created_at: string
updated_at: string
}
export interface ProjectCreate {
name: string
description?: string
description: string
type: string
}
export interface ProjectUpdate {
name?: string
description?: string
type?: string
}

View File

@@ -47,6 +47,10 @@
<el-icon><Plus /></el-icon>
创建项目
</el-button>
<el-button size="large" @click="goToCrawler" class="btn-secondary">
<el-icon><Connection /></el-icon>
数据爬虫
</el-button>
<el-button size="large" @click="goToModels" class="btn-secondary">
<el-icon><Cpu /></el-icon>
模型管理
@@ -56,6 +60,47 @@
<!-- Hero Visual - Modern Abstract Composition -->
<div class="hero-visual">
<!-- Galaxy Background -->
<div class="galaxy-bg">
<!-- Nebula clouds -->
<div class="nebula-cloud nebula-1"></div>
<div class="nebula-cloud nebula-2"></div>
<div class="nebula-cloud nebula-3"></div>
<!-- Galaxy core -->
<div class="galaxy-core"></div>
<!-- Spiral arms -->
<div class="galaxy-spiral">
<div class="spiral-arm spiral-arm-1"></div>
<div class="spiral-arm spiral-arm-2"></div>
<div class="spiral-arm spiral-arm-3"></div>
</div>
<!-- Orbit rings with stars -->
<div class="orbit-ring orbit-ring-1">
<span class="orbit-star"></span>
<span class="orbit-star"></span>
<span class="orbit-star"></span>
<span class="orbit-star"></span>
</div>
<div class="orbit-ring orbit-ring-2">
<span class="orbit-star"></span>
<span class="orbit-star"></span>
<span class="orbit-star"></span>
<span class="orbit-star"></span>
</div>
<div class="orbit-ring orbit-ring-3">
<span class="orbit-star"></span>
<span class="orbit-star"></span>
<span class="orbit-star"></span>
</div>
<div class="orbit-ring orbit-ring-4">
<span class="orbit-star"></span>
<span class="orbit-star"></span>
</div>
</div>
<!-- Light rays -->
<div class="light-rays">
<div class="ray"></div>
@@ -134,7 +179,7 @@
<div class="section-header">
<div class="section-title">
<h2>我的项目</h2>
<p>{{ projects.length }} 个项目</p>
<p>{{ total }} 个项目</p>
</div>
<el-button type="primary" @click="createProject" class="add-btn">
<el-icon><Plus /></el-icon>
@@ -165,6 +210,29 @@
@delete="confirmDelete"
/>
</div>
<!-- Pagination -->
<div class="pagination-wrapper" v-if="needPagination">
<div class="pagination-minimal">
<span class="page-info"> {{ currentPage }} / {{ totalPages }} </span>
<div class="page-arrows">
<button
class="arrow-btn"
:disabled="currentPage === 1"
@click="handlePageChange(currentPage - 1)"
>
<el-icon><ArrowLeft /></el-icon>
</button>
<button
class="arrow-btn"
:disabled="currentPage === totalPages"
@click="handlePageChange(currentPage + 1)"
>
<el-icon><ArrowRight /></el-icon>
</button>
</div>
</div>
</div>
</section>
<!-- Create Dialog -->
@@ -185,10 +253,10 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { FolderAdd, Check, Connection, Clock, Lock, TrendCharts } from '@element-plus/icons-vue'
import { FolderAdd, Check, Connection, Clock, Lock, TrendCharts, ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
import { projectApi } from '@/api'
import type { Project, ProjectCreate } from '@/types'
@@ -208,29 +276,62 @@ const projectToDelete = ref(null)
const submitting = ref(false)
const deleting = ref(false)
// Pagination
const currentPage = ref(1)
const pageSize = ref(9)
const total = ref(0)
const fetchProjects = async () => {
loading.value = true
try {
const res = await projectApi.list()
// New paginated format: {items: [...], total, page, page_size}
projects.value = res.items || res || []
const res = await projectApi.list({ page: currentPage.value, page_size: pageSize.value })
// API returns: { items: [], total, page, page_size, total_pages }
if (res && typeof res === 'object' && 'items' in res) {
projects.value = res.items || []
total.value = res.total || 0
} else if (Array.isArray(res)) {
projects.value = res
total.value = res.length
} else {
projects.value = []
total.value = 0
}
} catch (error) {
projects.value = []
total.value = 0
} finally {
loading.value = false
}
}
const handlePageChange = (page: number) => {
currentPage.value = page
fetchProjects()
}
const needPagination = computed(() => total.value > pageSize.value || projects.value.length === pageSize.value)
const totalPages = computed(() => Math.ceil(total.value / pageSize.value))
const createProject = () => {
dialogVisible.value = true
}
const handleCreateSubmit = async (formData) => {
// Simple validation
// Validation - name, description and type are required
if (!formData.name || formData.name.trim() === '') {
ElMessage.warning('请输入项目名称')
return
}
if (!formData.description || formData.description.trim() === '') {
ElMessage.warning('请输入项目描述')
return
}
if (!formData.type) {
ElMessage.warning('请选择项目类型')
return
}
console.log('Creating project with form:', formData)
submitting.value = true
@@ -278,6 +379,7 @@ const handleDelete = async () => {
}
const goToDataSquare = () => router.push('/data-square')
const goToCrawler = () => router.push('/crawler')
const goToModels = () => router.push('/models')
onMounted(() => fetchProjects())

View File

@@ -205,7 +205,8 @@ const fetchFiles = async () => {
loading.value = true
try {
const res = await fileApi.list(projectId.value)
files.value = res.data.files || []
// API returns array directly via interceptor
files.value = res || []
} catch (error) {
files.value = []
} finally {