From 43c2dc0d8d7c04cc7673c98044e9165fac394bde Mon Sep 17 00:00:00 2001 From: wangzzpeng <1032675359@qq.com> Date: Mon, 28 Sep 2026 10:47:49 +0800 Subject: [PATCH] =?UTF-8?q?=E6=81=A2=E5=A4=8D=E4=BF=AE=E6=94=B9=E7=9A=84?= =?UTF-8?q?=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/auth.ts | 80 +-- src/api/cases.ts | 291 +++------ src/api/demand.ts | 192 +++++- src/api/home.ts | 261 +++----- src/api/profile.ts | 44 +- src/api/project.ts | 169 +++-- src/components/AppFooter.vue | 28 +- src/components/AppHeader.vue | 30 +- src/components/CaseCard.vue | 166 +++-- src/layout/index.vue | 19 +- src/router/routes.ts | 5 +- src/stores/modules/cases.ts | 226 ++++++- src/stores/modules/demand.ts | 321 ++++++++-- src/stores/modules/home.ts | 248 ++++++- src/stores/modules/project.ts | 121 +++- src/stores/modules/user.ts | 36 +- src/styles/mixins.less | 23 + src/styles/variables.less | 28 +- src/types/business/cases.d.ts | 261 +++++++- src/types/business/demand.d.ts | 195 +++++- src/types/business/home.d.ts | 197 +++++- src/types/business/project.d.ts | 307 ++++++++- src/types/business/user.d.ts | 15 + src/types/router.d.ts | 7 + src/utils/format.ts | 29 +- src/utils/request.ts | 41 ++ src/views/cases/cpns/CaseDetailHeader.vue | 247 +++---- src/views/cases/cpns/CaseRichText.vue | 71 +- src/views/cases/cpns/CasesFilter.vue | 640 +++++++++++++------ src/views/cases/cpns/CasesGrid.vue | 33 +- src/views/cases/detail.vue | 64 +- src/views/cases/index.ts | 2 + src/views/cases/index.vue | 114 +++- src/views/demand/index.ts | 9 +- src/views/demand/index.vue | 226 ++++--- src/views/home/cpns/HomeBusinessCards.vue | 95 ++- src/views/home/cpns/HomeCaseShowcase.vue | 135 +++- src/views/home/cpns/HomeQuickEntries.vue | 165 ++++- src/views/home/cpns/HomeStats.vue | 21 +- src/views/home/cpns/HomeWelcome.vue | 55 +- src/views/home/detail.vue | 28 +- src/views/home/index.ts | 4 + src/views/home/index.vue | 59 +- src/views/login/index.vue | 5 +- src/views/login/register.vue | 46 +- src/views/project/after-sales.vue | 10 +- src/views/project/cpns/ProjectAfterSales.vue | 4 +- src/views/project/cpns/ProjectFiles.vue | 6 +- src/views/project/cpns/ProjectFilter.vue | 470 +++++++++++--- src/views/project/cpns/ProjectFinance.vue | 10 +- src/views/project/cpns/ProjectInfoCard.vue | 3 +- src/views/project/cpns/ProjectMilestones.vue | 2 +- src/views/project/cpns/ProjectTable.vue | 182 +++--- src/views/project/cpns/ProjectTeam.vue | 2 +- src/views/project/detail.vue | 22 +- src/views/project/index.ts | 2 + src/views/project/index.vue | 127 ++-- 57 files changed, 4557 insertions(+), 1642 deletions(-) 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 = [

+ + - -