feat(project): group assignment records by business type
Add four business tabs and identifying fields so users can locate orders before assigning projects. Introduce a permission-scoped detail query with child EXISTS searches and batched enrichment, preserving statistics and assignment writes. Approved Figma v2; independent source review completed. Tests authored but not executed under the user self-test arrangement. Related to issue #13.
This commit is contained in:
1 parent
2a7e763cdf
commit
29f9866a24
9 files changed
+611
-22
No files matched your search
@@ -21,6 +21,33 @@ export type ProjectOption = {
|
||||
disabled?: boolean
|
||||
}
|
||||
export type ProjectRecord = Record<string, any>
|
||||
export type AssignmentBizType = Exclude<BizType, 'presign'>
|
||||
export interface AssignmentRecord {
|
||||
id: string
|
||||
bizType: AssignmentBizType
|
||||
bizNo: string | null
|
||||
status: string
|
||||
projectId: string | null
|
||||
projectName: string | null
|
||||
teamId: string | null
|
||||
teamName: string | null
|
||||
createTime: string | null
|
||||
motherName?: string
|
||||
motherPhone?: string
|
||||
hospitalName?: string
|
||||
salesUserName?: string
|
||||
collectionDate?: string
|
||||
donationDate?: string
|
||||
applicationName?: string | null
|
||||
channelName?: string | null
|
||||
serviceOrderName?: string | null
|
||||
patientName?: string
|
||||
patientPhone?: string
|
||||
projectTitle?: string | null
|
||||
customerName?: string | null
|
||||
customerContact?: string | null
|
||||
customerPhone?: string | null
|
||||
}
|
||||
const base = '/web/admin/project-management'
|
||||
export const fetchProjectOptions = (params: {
|
||||
bizType?: BizType
|
||||
@@ -58,6 +85,11 @@ export const fetchProjectRecords = (params: Record<string, unknown>) =>
|
||||
url: `${base}/records`,
|
||||
params
|
||||
})
|
||||
export const fetchAssignmentRecords = (params: Record<string, unknown>) =>
|
||||
request.get<{ list: AssignmentRecord[]; total: number; statusCounts: Record<string, number> }>({
|
||||
url: `${base}/assignments/records`,
|
||||
params
|
||||
})
|
||||
export const previewAssignment = (data: ProjectRecord) =>
|
||||
request.post<{ records: ProjectRecord[] }>({ url: `${base}/assignments/preview`, data })
|
||||
export const saveAssignment = (data: ProjectRecord) =>
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div v-if="assignment" class="panel-pill-tabs panel-pill-tabs--lg">
|
||||
<div class="panel-pill-tabs__track" role="tablist" aria-label="业务类型">
|
||||
<button
|
||||
v-for="item in assignmentTabs"
|
||||
:key="item.key"
|
||||
type="button"
|
||||
role="tab"
|
||||
class="panel-pill-tabs__btn"
|
||||
:class="{ 'is-active': query.bizType === item.key }"
|
||||
:aria-selected="query.bizType === item.key"
|
||||
@click="switchBusiness(item.key)"
|
||||
>
|
||||
{{ item.title }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-block">
|
||||
<el-form class="filter-form" label-position="top" @submit.prevent="search"
|
||||
><div class="filter-grid">
|
||||
<el-form-item label="业务类型"
|
||||
><el-select v-model="query.bizType" @change="search"
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="query.keyword" :placeholder="keywordHint" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!assignment" label="业务类型"
|
||||
><el-select v-model="query.bizType" @change="resetBusinessFilters"
|
||||
><el-option
|
||||
v-for="(label, key) in bizLabels"
|
||||
:key="key"
|
||||
@@ -12,12 +31,20 @@
|
||||
:label="label" /></el-select
|
||||
></el-form-item>
|
||||
<el-form-item label="项目组"
|
||||
><ProjectSelect filter team v-model="query.teamId" :biz-type="query.bizType"
|
||||
><ProjectSelect
|
||||
:key="`team-${query.bizType}`"
|
||||
filter
|
||||
team
|
||||
v-model="query.teamId"
|
||||
:biz-type="query.bizType"
|
||||
/></el-form-item>
|
||||
<el-form-item label="项目"
|
||||
><ProjectSelect filter v-model="query.projectId" :biz-type="query.bizType"
|
||||
><ProjectSelect
|
||||
:key="`project-${query.bizType}`"
|
||||
filter
|
||||
v-model="query.projectId"
|
||||
:biz-type="query.bizType"
|
||||
/></el-form-item>
|
||||
<el-form-item label="关键词"><el-input v-model="query.keyword" clearable /></el-form-item>
|
||||
<el-form-item label="当前状态"
|
||||
><el-select v-model="query.status" clearable
|
||||
><el-option
|
||||
@@ -46,8 +73,20 @@
|
||||
></el-form
|
||||
>
|
||||
</div>
|
||||
<el-alert v-if="loadError" type="error" :closable="false" title="列表加载失败,请重试">
|
||||
<el-button link type="primary" @click="load">
|
||||
<el-icon><RefreshRight /></el-icon>重试
|
||||
</el-button>
|
||||
</el-alert>
|
||||
<div class="list-body list-wrap"
|
||||
><el-table :data="rows" v-loading="loading" class="list-table" stripe>
|
||||
><el-table
|
||||
:key="query.bizType"
|
||||
:data="rows"
|
||||
v-loading="loading"
|
||||
class="list-table"
|
||||
stripe
|
||||
:empty-text="loading ? '正在加载…' : loadError ? '数据加载失败' : '暂无符合条件的单据'"
|
||||
>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
@@ -56,18 +95,29 @@
|
||||
header-align="center"
|
||||
:index="(index) => (query.page - 1) * query.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column prop="bizNo" label="单据编号" min-width="180" />
|
||||
<el-table-column
|
||||
v-for="column in visibleColumns"
|
||||
:key="column.prop"
|
||||
:prop="column.prop"
|
||||
:label="column.label"
|
||||
:min-width="column.minWidth"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ formatDisplayOrDash(row[column.prop]) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="110"
|
||||
><template #default="{ row }"
|
||||
><el-tag :type="statusTagType(row.status)" size="small">{{
|
||||
statusLabel(row.status)
|
||||
}}</el-tag></template
|
||||
></el-table-column
|
||||
>
|
||||
<el-table-column prop="teamName" label="项目组" min-width="140" />
|
||||
<el-table-column prop="projectName" label="项目" min-width="160"
|
||||
<el-table-column prop="projectName" label="所属项目" min-width="160"
|
||||
><template #default="{ row }">{{
|
||||
row.projectName || '未归属'
|
||||
}}</template></el-table-column
|
||||
>
|
||||
<el-table-column label="状态" min-width="110"
|
||||
><template #default="{ row }"
|
||||
><el-tag type="info">{{ statusLabel(row.status) }}</el-tag></template
|
||||
></el-table-column
|
||||
>
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="180">
|
||||
<template #default="{ row }">{{ formatDisplayOrDash(row.createTime) }}</template>
|
||||
</el-table-column>
|
||||
@@ -83,11 +133,12 @@
|
||||
> </el-table
|
||||
><div class="pagination list-pager-bar"
|
||||
><el-pagination
|
||||
v-model:current-page="query.page"
|
||||
:current-page="query.page"
|
||||
:page-size="query.pageSize"
|
||||
:disabled="loading"
|
||||
:total="total"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="load" /></div
|
||||
@current-change="changePage" /></div
|
||||
></div>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
@@ -102,7 +153,8 @@
|
||||
><el-form-item label="目标项目" required
|
||||
><ProjectSelect
|
||||
v-model="target"
|
||||
:biz-type="query.bizType"
|
||||
:key="selected.id"
|
||||
:biz-type="selected.bizType"
|
||||
@update:model-value="previewRows = []" /></el-form-item
|
||||
><el-form-item label="调整原因" required
|
||||
><el-input v-model="reason" type="textarea" :maxlength="500" /></el-form-item
|
||||
@@ -175,18 +227,25 @@
|
||||
protocolStatusLabel,
|
||||
donationStatusLabel,
|
||||
presignOrderStatusLabel,
|
||||
applicationStatusLabel
|
||||
applicationStatusLabel,
|
||||
protocolStatusTagType,
|
||||
donationStatusTagType,
|
||||
presignOrderStatusTagType,
|
||||
applicationStatusTagType
|
||||
} from '@/utils/status-tag-registry'
|
||||
import { Search, Edit, Clock, Close, View, Check } from '@element-plus/icons-vue'
|
||||
import { Search, Edit, Clock, Close, View, Check, RefreshRight } from '@element-plus/icons-vue'
|
||||
import { assignmentTabs, assignmentColumns, assignmentKeywordHints } from './assignment-columns'
|
||||
import ProjectSelect from '@/components/business/ProjectSelect.vue'
|
||||
import { assignmentSnapshot } from '@/utils/project-management'
|
||||
import {
|
||||
bizLabels,
|
||||
fetchProjectRecords,
|
||||
fetchAssignmentRecords,
|
||||
previewAssignment,
|
||||
saveAssignment,
|
||||
fetchAssignmentHistory,
|
||||
type BizType,
|
||||
type AssignmentBizType,
|
||||
type ProjectRecord
|
||||
} from '@/api/project-management'
|
||||
const props = defineProps<{ assignment?: boolean; filters?: Record<string, any> }>()
|
||||
@@ -205,6 +264,7 @@
|
||||
total = ref(0),
|
||||
statusCounts = ref<Record<string, number>>({}),
|
||||
loading = ref(false),
|
||||
loadError = ref(false),
|
||||
visible = ref(false),
|
||||
busy = ref(false),
|
||||
target = ref(''),
|
||||
@@ -213,6 +273,15 @@
|
||||
previewRows = ref<ProjectRecord[]>([]),
|
||||
historyVisible = ref(false),
|
||||
historyRows = ref<ProjectRecord[]>([])
|
||||
const visibleColumns = computed(() =>
|
||||
props.assignment
|
||||
? assignmentColumns[query.bizType as AssignmentBizType]
|
||||
: [{ prop: 'bizNo', label: '单据编号', minWidth: 180 }]
|
||||
)
|
||||
const keywordHint = computed(() =>
|
||||
props.assignment ? assignmentKeywordHints[query.bizType as AssignmentBizType] : '请输入单据编号'
|
||||
)
|
||||
let loadVersion = 0
|
||||
function statusLabel(status: string) {
|
||||
return query.bizType === 'presign'
|
||||
? presignOrderStatusLabel(status)
|
||||
@@ -222,27 +291,69 @@
|
||||
? donationStatusLabel(status)
|
||||
: applicationStatusLabel(status)
|
||||
}
|
||||
function statusTagType(status: string) {
|
||||
return query.bizType === 'presign'
|
||||
? presignOrderStatusTagType(status)
|
||||
: query.bizType === 'cell_storage'
|
||||
? protocolStatusTagType(status)
|
||||
: query.bizType === 'research_donation'
|
||||
? donationStatusTagType(status)
|
||||
: applicationStatusTagType(status)
|
||||
}
|
||||
async function load() {
|
||||
const version = ++loadVersion
|
||||
loading.value = true
|
||||
loadError.value = false
|
||||
rows.value = []
|
||||
try {
|
||||
const result = await fetchProjectRecords({
|
||||
const fetchRecords = props.assignment ? fetchAssignmentRecords : fetchProjectRecords
|
||||
const result = await fetchRecords({
|
||||
...props.filters,
|
||||
...query,
|
||||
teamId: query.teamId || undefined,
|
||||
startDate: dateRange.value?.[0],
|
||||
endDate: dateRange.value?.[1]
|
||||
})
|
||||
if (version !== loadVersion) return
|
||||
rows.value = result.list
|
||||
total.value = result.total
|
||||
statusCounts.value = result.statusCounts || {}
|
||||
} catch {
|
||||
if (version === loadVersion) {
|
||||
loadError.value = true
|
||||
total.value = 0
|
||||
statusCounts.value = {}
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (version === loadVersion) loading.value = false
|
||||
}
|
||||
}
|
||||
function resetBusinessFilters() {
|
||||
query.page = 1
|
||||
total.value = 0
|
||||
query.status = ''
|
||||
query.teamId = ''
|
||||
query.projectId = ''
|
||||
query.keyword = ''
|
||||
statusCounts.value = {}
|
||||
visible.value = false
|
||||
historyVisible.value = false
|
||||
search()
|
||||
}
|
||||
function switchBusiness(kind: AssignmentBizType) {
|
||||
if (query.bizType === kind) return
|
||||
query.bizType = kind
|
||||
resetBusinessFilters()
|
||||
}
|
||||
function search() {
|
||||
query.page = 1
|
||||
load()
|
||||
}
|
||||
function changePage(page: number) {
|
||||
if (loading.value || loadError.value || page === query.page) return
|
||||
query.page = page
|
||||
load()
|
||||
}
|
||||
function open(row: ProjectRecord) {
|
||||
selected.value = row
|
||||
target.value = ''
|
||||
@@ -257,13 +368,16 @@
|
||||
}
|
||||
busy.value = true
|
||||
const previewTarget = target.value
|
||||
const previewRecord = selected.value
|
||||
try {
|
||||
const result = await previewAssignment({
|
||||
bizType: selected.value.bizType,
|
||||
id: selected.value.id,
|
||||
projectId: target.value
|
||||
})
|
||||
previewRows.value = target.value === previewTarget ? result.records : []
|
||||
if (selected.value === previewRecord && target.value === previewTarget && visible.value) {
|
||||
previewRows.value = result.records
|
||||
}
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
@@ -288,7 +402,9 @@
|
||||
}
|
||||
}
|
||||
async function history(row: ProjectRecord) {
|
||||
const version = loadVersion
|
||||
const result = await fetchAssignmentHistory({ bizType: row.bizType, id: row.id })
|
||||
if (version !== loadVersion) return
|
||||
historyRows.value = result.list
|
||||
historyVisible.value = true
|
||||
}
|
||||
@@ -301,11 +417,19 @@
|
||||
: []
|
||||
Object.assign(
|
||||
query,
|
||||
{ bizType: 'cell_storage', projectId: '', teamId: '', unassigned: false },
|
||||
{
|
||||
bizType: 'cell_storage',
|
||||
projectId: '',
|
||||
teamId: '',
|
||||
keyword: '',
|
||||
status: '',
|
||||
unassigned: false
|
||||
},
|
||||
props.filters || {}
|
||||
)
|
||||
search()
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
onBeforeUnmount(() => loadVersion++)
|
||||
</script>
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { AssignmentBizType, AssignmentRecord } from '@/api/project-management'
|
||||
|
||||
interface AssignmentColumn {
|
||||
prop: keyof AssignmentRecord
|
||||
label: string
|
||||
minWidth: number
|
||||
}
|
||||
|
||||
export const assignmentTabs: { key: AssignmentBizType; title: string }[] = [
|
||||
{ key: 'cell_storage', title: '细胞存储' },
|
||||
{ key: 'research_donation', title: '科研捐赠' },
|
||||
{ key: 'pilot_service', title: '中试服务' },
|
||||
{ key: 'basic_experiment', title: '基础实验' }
|
||||
]
|
||||
|
||||
const collectionColumns: AssignmentColumn[] = [
|
||||
{ prop: 'motherName', label: '母亲姓名', minWidth: 120 },
|
||||
{ prop: 'motherPhone', label: '母亲电话', minWidth: 150 },
|
||||
{ prop: 'hospitalName', label: '采集医院', minWidth: 180 },
|
||||
{ prop: 'salesUserName', label: '市场专员', minWidth: 120 }
|
||||
]
|
||||
|
||||
export const assignmentColumns: Record<AssignmentBizType, AssignmentColumn[]> = {
|
||||
cell_storage: [
|
||||
{ prop: 'bizNo', label: '协议单号', minWidth: 180 },
|
||||
...collectionColumns,
|
||||
{ prop: 'collectionDate', label: '采集日期', minWidth: 140 }
|
||||
],
|
||||
research_donation: [
|
||||
{ prop: 'bizNo', label: '捐赠单号', minWidth: 180 },
|
||||
...collectionColumns,
|
||||
{ prop: 'donationDate', label: '捐赠日期', minWidth: 140 }
|
||||
],
|
||||
pilot_service: [
|
||||
{ prop: 'bizNo', label: '申请单号', minWidth: 180 },
|
||||
{ prop: 'applicationName', label: '申请名称', minWidth: 180 },
|
||||
{ prop: 'channelName', label: '渠道名称', minWidth: 160 },
|
||||
{ prop: 'serviceOrderName', label: '服务订单名称', minWidth: 180 },
|
||||
{ prop: 'patientName', label: '患者姓名', minWidth: 120 },
|
||||
{ prop: 'patientPhone', label: '患者电话', minWidth: 150 }
|
||||
],
|
||||
basic_experiment: [
|
||||
{ prop: 'bizNo', label: '申请单号', minWidth: 180 },
|
||||
{ prop: 'projectTitle', label: '课题名称', minWidth: 200 },
|
||||
{ prop: 'customerName', label: '客户名称', minWidth: 180 },
|
||||
{ prop: 'customerContact', label: '客户联系人', minWidth: 140 },
|
||||
{ prop: 'customerPhone', label: '联系电话', minWidth: 150 }
|
||||
]
|
||||
}
|
||||
|
||||
export const assignmentKeywordHints: Record<AssignmentBizType, string> = {
|
||||
cell_storage: '协议单号 / 母亲姓名、电话 / 医院 / 市场专员',
|
||||
research_donation: '捐赠单号 / 母亲姓名、电话 / 医院 / 市场专员',
|
||||
pilot_service: '申请单号、名称 / 渠道 / 服务订单 / 患者姓名、电话',
|
||||
basic_experiment: '申请单号 / 课题 / 客户名称、联系人、电话'
|
||||
}
|
||||
Reference in new issue
Block a user