Browse Source

处置人,核验人做成统一选择控件

zengy 3 days ago
parent
commit
9205cbbd8d

+ 16 - 5
src/components/ActionFormDrawer.vue

@@ -2,6 +2,7 @@
 import { computed, ref, watch } from 'vue'
 import { computed, ref, watch } from 'vue'
 import FormField from './FormField.vue'
 import FormField from './FormField.vue'
 import UploadPanel from './UploadPanel.vue'
 import UploadPanel from './UploadPanel.vue'
+import DepartmentUserPicker, { type DepartmentUserOption } from './DepartmentUserPicker.vue'
 import type { AssignableUser } from '@/services/workorders'
 import type { AssignableUser } from '@/services/workorders'
 
 
 export interface WorkOrderActionPayload {
 export interface WorkOrderActionPayload {
@@ -27,6 +28,8 @@ const person = ref('')
 const reviewer = ref('')
 const reviewer = ref('')
 const handlerId = ref<number>()
 const handlerId = ref<number>()
 const reviewerId = ref<number>()
 const reviewerId = ref<number>()
+const userPickerOpen = ref(false)
+const userPickerTarget = ref<'handler' | 'reviewer'>('handler')
 const files = ref<string[]>([])
 const files = ref<string[]>([])
 const result = ref('通过')
 const result = ref('通过')
 const disposer = ref('张伟')
 const disposer = ref('张伟')
@@ -65,11 +68,12 @@ function userLabel(user: AssignableUser) { return user.deptName ? `${user.nickNa
 function pickUser(kind: 'handler' | 'reviewer') {
 function pickUser(kind: 'handler' | 'reviewer') {
   const options = kind === 'handler' ? props.handlerOptions : props.reviewerOptions
   const options = kind === 'handler' ? props.handlerOptions : props.reviewerOptions
   if (!options.length) { uni.showToast({ title: '暂无可选人员', icon: 'none' }); return }
   if (!options.length) { uni.showToast({ title: '暂无可选人员', icon: 'none' }); return }
-  uni.showActionSheet({ itemList: options.map(userLabel), success:r => {
-    const selected = options[r.tapIndex]
-    if (kind === 'handler') { handlerId.value = selected.userId; person.value = userLabel(selected) }
-    else { reviewerId.value = selected.userId; reviewer.value = userLabel(selected) }
-  } })
+  userPickerTarget.value = kind
+  userPickerOpen.value = true
+}
+function selectUser(selected: DepartmentUserOption) {
+  if (userPickerTarget.value === 'handler') { handlerId.value = selected.userId; person.value = userLabel(selected) }
+  else { reviewerId.value = selected.userId; reviewer.value = userLabel(selected) }
 }
 }
 
 
 function invalid(message: string) { uni.showToast({ title: message, icon: 'none' }); return false }
 function invalid(message: string) { uni.showToast({ title: message, icon: 'none' }); return false }
@@ -151,6 +155,13 @@ function submit() {
       </scroll-view>
       </scroll-view>
       <view class="drawer-actions"><button class="cancel" :disabled="submitting" @tap="close">取消</button><button class="submit" :disabled="submitting" @tap="submit">{{ submitting ? '正在提交...' : action === '派单' ? '确认派单' : action === '发布' ? '确认发布' : action === '接收' ? '确认接收' : '确认提交' }}</button></view>
       <view class="drawer-actions"><button class="cancel" :disabled="submitting" @tap="close">取消</button><button class="submit" :disabled="submitting" @tap="submit">{{ submitting ? '正在提交...' : action === '派单' ? '确认派单' : action === '发布' ? '确认发布' : action === '接收' ? '确认接收' : '确认提交' }}</button></view>
     </view>
     </view>
+    <DepartmentUserPicker
+      v-model="userPickerOpen"
+      :title="userPickerTarget === 'handler' ? '选择处置人' : '选择核验人'"
+      :options="userPickerTarget === 'handler' ? handlerOptions : reviewerOptions"
+      :selected-id="userPickerTarget === 'handler' ? handlerId : reviewerId"
+      @select="selectUser"
+    />
   </view>
   </view>
 </template>
 </template>
 
 

+ 85 - 0
src/components/DepartmentUserPicker.vue

@@ -0,0 +1,85 @@
+<script setup lang="ts">
+import { computed, ref, watch } from 'vue'
+
+export interface DepartmentUserOption {
+  userId: number
+  userName?: string
+  nickName: string
+  deptId?: number
+  deptName?: string
+}
+
+const props = withDefaults(defineProps<{
+  modelValue: boolean
+  title: string
+  options?: DepartmentUserOption[]
+  selectedId?: number
+}>(), { options: () => [] })
+
+const emit = defineEmits<{
+  'update:modelValue': [value: boolean]
+  select: [value: DepartmentUserOption]
+}>()
+
+const keyword = ref('')
+const departmentName = (person: DepartmentUserOption) => person.deptName?.trim() || '未分配部门'
+const personName = (person: DepartmentUserOption) => person.nickName || person.userName || `用户${person.userId}`
+
+const filtered = computed(() => {
+  const search = keyword.value.trim().toLowerCase()
+  return props.options.filter(person => {
+    const dept = departmentName(person)
+    if (!search) return true
+    return [personName(person), person.userName, dept].some(value => String(value || '').toLowerCase().includes(search))
+  })
+})
+const groups = computed(() => {
+  const result = new Map<string, DepartmentUserOption[]>()
+  filtered.value.forEach(person => {
+    const dept = departmentName(person)
+    if (!result.has(dept)) result.set(dept, [])
+    result.get(dept)!.push(person)
+  })
+  return [...result].map(([name, users]) => ({ name, users }))
+})
+
+watch(() => props.modelValue, open => {
+  if (!open) return
+  keyword.value = ''
+})
+
+function choose(person: DepartmentUserOption) {
+  emit('select', person)
+  emit('update:modelValue', false)
+}
+function updateKeyword(event: Event) {
+  keyword.value = String((event as CustomEvent<{ value?: string }>).detail?.value || '')
+}
+</script>
+
+<template>
+  <view v-if="modelValue" class="picker-root">
+    <view class="picker-mask" @tap="$emit('update:modelValue', false)" />
+    <view class="picker-panel">
+      <view class="picker-head"><text>{{ title }}</text><text class="picker-close" @tap="$emit('update:modelValue', false)">×</text></view>
+      <view class="search-box">
+        <text class="search-icon">⌕</text>
+        <input :value="keyword" placeholder="搜索姓名、账号或部门" @input="updateKeyword" />
+      </view>
+      <scroll-view class="people-scroll" scroll-y>
+        <view v-if="!groups.length" class="empty">没有符合条件的人员</view>
+        <view v-for="group in groups" :key="group.name" class="people-group">
+          <view class="group-head"><text>{{ group.name }}</text></view>
+          <view v-for="person in group.users" :key="person.userId" class="person" :class="{ selected: person.userId === selectedId }" @tap="choose(person)">
+            <view class="person-info"><text>{{ personName(person) }}</text><text v-if="person.userName && person.userName !== person.nickName">{{ person.userName }}</text></view>
+            <text v-if="person.userId === selectedId" class="check">✓</text>
+          </view>
+        </view>
+      </scroll-view>
+    </view>
+  </view>
+</template>
+
+<style scoped lang="scss">
+.picker-root{position:fixed;inset:0;z-index:260}.picker-mask{position:absolute;inset:0;background:rgba(5,18,35,.58)}.picker-panel{position:absolute;right:0;bottom:0;left:0;display:flex;flex-direction:column;max-height:82vh;overflow:hidden;border-radius:32rpx 32rpx 0 0;background:#fff;box-shadow:0 -12rpx 40rpx rgba(4,20,38,.2)}.picker-head{display:flex;flex:0 0 96rpx;align-items:center;justify-content:space-between;padding:0 30rpx;color:#122b48;border-bottom:1rpx solid #edf1f5;font-size:31rpx;font-weight:700}.picker-close{display:flex;align-items:center;justify-content:center;width:58rpx;height:58rpx;color:#7f8c9b;border-radius:50%;background:#f2f5f8;font-size:42rpx;font-weight:300}.search-box{display:flex;flex:0 0 76rpx;align-items:center;gap:12rpx;margin:22rpx 28rpx 12rpx;padding:0 20rpx;border:1rpx solid #dce4ed;border-radius:12rpx;background:#f7f9fb}.search-box input{flex:1;height:76rpx;color:#34485f;font-size:26rpx}.search-icon{color:#8b98a8;font-size:32rpx}.people-scroll{height:760rpx;min-height:360rpx;padding-bottom:calc(22rpx + env(safe-area-inset-bottom))}.people-group{padding:0 28rpx}.group-head{display:flex;align-items:center;justify-content:space-between;padding:20rpx 4rpx 12rpx;color:#75859a;font-size:22rpx}.person{display:flex;align-items:center;min-height:94rpx;margin-bottom:12rpx;padding:0 20rpx;border:1rpx solid #e4e9ef;border-radius:12rpx;background:#fff}.person.selected{border-color:#7991b0;background:#eef3f8}.person-info{display:flex;flex:1;min-width:0;flex-direction:column;color:#263b52;font-size:27rpx}.person-info text:last-child:not(:first-child){margin-top:4rpx;color:#8b98a8;font-size:21rpx}.check{color:#08264c;font-size:31rpx;font-weight:700}.empty{padding:90rpx 30rpx;color:#98a4b2;text-align:center;font-size:25rpx}
+</style>

+ 24 - 2
src/components/WarningActionDrawer.vue

@@ -2,6 +2,7 @@
 import { computed, ref, watch } from 'vue'
 import { computed, ref, watch } from 'vue'
 import FormField from './FormField.vue'
 import FormField from './FormField.vue'
 import UploadPanel from './UploadPanel.vue'
 import UploadPanel from './UploadPanel.vue'
+import DepartmentUserPicker, { type DepartmentUserOption } from './DepartmentUserPicker.vue'
 import type { WarningParticipant } from '@/services/warnings'
 import type { WarningParticipant } from '@/services/warnings'
 import { useUserStore } from '@/stores/user'
 import { useUserStore } from '@/stores/user'
 const user = useUserStore()
 const user = useUserStore()
@@ -22,6 +23,8 @@ const content = ref('')
 const files = ref<string[]>([])
 const files = ref<string[]>([])
 const handlerIndex = ref(-1)
 const handlerIndex = ref(-1)
 const reviewerIndex = ref(-1)
 const reviewerIndex = ref(-1)
+const userPickerOpen = ref(false)
+const userPickerTarget = ref<'handler' | 'reviewer'>('handler')
 const participantLabel = (person: WarningParticipant) => `${person.nickName || person.userName}(${person.deptName || person.userName})`
 const participantLabel = (person: WarningParticipant) => `${person.nickName || person.userName}(${person.deptName || person.userName})`
 const handlerLabels = computed(() => ['请选择处置人', ...props.handlers.map(participantLabel)])
 const handlerLabels = computed(() => ['请选择处置人', ...props.handlers.map(participantLabel)])
 const reviewerLabels = computed(() => ['请选择核验人', ...props.reviewers.map(participantLabel)])
 const reviewerLabels = computed(() => ['请选择核验人', ...props.reviewers.map(participantLabel)])
@@ -42,6 +45,18 @@ function selectIndex(event: Event, target: 'handler' | 'reviewer') {
   else reviewerIndex.value = index
   else reviewerIndex.value = index
 }
 }
 
 
+function openUserPicker(target: 'handler' | 'reviewer') {
+  const options = target === 'handler' ? props.handlers : props.reviewers
+  if (!options.length) return uni.showToast({ title: '暂无符合条件的人员', icon: 'none' })
+  userPickerTarget.value = target
+  userPickerOpen.value = true
+}
+
+function selectParticipant(person: DepartmentUserOption) {
+  if (userPickerTarget.value === 'handler') handlerIndex.value = props.handlers.findIndex(item => item.userId === person.userId)
+  else reviewerIndex.value = props.reviewers.findIndex(item => item.userId === person.userId)
+}
+
 function submit() {
 function submit() {
   const handler = props.handlers[handlerIndex.value]?.userName || ''
   const handler = props.handlers[handlerIndex.value]?.userName || ''
   const reviewer = props.reviewers[reviewerIndex.value]?.userName || ''
   const reviewer = props.reviewers[reviewerIndex.value]?.userName || ''
@@ -59,8 +74,8 @@ function submit() {
       <view class="drawer-head"><text>{{ title }}</text><text class="close" @tap="!submitting && $emit('update:modelValue', false)">×</text></view>
       <view class="drawer-head"><text>{{ title }}</text><text class="close" @tap="!submitting && $emit('update:modelValue', false)">×</text></view>
       <scroll-view class="drawer-body" scroll-y>
       <scroll-view class="drawer-body" scroll-y>
         <template v-if="action === 'PUBLISH'">
         <template v-if="action === 'PUBLISH'">
-          <view class="field"><text class="label"><text class="required-mark">*</text>处置人</text><picker :range="handlerLabels" :value="handlerIndex + 1" :disabled="!handlers.length" @change="selectIndex($event, 'handler')"><view class="select">{{ handlerLabels[handlerIndex + 1] }}<text>⌄</text></view></picker><text v-if="!handlers.length" class="tip">暂无符合条件且拥有预警处置权限的人员</text></view>
-          <view class="field"><text class="label"><text class="required-mark">*</text>核验人</text><picker :range="reviewerLabels" :value="reviewerIndex + 1" :disabled="!reviewers.length" @change="selectIndex($event, 'reviewer')"><view class="select">{{ reviewerLabels[reviewerIndex + 1] }}<text>⌄</text></view></picker><text v-if="!reviewers.length" class="tip">暂无符合条件且拥有预警核验权限的人员</text></view>
+          <view class="field"><text class="label"><text class="required-mark">*</text>处置人</text><view class="select" @tap="openUserPicker('handler')"><text>{{ handlerLabels[handlerIndex + 1] }}</text><text>⌄</text></view><text v-if="!handlers.length" class="tip">暂无符合条件且拥有预警处置权限的人员</text></view>
+          <view class="field"><text class="label"><text class="required-mark">*</text>核验人</text><view class="select" @tap="openUserPicker('reviewer')"><text>{{ reviewerLabels[reviewerIndex + 1] }}</text><text>⌄</text></view><text v-if="!reviewers.length" class="tip">暂无符合条件且拥有预警核验权限的人员</text></view>
         </template>
         </template>
         <FormField v-if="action === 'RECEIVE' || action === 'START'" v-model="remark" label="备注" type="textarea" placeholder="可填写现场情况或操作说明" />
         <FormField v-if="action === 'RECEIVE' || action === 'START'" v-model="remark" label="备注" type="textarea" placeholder="可填写现场情况或操作说明" />
         <template v-if="action === 'HANDLE'">
         <template v-if="action === 'HANDLE'">
@@ -74,6 +89,13 @@ function submit() {
       </scroll-view>
       </scroll-view>
       <view class="drawer-actions"><button class="cancel" :disabled="submitting" @tap="$emit('update:modelValue', false)">取消</button><button class="submit" :disabled="submitting" @tap="submit">{{ submitting ? '正在提交...' : '确认提交' }}</button></view>
       <view class="drawer-actions"><button class="cancel" :disabled="submitting" @tap="$emit('update:modelValue', false)">取消</button><button class="submit" :disabled="submitting" @tap="submit">{{ submitting ? '正在提交...' : '确认提交' }}</button></view>
     </view>
     </view>
+    <DepartmentUserPicker
+      v-model="userPickerOpen"
+      :title="userPickerTarget === 'handler' ? '选择处置人' : '选择核验人'"
+      :options="userPickerTarget === 'handler' ? handlers : reviewers"
+      :selected-id="userPickerTarget === 'handler' ? handlers[handlerIndex]?.userId : reviewers[reviewerIndex]?.userId"
+      @select="selectParticipant"
+    />
   </view>
   </view>
 </template>
 </template>
 
 

+ 24 - 4
src/pages/emergency/event-detail.vue

@@ -6,6 +6,7 @@ import SegmentTabs from '@/components/SegmentTabs.vue'
 import FormField from '@/components/FormField.vue'
 import FormField from '@/components/FormField.vue'
 import UploadPanel from '@/components/UploadPanel.vue'
 import UploadPanel from '@/components/UploadPanel.vue'
 import PhotoPreview from '@/components/PhotoPreview.vue'
 import PhotoPreview from '@/components/PhotoPreview.vue'
+import DepartmentUserPicker, { type DepartmentUserOption } from '@/components/DepartmentUserPicker.vue'
 import { hasUserPermission, useUserStore } from '@/stores/user'
 import { hasUserPermission, useUserStore } from '@/stores/user'
 import { eventStatusCode, eventStatusText, eventStatusTone, getEmergencyDict, getEmergencyEvent, getEmergencyHandlerOptions, getEmergencyVerifierOptions, auditEmergencyEvent, reportEmergencyEvent, resolveEmergencyAttachment, splitEmergencyAttachments, startEmergencyEvent, submitVerifyEmergencyEvent, trackEmergencyEvent, transferEmergencyEvent, uploadEmergencyFile, verifyEmergencyEvent, type EmergencyEvent, type EmergencyLog, type EmergencyOption, type UserOption } from '@/services/emergency'
 import { eventStatusCode, eventStatusText, eventStatusTone, getEmergencyDict, getEmergencyEvent, getEmergencyHandlerOptions, getEmergencyVerifierOptions, auditEmergencyEvent, reportEmergencyEvent, resolveEmergencyAttachment, splitEmergencyAttachments, startEmergencyEvent, submitVerifyEmergencyEvent, trackEmergencyEvent, transferEmergencyEvent, uploadEmergencyFile, verifyEmergencyEvent, type EmergencyEvent, type EmergencyLog, type EmergencyOption, type UserOption } from '@/services/emergency'
 
 
@@ -17,6 +18,7 @@ const releaseReason = ref(''), releaseNote = ref(''), targets = ref<string[]>([]
 const visitDate = ref(''), visitTime = ref('')
 const visitDate = ref(''), visitTime = ref('')
 const categories = ref<EmergencyOption[]>([]), levels = ref<EmergencyOption[]>([]), releaseReasons = ref<EmergencyOption[]>([]), transferDepts = ref<EmergencyOption[]>([]), trends = ref<EmergencyOption[]>([]), yesNo = ref<EmergencyOption[]>([])
 const categories = ref<EmergencyOption[]>([]), levels = ref<EmergencyOption[]>([]), releaseReasons = ref<EmergencyOption[]>([]), transferDepts = ref<EmergencyOption[]>([]), trends = ref<EmergencyOption[]>([]), yesNo = ref<EmergencyOption[]>([])
 const handlerOptions = ref<UserOption[]>([]), verifierOptions = ref<UserOption[]>([])
 const handlerOptions = ref<UserOption[]>([]), verifierOptions = ref<UserOption[]>([])
+const userPickerOpen = ref(false), userPickerTarget = ref<'handler' | 'reviewer'>('handler')
 const photoPreview = ref<InstanceType<typeof PhotoPreview>>()
 const photoPreview = ref<InstanceType<typeof PhotoPreview>>()
 const hasPerm = hasUserPermission
 const hasPerm = hasUserPermission
 const currentUserId = computed(() => Number(userStore.profile?.userId || 0))
 const currentUserId = computed(() => Number(userStore.profile?.userId || 0))
@@ -80,8 +82,19 @@ function openAction(value: Action) {
 }
 }
 function toggleTarget(target: string) { targets.value = targets.value.includes(target) ? targets.value.filter(item => item !== target) : [...targets.value, target] }
 function toggleTarget(target: string) { targets.value = targets.value.includes(target) ? targets.value.filter(item => item !== target) : [...targets.value, target] }
 function chooseTarget() { pick('选择部门', transferDepts.value, undefined, value => toggleTarget(transferDepts.value.find(item => item.value === value)?.label || value)) }
 function chooseTarget() { pick('选择部门', transferDepts.value, undefined, value => toggleTarget(transferDepts.value.find(item => item.value === value)?.label || value)) }
-function chooseHandler() { pick('选择处置人', handlerOptions.value.map(item => ({ value: item.userId, label: item.deptName ? `${item.nickName}(${item.deptName})` : item.nickName })), handlerId.value, value => { handlerId.value = Number(value) }) }
-function chooseReviewer() { pick('选择核验人', verifierOptions.value.map(item => ({ value: item.userId, label: item.deptName ? `${item.nickName}(${item.deptName})` : item.nickName })), reviewerId.value, value => { reviewerId.value = Number(value) }) }
+function chooseHandler() {
+  if (!handlerOptions.value.length) return uni.showToast({ title: '暂无符合条件的处置人', icon: 'none' })
+  userPickerTarget.value = 'handler'; userPickerOpen.value = true
+}
+function chooseReviewer() {
+  if (!verifierOptions.value.length) return uni.showToast({ title: '暂无符合条件的核验人', icon: 'none' })
+  userPickerTarget.value = 'reviewer'; userPickerOpen.value = true
+}
+function selectUser(person: DepartmentUserOption) {
+  if (userPickerTarget.value === 'handler') handlerId.value = person.userId
+  else reviewerId.value = person.userId
+}
+function userLabel(person?: UserOption) { return person ? (person.deptName ? `${person.nickName}(${person.deptName})` : person.nickName) : '' }
 function chooseCategory() { pick('选择事件类别', categories.value, category.value, value => { category.value = value }) }
 function chooseCategory() { pick('选择事件类别', categories.value, category.value, value => { category.value = value }) }
 function chooseLevel() { pick('选择事件等级', levels.value, level.value, value => { level.value = value }) }
 function chooseLevel() { pick('选择事件等级', levels.value, level.value, value => { level.value = value }) }
 function chooseReleaseReason() { pick('选择解除原因', releaseReasons.value, releaseReason.value, value => { releaseReason.value = value }) }
 function chooseReleaseReason() { pick('选择解除原因', releaseReasons.value, releaseReason.value, value => { releaseReason.value = value }) }
@@ -160,8 +173,8 @@ async function submit() {
           <view v-if="verifyResult === '0'" class="verification-fields">
           <view v-if="verifyResult === '0'" class="verification-fields">
             <FormField :model-value="categories.find(item=>item.value===category)?.label || ''" label="事件类别" type="select" required placeholder="请选择事件类别" @select="chooseCategory" />
             <FormField :model-value="categories.find(item=>item.value===category)?.label || ''" label="事件类别" type="select" required placeholder="请选择事件类别" @select="chooseCategory" />
             <FormField :model-value="levels.find(item=>item.value===level)?.label || ''" label="事件等级" type="select" required placeholder="请选择事件等级" @select="chooseLevel" />
             <FormField :model-value="levels.find(item=>item.value===level)?.label || ''" label="事件等级" type="select" required placeholder="请选择事件等级" @select="chooseLevel" />
-            <FormField :model-value="handlerOptions.find(item=>item.userId===handlerId)?.nickName || ''" label="处置人" type="select" required placeholder="请选择处置人" @select="chooseHandler" />
-            <FormField :model-value="verifierOptions.find(item=>item.userId===reviewerId)?.nickName || ''" label="核验人" type="select" required placeholder="请选择核验人" @select="chooseReviewer" />
+            <FormField :model-value="userLabel(handlerOptions.find(item=>item.userId===handlerId))" label="处置人" type="select" required placeholder="请选择处置人" @select="chooseHandler" />
+            <FormField :model-value="userLabel(verifierOptions.find(item=>item.userId===reviewerId))" label="核验人" type="select" required placeholder="请选择核验人" @select="chooseReviewer" />
           </view>
           </view>
           <FormField v-model="description" label="核实意见" type="textarea" placeholder="请输入核实意见" />
           <FormField v-model="description" label="核实意见" type="textarea" placeholder="请输入核实意见" />
         </view>
         </view>
@@ -170,6 +183,13 @@ async function submit() {
       <template v-else-if="action === '核验'"><view class="form-row"><text class="field-label"><text class="required">*</text>核验结果</text><view class="choices"><button :class="{active:verifyResult==='0'}" @tap="verifyResult='0'">已解除</button><button :class="{active:verifyResult==='1'}" @tap="verifyResult='1'">未解除</button></view></view><FormField v-model="description" label="核验意见" type="textarea" required placeholder="请输入核验意见" /></template>
       <template v-else-if="action === '核验'"><view class="form-row"><text class="field-label"><text class="required">*</text>核验结果</text><view class="choices"><button :class="{active:verifyResult==='0'}" @tap="verifyResult='0'">已解除</button><button :class="{active:verifyResult==='1'}" @tap="verifyResult='1'">未解除</button></view></view><FormField v-model="description" label="核验意见" type="textarea" required placeholder="请输入核验意见" /></template>
       <template v-else><FormField v-model="description" label="备注" type="textarea" required placeholder="请输入备注" /><template v-if="action === '处置跟踪'"><FormField :model-value="trends.find(item=>item.value===trend)?.label || ''" label="态势评估" type="select" placeholder="请选择" @select="chooseTrend" /><FormField :model-value="yesNo.find(item=>item.value===upgrade)?.label || ''" label="是否需要升级" type="select" placeholder="请选择" @select="chooseUpgrade" /><view class="form-row"><text class="field-label">下次回访时间</text><view class="datetime-pickers"><picker mode="date" :value="visitDate" @change="visitDate=String($event.detail.value);updateVisitTime()"><view class="pick-button">{{ visitDate || '选择日期' }}</view></picker><picker mode="time" :value="visitTime" @change="visitTime=String($event.detail.value);updateVisitTime()"><view class="pick-button">{{ visitTime || '选择时间' }}</view></picker></view></view></template><view class="upload-field"><text class="field-label">附件</text><UploadPanel v-model="files" :max-count="5" /></view></template>
       <template v-else><FormField v-model="description" label="备注" type="textarea" required placeholder="请输入备注" /><template v-if="action === '处置跟踪'"><FormField :model-value="trends.find(item=>item.value===trend)?.label || ''" label="态势评估" type="select" placeholder="请选择" @select="chooseTrend" /><FormField :model-value="yesNo.find(item=>item.value===upgrade)?.label || ''" label="是否需要升级" type="select" placeholder="请选择" @select="chooseUpgrade" /><view class="form-row"><text class="field-label">下次回访时间</text><view class="datetime-pickers"><picker mode="date" :value="visitDate" @change="visitDate=String($event.detail.value);updateVisitTime()"><view class="pick-button">{{ visitDate || '选择日期' }}</view></picker><picker mode="time" :value="visitTime" @change="visitTime=String($event.detail.value);updateVisitTime()"><view class="pick-button">{{ visitTime || '选择时间' }}</view></picker></view></view></template><view class="upload-field"><text class="field-label">附件</text><UploadPanel v-model="files" :max-count="5" /></view></template>
     </scroll-view><view class="drawer-actions"><button v-if="action !== '核实'" class="cancel" :disabled="submitting" @tap="drawer=false">取消</button><button class="submit" :class="{ 'single-submit': action === '核实' }" :disabled="submitting" @tap="submit">{{ submitting ? '正在提交...' : '确认提交' }}</button></view></view></view>
     </scroll-view><view class="drawer-actions"><button v-if="action !== '核实'" class="cancel" :disabled="submitting" @tap="drawer=false">取消</button><button class="submit" :class="{ 'single-submit': action === '核实' }" :disabled="submitting" @tap="submit">{{ submitting ? '正在提交...' : '确认提交' }}</button></view></view></view>
+    <DepartmentUserPicker
+      v-model="userPickerOpen"
+      :title="userPickerTarget === 'handler' ? '选择处置人' : '选择核验人'"
+      :options="userPickerTarget === 'handler' ? handlerOptions : verifierOptions"
+      :selected-id="userPickerTarget === 'handler' ? handlerId : reviewerId"
+      @select="selectUser"
+    />
   </view>
   </view>
 </template>
 </template>
 
 

+ 3 - 4
src/pages/login/index.vue

@@ -2,7 +2,7 @@
 import { onLoad } from '@dcloudio/uni-app'
 import { onLoad } from '@dcloudio/uni-app'
 import { ref } from 'vue'
 import { ref } from 'vue'
 import { getCaptcha, loginAccount } from '@/services/auth'
 import { getCaptcha, loginAccount } from '@/services/auth'
-import { getToken, setToken } from '@/utils/auth'
+import { getRememberedUsername, getToken, setRememberedUsername, setToken } from '@/utils/auth'
 import { useUserStore } from '@/stores/user'
 import { useUserStore } from '@/stores/user'
 
 
 const username = ref('')
 const username = ref('')
@@ -61,8 +61,7 @@ async function login() {
     if (!response.token) throw new Error('登录成功但未返回访问令牌')
     if (!response.token) throw new Error('登录成功但未返回访问令牌')
 
 
     setToken(response.token)
     setToken(response.token)
-    if (remember.value) uni.setStorageSync('remembered-username', username.value.trim())
-    else uni.removeStorageSync('remembered-username')
+    setRememberedUsername(remember.value ? username.value.trim() : undefined)
     uni.switchTab({ url: '/pages/message/index' })
     uni.switchTab({ url: '/pages/message/index' })
   } catch (error) {
   } catch (error) {
     uni.showToast({ title: error instanceof Error ? error.message : '登录失败', icon: 'none' })
     uni.showToast({ title: error instanceof Error ? error.message : '登录失败', icon: 'none' })
@@ -92,7 +91,7 @@ onLoad(async () => {
       checkingSession.value = false
       checkingSession.value = false
     }
     }
   }
   }
-  const rememberedUsername = uni.getStorageSync('remembered-username')
+  const rememberedUsername = getRememberedUsername()
   if (rememberedUsername) {
   if (rememberedUsername) {
     username.value = rememberedUsername
     username.value = rememberedUsername
     remember.value = true
     remember.value = true

+ 1 - 1
src/pages/office/alarms.vue

@@ -19,7 +19,7 @@ async function load(reset = false) {
   loading.value = true; error.value = ''; retryReset.value = reset
   loading.value = true; error.value = ''; retryReset.value = reset
   try {
   try {
     const result = await getMobileAlarms({ pageNum: next, pageSize: 10, keyword: keyword.value.trim() || undefined,
     const result = await getMobileAlarms({ pageNum: next, pageSize: 10, keyword: keyword.value.trim() || undefined,
-      alarmLevel: levelFilter.value || undefined, auditStatus: active.value ? active.value - 1 : undefined })
+      alarmLevel: levelFilter.value ? String(levelFilter.value) : undefined, auditStatus: active.value ? active.value - 1 : undefined })
     if (request !== requestId) return
     if (request !== requestId) return
     alarms.value = reset ? result.records : alarms.value.concat(result.records)
     alarms.value = reset ? result.records : alarms.value.concat(result.records)
     total.value = Number(result.total); pageNum.value = next; loaded.value = true
     total.value = Number(result.total); pageNum.value = next; loaded.value = true

+ 3 - 1
src/pages/profile/index.vue

@@ -4,7 +4,7 @@ import { onPullDownRefresh, onShow } from '@dcloudio/uni-app'
 import { navigateTo } from '@/utils/navigation'
 import { navigateTo } from '@/utils/navigation'
 import { logoutAccount } from '@/services/auth'
 import { logoutAccount } from '@/services/auth'
 import { resolveAvatarUrl, uploadAvatar } from '@/services/profile'
 import { resolveAvatarUrl, uploadAvatar } from '@/services/profile'
-import { removeToken } from '@/utils/auth'
+import { removeToken, setRememberedUsername } from '@/utils/auth'
 import { useUserStore } from '@/stores/user'
 import { useUserStore } from '@/stores/user'
 import { getMobilePendingWorkOrders } from '@/services/workorders'
 import { getMobilePendingWorkOrders } from '@/services/workorders'
 
 
@@ -93,6 +93,8 @@ async function handleLogout() {
     uni.showToast({ title: error instanceof Error ? error.message : '退出接口调用失败', icon: 'none' })
     uni.showToast({ title: error instanceof Error ? error.message : '退出接口调用失败', icon: 'none' })
   } finally {
   } finally {
     uni.hideLoading()
     uni.hideLoading()
+    // 记住本次登录的用户名,退出后回到登录页可自动回填
+    setRememberedUsername(profile.value?.userName)
     removeToken()
     removeToken()
     userStore.clearProfile()
     userStore.clearProfile()
     loggingOut.value = false
     loggingOut.value = false

+ 3 - 3
src/services/alarms.ts

@@ -7,7 +7,7 @@ export interface Alarm {
   deviceName?: string
   deviceName?: string
   deviceCode?: string
   deviceCode?: string
   alarmContent?: string
   alarmContent?: string
-  alarmLevel?: number
+  alarmLevel?: string
   alarmLevelName?: string
   alarmLevelName?: string
   auditStatus?: number
   auditStatus?: number
   auditStatusName?: string
   auditStatusName?: string
@@ -36,7 +36,7 @@ export interface AlarmLog {
 }
 }
 interface DataResponse<T> extends ApiResponse { data: T }
 interface DataResponse<T> extends ApiResponse { data: T }
 const BASE = '/api/mobile/alarm'
 const BASE = '/api/mobile/alarm'
-export function getMobileAlarms(params: { pageNum: number; pageSize: number; keyword?: string; alarmLevel?: number; auditStatus?: number }) {
+export function getMobileAlarms(params: { pageNum: number; pageSize: number; keyword?: string; alarmLevel?: string; auditStatus?: number }) {
   return requestApi<DataResponse<{ records: Alarm[]; total: number }>>(BASE, { data: params }).then(r => r.data)
   return requestApi<DataResponse<{ records: Alarm[]; total: number }>>(BASE, { data: params }).then(r => r.data)
 }
 }
 export const getMobileAlarm = (id: string) => requestApi<DataResponse<Alarm>>(`${BASE}/${encodeURIComponent(id)}`).then(r => r.data)
 export const getMobileAlarm = (id: string) => requestApi<DataResponse<Alarm>>(`${BASE}/${encodeURIComponent(id)}`).then(r => r.data)
@@ -66,5 +66,5 @@ export function alarmPhotos(value?: string) {
 }
 }
 export const alarmAuditText = (alarm: Alarm) => alarm.auditStatus === 0 ? '待核实' : alarm.auditStatusName || ({ 1: '确认报警', 2: '误报警' }[Number(alarm.auditStatus)] || '未知结果')
 export const alarmAuditText = (alarm: Alarm) => alarm.auditStatus === 0 ? '待核实' : alarm.auditStatusName || ({ 1: '确认报警', 2: '误报警' }[Number(alarm.auditStatus)] || '未知结果')
 export const alarmAuditTone = (status?: number) => status === 0 ? 'orange' : status === 1 ? '' : 'gray'
 export const alarmAuditTone = (status?: number) => status === 0 ? 'orange' : status === 1 ? '' : 'gray'
-export const alarmLevelTone = (level?: number) => ({ 1: 'red', 2: 'orange', 3: '', 4: 'green' }[Number(level)] || '')
+export const alarmLevelTone = (level?: string) => ({ 1: 'red', 2: 'orange', 3: '', 4: 'green' }[Number(level)] || '')
 export const alarmTime = (value?: string) => value?.replace('T', ' ') || '—'
 export const alarmTime = (value?: string) => value?.replace('T', ' ') || '—'

+ 6 - 2
src/services/api.ts

@@ -1,4 +1,4 @@
-import { getToken, removeToken } from '@/utils/auth'
+import { getToken, removeToken, setRememberedUsername } from '@/utils/auth'
 import { useUserStore } from '@/stores/user'
 import { useUserStore } from '@/stores/user'
 
 
 export interface ApiResponse {
 export interface ApiResponse {
@@ -45,8 +45,12 @@ let redirectingToLogin = false
 export function handleUnauthorized(statusCode: number, code?: number) {
 export function handleUnauthorized(statusCode: number, code?: number) {
   if (statusCode !== 401 && Number(code) !== 401) return false
   if (statusCode !== 401 && Number(code) !== 401) return false
 
 
+  const userStore = useUserStore()
+  // 会话超时也要记住账号,登录页可自动回填用户名;profile 为空(如刷新后
+  // 会话校验失败)时保留已有的记忆,不要覆盖成空值。
+  if (userStore.profile?.userName) setRememberedUsername(userStore.profile.userName)
   removeToken()
   removeToken()
-  useUserStore().clearProfile()
+  userStore.clearProfile()
   const pages = getCurrentPages()
   const pages = getCurrentPages()
   if (!redirectingToLogin && pages[pages.length - 1]?.route !== 'pages/login/index') {
   if (!redirectingToLogin && pages[pages.length - 1]?.route !== 'pages/login/index') {
     redirectingToLogin = true
     redirectingToLogin = true

+ 11 - 0
src/utils/auth.ts

@@ -1,4 +1,5 @@
 const TOKEN_KEY = 'Admin-Token'
 const TOKEN_KEY = 'Admin-Token'
+const REMEMBERED_USERNAME_KEY = 'remembered-username'
 
 
 export function getToken(): string {
 export function getToken(): string {
   return uni.getStorageSync(TOKEN_KEY) || ''
   return uni.getStorageSync(TOKEN_KEY) || ''
@@ -11,3 +12,13 @@ export function setToken(token: string) {
 export function removeToken() {
 export function removeToken() {
   uni.removeStorageSync(TOKEN_KEY)
   uni.removeStorageSync(TOKEN_KEY)
 }
 }
+
+/** 登录页回填用的上次登录账号。 */
+export function getRememberedUsername(): string {
+  return uni.getStorageSync(REMEMBERED_USERNAME_KEY) || ''
+}
+
+export function setRememberedUsername(username?: string) {
+  if (username) uni.setStorageSync(REMEMBERED_USERNAME_KEY, username)
+  else uni.removeStorageSync(REMEMBERED_USERNAME_KEY)
+}