feat: UI rewrite R6 - final round (10 pages)
- fxgl: groupsClock/teamList/instructionList/receive/materialList/inventory (6) - xjgl: constructionList/detail (2) - map: tckzPop/ybPop redesign (2) All 63 pages now use consistent design system
This commit is contained in:
@@ -1,87 +1,150 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* 班组人员打卡列表
|
* 班组人员打卡列表 — 防汛值班打卡记录
|
||||||
*
|
*
|
||||||
* 展示防汛值班人员的打卡记录,
|
* 展示防汛值班人员的打卡记录,支持按班组切换和下拉刷新。
|
||||||
* 支持按班组切换和日期筛选。
|
* DESIGN: #1E74FF NavBar, #F4F7F8 bg, white cards border-radius 10px.
|
||||||
*/
|
*/
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { showToast } from 'vant'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
// ── 状态 ──
|
||||||
const activeTab = ref(0)
|
const activeTab = ref(0)
|
||||||
const dateFilter = ref('')
|
const loading = ref(false)
|
||||||
|
const refreshing = ref(false)
|
||||||
|
|
||||||
/** 打卡状态映射 */
|
// ── 映射 ──
|
||||||
const statusMap: Record<string, string> = {
|
interface ClockRecord {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
team: string
|
||||||
|
time: string
|
||||||
|
status: 'clocked' | 'late' | 'missing'
|
||||||
|
location: string
|
||||||
|
avatar?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusMap: Record<ClockRecord['status'], string> = {
|
||||||
clocked: '已打卡',
|
clocked: '已打卡',
|
||||||
late: '迟到',
|
late: '迟到',
|
||||||
missing: '缺卡',
|
missing: '缺卡',
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusColorMap: Record<string, 'success' | 'warning' | 'danger'> = {
|
const statusColorMap: Record<ClockRecord['status'], 'success' | 'warning' | 'danger'> = {
|
||||||
clocked: 'success',
|
clocked: 'success',
|
||||||
late: 'warning',
|
late: 'warning',
|
||||||
missing: 'danger',
|
missing: 'danger',
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 模拟打卡数据 */
|
const teamTabs = ['全部', 'A组', 'B组', 'C组']
|
||||||
const mockRecords = [
|
|
||||||
|
// ── 数据 ──
|
||||||
|
const records = ref<ClockRecord[]>([])
|
||||||
|
|
||||||
|
const mockRecords: ClockRecord[] = [
|
||||||
{ id: 1, name: '张建国', team: 'A组', time: '08:25', status: 'clocked', location: '泵站1号' },
|
{ id: 1, name: '张建国', team: 'A组', time: '08:25', status: 'clocked', location: '泵站1号' },
|
||||||
{ id: 2, name: '李明辉', team: 'A组', time: '08:32', status: 'late', location: '泵站1号' },
|
{ id: 2, name: '李明辉', team: 'A组', time: '08:32', status: 'late', location: '泵站1号' },
|
||||||
{ id: 3, name: '王强', team: 'B组', time: '08:15', status: 'clocked', location: '闸门3号' },
|
{ id: 3, name: '王强', team: 'B组', time: '08:15', status: 'clocked', location: '闸门3号' },
|
||||||
{ id: 4, name: '赵勇', team: 'B组', time: '--:--', status: 'missing', location: '闸门3号' },
|
{ id: 4, name: '赵勇', team: 'B组', time: '--:--', status: 'missing', location: '闸门3号' },
|
||||||
{ id: 5, name: '陈志远', team: 'A组', time: '08:20', status: 'clocked', location: '河道巡查点' },
|
{ id: 5, name: '陈志远', team: 'A组', time: '08:20', status: 'clocked', location: '河道巡查点' },
|
||||||
|
{ id: 6, name: '周文博', team: 'C组', time: '08:10', status: 'clocked', location: '泵站2号' },
|
||||||
|
{ id: 7, name: '刘大伟', team: 'C组', time: '08:45', status: 'late', location: '泵站2号' },
|
||||||
|
{ id: 8, name: '孙志强', team: 'B组', time: '08:18', status: 'clocked', location: '河道巡查点' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// ── 计算属性 ──
|
||||||
const filteredList = computed(() => {
|
const filteredList = computed(() => {
|
||||||
let list = mockRecords
|
if (activeTab.value === 0) return records.value
|
||||||
if (activeTab.value === 1) list = list.filter(r => r.team === 'A组')
|
const team = teamTabs[activeTab.value]
|
||||||
else if (activeTab.value === 2) list = list.filter(r => r.team === 'B组')
|
return records.value.filter(r => r.team === team)
|
||||||
return list
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ── 方法 ──
|
||||||
|
async function fetchData() {
|
||||||
|
loading.value = true
|
||||||
|
// 模拟异步请求
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 400))
|
||||||
|
records.value = mockRecords
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRefresh() {
|
||||||
|
refreshing.value = true
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 600))
|
||||||
|
records.value = [...mockRecords].reverse()
|
||||||
|
refreshing.value = false
|
||||||
|
showToast('刷新成功')
|
||||||
|
}
|
||||||
|
|
||||||
function goTeamList() {
|
function goTeamList() {
|
||||||
router.push('/teamList')
|
router.push('/teamList')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
|
<!-- 导航栏 -->
|
||||||
<van-nav-bar title="打卡记录" left-arrow fixed placeholder @click-left="router.back()">
|
<van-nav-bar title="打卡记录" left-arrow fixed placeholder @click-left="router.back()">
|
||||||
<template #right>
|
<template #right>
|
||||||
<van-icon name="user-o" size="20" @click="goTeamList" />
|
<van-icon name="user-o" size="20" @click="goTeamList" />
|
||||||
</template>
|
</template>
|
||||||
</van-nav-bar>
|
</van-nav-bar>
|
||||||
|
|
||||||
<van-tabs v-model:active="activeTab" sticky>
|
<!-- 标签页 -->
|
||||||
<van-tab title="全部" />
|
<van-tabs v-model:active="activeTab" sticky :swipeable="false" color="var(--color-primary)">
|
||||||
<van-tab title="A组" />
|
<van-tab v-for="name in teamTabs" :key="name" :title="name" />
|
||||||
<van-tab title="B组" />
|
|
||||||
</van-tabs>
|
</van-tabs>
|
||||||
|
|
||||||
<div class="card-list">
|
<!-- 下拉刷新 + 列表 -->
|
||||||
<van-empty v-if="filteredList.length === 0" description="暂无打卡记录" />
|
<van-pull-refresh v-model="refreshing" @refresh="onRefresh" class="pull-refresh-wrap">
|
||||||
<van-card
|
<!-- 加载骨架 -->
|
||||||
|
<div v-if="loading" class="skeleton-wrap">
|
||||||
|
<div v-for="i in 4" :key="i" class="skeleton-card">
|
||||||
|
<van-skeleton title avatar :row="1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<van-empty v-else-if="filteredList.length === 0" description="暂无打卡记录">
|
||||||
|
<van-button type="primary" size="small" @click="onRefresh">重新加载</van-button>
|
||||||
|
</van-empty>
|
||||||
|
|
||||||
|
<!-- 打卡卡片列表 -->
|
||||||
|
<div v-else class="card-list">
|
||||||
|
<div
|
||||||
v-for="item in filteredList"
|
v-for="item in filteredList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:title="item.name"
|
class="clock-card"
|
||||||
:desc="`点位: ${item.location}`"
|
|
||||||
>
|
>
|
||||||
<template #tags>
|
<div class="card-hd">
|
||||||
|
<span class="card-name">{{ item.name }}</span>
|
||||||
<van-tag :type="statusColorMap[item.status]" size="medium">
|
<van-tag :type="statusColorMap[item.status]" size="medium">
|
||||||
{{ statusMap[item.status] }}
|
{{ statusMap[item.status] }}
|
||||||
</van-tag>
|
</van-tag>
|
||||||
</template>
|
</div>
|
||||||
<template #footer>
|
<div class="card-bd">
|
||||||
<div class="card-meta">
|
<div class="card-info">
|
||||||
|
<van-icon name="location-o" size="14" />
|
||||||
|
<span>{{ item.location }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-info">
|
||||||
|
<van-icon name="clock-o" size="14" />
|
||||||
<span>打卡时间: {{ item.time }}</span>
|
<span>打卡时间: {{ item.time }}</span>
|
||||||
<span>{{ item.team }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</van-card>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card-ft">
|
||||||
|
<span class="card-team">{{ item.team }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</van-pull-refresh>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -98,20 +161,78 @@ function goTeamList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-list {
|
.pull-refresh-wrap {
|
||||||
padding: 0 8px;
|
min-height: calc(100vh - 90px);
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.van-card) {
|
// ── 骨架屏 ──
|
||||||
margin: 8px;
|
.skeleton-wrap {
|
||||||
border-radius: 10px;
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
.skeleton-card {
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
background: var(--color-bg-card);
|
background: var(--color-bg-card);
|
||||||
}
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-meta {
|
// ── 卡片列表 ──
|
||||||
|
.card-list {
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clock-card {
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
.card-hd {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
justify-content: space-between;
|
||||||
font-size: 12px;
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-bd {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 13px;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-ft {
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid var(--color-border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-team {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-placeholder);
|
||||||
|
background: var(--color-primary-bg);
|
||||||
|
color: var(--color-primary);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 空状态 ──
|
||||||
|
:deep(.van-empty) {
|
||||||
|
padding: 60px 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,52 +1,74 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* 防汛指令列表
|
* 防汛指令列表 — 接收与处理防汛指令
|
||||||
*
|
*
|
||||||
* 展示下发的防汛指令,支持按状态筛选和搜索,
|
* 展示下发的防汛指令,支持按状态筛选、搜索和下拉刷新,
|
||||||
* 点击可查看指令详情并处理。
|
* 每条指令显示级别标签和发布时间。
|
||||||
|
* DESIGN: #1E74FF NavBar, #F4F7F8 bg, white cards border-radius 10px.
|
||||||
*/
|
*/
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { showToast } from 'vant'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
// ── 类型 ──
|
||||||
|
type InstructionStatus = 'pending' | 'accepted' | 'completed'
|
||||||
|
type InstructionLevel = 'urgent' | 'normal'
|
||||||
|
|
||||||
|
interface Instruction {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
level: InstructionLevel
|
||||||
|
from: string
|
||||||
|
time: string
|
||||||
|
status: InstructionStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 状态 ──
|
||||||
const searchText = ref('')
|
const searchText = ref('')
|
||||||
const activeTab = ref(0)
|
const activeTab = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const refreshing = ref(false)
|
||||||
|
|
||||||
/** 状态映射 */
|
// ── 映射 ──
|
||||||
const statusMap: Record<string, string> = {
|
const statusMap: Record<InstructionStatus, string> = {
|
||||||
pending: '待接收',
|
pending: '待接收',
|
||||||
accepted: '已接收',
|
accepted: '已接收',
|
||||||
completed: '已完成',
|
completed: '已完成',
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusColorMap: Record<string, 'warning' | 'primary' | 'success'> = {
|
const statusStyleMap: Record<InstructionStatus, { type: 'warning' | 'primary' | 'success'; bg: string }> = {
|
||||||
pending: 'warning',
|
pending: { type: 'warning', bg: 'var(--color-warning-bg)' },
|
||||||
accepted: 'primary',
|
accepted: { type: 'primary', bg: 'var(--color-primary-bg)' },
|
||||||
completed: 'success',
|
completed: { type: 'success', bg: 'var(--color-success-bg)' },
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 级别映射 */
|
const levelMap: Record<InstructionLevel, string> = {
|
||||||
const levelMap: Record<string, string> = {
|
|
||||||
urgent: '紧急',
|
urgent: '紧急',
|
||||||
normal: '普通',
|
normal: '普通',
|
||||||
}
|
}
|
||||||
|
|
||||||
const levelColorMap: Record<string, string> = {
|
const levelStyleMap: Record<InstructionLevel, { color: string; bg: string }> = {
|
||||||
urgent: '#ee0a24',
|
urgent: { color: '#fff', bg: '#ee0a24' },
|
||||||
normal: '#1989fa',
|
normal: { color: '#fff', bg: '#1989fa' },
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 模拟指令数据 */
|
// ── 数据 ──
|
||||||
const mockInstructions = [
|
const instructions = ref<Instruction[]>([])
|
||||||
|
|
||||||
|
const mockInstructions: Instruction[] = [
|
||||||
{ id: 1, title: '启动III级防汛应急响应', level: 'urgent', from: '市防汛指挥部', time: '2025-06-15 08:00', status: 'pending' },
|
{ id: 1, title: '启动III级防汛应急响应', level: 'urgent', from: '市防汛指挥部', time: '2025-06-15 08:00', status: 'pending' },
|
||||||
{ id: 2, title: '河道巡查任务通知', level: 'normal', from: '水务局', time: '2025-06-14 14:30', status: 'accepted' },
|
{ id: 2, title: '河道巡查任务通知', level: 'normal', from: '水务局', time: '2025-06-14 14:30', status: 'accepted' },
|
||||||
{ id: 3, title: '泵站设备检修通知', level: 'normal', from: '运维中心', time: '2025-06-13 09:00', status: 'completed' },
|
{ id: 3, title: '泵站设备检修通知', level: 'normal', from: '运维中心', time: '2025-06-13 09:00', status: 'completed' },
|
||||||
{ id: 4, title: '重点积水路段值守', level: 'urgent', from: '市政管理处', time: '2025-06-15 07:30', status: 'pending' },
|
{ id: 4, title: '重点积水路段值守', level: 'urgent', from: '市政管理处', time: '2025-06-15 07:30', status: 'pending' },
|
||||||
|
{ id: 5, title: '防洪物资调配通知', level: 'urgent', from: '市防汛指挥部', time: '2025-06-15 09:00', status: 'pending' },
|
||||||
|
{ id: 6, title: '排水管网排查任务', level: 'normal', from: '水务局', time: '2025-06-14 10:00', status: 'accepted' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// ── 计算属性 ──
|
||||||
const filteredList = computed(() => {
|
const filteredList = computed(() => {
|
||||||
let list = mockInstructions
|
let list = instructions.value
|
||||||
if (searchText.value) {
|
if (searchText.value) {
|
||||||
const kw = searchText.value.toLowerCase()
|
const kw = searchText.value.toLowerCase()
|
||||||
list = list.filter(i =>
|
list = list.filter(i =>
|
||||||
@@ -54,55 +76,110 @@ const filteredList = computed(() => {
|
|||||||
i.from.toLowerCase().includes(kw)
|
i.from.toLowerCase().includes(kw)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (activeTab.value === 1) list = list.filter(i => i.status === 'pending')
|
const tabStatus: (InstructionStatus | null)[] = [null, 'pending', 'accepted', 'completed']
|
||||||
else if (activeTab.value === 2) list = list.filter(i => i.status === 'accepted')
|
const filter = tabStatus[activeTab.value]
|
||||||
else if (activeTab.value === 3) list = list.filter(i => i.status === 'completed')
|
if (filter) list = list.filter(i => i.status === filter)
|
||||||
return list
|
return list
|
||||||
})
|
})
|
||||||
|
|
||||||
function goReceive(id: number) {
|
const pendingCount = computed(() => instructions.value.filter(i => i.status === 'pending').length)
|
||||||
router.push('/instructionReceive')
|
|
||||||
|
// ── 方法 ──
|
||||||
|
async function fetchData() {
|
||||||
|
loading.value = true
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 400))
|
||||||
|
instructions.value = mockInstructions
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onRefresh() {
|
||||||
|
refreshing.value = true
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 600))
|
||||||
|
instructions.value = [...mockInstructions]
|
||||||
|
refreshing.value = false
|
||||||
|
showToast('刷新成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
function goReceive(item: Instruction) {
|
||||||
|
router.push({ path: '/instructionReceive', query: { id: item.id } })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
|
<!-- 导航栏 -->
|
||||||
<van-nav-bar title="防汛指令" left-arrow fixed placeholder @click-left="router.back()" />
|
<van-nav-bar title="防汛指令" left-arrow fixed placeholder @click-left="router.back()" />
|
||||||
|
|
||||||
<van-search v-model="searchText" placeholder="搜索指令标题、来源" shape="round" />
|
<!-- 搜索栏 -->
|
||||||
|
<van-search v-model="searchText" placeholder="搜索指令标题、来源单位" shape="round" />
|
||||||
|
|
||||||
<van-tabs v-model:active="activeTab" sticky>
|
<!-- 标签页 -->
|
||||||
|
<van-tabs v-model:active="activeTab" sticky color="var(--color-primary)">
|
||||||
<van-tab title="全部" />
|
<van-tab title="全部" />
|
||||||
<van-tab title="待接收" />
|
<van-tab :title="`待接收${pendingCount ? ` (${pendingCount})` : ''}`" />
|
||||||
<van-tab title="已接收" />
|
<van-tab title="已接收" />
|
||||||
<van-tab title="已完成" />
|
<van-tab title="已完成" />
|
||||||
</van-tabs>
|
</van-tabs>
|
||||||
|
|
||||||
<div class="card-list">
|
<!-- 下拉刷新 + 列表 -->
|
||||||
<van-empty v-if="filteredList.length === 0" description="暂无指令" />
|
<van-pull-refresh v-model="refreshing" @refresh="onRefresh" class="pull-refresh-wrap">
|
||||||
<van-card
|
<!-- 加载骨架 -->
|
||||||
|
<div v-if="loading" class="skeleton-wrap">
|
||||||
|
<div v-for="i in 4" :key="i" class="skeleton-card">
|
||||||
|
<van-skeleton title :row="2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<van-empty v-else-if="filteredList.length === 0" description="暂无指令">
|
||||||
|
<van-button type="primary" size="small" @click="onRefresh">刷新</van-button>
|
||||||
|
</van-empty>
|
||||||
|
|
||||||
|
<!-- 指令卡片列表 -->
|
||||||
|
<div v-else class="card-list">
|
||||||
|
<div
|
||||||
v-for="item in filteredList"
|
v-for="item in filteredList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:title="item.title"
|
class="inst-card"
|
||||||
:desc="`来自: ${item.from}`"
|
@click="goReceive(item)"
|
||||||
@click="goReceive(item.id)"
|
>
|
||||||
|
<div class="card-hd">
|
||||||
|
<div class="card-title-wrap">
|
||||||
|
<span
|
||||||
|
class="level-badge"
|
||||||
|
:style="{ background: levelStyleMap[item.level].bg, color: levelStyleMap[item.level].color }"
|
||||||
>
|
>
|
||||||
<template #tags>
|
|
||||||
<van-tag :color="levelColorMap[item.level]" size="medium" text-color="#fff">
|
|
||||||
{{ levelMap[item.level] }}
|
{{ levelMap[item.level] }}
|
||||||
</van-tag>
|
</span>
|
||||||
<van-tag :type="statusColorMap[item.status]" size="medium">
|
<span class="card-title">{{ item.title }}</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="status-badge"
|
||||||
|
:style="{ background: statusStyleMap[item.status].bg }"
|
||||||
|
>
|
||||||
|
<van-tag :type="statusStyleMap[item.status].type" size="medium">
|
||||||
{{ statusMap[item.status] }}
|
{{ statusMap[item.status] }}
|
||||||
</van-tag>
|
</van-tag>
|
||||||
</template>
|
</span>
|
||||||
<template #footer>
|
</div>
|
||||||
<div class="card-meta">
|
<div class="card-bd">
|
||||||
|
<div class="card-info">
|
||||||
|
<van-icon name="user-o" size="14" />
|
||||||
|
<span>{{ item.from }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-info">
|
||||||
|
<van-icon name="clock-o" size="14" />
|
||||||
<span>{{ item.time }}</span>
|
<span>{{ item.time }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</van-card>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</van-pull-refresh>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -118,22 +195,87 @@ function goReceive(id: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-list {
|
.pull-refresh-wrap {
|
||||||
padding: 0 8px;
|
min-height: calc(100vh - 140px);
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.van-card) {
|
// ── 骨架屏 ──
|
||||||
margin: 8px;
|
.skeleton-wrap {
|
||||||
border-radius: 10px;
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
.skeleton-card {
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
background: var(--color-bg-card);
|
background: var(--color-bg-card);
|
||||||
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.van-tag) {
|
// ── 指令卡片 ──
|
||||||
margin-right: 4px;
|
.card-list {
|
||||||
}
|
padding: 8px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-meta {
|
.inst-card {
|
||||||
font-size: 12px;
|
background: var(--color-bg-card);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hd {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
line-height: 1.4;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-bd {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 13px;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,53 +1,115 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* 指令接收详情
|
* 指令接收详情 — 防汛指令接收与处理
|
||||||
*
|
*
|
||||||
* 展示防汛指令的详细内容和操作按钮,
|
* 展示防汛指令的完整内容,提供接收确认和退回操作,
|
||||||
* 支持接收确认和退回操作。
|
* 接收后可填写备注并确认完成。
|
||||||
|
* DESIGN: #1E74FF NavBar, #F4F7F8 bg, white cards border-radius 10px.
|
||||||
*/
|
*/
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { showSuccessToast, showConfirmDialog } from 'vant'
|
import { showSuccessToast, showConfirmDialog, showToast } from 'vant'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
/** 接收状态 */
|
// ── 类型 ──
|
||||||
|
type InstructionLevel = 'urgent' | 'normal'
|
||||||
|
interface InstructionDetail {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
level: InstructionLevel
|
||||||
|
from: string
|
||||||
|
time: string
|
||||||
|
deadline?: string
|
||||||
|
content: string
|
||||||
|
contact: string
|
||||||
|
phone: string
|
||||||
|
attachments?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 状态 ──
|
||||||
|
const loading = ref(true)
|
||||||
const accepted = ref(false)
|
const accepted = ref(false)
|
||||||
const remark = ref('')
|
const remark = ref('')
|
||||||
|
const showDeclineDialog = ref(false)
|
||||||
|
const declineReason = ref('')
|
||||||
|
const actionLoading = ref(false)
|
||||||
|
|
||||||
/** 模拟指令详情 */
|
// ── 映射 ──
|
||||||
const instruction = {
|
const levelMap: Record<InstructionLevel, { label: string; color: string }> = {
|
||||||
id: 1,
|
urgent: { label: '紧急', color: '#ee0a24' },
|
||||||
|
normal: { label: '普通', color: '#1989fa' },
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 数据 ──
|
||||||
|
const instruction = ref<InstructionDetail>({
|
||||||
|
id: 0,
|
||||||
|
title: '',
|
||||||
|
level: 'normal',
|
||||||
|
from: '',
|
||||||
|
time: '',
|
||||||
|
content: '',
|
||||||
|
contact: '',
|
||||||
|
phone: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockDetail: InstructionDetail = {
|
||||||
|
id: Number(route.query.id) || 1,
|
||||||
title: '启动III级防汛应急响应',
|
title: '启动III级防汛应急响应',
|
||||||
level: 'urgent',
|
level: 'urgent',
|
||||||
from: '市防汛指挥部',
|
from: '市防汛指挥部',
|
||||||
time: '2025-06-15 08:00',
|
time: '2025-06-15 08:00',
|
||||||
content: '根据市气象台发布的暴雨橙色预警,预计未来6小时内我市将出现大范围强降雨,累计雨量可达80-120毫米。经研究决定,自2025年6月15日08时起启动III级防汛应急响应。请各相关单位按要求落实防汛措施。',
|
deadline: '2025-06-15 18:00',
|
||||||
|
content:
|
||||||
|
'根据市气象台发布的暴雨橙色预警,预计未来6小时内我市将出现大范围强降雨,累计雨量可达80-120毫米。经研究决定,自2025年6月15日08时起启动III级防汛应急响应。\n\n请各相关单位按要求落实以下防汛措施:\n1. 立即启动防汛应急预案,做好人员值班安排;\n2. 对重点区域进行巡查,排查安全隐患;\n3. 确保防汛物资储备充足,设备处于良好状态;\n4. 保持通讯畅通,及时上报汛情信息。',
|
||||||
contact: '张主任',
|
contact: '张主任',
|
||||||
phone: '010-88886666',
|
phone: '010-88886666',
|
||||||
|
attachments: ['防汛应急预案.pdf', '值班安排表.xlsx'],
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAccept() {
|
// ── 方法 ──
|
||||||
|
async function onAccept() {
|
||||||
|
actionLoading.value = true
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
accepted.value = true
|
accepted.value = true
|
||||||
|
actionLoading.value = false
|
||||||
showSuccessToast('已接收指令')
|
showSuccessToast('已接收指令')
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDecline() {
|
function onDecline() {
|
||||||
showConfirmDialog({
|
showDeclineDialog.value = true
|
||||||
title: '退回指令',
|
}
|
||||||
message: '确定要退回该指令吗?请填写退回原因。',
|
|
||||||
})
|
async function confirmDecline() {
|
||||||
.then(() => {
|
if (!declineReason.value.trim()) {
|
||||||
|
showToast('请填写退回原因')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
showDeclineDialog.value = false
|
||||||
showSuccessToast('已退回')
|
showSuccessToast('已退回')
|
||||||
router.back()
|
router.back()
|
||||||
})
|
|
||||||
.catch(() => {})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onComplete() {
|
||||||
|
actionLoading.value = true
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
actionLoading.value = false
|
||||||
|
showSuccessToast('处理完成')
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300))
|
||||||
|
instruction.value = mockDetail
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
|
<!-- 导航栏 -->
|
||||||
<van-nav-bar
|
<van-nav-bar
|
||||||
title="指令详情"
|
title="指令详情"
|
||||||
left-arrow
|
left-arrow
|
||||||
@@ -56,20 +118,73 @@ function onDecline() {
|
|||||||
@click-left="router.back()"
|
@click-left="router.back()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<van-cell-group inset>
|
<!-- 加载状态 -->
|
||||||
<van-cell title="指令标题" :value="instruction.title" />
|
<van-loading v-if="loading" class="loading-center" size="24" />
|
||||||
<van-cell title="来源单位" :value="instruction.from" />
|
|
||||||
<van-cell title="发布时间" :value="instruction.time" />
|
|
||||||
<van-cell title="联 系 人" :value="instruction.contact" />
|
|
||||||
<van-cell title="联系电话" :value="instruction.phone" />
|
|
||||||
</van-cell-group>
|
|
||||||
|
|
||||||
<van-cell-group inset style="margin-top: 12px">
|
<template v-else>
|
||||||
<van-cell title="指令内容" />
|
<!-- 指令级别标签 -->
|
||||||
<div class="content-block">{{ instruction.content }}</div>
|
<div class="level-header">
|
||||||
</van-cell-group>
|
<span
|
||||||
|
class="level-tag"
|
||||||
|
:style="{ background: levelMap[instruction.level].color }"
|
||||||
|
>
|
||||||
|
{{ levelMap[instruction.level].label }}
|
||||||
|
</span>
|
||||||
|
<span class="level-label">防汛指令</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<van-cell-group v-if="accepted" inset style="margin-top: 12px">
|
<!-- 基本信息卡片 -->
|
||||||
|
<div class="info-card">
|
||||||
|
<div class="card-title-row">{{ instruction.title }}</div>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">来源单位</span>
|
||||||
|
<span class="info-value">{{ instruction.from }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">发布时间</span>
|
||||||
|
<span class="info-value">{{ instruction.time }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="instruction.deadline" class="info-item">
|
||||||
|
<span class="info-label">截止时间</span>
|
||||||
|
<span class="info-value deadline">{{ instruction.deadline }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">联系人</span>
|
||||||
|
<span class="info-value">{{ instruction.contact }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">联系电话</span>
|
||||||
|
<span class="info-value">
|
||||||
|
<a :href="`tel:${instruction.phone}`">{{ instruction.phone }}</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 指令内容卡片 -->
|
||||||
|
<div class="content-card">
|
||||||
|
<div class="section-label">指令内容</div>
|
||||||
|
<div class="content-text">{{ instruction.content }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 附件卡片 -->
|
||||||
|
<div v-if="instruction.attachments?.length" class="info-card">
|
||||||
|
<div class="section-label">附件</div>
|
||||||
|
<div class="attach-list">
|
||||||
|
<div
|
||||||
|
v-for="(file, i) in instruction.attachments"
|
||||||
|
:key="i"
|
||||||
|
class="attach-item"
|
||||||
|
>
|
||||||
|
<van-icon name="description" size="18" color="var(--color-primary)" />
|
||||||
|
<span>{{ file }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 接收后备注 -->
|
||||||
|
<div v-if="accepted" class="info-card">
|
||||||
<van-field
|
<van-field
|
||||||
v-model="remark"
|
v-model="remark"
|
||||||
label="备注"
|
label="备注"
|
||||||
@@ -78,38 +193,66 @@ function onDecline() {
|
|||||||
rows="2"
|
rows="2"
|
||||||
autosize
|
autosize
|
||||||
/>
|
/>
|
||||||
</van-cell-group>
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮区 -->
|
||||||
<div class="action-bar">
|
<div class="action-bar">
|
||||||
|
<!-- 未接收状态 -->
|
||||||
|
<template v-if="!accepted">
|
||||||
<van-button
|
<van-button
|
||||||
v-if="!accepted"
|
|
||||||
type="primary"
|
type="primary"
|
||||||
block
|
block
|
||||||
round
|
round
|
||||||
|
size="large"
|
||||||
|
:loading="actionLoading"
|
||||||
@click="onAccept"
|
@click="onAccept"
|
||||||
>
|
>
|
||||||
接收指令
|
接收指令
|
||||||
</van-button>
|
</van-button>
|
||||||
<van-button
|
<van-button
|
||||||
v-else
|
|
||||||
type="success"
|
|
||||||
block
|
|
||||||
round
|
|
||||||
@click="router.back()"
|
|
||||||
>
|
|
||||||
确认完成
|
|
||||||
</van-button>
|
|
||||||
<van-button
|
|
||||||
v-if="!accepted"
|
|
||||||
type="default"
|
type="default"
|
||||||
block
|
block
|
||||||
round
|
round
|
||||||
style="margin-top: 10px"
|
size="large"
|
||||||
|
class="btn-decline"
|
||||||
@click="onDecline"
|
@click="onDecline"
|
||||||
>
|
>
|
||||||
退回
|
退回
|
||||||
</van-button>
|
</van-button>
|
||||||
|
</template>
|
||||||
|
<!-- 已接收状态 -->
|
||||||
|
<template v-else>
|
||||||
|
<van-button
|
||||||
|
type="success"
|
||||||
|
block
|
||||||
|
round
|
||||||
|
size="large"
|
||||||
|
:loading="actionLoading"
|
||||||
|
@click="onComplete"
|
||||||
|
>
|
||||||
|
确认完成
|
||||||
|
</van-button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 退回弹窗 -->
|
||||||
|
<van-dialog
|
||||||
|
v-model:show="showDeclineDialog"
|
||||||
|
title="退回指令"
|
||||||
|
show-cancel-button
|
||||||
|
@confirm="confirmDecline"
|
||||||
|
>
|
||||||
|
<div class="decline-body">
|
||||||
|
<van-field
|
||||||
|
v-model="declineReason"
|
||||||
|
type="textarea"
|
||||||
|
rows="3"
|
||||||
|
autosize
|
||||||
|
placeholder="请填写退回原因"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</van-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -117,7 +260,7 @@ function onDecline() {
|
|||||||
.page-container {
|
.page-container {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--color-bg-page);
|
background: var(--color-bg-page);
|
||||||
padding-bottom: 24px;
|
padding-bottom: 150px;
|
||||||
|
|
||||||
:deep(.van-nav-bar) {
|
:deep(.van-nav-bar) {
|
||||||
background: var(--color-primary);
|
background: var(--color-primary);
|
||||||
@@ -127,16 +270,144 @@ function onDecline() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-block {
|
.loading-center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 级别头部 ──
|
||||||
|
.level-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
|
|
||||||
|
.level-tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 卡片通用 ──
|
||||||
|
.info-card,
|
||||||
|
.content-card {
|
||||||
|
margin: 0 12px 10px;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title-row {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid var(--color-border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 信息网格 ──
|
||||||
|
.info-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
width: 70px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
|
||||||
|
&.deadline {
|
||||||
|
color: var(--color-danger);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 段落标签 ──
|
||||||
|
.section-label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 内容文本 ──
|
||||||
|
.content-text {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
color: var(--color-text-regular);
|
color: var(--color-text-regular);
|
||||||
background: var(--color-bg-card);
|
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 附件列表 ──
|
||||||
|
.attach-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attach-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--color-bg-page);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 操作按钮区 ──
|
||||||
.action-bar {
|
.action-bar {
|
||||||
padding: 24px 16px;
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 12px 16px;
|
||||||
|
padding-bottom: calc(12px + env(safe-area-inset-bottom));
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
box-shadow: 0 -1px 4px rgba(0, 0, 0, 0.06);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-decline {
|
||||||
|
--van-button-default-color: var(--color-danger) !important;
|
||||||
|
--van-button-default-border-color: var(--color-danger) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 退回弹窗 ──
|
||||||
|
.decline-body {
|
||||||
|
padding: 12px 16px 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,50 +1,103 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* 物资出入库
|
* 物资出入库 — 防汛物资出入库操作
|
||||||
*
|
*
|
||||||
* 对防汛物资进行入库或出库操作,
|
* 对防汛物资进行入库或出库操作,记录数量、批次号、
|
||||||
* 记录数量、类型和操作说明。
|
* 操作人和备注。支持表单验证和操作确认。
|
||||||
|
* DESIGN: #1E74FF NavBar, #F4F7F8 bg, white cards border-radius 10px.
|
||||||
*/
|
*/
|
||||||
import { ref } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { showSuccessToast } from 'vant'
|
import { showSuccessToast, showToast } from 'vant'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
/** 操作类型 */
|
// ── 类型 ──
|
||||||
const opType = ref<'in' | 'out'>('in')
|
type OpType = 'in' | 'out'
|
||||||
|
|
||||||
|
interface MaterialInfo {
|
||||||
|
id: string | string[]
|
||||||
|
name: string
|
||||||
|
stock: number
|
||||||
|
unit: string
|
||||||
|
location: string
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 状态 ──
|
||||||
|
const opType = ref<OpType>('in')
|
||||||
const quantity = ref('')
|
const quantity = ref('')
|
||||||
const batchNo = ref('')
|
const batchNo = ref('')
|
||||||
const operator = ref('')
|
const operator = ref('')
|
||||||
const remark = ref('')
|
const remark = ref('')
|
||||||
|
const submitting = ref(false)
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
/** 模拟物资信息 */
|
// ── 数据 ──
|
||||||
const material = {
|
const material = ref<MaterialInfo>({
|
||||||
id: route.params.id,
|
id: route.params.id || '1',
|
||||||
name: '柴油水泵',
|
name: '柴油水泵',
|
||||||
stock: 12,
|
stock: 12,
|
||||||
unit: '台',
|
unit: '台',
|
||||||
location: '1号仓库',
|
location: '1号仓库',
|
||||||
|
type: '水泵类',
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── 计算属性 ──
|
||||||
|
const submitLabel = computed(() => opType.value === 'in' ? '确认入库' : '确认出库')
|
||||||
|
const opLabel = computed(() => opType.value === 'in' ? '入库' : '出库')
|
||||||
|
|
||||||
|
/** 出库时检查是否超量 */
|
||||||
|
const isOverStock = computed(() => {
|
||||||
|
if (opType.value === 'out' && quantity.value) {
|
||||||
|
return Number(quantity.value) > material.value.stock
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── 方法 ──
|
||||||
|
function onRadioChange(val: OpType) {
|
||||||
|
opType.value = val
|
||||||
|
quantity.value = '' // 切换类型时清空数量
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSubmit() {
|
function onSubmit() {
|
||||||
if (!quantity.value) {
|
if (!quantity.value) {
|
||||||
showSuccessToast('请填写数量')
|
showToast('请填写数量')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!operator.value) {
|
const num = Number(quantity.value)
|
||||||
showSuccessToast('请填写操作人')
|
if (num <= 0 || !Number.isInteger(num)) {
|
||||||
|
showToast('请输入正整数')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const action = opType.value === 'in' ? '入库' : '出库'
|
if (opType.value === 'out' && num > material.value.stock) {
|
||||||
showSuccessToast(`${action}成功`)
|
showToast('出库数量不能超过当前库存')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!operator.value.trim()) {
|
||||||
|
showToast('请填写操作人')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
submitting.value = false
|
||||||
|
showSuccessToast(`${opLabel.value}成功`)
|
||||||
router.back()
|
router.back()
|
||||||
|
}, 600)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300))
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
|
<!-- 导航栏 -->
|
||||||
<van-nav-bar
|
<van-nav-bar
|
||||||
title="出入库"
|
title="出入库"
|
||||||
left-arrow
|
left-arrow
|
||||||
@@ -53,50 +106,89 @@ function onSubmit() {
|
|||||||
@click-left="router.back()"
|
@click-left="router.back()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<van-cell-group inset>
|
<!-- 加载状态 -->
|
||||||
<van-cell title="物资名称" :value="material.name" />
|
<van-loading v-if="loading" class="loading-center" size="24" />
|
||||||
<van-cell title="当前库存" :value="`${material.stock} ${material.unit}`" />
|
|
||||||
<van-cell title="存放位置" :value="material.location" />
|
|
||||||
</van-cell-group>
|
|
||||||
|
|
||||||
<van-form @submit="onSubmit" style="margin-top: 12px">
|
<template v-else>
|
||||||
<van-cell-group inset>
|
<!-- 物资信息卡片 -->
|
||||||
<van-field
|
<div class="info-card">
|
||||||
name="opType"
|
<div class="card-title-row">{{ material.name }}</div>
|
||||||
label="操作类型"
|
<div class="info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">当前库存</span>
|
||||||
|
<span class="info-value stock-val">{{ material.stock }} {{ material.unit }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">存放位置</span>
|
||||||
|
<span class="info-value">{{ material.location }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作表单 -->
|
||||||
|
<van-form @submit="onSubmit" class="form-wrap">
|
||||||
|
<!-- 操作类型 (Radio 组) -->
|
||||||
|
<div class="form-card">
|
||||||
|
<div class="form-label">操作类型</div>
|
||||||
|
<van-radio-group
|
||||||
|
:model-value="opType"
|
||||||
|
direction="horizontal"
|
||||||
|
class="radio-group"
|
||||||
>
|
>
|
||||||
<template #input>
|
<van-radio
|
||||||
<van-radio-group v-model="opType" direction="horizontal">
|
name="in"
|
||||||
<van-radio name="in">入库</van-radio>
|
icon-size="18"
|
||||||
<van-radio name="out">出库</van-radio>
|
checked-color="var(--color-success)"
|
||||||
|
@click="onRadioChange('in')"
|
||||||
|
>
|
||||||
|
<span class="radio-label" :class="{ active: opType === 'in' }">入库</span>
|
||||||
|
</van-radio>
|
||||||
|
<van-radio
|
||||||
|
name="out"
|
||||||
|
icon-size="18"
|
||||||
|
checked-color="var(--color-danger)"
|
||||||
|
@click="onRadioChange('out')"
|
||||||
|
>
|
||||||
|
<span class="radio-label" :class="{ active: opType === 'out' }">出库</span>
|
||||||
|
</van-radio>
|
||||||
</van-radio-group>
|
</van-radio-group>
|
||||||
</template>
|
</div>
|
||||||
</van-field>
|
|
||||||
|
<!-- 表单字段 -->
|
||||||
|
<div class="form-card">
|
||||||
<van-field
|
<van-field
|
||||||
v-model="quantity"
|
v-model="quantity"
|
||||||
name="quantity"
|
name="quantity"
|
||||||
label="数量"
|
:label="`${opLabel}数量`"
|
||||||
:placeholder="`请输入${opType === 'in' ? '入库' : '出库'}数量`"
|
:placeholder="`请输入${opLabel}数量`"
|
||||||
type="digit"
|
type="digit"
|
||||||
:rules="[{ required: true, message: '请填写数量' }]"
|
clearable
|
||||||
|
:rules="[{ required: true, message: `请填写${opLabel}数量` }]"
|
||||||
|
:error="isOverStock"
|
||||||
|
:error-message="isOverStock ? '出库数量不能超过当前库存' : ''"
|
||||||
>
|
>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<span class="unit-text">{{ material.unit }}</span>
|
<span class="unit-text">{{ material.unit }}</span>
|
||||||
</template>
|
</template>
|
||||||
</van-field>
|
</van-field>
|
||||||
|
|
||||||
<van-field
|
<van-field
|
||||||
v-model="batchNo"
|
v-model="batchNo"
|
||||||
name="batchNo"
|
name="batchNo"
|
||||||
label="批次号"
|
label="批次号"
|
||||||
placeholder="可选填写批次号"
|
placeholder="可选填写批次号"
|
||||||
|
clearable
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<van-field
|
<van-field
|
||||||
v-model="operator"
|
v-model="operator"
|
||||||
name="operator"
|
name="operator"
|
||||||
label="操作人"
|
label="操作人"
|
||||||
placeholder="请输入操作人姓名"
|
placeholder="请输入操作人姓名"
|
||||||
|
clearable
|
||||||
:rules="[{ required: true, message: '请填写操作人' }]"
|
:rules="[{ required: true, message: '请填写操作人' }]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<van-field
|
<van-field
|
||||||
v-model="remark"
|
v-model="remark"
|
||||||
name="remark"
|
name="remark"
|
||||||
@@ -105,15 +197,41 @@ function onSubmit() {
|
|||||||
type="textarea"
|
type="textarea"
|
||||||
rows="2"
|
rows="2"
|
||||||
autosize
|
autosize
|
||||||
|
maxlength="200"
|
||||||
|
show-word-limit
|
||||||
/>
|
/>
|
||||||
</van-cell-group>
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作预览提示 -->
|
||||||
|
<div class="preview-hint" :class="opType">
|
||||||
|
<van-icon
|
||||||
|
:name="opType === 'in' ? 'arrow-up' : 'arrow-down'"
|
||||||
|
size="16"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{{ opType === 'in' ? '入库' : '出库' }}
|
||||||
|
<template v-if="quantity">
|
||||||
|
<strong>{{ quantity }}</strong> {{ material.unit }}
|
||||||
|
</template>
|
||||||
|
{{ material.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 提交按钮 -->
|
||||||
<div class="submit-wrap">
|
<div class="submit-wrap">
|
||||||
<van-button type="primary" block round native-type="submit">
|
<van-button
|
||||||
确认{{ opType === 'in' ? '入库' : '出库' }}
|
:type="opType === 'in' ? 'primary' : 'danger'"
|
||||||
|
block
|
||||||
|
round
|
||||||
|
size="large"
|
||||||
|
native-type="submit"
|
||||||
|
:loading="submitting"
|
||||||
|
>
|
||||||
|
{{ submitLabel }}
|
||||||
</van-button>
|
</van-button>
|
||||||
</div>
|
</div>
|
||||||
</van-form>
|
</van-form>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -121,6 +239,7 @@ function onSubmit() {
|
|||||||
.page-container {
|
.page-container {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--color-bg-page);
|
background: var(--color-bg-page);
|
||||||
|
padding-bottom: 40px;
|
||||||
|
|
||||||
:deep(.van-nav-bar) {
|
:deep(.van-nav-bar) {
|
||||||
background: var(--color-primary);
|
background: var(--color-primary);
|
||||||
@@ -130,12 +249,148 @@ function onSubmit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 物资信息卡片 ──
|
||||||
|
.info-card {
|
||||||
|
margin: 0 12px 10px;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title-row {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid var(--color-border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
width: 70px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
|
||||||
|
&.stock-val {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 表单 ──
|
||||||
|
.form-wrap {
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 12px 0 0;
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
:deep(.van-cell) {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.van-cell::after) {
|
||||||
|
left: 16px;
|
||||||
|
right: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
padding: 0 16px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 操作类型 Radio ──
|
||||||
|
.radio-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 32px;
|
||||||
|
padding: 8px 16px 12px;
|
||||||
|
|
||||||
|
:deep(.van-radio) {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-label {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
transition: color 0.2s;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.unit-text {
|
.unit-text {
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 预览提示 ──
|
||||||
|
.preview-hint {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&.in {
|
||||||
|
background: var(--color-success-bg);
|
||||||
|
color: var(--color-success-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.out {
|
||||||
|
background: var(--color-danger-bg);
|
||||||
|
color: var(--color-danger-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 提交按钮 ──
|
||||||
.submit-wrap {
|
.submit-wrap {
|
||||||
padding: 24px 16px;
|
padding: 8px 4px 40px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,56 +1,80 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* 防汛物资管理
|
* 防汛物资管理 — 物资库存列表
|
||||||
*
|
*
|
||||||
* 展示防汛物资库存列表,支持按类型筛选和搜索,
|
* 展示防汛物资库存情况,支持按类型筛选、搜索和下拉刷新,
|
||||||
* 显示各物资的库存数量。
|
* 每条物资显示库存数量和库存状态。
|
||||||
|
* DESIGN: #1E74FF NavBar, #F4F7F8 bg, white cards border-radius 10px.
|
||||||
*/
|
*/
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { showToast } from 'vant'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
// ── 类型 ──
|
||||||
|
type MaterialType = 'pump' | 'sandbag' | 'tool' | 'rescue'
|
||||||
|
type StockStatus = 'sufficient' | 'normal' | 'low' | 'shortage'
|
||||||
|
|
||||||
|
interface Material {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
type: MaterialType
|
||||||
|
stock: number
|
||||||
|
unit: string
|
||||||
|
minStock: number
|
||||||
|
status: StockStatus
|
||||||
|
location: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 状态 ──
|
||||||
const searchText = ref('')
|
const searchText = ref('')
|
||||||
const activeTab = ref(0)
|
const activeTab = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const refreshing = ref(false)
|
||||||
|
|
||||||
/** 物资类型映射 */
|
// ── 映射 ──
|
||||||
const typeMap: Record<string, string> = {
|
const typeMap: Record<MaterialType, string> = {
|
||||||
pump: '水泵类',
|
pump: '水泵类',
|
||||||
sandbag: '沙袋类',
|
sandbag: '沙袋类',
|
||||||
tool: '工具类',
|
tool: '工具类',
|
||||||
rescue: '救援类',
|
rescue: '救援类',
|
||||||
}
|
}
|
||||||
|
|
||||||
const typeColorMap: Record<string, string> = {
|
const typeColorMap: Record<MaterialType, string> = {
|
||||||
pump: '#1989fa',
|
pump: '#1989fa',
|
||||||
sandbag: '#ff976a',
|
sandbag: '#ff976a',
|
||||||
tool: '#07c160',
|
tool: '#07c160',
|
||||||
rescue: '#ee0a24',
|
rescue: '#ee0a24',
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 库存状态 */
|
const stockStatusMap: Record<StockStatus, { label: string; type: string; icon: string }> = {
|
||||||
const stockStatusMap: Record<string, { label: string; type: 'success' | 'warning' | 'danger' }> = {
|
sufficient: { label: '充足', type: 'success', icon: 'passed' },
|
||||||
sufficient: { label: '充足', type: 'success' },
|
normal: { label: '正常', type: 'primary', icon: 'info-o' },
|
||||||
normal: { label: '正常', type: '' as 'success' },
|
low: { label: '偏低', type: 'warning', icon: 'warning-o' },
|
||||||
low: { label: '偏低', type: 'warning' },
|
shortage: { label: '紧缺', type: 'danger', icon: 'close' },
|
||||||
shortage: { label: '紧缺', type: 'danger' },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 模拟物资数据 */
|
const typeTabs = ['all', 'pump', 'sandbag', 'tool', 'rescue']
|
||||||
const mockMaterials = [
|
const typeTabNames = ['全部', '水泵类', '沙袋类', '工具类', '救援类']
|
||||||
|
|
||||||
|
// ── 数据 ──
|
||||||
|
const materials = ref<Material[]>([])
|
||||||
|
|
||||||
|
const mockMaterials: Material[] = [
|
||||||
{ id: 1, name: '柴油水泵', type: 'pump', stock: 12, unit: '台', minStock: 5, status: 'sufficient', location: '1号仓库' },
|
{ id: 1, name: '柴油水泵', type: 'pump', stock: 12, unit: '台', minStock: 5, status: 'sufficient', location: '1号仓库' },
|
||||||
{ id: 2, name: '防洪沙袋', type: 'sandbag', stock: 500, unit: '个', minStock: 200, status: 'sufficient', location: '2号仓库' },
|
{ id: 2, name: '防洪沙袋', type: 'sandbag', stock: 500, unit: '个', minStock: 200, status: 'sufficient', location: '2号仓库' },
|
||||||
{ id: 3, name: '救生衣', type: 'rescue', stock: 30, unit: '件', minStock: 50, status: 'low', location: '1号仓库' },
|
{ id: 3, name: '救生衣', type: 'rescue', stock: 30, unit: '件', minStock: 50, status: 'low', location: '1号仓库' },
|
||||||
{ id: 4, name: '铁锹', type: 'tool', stock: 8, unit: '把', minStock: 20, status: 'shortage', location: '2号仓库' },
|
{ id: 4, name: '铁锹', type: 'tool', stock: 8, unit: '把', minStock: 20, status: 'shortage', location: '2号仓库' },
|
||||||
{ id: 5, name: '发电机', type: 'pump', stock: 6, unit: '台', minStock: 3, status: 'normal', location: '1号仓库' },
|
{ id: 5, name: '发电机', type: 'pump', stock: 6, unit: '台', minStock: 3, status: 'normal', location: '1号仓库' },
|
||||||
{ id: 6, name: '应急照明灯', type: 'tool', stock: 15, unit: '个', minStock: 10, status: 'normal', location: '3号仓库' },
|
{ id: 6, name: '应急照明灯', type: 'tool', stock: 15, unit: '个', minStock: 10, status: 'normal', location: '3号仓库' },
|
||||||
|
{ id: 7, name: '救生圈', type: 'rescue', stock: 45, unit: '个', minStock: 30, status: 'sufficient', location: '3号仓库' },
|
||||||
|
{ id: 8, name: '编织袋', type: 'sandbag', stock: 80, unit: '条', minStock: 300, status: 'shortage', location: '2号仓库' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const typeTabs = ['all', 'pump', 'sandbag', 'tool', 'rescue']
|
// ── 计算属性 ──
|
||||||
const typeTabNames = ['全部', '水泵类', '沙袋类', '工具类', '救援类']
|
|
||||||
|
|
||||||
const filteredList = computed(() => {
|
const filteredList = computed(() => {
|
||||||
let list = mockMaterials
|
let list = materials.value
|
||||||
if (searchText.value) {
|
if (searchText.value) {
|
||||||
const kw = searchText.value.toLowerCase()
|
const kw = searchText.value.toLowerCase()
|
||||||
list = list.filter(m =>
|
list = list.filter(m =>
|
||||||
@@ -64,46 +88,122 @@ const filteredList = computed(() => {
|
|||||||
return list
|
return list
|
||||||
})
|
})
|
||||||
|
|
||||||
function goInventory(id: number) {
|
// ── 方法 ──
|
||||||
router.push(`/managementInventory/${id}`)
|
async function fetchData() {
|
||||||
|
loading.value = true
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 400))
|
||||||
|
materials.value = mockMaterials
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onRefresh() {
|
||||||
|
refreshing.value = true
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 600))
|
||||||
|
materials.value = [...mockMaterials]
|
||||||
|
refreshing.value = false
|
||||||
|
showToast('刷新成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
function goInventory(item: Material) {
|
||||||
|
router.push(`/managementInventory/${item.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 库存进度百分比 */
|
||||||
|
function stockPercent(item: Material): number {
|
||||||
|
const max = Math.max(item.minStock * 2, item.stock)
|
||||||
|
return Math.min((item.stock / max) * 100, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
|
<!-- 导航栏 -->
|
||||||
<van-nav-bar title="物资管理" left-arrow fixed placeholder @click-left="router.back()" />
|
<van-nav-bar title="物资管理" left-arrow fixed placeholder @click-left="router.back()" />
|
||||||
|
|
||||||
|
<!-- 搜索栏 -->
|
||||||
<van-search v-model="searchText" placeholder="搜索物资名称、存放位置" shape="round" />
|
<van-search v-model="searchText" placeholder="搜索物资名称、存放位置" shape="round" />
|
||||||
|
|
||||||
<van-tabs v-model:active="activeTab" sticky>
|
<!-- 标签页 -->
|
||||||
|
<van-tabs v-model:active="activeTab" sticky color="var(--color-primary)">
|
||||||
<van-tab v-for="(name, i) in typeTabNames" :key="i" :title="name" />
|
<van-tab v-for="(name, i) in typeTabNames" :key="i" :title="name" />
|
||||||
</van-tabs>
|
</van-tabs>
|
||||||
|
|
||||||
<div class="card-list">
|
<!-- 下拉刷新 + 列表 -->
|
||||||
<van-empty v-if="filteredList.length === 0" description="暂无物资" />
|
<van-pull-refresh v-model="refreshing" @refresh="onRefresh" class="pull-refresh-wrap">
|
||||||
<van-card
|
<!-- 加载骨架 -->
|
||||||
|
<div v-if="loading" class="skeleton-wrap">
|
||||||
|
<div v-for="i in 4" :key="i" class="skeleton-card">
|
||||||
|
<van-skeleton title :row="2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<van-empty v-else-if="filteredList.length === 0" description="暂无物资">
|
||||||
|
<van-button type="primary" size="small" @click="onRefresh">刷新</van-button>
|
||||||
|
</van-empty>
|
||||||
|
|
||||||
|
<!-- 物资卡片列表 -->
|
||||||
|
<div v-else class="card-list">
|
||||||
|
<div
|
||||||
v-for="item in filteredList"
|
v-for="item in filteredList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:title="item.name"
|
class="mat-card"
|
||||||
:desc="`存放位置: ${item.location}`"
|
@click="goInventory(item)"
|
||||||
@click="goInventory(item.id)"
|
>
|
||||||
|
<div class="card-hd">
|
||||||
|
<div class="card-title-wrap">
|
||||||
|
<span
|
||||||
|
class="type-badge"
|
||||||
|
:style="{ background: typeColorMap[item.type] }"
|
||||||
>
|
>
|
||||||
<template #tags>
|
|
||||||
<van-tag :color="typeColorMap[item.type]" size="medium" text-color="#fff">
|
|
||||||
{{ typeMap[item.type] }}
|
{{ typeMap[item.type] }}
|
||||||
</van-tag>
|
</span>
|
||||||
<van-tag :type="stockStatusMap[item.status].type" size="medium">
|
<span class="card-name">{{ item.name }}</span>
|
||||||
|
</div>
|
||||||
|
<van-tag :type="stockStatusMap[item.status].type as any" size="medium">
|
||||||
{{ stockStatusMap[item.status].label }}
|
{{ stockStatusMap[item.status].label }}
|
||||||
</van-tag>
|
</van-tag>
|
||||||
</template>
|
|
||||||
<template #footer>
|
|
||||||
<div class="card-meta">
|
|
||||||
<span class="stock-count">库存: {{ item.stock }} {{ item.unit }}</span>
|
|
||||||
<span class="stock-min">最低库存: {{ item.minStock }} {{ item.unit }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</van-card>
|
<div class="card-bd">
|
||||||
|
<div class="stock-info">
|
||||||
|
<div class="stock-row">
|
||||||
|
<span class="stock-label">库存数量</span>
|
||||||
|
<span class="stock-value" :class="{ 'stock-low': item.status === 'low' || item.status === 'shortage' }">
|
||||||
|
{{ item.stock }} <small>{{ item.unit }}</small>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stock-row">
|
||||||
|
<span class="stock-label">最低库存</span>
|
||||||
|
<span class="stock-min">{{ item.minStock }} {{ item.unit }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 库存进度条 -->
|
||||||
|
<div class="stock-progress">
|
||||||
|
<div
|
||||||
|
class="progress-fill"
|
||||||
|
:style="{
|
||||||
|
width: stockPercent(item) + '%',
|
||||||
|
background: typeColorMap[item.type],
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-ft">
|
||||||
|
<span class="card-location">
|
||||||
|
<van-icon name="location-o" size="13" />
|
||||||
|
{{ item.location }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</van-pull-refresh>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -120,32 +220,141 @@ function goInventory(id: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-list {
|
.pull-refresh-wrap {
|
||||||
padding: 0 8px;
|
min-height: calc(100vh - 140px);
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.van-card) {
|
// ── 骨架屏 ──
|
||||||
margin: 8px;
|
.skeleton-wrap {
|
||||||
border-radius: 10px;
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
.skeleton-card {
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
background: var(--color-bg-card);
|
background: var(--color-bg-card);
|
||||||
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.van-tag) {
|
// ── 物资卡片 ──
|
||||||
margin-right: 4px;
|
.card-list {
|
||||||
}
|
padding: 8px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-meta {
|
.mat-card {
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hd {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
justify-content: space-between;
|
||||||
font-size: 12px;
|
align-items: flex-start;
|
||||||
color: var(--color-text-secondary);
|
margin-bottom: 14px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.stock-count {
|
.card-title-wrap {
|
||||||
font-weight: 500;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-name {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-bd {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 库存信息 ──
|
||||||
|
.stock-info {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-value {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
|
||||||
|
&.stock-low {
|
||||||
|
color: var(--color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.stock-min {
|
.stock-min {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 进度条 ──
|
||||||
|
.stock-progress {
|
||||||
|
height: 6px;
|
||||||
|
background: var(--color-border-light);
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 底部 ──
|
||||||
|
.card-ft {
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid var(--color-border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-location {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
color: var(--color-text-placeholder);
|
color: var(--color-text-placeholder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,84 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* 选择班组成员
|
* 选择班组成员 — 防汛任务人员分配
|
||||||
*
|
*
|
||||||
* 展示班组成员列表,支持搜索和多选,
|
* 展示班组成员列表,支持搜索和多选勾选,
|
||||||
* 用于分配防汛任务时的成员勾选。
|
* 按班组分组展示,用于分配防汛任务时选定人员。
|
||||||
|
* DESIGN: #1E74FF NavBar, #F4F7F8 bg, white cards border-radius 10px.
|
||||||
*/
|
*/
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { showSuccessToast } from 'vant'
|
import { showToast, showSuccessToast } from 'vant'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
|
// ── 类型 ──
|
||||||
|
interface Member {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
phone: string
|
||||||
|
team: string
|
||||||
|
role: '组长' | '组员'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 状态 ──
|
||||||
const searchText = ref('')
|
const searchText = ref('')
|
||||||
const checkedIds = ref<number[]>([])
|
const checkedIds = ref<number[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
/** 模拟成员数据 */
|
// ── 模拟数据 ──
|
||||||
const mockMembers = [
|
const members = ref<Member[]>([])
|
||||||
|
|
||||||
|
const mockMembers: Member[] = [
|
||||||
{ id: 1, name: '张建国', phone: '13800001001', team: 'A组', role: '组长' },
|
{ id: 1, name: '张建国', phone: '13800001001', team: 'A组', role: '组长' },
|
||||||
{ id: 2, name: '李明辉', phone: '13800001002', team: 'A组', role: '组员' },
|
{ id: 2, name: '李明辉', phone: '13800001002', team: 'A组', role: '组员' },
|
||||||
{ id: 3, name: '陈志远', phone: '13800001003', team: 'A组', role: '组员' },
|
{ id: 3, name: '陈志远', phone: '13800001003', team: 'A组', role: '组员' },
|
||||||
{ id: 4, name: '王强', phone: '13800001004', team: 'B组', role: '组长' },
|
{ id: 4, name: '王强', phone: '13800001004', team: 'B组', role: '组长' },
|
||||||
{ id: 5, name: '赵勇', phone: '13800001005', team: 'B组', role: '组员' },
|
{ id: 5, name: '赵勇', phone: '13800001005', team: 'B组', role: '组员' },
|
||||||
{ id: 6, name: '周文博', phone: '13800001006', team: 'B组', role: '组员' },
|
{ id: 6, name: '周文博', phone: '13800001006', team: 'B组', role: '组员' },
|
||||||
|
{ id: 7, name: '刘大伟', phone: '13800001007', team: 'C组', role: '组长' },
|
||||||
|
{ id: 8, name: '孙志强', phone: '13800001008', team: 'C组', role: '组员' },
|
||||||
|
{ id: 9, name: '马小军', phone: '13800001009', team: 'C组', role: '组员' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// ── 计算属性 ──
|
||||||
const filteredList = computed(() => {
|
const filteredList = computed(() => {
|
||||||
if (!searchText.value) return mockMembers
|
if (!searchText.value) return members.value
|
||||||
const kw = searchText.value.toLowerCase()
|
const kw = searchText.value.toLowerCase()
|
||||||
return mockMembers.filter(m =>
|
return members.value.filter(m =>
|
||||||
m.name.toLowerCase().includes(kw) ||
|
m.name.toLowerCase().includes(kw) ||
|
||||||
m.phone.includes(kw)
|
m.phone.includes(kw) ||
|
||||||
|
m.team.toLowerCase().includes(kw)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 按班组分组 */
|
||||||
|
const groupedMembers = computed(() => {
|
||||||
|
const groups: Record<string, Member[]> = {}
|
||||||
|
for (const m of filteredList.value) {
|
||||||
|
if (!groups[m.team]) groups[m.team] = []
|
||||||
|
groups[m.team].push(m)
|
||||||
|
}
|
||||||
|
return groups
|
||||||
|
})
|
||||||
|
|
||||||
|
const isAllChecked = computed(() =>
|
||||||
|
filteredList.value.length > 0 &&
|
||||||
|
filteredList.value.every(m => checkedIds.value.includes(m.id))
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── 方法 ──
|
||||||
|
function toggleCheck(id: number) {
|
||||||
|
const idx = checkedIds.value.indexOf(id)
|
||||||
|
if (idx >= 0) {
|
||||||
|
checkedIds.value.splice(idx, 1)
|
||||||
|
} else {
|
||||||
|
checkedIds.value.push(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleAll() {
|
function toggleAll() {
|
||||||
if (checkedIds.value.length === filteredList.value.length) {
|
if (isAllChecked.value) {
|
||||||
checkedIds.value = []
|
checkedIds.value = []
|
||||||
} else {
|
} else {
|
||||||
checkedIds.value = filteredList.value.map(m => m.id)
|
checkedIds.value = filteredList.value.map(m => m.id)
|
||||||
@@ -43,36 +86,60 @@ function toggleAll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onConfirm() {
|
function onConfirm() {
|
||||||
|
if (checkedIds.value.length === 0) {
|
||||||
|
showToast('请至少选择一名成员')
|
||||||
|
return
|
||||||
|
}
|
||||||
showSuccessToast(`已选择 ${checkedIds.value.length} 人`)
|
showSuccessToast(`已选择 ${checkedIds.value.length} 人`)
|
||||||
router.back()
|
router.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
loading.value = true
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300))
|
||||||
|
members.value = mockMembers
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
|
<!-- 导航栏 -->
|
||||||
<van-nav-bar title="选择班组成员" left-arrow fixed placeholder @click-left="router.back()">
|
<van-nav-bar title="选择班组成员" left-arrow fixed placeholder @click-left="router.back()">
|
||||||
<template #right>
|
<template #right>
|
||||||
<span class="nav-action" @click="toggleAll">
|
<span class="nav-action" @click="toggleAll">
|
||||||
{{ checkedIds.length === filteredList.length && filteredList.length > 0 ? '取消全选' : '全选' }}
|
{{ isAllChecked ? '取消全选' : '全选' }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</van-nav-bar>
|
</van-nav-bar>
|
||||||
|
|
||||||
<van-search v-model="searchText" placeholder="搜索姓名、手机号" shape="round" />
|
<!-- 搜索栏 -->
|
||||||
|
<van-search v-model="searchText" placeholder="搜索姓名、手机号、班组" shape="round" />
|
||||||
|
|
||||||
<van-checkbox-group v-model="checkedIds">
|
<!-- 已选提示 -->
|
||||||
|
<div v-if="checkedIds.length > 0" class="selected-bar">
|
||||||
|
<span>已选 <strong>{{ checkedIds.length }}</strong> 人</span>
|
||||||
|
<span class="selected-clear" @click="checkedIds = []">清空</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<van-loading v-if="loading" class="loading-center" size="24" />
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<van-empty v-else-if="filteredList.length === 0" description="暂未找到成员" />
|
||||||
|
|
||||||
|
<!-- 成员列表 (按班组分组) -->
|
||||||
|
<van-checkbox-group v-else v-model="checkedIds" class="member-list">
|
||||||
|
<template v-for="(group, team) in groupedMembers" :key="team">
|
||||||
|
<div class="group-header">{{ team }}</div>
|
||||||
<van-cell-group inset>
|
<van-cell-group inset>
|
||||||
<van-cell
|
<van-cell
|
||||||
v-for="item in filteredList"
|
v-for="item in group"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:title="item.name"
|
:title="item.name"
|
||||||
:label="`${item.team} · ${item.role}`"
|
:label="item.phone"
|
||||||
clickable
|
clickable
|
||||||
@click="() => {
|
@click="toggleCheck(item.id)"
|
||||||
const idx = checkedIds.indexOf(item.id)
|
|
||||||
if (idx >= 0) checkedIds.splice(idx, 1)
|
|
||||||
else checkedIds.push(item.id)
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<template #value>
|
<template #value>
|
||||||
<van-tag :type="item.role === '组长' ? 'primary' : undefined" size="medium">
|
<van-tag :type="item.role === '组长' ? 'primary' : undefined" size="medium">
|
||||||
@@ -80,16 +147,26 @@ function onConfirm() {
|
|||||||
</van-tag>
|
</van-tag>
|
||||||
</template>
|
</template>
|
||||||
<template #right-icon>
|
<template #right-icon>
|
||||||
<van-checkbox :name="item.id" />
|
<van-checkbox
|
||||||
|
:name="item.id"
|
||||||
|
@click.stop
|
||||||
|
:icon-size="20"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</van-cell>
|
</van-cell>
|
||||||
</van-cell-group>
|
</van-cell-group>
|
||||||
|
</template>
|
||||||
</van-checkbox-group>
|
</van-checkbox-group>
|
||||||
|
|
||||||
<van-empty v-if="filteredList.length === 0" description="暂未找到成员" />
|
<!-- 底部确认栏 -->
|
||||||
|
|
||||||
<div class="footer-bar">
|
<div class="footer-bar">
|
||||||
<van-button type="primary" block round @click="onConfirm">
|
<van-button
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
round
|
||||||
|
:disabled="checkedIds.length === 0"
|
||||||
|
@click="onConfirm"
|
||||||
|
>
|
||||||
确认选择 ({{ checkedIds.length }})
|
确认选择 ({{ checkedIds.length }})
|
||||||
</van-button>
|
</van-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,6 +193,56 @@ function onConfirm() {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 已选提示栏 ──
|
||||||
|
.selected-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
background: var(--color-primary-bg);
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-clear {
|
||||||
|
color: var(--color-danger);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 加载 ──
|
||||||
|
.loading-center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 分组 ──
|
||||||
|
.group-header {
|
||||||
|
padding: 12px 16px 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-list {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.van-cell-group) {
|
||||||
|
margin: 4px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.van-cell) {
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 底部栏 ──
|
||||||
.footer-bar {
|
.footer-bar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
defineProps<{
|
||||||
show: boolean
|
show: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -14,17 +14,23 @@ const emit = defineEmits<{
|
|||||||
'update:show': [val: boolean]
|
'update:show': [val: boolean]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
/** 模拟台账数据 */
|
/** 台账数据 */
|
||||||
const records = ref([
|
const records = ref([
|
||||||
{ id: 1, name: 'DN300 雨水管-城北段', type: '雨水管网', length: '2.3km' },
|
{ id: 1, name: 'DN300 雨水管-城北段', type: '雨水管网', length: '2.3km' },
|
||||||
{ id: 2, name: 'DN400 污水管-高新区', type: '污水管网', length: '1.8km' },
|
{ id: 2, name: 'DN400 污水管-高新区', type: '污水管网', length: '1.8km' },
|
||||||
{ id: 3, name: '1# 提升泵站', type: '泵站', length: '-' },
|
{ id: 3, name: '1# 提升泵站', type: '泵站', length: '-' },
|
||||||
{ id: 4, name: '中山河节制闸', type: '水闸', length: '-' },
|
{ id: 4, name: '中山河节制闸', type: '水闸', length: '-' },
|
||||||
|
{ id: 5, name: 'DN200 给水管-老城区', type: '给水管网', length: '3.1km' },
|
||||||
])
|
])
|
||||||
|
|
||||||
function onClose() {
|
function onClose() {
|
||||||
emit('update:show', false)
|
emit('update:show', false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onItemClick(item: typeof records.value[number]) {
|
||||||
|
// 处理台账项点击
|
||||||
|
console.log('selected:', item)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -35,65 +41,95 @@ function onClose() {
|
|||||||
:style="{ height: '50%' }"
|
:style="{ height: '50%' }"
|
||||||
@update:show="emit('update:show', $event)"
|
@update:show="emit('update:show', $event)"
|
||||||
>
|
>
|
||||||
<div class="tckz-pop">
|
<div class="pop-container">
|
||||||
|
<!-- Header -->
|
||||||
<div class="pop-header">
|
<div class="pop-header">
|
||||||
<span class="pop-title">台账信息</span>
|
<span class="pop-title">台账信息</span>
|
||||||
<van-icon name="cross" size="20" color="#999" @click="onClose" />
|
<span class="pop-count">{{ records.length }} 条记录</span>
|
||||||
|
<van-icon name="cross" size="20" color="#C8C9CC" @click="onClose" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
<div class="pop-body">
|
<div class="pop-body">
|
||||||
<div
|
<div
|
||||||
v-for="item in records"
|
v-for="item in records"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="record-item"
|
class="record-item"
|
||||||
|
@click="onItemClick(item)"
|
||||||
>
|
>
|
||||||
<div class="record-icon">
|
<div class="record-icon-wrap">
|
||||||
<van-icon name="notes-o" size="20" color="#1989fa" />
|
<van-icon name="notes-o" size="22" color="#1E74FF" />
|
||||||
</div>
|
</div>
|
||||||
<div class="record-info">
|
<div class="record-info">
|
||||||
<span class="record-name">{{ item.name }}</span>
|
<span class="record-name">{{ item.name }}</span>
|
||||||
<span class="record-meta">{{ item.type }} · {{ item.length }}</span>
|
<span class="record-meta">{{ item.type }} · {{ item.length }}</span>
|
||||||
</div>
|
</div>
|
||||||
<van-icon name="arrow" color="#ccc" />
|
<van-icon name="arrow" color="#C8C9CC" size="16" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Safe Area -->
|
||||||
|
<div class="pop-safe-bottom" />
|
||||||
</div>
|
</div>
|
||||||
</van-popup>
|
</van-popup>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.tckz-pop {
|
.pop-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Header ──
|
||||||
.pop-header {
|
.pop-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px 20px;
|
gap: 8px;
|
||||||
border-bottom: 1px solid var(--color-border);
|
padding: 18px 20px;
|
||||||
|
border-bottom: 1px solid #EBEDF0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
.pop-title {
|
.pop-title {
|
||||||
font-size: 16px;
|
font-size: 17px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--color-text-regular);
|
color: #323233;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pop-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #969799;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Body ──
|
||||||
.pop-body {
|
.pop-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 8px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.record-item {
|
.record-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px 20px;
|
padding: 14px 20px;
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid #F2F3F5;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
|
||||||
.record-icon {
|
&:active {
|
||||||
|
background: #F7F8FA;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-icon-wrap {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #E3F2FD;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
@@ -103,17 +139,27 @@ function onClose() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
.record-name {
|
.record-name {
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
color: var(--color-text-regular);
|
color: #323233;
|
||||||
|
font-weight: 500;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.record-meta {
|
.record-meta {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--color-text-placeholder);
|
color: #969799;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Safe Area ──
|
||||||
|
.pop-safe-bottom {
|
||||||
|
height: env(safe-area-inset-bottom, 16px);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const emit = defineEmits<{
|
|||||||
'update:show': [val: boolean]
|
'update:show': [val: boolean]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
/** 模拟仪表数据 */
|
/** 仪表数据 */
|
||||||
const gauges = ref([
|
const gauges = ref([
|
||||||
{ id: 1, name: '流量', value: '12.5', unit: 'm³/h', status: 'normal' },
|
{ id: 1, name: '流量', value: '12.5', unit: 'm³/h', status: 'normal' },
|
||||||
{ id: 2, name: '压力', value: '0.35', unit: 'MPa', status: 'normal' },
|
{ id: 2, name: '压力', value: '0.35', unit: 'MPa', status: 'normal' },
|
||||||
@@ -22,9 +22,13 @@ const gauges = ref([
|
|||||||
{ id: 4, name: '水质', value: '6.8', unit: 'pH', status: 'normal' },
|
{ id: 4, name: '水质', value: '6.8', unit: 'pH', status: 'normal' },
|
||||||
])
|
])
|
||||||
|
|
||||||
const statusColorMap: Record<string, string> = {
|
const statusCfg: Record<string, { color: string; bg: string; label: string }> = {
|
||||||
normal: '#07c160',
|
normal: { color: '#07C160', bg: '#E8F8EF', label: '正常' },
|
||||||
alarm: '#ee0a24',
|
alarm: { color: '#EE0A24', bg: '#FDECEC', label: '告警' },
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
emit('update:show', false)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -33,105 +37,154 @@ const statusColorMap: Record<string, string> = {
|
|||||||
:show="show"
|
:show="show"
|
||||||
position="bottom"
|
position="bottom"
|
||||||
round
|
round
|
||||||
:style="{ height: '40%' }"
|
:style="{ height: '42%' }"
|
||||||
@update:show="emit('update:show', $event)"
|
@update:show="emit('update:show', $event)"
|
||||||
>
|
>
|
||||||
<div class="yb-pop">
|
<div class="pop-container">
|
||||||
|
<!-- Header -->
|
||||||
<div class="pop-header">
|
<div class="pop-header">
|
||||||
<span class="pop-title">实时仪表</span>
|
<span class="pop-title">实时监测</span>
|
||||||
<van-icon name="cross" size="20" color="#999" @click="emit('update:show', false)" />
|
<span class="pop-subtitle">数据更新时间: 刚刚</span>
|
||||||
|
<van-icon name="cross" size="20" color="#C8C9CC" @click="onClose" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
<div class="pop-body">
|
<div class="pop-body">
|
||||||
<div
|
<div
|
||||||
v-for="gauge in gauges"
|
v-for="gauge in gauges"
|
||||||
:key="gauge.id"
|
:key="gauge.id"
|
||||||
class="gauge-item"
|
class="gauge-card"
|
||||||
|
:style="{ borderColor: statusCfg[gauge.status].color }"
|
||||||
>
|
>
|
||||||
|
<div class="gauge-header">
|
||||||
<span
|
<span
|
||||||
class="gauge-dot"
|
class="gauge-dot"
|
||||||
:style="{ backgroundColor: statusColorMap[gauge.status] }"
|
:style="{ backgroundColor: statusCfg[gauge.status].color }"
|
||||||
/>
|
/>
|
||||||
<div class="gauge-info">
|
<span class="gauge-name">{{ gauge.name }}</span>
|
||||||
<span class="gauge-label">{{ gauge.name }}</span>
|
<span
|
||||||
<span class="gauge-value">
|
class="gauge-status"
|
||||||
{{ gauge.value }}
|
:style="{ color: statusCfg[gauge.status].color, background: statusCfg[gauge.status].bg }"
|
||||||
<small>{{ gauge.unit }}</small>
|
>
|
||||||
|
{{ statusCfg[gauge.status].label }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="gauge-value">
|
||||||
|
<span class="value-num">{{ gauge.value }}</span>
|
||||||
|
<span class="value-unit">{{ gauge.unit }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Safe Area -->
|
||||||
|
<div class="pop-safe-bottom" />
|
||||||
|
</div>
|
||||||
</van-popup>
|
</van-popup>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.yb-pop {
|
.pop-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
background: #F4F7F8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Header ──
|
||||||
.pop-header {
|
.pop-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px 20px;
|
gap: 8px;
|
||||||
border-bottom: 1px solid var(--color-border);
|
padding: 18px 20px;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #EBEDF0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
.pop-title {
|
.pop-title {
|
||||||
font-size: 16px;
|
font-size: 17px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--color-text-regular);
|
color: #323233;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pop-subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #969799;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Body ──
|
||||||
.pop-body {
|
.pop-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-content: flex-start;
|
||||||
justify-content: center;
|
gap: 12px;
|
||||||
gap: 16px;
|
padding: 16px 12px;
|
||||||
padding: 24px 20px;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gauge-item {
|
// ── Gauge Card ──
|
||||||
width: calc(50% - 8px);
|
.gauge-card {
|
||||||
|
width: calc(50% - 6px);
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 14px;
|
||||||
|
border-left: 3px solid #EBEDF0;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #f7f8fa;
|
gap: 8px;
|
||||||
border-radius: 10px;
|
|
||||||
padding: 16px;
|
|
||||||
|
|
||||||
.gauge-dot {
|
.gauge-dot {
|
||||||
width: 10px;
|
width: 8px;
|
||||||
height: 10px;
|
height: 8px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin-right: 12px;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gauge-info {
|
.gauge-name {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
|
|
||||||
.gauge-label {
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--color-text-secondary);
|
color: #969799;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-status {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.gauge-value {
|
.gauge-value {
|
||||||
font-size: 22px;
|
display: flex;
|
||||||
font-weight: 700;
|
align-items: baseline;
|
||||||
color: var(--color-text-regular);
|
gap: 4px;
|
||||||
|
|
||||||
small {
|
.value-num {
|
||||||
font-size: 12px;
|
font-size: 26px;
|
||||||
font-weight: 400;
|
font-weight: 700;
|
||||||
color: var(--color-text-placeholder);
|
color: #323233;
|
||||||
}
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.value-unit {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #969799;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Safe Area ──
|
||||||
|
.pop-safe-bottom {
|
||||||
|
height: env(safe-area-inset-bottom, 12px);
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #F4F7F8;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,27 +5,55 @@
|
|||||||
* 展示工程项目的详细信息,包括基本信息、
|
* 展示工程项目的详细信息,包括基本信息、
|
||||||
* 施工进度、现场照片等。
|
* 施工进度、现场照片等。
|
||||||
*/
|
*/
|
||||||
import { ref } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const detailId = computed(() => route.params.id as string | undefined)
|
||||||
|
|
||||||
/** 模拟项目详情 */
|
interface ProjectDetail {
|
||||||
const project = {
|
id: string
|
||||||
id: route.params.id,
|
name: string
|
||||||
|
no: string
|
||||||
|
company: string
|
||||||
|
progress: number
|
||||||
|
status: 'building' | 'paused' | 'completed'
|
||||||
|
manager: string
|
||||||
|
phone: string
|
||||||
|
startDate: string
|
||||||
|
endDate: string
|
||||||
|
budget: string
|
||||||
|
address: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const project = ref<ProjectDetail>({
|
||||||
|
id: detailId.value || '1',
|
||||||
name: '城北雨水管网改造工程',
|
name: '城北雨水管网改造工程',
|
||||||
no: 'XM-2025-001',
|
no: 'XM-2025-001',
|
||||||
company: '中建三局',
|
company: '中建三局',
|
||||||
manager: '赵工',
|
|
||||||
phone: '13800001010',
|
|
||||||
progress: 75,
|
progress: 75,
|
||||||
status: 'building',
|
status: 'building',
|
||||||
|
manager: '赵工',
|
||||||
|
phone: '13800001010',
|
||||||
startDate: '2025-03-01',
|
startDate: '2025-03-01',
|
||||||
endDate: '2025-09-30',
|
endDate: '2025-12-31',
|
||||||
budget: '1250万元',
|
budget: '1250万元',
|
||||||
address: '城北工业园区大道路段',
|
address: '城北工业园区大道路段',
|
||||||
description: '该项目对城北工业园区范围内雨水管网进行全面改造升级,包括主管网更换、检查井增设、雨水口改造等内容。改造完成后将显著提升区域排水能力。',
|
description: '该项目对城北工业园区范围内雨水管网进行全面改造升级,包括主管网更换、检查井增设、雨水口改造等内容。改造完成后将显著提升区域排水能力,惠及周边30万居民。',
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusCfg: Record<string, { label: string; color: string; bg: string }> = {
|
||||||
|
building: { label: '在建', color: '#1E74FF', bg: '#E3F2FD' },
|
||||||
|
paused: { label: '暂停', color: '#FF976A', bg: '#FFF3ED' },
|
||||||
|
completed: { label: '竣工', color: '#07C160', bg: '#E8F8EF' },
|
||||||
|
}
|
||||||
|
|
||||||
|
function progressColor(pct: number): string {
|
||||||
|
if (pct >= 80) return '#07C160'
|
||||||
|
if (pct >= 40) return '#1E74FF'
|
||||||
|
return '#FF976A'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 模拟现场照片 */
|
/** 模拟现场照片 */
|
||||||
@@ -36,21 +64,21 @@ const photos = ref([
|
|||||||
{ url: 'https://img.yzcdn.cn/vant/apple-3.jpg' },
|
{ url: 'https://img.yzcdn.cn/vant/apple-3.jpg' },
|
||||||
])
|
])
|
||||||
|
|
||||||
function progressColor(pct: number): string {
|
/** 模拟里程碑 */
|
||||||
if (pct >= 80) return '#07c160'
|
const milestones = ref([
|
||||||
if (pct >= 40) return '#1989fa'
|
{ text: '项目立项', time: '2025-02-15', done: true },
|
||||||
return '#ff976a'
|
{ text: '施工设计', time: '2025-03-15', done: true },
|
||||||
}
|
{ text: '管网铺设', time: '2025-06-30', done: true },
|
||||||
|
{ text: '泵站建设', time: '2025-09-30', done: false },
|
||||||
|
{ text: '竣工验收', time: '2025-12-31', done: false },
|
||||||
|
])
|
||||||
|
|
||||||
const statusMap: Record<string, string> = {
|
const activeStep = computed(() => milestones.value.filter(m => m.done).length - 1)
|
||||||
building: '在建',
|
|
||||||
paused: '暂停',
|
|
||||||
completed: '竣工',
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page-container">
|
<div class="page">
|
||||||
|
<!-- NavBar -->
|
||||||
<van-nav-bar
|
<van-nav-bar
|
||||||
title="项目详情"
|
title="项目详情"
|
||||||
left-arrow
|
left-arrow
|
||||||
@@ -59,42 +87,119 @@ const statusMap: Record<string, string> = {
|
|||||||
@click-left="router.back()"
|
@click-left="router.back()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<van-cell-group inset>
|
<!-- Status Banner -->
|
||||||
<van-cell title="项目名称" :value="project.name" />
|
<div class="status-banner">
|
||||||
<van-cell title="项目编号" :value="project.no" />
|
<span
|
||||||
<van-cell title="施工单位" :value="project.company" />
|
class="status-badge"
|
||||||
<van-cell title="项目负责人" :value="project.manager" />
|
:style="{ color: statusCfg[project.status].color, background: statusCfg[project.status].bg }"
|
||||||
<van-cell title="联系电话" :value="project.phone" />
|
>
|
||||||
<van-cell title="项目状态">
|
{{ statusCfg[project.status].label }}
|
||||||
<template #value>
|
</span>
|
||||||
<van-tag type="primary" size="medium">{{ statusMap[project.status] }}</van-tag>
|
<span class="status-name">{{ project.name }}</span>
|
||||||
</template>
|
</div>
|
||||||
</van-cell>
|
|
||||||
<van-cell title="施工地址" :value="project.address" />
|
|
||||||
<van-cell title="开工日期" :value="project.startDate" />
|
|
||||||
<van-cell title="计划竣工" :value="project.endDate" />
|
|
||||||
<van-cell title="项目预算" :value="project.budget" />
|
|
||||||
</van-cell-group>
|
|
||||||
|
|
||||||
<van-cell-group inset style="margin-top: 12px">
|
<!-- Section: 基本信息 -->
|
||||||
<van-cell title="施工进度" />
|
<div class="section">
|
||||||
<div class="progress-section">
|
<div class="section-header">
|
||||||
|
<span class="section-accent"></span>
|
||||||
|
<span class="section-title">基本信息</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">项目编号</span>
|
||||||
|
<span class="info-value">{{ project.no }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">施工单位</span>
|
||||||
|
<span class="info-value">{{ project.company }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">负责人</span>
|
||||||
|
<span class="info-value">{{ project.manager }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">联系电话</span>
|
||||||
|
<span class="info-value">{{ project.phone }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">施工地址</span>
|
||||||
|
<span class="info-value">{{ project.address }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">计划工期</span>
|
||||||
|
<span class="info-value">{{ project.startDate }} ~ {{ project.endDate }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item full-width">
|
||||||
|
<span class="info-label">预算金额</span>
|
||||||
|
<span class="info-value highlight">{{ project.budget }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section: 项目描述 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-accent"></span>
|
||||||
|
<span class="section-title">项目描述</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<p class="desc-text">{{ project.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section: 工程进度 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-accent"></span>
|
||||||
|
<span class="section-title">工程进度</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
<van-progress
|
<van-progress
|
||||||
:percentage="project.progress"
|
:percentage="project.progress"
|
||||||
:color="progressColor(project.progress)"
|
:color="progressColor(project.progress)"
|
||||||
:pivot-text="`${project.progress}%`"
|
:pivot-text="`${project.progress}%`"
|
||||||
stroke-width="12"
|
stroke-width="10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</van-cell-group>
|
</div>
|
||||||
|
|
||||||
<van-cell-group inset style="margin-top: 12px">
|
<!-- Section: 项目里程碑 -->
|
||||||
<van-cell title="项目概述" />
|
<div class="section">
|
||||||
<div class="content-block">{{ project.description }}</div>
|
<div class="section-header">
|
||||||
</van-cell-group>
|
<span class="section-accent"></span>
|
||||||
|
<span class="section-title">项目里程碑</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<van-steps
|
||||||
|
:active="activeStep"
|
||||||
|
direction="vertical"
|
||||||
|
active-color="#1E74FF"
|
||||||
|
>
|
||||||
|
<van-step v-for="(step, idx) in milestones" :key="idx">
|
||||||
|
<template #active-icon>
|
||||||
|
<van-icon name="checked" color="#1E74FF" />
|
||||||
|
</template>
|
||||||
|
<template #inactive-icon>
|
||||||
|
<van-icon name="clock-o" color="#C8C9CC" />
|
||||||
|
</template>
|
||||||
|
<div class="step-content">
|
||||||
|
<h4 :class="{ 'step-done': step.done }">{{ step.text }}</h4>
|
||||||
|
<p>{{ step.time }}</p>
|
||||||
|
</div>
|
||||||
|
</van-step>
|
||||||
|
</van-steps>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="photo-section" v-if="photos.length > 0">
|
<!-- Section: 现场照片 -->
|
||||||
<div class="section-title">现场照片</div>
|
<div class="section" v-if="photos.length > 0">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-accent"></span>
|
||||||
|
<span class="section-title">现场照片</span>
|
||||||
|
</div>
|
||||||
|
<div class="card photo-card">
|
||||||
<div class="photo-grid">
|
<div class="photo-grid">
|
||||||
<van-image
|
<van-image
|
||||||
v-for="(photo, idx) in photos"
|
v-for="(photo, idx) in photos"
|
||||||
@@ -105,50 +210,165 @@ const statusMap: Record<string, string> = {
|
|||||||
fit="cover"
|
fit="cover"
|
||||||
radius="8"
|
radius="8"
|
||||||
lazy-load
|
lazy-load
|
||||||
|
class="photo-item"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom spacing -->
|
||||||
|
<div class="bottom-spacer" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.page-container {
|
.page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--color-bg-page);
|
background: #F4F7F8;
|
||||||
padding-bottom: 24px;
|
padding-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── NavBar ──
|
||||||
:deep(.van-nav-bar) {
|
:deep(.van-nav-bar) {
|
||||||
background: var(--color-primary);
|
background: #1E74FF;
|
||||||
--van-nav-bar-title-text-color: #fff;
|
--van-nav-bar-title-text-color: #fff;
|
||||||
--van-nav-bar-text-color: #fff;
|
--van-nav-bar-text-color: #fff;
|
||||||
--van-nav-bar-icon-color: #fff;
|
--van-nav-bar-icon-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Status Banner ──
|
||||||
|
.status-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 16px;
|
||||||
|
margin: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-section {
|
.status-name {
|
||||||
padding: 12px 16px;
|
font-size: 15px;
|
||||||
background: var(--color-bg-card);
|
font-weight: 500;
|
||||||
|
color: #323233;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-block {
|
// ── Section ──
|
||||||
padding: 12px 16px;
|
.section {
|
||||||
font-size: 14px;
|
margin: 0 12px 10px;
|
||||||
line-height: 1.8;
|
|
||||||
color: var(--color-text-regular);
|
.section-header {
|
||||||
background: var(--color-bg-card);
|
display: flex;
|
||||||
white-space: pre-wrap;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.photo-section {
|
.section-accent {
|
||||||
margin-top: 12px;
|
width: 3px;
|
||||||
padding: 12px 16px;
|
height: 16px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #1E74FF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #323233;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Card ──
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Info Grid ──
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #F2F3F5;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
&:nth-child(odd) {
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.full-width {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #323233;
|
||||||
|
word-break: break-all;
|
||||||
|
|
||||||
|
&.highlight {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #EE0A24;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Description ──
|
||||||
|
.desc-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #646566;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Steps ──
|
||||||
|
:deep(.van-step__title) {
|
||||||
|
.step-content {
|
||||||
|
h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #969799;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
&.step-done {
|
||||||
|
color: #323233;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-bottom: 10px;
|
}
|
||||||
color: var(--color-text-primary);
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #C8C9CC;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Photos ──
|
||||||
|
.photo-card {
|
||||||
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.photo-grid {
|
.photo-grid {
|
||||||
@@ -156,4 +376,14 @@ const statusMap: Record<string, string> = {
|
|||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.photo-item {
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Bottom Spacer ──
|
||||||
|
.bottom-spacer {
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -9,41 +9,43 @@ import { ref, computed } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const searchText = ref('')
|
const searchText = ref('')
|
||||||
const activeTab = ref(0)
|
const activeTab = ref(0)
|
||||||
|
|
||||||
/** 状态映射 */
|
interface ConstructionProject {
|
||||||
const statusMap: Record<string, string> = {
|
id: number
|
||||||
building: '在建',
|
name: string
|
||||||
paused: '暂停',
|
no: string
|
||||||
completed: '竣工',
|
company: string
|
||||||
|
progress: number
|
||||||
|
status: 'building' | 'paused' | 'completed'
|
||||||
|
manager: string
|
||||||
|
startDate: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusColorMap: Record<string, 'primary' | 'warning' | 'success'> = {
|
const projects: ConstructionProject[] = [
|
||||||
building: 'primary',
|
|
||||||
paused: 'warning',
|
|
||||||
completed: 'success',
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 进度颜色 */
|
|
||||||
function progressColor(pct: number): string {
|
|
||||||
if (pct >= 80) return '#07c160'
|
|
||||||
if (pct >= 40) return '#1989fa'
|
|
||||||
return '#ff976a'
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 模拟项目数据 */
|
|
||||||
const mockProjects = [
|
|
||||||
{ id: 1, name: '城北雨水管网改造工程', no: 'XM-2025-001', company: '中建三局', progress: 75, status: 'building', manager: '赵工', startDate: '2025-03-01' },
|
{ id: 1, name: '城北雨水管网改造工程', no: 'XM-2025-001', company: '中建三局', progress: 75, status: 'building', manager: '赵工', startDate: '2025-03-01' },
|
||||||
{ id: 2, name: '东风路排水泵站新建项目', no: 'XM-2025-002', company: '水务工程公司', progress: 30, status: 'building', manager: '钱工', startDate: '2025-04-15' },
|
{ id: 2, name: '东风路排水泵站新建项目', no: 'XM-2025-002', company: '水务工程公司', progress: 30, status: 'building', manager: '钱工', startDate: '2025-04-15' },
|
||||||
{ id: 3, name: '中山河河道整治工程', no: 'XM-2025-003', company: '市政建设集团', progress: 90, status: 'building', manager: '孙工', startDate: '2025-01-10' },
|
{ id: 3, name: '中山河河道整治工程', no: 'XM-2025-003', company: '市政建设集团', progress: 90, status: 'building', manager: '孙工', startDate: '2025-01-10' },
|
||||||
{ id: 4, name: '开发区污水管网铺设', no: 'XM-2025-004', company: '中交一公局', progress: 0, status: 'paused', manager: '李工', startDate: '2025-05-01' },
|
{ id: 4, name: '开发区污水管网铺设', no: 'XM-2025-004', company: '中交一公局', progress: 0, status: 'paused', manager: '李工', startDate: '2025-05-01' },
|
||||||
{ id: 5, name: '老城区雨污分流改造', no: 'XM-2025-005', company: '中铁十二局', progress: 100, status: 'completed', manager: '周工', startDate: '2024-09-01' },
|
{ id: 5, name: '老城区雨污分流改造', no: 'XM-2025-005', company: '中铁十二局', progress: 100, status: 'completed', manager: '周工', startDate: '2024-09-01' },
|
||||||
|
{ id: 6, name: '南湖片区海绵城市试点', no: 'XM-2025-006', company: '葛洲坝集团', progress: 55, status: 'building', manager: '吴工', startDate: '2025-06-01' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const statusCfg: Record<string, { label: string; color: string; bg: string }> = {
|
||||||
|
building: { label: '在建', color: '#1E74FF', bg: '#E3F2FD' },
|
||||||
|
paused: { label: '暂停', color: '#FF976A', bg: '#FFF3ED' },
|
||||||
|
completed: { label: '竣工', color: '#07C160', bg: '#E8F8EF' },
|
||||||
|
}
|
||||||
|
|
||||||
|
function progressColor(pct: number): string {
|
||||||
|
if (pct >= 80) return '#07C160'
|
||||||
|
if (pct >= 40) return '#1E74FF'
|
||||||
|
return '#FF976A'
|
||||||
|
}
|
||||||
|
|
||||||
const filteredList = computed(() => {
|
const filteredList = computed(() => {
|
||||||
let list = mockProjects
|
let list = projects
|
||||||
if (searchText.value) {
|
if (searchText.value) {
|
||||||
const kw = searchText.value.toLowerCase()
|
const kw = searchText.value.toLowerCase()
|
||||||
list = list.filter(p =>
|
list = list.filter(p =>
|
||||||
@@ -64,85 +66,187 @@ function goDetail(id: number) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page-container">
|
<div class="page">
|
||||||
<van-nav-bar title="工程项目" left-arrow fixed placeholder @click-left="router.back()" />
|
<!-- NavBar -->
|
||||||
|
<van-nav-bar
|
||||||
|
title="工程项目"
|
||||||
|
left-arrow
|
||||||
|
fixed
|
||||||
|
placeholder
|
||||||
|
@click-left="router.back()"
|
||||||
|
/>
|
||||||
|
|
||||||
<van-search v-model="searchText" placeholder="搜索项目名称、编号、单位" shape="round" />
|
<!-- Search -->
|
||||||
|
<van-search
|
||||||
|
v-model="searchText"
|
||||||
|
placeholder="搜索项目名称、编号、单位"
|
||||||
|
shape="round"
|
||||||
|
class="search-bar"
|
||||||
|
/>
|
||||||
|
|
||||||
<van-tabs v-model:active="activeTab" sticky>
|
<!-- Tabs -->
|
||||||
|
<van-tabs v-model:active="activeTab" sticky swipeable class="tabs-bar">
|
||||||
<van-tab title="全部" />
|
<van-tab title="全部" />
|
||||||
<van-tab title="在建" />
|
<van-tab title="在建" />
|
||||||
<van-tab title="暂停" />
|
<van-tab title="暂停" />
|
||||||
<van-tab title="竣工" />
|
<van-tab title="竣工" />
|
||||||
</van-tabs>
|
</van-tabs>
|
||||||
|
|
||||||
<div class="card-list">
|
<!-- Project List -->
|
||||||
|
<div class="content">
|
||||||
<van-empty v-if="filteredList.length === 0" description="暂无项目" />
|
<van-empty v-if="filteredList.length === 0" description="暂无项目" />
|
||||||
<van-card
|
|
||||||
|
<div
|
||||||
v-for="item in filteredList"
|
v-for="item in filteredList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:title="item.name"
|
class="proj-card"
|
||||||
:desc="`施工单位: ${item.company}`"
|
|
||||||
@click="goDetail(item.id)"
|
@click="goDetail(item.id)"
|
||||||
>
|
>
|
||||||
<template #tags>
|
<div class="card-accent" :style="{ background: progressColor(item.progress) }" />
|
||||||
<van-tag :type="statusColorMap[item.status]" size="medium">
|
|
||||||
{{ statusMap[item.status] }}
|
<div class="card-body">
|
||||||
</van-tag>
|
<div class="card-header">
|
||||||
</template>
|
<span class="card-title">{{ item.name }}</span>
|
||||||
<template #footer>
|
<span
|
||||||
<div class="progress-wrap">
|
class="status-tag"
|
||||||
|
:style="{ color: statusCfg[item.status].color, background: statusCfg[item.status].bg }"
|
||||||
|
>
|
||||||
|
{{ statusCfg[item.status].label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-info">
|
||||||
|
<span class="info-text">{{ item.no }}</span>
|
||||||
|
<span class="info-divider">|</span>
|
||||||
|
<span class="info-text">{{ item.company }}</span>
|
||||||
|
<span class="info-divider">|</span>
|
||||||
|
<span class="info-text">负责人: {{ item.manager }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<van-progress
|
<van-progress
|
||||||
:percentage="item.progress"
|
:percentage="item.progress"
|
||||||
:color="progressColor(item.progress)"
|
:color="progressColor(item.progress)"
|
||||||
:pivot-text="`${item.progress}%`"
|
:pivot-text="`${item.progress}%`"
|
||||||
|
stroke-width="6"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<van-icon name="arrow" color="#C8C9CC" class="card-arrow" />
|
||||||
</div>
|
</div>
|
||||||
<div class="card-meta">
|
|
||||||
<span>{{ item.no }}</span>
|
|
||||||
<span>负责人: {{ item.manager }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</van-card>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.page-container {
|
.page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--color-bg-page);
|
background: #F4F7F8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── NavBar ──
|
||||||
:deep(.van-nav-bar) {
|
:deep(.van-nav-bar) {
|
||||||
background: var(--color-primary);
|
background: #1E74FF;
|
||||||
--van-nav-bar-title-text-color: #fff;
|
--van-nav-bar-title-text-color: #fff;
|
||||||
--van-nav-bar-text-color: #fff;
|
--van-nav-bar-text-color: #fff;
|
||||||
--van-nav-bar-icon-color: #fff;
|
--van-nav-bar-icon-color: #fff;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.card-list {
|
// ── Search ──
|
||||||
padding: 0 8px;
|
.search-bar {
|
||||||
|
:deep(.van-search__content) {
|
||||||
:deep(.van-card) {
|
background: #fff;
|
||||||
margin: 8px;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: var(--color-bg-card);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.van-tag) {
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-wrap {
|
// ── Tabs ──
|
||||||
margin: 8px 0;
|
.tabs-bar {
|
||||||
|
:deep(.van-tabs__nav) {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-meta {
|
// ── Content ──
|
||||||
|
.content {
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Card ──
|
||||||
|
.proj-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
position: relative;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.15s, box-shadow 0.15s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.985);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-accent {
|
||||||
|
width: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 14px 16px;
|
||||||
|
position: relative;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #323233;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.info-text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--color-text-secondary);
|
color: #969799;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-divider {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #EBEDF0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-arrow {
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user