1
0

2 Commits 8f6db68057 ... d076a44cf5

Autor SHA1 Nachricht Datum
  zhangzhicheng d076a44cf5 Merge branch 'master' of http://gogs.gzzzyd.com/xuyunhui/aquagreen_wechat_app vor 1 Woche
  zhangzhicheng d12d1a9af4 feat: 对齐登录鉴权与用户信息字段,接入通知/知识库/每日检查接口并完善首页与任务详情 vor 1 Woche

+ 16 - 4
src/api/auth.ts

@@ -5,7 +5,7 @@ export type MpRoleType = 'mp-sales' | 'mp-dispatch' | 'mp-construction'
 export interface LoginParams {
   username: string
   password: string
-  roleType?: MpRoleType
+  roleType?: MpRoleType | 'mp'
 }
 
 export interface LoginMenu {
@@ -35,11 +35,22 @@ export interface LoginResult {
   phone?: string
   email?: string
   dept?: { deptName?: string; deptId?: number; name?: string }
-  position?: string
+  deptId?: number
+  deptName?: string
+  staffCode?: string
   employeeNo?: string
+  entryDate?: string
   hireDate?: string
+  teamId?: number
   team?: string
-  department?: string
+  teamName?: string
+  dutyStatus?: string
+  skillLevel?: string
+  certifications?: string
+  tags?: string
+  skillTags?: string
+  currentTaskId?: number
+  postNames?: string
   needChangePassword?: boolean
   menus?: LoginMenu[]
   permissions?: string[]
@@ -47,7 +58,8 @@ export interface LoginResult {
 }
 
 export function login(params: LoginParams) {
-  return post<LoginResult>('/auth/login', params)
+  // 未指定具体端点时传 'mp' 标记,表示小程序端登录(后端据此签发 mp token)
+  return post<LoginResult>('/auth/login', { ...params, roleType: params.roleType ?? 'mp' })
 }
 
 export function logout() {

+ 49 - 0
src/api/dailyCheck.ts

@@ -0,0 +1,49 @@
+import { get, post } from './request'
+import type { DispatchSchedule } from './schedule'
+
+export interface DailyCheckItem {
+  id: string
+  category: string
+  name: string
+  required: boolean
+  checked: boolean
+}
+
+export interface DailyCheckRecord {
+  checkId?: number
+  checkDate: string
+  vehicleId?: number
+  vehicleNo?: string
+  userId: number
+  userName?: string
+  checkStatus?: string
+  checkItems: DailyCheckItem[] | string
+  photoUrls?: string[] | string
+  videoUrls?: string[] | string
+  remark?: string
+  createTime?: string
+}
+
+export interface DailyCheckScheduleToday {
+  schedule: DispatchSchedule | null
+  vehicles: {
+    vehicleId?: number
+    plateNo?: string
+    vehicleType?: string
+    status?: string
+    driverName?: string
+    driverPhone?: string
+  }[]
+}
+
+export function getTodaySchedule(userId: number) {
+  return get<DailyCheckScheduleToday>('/daily-check/schedule-today', { userId })
+}
+
+export function getTodayCheckRecord(userId: number) {
+  return get<DailyCheckRecord | null>('/daily-check/today', { userId })
+}
+
+export function submitDailyCheck(data: DailyCheckRecord) {
+  return post<void>('/daily-check/submit', data)
+}

+ 44 - 0
src/api/knowledge.ts

@@ -0,0 +1,44 @@
+import { get } from './request'
+
+export interface KnowledgeItem {
+  id: number
+  title: string
+  category: string
+  content: string
+  readCount: number
+  createTime: string
+}
+
+export interface KnowledgeCategory {
+  key: string
+  name: string
+}
+
+export function getKnowledgeList(pageNum = 1, pageSize = 10, category?: string) {
+  return get<{ rows: KnowledgeItem[]; total: number }>('/knowledge/list', { pageNum, pageSize, category })
+}
+
+export function getKnowledgeDetail(id: number) {
+  return get<KnowledgeItem>(`/knowledge/${id}`)
+}
+
+export function getKnowledgeCategories() {
+  return get<KnowledgeCategory[]>('/knowledge/categories')
+}
+
+const CATEGORY_NAMES: Record<string, string> = {
+  industry: '行业规范',
+  safety: '安全知识',
+  equipment: '设备操作',
+}
+
+export function categoryName(key?: string): string {
+  if (!key) return '-'
+  return CATEGORY_NAMES[key] || key
+}
+
+export function summaryOf(content?: string, len = 50): string {
+  if (!content) return ''
+  const text = content.replace(/\s+/g, ' ').trim()
+  return text.length > len ? text.slice(0, len) + '...' : text
+}

+ 20 - 12
src/components/home/ConstructionHome.vue

@@ -173,17 +173,20 @@
           <text class="section-more" @click="goToNoticeList">更多</text>
         </view>
         <view
-          v-for="(notice, index) in notices"
-          :key="index"
+          v-for="notice in notices"
+          :key="notice.noticeId"
           class="list-row"
           hover-class="row-hover"
           :hover-start-time="0"
           :hover-stay-time="120"
-          @click="goToNoticeDetail"
+          @click="goToNoticeDetail(notice.noticeId)"
         >
-          <view class="dot mr-3" :class="notice.important ? 'dot-active' : 'dot-muted'"></view>
+          <view class="dot mr-3" :class="notice.priority >= 2 ? 'dot-active' : 'dot-muted'"></view>
           <text class="flex-1 row-title truncate mr-2">{{ notice.title }}</text>
-          <text class="row-date flex-shrink-0">{{ notice.date }}</text>
+          <text class="row-date flex-shrink-0">{{ formatDate(notice.publishTime, 'YYYY-MM-DD') }}</text>
+        </view>
+        <view v-if="notices.length === 0" class="list-row">
+          <text style="font-size: 12px; color: #9ca3af;">暂无通知</text>
         </view>
       </view>
     </view>
@@ -196,6 +199,8 @@ import { useAuthStore } from '@/stores/auth'
 import { useTaskStore } from '@/stores/task'
 import StatusBar from '@/components/common/StatusBar.vue'
 import StatusTag from '@/components/common/StatusTag.vue'
+import { getNoticeList } from '@/api/notice'
+import { formatDate } from '@/utils'
 
 const authStore = useAuthStore()
 const taskStore = useTaskStore()
@@ -225,10 +230,7 @@ const greetText = computed(() => {
   return '晚上好'
 })
 
-const notices = [
-  { title: '关于规范施工现场安全管理的通知', date: '2026-04-20', important: true },
-  { title: '下周安全培训会议安排', date: '2026-04-18', important: false },
-]
+const notices = ref<any[]>([])
 
 function goToTaskList() {
   uni.switchTab({ url: '/pages/task/task' })
@@ -263,13 +265,19 @@ function goToNoticeList() {
   uni.navigateTo({ url: '/subPackages/pages-construction/noticeList' })
 }
 
-function goToNoticeDetail() {
-  uni.navigateTo({ url: '/subPackages/pages-construction/noticeDetail' })
+function goToNoticeDetail(noticeId: number) {
+  uni.navigateTo({ url: `/subPackages/pages-construction/noticeDetail?id=${noticeId}` })
 }
 
-onMounted(() => {
+onMounted(async () => {
   const today = new Date().toISOString().split('T')[0]
   taskStore.fetchMyTasks(today)
+  try {
+    const res = await getNoticeList(1, 3)
+    notices.value = res.rows || []
+  } catch (error) {
+    console.error('获取通知列表失败:', error)
+  }
 })
 </script>
 

+ 36 - 21
src/components/home/SalesHome.vue

@@ -170,16 +170,19 @@
         </view>
         <view
           v-for="(notice, index) in notices"
-          :key="index"
+          :key="notice.noticeId"
           class="list-row"
           hover-class="row-hover"
           :hover-start-time="0"
           :hover-stay-time="120"
-          @click="goToNoticeDetail"
+          @click="goToNoticeDetail(notice.noticeId)"
         >
           <view class="dot mr-3" :class="index === 0 ? 'dot-active' : 'dot-muted'"></view>
           <text class="flex-1 row-title truncate mr-2">{{ notice.title }}</text>
-          <text class="row-date flex-shrink-0">{{ notice.date }}</text>
+          <text class="row-date flex-shrink-0">{{ formatDate(notice.publishTime, 'YYYY-MM-DD') }}</text>
+        </view>
+        <view v-if="notices.length === 0" class="list-row">
+          <text style="font-size: 12px; color: #9ca3af;">暂无通知</text>
         </view>
       </view>
 
@@ -191,22 +194,25 @@
           <text class="section-more" @click="goToKnowledgeList">更多</text>
         </view>
         <view
-          v-for="(item, index) in knowledgeList"
-          :key="index"
+          v-for="item in knowledgeList"
+          :key="item.id"
           class="list-row"
           hover-class="row-hover"
           :hover-start-time="0"
           :hover-stay-time="120"
-          @click="goToKnowledgeDetail"
+          @click="goToKnowledgeDetail(item.id)"
         >
           <view class="icon-chip mr-3">
             <text class="uni-icons uniui-chart chip-glyph"></text>
           </view>
           <view class="flex-1 min-w-0">
             <text class="row-title truncate">{{ item.title }}</text>
-            <text class="row-sub truncate">{{ item.summary }}</text>
+            <text class="row-sub truncate">{{ summaryOf(item.content) }}</text>
           </view>
         </view>
+        <view v-if="knowledgeList.length === 0" class="list-row">
+          <text style="font-size: 12px; color: #9ca3af;">暂无知识内容</text>
+        </view>
       </view>
     </view>
   </view>
@@ -219,6 +225,9 @@ import { useProjectStore } from '@/stores/project'
 import { useTaskStore } from '@/stores/task'
 import StatusBar from '@/components/common/StatusBar.vue'
 import StatusTag from '@/components/common/StatusTag.vue'
+import { getNoticeList } from '@/api/notice'
+import { getKnowledgeList, summaryOf, type KnowledgeItem } from '@/api/knowledge'
+import { formatDate } from '@/utils'
 
 const authStore = useAuthStore()
 const projectStore = useProjectStore()
@@ -238,15 +247,9 @@ const greetText = computed(() => {
   return '晚上好'
 })
 
-const notices = [
-  { title: '关于2024年第一季度销售激励方案的通知', date: '2024-01-15' },
-  { title: '春节期间服务安排调整公告', date: '2024-01-10' },
-]
+const notices = ref<any[]>([])
 
-const knowledgeList = [
-  { title: '2024年清洁行业市场分析报告', summary: '全面解读清洁行业发展趋势、市场规模及竞争格局...' },
-  { title: '客户开发技巧与话术指南', summary: '提升销售业绩的实战技巧,有效沟通方法汇总...' },
-]
+const knowledgeList = ref<KnowledgeItem[]>([])
 
 function goToProjectList() {
   uni.navigateTo({ url: '/subPackages/pages-common/projectList' })
@@ -258,7 +261,7 @@ function goToTaskList() {
 
 function goToTaskDetail(taskId: string) {
   taskStore.setCurrentTask(taskId)
-  uni.navigateTo({ url: '/subPackages/pages-common/taskDetail' })
+  uni.navigateTo({ url: `/subPackages/pages-common/taskDetail?id=${taskId}` })
 }
 
 function goToTaskReminder() {
@@ -273,16 +276,16 @@ function goToNoticeList() {
   uni.navigateTo({ url: '/subPackages/pages-common/noticeList' })
 }
 
-function goToNoticeDetail() {
-  uni.navigateTo({ url: '/subPackages/pages-common/noticeDetail' })
+function goToNoticeDetail(noticeId: number) {
+  uni.navigateTo({ url: `/subPackages/pages-common/noticeDetail?id=${noticeId}` })
 }
 
 function goToKnowledgeList() {
   uni.navigateTo({ url: '/subPackages/pages-common/knowledgeList' })
 }
 
-function goToKnowledgeDetail() {
-  uni.navigateTo({ url: '/subPackages/pages-common/knowledgeDetail' })
+function goToKnowledgeDetail(id: number) {
+  uni.navigateTo({ url: `/subPackages/pages-common/knowledgeDetail?id=${id}` })
 }
 
 function goToCustomer() {
@@ -293,9 +296,21 @@ function goToContract() {
   uni.navigateTo({ url: '/subPackages/pages-contract/contract' })
 }
 
-onMounted(() => {
+onMounted(async () => {
   projectStore.fetchProjects()
   taskStore.fetchTasks()
+  try {
+    const res = await getNoticeList(1, 3)
+    notices.value = res.rows || []
+  } catch (error) {
+    console.error('获取通知列表失败:', error)
+  }
+  try {
+    const res = await getKnowledgeList(1, 2)
+    knowledgeList.value = res.rows || []
+  } catch (error) {
+    console.error('获取知识列表失败:', error)
+  }
 })
 </script>
 

+ 51 - 45
src/components/my/ProfileView.vue

@@ -39,50 +39,22 @@
             <text class="info-label">姓名</text>
             <text class="info-value">{{ userInfo?.name || '-' }}</text>
           </view>
-          <template v-if="role === 'construction'">
-            <view class="info-row">
-              <text class="info-label">班组</text>
-              <text class="info-value">{{ userInfo?.team || '-' }}</text>
-            </view>
-            <view class="info-row">
-              <text class="info-label">职位</text>
-              <text class="info-value">{{ userInfo?.position || '-' }}</text>
-            </view>
-            <view class="info-row">
-              <text class="info-label">工号</text>
-              <text class="info-value">{{ userInfo?.employeeNo || '-' }}</text>
-            </view>
-            <view class="info-row">
-              <text class="info-label">联系电话</text>
-              <text class="info-value">{{ userInfo?.phone || '-' }}</text>
-            </view>
-          </template>
-          <template v-else>
-            <view class="info-row">
-              <text class="info-label">部门</text>
-              <text class="info-value">{{ userInfo?.department || '-' }}</text>
-            </view>
-            <view class="info-row">
-              <text class="info-label">职位</text>
-              <text class="info-value">{{ userInfo?.position || '-' }}</text>
-            </view>
-            <view class="info-row">
-              <text class="info-label">工号</text>
-              <text class="info-value">{{ userInfo?.employeeNo || '-' }}</text>
-            </view>
-            <view class="info-row">
-              <text class="info-label">电话</text>
-              <text class="info-value">{{ userInfo?.phone || '-' }}</text>
-            </view>
-            <view class="info-row">
-              <text class="info-label">邮箱</text>
-              <text class="info-value break-all">{{ userInfo?.email || '-' }}</text>
-            </view>
-            <view class="info-row">
-              <text class="info-label">入职日期</text>
-              <text class="info-value">{{ userInfo?.hireDate || '-' }}</text>
-            </view>
-          </template>
+          <view class="info-row">
+            <text class="info-label">部门</text>
+            <text class="info-value">{{ userInfo?.department || '-' }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">岗位</text>
+            <text class="info-value">{{ userInfo?.postNames || '-' }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">工号</text>
+            <text class="info-value">{{ userInfo?.employeeNo || '-' }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">联系电话</text>
+            <text class="info-value">{{ userInfo?.phone || '-' }}</text>
+          </view>
         </view>
       </view>
 
@@ -157,8 +129,9 @@
 </template>
 
 <script setup lang="ts">
-import { computed } from 'vue'
+import { computed, onMounted } from 'vue'
 import { useAuthStore } from '@/stores/auth'
+import { getTeamSimpleList } from '@/api/team'
 import StatusBar from '@/components/common/StatusBar.vue'
 
 const authStore = useAuthStore()
@@ -190,6 +163,39 @@ const secondaryMeta = computed(() => {
   return userInfo.value?.position || ''
 })
 
+const dutyStatusMap: Record<string, { label: string; color: string }> = {
+  on_duty: { label: '在岗', color: '#22c55e' },
+  in_use: { label: '施工中', color: '#3b82f6' },
+  idle: { label: '空闲', color: '#9ca3af' },
+  rest: { label: '休息', color: '#f59e0b' },
+}
+
+const dutyStatusText = computed(() => {
+  const status = userInfo.value?.dutyStatus
+  return status ? dutyStatusMap[status]?.label || status : '-'
+})
+
+const dutyStatusColor = computed(() => {
+  const status = userInfo.value?.dutyStatus
+  return status ? dutyStatusMap[status]?.color || '#6b7280' : '#6b7280'
+})
+
+onMounted(async () => {
+  const user = userInfo.value
+  if (user?.teamId && !user.team) {
+    try {
+      const teams = await getTeamSimpleList()
+      const team = teams.find((t) => t.value === user.teamId)
+      if (team) {
+        authStore.user = { ...user, team: team.label }
+        uni.setStorageSync('user', JSON.stringify(authStore.user))
+      }
+    } catch {
+      // 忽略失败
+    }
+  }
+})
+
 function goToChangePassword() {
   uni.navigateTo({ url: '/subPackages/pages-common/changePassword' })
 }

+ 83 - 17
src/components/task/ConstructionTaskCard.vue

@@ -40,18 +40,20 @@
     </view>
 
     <view class="pt-3 border-t border-gray-100">
-      <view class="flex items-center justify-between">
+      <view class="flow-row">
         <view
-          v-for="step in steps"
+          v-for="(step, i) in steps"
           :key="step.key"
-          class="flex-1 flex flex-col items-center"
+          class="flow-step"
         >
-          <view
-            class="step-dot w-6 h-6 rounded-full flex items-center justify-center text-xs mb-1"
-            :class="step.active ? 'step-dot-on' : 'step-dot-off'"
-          >
-            {{ step.label }}
+          <view class="flow-node-wrap">
+            <view v-if="i !== 0" class="flow-line" :class="step.active ? 'flow-line-active' : ''" />
+            <view class="flow-node" :class="step.active ? 'flow-node-active' : (step.current ? 'flow-node-current' : '')">
+              <text v-if="step.active" class="uni-icons uniui-checkmarkempty flow-check"></text>
+              <text v-else class="flow-node-text">{{ i + 1 }}</text>
+            </view>
           </view>
+          <text class="flow-label" :class="step.active ? 'flow-label-active' : ''">{{ step.label }}</text>
         </view>
       </view>
     </view>
@@ -73,7 +75,7 @@ defineEmits<{
 
 const steps = computed(() => {
   const status = props.task.status
-  return [
+  const list = [
     { key: 'confirm', label: '确认', active: true },
     { key: 'depart', label: '出车', active: ['departed', 'arrived', 'constructing', 'completed', 'inspecting'].includes(status) },
     { key: 'arrive', label: '到达', active: ['arrived', 'constructing', 'completed', 'inspecting'].includes(status) },
@@ -82,6 +84,8 @@ const steps = computed(() => {
     { key: 'clean', label: '清洗', active: props.task.cleanTime != null || status === 'inspecting' || status === 'completed' },
     { key: 'inspect', label: '验收', active: status === 'inspecting' || status === 'completed' },
   ]
+  const currentIndex = list.findIndex(s => !s.active)
+  return list.map((s, i) => ({ ...s, current: i === currentIndex }))
 })
 </script>
 
@@ -105,18 +109,80 @@ const steps = computed(() => {
   background-color: rgba(164, 216, 152, 0.2);
   color: #368f6f;
 }
-.step-dot {
-  font-weight: 600;
+/* 流程条:与任务详情 SOP 流程条一致 */
+.flow-row {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+}
+.flow-step {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  min-width: 0;
+}
+.flow-node-wrap {
+  width: 100%;
+  height: 24px;
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.flow-line {
+  position: absolute;
+  top: 50%;
+  left: 0;
+  right: 50%;
+  height: 2px;
+  background-color: #e5e7eb;
+  transform: translateY(-50%);
+}
+.flow-line-active {
+  background-color: #368f6f;
+}
+.flow-node {
+  width: 22px;
+  height: 22px;
+  border-radius: 50%;
+  background-color: #f3f4f6;
+  border: 2px solid #e5e7eb;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1;
+}
+.flow-node-active {
+  background-color: #368f6f;
+  border-color: #368f6f;
+}
+.flow-node-current {
+  background-color: #fff;
+  border-color: #368f6f;
+  box-shadow: 0 0 0 3px rgba(54, 143, 111, 0.2);
 }
-.step-dot-on {
-  background: linear-gradient(135deg, #368f6f 0%, #5ab8d0 100%);
-  color: #ffffff;
-  box-shadow: 0 4px 10px -4px rgba(54, 143, 111, 0.5);
+.flow-check {
+  color: #fff;
+  font-size: 11px;
 }
-.step-dot-off {
-  background-color: rgba(164, 216, 152, 0.15);
+.flow-node-text {
+  font-size: 10px;
   color: #9ca3af;
 }
+.flow-node-current .flow-node-text {
+  color: #368f6f;
+  font-weight: 600;
+}
+.flow-label {
+  margin-top: 6px;
+  font-size: 10px;
+  color: #9ca3af;
+  text-align: center;
+}
+.flow-label-active {
+  color: #368f6f;
+}
 .scale-down {
   transition: transform 0.15s ease;
 }

+ 40 - 14
src/stores/auth.ts

@@ -1,7 +1,9 @@
 import { defineStore } from 'pinia'
 import { ref, computed } from 'vue'
 import type { User, UserRole } from '../types'
-import { login, logout, type LoginMenu } from '../api/auth'
+import { login, logout } from '../api/auth'
+import type { LoginMenu } from '../api/auth'
+import { getTeamSimpleList } from '../api/team'
 
 export const useAuthStore = defineStore('auth', () => {
   // State
@@ -25,14 +27,17 @@ export const useAuthStore = defineStore('auth', () => {
   const homePath = computed(() => roleInfo.value?.homePath || '')
   const userEndpoints = computed(() => roleInfo.value?.endpoints || [])
 
-  // 后端角色标识映射到前端业务端
-  const backendRoleToFrontend = (roleKey?: string): UserRole => {
+  // 由账号绑定的可登录端点推导所属业务端
+  const endpointToRole = (endpoints?: string[]): UserRole | null => {
     const map: Record<string, UserRole> = {
-      front: 'sales',
-      dispatch: 'dispatch',
-      construction: 'construction',
+      'mp-sales': 'sales',
+      'mp-dispatch': 'dispatch',
+      'mp-construction': 'construction',
     }
-    return map[roleKey || ''] || 'sales'
+    for (const code of endpoints || []) {
+      if (map[code]) return map[code]
+    }
+    return null
   }
 
   // Actions
@@ -43,20 +48,41 @@ export const useAuthStore = defineStore('auth', () => {
   ) {
     const res = await login({ username, password, roleType: selectedRole })
 
-    const finalRole = backendRoleToFrontend(res.roleKey)
+    let teamName = ''
+    try {
+      const teams = await getTeamSimpleList()
+      const team = teams.find((t) => t.value === res.teamId)
+      teamName = team?.label || ''
+    } catch {
+      teamName = ''
+    }
+
+    const finalRole = endpointToRole(res.endpoints)
+    if (!finalRole) {
+      uni.showToast({ title: '当前账号未配置可登录端点,无法登录', icon: 'none' })
+      throw new Error('当前账号未配置可登录端点')
+    }
 
     const userData: User = {
       id: String(res.userId),
       name: res.nickName || res.userName,
-      phone: res.phonenumber || '',
+      phone: res.phonenumber || res.phone || '',
       avatar: res.avatar,
       role: finalRole,
-      department: res.dept?.deptName || '',
-      position: '',
-      employeeNo: '',
+      department: res.deptName || res.dept?.deptName || '',
+      deptId: res.deptId || res.dept?.deptId,
+      employeeNo: res.staffCode || res.employeeNo || '',
       email: res.email || '',
-      hireDate: '',
-      team: '',
+      hireDate: res.entryDate || res.hireDate || '',
+      team: teamName || res.teamName || res.team || '',
+      teamId: res.teamId,
+      postNames: res.postNames || '',
+      dutyStatus: res.dutyStatus || '',
+      skillLevel: res.skillLevel || '',
+      certifications: res.certifications || '',
+      tags: res.tags || '',
+      skillTags: res.skillTags || '',
+      currentTaskId: res.currentTaskId,
       status: 'active',
     }
 

+ 2 - 11
src/stores/common.ts

@@ -1,28 +1,19 @@
 import { defineStore } from 'pinia'
 import { ref } from 'vue'
-import type { Knowledge, FAQ } from '@/types'
-import { mockKnowledges, mockFAQs } from '@/mock'
+import type { FAQ } from '@/types'
+import { mockFAQs } from '@/mock'
 
 export const useCommonStore = defineStore('common', () => {
-  const knowledges = ref<Knowledge[]>([...mockKnowledges])
   const faqs = ref<FAQ[]>([...mockFAQs])
-  const currentKnowledge = ref<Knowledge | null>(null)
   const currentFaq = ref<FAQ | null>(null)
 
-  function setCurrentKnowledge(id: string) {
-    currentKnowledge.value = knowledges.value.find((k) => k.id === id) || null
-  }
-
   function setCurrentFaq(id: string) {
     currentFaq.value = faqs.value.find((f) => f.id === id) || null
   }
 
   return {
-    knowledges,
     faqs,
-    currentKnowledge,
     currentFaq,
-    setCurrentKnowledge,
     setCurrentFaq,
   }
 })

+ 15 - 18
src/subPackages/pages-common/knowledgeDetail.vue

@@ -15,11 +15,6 @@
             <text class="meta-text">{{ knowledge?.createTime ? formatDate(knowledge.createTime, 'YYYY-MM-DD') : '' }}</text>
           </view>
 
-          <view class="summary-box">
-            <text class="summary-title">摘要</text>
-            <text class="summary-text">{{ knowledge?.summary || '暂无摘要' }}</text>
-          </view>
-
           <view class="content-box">
             <text class="summary-title">内容</text>
             <text class="summary-text pre-wrap">{{ knowledge?.content || '暂无内容' }}</text>
@@ -27,7 +22,7 @@
 
           <view class="meta-line">
             <text class="meta-label">分类</text>
-            <text class="type-tag">{{ knowledge?.category || '-' }}</text>
+            <text class="type-tag">{{ categoryName(knowledge?.category) }}</text>
           </view>
         </view>
       </view>
@@ -36,27 +31,29 @@
 </template>
 
 <script setup lang="ts">
-import { ref, computed, onMounted } from 'vue'
+import { ref, computed } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
 import TopBar from '@/components/common/TopBar.vue'
+import { getKnowledgeDetail, categoryName, type KnowledgeItem } from '@/api/knowledge'
 import { formatDate } from '@/utils'
-import type { Knowledge } from '@/types'
 
-const knowledge = ref<Knowledge | null>(null)
+const knowledge = ref<KnowledgeItem | null>(null)
 
 const icon = computed(() => {
-  const category = knowledge.value?.category || ''
   const iconMap: Record<string, string> = {
-    '安全规范': '️',
-    '操作指南': '',
-    '设备维护': '',
+    industry: '',
+    safety: '️',
+    equipment: '',
   }
-  return iconMap[category] || ''
+  return iconMap[knowledge.value?.category || ''] || ''
 })
 
-onMounted(() => {
-  const data = uni.getStorageSync('current_knowledge')
-  if (data) {
-    knowledge.value = data
+onLoad(async (options) => {
+  if (!options?.id) return
+  try {
+    knowledge.value = await getKnowledgeDetail(Number(options.id))
+  } catch (error) {
+    console.error('获取知识详情失败:', error)
   }
 })
 </script>

+ 25 - 12
src/subPackages/pages-common/knowledgeList.vue

@@ -3,7 +3,11 @@
     <TopBar title="行业知识" show-back />
 
     <view class="px-4 pt-3">
-      <view v-if="knowledges.length === 0">
+      <view v-if="loading" class="py-10 text-center">
+        <text style="font-size: 13px; color: #9ca3af;">加载中...</text>
+      </view>
+
+      <view v-else-if="knowledges.length === 0">
         <EmptyState message="暂无知识内容" />
       </view>
 
@@ -22,7 +26,7 @@
           </view>
           <view class="flex-1 min-w-0 mr-2">
             <text class="row-title truncate">{{ item.title }}</text>
-            <text class="row-sub row-clamp">{{ item.summary }}</text>
+            <text class="row-sub row-clamp">{{ summaryOf(item.content) }}</text>
             <text class="row-date">{{ formatDate(item.createTime, 'YYYY-MM-DD') }}</text>
           </view>
           <text class="uni-icons uniui-arrowright row-arrow"></text>
@@ -33,21 +37,30 @@
 </template>
 
 <script setup lang="ts">
-import { computed } from 'vue'
+import { ref, onMounted } from 'vue'
 import TopBar from '@/components/common/TopBar.vue'
 import EmptyState from '@/components/common/EmptyState.vue'
-import { useCommonStore } from '@/stores/common'
-import { formatDate, navigateTo } from '@/utils'
-import type { Knowledge } from '@/types'
+import { getKnowledgeList, summaryOf, type KnowledgeItem } from '@/api/knowledge'
+import { formatDate } from '@/utils'
 
-const commonStore = useCommonStore()
-const knowledges = computed(() => commonStore.knowledges)
+const loading = ref(false)
+const knowledges = ref<KnowledgeItem[]>([])
 
-function goToDetail(item: Knowledge) {
-  commonStore.setCurrentKnowledge(item.id)
-  uni.setStorageSync('current_knowledge', item)
-  navigateTo('/subPackages/pages-common/knowledgeDetail')
+function goToDetail(item: KnowledgeItem) {
+  uni.navigateTo({ url: `/subPackages/pages-common/knowledgeDetail?id=${item.id}` })
 }
+
+onMounted(async () => {
+  loading.value = true
+  try {
+    const res = await getKnowledgeList(1, 50)
+    knowledges.value = res.rows || []
+  } catch (error) {
+    console.error('获取知识列表失败:', error)
+  } finally {
+    loading.value = false
+  }
+})
 </script>
 
 <style scoped>

+ 179 - 40
src/subPackages/pages-common/taskDetail.vue

@@ -2,26 +2,30 @@
   <view class="sub-page">
     <TopBar title="任务详情" show-back />
 
-    <view class="px-4 pt-3">
+    <view v-if="loading" class="py-10 text-center">
+      <text class="empty-text">加载中...</text>
+    </view>
+
+    <view v-else-if="task" class="px-4 pt-3">
       <!-- 任务状态卡片 -->
       <view class="glass-card p-4">
         <view class="flex justify-between items-start mb-3">
           <view class="flex-1 min-w-0 mr-2">
-            <text class="notice-title">{{ task.projectName }}</text>
+            <text class="notice-title">{{ taskTitle }}</text>
             <view class="mt-2 flex items-center">
               <StatusTag :status="task.status" />
-              <text v-if="task.isEmergency" class="emergency-tag ml-2">应急</text>
+              <text v-if="isEmergency" class="emergency-tag ml-2">应急</text>
             </view>
           </view>
           <text class="meta-text flex-shrink-0">{{ task.taskNo }}</text>
         </view>
-        <view class="meta-line mb-2">
+        <view v-if="task.address" class="meta-line mb-2">
           <text class="uni-icons uniui-location-filled meta-icon mr-2"></text>
           <text class="meta-value flex-1">{{ task.address }}</text>
         </view>
         <view class="meta-line">
           <text class="uni-icons uniui-clock-filled meta-icon mr-2"></text>
-          <text class="meta-value">{{ task.scheduleDate }} {{ task.scheduleTime }}</text>
+          <text class="meta-value">{{ serviceTimeText }}</text>
         </view>
       </view>
 
@@ -33,48 +37,57 @@
         </view>
         <view class="info-row">
           <text class="info-label">服务类型</text>
-          <text class="info-value">{{ task.serviceType }}</text>
+          <text class="info-value">{{ constructionTypeText }}</text>
         </view>
         <view class="info-row">
           <text class="info-label">客户名称</text>
-          <text class="info-value">{{ task.customerName }}</text>
+          <text class="info-value">{{ customerName }}</text>
         </view>
         <view class="info-row">
           <text class="info-label">联系电话</text>
-          <text class="info-value">{{ task.customerPhone }}</text>
+          <view class="flex items-center">
+            <text class="info-value mr-2">{{ customerPhone || '-' }}</text>
+            <button
+              v-if="customerPhone"
+              class="phone-circle"
+              @click="callPhone(customerPhone)"
+            >
+              <text class="uni-icons uniui-phone-filled phone-icon"></text>
+            </button>
+          </view>
         </view>
         <view class="info-row">
           <text class="info-label">项目地址</text>
-          <text class="info-value info-value-left">{{ task.address }}</text>
+          <text class="info-value info-value-left">{{ task.address || '-' }}</text>
         </view>
         <view class="info-row">
           <text class="info-label">预约时间</text>
-          <text class="info-value">{{ task.scheduleDate }} {{ task.scheduleTime }}</text>
+          <text class="info-value">{{ serviceTimeText }}</text>
         </view>
         <view class="info-row">
           <text class="info-label">紧急程度</text>
-          <text class="info-value" :class="task.isEmergency ? 'urgent-text' : 'normal-text'">
-            {{ task.isEmergency ? '紧急' : '普通' }}
+          <text class="info-value" :class="isEmergency ? 'urgent-text' : 'normal-text'">
+            {{ urgencyText }}
           </text>
         </view>
       </view>
 
       <!-- 服务点信息 -->
-      <view v-if="task.servicePoints && task.servicePoints.length > 0" class="glass-card">
+      <view v-if="servicePoints.length > 0" class="glass-card">
         <view class="section-head">
           <view class="section-bar mr-2"></view>
           <text class="section-title flex-1">服务点信息</text>
         </view>
         <view
-          v-for="(point, index) in task.servicePoints"
+          v-for="(point, index) in servicePoints"
           :key="index"
           class="point-row"
         >
           <view class="flex justify-between items-center mb-2">
             <text class="point-name">{{ point.name }}</text>
-            <text class="type-tag">{{ point.serviceType }}</text>
+            <text v-if="point.serviceType" class="type-tag">{{ point.serviceType }}</text>
           </view>
-          <view class="flex items-center">
+          <view v-if="point.address" class="flex items-center">
             <text class="uni-icons uniui-location-filled meta-icon mr-1"></text>
             <text class="meta-text">{{ point.address }}</text>
           </view>
@@ -92,43 +105,148 @@
         </view>
       </view>
     </view>
+
+    <EmptyState v-else message="任务不存在" />
   </view>
 </template>
 
 <script setup lang="ts">
 import { ref, computed } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
 import { useTaskStore } from '@/stores/task'
 import TopBar from '@/components/common/TopBar.vue'
 import StatusTag from '@/components/common/StatusTag.vue'
+import EmptyState from '@/components/common/EmptyState.vue'
 import Timeline from '@/components/common/Timeline.vue'
 import type { TimelineRecord } from '@/components/common/Timeline.vue'
+import { getTaskDetail } from '@/api/task'
+import { getProjectDetail } from '@/api/project'
+import { formatDate } from '@/utils'
+import type { ServicePoint } from '@/types'
 
 const taskStore = useTaskStore()
-const task = computed(() => taskStore.currentTask || {})
-
-const timelineRecords = ref<TimelineRecord[]>([
-  {
-    action: '任务已创建,等待调度',
-    time: '2026-04-20 09:00',
-    operator: '销售-李明',
-    role: 'sales',
-    isCurrent: false
-  },
-  {
-    action: '任务已排班,分配工程一班',
-    time: '2026-04-20 10:30',
-    operator: '调度-王调度',
-    role: 'dispatch',
-    isCurrent: false
-  },
-  {
-    action: '工程人员已出发',
-    time: '2026-04-20 11:00',
-    operator: '施工-张师傅',
-    role: 'worker',
-    isCurrent: true
+
+const task = ref<any>(null)
+const project = ref<any>(null)
+const loading = ref(false)
+
+const extra = computed(() => {
+  if (!task.value?.extraData) return {} as any
+  try {
+    return JSON.parse(task.value.extraData)
+  } catch {
+    return {} as any
+  }
+})
+
+const taskTitle = computed(() => {
+  const t = task.value
+  if (!t) return ''
+  return t.taskName || extra.value.projectName || t.faultLocation || '未命名任务'
+})
+
+const isEmergency = computed(() => {
+  const t = task.value
+  if (!t) return false
+  return t.taskType === 'emergency' || ['urgent', 'very_urgent'].includes(t.urgencyLevel)
+})
+
+const urgencyText = computed(() => {
+  const map: Record<string, string> = { normal: '普通', urgent: '紧急', very_urgent: '特急' }
+  const level = task.value?.urgencyLevel
+  return level ? map[level] || level : '普通'
+})
+
+const constructionTypeText = computed(() => {
+  const t = task.value
+  if (!t) return '-'
+  const map: Record<string, string> = { dredge: '疏通', clean: '清掏', inspect: '排查', repair: '维修', emergency: '应急' }
+  return t.constructionType || (t.taskType ? map[t.taskType] || t.taskType : '-')
+})
+
+const serviceTimeText = computed(() => {
+  const t = task.value
+  if (!t) return '待定'
+  const date = t.planDate || ''
+  const rangeMap: Record<string, string> = { morning: '上午', afternoon: '下午', evening: '晚上' }
+  const range = rangeMap[t.planTimeRange] || t.planTimeRange || ''
+  return date || range ? `${date} ${range}`.trim() : '待定'
+})
+
+const customerName = computed(() => {
+  return project.value?.customerName || extra.value.customerName || project.value?.primaryContactName || '-'
+})
+
+const customerPhone = computed(() => {
+  return project.value?.customerPhone || extra.value.customerPhone || project.value?.primaryContactPhone || ''
+})
+
+const servicePoints = computed<ServicePoint[]>(() => {
+  const raw = project.value?.servicePoints
+  if (!raw) return []
+  try {
+    const list = typeof raw === 'string' ? JSON.parse(raw) : raw
+    return Array.isArray(list) ? list : []
+  } catch {
+    return []
+  }
+})
+
+const timelineRecords = computed<TimelineRecord[]>(() => {
+  const logs: any[] = task.value?.logList || []
+  return logs.map(log => ({
+    action: log.operation || log.action || '状态更新',
+    time: log.createTime ? formatDate(log.createTime, 'YYYY-MM-DD HH:mm') : '',
+    operator: log.operatorName || '系统',
+    role: log.operatorRole || roleFromOperationType(log.operationType),
+  }))
+})
+
+function roleFromOperationType(type: string): string {
+  const map: Record<string, string> = {
+    create: 'sales',
+    audit: 'dispatch',
+    dispatch: 'dispatch',
+    urge: 'sales',
+    cancel: 'sales',
+  }
+  return map[type] || 'construction'
+}
+
+function callPhone(phone: string) {
+  if (!phone) return
+  uni.makePhoneCall({ phoneNumber: phone })
+}
+
+onLoad((options) => {
+  if (options?.id) {
+    loadDetail(Number(options.id))
+  } else if (taskStore.currentTask) {
+    // 兼容未携带 id 的入口:使用列表中已加载的任务数据
+    task.value = taskStore.currentTask
+    loadProject(taskStore.currentTask.projectId)
   }
-])
+})
+
+async function loadDetail(taskId: number) {
+  loading.value = true
+  try {
+    const res: any = await getTaskDetail(taskId)
+    task.value = res
+    loadProject(res?.projectId)
+  } catch (e) {
+    console.error('加载任务详情失败', e)
+  } finally {
+    loading.value = false
+  }
+}
+
+function loadProject(projectId: number | string | undefined) {
+  if (!projectId) return
+  getProjectDetail(projectId)
+    .then(p => { project.value = p })
+    .catch(() => {})
+}
 </script>
 
 <style scoped>
@@ -260,4 +378,25 @@ const timelineRecords = ref<TimelineRecord[]>([
   color: #368f6f;
   background-color: rgba(164, 216, 152, 0.24);
 }
+
+.phone-circle {
+  width: 28px;
+  height: 28px;
+  border-radius: 50%;
+  flex-shrink: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background-color: rgba(164, 216, 152, 0.24);
+}
+
+.phone-icon {
+  font-size: 13px;
+  color: #368f6f;
+}
+
+.empty-text {
+  font-size: 13px;
+  color: #9ca3af;
+}
 </style>

+ 259 - 39
src/subPackages/pages-construction/dailyTask.vue

@@ -23,16 +23,62 @@
         </view>
       </view>
 
+      <!-- 今日排班 -->
+      <view v-if="schedule" class="glass-card p-4">
+        <view class="section-head-inner mb-3">
+          <view class="section-bar mr-2"></view>
+          <text class="section-title flex-1">今日排班</text>
+        </view>
+        <view class="info-row">
+          <text class="info-label">班组</text>
+          <text class="info-value">{{ schedule.teamName || '-' }}</text>
+        </view>
+        <view class="info-row">
+          <text class="info-label">班次</text>
+          <text class="info-value">{{ shiftTypeLabel(schedule.shiftType) }}</text>
+        </view>
+        <view class="info-row">
+          <text class="info-label">分配车辆</text>
+          <text class="info-value">{{ vehiclePlateNos || '未分配车辆' }}</text>
+        </view>
+      </view>
+      <view v-else-if="!scheduleLoading" class="glass-card p-4">
+        <view class="empty-wrap">
+          <text class="empty-title">今日无有效排班</text>
+          <text class="empty-text">当前账号所属班组今天没有排班记录,无法选择检查车辆。</text>
+        </view>
+      </view>
+
       <!-- 车辆检查 -->
       <view class="glass-card p-4">
         <view class="section-head-inner mb-3">
           <view class="section-bar mr-2"></view>
           <text class="section-title flex-1">车辆检查</text>
-          <text v-if="vehicleCheckStatus" class="check-state check-state-done">已完成</text>
+          <text v-if="todayRecord" class="check-state check-state-done">已完成</text>
           <text v-else class="check-state check-state-pending">待检查</text>
         </view>
 
-        <view v-if="!vehicleCheckStatus">
+        <view v-if="!todayRecord">
+          <view class="form-row mb-3">
+            <text class="form-label">检查车辆</text>
+            <picker
+              v-if="vehicles.length > 0"
+              mode="selector"
+              :range="vehicleOptions"
+              range-key="label"
+              :value="selectedVehicleIndex"
+              @change="onVehicleChange"
+            >
+              <view class="picker-value">
+                <text>{{ selectedVehicle ? selectedVehicle.label : '请选择检查车辆' }}</text>
+                <text class="uni-icons uniui-arrowright arrow-icon"></text>
+              </view>
+            </picker>
+            <view v-else class="picker-value disabled">
+              <text>{{ schedule ? '今日未分配车辆' : '无排班信息' }}</text>
+            </view>
+          </view>
+
           <view class="check-group">
             <text class="check-group-title">常规检查</text>
             <view
@@ -93,10 +139,10 @@
             hover-class="submit-btn-hover"
             :hover-start-time="0"
             :hover-stay-time="120"
-            :disabled="!vehicleCheckStatus"
+            :disabled="!canSubmit || submitting"
             @click="submitCheck"
           >
-            提交检查
+            {{ submitting ? '提交中...' : '提交检查' }}
           </button>
         </view>
 
@@ -105,6 +151,7 @@
             <text class="uni-icons uniui-checkmarkempty done-tick"></text>
           </view>
           <text class="done-text">今日车辆检查已完成</text>
+          <text v-if="todayRecord.vehicleNo" class="done-meta">检查车辆:{{ todayRecord.vehicleNo }}</text>
           <text v-if="remark" class="done-meta">备注:{{ remark }}</text>
           <text class="done-meta">{{ today }}</text>
         </view>
@@ -150,48 +197,81 @@
 <script setup lang="ts">
 import { ref, computed, onMounted } from 'vue'
 import { useTaskStore } from '../../stores/task'
+import { useAuthStore } from '../../stores/auth'
+import {
+  getTodaySchedule,
+  getTodayCheckRecord,
+  submitDailyCheck,
+  type DailyCheckScheduleToday,
+  type DailyCheckRecord,
+  type DailyCheckItem,
+} from '../../api/dailyCheck'
 import TopBar from '../../components/common/TopBar.vue'
 import StatusTag from '../../components/common/StatusTag.vue'
 import EmptyState from '../../components/common/EmptyState.vue'
 
 const taskStore = useTaskStore()
+const authStore = useAuthStore()
 
 const today = new Date().toISOString().split('T')[0]
-const storageKey = `vehicle_check_${today}`
 const remarkKey = `vehicle_check_remark_${today}`
 
 const checkItems = [
-  { key: 'exterior', label: '车辆外观检查' },
-  { key: 'tire', label: '轮胎气压检查' },
-  { key: 'fluid', label: '油液检查' },
-  { key: 'brake', label: '刹车系统检查' },
-  { key: 'safety', label: '安全设备检查' },
+  { key: 'v1', label: '车辆外观无损坏' },
+  { key: 'v2', label: '轮胎气压正常' },
+  { key: 'v3', label: '机油液位正常' },
+  { key: 'v4', label: '冷却液液位正常' },
+  { key: 'v5', label: '灯光系统正常' },
+  { key: 'v6', label: '刹车系统正常' },
+  { key: 'v7', label: '车辆清洁' },
 ]
 
 const equipmentItems = [
-  { key: 'pump', label: '抽水泵检查' },
-  { key: 'hose', label: '高压水管检查' },
-  { key: 'tool', label: '施工工具检查' },
-  { key: 'protective', label: '防护用品检查' },
+  { key: 'e1', label: '高压清洗设备' },
+  { key: 'e2', label: '吸污设备' },
+  { key: 'e3', label: '疏通设备' },
+  { key: 'e4', label: '安全防护设备' },
+  { key: 'e5', label: '应急工具包' },
+  { key: 'e6', label: '通讯设备' },
 ]
 
 const systemItems = [
-  { key: 'power', label: '动力系统' },
-  { key: 'hydraulic', label: '液压系统' },
-  { key: 'electrical', label: '电气系统' },
+  { key: 's1', label: 'GPS定位系统' },
+  { key: 's2', label: '视频监控系统' },
+  { key: 's3', label: '对讲系统' },
 ]
 
 const allItems = [...checkItems, ...equipmentItems, ...systemItems]
 
-const initialChecks: Record<string, boolean> = {}
-allItems.forEach((item) => {
-  initialChecks[item.key] = false
+const scheduleLoading = ref(false)
+const submitting = ref(false)
+const schedule = ref<DailyCheckScheduleToday['schedule']>(null)
+const vehicles = ref<DailyCheckScheduleToday['vehicles']>([])
+const selectedVehicleIndex = ref(0)
+const checks = ref<Record<string, boolean>>({})
+const remark = ref('')
+const todayRecord = ref<DailyCheckRecord | null>(null)
+
+const userId = computed(() => authStore.user?.id ? Number(authStore.user.id) : 0)
+const userName = computed(() => authStore.user?.name || '')
+
+const vehicleOptions = computed(() => {
+  return vehicles.value.map((v) => ({
+    value: v.vehicleId,
+    label: `${v.plateNo} ${v.vehicleType ? `(${v.vehicleType})` : ''}`,
+  }))
 })
 
-const checks = ref<Record<string, boolean>>({ ...initialChecks })
-const remark = ref('')
+const selectedVehicle = computed(() => {
+  return vehicleOptions.value[selectedVehicleIndex.value] || null
+})
+
+const vehiclePlateNos = computed(() => {
+  return vehicles.value.map((v) => v.plateNo).filter(Boolean).join('、')
+})
 
-const vehicleCheckStatus = computed(() => {
+const canSubmit = computed(() => {
+  if (!selectedVehicle.value) return false
   return allItems.every((item) => checks.value[item.key])
 })
 
@@ -205,19 +285,90 @@ const todayTasks = computed<any[]>(() => {
   )
 })
 
+function shiftTypeLabel(shiftType?: string) {
+  const map: Record<string, string> = {
+    day: '白班',
+    night: '夜班',
+    emergency: '应急班',
+    month_plan: '月计划',
+  }
+  return map[shiftType || ''] || shiftType || '-'
+}
+
 function toggleCheck(type: string) {
+  if (todayRecord.value) return
   checks.value[type] = !checks.value[type]
-  uni.setStorageSync(storageKey, JSON.stringify(checks.value))
 }
 
-function submitCheck() {
-  if (!vehicleCheckStatus.value) {
-    uni.showToast({ title: '请完成所有检查项', icon: 'none' })
+async function loadSchedule() {
+  if (!userId.value) return
+  scheduleLoading.value = true
+  try {
+    const data = await getTodaySchedule(userId.value)
+    schedule.value = data.schedule
+    vehicles.value = data.vehicles || []
+    selectedVehicleIndex.value = 0
+  } catch (error) {
+    console.error('加载排班失败', error)
+  } finally {
+    scheduleLoading.value = false
+  }
+}
+
+async function loadTodayRecord() {
+  if (!userId.value) return
+  try {
+    const data = await getTodayCheckRecord(userId.value)
+    todayRecord.value = data || null
+  } catch (error) {
+    console.error('加载今日检查记录失败', error)
+  }
+}
+
+function onVehicleChange(e: any) {
+  selectedVehicleIndex.value = e.detail.value || 0
+}
+
+async function submitCheck() {
+  if (!canSubmit.value) {
+    uni.showToast({ title: '请选择车辆并完成所有检查项', icon: 'none' })
     return
   }
-  uni.setStorageSync(storageKey, JSON.stringify(checks.value))
-  uni.setStorageSync(remarkKey, remark.value)
-  uni.showToast({ title: '提交成功', icon: 'success' })
+  if (!selectedVehicle.value) {
+    uni.showToast({ title: '请选择检查车辆', icon: 'none' })
+    return
+  }
+
+  const checkItemsData: DailyCheckItem[] = allItems.map((item) => ({
+    id: item.key,
+    category: item.key.startsWith('v') ? 'vehicle' : item.key.startsWith('e') ? 'equipment' : 'system',
+    name: item.label,
+    required: true,
+    checked: !!checks.value[item.key],
+  }))
+
+  const payload: DailyCheckRecord = {
+    checkDate: today,
+    vehicleId: selectedVehicle.value.value,
+    vehicleNo: selectedVehicle.value.label.split(' ')[0],
+    userId: userId.value,
+    userName: userName.value,
+    checkItems: checkItemsData,
+    remark: remark.value,
+  }
+
+  submitting.value = true
+  try {
+    await submitDailyCheck(payload)
+    uni.setStorageSync(remarkKey, remark.value)
+    uni.showToast({ title: '提交成功', icon: 'success' })
+    await loadTodayRecord()
+  } catch (error) {
+    console.error('提交检查失败', error)
+    uni.showToast({ title: '提交失败', icon: 'none' })
+  } finally {
+    submitting.value = false
+  }
 }
 
 function goToDetail(taskId: string | number) {
@@ -226,19 +377,17 @@ function goToDetail(taskId: string | number) {
 }
 
 onMounted(() => {
-  const saved = uni.getStorageSync(storageKey)
-  if (saved) {
-    try {
-      checks.value = JSON.parse(saved)
-    } catch {
-      // 解析失败则使用默认值
-    }
-  }
+  allItems.forEach((item) => {
+    checks.value[item.key] = false
+  })
+
   const savedRemark = uni.getStorageSync(remarkKey)
   if (savedRemark) {
     remark.value = savedRemark
   }
 
+  loadSchedule()
+  loadTodayRecord()
   taskStore.fetchMyTasks(today)
 })
 </script>
@@ -327,6 +476,45 @@ onMounted(() => {
   color: #6b7280;
 }
 
+/* 排班信息 */
+.info-row {
+  display: flex;
+  align-items: center;
+  padding: 6px 0;
+}
+
+.info-label {
+  width: 72px;
+  font-size: 13px;
+  color: #6b7280;
+}
+
+.info-value {
+  flex: 1;
+  font-size: 14px;
+  color: #1f2937;
+  font-weight: 500;
+}
+
+.empty-wrap {
+  padding: 12px 0;
+  text-align: center;
+}
+
+.empty-title {
+  display: block;
+  font-size: 14px;
+  color: #b7791f;
+  font-weight: 600;
+}
+
+.empty-text {
+  display: block;
+  margin-top: 4px;
+  font-size: 12px;
+  color: #9ca3af;
+}
+
 /* 检查项 */
 .check-state {
   font-size: 12px;
@@ -341,6 +529,38 @@ onMounted(() => {
   color: #b7791f;
 }
 
+.form-row {
+  display: flex;
+  align-items: center;
+  padding: 8px 0 16px;
+  border-bottom: 1px solid rgba(164, 216, 152, 0.18);
+  margin-bottom: 12px;
+}
+
+.form-label {
+  width: 72px;
+  font-size: 14px;
+  color: #4b5563;
+}
+
+.picker-value {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 14px;
+  color: #1f2937;
+}
+
+.picker-value.disabled {
+  color: #9ca3af;
+}
+
+.arrow-icon {
+  font-size: 14px;
+  color: #9ca3af;
+}
+
 .check-group {
   margin-bottom: 12px;
   padding-top: 12px;
@@ -482,7 +702,7 @@ onMounted(() => {
 }
 
 .row-hover {
-  background-color: rgba(164, 216, 152, 0.1);
+  background-color: rgba(164, 216, 152, 0.3);
 }
 
 .icon-chip {

+ 12 - 46
src/subPackages/pages-construction/noticeDetail.vue

@@ -6,23 +6,15 @@
       <view v-if="notice" class="glass-card p-5">
         <view class="flex items-start justify-between mb-3">
           <text class="notice-title flex-1 mr-2">{{ notice.title }}</text>
-          <text v-if="notice.important" class="important-tag flex-shrink-0">重要</text>
+          <text v-if="notice.priority >= 2" class="important-tag flex-shrink-0">重要</text>
         </view>
         <view class="meta-row mb-4">
-          <text class="meta-text">{{ formatDate(notice.createTime, 'YYYY-MM-DD') }}</text>
+          <text class="meta-text">{{ formatDate(notice.publishTime, 'YYYY-MM-DD') }}</text>
           <text class="meta-text mx-2">|</text>
-          <text class="meta-text">调度中心</text>
+          <text class="meta-text">{{ notice.publisherName }}</text>
         </view>
         <view class="notice-body">
-          <text class="body-p">各位施工同事:</text>
-          <text class="body-p">{{ notice.summary }}</text>
-          <text class="body-item">一、进入施工现场必须佩戴安全帽、安全背心等防护用品。</text>
-          <text class="body-item">二、施工前必须进行安全检查,确保设备正常运转。</text>
-          <text class="body-item">三、施工现场必须设置警示标志,做好安全防护措施。</text>
-          <text class="body-item">四、发现安全隐患及时上报,不得隐瞒。</text>
-          <text class="body-p">请各位同事严格遵守以上规定,确保施工安全。</text>
-          <text class="body-sign">调度中心</text>
-          <text class="body-sign">{{ formatDate(notice.createTime, 'YYYY年MM月DD日') }}</text>
+          <text class="body-p">{{ notice.content }}</text>
         </view>
       </view>
       <EmptyState v-else message="公告不存在" icon="" />
@@ -35,43 +27,17 @@ import { ref } from 'vue'
 import { onLoad } from '@dcloudio/uni-app'
 import TopBar from '../../components/common/TopBar.vue'
 import EmptyState from '../../components/common/EmptyState.vue'
+import { getNoticeDetail } from '../../api/notice'
 import { formatDate } from '../../utils'
 
-interface Notice {
-  id: string
-  title: string
-  summary: string
-  createTime: string
-  important?: boolean
-}
-
-const notice = ref<Notice | null>(null)
-
-const allNotices: Notice[] = [
-  {
-    id: 'n1',
-    title: '关于规范施工现场安全管理的通知',
-    summary: '为进一步规范施工现场安全管理,确保施工人员人身安全和施工质量,现就相关事项通知如下:',
-    createTime: '2026-04-20T09:00:00',
-    important: true,
-  },
-  {
-    id: 'n2',
-    title: '下周安全培训会议安排',
-    summary: '各位同事,下周二将举行安全培训会议,请准时参加。',
-    createTime: '2026-04-18T10:00:00',
-  },
-  {
-    id: 'n3',
-    title: '设备维护保养规范更新',
-    summary: '最新设备维护保养规范已更新,请各位同事查阅学习。',
-    createTime: '2026-04-15T14:00:00',
-  },
-]
+const notice = ref<any>(null)
 
-onLoad((options) => {
-  if (options?.id) {
-    notice.value = allNotices.find(n => n.id === options.id) || null
+onLoad(async (options) => {
+  if (!options?.id) return
+  try {
+    notice.value = await getNoticeDetail(Number(options.id))
+  } catch (error) {
+    console.error('获取通知详情失败:', error)
   }
 })
 </script>

+ 9 - 19
src/subPackages/pages-dispatch/noticeDetail.vue

@@ -6,7 +6,7 @@
       <view v-if="notice" class="glass-card p-5">
         <text class="notice-title">{{ notice.title }}</text>
         <view class="meta-row">
-          <text class="meta-text">{{ formatDate(notice.createTime, 'YYYY-MM-DD HH:mm') }}</text>
+          <text class="meta-text">{{ notice.publisherName }} · {{ formatDate(notice.publishTime, 'YYYY-MM-DD HH:mm') }}</text>
         </view>
         <view class="notice-body">
           <text class="body-p">{{ notice.content }}</text>
@@ -23,27 +23,17 @@ import { ref } from 'vue'
 import { onLoad } from '@dcloudio/uni-app'
 import TopBar from '../../components/common/TopBar.vue'
 import EmptyState from '../../components/common/EmptyState.vue'
+import { getNoticeDetail } from '../../api/notice'
 import { formatDate } from '../../utils'
 
-interface Notice {
-  id: string
-  title: string
-  content: string
-  createTime: string
-}
-
-const allNotices: Notice[] = [
-  { id: 'n1', title: '关于调度系统升级的通知', content: '为了提升系统性能和用户体验,我们将于本周末对调度系统进行升级维护。升级期间系统可能短暂不可用,请提前做好工作安排。升级后新增排班自动提醒功能,敬请期待。', createTime: '2026-01-20T09:00:00' },
-  { id: 'n2', title: '春节期间值班安排公告', content: '春节将至,为确保节日期间服务不中断,请各班组做好值班安排。具体值班表已发送至各班组负责人,如有疑问请联系调度中心。祝大家新春快乐!', createTime: '2026-01-15T09:00:00' },
-  { id: 'n3', title: '安全生产月活动通知', content: '本月为安全生产月,公司将开展安全培训和应急演练。请所有员工参加,提升安全意识和应急处理能力。具体安排另行通知。', createTime: '2026-02-28T10:00:00' },
-  { id: 'n4', title: '新员工培训计划安排', content: '本月将有新员工入职,请各班组安排老员工进行带教。培训内容包括:系统操作、安全规范、服务流程等。请各班组提前做好准备。', createTime: '2026-03-10T14:00:00' },
-]
-
-const notice = ref<Notice | null>(null)
+const notice = ref<any>(null)
 
-onLoad((options) => {
-  if (options?.id) {
-    notice.value = allNotices.find(n => n.id === options.id) || null
+onLoad(async (options) => {
+  if (!options?.id) return
+  try {
+    notice.value = await getNoticeDetail(Number(options.id))
+  } catch (error) {
+    console.error('获取通知详情失败:', error)
   }
 })
 </script>

+ 12 - 46
src/subPackages/pages-sales/knowledgeDetail.vue

@@ -7,14 +7,14 @@
       <view class="glass-card p-5">
         <text class="notice-title">{{ knowledge.title }}</text>
         <view class="flex items-center mt-3 gap-4">
-          <text class="type-tag">{{ knowledge.category }}</text>
+          <text class="type-tag">{{ categoryName(knowledge.category) }}</text>
           <view class="flex items-center">
             <text class="uni-icons uniui-calendar-filled meta-icon mr-1"></text>
             <text class="meta-text">{{ formatDate(knowledge.createTime) }}</text>
           </view>
           <view class="flex items-center">
             <text class="uni-icons uniui-eye-filled meta-icon mr-1"></text>
-            <text class="meta-text">{{ knowledge.viewCount }}</text>
+            <text class="meta-text">{{ knowledge.readCount }}</text>
           </view>
         </view>
       </view>
@@ -27,32 +27,6 @@
         </view>
         <text class="body-p">{{ knowledge.content }}</text>
       </view>
-
-      <!-- 附件列表 -->
-      <view v-if="knowledge.attachments?.length" class="glass-card">
-        <view class="section-head">
-          <view class="section-bar mr-2"></view>
-          <text class="section-title flex-1">相关附件</text>
-        </view>
-        <view
-          v-for="(file, index) in knowledge.attachments"
-          :key="index"
-          class="list-row"
-          hover-class="row-hover"
-          :hover-start-time="0"
-          :hover-stay-time="120"
-          @click="downloadFile(file.url)"
-        >
-          <view class="icon-chip mr-3">
-            <text class="uni-icons uniui-download-filled chip-glyph"></text>
-          </view>
-          <view class="flex-1 min-w-0 mr-2">
-            <text class="row-title truncate">{{ file.name }}</text>
-            <text class="row-sub truncate">{{ file.size }}</text>
-          </view>
-          <text class="uni-icons uniui-arrowright row-arrow"></text>
-        </view>
-      </view>
     </view>
 
     <view v-else class="py-20 text-center">
@@ -62,29 +36,21 @@
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted } from 'vue'
+import { ref } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
 import TopBar from '../../components/common/TopBar.vue'
 import EmptyState from '../../components/common/EmptyState.vue'
+import { getKnowledgeDetail, categoryName, type KnowledgeItem } from '../../api/knowledge'
 import { formatDate } from '../../utils'
 
-const knowledge = ref<any>(null)
-
-function downloadFile(url: string) {
-  uni.showToast({ title: '开始下载...', icon: 'none' })
-}
+const knowledge = ref<KnowledgeItem | null>(null)
 
-onMounted(() => {
-  knowledge.value = {
-    id: '1',
-    title: '化粪池清掏操作规范',
-    category: '操作规范',
-    content: '一、作业前准备\n1. 检查作业人员安全防护装备是否齐全\n2. 确认作业区域通风良好\n3. 设置安全警示标识,禁止无关人员进入\n4. 检查作业设备是否正常\n\n二、作业流程\n1. 打开井盖,进行通风换气\n2. 使用气体检测仪检测有害气体浓度\n3. 确认安全后,使用专业设备进行清掏\n4. 清掏完成后,对井内进行冲洗\n5. 恢复井盖,清理作业现场\n\n三、安全注意事项\n1. 作业人员必须佩戴防毒面具和安全绳\n2. 井下作业时间不得超过30分钟\n3. 必须配备监护人员在井上监护\n4. 发现异常情况立即停止作业并撤离',
-    createTime: '2026-06-20',
-    viewCount: 128,
-    attachments: [
-      { name: '操作规范V2.0.pdf', size: '2.3MB', url: '#' },
-      { name: '安全流程图.png', size: '1.1MB', url: '#' },
-    ],
+onLoad(async (options) => {
+  if (!options?.id) return
+  try {
+    knowledge.value = await getKnowledgeDetail(Number(options.id))
+  } catch (error) {
+    console.error('获取知识详情失败:', error)
   }
 })
 </script>

+ 43 - 30
src/subPackages/pages-sales/knowledgeList.vue

@@ -46,9 +46,9 @@
         >
           <view class="flex items-center justify-between mb-2">
             <text class="row-title truncate flex-1 min-w-0 mr-2">{{ item.title }}</text>
-            <text class="type-tag flex-shrink-0">{{ item.category }}</text>
+            <text class="type-tag flex-shrink-0">{{ categoryName(item.category) }}</text>
           </view>
-          <text class="row-sub notice-content">{{ item.summary }}</text>
+          <text class="row-sub notice-content">{{ summaryOf(item.content) }}</text>
           <view class="flex items-center justify-between mt-2">
             <view class="flex items-center">
               <text class="uni-icons uniui-calendar-filled meta-icon mr-1"></text>
@@ -56,7 +56,7 @@
             </view>
             <view class="flex items-center">
               <text class="uni-icons uniui-eye-filled meta-icon mr-1"></text>
-              <text class="row-date">{{ item.viewCount }}</text>
+              <text class="row-date">{{ item.readCount }}</text>
             </view>
           </view>
         </view>
@@ -66,48 +66,61 @@
 </template>
 
 <script setup lang="ts">
-import { ref, computed, onMounted } from 'vue'
+import { ref, computed, onMounted, watch } from 'vue'
 import TopBar from '../../components/common/TopBar.vue'
 import EmptyState from '../../components/common/EmptyState.vue'
+import {
+  getKnowledgeList,
+  getKnowledgeCategories,
+  categoryName,
+  summaryOf,
+  type KnowledgeItem,
+} from '../../api/knowledge'
 import { formatDate } from '../../utils'
 
 const loading = ref(false)
 const searchText = ref('')
 const currentCategory = ref('all')
 
-const categories = [
-  { value: 'all', label: '全部' },
-  { value: 'operation', label: '操作规范' },
-  { value: 'safety', label: '安全知识' },
-  { value: 'equipment', label: '设备维护' },
-  { value: 'technology', label: '技术文档' },
-]
-
-const knowledges = ref([
-  { id: '1', title: '化粪池清掏操作规范', category: '操作规范', summary: '详细介绍了化粪池清掏的标准操作流程、安全注意事项及应急处理方案...', createTime: '2026-06-20', viewCount: 128 },
-  { id: '2', title: '管道疏通技术要点', category: '技术文档', summary: '管道疏通的常用方法、设备选型及常见问题解决方案...', createTime: '2026-06-18', viewCount: 95 },
-  { id: '3', title: '施工现场安全守则', category: '安全知识', summary: '施工现场的安全防护要求、危险源识别及个人防护措施...', createTime: '2026-06-15', viewCount: 210 },
-  { id: '4', title: '高压清洗车维护指南', category: '设备维护', summary: '高压清洗车的日常保养、常见故障排查及维修方法...', createTime: '2026-06-10', viewCount: 76 },
-])
+const categories = ref([{ value: 'all', label: '全部' }])
+const knowledges = ref<KnowledgeItem[]>([])
 
 const filteredKnowledges = computed(() => {
-  let result = knowledges.value
-  if (currentCategory.value !== 'all') {
-    result = result.filter(k => k.category === categories.find(c => c.value === currentCategory.value)?.label)
-  }
-  if (searchText.value) {
-    const keyword = searchText.value.toLowerCase()
-    result = result.filter(k => k.title.toLowerCase().includes(keyword) || k.summary.toLowerCase().includes(keyword))
-  }
-  return result
+  if (!searchText.value) return knowledges.value
+  const keyword = searchText.value.toLowerCase()
+  return knowledges.value.filter(k =>
+    k.title.toLowerCase().includes(keyword) ||
+    (k.content || '').toLowerCase().includes(keyword)
+  )
 })
 
-function goToDetail(id: string) {
+function goToDetail(id: number) {
   uni.navigateTo({ url: `/subPackages/pages-sales/knowledgeDetail?id=${id}` })
 }
 
-onMounted(() => {
-  loading.value = false
+async function loadList() {
+  loading.value = true
+  try {
+    const category = currentCategory.value === 'all' ? undefined : currentCategory.value
+    const res = await getKnowledgeList(1, 50, category)
+    knowledges.value = res.rows || []
+  } catch (error) {
+    console.error('获取知识列表失败:', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+watch(currentCategory, loadList)
+
+onMounted(async () => {
+  loadList()
+  try {
+    const cats = await getKnowledgeCategories()
+    categories.value = [{ value: 'all', label: '全部' }, ...cats.map(c => ({ value: c.key, label: c.name }))]
+  } catch (error) {
+    console.error('获取知识分类失败:', error)
+  }
 })
 </script>
 

+ 1 - 1
src/subPackages/pages-sales/taskReminder.vue

@@ -68,7 +68,7 @@ const reminderTasks = computed(() => {
 
 function goToDetail(taskId: string) {
   taskStore.setCurrentTask(taskId)
-  uni.navigateTo({ url: '/subPackages/pages-common/taskDetail' })
+  uni.navigateTo({ url: `/subPackages/pages-common/taskDetail?id=${taskId}` })
 }
 
 function confirmTask(taskId: string) {

+ 9 - 1
src/types/index.ts

@@ -8,11 +8,19 @@ export interface User {
   avatar?: string
   role: UserRole
   department?: string
-  position?: string
+  deptId?: number
   employeeNo?: string
   email?: string
   hireDate?: string
   team?: string
+  teamId?: number
+  dutyStatus?: string
+  skillLevel?: string
+  certifications?: string
+  tags?: string
+  skillTags?: string
+  currentTaskId?: number
+  postNames?: string
   status: UserStatus
 }