恢复修改的数据

This commit is contained in:
wangzzpeng
2026-09-28 10:47:49 +08:00
parent f1e64ef864
commit 43c2dc0d8d
57 changed files with 4557 additions and 1642 deletions

View File

@@ -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<T> 调用(页面 → 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<AuthLoginResult> {
return request<AuthLoginResult>({
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<RegisterResult> {
return request<RegisterResult>({
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<void> {
return Promise.resolve()
}
/** 账号密码登录(mock:demo 账号可登录,其余提示先注册) */
export function loginByAccount(params: LoginByAccountParams): Promise<LoginResult> {
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<LoginResult> {
const { phone, code } = params
@@ -69,19 +90,6 @@ function validateAuthForm(
return null
}
/** 注册 */
export function register(params: RegisterParams): Promise<void> {
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<void> {
const message = validateAuthForm(

View File

@@ -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<T>。
// 本文件只放「函数签名 + 请求路径」,不做数据加工、不维护状态。
import { request, serializeQuery } from '@/utils/request'
import type {
CaseSelectionModel,
ProjectCaseAndCaseCountVo,
ProjectCaseUserBo,
ProjectCaseVo,
} from '@/types/business/cases'
// TODO(api): 待接口文档到位后,将以下 mock 替换为 request<T> 调用
/** 案例 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<ScaleRange, [number, number]> = {
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
/**
* 案例库分页列表(返回列表 + 业务数据统计)。
*
* <p>规模区间是嵌套对象(`scales[0].lowerLimit`),必须用 serializeQuery 序列化——
* axios 默认的 `scales[0][lowerLimit]` 后端绑不上,实测 HTTP 500。</p>
*
* @param bo 筛选条件(businessTypes 按案例表 business_type 原值匹配)
* @param pageNum 页码,从 1 开始(后端 PageQuery)
* @param pageSize 每页条数
*/
export function getCaseList(
bo: ProjectCaseUserBo,
pageNum: number,
pageSize: number,
): Promise<ProjectCaseAndCaseCountVo> {
return request<ProjectCaseAndCaseCountVo>({
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
/**
* 案例详情(含项目背景富文本与附件,并回填当前用户是否已收藏)。
*
* <p>注意:该接口每次调用都会把案例预览量 +1,勿在轮询 / 重试场景滥用。</p>
*/
export function getCaseDetail(id: string): Promise<ProjectCaseVo | null> {
return request<ProjectCaseVo | null>({
url: `/web/projectCase/${id}`,
method: 'get',
})
}
/** 案例列表(筛选 + 分页) */
export function getCaseList(query: CaseQuery): Promise<PageResult<Case>> {
const filtered = applyFilters(query)
const start = (query.page - 1) * query.pageSize
return Promise.resolve({
list: filtered.slice(start, start + query.pageSize),
total: filtered.length,
/**
* 案例收藏 / 取消收藏。
*
* <p>后端为 toggle 语义:已收藏则取消收藏。需登录态,匿名调用会被后端拦截。</p>
*/
export function collectCase(id: string): Promise<void> {
return request<void>({
url: '/web/projectCase/collect',
method: 'post',
data: { id },
})
}
/** 案例详情(不存在返回 null) */
export function getCaseDetail(id: string): Promise<Case | null> {
const found = MOCK_CASES.find((item) => item.id === id) ?? null
return Promise.resolve(found)
/**
* 筛选面板选项(业务类型统计 + 行业 + 规模档位)。
*
* <p>左侧筛选除完成时间外的选项都由本接口一次下发:业务类型与案例总数在
* {@code projectCaseStatisticsVo},行业在 {@code businessIndustry},规模在
* {@code upperAndLowerLimits}(label 已由后端拼好)。</p>
*/
export function getCaseCategories(): Promise<CaseSelectionModel> {
return request<CaseSelectionModel>({
url: '/web/projectCase/categories',
method: 'get',
})
}
/** 收藏/取消收藏,返回最新案例数据 */
export function toggleFavorite(id: string): Promise<Case> {
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<Partial<Record<BusinessType, number>>> {
const counts: Partial<Record<BusinessType, number>> = {}
for (const item of MOCK_CASES) {
counts[item.category] = (counts[item.category] ?? 0) + 1
}
return Promise.resolve(counts)
/**
* 案例浏览王(首页「案例库精选」)。
*
* <p>不传 type 时返回「各业务类型的浏览王」各一条;传 type 时按案例表
* business_type 的原值精确匹配,返回该类型全部案例。</p>
*/
export function getFeaturedCases(type?: string): Promise<ProjectCaseVo[]> {
return request<ProjectCaseVo[]>({
url: '/web/projectCase/mostViewed',
method: 'get',
params: type ? { type } : undefined,
})
}

View File

@@ -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<T> 调用(页面 → 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'
/**
* 业务类型列表(项目提报表,**免登录**)。
*
* <p>后端 `ProjectSubmissionWebController` 带 `@SaIgnore`,是需求页唯一匿名可用的接口;
* 它与首页 `/web/home-page/service-type-list` 共用同一个 `queryEnabledList()`,只返回
* `enabled = 1` 且未被逻辑删除的记录。</p>
*
* <p>返回项里 `subtitle` 是卡片描述来源、`logoUrl` 是卡片图标来源(实测是 OSS 文件标识,
* 需经 {@link getOssUrls} 换链)。</p>
*/
export function getSubmissionBizTypes(): Promise<SubmissionBizTypeVo[]> {
return request<SubmissionBizTypeVo[]>({
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 文件标识 → 可直接访问的地址。
*
* <p>提报列表的 `coverUrl` / `logoUrl` 下发的是 OSS 主键(如 2099753406859292674)而非地址,
* 直接当 `<img src>` 用会请求到本站的错误路径上,必须换链。</p>
*
* <p>⚠️ 本接口需要登录态;失败时调用方回退前端内置图标,且不阻塞页面。</p>
*/
export function getOssUrls(ids: (string | number)[]): Promise<OssUrlVo[]> {
return request<OssUrlVo[]>({
url: `/resource/oss/listByIds/${ids.join(',')}`,
method: 'get',
})
}
/**
* 业务类型字典(`vm_biz_type`)。
*
* <p>⚠️ 仅作**兜底**用:业务类型值域已改为提报列表 {@link getSubmissionBizTypes};
* 只有当提报表一条可用记录都没有时,store 才回退到本字典,避免页面无可选项。</p>
*/
export function getBizTypeDict(): Promise<DictOption[]> {
return request<DictOption[]>({
url: `/system/dict/data/type/${BIZ_TYPE_DICT}`,
method: 'get',
})
}
/** 预算范围选项(字典 vm_budget_range) */
export function getBudgetRangeDict(): Promise<DictOption[]> {
return request<DictOption[]>({
url: `/system/dict/data/type/${BUDGET_RANGE_DICT}`,
method: 'get',
})
}
/**
* 读取当前登录用户的需求提报草稿(无草稿返回 null)。
*
* <p>⚠️ 后端 `WorkUserOrderDraftController#getDraft` 在"该用户还没有草稿"时会 NPE:
* `VmWorkOrderDraftServiceImpl#queryById` 里 `draft` 为 null 仍继续执行 `draft.getId()`,
* 实测返回 code=500「发生未知异常」。这里刻意绕过统一 `request`(它会对非 200 弹错误提示),
* 直接用原始实例静默兜底:非 200 一律视作"无草稿"。后端补上判空后本段可简化。</p>
*/
export async function getDraftDemand(): Promise<DemandDraftVo | null> {
const resp = await service.request<ApiResponse<DemandDraftVo>>({
url: '/web/workOrderDraft/getDraft',
method: 'get',
})
if (resp.data?.code !== 200) return null
return resp.data.data ?? null
}
/**
* Demand → 后端 `WorkOrderDraftBo` 请求体。
*
* <p>显式列出 BO 的字段,**剔掉纯前端字段 `budgetManual`**(后端 BO/VO 与两张表都没有
* 「是否手写」的概念,留着虽然会被 Jackson 忽略,但容易被误读成后端支持该标记)。</p>
*
* <p>预算范围只有一个字段可下发:预设档位是字典 dictValue,手写时是用户填的金额文本
* ——后端没有可用的 `budgetCustom`(`WorkOrderDraftBo` 里被注释掉、表也无该列)。</p>
*
* <p>`developmentCycle` 是 Integer,表单未填时是空串,必须归一成 null:空串虽然能靠
* Jackson 的宽松转换塞进去,但依赖默认行为、且语义上是"没填"。</p>
*/
function toBoPayload(demand: Demand): Record<string, unknown> {
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<Demand | null> {
return Promise.resolve(storage.sessionGet<Demand>(DRAFT_KEY))
}
/** 保存草稿到本地 */
/** 新增/覆盖草稿(后端 insertByBo 按登录用户 upsert) */
export function saveDraft(demand: Demand): Promise<void> {
storage.sessionSet(DRAFT_KEY, { ...demand, status: 'draft' })
return Promise.resolve()
return request<void>({
url: '/web/workOrderDraft/add',
method: 'post',
data: toBoPayload(demand),
})
}
/** 清空当前用户的草稿(后端 cleanHistoryDrafts) */
export function resetDraft(): Promise<void> {
return request<void>({
url: '/web/workOrderDraft/reset',
method: 'post',
})
}
/** 上传附件(multipart,后端字段名 file,返回 SysOssVo) */
export function uploadDemandFile(file: File): Promise<OssUploadVo> {
const data = new FormData()
data.append('file', file)
return request<OssUploadVo>({
url: '/web/workOrderDraft/upload',
method: 'post',
data,
headers: { 'Content-Type': 'multipart/form-data' },
})
}
/**
* 提交需求(`POST /web/requirementPool/create`)。
*
* <p>入参与草稿共用同一个 BO(`WorkOrderDraftBo`),故请求体直接复用 {@link toBoPayload},
* 不需要字段名转换。</p>
*
* <p>后端 `createRequirement` 一个事务里做三件事:</p>
* <ul>
* <li>落 `vm_project_requirement`(需求主记录,生成项目编号、优先级=未评估、状态=待接洽);</li>
* <li>把**请求体里的 `attachmentInfo`** 逐条复制成需求附件(只复制 `ossId` 引用到
* `vm_project_requirement_attachment`,OSS 文件不重复上传)——注意这里是直接吃请求体,
* 不像 `/submit` 那样还得先落一次草稿,所以附件随本次请求一起走;</li>
* <li>清掉该用户的草稿(`cleanHistoryDrafts`)。</li>
* </ul>
*
* <p>后端必填校验:`bizType` / `projectName` / `requirementDesc` 任一为空返回
* 「请完整填写业务类型、项目名称和需求描述」;联系人姓名与电话缺失时先回退登录用户资料,
* 仍为空则报「请完善联系人姓名和联系电话」。</p>
*
* <p>⚠️ 本条链路**只落 `vm_project_requirement` 单表**。而「我的项目/需求」列表
* (`/web/requirementPool/list`)查的是 `vm_rd_project`,因此 create 提交的记录不会出现在
* 该列表里——要同时落两张表得走后端 `/web/requirementPool/submit`(`RdProjectBo`)。</p>
*/
export function submitDemand(demand: Demand): Promise<void> {
return request<void>({
url: '/web/requirementPool/create',
method: 'post',
data: toBoPayload(demand),
})
}

View File

@@ -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<T>。
// 本文件只放「函数签名 + 请求路径」,不写业务逻辑。
import { request } from '@/utils/request'
import type {
AgreementFormVo,
BannerVo,
PortalEntryGroup,
PortalEntryVo,
ProjectStatusCount,
ProjectSubmissionInfo,
ProjectSubmissionVo,
} from '@/types/business/home'
// TODO(api): 待接口文档到位后,将以下 mock 返回替换为 request<T> 调用(页面 → 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<HomeStats> {
return Promise.resolve({ pending: 3, inProgress: 3, total: 6 })
/** 项目状态统计 */
export function getProjectStatusCount(): Promise<ProjectStatusCount> {
return request<ProjectStatusCount>({
url: '/web/home-page/project-status-count',
method: 'get',
})
}
/** 业务线列表(首页 4 卡片) */
export function getBusinessLines(): Promise<BusinessLine[]> {
return Promise.resolve(BUSINESS_LINES)
/** 业务服务类型列表 */
export function getServiceTypeList(): Promise<ProjectSubmissionVo[]> {
return request<ProjectSubmissionVo[]>({
url: '/web/home-page/service-type-list',
method: 'get',
})
}
/** 业务详情(业务详情页) */
export function getBusinessDetail(type: BusinessType): Promise<BusinessLine> {
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<ProjectSubmissionInfo> {
return request<ProjectSubmissionInfo>({
url: `/web/home-page/service-type-detail/${id}`,
method: 'get',
})
}
/** 首页案例精选(category=all 时返回全部) */
export function getFeaturedCases(category: BusinessType | 'all' = 'all'): Promise<Case[]> {
const list = category === 'all'
? FEATURED_CASES
: FEATURED_CASES.filter((item) => item.category === category)
return Promise.resolve(list)
/** 轮播图列表 */
export function getCarouselList(): Promise<BannerVo[]> {
return request<BannerVo[]>({
url: '/web/home-page/carousel-list',
method: 'get',
})
}
/** 平台介绍(富文本 HTML) */
export function getPlatformIntro(): Promise<AgreementFormVo> {
return request<AgreementFormVo>({
url: '/web/home-page/platform-intro',
method: 'get',
})
}
/** 联系信息(富文本 HTML) */
export function getContactInfo(): Promise<AgreementFormVo> {
return request<AgreementFormVo>({
url: '/web/home-page/contact-info',
method: 'get',
})
}
// 首页「案例库精选」接口(/web/projectCase/mostViewed)按资源归位到 `@/api/cases` 的 getFeaturedCases。
/**
* 查询用户门户入口列表
*
* <p>登录态返回该用户个性化后的分组结果;匿名返回固定目录。
* 两组数据由 groupType 区分(1 = 快捷、0 = 其他)。</p>
*/
export function getPortalEntryList(): Promise<PortalEntryVo[]> {
return request<PortalEntryVo[]>({
url: '/vermins/portalEntryUser/list',
method: 'get',
})
}
/**
* 快捷 / 其他入口互转
*
* <p>后端 add 的入参是 @RequestParam,必须走 query 而非 body。</p>
*
* @param id 入口 id
* @param type 目标分组:1 = 快捷、0 = 其他
*/
export function switchPortalEntryGroup(id: number, type: PortalEntryGroup): Promise<void> {
return request<void>({
url: '/vermins/portalEntryUser/add',
method: 'post',
params: { id, type },
})
}

View File

@@ -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: '<p>传统产线效率低、人工依赖高,客户需要通过自动化升级提升产能与品控水平。</p>' },
{ title: '客户痛点', content: '<p>1、产线依赖人工巡检,异常响应慢;2、设备数据孤岛,缺乏统一监控;3、工艺参数无法追溯。</p>' },
{ title: '需求描述', content: '<p>建设产线数据采集与监控平台,完成关键工位自动化改造,打通 MES 系统接口。</p>' },
{ title: '交付物', content: '<p>1、产线自动化改造方案;2、PLC 控制程序与上位机软件;3、数据看板与运维手册;4、培训与质保服务。</p>' },
],
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: '<p>园区工厂多系统独立运行,能耗与设备管理粗放,需要整体数字化升级。</p>' },
{ title: '客户痛点', content: '<p>1、门禁/监控/能耗子系统数据不互通;2、能源浪费缺乏精细监测;3、决策缺少实时数据支撑。</p>' },
{ title: '需求描述', content: '<p>构建 IoT 平台接入设备数据,建设综合驾驶舱,实现能耗监测与智能告警。</p>' },
{ title: '交付物', content: '<p>1、IoT 接入平台;2、综合驾驶舱大屏;3、能耗分析系统;4、集成联调与培训服务。</p>' },
],
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: '<p>客户推出新一代智能穿戴产品,需要高集成度低功耗 PCB 设计并快速量产。</p>' },
{ title: '客户痛点', content: '<p>1、板载空间受限,器件布局密集;2、功耗要求严格;3、量产良率需爬坡。</p>' },
{ title: '需求描述', content: '<p>完成原理图与 PCB 设计、打样验证、SMT 贴片与整机装配量产。</p>' },
{ title: '交付物', content: '<p>1、原理图与 PCB 设计文件;2、样板与测试报告;3、量产良率报告;4、BOM 与工艺文件。</p>' },
],
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: '<p>传统园区管理普遍"数据孤岛",安防、能耗、物业、招商等系统独立运行,缺乏联动,管理手段依赖人工巡检和纸质流程。</p>' },
{ title: '客户痛点', content: '<p>1、运营效率低下:安保依赖人工巡逻;2、系统孤岛严重:门禁、监控、停车、能耗等子系统独立建设;3、能源浪费突出;4、服务体验欠佳;5、决策缺乏依据;6、安全隐患犹存。</p>' },
{ title: '需求描述', content: '<p>打造集"感知、预警、分析、决策、服务"于一体的智能园区综合管理平台,打通园区各子系统壁垒,实现全场景数字化覆盖。</p>' },
{ title: '交付物', content: '<p>1、Web 端管理平台(驾驶舱/安防/能源/物业);2、移动端服务门户;3、IoT 平台与数据大屏;4、全套技术文档;5、培训与不少于1年免费质保。</p>' },
],
video: '',
isFavorite: true,
favoriteCount: 0,
},
]

View File

@@ -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<T> 调用
// ─────────────────────────────────────────────────────────────────────────────
// 列表:真实接口
// 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` 上)。
*
* <p>自选区间优先(UI 上两组互斥,这里再兜一层);结束日一律补到 23:59:59,
* 否则 `endTime=2026-09-30` 会被绑成当天 00:00:00 而漏掉当天数据。</p>
*/
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<ProjectListResult> {
return request<ProjectListResult>({
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),
},
})
}
/**
* 业务类型筛选条件(左侧筛选栏「业务类型」的值域 + 件数)。
*
* <p>与列表分开取:`/list` 的 `requirementStatistics` 后端恒为 null,统计由本端点单独下发,
* 且是**全量口径**(不随筛选、分页变化),所以进页面拉一次即可。</p>
*/
export function getProjectFilterConditions(): Promise<ProjectStatistics> {
return request<ProjectStatistics>({
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<PageResult<Project>> {
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<Project | null> {
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<RequirementDetailVo | null>({
url: `/web/requirementPool/get/${id}`,
method: 'get',
}).then((vo) => (vo ? toProjectDetail(vo) : null))
}
/** 上传项目文件(追加进详情文件列表) */
/** 上传项目文件(mock:追加进详情文件列表) */
export function uploadProjectFile(file: File): Promise<FileInfo> {
const info: FileInfo = {
name: file.name,
@@ -230,7 +307,7 @@ export function uploadProjectFile(file: File): Promise<FileInfo> {
return Promise.resolve({ ...info })
}
/** 删除项目文件(按索引从详情文件列表移除) */
/** 删除项目文件(mock:按索引从详情文件列表移除) */
export function deleteProjectFile(index: number): Promise<void> {
if (index >= 0 && index < PROJECT_DETAIL.files.length) {
PROJECT_DETAIL.files.splice(index, 1)
@@ -238,7 +315,7 @@ export function deleteProjectFile(index: number): Promise<void> {
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<AfterSales> {
const result: AfterSales = {
status: '无',