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:
zlwuandCursor committed 2026-08-10 14:03:36 +08:00
1 parent cfe0e616ab
commit 8793fa07cc
19 files changed
+2460 -7

No files matched your search

+75
View File
@@ -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>
+61
View File
@@ -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>
+44
View File
@@ -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>
+25
View File
@@ -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>
+61
View File
@@ -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>
+113
View File
@@ -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>
+139
View File
@@ -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>
+164
View File
@@ -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>
+101
View File
@@ -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>
+100
View File
@@ -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>
+185 -1
View File
@@ -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;
}
+241 -1
View File
@@ -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;
}
+142 -1
View File
@@ -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;
}
+116 -1
View File
@@ -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;
}
+374 -1
View File
@@ -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;
}
+54 -1
View File
@@ -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;
}
+184 -1
View File
@@ -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;
}
+262
View File
@@ -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: '已完成' }
]
+19
View File
@@ -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']