feat(csmanualentry): 手动录单支持收号出号分组

- 录单记录新增组别(收号/出号),唯一约束放宽为编号+组别,同一编号两组各可登记一次
- 操作人选项支持组别标记,录单选中后自动带出组别
- 列表新增组别列与筛选,今日统计按收号/出号分列
This commit is contained in:
yml2213
2026-11-06 14:02:28 +08:00
parent 9d55b4510c
commit e027f88fbb
11 changed files with 431 additions and 74 deletions
@@ -5,9 +5,13 @@ import type { ApiResponse, PaginatedResult } from '@/shared/types/types'
/** 录单可选项类型:平台 / 操作人 */
export type ManualEntryOptionType = 'platform' | 'operator'
/** 录单组别:收号 / 出号 */
export type ManualEntryGroup = 'collect' | 'sell'
export interface ManualEntryItem {
id: number
entry_no: string
entry_group: ManualEntryGroup
platform: string
operator: string
admin_id: number
@@ -21,6 +25,8 @@ export interface ManualEntryOption {
id: number
type: ManualEntryOptionType
name: string
/** 操作人组别标记:collect=收号, sell=出号, ''=未分组 */
group_tag: ManualEntryGroup | ''
sort_order: number
created_at: string
}
@@ -34,10 +40,13 @@ export interface ManualEntryAgentStats {
admin_id: number
admin_name: string
count: number
collect_count: number
sell_count: number
}
export interface CreateManualEntryRequest {
entry_no: string
entry_group: ManualEntryGroup
platform?: string
operator?: string
remark?: string
@@ -45,6 +54,7 @@ export interface CreateManualEntryRequest {
export interface UpdateManualEntryRequest {
entry_no: string
entry_group: ManualEntryGroup
platform?: string
operator?: string
remark?: string
@@ -52,6 +62,7 @@ export interface UpdateManualEntryRequest {
export interface ManualEntryListQuery {
entry_no?: string
entry_group?: ManualEntryGroup
keyword?: string
admin_id?: number
start_date?: string
@@ -113,6 +124,7 @@ export async function fetchManualEntryOptions() {
export async function createManualEntryOption(payload: {
type: ManualEntryOptionType
name: string
group_tag?: ManualEntryGroup | ''
}) {
const { data } = await apiClient.post<ApiResponse<ManualEntryOption>>(
'/admin/manual-entries/options',
@@ -121,10 +133,13 @@ export async function createManualEntryOption(payload: {
return data.data
}
export async function updateManualEntryOption(id: number, name: string) {
export async function updateManualEntryOption(
id: number,
payload: { name: string; group_tag?: ManualEntryGroup | '' }
) {
const { data } = await apiClient.put<ApiResponse<ManualEntryOption>>(
`/admin/manual-entries/options/${id}`,
{ name }
payload
)
return data.data
}
@@ -70,6 +70,10 @@
width: 200px;
}
.filter-group {
width: 120px;
}
.filter-keyword {
width: 320px;
}
@@ -119,6 +123,15 @@
gap: 8px;
}
.option-config-add :deep(.option-config-group-select) {
width: 96px;
flex-shrink: 0;
}
.option-config-group-select {
width: 96px;
}
.option-config-list {
display: flex;
flex-direction: column;
@@ -11,6 +11,7 @@ import {
fetchManualEntryOptions,
updateManualEntry,
updateManualEntryOption,
type ManualEntryGroup,
type ManualEntryItem,
type ManualEntryOption,
type ManualEntryOptionType,
@@ -24,6 +25,30 @@ import AdminTablePagination from '../components/AdminTablePagination.vue'
const adminSession = useAdminSessionStore()
/** 录单组别常量与展示文案 */
const GROUP_COLLECT: ManualEntryGroup = 'collect'
const GROUP_SELL: ManualEntryGroup = 'sell'
const groupLabels: Record<ManualEntryGroup, string> = {
collect: '收号',
sell: '出号',
}
/** 组别展示文案 */
function groupLabel(group: string): string {
return groupLabels[group as ManualEntryGroup] || group
}
/** 组别标签颜色:收号=警示橙,出号=成功绿 */
function groupTagType(group: string): 'warning' | 'success' {
return group === GROUP_SELL ? 'success' : 'warning'
}
/** 配置弹窗里修改操作人组别标记(el-select 值规整) */
function onOperatorGroupSelect(item: ManualEntryOption, value: unknown) {
const tag: ManualEntryGroup | '' =
value === 'collect' || value === 'sell' ? (value as ManualEntryGroup) : ''
void handleOperatorGroupChange(item, tag)
}
const currentAdminName = computed(() => adminSession.nickname || adminSession.username || '管理员')
const canCreate = computed(() => adminSession.hasPermission('cs_manual_entry:create'))
const canUpdate = computed(() => adminSession.hasPermission('cs_manual_entry:update'))
@@ -33,6 +58,7 @@ const canManageOptions = computed(() => adminSession.hasPermission('cs_manual_en
const filters = reactive({
keyword: '',
entry_no: '',
entry_group: '' as ManualEntryGroup | '',
dateRange: null as string[] | null,
})
@@ -41,11 +67,22 @@ const optionsLoading = ref(false)
const entryForm = reactive({
entry_no: '',
entry_group: GROUP_COLLECT as ManualEntryGroup,
platform: '',
operator: '',
remark: '',
})
/**
* 选中带组别标记的操作人后自动带出组别(仍可手动修改)。
*/
function handleEntryOperatorChange(name: string) {
const matched = options.value.operators.find(item => item.name === name)
if (matched?.group_tag) {
entryForm.entry_group = matched.group_tag
}
}
const submitting = ref(false)
const {
@@ -61,6 +98,7 @@ const {
return fetchManualEntries(page, pageSize, {
keyword: filters.keyword || undefined,
entry_no: filters.entry_no || undefined,
entry_group: filters.entry_group || undefined,
start_date: startDate,
end_date: endDate,
})
@@ -85,6 +123,7 @@ async function loadOptions() {
function resetFilters() {
filters.keyword = ''
filters.entry_no = ''
filters.entry_group = ''
filters.dateRange = null
currentPage.value = 1
void loadEntries()
@@ -101,9 +140,13 @@ async function handleCreate() {
ElMessage.warning('请输入编号')
return
}
if (!entryForm.entry_group) {
ElMessage.warning('请选择组别')
return
}
try {
await ElMessageBox.confirm(
`确认将编号 ${entryNo} 登记到【${currentAdminName.value}】名下吗?`,
`确认将编号 ${entryNo} 以【${groupLabels[entryForm.entry_group]}】组别登记到【${currentAdminName.value}】名下吗?`,
'录单确认',
{ confirmButtonText: '确认登记', cancelButtonText: '取消', type: 'warning' }
)
@@ -114,11 +157,14 @@ async function handleCreate() {
try {
const result = await createManualEntry({
entry_no: entryNo,
entry_group: entryForm.entry_group,
platform: entryForm.platform,
operator: entryForm.operator,
remark: entryForm.remark,
})
ElMessage.success(`编号 ${result.entry.entry_no} 已登记到【${result.entry.admin_name}】`)
ElMessage.success(
`编号 ${result.entry.entry_no}(${groupLabels[result.entry.entry_group]})已登记到【${result.entry.admin_name}】`
)
entryForm.entry_no = ''
entryForm.platform = ''
entryForm.operator = ''
@@ -136,7 +182,13 @@ async function handleCreate() {
const editVisible = ref(false)
const editSubmitting = ref(false)
const editingEntry = ref<ManualEntryItem | null>(null)
const editForm = reactive({ entry_no: '', platform: '', operator: '', remark: '' })
const editForm = reactive({
entry_no: '',
entry_group: GROUP_COLLECT as ManualEntryGroup,
platform: '',
operator: '',
remark: '',
})
/**
* 历史记录里的平台/操作人可能已被后台删除选项,
@@ -163,6 +215,7 @@ const editOperatorOptions = computed(() =>
function openEdit(row: ManualEntryItem) {
editingEntry.value = row
editForm.entry_no = row.entry_no
editForm.entry_group = row.entry_group || GROUP_COLLECT
editForm.platform = row.platform || ''
editForm.operator = row.operator || ''
editForm.remark = row.remark
@@ -180,6 +233,7 @@ async function handleUpdate() {
try {
await updateManualEntry(editingEntry.value.id, {
entry_no: entryNo,
entry_group: editForm.entry_group,
platform: editForm.platform,
operator: editForm.operator,
remark: editForm.remark,
@@ -218,6 +272,7 @@ const configVisible = ref(false)
const optionSubmitting = ref(false)
const newPlatformName = ref('')
const newOperatorName = ref('')
const newOperatorGroupTag = ref<ManualEntryGroup | ''>('')
function openConfig() {
configVisible.value = true
@@ -234,9 +289,16 @@ async function handleAddOption(type: ManualEntryOptionType) {
}
optionSubmitting.value = true
try {
await createManualEntryOption({ type, name })
await createManualEntryOption({
type,
name,
group_tag: type === 'operator' ? newOperatorGroupTag.value || undefined : undefined,
})
if (type === 'platform') newPlatformName.value = ''
else newOperatorName.value = ''
else {
newOperatorName.value = ''
newOperatorGroupTag.value = ''
}
ElMessage.success('已添加')
await loadOptions()
} catch (error) {
@@ -264,7 +326,7 @@ async function handleRenameOption(item: ManualEntryOption) {
if (!name || name === item.name) return
optionSubmitting.value = true
try {
await updateManualEntryOption(item.id, name)
await updateManualEntryOption(item.id, { name, group_tag: item.group_tag })
ElMessage.success('已重命名')
await loadOptions()
} catch (error) {
@@ -274,6 +336,27 @@ async function handleRenameOption(item: ManualEntryOption) {
}
}
/**
* 修改操作人的组别标记(收号/出号/未分组),
* 仅影响之后录单时的默认组别,不改动历史记录。
*/
async function handleOperatorGroupChange(item: ManualEntryOption, groupTag: ManualEntryGroup | '') {
optionSubmitting.value = true
try {
await updateManualEntryOption(item.id, { name: item.name, group_tag: groupTag })
ElMessage.success(
groupTag
? `【${item.name}】已设为${groupLabels[groupTag]}组`
: `【${item.name}】已移除组别标记`
)
await loadOptions()
} catch (error) {
ElMessage.error(readError(error, '设置组别失败'))
} finally {
optionSubmitting.value = false
}
}
async function handleRemoveOption(item: ManualEntryOption) {
try {
await ElMessageBox.confirm(
@@ -329,6 +412,12 @@ async function handleRemoveOption(item: ManualEntryOption) {
@keyup.enter="handleCreate"
/>
</el-form-item>
<el-form-item label="组别">
<el-radio-group v-model="entryForm.entry_group">
<el-radio-button :value="GROUP_COLLECT">收号</el-radio-button>
<el-radio-button :value="GROUP_SELL">出号</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="平台(可选)">
<el-select
v-model="entryForm.platform"
@@ -346,7 +435,7 @@ async function handleRemoveOption(item: ManualEntryOption) {
/>
</el-select>
</el-form-item>
<el-form-item label="操作人(可选)">
<el-form-item label="操作人(可选,选中后自动带出组别)">
<el-select
v-model="entryForm.operator"
placeholder="请选择操作人"
@@ -354,11 +443,12 @@ async function handleRemoveOption(item: ManualEntryOption) {
filterable
class="full-control"
:loading="optionsLoading"
@change="handleEntryOperatorChange"
>
<el-option
v-for="item in options.operators"
:key="item.id"
:label="item.name"
:label="item.group_tag ? `${item.name}(${groupLabels[item.group_tag]})` : item.name"
:value="item.name"
/>
</el-select>
@@ -388,6 +478,15 @@ async function handleRemoveOption(item: ManualEntryOption) {
class="filter-entry-no"
@keyup.enter="applySearch"
/>
<el-select
v-model="filters.entry_group"
placeholder="全部组别"
clearable
class="filter-group"
>
<el-option label="收号" value="collect" />
<el-option label="出号" value="sell" />
</el-select>
<el-input
v-model="filters.keyword"
placeholder="按编号 / 客服名 / 登录名 / 平台 / 操作人模糊查询"
@@ -415,6 +514,14 @@ async function handleRemoveOption(item: ManualEntryOption) {
<span class="mono-entry-no">{{ row.entry_no }}</span>
</template>
</el-table-column>
<el-table-column prop="entry_group" label="组别" width="90">
<template #default="{ row }">
<el-tag v-if="row.entry_group" :type="groupTagType(row.entry_group)" size="small">
{{ groupLabel(row.entry_group) }}
</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="platform" label="平台" min-width="120">
<template #default="{ row }">{{ row.platform || '-' }}</template>
</el-table-column>
@@ -466,6 +573,12 @@ async function handleRemoveOption(item: ManualEntryOption) {
clearable
/>
</el-form-item>
<el-form-item label="组别">
<el-radio-group v-model="editForm.entry_group">
<el-radio-button :value="GROUP_COLLECT">收号</el-radio-button>
<el-radio-button :value="GROUP_SELL">出号</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="平台">
<el-select
v-model="editForm.platform"
@@ -523,6 +636,7 @@ async function handleRemoveOption(item: ManualEntryOption) {
>
<p class="option-config-tip">
这里维护的可选项会出现在录单表单的下拉中;删除选项不会影响已登记的历史记录。
操作人可设置组别标记(收号/出号),录单选中后自动带出对应组别。
</p>
<div class="option-config-grid">
<section class="option-config-panel">
@@ -569,6 +683,15 @@ async function handleRemoveOption(item: ManualEntryOption) {
clearable
@keyup.enter="handleAddOption('operator')"
/>
<el-select
v-model="newOperatorGroupTag"
placeholder="组别"
clearable
class="option-config-group-select"
>
<el-option label="收号" value="collect" />
<el-option label="出号" value="sell" />
</el-select>
<el-button
type="primary"
:loading="optionSubmitting"
@@ -581,6 +704,17 @@ async function handleRemoveOption(item: ManualEntryOption) {
<li v-for="item in options.operators" :key="item.id">
<span class="option-config-name">{{ item.name }}</span>
<span class="option-config-actions">
<el-select
:model-value="item.group_tag"
placeholder="未分组"
clearable
size="small"
class="option-config-group-select"
@update:model-value="(value: unknown) => onOperatorGroupSelect(item, value)"
>
<el-option label="收号" value="collect" />
<el-option label="出号" value="sell" />
</el-select>
<el-button link type="primary" size="small" @click="handleRenameOption(item)">
重命名
</el-button>