feat 完善注释
This commit is contained in:
@@ -8,16 +8,8 @@ import type {
|
|||||||
} from '@/types/business/user'
|
} from '@/types/business/user'
|
||||||
import { isPhone, isValidPassword } from '@/utils/validate'
|
import { isPhone, isValidPassword } from '@/utils/validate'
|
||||||
|
|
||||||
/**
|
|
||||||
* PC 端 clientId(sys_client 种子数据,grantType 含 password)。
|
|
||||||
* 与请求头 clientid 同源(见 @/utils/request),改一处即可整体生效。
|
|
||||||
*/
|
|
||||||
export const PC_CLIENT_ID = CLIENT_ID
|
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> {
|
export function loginByAccountApi(data: { username: string; password: string }): Promise<AuthLoginResult> {
|
||||||
return request<AuthLoginResult>({
|
return request<AuthLoginResult>({
|
||||||
url: '/auth/login',
|
url: '/auth/login',
|
||||||
@@ -31,10 +23,6 @@ export function loginByAccountApi(data: { username: string; password: string }):
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 账号密码注册:POST /auth/register
|
|
||||||
* userType=sys_user;需后端开启注册开关(sys.account.registerUser=true),否则返回「当前系统没有开启注册功能!」
|
|
||||||
*/
|
|
||||||
export function registerByAccountApi(data: { username: string; password: string }): Promise<RegisterResult> {
|
export function registerByAccountApi(data: { username: string; password: string }): Promise<RegisterResult> {
|
||||||
return request<RegisterResult>({
|
return request<RegisterResult>({
|
||||||
url: '/auth/register',
|
url: '/auth/register',
|
||||||
@@ -53,13 +41,11 @@ export function registerByAccountApi(data: { username: string; password: string
|
|||||||
|
|
||||||
const SIX_DIGIT_CODE = /^\d{6}$/
|
const SIX_DIGIT_CODE = /^\d{6}$/
|
||||||
|
|
||||||
/** 发送短信验证码 */
|
|
||||||
export function sendSmsCode(phone: string): Promise<void> {
|
export function sendSmsCode(phone: string): Promise<void> {
|
||||||
if (!isPhone(phone)) return Promise.reject(new Error('手机号格式不正确'))
|
if (!isPhone(phone)) return Promise.reject(new Error('手机号格式不正确'))
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 手机验证码登录(mock:验证码须为 6 位数字) */
|
|
||||||
export function loginBySms(params: LoginBySmsParams): Promise<LoginResult> {
|
export function loginBySms(params: LoginBySmsParams): Promise<LoginResult> {
|
||||||
const { phone, code } = params
|
const { phone, code } = params
|
||||||
if (!isPhone(phone)) return Promise.reject(new Error('手机号格式不正确'))
|
if (!isPhone(phone)) return Promise.reject(new Error('手机号格式不正确'))
|
||||||
@@ -90,7 +76,6 @@ function validateAuthForm(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 重置密码 */
|
|
||||||
export function resetPassword(params: ResetPasswordParams): Promise<void> {
|
export function resetPassword(params: ResetPasswordParams): Promise<void> {
|
||||||
const message = validateAuthForm(
|
const message = validateAuthForm(
|
||||||
params.phone,
|
params.phone,
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
// 案例库接口(真实后端绑定)
|
|
||||||
// 后端: 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 { request, serializeQuery } from '@/utils/request'
|
||||||
import type {
|
import type {
|
||||||
CaseSelectionModel,
|
CaseSelectionModel,
|
||||||
@@ -13,16 +7,6 @@ import type {
|
|||||||
ProjectCaseVo,
|
ProjectCaseVo,
|
||||||
} from '@/types/business/cases'
|
} from '@/types/business/cases'
|
||||||
|
|
||||||
/**
|
|
||||||
* 案例库分页列表(返回列表 + 业务数据统计)。
|
|
||||||
*
|
|
||||||
* <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(
|
export function getCaseList(
|
||||||
bo: ProjectCaseUserBo,
|
bo: ProjectCaseUserBo,
|
||||||
pageNum: number,
|
pageNum: number,
|
||||||
@@ -36,11 +20,6 @@ export function getCaseList(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 案例详情(含项目背景富文本与附件,并回填当前用户是否已收藏)。
|
|
||||||
*
|
|
||||||
* <p>注意:该接口每次调用都会把案例预览量 +1,勿在轮询 / 重试场景滥用。</p>
|
|
||||||
*/
|
|
||||||
export function getCaseDetail(id: string): Promise<ProjectCaseVo | null> {
|
export function getCaseDetail(id: string): Promise<ProjectCaseVo | null> {
|
||||||
return request<ProjectCaseVo | null>({
|
return request<ProjectCaseVo | null>({
|
||||||
url: `/web/projectCase/${id}`,
|
url: `/web/projectCase/${id}`,
|
||||||
@@ -48,11 +27,6 @@ export function getCaseDetail(id: string): Promise<ProjectCaseVo | null> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 案例收藏 / 取消收藏。
|
|
||||||
*
|
|
||||||
* <p>后端为 toggle 语义:已收藏则取消收藏。需登录态,匿名调用会被后端拦截。</p>
|
|
||||||
*/
|
|
||||||
export function collectCase(id: string): Promise<void> {
|
export function collectCase(id: string): Promise<void> {
|
||||||
return request<void>({
|
return request<void>({
|
||||||
url: '/web/projectCase/collect',
|
url: '/web/projectCase/collect',
|
||||||
@@ -61,13 +35,6 @@ export function collectCase(id: string): Promise<void> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 筛选面板选项(业务类型统计 + 行业 + 规模档位)。
|
|
||||||
*
|
|
||||||
* <p>左侧筛选除完成时间外的选项都由本接口一次下发:业务类型与案例总数在
|
|
||||||
* {@code projectCaseStatisticsVo},行业在 {@code businessIndustry},规模在
|
|
||||||
* {@code upperAndLowerLimits}(label 已由后端拼好)。</p>
|
|
||||||
*/
|
|
||||||
export function getCaseCategories(): Promise<CaseSelectionModel> {
|
export function getCaseCategories(): Promise<CaseSelectionModel> {
|
||||||
return request<CaseSelectionModel>({
|
return request<CaseSelectionModel>({
|
||||||
url: '/web/projectCase/categories',
|
url: '/web/projectCase/categories',
|
||||||
@@ -75,12 +42,6 @@ export function getCaseCategories(): Promise<CaseSelectionModel> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 案例浏览王(首页「案例库精选」)。
|
|
||||||
*
|
|
||||||
* <p>不传 type 时返回「各业务类型的浏览王」各一条;传 type 时按案例表
|
|
||||||
* business_type 的原值精确匹配,返回该类型全部案例。</p>
|
|
||||||
*/
|
|
||||||
export function getFeaturedCases(type?: string): Promise<ProjectCaseVo[]> {
|
export function getFeaturedCases(type?: string): Promise<ProjectCaseVo[]> {
|
||||||
return request<ProjectCaseVo[]>({
|
return request<ProjectCaseVo[]>({
|
||||||
url: '/web/projectCase/mostViewed',
|
url: '/web/projectCase/mostViewed',
|
||||||
|
|||||||
@@ -3,15 +3,12 @@ import type { Contract, ContractQuery } from '@/types/business/contract'
|
|||||||
|
|
||||||
// TODO(api): 待接口文档到位后,将以下 mock 替换为 request<T> 调用
|
// TODO(api): 待接口文档到位后,将以下 mock 替换为 request<T> 调用
|
||||||
|
|
||||||
/** 构造 mock 文件信息 */
|
|
||||||
function mockFile(name: string, size: number, uploadTime: string): FileInfo {
|
function mockFile(name: string, size: number, uploadTime: string): FileInfo {
|
||||||
return { name, size, uploadTime, url: '' }
|
return { name, size, uploadTime, url: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 详情 mock 所属合同 id */
|
|
||||||
const DETAIL_ID = 'contract-001'
|
const DETAIL_ID = 'contract-001'
|
||||||
|
|
||||||
/** 合同列表 mock(3 条,覆盖全部签署状态) */
|
|
||||||
const MOCK_CONTRACTS: Contract[] = [
|
const MOCK_CONTRACTS: Contract[] = [
|
||||||
{
|
{
|
||||||
id: 'contract-001',
|
id: 'contract-001',
|
||||||
@@ -56,7 +53,6 @@ const MOCK_CONTRACTS: Contract[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
/** 合同详情 mock */
|
|
||||||
const CONTRACT_DETAIL: Contract = {
|
const CONTRACT_DETAIL: Contract = {
|
||||||
id: DETAIL_ID,
|
id: DETAIL_ID,
|
||||||
contractNo: 'C8K2M202408001',
|
contractNo: 'C8K2M202408001',
|
||||||
@@ -72,7 +68,6 @@ const CONTRACT_DETAIL: Contract = {
|
|||||||
files: [mockFile('项目需求规格说明书.pdf', 2516582, '2024-10-15')],
|
files: [mockFile('项目需求规格说明书.pdf', 2516582, '2024-10-15')],
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 合同列表(keyword 筛选 + 分页) */
|
|
||||||
export function getContractList(query: ContractQuery): Promise<PageResult<Contract>> {
|
export function getContractList(query: ContractQuery): Promise<PageResult<Contract>> {
|
||||||
const filtered = MOCK_CONTRACTS.filter((item) => {
|
const filtered = MOCK_CONTRACTS.filter((item) => {
|
||||||
if (
|
if (
|
||||||
@@ -92,19 +87,16 @@ export function getContractList(query: ContractQuery): Promise<PageResult<Contra
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 合同详情(不存在返回 null) */
|
|
||||||
export function getContractDetail(id: string): Promise<Contract | null> {
|
export function getContractDetail(id: string): Promise<Contract | null> {
|
||||||
if (id === DETAIL_ID) return Promise.resolve({ ...CONTRACT_DETAIL, files: [...CONTRACT_DETAIL.files] })
|
if (id === DETAIL_ID) return Promise.resolve({ ...CONTRACT_DETAIL, files: [...CONTRACT_DETAIL.files] })
|
||||||
const found = MOCK_CONTRACTS.find((item) => item.id === id) ?? null
|
const found = MOCK_CONTRACTS.find((item) => item.id === id) ?? null
|
||||||
return Promise.resolve(found ? { ...found, files: [...found.files] } : null)
|
return Promise.resolve(found ? { ...found, files: [...found.files] } : null)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取签署二维码(返回有效期) */
|
|
||||||
export function getSignQr(_id: string): Promise<{ expiresInSeconds: number }> {
|
export function getSignQr(_id: string): Promise<{ expiresInSeconds: number }> {
|
||||||
return Promise.resolve({ expiresInSeconds: 272 })
|
return Promise.resolve({ expiresInSeconds: 272 })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 刷新签署二维码(返回有效期) */
|
|
||||||
export function refreshSignQr(_id: string): Promise<{ expiresInSeconds: number }> {
|
export function refreshSignQr(_id: string): Promise<{ expiresInSeconds: number }> {
|
||||||
return Promise.resolve({ expiresInSeconds: 272 })
|
return Promise.resolve({ expiresInSeconds: 272 })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,4 @@
|
|||||||
// 需求提报接口(真实后端绑定)
|
|
||||||
// 草稿 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 service, { request } from '@/utils/request'
|
||||||
import type { ApiResponse } from '@/types/api'
|
import type { ApiResponse } from '@/types/api'
|
||||||
import type {
|
import type {
|
||||||
@@ -20,21 +10,9 @@ import type {
|
|||||||
SubmissionBizTypeVo,
|
SubmissionBizTypeVo,
|
||||||
} from '@/types/business/demand'
|
} from '@/types/business/demand'
|
||||||
|
|
||||||
/** 业务类型字典类型(sys_dict_type.dict_type)——仅作提报列表为空时的兜底值域 */
|
|
||||||
export const BIZ_TYPE_DICT = 'vm_biz_type'
|
export const BIZ_TYPE_DICT = 'vm_biz_type'
|
||||||
/** 预算范围字典类型 */
|
|
||||||
export const BUDGET_RANGE_DICT = 'vm_budget_range'
|
export const BUDGET_RANGE_DICT = 'vm_budget_range'
|
||||||
|
|
||||||
/**
|
|
||||||
* 业务类型列表(项目提报表,**免登录**)。
|
|
||||||
*
|
|
||||||
* <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[]> {
|
export function getSubmissionBizTypes(): Promise<SubmissionBizTypeVo[]> {
|
||||||
return request<SubmissionBizTypeVo[]>({
|
return request<SubmissionBizTypeVo[]>({
|
||||||
url: '/web/projectSubmission/list',
|
url: '/web/projectSubmission/list',
|
||||||
@@ -42,14 +20,6 @@ export function getSubmissionBizTypes(): Promise<SubmissionBizTypeVo[]> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* OSS 文件标识 → 可直接访问的地址。
|
|
||||||
*
|
|
||||||
* <p>提报列表的 `coverUrl` / `logoUrl` 下发的是 OSS 主键(如 2099753406859292674)而非地址,
|
|
||||||
* 直接当 `<img src>` 用会请求到本站的错误路径上,必须换链。</p>
|
|
||||||
*
|
|
||||||
* <p>⚠️ 本接口需要登录态;失败时调用方回退前端内置图标,且不阻塞页面。</p>
|
|
||||||
*/
|
|
||||||
export function getOssUrls(ids: (string | number)[]): Promise<OssUrlVo[]> {
|
export function getOssUrls(ids: (string | number)[]): Promise<OssUrlVo[]> {
|
||||||
return request<OssUrlVo[]>({
|
return request<OssUrlVo[]>({
|
||||||
url: `/resource/oss/listByIds/${ids.join(',')}`,
|
url: `/resource/oss/listByIds/${ids.join(',')}`,
|
||||||
@@ -57,12 +27,6 @@ export function getOssUrls(ids: (string | number)[]): Promise<OssUrlVo[]> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 业务类型字典(`vm_biz_type`)。
|
|
||||||
*
|
|
||||||
* <p>⚠️ 仅作**兜底**用:业务类型值域已改为提报列表 {@link getSubmissionBizTypes};
|
|
||||||
* 只有当提报表一条可用记录都没有时,store 才回退到本字典,避免页面无可选项。</p>
|
|
||||||
*/
|
|
||||||
export function getBizTypeDict(): Promise<DictOption[]> {
|
export function getBizTypeDict(): Promise<DictOption[]> {
|
||||||
return request<DictOption[]>({
|
return request<DictOption[]>({
|
||||||
url: `/system/dict/data/type/${BIZ_TYPE_DICT}`,
|
url: `/system/dict/data/type/${BIZ_TYPE_DICT}`,
|
||||||
@@ -70,7 +34,6 @@ export function getBizTypeDict(): Promise<DictOption[]> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 预算范围选项(字典 vm_budget_range) */
|
|
||||||
export function getBudgetRangeDict(): Promise<DictOption[]> {
|
export function getBudgetRangeDict(): Promise<DictOption[]> {
|
||||||
return request<DictOption[]>({
|
return request<DictOption[]>({
|
||||||
url: `/system/dict/data/type/${BUDGET_RANGE_DICT}`,
|
url: `/system/dict/data/type/${BUDGET_RANGE_DICT}`,
|
||||||
@@ -78,14 +41,6 @@ export function getBudgetRangeDict(): Promise<DictOption[]> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 读取当前登录用户的需求提报草稿(无草稿返回 null)。
|
|
||||||
*
|
|
||||||
* <p>⚠️ 后端 `WorkUserOrderDraftController#getDraft` 在"该用户还没有草稿"时会 NPE:
|
|
||||||
* `VmWorkOrderDraftServiceImpl#queryById` 里 `draft` 为 null 仍继续执行 `draft.getId()`,
|
|
||||||
* 实测返回 code=500「发生未知异常」。这里刻意绕过统一 `request`(它会对非 200 弹错误提示),
|
|
||||||
* 直接用原始实例静默兜底:非 200 一律视作"无草稿"。后端补上判空后本段可简化。</p>
|
|
||||||
*/
|
|
||||||
export async function getDraftDemand(): Promise<DemandDraftVo | null> {
|
export async function getDraftDemand(): Promise<DemandDraftVo | null> {
|
||||||
const resp = await service.request<ApiResponse<DemandDraftVo>>({
|
const resp = await service.request<ApiResponse<DemandDraftVo>>({
|
||||||
url: '/web/workOrderDraft/getDraft',
|
url: '/web/workOrderDraft/getDraft',
|
||||||
@@ -95,18 +50,6 @@ export async function getDraftDemand(): Promise<DemandDraftVo | null> {
|
|||||||
return resp.data.data ?? 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> {
|
function toBoPayload(demand: Demand): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
id: demand.id,
|
id: demand.id,
|
||||||
@@ -122,7 +65,6 @@ function toBoPayload(demand: Demand): Record<string, unknown> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 新增/覆盖草稿(后端 insertByBo 按登录用户 upsert) */
|
|
||||||
export function saveDraft(demand: Demand): Promise<void> {
|
export function saveDraft(demand: Demand): Promise<void> {
|
||||||
return request<void>({
|
return request<void>({
|
||||||
url: '/web/workOrderDraft/add',
|
url: '/web/workOrderDraft/add',
|
||||||
@@ -131,7 +73,6 @@ export function saveDraft(demand: Demand): Promise<void> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 清空当前用户的草稿(后端 cleanHistoryDrafts) */
|
|
||||||
export function resetDraft(): Promise<void> {
|
export function resetDraft(): Promise<void> {
|
||||||
return request<void>({
|
return request<void>({
|
||||||
url: '/web/workOrderDraft/reset',
|
url: '/web/workOrderDraft/reset',
|
||||||
@@ -139,7 +80,6 @@ export function resetDraft(): Promise<void> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 上传附件(multipart,后端字段名 file,返回 SysOssVo) */
|
|
||||||
export function uploadDemandFile(file: File): Promise<OssUploadVo> {
|
export function uploadDemandFile(file: File): Promise<OssUploadVo> {
|
||||||
const data = new FormData()
|
const data = new FormData()
|
||||||
data.append('file', file)
|
data.append('file', file)
|
||||||
@@ -151,29 +91,6 @@ export function uploadDemandFile(file: File): Promise<OssUploadVo> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 提交需求(`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> {
|
export function submitDemand(demand: Demand): Promise<void> {
|
||||||
return request<void>({
|
return request<void>({
|
||||||
url: '/web/requirementPool/create',
|
url: '/web/requirementPool/create',
|
||||||
|
|||||||
@@ -17,10 +17,6 @@ import { toBills, toPlanBills, type PlanLike } from '@/utils/billVo'
|
|||||||
import { toPayMethod } from '@/utils/payMeta'
|
import { toPayMethod } from '@/utils/payMeta'
|
||||||
import { request } from '@/utils/request'
|
import { request } from '@/utils/request'
|
||||||
|
|
||||||
// 说明:账单 / 支付相关接口已接后端真数据;仅「发票」与「统计」仍是 mock。
|
|
||||||
// 另有已下线的遗留演示扫码页(pay.vue)用的一组 mock,见文件中部注释。
|
|
||||||
|
|
||||||
/** 遗留演示页用的假账单(真账单走 `GET /web/bill/list`,不读这里) */
|
|
||||||
const MOCK_BILLS: Bill[] = [
|
const MOCK_BILLS: Bill[] = [
|
||||||
{
|
{
|
||||||
id: 'bill-001',
|
id: 'bill-001',
|
||||||
@@ -80,7 +76,6 @@ const MOCK_BILLS: Bill[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
/** 发票 mock(可变:申请开票后状态在前端内存中翻转演示) */
|
|
||||||
const MOCK_INVOICES: Invoice[] = [
|
const MOCK_INVOICES: Invoice[] = [
|
||||||
{
|
{
|
||||||
id: 'invoice-001',
|
id: 'invoice-001',
|
||||||
@@ -113,7 +108,6 @@ const MOCK_INVOICES: Invoice[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
/** 发票抬头 mock(可变,CRUD 在前端内存中演示) */
|
|
||||||
const MOCK_TITLES: InvoiceTitle[] = [
|
const MOCK_TITLES: InvoiceTitle[] = [
|
||||||
{
|
{
|
||||||
id: 'title-001',
|
id: 'title-001',
|
||||||
@@ -131,10 +125,8 @@ const MOCK_TITLES: InvoiceTitle[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
/** 支付进行单(billId → 当前二维码) */
|
|
||||||
const MOCK_PAYMENTS = new Map<string, Payment>()
|
const MOCK_PAYMENTS = new Map<string, Payment>()
|
||||||
|
|
||||||
/** 已产生支付结果的账单(billId → 结果),预置已支付账单 */
|
|
||||||
const MOCK_RESULTS = new Map<string, PaymentResult>()
|
const MOCK_RESULTS = new Map<string, PaymentResult>()
|
||||||
MOCK_RESULTS.set('bill-001', {
|
MOCK_RESULTS.set('bill-001', {
|
||||||
method: 'wechat',
|
method: 'wechat',
|
||||||
@@ -163,18 +155,15 @@ const DEFAULT_STATS: FinanceStats = {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 财务统计(period 为展示周期参数,mock 恒返回原型数据) */
|
|
||||||
export function getFinanceStats(period?: string): Promise<FinanceStats> {
|
export function getFinanceStats(period?: string): Promise<FinanceStats> {
|
||||||
void period
|
void period
|
||||||
return Promise.resolve(structuredClone(DEFAULT_STATS))
|
return Promise.resolve(structuredClone(DEFAULT_STATS))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 真账单 id 一定是纯数字(`vm_bill.id` 是 bigint);非数字只可能来自已下线的演示mock */
|
|
||||||
function isRealBillId(id: string): boolean {
|
function isRealBillId(id: string): boolean {
|
||||||
return /^\d+$/.test(id)
|
return /^\d+$/.test(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 拉取「当前登录用户」的全部账单(`GET /web/bill/list`,不带 ids) */
|
|
||||||
async function fetchOwnBills(): Promise<Bill[]> {
|
async function fetchOwnBills(): Promise<Bill[]> {
|
||||||
const rows = await request<BillVo[]>({ url: '/web/bill/list', method: 'get' })
|
const rows = await request<BillVo[]>({ url: '/web/bill/list', method: 'get' })
|
||||||
return toBills(rows)
|
return toBills(rows)
|
||||||
@@ -190,13 +179,6 @@ function matchBillKeyword(item: Bill, keyword: string): boolean {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 账单列表(筛选 + 分页)。
|
|
||||||
*
|
|
||||||
* <p>⚠️ `GET /web/bill/list` 只按登录用户返回**全量**账单,后端没有 pageNum / keyword /
|
|
||||||
* status 查询参数;单个客户的账单量级是「几条 ~ 几十条」,故筛选与分页放在前端做。
|
|
||||||
* 若日后账单量变大,应给该端点补后端分页参数,这里再切成一次请求。</p>
|
|
||||||
*/
|
|
||||||
export async function getBillList(query: BillQuery): Promise<PageResult<Bill>> {
|
export async function getBillList(query: BillQuery): Promise<PageResult<Bill>> {
|
||||||
const all = await fetchOwnBills()
|
const all = await fetchOwnBills()
|
||||||
const filtered = all.filter((item) => {
|
const filtered = all.filter((item) => {
|
||||||
@@ -207,22 +189,15 @@ export async function getBillList(query: BillQuery): Promise<PageResult<Bill>> {
|
|||||||
return { list: filtered.slice(start, start + query.pageSize), total: filtered.length }
|
return { list: filtered.slice(start, start + query.pageSize), total: filtered.length }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 账单详情(不存在返回 null) */
|
|
||||||
export async function getBillDetail(id: string): Promise<Bill | null> {
|
export async function getBillDetail(id: string): Promise<Bill | null> {
|
||||||
const rows = await request<BillVo[]>({ url: '/web/bill/list', method: 'get', params: { ids: id } })
|
const rows = await request<BillVo[]>({ url: '/web/bill/list', method: 'get', params: { ids: id } })
|
||||||
return toBills(rows)[0] ?? null
|
return toBills(rows)[0] ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
// 遗留演示扫码页(`/finance/bills/:id/pay`,页面 pay.vue)
|
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
// 该页已无任何入口(收银台 checkout.vue 取代了它),但按既定决定保留。
|
|
||||||
// 它用的是下面这几个 mock 函数,与真账单互不干扰:真账单 id 一定是数字,
|
|
||||||
// 假账单 id 形如 `bill-001`,两条路径靠 `isRealBillId` 分流。
|
|
||||||
// 若要下线该页,这一整段(含 MOCK_PAYMENTS / MOCK_RESULTS)可一并删除。
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
/** 创建支付单(遗留页演示用:生成二维码支付信息) */
|
|
||||||
export function createPayment(billId: string, method: PayMethod): Promise<Payment> {
|
export function createPayment(billId: string, method: PayMethod): Promise<Payment> {
|
||||||
const bill = MOCK_BILLS.find((item) => item.id === billId)
|
const bill = MOCK_BILLS.find((item) => item.id === billId)
|
||||||
if (!bill) return Promise.reject(new Error('账单不存在'))
|
if (!bill) return Promise.reject(new Error('账单不存在'))
|
||||||
@@ -236,21 +211,10 @@ export function createPayment(billId: string, method: PayMethod): Promise<Paymen
|
|||||||
return Promise.resolve({ ...payment })
|
return Promise.resolve({ ...payment })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 切换支付方式(遗留页演示用:刷新二维码) */
|
|
||||||
export function switchPaymentMethod(billId: string, method: PayMethod): Promise<Payment> {
|
export function switchPaymentMethod(billId: string, method: PayMethod): Promise<Payment> {
|
||||||
return createPayment(billId, method)
|
return createPayment(billId, method)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询支付结果。
|
|
||||||
*
|
|
||||||
* <p><b>真账单</b>:直接读 `vm_bill` —— 账单被回调置为 PAID 时,`pay_time` /
|
|
||||||
* `transaction_no` / `pay_channel` 已由后端一并回写,这里拼出结果页所需结构;
|
|
||||||
* 未支付则返回 null。</p>
|
|
||||||
*
|
|
||||||
* <p><b>假账单</b>(id 形如 `bill-001`):走下面的遗留演示逻辑。真账单 id 一定是数字,
|
|
||||||
* 故只有已下线的 pay.vue 会命中这条分支。</p>
|
|
||||||
*/
|
|
||||||
export async function getPayResult(billId: string): Promise<PaymentResult | null> {
|
export async function getPayResult(billId: string): Promise<PaymentResult | null> {
|
||||||
if (isRealBillId(billId)) {
|
if (isRealBillId(billId)) {
|
||||||
const bill = await getBillDetail(billId)
|
const bill = await getBillDetail(billId)
|
||||||
@@ -266,7 +230,6 @@ export async function getPayResult(billId: string): Promise<PaymentResult | null
|
|||||||
return getLegacyPayResult(billId)
|
return getLegacyPayResult(billId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 遗留演示:假账单的支付结果(详见上方说明,随 pay.vue 一起可删) */
|
|
||||||
function getLegacyPayResult(billId: string): Promise<PaymentResult | null> {
|
function getLegacyPayResult(billId: string): Promise<PaymentResult | null> {
|
||||||
const existing = MOCK_RESULTS.get(billId)
|
const existing = MOCK_RESULTS.get(billId)
|
||||||
if (existing) return Promise.resolve(structuredClone(existing))
|
if (existing) return Promise.resolve(structuredClone(existing))
|
||||||
@@ -275,7 +238,6 @@ function getLegacyPayResult(billId: string): Promise<PaymentResult | null> {
|
|||||||
const bill = MOCK_BILLS.find((item) => item.id === billId)
|
const bill = MOCK_BILLS.find((item) => item.id === billId)
|
||||||
if (!payment || !bill) return Promise.resolve(null)
|
if (!payment || !bill) return Promise.resolve(null)
|
||||||
|
|
||||||
// 演示:存在支付单即视为完成支付(createPayment 由「完成支付」触发)
|
|
||||||
const tradeNo = bill.tradeNo ?? 'T20260819153035678K2M001'
|
const tradeNo = bill.tradeNo ?? 'T20260819153035678K2M001'
|
||||||
if (bill.status !== '已支付') {
|
if (bill.status !== '已支付') {
|
||||||
bill.status = '已支付'
|
bill.status = '已支付'
|
||||||
@@ -302,7 +264,6 @@ function matchInvoiceKeyword(item: Invoice, keyword: string): boolean {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 发票列表(筛选 + 分页) */
|
|
||||||
export function getInvoiceList(query: InvoiceQuery): Promise<PageResult<Invoice>> {
|
export function getInvoiceList(query: InvoiceQuery): Promise<PageResult<Invoice>> {
|
||||||
const filtered = MOCK_INVOICES.filter((item) => {
|
const filtered = MOCK_INVOICES.filter((item) => {
|
||||||
if (query.status !== 'all' && item.status !== query.status) return false
|
if (query.status !== 'all' && item.status !== query.status) return false
|
||||||
@@ -315,7 +276,6 @@ export function getInvoiceList(query: InvoiceQuery): Promise<PageResult<Invoice>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 申请开票(演示:状态置为开票中),返回最新发票 */
|
|
||||||
export function applyInvoice(id: string): Promise<Invoice> {
|
export function applyInvoice(id: string): Promise<Invoice> {
|
||||||
const found = MOCK_INVOICES.find((item) => item.id === id)
|
const found = MOCK_INVOICES.find((item) => item.id === id)
|
||||||
if (!found) return Promise.reject(new Error('发票记录不存在'))
|
if (!found) return Promise.reject(new Error('发票记录不存在'))
|
||||||
@@ -323,12 +283,10 @@ export function applyInvoice(id: string): Promise<Invoice> {
|
|||||||
return Promise.resolve({ ...found })
|
return Promise.resolve({ ...found })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 发票抬头列表 */
|
|
||||||
export function getInvoiceTitles(): Promise<InvoiceTitle[]> {
|
export function getInvoiceTitles(): Promise<InvoiceTitle[]> {
|
||||||
return Promise.resolve(MOCK_TITLES.map((item) => ({ ...item })))
|
return Promise.resolve(MOCK_TITLES.map((item) => ({ ...item })))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 新增发票抬头 */
|
|
||||||
export function createInvoiceTitle(payload: InvoiceTitlePayload): Promise<InvoiceTitle> {
|
export function createInvoiceTitle(payload: InvoiceTitlePayload): Promise<InvoiceTitle> {
|
||||||
const created: InvoiceTitle = {
|
const created: InvoiceTitle = {
|
||||||
id: `title-${Date.now()}`,
|
id: `title-${Date.now()}`,
|
||||||
@@ -339,7 +297,6 @@ export function createInvoiceTitle(payload: InvoiceTitlePayload): Promise<Invoic
|
|||||||
return Promise.resolve({ ...created })
|
return Promise.resolve({ ...created })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 编辑发票抬头 */
|
|
||||||
export function updateInvoiceTitle(id: string, payload: InvoiceTitlePayload): Promise<InvoiceTitle> {
|
export function updateInvoiceTitle(id: string, payload: InvoiceTitlePayload): Promise<InvoiceTitle> {
|
||||||
const found = MOCK_TITLES.find((item) => item.id === id)
|
const found = MOCK_TITLES.find((item) => item.id === id)
|
||||||
if (!found) return Promise.reject(new Error('抬头不存在'))
|
if (!found) return Promise.reject(new Error('抬头不存在'))
|
||||||
@@ -349,7 +306,6 @@ export function updateInvoiceTitle(id: string, payload: InvoiceTitlePayload): Pr
|
|||||||
return Promise.resolve({ ...found })
|
return Promise.resolve({ ...found })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 删除发票抬头 */
|
|
||||||
export function deleteInvoiceTitle(id: string): Promise<void> {
|
export function deleteInvoiceTitle(id: string): Promise<void> {
|
||||||
const index = MOCK_TITLES.findIndex((item) => item.id === id)
|
const index = MOCK_TITLES.findIndex((item) => item.id === id)
|
||||||
if (index < 0) return Promise.reject(new Error('抬头不存在'))
|
if (index < 0) return Promise.reject(new Error('抬头不存在'))
|
||||||
@@ -357,7 +313,6 @@ export function deleteInvoiceTitle(id: string): Promise<void> {
|
|||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 设为默认抬头(唯一默认) */
|
|
||||||
export function setDefaultTitle(id: string): Promise<InvoiceTitle[]> {
|
export function setDefaultTitle(id: string): Promise<InvoiceTitle[]> {
|
||||||
for (const item of MOCK_TITLES) {
|
for (const item of MOCK_TITLES) {
|
||||||
item.isDefault = item.id === id
|
item.isDefault = item.id === id
|
||||||
@@ -365,30 +320,13 @@ export function setDefaultTitle(id: string): Promise<InvoiceTitle[]> {
|
|||||||
return Promise.resolve(MOCK_TITLES.map((item) => ({ ...item })))
|
return Promise.resolve(MOCK_TITLES.map((item) => ({ ...item })))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 导出报表(演示:稍后由页面提示成功) */
|
|
||||||
export function exportReport(): Promise<void> {
|
export function exportReport(): Promise<void> {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
// 收银台(确认支付)——以下为**真实后端接口**,不是 mock
|
|
||||||
// 后端类:vermis-modules/…/controller/pc/pay/WebScanPayController,类级 @SaIgnore(匿名可调)
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
/**
|
|
||||||
* 取收银台「订单明细」——**复用项目详情「合同财务」那个现成接口**,不另开端点。
|
|
||||||
*
|
|
||||||
* <p>`GET /web/requirementPool/get/finance/{requirementId}`(`ProjectRequirementUserController`)
|
|
||||||
* 的 `{id}` 是**需求 id**;后端把「需求 + 项目」拼成一条联合结构(`loadProject` 用
|
|
||||||
* `vm_rd_project.requirement_id` 反查项目),再把该项目的付款计划随 `billPlans` 一并下发。</p>
|
|
||||||
*
|
|
||||||
* <p>本栈支付口径是 `vm_bill_plan.id`(`vm_finance_payment.bill_id` 实测存的就是计划 id),
|
|
||||||
* 故订单明细直接取这份 `billPlans`,只保留**本次要付的期**:
|
|
||||||
* URL 带 `ids`(期次 id)时按期次过滤,不带则取状态为「待支付」的期。</p>
|
|
||||||
*
|
|
||||||
* @param requirementId 需求 id(项目详情路由 `:id`)
|
|
||||||
* @param ids 期次 id(`vm_bill_plan.id`)列表,可选,用于精确指定付哪一期
|
|
||||||
*/
|
|
||||||
export async function getCheckoutBills(requirementId: string, ids: string[] = []): Promise<Bill[]> {
|
export async function getCheckoutBills(requirementId: string, ids: string[] = []): Promise<Bill[]> {
|
||||||
if (!/^\d+$/.test(requirementId)) return []
|
if (!/^\d+$/.test(requirementId)) return []
|
||||||
const vo = await request<{ billPlans?: PlanLike[] }>({
|
const vo = await request<{ billPlans?: PlanLike[] }>({
|
||||||
@@ -401,12 +339,6 @@ export async function getCheckoutBills(requirementId: string, ids: string[] = []
|
|||||||
: all.filter((bill) => bill.status === '待支付')
|
: all.filter((bill) => bill.status === '待支付')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 支付结果(收银台「我已完成支付」→ 结果页)。
|
|
||||||
*
|
|
||||||
* <p>口径同 {@link getCheckoutBills}:按**需求 id + 期次 id** 从 `billPlans` 里取出该期,
|
|
||||||
* 支付单状态为 `SUCCESS` 才算已支付——计划本身的 status 可能还没被回调推到 DONE。</p>
|
|
||||||
*/
|
|
||||||
export async function getPlanPayResult(requirementId: string, planId: string): Promise<PaymentResult | null> {
|
export async function getPlanPayResult(requirementId: string, planId: string): Promise<PaymentResult | null> {
|
||||||
const bills = await getCheckoutBills(requirementId, [planId])
|
const bills = await getCheckoutBills(requirementId, [planId])
|
||||||
const bill = bills[0]
|
const bill = bills[0]
|
||||||
@@ -420,17 +352,6 @@ export async function getPlanPayResult(requirementId: string, planId: string): P
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 微信 Native 下单(PC 网站扫码):`POST /pay/web/wechat/native`,`@SaIgnore`。
|
|
||||||
*
|
|
||||||
* @param orderNo 商户订单号(前端生成,见 `utils/payMeta.ts#createMerchantOrderNo`)
|
|
||||||
* @param amount 支付金额,单位 **元**(后端内部 `×100` 转「分」提交微信)
|
|
||||||
* @param billIds 本笔支付覆盖的账单 id(合并支付传多张)。
|
|
||||||
* 后端落 `vm_finance_payment`(status=CREATED)时把它序列化进
|
|
||||||
* `request_payload`,支付成功回调据此为**每张账单**落一行
|
|
||||||
* `vm_finance_payment_bill`,缺了它回调分摊落库会跳过。
|
|
||||||
* @returns `codeUrl`(形如 `weixin://wxpay/bizpayurl?pr=xxx`),前端渲染成二维码供扫
|
|
||||||
*/
|
|
||||||
export function createWechatNativeOrder(
|
export function createWechatNativeOrder(
|
||||||
orderNo: string,
|
orderNo: string,
|
||||||
amount: number,
|
amount: number,
|
||||||
@@ -443,16 +364,6 @@ export function createWechatNativeOrder(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 支付宝电脑网站支付下单:`POST /pay/web/alipay/page`,`@SaIgnore`。
|
|
||||||
*
|
|
||||||
* <p>入参对应后端 `IFinancePaymentBo`。其 `outTradeNo` / `status` 上的 `@NotBlank`
|
|
||||||
* 只声明在 AddGroup / EditGroup 分组上,而控制器用的是默认分组 ⇒ 只传
|
|
||||||
* `outTradeNo` + `amount` 即可(`payChannel` / `status` 由后端自己补),不会校验失败。</p>
|
|
||||||
*
|
|
||||||
* @returns `formBody` —— 后端走 `pageExecute(request, "GET")`,实测内容是
|
|
||||||
* **支付宝收银台跳转地址**(`https://…/gateway.do?…`)而不是 HTML 表单;前端两种形态都兜住。
|
|
||||||
*/
|
|
||||||
export function createAlipayPageOrder(
|
export function createAlipayPageOrder(
|
||||||
outTradeNo: string,
|
outTradeNo: string,
|
||||||
amount: number,
|
amount: number,
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
// 首页接口(真实后端绑定)
|
|
||||||
// 后端: 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 { request } from '@/utils/request'
|
||||||
import type {
|
import type {
|
||||||
AgreementFormVo,
|
AgreementFormVo,
|
||||||
@@ -17,7 +10,6 @@ import type {
|
|||||||
ProjectSubmissionVo,
|
ProjectSubmissionVo,
|
||||||
} from '@/types/business/home'
|
} from '@/types/business/home'
|
||||||
|
|
||||||
/** 项目状态统计 */
|
|
||||||
export function getProjectStatusCount(): Promise<ProjectStatusCount> {
|
export function getProjectStatusCount(): Promise<ProjectStatusCount> {
|
||||||
return request<ProjectStatusCount>({
|
return request<ProjectStatusCount>({
|
||||||
url: '/web/home-page/project-status-count',
|
url: '/web/home-page/project-status-count',
|
||||||
@@ -25,7 +17,6 @@ export function getProjectStatusCount(): Promise<ProjectStatusCount> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 业务服务类型列表 */
|
|
||||||
export function getServiceTypeList(): Promise<ProjectSubmissionVo[]> {
|
export function getServiceTypeList(): Promise<ProjectSubmissionVo[]> {
|
||||||
return request<ProjectSubmissionVo[]>({
|
return request<ProjectSubmissionVo[]>({
|
||||||
url: '/web/home-page/service-type-list',
|
url: '/web/home-page/service-type-list',
|
||||||
@@ -33,7 +24,6 @@ export function getServiceTypeList(): Promise<ProjectSubmissionVo[]> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 业务类型详情(聚合 VO:projectSubmission / projectServiceType / projectSubmissionAttachment / projectSubmissionCatalog) */
|
|
||||||
export function getServiceTypeDetail(id: number | string): Promise<ProjectSubmissionInfo> {
|
export function getServiceTypeDetail(id: number | string): Promise<ProjectSubmissionInfo> {
|
||||||
return request<ProjectSubmissionInfo>({
|
return request<ProjectSubmissionInfo>({
|
||||||
url: `/web/home-page/service-type-detail/${id}`,
|
url: `/web/home-page/service-type-detail/${id}`,
|
||||||
@@ -41,7 +31,6 @@ export function getServiceTypeDetail(id: number | string): Promise<ProjectSubmis
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 轮播图列表 */
|
|
||||||
export function getCarouselList(): Promise<BannerVo[]> {
|
export function getCarouselList(): Promise<BannerVo[]> {
|
||||||
return request<BannerVo[]>({
|
return request<BannerVo[]>({
|
||||||
url: '/web/home-page/carousel-list',
|
url: '/web/home-page/carousel-list',
|
||||||
@@ -49,7 +38,6 @@ export function getCarouselList(): Promise<BannerVo[]> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 平台介绍(富文本 HTML) */
|
|
||||||
export function getPlatformIntro(): Promise<AgreementFormVo> {
|
export function getPlatformIntro(): Promise<AgreementFormVo> {
|
||||||
return request<AgreementFormVo>({
|
return request<AgreementFormVo>({
|
||||||
url: '/web/home-page/platform-intro',
|
url: '/web/home-page/platform-intro',
|
||||||
@@ -57,7 +45,6 @@ export function getPlatformIntro(): Promise<AgreementFormVo> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 联系信息(富文本 HTML) */
|
|
||||||
export function getContactInfo(): Promise<AgreementFormVo> {
|
export function getContactInfo(): Promise<AgreementFormVo> {
|
||||||
return request<AgreementFormVo>({
|
return request<AgreementFormVo>({
|
||||||
url: '/web/home-page/contact-info',
|
url: '/web/home-page/contact-info',
|
||||||
@@ -65,14 +52,6 @@ export function getContactInfo(): Promise<AgreementFormVo> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 首页「案例库精选」接口(/web/projectCase/mostViewed)按资源归位到 `@/api/cases` 的 getFeaturedCases。
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询用户门户入口列表
|
|
||||||
*
|
|
||||||
* <p>登录态返回该用户个性化后的分组结果;匿名返回固定目录。
|
|
||||||
* 两组数据由 groupType 区分(1 = 快捷、0 = 其他)。</p>
|
|
||||||
*/
|
|
||||||
export function getPortalEntryList(): Promise<PortalEntryVo[]> {
|
export function getPortalEntryList(): Promise<PortalEntryVo[]> {
|
||||||
return request<PortalEntryVo[]>({
|
return request<PortalEntryVo[]>({
|
||||||
url: '/vermins/portalEntryUser/list',
|
url: '/vermins/portalEntryUser/list',
|
||||||
@@ -80,14 +59,6 @@ export function getPortalEntryList(): Promise<PortalEntryVo[]> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 快捷 / 其他入口互转
|
|
||||||
*
|
|
||||||
* <p>后端 add 的入参是 @RequestParam,必须走 query 而非 body。</p>
|
|
||||||
*
|
|
||||||
* @param id 入口 id
|
|
||||||
* @param type 目标分组:1 = 快捷、0 = 其他
|
|
||||||
*/
|
|
||||||
export function switchPortalEntryGroup(id: number, type: PortalEntryGroup): Promise<void> {
|
export function switchPortalEntryGroup(id: number, type: PortalEntryGroup): Promise<void> {
|
||||||
return request<void>({
|
return request<void>({
|
||||||
url: '/vermins/portalEntryUser/add',
|
url: '/vermins/portalEntryUser/add',
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type { Message, MessageQuery } from '@/types/business/message'
|
|||||||
|
|
||||||
// TODO(api): 待接口文档到位后,将以下 mock 替换为 request<T> 调用
|
// TODO(api): 待接口文档到位后,将以下 mock 替换为 request<T> 调用
|
||||||
|
|
||||||
/** 消息 mock(可变,已读状态在前端内存中翻转演示) */
|
|
||||||
const MOCK_MESSAGES: Message[] = [
|
const MOCK_MESSAGES: Message[] = [
|
||||||
{
|
{
|
||||||
id: 'msg-001',
|
id: 'msg-001',
|
||||||
@@ -107,7 +106,6 @@ function applyFilters(query: MessageQuery): Message[] {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 消息列表(筛选 + 分页) */
|
|
||||||
export function getMessageList(query: MessageQuery): Promise<PageResult<Message>> {
|
export function getMessageList(query: MessageQuery): Promise<PageResult<Message>> {
|
||||||
const filtered = applyFilters(query)
|
const filtered = applyFilters(query)
|
||||||
const start = (query.page - 1) * query.pageSize
|
const start = (query.page - 1) * query.pageSize
|
||||||
@@ -117,13 +115,11 @@ export function getMessageList(query: MessageQuery): Promise<PageResult<Message>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 消息详情(不存在返回 null) */
|
|
||||||
export function getMessageDetail(id: string): Promise<Message | null> {
|
export function getMessageDetail(id: string): Promise<Message | null> {
|
||||||
const found = MOCK_MESSAGES.find((item) => item.id === id) ?? null
|
const found = MOCK_MESSAGES.find((item) => item.id === id) ?? null
|
||||||
return Promise.resolve(found ? { ...found } : null)
|
return Promise.resolve(found ? { ...found } : null)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 全部标记已读 */
|
|
||||||
export function markAllRead(): Promise<void> {
|
export function markAllRead(): Promise<void> {
|
||||||
for (const item of MOCK_MESSAGES) {
|
for (const item of MOCK_MESSAGES) {
|
||||||
item.isRead = true
|
item.isRead = true
|
||||||
@@ -131,14 +127,12 @@ export function markAllRead(): Promise<void> {
|
|||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 单条标记已读 */
|
|
||||||
export function markRead(id: string): Promise<void> {
|
export function markRead(id: string): Promise<void> {
|
||||||
const found = MOCK_MESSAGES.find((item) => item.id === id)
|
const found = MOCK_MESSAGES.find((item) => item.id === id)
|
||||||
if (found) found.isRead = true
|
if (found) found.isRead = true
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 各分类消息总数(tab 角标) */
|
|
||||||
export function getTabCounts(): Promise<{ all: number; 在线客服: number; 状态通知: number; 系统通知: number }> {
|
export function getTabCounts(): Promise<{ all: number; 在线客服: number; 状态通知: number; 系统通知: number }> {
|
||||||
const counts = { all: MOCK_MESSAGES.length, 在线客服: 0, 状态通知: 0, 系统通知: 0 }
|
const counts = { all: MOCK_MESSAGES.length, 在线客服: 0, 状态通知: 0, 系统通知: 0 }
|
||||||
for (const item of MOCK_MESSAGES) {
|
for (const item of MOCK_MESSAGES) {
|
||||||
@@ -147,7 +141,6 @@ export function getTabCounts(): Promise<{ all: number; 在线客服: number; 状
|
|||||||
return Promise.resolve(counts)
|
return Promise.resolve(counts)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 未读消息总数(头部角标) */
|
|
||||||
export function getUnreadCount(): Promise<number> {
|
export function getUnreadCount(): Promise<number> {
|
||||||
return Promise.resolve(MOCK_MESSAGES.filter((item) => !item.isRead).length)
|
return Promise.resolve(MOCK_MESSAGES.filter((item) => !item.isRead).length)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,5 @@
|
|||||||
import service from '@/utils/request'
|
import service from '@/utils/request'
|
||||||
|
|
||||||
/**
|
|
||||||
* 同源预览:后端从 OSS 读取字节后以 `inline` 方式返回,绕开阿里云 OSS 桶「强制下载」配置。
|
|
||||||
*
|
|
||||||
* <p>⚠️ 该端点返回的是<b>裸字节</b>(非 `R.ok()` 包装的 `ApiResponse<T>`),故不走统一的
|
|
||||||
* `request<T>` 解包逻辑,直接用底层 axios 实例拿 `blob`:请求拦截器已自动带上
|
|
||||||
* `Authorization` 与 `clientid` 头,后端据此放行(端点需登录)。</p>
|
|
||||||
*
|
|
||||||
* <p>用 blob 而非 `window.open(url)` 的关键:blob URL 自身不带 `Content-Disposition`,
|
|
||||||
* 天然绕开 OSS 的强制下载;且全程同源,不受 OSS 跨域 / 强制下载限制。</p>
|
|
||||||
*/
|
|
||||||
export async function fetchOssPreviewBlob(ossId: string | number): Promise<Blob> {
|
export async function fetchOssPreviewBlob(ossId: string | number): Promise<Blob> {
|
||||||
const resp = await service.get(`/web/workOrderDraft/preview/${ossId}`, {
|
const resp = await service.get(`/web/workOrderDraft/preview/${ossId}`, {
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import type {
|
|||||||
|
|
||||||
// TODO(api): 待接口文档到位后,将以下 mock 替换为 request<T> 调用(页面 → store → api 数据流不变)
|
// TODO(api): 待接口文档到位后,将以下 mock 替换为 request<T> 调用(页面 → store → api 数据流不变)
|
||||||
|
|
||||||
/** 账号资料 mock(可变,昵称/绑定等操作在前端内存中演示) */
|
|
||||||
const MOCK_PROFILE: Profile = {
|
const MOCK_PROFILE: Profile = {
|
||||||
nickname: '王永志',
|
nickname: '王永志',
|
||||||
phone: '186****618',
|
phone: '186****618',
|
||||||
@@ -22,57 +21,48 @@ const MOCK_PROFILE: Profile = {
|
|||||||
passwordModifiedAt: '15天前',
|
passwordModifiedAt: '15天前',
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取账号资料 */
|
|
||||||
export function getProfile(): Promise<Profile> {
|
export function getProfile(): Promise<Profile> {
|
||||||
return Promise.resolve({ ...MOCK_PROFILE })
|
return Promise.resolve({ ...MOCK_PROFILE })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 修改昵称 */
|
|
||||||
export function updateNickname(name: string): Promise<Profile> {
|
export function updateNickname(name: string): Promise<Profile> {
|
||||||
MOCK_PROFILE.nickname = name
|
MOCK_PROFILE.nickname = name
|
||||||
return Promise.resolve({ ...MOCK_PROFILE })
|
return Promise.resolve({ ...MOCK_PROFILE })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 修改密码(验证码 + 新密码) */
|
|
||||||
export function changePassword(params: ChangePasswordParams): Promise<void> {
|
export function changePassword(params: ChangePasswordParams): Promise<void> {
|
||||||
MOCK_PROFILE.passwordModifiedAt = '刚刚'
|
MOCK_PROFILE.passwordModifiedAt = '刚刚'
|
||||||
void params
|
void params
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 绑定/换绑手机号 */
|
|
||||||
export function bindPhone(phone: string, code: string): Promise<Profile> {
|
export function bindPhone(phone: string, code: string): Promise<Profile> {
|
||||||
void code
|
void code
|
||||||
MOCK_PROFILE.phone = phone
|
MOCK_PROFILE.phone = phone
|
||||||
return Promise.resolve({ ...MOCK_PROFILE })
|
return Promise.resolve({ ...MOCK_PROFILE })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 绑定/换绑邮箱 */
|
|
||||||
export function bindEmail(email: string, code: string): Promise<Profile> {
|
export function bindEmail(email: string, code: string): Promise<Profile> {
|
||||||
void code
|
void code
|
||||||
MOCK_PROFILE.email = email
|
MOCK_PROFILE.email = email
|
||||||
return Promise.resolve({ ...MOCK_PROFILE })
|
return Promise.resolve({ ...MOCK_PROFILE })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 取消绑定邮箱 */
|
|
||||||
export function unbindEmail(): Promise<Profile> {
|
export function unbindEmail(): Promise<Profile> {
|
||||||
MOCK_PROFILE.email = ''
|
MOCK_PROFILE.email = ''
|
||||||
return Promise.resolve({ ...MOCK_PROFILE })
|
return Promise.resolve({ ...MOCK_PROFILE })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 注销前安全身份验证 */
|
|
||||||
export function verifyIdentity(phone: string, code: string): Promise<void> {
|
export function verifyIdentity(phone: string, code: string): Promise<void> {
|
||||||
void phone
|
void phone
|
||||||
void code
|
void code
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 注销账号 */
|
|
||||||
export function deleteAccount(): Promise<void> {
|
export function deleteAccount(): Promise<void> {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 收货地址 mock(isDefault 混合) */
|
|
||||||
const MOCK_ADDRESSES: Address[] = [
|
const MOCK_ADDRESSES: Address[] = [
|
||||||
{
|
{
|
||||||
id: 'addr-1',
|
id: 'addr-1',
|
||||||
@@ -116,12 +106,10 @@ function paginate<T>(list: T[], query: ListQuery): PageResult<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 收货地址列表(分页) */
|
|
||||||
export function getAddresses(query: ListQuery): Promise<PageResult<Address>> {
|
export function getAddresses(query: ListQuery): Promise<PageResult<Address>> {
|
||||||
return Promise.resolve(paginate(MOCK_ADDRESSES, query))
|
return Promise.resolve(paginate(MOCK_ADDRESSES, query))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 新增收货地址 */
|
|
||||||
export function createAddress(input: Omit<Address, 'id' | 'isDefault'>): Promise<Address> {
|
export function createAddress(input: Omit<Address, 'id' | 'isDefault'>): Promise<Address> {
|
||||||
const created: Address = {
|
const created: Address = {
|
||||||
...input,
|
...input,
|
||||||
@@ -132,7 +120,6 @@ export function createAddress(input: Omit<Address, 'id' | 'isDefault'>): Promise
|
|||||||
return Promise.resolve({ ...created })
|
return Promise.resolve({ ...created })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 编辑收货地址 */
|
|
||||||
export function updateAddress(id: string, input: Omit<Address, 'id' | 'isDefault'>): Promise<Address> {
|
export function updateAddress(id: string, input: Omit<Address, 'id' | 'isDefault'>): Promise<Address> {
|
||||||
const found = MOCK_ADDRESSES.find((item) => item.id === id)
|
const found = MOCK_ADDRESSES.find((item) => item.id === id)
|
||||||
if (!found) return Promise.reject(new Error('地址不存在'))
|
if (!found) return Promise.reject(new Error('地址不存在'))
|
||||||
@@ -140,14 +127,12 @@ export function updateAddress(id: string, input: Omit<Address, 'id' | 'isDefault
|
|||||||
return Promise.resolve({ ...found })
|
return Promise.resolve({ ...found })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 删除收货地址 */
|
|
||||||
export function deleteAddress(id: string): Promise<void> {
|
export function deleteAddress(id: string): Promise<void> {
|
||||||
const index = MOCK_ADDRESSES.findIndex((item) => item.id === id)
|
const index = MOCK_ADDRESSES.findIndex((item) => item.id === id)
|
||||||
if (index >= 0) MOCK_ADDRESSES.splice(index, 1)
|
if (index >= 0) MOCK_ADDRESSES.splice(index, 1)
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 设为默认地址 */
|
|
||||||
export function setDefaultAddress(id: string): Promise<Address[]> {
|
export function setDefaultAddress(id: string): Promise<Address[]> {
|
||||||
for (const item of MOCK_ADDRESSES) {
|
for (const item of MOCK_ADDRESSES) {
|
||||||
item.isDefault = item.id === id
|
item.isDefault = item.id === id
|
||||||
@@ -155,7 +140,6 @@ export function setDefaultAddress(id: string): Promise<Address[]> {
|
|||||||
return Promise.resolve(MOCK_ADDRESSES.map((item) => ({ ...item })))
|
return Promise.resolve(MOCK_ADDRESSES.map((item) => ({ ...item })))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 我的收藏 mock(4 条已收藏案例,字段与案例库原型一致) */
|
|
||||||
const MOCK_FAVORITES: Case[] = [
|
const MOCK_FAVORITES: Case[] = [
|
||||||
{
|
{
|
||||||
id: 'fav-1',
|
id: 'fav-1',
|
||||||
@@ -243,7 +227,6 @@ const MOCK_FAVORITES: Case[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
/** 我的收藏(关键字/分类筛选 + 分页) */
|
|
||||||
export function getFavorites(query: FavoritesQuery): Promise<PageResult<Case>> {
|
export function getFavorites(query: FavoritesQuery): Promise<PageResult<Case>> {
|
||||||
const filtered = MOCK_FAVORITES.filter((item) => {
|
const filtered = MOCK_FAVORITES.filter((item) => {
|
||||||
if (query.category !== 'all' && item.category !== query.category) return false
|
if (query.category !== 'all' && item.category !== query.category) return false
|
||||||
@@ -257,7 +240,6 @@ export function getFavorites(query: FavoritesQuery): Promise<PageResult<Case>> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 收藏分类计数(分类快捷按钮角标) */
|
|
||||||
export function getCaseCounts(): Promise<Partial<Record<BusinessType, number>>> {
|
export function getCaseCounts(): Promise<Partial<Record<BusinessType, number>>> {
|
||||||
const counts: Partial<Record<BusinessType, number>> = {
|
const counts: Partial<Record<BusinessType, number>> = {
|
||||||
internet: 1,
|
internet: 1,
|
||||||
@@ -268,7 +250,6 @@ export function getCaseCounts(): Promise<Partial<Record<BusinessType, number>>>
|
|||||||
return Promise.resolve(counts)
|
return Promise.resolve(counts)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 资质 mock(状态混合:已认证/审核中/未通过) */
|
|
||||||
const MOCK_QUALIFICATIONS: Qualification[] = [
|
const MOCK_QUALIFICATIONS: Qualification[] = [
|
||||||
{
|
{
|
||||||
id: 'qual-1',
|
id: 'qual-1',
|
||||||
@@ -324,12 +305,10 @@ const MOCK_QUALIFICATIONS: Qualification[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
/** 资质列表(分页) */
|
|
||||||
export function getQualifications(query: ListQuery): Promise<PageResult<Qualification>> {
|
export function getQualifications(query: ListQuery): Promise<PageResult<Qualification>> {
|
||||||
return Promise.resolve(paginate(MOCK_QUALIFICATIONS, query))
|
return Promise.resolve(paginate(MOCK_QUALIFICATIONS, query))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 新增资质(提交后进入审核中) */
|
|
||||||
export function createQualification(input: QualificationInput): Promise<Qualification> {
|
export function createQualification(input: QualificationInput): Promise<Qualification> {
|
||||||
const created: Qualification = {
|
const created: Qualification = {
|
||||||
...input,
|
...input,
|
||||||
@@ -340,7 +319,6 @@ export function createQualification(input: QualificationInput): Promise<Qualific
|
|||||||
return Promise.resolve({ ...created })
|
return Promise.resolve({ ...created })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 编辑资质(未通过重新提交后回到审核中) */
|
|
||||||
export function updateQualification(id: string, input: QualificationInput): Promise<Qualification> {
|
export function updateQualification(id: string, input: QualificationInput): Promise<Qualification> {
|
||||||
const found = MOCK_QUALIFICATIONS.find((item) => item.id === id)
|
const found = MOCK_QUALIFICATIONS.find((item) => item.id === id)
|
||||||
if (!found) return Promise.reject(new Error('资质不存在'))
|
if (!found) return Promise.reject(new Error('资质不存在'))
|
||||||
@@ -355,20 +333,17 @@ export function updateQualification(id: string, input: QualificationInput): Prom
|
|||||||
return Promise.resolve({ ...found })
|
return Promise.resolve({ ...found })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 删除资质 */
|
|
||||||
export function deleteQualification(id: string): Promise<void> {
|
export function deleteQualification(id: string): Promise<void> {
|
||||||
const index = MOCK_QUALIFICATIONS.findIndex((item) => item.id === id)
|
const index = MOCK_QUALIFICATIONS.findIndex((item) => item.id === id)
|
||||||
if (index >= 0) MOCK_QUALIFICATIONS.splice(index, 1)
|
if (index >= 0) MOCK_QUALIFICATIONS.splice(index, 1)
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 提交意见反馈 */
|
|
||||||
export function submitFeedback(feedback: Feedback): Promise<void> {
|
export function submitFeedback(feedback: Feedback): Promise<void> {
|
||||||
void feedback
|
void feedback
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 帮助中心 FAQ */
|
|
||||||
export function getHelpFaqs(): Promise<HelpFaqGroup[]> {
|
export function getHelpFaqs(): Promise<HelpFaqGroup[]> {
|
||||||
return Promise.resolve([
|
return Promise.resolve([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,55 +12,29 @@ import { request, serializeQuery } from '@/utils/request'
|
|||||||
import { formatDate } from '@/utils/format'
|
import { formatDate } from '@/utils/format'
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// 列表:真实接口
|
|
||||||
// GET /web/requirementPool/list → ProjectRequirementUserController#list
|
// GET /web/requirementPool/list → ProjectRequirementUserController#list
|
||||||
// → ProjectRequirementServiceImpl#queryUserPageList
|
// → ProjectRequirementServiceImpl#queryUserPageList
|
||||||
// 入参 RdProjectBo + PageQuery;出参 ProjectRequirementAndCountVo
|
|
||||||
// { requirementPage: { total, rows: RdProjectVo[] }, requirementStatistics: null }
|
// { requirementPage: { total, rows: RdProjectVo[] }, requirementStatistics: null }
|
||||||
// 鉴权:需登录态(匿名 401);每请求带 clientid 头(见 utils/request.ts 拦截器)。
|
|
||||||
// 筛选参数实测(tools/probe_project_api.py):
|
|
||||||
// key → like projectName OR projectNo OR requirementDesc
|
// key → like projectName OR projectNo OR requirementDesc
|
||||||
// businessTypes → IN business_type(**多选**;值是提报表主键 id,不是类型名)
|
|
||||||
// status → eq status(码值,见 RdProjectStatus)
|
|
||||||
// beginTime/endTime → between create_time(不是 submit_time!)
|
|
||||||
// pageNum/pageSize → PageQuery
|
// pageNum/pageSize → PageQuery
|
||||||
// ⚠️ 主表已改为**需求表** vm_project_requirement(LEFT JOIN 项目表),未立项的需求也会出现;
|
|
||||||
// 未立项行 progress 为 null(显示「—」)、status 回落需求表中的 INITIATING。
|
|
||||||
// ⚠️ 列表行的 businessType 是 SQL 里 JOIN 提报表**翻译后的中文名称**(如「互联网软件」),
|
|
||||||
// 与筛选参数 businessType(提报表 id)**不是同一口径**,渲染时不能用 typeCounts 反查。
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// 业务类型筛选条件:真实接口
|
|
||||||
// GET /web/requirementPool/getFilterConditions
|
// GET /web/requirementPool/getFilterConditions
|
||||||
// → ProjectRequirementUserController#getFilterConditions
|
// → ProjectRequirementUserController#getFilterConditions
|
||||||
// → ProjectRequirementServiceImpl#getFilterConditions
|
// → ProjectRequirementServiceImpl#getFilterConditions
|
||||||
// 出参 ProjectRequirementStatisticsVo:
|
|
||||||
// { total: 该用户需求总数,
|
|
||||||
// businessTypeCounts: [{ businessTypeId, businessTypeLabel, count }] }
|
// businessTypeCounts: [{ businessTypeId, businessTypeLabel, count }] }
|
||||||
// 口径:**全量、不分页**,所以件数不会随 pageNum/pageSize 变化,适合当筛选栏的固定值域;
|
|
||||||
// businessTypeId = vm_project_submission 主键 id(直接回传当 businessType 筛选参数);
|
|
||||||
// businessTypeLabel = 该表的 business_type 名称;提报表里没命中的脏数据不计入。
|
|
||||||
// ⚠️ /list 响应里的 requirementStatistics 后端仍是注释状态(实测恒 null),统计一律走本端点。
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** 两位补零 */
|
|
||||||
function pad(value: number): string {
|
function pad(value: number): string {
|
||||||
return String(value).padStart(2, '0')
|
return String(value).padStart(2, '0')
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Date → 'yyyy-MM-dd HH:mm:ss'(后端 RdProjectBo.beginTime/endTime 是 Date,实测该格式可绑定) */
|
|
||||||
function toDateTime(date: Date, endOfDay: boolean): string {
|
function toDateTime(date: Date, endOfDay: boolean): string {
|
||||||
const ymd = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
const ymd = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
||||||
return `${ymd} ${endOfDay ? '23:59:59' : '00:00:00'}`
|
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 } {
|
function toTimeRange(query: ProjectQuery): { beginTime?: string; endTime?: string } {
|
||||||
if (query.startDate || query.endDate) {
|
if (query.startDate || query.endDate) {
|
||||||
return {
|
return {
|
||||||
@@ -77,21 +51,15 @@ function toTimeRange(query: ProjectQuery): { beginTime?: string; endTime?: strin
|
|||||||
return { beginTime: toDateTime(start, false), endTime: toDateTime(new Date(), true) }
|
return { beginTime: toDateTime(start, false), endTime: toDateTime(new Date(), true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 项目列表(真实接口:服务端筛选 + 分页;业务类型统计走 getProjectFilterConditions) */
|
|
||||||
export function getProjectList(query: ProjectQuery): Promise<ProjectListResult> {
|
export function getProjectList(query: ProjectQuery): Promise<ProjectListResult> {
|
||||||
return request<ProjectListResult>({
|
return request<ProjectListResult>({
|
||||||
url: '/web/requirementPool/list',
|
url: '/web/requirementPool/list',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
// 多选数组必须走 serializeQuery,展开成 businessTypes[0]=…&businessTypes[1]=…
|
|
||||||
//(与 api/cases.ts 同一做法;不传则走 axios 默认的 businessTypes[]=,实测后端也能绑,
|
|
||||||
// 但这里取与文档/案例库一致的索引形式,避免依赖框架默认行为)
|
|
||||||
paramsSerializer: { serialize: serializeQuery },
|
paramsSerializer: { serialize: serializeQuery },
|
||||||
params: {
|
params: {
|
||||||
pageNum: query.pageNum,
|
pageNum: query.pageNum,
|
||||||
pageSize: query.pageSize,
|
pageSize: query.pageSize,
|
||||||
// 空串不出现在 querystring 里,与后端 eqIfText 的「有值才等值匹配」语义一致
|
|
||||||
key: query.keyword || undefined,
|
key: query.keyword || undefined,
|
||||||
// 多选:空数组不发;非空由 serializeQuery 展开成 businessTypes[0]=…&businessTypes[1]=…
|
|
||||||
businessTypes: query.businessTypes.length > 0 ? [...query.businessTypes] : undefined,
|
businessTypes: query.businessTypes.length > 0 ? [...query.businessTypes] : undefined,
|
||||||
status: query.status || undefined,
|
status: query.status || undefined,
|
||||||
...toTimeRange(query),
|
...toTimeRange(query),
|
||||||
@@ -99,12 +67,6 @@ export function getProjectList(query: ProjectQuery): Promise<ProjectListResult>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 业务类型筛选条件(左侧筛选栏「业务类型」的值域 + 件数)。
|
|
||||||
*
|
|
||||||
* <p>与列表分开取:`/list` 的 `requirementStatistics` 后端恒为 null,统计由本端点单独下发,
|
|
||||||
* 且是**全量口径**(不随筛选、分页变化),所以进页面拉一次即可。</p>
|
|
||||||
*/
|
|
||||||
export function getProjectFilterConditions(): Promise<ProjectStatistics> {
|
export function getProjectFilterConditions(): Promise<ProjectStatistics> {
|
||||||
return request<ProjectStatistics>({
|
return request<ProjectStatistics>({
|
||||||
url: '/web/requirementPool/getFilterConditions',
|
url: '/web/requirementPool/getFilterConditions',
|
||||||
@@ -112,17 +74,6 @@ export function getProjectFilterConditions(): Promise<ProjectStatistics> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 已完成项目(申请售后页「项目名称」下拉的候选)。
|
|
||||||
*
|
|
||||||
* <p>直接复用列表接口做状态过滤:`status=COMPLETED` 在后端比的是
|
|
||||||
* `COALESCE(vm_rd_project.status, vm_project_requirement.status)`,而 `COMPLETED` 只会
|
|
||||||
* 出现在**项目表**(未立项的需求是 `INITIATING`)⇒ 结果天然就是「自己名下已立项且已完成的项目」,
|
|
||||||
* 与后端申请售后的准入校验(只有 COMPLETED 可申请)口径一致。</p>
|
|
||||||
*
|
|
||||||
* <p>售后申请场景下候选量级不大,一次取回较大分页(后端无专用端点);后端默认排序
|
|
||||||
* `update_time DESC`。返回行的 `id` = **需求 id**,正是申请接口要传的那个 id。</p>
|
|
||||||
*/
|
|
||||||
export function getCompletedProjects(): Promise<RdProjectVo[]> {
|
export function getCompletedProjects(): Promise<RdProjectVo[]> {
|
||||||
return request<ProjectListResult>({
|
return request<ProjectListResult>({
|
||||||
url: '/web/requirementPool/list',
|
url: '/web/requirementPool/list',
|
||||||
@@ -136,7 +87,6 @@ export function getCompletedProjects(): Promise<RdProjectVo[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// 详情:真实接口(**4 个页签各一个端点**,同帧并行取回后合并)
|
|
||||||
// GET /web/requirementPool/get/progress/{id} → ProjectRequirementProgressVo
|
// GET /web/requirementPool/get/progress/{id} → ProjectRequirementProgressVo
|
||||||
// GET /web/requirementPool/get/files/{id} → ProjectRequirementFilesVo
|
// GET /web/requirementPool/get/files/{id} → ProjectRequirementFilesVo
|
||||||
// GET /web/requirementPool/get/finance/{id} → ProjectRequirementFinanceVo
|
// GET /web/requirementPool/get/finance/{id} → ProjectRequirementFinanceVo
|
||||||
@@ -144,26 +94,10 @@ export function getCompletedProjects(): Promise<RdProjectVo[]> {
|
|||||||
// → ProjectRequirementUserController#getProgress / #getFiles / #getFinance / #getAfterSales
|
// → ProjectRequirementUserController#getProgress / #getFiles / #getFinance / #getAfterSales
|
||||||
// → ProjectRequirementServiceImpl#queryProgress / queryFiles / queryFinance / queryAfterSales
|
// → ProjectRequirementServiceImpl#queryProgress / queryFiles / queryFinance / queryAfterSales
|
||||||
//
|
//
|
||||||
// 4 个 VO 都继承 ProjectRequirementBaseVo(基础信息始终带)⇒ 任一响应里都有项目名 /
|
|
||||||
// 编号 / 业务类型 / 状态 / 提交时间,所以详情页顶部信息卡只依赖其中一支即可,切页签不缺数据。
|
|
||||||
// 首屏用 getProjectDetailBundle **同帧并行**发 4 个请求(一次到位,避免首屏缺数据);
|
|
||||||
// **之后每次点击页签**再用 getProjectDetailTab 只拉对应那一支(点一次 = 一次请求),
|
|
||||||
// 响应回填到 store 里的 bundle 后重新映射 ⇒ 页面始终是**最新数据**,不重拉另外三支。
|
|
||||||
// 出参由 stores/modules/project.ts 缓存成 bundle,经 utils/projectVo.ts#toProjectDetail
|
|
||||||
// 映射成详情页沿用的 Project 形态。
|
|
||||||
// ⚠️ id 传的是**需求 id**(vm_project_requirement.id)= 列表行 RdProjectVo.id;
|
|
||||||
// 未立项的需求也能查到(项目侧字段为空);不存在 / 非本人 → 后端 4 支 data 均为 null。
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
|
||||||
* 详情页签(= `RequirementDetailBundle` 的键名,也就是 4 个端点的后缀)。
|
|
||||||
*
|
|
||||||
* <p>`views/project/detail.vue` 里的 `DetailTab` 是同一组值;那边不 import 本模块
|
|
||||||
* (页面层不直接依赖 api),靠字面量对齐。</p>
|
|
||||||
*/
|
|
||||||
export type ProjectDetailTab = 'progress' | 'files' | 'finance' | 'afterSales'
|
export type ProjectDetailTab = 'progress' | 'files' | 'finance' | 'afterSales'
|
||||||
|
|
||||||
/** 页签 → 端点(4 支都继承 ProjectRequirementBaseVo,基础信息任一支都有) */
|
|
||||||
const DETAIL_TAB_URL: Record<ProjectDetailTab, string> = {
|
const DETAIL_TAB_URL: Record<ProjectDetailTab, string> = {
|
||||||
progress: '/web/requirementPool/get/progress',
|
progress: '/web/requirementPool/get/progress',
|
||||||
files: '/web/requirementPool/get/files',
|
files: '/web/requirementPool/get/files',
|
||||||
@@ -171,14 +105,6 @@ const DETAIL_TAB_URL: Record<ProjectDetailTab, string> = {
|
|||||||
afterSales: '/web/requirementPool/get/afterSales',
|
afterSales: '/web/requirementPool/get/afterSales',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 单个页签取数(**点一次页签 = 一次请求**,拿该页签的最新数据)。
|
|
||||||
*
|
|
||||||
* <p>返回类型按页签收窄:`getProjectDetailTab(id, 'finance')` → `RequirementFinanceVo | null`,
|
|
||||||
* 所以调用方可以直接把结果写回 bundle 的对应键。</p>
|
|
||||||
*
|
|
||||||
* <p>`null` = 需求不存在 / 不属于当前用户(后端 `data: null`),调用方应保留现有数据并提示。</p>
|
|
||||||
*/
|
|
||||||
export function getProjectDetailTab<T extends ProjectDetailTab>(
|
export function getProjectDetailTab<T extends ProjectDetailTab>(
|
||||||
id: string,
|
id: string,
|
||||||
tab: T,
|
tab: T,
|
||||||
@@ -189,12 +115,6 @@ export function getProjectDetailTab<T extends ProjectDetailTab>(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 详情四支并行取回(**首屏用**);任一为 null ⇒ 整个 bundle 为 null。
|
|
||||||
*
|
|
||||||
* <p>4 支同源(都查同一行需求),正常不会只缺一支;缺一支即视为「需求不存在 / 非本人」,
|
|
||||||
* 详情页走整页空态。</p>
|
|
||||||
*/
|
|
||||||
export function getProjectDetailBundle(id: string): Promise<RequirementDetailBundle | null> {
|
export function getProjectDetailBundle(id: string): Promise<RequirementDetailBundle | null> {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
getProjectDetailTab(id, 'progress'),
|
getProjectDetailTab(id, 'progress'),
|
||||||
@@ -208,31 +128,12 @@ export function getProjectDetailBundle(id: string): Promise<RequirementDetailBun
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// 售后(**真实接口**)
|
|
||||||
// 申请 POST /web/requirementPool/afterSales/request body ProjectRequirementActionBo
|
|
||||||
// 撤回 POST /web/requirementPool/afterSales/withdraw body ProjectRequirementActionBo
|
|
||||||
// → ProjectRequirementUserController#requestAfterSale / #withdrawAfterSale
|
// → ProjectRequirementUserController#requestAfterSale / #withdrawAfterSale
|
||||||
// → ProjectRequirementServiceImpl#requestAfterSale / #withdrawAfterSale
|
// → ProjectRequirementServiceImpl#requestAfterSale / #withdrawAfterSale
|
||||||
// 附件上传 POST /web/workOrderDraft/upload(multipart,字段名 `file`)—— 只传 OSS 不落业务表;
|
|
||||||
// 提交时下发 `attachmentOssIds`,后端转存进 `vm_project_requirement_attachment`
|
|
||||||
// (phase = AFTER_SALE、target_id = 售后工单 id),OSS 文件不重复上传。
|
|
||||||
//
|
//
|
||||||
// ⚠️ 后端业务校验(失败抛 ServiceException,由统一 request 层弹提示):
|
|
||||||
// · 只有「已完成(COMPLETED)」的项目能申请售后;
|
|
||||||
// · 已有 REQUESTED / IN_PROGRESS 的售后时拒绝重复申请;
|
|
||||||
// · 撤回只允许 REQUESTED 状态,撤回后售后置 WITHDRAWN、项目回到已完成。
|
|
||||||
// ⚠️ 撤回接口收的是**需求 id**(不是售后工单 id),与后端 `checkOwner` 的校验口径一致。
|
|
||||||
//
|
//
|
||||||
// ⚠️ 详情页「项目文件」页签的上传 / 删除**仍无用户端接口**(上传入口在需求提报页),
|
|
||||||
// 所以这里不再保留 mock —— 详情数据来自真实接口,塞本地假数据会让刷新前后不一致。
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
|
||||||
* 上传售后附件(复用需求提报的 OSS 上传口)。
|
|
||||||
*
|
|
||||||
* <p>只传 OSS、不落业务表;返回项带 `ossId`,提交时由后端转存成售后附件。
|
|
||||||
* 后端响应没有文件大小,大小取本地 `File.size`。</p>
|
|
||||||
*/
|
|
||||||
export function uploadAfterSalesFile(file: File): Promise<AfterSalesAttachment> {
|
export function uploadAfterSalesFile(file: File): Promise<AfterSalesAttachment> {
|
||||||
const data = new FormData()
|
const data = new FormData()
|
||||||
data.append('file', file)
|
data.append('file', file)
|
||||||
@@ -250,12 +151,6 @@ export function uploadAfterSalesFile(file: File): Promise<AfterSalesAttachment>
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 申请售后(用户端)。
|
|
||||||
*
|
|
||||||
* <p>`id` 是**需求 id**(= 详情页路由参数);`payload` 的 `reason` / `note` 分别对应
|
|
||||||
* 页面的「问题描述」与「补充备注」。</p>
|
|
||||||
*/
|
|
||||||
export function submitAfterSales(
|
export function submitAfterSales(
|
||||||
id: string,
|
id: string,
|
||||||
payload: AfterSalesPayload,
|
payload: AfterSalesPayload,
|
||||||
@@ -268,17 +163,11 @@ export function submitAfterSales(
|
|||||||
type: payload.type,
|
type: payload.type,
|
||||||
reason: payload.reason,
|
reason: payload.reason,
|
||||||
note: payload.note,
|
note: payload.note,
|
||||||
// 空数组是安全的(后端 `appendAttachments` 对空集合直接 return,不 NPE)
|
|
||||||
attachmentOssIds: payload.attachmentOssIds,
|
attachmentOssIds: payload.attachmentOssIds,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 撤回售后申请(用户端)。
|
|
||||||
*
|
|
||||||
* <p>`id` 传**需求 id**;`reason` 是撤回原因(选填,后端会存进 `withdraw_reason`)。</p>
|
|
||||||
*/
|
|
||||||
export function cancelAfterSales(id: string, reason = ''): Promise<void> {
|
export function cancelAfterSales(id: string, reason = ''): Promise<void> {
|
||||||
return request<void>({
|
return request<void>({
|
||||||
url: '/web/requirementPool/afterSales/withdraw',
|
url: '/web/requirementPool/afterSales/withdraw',
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ function nowTime(): string {
|
|||||||
return `今天 ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
return `今天 ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 历史会话(欢迎语 + 用户提问 + 文件资料) */
|
|
||||||
export function getChatHistory(): Promise<ChatMessage[]> {
|
export function getChatHistory(): Promise<ChatMessage[]> {
|
||||||
return Promise.resolve([
|
return Promise.resolve([
|
||||||
{
|
{
|
||||||
@@ -40,7 +39,6 @@ export function getChatHistory(): Promise<ChatMessage[]> {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 发送文本消息(echo 用户消息,携带当前时间) */
|
|
||||||
export function sendChatMessage(content: string): Promise<ChatMessage> {
|
export function sendChatMessage(content: string): Promise<ChatMessage> {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
id: nextId('chat'),
|
id: nextId('chat'),
|
||||||
@@ -50,7 +48,6 @@ export function sendChatMessage(content: string): Promise<ChatMessage> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 上传聊天文件(生成文件卡片消息) */
|
|
||||||
export function uploadChatFile(file: File): Promise<ChatMessage> {
|
export function uploadChatFile(file: File): Promise<ChatMessage> {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
id: nextId('chat'),
|
id: nextId('chat'),
|
||||||
@@ -61,7 +58,6 @@ export function uploadChatFile(file: File): Promise<ChatMessage> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 客服自动回复 */
|
|
||||||
export function mockAutoReply(): Promise<ChatMessage> {
|
export function mockAutoReply(): Promise<ChatMessage> {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
id: nextId('chat'),
|
id: nextId('chat'),
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
// 用户资料接口(真实后端绑定)
|
|
||||||
// 后端:org.dromara.system.controller.system.SysUserController#profile → /system/user/profile
|
|
||||||
// 鉴权:需登录态
|
|
||||||
import { request } from '@/utils/request'
|
import { request } from '@/utils/request'
|
||||||
import type { UserProfileVo } from '@/types/business/user'
|
import type { UserProfileVo } from '@/types/business/user'
|
||||||
|
|
||||||
/**
|
|
||||||
* 当前登录用户资料(需求提报页预填联系人姓名 / 电话)。
|
|
||||||
*
|
|
||||||
* <p>⚠️ 后端返回的是 `R<Map>`,用户对象嵌在 **`data.user`** 里(同级还有 roles / posts),
|
|
||||||
* 不是平铺在 data 上——这里在 api 层解包,调用方拿到的是用户对象本身。</p>
|
|
||||||
*/
|
|
||||||
export async function getUserProfile(): Promise<UserProfileVo> {
|
export async function getUserProfile(): Promise<UserProfileVo> {
|
||||||
const data = await request<{ user: UserProfileVo }>({
|
const data = await request<{ user: UserProfileVo }>({
|
||||||
url: '/system/user/profile',
|
url: '/system/user/profile',
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useSiteStore } from '@/stores/modules/site'
|
|||||||
|
|
||||||
const siteStore = useSiteStore()
|
const siteStore = useSiteStore()
|
||||||
|
|
||||||
// 设计稿快速导航列原文:首页 / 案例中心 / 需求提报 / 下载 APP / 帮助中心
|
|
||||||
const quickLinks = [
|
const quickLinks = [
|
||||||
{ label: '首页', path: '/' },
|
{ label: '首页', path: '/' },
|
||||||
{ label: '案例中心', path: '/cases' },
|
{ label: '案例中心', path: '/cases' },
|
||||||
@@ -85,10 +84,6 @@ const coreBusiness = [
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
// 设计稿页脚 1920×481(首页稿 y1942→2423):
|
|
||||||
// 列区 y1996 起(页脚顶 +54),四列 x155/922/1257/1511,列宽 625/193/80/253
|
|
||||||
// 品牌名「虫子智造」@x200(相对列缩进 45),描述 @x155
|
|
||||||
// 版权区:©行 @x526(867 盒在 1920 画布居中)、ICP 行 @x841(相对 867 盒缩进 315),均左对齐
|
|
||||||
.app-footer {
|
.app-footer {
|
||||||
background: @footer-bg;
|
background: @footer-bg;
|
||||||
color: @white;
|
color: @white;
|
||||||
@@ -97,16 +92,16 @@ const coreBusiness = [
|
|||||||
.flex(row, flex-start, flex-start);
|
.flex(row, flex-start, flex-start);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1920px;
|
max-width: 1920px;
|
||||||
margin: 0 auto; // 视口宽于 1920 时整块居中;缺这行内容会贴左、右侧留白远大于左侧
|
margin: 0 auto;
|
||||||
height: 250px; // 页脚顶 → © 行(y1942→2274)
|
height: 250px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding-top: 30px;
|
padding-top: 30px;
|
||||||
padding-left: clamp(
|
padding-left: clamp(
|
||||||
@spacing-base,
|
@spacing-base,
|
||||||
calc((100% - 1609px) / 2),
|
calc((100% - 1609px) / 2),
|
||||||
155px
|
155px
|
||||||
); // 设计内容 x155→1764 = 1609 宽
|
);
|
||||||
overflow: hidden; // 防列内容撑出横向滚动
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__about {
|
&__about {
|
||||||
@@ -116,8 +111,8 @@ const coreBusiness = [
|
|||||||
|
|
||||||
&__brand {
|
&__brand {
|
||||||
.flex(row, flex-start);
|
.flex(row, flex-start);
|
||||||
margin-bottom: 20px; // 品牌名盒底 2037 → 描述 2057
|
margin-bottom: 20px;
|
||||||
margin-left: 45px; // 设计:品牌名 @x200,列起点 x155
|
margin-left: 45px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__name {
|
&__name {
|
||||||
@@ -131,16 +126,14 @@ const coreBusiness = [
|
|||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 23px; // 设计 625×115 五行
|
line-height: 23px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__col {
|
&__col {
|
||||||
.flex(column, flex-start, flex-start); // 第三参必须显式:mixin 默认 center 会把列项水平居中
|
.flex(column, flex-start, flex-start);
|
||||||
gap: 0;
|
gap: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
// 设计列位:核心业务 @922(列宽 193)、快速导航 @1257(80)、联系我们 @1511(253)
|
|
||||||
// 用 modifier 类定位;:nth-of-type 按标签序号计数(about 也算 div),会整体错位一档
|
|
||||||
&--core {
|
&--core {
|
||||||
flex: 0 0 193px;
|
flex: 0 0 193px;
|
||||||
margin-left: 142px;
|
margin-left: 142px;
|
||||||
@@ -156,7 +149,7 @@ const coreBusiness = [
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
margin-bottom: 20px; // 标题盒底 2037 → 列表 2057
|
margin-bottom: 20px;
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 41px;
|
line-height: 41px;
|
||||||
@@ -167,7 +160,7 @@ const coreBusiness = [
|
|||||||
&__link {
|
&__link {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 23px; // 设计 5 项 115 高
|
line-height: 23px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 0.2s;
|
transition: color 0.2s;
|
||||||
@@ -183,12 +176,11 @@ const coreBusiness = [
|
|||||||
line-height: 23px;
|
line-height: 23px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 版权区:867 盒在画布居中(1920 视口 → x526),盒内文字左对齐;ICP 缩进 315(→ x841)
|
|
||||||
&__bottom {
|
&__bottom {
|
||||||
width: 867px;
|
width: 867px;
|
||||||
max-width: calc(100% - 32px);
|
max-width: calc(100% - 32px);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding-bottom: 20px; // ICP 盒底 2336 → 页脚底 2423
|
padding-bottom: 20px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,11 +189,11 @@ const coreBusiness = [
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 23px;
|
line-height: 23px;
|
||||||
white-space: nowrap; // 设计稿单行(867×23),回退字体偏宽防折行
|
white-space: nowrap;
|
||||||
color: @white;
|
color: @white;
|
||||||
|
|
||||||
&--icp {
|
&--icp {
|
||||||
margin-top: 16px; // © 盒底 2297 → ICP 2313
|
margin-top: 16px;
|
||||||
margin-left: 315px;
|
margin-left: 315px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ interface NavItem {
|
|||||||
path: string
|
path: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设计稿页头(43 张稿一致):无 logo 文字,导航首项 x275(间隙 94),
|
|
||||||
// 右侧两枚 106×36 胶囊(快捷提报 x1559 / 在线客服 x1694,右缘 1800)
|
|
||||||
const navItems: NavItem[] = [
|
const navItems: NavItem[] = [
|
||||||
{ label: '首页', path: '/' },
|
{ label: '首页', path: '/' },
|
||||||
{ label: '案例库', path: '/cases' },
|
{ label: '案例库', path: '/cases' },
|
||||||
@@ -20,7 +18,6 @@ const navItems: NavItem[] = [
|
|||||||
{ label: '个人中心', path: '/profile' },
|
{ label: '个人中心', path: '/profile' },
|
||||||
]
|
]
|
||||||
|
|
||||||
// 🔔 消息提示:顶栏铃铛展示未读数量,点击进入消息中心
|
|
||||||
const messageStore = useMessageStore()
|
const messageStore = useMessageStore()
|
||||||
const { unreadCount } = storeToRefs(messageStore)
|
const { unreadCount } = storeToRefs(messageStore)
|
||||||
onMounted(() => void messageStore.fetchUnreadCount())
|
onMounted(() => void messageStore.fetchUnreadCount())
|
||||||
@@ -84,8 +81,6 @@ onMounted(() => void messageStore.fetchUnreadCount())
|
|||||||
background: @header-bg;
|
background: @header-bg;
|
||||||
border-bottom: 1px solid @header-border;
|
border-bottom: 1px solid @header-border;
|
||||||
|
|
||||||
// 设计稿:导航首项 x275、胶囊右缘 1800(= 1920 − 120)
|
|
||||||
// 左留白按 155 基准、右留白按 120 基准,各自 clamp 收窄,窄视口不溢出
|
|
||||||
&__inner {
|
&__inner {
|
||||||
.flex(row, flex-start);
|
.flex(row, flex-start);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -95,10 +90,9 @@ onMounted(() => void messageStore.fetchUnreadCount())
|
|||||||
padding-right: 15%;
|
padding-right: 15%;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导航首项 x275 = 左留白 155 + 120
|
|
||||||
&__nav {
|
&__nav {
|
||||||
.flex(row, center);
|
.flex(row, center);
|
||||||
gap: @header-nav-gap; // 设计实测 94(首页 x275→311,案例库 x405)
|
gap: @header-nav-gap;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
|
|
||||||
@@ -114,7 +108,6 @@ onMounted(() => void messageStore.fetchUnreadCount())
|
|||||||
position: relative;
|
position: relative;
|
||||||
.flex(row, center);
|
.flex(row, center);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
// 设计稿:导航项 18/500,未激活 #000000、激活 #085DFF
|
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: @black;
|
color: @black;
|
||||||
@@ -125,7 +118,6 @@ onMounted(() => void messageStore.fetchUnreadCount())
|
|||||||
color: @home-tab-active;
|
color: @home-tab-active;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 激活项底部短横线(设计实测 29×2px @y59,距底 11)
|
|
||||||
&.router-link-active::after {
|
&.router-link-active::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -142,12 +134,11 @@ onMounted(() => void messageStore.fetchUnreadCount())
|
|||||||
|
|
||||||
&__actions {
|
&__actions {
|
||||||
.flex(row, flex-end, center);
|
.flex(row, flex-end, center);
|
||||||
gap: 29px; // 设计:胶囊1 右缘 1665 → 胶囊2 左缘 1694
|
gap: 29px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设计稿:胶囊 106×36,图标 18 + 文字 16/500 白字
|
|
||||||
&__action {
|
&__action {
|
||||||
.flex(row, center);
|
.flex(row, center);
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
@@ -170,7 +161,6 @@ onMounted(() => void messageStore.fetchUnreadCount())
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔔 消息铃铛:展示未读数量红点,点击进入消息中心
|
|
||||||
&__bell {
|
&__bell {
|
||||||
position: relative;
|
position: relative;
|
||||||
.flex(row, center, center);
|
.flex(row, center, center);
|
||||||
@@ -200,7 +190,6 @@ onMounted(() => void messageStore.fetchUnreadCount())
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录:文字链接,点击直达登录页
|
|
||||||
&__login {
|
&__login {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|||||||
@@ -14,14 +14,12 @@ const emit = defineEmits<{
|
|||||||
favorite: [id: string]
|
favorite: [id: string]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
/** 卡片元信息:设计稿为「标签:值」三列两行 */
|
|
||||||
const meta = computed<Array<{ label: string; value: string }>>(() => [
|
const meta = computed<Array<{ label: string; value: string }>>(() => [
|
||||||
{ label: '客户类型', value: props.item.clientType || '--' },
|
{ label: '客户类型', value: props.item.clientType || '--' },
|
||||||
{ label: '所属行业', value: props.item.industry || '--' },
|
{ label: '所属行业', value: props.item.industry || '--' },
|
||||||
{ label: '规模', value: props.item.scale || '--' },
|
{ label: '规模', value: props.item.scale || '--' },
|
||||||
{ label: '项目周期', value: props.item.duration || '--' },
|
{ label: '项目周期', value: props.item.duration || '--' },
|
||||||
{ label: '服务团队', value: props.item.teamSize ? `${props.item.teamSize}人` : '--' },
|
{ label: '服务团队', value: props.item.teamSize ? `${props.item.teamSize}人` : '--' },
|
||||||
// 设计稿完成时间为 2026/8/10 形式,后端 completedAt 下发 2026-08-10
|
|
||||||
{ label: '完成时间', value: props.item.finishDate ? props.item.finishDate.replace(/-/g, '/') : '--' },
|
{ label: '完成时间', value: props.item.finishDate ? props.item.finishDate.replace(/-/g, '/') : '--' },
|
||||||
])
|
])
|
||||||
</script>
|
</script>
|
||||||
@@ -67,7 +65,6 @@ const meta = computed<Array<{ label: string; value: string }>>(() => [
|
|||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.case-card {
|
.case-card {
|
||||||
// 设计稿卡片 396×267 白底 bd #E1DFDF/1
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 267px;
|
height: 267px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -84,7 +81,6 @@ const meta = computed<Array<{ label: string; value: string }>>(() => [
|
|||||||
|
|
||||||
&__cover {
|
&__cover {
|
||||||
position: relative;
|
position: relative;
|
||||||
// 设计封面 396×137.81(通栏无留白)
|
|
||||||
height: 137px;
|
height: 137px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -101,7 +97,6 @@ const meta = computed<Array<{ label: string; value: string }>>(() => [
|
|||||||
|
|
||||||
&__tag {
|
&__tag {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
// 设计标签文字 @(628.6,540.35),卡 @(606,423) → 卡内 (22.6,117) 高 44
|
|
||||||
top: 87px;
|
top: 87px;
|
||||||
left: 22px;
|
left: 22px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
@@ -109,7 +104,6 @@ const meta = computed<Array<{ label: string; value: string }>>(() => [
|
|||||||
border-radius: @radius-sm;
|
border-radius: @radius-sm;
|
||||||
background: @primary-strong;
|
background: @primary-strong;
|
||||||
color: @white;
|
color: @white;
|
||||||
// 设计 28/500
|
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 44px;
|
line-height: 44px;
|
||||||
@@ -167,14 +161,12 @@ const meta = computed<Array<{ label: string; value: string }>>(() => [
|
|||||||
|
|
||||||
&__body {
|
&__body {
|
||||||
position: relative;
|
position: relative;
|
||||||
// 设计标题 @(626.45,578.03),卡 @(606,423) → 卡内 (20,155)
|
|
||||||
padding: 18px 20px 0;
|
padding: 18px 20px 0;
|
||||||
height: 129px;
|
height: 129px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
// 设计标题 16/500 #333333
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -186,10 +178,8 @@ const meta = computed<Array<{ label: string; value: string }>>(() => [
|
|||||||
&__meta {
|
&__meta {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
// 设计两行:y610.33 / y631.86 → 行距 21.5
|
|
||||||
gap: 21.5px 12px;
|
gap: 21.5px 12px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
// 设计元信息 10/400 #333333
|
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
line-height: 14px;
|
line-height: 14px;
|
||||||
color: @cases-text;
|
color: @cases-text;
|
||||||
@@ -205,11 +195,9 @@ const meta = computed<Array<{ label: string; value: string }>>(() => [
|
|||||||
|
|
||||||
&__more {
|
&__more {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
// 设计 @(926.67,659.85),卡 @(606,423) → 卡内 (320.67,236.85)
|
|
||||||
right: 20px;
|
right: 20px;
|
||||||
bottom: 14px;
|
bottom: 14px;
|
||||||
color: @primary-strong;
|
color: @primary-strong;
|
||||||
// 设计 12/400 #024FFF
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,8 +58,6 @@ function onCancel(): void {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
// 设计 24(取消绑定确认)/ 29(注销风险提示):白卡 ~840 设计px、无蒙层压暗、
|
|
||||||
// 底部两枚等宽整行按钮(次级 #EEF0F3 / 主色或危险色)
|
|
||||||
.confirm-dialog {
|
.confirm-dialog {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -68,7 +66,7 @@ function onCancel(): void {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
||||||
&__panel {
|
&__panel {
|
||||||
width: 660px; // 设计 840 × 0.77
|
width: 660px;
|
||||||
max-width: calc(100vw - 32px);
|
max-width: calc(100vw - 32px);
|
||||||
padding: @spacing-base;
|
padding: @spacing-base;
|
||||||
border-radius: @radius-lg;
|
border-radius: @radius-lg;
|
||||||
@@ -76,7 +74,6 @@ function onCancel(): void {
|
|||||||
box-shadow: @shadow-lg;
|
box-shadow: @shadow-lg;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 蓝条 + 标题 15 设计px w700(与个人中心各弹窗一致)
|
|
||||||
&__title {
|
&__title {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: @spacing-base;
|
margin-bottom: @spacing-base;
|
||||||
@@ -100,12 +97,11 @@ function onCancel(): void {
|
|||||||
|
|
||||||
&__body {
|
&__body {
|
||||||
margin-bottom: @spacing-lg;
|
margin-bottom: @spacing-lg;
|
||||||
font-size: 11px; // 设计 29 条目 13 → 11
|
font-size: 11px;
|
||||||
color: @text-color;
|
color: @text-color;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设计 24:两枚等宽整行按钮铺满卡宽
|
|
||||||
&__footer {
|
&__footer {
|
||||||
.flex(row, flex-start);
|
.flex(row, flex-start);
|
||||||
gap: @spacing-sm;
|
gap: @spacing-sm;
|
||||||
@@ -129,7 +125,6 @@ function onCancel(): void {
|
|||||||
color: @text-color;
|
color: @text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设计 24 主按钮 #024FFF / 设计 29 危险按钮 #EF4444
|
|
||||||
&--confirm {
|
&--confirm {
|
||||||
background: @primary-strong;
|
background: @primary-strong;
|
||||||
color: @white;
|
color: @white;
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ const emit = defineEmits<{
|
|||||||
.flex(column, flex-start);
|
.flex(column, flex-start);
|
||||||
gap: @spacing-sm;
|
gap: @spacing-sm;
|
||||||
|
|
||||||
// 设计稿(06/07 附件条):实底 #EDEDED 灰条、无描边
|
|
||||||
&__item {
|
&__item {
|
||||||
.flex(row, space-between);
|
.flex(row, space-between);
|
||||||
gap: @spacing-base;
|
gap: @spacing-base;
|
||||||
|
|||||||
@@ -37,12 +37,10 @@ function onDownload(): void {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="preview-dialog__body">
|
<div class="preview-dialog__body">
|
||||||
<!-- 加载中 -->
|
|
||||||
<div v-if="loading" class="preview-dialog__center">
|
<div v-if="loading" class="preview-dialog__center">
|
||||||
预览加载中…
|
预览加载中…
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 加载失败 / 无 ossId 等错误 -->
|
|
||||||
<div v-else-if="error" class="preview-dialog__center">
|
<div v-else-if="error" class="preview-dialog__center">
|
||||||
<p class="preview-dialog__msg">{{ error }}</p>
|
<p class="preview-dialog__msg">{{ error }}</p>
|
||||||
<AppButton v-if="file?.url" type="primary" @click="onDownload">
|
<AppButton v-if="file?.url" type="primary" @click="onDownload">
|
||||||
@@ -50,7 +48,6 @@ function onDownload(): void {
|
|||||||
</AppButton>
|
</AppButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 不支持内联预览(office 等) -->
|
|
||||||
<div v-else-if="kind === 'unsupported'" class="preview-dialog__center">
|
<div v-else-if="kind === 'unsupported'" class="preview-dialog__center">
|
||||||
<p class="preview-dialog__msg">
|
<p class="preview-dialog__msg">
|
||||||
当前格式({{ (file?.name ?? '').split('.').pop() }})不支持在线预览,请下载后查看。
|
当前格式({{ (file?.name ?? '').split('.').pop() }})不支持在线预览,请下载后查看。
|
||||||
@@ -60,7 +57,6 @@ function onDownload(): void {
|
|||||||
</AppButton>
|
</AppButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 图片 -->
|
|
||||||
<img
|
<img
|
||||||
v-else-if="kind === 'image' && objectUrl"
|
v-else-if="kind === 'image' && objectUrl"
|
||||||
:src="objectUrl"
|
:src="objectUrl"
|
||||||
@@ -68,7 +64,6 @@ function onDownload(): void {
|
|||||||
alt="预览"
|
alt="预览"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- PDF(blob 已带 application/pdf,iframe 内联渲染) -->
|
|
||||||
<iframe
|
<iframe
|
||||||
v-else-if="kind === 'pdf' && objectUrl"
|
v-else-if="kind === 'pdf' && objectUrl"
|
||||||
:src="objectUrl"
|
:src="objectUrl"
|
||||||
@@ -76,7 +71,6 @@ function onDownload(): void {
|
|||||||
title="预览"
|
title="预览"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 视频 -->
|
|
||||||
<video
|
<video
|
||||||
v-else-if="kind === 'video' && objectUrl"
|
v-else-if="kind === 'video' && objectUrl"
|
||||||
:src="objectUrl"
|
:src="objectUrl"
|
||||||
@@ -84,7 +78,6 @@ function onDownload(): void {
|
|||||||
controls
|
controls
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 音频 -->
|
|
||||||
<audio
|
<audio
|
||||||
v-else-if="kind === 'audio' && objectUrl"
|
v-else-if="kind === 'audio' && objectUrl"
|
||||||
:src="objectUrl"
|
:src="objectUrl"
|
||||||
@@ -92,7 +85,6 @@ function onDownload(): void {
|
|||||||
controls
|
controls
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 文本(大文件截断预览,避免卡死浏览器) -->
|
|
||||||
<div v-else-if="kind === 'text'" class="preview-dialog__text-wrap">
|
<div v-else-if="kind === 'text'" class="preview-dialog__text-wrap">
|
||||||
<p v-if="truncated" class="preview-dialog__hint">
|
<p v-if="truncated" class="preview-dialog__hint">
|
||||||
<span>文件较大({{ formatBytes(totalBytes ?? 0) }}),仅预览前 {{ formatBytes(PREVIEW_TEXT_MAX_BYTES) }},完整内容请下载。</span>
|
<span>文件较大({{ formatBytes(totalBytes ?? 0) }}),仅预览前 {{ formatBytes(PREVIEW_TEXT_MAX_BYTES) }},完整内容请下载。</span>
|
||||||
|
|||||||
@@ -71,26 +71,26 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.filter-panel {
|
.filter-panel {
|
||||||
padding: 24px 36px; // 设计04 搜索卡左右内缩 47 × 0.77
|
padding: 24px 36px;
|
||||||
border-radius: @radius-base;
|
border-radius: @radius-base;
|
||||||
background: @white;
|
background: @white;
|
||||||
|
|
||||||
&__bar {
|
&__bar {
|
||||||
.flex(row, flex-start);
|
.flex(row, flex-start);
|
||||||
gap: 18px; // 按钮间距 24 × 0.77
|
gap: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__input {
|
&__input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-width: 456px; // 设计输入框 592 × 0.77
|
max-width: 456px;
|
||||||
margin-right: 28px; // 与按钮总距 60 × 0.77(叠加 bar gap 18)
|
margin-right: 28px;
|
||||||
height: 38px; // 设计 49 × 0.77
|
height: 38px;
|
||||||
padding: 0 @spacing-base;
|
padding: 0 @spacing-base;
|
||||||
border: 1px solid @cases-input-border;
|
border: 1px solid @cases-input-border;
|
||||||
border-radius: @radius-sm;
|
border-radius: @radius-sm;
|
||||||
background: @white;
|
background: @white;
|
||||||
color: @cases-text;
|
color: @cases-text;
|
||||||
font-size: 15px; // 设计 20 × 0.77
|
font-size: 15px;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: border-color 0.2s;
|
transition: border-color 0.2s;
|
||||||
|
|
||||||
@@ -104,13 +104,13 @@ const emit = defineEmits<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__btn {
|
&__btn {
|
||||||
min-width: 77px; // 设计 100 × 0.77
|
min-width: 77px;
|
||||||
height: 38px; // 设计 49 × 0.77
|
height: 38px;
|
||||||
padding: 0 @spacing-base;
|
padding: 0 @spacing-base;
|
||||||
border-radius: @radius-sm;
|
border-radius: @radius-sm;
|
||||||
background: @cases-accent; // 设计 重置/搜索 均为 rgba(7,92,255)
|
background: @cases-accent;
|
||||||
color: @white;
|
color: @white;
|
||||||
font-size: 15px; // 设计 20 × 0.77
|
font-size: 15px;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -131,26 +131,26 @@ const emit = defineEmits<{
|
|||||||
&__chips {
|
&__chips {
|
||||||
.flex(row, flex-start, center);
|
.flex(row, flex-start, center);
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 11px 10px; // 行距 14 / 列距 13 × 0.77
|
gap: 11px 10px;
|
||||||
margin-top: @spacing-base;
|
margin-top: @spacing-base;
|
||||||
padding: 12px 18px 16px; // 「已选条件」面板内边距 16/24/21 × 0.77
|
padding: 12px 18px 16px;
|
||||||
border-radius: @radius-base;
|
border-radius: @radius-base;
|
||||||
background: @chips-panel-bg;
|
background: @chips-panel-bg;
|
||||||
font-size: 15px; // 设计 20 × 0.77
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__chips-label {
|
&__chips-label {
|
||||||
color: @cases-text; // 设计04 标题 rgba(51,51,51)
|
color: @cases-text;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__chip {
|
&__chip {
|
||||||
height: 25px; // 设计 32 × 0.77
|
height: 25px;
|
||||||
padding: 0 @spacing-sm;
|
padding: 0 @spacing-sm;
|
||||||
border-radius: @radius-round;
|
border-radius: @radius-round;
|
||||||
background: @white;
|
background: @white;
|
||||||
color: @cases-accent; // 设计 chips 文字 rgba(7,92,255)
|
color: @cases-accent;
|
||||||
font-size: 11px; // 设计 14 → 10.8,取可读下限 11
|
font-size: 11px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
&--clear {
|
&--clear {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: @primary-strong; // 设计15「清空」为纯文字蓝链
|
color: @primary-strong;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
|
||||||
* 页面头部(白底通栏):主标题 22/700 #1F2937 + 副标语 13/400 #6B7280。
|
|
||||||
*
|
|
||||||
* <p>43 张设计稿实测:标题顶 y125(顶栏 72 → 内容区 padding-top 53 含 1px 描边)、
|
|
||||||
* 行盒 29 → 副标语 ink y162(标题盒底 154 + 8)。整块白底高 179(72 → 251)。</p>
|
|
||||||
*/
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string
|
title: string
|
||||||
slogan?: string
|
slogan?: string
|
||||||
@@ -29,10 +23,10 @@ defineProps<Props>()
|
|||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.page-head {
|
.page-head {
|
||||||
background: @white;
|
background: @white;
|
||||||
padding-bottom: 72px; // 副标语盒底 179 → 页头块底 251(块高 179)
|
padding-bottom: 72px;
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
padding-top: 52px; // 顶栏 72 → 标题 ink 124(design 125)
|
padding-top: 52px;
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 29px;
|
line-height: 29px;
|
||||||
@@ -41,7 +35,7 @@ defineProps<Props>()
|
|||||||
|
|
||||||
&__slogan {
|
&__slogan {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
margin-top: 9px; // 标题盒底 153 → 副标语盒顶 162
|
margin-top: 9px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 17px;
|
line-height: 17px;
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ const emit = defineEmits<{ pageChange: [page: number] }>()
|
|||||||
|
|
||||||
const pageCount = computed(() => Math.max(1, Math.ceil(props.total / props.pageSize)))
|
const pageCount = computed(() => Math.max(1, Math.ceil(props.total / props.pageSize)))
|
||||||
|
|
||||||
/** 页码序列:≤7 全部展示,否则首尾 + 当前页窗口(… 省略) */
|
|
||||||
const pages = computed<Array<number | '...'>>(() => {
|
const pages = computed<Array<number | '...'>>(() => {
|
||||||
const n = pageCount.value
|
const n = pageCount.value
|
||||||
const { page } = props
|
const { page } = props
|
||||||
@@ -67,19 +66,19 @@ function go(page: number): void {
|
|||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.pagination {
|
.pagination {
|
||||||
.flex(row, center);
|
.flex(row, center);
|
||||||
gap: @spacing-xs; // 盒间距 6 × 0.77 ≈ 5
|
gap: @spacing-xs;
|
||||||
padding: 12px 0; // 网格 → 分页 36 × 0.77(叠加主栏 gap 18)
|
padding: 12px 0;
|
||||||
|
|
||||||
&__btn {
|
&__btn {
|
||||||
.flex(row, center);
|
.flex(row, center);
|
||||||
min-width: 20px; // 设计15 页码盒 26 × 0.77
|
min-width: 20px;
|
||||||
height: 25px; // 设计 32 × 0.77
|
height: 25px;
|
||||||
padding: 0 3px;
|
padding: 0 3px;
|
||||||
border: 1px solid @border-color;
|
border: 1px solid @border-color;
|
||||||
border-radius: @radius-sm;
|
border-radius: @radius-sm;
|
||||||
background: @white;
|
background: @white;
|
||||||
color: @text-color-secondary; // 设计15 实取 rgba(107,114,128)
|
color: @text-color-secondary;
|
||||||
font-size: 11px; // 设计 12 → 9.2,取可读下限 11
|
font-size: 11px;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
|
||||||
&:hover:not(:disabled):not(.is-ellipsis) {
|
&:hover:not(:disabled):not(.is-ellipsis) {
|
||||||
@@ -90,7 +89,7 @@ function go(page: number): void {
|
|||||||
&.is-active {
|
&.is-active {
|
||||||
border-color: @cases-page-active-bg;
|
border-color: @cases-page-active-bg;
|
||||||
background: @cases-page-active-bg;
|
background: @cases-page-active-bg;
|
||||||
color: @primary-strong; // 设计15 当前页文字 rgba(2,79,255)
|
color: @primary-strong;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-ellipsis {
|
&.is-ellipsis {
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ const levelIndex = computed(() => (level.value === 'strong' ? 3 : level.value ==
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- 设计稿:强度条常驻显示(未输入时整条为空轨道 + 空强度文字),
|
|
||||||
占位高度固定 15(条行)+ 18(提示行),保证表单纵向节奏与设计稿一致 -->
|
|
||||||
<div class="pwd-strength">
|
<div class="pwd-strength">
|
||||||
<div class="pwd-strength__bars">
|
<div class="pwd-strength__bars">
|
||||||
<span
|
<span
|
||||||
@@ -38,7 +36,6 @@ const levelIndex = computed(() => (level.value === 'strong' ? 3 : level.value ==
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
// 设计稿(54/55):满宽强度条(弱=首段红,余段浅灰轨道)+ 右端灰色强度文字 + 下方红色提示
|
|
||||||
.pwd-strength {
|
.pwd-strength {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
@@ -46,7 +43,6 @@ const levelIndex = computed(() => (level.value === 'strong' ? 3 : level.value ==
|
|||||||
column-gap: @spacing-sm;
|
column-gap: @spacing-sm;
|
||||||
row-gap: 3px;
|
row-gap: 3px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
// 设计稿:密码框底 → 条行 → 提示行 → 下一标签,节奏 15/15/18
|
|
||||||
min-height: 33px;
|
min-height: 33px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
@@ -55,7 +51,6 @@ const levelIndex = computed(() => (level.value === 'strong' ? 3 : level.value ==
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设计稿轨道实取 #EDEFF2,条高 5
|
|
||||||
&__bar {
|
&__bar {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 5px;
|
height: 5px;
|
||||||
@@ -75,7 +70,6 @@ const levelIndex = computed(() => (level.value === 'strong' ? 3 : level.value ==
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设计稿强度文字为 11px 中性灰(「弱」实取 rgba(156,163,175))
|
|
||||||
&__label {
|
&__label {
|
||||||
justify-self: end;
|
justify-self: end;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { computed, onMounted, ref } from 'vue'
|
|||||||
import { useCountDown } from '@/composables/useCountDown'
|
import { useCountDown } from '@/composables/useCountDown'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/** 二维码有效期(秒),默认 4:32 */
|
|
||||||
expiresIn?: number
|
expiresIn?: number
|
||||||
size?: number
|
size?: number
|
||||||
}
|
}
|
||||||
@@ -67,10 +66,9 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
// 扫码面板(设计 39:灰色二维码占位块 + 红色倒计时胶囊 + 灰色小字提示)
|
|
||||||
.qr-panel {
|
.qr-panel {
|
||||||
.flex(column, center);
|
.flex(column, center);
|
||||||
gap: 17px; // 二维码 → 倒计时(设计 22 × 0.77)
|
gap: 17px;
|
||||||
|
|
||||||
&__box {
|
&__box {
|
||||||
.flex(column, center);
|
.flex(column, center);
|
||||||
@@ -99,23 +97,22 @@ onMounted(() => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 状态行:胶囊(倒计时)+ 下方灰色提示,整体居中
|
|
||||||
&__status {
|
&__status {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: @spacing-sm;
|
gap: @spacing-sm;
|
||||||
font-size: 11px; // 设计 13/11 → 可读下限
|
font-size: 11px;
|
||||||
color: @text-color-disabled;
|
color: @text-color-disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__countdown {
|
&__countdown {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 16px; // 胶囊高 42 设计 × 0.77
|
padding: 8px 16px;
|
||||||
border-radius: @radius-round;
|
border-radius: @radius-round;
|
||||||
background: fade(@danger-color, 8%);
|
background: fade(@danger-color, 8%);
|
||||||
color: @danger-color; // 设计 39 实取 @danger-color
|
color: @danger-color;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ function onGetCode(): void {
|
|||||||
.flex(row, stretch);
|
.flex(row, stretch);
|
||||||
gap: @spacing-sm;
|
gap: @spacing-sm;
|
||||||
|
|
||||||
// 设计稿(02/54/55):验证码输入框 590×39(整行 680,右侧留 90 给「获取验证码」)
|
|
||||||
// ⚠️ 盒高/字号随登录页 @auth-font-scale 放大,与各表单页输入框保持同高
|
|
||||||
&__input {
|
&__input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -57,7 +55,6 @@ function onGetCode(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设计稿:「获取验证码」为蓝字纯文本(无边框),12/400 #024FFF
|
|
||||||
&__btn {
|
&__btn {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
min-width: 74px * @auth-font-scale;
|
min-width: 74px * @auth-font-scale;
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ withDefaults(defineProps<Props>(), { variant: 'info' })
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
// 设计稿(项目中心 09 付款计划徽标):约 57×22 设计px、圆角 4 设计px、11px 字、浅底彩色字
|
|
||||||
.status-tag {
|
.status-tag {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ defineProps<Props>()
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
// 设计稿(06/07 三步表单):白卡上竖排「大号蓝色序号 + 区块标题」,无连接线/分隔线/阴影
|
|
||||||
.step-form {
|
.step-form {
|
||||||
padding: @spacing-lg @spacing-xl;
|
padding: @spacing-lg @spacing-xl;
|
||||||
border-radius: @radius-base;
|
border-radius: @radius-base;
|
||||||
@@ -48,7 +47,6 @@ defineProps<Props>()
|
|||||||
gap: @spacing-xs;
|
gap: @spacing-xs;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设计稿序号:48 设计px ≈ 37px、#024FFF、与内容列对齐(列宽 46px)
|
|
||||||
&__dot {
|
&__dot {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 46px;
|
width: 46px;
|
||||||
@@ -62,20 +60,17 @@ defineProps<Props>()
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设计稿区块标题:28 设计px ≈ 22px、500、rgba(51,51,51)
|
|
||||||
&__label {
|
&__label {
|
||||||
font-size: @font-size-title;
|
font-size: @font-size-title;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: @demand-heading;
|
color: @demand-heading;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设计稿无步骤连接线
|
|
||||||
&__line {
|
&__line {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__body {
|
&__body {
|
||||||
// 内容列与序号后的标题对齐(序号列 46px)
|
|
||||||
margin-left: 46px;
|
margin-left: 46px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ function onDrop(event: DragEvent): void {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
// 设计稿(06/07 附件条):实底 #EDEDED 灰条、无描边、90 设计px ≈ 69px 高
|
|
||||||
.upload-area {
|
.upload-area {
|
||||||
.flex(column, center);
|
.flex(column, center);
|
||||||
gap: @spacing-xs;
|
gap: @spacing-xs;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { computed, onScopeDispose, ref } from 'vue'
|
import { computed, onScopeDispose, ref } from 'vue'
|
||||||
|
|
||||||
/** 倒计时(验证码 60s、二维码失效 4:32 等通用) */
|
|
||||||
export function useCountDown(defaultSeconds = 60) {
|
export function useCountDown(defaultSeconds = 60) {
|
||||||
const seconds = ref(0)
|
const seconds = ref(0)
|
||||||
let timer: ReturnType<typeof setInterval> | null = null
|
let timer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { onMounted, onUnmounted, ref, type Ref } from 'vue'
|
import { onMounted, onUnmounted, ref, type Ref } from 'vue'
|
||||||
|
|
||||||
/** 元素可见性(配合图片懒加载/曝光统计) */
|
|
||||||
export function useIntersection(
|
export function useIntersection(
|
||||||
target: Ref<Element | null | undefined>,
|
target: Ref<Element | null | undefined>,
|
||||||
options?: IntersectionObserverInit,
|
options?: IntersectionObserverInit,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { ref, type Ref } from 'vue'
|
import { ref, type Ref } from 'vue'
|
||||||
|
|
||||||
/** 请求 loading 状态管理 */
|
|
||||||
export function useLoading(initial = false): {
|
export function useLoading(initial = false): {
|
||||||
loading: Ref<boolean>
|
loading: Ref<boolean>
|
||||||
withLoading: <T>(fn: () => Promise<T>) => Promise<T>
|
withLoading: <T>(fn: () => Promise<T>) => Promise<T>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { computed, onMounted, onUnmounted, ref, type ComputedRef, type Ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, type ComputedRef, type Ref } from 'vue'
|
||||||
|
|
||||||
/** 滚动监听/吸顶/回到顶部 */
|
|
||||||
export function useScroll(threshold = 200): {
|
export function useScroll(threshold = 200): {
|
||||||
scrollY: Ref<number>
|
scrollY: Ref<number>
|
||||||
isAtTop: ComputedRef<boolean>
|
isAtTop: ComputedRef<boolean>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ function normalize(value: DebounceValue): DebounceBinding {
|
|||||||
return typeof value === 'function' ? { handler: value } : value
|
return typeof value === 'function' ? { handler: value } : value
|
||||||
}
|
}
|
||||||
|
|
||||||
/** v-debounce 防抖点击:v-debounce="fn" 或 v-debounce="{ handler: fn, delay: 500 }" */
|
|
||||||
const debounce: Directive<Element, DebounceValue> = {
|
const debounce: Directive<Element, DebounceValue> = {
|
||||||
mounted(el, binding) {
|
mounted(el, binding) {
|
||||||
const { handler, delay = 300 } = normalize(binding.value)
|
const { handler, delay = 300 } = normalize(binding.value)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import lazy from './lazy'
|
|||||||
import debounce from './debounce'
|
import debounce from './debounce'
|
||||||
import throttle from './throttle'
|
import throttle from './throttle'
|
||||||
|
|
||||||
/** main.ts 调用:统一注册自定义指令 */
|
|
||||||
export function setupDirectives(app: App): void {
|
export function setupDirectives(app: App): void {
|
||||||
app
|
app
|
||||||
.directive('lazy', lazy)
|
.directive('lazy', lazy)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type { Directive } from 'vue'
|
|||||||
|
|
||||||
const observerMap = new WeakMap<Element, IntersectionObserver>()
|
const observerMap = new WeakMap<Element, IntersectionObserver>()
|
||||||
|
|
||||||
/** v-lazy 图片懒加载:v-lazy="图片地址" */
|
|
||||||
const lazy: Directive<Element, string> = {
|
const lazy: Directive<Element, string> = {
|
||||||
mounted(el, binding) {
|
mounted(el, binding) {
|
||||||
if (!(el instanceof HTMLImageElement)) return
|
if (!(el instanceof HTMLImageElement)) return
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ function normalize(value: ThrottleValue): ThrottleBinding {
|
|||||||
return typeof value === 'function' ? { handler: value } : value
|
return typeof value === 'function' ? { handler: value } : value
|
||||||
}
|
}
|
||||||
|
|
||||||
/** v-throttle 节流点击:v-throttle="fn" 或 v-throttle="{ handler: fn, interval: 500 }" */
|
|
||||||
const throttle: Directive<Element, ThrottleValue> = {
|
const throttle: Directive<Element, ThrottleValue> = {
|
||||||
mounted(el, binding) {
|
mounted(el, binding) {
|
||||||
const { handler, interval = 500 } = normalize(binding.value)
|
const { handler, interval = 500 } = normalize(binding.value)
|
||||||
|
|||||||
1
src/env.d.ts
vendored
1
src/env.d.ts
vendored
@@ -1,7 +1,6 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
/** API 基础地址 */
|
|
||||||
readonly VITE_API_BASE_URL: string
|
readonly VITE_API_BASE_URL: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { computed } from 'vue'
|
|||||||
import { AppFooter, AppHeader, FilePreviewDialog } from '@/components'
|
import { AppFooter, AppHeader, FilePreviewDialog } from '@/components'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
// 需求提报 / 合同中心等特殊页不展示页脚(路由 meta.noFooter 控制)
|
|
||||||
const showFooter = computed(() => !route.meta.noFooter)
|
const showFooter = computed(() => !route.meta.noFooter)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -15,16 +14,11 @@ const showFooter = computed(() => !route.meta.noFooter)
|
|||||||
<router-view />
|
<router-view />
|
||||||
</main>
|
</main>
|
||||||
<AppFooter v-if="showFooter" />
|
<AppFooter v-if="showFooter" />
|
||||||
<!-- 全局文件预览弹窗(同源 OSS 预览,绕开桶强制下载) -->
|
|
||||||
<FilePreviewDialog />
|
<FilePreviewDialog />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
// 公共底栏全站常驻(设计稿 43 张中除登录类外均带 481 高页脚)
|
|
||||||
// sticky footer:.layout 至少占满视口 → 内容少时 footer 贴底不留白;
|
|
||||||
// 内容多时 .layout 随内容长高,footer 自然落在内容下方(滚动可见)。
|
|
||||||
// ⚠️ html 全局 zoom 会把内部 100vh 再缩一次,必须除以 @page-zoom(同 .auth-page)。
|
|
||||||
.layout {
|
.layout {
|
||||||
.flex(column, flex-start, stretch);
|
.flex(column, flex-start, stretch);
|
||||||
min-height: calc(100vh / @page-zoom);
|
min-height: calc(100vh / @page-zoom);
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import type { Router } from 'vue-router'
|
import type { Router } from 'vue-router'
|
||||||
import { storage } from '@/utils/storage'
|
import { storage } from '@/utils/storage'
|
||||||
|
|
||||||
/** 路由守卫:登录态校验 + document.title */
|
|
||||||
export function setupGuard(router: Router): void {
|
export function setupGuard(router: Router): void {
|
||||||
router.beforeEach((to) => {
|
router.beforeEach((to) => {
|
||||||
// 视觉走查旁路:仅本地开发且带 ?visual=1 时放行,便于逐页比对设计稿
|
|
||||||
if (
|
if (
|
||||||
import.meta.env.DEV
|
import.meta.env.DEV
|
||||||
&& new URLSearchParams(window.location.search).has('visual')
|
&& new URLSearchParams(window.location.search).has('visual')
|
||||||
|
|||||||
@@ -1,23 +1,15 @@
|
|||||||
import type { RouteRecordRaw } from 'vue-router'
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
/**
|
|
||||||
* 公共布局(AppHeader + router-view + AppFooter)下的路由
|
|
||||||
* 完整规划见 docs/page-architecture.md 第 2 节
|
|
||||||
* M3–M10 视图由子代理并行实现中,交付后本表即为终态
|
|
||||||
*/
|
|
||||||
const layoutRoutes: RouteRecordRaw = {
|
const layoutRoutes: RouteRecordRaw = {
|
||||||
path: '/',
|
path: '/',
|
||||||
component: () => import('@/layout/index.vue'),
|
component: () => import('@/layout/index.vue'),
|
||||||
children: [
|
children: [
|
||||||
// ── 公开 ──
|
|
||||||
{ path: '', name: 'Home', component: () => import('@/views/home'), meta: { title: '首页', footer: true } },
|
{ path: '', name: 'Home', component: () => import('@/views/home'), meta: { title: '首页', footer: true } },
|
||||||
{ path: 'business/:type', name: 'BusinessDetail', component: () => import('@/views/home/detail.vue'), meta: { title: '业务详情' } },
|
{ path: 'business/:type', name: 'BusinessDetail', component: () => import('@/views/home/detail.vue'), meta: { title: '业务详情' } },
|
||||||
{ path: 'cases', name: 'Cases', component: () => import('@/views/cases'), meta: { title: '成功案例库' } },
|
{ path: 'cases', name: 'Cases', component: () => import('@/views/cases'), meta: { title: '成功案例库' } },
|
||||||
{ path: 'cases/:id', name: 'CaseDetail', component: () => import('@/views/cases/detail.vue'), meta: { title: '案例详情' } },
|
{ path: 'cases/:id', name: 'CaseDetail', component: () => import('@/views/cases/detail.vue'), meta: { title: '案例详情' } },
|
||||||
// 需求提报依赖登录态:字典 / 业务类型 / 草稿 / 上传 / 提交接口匿名实测均 401
|
|
||||||
{ path: 'demand', name: 'Demand', component: () => import('@/views/demand'), meta: { title: '需求提报', requiresAuth: true, noFooter: true } },
|
{ path: 'demand', name: 'Demand', component: () => import('@/views/demand'), meta: { title: '需求提报', requiresAuth: true, noFooter: true } },
|
||||||
|
|
||||||
// ── 登录区 ──
|
|
||||||
{ path: 'project', name: 'Project', component: () => import('@/views/project'), meta: { title: '项目中心', requiresAuth: true } },
|
{ path: 'project', name: 'Project', component: () => import('@/views/project'), meta: { title: '项目中心', requiresAuth: true } },
|
||||||
{ path: 'project/:id', name: 'ProjectDetail', component: () => import('@/views/project/detail.vue'), meta: { title: '项目详情', requiresAuth: true } },
|
{ path: 'project/:id', name: 'ProjectDetail', component: () => import('@/views/project/detail.vue'), meta: { title: '项目详情', requiresAuth: true } },
|
||||||
{ path: 'project/:id/after-sales', name: 'AfterSales', component: () => import('@/views/project/after-sales.vue'), meta: { title: '申请售后', requiresAuth: true } },
|
{ path: 'project/:id/after-sales', name: 'AfterSales', component: () => import('@/views/project/after-sales.vue'), meta: { title: '申请售后', requiresAuth: true } },
|
||||||
@@ -29,7 +21,6 @@ const layoutRoutes: RouteRecordRaw = {
|
|||||||
{ path: 'finance/bills/:id', name: 'BillDetail', component: () => import('@/views/finance/bill-detail.vue'), meta: { title: '账单详情', requiresAuth: true } },
|
{ path: 'finance/bills/:id', name: 'BillDetail', component: () => import('@/views/finance/bill-detail.vue'), meta: { title: '账单详情', requiresAuth: true } },
|
||||||
{ path: 'finance/bills/:id/pay', name: 'BillPay', component: () => import('@/views/finance/pay.vue'), meta: { title: '扫码支付', requiresAuth: true } },
|
{ path: 'finance/bills/:id/pay', name: 'BillPay', component: () => import('@/views/finance/pay.vue'), meta: { title: '扫码支付', requiresAuth: true } },
|
||||||
{ path: 'finance/bills/:id/pay/result', name: 'PayResult', component: () => import('@/views/finance/pay-result.vue'), meta: { title: '支付成功', requiresAuth: true } },
|
{ path: 'finance/bills/:id/pay/result', name: 'PayResult', component: () => import('@/views/finance/pay-result.vue'), meta: { title: '支付成功', requiresAuth: true } },
|
||||||
// 收银台(确认支付):由账单明细勾选/单笔「去支付」带入 `?ids=bill-001,bill-002`
|
|
||||||
{ path: 'finance/checkout', name: 'Checkout', component: () => import('@/views/finance/checkout.vue'), meta: { title: '确认支付', requiresAuth: true } },
|
{ path: 'finance/checkout', name: 'Checkout', component: () => import('@/views/finance/checkout.vue'), meta: { title: '确认支付', requiresAuth: true } },
|
||||||
{ path: 'finance/invoices', name: 'Invoices', component: () => import('@/views/finance/invoices.vue'), meta: { title: '发票申请', requiresAuth: true } },
|
{ path: 'finance/invoices', name: 'Invoices', component: () => import('@/views/finance/invoices.vue'), meta: { title: '发票申请', requiresAuth: true } },
|
||||||
{ path: 'finance/invoice-titles', name: 'InvoiceTitles', component: () => import('@/views/finance/invoice-titles.vue'), meta: { title: '发票抬头', requiresAuth: true } },
|
{ path: 'finance/invoice-titles', name: 'InvoiceTitles', component: () => import('@/views/finance/invoice-titles.vue'), meta: { title: '发票抬头', requiresAuth: true } },
|
||||||
@@ -37,7 +28,6 @@ const layoutRoutes: RouteRecordRaw = {
|
|||||||
{ path: 'message/:id', name: 'MessageDetail', component: () => import('@/views/message/detail.vue'), meta: { title: '通知详情', requiresAuth: true } },
|
{ path: 'message/:id', name: 'MessageDetail', component: () => import('@/views/message/detail.vue'), meta: { title: '通知详情', requiresAuth: true } },
|
||||||
{ path: 'service', name: 'Service', component: () => import('@/views/service'), meta: { title: '在线客服', requiresAuth: true } },
|
{ path: 'service', name: 'Service', component: () => import('@/views/service'), meta: { title: '在线客服', requiresAuth: true } },
|
||||||
|
|
||||||
// ── 个人中心(左菜单嵌套)──
|
|
||||||
{
|
{
|
||||||
path: 'profile',
|
path: 'profile',
|
||||||
component: () => import('@/views/profile'),
|
component: () => import('@/views/profile'),
|
||||||
@@ -57,7 +47,6 @@ const layoutRoutes: RouteRecordRaw = {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 无全局布局的路由(登录注册独立品牌页 + 404) */
|
|
||||||
const plainRoutes: RouteRecordRaw[] = [
|
const plainRoutes: RouteRecordRaw[] = [
|
||||||
{ path: '/login', name: 'Login', component: () => import('@/views/login'), meta: { title: '登录' } },
|
{ path: '/login', name: 'Login', component: () => import('@/views/login'), meta: { title: '登录' } },
|
||||||
{ path: '/register', name: 'Register', component: () => import('@/views/login/register.vue'), meta: { title: '注册' } },
|
{ path: '/register', name: 'Register', component: () => import('@/views/login/register.vue'), meta: { title: '注册' } },
|
||||||
|
|||||||
@@ -13,33 +13,8 @@ import { collectCase, getCaseCategories, getCaseDetail, getCaseList } from '@/ap
|
|||||||
import { toCase } from '@/utils/caseVo'
|
import { toCase } from '@/utils/caseVo'
|
||||||
import type { CaseTypeNameMap } from '@/utils/caseVo'
|
import type { CaseTypeNameMap } from '@/utils/caseVo'
|
||||||
|
|
||||||
/**
|
|
||||||
* 案例库数据(列表 / 筛选 / 分页 / 详情 / 收藏)
|
|
||||||
*
|
|
||||||
* 后端接口(ProjectCaseUserController,前缀 /web/projectCase):
|
|
||||||
* - GET /list 列表(分页 + 跟随筛选条件的统计)
|
|
||||||
* - GET /categories 筛选面板选项(业务类型统计 + 所属行业 + 项目规模档位)
|
|
||||||
* - GET /{id} 详情(含项目背景富文本与附件,并回填 isFavorite)
|
|
||||||
* - POST /collect 收藏 / 取消收藏(toggle)
|
|
||||||
*
|
|
||||||
* ⚠️ 筛选面板**除完成时间外**的选项全部由后端下发,且**都来自 GET /categories**(一次请求):
|
|
||||||
* - 业务类型:projectCaseStatisticsVo.projectCaseCounts(按项目提报表补全类型全集)
|
|
||||||
* - 案例总数:projectCaseStatisticsVo.total
|
|
||||||
* - 所属行业:businessIndustry
|
|
||||||
* - 项目规模:upperAndLowerLimits(label 已由后端拼好)
|
|
||||||
* 完成时间后端无对应选项接口,由前端按快捷区间 + 日期选择器生成。
|
|
||||||
*
|
|
||||||
* ⚠️ 业务类型**下发的是提报主键 id**(不是类型名):案例表 business_type 列实测存的就是
|
|
||||||
* 该 id,传类型名后端按原值匹配不到(实测 total=0)。同一份 id→名称 字典还用于把列表 /
|
|
||||||
* 详情里的 id 翻译成展示名(见 typeNameById),否则标签会全部退化成「其他」。
|
|
||||||
*
|
|
||||||
* 后端 VO 与页面字段的差异(命名、业务类型取值、富文本、数值单位)由 utils/caseVo 统一对齐。
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** 每页条数(案例库为三列卡片栅格,9 = 3 行) */
|
|
||||||
const PAGE_SIZE = 9
|
const PAGE_SIZE = 9
|
||||||
|
|
||||||
/** 结果数预探测的防抖时长(筛选面板改动后即时回显「N 个结果」) */
|
|
||||||
const PROBE_DELAY = 300
|
const PROBE_DELAY = 300
|
||||||
|
|
||||||
function createDefaultQuery(): CaseQuery {
|
function createDefaultQuery(): CaseQuery {
|
||||||
@@ -55,7 +30,6 @@ function createDefaultQuery(): CaseQuery {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 筛选面板给的是 yyyy-MM-dd,后端 Date 入参按 yyyy-MM-dd HH:mm:ss 补全 */
|
|
||||||
function withTime(date: string, time: string): string {
|
function withTime(date: string, time: string): string {
|
||||||
return date.length === 10 ? `${date} ${time}` : date
|
return date.length === 10 ? `${date} ${time}` : date
|
||||||
}
|
}
|
||||||
@@ -64,12 +38,6 @@ function trimmed(raw: string | null | undefined): string {
|
|||||||
return (raw ?? '').trim()
|
return (raw ?? '').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 业务类型统计 → 筛选面板选项。
|
|
||||||
*
|
|
||||||
* <p>value 取**提报主键 id**(下发 businessTypes 用),label 取业务类型名称(展示用)。
|
|
||||||
* 不按前端 BusinessType 枚举归一化,选项一律用后端下发的原值。</p>
|
|
||||||
*/
|
|
||||||
function toTypeOptions(stats: ProjectCaseStatisticsVo | null): CaseTypeOption[] {
|
function toTypeOptions(stats: ProjectCaseStatisticsVo | null): CaseTypeOption[] {
|
||||||
return (stats?.projectCaseCounts ?? [])
|
return (stats?.projectCaseCounts ?? [])
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
@@ -80,7 +48,6 @@ function toTypeOptions(stats: ProjectCaseStatisticsVo | null): CaseTypeOption[]
|
|||||||
.filter((item) => item.value.length > 0 && item.label.length > 0)
|
.filter((item) => item.value.length > 0 && item.label.length > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 业务类型统计 → 「提报 id → 类型名」字典(供 utils/caseVo 翻译列表/详情的 business_type) */
|
|
||||||
function toTypeNameMap(stats: ProjectCaseStatisticsVo | null): CaseTypeNameMap {
|
function toTypeNameMap(stats: ProjectCaseStatisticsVo | null): CaseTypeNameMap {
|
||||||
const map: CaseTypeNameMap = {}
|
const map: CaseTypeNameMap = {}
|
||||||
for (const item of stats?.projectCaseCounts ?? []) {
|
for (const item of stats?.projectCaseCounts ?? []) {
|
||||||
@@ -95,25 +62,18 @@ export const useCasesStore = defineStore('cases', () => {
|
|||||||
const query = ref<CaseQuery>(createDefaultQuery())
|
const query = ref<CaseQuery>(createDefaultQuery())
|
||||||
const list = ref<Case[]>([])
|
const list = ref<Case[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
/** 待提交条件的结果数(筛选面板改动后即时探测,仅用于「确定筛选(N 个结果)」) */
|
|
||||||
const previewTotal = ref(0)
|
const previewTotal = ref(0)
|
||||||
|
|
||||||
/** 业务类型筛选项(接口下发:GET /categories 的统计;value=提报 id) */
|
|
||||||
const typeOptions = ref<CaseTypeOption[]>([])
|
const typeOptions = ref<CaseTypeOption[]>([])
|
||||||
/** 业务类型字典:提报 id → 类型名(列表/详情标签翻译 + 首页精选共用) */
|
|
||||||
const typeNameById = ref<CaseTypeNameMap>({})
|
const typeNameById = ref<CaseTypeNameMap>({})
|
||||||
/** 所属行业筛选项(接口下发:GET /categories) */
|
|
||||||
const industryOptions = ref<BusinessIndustryOption[]>([])
|
const industryOptions = ref<BusinessIndustryOption[]>([])
|
||||||
/** 项目规模筛选项(接口下发:GET /categories,label 由后端拼好) */
|
|
||||||
const scaleOptions = ref<ScaleLimit[]>([])
|
const scaleOptions = ref<ScaleLimit[]>([])
|
||||||
/** 案例总数(全量口径,用于业务类型「全部」行角标) */
|
|
||||||
const caseTotal = ref(0)
|
const caseTotal = ref(0)
|
||||||
|
|
||||||
const currentCase = ref<Case | null>(null)
|
const currentCase = ref<Case | null>(null)
|
||||||
|
|
||||||
let probeTimer: ReturnType<typeof setTimeout> | null = null
|
let probeTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
/** 页面筛选条件 → 后端查询条件(值均由接口下发,原样回传) */
|
|
||||||
function buildQuery(input: CaseQuery): {
|
function buildQuery(input: CaseQuery): {
|
||||||
bo: ProjectCaseUserBo
|
bo: ProjectCaseUserBo
|
||||||
pageNum: number
|
pageNum: number
|
||||||
@@ -121,14 +81,12 @@ export const useCasesStore = defineStore('cases', () => {
|
|||||||
} {
|
} {
|
||||||
const bo: ProjectCaseUserBo = {}
|
const bo: ProjectCaseUserBo = {}
|
||||||
if (input.categories.length > 0) {
|
if (input.categories.length > 0) {
|
||||||
// categories 存的已是提报主键 id(CaseTypeOption.value),直接下发
|
|
||||||
bo.businessTypes = [...input.categories]
|
bo.businessTypes = [...input.categories]
|
||||||
}
|
}
|
||||||
if (input.industries.length > 0) {
|
if (input.industries.length > 0) {
|
||||||
bo.businessIndustries = [...input.industries]
|
bo.businessIndustries = [...input.industries]
|
||||||
}
|
}
|
||||||
if (input.scales.length > 0) {
|
if (input.scales.length > 0) {
|
||||||
// 整档对象下发(后端接收 List<UpperAndLowerLimitsModel>),浅拷贝避免把响应式对象传出去
|
|
||||||
bo.scales = input.scales.map((item) => ({ ...item }))
|
bo.scales = input.scales.map((item) => ({ ...item }))
|
||||||
}
|
}
|
||||||
if (input.dateRange) {
|
if (input.dateRange) {
|
||||||
@@ -141,12 +99,6 @@ export const useCasesStore = defineStore('cases', () => {
|
|||||||
return { bo, pageNum: input.page, pageSize: input.pageSize }
|
return { bo, pageNum: input.page, pageSize: input.pageSize }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 筛选面板选项:一次 GET /categories 取回三组 + 业务类型字典。
|
|
||||||
*
|
|
||||||
* <p>后端 CaseSelectionModel 里同时带 projectCaseStatisticsVo,所以业务类型选项与案例总数
|
|
||||||
* 无需再单独打一次 /list —— 这里的所有选项都是「全量口径」,不随当前筛选条件收缩。</p>
|
|
||||||
*/
|
|
||||||
async function fetchCategories(): Promise<void> {
|
async function fetchCategories(): Promise<void> {
|
||||||
const result = await getCaseCategories()
|
const result = await getCaseCategories()
|
||||||
const stats = result.projectCaseStatisticsVo
|
const stats = result.projectCaseStatisticsVo
|
||||||
@@ -157,17 +109,10 @@ export const useCasesStore = defineStore('cases', () => {
|
|||||||
scaleOptions.value = result.upperAndLowerLimits ?? []
|
scaleOptions.value = result.upperAndLowerLimits ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 确保业务类型字典已就绪(幂等)。
|
|
||||||
*
|
|
||||||
* <p>首页「案例库精选」的标签翻译同样依赖该字典 —— 从首页直接进入时,
|
|
||||||
* 案例库页自身的 fetchCategories 还没跑过,故由调用方(首页 store)显式确保。</p>
|
|
||||||
*/
|
|
||||||
async function ensureCategories(): Promise<void> {
|
async function ensureCategories(): Promise<void> {
|
||||||
if (typeOptions.value.length === 0) await fetchCategories()
|
if (typeOptions.value.length === 0) await fetchCategories()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 列表取数 */
|
|
||||||
async function fetchCases(): Promise<void> {
|
async function fetchCases(): Promise<void> {
|
||||||
const { bo, pageNum, pageSize } = buildQuery(query.value)
|
const { bo, pageNum, pageSize } = buildQuery(query.value)
|
||||||
const result = await getCaseList(bo, pageNum, pageSize)
|
const result = await getCaseList(bo, pageNum, pageSize)
|
||||||
@@ -176,7 +121,6 @@ export const useCasesStore = defineStore('cases', () => {
|
|||||||
previewTotal.value = total.value
|
previewTotal.value = total.value
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 待提交条件的结果数(防抖,避免每次点击都打接口) */
|
|
||||||
async function probeTotal(): Promise<void> {
|
async function probeTotal(): Promise<void> {
|
||||||
const { bo } = buildQuery(query.value)
|
const { bo } = buildQuery(query.value)
|
||||||
const result = await getCaseList(bo, 1, 1)
|
const result = await getCaseList(bo, 1, 1)
|
||||||
@@ -188,19 +132,16 @@ export const useCasesStore = defineStore('cases', () => {
|
|||||||
probeTimer = setTimeout(() => void probeTotal(), PROBE_DELAY)
|
probeTimer = setTimeout(() => void probeTotal(), PROBE_DELAY)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 面板改条件(只改待提交条件,不取数;取数由「确定筛选」触发) */
|
|
||||||
function setQuery(next: CaseQuery): void {
|
function setQuery(next: CaseQuery): void {
|
||||||
query.value = next
|
query.value = next
|
||||||
scheduleProbe()
|
scheduleProbe()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 确定筛选(重置页码后取数) */
|
|
||||||
async function search(): Promise<void> {
|
async function search(): Promise<void> {
|
||||||
query.value = { ...query.value, page: 1 }
|
query.value = { ...query.value, page: 1 }
|
||||||
await fetchCases()
|
await fetchCases()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 重置全部条件 */
|
|
||||||
async function resetQuery(): Promise<void> {
|
async function resetQuery(): Promise<void> {
|
||||||
query.value = createDefaultQuery()
|
query.value = createDefaultQuery()
|
||||||
await fetchCases()
|
await fetchCases()
|
||||||
@@ -211,24 +152,16 @@ export const useCasesStore = defineStore('cases', () => {
|
|||||||
await fetchCases()
|
await fetchCases()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 「收藏列表」开关:只筛已收藏案例 */
|
|
||||||
async function toggleFavoriteOnly(): Promise<void> {
|
async function toggleFavoriteOnly(): Promise<void> {
|
||||||
query.value = { ...query.value, favoriteOnly: !query.value.favoriteOnly, page: 1 }
|
query.value = { ...query.value, favoriteOnly: !query.value.favoriteOnly, page: 1 }
|
||||||
await fetchCases()
|
await fetchCases()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 案例详情(后端查不到时返回 null,页面据此展示空态) */
|
|
||||||
async function fetchCaseDetail(id: string): Promise<void> {
|
async function fetchCaseDetail(id: string): Promise<void> {
|
||||||
const vo = await getCaseDetail(id)
|
const vo = await getCaseDetail(id)
|
||||||
currentCase.value = vo ? toCase(vo, typeNameById.value) : null
|
currentCase.value = vo ? toCase(vo, typeNameById.value) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 收藏 / 取消收藏。
|
|
||||||
*
|
|
||||||
* <p>后端为 toggle 语义且不返回最新状态,这里按调用成功的结果本地翻转,
|
|
||||||
* 避免再调一次详情接口(该接口会自增预览量)。</p>
|
|
||||||
*/
|
|
||||||
async function toggleFavorite(id: string): Promise<void> {
|
async function toggleFavorite(id: string): Promise<void> {
|
||||||
await collectCase(id)
|
await collectCase(id)
|
||||||
const index = list.value.findIndex((item) => item.id === id)
|
const index = list.value.findIndex((item) => item.id === id)
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ function createDefaultQuery(): ContractQuery {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 合同中心数据(列表 / 筛选 / 分页 / 详情 / 签署二维码) */
|
|
||||||
export const useContractStore = defineStore('contract', () => {
|
export const useContractStore = defineStore('contract', () => {
|
||||||
const query = ref<ContractQuery>(createDefaultQuery())
|
const query = ref<ContractQuery>(createDefaultQuery())
|
||||||
const list = ref<Contract[]>([])
|
const list = ref<Contract[]>([])
|
||||||
@@ -39,13 +38,11 @@ export const useContractStore = defineStore('contract', () => {
|
|||||||
query.value = next
|
query.value = next
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 确定筛选(重置页码后取数) */
|
|
||||||
async function search(): Promise<void> {
|
async function search(): Promise<void> {
|
||||||
query.value = { ...query.value, page: 1 }
|
query.value = { ...query.value, page: 1 }
|
||||||
await fetchList()
|
await fetchList()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 重置全部条件 */
|
|
||||||
async function resetQuery(): Promise<void> {
|
async function resetQuery(): Promise<void> {
|
||||||
query.value = createDefaultQuery()
|
query.value = createDefaultQuery()
|
||||||
await fetchList()
|
await fetchList()
|
||||||
@@ -56,7 +53,6 @@ export const useContractStore = defineStore('contract', () => {
|
|||||||
await fetchList()
|
await fetchList()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取/刷新签署二维码(refresh=true 时走刷新接口) */
|
|
||||||
async function fetchSignQr(id: string, refresh = false): Promise<void> {
|
async function fetchSignQr(id: string, refresh = false): Promise<void> {
|
||||||
const result = refresh ? await refreshSignQr(id) : await getSignQr(id)
|
const result = refresh ? await refreshSignQr(id) : await getSignQr(id)
|
||||||
signExpires.value = result.expiresInSeconds
|
signExpires.value = result.expiresInSeconds
|
||||||
|
|||||||
@@ -41,18 +41,10 @@ function createDefaultForm(): Demand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 字典按 dictSort 升序(后端已排序,前端再保一次序,避免脏序) */
|
|
||||||
function sortDict(list: DictOption[] | null): DictOption[] {
|
function sortDict(list: DictOption[] | null): DictOption[] {
|
||||||
return [...(list ?? [])].sort((a, b) => (a.dictSort ?? 0) - (b.dictSort ?? 0))
|
return [...(list ?? [])].sort((a, b) => (a.dictSort ?? 0) - (b.dictSort ?? 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 草稿 VO → 表单。空值回退到 base(base 里已放入登录用户资料预填),
|
|
||||||
* 否则草稿里的空 userName / userPhone 会把预填的联系人信息覆盖掉。
|
|
||||||
*
|
|
||||||
* <p>预算范围要单独反推:后端只回传一个 `budgetRange` 字符串、没有「是否手写」的标记
|
|
||||||
* (`budgetCustom` 恒为 null),故命中字典码值或文案即预设档位,否则视为手填文本。</p>
|
|
||||||
*/
|
|
||||||
function mergeDraft(base: Demand, draft: DemandDraftVo, budgetDict: DictOption[]): Demand {
|
function mergeDraft(base: Demand, draft: DemandDraftVo, budgetDict: DictOption[]): Demand {
|
||||||
const budget = resolveDraftBudget(draft.budgetRange, budgetDict)
|
const budget = resolveDraftBudget(draft.budgetRange, budgetDict)
|
||||||
return {
|
return {
|
||||||
@@ -70,12 +62,6 @@ function mergeDraft(base: Demand, draft: DemandDraftVo, budgetDict: DictOption[]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量把 OSS 文件标识换成可访问地址。
|
|
||||||
*
|
|
||||||
* <p>提报列表的 `logoUrl` 实测是 OSS 主键(如 2099753406859292674)。其中本身已是地址的
|
|
||||||
* 直接跳过;接口需要登录态,失败时返回空映射,由调用方回退前端内置图标(不阻塞页面)。</p>
|
|
||||||
*/
|
|
||||||
async function loadOssUrlMap(raws: (string | null)[]): Promise<Record<string, string>> {
|
async function loadOssUrlMap(raws: (string | null)[]): Promise<Record<string, string>> {
|
||||||
const ids = [...new Set(
|
const ids = [...new Set(
|
||||||
raws.map((item) => (item ?? '').trim()).filter((item) => item && !toAssetUrl(item)),
|
raws.map((item) => (item ?? '').trim()).filter((item) => item && !toAssetUrl(item)),
|
||||||
@@ -90,24 +76,15 @@ async function loadOssUrlMap(raws: (string | null)[]): Promise<Record<string, st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 需求提报数据(单页三段表单 + 真实接口:提报列表 / 字典 / 草稿 / 上传 / 提交) */
|
|
||||||
export const useDemandStore = defineStore('demand', () => {
|
export const useDemandStore = defineStore('demand', () => {
|
||||||
const form = ref<Demand>(createDefaultForm())
|
const form = ref<Demand>(createDefaultForm())
|
||||||
/** 业务类型卡片选项(**完全由提报列表接口决定**:接口几条就几条,前端不追加任何兜底项) */
|
|
||||||
const bizTypeOptions = ref<BizTypeOption[]>([])
|
const bizTypeOptions = ref<BizTypeOption[]>([])
|
||||||
/** 预算范围字典原始项(值域来源:vm_budget_range) */
|
|
||||||
const budgetDict = ref<DictOption[]>([])
|
const budgetDict = ref<DictOption[]>([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const uploading = ref(false)
|
const uploading = ref(false)
|
||||||
|
|
||||||
/**
|
|
||||||
* 预算范围选项:全部来自字典,只把「手填」项**固定排到最后**(设计稿要求末尾那一项是可输入的手填位)。
|
|
||||||
*
|
|
||||||
* <p>不动字典的值域,只调顺序。渲染方式由 `DemandInfoForm` 决定:非手填项渲染成胶囊按钮,
|
|
||||||
* 手填项渲染成**同排第 5 个输入框**(占位文案即字典 label)。</p>
|
|
||||||
*/
|
|
||||||
const budgetOptions = computed<DictOption[]>(() => {
|
const budgetOptions = computed<DictOption[]>(() => {
|
||||||
const list = budgetDict.value
|
const list = budgetDict.value
|
||||||
return [
|
return [
|
||||||
@@ -116,16 +93,6 @@ export const useDemandStore = defineStore('demand', () => {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* 业务类型选项:项目提报列表(免登录)→ logo 换链 → 映射卡片选项。
|
|
||||||
*
|
|
||||||
* <p>值域**完全由接口决定**:接口返回几条就渲染几张卡,前端不追加任何兜底项
|
|
||||||
* (包括「其他」——接口有才显示,没有就不显示)。</p>
|
|
||||||
*
|
|
||||||
* <p>仅当提报接口**整体不可用**(请求异常)或一条可用记录都没有(全停用/全删除)时,
|
|
||||||
* 才回退字典 `vm_biz_type`。这仍是接口驱动的降级路径(不是前端凭空造值),
|
|
||||||
* 目的是避免出现"一个业务类型都选不了、表单无法提交"的死局。</p>
|
|
||||||
*/
|
|
||||||
async function loadBizTypeOptions(): Promise<BizTypeOption[]> {
|
async function loadBizTypeOptions(): Promise<BizTypeOption[]> {
|
||||||
try {
|
try {
|
||||||
const submissions = await getSubmissionBizTypes()
|
const submissions = await getSubmissionBizTypes()
|
||||||
@@ -138,7 +105,6 @@ export const useDemandStore = defineStore('demand', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
// 提报接口不可用时落到字典兜底,避免整页初始化失败
|
|
||||||
}
|
}
|
||||||
const dict = sortDict(await getBizTypeDict())
|
const dict = sortDict(await getBizTypeDict())
|
||||||
return dict.map((item) => toBizTypeOptionFromDict(item.dictValue, item.dictLabel))
|
return dict.map((item) => toBizTypeOptionFromDict(item.dictValue, item.dictLabel))
|
||||||
@@ -148,14 +114,12 @@ export const useDemandStore = defineStore('demand', () => {
|
|||||||
form.value = { ...form.value, ...partial }
|
form.value = { ...form.value, ...partial }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 页面初始化:业务类型 + 预算字典 + 登录用户资料 + 已有草稿(并行) */
|
|
||||||
async function init(): Promise<void> {
|
async function init(): Promise<void> {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const [bizTypes, budgetDictRes, profile, draft] = await Promise.all([
|
const [bizTypes, budgetDictRes, profile, draft] = await Promise.all([
|
||||||
loadBizTypeOptions(),
|
loadBizTypeOptions(),
|
||||||
getBudgetRangeDict(),
|
getBudgetRangeDict(),
|
||||||
// 用户资料失败不阻塞页面(联系人留空时后端会回退取登录用户资料)
|
|
||||||
getUserProfile().catch(() => null),
|
getUserProfile().catch(() => null),
|
||||||
getDraftDemand(),
|
getDraftDemand(),
|
||||||
])
|
])
|
||||||
@@ -177,7 +141,6 @@ export const useDemandStore = defineStore('demand', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 上传附件:先传 OSS,再把附件记录挂到表单(提交时后端按 ossId 转存) */
|
|
||||||
async function addAttachment(file: File): Promise<void> {
|
async function addAttachment(file: File): Promise<void> {
|
||||||
if (uploading.value) return
|
if (uploading.value) return
|
||||||
uploading.value = true
|
uploading.value = true
|
||||||
@@ -203,14 +166,12 @@ export const useDemandStore = defineStore('demand', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 移除附件(仅从表单摘除,OSS 文件保留) */
|
|
||||||
function removeAttachment(target: DemandAttachment): void {
|
function removeAttachment(target: DemandAttachment): void {
|
||||||
form.value.attachmentInfo = form.value.attachmentInfo.filter(
|
form.value.attachmentInfo = form.value.attachmentInfo.filter(
|
||||||
(item) => !(item.ossId === target.ossId && item.id === target.id),
|
(item) => !(item.ossId === target.ossId && item.id === target.id),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 保存草稿(后端按登录用户 upsert) */
|
|
||||||
async function saveDraft(): Promise<void> {
|
async function saveDraft(): Promise<void> {
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
@@ -225,7 +186,6 @@ export const useDemandStore = defineStore('demand', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 重置:清空后端草稿并复位表单(保留登录账号的联系人预填) */
|
|
||||||
async function reset(): Promise<void> {
|
async function reset(): Promise<void> {
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
@@ -244,21 +204,6 @@ export const useDemandStore = defineStore('demand', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 提交需求:前端必填校验 → 提报接口(`/web/requirementPool/create`)→ 复位表单。
|
|
||||||
*
|
|
||||||
* <p>接口收的就是草稿那套 BO(`WorkOrderDraftBo`),附件**随本次请求体一起下发**:后端
|
|
||||||
* `createRequirement` 会逐条把 `attachmentInfo` 的 `ossId` 引用复制成需求附件,所以这里
|
|
||||||
* 不需要先落一次草稿(这点与后端 `/requirementPool/submit` 不同——那条链路才是从已落库
|
|
||||||
* 草稿里搬附件)。</p>
|
|
||||||
*
|
|
||||||
* <p>后端在同一事务里还会清掉该用户的草稿(`cleanHistoryDrafts`),故提交成功后草稿箱
|
|
||||||
* 一并清空,用户不会在下次进入页面时看到已经提报过的内容。</p>
|
|
||||||
*
|
|
||||||
* <p>⚠️ 该链路只落 `vm_project_requirement` 单表;「我的项目/需求」列表
|
|
||||||
* (`/web/requirementPool/list`)查的是 `vm_rd_project`,所以本页提交的记录**不会**出现在
|
|
||||||
* 那个列表里(详见 `api/demand.ts#submitDemand`)。</p>
|
|
||||||
*/
|
|
||||||
async function submit(): Promise<boolean> {
|
async function submit(): Promise<boolean> {
|
||||||
if (!form.value.bizType) {
|
if (!form.value.bizType) {
|
||||||
ElMessage.warning('请选择业务类型')
|
ElMessage.warning('请选择业务类型')
|
||||||
@@ -272,7 +217,6 @@ export const useDemandStore = defineStore('demand', () => {
|
|||||||
ElMessage.warning('请填写核心需求描述')
|
ElMessage.warning('请填写核心需求描述')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// 手写模式下金额会直接作为 budgetRange 提交,故空值必须拦住
|
|
||||||
if (form.value.budgetManual && !form.value.budgetRange.trim()) {
|
if (form.value.budgetManual && !form.value.budgetRange.trim()) {
|
||||||
ElMessage.warning('请填写预算金额,或改选一个预设档位')
|
ElMessage.warning('请填写预算金额,或改选一个预设档位')
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -43,42 +43,31 @@ function createDefaultInvoiceQuery(): InvoiceQuery {
|
|||||||
return { keyword: '', status: 'all', page: 1, pageSize: 6 }
|
return { keyword: '', status: 'all', page: 1, pageSize: 6 }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 财务中心数据(统计/账单/支付/发票/抬头) */
|
|
||||||
export const useFinanceStore = defineStore('finance', () => {
|
export const useFinanceStore = defineStore('finance', () => {
|
||||||
// —— 统计
|
|
||||||
const stats = ref<FinanceStats | null>(null)
|
const stats = ref<FinanceStats | null>(null)
|
||||||
|
|
||||||
// —— 账单列表
|
|
||||||
const list = ref<Bill[]>([])
|
const list = ref<Bill[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const query = ref<BillQuery>(createDefaultBillQuery())
|
const query = ref<BillQuery>(createDefaultBillQuery())
|
||||||
const currentBill = ref<Bill | null>(null)
|
const currentBill = ref<Bill | null>(null)
|
||||||
|
|
||||||
// —— 支付
|
|
||||||
const payment = ref<Payment | null>(null)
|
const payment = ref<Payment | null>(null)
|
||||||
const result = ref<PaymentResult | null>(null)
|
const result = ref<PaymentResult | null>(null)
|
||||||
|
|
||||||
// —— 收银台(确认支付页)
|
|
||||||
/** 收银台待支付账单(由路由 `?ids=` 拉取) */
|
|
||||||
const checkoutBills = ref<Bill[]>([])
|
const checkoutBills = ref<Bill[]>([])
|
||||||
const checkoutLoading = ref(false)
|
const checkoutLoading = ref(false)
|
||||||
/** 最近一次下单响应(微信 codeUrl / 支付宝跳转地址) */
|
|
||||||
const payOrder = ref<PayOrderResponse | null>(null)
|
const payOrder = ref<PayOrderResponse | null>(null)
|
||||||
|
|
||||||
// —— 发票
|
|
||||||
const invoices = ref<Invoice[]>([])
|
const invoices = ref<Invoice[]>([])
|
||||||
const invoiceTotal = ref(0)
|
const invoiceTotal = ref(0)
|
||||||
const invoiceQuery = ref<InvoiceQuery>(createDefaultInvoiceQuery())
|
const invoiceQuery = ref<InvoiceQuery>(createDefaultInvoiceQuery())
|
||||||
|
|
||||||
// —— 发票抬头
|
|
||||||
const titles = ref<InvoiceTitle[]>([])
|
const titles = ref<InvoiceTitle[]>([])
|
||||||
|
|
||||||
// —— 统计
|
|
||||||
async function fetchStats(period?: string): Promise<void> {
|
async function fetchStats(period?: string): Promise<void> {
|
||||||
stats.value = await getFinanceStats(period)
|
stats.value = await getFinanceStats(period)
|
||||||
}
|
}
|
||||||
|
|
||||||
// —— 账单
|
|
||||||
async function fetchList(): Promise<void> {
|
async function fetchList(): Promise<void> {
|
||||||
const pageData: PageResult<Bill> = await getBillList(query.value)
|
const pageData: PageResult<Bill> = await getBillList(query.value)
|
||||||
list.value = pageData.list
|
list.value = pageData.list
|
||||||
@@ -89,13 +78,11 @@ export const useFinanceStore = defineStore('finance', () => {
|
|||||||
query.value = next
|
query.value = next
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 确定筛选(重置页码后取数) */
|
|
||||||
async function search(): Promise<void> {
|
async function search(): Promise<void> {
|
||||||
query.value = { ...query.value, page: 1 }
|
query.value = { ...query.value, page: 1 }
|
||||||
await fetchList()
|
await fetchList()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 重置全部条件 */
|
|
||||||
async function resetQuery(): Promise<void> {
|
async function resetQuery(): Promise<void> {
|
||||||
query.value = createDefaultBillQuery()
|
query.value = createDefaultBillQuery()
|
||||||
await fetchList()
|
await fetchList()
|
||||||
@@ -110,12 +97,10 @@ export const useFinanceStore = defineStore('finance', () => {
|
|||||||
currentBill.value = await getBillDetail(id)
|
currentBill.value = await getBillDetail(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// —— 支付
|
|
||||||
async function createPayment(billId: string, method: PayMethod): Promise<void> {
|
async function createPayment(billId: string, method: PayMethod): Promise<void> {
|
||||||
payment.value = await createPaymentApi(billId, method)
|
payment.value = await createPaymentApi(billId, method)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 切换支付方式(刷新二维码) */
|
|
||||||
async function switchPaymentMethod(billId: string, method: PayMethod): Promise<void> {
|
async function switchPaymentMethod(billId: string, method: PayMethod): Promise<void> {
|
||||||
payment.value = await switchPaymentMethodApi(billId, method)
|
payment.value = await switchPaymentMethodApi(billId, method)
|
||||||
}
|
}
|
||||||
@@ -126,15 +111,6 @@ export const useFinanceStore = defineStore('finance', () => {
|
|||||||
: await getPayResult(billId)
|
: await getPayResult(billId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// —— 收银台(确认支付)
|
|
||||||
/**
|
|
||||||
* 拉取收银台订单明细。
|
|
||||||
*
|
|
||||||
* <p>入参是**需求 id**(项目详情路由 `:id`):数据走项目详情那个现成接口
|
|
||||||
* `GET /web/requirementPool/get/finance/{需求id}` 的 `billPlans`,不另开端点。
|
|
||||||
* `ids` 为可选的期次 id(`vm_bill_plan.id`),用于精确指定付哪一期;
|
|
||||||
* 不传则取该需求下状态为「待支付」的期。</p>
|
|
||||||
*/
|
|
||||||
async function loadCheckout(requirementId: string, ids: string[] = []): Promise<void> {
|
async function loadCheckout(requirementId: string, ids: string[] = []): Promise<void> {
|
||||||
checkoutLoading.value = true
|
checkoutLoading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -144,27 +120,18 @@ export const useFinanceStore = defineStore('finance', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 微信 Native 下单(拿到 codeUrl 后由页面渲染二维码)。
|
|
||||||
*
|
|
||||||
* @param billIds 本笔支付覆盖的账单 id。后端据此落 `vm_finance_payment` 并把它写进
|
|
||||||
* `request_payload`,支付成功回调再为每张账单落 `vm_finance_payment_bill`。
|
|
||||||
*/
|
|
||||||
async function payByWechatNative(orderNo: string, amount: number, billIds: string[]): Promise<void> {
|
async function payByWechatNative(orderNo: string, amount: number, billIds: string[]): Promise<void> {
|
||||||
payOrder.value = await createWechatNativeOrderApi(orderNo, amount, billIds)
|
payOrder.value = await createWechatNativeOrderApi(orderNo, amount, billIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 支付宝电脑网站支付下单(拿到跳转地址后由页面跳转) */
|
|
||||||
async function payByAlipayPage(orderNo: string, amount: number, billIds: string[]): Promise<void> {
|
async function payByAlipayPage(orderNo: string, amount: number, billIds: string[]): Promise<void> {
|
||||||
payOrder.value = await createAlipayPageOrderApi(orderNo, amount, billIds)
|
payOrder.value = await createAlipayPageOrderApi(orderNo, amount, billIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 清空下单响应(关闭二维码弹窗时调用) */
|
|
||||||
function clearPayOrder(): void {
|
function clearPayOrder(): void {
|
||||||
payOrder.value = null
|
payOrder.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// —— 发票
|
|
||||||
async function fetchInvoices(): Promise<void> {
|
async function fetchInvoices(): Promise<void> {
|
||||||
const pageData: PageResult<Invoice> = await getInvoiceList(invoiceQuery.value)
|
const pageData: PageResult<Invoice> = await getInvoiceList(invoiceQuery.value)
|
||||||
invoices.value = pageData.list
|
invoices.value = pageData.list
|
||||||
@@ -175,7 +142,6 @@ export const useFinanceStore = defineStore('finance', () => {
|
|||||||
invoiceQuery.value = next
|
invoiceQuery.value = next
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 发票确定筛选(重置页码后取数) */
|
|
||||||
async function searchInvoices(): Promise<void> {
|
async function searchInvoices(): Promise<void> {
|
||||||
invoiceQuery.value = { ...invoiceQuery.value, page: 1 }
|
invoiceQuery.value = { ...invoiceQuery.value, page: 1 }
|
||||||
await fetchInvoices()
|
await fetchInvoices()
|
||||||
@@ -186,14 +152,12 @@ export const useFinanceStore = defineStore('finance', () => {
|
|||||||
await fetchInvoices()
|
await fetchInvoices()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 申请开票(状态 → 开票中) */
|
|
||||||
async function applyInvoice(id: string): Promise<void> {
|
async function applyInvoice(id: string): Promise<void> {
|
||||||
const updated = await applyInvoiceApi(id)
|
const updated = await applyInvoiceApi(id)
|
||||||
const index = invoices.value.findIndex((item) => item.id === id)
|
const index = invoices.value.findIndex((item) => item.id === id)
|
||||||
if (index >= 0) invoices.value[index] = updated
|
if (index >= 0) invoices.value[index] = updated
|
||||||
}
|
}
|
||||||
|
|
||||||
// —— 发票抬头
|
|
||||||
async function fetchTitles(): Promise<void> {
|
async function fetchTitles(): Promise<void> {
|
||||||
titles.value = await getInvoiceTitles()
|
titles.value = await getInvoiceTitles()
|
||||||
}
|
}
|
||||||
@@ -217,7 +181,6 @@ export const useFinanceStore = defineStore('finance', () => {
|
|||||||
titles.value = await setDefaultTitleApi(id)
|
titles.value = await setDefaultTitleApi(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// —— 报表导出
|
|
||||||
async function exportReport(): Promise<void> {
|
async function exportReport(): Promise<void> {
|
||||||
await exportReportApi()
|
await exportReportApi()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,20 +28,6 @@ import { toCase } from '@/utils/caseVo'
|
|||||||
import { formatBusinessType, toBusinessType } from '@/utils/format'
|
import { formatBusinessType, toBusinessType } from '@/utils/format'
|
||||||
import { useCasesStore } from '@/stores/modules/cases'
|
import { useCasesStore } from '@/stores/modules/cases'
|
||||||
|
|
||||||
/**
|
|
||||||
* 首页数据(数据层:取数 + 状态 + 后端 VO → 页面类型映射,页面只调本 store)
|
|
||||||
*
|
|
||||||
* 后端接口:
|
|
||||||
* - UserSideWebHomePageController(前缀 /web/home-page):看板、业务服务线、业务详情
|
|
||||||
* - ProjectCaseUserController 的 /web/projectCase/mostViewed:案例库精选
|
|
||||||
*
|
|
||||||
* 页面/组件消费的类型(BusinessLine / Case / HomeStats)保持不变,
|
|
||||||
* 后端 VO 与页面字段的差异(字段名、业务类型取值、富文本、数值单位)全部在本层对齐。
|
|
||||||
*/
|
|
||||||
|
|
||||||
// 业务类型归一化(toBusinessType)已抽到 `@/utils/format`,首页与案例库共用。
|
|
||||||
|
|
||||||
/** 富文本转纯文本(服务内容区以 <pre> 展示,需去掉 HTML 标签,避免渲染出标签本身) */
|
|
||||||
function stripHtml(html: string): string {
|
function stripHtml(html: string): string {
|
||||||
return html
|
return html
|
||||||
.replace(/<br\s*\/?>/gi, '\n')
|
.replace(/<br\s*\/?>/gi, '\n')
|
||||||
@@ -55,7 +41,6 @@ function stripHtml(html: string): string {
|
|||||||
.trim()
|
.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 后端轮播 VO → 页面 Banner(仅启用项,按 sort 升序) */
|
|
||||||
function toBanners(list: BannerVo[]): Banner[] {
|
function toBanners(list: BannerVo[]): Banner[] {
|
||||||
return list
|
return list
|
||||||
.filter((item) => item.enabled !== false && Boolean(item.imageUrl))
|
.filter((item) => item.enabled !== false && Boolean(item.imageUrl))
|
||||||
@@ -66,7 +51,6 @@ function toBanners(list: BannerVo[]): Banner[] {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 后端「项目提报」→ 业务线卡片 / 业务详情 */
|
|
||||||
function toBusinessLine(vo: ProjectSubmissionVo): BusinessLine {
|
function toBusinessLine(vo: ProjectSubmissionVo): BusinessLine {
|
||||||
const serviceTypes: ServiceType[] = (vo.serviceTypes ?? []).map((item) => ({
|
const serviceTypes: ServiceType[] = (vo.serviceTypes ?? []).map((item) => ({
|
||||||
label: item.serviceName ?? '',
|
label: item.serviceName ?? '',
|
||||||
@@ -77,7 +61,6 @@ function toBusinessLine(vo: ProjectSubmissionVo): BusinessLine {
|
|||||||
.map((item) => stripHtml(item.content ?? ''))
|
.map((item) => stripHtml(item.content ?? ''))
|
||||||
.filter((text) => text.length > 0)
|
.filter((text) => text.length > 0)
|
||||||
.join('\n\n')
|
.join('\n\n')
|
||||||
// 列表接口的「项目背景」富文本按条映射为服务内容区块(保留 HTML 原文,详情页逐块渲染)
|
|
||||||
const contents: ServiceContentBlock[] = (vo.backgrounds ?? [])
|
const contents: ServiceContentBlock[] = (vo.backgrounds ?? [])
|
||||||
.map((item) => ({ title: '', content: item.content ?? '' }))
|
.map((item) => ({ title: '', content: item.content ?? '' }))
|
||||||
.filter((block) => block.content.trim().length > 0)
|
.filter((block) => block.content.trim().length > 0)
|
||||||
@@ -95,7 +78,6 @@ function toBusinessLine(vo: ProjectSubmissionVo): BusinessLine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 后端「业务详情聚合 VO」→ 业务线(主表单对象;服务类型 / 附件 / 富文本目录均为一对多 List,需按数组下钻读取) */
|
|
||||||
function toBusinessLineFromInfo(info: ProjectSubmissionInfo): BusinessLine {
|
function toBusinessLineFromInfo(info: ProjectSubmissionInfo): BusinessLine {
|
||||||
const ps = info.projectSubmission ?? ({} as ProjectSubmissionFlat)
|
const ps = info.projectSubmission ?? ({} as ProjectSubmissionFlat)
|
||||||
const serviceTypes: ServiceType[] = (info.projectServiceType ?? []).map((item) => ({
|
const serviceTypes: ServiceType[] = (info.projectServiceType ?? []).map((item) => ({
|
||||||
@@ -107,7 +89,6 @@ function toBusinessLineFromInfo(info: ProjectSubmissionInfo): BusinessLine {
|
|||||||
.map((item) => stripHtml(item.content ?? ''))
|
.map((item) => stripHtml(item.content ?? ''))
|
||||||
.filter((text) => text.length > 0)
|
.filter((text) => text.length > 0)
|
||||||
.join('\n\n')
|
.join('\n\n')
|
||||||
// 一、二、三…每条富文本目录 → 一个「服务内容」区块(保留 HTML,含内嵌图片/视频)
|
|
||||||
const contents: ServiceContentBlock[] = (info.projectSubmissionCatalog ?? [])
|
const contents: ServiceContentBlock[] = (info.projectSubmissionCatalog ?? [])
|
||||||
.map((item) => ({ title: item.title ?? '', content: item.content ?? '' }))
|
.map((item) => ({ title: item.title ?? '', content: item.content ?? '' }))
|
||||||
.filter((block) => block.content.trim().length > 0)
|
.filter((block) => block.content.trim().length > 0)
|
||||||
@@ -125,9 +106,6 @@ function toBusinessLineFromInfo(info: ProjectSubmissionInfo): BusinessLine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 后端案例 VO → 页面 Case 的映射已抽到 `@/utils/caseVo`(首页与案例库共用)。
|
|
||||||
|
|
||||||
/** 未登录时后端看板返回 null,这里兜底为零值 */
|
|
||||||
const EMPTY_STATUS: ProjectStatusCount = {
|
const EMPTY_STATUS: ProjectStatusCount = {
|
||||||
projectCount: 0,
|
projectCount: 0,
|
||||||
ongoingProjectCount: 0,
|
ongoingProjectCount: 0,
|
||||||
@@ -135,12 +113,6 @@ const EMPTY_STATUS: ProjectStatusCount = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useHomeStore = defineStore('home', () => {
|
export const useHomeStore = defineStore('home', () => {
|
||||||
/**
|
|
||||||
* 案例类型字典(提报 id → 类型名)由案例库 store 提供。
|
|
||||||
*
|
|
||||||
* <p>案例表 business_type 列存的是提报主键 id,精选案例标签要翻译成名称就得查这份字典,
|
|
||||||
* 故首页与案例库共用同一份(见 casesStore.ensureCategories)。</p>
|
|
||||||
*/
|
|
||||||
const casesStore = useCasesStore()
|
const casesStore = useCasesStore()
|
||||||
|
|
||||||
const stats = ref<HomeStats>({ pending: 0, inProgress: 0, total: 0 })
|
const stats = ref<HomeStats>({ pending: 0, inProgress: 0, total: 0 })
|
||||||
@@ -149,31 +121,20 @@ export const useHomeStore = defineStore('home', () => {
|
|||||||
const featuredCases = ref<Case[]>([])
|
const featuredCases = ref<Case[]>([])
|
||||||
const currentBusiness = ref<BusinessLine | null>(null)
|
const currentBusiness = ref<BusinessLine | null>(null)
|
||||||
|
|
||||||
/** 提报原始列表(内部缓存):业务线卡片与详情共用,详情需按提报主键 id 取数 */
|
|
||||||
const submissions = ref<ProjectSubmissionVo[]>([])
|
const submissions = ref<ProjectSubmissionVo[]>([])
|
||||||
|
|
||||||
/** 门户入口原始列表(含快捷 / 其他两组,由 groupType 区分) */
|
|
||||||
const portalEntries = ref<PortalEntryVo[]>([])
|
const portalEntries = ref<PortalEntryVo[]>([])
|
||||||
|
|
||||||
/** 门户入口是否已成功取到(失败时组件回退到内置默认入口) */
|
|
||||||
const portalEntriesLoaded = ref(false)
|
const portalEntriesLoaded = ref(false)
|
||||||
|
|
||||||
/** 快捷入口组(groupType = 1) */
|
|
||||||
const quickEntries = computed(() =>
|
const quickEntries = computed(() =>
|
||||||
portalEntries.value.filter((item) => item.groupType === '1'),
|
portalEntries.value.filter((item) => item.groupType === '1'),
|
||||||
)
|
)
|
||||||
|
|
||||||
/** 其他入口组(groupType ≠ 1) */
|
|
||||||
const otherEntries = computed(() =>
|
const otherEntries = computed(() =>
|
||||||
portalEntries.value.filter((item) => item.groupType !== '1'),
|
portalEntries.value.filter((item) => item.groupType !== '1'),
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* 拉取当前用户的门户入口。
|
|
||||||
*
|
|
||||||
* <p>该接口对游客(无登录态)与无权限用户不可用,失败时保持入口为空,
|
|
||||||
* 由调用方回退到默认入口,不影响首页其余数据。</p>
|
|
||||||
*/
|
|
||||||
async function fetchPortalEntries(): Promise<void> {
|
async function fetchPortalEntries(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const list = await getPortalEntryList()
|
const list = await getPortalEntryList()
|
||||||
@@ -185,27 +146,23 @@ export const useHomeStore = defineStore('home', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 快捷 / 其他入口互转,成功后重新拉取以同步后端排序 */
|
|
||||||
async function switchEntryGroup(id: number, type: PortalEntryGroup): Promise<void> {
|
async function switchEntryGroup(id: number, type: PortalEntryGroup): Promise<void> {
|
||||||
await switchPortalEntryGroupApi(id, type)
|
await switchPortalEntryGroupApi(id, type)
|
||||||
await fetchPortalEntries()
|
await fetchPortalEntries()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 确保提报列表已加载(业务详情页可能被直接打开,此时列表尚未取过) */
|
|
||||||
async function ensureSubmissions(): Promise<void> {
|
async function ensureSubmissions(): Promise<void> {
|
||||||
if (submissions.value.length === 0) {
|
if (submissions.value.length === 0) {
|
||||||
submissions.value = await getServiceTypeList()
|
submissions.value = await getServiceTypeList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 首页初始化:看板 + 轮播图 + 业务服务线 + 案例精选 */
|
|
||||||
async function fetchHomeData(): Promise<void> {
|
async function fetchHomeData(): Promise<void> {
|
||||||
const [statusCount, submissionList, cases, carouselList] = await Promise.all([
|
const [statusCount, submissionList, cases, carouselList] = await Promise.all([
|
||||||
getProjectStatusCount(),
|
getProjectStatusCount(),
|
||||||
getServiceTypeList(),
|
getServiceTypeList(),
|
||||||
getFeaturedCasesApi(),
|
getFeaturedCasesApi(),
|
||||||
getCarouselList().catch(() => [] as BannerVo[]),
|
getCarouselList().catch(() => [] as BannerVo[]),
|
||||||
// 案例标签需要「提报 id → 类型名」字典,与首页其余数据并行取(幂等,已加载则不发请求)
|
|
||||||
casesStore.ensureCategories(),
|
casesStore.ensureCategories(),
|
||||||
])
|
])
|
||||||
const status = statusCount ?? EMPTY_STATUS
|
const status = statusCount ?? EMPTY_STATUS
|
||||||
@@ -218,17 +175,9 @@ export const useHomeStore = defineStore('home', () => {
|
|||||||
businessLines.value = submissionList.map(toBusinessLine)
|
businessLines.value = submissionList.map(toBusinessLine)
|
||||||
featuredCases.value = cases.map((vo) => toCase(vo, casesStore.typeNameById))
|
featuredCases.value = cases.map((vo) => toCase(vo, casesStore.typeNameById))
|
||||||
banners.value = toBanners(carouselList ?? [])
|
banners.value = toBanners(carouselList ?? [])
|
||||||
// 入口个性化接口失败已被内部吞掉,不会影响上面的首页数据
|
|
||||||
await fetchPortalEntries()
|
await fetchPortalEntries()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 按业务类型切换案例精选。
|
|
||||||
*
|
|
||||||
* <p>后端 selectByType 按案例表 business_type 列的**原值**精确匹配,而该列实测存的是
|
|
||||||
* **提报主键 id**,故这里用 BusinessType 名称经 /categories 字典反查提报 id 再下发;
|
|
||||||
* 字典里查不到(提报表未登记该类型)时直接置空,不发无效请求。</p>
|
|
||||||
*/
|
|
||||||
async function fetchFeaturedCases(category: BusinessType | 'all' = 'all'): Promise<void> {
|
async function fetchFeaturedCases(category: BusinessType | 'all' = 'all'): Promise<void> {
|
||||||
if (category === 'all') {
|
if (category === 'all') {
|
||||||
featuredCases.value = (await getFeaturedCasesApi()).map((vo) => toCase(vo, casesStore.typeNameById))
|
featuredCases.value = (await getFeaturedCasesApi()).map((vo) => toCase(vo, casesStore.typeNameById))
|
||||||
@@ -244,7 +193,6 @@ export const useHomeStore = defineStore('home', () => {
|
|||||||
featuredCases.value = (await getFeaturedCasesApi(typeId)).map((vo) => toCase(vo, casesStore.typeNameById))
|
featuredCases.value = (await getFeaturedCasesApi(typeId)).map((vo) => toCase(vo, casesStore.typeNameById))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 业务详情:前端 BusinessType 反查提报主键后取详情 */
|
|
||||||
async function fetchBusinessDetail(type: BusinessType): Promise<void> {
|
async function fetchBusinessDetail(type: BusinessType): Promise<void> {
|
||||||
await ensureSubmissions()
|
await ensureSubmissions()
|
||||||
const target = submissions.value.find((item) => toBusinessType(item.businessType) === type)
|
const target = submissions.value.find((item) => toBusinessType(item.businessType) === type)
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
markRead as markReadApi,
|
markRead as markReadApi,
|
||||||
} from '@/api/message'
|
} from '@/api/message'
|
||||||
|
|
||||||
/** tab 计数结构 */
|
|
||||||
export interface MessageTabCounts {
|
export interface MessageTabCounts {
|
||||||
all: number
|
all: number
|
||||||
在线客服: number
|
在线客服: number
|
||||||
@@ -29,7 +28,6 @@ function createDefaultQuery(): MessageQuery {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 消息中心数据(列表 / 筛选 / 分页 / 已读) */
|
|
||||||
export const useMessageStore = defineStore('message', () => {
|
export const useMessageStore = defineStore('message', () => {
|
||||||
const query = ref<MessageQuery>(createDefaultQuery())
|
const query = ref<MessageQuery>(createDefaultQuery())
|
||||||
const list = ref<Message[]>([])
|
const list = ref<Message[]>([])
|
||||||
@@ -59,13 +57,11 @@ export const useMessageStore = defineStore('message', () => {
|
|||||||
query.value = next
|
query.value = next
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 确定搜索(重置页码后取数) */
|
|
||||||
async function search(): Promise<void> {
|
async function search(): Promise<void> {
|
||||||
query.value = { ...query.value, page: 1 }
|
query.value = { ...query.value, page: 1 }
|
||||||
await fetchList()
|
await fetchList()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 重置全部条件 */
|
|
||||||
async function resetQuery(): Promise<void> {
|
async function resetQuery(): Promise<void> {
|
||||||
query.value = createDefaultQuery()
|
query.value = createDefaultQuery()
|
||||||
await fetchList()
|
await fetchList()
|
||||||
@@ -76,7 +72,6 @@ export const useMessageStore = defineStore('message', () => {
|
|||||||
await fetchList()
|
await fetchList()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 全部标记已读 */
|
|
||||||
async function markAllRead(): Promise<void> {
|
async function markAllRead(): Promise<void> {
|
||||||
await markAllReadApi()
|
await markAllReadApi()
|
||||||
ElMessage.success('已全部标记为已读')
|
ElMessage.success('已全部标记为已读')
|
||||||
|
|||||||
@@ -4,13 +4,6 @@ import { fetchOssPreviewBlob } from '@/api/oss'
|
|||||||
import { resolvePreviewKind, PREVIEW_TEXT_MAX_BYTES, type PreviewKind } from '@/utils/filePreview'
|
import { resolvePreviewKind, PREVIEW_TEXT_MAX_BYTES, type PreviewKind } from '@/utils/filePreview'
|
||||||
import type { FileInfo } from '@/types/api'
|
import type { FileInfo } from '@/types/api'
|
||||||
|
|
||||||
/**
|
|
||||||
* OSS 文件预览全局状态。
|
|
||||||
*
|
|
||||||
* <p>所有 `FileList` 的「预览」统一走这里,弹一个全局 `FilePreviewDialog`:
|
|
||||||
* 有 `ossId` 的文件经同源后端接口取 blob 内联渲染(绕开桶强制下载);
|
|
||||||
* 无 `ossId` 的图片可退化为公网直链(子资源渲染不受强制下载影响),其余提示「不支持在线预览」。</p>
|
|
||||||
*/
|
|
||||||
export const useOssStore = defineStore('oss', () => {
|
export const useOssStore = defineStore('oss', () => {
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
const file = ref<FileInfo | null>(null)
|
const file = ref<FileInfo | null>(null)
|
||||||
@@ -19,9 +12,7 @@ export const useOssStore = defineStore('oss', () => {
|
|||||||
const textContent = ref<string | null>(null)
|
const textContent = ref<string | null>(null)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
/** 文本类文件超过预览上限被截断渲染 */
|
|
||||||
const truncated = ref(false)
|
const truncated = ref(false)
|
||||||
/** 被截断文件的实际大小(字节) */
|
|
||||||
const totalBytes = ref(0)
|
const totalBytes = ref(0)
|
||||||
|
|
||||||
function revokeObjectUrl(): void {
|
function revokeObjectUrl(): void {
|
||||||
@@ -44,13 +35,11 @@ export const useOssStore = defineStore('oss', () => {
|
|||||||
const previewKind = resolvePreviewKind(target.name)
|
const previewKind = resolvePreviewKind(target.name)
|
||||||
kind.value = previewKind
|
kind.value = previewKind
|
||||||
|
|
||||||
// 不支持内联渲染(office / 未知类型):直接提示,等用户下载
|
|
||||||
if (previewKind === 'unsupported') {
|
if (previewKind === 'unsupported') {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 无 ossId:图片可走公网直链(<img> 子资源渲染不受强制下载影响),其余无法预览
|
|
||||||
if (target.ossId === undefined || target.ossId === null) {
|
if (target.ossId === undefined || target.ossId === null) {
|
||||||
if (previewKind === 'image' && target.url) {
|
if (previewKind === 'image' && target.url) {
|
||||||
objectUrl.value = target.url
|
objectUrl.value = target.url
|
||||||
@@ -65,8 +54,6 @@ export const useOssStore = defineStore('oss', () => {
|
|||||||
try {
|
try {
|
||||||
const blob = await fetchOssPreviewBlob(target.ossId)
|
const blob = await fetchOssPreviewBlob(target.ossId)
|
||||||
if (previewKind === 'text') {
|
if (previewKind === 'text') {
|
||||||
// 整文件塞进 <pre> 会卡死浏览器(如 5MB+ 的 .sql/.log),
|
|
||||||
// 仅读取前 PREVIEW_TEXT_MAX_BYTES 渲染,超出截断并提示下载
|
|
||||||
const total = blob.size
|
const total = blob.size
|
||||||
if (total > PREVIEW_TEXT_MAX_BYTES) {
|
if (total > PREVIEW_TEXT_MAX_BYTES) {
|
||||||
textContent.value = await blob.slice(0, PREVIEW_TEXT_MAX_BYTES).text()
|
textContent.value = await blob.slice(0, PREVIEW_TEXT_MAX_BYTES).text()
|
||||||
@@ -76,7 +63,6 @@ export const useOssStore = defineStore('oss', () => {
|
|||||||
textContent.value = await blob.text()
|
textContent.value = await blob.text()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 图片 / PDF / 音视频由浏览器原生处理,5MB 量级毫无压力
|
|
||||||
objectUrl.value = URL.createObjectURL(blob)
|
objectUrl.value = URL.createObjectURL(blob)
|
||||||
}
|
}
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
|||||||
@@ -51,9 +51,7 @@ function createDefaultFavoritesQuery(): FavoritesQuery {
|
|||||||
return { page: 1, pageSize: 8, keyword: '', category: 'all' }
|
return { page: 1, pageSize: 8, keyword: '', category: 'all' }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 个人中心数据(资料/安全/地址/收藏/资质/反馈/帮助) */
|
|
||||||
export const useProfileStore = defineStore('profile', () => {
|
export const useProfileStore = defineStore('profile', () => {
|
||||||
// ── 账号资料 ──
|
|
||||||
const profile = ref<Profile | null>(null)
|
const profile = ref<Profile | null>(null)
|
||||||
|
|
||||||
async function fetchProfile(): Promise<void> {
|
async function fetchProfile(): Promise<void> {
|
||||||
@@ -65,7 +63,6 @@ export const useProfileStore = defineStore('profile', () => {
|
|||||||
ElMessage.success('昵称修改成功')
|
ElMessage.success('昵称修改成功')
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 账号安全 ──
|
|
||||||
async function changePasswordAction(params: ChangePasswordParams): Promise<void> {
|
async function changePasswordAction(params: ChangePasswordParams): Promise<void> {
|
||||||
await changePassword(params)
|
await changePassword(params)
|
||||||
profile.value = await getProfile()
|
profile.value = await getProfile()
|
||||||
@@ -97,7 +94,6 @@ export const useProfileStore = defineStore('profile', () => {
|
|||||||
ElMessage.success('注销成功')
|
ElMessage.success('注销成功')
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 收货地址 ──
|
|
||||||
const addresses = ref<Address[]>([])
|
const addresses = ref<Address[]>([])
|
||||||
const addressTotal = ref(0)
|
const addressTotal = ref(0)
|
||||||
const addressQuery = ref<ListQuery>(createDefaultAddressQuery())
|
const addressQuery = ref<ListQuery>(createDefaultAddressQuery())
|
||||||
@@ -138,7 +134,6 @@ export const useProfileStore = defineStore('profile', () => {
|
|||||||
await fetchAddresses()
|
await fetchAddresses()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 我的收藏 ──
|
|
||||||
const favorites = ref<Case[]>([])
|
const favorites = ref<Case[]>([])
|
||||||
const favoritesTotal = ref(0)
|
const favoritesTotal = ref(0)
|
||||||
const favoritesCounts = ref<Partial<Record<BusinessType, number>>>({})
|
const favoritesCounts = ref<Partial<Record<BusinessType, number>>>({})
|
||||||
@@ -169,7 +164,6 @@ export const useProfileStore = defineStore('profile', () => {
|
|||||||
await fetchFavorites()
|
await fetchFavorites()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 资质管理 ──
|
|
||||||
const qualifications = ref<Qualification[]>([])
|
const qualifications = ref<Qualification[]>([])
|
||||||
const qualificationTotal = ref(0)
|
const qualificationTotal = ref(0)
|
||||||
const qualificationQuery = ref<ListQuery>(createDefaultQualificationQuery())
|
const qualificationQuery = ref<ListQuery>(createDefaultQualificationQuery())
|
||||||
@@ -204,7 +198,6 @@ export const useProfileStore = defineStore('profile', () => {
|
|||||||
await fetchQualifications()
|
await fetchQualifications()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 意见反馈 / 帮助中心 ──
|
|
||||||
async function submitFeedbackAction(feedback: Feedback): Promise<void> {
|
async function submitFeedbackAction(feedback: Feedback): Promise<void> {
|
||||||
await submitFeedbackApi(feedback)
|
await submitFeedbackApi(feedback)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,12 +37,6 @@ function createDefaultQuery(): ProjectQuery {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 业务类型件数 —— 由 `getFilterConditions` 的统计构建(**全量口径**,不随筛选 / 分页变化)。
|
|
||||||
*
|
|
||||||
* <p>「全部」的件数取统计里的 `total`(该用户需求总数);统计拿不到时退回列表 `total`,
|
|
||||||
* 这种降级下只会渲染出「全部」一行。</p>
|
|
||||||
*/
|
|
||||||
function buildTypeCounts(
|
function buildTypeCounts(
|
||||||
statistics: ProjectStatistics | null | undefined,
|
statistics: ProjectStatistics | null | undefined,
|
||||||
total: number,
|
total: number,
|
||||||
@@ -58,49 +52,26 @@ function buildTypeCounts(
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 项目中心数据(列表 + 详情走真实接口;文件 / 售后仍是 mock,后端无对应接口) */
|
|
||||||
export const useProjectStore = defineStore('project', () => {
|
export const useProjectStore = defineStore('project', () => {
|
||||||
const query = ref<ProjectQuery>(createDefaultQuery())
|
const query = ref<ProjectQuery>(createDefaultQuery())
|
||||||
/** 列表行(后端 RdProjectVo;**不是**详情那套 mock 的 Project) */
|
|
||||||
const list = ref<RdProjectVo[]>([])
|
const list = ref<RdProjectVo[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const current = ref<Project | null>(null)
|
const current = ref<Project | null>(null)
|
||||||
/** 业务类型件数(左侧筛选栏用;由 `ensureFilterConditions` 单独拉取,全量口径) */
|
|
||||||
const typeCounts = ref<ProjectTypeCount[]>([{ value: 'all', label: '全部', count: 0 }])
|
const typeCounts = ref<ProjectTypeCount[]>([{ value: 'all', label: '全部', count: 0 }])
|
||||||
/** 业务类型筛选条件是否已拉过(值域在运行期不变,进页面拉一次即可) */
|
|
||||||
let filterConditionsLoaded = false
|
let filterConditionsLoaded = false
|
||||||
|
|
||||||
/**
|
|
||||||
* 已完成项目(**申请售后页「项目名称」下拉的候选**)。
|
|
||||||
*
|
|
||||||
* <p>口径见 `api/project.ts#getCompletedProjects`:自己名下已立项且状态为 COMPLETED 的项目,
|
|
||||||
* 与后端「只有已完成的项目可以申请售后」的准入校验一致。行的 `id` 就是申请接口要传的需求 id。</p>
|
|
||||||
*
|
|
||||||
* <p>与业务类型的筛选条件不同,这个值域**会随项目状态变化**,所以不缓存、
|
|
||||||
* 每次进申请页都重拉(见 {@link fetchCompletedProjects})。</p>
|
|
||||||
*/
|
|
||||||
const completedProjects = ref<RdProjectVo[]>([])
|
const completedProjects = ref<RdProjectVo[]>([])
|
||||||
|
|
||||||
/** 详情原始 bundle(4 支接口的响应);切页签时按页签替换其中一支后重新映射 */
|
|
||||||
const detailBundle = ref<RequirementDetailBundle | null>(null)
|
const detailBundle = ref<RequirementDetailBundle | null>(null)
|
||||||
/** 详情请求序号:需求 id 一变就自增,用来丢弃在途的「上一个需求」的响应 */
|
|
||||||
let detailSeq = 0
|
let detailSeq = 0
|
||||||
/** 各页签自己的请求序号:同一页签连点只应用最后一次响应 */
|
|
||||||
const tabSeq: Record<ProjectDetailTab, number> = {
|
const tabSeq: Record<ProjectDetailTab, number> = {
|
||||||
progress: 0,
|
progress: 0,
|
||||||
files: 0,
|
files: 0,
|
||||||
finance: 0,
|
finance: 0,
|
||||||
afterSales: 0,
|
afterSales: 0,
|
||||||
}
|
}
|
||||||
/** 正在刷新的页签('' = 无);页签按钮据此显示加载态 */
|
|
||||||
const loadingTab = ref<ProjectDetailTab | ''>('')
|
const loadingTab = ref<ProjectDetailTab | ''>('')
|
||||||
|
|
||||||
/**
|
|
||||||
* 取业务类型筛选条件(左侧筛选栏「业务类型」的值域 + 件数)。
|
|
||||||
*
|
|
||||||
* <p>幂等:成功拉到一次后不再重复请求(值域是全量口径,与当前筛选条件无关)。
|
|
||||||
* 失败时不标记已加载,下次进页面仍会重试;期间保留上一次结果,首屏即失败则只有「全部」一行。</p>
|
|
||||||
*/
|
|
||||||
async function ensureFilterConditions(): Promise<void> {
|
async function ensureFilterConditions(): Promise<void> {
|
||||||
if (filterConditionsLoaded) return
|
if (filterConditionsLoaded) return
|
||||||
try {
|
try {
|
||||||
@@ -112,13 +83,6 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 取列表(服务端筛选 + 分页;失败向上抛给调用方处理)。
|
|
||||||
*
|
|
||||||
* <p>⚠️ 这里**不**碰 `typeCounts`:`/list` 的 `requirementStatistics` 后端恒为 null,
|
|
||||||
* 拿它重建会把业务类型整列抹成只剩「全部」,且「全部」件数会变成筛选后的 total(偏小)。
|
|
||||||
* 业务类型的值域统一由 {@link ensureFilterConditions} 负责。</p>
|
|
||||||
*/
|
|
||||||
async function fetchList(): Promise<void> {
|
async function fetchList(): Promise<void> {
|
||||||
const result = await getProjectList(query.value)
|
const result = await getProjectList(query.value)
|
||||||
const page = result?.requirementPage
|
const page = result?.requirementPage
|
||||||
@@ -126,12 +90,6 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
total.value = page?.total ?? 0
|
total.value = page?.total ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 取「已完成项目」(申请售后页「项目名称」下拉的值域)。
|
|
||||||
*
|
|
||||||
* <p>失败只告警不清空:保留上一次的结果,页面下拉至少还有数据;首次即失败则为空数组,
|
|
||||||
* 申请页会退化成只显示当前项目的单项下拉(见 `after-sales.vue` 的兜底合并)。</p>
|
|
||||||
*/
|
|
||||||
async function fetchCompletedProjects(): Promise<void> {
|
async function fetchCompletedProjects(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
completedProjects.value = await getCompletedProjects()
|
completedProjects.value = await getCompletedProjects()
|
||||||
@@ -141,21 +99,11 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 取详情(**首屏**:4 支并行,一次到位)。
|
|
||||||
*
|
|
||||||
* <p>后端在「需求不存在」或「不属于当前用户」时 4 支都返回 `data: null` ⇒ 页面显示空态;
|
|
||||||
* 请求本身失败(如 500)也归为 null,不让异常冒到组件里。</p>
|
|
||||||
*
|
|
||||||
* <p>原始响应同时缓存进 `detailBundle`,之后切页签只替换其中一支(见 {@link refreshTab}),
|
|
||||||
* 不必重拉另外三支。</p>
|
|
||||||
*/
|
|
||||||
async function fetchDetail(id: string): Promise<void> {
|
async function fetchDetail(id: string): Promise<void> {
|
||||||
const seq = ++detailSeq
|
const seq = ++detailSeq
|
||||||
loadingTab.value = ''
|
loadingTab.value = ''
|
||||||
try {
|
try {
|
||||||
const bundle = await getProjectDetailBundle(id)
|
const bundle = await getProjectDetailBundle(id)
|
||||||
// 期间已切到别的需求 ⇒ 丢弃这次响应,别用旧项目覆盖新项目
|
|
||||||
if (seq !== detailSeq) return
|
if (seq !== detailSeq) return
|
||||||
if (!bundle) {
|
if (!bundle) {
|
||||||
detailBundle.value = null
|
detailBundle.value = null
|
||||||
@@ -173,17 +121,6 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 切页签刷新:**只调该页签对应的那一个接口**,拿到最新数据后落回 bundle 再重新映射。
|
|
||||||
*
|
|
||||||
* <p>后端 4 个页签各一个端点 ⇒ 「点一次页签 = 一次请求」,不需要重拉另外三支。
|
|
||||||
* 页签内容(里程碑 / 付款计划 / 附件 / 售后…)可能被别的入口改动,所以每次点击都重新取。</p>
|
|
||||||
*
|
|
||||||
* <p>并发保护两层:① 需求 id 变了(切项目)⇒ 丢弃在途响应;② 同一页签连点 ⇒ 只应用
|
|
||||||
* 最后一次响应(先到的慢响应被后到的覆盖)。</p>
|
|
||||||
*
|
|
||||||
* <p>后端返回 `null`(需求不存在 / 非本人)时**保留现有数据**,只告警、不清空页面。</p>
|
|
||||||
*/
|
|
||||||
async function refreshTab(tab: ProjectDetailTab): Promise<void> {
|
async function refreshTab(tab: ProjectDetailTab): Promise<void> {
|
||||||
const requestId = current.value?.id
|
const requestId = current.value?.id
|
||||||
if (!requestId) return
|
if (!requestId) return
|
||||||
@@ -223,7 +160,6 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
query.value = next
|
query.value = next
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 确定筛选 / 换页后的取数统一入口(失败只提示,不清空已有数据) */
|
|
||||||
async function load(): Promise<void> {
|
async function load(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await fetchList()
|
await fetchList()
|
||||||
@@ -233,13 +169,11 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 确定筛选(重置页码后取数) */
|
|
||||||
async function search(): Promise<void> {
|
async function search(): Promise<void> {
|
||||||
query.value = { ...query.value, pageNum: 1 }
|
query.value = { ...query.value, pageNum: 1 }
|
||||||
await load()
|
await load()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 重置全部条件 */
|
|
||||||
async function resetQuery(): Promise<void> {
|
async function resetQuery(): Promise<void> {
|
||||||
query.value = createDefaultQuery()
|
query.value = createDefaultQuery()
|
||||||
await load()
|
await load()
|
||||||
@@ -250,31 +184,16 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
await load()
|
await load()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 上传售后附件(提交前的暂存步骤)。
|
|
||||||
*
|
|
||||||
* <p>只传 OSS、不落业务表;返回项带 `ossId`,由申请页收集后在提交时下发
|
|
||||||
* `attachmentOssIds`,后端再转存成售后附件。</p>
|
|
||||||
*/
|
|
||||||
async function uploadAfterSalesFile(file: File): Promise<AfterSalesAttachment> {
|
async function uploadAfterSalesFile(file: File): Promise<AfterSalesAttachment> {
|
||||||
return uploadAfterSalesFileApi(file)
|
return uploadAfterSalesFileApi(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 申请售后(成功后就地重拉「售后信息」页签)。
|
|
||||||
*
|
|
||||||
* <p>不拿返回值本地拼状态:售后状态与工单附件都由后端决定,本地拼容易和真实数据漂移。
|
|
||||||
* 接口失败(如「只有已完成的项目可以申请售后」)会抛出,由统一 request 层弹提示,
|
|
||||||
* 这里不做兜底吞异常。</p>
|
|
||||||
*/
|
|
||||||
async function submitAfterSales(id: string, payload: AfterSalesPayload): Promise<void> {
|
async function submitAfterSales(id: string, payload: AfterSalesPayload): Promise<void> {
|
||||||
await submitAfterSalesApi(id, payload)
|
await submitAfterSalesApi(id, payload)
|
||||||
ElMessage.success('售后申请已提交')
|
ElMessage.success('售后申请已提交')
|
||||||
// 详情页在场时同步刷新该页签;从申请页提交时会立刻跳回详情,那边 onMounted 会重新取数
|
|
||||||
if (current.value?.id === id) await refreshTab('afterSales')
|
if (current.value?.id === id) await refreshTab('afterSales')
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 撤回售后申请(成功后就地重拉「售后信息」页签) */
|
|
||||||
async function cancelAfterSales(id: string): Promise<void> {
|
async function cancelAfterSales(id: string): Promise<void> {
|
||||||
await cancelAfterSalesApi(id)
|
await cancelAfterSalesApi(id)
|
||||||
ElMessage.success('售后申请已撤回')
|
ElMessage.success('售后申请已撤回')
|
||||||
@@ -289,24 +208,18 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
typeCounts,
|
typeCounts,
|
||||||
ensureFilterConditions,
|
ensureFilterConditions,
|
||||||
fetchList,
|
fetchList,
|
||||||
/** 已完成项目(申请售后页「项目名称」下拉候选) */
|
|
||||||
completedProjects,
|
completedProjects,
|
||||||
fetchCompletedProjects,
|
fetchCompletedProjects,
|
||||||
fetchDetail,
|
fetchDetail,
|
||||||
/** 切页签刷新(每次点击都调用,只拉该页签那一支) */
|
|
||||||
refreshTab,
|
refreshTab,
|
||||||
/** 正在刷新的页签,供按钮显示加载态 */
|
|
||||||
loadingTab,
|
loadingTab,
|
||||||
setQuery,
|
setQuery,
|
||||||
load,
|
load,
|
||||||
search,
|
search,
|
||||||
resetQuery,
|
resetQuery,
|
||||||
changePage,
|
changePage,
|
||||||
/** 上传售后附件(申请页用) */
|
|
||||||
uploadAfterSalesFile,
|
uploadAfterSalesFile,
|
||||||
/** 申请售后(成功后自动刷新售后页签) */
|
|
||||||
submitAfterSales,
|
submitAfterSales,
|
||||||
/** 撤回售后申请(成功后自动刷新售后页签) */
|
|
||||||
cancelAfterSales,
|
cancelAfterSales,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
uploadChatFile,
|
uploadChatFile,
|
||||||
} from '@/api/service'
|
} from '@/api/service'
|
||||||
|
|
||||||
/** 在线客服会话数据(消息列表 / 发送状态) */
|
|
||||||
export const useServiceStore = defineStore('service', () => {
|
export const useServiceStore = defineStore('service', () => {
|
||||||
const messages = ref<ChatMessage[]>([])
|
const messages = ref<ChatMessage[]>([])
|
||||||
const sending = ref(false)
|
const sending = ref(false)
|
||||||
@@ -17,7 +16,6 @@ export const useServiceStore = defineStore('service', () => {
|
|||||||
messages.value = await getChatHistory()
|
messages.value = await getChatHistory()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 发送文本:先推入用户消息,600ms 后推入客服自动回复 */
|
|
||||||
async function send(content: string): Promise<void> {
|
async function send(content: string): Promise<void> {
|
||||||
const trimmed = content.trim()
|
const trimmed = content.trim()
|
||||||
if (!trimmed || sending.value) return
|
if (!trimmed || sending.value) return
|
||||||
@@ -37,7 +35,6 @@ export const useServiceStore = defineStore('service', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 发送文件卡片消息 */
|
|
||||||
async function sendFile(file: File): Promise<void> {
|
async function sendFile(file: File): Promise<void> {
|
||||||
if (sending.value) return
|
if (sending.value) return
|
||||||
sending.value = true
|
sending.value = true
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
/** 站点公共数据(站点配置、全局 loading),非页面级 */
|
|
||||||
export const useSiteStore = defineStore('site', () => {
|
export const useSiteStore = defineStore('site', () => {
|
||||||
const globalLoading = ref(false)
|
const globalLoading = ref(false)
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import {
|
|||||||
} from '@/api/auth'
|
} from '@/api/auth'
|
||||||
import { storage } from '@/utils/storage'
|
import { storage } from '@/utils/storage'
|
||||||
|
|
||||||
/** 用户登录态(M4:token 持久化 key 'token',路由守卫读取同一 key) */
|
|
||||||
export const useUserStore = defineStore('user', () => {
|
export const useUserStore = defineStore('user', () => {
|
||||||
const token = ref<string | null>(storage.get<string>('token'))
|
const token = ref<string | null>(storage.get<string>('token'))
|
||||||
const user = ref<User | null>(null)
|
const user = ref<User | null>(null)
|
||||||
@@ -26,7 +25,6 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
|
|
||||||
const isLoggedIn = computed(() => Boolean(token.value))
|
const isLoggedIn = computed(() => Boolean(token.value))
|
||||||
|
|
||||||
/** 发送短信验证码(保留原 mock 逻辑) */
|
|
||||||
async function sendSmsCode(phone: string): Promise<void> {
|
async function sendSmsCode(phone: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await sendSmsCodeApi(phone)
|
await sendSmsCodeApi(phone)
|
||||||
@@ -38,14 +36,12 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 登录成功统一落库(token 持久化) */
|
|
||||||
function applyLogin(payload: { token: string; user: User | null }): void {
|
function applyLogin(payload: { token: string; user: User | null }): void {
|
||||||
token.value = payload.token
|
token.value = payload.token
|
||||||
user.value = payload.user
|
user.value = payload.user
|
||||||
storage.set('token', payload.token)
|
storage.set('token', payload.token)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 账号密码登录(对接真实后端 /auth/login,返回 access_token) */
|
|
||||||
async function loginByAccount(params: LoginByAccountParams): Promise<boolean> {
|
async function loginByAccount(params: LoginByAccountParams): Promise<boolean> {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -63,7 +59,6 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 手机验证码登录(保留原 mock 逻辑) */
|
|
||||||
async function loginBySms(params: LoginBySmsParams): Promise<boolean> {
|
async function loginBySms(params: LoginBySmsParams): Promise<boolean> {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -81,7 +76,6 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 注册(对接真实后端 /auth/register,账号+密码,无验证码) */
|
|
||||||
async function register(params: { username: string; password: string; confirmPassword: string }): Promise<boolean> {
|
async function register(params: { username: string; password: string; confirmPassword: string }): Promise<boolean> {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -98,7 +92,6 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 重置密码(保留原 mock 逻辑) */
|
|
||||||
async function resetPassword(params: ResetPasswordParams): Promise<boolean> {
|
async function resetPassword(params: ResetPasswordParams): Promise<boolean> {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -115,7 +108,6 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 退出登录(清除持久化 token) */
|
|
||||||
function logout(): void {
|
function logout(): void {
|
||||||
token.value = null
|
token.value = null
|
||||||
user.value = null
|
user.value = null
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
// 通用工具类
|
|
||||||
|
|
||||||
// 内容容器:设计稿内容宽 1780(x70→1850,specs2 全量复核)
|
|
||||||
// 1920 视口下 padding 70 → 内容 1780;窄视口只收窄留白,不产生横向滚动。
|
|
||||||
// ⚠️ 不能用固定 70px 留白:窄视口下 1780+140 > 视口宽 → 横向滚动条。
|
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: @container-width;
|
max-width: @container-width;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
// 留白夹在 16 与 70 之间:(1920 − 1780) / 2 = 70
|
|
||||||
padding: 0
|
padding: 0
|
||||||
clamp(@spacing-base, calc((100% - @page-content) / 2), @page-gutter);
|
clamp(@spacing-base, calc((100% - @page-content) / 2), @page-gutter);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 首页容器:首页设计稿单独基准(内容 1680,留白 120)
|
|
||||||
.container--home {
|
.container--home {
|
||||||
padding: 0 @home-gutter;
|
padding: 0 @home-gutter;
|
||||||
|
|
||||||
@@ -21,14 +15,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ 登录 / 注册 / 找回密码(蓝湖设计稿 02·51·54·55) ============
|
|
||||||
// 三页共用外壳:左侧 784 品牌渐变面板 + 右侧白底表单区(表单块在右区内居中)。
|
|
||||||
// 设计稿锚点:面板 (0,0) 784×1080;主标 (257,304) 40/700;标语 (257,715) 22/600。
|
|
||||||
.auth-page {
|
.auth-page {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
// ⚠️ html 上有全局 zoom(@page-zoom):内部 100vh 会被再缩一次,
|
|
||||||
// 渲染高 = 100vh × zoom,故必须除以 zoom 才能铺满真实视口(否则底部留灰带)。
|
|
||||||
min-height: calc(100vh / @page-zoom);
|
min-height: calc(100vh / @page-zoom);
|
||||||
background: @white;
|
background: @white;
|
||||||
|
|
||||||
@@ -38,7 +27,7 @@
|
|||||||
|
|
||||||
&__brand {
|
&__brand {
|
||||||
.flex(column, flex-start, center);
|
.flex(column, flex-start, center);
|
||||||
flex: 0 0 @login-brand-width; // 35%,右栏 flex:1 自动占 65%
|
flex: 0 0 @login-brand-width;
|
||||||
padding: 304px 24px 0;
|
padding: 304px 24px 0;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
160deg,
|
160deg,
|
||||||
@@ -74,7 +63,6 @@
|
|||||||
color: @white;
|
color: @white;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 游客模式固定 24px(不再乘 @auth-font-scale)
|
|
||||||
&__guest {
|
&__guest {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 28px;
|
top: 28px;
|
||||||
@@ -90,7 +78,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 表单项在右侧区域内水平 + 垂直居中(不再按设计稿固定锚点定位)
|
|
||||||
&__main {
|
&__main {
|
||||||
.flex(column, center, center);
|
.flex(column, center, center);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -101,8 +88,6 @@
|
|||||||
&__card {
|
&__card {
|
||||||
.flex(column, flex-start, flex-start);
|
.flex(column, flex-start, flex-start);
|
||||||
width: 693px;
|
width: 693px;
|
||||||
// 标题 / 副标题 / 表单标签一律左对齐(设计稿 x1039)。
|
|
||||||
// 显式声明是为了不继承任何父级 text-align:center —— 三页(登录/注册/找回)共用。
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,9 +99,6 @@
|
|||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 老大要求:主标 / 副标在**表单列内**居中(设计稿原锚点 x1039 是左对齐)。
|
|
||||||
// 表单列 = margin-left:17px + 宽 680;这里用 padding-left:17px 把居中盒同步右移,
|
|
||||||
// 使两者的视觉中轴与表单列中轴重合(而不是与 693 宽的卡片中轴重合,否则会偏左 7px)。
|
|
||||||
&__title,
|
&__title,
|
||||||
&__subtitle {
|
&__subtitle {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
// 样式入口(main.ts 引入;variables/mixins 由 vite additionalData 自动注入)
|
|
||||||
@import './reset.less';
|
@import './reset.less';
|
||||||
@import './common.less';
|
@import './common.less';
|
||||||
|
|
||||||
// 全局视觉缩放:设计 1920 画布整体 ×0.67(变量见 variables.less @page-zoom)。
|
|
||||||
// zoom 由布局引擎重排,等价于屏幕逻辑分辨率放大 1/zoom,通栏/居中/滚动行为全部保持。
|
|
||||||
// ≤1286px 物理视口(1280 屏等)降到 0.66,保证 1920 逻辑内容仍无横向滚动。
|
|
||||||
html {
|
html {
|
||||||
zoom: @page-zoom;
|
zoom: @page-zoom;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// 混合宏(flex、文本省略、滚动条)
|
|
||||||
|
|
||||||
.flex(@direction: row; @justify: flex-start; @align: center) {
|
.flex(@direction: row; @justify: flex-start; @align: center) {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -36,8 +35,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 卡片区块标题(左侧圆头竖条 + 16px 半粗)
|
|
||||||
// 项目详情设计稿规格:竖条 5×20 圆头 @project-accent,标题 16px/600,竖条→文字间距 10px
|
|
||||||
.section-title() {
|
.section-title() {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: 15px;
|
padding-left: 15px;
|
||||||
@@ -59,7 +56,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 表单文本控件(输入框 / 文本域共用外观;需求提报页设计稿规格:白底 + 浅灰描边 + 4px 圆角)
|
|
||||||
.text-control() {
|
.text-control() {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0 @spacing-base;
|
padding: 0 @spacing-base;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// 样式重置
|
|
||||||
|
|
||||||
*,
|
*,
|
||||||
*::before,
|
*::before,
|
||||||
|
|||||||
@@ -1,48 +1,35 @@
|
|||||||
// ★ less 变量唯一来源(颜色/间距/字号/圆角)
|
|
||||||
// 数据来源:蓝湖 MCP 实测(lanhu pid=b0052b6c,43 张设计稿已全量提取到本地,规格见 specs2/)。
|
|
||||||
// 换算约定:**1:1 像素级还原**——设计画布 1920px。直接使用设计标注 px,不做任何缩放。
|
|
||||||
// ⚠️ 内容区宽度按页面分两档(43 张设计稿实测结论):
|
|
||||||
// · 常规页(案例库/需求提报/项目/合同/财务/消息/客服/个人中心):内容宽 **1780**,左右留白各 **70**
|
|
||||||
// · 首页:内容宽 **1680**,左右留白各 **120**(首页设计稿单独一套基准)
|
|
||||||
// · 登录页:左品牌栏 784 + 右表单区 1136(无顶栏/页脚)
|
|
||||||
|
|
||||||
// 品牌色(设计稿实测)
|
|
||||||
@primary-color: #1f67ef;
|
@primary-color: #1f67ef;
|
||||||
@primary-strong: #024fff; // 内容强调 / 选中态(设计实测 #024FFF)
|
@primary-strong: #024fff;
|
||||||
@primary-hover: #598bff;
|
@primary-hover: #598bff;
|
||||||
@primary-light: #eaf1ff;
|
@primary-light: #eaf1ff;
|
||||||
|
|
||||||
// 功能色(设计稿实测:危险 #EF4444、成功 #16A34A)
|
|
||||||
@success-color: #16a34a;
|
@success-color: #16a34a;
|
||||||
@warning-color: #ff8f1f;
|
@warning-color: #ff8f1f;
|
||||||
@danger-color: #ef4444;
|
@danger-color: #ef4444;
|
||||||
@info-color: #8a94a6;
|
@info-color: #8a94a6;
|
||||||
|
|
||||||
// 文字色(设计稿灰阶:#121212 / #333333 / #666666 / #999999)
|
|
||||||
@heading-color: #121212;
|
@heading-color: #121212;
|
||||||
@text-color: #333333;
|
@text-color: #333333;
|
||||||
@text-color-secondary: #666666;
|
@text-color-secondary: #666666;
|
||||||
@text-color-disabled: #999999;
|
@text-color-disabled: #999999;
|
||||||
|
|
||||||
// 背景 / 边框(设计稿实测)
|
|
||||||
@bg-color: #f5f6f8;
|
@bg-color: #f5f6f8;
|
||||||
@bg-color-light: #fafbfc;
|
@bg-color-light: #fafbfc;
|
||||||
@border-color: #e4e7ec;
|
@border-color: #e4e7ec;
|
||||||
@border-color-dark: #cdd3dd;
|
@border-color-dark: #cdd3dd;
|
||||||
@border-color-btn: #d8dbdf; // 注销确认弹窗「暂不注销」底/描边(设计实取)
|
@border-color-btn: #d8dbdf;
|
||||||
@white: #ffffff;
|
@white: #ffffff;
|
||||||
@black: #000000;
|
@black: #000000;
|
||||||
@header-bg: #f4f5f9;
|
@header-bg: #f4f5f9;
|
||||||
@header-border: #d8d8d8;
|
@header-border: #d8d8d8;
|
||||||
@footer-bg: #0c1321;
|
@footer-bg: #0c1321;
|
||||||
|
|
||||||
// 圆角
|
|
||||||
@radius-sm: 4px;
|
@radius-sm: 4px;
|
||||||
@radius-base: 8px;
|
@radius-base: 8px;
|
||||||
@radius-lg: 12px;
|
@radius-lg: 12px;
|
||||||
@radius-round: 999px;
|
@radius-round: 999px;
|
||||||
|
|
||||||
// 间距
|
|
||||||
@spacing-xs: 4px;
|
@spacing-xs: 4px;
|
||||||
@spacing-sm: 8px;
|
@spacing-sm: 8px;
|
||||||
@spacing-base: 16px;
|
@spacing-base: 16px;
|
||||||
@@ -50,306 +37,234 @@
|
|||||||
@spacing-xl: 32px;
|
@spacing-xl: 32px;
|
||||||
@spacing-xxl: 48px;
|
@spacing-xxl: 48px;
|
||||||
|
|
||||||
// 字号(1:1,设计实测体系 —— 43 张设计稿全量统计:16px 为出现最多的正文级字号)
|
|
||||||
@font-size-xs: 10px;
|
@font-size-xs: 10px;
|
||||||
@font-size-sm: 12px;
|
@font-size-sm: 12px;
|
||||||
@font-size-base: 14px;
|
@font-size-base: 14px;
|
||||||
@font-size-body: 16px; // 正文基准(设计稿最高频字号;body 默认值)
|
@font-size-body: 16px;
|
||||||
@font-size-md: 16px;
|
@font-size-md: 16px;
|
||||||
@font-size-lg: 18px; // 顶栏导航 / 小标题(设计实测 18/500)
|
@font-size-lg: 18px;
|
||||||
@font-size-xl: 20px;
|
@font-size-xl: 20px;
|
||||||
@font-size-title: 28px; // 栏目标题(设计实测 28/500)
|
@font-size-title: 28px;
|
||||||
@font-size-xxl: 36px; // 大标题(设计实测 36/700)
|
@font-size-xxl: 36px;
|
||||||
@font-size-hero: 48px; // 头图主标题(设计实测 48/500、52/700、60/700)
|
@font-size-hero: 48px;
|
||||||
@font-size-hero-lg: 60px; // 首页头图主标题(设计实测 60/700)
|
@font-size-hero-lg: 60px;
|
||||||
|
|
||||||
// 首页头图(设计画板 y73 起,1920×661)
|
@banner-title-color: #12171a;
|
||||||
@banner-title-color: #12171a; // 头图标题/副标题色(设计实取)
|
@btn-gradient-from: #a061fe;
|
||||||
@btn-gradient-from: #a061fe; // 主按钮渐变起(设计实取)
|
@btn-gradient-to: #045dff;
|
||||||
@btn-gradient-to: #045dff; // 主按钮渐变止(设计实取)
|
@btn-ghost-color: #2b5ff0;
|
||||||
@btn-ghost-color: #2b5ff0; // 描边按钮文字色(设计像素实测)
|
|
||||||
|
|
||||||
// 布局(1:1,设计实测)
|
@container-width: 1920px;
|
||||||
@container-width: 1920px; // 设计画布 / 通栏块总宽
|
|
||||||
|
|
||||||
// 全局视觉缩放:设计稿按 1920 画布 1:1 实现,整体观感偏大,
|
|
||||||
// 按用户要求整体缩放 67%(html zoom,等比缩放、比例关系与通栏布局不破坏)。
|
|
||||||
// 1920 物理屏逻辑视口 ≈ 2866 ≥ 1920 → 内容居中、无横向滚动。
|
|
||||||
// 小屏适配(自行判断):物理视口 <1287px(如 1280 屏)时 1920×0.67≈1286 放不下,
|
|
||||||
// 降到 0.66 → 1280/0.66 ≈ 1939 ≥ 1920,仍无横向滚动;1366 屏 1366/0.67≈2039 ✓ 不需降档。
|
|
||||||
@page-zoom: 0.67;
|
@page-zoom: 0.67;
|
||||||
@page-zoom--sm: 0.66;
|
@page-zoom--sm: 0.66;
|
||||||
|
|
||||||
// 常规页内容区(specs2 全量复核 2026-09-30:案例库/需求提报/项目/合同/财务/消息详情/客服/
|
@page-content: 1780px;
|
||||||
// 收藏/发票抬头/业务详情 共 17 张设计稿内容块均为 x70→1850 = **1780 宽**;此前误标 155/1600
|
@page-gutter: 70px;
|
||||||
// 导致整站内容右移 85px、窄 170px,已修正回归设计稿)
|
|
||||||
@page-content: 1780px; // 常规页内容实宽(设计实测 x70→1850)
|
|
||||||
@page-gutter: 70px; // 常规页左右留白(设计实测)
|
|
||||||
|
|
||||||
// 个人中心系(个人中心/账号安全/地址管理等 13 张)视觉核对定版基准,独立不联动:
|
|
||||||
// 左列 x155 宽 200 + 右卡,视觉比对设计稿裁片确认(specs 嵌套组坐标为组内相对值,不可直接用)
|
|
||||||
@profile-content: 1600px;
|
@profile-content: 1600px;
|
||||||
@profile-gutter: 155px;
|
@profile-gutter: 155px;
|
||||||
|
|
||||||
// 首页专用(首页设计稿单独基准:x120→1800)
|
@home-content: 1680px;
|
||||||
@home-content: 1680px; // 首页内容实宽
|
@home-gutter: 120px;
|
||||||
@home-gutter: 120px; // 首页左右留白
|
|
||||||
|
|
||||||
// 兼容旧引用(= 常规页基准,即绝大多数页面)
|
|
||||||
@content-width: @page-content;
|
@content-width: @page-content;
|
||||||
@container-width-inner: @page-content;
|
@container-width-inner: @page-content;
|
||||||
|
|
||||||
@header-height: 72px; // 顶栏高(设计实测 72px)
|
@header-height: 72px;
|
||||||
@header-nav-gap: 94px; // 顶栏导航项文字间距(设计实测:首页 x275→311,案例库 x405)
|
@header-nav-gap: 94px;
|
||||||
@footer-height: 481px; // 页脚高(设计实测 481px)
|
@footer-height: 481px;
|
||||||
@footer-inset: 70px; // 页脚内容左右内缩(与常规页内容区对齐)
|
@footer-inset: 70px;
|
||||||
|
|
||||||
// 登录页(无顶栏/页脚)
|
@login-brand-width: 35%;
|
||||||
@login-brand-width: 35%; // 左侧品牌渐变栏宽度占比(右栏 flex:1 → 自动占 65%)
|
@login-form-offset: 1022px;
|
||||||
@login-form-offset: 1022px; // 右侧表单区起点
|
@auth-font-scale: 1.5;
|
||||||
@auth-font-scale: 1.5; // 登录页字体放大倍数(老大要求整体 ×2)
|
|
||||||
|
|
||||||
// 阴影
|
|
||||||
@shadow-base: 0 2px 8px rgba(31, 39, 51, 0.08);
|
@shadow-base: 0 2px 8px rgba(31, 39, 51, 0.08);
|
||||||
@shadow-lg: 0 4px 16px rgba(31, 39, 51, 0.12);
|
@shadow-lg: 0 4px 16px rgba(31, 39, 51, 0.12);
|
||||||
|
|
||||||
// 案例库(蓝湖设计稿 04/08/15;筛选面板 / 案例卡片 / 分页专用)
|
@filter-selected-bg: #e7eeff;
|
||||||
// ⚠️ 换算约定:设计画布内容宽 1780px → 应用内容宽 1368px,即 设计px × 0.77 ≈ CSS px;
|
@pill-bg: #ededed;
|
||||||
// 小字号(设计 ≤12px)按可读下限 11px 不跟缩。下列色值均为设计稿取色实测。
|
@pill-selected-bg: #075cff;
|
||||||
@filter-selected-bg: #e7eeff; // 业务类型选中行底色(设计15 实取 rgba(231,238,255))
|
@chips-panel-bg: #eaf1ff;
|
||||||
@pill-bg: #ededed; // 胶囊选项默认底色(设计04 实取 rgba(237,237,237))
|
@input-bg: #ededed;
|
||||||
@pill-selected-bg: #075cff; // 胶囊选项选中底色(设计04 实取 rgba(7,92,255))
|
@tag-hardware: #4caf50;
|
||||||
@chips-panel-bg: #eaf1ff; // 「已选条件」区底色(设计04 实取 rgba(234,241,255))
|
@tag-iot: #5720ba;
|
||||||
@input-bg: #ededed; // 日期区间胶囊底色(设计04 实取 rgba(237,237,237))
|
@tag-manufacturing: #ff8f1f;
|
||||||
@tag-hardware: #4caf50; // 业务标签:硬件电路(设计15 实取 #4CAF50)
|
@tag-other: #8a94a6;
|
||||||
@tag-iot: #5720ba; // 业务标签:物联网全案
|
|
||||||
@tag-manufacturing: #ff8f1f; // 业务标签:生产制造
|
|
||||||
@tag-other: #8a94a6; // 业务标签:其他
|
|
||||||
|
|
||||||
@cases-accent: #075cff; // 搜索/重置按钮、已选 chip 文字(设计04/08 实取 rgba(7,92,255))
|
@cases-accent: #075cff;
|
||||||
@cases-confirm: #2563eb; // 底部「确定筛选」按钮底色(设计04 实取)
|
@cases-confirm: #2563eb;
|
||||||
@cases-hero-title: #121212; // 「成功案例库」主标题(设计04 实取 18,18,18)
|
@cases-hero-title: #121212;
|
||||||
@cases-filter-title: #222222; // 「筛选条件」标题(设计04 实取 34,34,34)
|
@cases-filter-title: #222222;
|
||||||
@cases-text: #333333; // 筛选项 / 卡片正文(设计04 实取 51,51,51)
|
@cases-text: #333333;
|
||||||
@cases-muted: #666666; // 次级文字:计数 / 页面标语(设计04 实取 102,102,102)
|
@cases-muted: #666666;
|
||||||
@cases-input-border: #999999; // 搜索框描边·占位文字 / 复选框描边(设计04 实取 153,153,153)
|
@cases-input-border: #999999;
|
||||||
@cases-icon: #797979; // 搜索图标(设计04 实取 121,121,121)
|
@cases-icon: #797979;
|
||||||
@cases-reset-bg: #f0f0f0; // 底部「重置」按钮底色(设计04 实取 239.82→240)
|
@cases-reset-bg: #f0f0f0;
|
||||||
@cases-card-border: #e1dfdf; // 案例卡片描边(设计04 实取 224.6,222.7,222.7)
|
@cases-card-border: #e1dfdf;
|
||||||
@cases-card-shadow: 0 4px 10px rgba(0, 0, 0, 0.08); // 案例卡片常驻阴影(设计04 实取)
|
@cases-card-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
|
||||||
@cases-page-active-bg: #e0e9fa; // 分页当前页底色(设计15 实取 ≈ rgba(2,79,255,.12) 叠白)
|
@cases-page-active-bg: #e0e9fa;
|
||||||
|
|
||||||
// 需求提报(按需求提报设计稿 06/07 标注实测校准)
|
@demand-selected-bg: #024fff;
|
||||||
@demand-selected-bg: #024fff; // 业务类型卡片 / 预算胶囊选中底色(设计稿实取 rgba(2,79,255))
|
@demand-accent: #024fff;
|
||||||
@demand-accent: #024fff; // 业务类型选中描边·右上圆点 / 步骤序号(设计稿强调色 #024FFF)
|
@demand-secondary-bg: #075cff;
|
||||||
@demand-secondary-bg: #075cff; // 底部「保存草稿 / 重置 / 立即提报」按钮底色(设计稿实取 rgba(7,92,255))
|
|
||||||
|
|
||||||
// 项目中心(按项目中心设计稿标注校准)
|
@project-table-head-bg: #e8e8e8;
|
||||||
@project-table-head-bg: #e8e8e8; // 表头底色(设计稿实取 (232,232,232))
|
@project-table-line: #bbbbbb;
|
||||||
@project-table-line: #bbbbbb; // 表格网格线(设计稿实取 (187,187,187))
|
|
||||||
|
|
||||||
// 项目详情(按 docs/prototype/项目中心-演示.html 设计稿校准)
|
@project-accent: #024fff;
|
||||||
// ⚠️ 换算系数:设计画布内容宽 1776px → 应用内容宽 1368px(.container 1400 − 2×16),
|
@project-track: #edeff2;
|
||||||
// 即 设计px × 0.77 ≈ CSS px。下列数值 = 设计稿实测值 × 0.77 后取整。
|
@milestone-done: #22c55e;
|
||||||
@project-accent: #024fff; // 竖条 / 状态文字 / 进度条 / 激活页签 / 里程碑进行中(蓝湖设计稿实取 #024FFF)
|
@milestone-idle: #d6d9de;
|
||||||
@project-track: #edeff2; // 进度条轨道(蓝湖设计 13 实取 #EDEFF2)
|
@team-avatar-bg: #1f67ef;
|
||||||
@milestone-done: #22c55e; // 里程碑已完成圆点 + 已完成段连接线(蓝湖设计 13 实取 #22C55E)
|
|
||||||
@milestone-idle: #d6d9de; // 里程碑待开始圆环描边(蓝湖设计 13 实取)
|
|
||||||
@team-avatar-bg: #1f67ef; // 团队头像底色(蓝湖设计 13 实取 #1F67EF)
|
|
||||||
|
|
||||||
// 收银台 / 确认支付(按「支付」设计稿标注校准)
|
@checkout-card: #ffffff;
|
||||||
// ⚠️ 换算系数:设计稿 250px 宽 ≈ 375 视口(缩放 1.5×),下列数值 = 设计稿实测值 × 1.5 后取整。
|
@checkout-line: #e1e1e1;
|
||||||
// 页面底色沿用全站 @bg-color(#f5f6f8),与设计稿实取的 #f5f5f5 仅差 1/255,不再单独开变量。
|
@checkout-amount: #ff5500;
|
||||||
@checkout-card: #ffffff; // 卡片底色
|
@checkout-method-active-bg: #d3f3df;
|
||||||
@checkout-line: #e1e1e1; // 订单明细卡描边(设计稿实取 (225,225,225))
|
@checkout-wechat: #28c445;
|
||||||
@checkout-amount: #ff5500; // 合计金额(设计稿实取)
|
@checkout-alipay: #1677ff;
|
||||||
@checkout-method-active-bg: #d3f3df; // 选中支付行底色(设计稿实取)
|
@checkout-cta: #f59e0b;
|
||||||
@checkout-wechat: #28c445; // 微信绿(图标 / 选中 radio)
|
@checkout-cta-hover: #e08d05;
|
||||||
@checkout-alipay: #1677ff; // 支付宝蓝(图标)
|
@checkout-muted: #9b9b9b;
|
||||||
@checkout-cta: #f59e0b; // 确认支付按钮(设计稿实取)
|
@checkout-radio-idle: #c8c8c8;
|
||||||
@checkout-cta-hover: #e08d05; // 确认支付 hover
|
|
||||||
@checkout-muted: #9b9b9b; // 次级说明文字(设计稿实取)
|
|
||||||
@checkout-radio-idle: #c8c8c8; // 未选 radio 圆圈描边
|
|
||||||
|
|
||||||
// 项目详情 → 合同财务(付款计划卡 / 发票·合同文件行)
|
@finance-card-width: 285px;
|
||||||
// 来源 docs/prototype/项目中心-演示.html 第 1 帧(与 @project-accent 同源,换算同为 设计px × 0.77)。
|
@finance-card-radius: 10px;
|
||||||
// ⚠️ 小字号例外:设计 12px/10px 折算后是 9/8px(不可读),按可读下限取 12/11px,不跟缩。
|
@finance-meta-color: #6b7280;
|
||||||
@finance-card-width: 285px; // 计划卡定宽(蓝湖设计 09 实测 x70..440 = 370 设计px × 0.77),竖排左对齐
|
@finance-row-bg: #f7f8fa;
|
||||||
@finance-card-radius: 10px; // 计划卡圆角(蓝湖设计 09 实测 ≈13 设计px × 0.77)
|
@finance-paid: #16a34a;
|
||||||
@finance-meta-color: #6b7280; // 字段行「账单号:/ 关联项目:」(蓝湖设计 09 (107,114,128))
|
@finance-pending: #f59e0b;
|
||||||
@finance-row-bg: #f7f8fa; // 项目文件行底色(蓝湖设计 09/10/11 实取 #F7F8FA)
|
@finance-pending-amount: #f59e0b;
|
||||||
@finance-paid: #16a34a; // 已支付:徽标文字 + 金额(蓝湖设计 09 (22,163,74))
|
@finance-idle: #b3bfc4;
|
||||||
@finance-pending: #f59e0b; // 待支付:「立即支付」按钮底色 + 金额(蓝湖设计 09 实取)
|
@finance-cta-hover: #e08d05;
|
||||||
@finance-pending-amount: #f59e0b; // 待支付:金额文字(蓝湖设计 09 (245,158,11))
|
|
||||||
@finance-idle: #b3bfc4; // 未到期:金额文字(蓝湖设计 09 (179,185,196))
|
|
||||||
@finance-cta-hover: #e08d05; // 「立即支付」hover(与收银台 CTA 同色系)
|
|
||||||
|
|
||||||
// 首页 / 业务详情(蓝湖设计稿:03·05 首页,53 业务详情)
|
@home-ink: #121212;
|
||||||
// ⚠️ 换算约定同上:设计px × 0.77 ≈ CSS px;设计 ≤12px 小字号按可读下限取 12/11px。
|
@home-welcome: #2c2c2c;
|
||||||
@home-ink: #121212; // 栏目标题 / 卡片标题 / 分类标签文字(设计实取 #121212)
|
@home-body: #333333;
|
||||||
@home-welcome: #2c2c2c; // 首页欢迎语与副文案(设计实取 #2C2C2C)
|
@home-paragraph: #222222;
|
||||||
@home-body: #333333; // 详情页标题/标签、案例元信息(设计实取 #333333)
|
@home-breadcrumb: #666666;
|
||||||
@home-paragraph: #222222; // 详情页富文本正文(设计实取 #222222)
|
@home-card-border: #ebebeb;
|
||||||
@home-breadcrumb: #666666; // 详情页面包屑(设计实取 #666666)
|
@home-section-border: #e2e2e2;
|
||||||
@home-card-border: #ebebeb; // 首页仪表盘(欢迎卡/快捷入口卡)描边(设计实取)
|
@home-case-border: #e0dede;
|
||||||
@home-section-border: #e2e2e2; // 首页大区块卡(业务服务/案例库精选)描边(设计实取)
|
@home-business-border: #999999;
|
||||||
@home-case-border: #e0dede; // 案例卡片描边(设计实取 rgba(224,222,222))
|
@home-ring: #c6c2c2;
|
||||||
@home-business-border: #999999; // 业务卡片未选中描边(设计实取 #999999)
|
@home-tab-active: #085dff;
|
||||||
@home-ring: #c6c2c2; // 业务卡片右上圆环·未选中 / 快捷入口「更多」箭头(设计实取)
|
@home-banner-dot: #8d96ae;
|
||||||
@home-tab-active: #085dff; // 案例分类激活文字(设计实取 #085DFF,下划线仍用 @primary-color)
|
@home-banner-bg: #f3f4f8;
|
||||||
@home-banner-dot: #8d96ae; // 首页头图轮播指示条/点(设计实取 rgba(141,150,174))
|
@home-quick-blue: #a5d0ff;
|
||||||
@home-banner-bg: #f3f4f8; // 首页头图底色(设计实取,轮播无图时兜底)
|
@home-quick-purple: #dbc9fe;
|
||||||
@home-quick-blue: #a5d0ff; // 快捷入口·工单需求池(cyan,设计实取)
|
@home-quick-green: #c4f0be;
|
||||||
@home-quick-purple: #dbc9fe; // 快捷入口·案例库管理(purple,设计实取)
|
@home-quick-peach: #ffcbbe;
|
||||||
@home-quick-green: #c4f0be; // 快捷入口·客服工作台(green,设计实取)
|
@home-quick-pink: #ffcadb;
|
||||||
@home-quick-peach: #ffcbbe; // 快捷入口·合同台账(blue tone,设计稿实取浅橙)
|
|
||||||
@home-quick-pink: #ffcadb; // 快捷入口·财务台账(amber tone,设计稿实取浅粉)
|
|
||||||
@home-detail-hero-bg: linear-gradient(
|
@home-detail-hero-bg: linear-gradient(
|
||||||
115deg,
|
115deg,
|
||||||
#e2ebf7 0%,
|
#e2ebf7 0%,
|
||||||
#cfe7fd 55%,
|
#cfe7fd 55%,
|
||||||
#b6dfff 100%
|
#b6dfff 100%
|
||||||
); // 业务详情头图底(设计实取采样渐变)
|
);
|
||||||
@home-detail-logo-bg: #d9e5ff; // 业务详情头像块底色(设计实取 rgba(217.8,229.4,255))
|
@home-detail-logo-bg: #d9e5ff;
|
||||||
@home-service-web: #ae7eff; // 服务类型·Web应用 圆形底(设计实取)
|
@home-service-web: #ae7eff;
|
||||||
@home-service-mp: #00b240; // 服务类型·小程序 圆形底(设计实取)
|
@home-service-mp: #00b240;
|
||||||
@home-service-app: #fd9a83; // 服务类型·APP 圆形底(设计实取)
|
@home-service-app: #fd9a83;
|
||||||
@home-service-dev: #80e561; // 服务类型·二次开发 圆形底(设计实取)
|
@home-service-dev: #80e561;
|
||||||
@home-service-pc: #0081e3; // 服务类型·桌面工具 圆形底(设计实取)
|
@home-service-pc: #0081e3;
|
||||||
|
|
||||||
// 需求提报 / 登录注册(蓝湖设计稿)
|
@demand-ink: #121212;
|
||||||
// 设计稿索引:06·07 需求提报,02·51 登录,54 注册,55 找回密码
|
@demand-heading: #333333;
|
||||||
// ⚠️ 换算约定同上:设计px × 0.77 ≈ CSS px;设计 ≤12px 小字号按可读下限取 11/12px。
|
@demand-subtitle: #666666;
|
||||||
@demand-ink: #121212; // 需求提报页主标题 / 卡片标题 / 输入值(设计实取 rgba(18,18,18))
|
@demand-line: #999999;
|
||||||
@demand-heading: #333333; // 区块标题(01/02/03 旁)/ 表单标签 / 底部提示(设计实取 rgba(51,51,51))
|
@demand-upload-bg: #ededed;
|
||||||
@demand-subtitle: #666666; // 页面副标题「填写您的需求信息…」(设计实取 rgba(102,102,102))
|
@demand-radio-idle: #c7c2c2;
|
||||||
@demand-line: #999999; // 输入框·胶囊·卡片描边 + 占位文案(设计实取 rgba(153,153,153))
|
@auth-gradient-from: #505be8;
|
||||||
@demand-upload-bg: #ededed; // 附件上传区 / 文件行底色(设计实取 rgba(236.8,236.8,236.8)≈#EDEDED)
|
@auth-gradient-to: #8b3fe8;
|
||||||
@demand-radio-idle: #c7c2c2; // 业务类型未选中单选圈描边(设计实取 rgba(198.8,194.4,194.4))
|
@auth-track: #edeff2;
|
||||||
@auth-gradient-from: #505be8; // 登录/注册左侧品牌渐变起点(设计实取采样)
|
|
||||||
@auth-gradient-to: #8b3fe8; // 登录/注册左侧品牌渐变终点(设计实取采样)
|
|
||||||
@auth-track: #edeff2; // 密码强度条轨道底色(设计实取 #EDEFF2)
|
|
||||||
|
|
||||||
// 项目中心(蓝湖设计稿)
|
@project-divider: #f0f1f3;
|
||||||
// 蓝湖设计 09(合同财务)/10(售后信息)/11(项目文件)/12(申请售后)/13(项目进度)像素实取。
|
@project-breadcrumb: #666666;
|
||||||
// 换算约定同上:设计px × 0.77 ≈ CSS px;设计 ≤12px 小字号按可读下限取 11/12px。
|
@project-logo-bg: #f2f4f6;
|
||||||
@project-divider: #f0f1f3; // 页签下分割线 / 计划卡描边 / 表单底栏分隔线(设计 (240,241,243))
|
@project-banner-bg: #fdeeee;
|
||||||
@project-breadcrumb: #666666; // 详情页·申请售后页面包屑文字(设计 (102,102,102))
|
@project-banner-title: #d93025;
|
||||||
@project-logo-bg: #f2f4f6; // 详情头像方块「</>」底色(设计 09 实取 ≈#F1F3F5)
|
@project-apply: #e3464b;
|
||||||
@project-banner-bg: #fdeeee; // 售后工单提示横幅底色(设计 10 实取 #FDEEEE)
|
@project-quiet-btn: #eef0f3;
|
||||||
@project-banner-title: #d93025; // 横幅标题红(设计 10 (217,48,37))
|
@tag-success-bg: #e7f7ec;
|
||||||
@project-apply: #e3464b; // 「申请售后」按钮底色(设计 10 实取 #E3464B)
|
@tag-warning-bg: #fef3e2;
|
||||||
@project-quiet-btn: #eef0f3; // 浅灰按钮底:上传文档 / 取消 / 重置(设计 10·12 实取)
|
@tag-warning-text: #d97706;
|
||||||
@tag-success-bg: #e7f7ec; // 徽标底:已支付 / 成功(设计 09 实取)
|
@tag-info-bg: #f3f4f6;
|
||||||
@tag-warning-bg: #fef3e2; // 徽标底:待支付(设计 09 实取)
|
@milestone-line: #eeeff2;
|
||||||
@tag-warning-text: #d97706; // 徽标字:待支付(设计 09 (217,119,6))
|
|
||||||
@tag-info-bg: #f3f4f6; // 徽标底:未支付 / 中性(设计 09 实取)
|
|
||||||
@milestone-line: #eeeff2; // 里程碑未完成连接线(设计 13 实取)
|
|
||||||
|
|
||||||
// 项目详情(蓝湖设计稿 5 张:项目进度 / 项目文件 / 合同财务 / 售后信息 / 申请售后)
|
@project-ink: #1f2937;
|
||||||
// ⚠️ 换算约定:**1:1 像素级还原**,设计画布 1920px,直接使用设计标注 px,不做缩放。
|
@project-label: #6b7280;
|
||||||
// 实测锚点(1920 视口):面包屑 (70,116) 24/500 #666666;主标题 (142,212) 19/700;
|
@project-value: #1f2937;
|
||||||
// 页签行 (70,278) 1780×40,文字 13/600 激活 / 13/400 常规,节距 86;
|
@project-muted: #9ca3af;
|
||||||
// 分隔线 (70,317) 1780×1 #F0F1F3;区块标题 15/700 @(82,y)。
|
@project-cell: #4b5563;
|
||||||
@project-ink: #1f2937; // 主标题 / 区块标题 / 文件名(设计实取 #1F2937)
|
@project-card: #ffffff;
|
||||||
@project-label: #6b7280; // 「项目编号:」等字段标签(设计实取 #6B7280)
|
@project-field-bg: #f3f4f6;
|
||||||
@project-value: #1f2937; // 字段值(设计实取 #1F2937)
|
@project-input-border: #dde1e7;
|
||||||
@project-muted: #9ca3af; // 次行说明:上传时间 / 占位文字(设计实取 #9CA3AF)
|
@project-head-bg: #f7f8fa;
|
||||||
@project-cell: #4b5563; // 只读值 / 行内操作「预览·下载」(设计实取 #4B5563)
|
@project-upload-tag: #ef4444;
|
||||||
@project-card: #ffffff; // 表格区白底(设计实取)
|
@project-banner-soft: #fdeeee;
|
||||||
@project-field-bg: #f3f4f6; // 只读字段底(设计实取 #F3F4F6)
|
|
||||||
@project-input-border: #dde1e7; // 只读框 / 输入框描边(设计实取 #DDE1E7)
|
|
||||||
@project-head-bg: #f7f8fa; // 里程碑 / 文件行浅底(设计实取 #F7F8FA)
|
|
||||||
@project-upload-tag: #ef4444; // 上传文档红字(设计实取 #EF4444)
|
|
||||||
@project-banner-soft: #fdeeee; // 售后工单横幅底(设计实取 #FDEEEE)
|
|
||||||
|
|
||||||
// 项目详情 → 里程碑(设计实测)
|
@milestone-node: 356px;
|
||||||
@milestone-node: 356px; // 节点列宽(相邻节点 x70→426 = 356)
|
@milestone-dot: 26px;
|
||||||
@milestone-dot: 26px; // 已完成实心点直径(x235..261 = 26)
|
@milestone-ring: #e5e7eb;
|
||||||
@milestone-ring: #e5e7eb; // 待开始连接线(设计实取 #E5E7EB)
|
@milestone-idle-text: #9ca3af;
|
||||||
@milestone-idle-text: #9ca3af; // 待开始 / 进行中的状态说明(设计实取 #9CA3AF)
|
|
||||||
|
|
||||||
// 项目详情 → 项目团队(设计实测:头像 64,相邻中心距 99 → 间距 35)
|
|
||||||
@team-size: 64px;
|
@team-size: 64px;
|
||||||
@team-gap: 35px;
|
@team-gap: 35px;
|
||||||
|
|
||||||
// 合同中心(蓝湖设计稿)
|
@contract-accent: #075cff;
|
||||||
// 设计索引:41 顶部横幅+搜索栏,46/47 表格+分页,40/44/52 合同详情,45 面包屑,39 签署。
|
@contract-muted: #666666;
|
||||||
// ⚠️ 换算约定同上:设计px × 0.77 ≈ CSS px;设计 ≤12px 小字号按可读下限取 11px,不跟缩。
|
@contract-head-text: #222222;
|
||||||
@contract-accent: #075cff; // 列表:搜索/重置按钮、当前页码、「详情」链接(设计 46 实取 rgb(7,92,255))
|
@contract-cell-text: #333333;
|
||||||
@contract-muted: #666666; // 面包屑 / 顶部横幅副标语 / 分页箭头(设计 45·46 实取 #666666)
|
@contract-line: #999999;
|
||||||
@contract-head-text: #222222; // 表头文字(设计 46 实取 #222222)
|
@contract-table-head-bg: #ededed;
|
||||||
@contract-cell-text: #333333; // 表格正文(设计 46 实取 #333333)
|
@contract-placeholder: #b3b9c4;
|
||||||
@contract-line: #999999; // 表格边框·行分隔线·分页描边与页码(设计 46 实取 #999999)
|
@contract-reset-bg: #eef0f3;
|
||||||
@contract-table-head-bg: #ededed; // 表头底色(设计 46 实取 (237,237,237))
|
@contract-qr-bg: #d6d6d6;
|
||||||
@contract-placeholder: #b3b9c4; // 搜索框占位文字(设计 41 实取 (179,185,196))
|
|
||||||
@contract-reset-bg: #eef0f3; // 搜索行「重置」按钮底色(设计 41 实取)
|
|
||||||
@contract-qr-bg: #d6d6d6; // 签署页二维码占位底色(设计 39 实取 #D6D6D6)
|
|
||||||
|
|
||||||
// 财务中心(蓝湖设计稿)
|
|
||||||
// 设计索引:37 财务概览,30 账单明细,31 账单详情,33·34 扫码支付,32 支付结果,35·36 发票抬头。
|
|
||||||
// ⚠️ 换算约定:设计画布内容宽 1780 → 应用内容宽 1368(.container 1400 − 2×16),设计px × 0.77;
|
|
||||||
// 字号 = 设计px × 0.77,≤14 设计px 折算不足 11px 时按可读下限取 11px(15/16 → 12,18 → 14,20 → 15,22 → 17,26 → 20,34 → 26,38 → 29)。
|
|
||||||
// 复用同值既有 token:表格/卡片分隔线 @project-divider(#f0f1f3)、占位文字 @contract-placeholder(#b3b9c4)、
|
|
||||||
// 「重置」按钮底 @project-quiet-btn(#eef0f3)、二维码占位底 @contract-qr-bg(#d6d6d6)。
|
|
||||||
@finance-hero-gradient: linear-gradient(
|
@finance-hero-gradient: linear-gradient(
|
||||||
115deg,
|
115deg,
|
||||||
#024fff 0%,
|
#024fff 0%,
|
||||||
#4b4ff3 55%,
|
#4b4ff3 55%,
|
||||||
#7e75ec 100%
|
#7e75ec 100%
|
||||||
); // 概览「累计消费总额」卡渐变(设计 37 实取采样 #034FFF→#7E75EC)
|
);
|
||||||
@finance-chart-bar: #4a90c4; // 概览月度账单柱条(设计 37 实取采样)
|
@finance-chart-bar: #4a90c4;
|
||||||
@finance-chart-grid: #edeef2; // 概览图表网格线(设计 37 实取 rgba(237,239,242))
|
@finance-chart-grid: #edeef2;
|
||||||
@finance-table-head-bg: #f1f3f5; // 账单表头底色(设计 30 实取)
|
@finance-table-head-bg: #f1f3f5;
|
||||||
@finance-field-bg: #f3f4f6; // 搜索框底色(设计 30 实取 #F3F4F6)
|
@finance-field-bg: #f3f4f6;
|
||||||
@finance-page-active: #d5e2fe; // 分页当前页底色(设计 30/35 实取 #C5D7FE/#D5E2FE)
|
@finance-page-active: #d5e2fe;
|
||||||
@finance-summary-cta: #ffb754; // 底部汇总条「立即支付」按钮(设计 30 实取采样)
|
@finance-summary-cta: #ffb754;
|
||||||
@finance-result-circle: #7ed321; // 支付结果大圆勾底色(设计 32 实取采样)
|
@finance-result-circle: #7ed321;
|
||||||
@finance-safe-bg: #d2eedc; // 扫码支付「已通过安全验证」横幅底(设计 33 实取采样)
|
@finance-safe-bg: #d2eedc;
|
||||||
|
|
||||||
// 消息中心 / 在线客服(蓝湖设计稿)
|
@message-icon-bg: #3b82f6;
|
||||||
// 设计索引:43 消息中心列表,42 通知详情,38 在线客服聊天。
|
@service-user-avatar: #8d99ae;
|
||||||
// ⚠️ 换算约定同上:设计px × 0.77 ≈ CSS px;设计 ≤14px 小字号按可读下限取 11px。
|
|
||||||
// 复用同值既有 token:行分割线/聊天卡描边 @project-divider(#f0f1f3)、灰字 #9ca3af=@text-color-disabled、
|
|
||||||
// 搜索框底 @finance-field-bg(#f3f4f6)、「重置」底 @contract-reset-bg(#eef0f3)、占位字 @contract-placeholder(#b3b9c4)、
|
|
||||||
// 附件/输入框底 #f7f8fa=@finance-row-bg、聊天消息区底 #fafbfc=@bg-color-light、未读徽标红=@danger-color、
|
|
||||||
// 强调蓝(搜索/返回/发送/气泡/头像)#024fff=@primary-strong、分页激活底 #e1eafa≈@cases-page-active-bg。
|
|
||||||
@message-icon-bg: #3b82f6; // 消息行类型图标圆底(设计 43 实取 #3B82F6)
|
|
||||||
@service-user-avatar: #8d99ae; // 客服聊天·右侧用户头像底(设计 38 实取 #8D99AE)
|
|
||||||
|
|
||||||
// 个人中心(蓝湖设计稿)
|
@profile-nav-active-bg: #e7eeff;
|
||||||
// 设计索引:14 布局+专属客服热线,28 账号安全列表,22~29 安全类弹窗,20 地址管理,17 资质列表,
|
@profile-card-border: #f0f1f3;
|
||||||
// 18 意见反馈,16 帮助中心,19 关于我们。
|
@profile-line: #e5e7eb;
|
||||||
// ⚠️ 换算约定同上:设计px × 0.77 ≈ CSS px;字号 = round(设计px × 0.77),≤14 设计px 按可读下限取 11px
|
@profile-input-border: #dde1e7;
|
||||||
// (11/12/13/14 → 11,15/16 → 12,19/20 → 15,24 → 18)。
|
@profile-radius: 6px;
|
||||||
// 复用同值既有 token:页面底 @bg-color、强调蓝 #024fff=@primary-strong、卡片描边 #f0f1f3≈@project-divider、
|
@profile-page-bg: #f4f6f8;
|
||||||
// 成功绿底徽标 #e7f7ec≈@tag-success-bg、次级按钮底 #eef0f3≈@project-quiet-btn、文字灰阶 @text-color* 系列。
|
@profile-ink: #1f2937;
|
||||||
@profile-nav-active-bg: #e7eeff; // 左侧菜单选中项底色(设计 28 实取 rgba(231,238,255))
|
@profile-hotline-bg: #d6e4fb;
|
||||||
@profile-card-border: #f0f1f3; // 列表卡 / 分组卡描边(设计 28 实取 (240,241,243))
|
@profile-icon-pwd: #c7d2fe;
|
||||||
@profile-line: #e5e7eb; // 按钮描边 / 分隔线(设计 28 注销按钮描边实取 (229,231,235))
|
@profile-icon-phone: #fdebeb;
|
||||||
@profile-input-border: #dde1e7; // 输入框描边(设计 22 实取 (221,225,231))
|
@profile-icon-email: #fee2e2;
|
||||||
@profile-radius: 6px; // 输入框 / 小按钮 / 菜单选中项圆角(设计 8px × 0.77)
|
@profile-user-from: #5459e8;
|
||||||
@profile-page-bg: #f4f6f8; // 个人中心页底色(设计稿实取)
|
@profile-user-to: #d2bdf3;
|
||||||
@profile-ink: #1f2937; // 个人中心标题字色(设计稿实取)
|
@profile-user-avatar: #8976ed;
|
||||||
@profile-hotline-bg: #d6e4fb; // 专属客服热线卡底色(设计 14 实取 rgba(214,228,251))
|
@profile-page-active-bg: #d1e0fe;
|
||||||
@profile-icon-pwd: #c7d2fe; // 账号安全「修改密码」图标块底(设计 28 实取)
|
@profile-quiet-btn: #eef0f3;
|
||||||
@profile-icon-phone: #fdebeb; // 账号安全「绑定手机」图标块底(设计 28 实取)
|
|
||||||
@profile-icon-email: #fee2e2; // 账号安全「绑定邮箱」图标块底(设计 28 实取)
|
|
||||||
@profile-user-from: #5459e8; // 用户卡渐变起点(设计 14 左上角实取)
|
|
||||||
@profile-user-to: #d2bdf3; // 用户卡渐变终点(设计 14 右下角实取)
|
|
||||||
@profile-user-avatar: #8976ed; // 用户卡头像圆底(设计 14 实取)
|
|
||||||
@profile-page-active-bg: #d1e0fe; // 分页当前页底色(设计 20 实取 #D1E0FE)
|
|
||||||
@profile-quiet-btn: #eef0f3; // 弹窗次级(取消)按钮底色(设计 24 实取 rgba(238,240,243))
|
|
||||||
|
|
||||||
// 合同中心(蓝湖设计稿)· 补充
|
@contract-ink: #121212;
|
||||||
@contract-ink: #121212; // 页面最深标题色(设计实取 #121212)
|
@contract-page-bg: #f4f6f8;
|
||||||
@contract-page-bg: #f4f6f8; // 页面底色 / 详情只读值框底(设计 e6c3a361 实取 #F4F6F8)
|
|
||||||
|
|
||||||
// 财务中心(蓝湖设计稿)· 补充
|
@finance-pay-bg: #f7f8f9;
|
||||||
@finance-pay-bg: #f7f8f9; // 扫码支付页底色(设计实取 #f7f8f9)
|
@finance-muted-text: #6b7280;
|
||||||
// 财务中心概览 / 账单(设计 6cd4aa76 等,1920 画布 1:1 实取)
|
@finance-card-line: #f0f1f3;
|
||||||
@finance-muted-text: #6b7280; // 副标语 / 图表刻度 / 图例 / 次级文字(设计实取 #6B7280)
|
@finance-entry-text: #4b5563;
|
||||||
@finance-card-line: #f0f1f3; // 卡片描边(设计实取 #F0F1F3)
|
@finance-axis-text: #9ca3af;
|
||||||
@finance-entry-text: #4b5563; // 快捷入口文字(设计实取 #4B5563)
|
@finance-ink: #1f2937;
|
||||||
@finance-axis-text: #9ca3af; // 图表 Y 轴刻度(设计实取 #9CA3AF)
|
|
||||||
@finance-ink: #1f2937; // 区块标题(设计实取 #1F2937)
|
|
||||||
|
|||||||
7
src/types/api.d.ts
vendored
7
src/types/api.d.ts
vendored
@@ -1,33 +1,26 @@
|
|||||||
// 接口通用结构(PRD 4.8)
|
|
||||||
|
|
||||||
/** 业务类型(跨模块共享) */
|
|
||||||
export type BusinessType = 'internet' | 'hardware' | 'iot' | 'manufacturing' | 'other'
|
export type BusinessType = 'internet' | 'hardware' | 'iot' | 'manufacturing' | 'other'
|
||||||
|
|
||||||
/** 统一响应结构 */
|
|
||||||
export interface ApiResponse<T> {
|
export interface ApiResponse<T> {
|
||||||
code: number
|
code: number
|
||||||
msg: string
|
msg: string
|
||||||
data: T
|
data: T
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 分页结果 */
|
|
||||||
export interface PageResult<T> {
|
export interface PageResult<T> {
|
||||||
list: T[]
|
list: T[]
|
||||||
total: number
|
total: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 通用分页查询参数 */
|
|
||||||
export interface PageQuery {
|
export interface PageQuery {
|
||||||
page: number
|
page: number
|
||||||
pageSize: number
|
pageSize: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 附件/文件信息 */
|
|
||||||
export interface FileInfo {
|
export interface FileInfo {
|
||||||
name: string
|
name: string
|
||||||
size: number
|
size: number
|
||||||
uploadTime: string
|
uploadTime: string
|
||||||
url: string
|
url: string
|
||||||
/** OSS 主键(同源预览用);本地未上传文件为 undefined */
|
|
||||||
ossId?: string | number
|
ossId?: string | number
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/types/business/auth.d.ts
vendored
11
src/types/business/auth.d.ts
vendored
@@ -1,23 +1,12 @@
|
|||||||
// 账号密码登录/注册真实接口类型(对接 vermis-maker /auth/*)
|
|
||||||
// 归位于 types/business/auth.d.ts,与 api/auth.ts 对应
|
|
||||||
|
|
||||||
/** 登录成功返回,对应后端 LoginVo(字段名以 @JsonProperty 为准) */
|
|
||||||
export interface LoginResult {
|
export interface LoginResult {
|
||||||
/** 授权令牌(后端字段 access_token) */
|
|
||||||
access_token: string
|
access_token: string
|
||||||
/** 刷新令牌 */
|
|
||||||
refresh_token?: string
|
refresh_token?: string
|
||||||
/** access_token 有效期(秒) */
|
|
||||||
expire_in?: number
|
expire_in?: number
|
||||||
/** refresh_token 有效期(秒) */
|
|
||||||
refresh_expire_in?: number
|
refresh_expire_in?: number
|
||||||
/** 应用 id */
|
|
||||||
client_id?: string
|
client_id?: string
|
||||||
/** 令牌权限 */
|
|
||||||
scope?: string
|
scope?: string
|
||||||
/** 用户 openid */
|
|
||||||
openid?: string
|
openid?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 注册返回:后端 R<Void>,无 data */
|
|
||||||
export type RegisterResult = void
|
export type RegisterResult = void
|
||||||
|
|||||||
130
src/types/business/cases.d.ts
vendored
130
src/types/business/cases.d.ts
vendored
@@ -1,54 +1,24 @@
|
|||||||
// 案例库类型(案例实体 + 后端 VO,与 api/cases.ts 对应)
|
|
||||||
//
|
//
|
||||||
// 后端:vermis-maker com.ruoyi.vermins.controller.pc.web.ProjectCaseUserController(前缀 /web/projectCase)
|
|
||||||
// 说明:案例实体类型统一定义在本文件(含首页「案例库精选」消费的 ProjectCaseVo)。
|
|
||||||
|
|
||||||
import type { BusinessType } from '@/types/api'
|
import type { BusinessType } from '@/types/api'
|
||||||
|
|
||||||
/**
|
|
||||||
* 业务类型筛选项。
|
|
||||||
*
|
|
||||||
* <p>选项不是前端写死的,而是取自 GET /web/projectCase/categories 返回的
|
|
||||||
* {@link ProjectCaseStatisticsVo#projectCaseCounts}——后端按项目提报表统计后下发。</p>
|
|
||||||
*/
|
|
||||||
export interface CaseTypeOption {
|
export interface CaseTypeOption {
|
||||||
/**
|
|
||||||
* 下发值 = **提报主键 id**(如 "2099753433048526850")。
|
|
||||||
*
|
|
||||||
* <p>必须用 id 而非类型名:案例表 business_type 列实测存的就是提报主键 id,
|
|
||||||
* 传类型名(如「硬件电路」)后端按原值精确匹配不到 → 实测 total=0。</p>
|
|
||||||
*/
|
|
||||||
value: string
|
value: string
|
||||||
/** 展示名(后端下发的 businessType 名称,如「硬件电路」) */
|
|
||||||
label: string
|
label: string
|
||||||
/** 该业务类型的案例数(后端统计口径,可能为 0) */
|
|
||||||
count: number
|
count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 案例附件(页面消费,由后端 ProjectCaseAttachmentVo 映射而来) */
|
|
||||||
export interface CaseAttachment {
|
export interface CaseAttachment {
|
||||||
/** 原始文件名 */
|
|
||||||
name: string
|
name: string
|
||||||
/** 文件大小(后端已格式化的文本形式,可能为空) */
|
|
||||||
size: string
|
size: string
|
||||||
/** 附件访问地址 */
|
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 案例正文区块(后端「自定义目录」一项 = 页面一张白卡)。
|
|
||||||
*
|
|
||||||
* <p>标题由后端下发,前端不写死:增删区块(项目背景 / 客户痛点 / 需求描述 / 交付物 …)
|
|
||||||
* 完全由 vm_project_case_catalog 数据决定。</p>
|
|
||||||
*/
|
|
||||||
export interface CaseSection {
|
export interface CaseSection {
|
||||||
/** 区块标题(vm_project_case_catalog.title) */
|
|
||||||
title: string
|
title: string
|
||||||
/** 区块内容:富文本 HTML(可能内嵌 img / video) */
|
|
||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 案例 */
|
|
||||||
export interface Case {
|
export interface Case {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
@@ -56,224 +26,124 @@ export interface Case {
|
|||||||
cover: string
|
cover: string
|
||||||
finishDate: string
|
finishDate: string
|
||||||
teamSize: number
|
teamSize: number
|
||||||
/** 规模展示值(如 '300万'、'10-50万'),筛选区间见 CaseQuery.scales */
|
|
||||||
scale: string
|
scale: string
|
||||||
industry: string
|
industry: string
|
||||||
duration: string
|
duration: string
|
||||||
clientType: string
|
clientType: string
|
||||||
/**
|
|
||||||
* 正文区块列表(标题 + 富文本)。
|
|
||||||
*
|
|
||||||
* <p>来自后端 catalogs(vm_project_case_catalog,按 sort_order 升序);
|
|
||||||
* 列表 / 精选接口不下发,故那些场景为空数组。</p>
|
|
||||||
*/
|
|
||||||
sections: CaseSection[]
|
sections: CaseSection[]
|
||||||
/** 附件列表(仅案例详情接口返回,列表页为空) */
|
|
||||||
attachments?: CaseAttachment[]
|
attachments?: CaseAttachment[]
|
||||||
video: string
|
video: string
|
||||||
isFavorite: boolean
|
isFavorite: boolean
|
||||||
/** 收藏量(详情/列表接口下发,本地收藏翻转时同步 ±1) */
|
|
||||||
favoriteCount: number
|
favoriteCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 案例筛选条件。
|
|
||||||
*
|
|
||||||
* <p>三类选项全部由后端下发,前端不写死,且都来自 GET /categories({@link CaseSelectionModel}):
|
|
||||||
* 业务类型({@link CaseTypeOption})、所属行业、项目规模;仅完成日期区间由前端生成。</p>
|
|
||||||
*/
|
|
||||||
export interface CaseQuery {
|
export interface CaseQuery {
|
||||||
/** 业务类型**提报主键 id**(多选,空数组 = 全部;值域见 CaseTypeOption.value) */
|
|
||||||
categories: string[]
|
categories: string[]
|
||||||
/** 所属行业名称(多选,空数组 = 全部;值域见 BusinessIndustryOption.name) */
|
|
||||||
industries: string[]
|
industries: string[]
|
||||||
/** 项目规模档位(多选,空数组 = 全部;整档对象直接下发,值域见 /categories) */
|
|
||||||
scales: ScaleLimit[]
|
scales: ScaleLimit[]
|
||||||
dateRange: [string, string] | null
|
dateRange: [string, string] | null
|
||||||
/** 只看收藏(对应后端 isFavorite=1) */
|
|
||||||
favoriteOnly: boolean
|
favoriteOnly: boolean
|
||||||
keyword: string
|
keyword: string
|
||||||
page: number
|
page: number
|
||||||
pageSize: number
|
pageSize: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================ 后端 VO / BO ============================ */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /web/projectCase/list 的查询条件(后端 ProjectCaseUserBo)。
|
|
||||||
*
|
|
||||||
* <p>businessTypes 按案例表 business_type 列的**原值**精确匹配(in),而该列实测存的是
|
|
||||||
* **提报主键 id**(非类型名),所以前端下发的是 CaseTypeOption.value(id)。</p>
|
|
||||||
*/
|
|
||||||
export interface ProjectCaseUserBo {
|
export interface ProjectCaseUserBo {
|
||||||
/** 业务类型提报主键 id 列表 */
|
|
||||||
businessTypes?: string[]
|
businessTypes?: string[]
|
||||||
/** 业务/行业名称列表 */
|
|
||||||
businessIndustries?: string[]
|
businessIndustries?: string[]
|
||||||
/** 规模区间列表(区间之间取并集) */
|
|
||||||
scales?: ScaleLimit[]
|
scales?: ScaleLimit[]
|
||||||
/** 只看收藏:1 = 是;不传 = 全部 */
|
|
||||||
isFavorite?: number
|
isFavorite?: number
|
||||||
/** 关键字(案例名 / 客户类型 / 项目周期 / 团队人数) */
|
|
||||||
key?: string
|
key?: string
|
||||||
/** 开始时间 */
|
|
||||||
beginTime?: string
|
beginTime?: string
|
||||||
/** 结束时间 */
|
|
||||||
endTime?: string
|
endTime?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 规模区间(后端 UpperAndLowerLimitsModel):任一端为空表示该方向不设限 */
|
|
||||||
export interface ScaleLimit {
|
export interface ScaleLimit {
|
||||||
lowerLimit?: number | null
|
lowerLimit?: number | null
|
||||||
upperLimit?: number | null
|
upperLimit?: number | null
|
||||||
/** 展示样式:以下 / 区间 / 以上 */
|
|
||||||
model?: string | null
|
model?: string | null
|
||||||
/** 档位文字(后端拼好,如「28–46万」) */
|
|
||||||
label?: string | null
|
label?: string | null
|
||||||
sort?: number | null
|
sort?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 所属行业选项(后端 BusinessIndustryModel) */
|
|
||||||
export interface BusinessIndustryOption {
|
export interface BusinessIndustryOption {
|
||||||
name: string | null
|
name: string | null
|
||||||
sort: number | null
|
sort: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /web/projectCase/categories —— 筛选面板选项。
|
|
||||||
*
|
|
||||||
* <p>左侧筛选**除完成时间外**的选项全部由本接口一次下发:
|
|
||||||
* 业务类型({@link CaseSelectionModel.projectCaseStatisticsVo})、
|
|
||||||
* 所属行业({@link CaseSelectionModel.businessIndustry})、
|
|
||||||
* 项目规模({@link CaseSelectionModel.upperAndLowerLimits})。</p>
|
|
||||||
*/
|
|
||||||
export interface CaseSelectionModel {
|
export interface CaseSelectionModel {
|
||||||
businessIndustry: BusinessIndustryOption[] | null
|
businessIndustry: BusinessIndustryOption[] | null
|
||||||
upperAndLowerLimits: ScaleLimit[] | null
|
upperAndLowerLimits: ScaleLimit[] | null
|
||||||
/** 业务类型统计(承载业务类型筛选项与案例总数) */
|
|
||||||
projectCaseStatisticsVo: ProjectCaseStatisticsVo | null
|
projectCaseStatisticsVo: ProjectCaseStatisticsVo | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 案例总数 + 各业务类型案例数 */
|
|
||||||
export interface ProjectCaseStatisticsVo {
|
export interface ProjectCaseStatisticsVo {
|
||||||
/** 案例总数(口径:未删除且在架) */
|
|
||||||
total: number | null
|
total: number | null
|
||||||
/** 各业务类型的案例数(按案例数倒序) */
|
|
||||||
projectCaseCounts: ProjectCaseCountVo[] | null
|
projectCaseCounts: ProjectCaseCountVo[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 单个业务类型的案例数 */
|
|
||||||
export interface ProjectCaseCountVo {
|
export interface ProjectCaseCountVo {
|
||||||
/** 业务类型原值(提报主键 id 的字符串形式) */
|
|
||||||
id: string | null
|
id: string | null
|
||||||
/** 业务类型名称 */
|
|
||||||
businessType: string | null
|
businessType: string | null
|
||||||
caseCount: number | null
|
caseCount: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /web/projectCase/list —— 案例分页列表 + 业务数据统计。
|
|
||||||
*
|
|
||||||
* <p>统计口径是「本次查询结果」,即跟随筛选条件变化,不是全表统计。</p>
|
|
||||||
*/
|
|
||||||
export interface ProjectCaseAndCaseCountVo {
|
export interface ProjectCaseAndCaseCountVo {
|
||||||
projectCasePage: ProjectCasePageVo | null
|
projectCasePage: ProjectCasePageVo | null
|
||||||
projectCaseStatisticsVo: ProjectCaseStatisticsVo | null
|
projectCaseStatisticsVo: ProjectCaseStatisticsVo | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 分页体(后端 PageResult:rows + total) */
|
|
||||||
export interface ProjectCasePageVo {
|
export interface ProjectCasePageVo {
|
||||||
total: number
|
total: number
|
||||||
rows: ProjectCaseVo[]
|
rows: ProjectCaseVo[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 案例库视图对象(列表 / 详情 / mostViewed 共用;后端 Long 主键序列化为字符串) */
|
|
||||||
export interface ProjectCaseVo {
|
export interface ProjectCaseVo {
|
||||||
id: string
|
id: string
|
||||||
/** 案例编号 */
|
|
||||||
caseNo: string | null
|
caseNo: string | null
|
||||||
/** 案例名称 */
|
|
||||||
caseName: string | null
|
caseName: string | null
|
||||||
/** 业务类型原值(案例表该列存提报主键 id 或类型名称) */
|
|
||||||
businessType: string | null
|
businessType: string | null
|
||||||
/** 业务类型名称(后端 JOIN 提报表翻译后的展示名) */
|
|
||||||
projectTypeName: string | null
|
projectTypeName: string | null
|
||||||
/** 业务/行业 */
|
|
||||||
businessIndustry: string | null
|
businessIndustry: string | null
|
||||||
/** 上下架:1 上架,0 下架 */
|
|
||||||
enabled: number | null
|
enabled: number | null
|
||||||
/** 规模数值(单位:万) */
|
|
||||||
scale: number | null
|
scale: number | null
|
||||||
/** 预览量(详情接口每次调用自增 1) */
|
|
||||||
previewCount: number | null
|
previewCount: number | null
|
||||||
/** 浏览量(已废弃,勿用于排序) */
|
|
||||||
viewCount: number | null
|
viewCount: number | null
|
||||||
/** 收藏量 */
|
|
||||||
favoriteCount: number | null
|
favoriteCount: number | null
|
||||||
/** 封面(后端下发的是 OSS 文件标识,未必是可直接访问的 URL) */
|
|
||||||
coverUrl: string | null
|
coverUrl: string | null
|
||||||
/** 客户类型 */
|
|
||||||
customerType: string | null
|
customerType: string | null
|
||||||
/** 所属行业 */
|
|
||||||
industry: string | null
|
industry: string | null
|
||||||
/** 项目周期 */
|
|
||||||
projectCycle: string | null
|
projectCycle: string | null
|
||||||
/** 服务团队人数 */
|
|
||||||
serviceTeam: number | null
|
serviceTeam: number | null
|
||||||
/** 完成时间 yyyy-MM-dd */
|
|
||||||
completedAt: string | null
|
completedAt: string | null
|
||||||
/** 是否收藏:1 是,0 否(详情与列表接口均会回填) */
|
|
||||||
isFavorite: number | null
|
isFavorite: number | null
|
||||||
/**
|
|
||||||
* 项目背景富文本段列表(仅详情返回)。
|
|
||||||
*
|
|
||||||
* @deprecated 后端已改用 {@link ProjectCaseVo.catalogs}(带标题的自定义目录),该字段恒为 null。
|
|
||||||
*/
|
|
||||||
backgrounds: ProjectCaseBackgroundVo[] | null
|
backgrounds: ProjectCaseBackgroundVo[] | null
|
||||||
/** 正文区块列表(仅详情返回;后端按 sort_order、id 升序下发) */
|
|
||||||
catalogs: ProjectCaseCatalogVo[] | null
|
catalogs: ProjectCaseCatalogVo[] | null
|
||||||
/** 附件列表(仅详情返回) */
|
|
||||||
attachments: ProjectCaseAttachmentVo[] | null
|
attachments: ProjectCaseAttachmentVo[] | null
|
||||||
/** 更新时间 yyyy-MM-dd HH:mm:ss */
|
|
||||||
updateTime: string | null
|
updateTime: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 案例项目背景富文本段(vm_project_case_detail)。
|
|
||||||
*
|
|
||||||
* @deprecated 已被 {@link ProjectCaseCatalogVo} 取代(新结构带 title),后端恒下发 null。
|
|
||||||
*/
|
|
||||||
export interface ProjectCaseBackgroundVo {
|
export interface ProjectCaseBackgroundVo {
|
||||||
id: string | null
|
id: string | null
|
||||||
projectId: string | null
|
projectId: string | null
|
||||||
/** 富文本 HTML */
|
|
||||||
content: string | null
|
content: string | null
|
||||||
sortOrder: number | null
|
sortOrder: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 案例自定义目录 / 正文区块(vm_project_case_catalog) */
|
|
||||||
export interface ProjectCaseCatalogVo {
|
export interface ProjectCaseCatalogVo {
|
||||||
id: string | null
|
id: string | null
|
||||||
projectId: string | null
|
projectId: string | null
|
||||||
/** 目录标题(如「项目背景」「客户痛点」) */
|
|
||||||
title: string | null
|
title: string | null
|
||||||
/** 目录内容富文本 HTML */
|
|
||||||
content: string | null
|
content: string | null
|
||||||
/** 排序值,升序 */
|
|
||||||
sortOrder: number | null
|
sortOrder: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 案例附件(vm_project_case_attachment) */
|
|
||||||
export interface ProjectCaseAttachmentVo {
|
export interface ProjectCaseAttachmentVo {
|
||||||
id: string | null
|
id: string | null
|
||||||
projectId: string | null
|
projectId: string | null
|
||||||
/** OSS 标识 */
|
|
||||||
ossId: string | null
|
ossId: string | null
|
||||||
/** 原始文件名 */
|
|
||||||
originalName: string | null
|
originalName: string | null
|
||||||
/** 文件大小(文本形式) */
|
|
||||||
fileSize: string | null
|
fileSize: string | null
|
||||||
/** 附件访问地址 */
|
|
||||||
url: string | null
|
url: string | null
|
||||||
sortOrder: number | null
|
sortOrder: number | null
|
||||||
}
|
}
|
||||||
|
|||||||
4
src/types/business/contract.d.ts
vendored
4
src/types/business/contract.d.ts
vendored
@@ -1,11 +1,8 @@
|
|||||||
// 合同中心实体(PRD M6)
|
|
||||||
|
|
||||||
import type { FileInfo } from '@/types/api'
|
import type { FileInfo } from '@/types/api'
|
||||||
|
|
||||||
/** 签署状态 */
|
|
||||||
export type SignStatus = '履行中' | '已过期' | '待签署'
|
export type SignStatus = '履行中' | '已过期' | '待签署'
|
||||||
|
|
||||||
/** 合同 */
|
|
||||||
export interface Contract {
|
export interface Contract {
|
||||||
id: string
|
id: string
|
||||||
contractNo: string
|
contractNo: string
|
||||||
@@ -21,7 +18,6 @@ export interface Contract {
|
|||||||
files: FileInfo[]
|
files: FileInfo[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 合同列表查询条件 */
|
|
||||||
export interface ContractQuery {
|
export interface ContractQuery {
|
||||||
keyword: string
|
keyword: string
|
||||||
page: number
|
page: number
|
||||||
|
|||||||
95
src/types/business/demand.d.ts
vendored
95
src/types/business/demand.d.ts
vendored
@@ -1,162 +1,70 @@
|
|||||||
// 需求提报(对接真实后端)
|
|
||||||
// 草稿:POST /web/workOrderDraft/{getDraft,add,upload,reset} → vm_work_order_draft(WorkOrderDraftBo / Vo)
|
|
||||||
// 提交:POST /web/requirementPool/create → vm_project_requirement(入参 WorkOrderDraftBo,与草稿同 BO,附件随请求体)
|
|
||||||
// (同控制器的 /submit 入参是 RdProjectBo、会额外落 vm_rd_project,前端当前未使用)
|
|
||||||
// 业务类型:GET /web/projectSubmission/list(vm_project_submission,@SaIgnore 免登录)
|
|
||||||
// 与首页 /web/home-page/service-type-list 是同一个 service 方法,数据完全一致
|
|
||||||
// 预算范围:GET /system/dict/data/type/vm_budget_range(字典)
|
|
||||||
// 注意:草稿 / 提交 / 字典 需登录态(匿名实测 401);业务类型列表免登录
|
|
||||||
|
|
||||||
/** 业务类型图标标识(对应前端内联 SVG,见 views/demand/cpns/BizTypeIcon.vue) */
|
|
||||||
export type BizIconKey = 'code' | 'chip' | 'iot' | 'factory' | 'other'
|
export type BizIconKey = 'code' | 'chip' | 'iot' | 'factory' | 'other'
|
||||||
|
|
||||||
/** 字典项(GET /system/dict/data/type/{dictType}) */
|
|
||||||
export interface DictOption {
|
export interface DictOption {
|
||||||
dictLabel: string
|
dictLabel: string
|
||||||
dictValue: string
|
dictValue: string
|
||||||
dictSort: number
|
dictSort: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 项目提报列表项(GET /web/projectSubmission/list,只取业务类型卡片用得到的字段) */
|
|
||||||
export interface SubmissionBizTypeVo {
|
export interface SubmissionBizTypeVo {
|
||||||
/** 提报主键(业务详情接口 /web/projectSubmission/{id} 用它) */
|
|
||||||
id: string | number
|
id: string | number
|
||||||
/** 业务类型(既作展示名,也作下发给后端的值) */
|
|
||||||
businessType: string | null
|
businessType: string | null
|
||||||
/** 标题 */
|
|
||||||
title: string | null
|
title: string | null
|
||||||
/** 副标题(卡片一句话描述的数据源) */
|
|
||||||
subtitle: string | null
|
subtitle: string | null
|
||||||
/** 封面(⚠️ 实测存的是 OSS 文件标识,非完整地址) */
|
|
||||||
coverUrl: string | null
|
coverUrl: string | null
|
||||||
/** Logo(⚠️ 同上,需经 /resource/oss/listByIds 换取真实地址) */
|
|
||||||
logoUrl: string | null
|
logoUrl: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 业务类型卡片选项。
|
|
||||||
*
|
|
||||||
* <p>值域**完全由接口决定**:提报列表返回几条就渲染几张卡,前端不追加任何兜底项
|
|
||||||
* (「其他」也不例外——接口有才显示,没有就不显示)。字段映射:`businessType` 作下发值、
|
|
||||||
* `title` 作展示名、`subtitle` 作描述、`logoUrl` 作图标。但后端这两个字段目前是**半成品**——
|
|
||||||
* subtitle 常与类型名重复、logo 存的是 OSS 文件标识——故 desc / icon 保留前端映射表兜底
|
|
||||||
* (见 `utils/bizTypeMeta.ts`,只按类型名补描述/图标,不产生新选项)。</p>
|
|
||||||
*/
|
|
||||||
export interface BizTypeOption {
|
export interface BizTypeOption {
|
||||||
/** 下发给后端的值(= 提报 businessType,如「互联网软件」) */
|
|
||||||
value: string
|
value: string
|
||||||
/** 展示名 */
|
|
||||||
label: string
|
label: string
|
||||||
/** 一句话描述(后端 subtitle 可用时优先,否则用前端映射) */
|
|
||||||
desc: string
|
desc: string
|
||||||
/** 图标标识(前端内联 SVG;`iconUrl` 为空时渲染它) */
|
|
||||||
icon: BizIconKey
|
icon: BizIconKey
|
||||||
/** 后端 Logo 图片地址(已换链可直接访问时优先渲染,否则回退 `icon` 内联 SVG) */
|
|
||||||
iconUrl?: string
|
iconUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 草稿附件(vm_work_order_draft_attachment) */
|
|
||||||
export interface DemandAttachment {
|
export interface DemandAttachment {
|
||||||
id?: string | number | null
|
id?: string | number | null
|
||||||
draftId?: string | number | null
|
draftId?: string | number | null
|
||||||
/** OSS 文件主键 */
|
|
||||||
ossId?: string | number | null
|
ossId?: string | number | null
|
||||||
/** 原始文件名 */
|
|
||||||
originalName?: string | null
|
originalName?: string | null
|
||||||
/** 文件访问地址 */
|
|
||||||
url?: string | null
|
url?: string | null
|
||||||
/** 文件大小(字节) */
|
|
||||||
fileSize?: number | null
|
fileSize?: number | null
|
||||||
/** 文件格式后缀(pdf / docx / zip …) */
|
|
||||||
type?: string | null
|
type?: string | null
|
||||||
/** 上传时间(BaseEntity.createTime;本地上传时由前端补当前时间) */
|
|
||||||
createTime?: string | null
|
createTime?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 需求提报表单(字段与后端 WorkOrderDraftBo 对应,另有 1 个纯前端状态字段:budgetManual) */
|
|
||||||
export interface Demand {
|
export interface Demand {
|
||||||
/** 草稿主键(编辑已有草稿时回填) */
|
|
||||||
id?: string | number | null
|
id?: string | number | null
|
||||||
/** 业务类型(= 项目提报的 businessType,如「互联网软件」;后端落库存的也是这个名称,不是提报 id) */
|
|
||||||
bizType: string
|
bizType: string
|
||||||
/** 项目名称 */
|
|
||||||
projectName: string
|
projectName: string
|
||||||
/** 核心需求描述 */
|
|
||||||
requirementDesc: string
|
requirementDesc: string
|
||||||
/**
|
|
||||||
* 预算范围:选预设档位时为字典 `vm_budget_range` 的 dictValue('0'~'3');
|
|
||||||
* 在第 5 个「手填」输入框里写字时为用户自己填的金额文本(如「80万」)。
|
|
||||||
*
|
|
||||||
* <p>⚠️ 手填内容只能放本字段:后端 `WorkOrderDraftBo.budgetCustom` 是被注释掉的、
|
|
||||||
* `vm_work_order_draft` 表也没有 `budget_custom` 列(VO 上那个 `budgetCustom` 恒为 null),
|
|
||||||
* 所以没有第二个位置可以承载手填文本。提交落 `vm_project_requirement.budget_range`,
|
|
||||||
* 该列为 **varchar(30)**,故手填文本限长见 `utils/budgetMeta.ts#MANUAL_BUDGET_MAX`。</p>
|
|
||||||
*/
|
|
||||||
budgetRange: string
|
budgetRange: string
|
||||||
/** 是否由「手填」输入框提供预算(**纯前端状态**:后端 BO/VO 与两张表都没有该字段,下发前需剔除) */
|
|
||||||
budgetManual: boolean
|
budgetManual: boolean
|
||||||
/** 预计开发周期(工作日) */
|
|
||||||
developmentCycle: number | ''
|
developmentCycle: number | ''
|
||||||
/** 补充备注 */
|
|
||||||
supplementaryRemarks: string
|
supplementaryRemarks: string
|
||||||
/** 用户名称 */
|
|
||||||
userName: string
|
userName: string
|
||||||
/** 用户电话 */
|
|
||||||
userPhone: string
|
userPhone: string
|
||||||
/** 附件列表(后端按此逐条转存为需求附件) */
|
|
||||||
attachmentInfo: DemandAttachment[]
|
attachmentInfo: DemandAttachment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 直接提报请求体(对应后端 `RdProjectBo` 的可写子集,POST /web/requirementPool/submit)。
|
|
||||||
*
|
|
||||||
* <p>⚠️ **当前前端未使用**:提交走的是 `/web/requirementPool/create`(入参 `WorkOrderDraftBo`,
|
|
||||||
* 与草稿同一个 BO,附件随请求体下发)。本类型对应的是后端 `/submit` 链路,保留以备切回。</p>
|
|
||||||
*
|
|
||||||
* <p>⚠️ 与草稿 BO 的字段名**不同**,别混用:业务类型叫 `businessType`(草稿叫 `bizType`)、
|
|
||||||
* 补充备注叫 `extraRemark`(草稿叫 `supplementaryRemarks`)。</p>
|
|
||||||
*
|
|
||||||
* <p>⚠️ 两个字段**不在**此请求体里:</p>
|
|
||||||
* <ul>
|
|
||||||
* <li>`developmentCycle`(预计开发周期)—— `vm_project_requirement` 与 `vm_rd_project`
|
|
||||||
* 两张表都**没有**该列,所以提报链路无处存放(草稿表 `vm_work_order_draft.development_cycle`
|
|
||||||
* 里有,提交后即丢失,草稿路径同样如此);</li>
|
|
||||||
* <li>`attachmentInfo` —— `RdProjectBo` 无附件字段,后端按登录用户从**已落库草稿**里
|
|
||||||
* 按 `ossId` 搬运到 `vm_project_requirement_attachment`(故走该链路时调用方需先落一次草稿,
|
|
||||||
* 这也正是前端改回 `/create` 的原因之一)。</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* <p>`projectNo` / `sourceType` / `priority` / `status` / `progress` 在 BO 上虽标了 `@NotBlank`,
|
|
||||||
* 但注解都挂 `AddGroup` / `EditGroup`,而控制器是裸 `@Validated`(未指定分组)⇒ 实际**不生效**;
|
|
||||||
* 这些值由后端统一生成/兜默认(`project_no` 复用需求编号、`source_type=EXTERNAL`、
|
|
||||||
* `priority=UNASSESSED`、`status=IN_PROGRESS`、`progress=0`),前端不下发。</p>
|
|
||||||
*/
|
|
||||||
export interface DemandSubmitBo {
|
export interface DemandSubmitBo {
|
||||||
/** 业务类型名称(如「互联网软件」;后端落库存的是名称,不是提报 id) */
|
|
||||||
businessType: string
|
businessType: string
|
||||||
/** 项目名称 */
|
|
||||||
projectName: string
|
projectName: string
|
||||||
/** 核心需求描述 */
|
|
||||||
requirementDesc: string
|
requirementDesc: string
|
||||||
/** 联系人姓名(缺失时后端回退取登录用户资料) */
|
|
||||||
customerName: string
|
customerName: string
|
||||||
/** 联系人电话(同上) */
|
|
||||||
customerPhone: string
|
customerPhone: string
|
||||||
/** 预算范围:预设档位 = 字典码值;手填 = 用户输入的金额文本 */
|
|
||||||
budgetRange: string | null
|
budgetRange: string | null
|
||||||
/** 补充备注(对应草稿的 supplementaryRemarks) */
|
|
||||||
extraRemark: string | null
|
extraRemark: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 草稿详情(后端 WorkOrderDraftVo,取前端用得到的子集) */
|
|
||||||
export interface DemandDraftVo {
|
export interface DemandDraftVo {
|
||||||
id: string | number | null
|
id: string | number | null
|
||||||
bizType: string | null
|
bizType: string | null
|
||||||
projectName: string | null
|
projectName: string | null
|
||||||
requirementDesc: string | null
|
requirementDesc: string | null
|
||||||
/** 预算范围:预设档位=字典 dictValue;手填=用户在第 5 个输入框里写的文本(前端据此反推 budgetManual) */
|
|
||||||
budgetRange: string | null
|
budgetRange: string | null
|
||||||
/** ⚠️ 后端 VO 声明了该字段但**恒为 null**(Bo 里被注释、表无该列)→ 不要依赖它回填手填内容 */
|
|
||||||
budgetCustom: string | null
|
budgetCustom: string | null
|
||||||
developmentCycle: number | null
|
developmentCycle: number | null
|
||||||
supplementaryRemarks: string | null
|
supplementaryRemarks: string | null
|
||||||
@@ -165,7 +73,6 @@ export interface DemandDraftVo {
|
|||||||
attachmentInfo: DemandAttachment[] | null
|
attachmentInfo: DemandAttachment[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 附件上传返回(后端 SysOssVo) */
|
|
||||||
export interface OssUploadVo {
|
export interface OssUploadVo {
|
||||||
ossId: string | number
|
ossId: string | number
|
||||||
url: string
|
url: string
|
||||||
@@ -173,9 +80,7 @@ export interface OssUploadVo {
|
|||||||
fileSuffix: string
|
fileSuffix: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** OSS 换链返回项(GET /resource/oss/listByIds/{ids} 的子集) */
|
|
||||||
export interface OssUrlVo {
|
export interface OssUrlVo {
|
||||||
ossId: string | number
|
ossId: string | number
|
||||||
/** 可直接访问的地址(阿里云 OSS 公网 URL) */
|
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
|||||||
65
src/types/business/finance.d.ts
vendored
65
src/types/business/finance.d.ts
vendored
@@ -1,21 +1,8 @@
|
|||||||
// 财务中心实体(账单/支付/发票/抬头/统计)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 账单状态(页面展示口径,中文)。
|
|
||||||
*
|
|
||||||
* <p>与后端 {@code vm_bill.status} 的英文编码(见 `BillStatusCode`)是多对一的展示映射,
|
|
||||||
* 转换收口在 `utils/billVo.ts#toBillStatus`。</p>
|
|
||||||
*/
|
|
||||||
export type BillStatus = '待支付' | '已支付' | '未到期' | '已取消'
|
export type BillStatus = '待支付' | '已支付' | '未到期' | '已取消'
|
||||||
|
|
||||||
/** 后端账单状态编码(`vm_bill.status` 值域) */
|
|
||||||
export type BillStatusCode = 'UNSTARTED' | 'PENDING' | 'PAID' | 'CANCELLED'
|
export type BillStatusCode = 'UNSTARTED' | 'PENDING' | 'PAID' | 'CANCELLED'
|
||||||
|
|
||||||
/**
|
|
||||||
* 后端账单视图对象 —— `GET /web/bill/list` 的响应元素(对应后端 `BillVo`)。
|
|
||||||
*
|
|
||||||
* <p>金额单位 **元**;`id` 为雪花 id 时 JSON 里是字符串,小 id 是数字,一律 `String(id)` 归一。</p>
|
|
||||||
*/
|
|
||||||
export interface BillVo {
|
export interface BillVo {
|
||||||
id: number | string
|
id: number | string
|
||||||
billNo?: string
|
billNo?: string
|
||||||
@@ -24,36 +11,19 @@ export interface BillVo {
|
|||||||
projectName?: string
|
projectName?: string
|
||||||
userId?: number | string
|
userId?: number | string
|
||||||
amount?: number
|
amount?: number
|
||||||
/** 业务类型字典值:internet_software / hardware_circuit / iot_solution / manufacturing */
|
|
||||||
bizType?: string
|
bizType?: string
|
||||||
/** 账单类型:ADVANCE / PROGRESS / FINAL / OTHER */
|
|
||||||
billType?: string
|
billType?: string
|
||||||
/** 期次名,如「第2期·尾款」(页面「账单名称」列) */
|
|
||||||
periodName?: string
|
periodName?: string
|
||||||
/** 占项目总额比例(%) */
|
|
||||||
periodRatio?: number
|
periodRatio?: number
|
||||||
status?: BillStatusCode | string
|
status?: BillStatusCode | string
|
||||||
/** 支付通道:WECHAT / ALIPAY / BANK / WORKWX */
|
|
||||||
payChannel?: string
|
payChannel?: string
|
||||||
/** 交易流水号(支付成功回写) */
|
|
||||||
transactionNo?: string
|
transactionNo?: string
|
||||||
/** 交易名称(支付时回传) */
|
|
||||||
tradeName?: string
|
tradeName?: string
|
||||||
payTime?: string
|
payTime?: string
|
||||||
dueDate?: string
|
dueDate?: string
|
||||||
createTime?: string
|
createTime?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 后端分期计划视图对象(`vm_bill_plan` + 该期支付单信息,对应 `BillPlanVo`)。
|
|
||||||
*
|
|
||||||
* <p>由项目详情「合同财务」那个现成接口 `GET /web/requirementPool/get/finance/{需求id}`
|
|
||||||
* 的 `billPlans` 下发;收银台「订单明细」复用同一份数据
|
|
||||||
* (见 `api/finance.ts#getCheckoutBills`),没有再单开端点。</p>
|
|
||||||
*
|
|
||||||
* <p>⚠️ 本栈支付口径是**计划 id**:`vm_finance_payment.bill_id` 实测存的就是 `vm_bill_plan.id`,
|
|
||||||
* 所以收银台 `?ids=` 传的是这个 `id`,而不是 `vm_bill.id`。</p>
|
|
||||||
*/
|
|
||||||
export interface BillPlanVo {
|
export interface BillPlanVo {
|
||||||
id: number | string
|
id: number | string
|
||||||
projectId?: number | string
|
projectId?: number | string
|
||||||
@@ -61,74 +31,46 @@ export interface BillPlanVo {
|
|||||||
projectName?: string
|
projectName?: string
|
||||||
userId?: number | string
|
userId?: number | string
|
||||||
bizType?: string
|
bizType?: string
|
||||||
/** ⚠️ BigDecimal ⇒ 后端以**字符串**下发("120000.00"),用前必须 `Number()` 归一 */
|
|
||||||
totalAmount?: number | string
|
totalAmount?: number | string
|
||||||
/** 分期总期数 */
|
|
||||||
periods?: number
|
periods?: number
|
||||||
/** 本期期次序号(1 起) */
|
|
||||||
currentPeriod?: number
|
currentPeriod?: number
|
||||||
/** 每期比例规则,如 "40,30,30" */
|
|
||||||
periodRule?: string
|
periodRule?: string
|
||||||
nextDate?: string
|
nextDate?: string
|
||||||
/** 计划状态:ACTIVE / DONE / CLOSED / UNSTARTED */
|
|
||||||
status?: string
|
status?: string
|
||||||
/** 该期支付单的商户订单号(页面「账单号」用它);未支付为空 */
|
|
||||||
orderNo?: string
|
orderNo?: string
|
||||||
transactionNo?: string
|
transactionNo?: string
|
||||||
payChannel?: string
|
payChannel?: string
|
||||||
payAmount?: number | string
|
payAmount?: number | string
|
||||||
/** 支付单状态:CREATED / SUCCESS / FAILED / REFUNDED */
|
|
||||||
payStatus?: string
|
payStatus?: string
|
||||||
payTime?: string
|
payTime?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 发票状态 */
|
|
||||||
export type InvoiceStatus = '已开票' | '开票中' | '未开票'
|
export type InvoiceStatus = '已开票' | '开票中' | '未开票'
|
||||||
|
|
||||||
/** 支付方式 */
|
|
||||||
export type PayMethod = 'wechat' | 'alipay'
|
export type PayMethod = 'wechat' | 'alipay'
|
||||||
|
|
||||||
/**
|
|
||||||
* 后端统一支付响应 `org.dromara.pay.domain.PaymentResponse`(字段名与后端一致)。
|
|
||||||
*
|
|
||||||
* <p>扫码(Native)渠道回 `codeUrl`;支付宝电脑网站支付回 `formBody`
|
|
||||||
* ——注意该字段名虽叫 formBody,实测后端走 `pageExecute(request, "GET")`,
|
|
||||||
* 内容其实是**收银台跳转地址**而非 HTML 表单,前端两种形态都兜住。</p>
|
|
||||||
*/
|
|
||||||
export interface PayOrderResponse {
|
export interface PayOrderResponse {
|
||||||
/** 支付渠道:WECHAT / ALIPAY / UNIONPAY */
|
|
||||||
channel: string
|
channel: string
|
||||||
/** 支付场景:JSAPI / NATIVE / WEB_SCAN / WEB_PAGE / APP */
|
|
||||||
scene: string
|
scene: string
|
||||||
/** 扫码支付二维码内容:微信 Native 为 code_url,支付宝订单码为 qr_code */
|
|
||||||
codeUrl?: string
|
codeUrl?: string
|
||||||
/** 支付宝电脑网站支付返回的收银台跳转地址 / HTML 表单 */
|
|
||||||
formBody?: string
|
formBody?: string
|
||||||
/** 支付宝 APP 支付订单串 */
|
|
||||||
orderString?: string
|
orderString?: string
|
||||||
/** H5 支付链接 */
|
|
||||||
h5Url?: string
|
h5Url?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 账单(页面视图模型;由 `utils/billVo.ts#toBill` 从后端 `BillVo` 映射而来) */
|
|
||||||
export interface Bill {
|
export interface Bill {
|
||||||
id: string
|
id: string
|
||||||
/** 分期计划名,如 '第一期·预付款 比例33%'(后端 `periodName`) */
|
|
||||||
planName: string
|
planName: string
|
||||||
billNo: string
|
billNo: string
|
||||||
/** 交易流水号(后端 `transactionNo`) */
|
|
||||||
tradeNo?: string
|
tradeNo?: string
|
||||||
projectName: string
|
projectName: string
|
||||||
amount: number
|
amount: number
|
||||||
status: BillStatus
|
status: BillStatus
|
||||||
createTime: string
|
createTime: string
|
||||||
/** 支付通道英文编码(WECHAT / ALIPAY / BANK / WORKWX) */
|
|
||||||
payChannel?: string
|
payChannel?: string
|
||||||
/** 支付时间(后端 `payTime`) */
|
|
||||||
payTime?: string
|
payTime?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 账单查询条件 */
|
|
||||||
export interface BillQuery {
|
export interface BillQuery {
|
||||||
keyword: string
|
keyword: string
|
||||||
status: BillStatus | 'all'
|
status: BillStatus | 'all'
|
||||||
@@ -136,7 +78,6 @@ export interface BillQuery {
|
|||||||
pageSize: number
|
pageSize: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 支付单(二维码) */
|
|
||||||
export interface Payment {
|
export interface Payment {
|
||||||
billId: string
|
billId: string
|
||||||
method: PayMethod
|
method: PayMethod
|
||||||
@@ -144,7 +85,6 @@ export interface Payment {
|
|||||||
qrExpiresInSeconds: number
|
qrExpiresInSeconds: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 支付结果 */
|
|
||||||
export interface PaymentResult {
|
export interface PaymentResult {
|
||||||
method: PayMethod
|
method: PayMethod
|
||||||
amount: number
|
amount: number
|
||||||
@@ -153,7 +93,6 @@ export interface PaymentResult {
|
|||||||
bills: Bill[]
|
bills: Bill[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 发票 */
|
|
||||||
export interface Invoice {
|
export interface Invoice {
|
||||||
id: string
|
id: string
|
||||||
planName: string
|
planName: string
|
||||||
@@ -165,7 +104,6 @@ export interface Invoice {
|
|||||||
applyTime: string
|
applyTime: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 发票查询条件 */
|
|
||||||
export interface InvoiceQuery {
|
export interface InvoiceQuery {
|
||||||
keyword: string
|
keyword: string
|
||||||
status: InvoiceStatus | 'all'
|
status: InvoiceStatus | 'all'
|
||||||
@@ -173,7 +111,6 @@ export interface InvoiceQuery {
|
|||||||
pageSize: number
|
pageSize: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 发票抬头 */
|
|
||||||
export interface InvoiceTitle {
|
export interface InvoiceTitle {
|
||||||
id: string
|
id: string
|
||||||
type: '企业单位' | '非企业单位'
|
type: '企业单位' | '非企业单位'
|
||||||
@@ -182,14 +119,12 @@ export interface InvoiceTitle {
|
|||||||
isDefault: boolean
|
isDefault: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 新增/编辑发票抬头入参 */
|
|
||||||
export interface InvoiceTitlePayload {
|
export interface InvoiceTitlePayload {
|
||||||
type: InvoiceTitle['type']
|
type: InvoiceTitle['type']
|
||||||
title: string
|
title: string
|
||||||
taxNo: string
|
taxNo: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 财务统计 */
|
|
||||||
export interface FinanceStats {
|
export interface FinanceStats {
|
||||||
totalExpense: number
|
totalExpense: number
|
||||||
month: string
|
month: string
|
||||||
|
|||||||
75
src/types/business/home.d.ts
vendored
75
src/types/business/home.d.ts
vendored
@@ -1,53 +1,34 @@
|
|||||||
// 首页类型(VIEW 类型 + 后端真实 VO 类型 + 门户入口类型,统一归位于 types/business/home.d.ts)
|
|
||||||
//
|
//
|
||||||
// - VIEW 类型(BusinessLine / HomeStats / ServiceType / ServiceContentBlock / QuickLink):页面消费层
|
|
||||||
// - 后端 VO 类型(Project* / BannerVo / AgreementFormVo / ProjectCaseVo):vermis-maker
|
|
||||||
// com.ruoyi.vermins.controller.pc.web.UserSideWebHomePageController 与 ProjectCaseUserController 的真实返回
|
|
||||||
// - 门户入口类型(PortalEntry* / EntryTone / PortalEntryCard):IPortalEntryUserController,属首页快捷入口功能
|
|
||||||
|
|
||||||
import type { BusinessType } from '@/types/api'
|
import type { BusinessType } from '@/types/api'
|
||||||
|
|
||||||
/* ============================ VIEW 类型(页面消费层) ============================ */
|
|
||||||
|
|
||||||
/** 数据看板 */
|
|
||||||
export interface HomeStats {
|
export interface HomeStats {
|
||||||
pending: number
|
pending: number
|
||||||
inProgress: number
|
inProgress: number
|
||||||
total: number
|
total: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 首页头图轮播(VIEW 层,由 BannerVo 映射) */
|
|
||||||
export interface Banner {
|
export interface Banner {
|
||||||
id: number
|
id: number
|
||||||
imageUrl: string
|
imageUrl: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 快捷入口(VIEW 层占位,实际入口由门户接口驱动) */
|
|
||||||
export interface QuickLink {
|
export interface QuickLink {
|
||||||
label: string
|
label: string
|
||||||
path: string
|
path: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 服务类型子项 */
|
|
||||||
export interface ServiceType {
|
export interface ServiceType {
|
||||||
label: string
|
label: string
|
||||||
desc: string
|
desc: string
|
||||||
/** 图标(后端 icon_url 原样透传:图片地址时渲染为图,否则用名称首字兜底) */
|
|
||||||
icon?: string
|
icon?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 服务内容区块(对应后端富文本目录 `vm_project_submission_catalog` 的一条:
|
|
||||||
* 一条目录 = 详情页的一个「服务内容」区块,content 为富文本 HTML,可能内嵌图片/视频)
|
|
||||||
*/
|
|
||||||
export interface ServiceContentBlock {
|
export interface ServiceContentBlock {
|
||||||
/** 区块标题(后端 catalog.title,可为空) */
|
|
||||||
title: string
|
title: string
|
||||||
/** 区块内容(富文本 HTML 原文,直接渲染) */
|
|
||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 业务线(首页业务卡片 + 业务详情页共用) */
|
|
||||||
export interface BusinessLine {
|
export interface BusinessLine {
|
||||||
id: BusinessType
|
id: BusinessType
|
||||||
name: string
|
name: string
|
||||||
@@ -55,25 +36,17 @@ export interface BusinessLine {
|
|||||||
slogan: string
|
slogan: string
|
||||||
icon: string
|
icon: string
|
||||||
serviceContent: string
|
serviceContent: string
|
||||||
/** 服务内容区块列表(一条富文本目录一项,详情页逐块渲染,保留 HTML) */
|
|
||||||
contents: ServiceContentBlock[]
|
contents: ServiceContentBlock[]
|
||||||
serviceTypes: ServiceType[]
|
serviceTypes: ServiceType[]
|
||||||
video: string
|
video: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================ 后端真实 VO 类型 ============================ */
|
|
||||||
|
|
||||||
/** GET /web/home-page/project-status-count —— 项目状态统计 */
|
|
||||||
export interface ProjectStatusCount {
|
export interface ProjectStatusCount {
|
||||||
/** 项目数量 */
|
|
||||||
projectCount: number
|
projectCount: number
|
||||||
/** 进行中的项目数目 */
|
|
||||||
ongoingProjectCount: number
|
ongoingProjectCount: number
|
||||||
/** 待处理数目 */
|
|
||||||
pendingProjectCount: number
|
pendingProjectCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 服务类型子项(service-type-list / service-type-detail 内含) */
|
|
||||||
export interface ProjectServiceTypeVo {
|
export interface ProjectServiceTypeVo {
|
||||||
id: number | null
|
id: number | null
|
||||||
projectId: number | null
|
projectId: number | null
|
||||||
@@ -82,7 +55,6 @@ export interface ProjectServiceTypeVo {
|
|||||||
sortOrder: number | null
|
sortOrder: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 项目背景(富文本) */
|
|
||||||
export interface ProjectBackgroundVo {
|
export interface ProjectBackgroundVo {
|
||||||
id: number | null
|
id: number | null
|
||||||
projectId: number | null
|
projectId: number | null
|
||||||
@@ -90,13 +62,11 @@ export interface ProjectBackgroundVo {
|
|||||||
sortOrder: number | null
|
sortOrder: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 项目提报视图对象:service-type-list / service-type-detail/{id} 的返回 */
|
|
||||||
export interface ProjectSubmissionVo {
|
export interface ProjectSubmissionVo {
|
||||||
id: number
|
id: number
|
||||||
businessType: string | null
|
businessType: string | null
|
||||||
title: string | null
|
title: string | null
|
||||||
subtitle: string | null
|
subtitle: string | null
|
||||||
/** 是否启用:1 启用,0 停用 */
|
|
||||||
enabled: number | null
|
enabled: number | null
|
||||||
coverUrl: string | null
|
coverUrl: string | null
|
||||||
logoUrl: string | null
|
logoUrl: string | null
|
||||||
@@ -106,18 +76,13 @@ export interface ProjectSubmissionVo {
|
|||||||
updateTime: string | null
|
updateTime: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** GET /web/home-page/service-type-detail/{id} —— 项目提报详情(聚合 VO:主表单对象,三个子表均为一对多 List) */
|
|
||||||
export interface ProjectSubmissionInfo {
|
export interface ProjectSubmissionInfo {
|
||||||
projectSubmission: ProjectSubmissionFlat | null
|
projectSubmission: ProjectSubmissionFlat | null
|
||||||
/** 一对多:服务类型列表 */
|
|
||||||
projectServiceType: ProjectServiceTypeInfo[] | null
|
projectServiceType: ProjectServiceTypeInfo[] | null
|
||||||
/** 一对多:附件列表 */
|
|
||||||
projectSubmissionAttachment: ProjectSubmissionAttachmentInfo[] | null
|
projectSubmissionAttachment: ProjectSubmissionAttachmentInfo[] | null
|
||||||
/** 一对多:富文本目录列表 */
|
|
||||||
projectSubmissionCatalog: ProjectSubmissionCatalogInfo[] | null
|
projectSubmissionCatalog: ProjectSubmissionCatalogInfo[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 主表 vm_project_submission(详情接口下发) */
|
|
||||||
export interface ProjectSubmissionFlat {
|
export interface ProjectSubmissionFlat {
|
||||||
id: number | null
|
id: number | null
|
||||||
businessType: string | null
|
businessType: string | null
|
||||||
@@ -128,7 +93,6 @@ export interface ProjectSubmissionFlat {
|
|||||||
logoUrl: string | null
|
logoUrl: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 服务类型 vm_project_service_type(详情接口各取一条) */
|
|
||||||
export interface ProjectServiceTypeInfo {
|
export interface ProjectServiceTypeInfo {
|
||||||
id: number | null
|
id: number | null
|
||||||
projectId: number | null
|
projectId: number | null
|
||||||
@@ -137,7 +101,6 @@ export interface ProjectServiceTypeInfo {
|
|||||||
sortOrder: number | null
|
sortOrder: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 附件 vm_project_submission_attachment(详情接口各取一条) */
|
|
||||||
export interface ProjectSubmissionAttachmentInfo {
|
export interface ProjectSubmissionAttachmentInfo {
|
||||||
id: number | null
|
id: number | null
|
||||||
projectId: number | null
|
projectId: number | null
|
||||||
@@ -147,7 +110,6 @@ export interface ProjectSubmissionAttachmentInfo {
|
|||||||
sortOrder: number | null
|
sortOrder: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 目录(富文本)vm_project_submission_catalog(详情接口各取一条) */
|
|
||||||
export interface ProjectSubmissionCatalogInfo {
|
export interface ProjectSubmissionCatalogInfo {
|
||||||
id: number | null
|
id: number | null
|
||||||
projectId: number | null
|
projectId: number | null
|
||||||
@@ -156,78 +118,41 @@ export interface ProjectSubmissionCatalogInfo {
|
|||||||
sortOrder: number | null
|
sortOrder: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** GET /web/home-page/carousel-list —— 轮播图 */
|
|
||||||
export interface BannerVo {
|
export interface BannerVo {
|
||||||
id: number | null
|
id: number | null
|
||||||
imageUrl: string | null
|
imageUrl: string | null
|
||||||
sort: number | null
|
sort: number | null
|
||||||
/** 是否启用(true 启用,false 停用) */
|
|
||||||
enabled: boolean | null
|
enabled: boolean | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** GET /web/home-page/platform-intro、contact-info —— 协议/静态文章富文本 */
|
|
||||||
export interface AgreementFormVo {
|
export interface AgreementFormVo {
|
||||||
id: number | null
|
id: number | null
|
||||||
/** 协议类型(目前只有「关于我们」一条) */
|
|
||||||
type: string | null
|
type: string | null
|
||||||
/** 文章内容(富文本 HTML) */
|
|
||||||
content: string | null
|
content: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 案例库的实体类型与 VO(ProjectCaseVo / ProjectCaseUserBo / CaseSelectionModel 等)
|
|
||||||
// 已按「一页一套」归位到 `@/types/business/cases`,本文件不再重复定义。
|
|
||||||
|
|
||||||
/* ============================ 门户入口(首页快捷入口功能) ============================ */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 后端 IPortalEntryVo(入口目录 + 用户个性化分组)
|
|
||||||
*
|
|
||||||
* <p>注意字段来源分两处:</p>
|
|
||||||
* - 登录分支(selectByUserId):name / id / icon / route / groupType / sortOrder / status,
|
|
||||||
* 其中 id 取的是 vm_portal_entry.id(入口目录主键);
|
|
||||||
* - 匿名分支(selectVoList,MyBatis-Plus 自动 SQL):多带 entryId 等目录字段,groupType 为目录默认值。
|
|
||||||
*/
|
|
||||||
export interface PortalEntryVo {
|
export interface PortalEntryVo {
|
||||||
/** 入口主键 */
|
|
||||||
id: number | null
|
id: number | null
|
||||||
/** 入口标识 */
|
|
||||||
entryId: string | null
|
entryId: string | null
|
||||||
/** 显示名 */
|
|
||||||
name: string | null
|
name: string | null
|
||||||
/** 图标名 / URL */
|
|
||||||
icon: string | null
|
icon: string | null
|
||||||
/** 跳转路由 */
|
|
||||||
route: string | null
|
route: string | null
|
||||||
/** 权限标识(空 = 无需权限) */
|
|
||||||
permission: string | null
|
permission: string | null
|
||||||
/** 1 = 快捷、0 = 其他 */
|
|
||||||
groupType: string | null
|
groupType: string | null
|
||||||
/** 组内顺序 */
|
|
||||||
sortOrder: number | null
|
sortOrder: number | null
|
||||||
/** 1 = 启用、0 = 停用 */
|
|
||||||
status: boolean | null
|
status: boolean | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 入口分组:1 = 快捷入口、0 = 其他入口 */
|
|
||||||
export type PortalEntryGroup = '1' | '0'
|
export type PortalEntryGroup = '1' | '0'
|
||||||
|
|
||||||
/** 页面消费的入口卡片(视图层类型,与后端字段解耦) */
|
|
||||||
export interface PortalEntryCard {
|
export interface PortalEntryCard {
|
||||||
/** 入口 id(切换分组时回传后端) */
|
|
||||||
id: number
|
id: number
|
||||||
/** 显示名 */
|
|
||||||
label: string
|
label: string
|
||||||
/** 跳转路由(可能为空,为空时不可点击跳转) */
|
|
||||||
route: string
|
route: string
|
||||||
/** 当前分组:1 = 快捷、0 = 其他 */
|
|
||||||
groupType: PortalEntryGroup
|
groupType: PortalEntryGroup
|
||||||
/** 图标:emoji 字符或图片地址 */
|
|
||||||
icon: string
|
icon: string
|
||||||
/** icon 是否为图片地址 */
|
|
||||||
isImage: boolean
|
isImage: boolean
|
||||||
/** 图标底色主题 */
|
|
||||||
tone: EntryTone
|
tone: EntryTone
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 图标底色主题(设计稿为五色循环) */
|
|
||||||
export type EntryTone = 'purple' | 'green' | 'blue' | 'amber' | 'red' | 'cyan'
|
export type EntryTone = 'purple' | 'green' | 'blue' | 'amber' | 'red' | 'cyan'
|
||||||
|
|||||||
5
src/types/business/message.d.ts
vendored
5
src/types/business/message.d.ts
vendored
@@ -1,24 +1,19 @@
|
|||||||
// 消息实体(PRD M8 消息中心)
|
|
||||||
|
|
||||||
import type { FileInfo } from '@/types/api'
|
import type { FileInfo } from '@/types/api'
|
||||||
|
|
||||||
/** 消息类型 */
|
|
||||||
export type MessageType = '在线客服' | '状态通知' | '系统通知'
|
export type MessageType = '在线客服' | '状态通知' | '系统通知'
|
||||||
|
|
||||||
/** 站内消息 */
|
|
||||||
export interface Message {
|
export interface Message {
|
||||||
id: string
|
id: string
|
||||||
type: MessageType
|
type: MessageType
|
||||||
title: string
|
title: string
|
||||||
summary: string
|
summary: string
|
||||||
/** 正文纯文本,详情页 pre-wrap 渲染 */
|
|
||||||
content: string
|
content: string
|
||||||
publishTime: string
|
publishTime: string
|
||||||
isRead: boolean
|
isRead: boolean
|
||||||
attachments: FileInfo[]
|
attachments: FileInfo[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 消息查询条件 */
|
|
||||||
export interface MessageQuery {
|
export interface MessageQuery {
|
||||||
keyword: string
|
keyword: string
|
||||||
type: MessageType | 'all'
|
type: MessageType | 'all'
|
||||||
|
|||||||
14
src/types/business/profile.d.ts
vendored
14
src/types/business/profile.d.ts
vendored
@@ -1,23 +1,18 @@
|
|||||||
// 个人中心实体(M10:security/address/favorites/qualifications/feedback/help/about/hotline 共享)
|
|
||||||
|
|
||||||
import type { BusinessType, FileInfo } from '@/types/api'
|
import type { BusinessType, FileInfo } from '@/types/api'
|
||||||
|
|
||||||
/** 账号资料 */
|
|
||||||
export interface Profile {
|
export interface Profile {
|
||||||
nickname: string
|
nickname: string
|
||||||
phone: string
|
phone: string
|
||||||
email: string
|
email: string
|
||||||
/** 密码上次修改时间(展示值,如 '15天前') */
|
|
||||||
passwordModifiedAt: string
|
passwordModifiedAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 修改密码入参 */
|
|
||||||
export interface ChangePasswordParams {
|
export interface ChangePasswordParams {
|
||||||
code: string
|
code: string
|
||||||
password: string
|
password: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 收货地址 */
|
|
||||||
export interface Address {
|
export interface Address {
|
||||||
id: string
|
id: string
|
||||||
receiverName: string
|
receiverName: string
|
||||||
@@ -27,7 +22,6 @@ export interface Address {
|
|||||||
isDefault: boolean
|
isDefault: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 资质类型 */
|
|
||||||
export type QualificationType =
|
export type QualificationType =
|
||||||
| '营业执照'
|
| '营业执照'
|
||||||
| '生产许可证'
|
| '生产许可证'
|
||||||
@@ -36,10 +30,8 @@ export type QualificationType =
|
|||||||
| '授权书'
|
| '授权书'
|
||||||
| '其他资质'
|
| '其他资质'
|
||||||
|
|
||||||
/** 资质审核状态 */
|
|
||||||
export type QualificationStatus = '已认证' | '审核中' | '未通过'
|
export type QualificationStatus = '已认证' | '审核中' | '未通过'
|
||||||
|
|
||||||
/** 资质 */
|
|
||||||
export interface Qualification {
|
export interface Qualification {
|
||||||
id: string
|
id: string
|
||||||
type: QualificationType
|
type: QualificationType
|
||||||
@@ -52,35 +44,29 @@ export interface Qualification {
|
|||||||
attachments: FileInfo[]
|
attachments: FileInfo[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 资质新增/编辑提交载荷(id 存在为编辑) */
|
|
||||||
export type QualificationInput = Omit<Qualification, 'id' | 'status'> & { id?: string }
|
export type QualificationInput = Omit<Qualification, 'id' | 'status'> & { id?: string }
|
||||||
|
|
||||||
/** 意见反馈 */
|
|
||||||
export interface Feedback {
|
export interface Feedback {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
attachments: FileInfo[]
|
attachments: FileInfo[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 列表分页查询 */
|
|
||||||
export interface ListQuery {
|
export interface ListQuery {
|
||||||
page: number
|
page: number
|
||||||
pageSize: number
|
pageSize: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 我的收藏查询(关键字 + 分类 + 分页) */
|
|
||||||
export interface FavoritesQuery extends ListQuery {
|
export interface FavoritesQuery extends ListQuery {
|
||||||
keyword: string
|
keyword: string
|
||||||
category: BusinessType | 'all'
|
category: BusinessType | 'all'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 帮助中心 FAQ 条目 */
|
|
||||||
export interface HelpFaqItem {
|
export interface HelpFaqItem {
|
||||||
q: string
|
q: string
|
||||||
a: string
|
a: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 帮助中心 FAQ 分组 */
|
|
||||||
export interface HelpFaqGroup {
|
export interface HelpFaqGroup {
|
||||||
title: string
|
title: string
|
||||||
items: HelpFaqItem[]
|
items: HelpFaqItem[]
|
||||||
|
|||||||
228
src/types/business/project.d.ts
vendored
228
src/types/business/project.d.ts
vendored
@@ -1,15 +1,6 @@
|
|||||||
// 项目中心实体(PRD M5)
|
|
||||||
|
|
||||||
import type { BusinessType, FileInfo } from '@/types/api'
|
import type { BusinessType, FileInfo } from '@/types/api'
|
||||||
|
|
||||||
/**
|
|
||||||
* 项目状态(详情页展示口径,中文)。
|
|
||||||
*
|
|
||||||
* <p>取值 = 两张表状态码值的并集经 {@link PROJECT_STATUS_LABEL} 映射后的中文:
|
|
||||||
* 需求侧 `INITIATING`(待立项)/ `CANCELLED`(已取消),项目侧 `IN_PROGRESS` /
|
|
||||||
* `PENDING_ACCEPTANCE` / `PENDING_INTERNAL_ACCEPTANCE` / `COMPLETED` / `AFTER_SALE`。
|
|
||||||
* 「待内部验收」数量少但确实存在,故保留。</p>
|
|
||||||
*/
|
|
||||||
export type ProjectStatus =
|
export type ProjectStatus =
|
||||||
| '待立项'
|
| '待立项'
|
||||||
| '进行中'
|
| '进行中'
|
||||||
@@ -19,17 +10,6 @@ export type ProjectStatus =
|
|||||||
| '售后中'
|
| '售后中'
|
||||||
| '已取消'
|
| '已取消'
|
||||||
|
|
||||||
/**
|
|
||||||
* 列表行状态码值 —— 后端 `COALESCE(rd_project.status, project_requirement.status)`。
|
|
||||||
*
|
|
||||||
* <p>列表接口已改为「需求表为主 + LEFT JOIN 项目表」:已立项取项目状态,未立项回落到需求状态。
|
|
||||||
* 因此这里 = 两张表状态的<b>并集</b>:<br>
|
|
||||||
* · 项目表 `vm_rd_project.status`:IN_PROGRESS / PENDING_ACCEPTANCE /
|
|
||||||
* PENDING_INTERNAL_ACCEPTANCE / COMPLETED / AFTER_SALE / CANCELLED<br>
|
|
||||||
* · 需求表 `vm_project_requirement.status` 额外提供:<b>INITIATING(待立项)</b></p>
|
|
||||||
*
|
|
||||||
* <p>所以设计稿画的「待立项」现在是真实码值(= 需求已提报、尚未立项)。</p>
|
|
||||||
*/
|
|
||||||
export type RdProjectStatus =
|
export type RdProjectStatus =
|
||||||
| 'INITIATING'
|
| 'INITIATING'
|
||||||
| 'IN_PROGRESS'
|
| 'IN_PROGRESS'
|
||||||
@@ -39,20 +19,12 @@ export type RdProjectStatus =
|
|||||||
| 'AFTER_SALE'
|
| 'AFTER_SALE'
|
||||||
| 'CANCELLED'
|
| 'CANCELLED'
|
||||||
|
|
||||||
/**
|
|
||||||
* 项目列表行 —— 后端 `RdProjectVo`(`GET /web/requirementPool/list` 的 `requirementPage.rows`)。
|
|
||||||
*
|
|
||||||
* <p>`id` 后端是 Long,但全局 Jackson 配置序列化成字符串(已实测),故按 string 声明。
|
|
||||||
* `progress` 是 Integer,可能为 null(库里大量为空)⇒ 展示层要兜底成「—」。</p>
|
|
||||||
*/
|
|
||||||
export interface RdProjectVo {
|
export interface RdProjectVo {
|
||||||
id: string
|
id: string
|
||||||
/** 外部项目关联的需求主键,内部项目为 null */
|
|
||||||
requirementId?: string | null
|
requirementId?: string | null
|
||||||
projectNo: string
|
projectNo: string
|
||||||
/** EXTERNAL / INTERNAL */
|
/** EXTERNAL / INTERNAL */
|
||||||
sourceType?: string | null
|
sourceType?: string | null
|
||||||
/** 业务类型快照:存的是**提报表主键 id**(如 '1'),不是类型名 ⇒ 展示要经 label 映射 */
|
|
||||||
businessType?: string | null
|
businessType?: string | null
|
||||||
projectName: string
|
projectName: string
|
||||||
requirementDesc?: string | null
|
requirementDesc?: string | null
|
||||||
@@ -74,29 +46,17 @@ export interface RdProjectVo {
|
|||||||
updateTime?: string | null
|
updateTime?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 业务类型统计单项(后端 `ProjectRequirementCountVo`) */
|
|
||||||
export interface ProjectBusinessTypeCount {
|
export interface ProjectBusinessTypeCount {
|
||||||
/** 提报表主键 id(与列表筛选参数 `businessType` 同口径) */
|
|
||||||
businessTypeId: string
|
businessTypeId: string
|
||||||
/** 业务类型名称,如「互联网软件」 */
|
|
||||||
businessTypeLabel: string
|
businessTypeLabel: string
|
||||||
count: number
|
count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 业务数据统计(后端 `ProjectRequirementStatisticsVo`)。
|
|
||||||
*
|
|
||||||
* <p>来源是独立端点 `GET /web/requirementPool/getFilterConditions`(**全量口径**,不随筛选 / 分页变化)。</p>
|
|
||||||
*
|
|
||||||
* <p>⚠️ 列表接口 `/list` 响应里的同名字段后端仍是注释状态、实测恒为 `null`,**不要**拿它当统计源。
|
|
||||||
* 这里仍按「可能为 null」处理:拿不到时筛选栏只剩「全部」一行。</p>
|
|
||||||
*/
|
|
||||||
export interface ProjectStatistics {
|
export interface ProjectStatistics {
|
||||||
total?: number | null
|
total?: number | null
|
||||||
businessTypeCounts?: ProjectBusinessTypeCount[] | null
|
businessTypeCounts?: ProjectBusinessTypeCount[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 列表响应(后端 `ProjectRequirementAndCountVo`:分页数据 + 统计平级) */
|
|
||||||
export interface ProjectListResult {
|
export interface ProjectListResult {
|
||||||
requirementPage?: {
|
requirementPage?: {
|
||||||
total: number
|
total: number
|
||||||
@@ -105,38 +65,22 @@ export interface ProjectListResult {
|
|||||||
requirementStatistics?: ProjectStatistics | null
|
requirementStatistics?: ProjectStatistics | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 里程碑节点 */
|
|
||||||
export interface Milestone {
|
export interface Milestone {
|
||||||
name: string
|
name: string
|
||||||
status: '待开始' | '进行中' | '已完成'
|
status: '待开始' | '进行中' | '已完成'
|
||||||
time?: string
|
time?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 团队成员 */
|
|
||||||
export interface TeamMember {
|
export interface TeamMember {
|
||||||
role: string
|
role: string
|
||||||
name: string
|
name: string
|
||||||
avatarChar: string
|
avatarChar: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 项目账单(付款计划) */
|
|
||||||
export interface ProjectBill {
|
export interface ProjectBill {
|
||||||
id: string
|
id: string
|
||||||
/**
|
|
||||||
* 数据来源:
|
|
||||||
* - `bill` = 真账单(`vm_bill`)—— `id` 是账单 id,可跳收银台支付;
|
|
||||||
* - `plan` = 仅付款计划(`vm_bill_plan`)—— 后端未重启(响应无 `bills`)时的只读回落,
|
|
||||||
* **没有账单 id,不可支付**(计划 id 与账单 id 值域重叠,拿来当收银台入参会查到别的账单)。
|
|
||||||
*/
|
|
||||||
source: 'bill' | 'plan'
|
source: 'bill' | 'plan'
|
||||||
planName: string
|
planName: string
|
||||||
/**
|
|
||||||
* 期次占比(%),已归一成**可展示文本**(如 `"30"`;非整数比例保留一位小数)。
|
|
||||||
*
|
|
||||||
* <p>数据源:账单 `vm_bill.period_ratio`(BigDecimal,字符串下发),回落卡则由
|
|
||||||
* `periodRule` 第 N 段折算。设计稿卡片标题是「第2期·进度款 比例30%」⇒ 标题 = 期次名 + 比例。
|
|
||||||
* 缺失/非法时为空串,模板据此不渲染「比例」片段。</p>
|
|
||||||
*/
|
|
||||||
periodRatio?: string
|
periodRatio?: string
|
||||||
billNo: string
|
billNo: string
|
||||||
amount: number
|
amount: number
|
||||||
@@ -145,48 +89,19 @@ export interface ProjectBill {
|
|||||||
projectName: string
|
projectName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 售后工单(详情页组件契约)。
|
|
||||||
*
|
|
||||||
* <p>两态:`处理中` = 售后工单存在且未撤回/未作废(含后端 REQUESTED / IN_PROGRESS);
|
|
||||||
* `无` = 没有工单,或最近一条已 WITHDRAWN(撤回后回到可再次申请的状态)。</p>
|
|
||||||
*/
|
|
||||||
export interface AfterSales {
|
export interface AfterSales {
|
||||||
status: '无' | '处理中'
|
status: '无' | '处理中'
|
||||||
/**
|
|
||||||
* 售后类型 —— 申请页下拉的固定 8 项:软件 BUG 修复 / 软件 / 固件功能迭代 /
|
|
||||||
* 硬件 PCB & 电路改版调整 / 硬件故障排查维修 / 成品不良品返修 / 元器件物料替换变更 /
|
|
||||||
* 物联网整机联调与版本升级 / 交付资料补齐与技术答疑。
|
|
||||||
* 库内暂无对应字典,值域先写在前端(`views/project/after-sales.vue#TYPE_OPTIONS`)。
|
|
||||||
*/
|
|
||||||
type: string
|
type: string
|
||||||
/** 问题描述(后端 `reason`) */
|
|
||||||
description: string
|
description: string
|
||||||
/** 用户补充备注(后端 `remark`) */
|
|
||||||
remark: string
|
remark: string
|
||||||
/** 售后工单附件(后端 `afterSaleAttachments`,phase = AFTER_SALE) */
|
|
||||||
attachments: FileInfo[]
|
attachments: FileInfo[]
|
||||||
/** 申请时间(后端 `requestTime`) */
|
|
||||||
createTime: string
|
createTime: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 售后附件(已上传到 OSS,携带 ossId 以便提交时由后端转存)。
|
|
||||||
*
|
|
||||||
* <p>上传走需求提报的同一个 OSS 上传口(`POST /web/workOrderDraft/upload`),
|
|
||||||
* 提交时只传 `ossId` 列表,后端按 `phase = AFTER_SALE`、`target_id = 售后工单 id`
|
|
||||||
* 写进 `vm_project_requirement_attachment`,OSS 文件不重复上传。</p>
|
|
||||||
*/
|
|
||||||
export interface AfterSalesAttachment extends FileInfo {
|
export interface AfterSalesAttachment extends FileInfo {
|
||||||
/** OSS 主键(提交时进 `attachmentOssIds`) */
|
|
||||||
ossId: string | number
|
ossId: string | number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* OSS 上传返回(后端 `SysOssVo` 的子集)—— 与需求提报共用同一个上传口。
|
|
||||||
*
|
|
||||||
* <p>⚠️ `fileSuffix` 带前导点(`.pdf`);响应里**没有**文件大小,大小要取本地 `File.size`。</p>
|
|
||||||
*/
|
|
||||||
export interface AfterSalesUploadVo {
|
export interface AfterSalesUploadVo {
|
||||||
ossId: string | number
|
ossId: string | number
|
||||||
url: string
|
url: string
|
||||||
@@ -194,42 +109,18 @@ export interface AfterSalesUploadVo {
|
|||||||
fileSuffix?: string
|
fileSuffix?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 售后申请入参 —— **对齐后端 `ProjectRequirementActionBo`**。
|
|
||||||
*
|
|
||||||
* <p>字段名与页面语义的对应:页面「问题描述」→ {@link reason}、「补充备注」→ {@link note}。</p>
|
|
||||||
*/
|
|
||||||
export interface AfterSalesPayload {
|
export interface AfterSalesPayload {
|
||||||
/** 售后类型(下拉 8 项之一),最长 50 */
|
|
||||||
type: string
|
type: string
|
||||||
/** 问题描述 → 后端 `reason`,最长 500 */
|
|
||||||
reason: string
|
reason: string
|
||||||
/** 补充备注 → 后端 `note`(页面限 500,后端 BO 上限 2000) */
|
|
||||||
note: string
|
note: string
|
||||||
/** 附件 OSS 主键集合(空数组安全,后端 null 会走空判断) */
|
|
||||||
attachmentOssIds: Array<string | number>
|
attachmentOssIds: Array<string | number>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 项目详情页数据模型 —— **详情页组件契约**,不是后端原样字段。
|
|
||||||
*
|
|
||||||
* <p>后端详情已拆成 4 个页签接口(见 {@link RequirementDetailBundle}),由
|
|
||||||
* `utils/projectVo.ts#toProjectDetail` 合并映射成本类型;详情页 6 个子组件
|
|
||||||
* (InfoCard/Team/Milestones/Files/Finance/AfterSales)只认这套字段,因此换数据来源
|
|
||||||
* 不需要动组件。</p>
|
|
||||||
*/
|
|
||||||
export interface Project {
|
export interface Project {
|
||||||
id: string
|
id: string
|
||||||
projectNo: string
|
projectNo: string
|
||||||
projectName: string
|
projectName: string
|
||||||
/** 归一化后的枚举(未知类型会落到 'other',仅供图标/配色等视觉用途) */
|
|
||||||
businessType: BusinessType
|
businessType: BusinessType
|
||||||
/**
|
|
||||||
* 后端**原样下发**的业务类型名称(如「互联网软件」,来自提报表)。
|
|
||||||
*
|
|
||||||
* <p>⚠️ 展示一律用它:`businessType` 只有 5 个固定枚举,后台新增业务类型时会全部落成
|
|
||||||
* 「其他」——那是枚举的兜底,不是接口的值。</p>
|
|
||||||
*/
|
|
||||||
businessTypeName: string
|
businessTypeName: string
|
||||||
progress: number
|
progress: number
|
||||||
status: ProjectStatus
|
status: ProjectStatus
|
||||||
@@ -245,61 +136,32 @@ export interface Project {
|
|||||||
afterSales?: AfterSales
|
afterSales?: AfterSales
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 业务类型筛选项 + 件数(左侧筛选栏「业务类型」列表;「全部」的 value 为 'all') */
|
|
||||||
export interface ProjectTypeCount {
|
export interface ProjectTypeCount {
|
||||||
/** 'all' 表示「全部」;否则是后端下发的 `businessTypeId` */
|
|
||||||
value: string
|
value: string
|
||||||
/** 展示文案(后端 `businessTypeLabel`;「全部」由前端给) */
|
|
||||||
label: string
|
label: string
|
||||||
/** 该类型下的项目件数(后端全量口径;统计缺失时为 0) */
|
|
||||||
count: number
|
count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 项目列表查询条件(**已是后端口径**)。
|
|
||||||
*
|
|
||||||
* <p>与 `RdProjectBo` 的字段对应关系:`keyword → key`、`businessTypes → businessTypes`(提报表 id,多选 IN)、
|
|
||||||
* `status → status`(码值)、`startDate/endDate 或 timeShortcut → beginTime/endTime`(两者都落在
|
|
||||||
* `create_time` 上)、`pageNum/pageSize → PageQuery`。</p>
|
|
||||||
*
|
|
||||||
* <p>用空串 / 空数组表示「不限」而不是 'all'——空值直接不出现在 querystring 里,与后端 `eqIfText` 的语义一致。</p>
|
|
||||||
*/
|
|
||||||
export interface ProjectQuery {
|
export interface ProjectQuery {
|
||||||
keyword: string
|
keyword: string
|
||||||
/** 空数组 = 全部;元素为后端统计下发的 businessTypeId(**多选**,后端走 IN) */
|
|
||||||
businessTypes: string[]
|
businessTypes: string[]
|
||||||
/** '' = 全部;否则为 {@link RdProjectStatus} 码值 */
|
|
||||||
status: string
|
status: string
|
||||||
/** 提交时间快捷区间;与 startDate/endDate 互斥(选其一会把另一组清空) */
|
|
||||||
timeShortcut: 'all' | '3m' | '6m' | '1y'
|
timeShortcut: 'all' | '3m' | '6m' | '1y'
|
||||||
/** 提交时间自选区间起(YYYY-MM-DD,空串 = 不限) */
|
|
||||||
startDate: string
|
startDate: string
|
||||||
/** 提交时间自选区间止(YYYY-MM-DD,空串 = 不限) */
|
|
||||||
endDate: string
|
endDate: string
|
||||||
pageNum: number
|
pageNum: number
|
||||||
pageSize: number
|
pageSize: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// 详情契约:后端 4 个页签接口(都继承 ProjectRequirementBaseVo)
|
|
||||||
// GET /web/requirementPool/get/progress/{id} → ProjectRequirementProgressVo
|
// GET /web/requirementPool/get/progress/{id} → ProjectRequirementProgressVo
|
||||||
// GET /web/requirementPool/get/files/{id} → ProjectRequirementFilesVo
|
// GET /web/requirementPool/get/files/{id} → ProjectRequirementFilesVo
|
||||||
// GET /web/requirementPool/get/finance/{id} → ProjectRequirementFinanceVo
|
// GET /web/requirementPool/get/finance/{id} → ProjectRequirementFinanceVo
|
||||||
// GET /web/requirementPool/get/afterSales/{id} → ProjectRequirementAfterSalesVo
|
// GET /web/requirementPool/get/afterSales/{id} → ProjectRequirementAfterSalesVo
|
||||||
//
|
//
|
||||||
// 为什么拆 4 个:详情页本就是 4 个页签,单接口要一次查全 8 张表;拆开后可「按页签取数」。
|
|
||||||
// 4 个 VO 都带基础信息(继承 `ProjectRequirementBaseVo`)⇒ 任一页签的响应里都有项目名 /
|
|
||||||
// 编号 / 业务类型 / 状态 / 提交时间,所以前端可以只用 `progress` 那一支渲染顶部信息卡,
|
|
||||||
// 并保证切页签后信息卡不缺数据(合并逻辑见 utils/projectVo.ts#toProjectDetail)。
|
|
||||||
//
|
//
|
||||||
// 与列表一致:需求未立项时 `vm_rd_project` 没有对应行,项目侧字段(projectStage /
|
|
||||||
// projectMilestones / members / billPlans / invoices / contracts)为空数组 / null,
|
|
||||||
// 需求侧字段(基础信息 / requirementAttachments / afterSale)照常返回。
|
|
||||||
// ⚠️ id 传的是**需求 id**(vm_project_requirement.id)= 列表行的 RdProjectVo.id;
|
|
||||||
// 不存在 / 非本人 → 后端 `data` 为 null(详情页整页空态)。
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** 开发进度(后端 `ProjectStage`,表 `vm_project_stage`) */
|
|
||||||
export interface RequirementProjectStage {
|
export interface RequirementProjectStage {
|
||||||
id: string
|
id: string
|
||||||
projectId: string | null
|
projectId: string | null
|
||||||
@@ -309,12 +171,10 @@ export interface RequirementProjectStage {
|
|||||||
startTime: string | null
|
startTime: string | null
|
||||||
planTime: string | null
|
planTime: string | null
|
||||||
finishTime: string | null
|
finishTime: string | null
|
||||||
/** 开发进度百分比(列表页那个 progress 的详情侧来源) */
|
|
||||||
developmentProgress: number | null
|
developmentProgress: number | null
|
||||||
sort: number | null
|
sort: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 里程碑(后端 `RdProjectMilestone`,表 `vm_rd_project_milestone`) */
|
|
||||||
export interface RequirementMilestone {
|
export interface RequirementMilestone {
|
||||||
id: string
|
id: string
|
||||||
projectId: string | null
|
projectId: string | null
|
||||||
@@ -326,22 +186,18 @@ export interface RequirementMilestone {
|
|||||||
parallelFlag: boolean | null
|
parallelFlag: boolean | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 需求附件(后端 `ProjectRequirementAttachment`,表 `vm_project_requirement_attachment`) */
|
|
||||||
export interface RequirementAttachment {
|
export interface RequirementAttachment {
|
||||||
id: string
|
id: string
|
||||||
requirementId: string | null
|
requirementId: string | null
|
||||||
/** 附件所属阶段,实测有 `REQUIREMENT` */
|
|
||||||
phase: string | null
|
phase: string | null
|
||||||
ossId: string | null
|
ossId: string | null
|
||||||
originalName: string | null
|
originalName: string | null
|
||||||
url: string | null
|
url: string | null
|
||||||
fileSize: number | null
|
fileSize: number | null
|
||||||
sortOrder: number | null
|
sortOrder: number | null
|
||||||
/** 上传时间(继承 BaseEntity,后端会下发) */
|
|
||||||
createTime?: string | null
|
createTime?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 项目团队(后端 `RdProjectMember`,表 `vm_rd_project_member`) */
|
|
||||||
export interface RequirementMember {
|
export interface RequirementMember {
|
||||||
id: string
|
id: string
|
||||||
projectId: string | null
|
projectId: string | null
|
||||||
@@ -352,12 +208,6 @@ export interface RequirementMember {
|
|||||||
isOwner: boolean | null
|
isOwner: boolean | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 账单(后端 `Bill`,表 `vm_bill`)—— 「合同财务」页签卡片的**数据源**。
|
|
||||||
*
|
|
||||||
* <p>设计稿卡片上的字段(「账单号」+ 已支付/待支付/未到期)只有这张表才有;
|
|
||||||
* 且 `id` 直接就是收银台 `?ids=` 要传的**账单 id**(不是付款计划 id)。</p>
|
|
||||||
*/
|
|
||||||
export interface RequirementBill {
|
export interface RequirementBill {
|
||||||
id: string
|
id: string
|
||||||
billNo: string | null
|
billNo: string | null
|
||||||
@@ -365,14 +215,11 @@ export interface RequirementBill {
|
|||||||
projectNo: string | null
|
projectNo: string | null
|
||||||
projectName: string | null
|
projectName: string | null
|
||||||
userId: string | null
|
userId: string | null
|
||||||
/** ⚠️ BigDecimal ⇒ 后端以**字符串**下发("24000.00"),用前必须 `Number()` 归一 */
|
|
||||||
amount: string | number | null
|
amount: string | number | null
|
||||||
contractId: string | null
|
contractId: string | null
|
||||||
/** ADVANCE / PROGRESS / FINAL / OTHER */
|
/** ADVANCE / PROGRESS / FINAL / OTHER */
|
||||||
billType: string | null
|
billType: string | null
|
||||||
/** 期次名,如「第2期·进度款」 */
|
|
||||||
periodName: string | null
|
periodName: string | null
|
||||||
/** 占项目总额比例(%),如 40.00 */
|
|
||||||
periodRatio: string | number | null
|
periodRatio: string | number | null
|
||||||
/** UNSTARTED / PENDING / PAID / CANCELLED */
|
/** UNSTARTED / PENDING / PAID / CANCELLED */
|
||||||
status: string | null
|
status: string | null
|
||||||
@@ -381,95 +228,62 @@ export interface RequirementBill {
|
|||||||
tradeName: string | null
|
tradeName: string | null
|
||||||
payTime: string | null
|
payTime: string | null
|
||||||
dueDate: string | null
|
dueDate: string | null
|
||||||
/** 创建时间(继承 BaseEntity,后端会下发) */
|
|
||||||
createTime?: string | null
|
createTime?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 付款计划(后端 `BillPlan`,表 `vm_bill_plan`) */
|
|
||||||
export interface RequirementBillPlan {
|
export interface RequirementBillPlan {
|
||||||
id: string
|
id: string
|
||||||
projectId: string | null
|
projectId: string | null
|
||||||
projectNo: string | null
|
projectNo: string | null
|
||||||
projectName: string | null
|
projectName: string | null
|
||||||
/** ⚠️ BigDecimal ⇒ 后端以**字符串**下发("120000.00"),用前必须 `Number()` 归一 */
|
|
||||||
totalAmount: string | number | null
|
totalAmount: string | number | null
|
||||||
periods: number | null
|
periods: number | null
|
||||||
currentPeriod: number | null
|
currentPeriod: number | null
|
||||||
periodRule: string | null
|
periodRule: string | null
|
||||||
nextDate: string | null
|
nextDate: string | null
|
||||||
status: string | null
|
status: string | null
|
||||||
/**
|
|
||||||
* 该期支付单的商户订单号(`vm_finance_payment.out_trade_no`)——
|
|
||||||
* **页面「账单号」就显示它**;未支付(该期还没下过单)时为 null。
|
|
||||||
*/
|
|
||||||
orderNo: string | null
|
orderNo: string | null
|
||||||
/** 支付成功回写的交易流水号(`vm_finance_payment.transaction_no`);未支付为 null */
|
|
||||||
transactionNo: string | null
|
transactionNo: string | null
|
||||||
/** 支付通道:WECHAT/ALIPAY/...;未支付为 null */
|
|
||||||
payChannel: string | null
|
payChannel: string | null
|
||||||
/** 该期实付金额(元);未支付为 null */
|
|
||||||
payAmount: string | number | null
|
payAmount: string | number | null
|
||||||
/** 支付单状态:CREATED/SUCCESS/FAILED/REFUNDED;未支付为 null */
|
|
||||||
payStatus: string | null
|
payStatus: string | null
|
||||||
/** 支付时间;未支付为 null */
|
|
||||||
payTime: string | null
|
payTime: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 发票(后端 `Invoice`,表 `vm_invoice`) */
|
|
||||||
export interface RequirementInvoice {
|
export interface RequirementInvoice {
|
||||||
id: string
|
id: string
|
||||||
billId: string | null
|
billId: string | null
|
||||||
invoiceTitle: string | null
|
invoiceTitle: string | null
|
||||||
taxNo: string | null
|
taxNo: string | null
|
||||||
invoiceType: string | null
|
invoiceType: string | null
|
||||||
/** ⚠️ BigDecimal ⇒ 字符串下发,用前 `Number()` 归一 */
|
|
||||||
amount: string | number | null
|
amount: string | number | null
|
||||||
status: string | null
|
status: string | null
|
||||||
invoiceNo: string | null
|
invoiceNo: string | null
|
||||||
/** 电子发票文件地址 */
|
|
||||||
invoiceFile: string | null
|
invoiceFile: string | null
|
||||||
applyTime: string | null
|
applyTime: string | null
|
||||||
invoiceDate: string | null
|
invoiceDate: string | null
|
||||||
/** 申请时间(继承 BaseEntity,后端会下发) */
|
|
||||||
createTime?: string | null
|
createTime?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 合同(后端 `Contract`,表 `vm_contract`) */
|
|
||||||
export interface RequirementContract {
|
export interface RequirementContract {
|
||||||
id: string
|
id: string
|
||||||
contractNo: string | null
|
contractNo: string | null
|
||||||
projectId: string | null
|
projectId: string | null
|
||||||
title: string | null
|
title: string | null
|
||||||
/** ⚠️ BigDecimal ⇒ 字符串下发,用前 `Number()` 归一 */
|
|
||||||
amount: string | number | null
|
amount: string | number | null
|
||||||
signStatus: string | null
|
signStatus: string | null
|
||||||
status: string | null
|
status: string | null
|
||||||
signDate: string | null
|
signDate: string | null
|
||||||
/** 合同文件地址 */
|
|
||||||
file: string | null
|
file: string | null
|
||||||
/** 签署时间(继承 BaseEntity,后端会下发) */
|
|
||||||
createTime?: string | null
|
createTime?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 售后工单(后端 `ProjectRequirementAfterSale`,表 `vm_project_requirement_after_sale`) */
|
|
||||||
export interface RequirementAfterSale {
|
export interface RequirementAfterSale {
|
||||||
id: string
|
id: string
|
||||||
requirementId: string | null
|
requirementId: string | null
|
||||||
/**
|
|
||||||
* 售后类型 —— 申请页下拉的固定 8 项:软件 BUG 修复 / 软件 / 固件功能迭代 /
|
|
||||||
* 硬件 PCB & 电路改版调整 / 硬件故障排查维修 / 成品不良品返修 / 元器件物料替换变更 /
|
|
||||||
* 物联网整机联调与版本升级 / 交付资料补齐与技术答疑。
|
|
||||||
* 库内暂无对应字典,值域先写在前端(`views/project/after-sales.vue#TYPE_OPTIONS`)。
|
|
||||||
*/
|
|
||||||
type: string | null
|
type: string | null
|
||||||
/** 售后原因(用户在申请页填的「问题描述」) */
|
|
||||||
reason: string | null
|
reason: string | null
|
||||||
/** 用户补充备注(申请页的「补充备注」) */
|
|
||||||
remark: string | null
|
remark: string | null
|
||||||
/**
|
|
||||||
* REQUESTED(待受理)/ IN_PROGRESS(处理中)/ WITHDRAWN(已撤回)/ COMPLETED(已完成)
|
|
||||||
* —— 见 `ProjectRequirementConstants`。
|
|
||||||
*/
|
|
||||||
status: string | null
|
status: string | null
|
||||||
requestUserName: string | null
|
requestUserName: string | null
|
||||||
requestTime: string | null
|
requestTime: string | null
|
||||||
@@ -479,81 +293,39 @@ export interface RequirementAfterSale {
|
|||||||
withdrawReason: string | null
|
withdrawReason: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 4 个页签接口的公共基础信息(后端 `ProjectRequirementBaseVo`)。
|
|
||||||
*
|
|
||||||
* <p>字段全部来自需求表 `vm_project_requirement`,**与是否立项无关**,任意页签都填充。</p>
|
|
||||||
*/
|
|
||||||
export interface RequirementBaseVo {
|
export interface RequirementBaseVo {
|
||||||
/** 需求 id(= 详情接口路径参数 = 列表行 RdProjectVo.id) */
|
|
||||||
id: string
|
id: string
|
||||||
projectNo: string | null
|
projectNo: string | null
|
||||||
projectName: string | null
|
projectName: string | null
|
||||||
/** 业务类型**中文名**(如「硬件电路」),非英文 code */
|
|
||||||
businessType: string | null
|
businessType: string | null
|
||||||
/** 状态码值:需求侧 INITIATING/CANCELLED,项目侧 IN_PROGRESS/… */
|
|
||||||
status: string | null
|
status: string | null
|
||||||
/** 提交时间 */
|
|
||||||
createTime: string | null
|
createTime: string | null
|
||||||
/** 提交人名称 */
|
|
||||||
submitUserName: string | null
|
submitUserName: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 「项目进度」页签(后端 `ProjectRequirementProgressVo`):基础信息 + 开发进度 + 里程碑 + 团队 */
|
|
||||||
export interface RequirementProgressVo extends RequirementBaseVo {
|
export interface RequirementProgressVo extends RequirementBaseVo {
|
||||||
/** 开发进度;未立项为 null */
|
|
||||||
projectStage: RequirementProjectStage | null
|
projectStage: RequirementProjectStage | null
|
||||||
/**
|
|
||||||
* 总体进度百分比(= 列表 `RdProjectVo.progress`,同源 `vm_rd_project.progress`)。
|
|
||||||
* 后端在项目行没有 progress 时回落到 `projectStage.developmentProgress`;未立项为 null。
|
|
||||||
*/
|
|
||||||
developmentProgress?: number | null
|
developmentProgress?: number | null
|
||||||
projectMilestones: RequirementMilestone[]
|
projectMilestones: RequirementMilestone[]
|
||||||
/** 项目团队;未立项为空数组 */
|
|
||||||
members: RequirementMember[]
|
members: RequirementMember[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 「项目文件」页签(后端 `ProjectRequirementFilesVo`):基础信息 + 需求附件 */
|
|
||||||
export interface RequirementFilesVo extends RequirementBaseVo {
|
export interface RequirementFilesVo extends RequirementBaseVo {
|
||||||
/** 需求附件;与是否立项无关,始终填充 */
|
|
||||||
requirementAttachments: RequirementAttachment[]
|
requirementAttachments: RequirementAttachment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 「合同财务」页签(后端 `ProjectRequirementFinanceVo`):基础信息 + 账单 + 付款计划 + 发票 + 合同。
|
|
||||||
*
|
|
||||||
* <p><b>卡片优先用 `bills`</b>(真账单:账单号 / 本期金额 / 真状态 / 可支付 id)。
|
|
||||||
* 该字段**只在含「账单下发」改动的后端构建里才有**——运行态若是旧构建,响应里根本没有
|
|
||||||
* 这个 key,所以声明成可选,由 `utils/projectVo.ts` 判别后回落到 `billPlans`(只读卡片)。</p>
|
|
||||||
*/
|
|
||||||
export interface RequirementFinanceVo extends RequirementBaseVo {
|
export interface RequirementFinanceVo extends RequirementBaseVo {
|
||||||
/** 真账单(`vm_bill`);**旧构建不下发该字段(undefined)**,新构建即使无账单也会给空数组 */
|
|
||||||
bills?: RequirementBill[]
|
bills?: RequirementBill[]
|
||||||
/** 付款计划(`vm_bill_plan`):分期规则,作为 `bills` 缺失时的只读回落 */
|
|
||||||
billPlans: RequirementBillPlan[]
|
billPlans: RequirementBillPlan[]
|
||||||
invoices: RequirementInvoice[]
|
invoices: RequirementInvoice[]
|
||||||
contracts: RequirementContract[]
|
contracts: RequirementContract[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 「售后信息」页签(后端 `ProjectRequirementAfterSalesVo`):基础信息 + 售后工单 + 工单附件 */
|
|
||||||
export interface RequirementAfterSalesVo extends RequirementBaseVo {
|
export interface RequirementAfterSalesVo extends RequirementBaseVo {
|
||||||
/** 售后工单;无则 null */
|
|
||||||
afterSale: RequirementAfterSale | null
|
afterSale: RequirementAfterSale | null
|
||||||
/**
|
|
||||||
* 售后工单附件 —— 与需求附件同表 `vm_project_requirement_attachment`,
|
|
||||||
* 靠 `phase = AFTER_SALE` + `target_id = 售后工单 id` 区分。
|
|
||||||
* 后端无条件下发(无工单时为空数组),前端直接渲染即可。
|
|
||||||
*/
|
|
||||||
afterSaleAttachments: RequirementAttachment[]
|
afterSaleAttachments: RequirementAttachment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 详情页四接口聚合结果。
|
|
||||||
*
|
|
||||||
* <p>`api/project.ts#getProjectDetail` 同帧并行取回 4 个页签后交给
|
|
||||||
* `utils/projectVo.ts#toProjectDetail` 合并成组件契约 {@link Project};
|
|
||||||
* 任一接口返回 null(需求不存在 / 非本人)即整页空态。</p>
|
|
||||||
*/
|
|
||||||
export interface RequirementDetailBundle {
|
export interface RequirementDetailBundle {
|
||||||
progress: RequirementProgressVo
|
progress: RequirementProgressVo
|
||||||
files: RequirementFilesVo
|
files: RequirementFilesVo
|
||||||
|
|||||||
4
src/types/business/service.d.ts
vendored
4
src/types/business/service.d.ts
vendored
@@ -1,14 +1,10 @@
|
|||||||
// 在线客服会话实体(PRD M9 在线客服)
|
|
||||||
|
|
||||||
/** 会话角色 */
|
|
||||||
export type ChatRole = 'user' | 'assistant'
|
export type ChatRole = 'user' | 'assistant'
|
||||||
|
|
||||||
/** 会话消息 */
|
|
||||||
export interface ChatMessage {
|
export interface ChatMessage {
|
||||||
id: string
|
id: string
|
||||||
role: ChatRole
|
role: ChatRole
|
||||||
content: string
|
content: string
|
||||||
/** 文件消息卡片(有值时按文件渲染) */
|
|
||||||
file?: {
|
file?: {
|
||||||
name: string
|
name: string
|
||||||
size: number
|
size: number
|
||||||
|
|||||||
12
src/types/business/user.d.ts
vendored
12
src/types/business/user.d.ts
vendored
@@ -1,6 +1,4 @@
|
|||||||
// 用户/认证实体(M4,page-architecture)
|
|
||||||
|
|
||||||
/** 用户 */
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string
|
id: string
|
||||||
nickname: string
|
nickname: string
|
||||||
@@ -10,11 +8,6 @@ export interface User {
|
|||||||
passwordModifiedAt: string
|
passwordModifiedAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 登录用户资料(GET /system/user/profile,后端 SysUserVo 的子集)。
|
|
||||||
*
|
|
||||||
* <p>字段名与后端一致:`nickName` / `phoneNumber`(不是 nickname / phone)。</p>
|
|
||||||
*/
|
|
||||||
export interface UserProfileVo {
|
export interface UserProfileVo {
|
||||||
userId: string
|
userId: string
|
||||||
userName: string
|
userName: string
|
||||||
@@ -25,25 +18,21 @@ export interface UserProfileVo {
|
|||||||
deptName: string
|
deptName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 账号密码登录参数 */
|
|
||||||
export interface LoginByAccountParams {
|
export interface LoginByAccountParams {
|
||||||
account: string
|
account: string
|
||||||
password: string
|
password: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 短信验证码登录参数 */
|
|
||||||
export interface LoginBySmsParams {
|
export interface LoginBySmsParams {
|
||||||
phone: string
|
phone: string
|
||||||
code: string
|
code: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 登录结果 */
|
|
||||||
export interface LoginResult {
|
export interface LoginResult {
|
||||||
token: string
|
token: string
|
||||||
user: User
|
user: User
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 注册参数 */
|
|
||||||
export interface RegisterParams {
|
export interface RegisterParams {
|
||||||
phone: string
|
phone: string
|
||||||
code: string
|
code: string
|
||||||
@@ -52,7 +41,6 @@ export interface RegisterParams {
|
|||||||
agree: boolean
|
agree: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 找回/重置密码参数 */
|
|
||||||
export interface ResetPasswordParams {
|
export interface ResetPasswordParams {
|
||||||
phone: string
|
phone: string
|
||||||
code: string
|
code: string
|
||||||
|
|||||||
2
src/types/global.d.ts
vendored
2
src/types/global.d.ts
vendored
@@ -1,8 +1,6 @@
|
|||||||
// 全局声明(Window 扩展等,按需添加)
|
|
||||||
export {}
|
export {}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
// 全局挂载扩展位
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
src/types/router.d.ts
vendored
9
src/types/router.d.ts
vendored
@@ -1,18 +1,9 @@
|
|||||||
// RouteMeta 扩展
|
|
||||||
export {}
|
export {}
|
||||||
|
|
||||||
declare module 'vue-router' {
|
declare module 'vue-router' {
|
||||||
interface RouteMeta {
|
interface RouteMeta {
|
||||||
/** 页面标题(document.title) */
|
|
||||||
title?: string
|
title?: string
|
||||||
/** 是否需要登录 */
|
|
||||||
requiresAuth?: boolean
|
requiresAuth?: boolean
|
||||||
/**
|
|
||||||
* 是否展示公共底栏(AppFooter)。
|
|
||||||
*
|
|
||||||
* <p>默认不展示:底栏信息量大、只在门户首页作为站点信息区出现,
|
|
||||||
* 其余页面(列表/详情/登录区)不渲染,避免干扰正文。</p>
|
|
||||||
*/
|
|
||||||
footer?: boolean
|
footer?: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,5 @@
|
|||||||
// 资源地址工具(后端多个接口会下发「OSS 文件标识」而不是完整地址)
|
|
||||||
//
|
//
|
||||||
// 实测:案例 ProjectCaseVo.coverUrl、项目提报 ProjectSubmissionVo.coverUrl/logoUrl
|
|
||||||
// 都可能是 OSS 主键(如 2099753406859292674)。直接当 <img src> 用会请求到当前站点的
|
|
||||||
// 错误路径上(表现为 404 / 空白),故统一在这里收口判断。
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 仅当值本身是可直接访问的地址时才返回它,否则返回空串。
|
|
||||||
*
|
|
||||||
* <p>可接受:`http(s)://…`、协议相对 `//…`、站内绝对路径 `/…`。
|
|
||||||
* OSS 文件标识(纯数字)、相对路径(`a/b.png`)一律判为不可直接使用。</p>
|
|
||||||
*/
|
|
||||||
export function toAssetUrl(raw: string | null | undefined): string {
|
export function toAssetUrl(raw: string | null | undefined): string {
|
||||||
const value = (raw ?? '').trim()
|
const value = (raw ?? '').trim()
|
||||||
if (!value) return ''
|
if (!value) return ''
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
import type { Bill, BillStatus, BillVo } from '@/types/business/finance'
|
import type { Bill, BillStatus, BillVo } from '@/types/business/finance'
|
||||||
|
|
||||||
/**
|
|
||||||
* 后端账单实体 → 页面视图模型的映射收口。
|
|
||||||
*
|
|
||||||
* <p>后端 `vm_bill` 用的是英文状态编码(`UNSTARTED/PENDING/PAID/CANCELLED`)与
|
|
||||||
* `periodName` / `transactionNo` 等字段名,页面用的是中文状态与 `planName` / `tradeNo`,
|
|
||||||
* 两侧口径的换算**只在这里做**,页面与 store 一律只认 `Bill`。</p>
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** 后端状态编码 → 页面中文状态 */
|
|
||||||
const STATUS_TEXT: Record<string, BillStatus> = {
|
const STATUS_TEXT: Record<string, BillStatus> = {
|
||||||
UNSTARTED: '未到期',
|
UNSTARTED: '未到期',
|
||||||
PENDING: '待支付',
|
PENDING: '待支付',
|
||||||
@@ -16,21 +7,13 @@ const STATUS_TEXT: Record<string, BillStatus> = {
|
|||||||
CANCELLED: '已取消',
|
CANCELLED: '已取消',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 后端状态编码 → 页面中文状态。
|
|
||||||
*
|
|
||||||
* <p>值域外(历史脏数据,如早期 `updateBillStatus` 硬编码写进去的 `'1'`)一律回落
|
|
||||||
* 「未到期」——宁可少标一个「待支付」按钮,也不要在页面上渲染出 `undefined`。</p>
|
|
||||||
*/
|
|
||||||
export function toBillStatus(code?: string): BillStatus {
|
export function toBillStatus(code?: string): BillStatus {
|
||||||
if (!code) return '未到期'
|
if (!code) return '未到期'
|
||||||
return STATUS_TEXT[code] ?? '未到期'
|
return STATUS_TEXT[code] ?? '未到期'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 单条账单映射;入参为空时返回 null(便于 `?ids=` 查不到时直接判空) */
|
|
||||||
export function toBill(vo: BillVo): Bill {
|
export function toBill(vo: BillVo): Bill {
|
||||||
return {
|
return {
|
||||||
// 雪花 id 序列化成 string、小 id 是 number ⇒ 统一归一成 string
|
|
||||||
id: String(vo.id),
|
id: String(vo.id),
|
||||||
planName: vo.periodName ?? '',
|
planName: vo.periodName ?? '',
|
||||||
billNo: vo.billNo ?? '',
|
billNo: vo.billNo ?? '',
|
||||||
@@ -38,26 +21,17 @@ export function toBill(vo: BillVo): Bill {
|
|||||||
projectName: vo.projectName ?? '',
|
projectName: vo.projectName ?? '',
|
||||||
amount: Number(vo.amount ?? 0),
|
amount: Number(vo.amount ?? 0),
|
||||||
status: toBillStatus(vo.status),
|
status: toBillStatus(vo.status),
|
||||||
// 后端 Jackson 已按 yyyy-MM-dd HH:mm:ss 序列化,直接透出
|
|
||||||
createTime: vo.createTime ?? '',
|
createTime: vo.createTime ?? '',
|
||||||
payChannel: vo.payChannel,
|
payChannel: vo.payChannel,
|
||||||
payTime: vo.payTime,
|
payTime: vo.payTime,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 列表映射(过滤掉没有 id 的异常行) */
|
|
||||||
export function toBills(list: BillVo[] | null | undefined): Bill[] {
|
export function toBills(list: BillVo[] | null | undefined): Bill[] {
|
||||||
if (!list || list.length === 0) return []
|
if (!list || list.length === 0) return []
|
||||||
return list.filter((item) => item != null && item.id != null).map(toBill)
|
return list.filter((item) => item != null && item.id != null).map(toBill)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 后端**计划**状态编码(`vm_bill_plan`)→ 页面中文状态。
|
|
||||||
*
|
|
||||||
* <p>为什么不能复用 {@link toBillStatus}:计划表用的是 `DONE/ACTIVE`,账单表用的是
|
|
||||||
* `PAID/PENDING`,值域不重合——代跑会把「已支付」「待支付」全落成「未到期」。
|
|
||||||
* (`utils/projectVo.ts` 里那份私有的 `toBillStatus` 是同一口径的另一种写法。)</p>
|
|
||||||
*/
|
|
||||||
export function toPlanStatus(code?: string | null): BillStatus {
|
export function toPlanStatus(code?: string | null): BillStatus {
|
||||||
const value = (code ?? '').toUpperCase()
|
const value = (code ?? '').toUpperCase()
|
||||||
if (value === 'DONE' || value === 'PAID' || value === 'COMPLETED') return '已支付'
|
if (value === 'DONE' || value === 'PAID' || value === 'COMPLETED') return '已支付'
|
||||||
@@ -66,14 +40,6 @@ export function toPlanStatus(code?: string | null): BillStatus {
|
|||||||
return '未到期'
|
return '未到期'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 「付款计划」字段子集 —— 入参用**宽松结构**,两种声明都能喂进来。
|
|
||||||
*
|
|
||||||
* <p>同一张 `vm_bill_plan` 在前端有两份声明:`types/business/finance.d.ts#BillPlanVo`
|
|
||||||
* (字段可选)与 `types/business/project.d.ts#RequirementBillPlan`(字段可空 `| null`)。
|
|
||||||
* 后者赋给前者会被 TS 的可空检查拦下(`string | null` 不能给 `string | undefined`),
|
|
||||||
* 故映射函数统一收这个子集类型,两边都满足。</p>
|
|
||||||
*/
|
|
||||||
export interface PlanLike {
|
export interface PlanLike {
|
||||||
id?: number | string | null
|
id?: number | string | null
|
||||||
projectName?: string | null
|
projectName?: string | null
|
||||||
@@ -89,21 +55,12 @@ export interface PlanLike {
|
|||||||
payTime?: string | null
|
payTime?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 期次名,与后端 `BillServiceImpl#buildPeriodName` 同口径:首期预付款、末期尾款、中间进度款 */
|
|
||||||
function periodLabel(period: number, periods: number): string {
|
function periodLabel(period: number, periods: number): string {
|
||||||
if (period <= 1) return '预付款'
|
if (period <= 1) return '预付款'
|
||||||
if (period >= periods) return '尾款'
|
if (period >= periods) return '尾款'
|
||||||
return '进度款'
|
return '进度款'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 分期计划(`vm_bill_plan`)→ 账单/订单条目。
|
|
||||||
*
|
|
||||||
* <p>两处都消费它:项目详情「合同财务」卡片(`utils/projectVo.ts#toPlanCard`)与收银台
|
|
||||||
* 「订单明细」(`api/finance.ts`,数据来自 `GET /web/requirementPool/get/finance/{需求id}`
|
|
||||||
* 下发的 `billPlans`)。期次名与金额按后端 `BillServiceImpl#resolveRatio/resolveAmount`
|
|
||||||
* 同口径折算;「账单号」取该期支付单的 `out_trade_no`(未支付为空 ⇒ 页面显示「—」)。</p>
|
|
||||||
*/
|
|
||||||
export function toPlanBill(vo: PlanLike): Bill {
|
export function toPlanBill(vo: PlanLike): Bill {
|
||||||
const period = vo.currentPeriod ?? 0
|
const period = vo.currentPeriod ?? 0
|
||||||
const periods = vo.periods ?? 0
|
const periods = vo.periods ?? 0
|
||||||
@@ -111,7 +68,6 @@ export function toPlanBill(vo: PlanLike): Bill {
|
|||||||
const segment = rule[period - 1]
|
const segment = rule[period - 1]
|
||||||
const ratio = Number.isFinite(segment) ? segment : periods > 0 ? 100 / periods : 0
|
const ratio = Number.isFinite(segment) ? segment : periods > 0 ? 100 / periods : 0
|
||||||
const total = Number(vo.totalAmount ?? 0)
|
const total = Number(vo.totalAmount ?? 0)
|
||||||
// 支付成功以支付单为准:计划状态可能没被回调推到 DONE,但流水已经是 SUCCESS
|
|
||||||
const paid = (vo.payStatus ?? '').toUpperCase() === 'SUCCESS'
|
const paid = (vo.payStatus ?? '').toUpperCase() === 'SUCCESS'
|
||||||
return {
|
return {
|
||||||
id: String(vo.id ?? ''),
|
id: String(vo.id ?? ''),
|
||||||
@@ -129,7 +85,6 @@ export function toPlanBill(vo: PlanLike): Bill {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 计划列表映射(过滤掉没有 id 的异常行) */
|
|
||||||
export function toPlanBills(list: readonly PlanLike[] | null | undefined): Bill[] {
|
export function toPlanBills(list: readonly PlanLike[] | null | undefined): Bill[] {
|
||||||
if (!list || list.length === 0) return []
|
if (!list || list.length === 0) return []
|
||||||
return list.filter((item) => item != null && item.id != null).map(toPlanBill)
|
return list.filter((item) => item != null && item.id != null).map(toPlanBill)
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
// 业务类型卡片展示元信息(图标 + 一句话描述)
|
|
||||||
//
|
//
|
||||||
// 值域来源:项目提报列表 GET /web/projectSubmission/list(见 api/demand.ts),字段对应关系:
|
|
||||||
// value ← businessType(类型名,也是提交时下发给后端的值)
|
|
||||||
// label ← title
|
// label ← title
|
||||||
// desc ← subtitle
|
// desc ← subtitle
|
||||||
// icon ← logoUrl(实测存的是 OSS 文件标识,换链后才是图片地址)
|
|
||||||
//
|
//
|
||||||
// ⚠️ 后端这两个展示字段目前是半成品:subtitle 实测常与类型名重复(title/subtitle 都是
|
|
||||||
// 「硬件电路」),logoUrl 存的是 OSS 主键而非地址。所以本文件保留「按类型名映射 desc/icon」
|
|
||||||
// 的前端兜底,只在后端字段不可用时生效。后端把 subtitle / logo 填好后,BIZ_META 可整表删掉。
|
|
||||||
import type { BizIconKey, BizTypeOption, SubmissionBizTypeVo } from '@/types/business/demand'
|
import type { BizIconKey, BizTypeOption, SubmissionBizTypeVo } from '@/types/business/demand'
|
||||||
import { toAssetUrl } from '@/utils/asset'
|
import { toAssetUrl } from '@/utils/asset'
|
||||||
|
|
||||||
@@ -17,23 +10,14 @@ interface BizMeta {
|
|||||||
icon: BizIconKey
|
icon: BizIconKey
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 按业务类型名索引的展示元信息(前端兜底,后端字段缺失时使用) */
|
|
||||||
const BIZ_META: Record<string, BizMeta> = {
|
const BIZ_META: Record<string, BizMeta> = {
|
||||||
互联网软件: { desc: 'Web/小程序/APP/二次开发/桌面工具等', icon: 'code' },
|
互联网软件: { desc: 'Web/小程序/APP/二次开发/桌面工具等', icon: 'code' },
|
||||||
硬件电路: { desc: '原理图设计/PCB设计/电气效验等', icon: 'chip' },
|
硬件电路: { desc: '原理图设计/PCB设计/电气效验等', icon: 'chip' },
|
||||||
物联网全案: { desc: '软硬件开发+云端一体化/定制化开发等', icon: 'iot' },
|
物联网全案: { desc: '软硬件开发+云端一体化/定制化开发等', icon: 'iot' },
|
||||||
生产制造: { desc: 'PCB制版/SMT/整机装配/定制生产等', icon: 'factory' },
|
生产制造: { desc: 'PCB制版/SMT/整机装配/定制生产等', icon: 'factory' },
|
||||||
// 「其他」只在此处作为**展示元信息**(描述/图标)备着:只有当接口真的下发一条
|
|
||||||
// businessType = 「其他」时才用得上,本文件不会据此凭空生成选项。
|
|
||||||
其他: { desc: '若没有对应的服务内容请选择该选项', icon: 'other' },
|
其他: { desc: '若没有对应的服务内容请选择该选项', icon: 'other' },
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 提报项 → 业务类型卡片选项。
|
|
||||||
*
|
|
||||||
* @param ossUrlById OSS 文件标识 → 可访问地址 的映射(换链结果;取不到时传空对象)
|
|
||||||
* @returns 类型名为空的脏数据返回 null,由调用方过滤
|
|
||||||
*/
|
|
||||||
export function toBizTypeOption(
|
export function toBizTypeOption(
|
||||||
vo: SubmissionBizTypeVo,
|
vo: SubmissionBizTypeVo,
|
||||||
ossUrlById: Record<string, string> = {},
|
ossUrlById: Record<string, string> = {},
|
||||||
@@ -43,7 +27,6 @@ export function toBizTypeOption(
|
|||||||
const label = (vo.title ?? '').trim() || value
|
const label = (vo.title ?? '').trim() || value
|
||||||
const meta = BIZ_META[value]
|
const meta = BIZ_META[value]
|
||||||
const subtitle = (vo.subtitle ?? '').trim()
|
const subtitle = (vo.subtitle ?? '').trim()
|
||||||
// 后端 subtitle 常常直接填类型名本身,这种「描述」没有信息量 → 视为缺失,回退前端映射
|
|
||||||
const desc = subtitle && subtitle !== value && subtitle !== label
|
const desc = subtitle && subtitle !== value && subtitle !== label
|
||||||
? subtitle
|
? subtitle
|
||||||
: (meta?.desc ?? '')
|
: (meta?.desc ?? '')
|
||||||
@@ -53,12 +36,10 @@ export function toBizTypeOption(
|
|||||||
label,
|
label,
|
||||||
desc,
|
desc,
|
||||||
icon: meta?.icon ?? 'other',
|
icon: meta?.icon ?? 'other',
|
||||||
// logoUrl 有时本就填的是地址,有时是 OSS 标识(需换链)
|
|
||||||
iconUrl: toAssetUrl(logoRaw) || ossUrlById[logoRaw] || '',
|
iconUrl: toAssetUrl(logoRaw) || ossUrlById[logoRaw] || '',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 字典项 → 业务类型卡片选项(仅当提报表无可用数据、回退字典兜底时使用) */
|
|
||||||
export function toBizTypeOptionFromDict(value: string, label: string): BizTypeOption {
|
export function toBizTypeOptionFromDict(value: string, label: string): BizTypeOption {
|
||||||
const meta = BIZ_META[value]
|
const meta = BIZ_META[value]
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,45 +1,15 @@
|
|||||||
// 预算范围(字典 vm_budget_range)的展示/校验元信息
|
|
||||||
//
|
//
|
||||||
// 后端字典 `vm_budget_range` 共 5 项,最后一项(dictValue='4',label「手填」)语义为
|
|
||||||
// 「不在预设档位里、由用户自己写一个金额」。设计上:
|
|
||||||
// 前 4 项 = 普通胶囊按钮;最后一项 = **同排的第 5 个控件,本身就是一个输入框**
|
|
||||||
// (占位文案直接用字典 label「手填」),不再是"点一下再额外弹一个输入框"。
|
|
||||||
//
|
//
|
||||||
// ⚠️ 手写文本没有独立的落库位置:`WorkOrderDraftBo.budgetCustom` 被注释掉、
|
|
||||||
// `vm_work_order_draft` 表也没有 `budget_custom` 列(VO 上那个字段恒为 null),
|
|
||||||
// 所以手写内容直接写进 `budgetRange` 本身。而提交落库的
|
|
||||||
// `vm_project_requirement.budget_range` 是 **varchar(30)**,故输入框必须限长 30,
|
|
||||||
// 否则 MySQL 严格模式下会报 1406(Data too long)导致提交失败。
|
|
||||||
import type { DictOption } from '@/types/business/demand'
|
import type { DictOption } from '@/types/business/demand'
|
||||||
|
|
||||||
/** 字典里表示「自己填写」的那一项(后端 label 现为「手填」) */
|
|
||||||
const MANUAL_PATTERN = /手填|手写|自定义/
|
const MANUAL_PATTERN = /手填|手写|自定义/
|
||||||
|
|
||||||
/**
|
|
||||||
* 手写内容最大长度。
|
|
||||||
*
|
|
||||||
* <p>= `vm_project_requirement.budget_range` 的列宽 varchar(30)。注意后端 BO 上
|
|
||||||
* `@Size(max = 100)` 更宽松,真正的瓶颈是数据库列宽,故以 30 为准。</p>
|
|
||||||
*/
|
|
||||||
export const MANUAL_BUDGET_MAX = 30
|
export const MANUAL_BUDGET_MAX = 30
|
||||||
|
|
||||||
/** 该字典项是否为「手写」项(决定它在页面上渲染成按钮还是输入框) */
|
|
||||||
export function isManualBudget(label: string): boolean {
|
export function isManualBudget(label: string): boolean {
|
||||||
return MANUAL_PATTERN.test(label)
|
return MANUAL_PATTERN.test(label)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 回填草稿时区分「预设档位」与「手填内容」,并把预设档位**归一成 dictValue**。
|
|
||||||
*
|
|
||||||
* <p>判据:`budgetRange` 命中字典的 **dictValue 或 dictLabel** 即为预设档位;否则(非空)
|
|
||||||
* 视为手填文本。后端只回传一个 budgetRange 字符串、没有模式标记(`budgetCustom` 恒为 null),
|
|
||||||
* 只能这样反推。</p>
|
|
||||||
*
|
|
||||||
* <p>为什么要连 dictLabel 一起比、并且归一:字典 `vm_budget_range` 的 dictValue 是 '0'~'4',
|
|
||||||
* 但库里的历史草稿存的是**文案**(如 `1万以下`、`10万以上`)。只比 dictValue 的话,这些历史档位
|
|
||||||
* 会被误判成手填、串进输入框;而直接回填文案的话,页面按钮是按 dictValue 判选中的,
|
|
||||||
* 又会出现"值在、按钮不亮"的空档。故统一转换成 dictValue(下次保存草稿即把历史文案收敛成码值)。</p>
|
|
||||||
*/
|
|
||||||
export function resolveDraftBudget(
|
export function resolveDraftBudget(
|
||||||
budgetRange: string | null | undefined,
|
budgetRange: string | null | undefined,
|
||||||
dict: DictOption[],
|
dict: DictOption[],
|
||||||
|
|||||||
@@ -1,24 +1,13 @@
|
|||||||
// 案例后端 VO → 页面实体映射(纯函数,无 Vue 依赖)
|
|
||||||
//
|
//
|
||||||
// 后端 ProjectCaseVo 与页面 Case 的差异(字段命名、业务类型取值、富文本、数值单位)
|
|
||||||
// 统一在此对齐,供首页 store(案例库精选)与案例库 store(列表 / 详情)共用。
|
|
||||||
|
|
||||||
import type { Case, CaseAttachment, CaseSection, ProjectCaseVo } from '@/types/business/cases'
|
import type { Case, CaseAttachment, CaseSection, ProjectCaseVo } from '@/types/business/cases'
|
||||||
import { toAssetUrl } from '@/utils/asset'
|
import { toAssetUrl } from '@/utils/asset'
|
||||||
import { toBusinessType } from '@/utils/format'
|
import { toBusinessType } from '@/utils/format'
|
||||||
|
|
||||||
/** 规模展示值:后端下发「万」为单位的数值,页面按设计稿拼成「10万」 */
|
|
||||||
function toScaleText(scale: number | null): string {
|
function toScaleText(scale: number | null): string {
|
||||||
return scale == null ? '' : `${scale}万`
|
return scale == null ? '' : `${scale}万`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 正文区块:后端 catalogs 按顺序映射为「标题 + 富文本」。
|
|
||||||
*
|
|
||||||
* <p>标题由后端下发(vm_project_case_catalog.title),前端不再写死区块名;顺序沿用后端
|
|
||||||
* 的 sort_order、id 升序。去掉标签后为空的内容会被过滤掉,避免渲染出只有标题的空白卡。
|
|
||||||
* 列表 / 精选接口不下发 catalogs,故那些场景返回空数组。</p>
|
|
||||||
*/
|
|
||||||
function toSections(list: ProjectCaseVo['catalogs']): CaseSection[] {
|
function toSections(list: ProjectCaseVo['catalogs']): CaseSection[] {
|
||||||
return (list ?? [])
|
return (list ?? [])
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
@@ -28,7 +17,6 @@ function toSections(list: ProjectCaseVo['catalogs']): CaseSection[] {
|
|||||||
.filter((section) => section.content.replace(/<[^>]*>/g, '').replace(/ /gi, ' ').trim().length > 0)
|
.filter((section) => section.content.replace(/<[^>]*>/g, '').replace(/ /gi, ' ').trim().length > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 附件:过滤掉没有访问地址的脏数据 */
|
|
||||||
function toAttachments(list: ProjectCaseVo['attachments']): CaseAttachment[] {
|
function toAttachments(list: ProjectCaseVo['attachments']): CaseAttachment[] {
|
||||||
return (list ?? [])
|
return (list ?? [])
|
||||||
.filter((item) => (item.url ?? '').trim().length > 0)
|
.filter((item) => (item.url ?? '').trim().length > 0)
|
||||||
@@ -39,16 +27,8 @@ function toAttachments(list: ProjectCaseVo['attachments']): CaseAttachment[] {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 提报主键 id → 业务类型名称 的字典(来源 GET /categories 的 projectCaseCounts) */
|
|
||||||
export type CaseTypeNameMap = Record<string, string>
|
export type CaseTypeNameMap = Record<string, string>
|
||||||
|
|
||||||
/**
|
|
||||||
* 业务类型展示名解析。
|
|
||||||
*
|
|
||||||
* <p>优先级:后端 JOIN 出的 projectTypeName → 接口字典(id 反查名称)→ 原值。
|
|
||||||
* 实测案例表 business_type 列存的是**提报主键 id**(如 "2099753433048526850")且
|
|
||||||
* projectTypeName 为 null —— 若不查字典直接交给 toBusinessType,会全部归到「其他」。</p>
|
|
||||||
*/
|
|
||||||
function resolveCategoryName(vo: ProjectCaseVo, typeNameById?: CaseTypeNameMap): string {
|
function resolveCategoryName(vo: ProjectCaseVo, typeNameById?: CaseTypeNameMap): string {
|
||||||
return (
|
return (
|
||||||
vo.projectTypeName?.trim()
|
vo.projectTypeName?.trim()
|
||||||
@@ -58,19 +38,10 @@ function resolveCategoryName(vo: ProjectCaseVo, typeNameById?: CaseTypeNameMap):
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 后端案例 VO → 页面案例实体。
|
|
||||||
*
|
|
||||||
* <p>列表接口不返回 catalogs / attachments,页面实体对应字段为空
|
|
||||||
* (正文区块与附件仅详情接口下发)。</p>
|
|
||||||
*
|
|
||||||
* @param typeNameById 业务类型字典(id → 名称),取自案例库 store 的 /categories 结果
|
|
||||||
*/
|
|
||||||
export function toCase(vo: ProjectCaseVo, typeNameById?: CaseTypeNameMap): Case {
|
export function toCase(vo: ProjectCaseVo, typeNameById?: CaseTypeNameMap): Case {
|
||||||
return {
|
return {
|
||||||
id: String(vo.id),
|
id: String(vo.id),
|
||||||
title: vo.caseName ?? '',
|
title: vo.caseName ?? '',
|
||||||
// 案例表 business_type 存提报主键 id,展示名需经字典翻译(见 resolveCategoryName)
|
|
||||||
category: toBusinessType(resolveCategoryName(vo, typeNameById)),
|
category: toBusinessType(resolveCategoryName(vo, typeNameById)),
|
||||||
cover: toAssetUrl(vo.coverUrl),
|
cover: toAssetUrl(vo.coverUrl),
|
||||||
finishDate: vo.completedAt ?? '',
|
finishDate: vo.completedAt ?? '',
|
||||||
@@ -79,7 +50,6 @@ export function toCase(vo: ProjectCaseVo, typeNameById?: CaseTypeNameMap): Case
|
|||||||
industry: vo.industry || vo.businessIndustry || '',
|
industry: vo.industry || vo.businessIndustry || '',
|
||||||
duration: vo.projectCycle ?? '',
|
duration: vo.projectCycle ?? '',
|
||||||
clientType: vo.customerType ?? '',
|
clientType: vo.customerType ?? '',
|
||||||
// 正文区块(标题 + 富文本)由后端 catalogs 下发,前端不再写死区块名
|
|
||||||
sections: toSections(vo.catalogs),
|
sections: toSections(vo.catalogs),
|
||||||
attachments: toAttachments(vo.attachments),
|
attachments: toAttachments(vo.attachments),
|
||||||
video: '',
|
video: '',
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
import type { EntryTone, PortalEntryCard, PortalEntryGroup, PortalEntryVo } from '@/types/business/home'
|
import type { EntryTone, PortalEntryCard, PortalEntryGroup, PortalEntryVo } from '@/types/business/home'
|
||||||
|
|
||||||
/**
|
|
||||||
* 门户入口的视图映射:后端 icon 字段 → 页面可直接渲染的图标 + 底色
|
|
||||||
*
|
|
||||||
* <p>后端 icon 是「图标名 / url」自由文本,可能为空、可能是图片地址、
|
|
||||||
* 也可能是一个标识符(如 folder)。这里按优先级兜底,保证任何取值都能渲染出可视化图标。</p>
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** 图标底色(设计稿为五色循环),按序号轮转分配 */
|
|
||||||
const TONE_CYCLE: EntryTone[] = ['purple', 'green', 'blue', 'amber', 'red']
|
const TONE_CYCLE: EntryTone[] = ['purple', 'green', 'blue', 'amber', 'red']
|
||||||
|
|
||||||
/** 显示名 → emoji 兜底表(后端 icon 为空或非 emoji 时使用) */
|
|
||||||
const NAME_ICON_MAP: Record<string, string> = {
|
const NAME_ICON_MAP: Record<string, string> = {
|
||||||
项目中心: '🗂️',
|
项目中心: '🗂️',
|
||||||
需求中心: '🧾',
|
需求中心: '🧾',
|
||||||
@@ -42,20 +33,16 @@ const NAME_ICON_MAP: Record<string, string> = {
|
|||||||
关于我们: 'ℹ️',
|
关于我们: 'ℹ️',
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 兜底图标 */
|
|
||||||
const DEFAULT_ICON = '📦'
|
const DEFAULT_ICON = '📦'
|
||||||
|
|
||||||
/** 是否图片地址 */
|
|
||||||
function isImageUrl(value: string): boolean {
|
function isImageUrl(value: string): boolean {
|
||||||
return /^(https?:)?\/\//i.test(value) || value.startsWith('/')
|
return /^(https?:)?\/\//i.test(value) || value.startsWith('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 是否可直接当作 emoji / 字符图标使用(非纯字母数字标识符) */
|
|
||||||
function isGlyph(value: string): boolean {
|
function isGlyph(value: string): boolean {
|
||||||
return !/^[a-zA-Z0-9_-]+$/.test(value)
|
return !/^[a-zA-Z0-9_-]+$/.test(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 解析单项图标与底色 */
|
|
||||||
function resolveIcon(icon: string | null, name: string): { icon: string; isImage: boolean } {
|
function resolveIcon(icon: string | null, name: string): { icon: string; isImage: boolean } {
|
||||||
const raw = (icon ?? '').trim()
|
const raw = (icon ?? '').trim()
|
||||||
if (raw) {
|
if (raw) {
|
||||||
@@ -65,12 +52,6 @@ function resolveIcon(icon: string | null, name: string): { icon: string; isImage
|
|||||||
return { icon: NAME_ICON_MAP[name] ?? DEFAULT_ICON, isImage: false }
|
return { icon: NAME_ICON_MAP[name] ?? DEFAULT_ICON, isImage: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 后端入口 → 页面卡片
|
|
||||||
*
|
|
||||||
* @param vo 后端入口
|
|
||||||
* @param index 组内序号(决定底色轮转)
|
|
||||||
*/
|
|
||||||
export function toPortalEntryCard(vo: PortalEntryVo, index: number): PortalEntryCard | null {
|
export function toPortalEntryCard(vo: PortalEntryVo, index: number): PortalEntryCard | null {
|
||||||
if (vo.id == null) return null
|
if (vo.id == null) return null
|
||||||
const label = (vo.name ?? '').trim() || '未命名入口'
|
const label = (vo.name ?? '').trim() || '未命名入口'
|
||||||
@@ -86,7 +67,6 @@ export function toPortalEntryCard(vo: PortalEntryVo, index: number): PortalEntry
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 后端入口列表 → 页面卡片列表(过滤无效项,保持后端下发顺序) */
|
|
||||||
export function toPortalEntryCards(list: PortalEntryVo[]): PortalEntryCard[] {
|
export function toPortalEntryCards(list: PortalEntryVo[]): PortalEntryCard[] {
|
||||||
return list
|
return list
|
||||||
.map((vo, index) => toPortalEntryCard(vo, index))
|
.map((vo, index) => toPortalEntryCard(vo, index))
|
||||||
|
|||||||
@@ -1,15 +1,4 @@
|
|||||||
/**
|
|
||||||
* 文件「类型标识」的唯一口径(后缀 → 标识色 / 缩写)。
|
|
||||||
*
|
|
||||||
* <p>设计稿里文件行左侧是一个按类型着色的标识块(PDF 红、Word 蓝、Excel 绿…),
|
|
||||||
* 需求提报页(`views/demand/cpns/DemandAttachments.vue`)已按这套色板实现,
|
|
||||||
* 项目详情「合同财务」的发票/合同行沿用同一份,避免两处各写一张色表后走偏。</p>
|
|
||||||
*
|
|
||||||
* <p>用法:`fileToneOf(file)` 取色、`fileLabelOf(file)` 取缩写,把它们放到
|
|
||||||
* `style="{ color, backgroundColor: color + '1a' }"`(1a ≈ 10% 透明)即可。</p>
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** 文件标识色(设计稿口径)。未命中的后缀统一走中性灰。 */
|
|
||||||
const FILE_TONES: Record<string, string> = {
|
const FILE_TONES: Record<string, string> = {
|
||||||
pdf: '#f54a45',
|
pdf: '#f54a45',
|
||||||
doc: '#2f6bff',
|
doc: '#2f6bff',
|
||||||
@@ -29,7 +18,6 @@ const FILE_TONES: Record<string, string> = {
|
|||||||
webp: '#7a3fd6',
|
webp: '#7a3fd6',
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 缩写展示(长后缀截断,压缩包统一 ZIP) */
|
|
||||||
const FILE_LABELS: Record<string, string> = {
|
const FILE_LABELS: Record<string, string> = {
|
||||||
doc: 'DOC',
|
doc: 'DOC',
|
||||||
docx: 'DOC',
|
docx: 'DOC',
|
||||||
@@ -43,16 +31,6 @@ const FILE_LABELS: Record<string, string> = {
|
|||||||
'7z': 'ZIP',
|
'7z': 'ZIP',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 文件对象的**最小**形状:详情页/提报页/消息页的 `FileInfo` 都能满足。
|
|
||||||
*
|
|
||||||
* <p>`type` 是需求提报页带上来的后缀(可能带前导点 `.pdf`,后端 `fileSuffix` 口径如此);
|
|
||||||
* `name` / `originalName` 二者取其一(提报页用 `originalName`,详情页用 `name`)。</p>
|
|
||||||
*
|
|
||||||
* <p>⚠️ `url` 是**最后**的兜底:合同财务页的发票行,展示名取的是发票抬头
|
|
||||||
* (如「贵州命勤科技有限公司」)而不是文件名 ⇒ 只能从 `invoiceFile` 地址上认后缀,
|
|
||||||
* 否则那一行的类型标识会全落成灰底 `FILE`。</p>
|
|
||||||
*/
|
|
||||||
export interface FileLike {
|
export interface FileLike {
|
||||||
name?: string | null
|
name?: string | null
|
||||||
originalName?: string | null
|
originalName?: string | null
|
||||||
@@ -60,7 +38,6 @@ export interface FileLike {
|
|||||||
url?: string | null
|
url?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 取后缀(小写、无前导点);优先显式 `type`,其次文件名,最后从 url 猜 */
|
|
||||||
export function fileSuffixOf(file: FileLike | string): string {
|
export function fileSuffixOf(file: FileLike | string): string {
|
||||||
if (typeof file === 'string') return suffixFromName(file)
|
if (typeof file === 'string') return suffixFromName(file)
|
||||||
const type = (file.type ?? '').replace(/^\./, '').toLowerCase()
|
const type = (file.type ?? '').replace(/^\./, '').toLowerCase()
|
||||||
@@ -75,19 +52,16 @@ function suffixFromName(name: string): string {
|
|||||||
return dot >= 0 ? name.slice(dot + 1).toLowerCase() : ''
|
return dot >= 0 ? name.slice(dot + 1).toLowerCase() : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 从地址认后缀:先剥掉 query/hash,再取最后一段的扩展名(带签名参数的 OSS 直链常见) */
|
|
||||||
function suffixFromUrl(url: string): string {
|
function suffixFromUrl(url: string): string {
|
||||||
const path = url.split('?')[0].split('#')[0]
|
const path = url.split('?')[0].split('#')[0]
|
||||||
const last = path.slice(path.lastIndexOf('/') + 1)
|
const last = path.slice(path.lastIndexOf('/') + 1)
|
||||||
return suffixFromName(last)
|
return suffixFromName(last)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 标识色(未命中后缀 → 中性灰 `#8a94a6`) */
|
|
||||||
export function fileToneOf(file: FileLike | string): string {
|
export function fileToneOf(file: FileLike | string): string {
|
||||||
return FILE_TONES[fileSuffixOf(file)] ?? '#8a94a6'
|
return FILE_TONES[fileSuffixOf(file)] ?? '#8a94a6'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 缩写(未命中 → 后缀前 3 位大写;连后缀都没有 → `FILE`) */
|
|
||||||
export function fileLabelOf(file: FileLike | string): string {
|
export function fileLabelOf(file: FileLike | string): string {
|
||||||
const suffix = fileSuffixOf(file)
|
const suffix = fileSuffixOf(file)
|
||||||
return FILE_LABELS[suffix] ?? (suffix ? suffix.slice(0, 3).toUpperCase() : 'FILE')
|
return FILE_LABELS[suffix] ?? (suffix ? suffix.slice(0, 3).toUpperCase() : 'FILE')
|
||||||
|
|||||||
@@ -1,26 +1,16 @@
|
|||||||
/**
|
|
||||||
* 预览类型判定(纯函数,无副作用,便于单测与复用)。
|
|
||||||
*
|
|
||||||
* <p>为什么集中在一个文件:前端多处 `FileList` 预览都按「后缀 → 渲染方式」分流,
|
|
||||||
* 统一这里避免每个页面各写一套正则 / 后缀表。后端同源预览端点(`/web/workOrderDraft/preview/{ossId}`)
|
|
||||||
* 按后缀纠正 Content-Type,这里与之后端口径保持一致的归类。</p>
|
|
||||||
*/
|
|
||||||
export type PreviewKind = 'image' | 'pdf' | 'video' | 'audio' | 'text' | 'unsupported'
|
export type PreviewKind = 'image' | 'pdf' | 'video' | 'audio' | 'text' | 'unsupported'
|
||||||
|
|
||||||
const IMAGE_SUFFIXES = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp']
|
const IMAGE_SUFFIXES = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp']
|
||||||
const PDF_SUFFIXES = ['pdf']
|
const PDF_SUFFIXES = ['pdf']
|
||||||
const VIDEO_SUFFIXES = ['mp4', 'webm', 'mov', 'avi', 'm4v']
|
const VIDEO_SUFFIXES = ['mp4', 'webm', 'mov', 'avi', 'm4v']
|
||||||
const AUDIO_SUFFIXES = ['mp3', 'wav', 'ogg', 'm4a']
|
const AUDIO_SUFFIXES = ['mp3', 'wav', 'ogg', 'm4a']
|
||||||
// 文本类:含 html/htm/svg —— 统一按纯文本展示源码,避免内联渲染触发存储型 XSS
|
|
||||||
const TEXT_SUFFIXES = ['txt', 'sql', 'log', 'csv', 'json', 'xml', 'md', 'yml', 'yaml', 'html', 'htm', 'svg']
|
const TEXT_SUFFIXES = ['txt', 'sql', 'log', 'csv', 'json', 'xml', 'md', 'yml', 'yaml', 'html', 'htm', 'svg']
|
||||||
|
|
||||||
/** 取小写字尾(不含点);无点返回空串 */
|
|
||||||
export function getSuffix(name: string): string {
|
export function getSuffix(name: string): string {
|
||||||
const idx = name.lastIndexOf('.')
|
const idx = name.lastIndexOf('.')
|
||||||
return idx >= 0 ? name.slice(idx + 1).toLowerCase() : ''
|
return idx >= 0 ? name.slice(idx + 1).toLowerCase() : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 按文件名判定预览类型;office / 未知后缀一律 unsupported(前端不内联,给下载) */
|
|
||||||
export function resolvePreviewKind(name: string): PreviewKind {
|
export function resolvePreviewKind(name: string): PreviewKind {
|
||||||
const ext = getSuffix(name)
|
const ext = getSuffix(name)
|
||||||
if (IMAGE_SUFFIXES.includes(ext)) return 'image'
|
if (IMAGE_SUFFIXES.includes(ext)) return 'image'
|
||||||
@@ -31,18 +21,12 @@ export function resolvePreviewKind(name: string): PreviewKind {
|
|||||||
return 'unsupported'
|
return 'unsupported'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 是否可被前端内联预览(true = image/pdf/video/audio/text) */
|
|
||||||
export function isPreviewable(name: string): boolean {
|
export function isPreviewable(name: string): boolean {
|
||||||
return resolvePreviewKind(name) !== 'unsupported'
|
return resolvePreviewKind(name) !== 'unsupported'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 文本内联预览上限(1MB)。
|
|
||||||
* 整文件塞进 `<pre>` 会卡死浏览器(如 5MB+ 的 .sql/.log),故只读取前 1MB 渲染,超出截断并提示下载。
|
|
||||||
*/
|
|
||||||
export const PREVIEW_TEXT_MAX_BYTES = 1024 * 1024
|
export const PREVIEW_TEXT_MAX_BYTES = 1024 * 1024
|
||||||
|
|
||||||
/** 人类可读体积(B/KB/MB/GB) */
|
|
||||||
export function formatBytes(bytes: number): string {
|
export function formatBytes(bytes: number): string {
|
||||||
if (!bytes || bytes < 0) return '0 B'
|
if (!bytes || bytes < 0) return '0 B'
|
||||||
const units = ['B', 'KB', 'MB', 'GB']
|
const units = ['B', 'KB', 'MB', 'GB']
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// 格式化工具(无 Vue 依赖)
|
|
||||||
|
|
||||||
import type { BusinessType } from '@/types/api'
|
import type { BusinessType } from '@/types/api'
|
||||||
|
|
||||||
@@ -12,12 +11,6 @@ const BUSINESS_TYPE_LABELS: Record<BusinessType, string> = {
|
|||||||
|
|
||||||
const BUSINESS_TYPE_CODES = Object.keys(BUSINESS_TYPE_LABELS) as BusinessType[]
|
const BUSINESS_TYPE_CODES = Object.keys(BUSINESS_TYPE_LABELS) as BusinessType[]
|
||||||
|
|
||||||
/**
|
|
||||||
* 后端业务类型归一化:兼容英文 code(internet)与中文名称(互联网软件),未知值归 other。
|
|
||||||
*
|
|
||||||
* <p>后端案例表 / 项目提报表的 business_type 是自由文本,历史数据里两种写法混存,
|
|
||||||
* 展示与筛选都需要先归一化到前端 BusinessType。</p>
|
|
||||||
*/
|
|
||||||
export function toBusinessType(raw: string | null | undefined): BusinessType {
|
export function toBusinessType(raw: string | null | undefined): BusinessType {
|
||||||
const value = (raw ?? '').trim()
|
const value = (raw ?? '').trim()
|
||||||
if (!value) return 'other'
|
if (!value) return 'other'
|
||||||
@@ -29,19 +22,10 @@ export function toBusinessType(raw: string | null | undefined): BusinessType {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 业务类型展示名 */
|
|
||||||
export function formatBusinessType(type: BusinessType): string {
|
export function formatBusinessType(type: BusinessType): string {
|
||||||
return BUSINESS_TYPE_LABELS[type]
|
return BUSINESS_TYPE_LABELS[type]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 金额千分位(默认两位小数)。
|
|
||||||
*
|
|
||||||
* <p>⚠️ `value` 必须容错字符串:后端 `BigDecimal` 经全局 Jackson 配置序列化成
|
|
||||||
* <b>字符串</b>(如 `"120000.00"`),直接 `value.toFixed()` 会抛
|
|
||||||
* `TypeError: value.toFixed is not a function`——在模板里调用会让<b>整个组件渲染失败</b>
|
|
||||||
* (实测「合同财务」页签白屏)。这里统一 `Number()` 归一,非法值按 0 处理。</p>
|
|
||||||
*/
|
|
||||||
export function formatMoney(value: number | string | null | undefined, decimals = 2): string {
|
export function formatMoney(value: number | string | null | undefined, decimals = 2): string {
|
||||||
const num = Number(value)
|
const num = Number(value)
|
||||||
const fixed = (Number.isFinite(num) ? num : 0).toFixed(decimals)
|
const fixed = (Number.isFinite(num) ? num : 0).toFixed(decimals)
|
||||||
@@ -54,7 +38,6 @@ function pad(n: number): string {
|
|||||||
return n < 10 ? `0${n}` : String(n)
|
return n < 10 ? `0${n}` : String(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 日期格式化(YYYY-MM-DD HH:mm:ss 可裁剪) */
|
|
||||||
export function formatDate(input: Date | string | number, pattern = 'YYYY-MM-DD HH:mm:ss'): string {
|
export function formatDate(input: Date | string | number, pattern = 'YYYY-MM-DD HH:mm:ss'): string {
|
||||||
const date = input instanceof Date ? input : new Date(input)
|
const date = input instanceof Date ? input : new Date(input)
|
||||||
if (Number.isNaN(date.getTime())) return ''
|
if (Number.isNaN(date.getTime())) return ''
|
||||||
@@ -69,7 +52,6 @@ export function formatDate(input: Date | string | number, pattern = 'YYYY-MM-DD
|
|||||||
return pattern.replace(/YYYY|MM|DD|HH|mm|ss/g, (token) => map[token] ?? token)
|
return pattern.replace(/YYYY|MM|DD|HH|mm|ss/g, (token) => map[token] ?? token)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 文件大小(B/KB/MB/GB) */
|
|
||||||
export function formatFileSize(bytes: number): string {
|
export function formatFileSize(bytes: number): string {
|
||||||
if (bytes < 1024) return `${bytes}B`
|
if (bytes < 1024) return `${bytes}B`
|
||||||
const kb = bytes / 1024
|
const kb = bytes / 1024
|
||||||
|
|||||||
@@ -1,34 +1,21 @@
|
|||||||
import type { PayMethod } from '@/types/business/finance'
|
import type { PayMethod } from '@/types/business/finance'
|
||||||
|
|
||||||
/** 收银台「选择支付方式」条目 */
|
|
||||||
export interface PayMethodMeta {
|
export interface PayMethodMeta {
|
||||||
/** 前端标识,与 `PayMethod` 同源 */
|
|
||||||
value: PayMethod
|
value: PayMethod
|
||||||
/** 展示名 */
|
|
||||||
label: string
|
label: string
|
||||||
/** 副标题(设计稿:微信 = 推荐使用 / 支付宝 = 其他用户选择) */
|
|
||||||
hint: string
|
hint: string
|
||||||
/** 后端 `PayChannelEnum` 取值 */
|
|
||||||
channel: 'WECHAT' | 'ALIPAY'
|
channel: 'WECHAT' | 'ALIPAY'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 支付方式展示元数据(顺序即收银台渲染顺序,默认选中第一项) */
|
|
||||||
export const PAY_METHODS: PayMethodMeta[] = [
|
export const PAY_METHODS: PayMethodMeta[] = [
|
||||||
{ value: 'wechat', label: '微信支付', hint: '推荐使用', channel: 'WECHAT' },
|
{ value: 'wechat', label: '微信支付', hint: '推荐使用', channel: 'WECHAT' },
|
||||||
{ value: 'alipay', label: '支付宝支付', hint: '其他用户选择', channel: 'ALIPAY' },
|
{ value: 'alipay', label: '支付宝支付', hint: '其他用户选择', channel: 'ALIPAY' },
|
||||||
]
|
]
|
||||||
|
|
||||||
/** 前端支付方式 → 后端渠道枚举 */
|
|
||||||
export function toPayChannel(method: PayMethod): PayMethodMeta['channel'] {
|
export function toPayChannel(method: PayMethod): PayMethodMeta['channel'] {
|
||||||
return PAY_METHODS.find((item) => item.value === method)?.channel ?? 'WECHAT'
|
return PAY_METHODS.find((item) => item.value === method)?.channel ?? 'WECHAT'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 后端渠道编码 → 展示名。
|
|
||||||
*
|
|
||||||
* <p>同一份语义在两张表里大小写不一致(`vm_bill.pay_channel` 存 `WECHAT`,
|
|
||||||
* `vm_finance_payment.pay_channel` 存 `wechat`),这里统一 `toUpperCase` 兜住。</p>
|
|
||||||
*/
|
|
||||||
const CHANNEL_LABELS: Record<string, string> = {
|
const CHANNEL_LABELS: Record<string, string> = {
|
||||||
WECHAT: '微信支付',
|
WECHAT: '微信支付',
|
||||||
ALIPAY: '支付宝支付',
|
ALIPAY: '支付宝支付',
|
||||||
@@ -36,13 +23,11 @@ const CHANNEL_LABELS: Record<string, string> = {
|
|||||||
WORKWX: '企业微信',
|
WORKWX: '企业微信',
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 后端渠道编码 → 中文展示名;未知编码原样返回,空值返回「—」 */
|
|
||||||
export function toChannelLabel(code?: string): string {
|
export function toChannelLabel(code?: string): string {
|
||||||
if (!code) return '—'
|
if (!code) return '—'
|
||||||
return CHANNEL_LABELS[code.toUpperCase()] ?? code
|
return CHANNEL_LABELS[code.toUpperCase()] ?? code
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 后端渠道编码 → 前端 `PayMethod`(用于支付结果页);无法识别时按微信兜底 */
|
|
||||||
export function toPayMethod(code?: string): PayMethod {
|
export function toPayMethod(code?: string): PayMethod {
|
||||||
return code?.toUpperCase() === 'ALIPAY' ? 'alipay' : 'wechat'
|
return code?.toUpperCase() === 'ALIPAY' ? 'alipay' : 'wechat'
|
||||||
}
|
}
|
||||||
@@ -51,16 +36,6 @@ function pad(value: number, length = 2): string {
|
|||||||
return String(value).padStart(length, '0')
|
return String(value).padStart(length, '0')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成商户订单号。
|
|
||||||
*
|
|
||||||
* <p>后端没有「生成订单号」的接口(Native / 支付宝都要求调用方传 `outTradeNo`),
|
|
||||||
* 这里按后端 `SequenceUtils.getDateId("PAY")` 的同款口径在前端生成:
|
|
||||||
* 形如 `PAY202609281530120731` = `PAY` + yyyyMMddHHmmss + 4 位随机。</p>
|
|
||||||
*
|
|
||||||
* <p>微信 Native 与支付宝的 `out_trade_no` 都要求 **6–32 位**、同一商户号下唯一,
|
|
||||||
* 19 位满足要求;`PAY` 前缀也与后端「PAY…」的既有流水号风格一致,便于对账。</p>
|
|
||||||
*/
|
|
||||||
export function createMerchantOrderNo(now: Date = new Date()): string {
|
export function createMerchantOrderNo(now: Date = new Date()): string {
|
||||||
const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`
|
const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`
|
||||||
+ `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
|
+ `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
|
||||||
|
|||||||
@@ -1,22 +1,5 @@
|
|||||||
import type { RdProjectStatus } from '@/types/business/project'
|
import type { RdProjectStatus } from '@/types/business/project'
|
||||||
|
|
||||||
/**
|
|
||||||
* 项目中心筛选栏的静态选项。
|
|
||||||
*
|
|
||||||
* <p>⚠️ 这里**只放后端当前没有下发的组**。业务类型的值域与件数必须走数据层
|
|
||||||
* (`stores/modules/project.ts#typeCounts`,来自后端 `requirementStatistics.businessTypeCounts`),
|
|
||||||
* 不要在这个文件里再定义一份,否则接口一改就错位。</p>
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 项目状态筛选项。
|
|
||||||
*
|
|
||||||
* <p>`value` 是后端列表状态的**码值**(`COALESCE(rd_project.status, project_requirement.status)`),
|
|
||||||
* 空串表示「全部」(不出现在 querystring 里)。顺序与设计稿的 2 列网格一致。</p>
|
|
||||||
*
|
|
||||||
* <p>列表改成「需求表为主 + LEFT JOIN 项目表」后,未立项的需求状态回落为 `INITIATING`
|
|
||||||
* ⇒ 设计稿画的「待立项」成了真实码值,故补进来(8 项 = 4 行 × 2 列,正好铺满)。</p>
|
|
||||||
*/
|
|
||||||
export const PROJECT_STATUS_OPTIONS: Array<{ label: string; value: RdProjectStatus | '' }> = [
|
export const PROJECT_STATUS_OPTIONS: Array<{ label: string; value: RdProjectStatus | '' }> = [
|
||||||
{ label: '全部', value: '' },
|
{ label: '全部', value: '' },
|
||||||
{ label: '待立项', value: 'INITIATING' },
|
{ label: '待立项', value: 'INITIATING' },
|
||||||
@@ -28,7 +11,6 @@ export const PROJECT_STATUS_OPTIONS: Array<{ label: string; value: RdProjectStat
|
|||||||
{ label: '已取消', value: 'CANCELLED' },
|
{ label: '已取消', value: 'CANCELLED' },
|
||||||
]
|
]
|
||||||
|
|
||||||
/** 码值 → 中文(表格状态列用;由上面那份选项派生,避免两处维护) */
|
|
||||||
export const PROJECT_STATUS_LABEL: Record<string, string> = PROJECT_STATUS_OPTIONS
|
export const PROJECT_STATUS_LABEL: Record<string, string> = PROJECT_STATUS_OPTIONS
|
||||||
.filter((option) => option.value !== '')
|
.filter((option) => option.value !== '')
|
||||||
.reduce<Record<string, string>>((acc, option) => {
|
.reduce<Record<string, string>>((acc, option) => {
|
||||||
@@ -36,7 +18,6 @@ export const PROJECT_STATUS_LABEL: Record<string, string> = PROJECT_STATUS_OPTIO
|
|||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
/** 提交时间快捷区间(后端未下发该组,前端换算成 beginTime/endTime 后下发) */
|
|
||||||
export const PROJECT_TIME_OPTIONS: Array<{
|
export const PROJECT_TIME_OPTIONS: Array<{
|
||||||
label: string
|
label: string
|
||||||
value: 'all' | '3m' | '6m' | '1y'
|
value: 'all' | '3m' | '6m' | '1y'
|
||||||
|
|||||||
@@ -18,44 +18,16 @@ import type {
|
|||||||
import { toBusinessType } from '@/utils/format'
|
import { toBusinessType } from '@/utils/format'
|
||||||
import { PROJECT_STATUS_LABEL } from '@/utils/projectMeta'
|
import { PROJECT_STATUS_LABEL } from '@/utils/projectMeta'
|
||||||
|
|
||||||
/**
|
|
||||||
* 需求详情(后端 **4 个页签接口**的聚合结果 `RequirementDetailBundle`)→ 详情页 `Project`。
|
|
||||||
*
|
|
||||||
* <p>为什么不直接改组件:详情页 6 个子组件(InfoCard/Team/Milestones/Files/Finance/
|
|
||||||
* AfterSales)都按 `Project` 这套字段写的,改数据来源不动组件契约,改动面最小。</p>
|
|
||||||
*
|
|
||||||
* <p><b>基础信息取哪一支都行</b>:4 个页签 VO 都继承 `ProjectRequirementBaseVo` 且同源
|
|
||||||
* (需求表同一行),这里统一取 `bundle.progress` 那一支当基础信息(UI 上它最先渲染)。</p>
|
|
||||||
*
|
|
||||||
* <p><b>未立项是常态</b>:需求在 `vm_rd_project` 里没有对应行时,后端返回的
|
|
||||||
* `projectStage` / `projectMilestones` / `members` / `bills` / `billPlans` / `invoices` / `contracts`
|
|
||||||
* 全为空 —— 这里一律兜底成空数组 / 0 / undefined,详情页显示为空态,不报错。</p>
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 状态 → 详情页中文。
|
|
||||||
*
|
|
||||||
* <p>⚠️ 后端 `selectUserProjectPage` / 详情侧已用 `sys_dict_data`(`vm_requirement_status`)
|
|
||||||
* 把状态翻成**中文**下发(如「进行中」),但码值(`INITIATING` 等)也可能出现
|
|
||||||
* (直接查子表、或字典缺项时)⇒ 两种口径都要认,认不出的一律兜底「待立项」(未立项最常见)。</p>
|
|
||||||
*/
|
|
||||||
const PROJECT_STATUS_LABELS: string[] = Object.values(PROJECT_STATUS_LABEL)
|
const PROJECT_STATUS_LABELS: string[] = Object.values(PROJECT_STATUS_LABEL)
|
||||||
|
|
||||||
function toProjectStatus(raw: string | null | undefined): ProjectStatus {
|
function toProjectStatus(raw: string | null | undefined): ProjectStatus {
|
||||||
const value = (raw ?? '').trim()
|
const value = (raw ?? '').trim()
|
||||||
if (!value) return '待立项'
|
if (!value) return '待立项'
|
||||||
// 先按码值查表;后端已是中文名时直接采信
|
|
||||||
const label = PROJECT_STATUS_LABEL[value]
|
const label = PROJECT_STATUS_LABEL[value]
|
||||||
?? (PROJECT_STATUS_LABELS.includes(value) ? value : undefined)
|
?? (PROJECT_STATUS_LABELS.includes(value) ? value : undefined)
|
||||||
return (label as ProjectStatus | undefined) ?? '待立项'
|
return (label as ProjectStatus | undefined) ?? '待立项'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 里程碑码值 → 三态。
|
|
||||||
*
|
|
||||||
* <p>详情页 `Milestone.status` 只有「待开始 / 进行中 / 已完成」三态(设计稿如此),
|
|
||||||
* 而后端 `status` 是自由文本 ⇒ 按关键词归类,认不出的一律「待开始」。</p>
|
|
||||||
*/
|
|
||||||
function toMilestoneStatus(raw: string | null | undefined): Milestone['status'] {
|
function toMilestoneStatus(raw: string | null | undefined): Milestone['status'] {
|
||||||
const value = (raw ?? '').toUpperCase()
|
const value = (raw ?? '').toUpperCase()
|
||||||
if (value.includes('COMPLET') || value.includes('DONE') || value.includes('FINISH')) return '已完成'
|
if (value.includes('COMPLET') || value.includes('DONE') || value.includes('FINISH')) return '已完成'
|
||||||
@@ -63,78 +35,42 @@ function toMilestoneStatus(raw: string | null | undefined): Milestone['status']
|
|||||||
return '待开始'
|
return '待开始'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 账单/付款计划状态 → 三态。
|
|
||||||
*
|
|
||||||
* <p>⚠️ 数据源是 `vm_bill_plan`,实体注释的值域只有 **ACTIVE / DONE / CLOSED**
|
|
||||||
* (不是 `vm_bill` 的 UNSTARTED/PENDING/PAID)。两者语义对照:
|
|
||||||
* 计划执行中 = 还有期次要付 ⇒ 「待支付」;计划结束 = 已付清 ⇒ 「已支付」。</p>
|
|
||||||
*
|
|
||||||
* <p><b>「未到期」在 `vm_bill_plan` 里没有对应码值</b>(已知契约缺口)——设计稿的第三张卡
|
|
||||||
* (第三期·尾款 → 未到期)目前靠 `UNSTARTED` 表达,所以这里显式认它,
|
|
||||||
* 而不是依赖「认不出就兜底未到期」的隐式行为;另兼容 `vm_bill` 的 PAID/PENDING 字面量。</p>
|
|
||||||
*/
|
|
||||||
function toBillStatus(raw: string | null | undefined): ProjectBill['status'] {
|
function toBillStatus(raw: string | null | undefined): ProjectBill['status'] {
|
||||||
const value = (raw ?? '').toUpperCase()
|
const value = (raw ?? '').toUpperCase()
|
||||||
// ⚠️ 关键词别用宽泛的「支付」:中文「**待**支付」也包含它,会先被这里截走误判成「已支付」。
|
|
||||||
if (value.includes('PAID') || value.includes('DONE') || value.includes('CLOSE')
|
if (value.includes('PAID') || value.includes('DONE') || value.includes('CLOSE')
|
||||||
|| value.includes('已支付')) return '已支付'
|
|| value.includes('已支付')) return '已支付'
|
||||||
if (value.includes('ACTIVE') || value.includes('WAIT') || value.includes('PENDING')
|
if (value.includes('ACTIVE') || value.includes('WAIT') || value.includes('PENDING')
|
||||||
|| value.includes('待支付')) return '待支付'
|
|| value.includes('待支付')) return '待支付'
|
||||||
if (value.includes('UNSTART') || value.includes('NOT_DUE') || value.includes('未到期')
|
if (value.includes('UNSTART') || value.includes('NOT_DUE') || value.includes('未到期')
|
||||||
|| value.includes('未开始')) return '未到期'
|
|| value.includes('未开始')) return '未到期'
|
||||||
// 认不出的一律「未到期」——不臆造成「已支付」(那是更坏的错)
|
|
||||||
return '未到期'
|
return '未到期'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 后端数值字段归一(BigDecimal 会以**字符串**下发,如 `"120000.00"`)。
|
|
||||||
*
|
|
||||||
* <p>直接当 number 用会崩:`formatMoney('120000.00')` → `value.toFixed is not a function`
|
|
||||||
* ⇒ 组件渲染失败、整个页签白屏(实测「合同财务」页签)。</p>
|
|
||||||
*/
|
|
||||||
function toNumber(value: number | string | null | undefined, fallback = 0): number {
|
function toNumber(value: number | string | null | undefined, fallback = 0): number {
|
||||||
const num = Number(value)
|
const num = Number(value)
|
||||||
return Number.isFinite(num) ? num : fallback
|
return Number.isFinite(num) ? num : fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* LocalDateTime 字符串 → 设计稿口径的「日」。
|
|
||||||
*
|
|
||||||
* <p>后端下发 `2026-09-03 00:00:00`(或 ISO `…T00:00:00`),两种都能截前 10 位;
|
|
||||||
* 空值返回 undefined,交给组件显示「—」。</p>
|
|
||||||
*/
|
|
||||||
function toDate(value: string | null | undefined): string | undefined {
|
function toDate(value: string | null | undefined): string | undefined {
|
||||||
return value ? value.slice(0, 10) : undefined
|
return value ? value.slice(0, 10) : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 期次占比 → 可展示文本。
|
|
||||||
*
|
|
||||||
* <p>后端 `period_ratio` / 计划 `periodRule` 都是「百分数」语义(30 表示 30%),但一个是
|
|
||||||
* BigDecimal 字符串(`"40.00"`)、一个是逗号串里的一段(`"40,30,30"` 的第 2 段 `"30"`)⇒
|
|
||||||
* 统一走 Number 归一。设计稿卡片标题写的是「比例33%」这种整数口径,非整数(按 3 期均分的
|
|
||||||
* 33.33)保留一位小数,避免出现 `比例33.333333%`。</p>
|
|
||||||
*/
|
|
||||||
function toRatioText(value: string | number | null | undefined): string {
|
function toRatioText(value: string | number | null | undefined): string {
|
||||||
const num = Number(value)
|
const num = Number(value)
|
||||||
if (!Number.isFinite(num) || num <= 0) return ''
|
if (!Number.isFinite(num) || num <= 0) return ''
|
||||||
return Number.isInteger(num) ? String(num) : String(Math.round(num * 10) / 10)
|
return Number.isInteger(num) ? String(num) : String(Math.round(num * 10) / 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 需求附件 → 详情页文件项(文件 tab 用) */
|
|
||||||
function attachmentToFile(item: RequirementAttachment): FileInfo {
|
function attachmentToFile(item: RequirementAttachment): FileInfo {
|
||||||
return {
|
return {
|
||||||
name: item.originalName ?? '',
|
name: item.originalName ?? '',
|
||||||
size: item.fileSize ?? 0,
|
size: item.fileSize ?? 0,
|
||||||
url: item.url ?? '',
|
url: item.url ?? '',
|
||||||
uploadTime: item.createTime ?? '',
|
uploadTime: item.createTime ?? '',
|
||||||
// 透传 ossId,供详情页「预览」走同源后端接口(绕开 OSS 桶强制下载)
|
|
||||||
ossId: item.ossId ?? undefined,
|
ossId: item.ossId ?? undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 发票 → 详情页文件项(财务 tab 的发票记录只是文件列表) */
|
|
||||||
function invoiceToFile(item: RequirementInvoice): FileInfo {
|
function invoiceToFile(item: RequirementInvoice): FileInfo {
|
||||||
return {
|
return {
|
||||||
name: item.invoiceTitle ?? item.invoiceNo ?? '发票',
|
name: item.invoiceTitle ?? item.invoiceNo ?? '发票',
|
||||||
@@ -144,80 +80,48 @@ function invoiceToFile(item: RequirementInvoice): FileInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 合同 → 详情页文件项(财务 tab 只展示一份合同文件,取第一条) */
|
|
||||||
function contractToFile(item: RequirementContract): FileInfo {
|
function contractToFile(item: RequirementContract): FileInfo {
|
||||||
return {
|
return {
|
||||||
name: item.title ?? item.contractNo ?? '合同文件',
|
name: item.title ?? item.contractNo ?? '合同文件',
|
||||||
size: 0,
|
size: 0,
|
||||||
url: item.file ?? '',
|
url: item.file ?? '',
|
||||||
// signDate 是 DATE 列,后端会下发成 '2026-08-20 00:00:00' ⇒ 只保留到日
|
|
||||||
uploadTime: toDate(item.signDate ?? item.createTime) ?? '',
|
uploadTime: toDate(item.signDate ?? item.createTime) ?? '',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 账单(`vm_bill`)→ 详情页「付款计划」卡片。
|
|
||||||
*
|
|
||||||
* <p><b>这是卡片的正解数据源。</b>设计稿卡片上的「账单号」与三态「已支付/待支付/未到期」
|
|
||||||
* 只有 `vm_bill` 才有:`billNo` 是真账单号、`amount` 是**本期**金额、
|
|
||||||
* `status` 是账单自己的 `PAID/PENDING/UNSTARTED`,而 `id` 正是收银台 `?ids=` 要传的
|
|
||||||
* **账单 id**,所以卡片上的「立即支付」可以直接跳过去。</p>
|
|
||||||
*
|
|
||||||
* <p>期次名直接用后端下发的 `periodName`(如「第2期·进度款」)——不再像过去那样
|
|
||||||
* 从付款计划拼「第N期/共M期」。</p>
|
|
||||||
*/
|
|
||||||
function toProjectBill(item: RequirementBill): ProjectBill {
|
function toProjectBill(item: RequirementBill): ProjectBill {
|
||||||
return {
|
return {
|
||||||
id: String(item.id),
|
id: String(item.id),
|
||||||
source: 'bill',
|
source: 'bill',
|
||||||
planName: item.periodName ?? '',
|
planName: item.periodName ?? '',
|
||||||
// 设计稿标题「第2期·进度款 比例30%」= 期次名 + 期次占比(后端 period_ratio)
|
|
||||||
periodRatio: toRatioText(item.periodRatio),
|
periodRatio: toRatioText(item.periodRatio),
|
||||||
billNo: item.billNo ?? '',
|
billNo: item.billNo ?? '',
|
||||||
amount: toNumber(item.amount),
|
amount: toNumber(item.amount),
|
||||||
status: toBillStatus(item.status),
|
status: toBillStatus(item.status),
|
||||||
// 后端存的是 transactionNo,页面叫 tradeNo(详见 types/business/finance.d.ts 的同一处映射)
|
|
||||||
tradeNo: item.transactionNo ?? undefined,
|
tradeNo: item.transactionNo ?? undefined,
|
||||||
projectName: item.projectName ?? '',
|
projectName: item.projectName ?? '',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 期次名,与后端 `BillServiceImpl#buildPeriodName` 同口径:首期预付款、末期尾款、中间进度款 */
|
|
||||||
function planPeriodLabel(period: number, periods: number): string {
|
function planPeriodLabel(period: number, periods: number): string {
|
||||||
if (period <= 1) return '预付款'
|
if (period <= 1) return '预付款'
|
||||||
if (period >= periods) return '尾款'
|
if (period >= periods) return '尾款'
|
||||||
return '进度款'
|
return '进度款'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 分期计划(`vm_bill_plan`)→ 卡片。**这是「合同财务」卡片的数据源。**
|
|
||||||
*
|
|
||||||
* <p>为什么是计划而不是账单:本栈的支付口径就是**计划 id** ——
|
|
||||||
* `vm_finance_payment.bill_id` 实测存的是 `vm_bill_plan.id`(该列 5 个取值在
|
|
||||||
* `vm_bill_plan` 全命中、在 `vm_bill` 只命中 2 个)。所以卡片「立即支付」传的也是计划 id,
|
|
||||||
* 下单落库、回调回写全都对得上,链路自洽。</p>
|
|
||||||
*
|
|
||||||
* <p>字段来源:期次名/比例/金额由 `currentPeriod` + `periodRule` 折算(与后端
|
|
||||||
* `BillServiceImpl#resolveRatio/resolveAmount` 同口径);「账单号」= 该期支付单的
|
|
||||||
* `out_trade_no`(后端别名 `orderNo`,未支付时为空 ⇒ 模板显示「—」);
|
|
||||||
* 「交易流水号」= `transactionNo`;状态优先看支付单(`payStatus=SUCCESS` 即已支付),
|
|
||||||
* 否则用计划状态(`DONE/ACTIVE/UNSTARTED`)。</p>
|
|
||||||
*/
|
|
||||||
function toPlanCard(item: RequirementBillPlan): ProjectBill {
|
function toPlanCard(item: RequirementBillPlan): ProjectBill {
|
||||||
const period = item.currentPeriod ?? 0
|
const period = item.currentPeriod ?? 0
|
||||||
const periods = item.periods ?? 0
|
const periods = item.periods ?? 0
|
||||||
const rule = (item.periodRule ?? '').split(',').map((part) => Number(part.trim()))
|
const rule = (item.periodRule ?? '').split(',').map((part) => Number(part.trim()))
|
||||||
const segment = rule[period - 1]
|
const segment = rule[period - 1]
|
||||||
const ratio = Number.isFinite(segment) ? segment : periods > 0 ? 100 / periods : 0
|
const ratio = Number.isFinite(segment) ? segment : periods > 0 ? 100 / periods : 0
|
||||||
// 支付成功以支付单为准:计划状态可能没被回调推到 DONE,但流水已经是 SUCCESS
|
|
||||||
const paid = (item.payStatus ?? '').toUpperCase() === 'SUCCESS'
|
const paid = (item.payStatus ?? '').toUpperCase() === 'SUCCESS'
|
||||||
return {
|
return {
|
||||||
id: String(item.id), // 计划 id —— 与收银台 ?ids= / vm_finance_payment.bill_id 同一口径
|
id: String(item.id),
|
||||||
source: 'plan',
|
source: 'plan',
|
||||||
planName: period > 0 && periods > 0
|
planName: period > 0 && periods > 0
|
||||||
? `第${period}期·${planPeriodLabel(period, periods)}`
|
? `第${period}期·${planPeriodLabel(period, periods)}`
|
||||||
: (item.periodRule ?? '付款计划'),
|
: (item.periodRule ?? '付款计划'),
|
||||||
// 比例口径与后端一致(见上方 ratio 折算),标题照样带「比例30%」
|
|
||||||
periodRatio: toRatioText(ratio),
|
periodRatio: toRatioText(ratio),
|
||||||
billNo: item.orderNo ?? '',
|
billNo: item.orderNo ?? '',
|
||||||
amount: Math.round(toNumber(item.totalAmount) * ratio) / 100,
|
amount: Math.round(toNumber(item.totalAmount) * ratio) / 100,
|
||||||
@@ -227,7 +131,6 @@ function toPlanCard(item: RequirementBillPlan): ProjectBill {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 项目成员 → 详情页团队成员卡 */
|
|
||||||
function toTeamMember(item: RequirementMember): TeamMember {
|
function toTeamMember(item: RequirementMember): TeamMember {
|
||||||
const name = item.userName ?? ''
|
const name = item.userName ?? ''
|
||||||
return {
|
return {
|
||||||
@@ -237,16 +140,6 @@ function toTeamMember(item: RequirementMember): TeamMember {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 售后工单。
|
|
||||||
*
|
|
||||||
* <p>详情页 `AfterSales` 只有「无 / 处理中」两态:撤回(WITHDRAWN)/ 作废 / 关闭 ⇒ 「无」
|
|
||||||
* (页面回到可再次申请的状态),其余(REQUESTED / IN_PROGRESS / COMPLETED)都算「处理中」。</p>
|
|
||||||
*
|
|
||||||
* <p>字段对应:`type` = 售后类型、`description` = 用户填的问题描述(后端 `reason`)、
|
|
||||||
* `remark` = 用户补充备注(后端 `remark`,不是处理人写的 `processNote`)、
|
|
||||||
* `attachments` = 售后工单附件(同表 `phase = AFTER_SALE`,由后端随页签下发)。</p>
|
|
||||||
*/
|
|
||||||
function toAfterSales(
|
function toAfterSales(
|
||||||
item: RequirementAfterSale,
|
item: RequirementAfterSale,
|
||||||
attachments: RequirementAttachment[] = [],
|
attachments: RequirementAttachment[] = [],
|
||||||
@@ -263,10 +156,8 @@ function toAfterSales(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 4 个页签接口的聚合结果 → 详情页数据模型(未立项时项目侧字段自动为空) */
|
|
||||||
export function toProjectDetail(bundle: RequirementDetailBundle): Project {
|
export function toProjectDetail(bundle: RequirementDetailBundle): Project {
|
||||||
const { progress, files, finance, afterSales } = bundle
|
const { progress, files, finance, afterSales } = bundle
|
||||||
// 基础信息 4 支同源(需求表同一行),统一取 progress 那一支
|
|
||||||
const base = progress
|
const base = progress
|
||||||
const { projectStage: stage } = progress
|
const { projectStage: stage } = progress
|
||||||
const contract = finance.contracts?.[0]
|
const contract = finance.contracts?.[0]
|
||||||
@@ -274,39 +165,24 @@ export function toProjectDetail(bundle: RequirementDetailBundle): Project {
|
|||||||
id: String(base.id),
|
id: String(base.id),
|
||||||
projectNo: base.projectNo ?? '',
|
projectNo: base.projectNo ?? '',
|
||||||
projectName: base.projectName ?? '',
|
projectName: base.projectName ?? '',
|
||||||
// 后端下发的是中文类型名(如「硬件电路」),归一化成前端 code;'0' 之类脏值归 other
|
|
||||||
businessType: toBusinessType(base.businessType),
|
businessType: toBusinessType(base.businessType),
|
||||||
// 同时保留原样名称:前端枚举只有 5 项,后台新增类型时会全落成「其他」,展示要用原名
|
|
||||||
businessTypeName: base.businessType ?? '',
|
businessTypeName: base.businessType ?? '',
|
||||||
// 总体进度百分比 —— 后端 `developmentProgress` 就是列表那个 progress(同源),
|
|
||||||
// 它缺失时才回落到阶段表的 development_progress;未立项两处都没有 ⇒ 0
|
|
||||||
progress: progress.developmentProgress ?? stage?.developmentProgress ?? 0,
|
progress: progress.developmentProgress ?? stage?.developmentProgress ?? 0,
|
||||||
status: toProjectStatus(base.status),
|
status: toProjectStatus(base.status),
|
||||||
submitTime: base.createTime ?? '',
|
submitTime: base.createTime ?? '',
|
||||||
startTime: toDate(stage?.startTime),
|
startTime: toDate(stage?.startTime),
|
||||||
endTime: toDate(stage?.finishTime ?? stage?.planTime),
|
endTime: toDate(stage?.finishTime ?? stage?.planTime),
|
||||||
// ⚠️ 后端里程碑/团队都是 `selectList(...eq(projectId))`,**没有 orderBy** ⇒ 返回顺序不保证
|
|
||||||
// (实测同一项目返回 id 5,8,6,7)。里程碑顺序会直接毁掉时间线语义,必须按 sortOrder 归位;
|
|
||||||
// 团队同理,项目负责人排最前。
|
|
||||||
milestones: [...(progress.projectMilestones ?? [])]
|
milestones: [...(progress.projectMilestones ?? [])]
|
||||||
.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))
|
.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
name: item.milestoneName ?? '',
|
name: item.milestoneName ?? '',
|
||||||
status: toMilestoneStatus(item.status),
|
status: toMilestoneStatus(item.status),
|
||||||
// 后端是 LocalDateTime('2026-09-03 00:00:00'),设计稿只到「日」
|
|
||||||
time: toDate(item.startTime),
|
time: toDate(item.startTime),
|
||||||
})),
|
})),
|
||||||
team: [...(progress.members ?? [])]
|
team: [...(progress.members ?? [])]
|
||||||
.sort((a, b) => Number(b.isOwner ?? false) - Number(a.isOwner ?? false))
|
.sort((a, b) => Number(b.isOwner ?? false) - Number(a.isOwner ?? false))
|
||||||
.map(toTeamMember),
|
.map(toTeamMember),
|
||||||
files: (files.requirementAttachments ?? []).map(attachmentToFile),
|
files: (files.requirementAttachments ?? []).map(attachmentToFile),
|
||||||
// ⚠️ 卡片优先 = 真账单(vm_bill)。判别位是 `bills` **这个 key 在不在**:
|
|
||||||
// 新构建无论有没有账单都会下发(无账单 = `[]` ⇒ 走空态,那才是「真的没有账单」);
|
|
||||||
// 旧构建(未重启)压根没这个 key ⇒ 回落`billPlans`(只读卡,`source='plan'`,不带支付按钮)。
|
|
||||||
// 教训:只认 `bills` 会让未重启的后端上整个页签「数据凭空消失」,所以必须留回落。
|
|
||||||
// 卡片主数据源 = 付款计划(`vm_bill_plan`):本栈的支付口径就是计划 id
|
|
||||||
// (`vm_finance_payment.bill_id` 实测存的就是它)。仅当计划为空时才回落 `bills`
|
|
||||||
// (`vm_bill`),不再像上一轮那样优先读 `bills`。
|
|
||||||
paymentPlans: (finance.billPlans ?? []).length > 0
|
paymentPlans: (finance.billPlans ?? []).length > 0
|
||||||
? (finance.billPlans ?? []).map(toPlanCard)
|
? (finance.billPlans ?? []).map(toPlanCard)
|
||||||
: (finance.bills ?? []).map(toProjectBill),
|
: (finance.bills ?? []).map(toProjectBill),
|
||||||
|
|||||||
@@ -3,21 +3,10 @@ import { ElMessage } from 'element-plus'
|
|||||||
import type { ApiResponse } from '@/types/api'
|
import type { ApiResponse } from '@/types/api'
|
||||||
import { storage } from '@/utils/storage'
|
import { storage } from '@/utils/storage'
|
||||||
|
|
||||||
// 后端 R.ok() 成功码为 200(org.dromara.common.core.constant.HttpStatus.SUCCESS)。
|
|
||||||
// 鉴权头 Authorization: Bearer <token> 已与后端 sa-token.token-prefix=Bearer 对齐,无需改动。
|
|
||||||
const SUCCESS_CODE = 200
|
const SUCCESS_CODE = 200
|
||||||
|
|
||||||
/**
|
|
||||||
* PC 端 clientId(sys_client 种子数据,grantType 含 password)。
|
|
||||||
*
|
|
||||||
* 后端 SecurityConfig 对每个已登录请求做多端隔离校验:
|
|
||||||
* header / query 里的 clientid 必须与登录时写入 token 的 clientid 一致,
|
|
||||||
* 否则抛 NotLoginException「客户端ID与Token不匹配」。
|
|
||||||
* 所以此值必须与登录、注册请求体的 clientId 完全同源。
|
|
||||||
*/
|
|
||||||
export const CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'
|
export const CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'
|
||||||
|
|
||||||
/** 全项目唯一 axios 实例 */
|
|
||||||
const service = axios.create({
|
const service = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
@@ -28,7 +17,6 @@ service.interceptors.request.use((config) => {
|
|||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
}
|
}
|
||||||
// 带 token 的请求必须回传 clientid,否则后端 SecurityConfig:97-106 校验不通过
|
|
||||||
config.headers.clientid = CLIENT_ID
|
config.headers.clientid = CLIENT_ID
|
||||||
return config
|
return config
|
||||||
})
|
})
|
||||||
@@ -38,13 +26,6 @@ service.interceptors.response.use(undefined, (error) => {
|
|||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询参数序列化(按 Spring MVC WebDataBinder 的绑定约定)。
|
|
||||||
*
|
|
||||||
* <p>axios 默认把嵌套对象序列化成 `scales[0][lowerLimit]`,而 Spring 只认点号路径
|
|
||||||
* `scales[0].lowerLimit`——实测用默认形式后端直接 HTTP 500,故需要序列化的接口
|
|
||||||
* 统一走本函数(`paramsSerializer: { serialize: serializeQuery }`)。</p>
|
|
||||||
*/
|
|
||||||
export function serializeQuery(params: Record<string, unknown>): string {
|
export function serializeQuery(params: Record<string, unknown>): string {
|
||||||
const parts: string[] = []
|
const parts: string[] = []
|
||||||
|
|
||||||
@@ -67,10 +48,6 @@ export function serializeQuery(params: Record<string, unknown>): string {
|
|||||||
return parts.join('&')
|
return parts.join('&')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 统一请求:自动解包 ApiResponse<T>,失败统一提示并 reject
|
|
||||||
* 页面/组件禁止直接 import axios,一律经 store 调用本函数
|
|
||||||
*/
|
|
||||||
export async function request<T>(config: AxiosRequestConfig): Promise<T> {
|
export async function request<T>(config: AxiosRequestConfig): Promise<T> {
|
||||||
const resp = await service.request<ApiResponse<T>>(config)
|
const resp = await service.request<ApiResponse<T>>(config)
|
||||||
const payload = resp.data
|
const payload = resp.data
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// localStorage/sessionStorage 泛型封装(无 Vue 依赖)
|
|
||||||
|
|
||||||
const PREFIX = 'cz:'
|
const PREFIX = 'cz:'
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
// 校验规则(无 Vue 依赖)
|
|
||||||
|
|
||||||
/** 手机号 */
|
|
||||||
export function isPhone(value: string): boolean {
|
export function isPhone(value: string): boolean {
|
||||||
return /^1[3-9]\d{9}$/.test(value)
|
return /^1[3-9]\d{9}$/.test(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 邮箱 */
|
|
||||||
export function isEmail(value: string): boolean {
|
export function isEmail(value: string): boolean {
|
||||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
|
||||||
}
|
}
|
||||||
@@ -17,10 +14,6 @@ export function isUrl(value: string): boolean {
|
|||||||
|
|
||||||
export type PasswordLevel = 'weak' | 'medium' | 'strong'
|
export type PasswordLevel = 'weak' | 'medium' | 'strong'
|
||||||
|
|
||||||
/**
|
|
||||||
* 密码强度等级(6-20 位)
|
|
||||||
* weak:仅字母或仅数字;medium:字母+数字;strong:字母+数字+特殊符号
|
|
||||||
*/
|
|
||||||
export function getPasswordLevel(password: string): PasswordLevel {
|
export function getPasswordLevel(password: string): PasswordLevel {
|
||||||
const hasLetter = /[a-zA-Z]/.test(password)
|
const hasLetter = /[a-zA-Z]/.test(password)
|
||||||
const hasDigit = /\d/.test(password)
|
const hasDigit = /\d/.test(password)
|
||||||
@@ -30,7 +23,6 @@ export function getPasswordLevel(password: string): PasswordLevel {
|
|||||||
return 'weak'
|
return 'weak'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 登录密码合法性(6-20 位) */
|
|
||||||
export function isValidPassword(password: string): boolean {
|
export function isValidPassword(password: string): boolean {
|
||||||
return password.length >= 6 && password.length <= 20
|
return password.length >= 6 && password.length <= 20
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,10 @@ const emit = defineEmits<{
|
|||||||
share: [id: string]
|
share: [id: string]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
/** 空值兜底(后端 customerType / industry 等可能不下发) */
|
|
||||||
function dash(value: string): string {
|
function dash(value: string): string {
|
||||||
return value || '--'
|
return value || '--'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 概览信息(设计稿:标签 + 全角冒号 + 值,单行铺开) */
|
|
||||||
const meta = computed(() => [
|
const meta = computed(() => [
|
||||||
{ label: '客户类型', value: dash(props.item.clientType) },
|
{ label: '客户类型', value: dash(props.item.clientType) },
|
||||||
{ label: '所属行业', value: dash(props.item.industry) },
|
{ label: '所属行业', value: dash(props.item.industry) },
|
||||||
@@ -126,7 +124,6 @@ const meta = computed(() => [
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__meta {
|
&__meta {
|
||||||
// 显式 stretch:否则元信息行会被居中收缩(设计稿为左起铺满整行)
|
|
||||||
.flex(column, flex-start, stretch);
|
.flex(column, flex-start, stretch);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
gap: @spacing-sm;
|
gap: @spacing-sm;
|
||||||
|
|||||||
@@ -9,22 +9,14 @@ interface Props {
|
|||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
/** 业务类型标签(设计稿:蓝色实心胶囊 + 白字) */
|
|
||||||
const tag = computed(() => formatBusinessType(props.item.category))
|
const tag = computed(() => formatBusinessType(props.item.category))
|
||||||
|
|
||||||
/**
|
|
||||||
* 封面图。
|
|
||||||
*
|
|
||||||
* <p>toCase 已把「非可访问地址」收口成空串,无封面时回退主题色底,
|
|
||||||
* 避免裂图或空白块。</p>
|
|
||||||
*/
|
|
||||||
const coverStyle = computed(() =>
|
const coverStyle = computed(() =>
|
||||||
props.item.cover ? { backgroundImage: `url(${props.item.cover})` } : undefined,
|
props.item.cover ? { backgroundImage: `url(${props.item.cover})` } : undefined,
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- 设计稿:封面通栏(不随内容容器收窄、无圆角),左下叠类型标签 + 案例名 -->
|
|
||||||
<section class="case-hero" :style="coverStyle">
|
<section class="case-hero" :style="coverStyle">
|
||||||
<div class="container case-hero__inner">
|
<div class="container case-hero__inner">
|
||||||
<span v-if="tag" class="case-hero__tag">{{ tag }}</span>
|
<span v-if="tag" class="case-hero__tag">{{ tag }}</span>
|
||||||
@@ -49,7 +41,6 @@ const coverStyle = computed(() =>
|
|||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
// 底部压暗,保证白字可读
|
|
||||||
background: linear-gradient(180deg, rgba(15, 23, 42, 0) 42%, rgba(15, 23, 42, 0.55));
|
background: linear-gradient(180deg, rgba(15, 23, 42, 0) 42%, rgba(15, 23, 42, 0.55));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +67,6 @@ const coverStyle = computed(() =>
|
|||||||
font-size: @font-size-xxl;
|
font-size: @font-size-xxl;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.35);
|
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.35);
|
||||||
// 最多两行,避免超长案例名撑破 hero
|
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
|
|||||||
@@ -7,13 +7,6 @@ interface Props {
|
|||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
/**
|
|
||||||
* 案例正文区块(设计稿:一段一张白卡,卡片标题带蓝色竖条)。
|
|
||||||
*
|
|
||||||
* <p>区块的**数量与标题都来自后端**(vm_project_case_catalog:title + content,
|
|
||||||
* 按 sort_order 升序),前端不再写死「项目背景 / 客户痛点 / 需求描述 / 交付物」,
|
|
||||||
* 后端增删目录即自动生效。空内容已在 utils/caseVo.ts#toSections 里过滤。</p>
|
|
||||||
*/
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -26,7 +19,6 @@ const props = defineProps<Props>()
|
|||||||
<h3 v-if="section.title" class="case-rich__title">
|
<h3 v-if="section.title" class="case-rich__title">
|
||||||
{{ section.title }}
|
{{ section.title }}
|
||||||
</h3>
|
</h3>
|
||||||
<!-- 后端下发富文本 HTML(可能内嵌 img / video),直接渲染 -->
|
|
||||||
<div class="case-rich__html" v-html="section.content" />
|
<div class="case-rich__html" v-html="section.content" />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +26,6 @@ const props = defineProps<Props>()
|
|||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.case-rich {
|
.case-rich {
|
||||||
// 同 detail.vue:显式 stretch,避免正文块被压成内容宽
|
|
||||||
.flex(column, flex-start, stretch);
|
.flex(column, flex-start, stretch);
|
||||||
gap: @spacing-base;
|
gap: @spacing-base;
|
||||||
|
|
||||||
@@ -70,8 +61,6 @@ const props = defineProps<Props>()
|
|||||||
|
|
||||||
:deep(p) {
|
:deep(p) {
|
||||||
margin-bottom: @spacing-sm;
|
margin-bottom: @spacing-sm;
|
||||||
// 后端富文本常把「1、…2、…」分条内容放在同一个 <p> 里用 \r\n 分行,
|
|
||||||
// 默认会被 HTML 折叠成一段,故保留换行(与旧纯文本分支的 pre-wrap 行为一致)
|
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,15 +7,10 @@ import type { BusinessIndustryOption, CaseQuery, CaseTypeOption, ScaleLimit } fr
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
query: CaseQuery
|
query: CaseQuery
|
||||||
/** 业务类型选项(接口下发:/categories 的统计;value=提报 id,label=类型名) */
|
|
||||||
typeOptions: CaseTypeOption[]
|
typeOptions: CaseTypeOption[]
|
||||||
/** 所属行业选项(接口下发:/categories) */
|
|
||||||
industryOptions: BusinessIndustryOption[]
|
industryOptions: BusinessIndustryOption[]
|
||||||
/** 项目规模选项(接口下发:/categories,label 已由后端拼好) */
|
|
||||||
scaleOptions: ScaleLimit[]
|
scaleOptions: ScaleLimit[]
|
||||||
/** 案例总数(全量口径,用于业务类型「全部」行角标) */
|
|
||||||
caseTotal: number
|
caseTotal: number
|
||||||
/** 待提交条件的结果数(「确定筛选(N 个结果)」) */
|
|
||||||
previewTotal: number
|
previewTotal: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +24,6 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
type TimeShortcut = 'all' | '3m' | '6m' | '1y' | 'custom'
|
type TimeShortcut = 'all' | '3m' | '6m' | '1y' | 'custom'
|
||||||
|
|
||||||
/** 完成时间后端未下发选项,保留前端快捷区间 */
|
|
||||||
const timeShortcuts: Array<{ label: string; value: Exclude<TimeShortcut, 'custom'>; months: number }> = [
|
const timeShortcuts: Array<{ label: string; value: Exclude<TimeShortcut, 'custom'>; months: number }> = [
|
||||||
{ label: '全部', value: 'all', months: 0 },
|
{ label: '全部', value: 'all', months: 0 },
|
||||||
{ label: '近3个月', value: '3m', months: 3 },
|
{ label: '近3个月', value: '3m', months: 3 },
|
||||||
@@ -37,12 +31,6 @@ const timeShortcuts: Array<{ label: string; value: Exclude<TimeShortcut, 'custom
|
|||||||
{ label: '近一年', value: '1y', months: 12 },
|
{ label: '近一年', value: '1y', months: 12 },
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
|
||||||
* 规模档位的稳定标识。
|
|
||||||
*
|
|
||||||
* <p>后端档位是「上下限」对象(label 只是展示文字),用上下限拼 key 比 label 可靠:
|
|
||||||
* 区间值唯一、且不依赖后端文案。</p>
|
|
||||||
*/
|
|
||||||
function scaleKey(item: ScaleLimit): string {
|
function scaleKey(item: ScaleLimit): string {
|
||||||
return `${item.lowerLimit ?? ''}-${item.upperLimit ?? ''}`
|
return `${item.lowerLimit ?? ''}-${item.upperLimit ?? ''}`
|
||||||
}
|
}
|
||||||
@@ -51,7 +39,6 @@ function scaleLabel(item: ScaleLimit): string {
|
|||||||
return item.label ?? `${item.lowerLimit ?? 0}-${item.upperLimit ?? '不限'}万`
|
return item.label ?? `${item.lowerLimit ?? 0}-${item.upperLimit ?? '不限'}万`
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 快捷区间 → 具体日期区间 */
|
|
||||||
function rangeOf(months: number): [string, string] {
|
function rangeOf(months: number): [string, string] {
|
||||||
const end = new Date()
|
const end = new Date()
|
||||||
const start = new Date()
|
const start = new Date()
|
||||||
@@ -59,7 +46,6 @@ function rangeOf(months: number): [string, string] {
|
|||||||
return [formatDate(start, 'YYYY-MM-DD'), formatDate(end, 'YYYY-MM-DD')]
|
return [formatDate(start, 'YYYY-MM-DD'), formatDate(end, 'YYYY-MM-DD')]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 当前区间命中的快捷项(用于选中态回显,避免额外维护一份状态) */
|
|
||||||
const activeShortcut = computed<TimeShortcut>(() => {
|
const activeShortcut = computed<TimeShortcut>(() => {
|
||||||
const range = props.query.dateRange
|
const range = props.query.dateRange
|
||||||
if (!range) return 'all'
|
if (!range) return 'all'
|
||||||
@@ -160,7 +146,6 @@ function clearChips(): void {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 业务类型(接口下发;带案例数) -->
|
|
||||||
<section class="case-filter__group">
|
<section class="case-filter__group">
|
||||||
<h3 class="case-filter__group-title">
|
<h3 class="case-filter__group-title">
|
||||||
业务类型
|
业务类型
|
||||||
@@ -191,7 +176,6 @@ function clearChips(): void {
|
|||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 所属行业(接口下发,多选) -->
|
|
||||||
<section class="case-filter__group">
|
<section class="case-filter__group">
|
||||||
<h3 class="case-filter__group-title">
|
<h3 class="case-filter__group-title">
|
||||||
所属行业
|
所属行业
|
||||||
@@ -216,7 +200,6 @@ function clearChips(): void {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 项目规模(接口下发,多选) -->
|
|
||||||
<section class="case-filter__group">
|
<section class="case-filter__group">
|
||||||
<h3 class="case-filter__group-title">
|
<h3 class="case-filter__group-title">
|
||||||
项目规模
|
项目规模
|
||||||
@@ -241,7 +224,6 @@ function clearChips(): void {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 完成时间(快捷区间 + 自定义;后端未下发该组选项) -->
|
|
||||||
<section class="case-filter__group">
|
<section class="case-filter__group">
|
||||||
<h3 class="case-filter__group-title">
|
<h3 class="case-filter__group-title">
|
||||||
完成时间
|
完成时间
|
||||||
@@ -271,7 +253,6 @@ function clearChips(): void {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 已选条件 -->
|
|
||||||
<section class="case-filter__selected">
|
<section class="case-filter__selected">
|
||||||
<div class="case-filter__selected-head">
|
<div class="case-filter__selected-head">
|
||||||
<span class="case-filter__selected-title">已选条件({{ chips.length }}个)</span>
|
<span class="case-filter__selected-title">已选条件({{ chips.length }}个)</span>
|
||||||
@@ -309,7 +290,6 @@ function clearChips(): void {
|
|||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.case-filter {
|
.case-filter {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
// 设计稿筛选面板 矩形 148286 @(70,272) 516×1461 白底
|
|
||||||
width: 516px;
|
width: 516px;
|
||||||
padding: 15px 0 40px;
|
padding: 15px 0 40px;
|
||||||
border-radius: @radius-base;
|
border-radius: @radius-base;
|
||||||
@@ -318,7 +298,6 @@ function clearChips(): void {
|
|||||||
&__head,
|
&__head,
|
||||||
&__group,
|
&__group,
|
||||||
&__actions {
|
&__actions {
|
||||||
// 设计稿内容左 x105 → 卡内 x35;行右 x536 → 卡内右 50
|
|
||||||
padding: 0 50px 0 35px;
|
padding: 0 50px 0 35px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,7 +306,6 @@ function clearChips(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
// 设计 @(105,287) 28/500 #222222
|
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 41px;
|
line-height: 41px;
|
||||||
@@ -347,20 +325,17 @@ function clearChips(): void {
|
|||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标题 → 第一组(设计 287+41=328 → 业务类型标题 339)
|
|
||||||
&__head + &__group {
|
&__head + &__group {
|
||||||
margin-top: 11px;
|
margin-top: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__group-title {
|
&__group-title {
|
||||||
// 设计 @(105,339) 24/500 #333333
|
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 35px;
|
line-height: 35px;
|
||||||
color: @cases-text;
|
color: @cases-text;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 业务类型:整行可点(设计行高 50,文案 20/400) */
|
|
||||||
&__rows {
|
&__rows {
|
||||||
margin-top: 19px;
|
margin-top: 19px;
|
||||||
}
|
}
|
||||||
@@ -419,7 +394,6 @@ function clearChips(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__row-count {
|
&__row-count {
|
||||||
// 设计计数 16/500 #666666
|
|
||||||
color: @cases-muted;
|
color: @cases-muted;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -430,7 +404,6 @@ function clearChips(): void {
|
|||||||
font-weight: inherit;
|
font-weight: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 胶囊选项(两列;设计胶囊高 53、行距 24、列距 33) */
|
|
||||||
&__pills {
|
&__pills {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@@ -506,7 +479,6 @@ function clearChips(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 已选条件 */
|
|
||||||
&__selected {
|
&__selected {
|
||||||
margin: 23px 50px 0 35px;
|
margin: 23px 50px 0 35px;
|
||||||
padding: 16px 24px 21px;
|
padding: 16px 24px 21px;
|
||||||
@@ -552,7 +524,6 @@ function clearChips(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 底部操作 */
|
|
||||||
&__actions {
|
&__actions {
|
||||||
.flex(row, flex-start, center);
|
.flex(row, flex-start, center);
|
||||||
gap: 13px;
|
gap: 13px;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import type { Case } from '@/types/business/cases'
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
cases: Case[]
|
cases: Case[]
|
||||||
/** 当前筛选条件下的案例总数 */
|
|
||||||
total: number
|
total: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,13 +40,11 @@ const emit = defineEmits<{
|
|||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.cases-grid {
|
.cases-grid {
|
||||||
&__total {
|
&__total {
|
||||||
// 设计计数 20px / 700 / #121212
|
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: @heading-color;
|
color: @heading-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设计卡片 396×267,列 x606/1030/1454 → 列距 28,行 y423/713 → 行距 23
|
|
||||||
&__list {
|
&__list {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { computed } from 'vue'
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
keyword: string
|
keyword: string
|
||||||
/** 是否只看收藏 */
|
|
||||||
favoriteOnly: boolean
|
favoriteOnly: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +59,6 @@ const keyword = computed<string>({
|
|||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.case-search {
|
.case-search {
|
||||||
.flex(row, flex-start, center);
|
.flex(row, flex-start, center);
|
||||||
// 设计稿搜索卡 矩形 148317 @(606,272) 1244×128;输入框 @(642,312) 592×49
|
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
height: 128px;
|
height: 128px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -70,7 +68,6 @@ const keyword = computed<string>({
|
|||||||
|
|
||||||
&__field {
|
&__field {
|
||||||
position: relative;
|
position: relative;
|
||||||
// 设计输入框 592 宽
|
|
||||||
flex: 0 0 592px;
|
flex: 0 0 592px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +97,6 @@ const keyword = computed<string>({
|
|||||||
transition: border-color 0.2s;
|
transition: border-color 0.2s;
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
// 设计 placeholder @(660,322) 20/400 #999999
|
|
||||||
color: @text-color-disabled;
|
color: @text-color-disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +106,6 @@ const keyword = computed<string>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__btn {
|
&__btn {
|
||||||
// 设计按钮文字 20/400 #FFFFFF,白底蓝字按钮宽 100
|
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
height: 49px;
|
height: 49px;
|
||||||
padding: 0 @spacing-base;
|
padding: 0 @spacing-base;
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import { computed, onMounted, watch } from 'vue'
|
import { computed, onMounted, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
// 页面容器:数据只经 store,不直接 import api
|
|
||||||
import { useCasesStore } from '@/stores/modules/cases'
|
import { useCasesStore } from '@/stores/modules/cases'
|
||||||
import CaseHero from './cpns/CaseHero.vue'
|
import CaseHero from './cpns/CaseHero.vue'
|
||||||
import CaseDetailHeader from './cpns/CaseDetailHeader.vue'
|
import CaseDetailHeader from './cpns/CaseDetailHeader.vue'
|
||||||
@@ -52,14 +51,12 @@ onMounted(load)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="item">
|
<template v-if="item">
|
||||||
<!-- 封面通栏:设计稿中不被内容容器收窄 -->
|
|
||||||
<CaseHero :item="item" />
|
<CaseHero :item="item" />
|
||||||
|
|
||||||
<div class="container case-detail__main">
|
<div class="container case-detail__main">
|
||||||
<CaseDetailHeader :item="item" @favorite="onFavorite" @share="onShare" />
|
<CaseDetailHeader :item="item" @favorite="onFavorite" @share="onShare" />
|
||||||
<CaseRichText :item="item" />
|
<CaseRichText :item="item" />
|
||||||
|
|
||||||
<!-- 设计稿:内容区整宽蓝色按钮 -->
|
|
||||||
<button
|
<button
|
||||||
class="case-detail__cta"
|
class="case-detail__cta"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -86,8 +83,6 @@ onMounted(load)
|
|||||||
font-size: @font-size-base;
|
font-size: @font-size-base;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⚠️ .flex 第三个参数是 align(默认 center),必须显式传 stretch,
|
|
||||||
// 否则各白块会被压成「内容宽 + 水平居中」(设计稿要求整宽)
|
|
||||||
&__main {
|
&__main {
|
||||||
.flex(column, flex-start, stretch);
|
.flex(column, flex-start, stretch);
|
||||||
gap: @spacing-base;
|
gap: @spacing-base;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { onMounted } from 'vue'
|
|||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { PaginationBar } from '@/components'
|
import { PaginationBar } from '@/components'
|
||||||
// 页面容器:只做组件引入、props 分发、事件中转,数据只经 store
|
|
||||||
import { useCasesStore } from '@/stores/modules/cases'
|
import { useCasesStore } from '@/stores/modules/cases'
|
||||||
import type { CaseQuery } from '@/types/business/cases'
|
import type { CaseQuery } from '@/types/business/cases'
|
||||||
import CasesFilter from './cpns/CasesFilter.vue'
|
import CasesFilter from './cpns/CasesFilter.vue'
|
||||||
@@ -49,7 +48,6 @@ function onViewDetail(id: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 筛选面板选项全部来自接口:业务类型 / 所属行业 / 项目规模三组都走 /categories,完成时间由前端算
|
|
||||||
void Promise.all([casesStore.fetchCategories(), casesStore.fetchCases()])
|
void Promise.all([casesStore.fetchCategories(), casesStore.fetchCases()])
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -112,43 +110,39 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.cases {
|
.cases {
|
||||||
// 设计实测:页脚顶 y1811,正文最底约 y1733 → 底距 78
|
|
||||||
padding-bottom: 78px;
|
padding-bottom: 78px;
|
||||||
|
|
||||||
// 设计稿页头条 矩形 148284 @(0,73) 1920×179 白底通栏;文案左缩进 112
|
|
||||||
&__hero {
|
&__hero {
|
||||||
height: 179px;
|
height: 179px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding-top: 32px; // 标题 @(112,105) → 73+32
|
padding-top: 32px;
|
||||||
background: @white;
|
background: @white;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 页头文案与内容区不对齐:设计实测文案在 x112(内容区 x70 + 42)
|
|
||||||
&__hero-inner {
|
&__hero-inner {
|
||||||
padding-left: 42px;
|
padding-left: 42px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 36px; // 设计 @(112,105) 36/700 #121212
|
font-size: 36px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 52px;
|
line-height: 52px;
|
||||||
color: @cases-hero-title;
|
color: @cases-hero-title;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__slogan {
|
&__slogan {
|
||||||
margin: 15px 0 0; // 副标题 @(112,172),标题底 105+52=157 → 间距 15
|
margin: 15px 0 0;
|
||||||
font-size: 24px; // 设计 24/500 #666666
|
font-size: 24px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 37px;
|
line-height: 37px;
|
||||||
color: @cases-muted;
|
color: @cases-muted;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设计稿:筛选面板 @(70,272) 516 宽,卡片区 @(606,423) 起 1244 宽,主区间距 20
|
|
||||||
&__body {
|
&__body {
|
||||||
.flex(row, flex-start, flex-start);
|
.flex(row, flex-start, flex-start);
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
padding-top: 20px; // 筛选面板顶 272 − 页头条底(73+179) = 20
|
padding-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__main {
|
&__main {
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="contract-info">
|
<div class="contract-info">
|
||||||
<!-- 区块 01:合同信息(相对卡顶 y36) -->
|
|
||||||
<h3 class="contract-info__section contract-info__section--1">
|
<h3 class="contract-info__section contract-info__section--1">
|
||||||
<span class="contract-info__step">01</span>
|
<span class="contract-info__step">01</span>
|
||||||
<span class="contract-info__section-title">合同信息</span>
|
<span class="contract-info__section-title">合同信息</span>
|
||||||
@@ -59,7 +58,6 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
|
|||||||
<StatusTag :label="contract.signStatus" :variant="SIGN_VARIANTS[contract.signStatus]" />
|
<StatusTag :label="contract.signStatus" :variant="SIGN_VARIANTS[contract.signStatus]" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 区块 02:关联项目(相对卡顶 y649) -->
|
|
||||||
<h3 class="contract-info__section contract-info__section--2">
|
<h3 class="contract-info__section contract-info__section--2">
|
||||||
<span class="contract-info__step">02</span>
|
<span class="contract-info__step">02</span>
|
||||||
<span class="contract-info__section-title">关联项目</span>
|
<span class="contract-info__section-title">关联项目</span>
|
||||||
@@ -76,7 +74,6 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
|
|||||||
<span class="contract-info__value">{{ contract.projectNo }}</span>
|
<span class="contract-info__value">{{ contract.projectNo }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 区块 03:合同文件(相对卡顶 y901) -->
|
|
||||||
<h3 class="contract-info__section contract-info__section--3">
|
<h3 class="contract-info__section contract-info__section--3">
|
||||||
<span class="contract-info__step">03</span>
|
<span class="contract-info__step">03</span>
|
||||||
<span class="contract-info__section-title">合同文件</span>
|
<span class="contract-info__section-title">合同文件</span>
|
||||||
@@ -110,26 +107,18 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
// 合同详情信息区(设计 e6c3a361,1920 画布 1:1 绝对定位)
|
|
||||||
// 白卡 (70,152) 1780×1152;卡内三行「步骤区」相对卡顶:
|
|
||||||
// 区块 01 合同信息 y36(步骤号 ink 188、区块名 211)、首标签 283、值框 326
|
|
||||||
// 区块 02 关联项目 y649(步骤号 801、区块名 824)、首标签 901、值框 944
|
|
||||||
// 区块 03 合同文件 y901(步骤号 1053、区块名 1076)、文件行 1153
|
|
||||||
// 值框 764×49 @x134/1010(卡内左 64 / 940),描边 #999999、圆角 8、底 #F4F6F8
|
|
||||||
// 行距 122(标签 283 → 405 → 527 → 649);列间距 112(776 → 940 = 164 − 764)
|
|
||||||
.contract-info {
|
.contract-info {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 1780px;
|
width: 1780px;
|
||||||
height: 1152px;
|
height: 1152px;
|
||||||
margin-top: 21px; // 面包屑盒底 131(72+32+27)→ 卡顶 152
|
margin-top: 21px;
|
||||||
border-radius: @radius-lg;
|
border-radius: @radius-lg;
|
||||||
background: @white;
|
background: @white;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
// 区块(步骤号 + 区块名)—— 绝对定位,四个区块
|
|
||||||
&__section {
|
&__section {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 43px; // 步骤号 x124 = 70 + 43(123)→ 区块行内相对 11 → 113 ✓
|
left: 43px;
|
||||||
top: 0;
|
top: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
|
|
||||||
@@ -140,7 +129,7 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
|
|||||||
|
|
||||||
&__section-title {
|
&__section-title {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 60px; // 区块名 ink x184 = 卡内 114 = 行 left 43 + 60
|
left: 60px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -148,8 +137,6 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
|
|||||||
color: @contract-cell-text;
|
color: @contract-cell-text;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 步骤号 01/02/03:48/500 #024FFF。absolute 定到区块名左侧 ——
|
|
||||||
// 区块行 left 43,区块名 ink 相对行左 60(= 184 − 124);步骤号 ink 落 0
|
|
||||||
&__step {
|
&__step {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -162,30 +149,24 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
|
|||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 字段(标签 + 值框)—— 绝对定位到卡面,行间距 122
|
|
||||||
&__field {
|
&__field {
|
||||||
.flex(column, flex-start, stretch);
|
.flex(column, flex-start, stretch);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 764px;
|
width: 764px;
|
||||||
gap: 14px; // 标签盒底 312 → 值框顶 326
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 值框顶(相对卡顶 = 设计 y − 152):326/448/570/692/944
|
|
||||||
&__field--1 { top: 174px; }
|
&__field--1 { top: 174px; }
|
||||||
&__field--2 { top: 296px; }
|
&__field--2 { top: 296px; }
|
||||||
&__field--3 { top: 418px; }
|
&__field--3 { top: 418px; }
|
||||||
&__field--4 { top: 540px; }
|
&__field--4 { top: 540px; }
|
||||||
&__field--5 { top: 770px; } // 标签 901 − 109(label 行盒 29 + gap 14 = 43 补偿)
|
&__field--5 { top: 770px; }
|
||||||
|
|
||||||
// 第 5 行(关联项目)标签需落设计 y901 → 卡内 749,而 --5 槽位顶 770 是给
|
|
||||||
// 「合同状态」这类值框直排的字段用的;带标签的行统一再抬 21
|
|
||||||
&__field--5:has(&__label) { top: 749px; }
|
&__field--5:has(&__label) { top: 749px; }
|
||||||
|
|
||||||
// 左右列:值框 x134 / x1010 → 卡内左 64 / 940
|
|
||||||
&__field--l { left: 64px; }
|
&__field--l { left: 64px; }
|
||||||
&__field--r { left: 940px; }
|
&__field--r { left: 940px; }
|
||||||
|
|
||||||
// 标签 20/500 #333
|
|
||||||
&__label {
|
&__label {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -193,7 +174,6 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
|
|||||||
color: @contract-cell-text;
|
color: @contract-cell-text;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 值框:764×49、#F4F6F8 底、1px #999999、圆角 8;文字内缩 18
|
|
||||||
&__value {
|
&__value {
|
||||||
.flex(row, flex-start, center);
|
.flex(row, flex-start, center);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -209,7 +189,6 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
|
|||||||
.ellipsis();
|
.ellipsis();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 合同状态(StatusTag)同样入值框,去彩色胶囊
|
|
||||||
&__field :deep(.status-tag) {
|
&__field :deep(.status-tag) {
|
||||||
.flex(row, flex-start, center);
|
.flex(row, flex-start, center);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -240,7 +219,6 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 合同文件:通栏 1640×90 @(124,1153),底 #EDEDED ──
|
|
||||||
&__files {
|
&__files {
|
||||||
.flex(column, flex-start, stretch);
|
.flex(column, flex-start, stretch);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -23,11 +23,10 @@ const emit = defineEmits<{ refresh: [] }>()
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
// 签署卡(设计 39:白底圆角卡、二维码直接置于卡面、右侧四步指引)
|
|
||||||
.sign-panel {
|
.sign-panel {
|
||||||
.flex(row, flex-start, flex-start);
|
.flex(row, flex-start, flex-start);
|
||||||
gap: 48px; // 二维码区 → 步骤区(设计 63 × 0.77)
|
gap: 48px;
|
||||||
padding: 28px 32px; // 卡内边距(设计 37/40 × 0.77)
|
padding: 28px 32px;
|
||||||
border-radius: @radius-lg;
|
border-radius: @radius-lg;
|
||||||
background: @white;
|
background: @white;
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
.scrollbar();
|
.scrollbar();
|
||||||
|
|
||||||
// 表格本体(设计 31ee32fc:1654×740,外框 1px #999、灰表头 64、行高 97、无竖线)
|
|
||||||
&__grid {
|
&__grid {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid @contract-line;
|
border: 1px solid @contract-line;
|
||||||
@@ -100,7 +99,6 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 长名称列按设计稿换行显示(设计首行双行文本)
|
|
||||||
td.contract-table__name {
|
td.contract-table__name {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
@@ -117,7 +115,7 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
|
|||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
height: 96px; // 设计行距 475→571
|
height: 96px;
|
||||||
color: @contract-cell-text;
|
color: @contract-cell-text;
|
||||||
line-height: 29px;
|
line-height: 29px;
|
||||||
}
|
}
|
||||||
@@ -130,7 +128,6 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
|
|||||||
background: @bg-color-light;
|
background: @bg-color-light;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 列宽按设计稿栅格比例微调(10 列合计 100%,日期/编号列留足 nowrap 宽度)
|
|
||||||
th:nth-child(1) { width: 11%; }
|
th:nth-child(1) { width: 11%; }
|
||||||
th:nth-child(2) { width: 9%; }
|
th:nth-child(2) { width: 9%; }
|
||||||
th:nth-child(3) { width: 10%; }
|
th:nth-child(3) { width: 10%; }
|
||||||
@@ -143,7 +140,6 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
|
|||||||
th:nth-child(10) { width: 11%; }
|
th:nth-child(10) { width: 11%; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 长名称列继承正文灰(设计 46 单元格统一 #333333)
|
|
||||||
&__name {
|
&__name {
|
||||||
color: @contract-cell-text;
|
color: @contract-cell-text;
|
||||||
}
|
}
|
||||||
@@ -177,7 +173,6 @@ const SIGN_VARIANTS: Record<SignStatus, 'primary' | 'success' | 'warning' | 'dan
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设计 46 状态列为纯文字,不使用彩色胶囊
|
|
||||||
:deep(.status-tag) {
|
:deep(.status-tag) {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
|||||||
@@ -36,44 +36,43 @@ const steps = [
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
// 四步签署指引(设计 39:小蓝圆序号 + 14/12 设计px 标题与说明,步距 63 设计px)
|
|
||||||
.sign-steps {
|
.sign-steps {
|
||||||
.flex(column, flex-start, stretch);
|
.flex(column, flex-start, stretch);
|
||||||
gap: 17px; // 步骤间距(设计 22 × 0.77)
|
gap: 17px;
|
||||||
|
|
||||||
&__item {
|
&__item {
|
||||||
.flex(row, flex-start, flex-start);
|
.flex(row, flex-start, flex-start);
|
||||||
gap: 11px; // 序号 → 文字(设计 14 × 0.77)
|
gap: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__index {
|
&__index {
|
||||||
.flex(row, center);
|
.flex(row, center);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 16px; // 序号圆(设计 21 × 0.77)
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
border-radius: @radius-round;
|
border-radius: @radius-round;
|
||||||
background: @primary-strong;
|
background: @primary-strong;
|
||||||
color: @white;
|
color: @white;
|
||||||
font-size: 11px; // 设计 12 → 可读下限
|
font-size: 11px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__body {
|
&__body {
|
||||||
.flex(column, flex-start, stretch);
|
.flex(column, flex-start, stretch);
|
||||||
gap: 5px; // 标题 → 说明(设计 6 × 0.77)
|
gap: 5px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
font-size: 11px; // 设计 14 → 可读下限
|
font-size: 11px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
color: @heading-color;
|
color: @heading-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__desc {
|
&__desc {
|
||||||
font-size: 11px; // 设计 12 → 可读下限
|
font-size: 11px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { AppButton } from '@/components'
|
import { AppButton } from '@/components'
|
||||||
import type { FileInfo } from '@/types/api'
|
import type { FileInfo } from '@/types/api'
|
||||||
// 页面容器:数据只经 store,不直接 import api
|
|
||||||
import { useContractStore } from '@/stores/modules/contract'
|
import { useContractStore } from '@/stores/modules/contract'
|
||||||
import ContractInfoCard from './cpns/ContractInfoCard.vue'
|
import ContractInfoCard from './cpns/ContractInfoCard.vue'
|
||||||
|
|
||||||
@@ -80,12 +79,10 @@ onMounted(load)
|
|||||||
.contract-detail {
|
.contract-detail {
|
||||||
padding-bottom: @spacing-xxl;
|
padding-bottom: @spacing-xxl;
|
||||||
|
|
||||||
// 内容列(面包屑 → 信息卡):卡自带 80 上距,这里不再叠加 gap
|
|
||||||
.container {
|
.container {
|
||||||
.flex(column, flex-start, stretch);
|
.flex(column, flex-start, stretch);
|
||||||
gap: 0;
|
gap: 0;
|
||||||
}
|
}
|
||||||
// 面包屑(设计 e6c3a361:(70,104) 24/500 #666666;顶栏 72 → 行盒顶 104)
|
|
||||||
&__breadcrumb {
|
&__breadcrumb {
|
||||||
padding-top: 32px;
|
padding-top: 32px;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
@@ -99,7 +96,6 @@ onMounted(load)
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
|
|
||||||
// 「返回」按钮
|
|
||||||
:deep(.app-button) {
|
:deep(.app-button) {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
padding: 0 32px;
|
padding: 0 32px;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { computed, onMounted } from 'vue'
|
|||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { FilterPanel, PaginationBar } from '@/components'
|
import { FilterPanel, PaginationBar } from '@/components'
|
||||||
// 页面容器:只做组件引入、props 分发、事件中转,数据只经 store
|
|
||||||
import { useContractStore } from '@/stores/modules/contract'
|
import { useContractStore } from '@/stores/modules/contract'
|
||||||
import ContractTable from './cpns/ContractTable.vue'
|
import ContractTable from './cpns/ContractTable.vue'
|
||||||
|
|
||||||
@@ -44,7 +43,6 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="contract">
|
<div class="contract">
|
||||||
<!-- 顶部横幅:白底通栏 + 标题/副标语(设计 41 横幅区、46 标题规格) -->
|
|
||||||
<div class="contract__head">
|
<div class="contract__head">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1 class="contract__title">
|
<h1 class="contract__title">
|
||||||
@@ -57,7 +55,6 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- 内容白卡:搜索行 + 表格 + 分页(设计 46/47 卡片) -->
|
|
||||||
<div class="contract__card">
|
<div class="contract__card">
|
||||||
<FilterPanel
|
<FilterPanel
|
||||||
v-model:keyword="keyword"
|
v-model:keyword="keyword"
|
||||||
@@ -87,13 +84,10 @@ onMounted(() => {
|
|||||||
.contract {
|
.contract {
|
||||||
padding-bottom: @spacing-xxl;
|
padding-bottom: @spacing-xxl;
|
||||||
|
|
||||||
// 横幅:设计 31ee32fc y72..251(1920×179)白底通栏
|
|
||||||
// 标题盒顶 96(ink 104)、副标语盒顶 161.5(ink 171)、横幅底 251
|
|
||||||
&__head {
|
&__head {
|
||||||
padding: 24px 0 52px;
|
padding: 24px 0 52px;
|
||||||
background: @white;
|
background: @white;
|
||||||
|
|
||||||
// 标题/副标语落 x112 → 内容区左缘 70 + 42
|
|
||||||
.container {
|
.container {
|
||||||
padding-left: 42px;
|
padding-left: 42px;
|
||||||
}
|
}
|
||||||
@@ -115,16 +109,14 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 内容白卡(设计 31ee32fc:x70 y271 1780×992 圆角白卡,无阴影)
|
|
||||||
.contract .contract__card {
|
.contract .contract__card {
|
||||||
margin-top: 20px; // 横幅底 251 → 卡顶 271
|
margin-top: 20px;
|
||||||
padding: 51px 64px 59px; // 搜索框 x134 = 70+64;表格 x135
|
padding: 51px 64px 59px;
|
||||||
border-radius: @radius-lg;
|
border-radius: @radius-lg;
|
||||||
background: @white;
|
background: @white;
|
||||||
|
|
||||||
// 搜索行并入卡片:去掉 FilterPanel 自带的卡面
|
|
||||||
:deep(.filter-panel) {
|
:deep(.filter-panel) {
|
||||||
margin-bottom: 39px; // 搜索框底 371 → 表头 411
|
margin-bottom: 39px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -138,8 +130,6 @@ onMounted(() => {
|
|||||||
:deep(.filter-panel__bar) {
|
:deep(.filter-panel__bar) {
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
// 设计:重置 x816、搜索 x940。搜索框右缘 726 →
|
|
||||||
// 重置 816(margin 90 − 12 padding = 78)、搜索 940(margin 52 − 12 = 40)
|
|
||||||
.filter-panel__btn {
|
.filter-panel__btn {
|
||||||
margin-left: 78px;
|
margin-left: 78px;
|
||||||
}
|
}
|
||||||
@@ -148,7 +138,6 @@ onMounted(() => {
|
|||||||
margin-left: 40px;
|
margin-left: 40px;
|
||||||
} }
|
} }
|
||||||
|
|
||||||
// 搜索框 134,322 592×49;占位 20/400 #999999
|
|
||||||
:deep(.filter-panel__input) {
|
:deep(.filter-panel__input) {
|
||||||
flex: 0 0 592px;
|
flex: 0 0 592px;
|
||||||
width: 592px;
|
width: 592px;
|
||||||
@@ -170,7 +159,6 @@ onMounted(() => {
|
|||||||
border-color: @primary-strong;
|
border-color: @primary-strong;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设计按钮:重置 x816、搜索 x940,均 20/400 纯文字(DOM 序相反,用 order 对齐)
|
|
||||||
:deep(.filter-panel__btn) {
|
:deep(.filter-panel__btn) {
|
||||||
order: 1;
|
order: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -200,7 +188,6 @@ onMounted(() => {
|
|||||||
color: @primary-color;
|
color: @primary-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分页(设计:右对齐,页码 ink 1180/1181,`<上一页` x1388、`下一页>` x1740)
|
|
||||||
:deep(.pagination) {
|
:deep(.pagination) {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -236,7 +223,6 @@ onMounted(() => {
|
|||||||
color: @contract-muted;
|
color: @contract-muted;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 首尾「上一页/下一页」:设计为无框纯文字
|
|
||||||
:deep(.pagination__btn:first-child),
|
:deep(.pagination__btn:first-child),
|
||||||
:deep(.pagination__btn:last-child) {
|
:deep(.pagination__btn:last-child) {
|
||||||
min-width: auto;
|
min-width: auto;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { storeToRefs } from 'pinia'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { AppButton } from '@/components'
|
import { AppButton } from '@/components'
|
||||||
// 页面容器:数据只经 store,不直接 import api
|
|
||||||
import { useContractStore } from '@/stores/modules/contract'
|
import { useContractStore } from '@/stores/modules/contract'
|
||||||
import ContractSignPanel from './cpns/ContractSignPanel.vue'
|
import ContractSignPanel from './cpns/ContractSignPanel.vue'
|
||||||
|
|
||||||
@@ -67,13 +66,11 @@ onMounted(load)
|
|||||||
padding-top: @spacing-lg;
|
padding-top: @spacing-lg;
|
||||||
padding-bottom: @spacing-xxl;
|
padding-bottom: @spacing-xxl;
|
||||||
|
|
||||||
// 内容列(面包屑 → 签署卡 → 返回):面包屑 → 卡片 12(设计 16 × 0.77)
|
|
||||||
.container {
|
.container {
|
||||||
.flex(column, flex-start, stretch);
|
.flex(column, flex-start, stretch);
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 面包屑(设计 39:12 设计px → 可读下限 11、#9CA3AF,当前项 #4B5563 加粗)
|
|
||||||
&__breadcrumb {
|
&__breadcrumb {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: @text-color-disabled;
|
color: @text-color-disabled;
|
||||||
@@ -87,17 +84,16 @@ onMounted(load)
|
|||||||
&__footer {
|
&__footer {
|
||||||
.flex(row, flex-end);
|
.flex(row, flex-end);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 19px; // 卡片底 → 按钮:设计 40 × 0.77(另计容器 12)
|
margin-top: 19px;
|
||||||
|
|
||||||
// 「返回」按钮(设计 39:#024FFF 实底、白字、小圆角)
|
|
||||||
:deep(.app-button) {
|
:deep(.app-button) {
|
||||||
height: 30px;
|
height: 30px;
|
||||||
padding: 0 24px; // 按钮宽约 98 设计 × 0.77
|
padding: 0 24px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: @radius-sm;
|
border-radius: @radius-sm;
|
||||||
background: @primary-strong;
|
background: @primary-strong;
|
||||||
color: @white;
|
color: @white;
|
||||||
font-size: 11px; // 设计 13 → 可读下限
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.app-button:hover) {
|
:deep(.app-button:hover) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user