fix(stats/self_input): 渠道字典、财务脱敏与同日同渠道全局唯一
自媒体来源改推广渠道字典;非白名单隐藏账户消耗/ROI;部门展示完整路径;业绩与消耗按日期+渠道全局去重。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -14,15 +14,32 @@
|
||||
@change="emit('change', $event)"
|
||||
@visible-change="onVisibleChange"
|
||||
>
|
||||
<el-option v-for="item in options" :key="item" :label="item" :value="item" />
|
||||
<el-option
|
||||
v-for="item in normalizedOptions"
|
||||
:key="item.name"
|
||||
:label="item.name"
|
||||
:value="item.name"
|
||||
/>
|
||||
<!-- 旧记录可能存在不在当前字典内的值:保留显示,避免编辑时反查不到 -->
|
||||
<el-option
|
||||
v-if="modelValue && !hasCurrentValue"
|
||||
:key="`__legacy_${modelValue}`"
|
||||
:label="`${modelValue}(已停用)`"
|
||||
:value="modelValue"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
withDefaults(
|
||||
interface OptionItem {
|
||||
name: string
|
||||
value?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: string
|
||||
options: string[]
|
||||
options: Array<OptionItem | string>
|
||||
loading?: boolean
|
||||
placeholder?: string
|
||||
clearable?: boolean
|
||||
@@ -49,6 +66,16 @@ const emit = defineEmits<{
|
||||
'visible-change': [visible: boolean]
|
||||
}>()
|
||||
|
||||
const normalizedOptions = computed<OptionItem[]>(() => {
|
||||
return (props.options || []).map((item) =>
|
||||
typeof item === 'string' ? { name: item } : { name: item.name, value: item.value }
|
||||
)
|
||||
})
|
||||
|
||||
const hasCurrentValue = computed(() => {
|
||||
return normalizedOptions.value.some((item) => item.name === props.modelValue)
|
||||
})
|
||||
|
||||
const onVisibleChange = (visible: boolean) => {
|
||||
emit('visible-change', visible)
|
||||
}
|
||||
|
||||
@@ -82,9 +82,17 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="remark" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="录入人" prop="creator_name" min-width="100" />
|
||||
<el-table-column label="部门" prop="dept_name" min-width="140" show-overflow-tooltip>
|
||||
<el-table-column label="部门" prop="dept_name" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.dept_name">{{ row.dept_name }}</span>
|
||||
<el-tooltip
|
||||
v-if="row.dept_path"
|
||||
:content="row.dept_path"
|
||||
placement="top"
|
||||
effect="dark"
|
||||
>
|
||||
<span class="dept-cell">{{ row.dept_name || '未分配' }}</span>
|
||||
</el-tooltip>
|
||||
<span v-else-if="row.dept_name">{{ row.dept_name }}</span>
|
||||
<span v-else class="text-gray-400">未分配</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -121,6 +129,7 @@
|
||||
v-if="showEdit"
|
||||
ref="editRef"
|
||||
:media-source-options="mediaSourceOptions"
|
||||
:media-source-loading="mediaSourceLoading"
|
||||
@success="handleCostSuccess"
|
||||
@close="showEdit = false"
|
||||
/>
|
||||
@@ -237,4 +246,9 @@ onMounted(() => {
|
||||
.personal-account-cost-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dept-cell {
|
||||
cursor: help;
|
||||
border-bottom: 1px dashed #c0c4cc;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -23,8 +23,9 @@
|
||||
<media-source-select
|
||||
v-model="formData.media_source"
|
||||
:options="mediaSourceOptions"
|
||||
placeholder="请选择或输入自媒体来源"
|
||||
:allow-create="true"
|
||||
:loading="mediaSourceLoading"
|
||||
placeholder="请选择自媒体来源"
|
||||
:allow-create="false"
|
||||
:disabled="mode === 'edit'"
|
||||
select-class="w-full"
|
||||
/>
|
||||
@@ -63,13 +64,16 @@ import {
|
||||
import Popup from '@/components/popup/index.vue'
|
||||
|
||||
import MediaSourceSelect from './MediaSourceSelect.vue'
|
||||
import type { MediaSourceOption } from './useMediaSourceOptions'
|
||||
|
||||
const props = withDefaults(
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
mediaSourceOptions?: string[]
|
||||
mediaSourceOptions?: MediaSourceOption[]
|
||||
mediaSourceLoading?: boolean
|
||||
}>(),
|
||||
{
|
||||
mediaSourceOptions: () => [],
|
||||
mediaSourceLoading: false,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
</el-card>
|
||||
|
||||
<div class="stats-kpi-grid">
|
||||
<div v-for="card in summaryCards" :key="card.key" class="stats-kpi-card">
|
||||
<div v-for="card in visibleSummaryCards" :key="card.key" class="stats-kpi-card">
|
||||
<div class="stats-kpi-label">{{ card.label }}</div>
|
||||
<div class="stats-kpi-value" :class="{ 'is-money': card.type === 'money' }">
|
||||
{{ renderMetric(card.key, overview.summary, card.type) }}
|
||||
@@ -105,14 +105,22 @@
|
||||
<el-table-column label="日期" prop="yeji_date" min-width="110" fixed="left" />
|
||||
<el-table-column label="自媒体来源" prop="media_source" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="录入人" prop="creator_name" min-width="100" />
|
||||
<el-table-column label="部门" prop="dept_name" min-width="140" show-overflow-tooltip>
|
||||
<el-table-column label="部门" prop="dept_name" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.dept_name">{{ row.dept_name }}</span>
|
||||
<el-tooltip
|
||||
v-if="row.dept_path"
|
||||
:content="row.dept_path"
|
||||
placement="top"
|
||||
effect="dark"
|
||||
>
|
||||
<span class="dept-cell">{{ row.dept_name || '未分配' }}</span>
|
||||
</el-tooltip>
|
||||
<span v-else-if="row.dept_name">{{ row.dept_name }}</span>
|
||||
<span v-else class="text-gray-400">未分配</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-for="col in tableColumns"
|
||||
v-for="col in visibleTableColumns"
|
||||
:key="col.key"
|
||||
:label="col.label"
|
||||
:min-width="col.minWidth || 100"
|
||||
@@ -154,6 +162,7 @@
|
||||
v-if="showYejiEdit"
|
||||
ref="yejiEditRef"
|
||||
:media-source-options="mediaSourceOptions"
|
||||
:media-source-loading="mediaSourceLoading"
|
||||
@success="handleYejiSuccess"
|
||||
@close="showYejiEdit = false"
|
||||
/>
|
||||
@@ -210,8 +219,12 @@ const deptTreeProps = {
|
||||
const overview = reactive<Record<string, any>>({
|
||||
date_range: [],
|
||||
summary: {},
|
||||
can_view_finance: false,
|
||||
})
|
||||
|
||||
const FINANCE_KEYS = new Set(['account_cost', 'cash_cost', 'roi'])
|
||||
const canViewFinance = computed(() => Boolean(overview.can_view_finance))
|
||||
|
||||
const queryParams = reactive({
|
||||
media_source: '',
|
||||
dept_id: undefined as number | undefined,
|
||||
@@ -311,6 +324,18 @@ const tableColumns: MetricColumn[] = [
|
||||
{ key: 'roi', label: 'ROI', type: 'ratio', minWidth: 80 },
|
||||
]
|
||||
|
||||
const visibleSummaryCards = computed(() =>
|
||||
canViewFinance.value
|
||||
? summaryCards
|
||||
: summaryCards.filter((card) => !FINANCE_KEYS.has(card.key))
|
||||
)
|
||||
|
||||
const visibleTableColumns = computed(() =>
|
||||
canViewFinance.value
|
||||
? tableColumns
|
||||
: tableColumns.filter((col) => !FINANCE_KEYS.has(col.key))
|
||||
)
|
||||
|
||||
const dateRangeText = computed(() => {
|
||||
if (!overview.date_range?.length) return '未选择'
|
||||
return `${overview.date_range[0]} 至 ${overview.date_range[1]}`
|
||||
@@ -414,6 +439,11 @@ onMounted(async () => {
|
||||
box-shadow: 0 10px 24px rgba(74, 120, 255, 0.08);
|
||||
}
|
||||
|
||||
.dept-cell {
|
||||
cursor: help;
|
||||
border-bottom: 1px dashed #c0c4cc;
|
||||
}
|
||||
|
||||
.stats-kpi-label {
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
|
||||
@@ -1,12 +1,42 @@
|
||||
import { getSelfInputMediaSourceOptions } from '@/api/self_input_stats'
|
||||
|
||||
/**
|
||||
* 自录转化统计:自媒体来源下拉(从已录入业绩/消耗去重,两页共用同一接口)
|
||||
* 自录转化统计:自媒体来源选项(来自字典「推广渠道」channels)。
|
||||
* 业绩页与账户消耗页共用同一接口,下拉打开时刷新,保证两侧字典变更同步。
|
||||
*/
|
||||
export interface MediaSourceOption {
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export function useMediaSourceOptions() {
|
||||
const mediaSourceOptions = ref<string[]>([])
|
||||
const mediaSourceOptions = ref<MediaSourceOption[]>([])
|
||||
const mediaSourceLoading = ref(false)
|
||||
|
||||
const normalize = (raw: any): MediaSourceOption[] => {
|
||||
if (!Array.isArray(raw)) return []
|
||||
const list: MediaSourceOption[] = []
|
||||
const seen = new Set<string>()
|
||||
for (const item of raw) {
|
||||
if (typeof item === 'string') {
|
||||
const name = item.trim()
|
||||
if (name && !seen.has(name)) {
|
||||
seen.add(name)
|
||||
list.push({ name, value: '' })
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (item && typeof item === 'object') {
|
||||
const name = String(item.name ?? '').trim()
|
||||
if (name && !seen.has(name)) {
|
||||
seen.add(name)
|
||||
list.push({ name, value: String(item.value ?? '') })
|
||||
}
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
const loadMediaSourceOptions = async (force = false) => {
|
||||
if (!force && mediaSourceOptions.value.length > 0) {
|
||||
return
|
||||
@@ -14,7 +44,7 @@ export function useMediaSourceOptions() {
|
||||
mediaSourceLoading.value = true
|
||||
try {
|
||||
const res = await getSelfInputMediaSourceOptions()
|
||||
mediaSourceOptions.value = Array.isArray(res) ? res : []
|
||||
mediaSourceOptions.value = normalize(res)
|
||||
} catch {
|
||||
mediaSourceOptions.value = []
|
||||
} finally {
|
||||
|
||||
@@ -23,8 +23,9 @@
|
||||
<media-source-select
|
||||
v-model="formData.media_source"
|
||||
:options="mediaSourceOptions"
|
||||
placeholder="请选择或输入自媒体来源"
|
||||
:allow-create="true"
|
||||
:loading="mediaSourceLoading"
|
||||
placeholder="请选择自媒体来源"
|
||||
:allow-create="false"
|
||||
:disabled="mode === 'edit'"
|
||||
select-class="w-full"
|
||||
/>
|
||||
@@ -134,13 +135,16 @@ import { personalYejiAdd, personalYejiEdit } from '@/api/self_input_stats'
|
||||
import Popup from '@/components/popup/index.vue'
|
||||
|
||||
import MediaSourceSelect from './MediaSourceSelect.vue'
|
||||
import type { MediaSourceOption } from './useMediaSourceOptions'
|
||||
|
||||
const props = withDefaults(
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
mediaSourceOptions?: string[]
|
||||
mediaSourceOptions?: MediaSourceOption[]
|
||||
mediaSourceLoading?: boolean
|
||||
}>(),
|
||||
{
|
||||
mediaSourceOptions: () => [],
|
||||
mediaSourceLoading: false,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user