This commit is contained in:
zlwu committed 2026-08-17 20:32:18 +08:00
1 parent 9345cf4ad8
commit c3bd2c00ae
51 files changed
+14486 -15765

No files matched your search

+2
View File
@@ -115,7 +115,9 @@ export interface PlanListParams {
executionStatus?: string
createdFrom?: string
createdTo?: string
/** 筛选区间开始:与计划有效期(起始~截止)有交集 */
startDateFrom?: string
/** 筛选区间结束:与计划有效期(起始~截止)有交集 */
startDateTo?: string
}
+3
View File
@@ -16,6 +16,8 @@ export interface DutySlotItem {
shiftLabel?: string
capacity: number
taken?: number
/** 班次工作安排 */
remark?: string | null
assignments: DutyAssignmentItem[]
}
@@ -47,6 +49,7 @@ export function saveDutyDay(data: {
slots: Array<{
shiftType: ShiftType
capacity: number
remark?: string | null
assignments: DutyAssignmentItem[]
}>
linkStatus?: LinkStatus
+1
View File
@@ -12,6 +12,7 @@ export interface PublicDutySlot {
shiftLabel?: string
capacity: number
taken: number
remark?: string | null
assignments: Array<{ id?: string; source: string; personName: string; userId?: string }>
}
+64 -45
View File
@@ -405,10 +405,17 @@
loadBxAttachments
} from '@/utils/bx/attachment'
import { invoicesToLinkedAttachmentFiles } from '@/utils/bx/invoice-link-files'
import { matchesApplyDateRange, type ApplyDateRange } from '@/utils/bx/apply-date-filter'
import { buildApplyDateRangeQuery, type ApplyDateRange } from '@/utils/bx/apply-date-filter'
import { usePagedTable } from '@/hooks/bx/usePagedTable'
import { useApplicantOptions } from '@/hooks/bx/useApplicantOptions'
const STATUS_LABEL_TO_CODE: Record<string, string> = {
待提交: 'draft',
审批中: 'pending',
审批通过: 'approved',
审批驳回: 'rejected'
}
type DialogKey = 'viewDetail'
const loading = ref(false)
@@ -464,7 +471,7 @@
const filterForm = reactive({
code: '',
bizType: '' as string,
applicantId: undefined as number | undefined,
applicantId: undefined as string | number | undefined,
status: '' as string,
applyDateRange: null as ApplyDateRange,
amountRange: '',
@@ -474,7 +481,7 @@
const appliedFilter = reactive({
code: '',
bizType: '' as string,
applicantId: undefined as number | undefined,
applicantId: undefined as string | number | undefined,
status: '' as string,
applyDateRange: null as ApplyDateRange,
amountRange: '',
@@ -485,6 +492,7 @@
Object.assign(appliedFilter, { ...filterForm })
}
/** 接口已支持的条件在 loadData 中下发;此处仅处理合并列表后的本地条件 */
function filterMergedRows(rows: any[]) {
let list = [...rows]
const quickKw = appliedQuickSearch.value.trim().toLowerCase()
@@ -502,32 +510,11 @@
.includes(quickKw)
)
}
const code = appliedFilter.code.trim()
if (code) list = list.filter((r: any) => String(r.applyCode || '').includes(code))
if (appliedFilter.bizType && appliedFilter.bizType !== '全部') {
list = list.filter((r: any) => r.bizType === appliedFilter.bizType)
}
if (appliedFilter.applicantId != null) {
list = list.filter((r: any) => Number(r.applicantId) === Number(appliedFilter.applicantId))
}
if (appliedFilter.applyDateRange?.[0] || appliedFilter.applyDateRange?.[1]) {
list = list.filter((r: any) =>
matchesApplyDateRange(r.applyTime, appliedFilter.applyDateRange)
)
}
if (appliedFilter.status && appliedFilter.status !== '全部') {
if (appliedFilter.status === '已发放') {
list = list.filter((r: any) => ['issued', 'paid', 'offset'].includes(r.status))
} else {
const map: Record<string, string> = {
待提交: 'draft',
审批中: 'pending',
审批通过: 'approved',
审批驳回: 'rejected'
}
const want = map[appliedFilter.status]
if (want) list = list.filter((r: any) => r.status === want)
}
if (appliedFilter.status === '已发放') {
list = list.filter((r: any) => ['issued', 'paid', 'offset'].includes(r.status))
}
if (appliedFilter.currentNode && appliedFilter.currentNode !== '全部') {
list = list.filter((r: any) => (r.currentNode || '') === appliedFilter.currentNode)
@@ -548,6 +535,21 @@
return list
}
function buildApiQuery() {
const q: Record<string, string | number> = {}
const code = appliedFilter.code.trim()
if (code) q.keyword = code
if (appliedFilter.applicantId != null && appliedFilter.applicantId !== '') {
q.applicantId = String(appliedFilter.applicantId)
}
if (appliedFilter.status && appliedFilter.status !== '全部' && appliedFilter.status !== '已发放') {
const statusCode = STATUS_LABEL_TO_CODE[appliedFilter.status]
if (statusCode) q.status = statusCode
}
buildApplyDateRangeQuery(appliedFilter.applyDateRange, q)
return q
}
const filteredMergedRows = computed(() => filterMergedRows(mergedRows.value))
const pager = usePagedTable(() => filteredMergedRows.value, { initialPageSize: 20 })
@@ -641,29 +643,44 @@
'已发放'
]
function mapAdvanceRow(r: any) {
return {
...r,
bizType: '备用金',
title: r.reason,
applyCode: r.applyCode || r.code,
applicantName: r.applicantName || r.user,
applicantId: r.applicantId
}
}
function mapReimburseRow(r: any) {
return {
...r,
bizType: '报销单',
title: r.title,
applyCode: r.applyCode || r.code,
applicantName: r.applicantName || r.user,
applicantId: r.applicantId
}
}
async function loadData() {
loading.value = true
try {
const [advRes, reimRes]: any[] = await Promise.all([
advanceApi.list({ page: 1, pageSize: 999 }),
reimburseApi.list({ page: 1, pageSize: 999 })
const listParams = { page: 1, pageSize: 9999, ...buildApiQuery() }
const [advRes, reimRes] = await Promise.allSettled([
advanceApi.list(listParams),
reimburseApi.list(listParams)
])
const advRows = (advRes.data?.rows || []).map((r: any) => ({
...r,
bizType: '备用金',
title: r.reason,
applyCode: r.applyCode || r.code,
applicantName: r.applicantName || r.user,
applicantId: r.applicantId
}))
const reimRows = (reimRes.data?.rows || []).map((r: any) => ({
...r,
bizType: '报销单',
title: r.title,
applyCode: r.applyCode || r.code,
applicantName: r.applicantName || r.user,
applicantId: r.applicantId
}))
const advRows =
advRes.status === 'fulfilled'
? ((advRes.value as any).data?.rows || []).map(mapAdvanceRow)
: []
const reimRows =
reimRes.status === 'fulfilled'
? ((reimRes.value as any).data?.rows || []).map(mapReimburseRow)
: []
mergedRows.value = [...advRows, ...reimRows]
} catch {
// error handled by interceptor
@@ -677,6 +694,7 @@
appliedQuickSearch.value = ''
syncAppliedFilter()
pager.currentPage.value = 1
loadData()
}
useDbInstantSearch(
@@ -714,6 +732,7 @@
currentNode: ''
})
pager.currentPage.value = 1
loadData()
}
function handleQuickSearch() {
+1 -1
View File
@@ -758,7 +758,7 @@
const handleDateRangeChange = () => {
activeDateShortcut.value = ''
currentPage.value = 1
handleSearch()
}
const resetFormData = () => {
+51 -20
View File
@@ -110,6 +110,7 @@
:key="ci"
class="chip"
:class="chip.cls"
:title="chip.title || chip.text"
>
{{ chip.text }}
</span>
@@ -179,7 +180,7 @@
v-model="dialogVisible"
class="dialog-form-layout duty-day-dialog"
:title="`配置 · ${editingDate}`"
width="640px"
width="880px"
align-center
destroy-on-close
:close-on-click-modal="false"
@@ -198,23 +199,35 @@
</div>
</div>
<template v-if="slot.enabled">
<el-form-item label="直接安排(值班人员)">
<el-select
v-model="slot.pendingUserId"
filterable
clearable
placeholder="选择员工后添加"
style="width: 100%"
@change="(v) => addAssign(slot, String(v || ''))"
>
<el-option
v-for="u in userOptions"
:key="u.value"
:label="u.label"
:value="u.value"
<div class="form-grid-2 slot-assign-row">
<el-form-item label="值班安排">
<el-select
v-model="slot.pendingUserId"
filterable
clearable
placeholder="选择员工后添加"
style="width: 100%"
@change="(v) => addAssign(slot, String(v || ''))"
>
<el-option
v-for="u in userOptions"
:key="u.value"
:label="u.label"
:value="u.value"
/>
</el-select>
</el-form-item>
<el-form-item label="工作安排">
<el-input
v-model="slot.remark"
type="textarea"
:rows="2"
maxlength="500"
show-word-limit
placeholder="可选,填写本班次工作安排"
/>
</el-select>
</el-form-item>
</el-form-item>
</div>
<div class="person-tags">
<el-tag
v-for="(a, ai) in slot.assignments"
@@ -320,6 +333,7 @@
shiftType: ShiftType
enabled: boolean
capacity: number
remark: string
pendingUserId: string
assignments: DutySlotItem['assignments']
}
@@ -329,6 +343,7 @@
shiftType: t,
enabled: false,
capacity: 1,
remark: '',
pendingUserId: '',
assignments: []
}))
@@ -473,7 +488,7 @@
dateStr: string
isToday: boolean
summary?: DutyDayItem
chips: Array<{ text: string; cls: string }>
chips: Array<{ text: string; cls: string; title?: string }>
}> = []
const prevDays = new Date(y, m - 1, 0).getDate()
@@ -484,7 +499,7 @@
for (let d = 1; d <= daysInMonth; d++) {
const dateStr = toDateStr(y, m, d)
const summary = dayMap.value.get(dateStr)
const chips: Array<{ text: string; cls: string }> = []
const chips: Array<{ text: string; cls: string; title?: string }> = []
if (summary?.slots?.length) {
for (const s of summary.slots) {
if (shiftFilter && s.shiftType !== shiftFilter) continue
@@ -494,6 +509,7 @@
if (filterKey && !matched.length) continue
const names = matched.map((a) => a.personName).filter(Boolean)
const taken = s.taken ?? (s.assignments || []).length
const remark = (s.remark || '').trim()
let text: string
if (filterKey) {
text = `${s.shiftLabel || SHIFT_LABELS[s.shiftType]} ${names.join('、')}`
@@ -502,7 +518,14 @@
} else {
text = `${s.shiftLabel || SHIFT_LABELS[s.shiftType]} ${taken}/${s.capacity}`
}
chips.push({ text, cls: SHIFT_CLS[s.shiftType] || 'am' })
if (remark) {
text = `${text} · ${remark}`
}
chips.push({
text,
cls: SHIFT_CLS[s.shiftType] || 'am',
title: remark || undefined
})
}
if (!hasFilter && !chips.length) {
chips.push({ text: '未排班 · 点击配置', cls: 'empty' })
@@ -598,6 +621,7 @@
const found = detail?.slots?.find((s) => s.shiftType === fs.shiftType)
fs.enabled = !!found
fs.capacity = found?.capacity || 1
fs.remark = (found?.remark || '').trim()
fs.pendingUserId = ''
fs.assignments = found ? [...(found.assignments || [])] : []
}
@@ -665,6 +689,7 @@
.map((s) => ({
shiftType: s.shiftType,
capacity: s.capacity,
remark: s.remark.trim() || null,
assignments: s.assignments
.filter((a) => a.source === 'assign')
.map((a) => ({
@@ -1147,6 +1172,12 @@
justify-content: space-between;
margin-bottom: 8px;
}
.slot-assign-row {
margin-bottom: 4px;
:deep(.el-form-item) {
margin-bottom: 0;
}
}
.cap-field {
display: flex;
gap: 8px;
+128 -43
View File
@@ -11,6 +11,15 @@
<span>收藏</span>
<span class="n">{{ favoriteList.length }}</span>
</button>
<button
type="button"
class="portal-home-aside__item"
:class="{ 'is-on': isTodoView }"
@click="activeModule = TODO_KEY"
>
<span>待办列表</span>
<span class="n">{{ formatPendingCount(todoTotalSum) }}</span>
</button>
<button
v-for="mod in modules"
:key="mod.modulePath"
@@ -77,6 +86,44 @@
<p v-else class="portal-home-empty">暂无收藏,可在各模块菜单卡片上点星标添加</p>
</section>
<section v-else-if="isTodoView" class="portal-home-block portal-home-block--flat">
<div v-if="todoList.length" class="portal-home-grid">
<button
v-for="card in todoList"
:key="'todo-' + card.path"
type="button"
class="portal-home-card"
@click="openCard(card.path)"
>
<span
class="portal-home-card__pending"
:aria-label="`${pendingCountFor(card)} 条待审批`"
>
{{ formatPendingCount(pendingCountFor(card)) }}
</span>
<span class="portal-home-card__badge" aria-hidden="true">
{{ titleAbbr(card.title) }}
</span>
<span class="portal-home-card__body">
<span class="portal-home-card__title">{{ card.title }}</span>
</span>
<button
type="button"
class="portal-home-card__star"
:class="{ 'is-on': favoritePathSet.has(card.path) }"
:aria-label="favoritePathSet.has(card.path) ? '取消收藏' : '加入收藏'"
@click.stop="onToggleStar(card.path)"
>
<ElIcon>
<StarFilled v-if="favoritePathSet.has(card.path)" />
<Star v-else />
</ElIcon>
</button>
</button>
</div>
<p v-else class="portal-home-empty">暂无待办</p>
</section>
<template v-else>
<section
v-for="(group, gIdx) in activeGroups"
@@ -156,6 +203,7 @@
defineOptions({ name: 'PortalHome' })
const FAV_KEY = '__favorites__'
const TODO_KEY = '__todos__'
/** CRM 报价/合同审批:侧栏角标体系尚未覆盖,首页单独拉取 */
const PENDING_CARD_RULES: Array<{
key: 'quote' | 'contract'
@@ -194,9 +242,10 @@
const favoritePathSet = computed(() => new Set(favItems.value.map((i) => i.path)))
const isFavView = computed(() => activeModule.value === FAV_KEY)
const isTodoView = computed(() => activeModule.value === TODO_KEY)
const activeSection = computed(() => {
if (isFavView.value) return undefined
if (isFavView.value || isTodoView.value) return undefined
return modules.value.find((m) => m.modulePath === activeModule.value)
})
@@ -204,48 +253,6 @@
const activeCards = computed(() => activeSection.value?.cards || [])
const panelTitle = computed(() => {
if (isFavView.value) return '收藏'
return activeSection.value?.moduleTitle || ''
})
const panelDesc = computed(() =>
isFavView.value ? '可拖拽调整顺序,星标取消收藏' : '按二级菜单分组展示,点击卡片进入功能'
)
const panelCount = computed(() =>
isFavView.value ? favoriteList.value.length : activeCards.value.length
)
const titleAbbr = (title: string) => {
const t = String(title || '').trim()
if (!t) return '?'
return t.slice(0, 1)
}
const ensureActiveModule = () => {
if (activeModule.value === FAV_KEY) return
const list = modules.value
if (!list.length) {
activeModule.value = FAV_KEY
return
}
if (!list.some((m) => m.modulePath === activeModule.value)) {
activeModule.value = FAV_KEY
}
}
const syncFavoriteList = () => {
favoriteList.value = favItems.value.map(
(i) => findHomeCardByPath(modules.value, i.path) || { path: i.path, title: i.path }
)
}
const resolveCurrentUserId = (): string => {
const info = (userStore.info || {}) as { id?: string | number; userId?: string | number }
return String(info.id || info.userId || '')
}
const resolveBadgeMenuCode = (card: PortalHomeCardItem) => {
const code = String(card.menuCode || '').trim()
if (code && MENU_BADGE_CODE_SET.has(code)) return code
@@ -276,6 +283,84 @@
const formatPendingCount = (n: number) => (n > 99 ? '99+' : String(n))
const allHomeCards = computed(() => {
const seen = new Set<string>()
const out: PortalHomeCardItem[] = []
for (const sec of modules.value) {
for (const card of sec.cards) {
if (seen.has(card.path)) continue
seen.add(card.path)
out.push(card)
}
}
return out
})
const todoEntries = computed(() => {
void badgeCounts.value
void badgeLoaded.value
void pendingCounts.value
return allHomeCards.value
.map((card) => ({ card, count: pendingCountFor(card) }))
.filter((x) => x.count > 0)
.sort(
(a, b) => b.count - a.count || String(a.card.title).localeCompare(String(b.card.title), 'zh')
)
})
const todoList = computed(() => todoEntries.value.map((x) => x.card))
const todoTotalSum = computed(() =>
todoEntries.value.reduce((sum, x) => sum + x.count, 0)
)
const panelTitle = computed(() => {
if (isFavView.value) return '收藏'
if (isTodoView.value) return '待办列表'
return activeSection.value?.moduleTitle || ''
})
const panelDesc = computed(() => {
if (isFavView.value) return '可拖拽调整顺序,星标取消收藏'
if (isTodoView.value) return '汇总当前有待办的菜单,按数量从多到少排列'
return '按二级菜单分组展示,点击卡片进入功能'
})
const panelCount = computed(() => {
if (isFavView.value) return favoriteList.value.length
if (isTodoView.value) return todoList.value.length
return activeCards.value.length
})
const titleAbbr = (title: string) => {
const t = String(title || '').trim()
if (!t) return '?'
return t.slice(0, 1)
}
const ensureActiveModule = () => {
if (activeModule.value === FAV_KEY || activeModule.value === TODO_KEY) return
const list = modules.value
if (!list.length) {
activeModule.value = FAV_KEY
return
}
if (!list.some((m) => m.modulePath === activeModule.value)) {
activeModule.value = FAV_KEY
}
}
const syncFavoriteList = () => {
favoriteList.value = favItems.value.map(
(i) => findHomeCardByPath(modules.value, i.path) || { path: i.path, title: i.path }
)
}
const resolveCurrentUserId = (): string => {
const info = (userStore.info || {}) as { id?: string | number; userId?: string | number }
return String(info.id || info.userId || '')
}
const loadCrmPendingCounts = async () => {
const userId = resolveCurrentUserId()
if (!userId) {
+15
View File
@@ -59,6 +59,9 @@
<span>名额</span>
<strong>{{ slot.taken }} / {{ slot.capacity }}</strong>
</div>
<p v-if="slot.remark" class="slot-remark">
工作安排:<strong>{{ slot.remark }}</strong>
</p>
<div class="bar"><i :style="{ width: barWidth(slot) }" :class="slot.shiftType"></i></div>
<div class="people">
<span v-for="(a, i) in slot.assignments" :key="i" class="person">{{ a.personName }}</span>
@@ -376,6 +379,18 @@
font-size: 15px;
color: #1f2a26;
}
.slot-remark {
margin: 8px 0 0;
font-size: 13px;
line-height: 1.45;
color: #6b7b74;
white-space: pre-wrap;
word-break: break-word;
}
.slot-remark strong {
font-weight: 600;
color: #1f2a26;
}
.bar {
height: 7px;
margin-top: 8px;