fix(stats/self_input): 修复保存不入库/路由 404,补全部门与自媒体来源下拉

- 模型去掉 defaultSoftDelete=0,避免 ThinkPHP 软删过滤导致新增记录"隐形"
- 菜单 paths 改相对路径并新增「自录数据统计」目录,迁移已有菜单
- Lists/Overview 补部门关联、支持按部门筛选
- 数据权限改由 self_input_stats_view_all_roles 白名单 + DataScope 控制,
  编辑/删除完全交给按钮权限校验(移除"仅本人可改"硬限制)
- 新增 /stats.self_input/mediaSourceOptions 接口,统计页/账户消耗页/录入弹窗
  共用 useMediaSourceOptions + MediaSourceSelect,去重动态加载

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-22 11:47:15 +08:00
co-authored by Cursor
parent 183b6a1acf
commit bba989c0af
19 changed files with 621 additions and 130 deletions
+98 -22
View File
@@ -25,13 +25,32 @@
/>
</el-form-item>
<el-form-item label="自媒体来源">
<el-input
v-model="queryParams.media_source"
placeholder="筛选自媒体来源"
<el-form-item label="部门">
<el-tree-select
v-model="queryParams.dept_id"
:data="deptOptions"
node-key="id"
:props="deptTreeProps"
:default-expand-all="true"
check-strictly
placeholder="全部部门"
clearable
filterable
style="width: 220px"
@keyup.enter="handleQuery"
@change="handleQuery"
/>
</el-form-item>
<el-form-item label="自媒体来源">
<media-source-select
v-model="queryParams.media_source"
:options="mediaSourceOptions"
:loading="mediaSourceLoading"
placeholder="全部来源"
:allow-create="false"
select-style="width: 220px"
@change="handleQuery"
@visible-change="onMediaSourceDropdownVisible"
/>
</el-form-item>
@@ -65,12 +84,13 @@
</template>
新增业绩
</el-button>
<router-link
<el-button
v-if="accountCostEntryPath"
v-perms="['stats.personal_account_cost/lists']"
to="/stats/self_input/account_cost"
@click="goAccountCostEntry"
>
<el-button>账户消耗录入</el-button>
</router-link>
账户消耗录入
</el-button>
</div>
</div>
</template>
@@ -85,6 +105,12 @@
<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>
<template #default="{ row }">
<span v-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"
:key="col.key"
@@ -100,7 +126,6 @@
<el-table-column label="操作" width="140" fixed="right">
<template #default="{ row }">
<el-button
v-if="canMutateRow(row)"
v-perms="['stats.personal_yeji/edit']"
type="primary"
link
@@ -109,7 +134,6 @@
编辑
</el-button>
<el-button
v-if="canMutateRow(row)"
v-perms="['stats.personal_yeji/delete']"
type="danger"
link
@@ -126,7 +150,13 @@
</div>
</el-card>
<yeji-edit-popup v-if="showYejiEdit" ref="yejiEditRef" @success="fetchOverview" @close="showYejiEdit = false" />
<yeji-edit-popup
v-if="showYejiEdit"
ref="yejiEditRef"
:media-source-options="mediaSourceOptions"
@success="handleYejiSuccess"
@close="showYejiEdit = false"
/>
</div>
</template>
@@ -135,11 +165,14 @@ import {
getSelfInputOverview,
personalYejiDelete,
} from '@/api/self_input_stats'
import { deptAll } from '@/api/org/department'
import { usePaging } from '@/hooks/usePaging'
import useUserStore from '@/stores/modules/user'
import { getRoutePath } from '@/router'
import feedback from '@/utils/feedback'
import MediaSourceSelect from './MediaSourceSelect.vue'
import YejiEditPopup from './yeji-edit.vue'
import { useMediaSourceOptions } from './useMediaSourceOptions'
type MetricType = 'count' | 'money' | 'percent' | 'ratio'
@@ -156,10 +189,23 @@ interface MetricColumn {
minWidth?: number
}
const userStore = useUserStore()
const router = useRouter()
const yejiEditRef = shallowRef<InstanceType<typeof YejiEditPopup>>()
const showYejiEdit = ref(false)
const dateRange = ref<string[]>([])
const deptOptions = ref<any[]>([])
const {
mediaSourceOptions,
mediaSourceLoading,
loadMediaSourceOptions,
refreshMediaSourceOptions,
} = useMediaSourceOptions()
const deptTreeProps = {
value: 'id',
label: 'name',
children: 'children',
}
const overview = reactive<Record<string, any>>({
date_range: [],
@@ -168,11 +214,20 @@ const overview = reactive<Record<string, any>>({
const queryParams = reactive({
media_source: '',
dept_id: undefined as number | undefined,
time_type: 'today',
start_date: '',
end_date: '',
})
const accountCostEntryPath = computed(() => getRoutePath('stats.personal_account_cost/lists') || '')
const goAccountCostEntry = () => {
if (accountCostEntryPath.value) {
router.push(accountCostEntryPath.value)
}
}
const fetchOverviewList = async (params: Record<string, any>) => {
if (queryParams.time_type === 'custom') {
params.start_date = dateRange.value[0] || ''
@@ -183,6 +238,33 @@ const fetchOverviewList = async (params: Record<string, any>) => {
return res
}
const loadDeptOptions = async () => {
try {
const res = await deptAll({ apply_data_scope: 1 })
if (Array.isArray(res)) {
deptOptions.value = res
}
} catch (e) {
deptOptions.value = []
}
}
const onMediaSourceDropdownVisible = (visible: boolean) => {
if (visible) {
refreshMediaSourceOptions()
}
}
const handleYejiSuccess = async () => {
await refreshMediaSourceOptions()
await fetchOverview()
}
onMounted(() => {
loadDeptOptions()
loadMediaSourceOptions()
})
const { pager, getLists } = usePaging({
fetchFun: fetchOverviewList,
params: queryParams,
@@ -234,14 +316,6 @@ const dateRangeText = computed(() => {
return `${overview.date_range[0]}${overview.date_range[1]}`
})
const currentAdminId = computed(() => Number(userStore.userInfo?.id || 0))
const isRoot = computed(() => Number(userStore.userInfo?.root || 0) === 1)
const canMutateRow = (row: Record<string, any>) => {
if (isRoot.value) return true
return Number(row.creator_id) === currentAdminId.value
}
const fetchOverview = async () => {
try {
await getLists()
@@ -273,6 +347,7 @@ const handleQuery = () => {
const handleReset = () => {
queryParams.media_source = ''
queryParams.dept_id = undefined
queryParams.time_type = 'today'
dateRange.value = []
pager.page = 1
@@ -289,6 +364,7 @@ const renderMetric = (key: string, source: Record<string, any>, type = 'count')
}
const handleAddYeji = async () => {
await refreshMediaSourceOptions()
showYejiEdit.value = true
await nextTick()
yejiEditRef.value?.open('add')