feat(mobile-h5): add styles, mocks, components, and demo pages
Land Token-based SCSS modules, menu/biz mocks, reusable shell components, and home plus list/detail/form/comps template pages aligned with the Vant Weapp UI demo. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
cfe0e616ab
commit
8793fa07cc
19 files changed
+2460
-7
No files matched your search
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<view class="d-attach-list">
|
||||
<view class="d-attach-upload" @click="$emit('upload')">
|
||||
<view class="up-icon"><van-icon name="plus" /></view>
|
||||
<view class="up-text">
|
||||
<view class="up-title">添加附件</view>
|
||||
<view class="up-sub">支持 PDF / 图片 / Office,可多选上传</view>
|
||||
</view>
|
||||
<van-icon name="arrow" color="#0a9f6d" />
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-for="a in files"
|
||||
:key="a.name"
|
||||
class="d-attach-card"
|
||||
:class="fileTypeClass(a.name)"
|
||||
>
|
||||
<view class="file-icon">
|
||||
<van-icon :name="fileTypeIcon(a.name)" />
|
||||
<text class="ext">{{ fileExt(a.name) }}</text>
|
||||
</view>
|
||||
<view class="file-body">
|
||||
<view class="file-name">{{ a.name }}</view>
|
||||
<view class="file-meta">
|
||||
<text>{{ a.size || '—' }}</text>
|
||||
<text>{{ a.time || '—' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="file-ops">
|
||||
<view class="op-btn btn-preview" @click="$emit('preview', a)">
|
||||
<van-icon name="eye-o" />
|
||||
</view>
|
||||
<view class="op-btn btn-download" @click="$emit('download', a)">
|
||||
<view class="icon-download">
|
||||
<view style="width:28rpx;height:28rpx;position:relative">
|
||||
<van-icon name="down" size="14px" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AttachList',
|
||||
props: {
|
||||
files: { type: Array, default: () => [] }
|
||||
},
|
||||
emits: ['upload', 'preview', 'download'],
|
||||
methods: {
|
||||
fileExt(name) {
|
||||
const i = (name || '').lastIndexOf('.')
|
||||
return i >= 0 ? name.slice(i + 1).slice(0, 4) : 'FILE'
|
||||
},
|
||||
fileTypeClass(name) {
|
||||
const ext = this.fileExt(name).toLowerCase()
|
||||
if (ext === 'pdf') return 'ft-pdf'
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext)) return 'ft-img'
|
||||
if (['xls', 'xlsx', 'csv'].includes(ext)) return 'ft-xls'
|
||||
if (['doc', 'docx'].includes(ext)) return 'ft-doc'
|
||||
return 'ft-other'
|
||||
},
|
||||
fileTypeIcon(name) {
|
||||
const cls = this.fileTypeClass(name)
|
||||
if (cls === 'ft-pdf') return 'description'
|
||||
if (cls === 'ft-img') return 'photo-o'
|
||||
if (cls === 'ft-xls') return 'bar-chart-o'
|
||||
if (cls === 'ft-doc') return 'records'
|
||||
return 'bag-o'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<view class="biz-card" :class="'status-' + item.tagType">
|
||||
<view class="card-inner">
|
||||
<view class="row-top">
|
||||
<view class="type-icon">
|
||||
<van-icon :name="item.icon" />
|
||||
</view>
|
||||
<view class="title-wrap">
|
||||
<view class="type-chip">{{ item.typeLabel }}</view>
|
||||
<view class="title">{{ item.title }}</view>
|
||||
</view>
|
||||
<text class="status-pill">{{ item.status }}</text>
|
||||
</view>
|
||||
|
||||
<view class="body-row">
|
||||
<view class="desc-box">{{ item.desc }}</view>
|
||||
<view class="side-metric">
|
||||
<template v-if="item.amount">
|
||||
<text class="amt-label">金额</text>
|
||||
<text class="amt">¥{{ item.amount }}</text>
|
||||
</template>
|
||||
<template v-else>
|
||||
<text class="amt-label">进度</text>
|
||||
<text class="amt" style="font-size: 26rpx">{{ item.progress || '—' }}</text>
|
||||
</template>
|
||||
<text class="time">{{ item.time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="meta-row">
|
||||
<text class="mi code-pill">{{ item.code }}</text>
|
||||
<text class="mi">{{ item.owner }}</text>
|
||||
</view>
|
||||
|
||||
<view class="footer">
|
||||
<view class="left-hint">
|
||||
<view class="dot" />
|
||||
{{ item.hint || '点击操作处理' }}
|
||||
</view>
|
||||
<view class="ops">
|
||||
<van-button size="small" plain hairline type="default" @click.stop="$emit('view', item)">
|
||||
查看
|
||||
</van-button>
|
||||
<van-button size="small" type="primary" color="#0a9f6d" @click.stop="$emit('action', item)">
|
||||
处理
|
||||
</van-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'BizListCard',
|
||||
props: {
|
||||
item: { type: Object, required: true }
|
||||
},
|
||||
emits: ['view', 'action']
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<view class="flow-list">
|
||||
<view
|
||||
v-for="(log, idx) in logs"
|
||||
:key="idx"
|
||||
class="flow-item"
|
||||
:class="log.tone"
|
||||
>
|
||||
<view class="flow-rail">
|
||||
<view class="flow-dot">
|
||||
<van-icon :name="log.icon || 'checked'" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="flow-card">
|
||||
<view class="flow-top">
|
||||
<view class="flow-title">{{ log.title }}</view>
|
||||
<text class="flow-badge">{{
|
||||
log.badge || (log.tone === 'warn' ? '进行中' : '已完成')
|
||||
}}</text>
|
||||
</view>
|
||||
<view class="flow-desc">{{ log.desc }}</view>
|
||||
<view class="flow-meta">
|
||||
<view v-if="log.actor" class="chip">
|
||||
<van-icon name="manager-o" />
|
||||
{{ log.actor }}
|
||||
</view>
|
||||
<view class="chip">
|
||||
<van-icon name="clock-o" />
|
||||
{{ log.time }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FlowTimeline',
|
||||
props: {
|
||||
logs: { type: Array, default: () => [] }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<view class="menu-grid">
|
||||
<view
|
||||
v-for="m in menus"
|
||||
:key="m.id"
|
||||
class="menu-card"
|
||||
@click="$emit('select', m)"
|
||||
>
|
||||
<view class="menu-icon" :class="m.tone || ''">
|
||||
<van-icon :name="m.icon" />
|
||||
</view>
|
||||
<text class="menu-title">{{ m.title }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'MenuGrid',
|
||||
props: {
|
||||
menus: { type: Array, default: () => [] }
|
||||
},
|
||||
emits: ['select']
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<view class="todo-panel panel" style="padding: 0; overflow: hidden">
|
||||
<view class="panel-hd" style="padding: 28rpx 24rpx 0">
|
||||
<text class="panel-title">待办摘要</text>
|
||||
<view class="hint" @click="$emit('more')">查看全部 ›</view>
|
||||
</view>
|
||||
<view style="padding: 24rpx">
|
||||
<view class="todo-stats">
|
||||
<view class="todo-stat tone-warn" @click="$emit('stat-click', 'approve')">
|
||||
<view class="stat-icon"><van-icon name="passed" /></view>
|
||||
<text class="num">{{ stats.approve }}</text>
|
||||
<text class="label">待审批</text>
|
||||
</view>
|
||||
<view class="todo-stat tone-pay" @click="$emit('stat-click', 'pay')">
|
||||
<view class="stat-icon"><van-icon name="balance-list-o" /></view>
|
||||
<text class="num">{{ stats.pay }}</text>
|
||||
<text class="label">待确认缴费</text>
|
||||
</view>
|
||||
<view class="todo-stat tone-danger" @click="$emit('stat-click', 'alert')">
|
||||
<view class="stat-icon"><van-icon name="warning-o" /></view>
|
||||
<text class="num">{{ stats.alert }}</text>
|
||||
<text class="label">异常提醒</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="todo-list">
|
||||
<view
|
||||
v-for="t in items"
|
||||
:key="t.id"
|
||||
class="todo-item"
|
||||
@click="$emit('item-click', t)"
|
||||
>
|
||||
<view class="ti-icon" :class="t.iconClass">
|
||||
<van-icon :name="t.icon" />
|
||||
</view>
|
||||
<view class="ti-body">
|
||||
<view class="ti-title">{{ t.title }}</view>
|
||||
<view class="ti-desc">{{ t.desc }}</view>
|
||||
</view>
|
||||
<view class="ti-right">
|
||||
<van-tag :type="t.tagType" plain>{{ t.tag }}</van-tag>
|
||||
<text class="ti-time">{{ t.time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TodoPanel',
|
||||
props: {
|
||||
stats: {
|
||||
type: Object,
|
||||
default: () => ({ approve: 0, pay: 0, alert: 0 })
|
||||
},
|
||||
items: { type: Array, default: () => [] }
|
||||
},
|
||||
emits: ['stat-click', 'item-click', 'more']
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<view class="page-shell comps-page">
|
||||
<view class="list-hero">
|
||||
<van-nav-bar title="组件风格" left-arrow :border="false" @click-left="goHome" />
|
||||
<view class="list-hero-copy">
|
||||
<view class="eyebrow">DESIGN SYSTEM</view>
|
||||
<view class="title">组件合集</view>
|
||||
<view class="subline">按钮 · 状态 · 反馈 · 附件等基础样式</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="home-body">
|
||||
<view class="panel">
|
||||
<view class="panel-hd"><text class="panel-title">按钮 Button</text></view>
|
||||
<view class="comp-row">
|
||||
<van-button type="primary" size="small" color="#0a9f6d" round>主按钮</van-button>
|
||||
<van-button type="primary" size="small" plain hairline color="#0a9f6d" round>描边</van-button>
|
||||
<van-button size="small" plain hairline round>次要</van-button>
|
||||
<van-button type="danger" size="small" plain hairline round>危险</van-button>
|
||||
</view>
|
||||
<van-button
|
||||
type="primary"
|
||||
color="#0a9f6d"
|
||||
block
|
||||
round
|
||||
custom-style="margin-top: 16rpx; height: 88rpx;"
|
||||
>
|
||||
通栏主按钮(44px)
|
||||
</van-button>
|
||||
</view>
|
||||
|
||||
<view class="panel">
|
||||
<view class="panel-hd"><text class="panel-title">状态 Tag</text></view>
|
||||
<view class="comp-row">
|
||||
<van-tag type="warning" plain round>待处理</van-tag>
|
||||
<van-tag type="primary" color="#0a9f6d" round>进行中</van-tag>
|
||||
<van-tag type="success" plain round>已完成</van-tag>
|
||||
<van-tag type="danger" plain round>异常</van-tag>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel">
|
||||
<view class="panel-hd"><text class="panel-title">通知 / 空态 / 加载</text></view>
|
||||
<van-notice-bar
|
||||
left-icon="volume-o"
|
||||
text="移动端菜单在「菜单管理 → 移动端菜单」下配置,按角色授权后展示。"
|
||||
background="#e9f9f2"
|
||||
color="#087f57"
|
||||
/>
|
||||
<view style="min-height: 360rpx; margin-top: 24rpx">
|
||||
<van-empty description="暂无相关数据" />
|
||||
</view>
|
||||
<view style="display: flex; justify-content: center; padding: 32rpx 0">
|
||||
<van-loading size="24px" color="#0a9f6d">加载中</van-loading>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel">
|
||||
<view class="panel-hd"><text class="panel-title">反馈</text></view>
|
||||
<view class="comp-row">
|
||||
<van-button size="small" plain hairline color="#0a9f6d" @click="toast('操作成功')">
|
||||
Toast
|
||||
</van-button>
|
||||
<van-button size="small" plain hairline color="#0a9f6d" @click="showDialog">
|
||||
Dialog
|
||||
</van-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel">
|
||||
<view class="panel-hd"><text class="panel-title">附件卡片</text></view>
|
||||
<AttachList
|
||||
:files="[{ name: '质检报告.docx', size: '1.2 MB', time: '08-10 10:20' }]"
|
||||
@upload="toast('上传')"
|
||||
@preview="toast('预览')"
|
||||
@download="toast('下载')"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AttachList from '../../components/AttachList.vue'
|
||||
|
||||
export default {
|
||||
components: { AttachList },
|
||||
methods: {
|
||||
goHome() {
|
||||
uni.reLaunch({ url: '/pages/index/index' })
|
||||
},
|
||||
toast(msg) {
|
||||
uni.showToast({ title: msg, icon: 'none' })
|
||||
},
|
||||
showDialog() {
|
||||
uni.showModal({
|
||||
title: '确认提交',
|
||||
content: '提交后将进入审批流程,是否继续?',
|
||||
confirmColor: '#0a9f6d'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.comp-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<view class="detail-page" v-if="detail">
|
||||
<view class="detail-hero" :class="'status-' + detail.tagType">
|
||||
<van-nav-bar
|
||||
title="业务详情"
|
||||
left-arrow
|
||||
:border="false"
|
||||
@click-left="goBack"
|
||||
/>
|
||||
<view class="detail-hero-body">
|
||||
<view class="type-line">
|
||||
<text class="type-chip">{{ detail.typeLabel }}</text>
|
||||
<text class="status-chip">{{ detail.status }}</text>
|
||||
</view>
|
||||
<view class="hero-title">{{ detail.title }}</view>
|
||||
<view class="code">{{ detail.code }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="detail-metric-dock">
|
||||
<view class="detail-metric">
|
||||
<text class="lab">{{ detail.amount ? '金额' : '进度' }}</text>
|
||||
<text class="val" :class="{ money: !!detail.amount }">
|
||||
{{ detail.amount ? '¥' + detail.amount : detail.progress || '—' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="detail-metric">
|
||||
<text class="lab">负责人</text>
|
||||
<text class="val">{{ detail.owner }}</text>
|
||||
</view>
|
||||
<view class="detail-metric">
|
||||
<text class="lab">更新时间</text>
|
||||
<text class="val">{{ detail.time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="detail-body">
|
||||
<view class="detail-section">
|
||||
<view class="sec-hd"><text class="sec-title">基础信息</text></view>
|
||||
<view class="kv-list">
|
||||
<view class="kv-row"><text class="k">业务类型</text><text class="v">{{ detail.typeLabel }}</text></view>
|
||||
<view class="kv-row"><text class="k">所属部门</text><text class="v">{{ detail.dept }}</text></view>
|
||||
<view class="kv-row"><text class="k">客户/对象</text><text class="v">{{ detail.customer }}</text></view>
|
||||
<view class="kv-row"><text class="k">创建时间</text><text class="v">{{ detail.createdAt }}</text></view>
|
||||
</view>
|
||||
<view class="detail-note">{{ detail.desc }}</view>
|
||||
</view>
|
||||
|
||||
<view class="detail-section">
|
||||
<view class="sec-hd"><text class="sec-title">业务进度</text></view>
|
||||
<van-steps :active="detail.stepActive" active-color="#0a9f6d">
|
||||
<van-step v-for="s in detail.steps" :key="s">{{ s }}</van-step>
|
||||
</van-steps>
|
||||
</view>
|
||||
|
||||
<view class="detail-section" v-if="detail.payments && detail.payments.length">
|
||||
<view class="sec-hd">
|
||||
<text class="sec-title">缴费明细</text>
|
||||
<text class="sec-more">全部</text>
|
||||
</view>
|
||||
<view v-for="p in detail.payments" :key="p.id" class="pay-row">
|
||||
<view>
|
||||
<view class="name">{{ p.name }}</view>
|
||||
<view class="sub">{{ p.sub }}</view>
|
||||
</view>
|
||||
<view>
|
||||
<view class="amt">¥{{ p.amount }}</view>
|
||||
<van-tag :type="p.tagType" plain>{{ p.status }}</van-tag>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="detail-section">
|
||||
<view class="sec-hd">
|
||||
<text class="sec-title">附件</text>
|
||||
<text class="hint">{{ (detail.attachments || []).length }} 个文件</text>
|
||||
</view>
|
||||
<AttachList
|
||||
:files="detail.attachments || []"
|
||||
@upload="toast('上传附件')"
|
||||
@preview="onPreview"
|
||||
@download="onDownload"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="detail-section">
|
||||
<view class="sec-hd">
|
||||
<text class="sec-title">流转记录</text>
|
||||
<text class="hint">{{ (detail.logs || []).length }} 步</text>
|
||||
</view>
|
||||
<FlowTimeline :logs="detail.logs || []" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="detail-footer">
|
||||
<van-button class="btn-flex" plain hairline type="default" @click="goBack">返回</van-button>
|
||||
<van-button class="btn-flex" type="primary" color="#0a9f6d" round @click="goEdit">
|
||||
编辑处理
|
||||
</van-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AttachList from '../../components/AttachList.vue'
|
||||
import FlowTimeline from '../../components/FlowTimeline.vue'
|
||||
import { getById } from '../../utils/biz-mock.js'
|
||||
|
||||
export default {
|
||||
components: { AttachList, FlowTimeline },
|
||||
data() {
|
||||
return { detail: null }
|
||||
},
|
||||
onLoad(query) {
|
||||
this.detail = getById(query.id)
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
uni.navigateBack({
|
||||
fail: () => uni.reLaunch({ url: '/pages/demo/list' })
|
||||
})
|
||||
},
|
||||
goEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages/demo/form?mode=edit&id=${this.detail.id}`
|
||||
})
|
||||
},
|
||||
toast(msg) {
|
||||
uni.showToast({ title: msg, icon: 'none' })
|
||||
},
|
||||
onPreview(a) {
|
||||
this.toast('预览 ' + a.name)
|
||||
},
|
||||
onDownload(a) {
|
||||
this.toast('下载 ' + a.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<view class="form-page">
|
||||
<view class="form-nav">
|
||||
<van-nav-bar
|
||||
:title="mode === 'create' ? '新增业务' : '编辑业务'"
|
||||
left-arrow
|
||||
:border="false"
|
||||
@click-left="goBack"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-section-title">基础信息</view>
|
||||
<van-cell-group inset>
|
||||
<van-field
|
||||
:value="form.name"
|
||||
label="名称"
|
||||
placeholder="请输入名称"
|
||||
required
|
||||
@change="(e) => (form.name = e.detail)"
|
||||
/>
|
||||
<van-field
|
||||
:value="form.code"
|
||||
label="业务单号"
|
||||
placeholder="系统自动生成或手工录入"
|
||||
@change="(e) => (form.code = e.detail)"
|
||||
/>
|
||||
<van-field
|
||||
:value="form.typeLabel"
|
||||
label="类型"
|
||||
placeholder="请选择类型"
|
||||
is-link
|
||||
readonly
|
||||
@click-input="showType = true"
|
||||
/>
|
||||
<van-cell title="加急">
|
||||
<van-switch
|
||||
slot="right-icon"
|
||||
:checked="form.urgent"
|
||||
size="20px"
|
||||
active-color="#0a9f6d"
|
||||
@change="(e) => (form.urgent = e.detail)"
|
||||
/>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
|
||||
<view class="form-section-title">业务详情</view>
|
||||
<van-cell-group inset>
|
||||
<van-field
|
||||
:value="form.owner"
|
||||
label="负责人"
|
||||
placeholder="请输入负责人"
|
||||
@change="(e) => (form.owner = e.detail)"
|
||||
/>
|
||||
<van-field
|
||||
:value="form.date"
|
||||
label="业务日期"
|
||||
placeholder="YYYY-MM-DD"
|
||||
@change="(e) => (form.date = e.detail)"
|
||||
/>
|
||||
<van-field
|
||||
:value="form.amount"
|
||||
type="digit"
|
||||
label="金额"
|
||||
placeholder="0.00"
|
||||
@change="(e) => (form.amount = e.detail)"
|
||||
/>
|
||||
<van-field
|
||||
:value="form.remark"
|
||||
label="备注"
|
||||
type="textarea"
|
||||
placeholder="选填,最多 200 字"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
autosize
|
||||
@change="(e) => (form.remark = e.detail)"
|
||||
/>
|
||||
</van-cell-group>
|
||||
|
||||
<view class="form-tip">必填项校验失败时 Toast 提示。底栏固定「取消 / 保存」。</view>
|
||||
|
||||
<view class="form-footer">
|
||||
<van-button class="btn-flex" plain hairline type="default" @click="goBack">取消</van-button>
|
||||
<van-button class="btn-flex" type="primary" color="#0a9f6d" round @click="onSave">
|
||||
保存
|
||||
</van-button>
|
||||
</view>
|
||||
|
||||
<van-action-sheet
|
||||
:show="showType"
|
||||
:actions="typeActions"
|
||||
cancel-text="取消"
|
||||
@close="showType = false"
|
||||
@select="onTypeSelect"
|
||||
@cancel="showType = false"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getById } from '../../utils/biz-mock.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
mode: 'create',
|
||||
showType: false,
|
||||
typeActions: [
|
||||
{ name: '采集协议' },
|
||||
{ name: '样本接收' },
|
||||
{ name: '质检工单' },
|
||||
{ name: '出库申请' }
|
||||
],
|
||||
form: {
|
||||
name: '',
|
||||
code: '',
|
||||
typeLabel: '',
|
||||
urgent: false,
|
||||
owner: '张三',
|
||||
date: '',
|
||||
amount: '',
|
||||
remark: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
onLoad(query) {
|
||||
this.mode = query.mode === 'edit' ? 'edit' : 'create'
|
||||
if (this.mode === 'edit' && query.id) {
|
||||
const item = getById(query.id)
|
||||
this.form = {
|
||||
name: item.title,
|
||||
code: item.code,
|
||||
typeLabel: item.typeLabel,
|
||||
urgent: item.tagType === 'danger',
|
||||
owner: item.owner,
|
||||
date: '2026-08-10',
|
||||
amount: (item.amount || '').replace(/,/g, ''),
|
||||
remark: item.desc
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
uni.navigateBack({
|
||||
fail: () => uni.reLaunch({ url: '/pages/demo/list' })
|
||||
})
|
||||
},
|
||||
onTypeSelect(e) {
|
||||
this.form.typeLabel = e.detail.name
|
||||
this.showType = false
|
||||
},
|
||||
onSave() {
|
||||
if (!this.form.name || !String(this.form.name).trim()) {
|
||||
uni.showToast({ title: '请填写名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.showToast({
|
||||
title: this.mode === 'create' ? '已保存(新增)' : '已保存(编辑)',
|
||||
icon: 'success'
|
||||
})
|
||||
setTimeout(() => this.goBack(), 500)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<view class="page-shell">
|
||||
<view class="list-hero">
|
||||
<van-nav-bar
|
||||
title="业务列表"
|
||||
left-arrow
|
||||
:border="false"
|
||||
@click-left="goHome"
|
||||
>
|
||||
<template #right>
|
||||
<van-icon name="plus" size="20px" color="#fff" @click="goCreate" />
|
||||
</template>
|
||||
</van-nav-bar>
|
||||
<view class="list-hero-copy">
|
||||
<view class="eyebrow">WORKSPACE</view>
|
||||
<view class="title">今日待办一览</view>
|
||||
<view class="subline">
|
||||
<text>当前展示</text>
|
||||
<text class="em">{{ list.length }} 条</text>
|
||||
<text v-if="keyword">· 已筛选</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="list-search">
|
||||
<van-search
|
||||
:value="keyword"
|
||||
shape="round"
|
||||
background="transparent"
|
||||
placeholder="搜索单号 / 名称 / 负责人"
|
||||
@change="onSearchChange"
|
||||
@search="onSearchChange"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="list-filter-dock">
|
||||
<view class="seg-tabs">
|
||||
<view
|
||||
v-for="(tab, idx) in tabs"
|
||||
:key="tab.key"
|
||||
class="seg-tab"
|
||||
:class="{ active: listTab === idx }"
|
||||
@click="listTab = idx"
|
||||
>
|
||||
<text class="lab">{{ tab.label }}</text>
|
||||
<text class="count">{{ tabCount(idx) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="list-wrap">
|
||||
<BizListCard
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
@view="goDetail"
|
||||
@action="goEdit"
|
||||
/>
|
||||
<van-empty v-if="!list.length" description="暂无数据" />
|
||||
<view v-else class="list-end">已经到底了</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BizListCard from '../../components/BizListCard.vue'
|
||||
import { filterByTab, tabCount, LIST_TABS } from '../../utils/biz-mock.js'
|
||||
|
||||
export default {
|
||||
components: { BizListCard },
|
||||
data() {
|
||||
return {
|
||||
keyword: '',
|
||||
listTab: 0,
|
||||
tabs: LIST_TABS
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
list() {
|
||||
return filterByTab(this.listTab, this.keyword)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
tabCount,
|
||||
onSearchChange(e) {
|
||||
this.keyword = (e && e.detail !== undefined ? e.detail : e) || ''
|
||||
},
|
||||
goHome() {
|
||||
uni.reLaunch({ url: '/pages/index/index' })
|
||||
},
|
||||
goCreate() {
|
||||
uni.navigateTo({ url: '/pages/demo/form?mode=create' })
|
||||
},
|
||||
goDetail(item) {
|
||||
uni.navigateTo({ url: `/pages/demo/detail?id=${item.id}` })
|
||||
},
|
||||
goEdit(item) {
|
||||
uni.navigateTo({ url: `/pages/demo/form?mode=edit&id=${item.id}` })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<view class="page-shell">
|
||||
<view class="home-header">
|
||||
<view class="brand">中科兴皖 · 移动工作台</view>
|
||||
<view class="hello">你好,{{ userName }}</view>
|
||||
<view class="meta">
|
||||
<text>{{ roleName }}</text>
|
||||
<view class="dot" />
|
||||
<text>{{ deptName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="home-body">
|
||||
<view class="perm-banner">
|
||||
菜单卡片按角色权限动态渲染;无权限的功能入口不会出现在宫格中。
|
||||
</view>
|
||||
|
||||
<view class="panel">
|
||||
<view class="panel-hd">
|
||||
<text class="panel-title">功能菜单</text>
|
||||
<text class="hint">共 {{ menus.length }} 项</text>
|
||||
</view>
|
||||
<MenuGrid v-if="menus.length" :menus="menus" @select="onMenuSelect" />
|
||||
<van-empty v-else description="暂无可用菜单权限" />
|
||||
</view>
|
||||
|
||||
<TodoPanel
|
||||
:stats="todoStats"
|
||||
:items="todoItems"
|
||||
@more="goList"
|
||||
@stat-click="goList"
|
||||
@item-click="goList"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MenuGrid from '../../components/MenuGrid.vue'
|
||||
import TodoPanel from '../../components/TodoPanel.vue'
|
||||
import {
|
||||
filterMenusByPermission,
|
||||
DEFAULT_GRANTED_IDS
|
||||
} from '../../utils/menu-mock.js'
|
||||
|
||||
export default {
|
||||
components: { MenuGrid, TodoPanel },
|
||||
data() {
|
||||
return {
|
||||
userName: '张三',
|
||||
roleName: '业务专员',
|
||||
deptName: '样本运营部',
|
||||
menus: filterMenusByPermission(DEFAULT_GRANTED_IDS),
|
||||
todoStats: { approve: 3, pay: 1, alert: 1 },
|
||||
todoItems: [
|
||||
{
|
||||
id: 't1',
|
||||
title: '采集协议审批',
|
||||
desc: '王女士 · 分期首期待复核',
|
||||
tag: '待审批',
|
||||
tagType: 'warning',
|
||||
time: '09:20',
|
||||
icon: 'todo-list-o',
|
||||
iconClass: 'i-approve'
|
||||
},
|
||||
{
|
||||
id: 't2',
|
||||
title: '缴费确认',
|
||||
desc: 'XY20260810001 · ¥12,800',
|
||||
tag: '待确认',
|
||||
tagType: 'warning',
|
||||
time: '昨天',
|
||||
icon: 'balance-list-o',
|
||||
iconClass: 'i-pay'
|
||||
},
|
||||
{
|
||||
id: 't3',
|
||||
title: '质检异常',
|
||||
desc: 'QC-8821 · 结论待录入',
|
||||
tag: '异常',
|
||||
tagType: 'danger',
|
||||
time: '08-08',
|
||||
icon: 'warning-o',
|
||||
iconClass: 'i-alert'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onMenuSelect(m) {
|
||||
if (m.path) {
|
||||
uni.navigateTo({ url: m.path })
|
||||
}
|
||||
},
|
||||
goList() {
|
||||
uni.navigateTo({ url: '/pages/demo/list' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1 +1,185 @@
|
||||
// Task 4: attachment styles (.d-attach-list, .d-attach-upload, .d-attach-card, file type classes)
|
||||
.d-attach-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.d-attach-upload {
|
||||
width: 100%;
|
||||
border: 3rpx dashed #9ad9be;
|
||||
border-radius: 28rpx;
|
||||
background: linear-gradient(135deg, rgba(233, 249, 242, 0.95), rgba(255, 255, 255, 0.95));
|
||||
padding: 28rpx 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.d-attach-upload:active {
|
||||
border-color: #0a9f6d;
|
||||
}
|
||||
|
||||
.d-attach-upload .up-icon {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 24rpx;
|
||||
background: linear-gradient(145deg, #2ecf8f, #0a9f6d);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 44rpx;
|
||||
box-shadow: 0 12rpx 28rpx rgba(10, 159, 109, 0.28);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.d-attach-upload .up-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.d-attach-upload .up-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #087f57;
|
||||
}
|
||||
|
||||
.d-attach-upload .up-sub {
|
||||
margin-top: 4rpx;
|
||||
font-size: 22rpx;
|
||||
color: #6b7b74;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.d-attach-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 28rpx;
|
||||
background: #fff;
|
||||
border: 1rpx solid #e4eee9;
|
||||
box-shadow: 0 8rpx 28rpx rgba(20, 55, 40, 0.05);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.d-attach-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 6rpx;
|
||||
background: var(--file-accent, #0a9f6d);
|
||||
}
|
||||
|
||||
.d-attach-card.ft-pdf {
|
||||
--file-accent: #d14343;
|
||||
}
|
||||
.d-attach-card.ft-img {
|
||||
--file-accent: #0a9f6d;
|
||||
}
|
||||
.d-attach-card.ft-xls {
|
||||
--file-accent: #189b65;
|
||||
}
|
||||
.d-attach-card.ft-doc {
|
||||
--file-accent: #3b82f6;
|
||||
}
|
||||
.d-attach-card.ft-other {
|
||||
--file-accent: #d6a343;
|
||||
}
|
||||
|
||||
.d-attach-card .file-icon {
|
||||
width: 92rpx;
|
||||
height: 92rpx;
|
||||
border-radius: 26rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2rpx;
|
||||
flex-shrink: 0;
|
||||
color: #fff;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.d-attach-card.ft-pdf .file-icon {
|
||||
background: linear-gradient(145deg, #f07171, #d14343);
|
||||
}
|
||||
.d-attach-card.ft-img .file-icon {
|
||||
background: linear-gradient(145deg, #3dca96, #0a9f6d);
|
||||
}
|
||||
.d-attach-card.ft-xls .file-icon {
|
||||
background: linear-gradient(145deg, #4fbf8a, #189b65);
|
||||
}
|
||||
.d-attach-card.ft-doc .file-icon {
|
||||
background: linear-gradient(145deg, #6ea8ff, #3b82f6);
|
||||
}
|
||||
.d-attach-card.ft-other .file-icon {
|
||||
background: linear-gradient(145deg, #e4bc6a, #d6a343);
|
||||
}
|
||||
|
||||
.d-attach-card .file-icon .ext {
|
||||
font-size: 20rpx;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.d-attach-card .file-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.d-attach-card .file-name {
|
||||
font-size: 26rpx;
|
||||
font-weight: 650;
|
||||
color: #173128;
|
||||
line-height: 1.35;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.d-attach-card .file-meta {
|
||||
margin-top: 8rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
font-size: 22rpx;
|
||||
color: #8aa198;
|
||||
}
|
||||
|
||||
.d-attach-card .file-ops {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.d-attach-card .file-ops .op-btn {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.d-attach-card .file-ops .btn-preview {
|
||||
background: var(--brand-green-soft, #e9f9f2);
|
||||
color: var(--brand-green-dark, #087f57);
|
||||
}
|
||||
|
||||
.d-attach-card .file-ops .btn-download {
|
||||
background: #f0f5f3;
|
||||
color: #4d635a;
|
||||
}
|
||||
|
||||
.d-attach-card .file-ops .icon-download {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
display: block;
|
||||
}
|
||||
@@ -1 +1,241 @@
|
||||
// Task 4: detail page styles (.detail-page, .detail-hero, .detail-metric-dock, .detail-section, .detail-footer)
|
||||
@import '../mixins.scss';
|
||||
|
||||
.detail-page {
|
||||
min-height: 100vh;
|
||||
background:
|
||||
radial-gradient(760rpx 400rpx at 100% 8%, rgba(214, 163, 67, 0.14), transparent 55%),
|
||||
linear-gradient(180deg, #e8f5ef 0%, #f4f7f6 42%, #f7f9f8 100%);
|
||||
padding-bottom: 176rpx;
|
||||
}
|
||||
|
||||
.detail-hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 88rpx 28rpx 56rpx;
|
||||
border-radius: 0 0 56rpx 56rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.detail-hero.status-warning {
|
||||
background: linear-gradient(155deg, #a67b20 0%, #d6a343 45%, #e8c06a 100%);
|
||||
}
|
||||
.detail-hero.status-primary {
|
||||
@include hero-green;
|
||||
border-radius: 0 0 56rpx 56rpx;
|
||||
}
|
||||
.detail-hero.status-danger {
|
||||
background: linear-gradient(155deg, #9c2f2f 0%, #d14343 50%, #e36a6a 100%);
|
||||
}
|
||||
.detail-hero.status-success {
|
||||
background: linear-gradient(155deg, #0f6b48 0%, #189b65 52%, #3bb883 100%);
|
||||
}
|
||||
|
||||
.detail-hero-body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.detail-hero .type-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.detail-hero .type-chip,
|
||||
.detail-hero .status-chip {
|
||||
font-size: 22rpx;
|
||||
font-weight: 650;
|
||||
padding: 6rpx 18rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
.detail-hero .type-chip {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.detail-hero .status-chip {
|
||||
background: #fff;
|
||||
color: #173128;
|
||||
}
|
||||
|
||||
.detail-hero .hero-title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 750;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.detail-hero .code {
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
opacity: 0.92;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
|
||||
.detail-metric-dock {
|
||||
margin: -36rpx 24rpx 0;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 36rpx;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border: 1rpx solid rgba(220, 235, 228, 0.95);
|
||||
box-shadow: 0 24rpx 60rpx rgba(15, 55, 40, 0.1);
|
||||
}
|
||||
|
||||
.detail-metric {
|
||||
text-align: center;
|
||||
padding: 8rpx 4rpx;
|
||||
}
|
||||
|
||||
.detail-metric .lab {
|
||||
display: block;
|
||||
font-size: 20rpx;
|
||||
color: #8aa198;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.detail-metric .val {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 750;
|
||||
color: #173128;
|
||||
line-height: 1.25;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.detail-metric .val.money {
|
||||
color: #9a7424;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.detail-body {
|
||||
padding: 32rpx 24rpx 0;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
@include surface-card;
|
||||
border-radius: 36rpx;
|
||||
padding: 28rpx 28rpx 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.detail-section .sec-hd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.detail-section .sec-hd .sec-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #173128;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.detail-section .sec-hd .sec-title::before {
|
||||
content: '';
|
||||
width: 8rpx;
|
||||
height: 28rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(180deg, #12b37a, #087f57);
|
||||
}
|
||||
|
||||
.detail-section .sec-hd .sec-more {
|
||||
font-size: 24rpx;
|
||||
color: var(--brand-green, #0a9f6d);
|
||||
}
|
||||
|
||||
.kv-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.kv-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 24rpx;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.kv-row .k {
|
||||
width: 144rpx;
|
||||
flex-shrink: 0;
|
||||
color: #8aa198;
|
||||
}
|
||||
|
||||
.kv-row .v {
|
||||
flex: 1;
|
||||
color: #173128;
|
||||
font-weight: 550;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.detail-note {
|
||||
margin-top: 24rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
background: linear-gradient(135deg, #fff8ec, #fffdf8);
|
||||
border: 1rpx solid #f0e0b8;
|
||||
color: #8a6520;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.pay-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #f7faf9;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.pay-row .name {
|
||||
font-size: 26rpx;
|
||||
font-weight: 650;
|
||||
color: #173128;
|
||||
}
|
||||
|
||||
.pay-row .sub {
|
||||
margin-top: 4rpx;
|
||||
font-size: 22rpx;
|
||||
color: #8aa198;
|
||||
}
|
||||
|
||||
.pay-row .amt {
|
||||
font-size: 28rpx;
|
||||
font-weight: 750;
|
||||
color: #9a7424;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.detail-footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding: 24rpx 28rpx calc(24rpx + env(safe-area-inset-bottom));
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border-top: 1rpx solid var(--border, #dcebe4);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.detail-footer .btn-flex {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -1 +1,142 @@
|
||||
// Task 4: flow timeline styles (.flow-list, .flow-item, .flow-dot, .flow-card)
|
||||
.flow-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flow-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
padding-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.flow-item:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.flow-item:not(:last-child) .flow-rail::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 72rpx;
|
||||
bottom: -28rpx;
|
||||
width: 4rpx;
|
||||
transform: translateX(-50%);
|
||||
background: linear-gradient(180deg, rgba(10, 159, 109, 0.45), rgba(220, 235, 228, 0.9));
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
|
||||
.flow-rail {
|
||||
position: relative;
|
||||
width: 72rpx;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flow-dot {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
color: #fff;
|
||||
box-shadow: 0 12rpx 28rpx rgba(8, 127, 87, 0.2);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.flow-item.done .flow-dot {
|
||||
background: linear-gradient(145deg, #2ecf8f, #0a9f6d);
|
||||
}
|
||||
.flow-item.warn .flow-dot {
|
||||
background: linear-gradient(145deg, #ecc06a, #d6a343);
|
||||
box-shadow: 0 12rpx 28rpx rgba(214, 163, 67, 0.28);
|
||||
}
|
||||
.flow-item.danger .flow-dot {
|
||||
background: linear-gradient(145deg, #ef7a7a, #d14343);
|
||||
box-shadow: 0 12rpx 28rpx rgba(209, 67, 67, 0.25);
|
||||
}
|
||||
|
||||
.flow-card {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border-radius: 28rpx;
|
||||
padding: 22rpx 24rpx;
|
||||
background: linear-gradient(160deg, #f7fbf9 0%, #ffffff 55%);
|
||||
border: 1rpx solid #e4eee9;
|
||||
box-shadow: 0 8rpx 24rpx rgba(20, 55, 40, 0.04);
|
||||
}
|
||||
|
||||
.flow-item.warn .flow-card {
|
||||
background: linear-gradient(160deg, #fff9ef 0%, #ffffff 60%);
|
||||
border-color: #f0e0b8;
|
||||
}
|
||||
.flow-item.danger .flow-card {
|
||||
background: linear-gradient(160deg, #fff4f4 0%, #ffffff 60%);
|
||||
border-color: #f3d0d0;
|
||||
}
|
||||
|
||||
.flow-card .flow-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.flow-card .flow-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #173128;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.flow-card .flow-badge {
|
||||
flex-shrink: 0;
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
.flow-item.done .flow-badge {
|
||||
background: rgba(10, 159, 109, 0.12);
|
||||
color: #087f57;
|
||||
}
|
||||
.flow-item.warn .flow-badge {
|
||||
background: rgba(214, 163, 67, 0.16);
|
||||
color: #8f6718;
|
||||
}
|
||||
.flow-item.danger .flow-badge {
|
||||
background: rgba(209, 67, 67, 0.12);
|
||||
color: #b13535;
|
||||
}
|
||||
|
||||
.flow-card .flow-desc {
|
||||
font-size: 24rpx;
|
||||
color: #5d746a;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.flow-card .flow-meta {
|
||||
margin-top: 16rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flow-card .flow-meta .chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #6b7b74;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 1rpx solid #e8f0ec;
|
||||
border-radius: 999rpx;
|
||||
padding: 4rpx 16rpx;
|
||||
}
|
||||
@@ -1 +1,116 @@
|
||||
// Task 4: form shell styles (.form-page, .form-section-title, .form-footer)
|
||||
@import '../mixins.scss';
|
||||
|
||||
.form-page {
|
||||
min-height: 100vh;
|
||||
background: var(--bg-page, #f5f8f7);
|
||||
padding-bottom: 176rpx;
|
||||
}
|
||||
|
||||
.form-nav {
|
||||
@include hero-green;
|
||||
padding: 88rpx 16rpx 20rpx;
|
||||
}
|
||||
|
||||
.form-section-title {
|
||||
margin: 28rpx 32rpx 16rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted, #6b7b74);
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
margin: 16rpx 32rpx 0;
|
||||
font-size: 22rpx;
|
||||
color: var(--text-muted, #6b7b74);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.form-footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding: 24rpx 28rpx calc(24rpx + env(safe-area-inset-bottom));
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border-top: 1rpx solid var(--border, #dcebe4);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.form-footer .btn-flex {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.home-header {
|
||||
@include hero-green;
|
||||
padding: 108rpx 32rpx 56rpx;
|
||||
}
|
||||
|
||||
.home-header .brand {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.88;
|
||||
letter-spacing: 0.04em;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.home-header .hello {
|
||||
font-size: 44rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.home-header .meta {
|
||||
margin-top: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
font-size: 24rpx;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.home-header .meta .dot {
|
||||
width: 8rpx;
|
||||
height: 8rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.home-body {
|
||||
padding: 28rpx 24rpx 48rpx;
|
||||
}
|
||||
|
||||
.panel {
|
||||
@include surface-card;
|
||||
padding: 28rpx 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.panel-hd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
.panel-hd .panel-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 650;
|
||||
color: var(--text-primary, #1f2a26);
|
||||
}
|
||||
|
||||
.panel-hd .hint {
|
||||
font-size: 22rpx;
|
||||
color: var(--text-muted, #6b7b74);
|
||||
}
|
||||
|
||||
.perm-banner {
|
||||
padding: 20rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
background: var(--barley-yellow-soft, #fff7e8);
|
||||
border: 1rpx solid #f0e0b8;
|
||||
font-size: 24rpx;
|
||||
color: #8a6520;
|
||||
margin-bottom: 24rpx;
|
||||
line-height: 1.45;
|
||||
}
|
||||
@@ -1 +1,374 @@
|
||||
// Task 4: list page styles (.list-hero, .list-filter-dock, .seg-tabs, .biz-card, status variants)
|
||||
@import '../mixins.scss';
|
||||
|
||||
.list-hero {
|
||||
@include hero-green;
|
||||
padding: 88rpx 28rpx 44rpx;
|
||||
}
|
||||
|
||||
.list-hero::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 360rpx;
|
||||
height: 360rpx;
|
||||
right: -100rpx;
|
||||
bottom: -140rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.list-hero-copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin: 8rpx 4rpx 28rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.list-hero-copy .eyebrow {
|
||||
font-size: 22rpx;
|
||||
letter-spacing: 0.08em;
|
||||
opacity: 0.82;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.list-hero-copy .title {
|
||||
font-size: 44rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.list-hero-copy .subline {
|
||||
margin-top: 12rpx;
|
||||
font-size: 24rpx;
|
||||
opacity: 0.88;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.list-hero-copy .subline .em {
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
border-radius: 999rpx;
|
||||
padding: 2rpx 16rpx;
|
||||
}
|
||||
|
||||
.list-search {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.list-filter-dock {
|
||||
margin: -28rpx 24rpx 0;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 1rpx solid rgba(220, 235, 228, 0.95);
|
||||
border-radius: 36rpx;
|
||||
padding: 20rpx;
|
||||
box-shadow: 0 20rpx 56rpx rgba(15, 55, 40, 0.08);
|
||||
}
|
||||
|
||||
.seg-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8rpx;
|
||||
padding: 8rpx;
|
||||
border-radius: 28rpx;
|
||||
background: #eef6f2;
|
||||
}
|
||||
|
||||
.seg-tab {
|
||||
border-radius: 22rpx;
|
||||
padding: 16rpx 8rpx;
|
||||
color: #6b7b74;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.seg-tab .count {
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: #9aada4;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.seg-tab.active {
|
||||
background: #fff;
|
||||
color: var(--brand-green-dark, #087f57);
|
||||
font-weight: 650;
|
||||
box-shadow: 0 8rpx 24rpx rgba(8, 127, 87, 0.14);
|
||||
}
|
||||
|
||||
.seg-tab.active .count {
|
||||
color: var(--brand-green, #0a9f6d);
|
||||
}
|
||||
|
||||
.list-wrap {
|
||||
padding: 32rpx 24rpx 0;
|
||||
}
|
||||
|
||||
.biz-card {
|
||||
position: relative;
|
||||
border-radius: 40rpx;
|
||||
margin-bottom: 28rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20rpx 56rpx rgba(20, 55, 40, 0.08);
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.biz-card.status-warning {
|
||||
background: linear-gradient(145deg, #fff9ef 0%, #ffffff 46%, #fffaf3 100%);
|
||||
}
|
||||
.biz-card.status-primary {
|
||||
background: linear-gradient(145deg, #eaf8f1 0%, #ffffff 48%, #f3fbf7 100%);
|
||||
}
|
||||
.biz-card.status-danger {
|
||||
background: linear-gradient(145deg, #fff1f1 0%, #ffffff 48%, #fff7f7 100%);
|
||||
}
|
||||
.biz-card.status-success {
|
||||
background: linear-gradient(145deg, #eef8f3 0%, #ffffff 50%, #f6fbf8 100%);
|
||||
}
|
||||
|
||||
.biz-card .card-inner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 28rpx 28rpx 24rpx;
|
||||
}
|
||||
|
||||
.biz-card .row-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.biz-card .type-icon {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
font-size: 44rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.biz-card.status-warning .type-icon {
|
||||
background: linear-gradient(145deg, #ffe8b8, #f6d089);
|
||||
color: #7a5610;
|
||||
}
|
||||
.biz-card.status-primary .type-icon {
|
||||
background: linear-gradient(145deg, #8fe0bf, #0a9f6d);
|
||||
}
|
||||
.biz-card.status-danger .type-icon {
|
||||
background: linear-gradient(145deg, #ffb4b4, #e85a5a);
|
||||
}
|
||||
.biz-card.status-success .type-icon {
|
||||
background: linear-gradient(145deg, #b8efd4, #189b65);
|
||||
}
|
||||
|
||||
.biz-card .title-wrap {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.biz-card .type-chip {
|
||||
display: inline-flex;
|
||||
font-size: 20rpx;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.biz-card.status-warning .type-chip {
|
||||
background: rgba(214, 163, 67, 0.16);
|
||||
color: #8f6718;
|
||||
}
|
||||
.biz-card.status-primary .type-chip {
|
||||
background: rgba(10, 159, 109, 0.12);
|
||||
color: #087f57;
|
||||
}
|
||||
.biz-card.status-danger .type-chip {
|
||||
background: rgba(209, 67, 67, 0.12);
|
||||
color: #b13535;
|
||||
}
|
||||
.biz-card.status-success .type-chip {
|
||||
background: rgba(24, 155, 101, 0.12);
|
||||
color: #147a50;
|
||||
}
|
||||
|
||||
.biz-card .title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
color: #173128;
|
||||
}
|
||||
|
||||
.biz-card .status-pill {
|
||||
flex-shrink: 0;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
padding: 8rpx 18rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
.biz-card.status-warning .status-pill {
|
||||
background: #fff;
|
||||
color: #a67b20;
|
||||
box-shadow: 0 4rpx 16rpx rgba(214, 163, 67, 0.22);
|
||||
}
|
||||
.biz-card.status-primary .status-pill {
|
||||
background: #0a9f6d;
|
||||
color: #fff;
|
||||
}
|
||||
.biz-card.status-danger .status-pill {
|
||||
background: #d14343;
|
||||
color: #fff;
|
||||
}
|
||||
.biz-card.status-success .status-pill {
|
||||
background: rgba(24, 155, 101, 0.12);
|
||||
color: #147a50;
|
||||
}
|
||||
|
||||
.biz-card .body-row {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.biz-card .desc-box {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
padding: 20rpx 22rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.9);
|
||||
font-size: 24rpx;
|
||||
color: #3f564c;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.biz-card .side-metric {
|
||||
width: 172rpx;
|
||||
flex-shrink: 0;
|
||||
border-radius: 24rpx;
|
||||
padding: 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
text-align: right;
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.biz-card .side-metric .amt-label {
|
||||
font-size: 20rpx;
|
||||
color: #8aa198;
|
||||
}
|
||||
|
||||
.biz-card .side-metric .amt {
|
||||
margin-top: 4rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 800;
|
||||
color: #173128;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.biz-card.status-warning .side-metric .amt {
|
||||
color: #9a7424;
|
||||
}
|
||||
.biz-card.status-primary .side-metric .amt {
|
||||
color: #087f57;
|
||||
}
|
||||
.biz-card.status-danger .side-metric .amt {
|
||||
color: #b13535;
|
||||
}
|
||||
|
||||
.biz-card .side-metric .time {
|
||||
margin-top: 12rpx;
|
||||
font-size: 20rpx;
|
||||
color: #8aa198;
|
||||
}
|
||||
|
||||
.biz-card .meta-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.biz-card .meta-row .mi {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #5d746a;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border-radius: 999rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
}
|
||||
|
||||
.biz-card .code-pill {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.biz-card .footer {
|
||||
margin-top: 24rpx;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1rpx dashed rgba(140, 170, 155, 0.28);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.biz-card .footer .left-hint {
|
||||
font-size: 22rpx;
|
||||
color: #7f968c;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.biz-card .footer .left-hint .dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.biz-card.status-warning .footer .left-hint {
|
||||
color: #a67b20;
|
||||
}
|
||||
.biz-card.status-primary .footer .left-hint {
|
||||
color: #0a9f6d;
|
||||
}
|
||||
.biz-card.status-danger .footer .left-hint {
|
||||
color: #d14343;
|
||||
}
|
||||
.biz-card.status-success .footer .left-hint {
|
||||
color: #189b65;
|
||||
}
|
||||
|
||||
.biz-card .footer .ops {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.list-end {
|
||||
padding: 16rpx 0 56rpx;
|
||||
text-align: center;
|
||||
font-size: 22rpx;
|
||||
color: #8aa198;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
@@ -1 +1,54 @@
|
||||
// Task 4: menu-grid styles (.menu-grid, .menu-card, .menu-icon, .menu-title)
|
||||
@import '../mixins.scss';
|
||||
|
||||
.menu-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20rpx 12rpx;
|
||||
}
|
||||
|
||||
.menu-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 16rpx;
|
||||
min-height: 168rpx;
|
||||
padding: 20rpx 8rpx 12rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.menu-card:active {
|
||||
background: var(--brand-green-soft, #e9f9f2);
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--brand-green-soft, #e9f9f2);
|
||||
color: var(--brand-green-dark, #087f57);
|
||||
font-size: 48rpx;
|
||||
box-shadow: inset 0 0 0 1rpx rgba(10, 159, 109, 0.08);
|
||||
}
|
||||
|
||||
.menu-icon.tone-yellow {
|
||||
background: var(--barley-yellow-soft, #fff7e8);
|
||||
color: #a67b20;
|
||||
}
|
||||
|
||||
.menu-icon.tone-slate {
|
||||
background: #eef2f1;
|
||||
color: #3d524a;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
font-size: 24rpx;
|
||||
line-height: 1.25;
|
||||
color: var(--text-primary, #1f2a26);
|
||||
text-align: center;
|
||||
max-width: 144rpx;
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -1 +1,184 @@
|
||||
// Task 4: todo panel styles (.todo-panel, .todo-stats, .todo-stat, .todo-list, .todo-item)
|
||||
.todo-panel {
|
||||
padding: 28rpx 24rpx 24rpx;
|
||||
background: linear-gradient(165deg, rgba(233, 249, 242, 0.95) 0%, #fff 42%), #fff;
|
||||
}
|
||||
|
||||
.todo-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.todo-stat {
|
||||
position: relative;
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx 20rpx 20rpx;
|
||||
text-align: left;
|
||||
background: #fff;
|
||||
box-shadow: 0 4rpx 20rpx rgba(8, 127, 87, 0.06);
|
||||
border: 1rpx solid rgba(220, 235, 228, 0.9);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.todo-stat::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 6rpx;
|
||||
border-radius: 6rpx 0 0 6rpx;
|
||||
}
|
||||
|
||||
.todo-stat.tone-warn::before {
|
||||
background: var(--warning, #cf8a19);
|
||||
}
|
||||
.todo-stat.tone-pay::before {
|
||||
background: var(--barley-yellow, #d6a343);
|
||||
}
|
||||
.todo-stat.tone-danger::before {
|
||||
background: var(--danger, #d14343);
|
||||
}
|
||||
|
||||
.todo-stat.tone-warn {
|
||||
background: linear-gradient(180deg, #fffaf0 0%, #fff 70%);
|
||||
}
|
||||
.todo-stat.tone-pay {
|
||||
background: linear-gradient(180deg, #fff8ec 0%, #fff 70%);
|
||||
}
|
||||
.todo-stat.tone-danger {
|
||||
background: linear-gradient(180deg, #fff5f5 0%, #fff 70%);
|
||||
}
|
||||
|
||||
.todo-stat .num {
|
||||
display: block;
|
||||
font-size: 44rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
color: var(--text-primary, #1f2a26);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.todo-stat.tone-warn .num {
|
||||
color: #b77412;
|
||||
}
|
||||
.todo-stat.tone-pay .num {
|
||||
color: #9a7424;
|
||||
}
|
||||
.todo-stat.tone-danger .num {
|
||||
color: var(--danger, #d14343);
|
||||
}
|
||||
|
||||
.todo-stat .label {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: var(--text-muted, #6b7b74);
|
||||
}
|
||||
|
||||
.todo-stat .stat-icon {
|
||||
position: absolute;
|
||||
right: 16rpx;
|
||||
top: 16rpx;
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.todo-stat.tone-warn .stat-icon {
|
||||
background: rgba(207, 138, 25, 0.12);
|
||||
color: var(--warning, #cf8a19);
|
||||
}
|
||||
.todo-stat.tone-pay .stat-icon {
|
||||
background: rgba(214, 163, 67, 0.16);
|
||||
color: #a67b20;
|
||||
}
|
||||
.todo-stat.tone-danger .stat-icon {
|
||||
background: rgba(209, 67, 67, 0.12);
|
||||
color: var(--danger, #d14343);
|
||||
}
|
||||
|
||||
.todo-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.todo-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
width: 100%;
|
||||
padding: 20rpx;
|
||||
border: 1rpx solid var(--border, #dcebe4);
|
||||
border-radius: 24rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.todo-item:active {
|
||||
background: var(--brand-green-soft, #e9f9f2);
|
||||
border-color: #b7e5d0;
|
||||
}
|
||||
|
||||
.todo-item .ti-icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.todo-item .ti-icon.i-approve {
|
||||
background: #fff3e0;
|
||||
color: var(--warning, #cf8a19);
|
||||
}
|
||||
.todo-item .ti-icon.i-pay {
|
||||
background: var(--barley-yellow-soft, #fff7e8);
|
||||
color: #a67b20;
|
||||
}
|
||||
.todo-item .ti-icon.i-alert {
|
||||
background: #fdecec;
|
||||
color: var(--danger, #d14343);
|
||||
}
|
||||
|
||||
.todo-item .ti-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.todo-item .ti-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #1f2a26);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.todo-item .ti-desc {
|
||||
margin-top: 4rpx;
|
||||
font-size: 22rpx;
|
||||
color: var(--text-muted, #6b7b74);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.todo-item .ti-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.todo-item .ti-time {
|
||||
font-size: 20rpx;
|
||||
color: #9aada4;
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
const listData = [
|
||||
{
|
||||
id: 1,
|
||||
title: '脐带血采集协议-王女士',
|
||||
code: 'XY20260810001',
|
||||
typeLabel: '采集协议',
|
||||
icon: 'orders-o',
|
||||
owner: '李四',
|
||||
time: '08-10 09:20',
|
||||
amount: '12,800',
|
||||
progress: '',
|
||||
desc: '分期缴费 · 待财务确认首期款项',
|
||||
status: '待处理',
|
||||
tagType: 'warning',
|
||||
hint: '需尽快确认',
|
||||
customer: '王女士',
|
||||
dept: '样本运营部',
|
||||
createdAt: '2026-08-08 14:20',
|
||||
stepActive: 1,
|
||||
steps: ['建档', '缴费确认', '样本采集', '入库'],
|
||||
payments: [
|
||||
{
|
||||
id: 'p1',
|
||||
name: '第 1 期(首期)',
|
||||
sub: '应缴 08-10 · 微信转账',
|
||||
amount: '6,400',
|
||||
status: '待确认',
|
||||
tagType: 'warning'
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
name: '第 2 期',
|
||||
sub: '应缴 09-10 · 未到期',
|
||||
amount: '6,400',
|
||||
status: '待缴费',
|
||||
tagType: 'primary'
|
||||
}
|
||||
],
|
||||
attachments: [
|
||||
{ name: '协议扫描件.pdf', size: '2.4 MB', time: '08-08 14:18' },
|
||||
{ name: '身份证明.jpg', size: '860 KB', time: '08-08 14:19' }
|
||||
],
|
||||
logs: [
|
||||
{
|
||||
title: '提交审批',
|
||||
desc: '提交采集协议,等待财务确认首期',
|
||||
time: '08-08 14:22',
|
||||
tone: 'done',
|
||||
actor: '李四',
|
||||
icon: 'edit',
|
||||
badge: '已完成'
|
||||
},
|
||||
{
|
||||
title: '流转至财务',
|
||||
desc: '系统自动分配至财务确认岗',
|
||||
time: '08-08 14:23',
|
||||
tone: 'done',
|
||||
actor: '系统',
|
||||
icon: 'cluster-o',
|
||||
badge: '已完成'
|
||||
},
|
||||
{
|
||||
title: '待确认缴费',
|
||||
desc: '首期 ¥6,400 等待财务核对附件',
|
||||
time: '08-10 09:20',
|
||||
tone: 'warn',
|
||||
actor: '财务岗',
|
||||
icon: 'balance-list-o',
|
||||
badge: '进行中'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '样本接收单-批次 A12',
|
||||
code: 'YS20260809018',
|
||||
typeLabel: '样本接收',
|
||||
icon: 'logistics',
|
||||
owner: '王五',
|
||||
time: '08-09 16:40',
|
||||
amount: '',
|
||||
progress: '在途',
|
||||
desc: '冷链在途 · 预计今日 18:00 前到达库房',
|
||||
status: '进行中',
|
||||
tagType: 'primary',
|
||||
hint: '跟踪物流',
|
||||
customer: '批次 A12',
|
||||
dept: '仓储物流部',
|
||||
createdAt: '2026-08-09 09:10',
|
||||
stepActive: 2,
|
||||
steps: ['发运', '在途', '到库签收', '入库'],
|
||||
payments: [],
|
||||
attachments: [{ name: '冷链温度记录.pdf', size: '1.1 MB', time: '08-09 11:28' }],
|
||||
logs: [
|
||||
{
|
||||
title: '创建接收单',
|
||||
desc: '创建样本接收单并绑定批次 A12',
|
||||
time: '08-09 09:10',
|
||||
tone: 'done',
|
||||
actor: '王五',
|
||||
icon: 'orders-o',
|
||||
badge: '已完成'
|
||||
},
|
||||
{
|
||||
title: '冷链发出',
|
||||
desc: '承运方已取件,全程温控开启',
|
||||
time: '08-09 11:30',
|
||||
tone: 'done',
|
||||
actor: '承运方',
|
||||
icon: 'logistics',
|
||||
badge: '已完成'
|
||||
},
|
||||
{
|
||||
title: '运输中',
|
||||
desc: '当前位置:城东中转站',
|
||||
time: '08-09 16:40',
|
||||
tone: 'warn',
|
||||
actor: '物流跟踪',
|
||||
icon: 'location-o',
|
||||
badge: '进行中'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '质检工单-QC-8821',
|
||||
code: 'ZJ20260808007',
|
||||
typeLabel: '质检工单',
|
||||
icon: 'certificate',
|
||||
owner: '赵六',
|
||||
time: '08-08 11:05',
|
||||
amount: '',
|
||||
progress: '阻塞',
|
||||
desc: '检测结论待录入,阻塞下游入库流程',
|
||||
status: '异常',
|
||||
tagType: 'danger',
|
||||
hint: '优先处理',
|
||||
customer: '样本 S-8821',
|
||||
dept: '质检中心',
|
||||
createdAt: '2026-08-07 16:40',
|
||||
stepActive: 1,
|
||||
steps: ['取样', '检测', '结论录入', '关闭'],
|
||||
payments: [],
|
||||
attachments: [{ name: '初检原始数据.xlsx', size: '428 KB', time: '08-08 10:48' }],
|
||||
logs: [
|
||||
{
|
||||
title: '工单创建',
|
||||
desc: '系统根据接收单自动生成质检工单',
|
||||
time: '08-07 16:40',
|
||||
tone: 'done',
|
||||
actor: '系统',
|
||||
icon: 'add-o',
|
||||
badge: '已完成'
|
||||
},
|
||||
{
|
||||
title: '检测完成',
|
||||
desc: '完成实验操作,待录入结论',
|
||||
time: '08-08 10:50',
|
||||
tone: 'done',
|
||||
actor: '赵六',
|
||||
icon: 'certificate',
|
||||
badge: '已完成'
|
||||
},
|
||||
{
|
||||
title: '异常挂起',
|
||||
desc: '结论超时未录入,已阻塞入库',
|
||||
time: '08-08 11:05',
|
||||
tone: 'danger',
|
||||
actor: '质控规则',
|
||||
icon: 'warning-o',
|
||||
badge: '异常'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '出库申请-库位 B-03',
|
||||
code: 'CK20260807002',
|
||||
typeLabel: '库位出库',
|
||||
icon: 'send-gift-o',
|
||||
owner: '张三',
|
||||
time: '08-07 14:22',
|
||||
amount: '',
|
||||
progress: '完结',
|
||||
desc: '复核已完成,可归档关闭',
|
||||
status: '已完成',
|
||||
tagType: 'success',
|
||||
hint: '已完结',
|
||||
customer: '库位 B-03',
|
||||
dept: '仓储物流部',
|
||||
createdAt: '2026-08-06 10:00',
|
||||
stepActive: 3,
|
||||
steps: ['申请', '复核', '出库', '归档'],
|
||||
payments: [],
|
||||
attachments: [{ name: '出库单签收.pdf', size: '980 KB', time: '08-07 14:18' }],
|
||||
logs: [
|
||||
{
|
||||
title: '提交申请',
|
||||
desc: '发起出库申请,指定库位 B-03',
|
||||
time: '08-06 10:00',
|
||||
tone: 'done',
|
||||
actor: '张三',
|
||||
icon: 'edit',
|
||||
badge: '已完成'
|
||||
},
|
||||
{
|
||||
title: '复核通过',
|
||||
desc: '库管复核通过,允许出库',
|
||||
time: '08-07 11:10',
|
||||
tone: 'done',
|
||||
actor: '库管',
|
||||
icon: 'passed',
|
||||
badge: '已完成'
|
||||
},
|
||||
{
|
||||
title: '出库完成',
|
||||
desc: '实物已交接,单据归档',
|
||||
time: '08-07 14:22',
|
||||
tone: 'done',
|
||||
actor: '张三',
|
||||
icon: 'checked',
|
||||
badge: '已完成'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
export function getList() {
|
||||
return listData.slice()
|
||||
}
|
||||
|
||||
export function getById(id) {
|
||||
const n = Number(id)
|
||||
return listData.find((r) => r.id === n) || listData[0]
|
||||
}
|
||||
|
||||
function rowsByTab(idx) {
|
||||
if (idx === 1) return listData.filter((r) => r.status === '进行中')
|
||||
if (idx === 2) return listData.filter((r) => r.status === '待处理')
|
||||
if (idx === 3) return listData.filter((r) => r.status === '已完成')
|
||||
return listData
|
||||
}
|
||||
|
||||
export function tabCount(idx) {
|
||||
return rowsByTab(idx).length
|
||||
}
|
||||
|
||||
export function filterByTab(tabIndex, keyword) {
|
||||
const kw = (keyword || '').trim()
|
||||
let rows = rowsByTab(tabIndex)
|
||||
if (!kw) return rows
|
||||
return rows.filter(
|
||||
(r) => r.title.includes(kw) || r.code.includes(kw) || r.owner.includes(kw)
|
||||
)
|
||||
}
|
||||
|
||||
export const LIST_TABS = [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'doing', label: '进行中' },
|
||||
{ key: 'pending', label: '待处理' },
|
||||
{ key: 'done', label: '已完成' }
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
/** 模拟「菜单管理 → 移动端菜单」授权结果 */
|
||||
export const ALL_MOBILE_MENUS = [
|
||||
{ id: 'm1', title: '待办审批', icon: 'todo-list-o', path: '/pages/demo/list' },
|
||||
{ id: 'm2', title: '采集协议', icon: 'orders-o', path: '/pages/demo/list' },
|
||||
{ id: 'm3', title: '缴费确认', icon: 'balance-list-o', path: '/pages/demo/list' },
|
||||
{ id: 'm4', title: '样本接收', icon: 'logistics', path: '/pages/demo/list' },
|
||||
{ id: 'm5', title: '质检工单', icon: 'certificate', path: '/pages/demo/list' },
|
||||
{ id: 'm6', title: '生产任务', icon: 'cluster-o', path: '/pages/demo/list' },
|
||||
{ id: 'm7', title: '库位出库', icon: 'send-gift-o', path: '/pages/demo/list' },
|
||||
{ id: 'm8', title: '组件示例', icon: 'apps-o', path: '/pages/demo/comps' }
|
||||
]
|
||||
|
||||
/** @param {string[]} grantedIds */
|
||||
export function filterMenusByPermission(grantedIds) {
|
||||
const set = new Set(grantedIds || [])
|
||||
return ALL_MOBILE_MENUS.filter((m) => set.has(m.id))
|
||||
}
|
||||
|
||||
export const DEFAULT_GRANTED_IDS = ['m1', 'm2', 'm3', 'm4', 'm5', 'm7', 'm8']
|
||||
Reference in new issue
Block a user