Files
GEAR-OA/gear-ui3/src/views/mes/production/index.vue
朱昊天 9f4e1c39ad 生产模块完成任务接口加幂等:重复点击“完成”不应重复生成回执
优化页面体验
任务允许重复但要合并
2026-06-01 13:56:39 +08:00

833 lines
27 KiB
Vue

<template>
<div class="production-page app-container" v-loading="loading">
<el-card shadow="never" class="page-head">
<div class="page-head__row">
<div class="page-title">
<div class="page-title__main">生产任务</div>
<div class="page-title__sub">任务清单 / 明细 / 回执</div>
</div>
<div class="page-actions">
<el-button type="primary" @click="openAdd">新增任务</el-button>
<el-button @click="loadTasks">刷新</el-button>
</div>
</div>
<div class="page-head__row page-head__row--date">
<div class="date-bar">
<el-radio-group v-model="datePreset" size="small" @change="applyDatePreset">
<el-radio-button label="today">今天</el-radio-button>
<el-radio-button label="yesterday">昨天</el-radio-button>
<el-radio-button label="last7">近7天</el-radio-button>
<el-radio-button label="thisMonth">本月</el-radio-button>
<el-radio-button label="custom">自定义</el-radio-button>
</el-radio-group>
<el-popover
v-if="datePreset === 'custom'"
v-model:visible="customPopoverOpen"
placement="bottom-start"
trigger="click"
width="360"
>
<template #reference>
<el-button size="small" plain class="custom-range-btn">{{ customRangeLabel }}</el-button>
</template>
<el-date-picker
v-model="customRange"
type="daterange"
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
start-placeholder="开始日期"
end-placeholder="结束日期"
unlink-panels
teleported
style="width: 320px;"
@change="onCustomRangePicked"
/>
</el-popover>
</div>
</div>
<div class="page-head__row page-head__row--filters">
<el-form :inline="true" class="filter-form">
<el-form-item label="状态">
<el-select v-model="filters.status" placeholder="全部" clearable style="width: 140px;">
<el-option label="进行中" value="1" />
<el-option label="已完成" value="2" />
<el-option label="已暂停" value="3" />
</el-select>
</el-form-item>
<el-form-item label="关键词">
<el-input v-model="filters.keyword" placeholder="任务名称/编号" clearable style="width: 240px;" />
</el-form-item>
</el-form>
<div class="page-stats">
<el-tag type="info" effect="light">全部 {{ taskList.length }}</el-tag>
<el-tag type="success" effect="light" style="margin-left: 8px;">进行中 {{ runningCount }}</el-tag>
<el-tag type="warning" effect="light" style="margin-left: 8px;">暂停 {{ pausedCount }}</el-tag>
<el-tag type="danger" effect="light" style="margin-left: 8px;">完成 {{ finishedCount }}</el-tag>
</div>
</div>
</el-card>
<el-row :gutter="12">
<el-col :span="6">
<el-card shadow="never" class="left-card">
<template #header>
<div class="card-header">
<span>任务清单</span>
<div>
<el-button type="primary" link @click="openAdd">新增</el-button>
<el-button type="primary" link @click="loadTasks">刷新</el-button>
</div>
</div>
</template>
<div class="task-list">
<el-empty v-if="!filteredTaskList.length" description="暂无任务" />
<el-scrollbar v-else height="640px">
<div
v-for="t in filteredTaskList"
:key="t.taskId"
class="task-item"
:class="{ active: t.taskId === selectedTaskId }"
@click="selectTask(t.taskId)"
>
<div class="task-item__top">
<div class="task-item__title">{{ t.taskName || t.taskCode || ('任务' + t.taskId) }}</div>
<el-tag size="small" :type="statusTagType(t.status)" effect="light">{{ statusLabel(t.status) }}</el-tag>
</div>
<div class="task-item__sub">
<span>计划 {{ formatQty(t.planQty) }}</span>
<span style="margin-left: 10px;">已完 {{ formatQty(t.finishedQty) }}</span>
<span style="margin-left: 10px;">未完 {{ formatQty(t.unfinishedQty) }}</span>
</div>
<div class="task-item__meta">
<span v-if="t.planStartTime || t.planEndTime">计划 {{ formatTimeRange(t.planStartTime, t.planEndTime) }}</span>
<span v-if="t.createBy" style="margin-left: 10px;">创建 {{ t.createBy }}</span>
<span v-if="t.updateTime" style="margin-left: 10px;">更新 {{ formatTime(t.updateTime) }}</span>
</div>
<el-progress :percentage="taskProgressPercent(t)" :stroke-width="8" :show-text="false" />
</div>
</el-scrollbar>
</div>
</el-card>
</el-col>
<el-col :span="18">
<el-card shadow="never" class="mb12">
<template #header>
<div class="card-header">
<span>任务明细</span>
<div v-if="selectedTask" class="card-header__right">
<div class="card-meta">
<span class="meta-main">{{ selectedTask.taskName || selectedTask.taskCode || ('任务' + selectedTask.taskId) }}</span>
<el-tag size="small" :type="statusTagType(selectedTask.status)" effect="light" style="margin-left: 8px;">{{ statusLabel(selectedTask.status) }}</el-tag>
</div>
<el-button
v-if="String(selectedTask.status) === '1'"
size="small"
type="primary"
:loading="completeLoading"
@click="completeSelectedTask"
>
完成任务
</el-button>
</div>
</div>
</template>
<el-empty v-if="!selectedTaskId" description="请选择任务" />
<el-table v-else :data="productRows" size="small" border stripe :header-cell-style="{ background: '#f5f7fa' }">
<el-table-column label="产品" prop="productName" min-width="180" show-overflow-tooltip />
<el-table-column label="规格" prop="spec" width="140" show-overflow-tooltip />
<el-table-column label="型号" prop="model" width="140" show-overflow-tooltip />
<el-table-column label="计划数量" prop="planQty" width="120" align="right">
<template #default="scope">{{ formatQty(scope.row.planQty) }}</template>
</el-table-column>
<el-table-column label="已完成" prop="finishedQty" width="120" align="right">
<template #default="scope">{{ formatQty(scope.row.finishedQty) }}</template>
</el-table-column>
<el-table-column label="不良" prop="badQty" width="120" align="right">
<template #default="scope">{{ formatQty(scope.row.badQty) }}</template>
</el-table-column>
<el-table-column label="单位" prop="unit" width="90" />
</el-table>
</el-card>
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>任务回执</span>
<div v-if="selectedTask" class="card-meta">
<span class="meta-sub">主材 / 辅材 / 产品概况</span>
</div>
</div>
</template>
<el-empty v-if="!selectedTaskId" description="请选择任务" />
<el-tabs v-else type="border-card">
<el-tab-pane label="主材">
<el-table :data="mainMaterialRows" size="small" border stripe :header-cell-style="{ background: '#f5f7fa' }">
<el-table-column label="名称" prop="materialName" min-width="180" show-overflow-tooltip />
<el-table-column label="计划" prop="planQty" width="120" align="right">
<template #default="scope">{{ formatQty(scope.row.planQty) }}</template>
</el-table-column>
<el-table-column label="已用" prop="usedQty" width="120" align="right">
<template #default="scope">{{ formatQty(scope.row.usedQty) }}</template>
</el-table-column>
<el-table-column label="单位" prop="unit" width="90" />
</el-table>
</el-tab-pane>
<el-tab-pane label="辅材">
<el-table :data="auxMaterialRows" size="small" border stripe :header-cell-style="{ background: '#f5f7fa' }">
<el-table-column label="名称" prop="materialName" min-width="180" show-overflow-tooltip />
<el-table-column label="计划" prop="planQty" width="120" align="right">
<template #default="scope">{{ formatQty(scope.row.planQty) }}</template>
</el-table-column>
<el-table-column label="已用" prop="usedQty" width="120" align="right">
<template #default="scope">{{ formatQty(scope.row.usedQty) }}</template>
</el-table-column>
<el-table-column label="单位" prop="unit" width="90" />
</el-table>
</el-tab-pane>
<el-tab-pane label="产品概况">
<el-table :data="productRows" size="small" border stripe :header-cell-style="{ background: '#f5f7fa' }">
<el-table-column label="产品" prop="productName" min-width="180" show-overflow-tooltip />
<el-table-column label="规格/型号" min-width="200">
<template #default="scope">
{{ (scope.row.spec || '-') + ' / ' + (scope.row.model || '-') }}
</template>
</el-table-column>
<el-table-column label="计划" prop="planQty" width="120" align="right">
<template #default="scope">{{ formatQty(scope.row.planQty) }}</template>
</el-table-column>
<el-table-column label="已完成" prop="finishedQty" width="120" align="right">
<template #default="scope">{{ formatQty(scope.row.finishedQty) }}</template>
</el-table-column>
<el-table-column label="不良" prop="badQty" width="120" align="right">
<template #default="scope">{{ formatQty(scope.row.badQty) }}</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
</el-row>
<el-dialog v-model="addOpen" title="新增生产任务" width="1100px" top="5vh" append-to-body>
<el-form ref="addFormRef" :model="addForm" :rules="addRules" label-width="100px">
<el-row :gutter="12">
<el-col :span="8">
<el-form-item label="任务编号" prop="taskCode">
<el-input v-model="addForm.taskCode" placeholder="可不填" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="任务名称" prop="taskName">
<el-input v-model="addForm.taskName" placeholder="请输入任务名称" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="状态" prop="status">
<el-select v-model="addForm.status" placeholder="请选择">
<el-option label="进行中" value="1" />
<el-option label="已完成" value="2" />
<el-option label="已暂停" value="3" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="计划开始" prop="planStartTime">
<el-date-picker v-model="addForm.planStartTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="计划结束" prop="planEndTime">
<el-date-picker v-model="addForm.planEndTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="addForm.remark" type="textarea" :rows="2" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-divider content-position="left">生产产品明细</el-divider>
<div style="margin-bottom: 8px;">
<el-button type="primary" @click="addProductLine">新增产品行</el-button>
</div>
<el-table :data="addProducts" border size="small" :header-cell-style="{ background: '#f5f7fa' }">
<el-table-column label="产品" min-width="220">
<template #default="scope">
<el-select v-model="scope.row.productId" filterable clearable placeholder="请选择" style="width: 100%;" @change="onProductPicked(scope.row)">
<el-option v-for="p in productOptions" :key="p.productId" :label="p.productName" :value="p.productId" />
</el-select>
</template>
</el-table-column>
<el-table-column label="计划数量" width="140" align="right">
<template #default="scope">
<el-input-number v-model="scope.row.planQty" :min="0" :precision="4" controls-position="right" style="width: 120px;" />
</template>
</el-table-column>
<el-table-column label="单位" width="140">
<template #default="scope">
<el-input v-model="scope.row.unit" placeholder="单位" />
</template>
</el-table-column>
<el-table-column label="备注" min-width="200">
<template #default="scope">
<el-input v-model="scope.row.remark" placeholder="备注" />
</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center">
<template #default="scope">
<el-button type="danger" link @click="removeProductLine(scope.$index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="addOpen = false">取消</el-button>
<el-button type="primary" :loading="addSaving" @click="submitAdd">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="Production">
import { computed, onMounted, ref } from 'vue'
import { addProductionTask, completeProductionTask, getProductionTask, listProductionTask } from '@/api/mes/productionTask'
import { listProductBase } from '@/api/mat/product'
import { ElMessage, ElMessageBox } from 'element-plus'
const loading = ref(false)
const taskList = ref([])
const selectedTaskId = ref(null)
const productRows = ref([])
const materialRows = ref([])
const datePreset = ref('today')
const customRange = ref([])
const customPopoverOpen = ref(false)
const filters = ref({
status: '1',
keyword: '',
beginTime: '',
endTime: ''
})
const addOpen = ref(false)
const addSaving = ref(false)
const completeLoading = ref(false)
const addFormRef = ref()
const addForm = ref({
taskCode: '',
taskName: '',
status: '1',
planStartTime: '',
planEndTime: '',
remark: ''
})
const addRules = {
taskName: [{ required: true, message: '任务名称不能为空', trigger: 'blur' }]
}
const productOptions = ref([])
const addProducts = ref([])
function toNumber(v) {
const n = Number(v)
return Number.isFinite(n) ? n : 0
}
function formatQty(v) {
return toNumber(v).toFixed(2)
}
function normalizeRole(v) {
return String(v || '').toLowerCase()
}
function formatTime(v) {
if (!v) return '-'
if (v instanceof Date) {
return formatDateTime(v).slice(0, 16)
}
const s = String(v)
if (s.length >= 16) return s.slice(0, 16)
return s
}
function formatTimeRange(start, end) {
const a = start ? formatTime(start) : '-'
const b = end ? formatTime(end) : '-'
return `${a} ~ ${b}`
}
function pad2(n) {
return String(n).padStart(2, '0')
}
function formatDateTime(dt) {
const d = dt instanceof Date ? dt : new Date(dt)
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`
}
function startOfDay(d) {
const x = new Date(d)
x.setHours(0, 0, 0, 0)
return x
}
function endOfDay(d) {
const x = new Date(d)
x.setHours(23, 59, 59, 999)
return x
}
function addDays(d, n) {
const x = new Date(d)
x.setDate(x.getDate() + n)
return x
}
function startOfMonth(d) {
const x = new Date(d)
x.setDate(1)
x.setHours(0, 0, 0, 0)
return x
}
function applyDatePreset() {
const now = new Date()
let begin = null
let end = null
const p = String(datePreset.value || '')
if (p === 'today') {
begin = startOfDay(now)
end = endOfDay(now)
} else if (p === 'yesterday') {
const y = addDays(now, -1)
begin = startOfDay(y)
end = endOfDay(y)
} else if (p === 'last7') {
begin = startOfDay(addDays(now, -6))
end = endOfDay(now)
} else if (p === 'thisMonth') {
begin = startOfMonth(now)
end = endOfDay(now)
} else if (p === 'custom') {
if (!customRange.value || customRange.value.length !== 2) {
const b = startOfDay(now)
const e = endOfDay(now)
customRange.value = [formatDateTime(b).slice(0, 10), formatDateTime(e).slice(0, 10)]
filters.value.beginTime = formatDateTime(b)
filters.value.endTime = formatDateTime(e)
loadTasks()
}
customPopoverOpen.value = true
return
}
if (begin && end) {
filters.value.beginTime = formatDateTime(begin)
filters.value.endTime = formatDateTime(end)
loadTasks()
}
}
function applyCustomRange(v) {
if (!v || v.length !== 2) return
datePreset.value = 'custom'
const begin = startOfDay(new Date(v[0]))
const end = endOfDay(new Date(v[1]))
filters.value.beginTime = formatDateTime(begin)
filters.value.endTime = formatDateTime(end)
loadTasks()
}
const customRangeLabel = computed(() => {
const v = customRange.value
if (Array.isArray(v) && v.length === 2 && v[0] && v[1]) {
return `${v[0]} ~ ${v[1]}`
}
return '选择日期范围'
})
function onCustomRangePicked(v) {
applyCustomRange(v)
customPopoverOpen.value = false
}
function statusLabel(v) {
const s = String(v || '')
if (s === '1') return '进行中'
if (s === '2') return '已完成'
if (s === '3') return '已暂停'
return s || '-'
}
function statusTagType(v) {
const s = String(v || '')
if (s === '1') return 'success'
if (s === '2') return 'danger'
if (s === '3') return 'warning'
return 'info'
}
function taskProgressPercent(t) {
const plan = toNumber(t && t.planQty)
const finished = toNumber(t && t.finishedQty)
if (plan <= 0) return 0
const p = Math.round((finished / plan) * 100)
return Math.max(0, Math.min(100, p))
}
function selectTask(taskId) {
selectedTaskId.value = taskId
loading.value = true
return getProductionTask(taskId)
.then((res) => {
const data = res && res.data ? res.data : {}
productRows.value = Array.isArray(data.products) ? data.products : []
materialRows.value = Array.isArray(data.materials) ? data.materials : []
})
.finally(() => {
loading.value = false
})
}
const mainMaterialRows = computed(() => (materialRows.value || []).filter((r) => normalizeRole(r && r.materialRole) === 'main'))
const auxMaterialRows = computed(() => (materialRows.value || []).filter((r) => normalizeRole(r && r.materialRole) === 'aux'))
const selectedTask = computed(() => (taskList.value || []).find((t) => String(t.taskId) === String(selectedTaskId.value)) || null)
const runningCount = computed(() => (taskList.value || []).filter((t) => String(t && t.status) === '1').length)
const finishedCount = computed(() => (taskList.value || []).filter((t) => String(t && t.status) === '2').length)
const pausedCount = computed(() => (taskList.value || []).filter((t) => String(t && t.status) === '3').length)
const filteredTaskList = computed(() => {
const list = Array.isArray(taskList.value) ? taskList.value : []
const st = String(filters.value && filters.value.status ? filters.value.status : '')
const kw = String(filters.value && filters.value.keyword ? filters.value.keyword : '').trim().toLowerCase()
return list.filter((t) => {
if (st && String(t && t.status) !== st) return false
if (kw) {
const a = String(t && t.taskName ? t.taskName : '').toLowerCase()
const b = String(t && t.taskCode ? t.taskCode : '').toLowerCase()
if (!a.includes(kw) && !b.includes(kw)) return false
}
return true
})
})
function loadTasks() {
loading.value = true
const q = {
pageNum: 1,
pageSize: 9999,
status: filters.value && filters.value.status ? filters.value.status : undefined,
beginTime: filters.value && filters.value.beginTime ? filters.value.beginTime : undefined,
endTime: filters.value && filters.value.endTime ? filters.value.endTime : undefined
}
return listProductionTask(q)
.then((res) => {
const rows = (res && res.rows) ? res.rows : []
taskList.value = rows
if (!selectedTaskId.value && taskList.value.length) {
const first = filteredTaskList.value && filteredTaskList.value.length ? filteredTaskList.value[0] : taskList.value[0]
return selectTask(first.taskId)
}
if (selectedTaskId.value) {
const hit = (taskList.value || []).some((t) => t.taskId === selectedTaskId.value)
if (hit) return selectTask(selectedTaskId.value)
}
})
.finally(() => {
loading.value = false
})
}
function openAdd() {
addOpen.value = true
addForm.value = {
taskCode: '',
taskName: '',
status: '1',
planStartTime: '',
planEndTime: '',
remark: ''
}
addProducts.value = []
addProductLine()
ensureProductOptions()
}
function ensureProductOptions() {
if (productOptions.value && productOptions.value.length) return
listProductBase({ pageNum: 1, pageSize: 1000 }).then((res) => {
productOptions.value = (res && res.rows) ? res.rows : []
})
}
function addProductLine() {
addProducts.value.push({ productId: null, planQty: 0, unit: '', remark: '' })
}
function removeProductLine(idx) {
addProducts.value.splice(idx, 1)
}
function onProductPicked(row) {
if (!row || !row.productId) return
const hit = (productOptions.value || []).find((p) => String(p.productId) === String(row.productId))
if (hit && !row.unit) {
row.unit = ''
}
}
function mergeProductLines(lines) {
const list = Array.isArray(lines) ? lines : []
const map = new Map()
list.forEach((p) => {
if (!p || p.productId == null) return
const key = String(p.productId)
const planQty = toNumber(p.planQty)
if (!map.has(key)) {
map.set(key, {
productId: p.productId,
planQty,
unit: p.unit || '',
remark: p.remark || ''
})
return
}
const hit = map.get(key)
hit.planQty = toNumber(hit.planQty) + planQty
if (!hit.unit && p.unit) hit.unit = p.unit
if (!hit.remark && p.remark) hit.remark = p.remark
})
return Array.from(map.values())
}
function submitAdd() {
if (!addFormRef.value) return
addFormRef.value.validate((valid) => {
if (!valid) return
const task = Object.assign({}, addForm.value)
const products = mergeProductLines(addProducts.value).map((p) => ({
productId: p.productId,
planQty: p.planQty || 0,
finishedQty: 0,
badQty: 0,
unit: p.unit || '',
remark: p.remark || ''
}))
addSaving.value = true
addProductionTask({ task, products })
.then((res) => {
const taskId = res && res.data ? res.data : null
ElMessage.success('已新增')
addOpen.value = false
return loadTasks().then(() => {
if (taskId != null) {
selectedTaskId.value = taskId
return selectTask(taskId)
}
})
})
.finally(() => {
addSaving.value = false
})
})
}
function completeSelectedTask() {
const task = selectedTask.value
const taskId = selectedTaskId.value
if (!task || taskId == null) return
completeLoading.value = true
return ElMessageBox.confirm('完成后将根据产品配方自动生成主材/辅材回执,是否继续?', '确认完成', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => completeProductionTask(taskId))
.then(() => {
ElMessage.success('已完成')
return loadTasks().then(() => selectTask(taskId))
})
.catch((e) => {
const s = String(e || '')
if (s && s !== 'cancel' && s !== 'close') {
ElMessage.error('操作失败')
}
})
.finally(() => {
completeLoading.value = false
})
}
onMounted(() => {
applyDatePreset()
})
</script>
<style scoped>
.production-page {
padding: 0;
}
.production-page :deep(.el-card) {
border-radius: 10px;
}
.production-page :deep(.el-card__header) {
background: #fafafa;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.card-header__right {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.card-meta {
display: flex;
align-items: center;
justify-content: flex-end;
min-width: 0;
}
.meta-main {
max-width: 520px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #303133;
font-weight: 700;
}
.meta-sub {
color: #909399;
font-size: 12px;
}
.page-head {
margin-bottom: 12px;
}
.page-head__row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.page-head__row--filters {
margin-top: 10px;
}
.page-head__row--date {
margin-top: 10px;
}
.date-bar {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
flex-wrap: wrap;
}
.custom-range-btn {
min-width: 180px;
justify-content: flex-start;
}
.page-title__main {
font-size: 18px;
font-weight: 800;
color: #303133;
}
.page-title__sub {
margin-top: 2px;
font-size: 12px;
color: #909399;
}
.page-actions {
display: flex;
gap: 8px;
}
.filter-form :deep(.el-form-item) {
margin-bottom: 0;
}
.page-stats {
display: flex;
align-items: center;
justify-content: flex-end;
}
.task-item {
padding: 10px 10px;
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
margin-bottom: 10px;
cursor: pointer;
background: #fff;
transition: all 0.15s ease;
}
.task-item:hover {
border-color: rgba(64, 158, 255, 0.55);
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06);
}
.task-item.active {
border-color: #409eff;
background: rgba(64, 158, 255, 0.08);
}
.task-item__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.task-item__title {
font-size: 14px;
font-weight: 700;
color: #303133;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-item__sub {
margin-top: 6px;
font-size: 12px;
color: #909399;
margin-bottom: 8px;
}
.task-item__meta {
margin-top: -2px;
margin-bottom: 8px;
font-size: 12px;
color: #c0c4cc;
}
.mb12 {
margin-bottom: 12px;
}
</style>