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

@@ -27,7 +27,7 @@ logger = logging.getLogger("yg_dataset.projects")
project_crud = CRUDBase(Project) project_crud = CRUDBase(Project)
@router.get("", response_model=ApiResponse) @router.get("", response_model=PaginatedResponse)
async def list_projects( async def list_projects(
page: int = Query(1, ge=1, description="Page number"), page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(20, ge=1, le=100, description="Page size"), page_size: int = Query(20, ge=1, le=100, description="Page size"),

View File

@@ -14,6 +14,7 @@ class Project(Base, UUIDMixin, TimestampMixin):
name = Column(String(255), nullable=False) name = Column(String(255), nullable=False)
description = Column(Text) description = Column(Text)
type = Column(String(50), default="qa") # qa, table, database
# Relationships # Relationships
files = relationship("File", back_populates="project", cascade="all, delete-orphan") files = relationship("File", back_populates="project", cascade="all, delete-orphan")

View File

@@ -11,6 +11,7 @@ class ProjectBase(BaseModel):
"""Base project schema""" """Base project schema"""
name: str = Field(..., min_length=1, max_length=255) name: str = Field(..., min_length=1, max_length=255)
description: Optional[str] = Field(None, max_length=2000) description: Optional[str] = Field(None, max_length=2000)
type: str = Field(default="qa") # qa, table, database
class ProjectCreate(ProjectBase): class ProjectCreate(ProjectBase):
@@ -22,6 +23,7 @@ class ProjectUpdate(BaseModel):
"""Project update schema""" """Project update schema"""
name: Optional[str] = Field(None, min_length=1, max_length=255) name: Optional[str] = Field(None, min_length=1, max_length=255)
description: Optional[str] = Field(None, max_length=2000) description: Optional[str] = Field(None, max_length=2000)
type: Optional[str] = Field(None)
class ProjectResponse(ProjectBase): class ProjectResponse(ProjectBase):

View File

@@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.0", "@element-plus/icons-vue": "^2.3.0",
"@vueuse/core": "^11.0.0", "@vueuse/core": "^11.0.0",
"ant-design-vue": "^4.2.6",
"axios": "^1.7.0", "axios": "^1.7.0",
"element-plus": "^2.8.0", "element-plus": "^2.8.0",
"pinia": "^2.2.0", "pinia": "^2.2.0",
@@ -26,6 +27,43 @@
"vue-tsc": "^3.2.5" "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": { "node_modules/@babel/helper-string-parser": {
"version": "7.27.1", "version": "7.27.1",
"resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "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": ">=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": { "node_modules/@babel/types": {
"version": "7.29.0", "version": "7.29.0",
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz",
@@ -97,6 +144,18 @@
"vue": "^3.2.0" "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": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
@@ -1241,6 +1300,16 @@
"win32" "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": { "node_modules/@types/estree": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
@@ -1486,6 +1555,61 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/async-validator": {
"version": "4.2.5", "version": "4.2.5",
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
@@ -1579,6 +1703,23 @@
"node": ">= 0.8" "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": { "node_modules/crc-32": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz", "resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz",
@@ -1623,6 +1764,18 @@
"node": ">=8" "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": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -2020,6 +2173,21 @@
"node": ">=0.10.0" "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": { "node_modules/lodash": {
"version": "4.17.23", "version": "4.17.23",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.23.tgz", "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.23.tgz",
@@ -2043,6 +2211,18 @@
"lodash-es": "*" "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": { "node_modules/magic-string": {
"version": "0.30.21", "version": "0.30.21",
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", "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": "^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": { "node_modules/node-addon-api": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz", "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz",
@@ -2223,6 +2409,12 @@
"url": "https://paulmillr.com/funding/" "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": { "node_modules/rollup": {
"version": "4.59.0", "version": "4.59.0",
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.59.0.tgz", "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.59.0.tgz",
@@ -2647,6 +2839,21 @@
"node": ">=14.0.0" "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": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -2668,6 +2875,12 @@
"node": ">=0.8" "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": { "node_modules/supports-color": {
"version": "8.1.1", "version": "8.1.1",
"resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz",
@@ -2707,6 +2920,15 @@
"node": ">=16.0.0" "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": { "node_modules/tinyglobby": {
"version": "0.2.15", "version": "0.2.15",
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -2913,6 +3135,30 @@
"typescript": ">=5.0.0" "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": { "node_modules/wmf": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz", "resolved": "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz",

View File

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

View File

@@ -248,6 +248,36 @@ html, body, #app {
background: var(--bg-tertiary); 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 { .el-textarea__inner {
background: var(--bg-tertiary) !important; background: var(--bg-tertiary) !important;
border: 1px solid var(--border-subtle) !important; border: 1px solid var(--border-subtle) !important;

View File

@@ -26,6 +26,17 @@ request.interceptors.response.use(
// Handle new ApiResponse format // Handle new ApiResponse format
if (data.success !== undefined) { if (data.success !== undefined) {
if (data.success) { 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 return data.data // Return the actual data
} else { } else {
return Promise.reject(new Error(data.message || data.error || '请求失败')) return Promise.reject(new Error(data.message || data.error || '请求失败'))
@@ -41,9 +52,10 @@ request.interceptors.response.use(
) )
export const projectApi = { 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}`), 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), update: (id: string, data: ProjectUpdate) => request.put<Project>(`/projects/${id}`, data),
delete: (id: string) => request.delete(`/projects/${id}`) delete: (id: string) => request.delete(`/projects/${id}`)
} }
@@ -53,14 +65,14 @@ export const fileApi = {
request.post(`/projects/${projectId}/files/upload`, formData, { request.post(`/projects/${projectId}/files/upload`, formData, {
headers: { 'Content-Type': 'multipart/form-data' } 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}`), get: (projectId: string, fileId: string) => request.get(`/projects/${projectId}/files/${fileId}`),
delete: (projectId: string, fileId: string) => request.delete(`/projects/${projectId}/files/${fileId}`) delete: (projectId: string, fileId: string) => request.delete(`/projects/${projectId}/files/${fileId}`)
} }
export const chunkApi = { export const chunkApi = {
split: (projectId: string, data: any) => request.post(`/projects/${projectId}/chunks/split`, data), 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}`), 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), 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}`) delete: (projectId: string, chunkId: string) => request.delete(`/projects/${projectId}/chunks/${chunkId}`)
@@ -74,8 +86,8 @@ export const questionApi = {
} }
export const datasetApi = { export const datasetApi = {
list: (projectId: string) => request.get(`/projects/${projectId}/datasets/`), list: (projectId: string) => request.get(`/projects/${projectId}/datasets`),
create: (projectId: string, data: any) => request.post(`/projects/${projectId}/datasets/`, data), create: (projectId: string, data: any) => request.post(`/projects/${projectId}/datasets`, data),
get: (projectId: string, datasetId: string) => request.get(`/projects/${projectId}/datasets/${datasetId}`), get: (projectId: string, datasetId: string) => request.get(`/projects/${projectId}/datasets/${datasetId}`),
delete: (projectId: string, datasetId: string) => request.delete(`/projects/${projectId}/datasets/${datasetId}`), delete: (projectId: string, datasetId: string) => request.delete(`/projects/${projectId}/datasets/${datasetId}`),
export: (projectId: string, datasetId: string, data: any) => export: (projectId: string, datasetId: string, data: any) =>

View File

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

View File

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

View File

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

View File

@@ -51,6 +51,11 @@ const routes = [
path: '/models', path: '/models',
name: 'ModelSettings', name: 'ModelSettings',
component: () => import('@/views/ModelSettingsView.vue') 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 id: string
name: string name: string
description?: string description?: string
type: string
created_at: string created_at: string
updated_at: string updated_at: string
} }
export interface ProjectCreate { export interface ProjectCreate {
name: string name: string
description?: string description: string
type: string
} }
export interface ProjectUpdate { export interface ProjectUpdate {
name?: string name?: string
description?: string description?: string
type?: string
} }

View File

@@ -47,6 +47,10 @@
<el-icon><Plus /></el-icon> <el-icon><Plus /></el-icon>
创建项目 创建项目
</el-button> </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-button size="large" @click="goToModels" class="btn-secondary">
<el-icon><Cpu /></el-icon> <el-icon><Cpu /></el-icon>
模型管理 模型管理
@@ -56,6 +60,47 @@
<!-- Hero Visual - Modern Abstract Composition --> <!-- Hero Visual - Modern Abstract Composition -->
<div class="hero-visual"> <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 --> <!-- Light rays -->
<div class="light-rays"> <div class="light-rays">
<div class="ray"></div> <div class="ray"></div>
@@ -134,7 +179,7 @@
<div class="section-header"> <div class="section-header">
<div class="section-title"> <div class="section-title">
<h2>我的项目</h2> <h2>我的项目</h2>
<p>{{ projects.length }} 个项目</p> <p>{{ total }} 个项目</p>
</div> </div>
<el-button type="primary" @click="createProject" class="add-btn"> <el-button type="primary" @click="createProject" class="add-btn">
<el-icon><Plus /></el-icon> <el-icon><Plus /></el-icon>
@@ -165,6 +210,29 @@
@delete="confirmDelete" @delete="confirmDelete"
/> />
</div> </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> </section>
<!-- Create Dialog --> <!-- Create Dialog -->
@@ -185,10 +253,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' 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 { projectApi } from '@/api'
import type { Project, ProjectCreate } from '@/types' import type { Project, ProjectCreate } from '@/types'
@@ -208,29 +276,62 @@ const projectToDelete = ref(null)
const submitting = ref(false) const submitting = ref(false)
const deleting = ref(false) const deleting = ref(false)
// Pagination
const currentPage = ref(1)
const pageSize = ref(9)
const total = ref(0)
const fetchProjects = async () => { const fetchProjects = async () => {
loading.value = true loading.value = true
try { try {
const res = await projectApi.list() const res = await projectApi.list({ page: currentPage.value, page_size: pageSize.value })
// New paginated format: {items: [...], total, page, page_size} // API returns: { items: [], total, page, page_size, total_pages }
projects.value = res.items || res || [] 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) { } catch (error) {
projects.value = [] projects.value = []
total.value = 0
} finally { } finally {
loading.value = false 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 = () => { const createProject = () => {
dialogVisible.value = true dialogVisible.value = true
} }
const handleCreateSubmit = async (formData) => { const handleCreateSubmit = async (formData) => {
// Simple validation // Validation - name, description and type are required
if (!formData.name || formData.name.trim() === '') { if (!formData.name || formData.name.trim() === '') {
ElMessage.warning('请输入项目名称') ElMessage.warning('请输入项目名称')
return return
} }
if (!formData.description || formData.description.trim() === '') {
ElMessage.warning('请输入项目描述')
return
}
if (!formData.type) {
ElMessage.warning('请选择项目类型')
return
}
console.log('Creating project with form:', formData) console.log('Creating project with form:', formData)
submitting.value = true submitting.value = true
@@ -278,6 +379,7 @@ const handleDelete = async () => {
} }
const goToDataSquare = () => router.push('/data-square') const goToDataSquare = () => router.push('/data-square')
const goToCrawler = () => router.push('/crawler')
const goToModels = () => router.push('/models') const goToModels = () => router.push('/models')
onMounted(() => fetchProjects()) onMounted(() => fetchProjects())

View File

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