恢复修改的数据
This commit is contained in:
@@ -1,60 +1,266 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { Demand } from '@/types/business/demand'
|
||||
import type { FileInfo } from '@/types/api'
|
||||
import type {
|
||||
BizTypeOption,
|
||||
Demand,
|
||||
DemandAttachment,
|
||||
DemandDraftVo,
|
||||
DictOption,
|
||||
} from '@/types/business/demand'
|
||||
import {
|
||||
getBizTypeDict,
|
||||
getBudgetRangeDict,
|
||||
getDraftDemand,
|
||||
getOssUrls,
|
||||
getSubmissionBizTypes,
|
||||
resetDraft as resetDraftApi,
|
||||
saveDraft as saveDraftApi,
|
||||
submitDemand,
|
||||
uploadDemandFile,
|
||||
} from '@/api/demand'
|
||||
import { getUserProfile } from '@/api/user'
|
||||
import { toAssetUrl } from '@/utils/asset'
|
||||
import { toBizTypeOption, toBizTypeOptionFromDict } from '@/utils/bizTypeMeta'
|
||||
import { isManualBudget, resolveDraftBudget } from '@/utils/budgetMeta'
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { getDraftDemand, saveDraft as saveDraftApi, submitDemand } from '@/api/demand'
|
||||
|
||||
function createDefaultForm(): Demand {
|
||||
return {
|
||||
businessType: 'internet',
|
||||
id: null,
|
||||
bizType: '',
|
||||
projectName: '',
|
||||
attachments: [],
|
||||
coreDescription: '',
|
||||
expectedDuration: '',
|
||||
requirementDesc: '',
|
||||
budgetRange: '',
|
||||
userName: '王永志',
|
||||
userPhone: '1866****9618',
|
||||
remark: '',
|
||||
status: 'draft',
|
||||
budgetManual: false,
|
||||
developmentCycle: '',
|
||||
supplementaryRemarks: '',
|
||||
userName: '',
|
||||
userPhone: '',
|
||||
attachmentInfo: [],
|
||||
}
|
||||
}
|
||||
|
||||
/** 需求提报数据(M3:表单 / 步骤 / 提交与草稿) */
|
||||
/** 字典按 dictSort 升序(后端已排序,前端再保一次序,避免脏序) */
|
||||
function sortDict(list: DictOption[] | null): DictOption[] {
|
||||
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 {
|
||||
const budget = resolveDraftBudget(draft.budgetRange, budgetDict)
|
||||
return {
|
||||
id: draft.id ?? null,
|
||||
bizType: draft.bizType || base.bizType,
|
||||
projectName: draft.projectName || base.projectName,
|
||||
requirementDesc: draft.requirementDesc || base.requirementDesc,
|
||||
budgetRange: budget.value,
|
||||
budgetManual: budget.manual,
|
||||
developmentCycle: draft.developmentCycle ?? base.developmentCycle,
|
||||
supplementaryRemarks: draft.supplementaryRemarks || base.supplementaryRemarks,
|
||||
userName: draft.userName || base.userName,
|
||||
userPhone: draft.userPhone || base.userPhone,
|
||||
attachmentInfo: draft.attachmentInfo ?? [],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量把 OSS 文件标识换成可访问地址。
|
||||
*
|
||||
* <p>提报列表的 `logoUrl` 实测是 OSS 主键(如 2099753406859292674)。其中本身已是地址的
|
||||
* 直接跳过;接口需要登录态,失败时返回空映射,由调用方回退前端内置图标(不阻塞页面)。</p>
|
||||
*/
|
||||
async function loadOssUrlMap(raws: (string | null)[]): Promise<Record<string, string>> {
|
||||
const ids = [...new Set(
|
||||
raws.map((item) => (item ?? '').trim()).filter((item) => item && !toAssetUrl(item)),
|
||||
)]
|
||||
if (ids.length === 0) return {}
|
||||
try {
|
||||
const list = await getOssUrls(ids)
|
||||
return Object.fromEntries(list.map((item) => [String(item.ossId), item.url]))
|
||||
}
|
||||
catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
/** 需求提报数据(单页三段表单 + 真实接口:提报列表 / 字典 / 草稿 / 上传 / 提交) */
|
||||
export const useDemandStore = defineStore('demand', () => {
|
||||
const form = ref<Demand>(createDefaultForm())
|
||||
const step = ref(1)
|
||||
/** 业务类型卡片选项(**完全由提报列表接口决定**:接口几条就几条,前端不追加任何兜底项) */
|
||||
const bizTypeOptions = ref<BizTypeOption[]>([])
|
||||
/** 预算范围字典原始项(值域来源:vm_budget_range) */
|
||||
const budgetDict = ref<DictOption[]>([])
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const submitting = ref(false)
|
||||
const uploading = ref(false)
|
||||
|
||||
function setStep(next: number): void {
|
||||
step.value = next
|
||||
/**
|
||||
* 预算范围选项:全部来自字典,只把「手填」项**固定排到最后**(设计稿要求末尾那一项是可输入的手填位)。
|
||||
*
|
||||
* <p>不动字典的值域,只调顺序。渲染方式由 `DemandInfoForm` 决定:非手填项渲染成胶囊按钮,
|
||||
* 手填项渲染成**同排第 5 个输入框**(占位文案即字典 label)。</p>
|
||||
*/
|
||||
const budgetOptions = computed<DictOption[]>(() => {
|
||||
const list = budgetDict.value
|
||||
return [
|
||||
...list.filter((item) => !isManualBudget(item.dictLabel)),
|
||||
...list.filter((item) => isManualBudget(item.dictLabel)),
|
||||
]
|
||||
})
|
||||
|
||||
/**
|
||||
* 业务类型选项:项目提报列表(免登录)→ logo 换链 → 映射卡片选项。
|
||||
*
|
||||
* <p>值域**完全由接口决定**:接口返回几条就渲染几张卡,前端不追加任何兜底项
|
||||
* (包括「其他」——接口有才显示,没有就不显示)。</p>
|
||||
*
|
||||
* <p>仅当提报接口**整体不可用**(请求异常)或一条可用记录都没有(全停用/全删除)时,
|
||||
* 才回退字典 `vm_biz_type`。这仍是接口驱动的降级路径(不是前端凭空造值),
|
||||
* 目的是避免出现"一个业务类型都选不了、表单无法提交"的死局。</p>
|
||||
*/
|
||||
async function loadBizTypeOptions(): Promise<BizTypeOption[]> {
|
||||
try {
|
||||
const submissions = await getSubmissionBizTypes()
|
||||
if (submissions.length > 0) {
|
||||
const ossUrlById = await loadOssUrlMap(submissions.map((item) => item.logoUrl))
|
||||
const options = submissions
|
||||
.map((item) => toBizTypeOption(item, ossUrlById))
|
||||
.filter((item): item is BizTypeOption => item !== null)
|
||||
if (options.length > 0) return options
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// 提报接口不可用时落到字典兜底,避免整页初始化失败
|
||||
}
|
||||
const dict = sortDict(await getBizTypeDict())
|
||||
return dict.map((item) => toBizTypeOptionFromDict(item.dictValue, item.dictLabel))
|
||||
}
|
||||
|
||||
function patchForm(partial: Partial<Demand>): void {
|
||||
form.value = { ...form.value, ...partial }
|
||||
}
|
||||
|
||||
/** File → FileInfo(mock:url 为空,uploadTime 取当前时间) */
|
||||
function addAttachment(file: File): void {
|
||||
const info: FileInfo = {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
uploadTime: formatDate(new Date()),
|
||||
url: '',
|
||||
/** 页面初始化:业务类型 + 预算字典 + 登录用户资料 + 已有草稿(并行) */
|
||||
async function init(): Promise<void> {
|
||||
loading.value = true
|
||||
try {
|
||||
const [bizTypes, budgetDictRes, profile, draft] = await Promise.all([
|
||||
loadBizTypeOptions(),
|
||||
getBudgetRangeDict(),
|
||||
// 用户资料失败不阻塞页面(联系人留空时后端会回退取登录用户资料)
|
||||
getUserProfile().catch(() => null),
|
||||
getDraftDemand(),
|
||||
])
|
||||
bizTypeOptions.value = bizTypes
|
||||
budgetDict.value = sortDict(budgetDictRes)
|
||||
|
||||
const base = createDefaultForm()
|
||||
if (profile) {
|
||||
base.userName = profile.nickName || profile.userName || ''
|
||||
base.userPhone = profile.phoneNumber || ''
|
||||
}
|
||||
form.value = draft ? mergeDraft(base, draft, budgetDict.value) : base
|
||||
}
|
||||
catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '需求提报数据加载失败')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
form.value.attachments = [...form.value.attachments, info]
|
||||
}
|
||||
|
||||
function removeAttachment(target: FileInfo): void {
|
||||
form.value.attachments = form.value.attachments.filter(
|
||||
(item) => !(item.name === target.name && item.uploadTime === target.uploadTime),
|
||||
/** 上传附件:先传 OSS,再把附件记录挂到表单(提交时后端按 ossId 转存) */
|
||||
async function addAttachment(file: File): Promise<void> {
|
||||
if (uploading.value) return
|
||||
uploading.value = true
|
||||
try {
|
||||
const oss = await uploadDemandFile(file)
|
||||
const suffix = (oss.fileSuffix || file.name.split('.').pop() || '').replace(/^\./, '')
|
||||
const item: DemandAttachment = {
|
||||
ossId: oss.ossId,
|
||||
originalName: oss.originalName || file.name,
|
||||
url: oss.url,
|
||||
fileSize: file.size,
|
||||
type: suffix,
|
||||
createTime: formatDate(new Date()),
|
||||
}
|
||||
form.value.attachmentInfo = [...form.value.attachmentInfo, item]
|
||||
ElMessage.success(`「${item.originalName}」上传成功`)
|
||||
}
|
||||
catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '附件上传失败')
|
||||
}
|
||||
finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 移除附件(仅从表单摘除,OSS 文件保留) */
|
||||
function removeAttachment(target: DemandAttachment): void {
|
||||
form.value.attachmentInfo = form.value.attachmentInfo.filter(
|
||||
(item) => !(item.ossId === target.ossId && item.id === target.id),
|
||||
)
|
||||
}
|
||||
|
||||
/** 提交需求:必填校验通过后调 api,成功返回 true(页面可据此跳转) */
|
||||
/** 保存草稿(后端按登录用户 upsert) */
|
||||
async function saveDraft(): Promise<void> {
|
||||
saving.value = true
|
||||
try {
|
||||
await saveDraftApi(form.value)
|
||||
ElMessage.success('草稿已保存')
|
||||
}
|
||||
catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '草稿保存失败')
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置:清空后端草稿并复位表单(保留登录账号的联系人预填) */
|
||||
async function reset(): Promise<void> {
|
||||
saving.value = true
|
||||
try {
|
||||
await resetDraftApi()
|
||||
const base = createDefaultForm()
|
||||
base.userName = form.value.userName
|
||||
base.userPhone = form.value.userPhone
|
||||
form.value = base
|
||||
ElMessage.success('已重置')
|
||||
}
|
||||
catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '重置失败')
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交需求:前端必填校验 → 提报接口(`/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> {
|
||||
if (!form.value.businessType) {
|
||||
if (!form.value.bizType) {
|
||||
ElMessage.warning('请选择业务类型')
|
||||
return false
|
||||
}
|
||||
@@ -62,15 +268,23 @@ export const useDemandStore = defineStore('demand', () => {
|
||||
ElMessage.warning('请填写项目名称')
|
||||
return false
|
||||
}
|
||||
if (!form.value.coreDescription.trim()) {
|
||||
if (!form.value.requirementDesc.trim()) {
|
||||
ElMessage.warning('请填写核心需求描述')
|
||||
return false
|
||||
}
|
||||
loading.value = true
|
||||
// 手写模式下金额会直接作为 budgetRange 提交,故空值必须拦住
|
||||
if (form.value.budgetManual && !form.value.budgetRange.trim()) {
|
||||
ElMessage.warning('请填写预算金额,或改选一个预设档位')
|
||||
return false
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await submitDemand(form.value)
|
||||
form.value.status = 'submitted'
|
||||
ElMessage.success('提交后我们的专业顾问将在一个工作日内与您联系')
|
||||
const base = createDefaultForm()
|
||||
base.userName = form.value.userName
|
||||
base.userPhone = form.value.userPhone
|
||||
form.value = base
|
||||
ElMessage.success('提交成功,专业顾问将在一个工作日内与您联系')
|
||||
return true
|
||||
}
|
||||
catch (error) {
|
||||
@@ -78,49 +292,24 @@ export const useDemandStore = defineStore('demand', () => {
|
||||
return false
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 保存草稿到 sessionStorage */
|
||||
async function saveDraft(): Promise<void> {
|
||||
loading.value = true
|
||||
try {
|
||||
await saveDraftApi(form.value)
|
||||
form.value.status = 'draft'
|
||||
ElMessage.success('草稿已保存')
|
||||
}
|
||||
catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '草稿保存失败')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 恢复本地草稿(无草稿则保持默认表单) */
|
||||
async function loadDraft(): Promise<void> {
|
||||
const draft = await getDraftDemand()
|
||||
if (draft) form.value = { ...createDefaultForm(), ...draft }
|
||||
}
|
||||
|
||||
/** 重置表单与步骤 */
|
||||
function reset(): void {
|
||||
form.value = createDefaultForm()
|
||||
step.value = 1
|
||||
}
|
||||
|
||||
return {
|
||||
form,
|
||||
step,
|
||||
bizTypeOptions,
|
||||
budgetOptions,
|
||||
loading,
|
||||
setStep,
|
||||
saving,
|
||||
submitting,
|
||||
uploading,
|
||||
init,
|
||||
patchForm,
|
||||
addAttachment,
|
||||
removeAttachment,
|
||||
submit,
|
||||
saveDraft,
|
||||
loadDraft,
|
||||
reset,
|
||||
submit,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user