Commit 2171bcc2f380de329ad1f9f61ae6bf8ce1c1abaf

Authored by 史婷婷
1 parent ac0a26c9

feat: card组件+filter组件+单选组件+缺省页+客户开发列表-list功能

  1 +import request from '@/utils/request'
  2 +/**
  3 + * 根据编号获取数据字段
  4 + * @param code
  5 + */
  6 +export function getDicByCodeApi(code) {
  7 + return request({
  8 + url: `/system/dic/item/getDicByCode`,
  9 + method: 'get',
  10 + params: {
  11 + code
  12 + },
  13 + })
  14 +}
  1 +import request from '@/utils/request'
  2 +import { ContentTypeEnum } from '@/utils/httpEnum';
  3 +
  4 +// 模拟字典接口,后续你可以替换为真实接口请求 utils/request.js
  5 +export function getTabsDict() {
  6 + return Promise.resolve([
  7 + { label: '全部', value: 'all' },
  8 + { label: '待办', value: 'todo' },
  9 + { label: '已办', value: 'done' }
  10 + ])
  11 +}
  12 +
  13 +export function getFactoryDict() {
  14 + return Promise.resolve([
  15 + { label: '全部', value: '' },
  16 + { label: '一、二分厂', value: '12' },
  17 + { label: '三、四分厂', value: '34' }
  18 + ])
  19 +}
  20 +
  21 +export function getDeptOptionsDict() {
  22 + return Promise.resolve([
  23 + { value: '一分厂', text: '一分厂' },
  24 + { value: '二分厂', text: '二分厂' },
  25 + { value: '三分厂', text: '三分厂' },
  26 + { value: '四分厂', text: '四分厂' },
  27 + { value: '四分厂2', text: '四分厂2' },
  28 + { value: '四分厂3', text: '四分厂3' },
  29 + { value: '四分厂4', text: '四分厂4' },
  30 + { value: '四分厂5', text: '四分厂5' }
  31 + ])
  32 +}
  33 +
  34 +export const statusOptions = [
  35 + { value: 1, text: '审核中' },
  36 + { value: 2, text: '审核通过' },
  37 + { value: 3, text: '已驳回' },
  38 + { value: 4, text: '已取消' }
  39 +];
  40 +
  41 +const baseUrl = '/customer/develop';
  42 +
  43 +// 查询列表
  44 +export function queryApi(params) {
  45 + return request({
  46 + url: `${baseUrl}/query`,
  47 + method: 'get',
  48 + params
  49 + })
  50 +}
  51 +
  52 +// 取消
  53 +export function cancelApi(id) {
  54 + return request({
  55 + url: `${baseUrl}/cancel`,
  56 + method: 'get',
  57 + params: { id }
  58 + })
  59 +}
  60 +// 根据ID查询详情数据
  61 +export function getDetailApi(id) {
  62 + return request({
  63 + url: `${baseUrl}`,
  64 + method: 'get',
  65 + params: { id }
  66 + })
  67 +}
  68 +
  69 +// 新增保存
  70 +export function createApi(params) {
  71 + return request({
  72 + url: `${baseUrl}`,
  73 + method: 'post',
  74 + data: params,
  75 + contentType: ContentTypeEnum.FORM_URLENCODED
  76 + })
  77 +}
  78 +
  79 +
  80 +// 修改保存
  81 +export function updateApi(params) {
  82 + return request({
  83 + url: `${baseUrl}`,
  84 + method: 'put',
  85 + data: params,
  86 + contentType: ContentTypeEnum.FORM_URLENCODED
  87 + })
  88 +}
  89 +
  90 +// 批量审批
  91 +export function batchApproveApi(params) {
  92 + return request({
  93 + url: `/flow/task/approve/batch`,
  94 + method: 'post',
  95 + data: params,
  96 + })
  97 +}
  98 +
  99 +// 待办类型数量统计
  100 +export function getTodoTypeStatisticsApi() {
  101 + return request({
  102 + url: `${baseUrl}/todoTypeStatistics`,
  103 + method: 'get',
  104 + })
  105 +}
  106 +
  107 +// 客户池查询
  108 +export function customerQueryApi(params) {
  109 + return request({
  110 + url: `/basedata/customer/query`,
  111 + method: 'get',
  112 + params
  113 + })
  114 +}
  115 +
  116 +// 科办查询
  117 +export function officeQueryApi(params) {
  118 + return request({
  119 + url: `/baseData/office/query`,
  120 + method: 'get',
  121 + params
  122 + })
  123 +}
  1 +<template>
  2 + <view class="card-wrapper">
  3 + <scroll-view
  4 + class="scroll"
  5 + scroll-y
  6 + :lower-threshold="lowerThreshold"
  7 + :refresher-enabled="enableRefresh"
  8 + :refresher-triggered="refresherTriggered"
  9 + @refresherrefresh="onRefresh"
  10 + @scrolltolower="onLoadMore"
  11 + @scroll="onScroll"
  12 + >
  13 + <slot name="header"></slot>
  14 +
  15 + <view v-if="hasError && !loading" class="error">
  16 + <text>加载失败</text>
  17 + <button class="retry" @click="reload">重试</button>
  18 + </view>
  19 +
  20 + <Empty v-if="items.length === 0 && !loading && !hasError" text="暂无数据" />
  21 +
  22 + <view
  23 + v-for="(item, idx) in items"
  24 + :key="getKey(item, idx)"
  25 + :class="['card-item', { 'select-item': selectable }]"
  26 + @click="toggleSelect(item, idx)"
  27 + >
  28 + <view v-if="selectable" class="check">
  29 + <view class="dot" :class="{ checked: isSelected(item, idx) }">
  30 + <uni-icons v-if="isSelected(item, idx)" type="checkmarkempty" color="#fff" size="14" />
  31 + </view>
  32 + </view>
  33 + <!-- 父组件传入的卡片内容 -->
  34 + <slot :item="item" :selected="isSelected(item, idx)"></slot>
  35 + </view>
  36 +
  37 + <!-- 当列表为空时不显示“没有更多数据了”,但加载时仍显示 loading -->
  38 + <uni-load-more v-if="items.length > 0 || loading" :status="loadMoreStatus" />
  39 + </scroll-view>
  40 + </view>
  41 +
  42 +</template>
  43 +
  44 +<script>
  45 +export default {
  46 + name: 'CardList',
  47 + components: { Empty: () => import('../empty/index.vue') },
  48 + props: {
  49 + // 请求方法,签名:({ pageIndex, pageSize, query, extra }) => Promise<{ records|list, totalCount }>
  50 + fetchFn: { type: Function, default: null },
  51 + // 筛选条件对象(会被监听,变化后自动刷新)
  52 + query: { type: Object, default: () => ({}) },
  53 + // 其他筛选值或附加参数(会被监听,变化后自动刷新)
  54 + extra: { type: Object, default: () => ({}) },
  55 + pageSize: { type: Number, default: 10 },
  56 + immediate: { type: Boolean, default: true },
  57 + enableRefresh: { type: Boolean, default: true },
  58 + enableLoadMore: { type: Boolean, default: true },
  59 + // 支持多选
  60 + selectable: { type: Boolean, default: false },
  61 + rowKey: { type: String, default: 'id' },
  62 + // v-model:selectedKeys
  63 + selectedKeys: { type: Array, default: () => [] }
  64 + },
  65 + data() {
  66 + return {
  67 + items: [],
  68 + pageIndex: 1,
  69 + totalCount: 0,
  70 + loading: false,
  71 + finished: false,
  72 + hasError: false,
  73 + refresherTriggered: false,
  74 + lowerThreshold: 120,
  75 + scrollCooldownUntil: 0,
  76 + loadMoreCooldownUntil: 0
  77 + }
  78 + },
  79 + computed: {
  80 + loadMoreStatus() {
  81 + if (this.loading) return 'loading'
  82 + if (this.finished) return 'noMore'
  83 + return 'more'
  84 + }
  85 + },
  86 + watch: {
  87 + query: {
  88 + deep: true,
  89 + handler() {
  90 + this.reload()
  91 + }
  92 + },
  93 + extra: {
  94 + deep: true,
  95 + handler() {
  96 + this.reload()
  97 + }
  98 + }
  99 + },
  100 + methods: {
  101 + getKey(item, idx) {
  102 + const k = this.rowKey && item && item[this.rowKey] != null ? item[this.rowKey] : idx
  103 + return k
  104 + },
  105 + isSelected(item, idx) {
  106 + const key = this.getKey(item, idx)
  107 + return this.selectedKeys.includes(key)
  108 + },
  109 + toggleSelect(item, idx) {
  110 + if (!this.selectable) return
  111 + const key = this.getKey(item, idx)
  112 + const next = this.selectedKeys.slice(0)
  113 + const i = next.indexOf(key)
  114 + if (i >= 0) next.splice(i, 1)
  115 + else next.push(key)
  116 + this.$emit('update:selectedKeys', next)
  117 + },
  118 + onScroll(e) {
  119 + if (!this.enableLoadMore || this.loading || this.finished) return
  120 + // 刷新后的短暂冷却期内不触发,避免误触发加载更多
  121 + if (this.scrollCooldownUntil && Date.now() < this.scrollCooldownUntil) return
  122 + const d = (e && e.detail) ? e.detail : {}
  123 + const scrollTop = Number(d.scrollTop || 0)
  124 + const scrollHeight = Number(d.scrollHeight || (e && e.target && e.target.scrollHeight) || 0)
  125 + const clientHeight = Number(d.clientHeight || (e && e.target && e.target.clientHeight) || 0)
  126 + const threshold = Number(this.lowerThreshold || 120)
  127 + const canScroll = scrollHeight > clientHeight
  128 + const nearBottom = scrollTop + clientHeight + threshold >= scrollHeight
  129 + if (canScroll && nearBottom && scrollTop > 0) {
  130 + this.onLoadMore()
  131 + }
  132 + },
  133 + async fetch() {
  134 + if (!this.fetchFn) return
  135 + this.loading = true
  136 + this.hasError = false
  137 + try {
  138 + // 捕获请求时的页码,避免并发返回顺序导致错误的拼接/覆盖
  139 + const reqPage = this.pageIndex
  140 + const res = await this.fetchFn({
  141 + pageIndex: reqPage,
  142 + pageSize: this.pageSize,
  143 + query: this.query,
  144 + extra: this.extra
  145 + })
  146 + const list = (res && (res.records || res.list || res.rows || res.items || res.datas))
  147 + ? (res.records || res.list || res.rows || res.items || res.datas)
  148 + : [];
  149 + const totalCount = (res && res.totalCount != null) ? res.totalCount : 0;
  150 + // 根据请求发起时的页码判断赋值/拼接,避免错误追加旧数据
  151 + if (reqPage === 1) this.items = list
  152 + else this.items = this.items.concat(list)
  153 + this.totalCount = totalCount
  154 + this.finished = (res && typeof res.hasNext === 'boolean')
  155 + ? !res.hasNext
  156 + : (this.items.length >= totalCount || list.length < this.pageSize)
  157 + this.$emit('loaded', { items: this.items, totalCount: this.totalCount, pageIndex: this.pageIndex })
  158 + } catch (e) {
  159 + console.error('[CardList] fetch error', e)
  160 + this.hasError = true
  161 + this.$emit('error', e)
  162 + } finally {
  163 + this.loading = false
  164 + this.refresherTriggered = false
  165 + }
  166 + },
  167 + reload() {
  168 + // 切换条件(如 tab/分厂)与刷新时,重置数据,不与之前请求数据拼接
  169 + this.pageIndex = 1
  170 + this.items = []
  171 + this.totalCount = 0
  172 + this.finished = false
  173 + // 短暂禁止触发加载更多,避免滚动到底部立即追加
  174 + this.loadMoreCooldownUntil = Date.now() + 800
  175 + this.fetch()
  176 + },
  177 + onRefresh() {
  178 + if (!this.enableRefresh) return
  179 + this.refresherTriggered = true
  180 + // 设置滚动触发冷却期,避免刷新结束立刻触发加载更多
  181 + this.scrollCooldownUntil = Date.now() + 800
  182 + // 同步禁止 scrolltolower 的加载更多
  183 + this.loadMoreCooldownUntil = Date.now() + 800
  184 + this.reload()
  185 + this.$emit('refresh')
  186 + },
  187 + onLoadMore() {
  188 + if (!this.enableLoadMore || this.loading || this.finished) return
  189 + // 刷新/重载后的短暂冷却期,避免自动触底立即追加
  190 + if (this.loadMoreCooldownUntil && Date.now() < this.loadMoreCooldownUntil) return
  191 + this.pageIndex += 1
  192 + this.fetch()
  193 + },
  194 + clear() {
  195 + this.items = []
  196 + this.totalCount = 0
  197 + this.pageIndex = 1
  198 + this.finished = false
  199 + }
  200 + },
  201 + mounted() {
  202 + if (this.immediate) this.fetch()
  203 + }
  204 +}
  205 +</script>
  206 +
  207 +<style lang="scss" scoped>
  208 +@import '../../static/scss/global.scss';
  209 +.card-wrapper {
  210 + display: flex;
  211 + flex-direction: column;
  212 + height: 100%;
  213 +}
  214 +.scroll {
  215 + flex: 1;
  216 + height: 100%;
  217 +}
  218 +.card-item {
  219 + background: #fff;
  220 + padding: 32rpx;
  221 + position: relative;
  222 + margin-bottom: 20rpx;
  223 + &.select-item {
  224 + left: 96rpx;
  225 + }
  226 +}
  227 +.check {
  228 + position: absolute;
  229 + left: -60rpx;
  230 + top: 50%;
  231 + transform: translateY(-50%);
  232 +}
  233 +.dot {
  234 + width: 32rpx;
  235 + height: 32rpx;
  236 + border-radius: 50%;
  237 + border: 2rpx solid #dcdcdc;
  238 + display: flex;
  239 + align-items: center;
  240 + justify-content: center;
  241 +}
  242 +.checked {
  243 + background: $theme-primary;
  244 + border-color: $theme-primary;
  245 +}
  246 +.error {
  247 + display: flex;
  248 + align-items: center;
  249 + justify-content: center;
  250 + gap: 20rpx;
  251 + padding: 60rpx 0;
  252 + color: #f56c6c;
  253 +}
  254 +.retry {
  255 + border: 1rpx solid #f56c6c;
  256 + color: #f56c6c;
  257 + background: #fff;
  258 +}
  259 +</style>
  1 +<template>
  2 + <view class="empty-wrap">
  3 + <image class="empty-bg" src='/static/images/empty/empty_bg.png'/>
  4 + <view class="empty-text"><text>{{ text }}</text></view>
  5 +
  6 + </view>
  7 +</template>
  8 +
  9 +<script>
  10 +export default {
  11 + name: 'Empty',
  12 + props: {
  13 + text: { type: String, default: '暂无数据' }
  14 + }
  15 +}
  16 +</script>
  17 +
  18 +<style scoped lang="scss">
  19 +.empty-wrap {
  20 + padding: 240rpx 0;
  21 + text-align: center;
  22 + .empty-text {
  23 + font-size: 26rpx;
  24 + color: rgba(0,0,0,0.4);
  25 + }
  26 + .empty-bg {
  27 + width: 440rpx;
  28 + height: 282rpx;
  29 + }
  30 +}
  31 +
  32 +</style>
  1 +<template>
  2 + <uni-popup ref="popup" type="bottom" :mask-click="false" :safe-area="true">
  3 + <view class="filter-modal">
  4 + <view class="header">
  5 + <text class="title">{{ title }}</text>
  6 + <text class="close" @click="onClose">×</text>
  7 + </view>
  8 +
  9 + <view class="body">
  10 + <!-- 父组件传入表单内容,通过作用域插槽拿到 model -->
  11 + <slot :model="innerModel"></slot>
  12 + </view>
  13 +
  14 + <view class="footer">
  15 + <button class="btn reset" @click="onReset">重置</button>
  16 + <button class="btn confirm" type="primary" @click="onConfirm">确认</button>
  17 + </view>
  18 + </view>
  19 + </uni-popup>
  20 +</template>
  21 +
  22 +<script>
  23 +export default {
  24 + name: 'FilterModal',
  25 + props: {
  26 + // v-model:visible 控制显示;初始值
  27 + visible: { type: Boolean, default: false },
  28 + // v-model:value 过滤表单对象
  29 + value: { type: Object, default: () => ({}) },
  30 + title: { type: String, default: '筛选' }
  31 + },
  32 + data() {
  33 + return {
  34 + innerModel: JSON.parse(JSON.stringify(this.value || {}))
  35 + }
  36 + },
  37 + watch: {
  38 + value: {
  39 + deep: true,
  40 + handler(v) {
  41 + this.innerModel = JSON.parse(JSON.stringify(v || {}))
  42 + }
  43 + },
  44 + visible(v) {
  45 + if (v) this.open()
  46 + else this.close()
  47 + }
  48 + },
  49 + mounted() {
  50 + if (this.visible) this.open()
  51 + },
  52 + methods: {
  53 + open() {
  54 + this.$refs.popup && this.$refs.popup.open()
  55 + this.$emit('update:visible', true)
  56 + },
  57 + close() {
  58 + this.$refs.popup && this.$refs.popup.close()
  59 + this.$emit('update:visible', false)
  60 + },
  61 + onClose() {
  62 + this.close()
  63 + this.$emit('close')
  64 + },
  65 + onReset() {
  66 + const m = this.innerModel || {}
  67 + Object.keys(m).forEach(k => {
  68 + const val = m[k]
  69 + if (Array.isArray(val)) m[k] = []
  70 + else if (val && typeof val === 'object') m[k] = null
  71 + else m[k] = ''
  72 + })
  73 + // 不关闭弹框,仅告知父组件当前值
  74 + this.$emit('reset', { ...m })
  75 + this.$emit('update:value', { ...m })
  76 + },
  77 + onConfirm() {
  78 + const payload = { ...this.innerModel }
  79 + this.$emit('confirm', payload)
  80 + this.$emit('input', payload)
  81 + this.$emit('update:value', payload)
  82 + this.close()
  83 + }
  84 + }
  85 +}
  86 +</script>
  87 +
  88 +<style lang="scss" scoped>
  89 +.filter-modal {
  90 + width: 100%;
  91 + max-height: 70vh;
  92 + min-height: 40vh;
  93 + background: #fff;
  94 + border-radius: 20rpx 20rpx 0 0;
  95 + overflow: hidden;
  96 + display: flex;
  97 + flex-direction: column;
  98 +}
  99 +.header {
  100 + flex: 0 0 auto;
  101 + display: flex;
  102 + align-items: center;
  103 + justify-content: space-between;
  104 + padding: 24rpx 28rpx;
  105 + border-bottom: 1rpx solid #f0f0f0;
  106 +}
  107 +.title {
  108 + font-size: 36rpx;
  109 + font-weight: 600;
  110 + flex: 1;
  111 + text-align: center;
  112 +}
  113 +.close {
  114 + font-size: 36rpx;
  115 + color: #666;
  116 +}
  117 +.body {
  118 + flex: 1 1 auto;
  119 + padding: 32rpx 32rpx 48rpx;
  120 + overflow-y: auto;
  121 +}
  122 +.footer {
  123 + flex: 0 0 auto;
  124 + display: flex;
  125 + gap: 32rpx;
  126 + padding: 32rpx;
  127 + border-top: 1rpx solid #f5f5f5;
  128 +}
  129 +.btn {
  130 + flex: 1;
  131 + height: 80rpx;
  132 + line-height: 80rpx;
  133 + border-radius: 12rpx;
  134 + font-size: 32rpx;
  135 +}
  136 +.reset {
  137 + background: $theme-primary-plain-bg;
  138 + color: $theme-primary;
  139 + &::after {
  140 + border: none;
  141 + }
  142 +}
  143 +.confirm {
  144 + background: $theme-primary;
  145 + color: #fff;
  146 +}
  147 +</style>
  1 +<template>
  2 + <uni-popup ref="popup" type="bottom" :mask-click="false" :safe-area="true">
  3 + <view class="sheet">
  4 + <view class="sheet-header">
  5 + <text class="cancel" @click="onCancel">取消</text>
  6 + <text class="title">{{ title }}</text>
  7 + <text class="ok" @click="onConfirm">确认</text>
  8 + </view>
  9 + <view class="sheet-body">
  10 + <view
  11 + v-for="(opt, i) in options"
  12 + :key="i"
  13 + :class="['option', { selected: isSelected(opt) }]"
  14 + @click="select(opt)"
  15 + >
  16 + <text class="label">{{ displayOf(opt) }}</text>
  17 + </view>
  18 + </view>
  19 + </view>
  20 + </uni-popup>
  21 +</template>
  22 +
  23 +<script>
  24 +export default {
  25 + name: 'SingleSelectSheet',
  26 + props: {
  27 + visible: { type: Boolean, default: false },
  28 + title: { type: String, default: '请选择' },
  29 + // 统一约定:options 为 [{ label: string, value: string|number }]
  30 + options: { type: Array, default: () => [] },
  31 + value: { type: [String, Number], default: '' }
  32 + },
  33 + data() {
  34 + return {
  35 + innerValue: this.value,
  36 + innerOption: null
  37 + }
  38 + },
  39 + watch: {
  40 + value(v) { this.innerValue = v },
  41 + visible(v) { v ? this.open() : this.close() }
  42 + },
  43 + mounted() {
  44 + if (this.visible) this.open()
  45 + },
  46 + methods: {
  47 + open() {
  48 + // 打开弹框时回显当前已选中的值
  49 + this.innerValue = this.value
  50 + this.innerOption = (this.options || []).find(o => this.valueOf(o) === this.value) || null
  51 + this.$refs.popup && this.$refs.popup.open()
  52 + this.$emit('update:visible', true)
  53 + },
  54 + close() {
  55 + // 关闭时同步回显值,避免取消后残留未确认值
  56 + this.innerValue = this.value
  57 + this.$refs.popup && this.$refs.popup.close()
  58 + this.$emit('update:visible', false)
  59 + },
  60 + displayOf(opt) {
  61 + if (!opt) return ''
  62 + return opt.label != null ? String(opt.label) : ''
  63 + },
  64 + valueOf(opt) {
  65 + if (!opt) return ''
  66 + return opt.value != null ? opt.value : ''
  67 + },
  68 + isSelected(opt) {
  69 + return this.valueOf(opt) === this.innerValue
  70 + },
  71 + select(opt) {
  72 + this.innerValue = this.valueOf(opt)
  73 + this.innerOption = opt
  74 + },
  75 + onCancel() {
  76 + this.close()
  77 + this.$emit('cancel')
  78 + },
  79 + onConfirm() {
  80 + const val = this.innerValue
  81 + const opt = this.innerOption || (this.options || []).find(o => this.valueOf(o) === val) || null
  82 + const label = opt ? this.displayOf(opt) : ''
  83 + this.$emit('confirm', { value: val, label })
  84 + this.$emit('input', val)
  85 + this.$emit('update:value', val)
  86 + this.close()
  87 + }
  88 + }
  89 +}
  90 +</script>
  91 +
  92 +<style lang="scss" scoped>
  93 +.sheet {
  94 + width: 100%;
  95 + height: 45vh;
  96 + background: #fff;
  97 + border-radius: 20rpx 20rpx 0 0;
  98 + display: flex;
  99 + flex-direction: column;
  100 +}
  101 +.sheet-header {
  102 + display: flex;
  103 + align-items: center;
  104 + justify-content: space-between;
  105 + padding: 30rpx 32rpx;
  106 + border-bottom: 1rpx solid #f0f0f0;
  107 +}
  108 +.title {
  109 + font-size: 36rpx;
  110 + font-weight: 600;
  111 +}
  112 +.cancel {
  113 + color: rgba(0,0,0,0.6);
  114 + font-size: 28rpx;
  115 +}
  116 +.ok {
  117 + color: $theme-primary;
  118 + font-size: 28rpx;
  119 +}
  120 +.sheet-body {
  121 + flex: 1 1 auto;
  122 + overflow-y: auto;
  123 + padding: 32rpx;
  124 +}
  125 +.option {
  126 + height: 80rpx;
  127 + line-height: 80rpx;
  128 + padding: 0 20rpx;
  129 + background: #fff;
  130 + text-align: center;
  131 + border-radius: 12rpx;
  132 + font-size: 32rpx;
  133 + .label {
  134 + color: rgba(0,0,0,0.6);
  135 + font-size: 32rpx;
  136 + }
  137 + &.selected {
  138 + background: #f3f3f3;
  139 + .label {
  140 + color: rgba(0,0,0,0.9);
  141 + }
  142 + }
  143 +}
  144 +
  145 +</style>
@@ -17,22 +17,6 @@ @@ -17,22 +17,6 @@
17 "navigationStyle": "custom" 17 "navigationStyle": "custom"
18 } 18 }
19 }, { 19 }, {
20 - "path": "pages/data/index",  
21 - "style": {  
22 - "navigationBarTitleText": "资料"  
23 - }  
24 - }, {  
25 - "path": "pages/business/index",  
26 - "style": {  
27 - "navigationBarTitleText": "业务"  
28 - }  
29 - }, {  
30 - "path": "pages/report/index",  
31 - "style": {  
32 - "navigationBarTitleText": "财务"  
33 -  
34 - }  
35 - }, {  
36 "path": "pages/mine/index", 20 "path": "pages/mine/index",
37 "style": { 21 "style": {
38 "navigationBarTitleText": "我的", 22 "navigationBarTitleText": "我的",
@@ -44,21 +28,11 @@ @@ -44,21 +28,11 @@
44 "navigationBarTitleText": "我的2" 28 "navigationBarTitleText": "我的2"
45 } 29 }
46 }, { 30 }, {
47 - "path": "pages/mine/avatar/index",  
48 - "style": {  
49 - "navigationBarTitleText": "修改头像"  
50 - }  
51 - }, {  
52 "path": "pages/mine/info/index", 31 "path": "pages/mine/info/index",
53 "style": { 32 "style": {
54 "navigationBarTitleText": "个人信息" 33 "navigationBarTitleText": "个人信息"
55 } 34 }
56 }, { 35 }, {
57 - "path": "pages/mine/info/edit",  
58 - "style": {  
59 - "navigationBarTitleText": "编辑资料"  
60 - }  
61 - }, {  
62 "path": "pages/mine/pwd/index", 36 "path": "pages/mine/pwd/index",
63 "style": { 37 "style": {
64 "navigationBarTitleText": "修改密码" 38 "navigationBarTitleText": "修改密码"
@@ -69,24 +43,16 @@ @@ -69,24 +43,16 @@
69 "navigationBarTitleText": "应用设置" 43 "navigationBarTitleText": "应用设置"
70 } 44 }
71 }, { 45 }, {
72 - "path": "pages/mine/help/index",  
73 - "style": {  
74 - "navigationBarTitleText": "常见问题"  
75 - }  
76 - }, {  
77 "path": "pages/mine/about/index", 46 "path": "pages/mine/about/index",
78 "style": { 47 "style": {
79 "navigationBarTitleText": "关于我们" 48 "navigationBarTitleText": "关于我们"
80 } 49 }
81 }, { 50 }, {
82 - "path": "pages/common/webview/index",  
83 - "style": {  
84 - "navigationBarTitleText": "浏览网页"  
85 - }  
86 - }, {  
87 - "path": "pages/common/textview/index", 51 + "path": "pages/dev_manage/index",
88 "style": { 52 "style": {
89 - "navigationBarTitleText": "浏览文本" 53 + "navigationBarTitleText": "客户开发管理",
  54 + "navigationBarBackgroundColor": "#ffffff",
  55 + "navigationBarTextStyle": "black"
90 } 56 }
91 } 57 }
92 ], 58 ],
  1 +<template>
  2 + <view class="page">
  3 + <view class="dev-list-fixed">
  4 + <view class="search-row">
  5 + <uni-search-bar
  6 + v-model="searchKeyword"
  7 + radius="6"
  8 + placeholder="请输入客户名称"
  9 + clearButton="auto"
  10 + cancelButton="none"
  11 + bgColor="#F3F3F3"
  12 + textColor="rgba(0,0,0,0.4)"
  13 + @confirm="search"
  14 + @input="onSearchInput"
  15 + />
  16 + <view class="tool-icons">
  17 + <image class="tool-icon"
  18 + :src="batchMode ? '/static/images/dev_manage/close_icon.png' : '/static/images/dev_manage/batch_icon.png'"
  19 + @click="toggleBatch"
  20 + />
  21 + <image class="tool-icon" src="/static/images/dev_manage/add_icon.png" @click="onAdd" />
  22 + <image class="tool-icon" src="/static/images/dev_manage/filter_icon.png" @click="openFilter" />
  23 + </view>
  24 + </view>
  25 +
  26 + <!-- 页内 tabs -->
  27 + <view class="tabs">
  28 + <view
  29 + v-for="(t, i) in tabs"
  30 + :key="i"
  31 + :class="['tab', { active: todoType === t.value }]"
  32 + @click="switchTab(t)"
  33 + >
  34 + {{ t.label }}
  35 + </view>
  36 + </view>
  37 + <view :class="['sub-tabs', `bg${currentWorkshopTypeIndex + 1}`]">
  38 + <view
  39 + v-for="(f, i) in workshopTypeList"
  40 + :key="i"
  41 + :class="['tab', { active: workshopType === f.value }]"
  42 + @click="switchWorkshopType(f, i)"
  43 + >
  44 + {{ f.label }}
  45 + </view>
  46 + </view>
  47 + </view>
  48 +
  49 +
  50 + <!-- 列表卡片组件 -->
  51 + <view :class="['list-box', { 'pad-batch': batchMode }]">
  52 + <card-list
  53 + ref="cardRef"
  54 + :fetch-fn="fetchList"
  55 + :query="query"
  56 + :extra="extraParams"
  57 + :selectable="batchMode"
  58 + row-key="id"
  59 + :selectedKeys.sync="selectedKeys"
  60 + :enable-refresh="true"
  61 + :enable-load-more="true"
  62 + @loaded="onCardLoaded"
  63 + @error="onCardError"
  64 + >
  65 + <template v-slot="{ item, selected }">
  66 + <view class="card">
  67 + <view class="card-header">
  68 + <text class="title omit2">{{ item.customer.name }}</text>
  69 + <text :class="['status', `status_${item.status}`]">{{ filterStatus(item.status) }}</text>
  70 + </view>
  71 + <view class="info-row">
  72 + <text>生产厂</text><text>{{ item.workshop.name }}</text>
  73 + </view>
  74 + <view class="info-row">
  75 + <text>科办</text><text>{{ item.office.name }}</text>
  76 + </view>
  77 + <view class="info-row">
  78 + <text>产品品种</text><text>{{ item.productVariety.name }}</text>
  79 + </view>
  80 + <view class="info-row">
  81 + <text>创建时间</text><text>{{ item.createTime }}</text>
  82 + </view>
  83 + </view>
  84 + </template>
  85 + </card-list>
  86 + </view>
  87 +
  88 + <!-- 批量操作条 -->
  89 + <view v-if="batchMode" class="batch-bar">
  90 + <button class="bbtn reject" @click="batchReject">驳回</button>
  91 + <button class="bbtn" type="primary" @click="batchPass">通过</button>
  92 + </view>
  93 +
  94 + <!-- 筛选弹框 -->
  95 + <filter-modal
  96 + :visible.sync="filterVisible"
  97 + :value.sync="filterForm"
  98 + title="筛选"
  99 + @reset="onFilterReset"
  100 + @confirm="onFilterConfirm"
  101 + >
  102 + <template v-slot="{ model }">
  103 + <view class="filter-form">
  104 + <view class="form-item">
  105 + <view class="label">审核状态</view>
  106 + <uni-data-checkbox
  107 + mode="tag"
  108 + :multiple="false"
  109 + :value-field="'value'"
  110 + :text-field="'text'"
  111 + v-model="model.status"
  112 + @change="onStatusChange"
  113 + :localdata="statusOptions"
  114 + />
  115 + </view>
  116 + <view class="form-item">
  117 + <view class="label">科办</view>
  118 + <view class="fake-select" @click="openOfficeSelect">
  119 + <text v-if="!model.officeId" class="placeholder">请选择</text>
  120 + <text v-else class="value">{{ model.officeName }}</text>
  121 + </view>
  122 + </view>
  123 + <view class="form-item">
  124 + <view class="label">创建时间</view>
  125 + <uni-datetime-picker type="daterange" v-model="model.dateRange" start="2023-01-01" />
  126 + </view>
  127 + </view>
  128 + </template>
  129 + </filter-modal>
  130 + <!-- 科办单选底部弹层(通用组件) -->
  131 + <single-select-sheet
  132 + :visible.sync="officeSelectVisible"
  133 + :options="officeList"
  134 + :value.sync="filterForm.officeId"
  135 + title="科办"
  136 + @confirm="onOfficeConfirm"
  137 + />
  138 + </view>
  139 +</template>
  140 +
  141 +<script>
  142 +import CardList from '@/components/card/index.vue'
  143 +import FilterModal from '@/components/filter/index.vue'
  144 +import SingleSelectSheet from '@/components/single-select/index.vue'
  145 +import { queryApi, officeQueryApi, statusOptions, getTodoTypeStatisticsApi } from '@/api/devManage.js'
  146 +import {getDicByCodes, getDicName} from '@/utils/dic';
  147 +
  148 +export default {
  149 + components: { CardList, FilterModal, SingleSelectSheet },
  150 + data() {
  151 + return {
  152 + searchKeyword: '',
  153 + officeList: [], // 科办列表下拉选
  154 + tabs: [],
  155 + todoType: '',
  156 + workshopTypeList: [
  157 + {
  158 + label: '全部',
  159 + value: ''
  160 + },
  161 + {
  162 + label: '一、二分厂',
  163 + value: 'TYPE_1'
  164 + },
  165 + {
  166 + label: '三、四分厂',
  167 + value: 'TYPE_2'
  168 + }
  169 + ],
  170 + workshopType: '',
  171 + currentWorkshopTypeIndex: 0,
  172 + // 字典选项缓存
  173 + // dicOptions: {
  174 + // ENTERPRISE_TYPE: [], // 客户类型
  175 + // },
  176 + // 给到 card 的筛选值
  177 + query: { status: '', officeId: '', officeName: '', dateRange: [] },
  178 + extraParams: {},
  179 +
  180 + // 批量选择
  181 + batchMode: false,
  182 + selectedKeys: [],
  183 + rowKey: 'customerId',
  184 +
  185 + // 筛选弹框
  186 + filterVisible: false,
  187 + filterForm: { status: '', officeId: '', officeName: '', dateRange: [] },
  188 + officeSelectVisible: false,
  189 + // 审核状态枚举(来自 api/devManage.js)
  190 + statusOptions,
  191 + }
  192 + },
  193 + computed: {
  194 + extraCombined() {
  195 + return {
  196 + keyword: this.searchKeyword,
  197 + todoType: this.todoType || undefined,
  198 + workshopType: this.workshopType || undefined
  199 + }
  200 + }
  201 + },
  202 + watch: {
  203 + extraCombined: {
  204 + deep: true,
  205 + handler(v) {
  206 + this.extraParams = v
  207 + },
  208 + immediate: true
  209 + }
  210 + },
  211 + created() {
  212 + this.getOffice();
  213 + // this.loadAllDicData();
  214 + // 获取待办类型数量统计
  215 + this.getTodoTypeStatisticsFun()
  216 + },
  217 + // 页面触底兜底:当页面自身滚动到底部时,转调卡片组件加载更多
  218 + onReachBottom() {
  219 + if (this.$refs && this.$refs.cardRef && this.$refs.cardRef.onLoadMore) {
  220 + this.$refs.cardRef.onLoadMore()
  221 + }
  222 + },
  223 + beforeDestroy() {
  224 + if (this.searchDebounceTimer) {
  225 + clearTimeout(this.searchDebounceTimer)
  226 + this.searchDebounceTimer = null
  227 + }
  228 + },
  229 + methods: {
  230 + onCardLoaded({ items }) {
  231 + this.currentItems = items
  232 + },
  233 + onCardError() {
  234 + uni.showToast({ title: '列表加载失败', icon: 'none' })
  235 + },
  236 + onSearch() {
  237 + this.extraParams = { ...this.extraParams, keyword: this.searchKeyword }
  238 + },
  239 + // 输入实时搜索:300ms 防抖更新关键字并触发 CardList 刷新
  240 + onSearchInput(val) {
  241 + this.searchKeyword = typeof val === 'string' ? val : (val && val.value) ? val.value : this.searchKeyword
  242 + if (this.searchDebounceTimer) clearTimeout(this.searchDebounceTimer)
  243 + this.searchDebounceTimer = setTimeout(() => {
  244 + this.extraParams = { ...this.extraParams, keyword: this.searchKeyword }
  245 + this.searchDebounceTimer = null
  246 + }, 300)
  247 + },
  248 + // uni-search-bar 确认搜索:更新关键字并触发 CardList 刷新
  249 + search(e) {
  250 + const val = e && e.value != null ? e.value : this.searchKeyword
  251 + this.searchKeyword = val
  252 + this.extraParams = { ...this.extraParams, keyword: val }
  253 + },
  254 + switchTab(item) {
  255 + this.todoType = item.value;
  256 + this.extraParams = { ...this.extraParams, todoType: item.value }
  257 + },
  258 + switchWorkshopType(item, i) {
  259 + this.workshopType = item.value;
  260 + this.currentWorkshopTypeIndex = i;
  261 + this.extraParams = { ...this.extraParams, workshopType: item.value }
  262 + },
  263 + openFilter() {
  264 + this.filterVisible = true
  265 + },
  266 + onFilterReset(payload) {
  267 + console.log('onFilterReset',payload)
  268 + // 保持弹框不关闭,仅同步表单
  269 + this.filterForm = payload
  270 + },
  271 + onFilterConfirm(payload) {
  272 + // 兜底:部分端上 uni-data-checkbox(mode=tag) 不触发 v-model
  273 + if ((payload.status === '' || payload.status == null) && this.filterForm.status !== '') {
  274 + payload.status = this.filterForm.status
  275 + }
  276 + console.log('onFilterConfirm',payload)
  277 + // 关闭弹框,父组件给到 card 的条件
  278 + this.query = { ...payload }
  279 + },
  280 + onStatusChange(e) {
  281 + const raw = e && e.detail && e.detail.value !== undefined
  282 + ? e.detail.value
  283 + : (e && e.value !== undefined ? e.value : '')
  284 + // 直接同步到外层 filterForm,驱动 FilterModal 的 innerModel 更新
  285 + this.filterForm.status = raw
  286 + },
  287 + openOfficeSelect() {
  288 + this.officeSelectVisible = true
  289 + },
  290 + onOfficeConfirm(payload) {
  291 + const val = payload && payload.value != null ? payload.value : ''
  292 + const label = payload && payload.label != null ? payload.label : ''
  293 + this.filterForm.officeId = val
  294 + this.filterForm.officeName = label
  295 + },
  296 + toggleBatch() {
  297 + this.batchMode = !this.batchMode
  298 + if (!this.batchMode) this.selectedKeys = []
  299 + },
  300 + batchReject() {
  301 + if (this.selectedKeys.length === 0) return uni.showToast({ title: '请选择记录', icon: 'none' })
  302 + uni.showToast({ title: '已发起驳回', icon: 'none' })
  303 + },
  304 + batchPass() {
  305 + if (this.selectedKeys.length === 0) return uni.showToast({ title: '请选择记录', icon: 'none' })
  306 + uni.showToast({ title: '已发起通过', icon: 'none' })
  307 + },
  308 + onAdd() {
  309 + uni.showToast({ title: '点击新增', icon: 'none' })
  310 + },
  311 + // 列表接口(真实请求)
  312 + fetchList({ pageIndex, pageSize, query, extra }) {
  313 + const params = { pageIndex, pageSize, ...extra, ...query }
  314 + // 处理日期范围
  315 + if (Array.isArray(params.dateRange) && params.dateRange.length === 2) {
  316 + params.createStartTime = params.dateRange[0] + ' 00:00:00'
  317 + params.createEndTime = params.dateRange[1] + ' 23:59:59'
  318 + delete params.dateRange
  319 + }
  320 + // 关键字
  321 + if (this.searchKeyword) {
  322 + params.keyword = this.searchKeyword
  323 + }
  324 + return queryApi(params)
  325 + .then(res => {
  326 + console.log('fetchList', res)
  327 + const _data = res.data || {};
  328 + const records = _data.datas || [];
  329 + const totalCount = _data.totalCount || 0;
  330 + const hasNext = _data.hasNext || false
  331 + return { records, totalCount, hasNext }
  332 + })
  333 + .catch(err => {
  334 + console.error('fetchList error', err)
  335 + this.onCardError()
  336 + return { records: [], totalCount: 0, hasNext: false }
  337 + })
  338 + },
  339 + getOffice() {
  340 + officeQueryApi({
  341 + pageIndex: 1,
  342 + pageSize: 9999,
  343 + }).then((res) => {
  344 + const _data = res.data.datas.map(item => {
  345 + return {
  346 + label: item.name,
  347 + value: item.id
  348 + }
  349 + }) || []
  350 + console.log('officeList_data', _data)
  351 + this.officeList = _data;
  352 + });
  353 + },
  354 + getTodoTypeStatisticsFun() {
  355 + getTodoTypeStatisticsApi().then((res) => {
  356 + this.tabs = [
  357 + {
  358 + label: `全部(${res.data.allCount > 999 ? '999+' : res.data.allCount || 0})`,
  359 + value: ''
  360 + },
  361 + {
  362 + label: `已办(${res.data.completedCount > 999 ? '999+' : res.data.completedCount || 0})`,
  363 + value: 'COMPLETED'
  364 + },
  365 + {
  366 + label: `待办(${res.data.todoCount > 999 ? '999+' : res.data.todoCount || 0})`,
  367 + value: 'WAIT'
  368 + }
  369 + ]
  370 + });
  371 + },
  372 + // 加载所有需要的字典数据
  373 + // async loadAllDicData() {
  374 + // try {
  375 + // // 批量加载所有字典数据
  376 + // const dicCodes = ['ENTERPRISE_TYPE'];
  377 + // const results = await getDicByCodes(dicCodes);
  378 + // // 更新字典选项
  379 + // this.dicOptions.ENTERPRISE_TYPE = results.ENTERPRISE_TYPE.data || [];
  380 + // } catch (error) {
  381 + // console.error('加载字典数据失败:', error);
  382 + // }
  383 + // },
  384 + filterStatus(status) {
  385 + return this.statusOptions.filter(item => item.value === status)[0].text || '';
  386 + }
  387 + }
  388 +}
  389 +</script>
  390 +
  391 +<style lang="scss" scoped>
  392 +.page {
  393 + display: flex;
  394 + flex-direction: column;
  395 + height: 100vh;
  396 +}
  397 +.dev-list-fixed {
  398 + position: fixed;
  399 + top: 96rpx;
  400 + left: 0;
  401 + right: 0;
  402 + z-index: 2;
  403 + background: #fff;
  404 +
  405 + .search-row {
  406 + display: flex;
  407 + align-items: center;
  408 + padding: 16rpx 32rpx;
  409 + .uni-searchbar {
  410 + padding: 0;
  411 + flex: 1;
  412 + }
  413 + .tool-icons {
  414 + display: flex;
  415 + .tool-icon {
  416 + width: 48rpx; height: 48rpx; display: block;
  417 + margin-left: 32rpx;
  418 + }
  419 + }
  420 + }
  421 +
  422 + .tabs {
  423 + display: flex;
  424 + align-items: center;
  425 + justify-content: space-between;
  426 + padding: 26rpx 0;
  427 + border-bottom: solid 1px #E7E7E7;
  428 + .tab {
  429 + width: 180rpx;
  430 + text-align: center;
  431 + color: #666;
  432 + color: rgba(0,0,0,0.9);
  433 + line-height: 44rpx;
  434 + position: relative;
  435 + }
  436 + .tab.active { color: $theme-primary; font-weight: 600; }
  437 + .tab.active::after {
  438 + content: '';
  439 + position: absolute;
  440 + left: 50%;
  441 + transform: translateX(-50%);
  442 + bottom: -13px;
  443 + width: 32rpx;
  444 + height: 6rpx;
  445 + border-radius: 4rpx;
  446 + background: $theme-primary;
  447 + }
  448 + }
  449 + .sub-tabs {
  450 + display: flex;
  451 + align-items: center;
  452 + height: 96rpx;
  453 + &.bg1 {
  454 + background-image: url('/static/images/dev_manage/tab_1_icon.png');
  455 + background-repeat: no-repeat;
  456 + background-position: right center;
  457 + background-size: cover;
  458 + }
  459 + &.bg2 {
  460 + background-image: url('/static/images/dev_manage/tab_2_icon.png');
  461 + background-repeat: no-repeat;
  462 + background-position: right center;
  463 + background-size: cover;
  464 + }
  465 + &.bg3 {
  466 + background-image: url('/static/images/dev_manage/tab_3_icon.png');
  467 + background-repeat: no-repeat;
  468 + background-position: right center;
  469 + background-size: cover;
  470 + }
  471 + .tab {
  472 + width: 50%;
  473 + height: 96rpx;
  474 + line-height: 96rpx;
  475 + text-align: center;
  476 + font-weight: 600;
  477 + color: rgba(0,0,0,0.9);
  478 + &.active {
  479 + color: $theme-primary;
  480 + }
  481 + }
  482 + }
  483 +}
  484 +/* 仅当前页覆盖 uni-search-bar 盒子高度 */
  485 +::v-deep .uni-searchbar__box {
  486 + height: 80rpx !important;
  487 + justify-content: start;
  488 + .uni-searchbar__box-search-input {
  489 + font-size: 32rpx !important;
  490 + }
  491 +}
  492 +
  493 +.list-box {
  494 + flex: 1;
  495 + padding-top: 314rpx;
  496 + &.pad-batch { padding-bottom: 144rpx; }
  497 + .card { position: relative; }
  498 + .card-header {
  499 + margin-bottom: 28rpx;
  500 + position: relative;
  501 +
  502 + .title {
  503 + font-size: 36rpx;
  504 + font-weight: 600;
  505 + line-height: 50rpx;
  506 + color: #323241;
  507 + width: 578rpx;
  508 + }
  509 +
  510 + .status {
  511 + font-size: 30rpx;
  512 + font-weight: 600;
  513 + position: absolute;
  514 + top: -36rpx;
  515 + right: -32rpx;
  516 + height: 48rpx;
  517 + line-height: 48rpx;
  518 + color: #fff;
  519 + font-size: 24rpx;
  520 + padding: 0 14rpx;
  521 + border-radius: 6rpx;
  522 + &.status_1 {
  523 + background: #3D48A3;
  524 + }
  525 + &.status_2 {
  526 + background: #2BA471;
  527 + }
  528 + &.status_3 {
  529 + background: #D54941;
  530 + }
  531 + &.status_4 {
  532 + background: #E7E7E7;
  533 + color: #323241;
  534 + }
  535 + }
  536 + }
  537 + .info-row {
  538 + display: flex;
  539 + align-items: center;
  540 + color: rgba(0,0,0,0.6);
  541 + font-size: 28rpx;
  542 + margin-bottom: 24rpx;
  543 + height: 32rpx;
  544 + &:last-child {
  545 + margin-bottom: 0;
  546 + }
  547 + text {
  548 + width: 60%;
  549 + &:last-child {
  550 + color: rgba(0,0,0,0.9);
  551 + width: 40%;
  552 + }
  553 + }
  554 + }
  555 +}
  556 +
  557 +.batch-bar {
  558 + position: fixed;
  559 + left: 0;
  560 + right: 0;
  561 + bottom: 0;
  562 + display: flex;
  563 + gap: 36rpx;
  564 + padding: 32rpx 30rpx;
  565 + background: #fff;
  566 + box-shadow: 0 -4rpx 12rpx rgba(0,0,0,0.04);
  567 + .bbtn {
  568 + flex: 1;
  569 + height: 80rpx;
  570 + line-height: 80rpx;
  571 + border-radius: 12rpx;
  572 + font-size: 32rpx;
  573 + display: flex;
  574 + align-items: center;
  575 + justify-content: center;
  576 + }
  577 + .reject {
  578 + background: #fff;
  579 + border: 1rpx solid $uni-color-error;
  580 + color: $uni-color-error;
  581 + }
  582 +}
  583 +
  584 +.filter-form {
  585 + .form-item { margin-bottom: 24rpx; }
  586 + .label {
  587 + margin-bottom: 20rpx;
  588 + color: rgba(0,0,0,0.9);
  589 + height: 44rpx;
  590 + line-height: 44rpx;
  591 + font-size: 30rpx;
  592 + }
  593 + .fake-select {
  594 + height: 80rpx; line-height: 80rpx; padding: 0 20rpx; background: #f3f3f3; border-radius: 12rpx;
  595 + .placeholder { color: #999; }
  596 + .value { color: #333; }
  597 + }
  598 +}
  599 +
  600 +/* 深度覆盖 uni-data-checkbox(mode=tag)内部的 tag 展示与间距 */
  601 +::v-deep .filter-form .uni-data-checklist .checklist-group {
  602 + .checklist-box {
  603 + &.is--tag {
  604 + width: 212rpx;
  605 + margin-top: 0;
  606 + margin-bottom: 24rpx;
  607 + margin-right: 24rpx;
  608 + height: 80rpx;
  609 + padding: 0;
  610 + border-radius: 12rpx;
  611 + background-color: #f3f3f3;
  612 + border-color: #f3f3f3;
  613 + &:nth-child(3n) {
  614 + margin-right: 0;
  615 + }
  616 + .checklist-content {
  617 + display: flex;
  618 + justify-content: center;
  619 + }
  620 + .checklist-text {
  621 + color: rgba(0,0,0,0.9);
  622 + font-size: 28rpx;
  623 + }
  624 + }
  625 + &.is-checked {
  626 + background-color: $theme-primary-plain-bg !important;
  627 + border-color: $theme-primary-plain-bg !important;
  628 + .checklist-text {
  629 + color: $theme-primary !important;
  630 + }
  631 + }
  632 + }
  633 +
  634 +}
  635 +
  636 +</style>
@@ -15,14 +15,37 @@ @@ -15,14 +15,37 @@
15 15
16 /* 页面原生导航标题字号:H5 覆盖为 18px */ 16 /* 页面原生导航标题字号:H5 覆盖为 18px */
17 /* #ifdef H5 */ 17 /* #ifdef H5 */
  18 +.uni-page-head {
  19 + height: 96rpx !important;
  20 +}
18 .uni-page-head .uni-page-head-title { 21 .uni-page-head .uni-page-head-title {
19 - font-size: 18px !important; 22 + font-size: 36rpx !important;
  23 + line-height: 96rpx !important;
  24 + color: rgba(0,0,0,0.9) !important;
  25 +}
  26 +.uni-page-head .uni-page-head-btn .uni-icons,
  27 +.uni-page-head .uni-page-head-btn .uniui-back {
  28 + color: rgba(0,0,0,0.9) !important;
20 } 29 }
21 /* #endif */ 30 /* #endif */
22 31
23 /* 组件 uni-nav-bar 标题字号统一为 18px */ 32 /* 组件 uni-nav-bar 标题字号统一为 18px */
24 .uni-nav-bar-text { 33 .uni-nav-bar-text {
25 - font-size: 18px !important; 34 + font-size: 36rpx !important;
  35 +}
  36 +
  37 +/* 全局导航栏背景采用主题色(含内容容器与头部),并统一文字/图标为白色 */
  38 +.uni-navbar__content,
  39 +.uni-navbar__header {
  40 + background-color: $theme-primary !important;
  41 +}
  42 +.uni-navbar--border {
  43 + border-bottom-color: $theme-primary-plain-border !important;
  44 +}
  45 +.uni-navbar__header .uni-nav-bar-text,
  46 +.uni-navbar__header .uni-navbar-btn-text text,
  47 +.uni-navbar__header .uni-icons {
  48 + color: #ffffff !important;
26 } 49 }
27 50
28 /* 全端覆盖 primary 按钮主色(内置 <button type="primary">) */ 51 /* 全端覆盖 primary 按钮主色(内置 <button type="primary">) */
@@ -9,7 +9,7 @@ $theme-primary: #3d48a3; @@ -9,7 +9,7 @@ $theme-primary: #3d48a3;
9 $theme-primary-hover: darken($theme-primary, 6%); 9 $theme-primary-hover: darken($theme-primary, 6%);
10 $theme-primary-active: darken($theme-primary, 10%); 10 $theme-primary-active: darken($theme-primary, 10%);
11 $theme-primary-disabled: mix(#ffffff, $theme-primary, 60%); 11 $theme-primary-disabled: mix(#ffffff, $theme-primary, 60%);
12 -$theme-primary-plain-bg: mix(#ffffff, $theme-primary, 92%); 12 +$theme-primary-plain-bg: mix(#ffffff, $theme-primary, 88%);
13 $theme-primary-plain-border: mix(#ffffff, $theme-primary, 30%); 13 $theme-primary-plain-border: mix(#ffffff, $theme-primary, 30%);
14 // 半透明主题色(0.5 不透明度,用于禁用态或蒙层) 14 // 半透明主题色(0.5 不透明度,用于禁用态或蒙层)
15 $theme-primary-alpha-50: rgba($theme-primary, 0.5); 15 $theme-primary-alpha-50: rgba($theme-primary, 0.5);
  1 +import { getDicByCodeApi } from '@/api/base.js';
  2 +
  3 +/**
  4 + * 获取单个字典数据(无缓存版本)
  5 + * @param {string} code
  6 + * @returns {Promise<Array>}
  7 + */
  8 +export function getDicByCode(code) {
  9 + return getDicByCodeApi(code);
  10 +}
  11 +
  12 +/**
  13 + * 批量获取多个字典数据(无缓存版本)
  14 + * @param {string[]} codes
  15 + * @returns {Promise<Object>}
  16 + */
  17 +export function getDicByCodes(codes) {
  18 + const list = Array.isArray(codes) ? codes : [];
  19 + const promises = list.map(code => getDicByCodeApi(code));
  20 + return Promise.all(promises).then(results => {
  21 + const result = {};
  22 + list.forEach((code, index) => {
  23 + result[code] = results[index] || [];
  24 + });
  25 + return result;
  26 + });
  27 +}
  28 +
  29 +/**
  30 + * 获取字典项的名称(需要传入字典项数组)
  31 + * @param {string} code 字典编码(仅用于标识,实际不使用)
  32 + * @param {string} value 字典值
  33 + * @param {Array} items 字典项数组
  34 + * @returns {string}
  35 + */
  36 +export function getDicName(code, value, items = []) {
  37 + const list = Array.isArray(items) ? items : [];
  38 + const item = list.find(it => it && it.code === value);
  39 + return item ? item.name : value;
  40 +}
  1 +/**
  2 + * 内容类型枚举(纯 JS 兼容导出)
  3 + * 说明:原文件使用了 TypeScript 的 enum 语法,但当前文件为 .js。
  4 + * 为避免在不同构建目标(H5/小程序/APP)下因语法不兼容导致构建或运行问题,
  5 + * 改为导出只读对象,保持同名导出与调用方式不变。
  6 + */
  7 +export const ContentTypeEnum = Object.freeze({
  8 + // JSON
  9 + JSON: 'application/json;charset=UTF-8',
  10 + // 表单(urlencoded)
  11 + FORM_URLENCODED: 'application/x-www-form-urlencoded;charset=UTF-8',
  12 + // 文件上传(multipart/form-data)
  13 + BLOB: 'multipart/form-data;charset=UTF-8'
  14 +})