diff --git a/src/api/auth.ts b/src/api/auth.ts
index b7b1267..cbbae6f 100644
--- a/src/api/auth.ts
+++ b/src/api/auth.ts
@@ -1,14 +1,55 @@
+import { request, CLIENT_ID } from '@/utils/request'
+import type { LoginResult as AuthLoginResult, RegisterResult } from '@/types/business/auth'
import type {
- LoginByAccountParams,
LoginBySmsParams,
LoginResult,
- RegisterParams,
ResetPasswordParams,
User,
} from '@/types/business/user'
import { isPhone, isValidPassword } from '@/utils/validate'
-// TODO(api): 待接口文档到位后,将以下 mock 替换为 request 调用(页面 → store → api 数据流不变)
+/**
+ * PC 端 clientId(sys_client 种子数据,grantType 含 password)。
+ * 与请求头 clientid 同源(见 @/utils/request),改一处即可整体生效。
+ */
+export const PC_CLIENT_ID = CLIENT_ID
+
+/**
+ * 账号密码登录:POST /auth/login
+ * 后端要求请求体含 clientId + grantType + username + password(api-decrypt 未开启,明文即可)
+ */
+export function loginByAccountApi(data: { username: string; password: string }): Promise {
+ return request({
+ url: '/auth/login',
+ method: 'post',
+ data: {
+ username: data.username,
+ password: data.password,
+ clientId: PC_CLIENT_ID,
+ grantType: 'password',
+ },
+ })
+}
+
+/**
+ * 账号密码注册:POST /auth/register
+ * userType=sys_user;需后端开启注册开关(sys.account.registerUser=true),否则返回「当前系统没有开启注册功能!」
+ */
+export function registerByAccountApi(data: { username: string; password: string }): Promise {
+ return request({
+ url: '/auth/register',
+ method: 'post',
+ data: {
+ username: data.username,
+ password: data.password,
+ userType: 'sys_user',
+ clientId: PC_CLIENT_ID,
+ grantType: 'password',
+ },
+ })
+}
+
+// TODO(api): 以下为原 mock 逻辑(保留,待后端对应接口就绪后替换)
const SIX_DIGIT_CODE = /^\d{6}$/
@@ -18,26 +59,6 @@ export function sendSmsCode(phone: string): Promise {
return Promise.resolve()
}
-/** 账号密码登录(mock:demo 账号可登录,其余提示先注册) */
-export function loginByAccount(params: LoginByAccountParams): Promise {
- const { account, password } = params
- if (!account.trim() || !password) {
- return Promise.reject(new Error('请输入账号和密码'))
- }
- if (account === 'demo') {
- const user: User = {
- id: 'u1',
- nickname: '王永志',
- avatar: '',
- phone: '186****618',
- email: 'yongzh******4@163.com',
- passwordModifiedAt: '',
- }
- return Promise.resolve({ token: `mock-token-${Date.now()}`, user })
- }
- return Promise.reject(new Error('登录失败,请先注册账号'))
-}
-
/** 手机验证码登录(mock:验证码须为 6 位数字) */
export function loginBySms(params: LoginBySmsParams): Promise {
const { phone, code } = params
@@ -69,19 +90,6 @@ function validateAuthForm(
return null
}
-/** 注册 */
-export function register(params: RegisterParams): Promise {
- const message = validateAuthForm(
- params.phone,
- params.code,
- params.password,
- params.confirmPassword,
- params.agree,
- )
- if (message) return Promise.reject(new Error(message))
- return Promise.resolve()
-}
-
/** 重置密码 */
export function resetPassword(params: ResetPasswordParams): Promise {
const message = validateAuthForm(
diff --git a/src/api/cases.ts b/src/api/cases.ts
index a38f337..ba9bde9 100644
--- a/src/api/cases.ts
+++ b/src/api/cases.ts
@@ -1,227 +1,90 @@
-import type { BusinessType, PageResult } from '@/types/api'
-import type { Case, CaseQuery, ScaleRange } from '@/types/business/cases'
+// 案例库接口(真实后端绑定)
+// 后端: vermis-maker com.ruoyi.vermins.controller.pc.web.ProjectCaseUserController
+// 前缀: /web/projectCase
+//
+// 使用约定(与项目 SKILL.md 一致):
+// 页面/组件禁止直接 import axios,一律经 store 调用本模块的 request。
+// 本文件只放「函数签名 + 请求路径」,不做数据加工、不维护状态。
+import { request, serializeQuery } from '@/utils/request'
+import type {
+ CaseSelectionModel,
+ ProjectCaseAndCaseCountVo,
+ ProjectCaseUserBo,
+ ProjectCaseVo,
+} from '@/types/business/cases'
-// TODO(api): 待接口文档到位后,将以下 mock 替换为 request 调用
-
-/** 案例 mock(可变,收藏状态在前端内存中翻转演示) */
-const MOCK_CASES: Case[] = [
- {
- id: 'case-001',
- title: '自动化生产线升级改造项目',
- category: 'manufacturing',
- cover: '',
- finishDate: '2026/8/10',
- teamSize: 50,
- scale: '300万',
- industry: '自动化',
- duration: '150个工作日',
- clientType: '企业',
- background: '传统产线效率低、人工依赖高,客户需要通过自动化升级提升产能与品控水平。',
- painPoints: '1、产线依赖人工巡检,异常响应慢;2、设备数据孤岛,缺乏统一监控;3、工艺参数无法追溯。',
- requirements: '建设产线数据采集与监控平台,完成关键工位自动化改造,打通 MES 系统接口。',
- deliverables: '1、产线自动化改造方案;2、PLC 控制程序与上位机软件;3、数据看板与运维手册;4、培训与质保服务。',
- video: '',
- isFavorite: false,
- },
- {
- id: 'case-002',
- title: '智能工厂物联网整体解决方案',
- category: 'iot',
- cover: '',
- finishDate: '2026/8/10',
- teamSize: 30,
- scale: '500万',
- industry: '工厂服务',
- duration: '200个工作日',
- clientType: '企业',
- background: '园区工厂多系统独立运行,能耗与设备管理粗放,需要整体数字化升级。',
- painPoints: '1、门禁/监控/能耗子系统数据不互通;2、能源浪费缺乏精细监测;3、决策缺少实时数据支撑。',
- requirements: '构建 IoT 平台接入设备数据,建设综合驾驶舱,实现能耗监测与智能告警。',
- deliverables: '1、IoT 接入平台;2、综合驾驶舱大屏;3、能耗分析系统;4、集成联调与培训服务。',
- video: '',
- isFavorite: false,
- },
- {
- id: 'case-003',
- title: '智能穿戴设备PCB设计与量产',
- category: 'hardware',
- cover: '',
- finishDate: '2026/8/10',
- teamSize: 50,
- scale: '10-50万',
- industry: '智能设备',
- duration: '30个工作日',
- clientType: '企业',
- background: '客户推出新一代智能穿戴产品,需要高集成度低功耗 PCB 设计并快速量产。',
- painPoints: '1、板载空间受限,器件布局密集;2、功耗要求严格;3、量产良率需爬坡。',
- requirements: '完成原理图与 PCB 设计、打样验证、SMT 贴片与整机装配量产。',
- deliverables: '1、原理图与 PCB 设计文件;2、样板与测试报告;3、量产良率报告;4、BOM 与工艺文件。',
- video: '',
- isFavorite: false,
- },
- {
- id: 'case-004',
- title: '智能园区综合管理平台开发',
- category: 'internet',
- cover: '',
- finishDate: '2026/8/10',
- teamSize: 20,
- scale: '200万',
- industry: '园区服务',
- duration: '200个工作日',
- clientType: '企业',
- background: '传统园区管理普遍"数据孤岛",安防、能耗、物业、招商等系统独立运行,缺乏联动,管理手段依赖人工巡检和纸质流程。',
- painPoints: '1、运营效率低下:安保依赖人工巡逻;2、系统孤岛严重:门禁、监控、停车、能耗等子系统独立建设;3、能源浪费突出;4、服务体验欠佳;5、决策缺乏依据;6、安全隐患犹存。',
- requirements: '打造集"感知、预警、分析、决策、服务"于一体的智能园区综合管理平台,打通园区各子系统壁垒,实现全场景数字化覆盖。',
- deliverables: '1、Web 端管理平台(驾驶舱/安防/能源/物业);2、移动端服务门户;3、IoT 平台与数据大屏;4、全套技术文档;5、培训与不少于1年免费质保。',
- video: '',
- isFavorite: false,
- },
- {
- id: 'case-005',
- title: '电商中台系统定制开发',
- category: 'internet',
- cover: '',
- finishDate: '2026/7/20',
- teamSize: 15,
- scale: '150万',
- industry: '电商零售',
- duration: '120个工作日',
- clientType: '企业',
- background: '品牌方多渠道订单分散,库存与会员数据不通,需要统一电商中台。',
- painPoints: '1、多平台库存不同步;2、会员体系割裂;3、营销活动上线周期长。',
- requirements: '建设商品/库存/订单/会员中台,开放 API 对接主流电商平台。',
- deliverables: '1、中台系统与管理后台;2、开放 API 与对接文档;3、数据迁移方案;4、上线陪跑服务。',
- video: '',
- isFavorite: false,
- },
- {
- id: 'case-006',
- title: '教培机构在线学习平台',
- category: 'internet',
- cover: '',
- finishDate: '2026/6/30',
- teamSize: 12,
- scale: '80万',
- industry: '教育培训',
- duration: '90个工作日',
- clientType: '企业',
- background: '教培机构需要线上授课、作业批改与学员管理一体化能力。',
- painPoints: '1、授课依赖第三方工具,数据不沉淀;2、续费转化缺少数据支撑。',
- requirements: '交付直播授课、题库作业、班级管理、数据分析完整平台(Web + 小程序)。',
- deliverables: '1、Web 学习平台;2、学员端小程序;3、题库与直播模块;4、运维部署手册。',
- video: '',
- isFavorite: false,
- },
- {
- id: 'case-007',
- title: '医院智能随访系统开发',
- category: 'internet',
- cover: '',
- finishDate: '2026/5/18',
- teamSize: 18,
- scale: '250万',
- industry: '医疗健康',
- duration: '180个工作日',
- clientType: '企业',
- background: '三甲医院需要对出院患者进行结构化随访,提升复诊率与科研数据质量。',
- painPoints: '1、电话随访效率低;2、随访数据非结构化,科研难复用。',
- requirements: '建设随访计划引擎、患者端触达与随访数据仓,对接 HIS 系统。',
- deliverables: '1、随访管理平台;2、患者端小程序;3、HIS 对接中间件;4、随访报表与数据字典。',
- video: '',
- isFavorite: false,
- },
- {
- id: 'case-008',
- title: '园区光伏能耗管理系统',
- category: 'iot',
- cover: '',
- finishDate: '2026/4/25',
- teamSize: 10,
- scale: '120万',
- industry: '能源环保',
- duration: '60个工作日',
- clientType: '企业',
- background: '园区部署屋顶光伏后需要实时监控发电与用电,实现削峰填谷。',
- painPoints: '1、发电数据分散在多品牌逆变器;2、能耗缺少分项计量。',
- requirements: '接入逆变器/电表数据,建设发电与能耗看板,输出节能策略。',
- deliverables: '1、能耗采集网关适配;2、监控看板系统;3、告警与报表;4、验收测试报告。',
- video: '',
- isFavorite: false,
- },
-]
-
-const SCALE_BOUNDS: Record = {
- lt50w: [0, 50],
- '50to100w': [50, 100],
- '100to300w': [100, 300],
- '300to500w': [300, 500],
- gt500w: [500, Number.POSITIVE_INFINITY],
-}
-
-/** 从展示值解析规模数值(万),如 '300万' → 300,'10-50万' → 10 */
-function parseScaleValue(scale: string): number {
- const value = Number.parseFloat(scale)
- return Number.isNaN(value) ? 0 : value
-}
-
-function matchScale(item: Case, selected: ScaleRange[]): boolean {
- if (selected.length === 0) return true
- const value = parseScaleValue(item.scale)
- return selected.some((key) => {
- const [min, max] = SCALE_BOUNDS[key]
- return value >= min && value < max
+/**
+ * 案例库分页列表(返回列表 + 业务数据统计)。
+ *
+ * 规模区间是嵌套对象(`scales[0].lowerLimit`),必须用 serializeQuery 序列化——
+ * axios 默认的 `scales[0][lowerLimit]` 后端绑不上,实测 HTTP 500。
+ *
+ * @param bo 筛选条件(businessTypes 按案例表 business_type 原值匹配)
+ * @param pageNum 页码,从 1 开始(后端 PageQuery)
+ * @param pageSize 每页条数
+ */
+export function getCaseList(
+ bo: ProjectCaseUserBo,
+ pageNum: number,
+ pageSize: number,
+): Promise {
+ return request({
+ url: '/web/projectCase/list',
+ method: 'get',
+ params: { ...bo, pageNum, pageSize },
+ paramsSerializer: { serialize: serializeQuery },
})
}
-function matchDate(item: Case, range: [string, string] | null): boolean {
- if (!range) return true
- const time = new Date(item.finishDate).getTime()
- const start = new Date(range[0]).getTime()
- const end = new Date(range[1]).getTime()
- return time >= start && time <= end
-}
-
-function applyFilters(query: CaseQuery): Case[] {
- return MOCK_CASES.filter((item) => {
- if (query.category !== 'all' && item.category !== query.category) return false
- if (query.industries.length > 0 && !query.industries.includes(item.industry)) return false
- if (!matchScale(item, query.scales)) return false
- if (!matchDate(item, query.dateRange)) return false
- if (query.keyword && !item.title.includes(query.keyword)) return false
- return true
+/**
+ * 案例详情(含项目背景富文本与附件,并回填当前用户是否已收藏)。
+ *
+ * 注意:该接口每次调用都会把案例预览量 +1,勿在轮询 / 重试场景滥用。
+ */
+export function getCaseDetail(id: string): Promise {
+ return request({
+ url: `/web/projectCase/${id}`,
+ method: 'get',
})
}
-/** 案例列表(筛选 + 分页) */
-export function getCaseList(query: CaseQuery): Promise> {
- const filtered = applyFilters(query)
- const start = (query.page - 1) * query.pageSize
- return Promise.resolve({
- list: filtered.slice(start, start + query.pageSize),
- total: filtered.length,
+/**
+ * 案例收藏 / 取消收藏。
+ *
+ * 后端为 toggle 语义:已收藏则取消收藏。需登录态,匿名调用会被后端拦截。
+ */
+export function collectCase(id: string): Promise {
+ return request({
+ url: '/web/projectCase/collect',
+ method: 'post',
+ data: { id },
})
}
-/** 案例详情(不存在返回 null) */
-export function getCaseDetail(id: string): Promise {
- const found = MOCK_CASES.find((item) => item.id === id) ?? null
- return Promise.resolve(found)
+/**
+ * 筛选面板选项(业务类型统计 + 行业 + 规模档位)。
+ *
+ * 左侧筛选除完成时间外的选项都由本接口一次下发:业务类型与案例总数在
+ * {@code projectCaseStatisticsVo},行业在 {@code businessIndustry},规模在
+ * {@code upperAndLowerLimits}(label 已由后端拼好)。
+ */
+export function getCaseCategories(): Promise {
+ return request({
+ url: '/web/projectCase/categories',
+ method: 'get',
+ })
}
-/** 收藏/取消收藏,返回最新案例数据 */
-export function toggleFavorite(id: string): Promise {
- const found = MOCK_CASES.find((item) => item.id === id)
- if (!found) return Promise.reject(new Error('案例不存在'))
- found.isFavorite = !found.isFavorite
- return Promise.resolve({ ...found })
-}
-
-/** 业务类型计数(筛选面板角标) */
-export function getCaseCounts(): Promise>> {
- const counts: Partial> = {}
- for (const item of MOCK_CASES) {
- counts[item.category] = (counts[item.category] ?? 0) + 1
- }
- return Promise.resolve(counts)
+/**
+ * 案例浏览王(首页「案例库精选」)。
+ *
+ * 不传 type 时返回「各业务类型的浏览王」各一条;传 type 时按案例表
+ * business_type 的原值精确匹配,返回该类型全部案例。
+ */
+export function getFeaturedCases(type?: string): Promise {
+ return request({
+ url: '/web/projectCase/mostViewed',
+ method: 'get',
+ params: type ? { type } : undefined,
+ })
}
diff --git a/src/api/demand.ts b/src/api/demand.ts
index f35d9f2..bb40589 100644
--- a/src/api/demand.ts
+++ b/src/api/demand.ts
@@ -1,25 +1,183 @@
-import type { Demand } from '@/types/business/demand'
-import { storage } from '@/utils/storage'
+// 需求提报接口(真实后端绑定)
+// 草稿 com.ruoyi.vermins.controller.pc.web.WorkUserOrderDraftController → /web/workOrderDraft
+// 提交 com.ruoyi.vermins.controller.pc.web.ProjectRequirementUserController → /web/requirementPool/create(WorkOrderDraftBo)
+// 同控制器的 /submit(RdProjectBo)是直接提报入口(额外落 vm_rd_project),前端当前未使用
+// 业务类型 com.ruoyi.vermins.controller.pc.ProjectSubmissionWebController → /web/projectSubmission(@SaIgnore 免登录)
+// 预算范围 org.dromara.system.controller.system.SysDictDataController → /system/dict/data/type/{dictType}
+// 图片换链 org.dromara.resource.controller.SysOssController → /resource/oss/listByIds/{ids}
+// 鉴权:业务类型列表免登录;草稿 / 提交 / 字典 / OSS 需登录态(匿名实测 401,故 /demand 路由带 requiresAuth)
+//
+// 使用约定(与项目 SKILL.md 一致):
+// 页面/组件禁止直接 import axios,一律经 store 调用本模块;本文件只放「函数签名 + 请求路径」。
+import service, { request } from '@/utils/request'
+import type { ApiResponse } from '@/types/api'
+import type {
+ Demand,
+ DemandDraftVo,
+ DictOption,
+ OssUploadVo,
+ OssUrlVo,
+ SubmissionBizTypeVo,
+} from '@/types/business/demand'
-// TODO(api): 待接口文档到位后,将以下 mock 替换为 request 调用(页面 → store → api 数据流不变)
+/** 业务类型字典类型(sys_dict_type.dict_type)——仅作提报列表为空时的兜底值域 */
+export const BIZ_TYPE_DICT = 'vm_biz_type'
+/** 预算范围字典类型 */
+export const BUDGET_RANGE_DICT = 'vm_budget_range'
-const DRAFT_KEY = 'demand-draft'
+/**
+ * 业务类型列表(项目提报表,**免登录**)。
+ *
+ * 后端 `ProjectSubmissionWebController` 带 `@SaIgnore`,是需求页唯一匿名可用的接口;
+ * 它与首页 `/web/home-page/service-type-list` 共用同一个 `queryEnabledList()`,只返回
+ * `enabled = 1` 且未被逻辑删除的记录。
+ *
+ * 返回项里 `subtitle` 是卡片描述来源、`logoUrl` 是卡片图标来源(实测是 OSS 文件标识,
+ * 需经 {@link getOssUrls} 换链)。
+ */
+export function getSubmissionBizTypes(): Promise {
+ return request({
+ url: '/web/projectSubmission/list',
+ method: 'get',
+ })
+}
-/** 提交需求(mock:返回提报单 id) */
-export function submitDemand(demand: Demand): Promise<{ id: string }> {
- if (!demand.businessType || !demand.projectName || !demand.coreDescription) {
- return Promise.reject(new Error('需求信息不完整,无法提交'))
+/**
+ * OSS 文件标识 → 可直接访问的地址。
+ *
+ * 提报列表的 `coverUrl` / `logoUrl` 下发的是 OSS 主键(如 2099753406859292674)而非地址,
+ * 直接当 `
` 用会请求到本站的错误路径上,必须换链。
+ *
+ * ⚠️ 本接口需要登录态;失败时调用方回退前端内置图标,且不阻塞页面。
+ */
+export function getOssUrls(ids: (string | number)[]): Promise {
+ return request({
+ url: `/resource/oss/listByIds/${ids.join(',')}`,
+ method: 'get',
+ })
+}
+
+/**
+ * 业务类型字典(`vm_biz_type`)。
+ *
+ * ⚠️ 仅作**兜底**用:业务类型值域已改为提报列表 {@link getSubmissionBizTypes};
+ * 只有当提报表一条可用记录都没有时,store 才回退到本字典,避免页面无可选项。
+ */
+export function getBizTypeDict(): Promise {
+ return request({
+ url: `/system/dict/data/type/${BIZ_TYPE_DICT}`,
+ method: 'get',
+ })
+}
+
+/** 预算范围选项(字典 vm_budget_range) */
+export function getBudgetRangeDict(): Promise {
+ return request({
+ url: `/system/dict/data/type/${BUDGET_RANGE_DICT}`,
+ method: 'get',
+ })
+}
+
+/**
+ * 读取当前登录用户的需求提报草稿(无草稿返回 null)。
+ *
+ * ⚠️ 后端 `WorkUserOrderDraftController#getDraft` 在"该用户还没有草稿"时会 NPE:
+ * `VmWorkOrderDraftServiceImpl#queryById` 里 `draft` 为 null 仍继续执行 `draft.getId()`,
+ * 实测返回 code=500「发生未知异常」。这里刻意绕过统一 `request`(它会对非 200 弹错误提示),
+ * 直接用原始实例静默兜底:非 200 一律视作"无草稿"。后端补上判空后本段可简化。
+ */
+export async function getDraftDemand(): Promise {
+ const resp = await service.request>({
+ url: '/web/workOrderDraft/getDraft',
+ method: 'get',
+ })
+ if (resp.data?.code !== 200) return null
+ return resp.data.data ?? null
+}
+
+/**
+ * Demand → 后端 `WorkOrderDraftBo` 请求体。
+ *
+ * 显式列出 BO 的字段,**剔掉纯前端字段 `budgetManual`**(后端 BO/VO 与两张表都没有
+ * 「是否手写」的概念,留着虽然会被 Jackson 忽略,但容易被误读成后端支持该标记)。
+ *
+ * 预算范围只有一个字段可下发:预设档位是字典 dictValue,手写时是用户填的金额文本
+ * ——后端没有可用的 `budgetCustom`(`WorkOrderDraftBo` 里被注释掉、表也无该列)。
+ *
+ * `developmentCycle` 是 Integer,表单未填时是空串,必须归一成 null:空串虽然能靠
+ * Jackson 的宽松转换塞进去,但依赖默认行为、且语义上是"没填"。
+ */
+function toBoPayload(demand: Demand): Record {
+ return {
+ id: demand.id,
+ userName: demand.userName,
+ userPhone: demand.userPhone,
+ bizType: demand.bizType,
+ budgetRange: demand.budgetRange,
+ developmentCycle: demand.developmentCycle === '' ? null : demand.developmentCycle,
+ supplementaryRemarks: demand.supplementaryRemarks,
+ projectName: demand.projectName,
+ requirementDesc: demand.requirementDesc,
+ attachmentInfo: demand.attachmentInfo,
}
- return Promise.resolve({ id: `demand-${Date.now()}` })
}
-/** 读取本地草稿(无草稿返回 null) */
-export function getDraftDemand(): Promise {
- return Promise.resolve(storage.sessionGet(DRAFT_KEY))
-}
-
-/** 保存草稿到本地 */
+/** 新增/覆盖草稿(后端 insertByBo 按登录用户 upsert) */
export function saveDraft(demand: Demand): Promise {
- storage.sessionSet(DRAFT_KEY, { ...demand, status: 'draft' })
- return Promise.resolve()
+ return request({
+ url: '/web/workOrderDraft/add',
+ method: 'post',
+ data: toBoPayload(demand),
+ })
+}
+
+/** 清空当前用户的草稿(后端 cleanHistoryDrafts) */
+export function resetDraft(): Promise {
+ return request({
+ url: '/web/workOrderDraft/reset',
+ method: 'post',
+ })
+}
+
+/** 上传附件(multipart,后端字段名 file,返回 SysOssVo) */
+export function uploadDemandFile(file: File): Promise {
+ const data = new FormData()
+ data.append('file', file)
+ return request({
+ url: '/web/workOrderDraft/upload',
+ method: 'post',
+ data,
+ headers: { 'Content-Type': 'multipart/form-data' },
+ })
+}
+
+/**
+ * 提交需求(`POST /web/requirementPool/create`)。
+ *
+ * 入参与草稿共用同一个 BO(`WorkOrderDraftBo`),故请求体直接复用 {@link toBoPayload},
+ * 不需要字段名转换。
+ *
+ * 后端 `createRequirement` 一个事务里做三件事:
+ *
+ * - 落 `vm_project_requirement`(需求主记录,生成项目编号、优先级=未评估、状态=待接洽);
+ * - 把**请求体里的 `attachmentInfo`** 逐条复制成需求附件(只复制 `ossId` 引用到
+ * `vm_project_requirement_attachment`,OSS 文件不重复上传)——注意这里是直接吃请求体,
+ * 不像 `/submit` 那样还得先落一次草稿,所以附件随本次请求一起走;
+ * - 清掉该用户的草稿(`cleanHistoryDrafts`)。
+ *
+ *
+ * 后端必填校验:`bizType` / `projectName` / `requirementDesc` 任一为空返回
+ * 「请完整填写业务类型、项目名称和需求描述」;联系人姓名与电话缺失时先回退登录用户资料,
+ * 仍为空则报「请完善联系人姓名和联系电话」。
+ *
+ * ⚠️ 本条链路**只落 `vm_project_requirement` 单表**。而「我的项目/需求」列表
+ * (`/web/requirementPool/list`)查的是 `vm_rd_project`,因此 create 提交的记录不会出现在
+ * 该列表里——要同时落两张表得走后端 `/web/requirementPool/submit`(`RdProjectBo`)。
+ */
+export function submitDemand(demand: Demand): Promise {
+ return request({
+ url: '/web/requirementPool/create',
+ method: 'post',
+ data: toBoPayload(demand),
+ })
}
diff --git a/src/api/home.ts b/src/api/home.ts
index 5adde7d..5bd904e 100644
--- a/src/api/home.ts
+++ b/src/api/home.ts
@@ -1,180 +1,97 @@
-import type { BusinessType } from '@/types/api'
-import type { Case } from '@/types/business/cases'
-import type { BusinessLine, HomeStats, ServiceType } from '@/types/business/home'
+// 首页接口(真实后端绑定)
+// 后端: vermis-maker com.ruoyi.vermins.controller.pc.web.UserSideWebHomePageController
+// 前缀: /web/home-page 鉴权: 公开接口,无需 token
+// 门户入口: com.ruoyi.vermins.controller.pc.web.IPortalEntryUserController(首页快捷入口功能,并入本文件)
+//
+// 使用约定(与项目 SKILL.md 一致):
+// 页面/组件禁止直接 import axios,一律经 store 调用本模块的 request。
+// 本文件只放「函数签名 + 请求路径」,不写业务逻辑。
+import { request } from '@/utils/request'
+import type {
+ AgreementFormVo,
+ BannerVo,
+ PortalEntryGroup,
+ PortalEntryVo,
+ ProjectStatusCount,
+ ProjectSubmissionInfo,
+ ProjectSubmissionVo,
+} from '@/types/business/home'
-// TODO(api): 待接口文档到位后,将以下 mock 返回替换为 request 调用(页面 → store → api 数据流不变)
-
-const SERVICE_TYPES: ServiceType[] = [
- { label: 'Web应用', desc: '响应式 Web 与后台管理系统定制开发' },
- { label: '小程序', desc: '微信/支付宝小程序开发与上架' },
- { label: 'APP', desc: 'iOS/Android 移动应用定制开发' },
- { label: '二次开发', desc: '基于现有系统的功能扩展与重构' },
- { label: '桌面工具', desc: '跨平台桌面端工具与自动化软件' },
-]
-
-const SERVICE_CONTENT = `一、产品咨询与规划服务
-1、业务诊断与梳理:深入调研客户现有业务流程,识别数字化痛点与改进机会。
-2、竞品分析与市场调研:分析行业标杆产品,提炼差异化竞争优势。
-3、产品定位与MVP规划:明确核心用户群体,规划最小可行性产品(MVP)功能集,制定迭代路线图。
-4、技术可行性评估:评估所选技术栈的成熟度、扩展性及与现有系统的兼容性。
-二、UI/UX 设计服务
-1、用户研究与体验地图:构建用户画像,梳理用户旅程,优化关键触点体验。
-2、交互原型设计:制作高保真可交互原型,直观展示产品逻辑与操作流程,支持早期验证。
-3、视觉UI设计:遵循最新设计趋势与品牌规范,输出图标、界面、动效等视觉元素。
-4、设计规范与组件库:建立统一的设计系统(Design System),确保多端体验一致性,提升后续迭代效率。
-三、技术开发服务
-5、前端开发:基于 React/Vue/Flutter 等框架,实现响应式Web、H5、小程序及跨平台移动端应用。
-6、后端开发:采用 Spring Cloud、Node.js、Go 等微服务架构,构建高并发、高可用的业务接口与逻辑。
-7、数据库设计:根据业务特性设计关系型(MySQL)与非关系型(Redis、MongoDB)数据库集群方案。
-8、DevOps 与云服务部署:搭建 CI/CD 自动化流水线,基于 Docker/K8s 进行容器化部署,保障系统弹性伸缩。
-四、质量保障(QA)服务
-1、功能测试:覆盖单元测试、集成测试、系统测试及UAT(用户验收测试)支持。
-2、性能测试:进行压力测试、负载测试,定位系统瓶颈,优化响应速度。
-3、安全测试:实施渗透测试、漏洞扫描,保障数据安全与隐私合规。
-4、兼容性测试:覆盖主流浏览器、操作系统及移动设备型号。
-五、运维与持续运营服务
-1、7x24小时监控:实时监控系统健康度、流量及异常日志。
-2、数据运营分析:接入埋点系统,输出用户行为分析报告,指导产品优化。
-3、迭代升级:基于用户反馈与业务增长需求,提供持续的功能迭代与版本更新。`
-
-const BUSINESS_LINES: BusinessLine[] = [
- {
- id: 'internet',
- name: '互联网软件',
- desc: 'Web/小程序/APP/二次开发/桌面工具等',
- slogan: '专业的软件定制开发服务,从需求到上线全流程保障',
- icon: '',
- serviceContent: SERVICE_CONTENT,
- serviceTypes: SERVICE_TYPES,
- video: '',
- },
- {
- id: 'hardware',
- name: '硬件电路',
- desc: '原理图设计/PCB设计/电气效验等',
- slogan: '专业的硬件电路设计服务,从方案到量产全流程保障',
- icon: '',
- serviceContent: SERVICE_CONTENT,
- serviceTypes: SERVICE_TYPES,
- video: '',
- },
- {
- id: 'iot',
- name: '物联网全案',
- desc: '软硬件开发+云端一体化/定制化开发等',
- slogan: '专业的物联网软硬一体化服务,从感知到云端全流程保障',
- icon: '',
- serviceContent: SERVICE_CONTENT,
- serviceTypes: SERVICE_TYPES,
- video: '',
- },
- {
- id: 'manufacturing',
- name: '生产制造',
- desc: 'PCB制版/SMT/整机装配/定制生产等',
- slogan: '专业的研发智造生产服务,从制版到交付全流程保障',
- icon: '',
- serviceContent: SERVICE_CONTENT,
- serviceTypes: SERVICE_TYPES,
- video: '',
- },
-]
-
-const FEATURED_CASES: Case[] = [
- {
- id: 'case-001',
- title: '自动化生产线升级改造项目',
- category: 'manufacturing',
- cover: '',
- finishDate: '2026/8/10',
- teamSize: 50,
- scale: '300万',
- industry: '自动化',
- duration: '150个工作日',
- clientType: '企业',
- background: '传统产线效率低、人工依赖高,客户需要通过自动化升级提升产能与品控水平。',
- painPoints: '1、产线依赖人工巡检,异常响应慢;2、设备数据孤岛,缺乏统一监控;3、工艺参数无法追溯。',
- requirements: '建设产线数据采集与监控平台,完成关键工位自动化改造,打通 MES 系统接口。',
- deliverables: '1、产线自动化改造方案;2、PLC 控制程序与上位机软件;3、数据看板与运维手册;4、培训与质保服务。',
- video: '',
- isFavorite: false,
- },
- {
- id: 'case-002',
- title: '智能工厂物联网整体解决方案',
- category: 'iot',
- cover: '',
- finishDate: '2026/8/10',
- teamSize: 30,
- scale: '500万',
- industry: '工厂服务',
- duration: '200个工作日',
- clientType: '企业',
- background: '园区工厂多系统独立运行,能耗与设备管理粗放,需要整体数字化升级。',
- painPoints: '1、门禁/监控/能耗子系统数据不互通;2、能源浪费缺乏精细监测;3、决策缺少实时数据支撑。',
- requirements: '构建 IoT 平台接入设备数据,建设综合驾驶舱,实现能耗监测与智能告警。',
- deliverables: '1、IoT 接入平台;2、综合驾驶舱大屏;3、能耗分析系统;4、集成联调与培训服务。',
- video: '',
- isFavorite: false,
- },
- {
- id: 'case-003',
- title: '智能穿戴设备PCB设计与量产',
- category: 'hardware',
- cover: '',
- finishDate: '2026/8/10',
- teamSize: 50,
- scale: '10-50万',
- industry: '智能设备',
- duration: '30个工作日',
- clientType: '企业',
- background: '客户推出新一代智能穿戴产品,需要高集成度低功耗 PCB 设计并快速量产。',
- painPoints: '1、板载空间受限,器件布局密集;2、功耗要求严格;3、量产良率需爬坡。',
- requirements: '完成原理图与 PCB 设计、打样验证、SMT 贴片与整机装配量产。',
- deliverables: '1、原理图与 PCB 设计文件;2、样板与测试报告;3、量产良率报告;4、BOM 与工艺文件。',
- video: '',
- isFavorite: false,
- },
- {
- id: 'case-004',
- title: '智能园区综合管理平台开发',
- category: 'internet',
- cover: '',
- finishDate: '2026/8/10',
- teamSize: 20,
- scale: '200万',
- industry: '园区服务',
- duration: '200个工作日',
- clientType: '企业',
- background: '随着新型城镇化建设的加速和"数字中国"战略的深入推进,传统园区管理模式正面临严峻挑战,普遍存在"数据孤岛"现象。',
- painPoints: '1、运营效率低下:安保依赖人工巡逻;2、系统孤岛严重:门禁、监控、停车等子系统独立建设;3、能源浪费突出;4、服务体验欠佳;5、决策缺乏依据;6、安全隐患犹存。',
- requirements: '打造集"感知、预警、分析、决策、服务"于一体的智能园区综合管理平台,打通园区各子系统壁垒。',
- deliverables: '1、Web 端管理平台(驾驶舱/安防/能源/物业);2、移动端服务门户;3、IoT 平台与数据大屏;4、全套技术文档;5、培训与不少于1年免费质保。',
- video: '',
- isFavorite: false,
- },
-]
-
-/** 首页数据看板 */
-export function getHomeStats(): Promise {
- return Promise.resolve({ pending: 3, inProgress: 3, total: 6 })
+/** 项目状态统计 */
+export function getProjectStatusCount(): Promise {
+ return request({
+ url: '/web/home-page/project-status-count',
+ method: 'get',
+ })
}
-/** 业务线列表(首页 4 卡片) */
-export function getBusinessLines(): Promise {
- return Promise.resolve(BUSINESS_LINES)
+/** 业务服务类型列表 */
+export function getServiceTypeList(): Promise {
+ return request({
+ url: '/web/home-page/service-type-list',
+ method: 'get',
+ })
}
-/** 业务详情(业务详情页) */
-export function getBusinessDetail(type: BusinessType): Promise {
- const found = BUSINESS_LINES.find((line) => line.id === type)
- return Promise.resolve(found ?? BUSINESS_LINES[0]!)
+/** 业务类型详情(聚合 VO:projectSubmission / projectServiceType / projectSubmissionAttachment / projectSubmissionCatalog) */
+export function getServiceTypeDetail(id: number | string): Promise {
+ return request({
+ url: `/web/home-page/service-type-detail/${id}`,
+ method: 'get',
+ })
}
-/** 首页案例精选(category=all 时返回全部) */
-export function getFeaturedCases(category: BusinessType | 'all' = 'all'): Promise {
- const list = category === 'all'
- ? FEATURED_CASES
- : FEATURED_CASES.filter((item) => item.category === category)
- return Promise.resolve(list)
+/** 轮播图列表 */
+export function getCarouselList(): Promise {
+ return request({
+ url: '/web/home-page/carousel-list',
+ method: 'get',
+ })
+}
+
+/** 平台介绍(富文本 HTML) */
+export function getPlatformIntro(): Promise {
+ return request({
+ url: '/web/home-page/platform-intro',
+ method: 'get',
+ })
+}
+
+/** 联系信息(富文本 HTML) */
+export function getContactInfo(): Promise {
+ return request({
+ url: '/web/home-page/contact-info',
+ method: 'get',
+ })
+}
+
+// 首页「案例库精选」接口(/web/projectCase/mostViewed)按资源归位到 `@/api/cases` 的 getFeaturedCases。
+
+/**
+ * 查询用户门户入口列表
+ *
+ * 登录态返回该用户个性化后的分组结果;匿名返回固定目录。
+ * 两组数据由 groupType 区分(1 = 快捷、0 = 其他)。
+ */
+export function getPortalEntryList(): Promise {
+ return request({
+ url: '/vermins/portalEntryUser/list',
+ method: 'get',
+ })
+}
+
+/**
+ * 快捷 / 其他入口互转
+ *
+ * 后端 add 的入参是 @RequestParam,必须走 query 而非 body。
+ *
+ * @param id 入口 id
+ * @param type 目标分组:1 = 快捷、0 = 其他
+ */
+export function switchPortalEntryGroup(id: number, type: PortalEntryGroup): Promise {
+ return request({
+ url: '/vermins/portalEntryUser/add',
+ method: 'post',
+ params: { id, type },
+ })
}
diff --git a/src/api/profile.ts b/src/api/profile.ts
index 06b5017..0b3e7b8 100644
--- a/src/api/profile.ts
+++ b/src/api/profile.ts
@@ -168,12 +168,15 @@ const MOCK_FAVORITES: Case[] = [
industry: '自动化',
duration: '150个工作日',
clientType: '企业',
- background: '传统产线效率低、人工依赖高,客户需要通过自动化升级提升产能与品控水平。',
- painPoints: '1、产线依赖人工巡检,异常响应慢;2、设备数据孤岛,缺乏统一监控;3、工艺参数无法追溯。',
- requirements: '建设产线数据采集与监控平台,完成关键工位自动化改造,打通 MES 系统接口。',
- deliverables: '1、产线自动化改造方案;2、PLC 控制程序与上位机软件;3、数据看板与运维手册;4、培训与质保服务。',
+ sections: [
+ { title: '项目背景', content: '传统产线效率低、人工依赖高,客户需要通过自动化升级提升产能与品控水平。
' },
+ { title: '客户痛点', content: '1、产线依赖人工巡检,异常响应慢;2、设备数据孤岛,缺乏统一监控;3、工艺参数无法追溯。
' },
+ { title: '需求描述', content: '建设产线数据采集与监控平台,完成关键工位自动化改造,打通 MES 系统接口。
' },
+ { title: '交付物', content: '1、产线自动化改造方案;2、PLC 控制程序与上位机软件;3、数据看板与运维手册;4、培训与质保服务。
' },
+ ],
video: '',
isFavorite: true,
+ favoriteCount: 0,
},
{
id: 'fav-2',
@@ -186,12 +189,15 @@ const MOCK_FAVORITES: Case[] = [
industry: '工厂服务',
duration: '200个工作日',
clientType: '企业',
- background: '园区工厂多系统独立运行,能耗与设备管理粗放,需要整体数字化升级。',
- painPoints: '1、门禁/监控/能耗子系统数据不互通;2、能源浪费缺乏精细监测;3、决策缺少实时数据支撑。',
- requirements: '构建 IoT 平台接入设备数据,建设综合驾驶舱,实现能耗监测与智能告警。',
- deliverables: '1、IoT 接入平台;2、综合驾驶舱大屏;3、能耗分析系统;4、集成联调与培训服务。',
+ sections: [
+ { title: '项目背景', content: '园区工厂多系统独立运行,能耗与设备管理粗放,需要整体数字化升级。
' },
+ { title: '客户痛点', content: '1、门禁/监控/能耗子系统数据不互通;2、能源浪费缺乏精细监测;3、决策缺少实时数据支撑。
' },
+ { title: '需求描述', content: '构建 IoT 平台接入设备数据,建设综合驾驶舱,实现能耗监测与智能告警。
' },
+ { title: '交付物', content: '1、IoT 接入平台;2、综合驾驶舱大屏;3、能耗分析系统;4、集成联调与培训服务。
' },
+ ],
video: '',
isFavorite: true,
+ favoriteCount: 0,
},
{
id: 'fav-3',
@@ -204,12 +210,15 @@ const MOCK_FAVORITES: Case[] = [
industry: '智能设备',
duration: '30个工作日',
clientType: '企业',
- background: '客户推出新一代智能穿戴产品,需要高集成度低功耗 PCB 设计并快速量产。',
- painPoints: '1、板载空间受限,器件布局密集;2、功耗要求严格;3、量产良率需爬坡。',
- requirements: '完成原理图与 PCB 设计、打样验证、SMT 贴片与整机装配量产。',
- deliverables: '1、原理图与 PCB 设计文件;2、样板与测试报告;3、量产良率报告;4、BOM 与工艺文件。',
+ sections: [
+ { title: '项目背景', content: '客户推出新一代智能穿戴产品,需要高集成度低功耗 PCB 设计并快速量产。
' },
+ { title: '客户痛点', content: '1、板载空间受限,器件布局密集;2、功耗要求严格;3、量产良率需爬坡。
' },
+ { title: '需求描述', content: '完成原理图与 PCB 设计、打样验证、SMT 贴片与整机装配量产。
' },
+ { title: '交付物', content: '1、原理图与 PCB 设计文件;2、样板与测试报告;3、量产良率报告;4、BOM 与工艺文件。
' },
+ ],
video: '',
isFavorite: true,
+ favoriteCount: 0,
},
{
id: 'fav-4',
@@ -222,12 +231,15 @@ const MOCK_FAVORITES: Case[] = [
industry: '园区服务',
duration: '200个工作日',
clientType: '企业',
- background: '传统园区管理普遍"数据孤岛",安防、能耗、物业、招商等系统独立运行,缺乏联动,管理手段依赖人工巡检和纸质流程。',
- painPoints: '1、运营效率低下:安保依赖人工巡逻;2、系统孤岛严重:门禁、监控、停车、能耗等子系统独立建设;3、能源浪费突出;4、服务体验欠佳;5、决策缺乏依据;6、安全隐患犹存。',
- requirements: '打造集"感知、预警、分析、决策、服务"于一体的智能园区综合管理平台,打通园区各子系统壁垒,实现全场景数字化覆盖。',
- deliverables: '1、Web 端管理平台(驾驶舱/安防/能源/物业);2、移动端服务门户;3、IoT 平台与数据大屏;4、全套技术文档;5、培训与不少于1年免费质保。',
+ sections: [
+ { title: '项目背景', content: '传统园区管理普遍"数据孤岛",安防、能耗、物业、招商等系统独立运行,缺乏联动,管理手段依赖人工巡检和纸质流程。
' },
+ { title: '客户痛点', content: '1、运营效率低下:安保依赖人工巡逻;2、系统孤岛严重:门禁、监控、停车、能耗等子系统独立建设;3、能源浪费突出;4、服务体验欠佳;5、决策缺乏依据;6、安全隐患犹存。
' },
+ { title: '需求描述', content: '打造集"感知、预警、分析、决策、服务"于一体的智能园区综合管理平台,打通园区各子系统壁垒,实现全场景数字化覆盖。
' },
+ { title: '交付物', content: '1、Web 端管理平台(驾驶舱/安防/能源/物业);2、移动端服务门户;3、IoT 平台与数据大屏;4、全套技术文档;5、培训与不少于1年免费质保。
' },
+ ],
video: '',
isFavorite: true,
+ favoriteCount: 0,
},
]
diff --git a/src/api/project.ts b/src/api/project.ts
index fdccd00..e0a516e 100644
--- a/src/api/project.ts
+++ b/src/api/project.ts
@@ -1,13 +1,124 @@
-import type { FileInfo, PageResult } from '@/types/api'
+import type { FileInfo } from '@/types/api'
import type {
AfterSales,
AfterSalesPayload,
Project,
+ ProjectListResult,
ProjectQuery,
+ ProjectStatistics,
+ RequirementDetailVo,
} from '@/types/business/project'
+import { request } from '@/utils/request'
import { formatDate } from '@/utils/format'
+import { toProjectDetail } from '@/utils/projectVo'
-// TODO(api): 待接口文档到位后,将以下 mock 替换为 request 调用
+// ─────────────────────────────────────────────────────────────────────────────
+// 列表:真实接口
+// GET /web/requirementPool/list → ProjectRequirementUserController#list
+// → ProjectRequirementServiceImpl#queryUserPageList
+// 入参 RdProjectBo + PageQuery;出参 ProjectRequirementAndCountVo
+// { requirementPage: { total, rows: RdProjectVo[] }, requirementStatistics: null }
+// 鉴权:需登录态(匿名 401);每请求带 clientid 头(见 utils/request.ts 拦截器)。
+// 筛选参数实测(tools/probe_project_api.py):
+// key → like projectName OR projectNo OR requirementDesc
+// businessType → eq business_type(值是提报表主键 id,不是类型名)
+// status → eq status(码值,见 RdProjectStatus)
+// beginTime/endTime → between create_time(不是 submit_time!)
+// pageNum/pageSize → PageQuery
+// ⚠️ 主表已改为**需求表** vm_project_requirement(LEFT JOIN 项目表),未立项的需求也会出现;
+// 未立项行 progress 为 null(显示「—」)、status 回落需求表中的 INITIATING。
+// ⚠️ 列表行的 businessType 是 SQL 里 JOIN 提报表**翻译后的中文名称**(如「互联网软件」),
+// 与筛选参数 businessType(提报表 id)**不是同一口径**,渲染时不能用 typeCounts 反查。
+// ─────────────────────────────────────────────────────────────────────────────
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 业务类型筛选条件:真实接口
+// GET /web/requirementPool/getFilterConditions
+// → ProjectRequirementUserController#getFilterConditions
+// → ProjectRequirementServiceImpl#getFilterConditions
+// 出参 ProjectRequirementStatisticsVo:
+// { total: 该用户需求总数,
+// businessTypeCounts: [{ businessTypeId, businessTypeLabel, count }] }
+// 口径:**全量、不分页**,所以件数不会随 pageNum/pageSize 变化,适合当筛选栏的固定值域;
+// businessTypeId = vm_project_submission 主键 id(直接回传当 businessType 筛选参数);
+// businessTypeLabel = 该表的 business_type 名称;提报表里没命中的脏数据不计入。
+// ⚠️ /list 响应里的 requirementStatistics 后端仍是注释状态(实测恒 null),统计一律走本端点。
+// ─────────────────────────────────────────────────────────────────────────────
+
+/** 两位补零 */
+function pad(value: number): string {
+ return String(value).padStart(2, '0')
+}
+
+/** Date → 'yyyy-MM-dd HH:mm:ss'(后端 RdProjectBo.beginTime/endTime 是 Date,实测该格式可绑定) */
+function toDateTime(date: Date, endOfDay: boolean): string {
+ const ymd = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
+ return `${ymd} ${endOfDay ? '23:59:59' : '00:00:00'}`
+}
+
+/**
+ * 提交时间条件 → 后端 `beginTime` / `endTime`(都落在 `create_time` 上)。
+ *
+ * 自选区间优先(UI 上两组互斥,这里再兜一层);结束日一律补到 23:59:59,
+ * 否则 `endTime=2026-09-30` 会被绑成当天 00:00:00 而漏掉当天数据。
+ */
+function toTimeRange(query: ProjectQuery): { beginTime?: string; endTime?: string } {
+ if (query.startDate || query.endDate) {
+ return {
+ beginTime: query.startDate ? `${query.startDate} 00:00:00` : undefined,
+ endTime: query.endDate ? `${query.endDate} 23:59:59` : undefined,
+ }
+ }
+ if (query.timeShortcut === 'all') return {}
+
+ const start = new Date()
+ if (query.timeShortcut === '3m') start.setMonth(start.getMonth() - 3)
+ else if (query.timeShortcut === '6m') start.setMonth(start.getMonth() - 6)
+ else start.setFullYear(start.getFullYear() - 1)
+ return { beginTime: toDateTime(start, false), endTime: toDateTime(new Date(), true) }
+}
+
+/** 项目列表(真实接口:服务端筛选 + 分页;业务类型统计走 getProjectFilterConditions) */
+export function getProjectList(query: ProjectQuery): Promise {
+ return request({
+ url: '/web/requirementPool/list',
+ method: 'get',
+ params: {
+ pageNum: query.pageNum,
+ pageSize: query.pageSize,
+ // 空串不出现在 querystring 里,与后端 eqIfText 的「有值才等值匹配」语义一致
+ key: query.keyword || undefined,
+ businessType: query.businessType || undefined,
+ status: query.status || undefined,
+ ...toTimeRange(query),
+ },
+ })
+}
+
+/**
+ * 业务类型筛选条件(左侧筛选栏「业务类型」的值域 + 件数)。
+ *
+ * 与列表分开取:`/list` 的 `requirementStatistics` 后端恒为 null,统计由本端点单独下发,
+ * 且是**全量口径**(不随筛选、分页变化),所以进页面拉一次即可。
+ */
+export function getProjectFilterConditions(): Promise {
+ return request({
+ url: '/web/requirementPool/getFilterConditions',
+ method: 'get',
+ })
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 详情:真实接口
+// GET /web/requirementPool/get/{id} → ProjectRequirementUserController#get
+// → ProjectRequirementServiceImpl#queryRequirementByUserId
+// 出参 ProjectRequirementUserVo,经 utils/projectVo.ts 映射成详情页沿用的 Project 形态。
+// ⚠️ id 传的是**需求 id**(vm_project_requirement.id)= 列表行 RdProjectVo.id。
+// 未立项的需求也能查到(项目侧字段为空);不存在 / 非本人 → 后端 data 为 null。
+//
+// 文件 / 售后:**仍是前端 mock** —— 该 Controller 只有 create / list / get /
+// serviceTypeList 四个接口,没有附件上传、售后提交/取消的后端实现。
+// ─────────────────────────────────────────────────────────────────────────────
/** 构造 mock 文件信息 */
function mockFile(name: string, size: number, uploadTime: string): FileInfo {
@@ -17,7 +128,7 @@ function mockFile(name: string, size: number, uploadTime: string): FileInfo {
/** 详情 mock 所属项目 id */
const DETAIL_ID = 'project-004'
-/** 项目列表 mock(6 条,覆盖全部状态,internet/iot 混合) */
+/** 项目列表 mock(6 条,覆盖全部状态,internet/iot 混合)——仅详情页链路在用 */
const MOCK_PROJECTS: Project[] = [
{
id: 'project-001',
@@ -176,49 +287,15 @@ const PROJECT_DETAIL: Project = {
},
}
-/** 提交时间快捷区间匹配 */
-function matchTimeShortcut(submitTime: string, shortcut: ProjectQuery['timeShortcut']): boolean {
- if (shortcut === 'all') return true
- // mock 时间为 '2026-8-17 14:00',替换为 / 保证各环境可解析
- const time = new Date(submitTime.replaceAll('-', '/')).getTime()
- const start = new Date()
- if (shortcut === '3m') start.setMonth(start.getMonth() - 3)
- if (shortcut === '6m') start.setMonth(start.getMonth() - 6)
- if (shortcut === '1y') start.setFullYear(start.getFullYear() - 1)
- return time >= start.getTime()
-}
-
-/** 项目列表(筛选 + 分页,pageSize 固定 8) */
-export function getProjectList(query: ProjectQuery): Promise> {
- const filtered = MOCK_PROJECTS.filter((item) => {
- if (
- query.keyword
- && !item.projectName.includes(query.keyword)
- && !item.projectNo.includes(query.keyword)
- ) {
- return false
- }
- if (query.businessType !== 'all' && item.businessType !== query.businessType) return false
- if (query.status !== 'all' && item.status !== query.status) return false
- if (!matchTimeShortcut(item.submitTime, query.timeShortcut)) return false
- return true
- })
- const pageSize = query.pageSize > 0 ? query.pageSize : 8
- const start = (query.page - 1) * pageSize
- return Promise.resolve({
- list: filtered.slice(start, start + pageSize),
- total: filtered.length,
- })
-}
-
-/** 项目详情(不存在返回 null;仅详情项目返回完整数据) */
+/** 项目详情(真实接口;不存在 / 非本人需求返回 null) */
export function getProjectDetail(id: string): Promise {
- if (id === DETAIL_ID) return Promise.resolve({ ...PROJECT_DETAIL })
- const found = MOCK_PROJECTS.find((item) => item.id === id) ?? null
- return Promise.resolve(found ? { ...found } : null)
+ return request({
+ url: `/web/requirementPool/get/${id}`,
+ method: 'get',
+ }).then((vo) => (vo ? toProjectDetail(vo) : null))
}
-/** 上传项目文件(追加进详情文件列表) */
+/** 上传项目文件(mock:追加进详情文件列表) */
export function uploadProjectFile(file: File): Promise {
const info: FileInfo = {
name: file.name,
@@ -230,7 +307,7 @@ export function uploadProjectFile(file: File): Promise {
return Promise.resolve({ ...info })
}
-/** 删除项目文件(按索引从详情文件列表移除) */
+/** 删除项目文件(mock:按索引从详情文件列表移除) */
export function deleteProjectFile(index: number): Promise {
if (index >= 0 && index < PROJECT_DETAIL.files.length) {
PROJECT_DETAIL.files.splice(index, 1)
@@ -238,7 +315,7 @@ export function deleteProjectFile(index: number): Promise {
return Promise.resolve()
}
-/** 提交售后工单 */
+/** 提交售后工单(mock) */
export function submitAfterSales(
projectId: string,
payload: AfterSalesPayload,
@@ -255,7 +332,7 @@ export function submitAfterSales(
return Promise.resolve({ ...result, attachments: [...result.attachments] })
}
-/** 取消售后工单 */
+/** 取消售后工单(mock) */
export function cancelAfterSales(projectId: string): Promise {
const result: AfterSales = {
status: '无',
diff --git a/src/components/AppFooter.vue b/src/components/AppFooter.vue
index bfc8cc8..c69916a 100644
--- a/src/components/AppFooter.vue
+++ b/src/components/AppFooter.vue
@@ -5,10 +5,10 @@ const siteStore = useSiteStore()
const quickLinks = [
{ label: '首页', path: '/' },
- { label: '案例中心', path: '/cases' },
- { label: '需求提报', path: '/demand' },
+ { label: '案例库', path: '/cases' },
+ { label: '需求池', path: '/demand' },
{ label: '下载 APP', path: '/' },
- { label: '帮助中心', path: '/profile/help' },
+ { label: '消息中心', path: '/message' },
]
const coreBusiness = [
@@ -40,6 +40,17 @@ const coreBusiness = [
+
+
-
-