This commit is contained in:
zlwu committed 2026-07-18 09:14:02 +08:00
1 parent a903fcf6d4
commit 259c55cc23
80 files changed
+6090 -123

No files matched your search

+1 -4
View File
@@ -60,10 +60,7 @@ onReachBottom(() => loadList(false))
</view>
<view class="mp-section">
<view v-if="!list.length && !loading" class="mp-empty">
<view class="mp-empty__icon" />
<text class="mp-empty__text">暂无借款单</text>
</view>
<mp-empty v-if="!list.length && !loading" title="暂无借款单" />
<view v-else class="mp-list">
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
<view class="mp-card__head">
+1 -4
View File
@@ -103,10 +103,7 @@ onReachBottom(() => loadList(false))
</view>
<view class="mp-section">
<view v-if="!list.length && !loading" class="mp-empty">
<view class="mp-empty__icon" />
<text class="mp-empty__text">暂无待审批</text>
</view>
<mp-empty v-if="!list.length && !loading" title="暂无待审批" />
<view v-else class="mp-list">
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
<view class="mp-card__head">
+1 -4
View File
@@ -113,10 +113,7 @@ onReachBottom(() => loadList(false))
</view>
<view class="mp-section">
<view v-if="!list.length && !loading" class="mp-empty">
<view class="mp-empty__icon" />
<text class="mp-empty__text">暂无单据</text>
</view>
<mp-empty v-if="!list.length && !loading" title="暂无单据" />
<view v-else class="mp-list">
<view v-for="row in list" :key="row._biz + '-' + row.id" class="mp-card" @click="goDetail(row)">
<view class="mp-card__head">
+1 -4
View File
@@ -60,10 +60,7 @@ onReachBottom(() => loadList(false))
</view>
<view class="mp-section">
<view v-if="!list.length && !loading" class="mp-empty">
<view class="mp-empty__icon" />
<text class="mp-empty__text">暂无报销单</text>
</view>
<mp-empty v-if="!list.length && !loading" title="暂无报销单" />
<view v-else class="mp-list">
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
<view class="mp-card__head">
+1 -4
View File
@@ -171,10 +171,7 @@ onReachBottom(() => {
</view>
<view class="mp-section">
<view v-if="!list.length && !loading" class="mp-empty">
<view class="mp-empty__icon" />
<text class="mp-empty__text">暂无单据</text>
</view>
<mp-empty v-if="!list.length && !loading" title="暂无单据" />
<view v-else class="mp-list">
<view v-for="row in list" :key="row._biz + '-' + row.id" class="mp-card" @click="goDetail(row)">
<view class="mp-card__head">
+1 -3
View File
@@ -151,9 +151,7 @@ onShow(() => load())
<view class="mp-block">
<text class="mp-block__title">跟进记录</text>
<view v-if="!follows.length" class="mp-empty" style="padding: 40rpx 0">
<text class="mp-empty__text">暂无跟进</text>
</view>
<mp-empty v-if="!follows.length" compact title="暂无跟进" />
<view v-else class="mp-list">
<view v-for="f in follows" :key="f.id" class="mp-card">
<view class="mp-card__head">
+1 -4
View File
@@ -119,10 +119,7 @@ onReachBottom(() => loadList(false))
</view>
<view class="mp-section">
<view v-if="!list.length && !loading" class="mp-empty">
<view class="mp-empty__icon" />
<text class="mp-empty__text">暂无线索</text>
</view>
<mp-empty v-if="!list.length && !loading" title="暂无线索" />
<view v-else class="mp-list">
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
<view class="mp-card__head">
+1 -4
View File
@@ -107,10 +107,7 @@ onReachBottom(() => loadList(false))
</view>
<view class="mp-section">
<view v-if="!list.length && !loading" class="mp-empty">
<view class="mp-empty__icon" />
<text class="mp-empty__text">暂无合同</text>
</view>
<mp-empty v-if="!list.length && !loading" title="暂无合同" />
<view v-else class="mp-list">
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
<view class="mp-card__head">
+1 -3
View File
@@ -99,9 +99,7 @@ onShow(() => load())
<view class="mp-block">
<text class="mp-block__title">跟进记录</text>
<view v-if="!follows.length" class="mp-empty" style="padding: 40rpx 0">
<text class="mp-empty__text">暂无跟进</text>
</view>
<mp-empty v-if="!follows.length" compact title="暂无跟进" />
<view v-else class="mp-list">
<view v-for="f in follows" :key="f.id" class="mp-card">
<view class="mp-card__head">
+1 -4
View File
@@ -104,10 +104,7 @@ onReachBottom(() => loadList(false))
</view>
<view class="mp-section">
<view v-if="!list.length && !loading" class="mp-empty">
<view class="mp-empty__icon" />
<text class="mp-empty__text">暂无客户</text>
</view>
<mp-empty v-if="!list.length && !loading" title="暂无客户" />
<view v-else class="mp-list">
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
<view class="mp-card__head">
+1 -3
View File
@@ -98,9 +98,7 @@ onShow(() => load())
<view class="mp-block">
<text class="mp-block__title">跟进记录</text>
<view v-if="!follows.length" class="mp-empty" style="padding: 40rpx 0">
<text class="mp-empty__text">暂无跟进</text>
</view>
<mp-empty v-if="!follows.length" compact title="暂无跟进" />
<view v-else class="mp-list">
<view v-for="f in follows" :key="f.id" class="mp-card">
<view class="mp-card__head">
+1 -4
View File
@@ -106,10 +106,7 @@ onReachBottom(() => loadList(false))
</view>
<view class="mp-section">
<view v-if="!list.length && !loading" class="mp-empty">
<view class="mp-empty__icon" />
<text class="mp-empty__text">暂无商机</text>
</view>
<mp-empty v-if="!list.length && !loading" title="暂无商机" />
<view v-else class="mp-list">
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
<view class="mp-card__head">
+1 -4
View File
@@ -102,10 +102,7 @@ onReachBottom(() => loadList(false))
</view>
<view class="mp-section">
<view v-if="!list.length && !loading" class="mp-empty">
<view class="mp-empty__icon" />
<text class="mp-empty__text">公海暂无客户</text>
</view>
<mp-empty v-if="!list.length && !loading" title="公海暂无客户" />
<view v-else class="mp-list">
<view v-for="row in list" :key="row.id" class="mp-card">
<view class="mp-card__head" @click="goDetail(row)">
@@ -0,0 +1,297 @@
<script setup>
import { computed, reactive, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import {
addProductCategory,
listProductCategoryTree,
updateProductCategory
} from '@/api/crm'
import { PAGE_PATHS } from '@/config/api'
const submitting = ref(false)
const loading = ref(false)
const editId = ref('')
const tree = ref([])
const showParentSheet = ref(false)
const form = reactive({
parentId: '0',
categoryName: '',
orderNum: 0,
status: '0',
remark: ''
})
const statusOptions = [
{ value: '0', label: '正常' },
{ value: '1', label: '停用' }
]
const pageTitle = computed(() => (editId.value ? '修改产品分类' : '新增产品分类'))
/** 收集某节点及其全部子孙 id(编辑时禁止选为上级) */
function collectDescendantIds(nodes, rootId, out = new Set()) {
;(nodes || []).forEach((n) => {
if (n.id === rootId) {
out.add(n.id)
const walk = (kids) => {
;(kids || []).forEach((c) => {
out.add(c.id)
walk(c.children)
})
}
walk(n.children)
} else if (n.children?.length) {
collectDescendantIds(n.children, rootId, out)
}
})
return out
}
function flattenParents(nodes, pathParts, excludeIds, out = []) {
;(nodes || []).forEach((n) => {
if (excludeIds.has(n.id)) return
const path = [...pathParts, n.categoryName]
out.push({
id: n.id,
label: path.join(' / '),
name: n.categoryName
})
if (n.children?.length) {
flattenParents(n.children, path, excludeIds, out)
}
})
return out
}
const parentOptions = computed(() => {
const exclude = editId.value
? collectDescendantIds(tree.value, editId.value)
: new Set()
return [{ id: '0', label: '顶级分类', name: '顶级分类' }].concat(
flattenParents(tree.value, [], exclude, [])
)
})
const parentLabel = computed(() => {
const hit = parentOptions.value.find((o) => o.id === (form.parentId || '0'))
return hit?.label || '顶级分类'
})
const statusIndex = computed(() =>
Math.max(
0,
statusOptions.findIndex((o) => o.value === form.status)
)
)
const findNodeById = (nodes, id) => {
for (const n of nodes || []) {
if (n.id === id) return n
const found = findNodeById(n.children, id)
if (found) return found
}
return null
}
const goBack = () => {
uni.navigateBack({
fail: () => uni.navigateTo({ url: PAGE_PATHS.crmProductCategoryList })
})
}
const loadTree = async () => {
loading.value = true
try {
tree.value = await listProductCategoryTree({})
if (editId.value) {
const node = findNodeById(tree.value, editId.value)
if (!node) {
uni.showToast({ title: '分类不存在', icon: 'none' })
return
}
form.parentId = node.parentId && node.parentId !== '0' ? node.parentId : '0'
form.categoryName = node.categoryName || ''
form.orderNum = node.orderNum ?? 0
form.status = node.status != null ? String(node.status) : '0'
form.remark = node.remark || ''
}
} catch (e) {
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
const openParentSheet = () => {
showParentSheet.value = true
}
const closeParentSheet = () => {
showParentSheet.value = false
}
const selectParent = (opt) => {
form.parentId = opt.id
closeParentSheet()
}
const onStatusChange = (e) => {
const idx = Number(e.detail.value)
form.status = statusOptions[idx]?.value || '0'
}
const onOrderInput = (e) => {
const raw = e.detail.value
const n = parseInt(raw, 10)
form.orderNum = Number.isFinite(n) && n >= 0 ? n : 0
}
const submit = async () => {
if (!form.categoryName.trim()) {
uni.showToast({ title: '请填写分类名称', icon: 'none' })
return
}
if (submitting.value) return
submitting.value = true
try {
const payload = {
parentId: form.parentId || '0',
categoryName: form.categoryName.trim(),
orderNum: form.orderNum ?? 0,
status: form.status || '0',
remark: form.remark || ''
}
if (editId.value) {
await updateProductCategory({ id: editId.value, ...payload })
uni.showToast({ title: '修改成功', icon: 'success' })
} else {
await addProductCategory(payload)
uni.showToast({ title: '新增成功', icon: 'success' })
}
setTimeout(() => goBack(), 400)
} catch (e) {
uni.showToast({ title: e.message || '保存失败', icon: 'none' })
} finally {
submitting.value = false
}
}
onLoad((query) => {
editId.value = query?.id ? String(query.id) : ''
if (!editId.value && query?.parentId) {
form.parentId = String(query.parentId)
}
loadTree()
})
</script>
<template>
<view class="mp-page mp-page--sub mp-page--with-bottom">
<view class="mp-header">
<text class="mp-subpage-back" @click="goBack">← 返回</text>
<text class="mp-header__title" style="margin-top: 16rpx">{{ pageTitle }}</text>
</view>
<view v-if="loading" class="mp-list-loading">加载中…</view>
<view v-else class="mp-form-panel">
<view class="mp-form-row" @click="openParentSheet">
<text class="mp-form-label">上级分类</text>
<view class="mp-form-picker mp-form-picker--link">{{ parentLabel }}</view>
</view>
<view class="mp-form-row">
<text class="mp-form-label mp-form-label--req">分类名称</text>
<input
v-model="form.categoryName"
class="mp-form-input"
placeholder="请输入分类名称"
/>
</view>
<view class="mp-form-row">
<text class="mp-form-label">排序号</text>
<input
class="mp-form-input"
type="number"
:value="String(form.orderNum)"
placeholder="数值越小越靠前"
@input="onOrderInput"
/>
</view>
<view class="mp-form-row">
<text class="mp-form-label">状态</text>
<picker :range="statusOptions" range-key="label" :value="statusIndex" @change="onStatusChange">
<view class="mp-form-picker">
{{ statusOptions[statusIndex]?.label || '正常' }}
</view>
</picker>
</view>
<view class="mp-form-row">
<text class="mp-form-label">备注</text>
<textarea v-model="form.remark" class="mp-form-textarea" placeholder="请输入备注" />
</view>
</view>
<view class="mp-bottom-bar">
<view class="mp-btn mp-btn--ghost" @click="goBack">取消</view>
<view class="mp-btn mp-btn--primary" @click="submit">
{{ submitting ? '提交中…' : '保存' }}
</view>
</view>
<view v-if="showParentSheet" class="mp-sheet-mask" @click="closeParentSheet">
<view class="mp-sheet" @click.stop>
<view class="mp-sheet__head">
<text class="mp-sheet__title">选择上级分类</text>
<text class="mp-sheet__cancel" @click="closeParentSheet">关闭</text>
</view>
<scroll-view scroll-y class="parent-sheet-body">
<view
v-for="opt in parentOptions"
:key="opt.id"
class="parent-opt"
:class="{ 'parent-opt--active': form.parentId === opt.id }"
@click="selectParent(opt)"
>
<text class="parent-opt__label">{{ opt.label }}</text>
<text v-if="form.parentId === opt.id" class="parent-opt__check">✓</text>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<style scoped lang="scss">
.parent-sheet-body {
max-height: 60vh;
padding: 8rpx 0 24rpx;
}
.parent-opt {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
padding: 28rpx 32rpx;
border-bottom: 1rpx solid var(--mp-border-light);
}
.parent-opt--active .parent-opt__label {
color: var(--mp-brand-green-dark);
font-weight: 600;
}
.parent-opt__label {
flex: 1;
font-size: 28rpx;
color: var(--mp-text);
line-height: 1.4;
}
.parent-opt__check {
color: var(--mp-brand-green);
font-size: 28rpx;
font-weight: 700;
}
</style>
@@ -0,0 +1,702 @@
<script setup>
import { computed, ref, watch } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import {
deleteProductCategory,
listProductCategoryTree
} from '@/api/crm'
import { PAGE_PATHS } from '@/config/api'
const keyword = ref('')
const statusFilter = ref('') // '' | '0' | '1'
const tree = ref([])
const loading = ref(false)
const expandedIds = ref([])
const allExpanded = ref(false)
let loadSeq = 0
let searchTimer = null
const statusChips = [
{ key: '', label: '全部' },
{ key: '0', label: '正常' },
{ key: '1', label: '停用' }
]
const statusLabel = (status) =>
String(status) === '0'
? { text: '正常', tag: 'success' }
: { text: '停用', tag: 'muted' }
const childCount = (node) => (node.children || []).length
function collectExpandableIds(nodes, out = []) {
;(nodes || []).forEach((n) => {
if (childCount(n) > 0) {
out.push(n.id)
collectExpandableIds(n.children, out)
}
})
return out
}
function defaultExpandLevel1(nodes) {
return (nodes || []).filter((n) => childCount(n) > 0).map((n) => n.id)
}
function flattenVisible(nodes, level, expandedSet, out = []) {
;(nodes || []).forEach((n) => {
const kids = childCount(n)
out.push({
id: n.id,
parentId: n.parentId,
categoryName: n.categoryName,
status: n.status,
orderNum: n.orderNum ?? 0,
level,
hasChildren: kids > 0,
childCount: kids
})
if (kids > 0 && expandedSet.has(n.id)) {
flattenVisible(n.children, level + 1, expandedSet, out)
}
})
return out
}
const visibleRows = computed(() => {
const set = new Set(expandedIds.value)
return flattenVisible(tree.value, 0, set, [])
})
const goBack = () => {
uni.navigateBack({
fail: () => uni.switchTab({ url: PAGE_PATHS.workbench })
})
}
/**
* 服务端过滤:categoryName / status 写入 POST JSON,经加密中间件解密后由 Pydantic 绑定。
* @param {{ categoryName?: string, status?: string }} [override]
*/
const loadTree = async (override = {}) => {
const seq = ++loadSeq
loading.value = true
try {
const kw = String(
override.categoryName != null ? override.categoryName : keyword.value
).trim()
const status =
override.status != null ? override.status : statusFilter.value
const rows = await listProductCategoryTree({
categoryName: kw || undefined,
status: status || undefined
})
if (seq !== loadSeq) return
tree.value = rows
const expandable = collectExpandableIds(rows)
const hasFilter = Boolean(kw) || Boolean(status)
if (hasFilter) {
expandedIds.value = expandable
allExpanded.value = true
} else if (allExpanded.value) {
expandedIds.value = expandable
} else {
expandedIds.value = defaultExpandLevel1(rows)
}
} catch (e) {
if (seq !== loadSeq) return
tree.value = []
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
} finally {
if (seq === loadSeq) loading.value = false
}
}
const scheduleSearch = () => {
clearTimeout(searchTimer)
searchTimer = setTimeout(() => {
loadTree({ categoryName: keyword.value })
}, 300)
}
const onSearch = () => {
clearTimeout(searchTimer)
loadTree({ categoryName: keyword.value })
}
const clearKeyword = () => {
if (!keyword.value) return
keyword.value = ''
clearTimeout(searchTimer)
loadTree({ categoryName: '' })
}
// 即时检索:防抖后走后台 tree 接口过滤(对齐 Web 300ms)
watch(keyword, (val, oldVal) => {
if (val === oldVal) return
scheduleSearch()
})
const switchStatus = (key) => {
if (statusFilter.value === key) return
statusFilter.value = key
clearTimeout(searchTimer)
loadTree({ status: key })
}
const toggleExpandAll = () => {
allExpanded.value = !allExpanded.value
if (allExpanded.value) {
expandedIds.value = collectExpandableIds(tree.value)
} else {
expandedIds.value = []
}
}
const toggleNode = (row) => {
if (!row.hasChildren) return
const set = new Set(expandedIds.value)
if (set.has(row.id)) set.delete(row.id)
else set.add(row.id)
expandedIds.value = Array.from(set)
const allIds = collectExpandableIds(tree.value)
allExpanded.value = allIds.length > 0 && allIds.every((id) => set.has(id))
}
const goForm = (query = {}) => {
const parts = Object.keys(query)
.filter((k) => query[k] != null && query[k] !== '')
.map((k) => `${k}=${encodeURIComponent(query[k])}`)
const qs = parts.length ? `?${parts.join('&')}` : ''
uni.navigateTo({ url: `${PAGE_PATHS.crmProductCategoryForm}${qs}` })
}
const goAdd = () => goForm()
const onEmptyAction = () => {
if (keyword.value.trim()) clearKeyword()
else goAdd()
}
const goAddChild = (row) => {
goForm({ parentId: row.id, parentName: row.categoryName })
}
const goEdit = (row) => {
goForm({ id: row.id })
}
const moreVisible = ref(false)
const moreTarget = ref(null)
const closeMore = () => {
moreVisible.value = false
moreTarget.value = null
}
const confirmDelete = (row) => {
uni.showModal({
title: '提示',
content: `是否确认删除「${row.categoryName}」?`,
confirmColor: '#d14343',
success: async (res) => {
if (!res.confirm) return
try {
await deleteProductCategory(row.id)
uni.showToast({ title: '删除成功', icon: 'success' })
loadTree()
} catch (e) {
uni.showToast({ title: e.message || '删除失败', icon: 'none' })
}
}
})
}
const openMore = (row) => {
moreTarget.value = row
moreVisible.value = true
}
const onMoreAction = (action) => {
const row = moreTarget.value
if (!row) return
closeMore()
// 等关闭动画后再跳转/弹窗,避免层级冲突
setTimeout(() => {
if (action === 'edit') goEdit(row)
else if (action === 'addChild') goAddChild(row)
else if (action === 'delete') confirmDelete(row)
}, 180)
}
onShow(() => loadTree())
</script>
<template>
<view class="mp-page mp-page--sub">
<view class="mp-header">
<view class="mp-header__row">
<text class="mp-subpage-back" @click="goBack">← 工作台</text>
<view class="cat-header-tools">
<view class="mp-header__action" @click="toggleExpandAll">
{{ allExpanded ? '全部折叠' : '全部展开' }}
</view>
<view class="mp-header__action" @click="goAdd">+ 新增</view>
</view>
</view>
<text class="mp-header__title" style="margin-top: 16rpx">产品分类</text>
<view class="mp-header__search">
<view class="mp-header__search-dot" />
<input
v-model="keyword"
class="mp-header__search-input"
type="text"
placeholder="搜索分类名称"
confirm-type="search"
@confirm="onSearch"
/>
<text v-if="keyword" class="cat-search-clear" @click="clearKeyword">清除</text>
</view>
</view>
<scroll-view scroll-x class="cat-chips" :show-scrollbar="false">
<view
v-for="chip in statusChips"
:key="chip.key || 'all'"
class="cat-chip"
:class="{ 'cat-chip--active': statusFilter === chip.key }"
@click="switchStatus(chip.key)"
>
{{ chip.label }}
</view>
</scroll-view>
<view class="mp-section">
<mp-empty
v-if="!visibleRows.length && !loading"
:title="keyword.trim() ? '未找到相关分类' : '暂无产品分类'"
:description="
keyword.trim()
? '试试更短的关键词,或清除搜索后查看全部'
: '还没有分类数据,可先新增一个顶级分类'
"
:action-text="keyword.trim() ? '清除搜索' : '新增分类'"
@action="onEmptyAction"
/>
<view v-else class="cat-tree">
<view
v-for="(row, index) in visibleRows"
:key="row.id"
class="cat-row"
:class="'cat-row--l' + Math.min(row.level, 2)"
>
<text class="cat-index">{{ index + 1 }}</text>
<view
class="cat-row__body"
:style="{ paddingLeft: row.level * 36 + 'rpx' }"
>
<view
class="cat-chevron"
:class="{
'cat-chevron--leaf': !row.hasChildren,
'cat-chevron--open': expandedIds.includes(row.id)
}"
@click.stop="toggleNode(row)"
>
▶
</view>
<view class="cat-main" @click="goEdit(row)">
<text class="cat-name">{{ row.categoryName }}</text>
<view class="cat-meta">
<text class="mp-tag" :class="'mp-tag--' + statusLabel(row.status).tag">
{{ statusLabel(row.status).text }}
</text>
<text class="cat-meta__text">排序 {{ row.orderNum }}</text>
<text v-if="row.hasChildren" class="cat-meta__text">{{ row.childCount }} 个子类</text>
</view>
</view>
<view class="cat-actions">
<view class="cat-icon-btn" @click.stop="goAddChild(row)">+</view>
<view class="cat-icon-btn cat-icon-btn--more" @click.stop="openMore(row)">⋯</view>
</view>
</view>
</view>
</view>
<view v-if="loading" class="mp-list-loading">加载中…</view>
</view>
<!-- 更多操作:自定义底部菜单(替代系统 ActionSheet) -->
<view
v-if="moreVisible"
class="cat-as-mask"
@click="closeMore"
@touchmove.stop.prevent
>
<view class="cat-as" @click.stop>
<view class="cat-as__handle" />
<view class="cat-as__head">
<text class="cat-as__eyebrow">操作分类</text>
<text class="cat-as__title">{{ moreTarget?.categoryName || '' }}</text>
</view>
<view class="cat-as__group">
<view class="cat-as__item" @click="onMoreAction('edit')">
<view class="cat-as__icon cat-as__icon--edit">编</view>
<view class="cat-as__meta">
<text class="cat-as__name">编辑</text>
<text class="cat-as__desc">修改名称、上级、状态等</text>
</view>
<text class="cat-as__arrow">›</text>
</view>
<view class="cat-as__item" @click="onMoreAction('addChild')">
<view class="cat-as__icon cat-as__icon--add">子</view>
<view class="cat-as__meta">
<text class="cat-as__name">新增子分类</text>
<text class="cat-as__desc">在此分类下创建下级</text>
</view>
<text class="cat-as__arrow">›</text>
</view>
<view class="cat-as__item cat-as__item--danger" @click="onMoreAction('delete')">
<view class="cat-as__icon cat-as__icon--del">删</view>
<view class="cat-as__meta">
<text class="cat-as__name">删除</text>
<text class="cat-as__desc">有子分类或关联产品时不可删</text>
</view>
<text class="cat-as__arrow">›</text>
</view>
</view>
<view class="cat-as__cancel" @click="closeMore">取消</view>
</view>
</view>
</view>
</template>
<style scoped lang="scss">
.cat-header-tools {
display: flex;
align-items: center;
gap: 12rpx;
}
.cat-search-clear {
flex-shrink: 0;
font-size: 24rpx;
color: var(--mp-brand-green-dark);
padding: 8rpx 4rpx 8rpx 12rpx;
}
.cat-chips {
white-space: nowrap;
padding: 20rpx 32rpx 0;
width: 100%;
}
.cat-chip {
display: inline-block;
margin-right: 16rpx;
padding: 12rpx 24rpx;
border-radius: 999rpx;
font-size: 24rpx;
color: var(--mp-muted);
background: var(--mp-surface);
border: 1rpx solid var(--mp-border);
}
.cat-chip--active {
color: var(--mp-brand-green-dark);
background: var(--mp-brand-green-soft);
border-color: #b7e6d2;
font-weight: 600;
}
.cat-tree {
background: var(--mp-surface);
border: 1rpx solid var(--mp-border);
border-radius: var(--mp-radius-lg);
overflow: hidden;
}
.cat-row {
display: flex;
align-items: center;
gap: 12rpx;
min-height: 104rpx;
padding: 20rpx 20rpx 20rpx 16rpx;
border-bottom: 1rpx solid var(--mp-border-light);
background: #fff;
}
.cat-row:last-child {
border-bottom: none;
}
.cat-row--l1 {
background: #fbfdff;
}
.cat-row--l2 {
background: #fafcfb;
}
.cat-index {
flex-shrink: 0;
width: 44rpx;
text-align: center;
font-size: 22rpx;
font-weight: 600;
font-variant-numeric: tabular-nums;
color: var(--mp-muted);
line-height: 1;
}
.cat-row__body {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 12rpx;
}
.cat-chevron {
width: 44rpx;
height: 44rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 18rpx;
color: var(--mp-muted);
flex-shrink: 0;
transition: transform 0.15s ease;
}
.cat-chevron--open {
transform: rotate(90deg);
color: var(--mp-brand-green);
}
.cat-chevron--leaf {
visibility: hidden;
}
.cat-main {
flex: 1;
min-width: 0;
}
.cat-name {
display: block;
font-size: 28rpx;
font-weight: 600;
color: var(--mp-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cat-meta {
margin-top: 6rpx;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12rpx;
}
.cat-meta__text {
font-size: 22rpx;
color: var(--mp-muted);
}
.cat-actions {
display: flex;
gap: 8rpx;
flex-shrink: 0;
}
.cat-icon-btn {
width: 64rpx;
height: 64rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 600;
color: var(--mp-brand-green-dark);
background: var(--mp-brand-green-soft);
}
.cat-icon-btn--more {
background: #f1f5f3;
color: var(--mp-muted);
font-size: 28rpx;
font-weight: 400;
}
/* ── 更多操作 Action Sheet ── */
.cat-as-mask {
position: fixed;
inset: 0;
z-index: 1000;
background: rgba(15, 30, 25, 0.48);
display: flex;
align-items: flex-end;
animation: cat-as-fade 0.2s ease;
}
.cat-as {
width: 100%;
padding: 0 24rpx calc(20rpx + env(safe-area-inset-bottom, 0px));
animation: cat-as-up 0.28s cubic-bezier(0.32, 0.72, 0, 1);
}
.cat-as__handle {
width: 72rpx;
height: 8rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.55);
margin: 0 auto 16rpx;
}
.cat-as__head {
background: var(--mp-surface);
border-radius: 28rpx 28rpx 0 0;
padding: 28rpx 32rpx 20rpx;
border-bottom: 1rpx solid var(--mp-border-light);
}
.cat-as__eyebrow {
display: block;
font-size: 22rpx;
color: var(--mp-muted);
margin-bottom: 6rpx;
}
.cat-as__title {
display: block;
font-size: 32rpx;
font-weight: 700;
color: var(--mp-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cat-as__group {
background: var(--mp-surface);
border-radius: 0 0 28rpx 28rpx;
overflow: hidden;
margin-bottom: 16rpx;
}
.cat-as__item {
display: flex;
align-items: center;
gap: 20rpx;
min-height: 112rpx;
padding: 24rpx 28rpx;
border-bottom: 1rpx solid var(--mp-border-light);
}
.cat-as__item:last-child {
border-bottom: none;
}
.cat-as__item:active {
background: #f7fbf9;
}
.cat-as__icon {
width: 72rpx;
height: 72rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
font-weight: 700;
flex-shrink: 0;
}
.cat-as__icon--edit {
color: var(--mp-brand-green-dark);
background: var(--mp-brand-green-soft);
}
.cat-as__icon--add {
color: #087f57;
background: #e9f9f2;
}
.cat-as__icon--del {
color: #d14343;
background: #fef2f2;
}
.cat-as__meta {
flex: 1;
min-width: 0;
}
.cat-as__name {
display: block;
font-size: 30rpx;
font-weight: 600;
color: var(--mp-text);
}
.cat-as__item--danger .cat-as__name {
color: #d14343;
}
.cat-as__desc {
display: block;
margin-top: 4rpx;
font-size: 22rpx;
color: var(--mp-muted);
line-height: 1.35;
}
.cat-as__arrow {
font-size: 36rpx;
color: #c5d5cd;
flex-shrink: 0;
line-height: 1;
}
.cat-as__cancel {
background: var(--mp-surface);
border-radius: 28rpx;
min-height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
font-weight: 600;
color: var(--mp-muted);
}
.cat-as__cancel:active {
background: #f3f7f5;
}
@keyframes cat-as-fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes cat-as-up {
from {
transform: translateY(24rpx);
opacity: 0.6;
}
to {
transform: translateY(0);
opacity: 1;
}
}
</style>
+1 -4
View File
@@ -105,10 +105,7 @@ onReachBottom(() => loadList(false))
</view>
<view class="mp-section">
<view v-if="!list.length && !loading" class="mp-empty">
<view class="mp-empty__icon" />
<text class="mp-empty__text">暂无报价</text>
</view>
<mp-empty v-if="!list.length && !loading" title="暂无报价" />
<view v-else class="mp-list">
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
<view class="mp-card__head">
+1 -4
View File
@@ -107,10 +107,7 @@ onShow(() => load())
</view>
<view class="mp-section">
<view v-if="!list.length && !loading" class="mp-empty">
<view class="mp-empty__icon" />
<text class="mp-empty__text">暂无出入库单</text>
</view>
<mp-empty v-if="!list.length && !loading" title="暂无出入库单" />
<view v-else class="mp-list">
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
<view class="mp-card__head">
+2 -6
View File
@@ -100,9 +100,7 @@ onShow(() => load())
<view class="mp-block">
<text class="mp-block__title">关联出入库({{ related.inventories?.length || 0 }})</text>
<view v-if="!related.inventories?.length" class="mp-empty" style="padding: 32rpx 0">
<text class="mp-empty__text">暂无出入库单</text>
</view>
<mp-empty v-if="!related.inventories?.length" compact title="暂无出入库单" />
<view v-else class="mp-list">
<view
v-for="inv in related.inventories"
@@ -118,9 +116,7 @@ onShow(() => load())
<view class="mp-block">
<text class="mp-block__title">关联地磅单({{ related.weighings?.length || 0 }})</text>
<view v-if="!related.weighings?.length" class="mp-empty" style="padding: 32rpx 0">
<text class="mp-empty__text">暂无地磅单</text>
</view>
<mp-empty v-if="!related.weighings?.length" compact title="暂无地磅单" />
<view v-else class="mp-list">
<view
v-for="w in related.weighings"
+1 -4
View File
@@ -87,10 +87,7 @@ onShow(() => load())
</view>
<view class="mp-section">
<view v-if="!list.length && !loading" class="mp-empty">
<view class="mp-empty__icon" />
<text class="mp-empty__text">暂无计划</text>
</view>
<mp-empty v-if="!list.length && !loading" title="暂无计划" />
<view v-else class="mp-list">
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
<view class="mp-card__head">
+1 -4
View File
@@ -137,10 +137,7 @@ onShow(() => load())
</view>
<view class="mp-section">
<view v-if="!list.length && !loading" class="mp-empty">
<view class="mp-empty__icon" />
<text class="mp-empty__text">暂无地磅单</text>
</view>
<mp-empty v-if="!list.length && !loading" title="暂无地磅单" />
<view v-else class="mp-list">
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
<view class="mp-card__head">
+1 -4
View File
@@ -81,10 +81,7 @@ onShow(() => load(true))
</view>
<view class="mp-section">
<view v-if="!list.length && !loading" class="mp-empty">
<view class="mp-empty__icon" />
<text class="mp-empty__text">暂无已归档计划</text>
</view>
<mp-empty v-if="!list.length && !loading" title="暂无已归档计划" />
<view v-else class="mp-list">
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
<view class="mp-card__head">
+1 -3
View File
@@ -114,9 +114,7 @@ onShow(() => load())
<view class="mp-block">
<text class="mp-block__title">产品购买证</text>
<view v-if="!products.length" class="mp-empty" style="padding: 32rpx 0">
<text class="mp-empty__text">暂无产品明细</text>
</view>
<mp-empty v-if="!products.length" compact title="暂无产品明细" />
<view v-for="p in products" :key="p.id" class="mp-block__panel" style="margin-bottom: 24rpx">
<view class="mp-kv">
<text class="mp-kv__label">{{ dash(p.productName) }}</text>
+1 -4
View File
@@ -118,10 +118,7 @@ onShow(() => load(true))
</view>
<view class="mp-section">
<view v-if="!list.length && !loading" class="mp-empty">
<view class="mp-empty__icon" />
<text class="mp-empty__text">暂无计划</text>
</view>
<mp-empty v-if="!list.length && !loading" title="暂无计划" />
<view v-else class="mp-list">
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
<view class="mp-card__head">
+1 -4
View File
@@ -106,10 +106,7 @@ onShow(() => load(true))
</view>
<view class="mp-section">
<view v-if="!list.length && !loading" class="mp-empty">
<view class="mp-empty__icon" />
<text class="mp-empty__text">暂无审批单据</text>
</view>
<mp-empty v-if="!list.length && !loading" title="暂无审批单据" />
<view v-else class="mp-list">
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
<view class="mp-card__head">
+4 -4
View File
@@ -173,10 +173,10 @@ onShow(() => load(true))
</view>
<view class="mp-section">
<view v-if="!list.length && !loading" class="mp-empty">
<view class="mp-empty__icon" />
<text class="mp-empty__text">{{ activeTab === 'unsettled' ? '暂无待结算计划' : '暂无结算单' }}</text>
</view>
<mp-empty
v-if="!list.length && !loading"
:title="activeTab === 'unsettled' ? '暂无待结算计划' : '暂无结算单'"
/>
<view v-else class="mp-list">
<!-- 结算单 -->
<template v-if="activeTab === 'bills'">
+1 -3
View File
@@ -147,9 +147,7 @@ onShow(() => load())
<view class="mp-block">
<text class="mp-block__title">产品明细</text>
<view v-if="!products.length" class="mp-empty" style="padding: 32rpx 0">
<text class="mp-empty__text">暂无明细</text>
</view>
<mp-empty v-if="!products.length" compact title="暂无明细" />
<view v-else class="mp-block__panel">
<view v-for="p in products" :key="p.id" class="mp-kv">
<text class="mp-kv__label">{{ dash(p.productName) }}</text>
+1 -4
View File
@@ -131,10 +131,7 @@ onShow(() => load(true))
</view>
<view class="mp-section">
<view v-if="!list.length && !loading" class="mp-empty">
<view class="mp-empty__icon" />
<text class="mp-empty__text">暂无运输计划</text>
</view>
<mp-empty v-if="!list.length && !loading" title="暂无运输计划" />
<view v-else class="mp-list">
<view v-for="row in list" :key="row.id" class="mp-card">
<view class="mp-card__head" @click="goDetail(row)">