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

+18 -5
View File
@@ -184,33 +184,47 @@
"182": "Community 182",
"183": "Ha",
"184": "highlight.ts",
"185": "handleConfirmApproval",
"186": "StorageKeyManager",
"187": "Community 187",
"188": "Community 188",
"189": "Community 189",
"190": "error-handle.ts",
"191": "fetchDesignerData",
"193": "loadDefaultCustomerOptions",
"191": "formatYmd",
"192": "loadDefaultCustomerOptions",
"193": "fetchData",
"194": "fetchData",
"195": "Community 195",
"196": "Community 196",
"197": "handleEdit",
"198": "fetchData",
"199": "fetchData",
"200": "index.js",
"201": "fetchCustomerDetail",
"202": "Community 202",
"203": "menuFavorite.ts",
"204": "fetchData",
"205": "index.vue",
"206": "price-letter.ts",
"207": "emit",
"208": "handleSearch",
"209": "Community 209",
"210": "Community 210",
"211": "loadUserOptions",
"212": "index.js",
"213": "Community 213",
"214": "Community 214",
"215": "handleDelete",
"216": "handleSearch",
"217": "index.js",
"218": "index.js",
"219": "handleAdd",
"220": "index.vue",
"221": "zh_CN.js",
"221": "getInventoryStatCellValue",
"222": "handleEdit",
"223": "Community 223",
"224": "Community 224",
"225": "getPlMaintenanceApplyStatusLabel",
"225": "zh_CN.js",
"226": "form.vue",
"227": "Community 227",
"228": "Community 228",
@@ -297,7 +311,6 @@
"405": "zh_CN.js",
"406": "Community 406",
"408": "Community 408",
"409": "DbCrmCustomerSelect.vue",
"410": "Community 410",
"412": "Community 412",
"416": "Community 416",
+1 -1
View File
@@ -1 +1 @@
/Users/wuzilong/work/00code/shanxia2.0/web-admin
.
@@ -184,39 +184,35 @@
"182": "Community 182",
"183": "Ha",
"184": "highlight.ts",
"185": "initLineBrandMetaFromProduct",
"185": "handleConfirmApproval",
"186": "StorageKeyManager",
"187": "Community 187",
"188": "Community 188",
"189": "Community 189",
"190": "error-handle.ts",
"191": "fetchDesignerData",
"192": "handleOpen",
"193": "loadDefaultCustomerOptions",
"192": "loadDefaultCustomerOptions",
"194": "fetchData",
"195": "Community 195",
"196": "Community 196",
"197": "handleEdit",
"198": "fetchCustomerDetail",
"199": "fetchData",
"200": "index.js",
"201": "fetchData",
"201": "fetchCustomerDetail",
"202": "Community 202",
"208": "menuFavorite.ts",
"204": "fetchData",
"206": "price-letter.ts",
"208": "handleSearch",
"209": "Community 209",
"210": "Community 210",
"211": "loadUserOptions",
"212": "index.js",
"213": "Community 213",
"214": "Community 214",
"216": "loadUserOptions",
"217": "index.js",
"218": "index.js",
"220": "index.vue",
"221": "zh_CN.js",
"223": "Community 223",
"224": "Community 224",
"225": "getPlMaintenanceApplyStatusLabel",
"226": "form.vue",
"227": "Community 227",
"228": "Community 228",
@@ -303,7 +299,6 @@
"405": "zh_CN.js",
"406": "Community 406",
"408": "Community 408",
"409": "DbCrmCustomerSelect.vue",
"410": "Community 410",
"412": "Community 412",
"416": "Community 416",
@@ -505,7 +500,6 @@
"1223": "emojiimages.min.js",
"1224": "emojis.js",
"1225": "emojis.min.js",
"1226": "index.js",
"1228": "ar.js",
"1229": "bg_BG.js",
"1230": "ca.js",
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+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;