Commit 2171bcc2f380de329ad1f9f61ae6bf8ce1c1abaf
1 parent
ac0a26c9
feat: card组件+filter组件+单选组件+缺省页+客户开发列表-list功能
Showing
20 changed files
with
1440 additions
and
41 deletions
api/base.js
0 → 100644
api/devManage.js
0 → 100644
| 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 | +} | |
| \ No newline at end of file | ... | ... |
components/card/index.vue
0 → 100644
| 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> | |
| \ No newline at end of file | ... | ... |
components/empty/index.vue
0 → 100644
| 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> | |
| \ No newline at end of file | ... | ... |
components/filter/index.vue
0 → 100644
| 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> | |
| \ No newline at end of file | ... | ... |
components/single-select/index.vue
0 → 100644
| 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> | |
| \ No newline at end of file | ... | ... |
| ... | ... | @@ -17,22 +17,6 @@ |
| 17 | 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 | 20 | "path": "pages/mine/index", |
| 37 | 21 | "style": { |
| 38 | 22 | "navigationBarTitleText": "我的", |
| ... | ... | @@ -44,21 +28,11 @@ |
| 44 | 28 | "navigationBarTitleText": "我的2" |
| 45 | 29 | } |
| 46 | 30 | }, { |
| 47 | - "path": "pages/mine/avatar/index", | |
| 48 | - "style": { | |
| 49 | - "navigationBarTitleText": "修改头像" | |
| 50 | - } | |
| 51 | - }, { | |
| 52 | 31 | "path": "pages/mine/info/index", |
| 53 | 32 | "style": { |
| 54 | 33 | "navigationBarTitleText": "个人信息" |
| 55 | 34 | } |
| 56 | 35 | }, { |
| 57 | - "path": "pages/mine/info/edit", | |
| 58 | - "style": { | |
| 59 | - "navigationBarTitleText": "编辑资料" | |
| 60 | - } | |
| 61 | - }, { | |
| 62 | 36 | "path": "pages/mine/pwd/index", |
| 63 | 37 | "style": { |
| 64 | 38 | "navigationBarTitleText": "修改密码" |
| ... | ... | @@ -69,24 +43,16 @@ |
| 69 | 43 | "navigationBarTitleText": "应用设置" |
| 70 | 44 | } |
| 71 | 45 | }, { |
| 72 | - "path": "pages/mine/help/index", | |
| 73 | - "style": { | |
| 74 | - "navigationBarTitleText": "常见问题" | |
| 75 | - } | |
| 76 | - }, { | |
| 77 | 46 | "path": "pages/mine/about/index", |
| 78 | 47 | "style": { |
| 79 | 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 | 52 | "style": { |
| 89 | - "navigationBarTitleText": "浏览文本" | |
| 53 | + "navigationBarTitleText": "客户开发管理", | |
| 54 | + "navigationBarBackgroundColor": "#ffffff", | |
| 55 | + "navigationBarTextStyle": "black" | |
| 90 | 56 | } |
| 91 | 57 | } |
| 92 | 58 | ], | ... | ... |
pages/dev_manage/index.vue
0 → 100644
| 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> | |
| \ No newline at end of file | ... | ... |
static/images/dev_manage/add_icon.png
0 → 100644
349 Bytes
static/images/dev_manage/batch_icon.png
0 → 100644
1018 Bytes
static/images/dev_manage/close_icon.png
0 → 100644
835 Bytes
static/images/dev_manage/filter_icon.png
0 → 100644
669 Bytes
static/images/dev_manage/tab_1_icon.png
0 → 100644
2.17 KB
static/images/dev_manage/tab_2_icon.png
0 → 100644
2.46 KB
static/images/dev_manage/tab_3_icon.png
0 → 100644
2.16 KB
static/images/empty/empty_bg.png
0 → 100644
63 KB
| ... | ... | @@ -15,14 +15,37 @@ |
| 15 | 15 | |
| 16 | 16 | /* 页面原生导航标题字号:H5 覆盖为 18px */ |
| 17 | 17 | /* #ifdef H5 */ |
| 18 | +.uni-page-head { | |
| 19 | + height: 96rpx !important; | |
| 20 | +} | |
| 18 | 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 | 30 | /* #endif */ |
| 22 | 31 | |
| 23 | 32 | /* 组件 uni-nav-bar 标题字号统一为 18px */ |
| 24 | 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 | 51 | /* 全端覆盖 primary 按钮主色(内置 <button type="primary">) */ | ... | ... |
| ... | ... | @@ -9,7 +9,7 @@ $theme-primary: #3d48a3; |
| 9 | 9 | $theme-primary-hover: darken($theme-primary, 6%); |
| 10 | 10 | $theme-primary-active: darken($theme-primary, 10%); |
| 11 | 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 | 13 | $theme-primary-plain-border: mix(#ffffff, $theme-primary, 30%); |
| 14 | 14 | // 半透明主题色(0.5 不透明度,用于禁用态或蒙层) |
| 15 | 15 | $theme-primary-alpha-50: rgba($theme-primary, 0.5); | ... | ... |
utils/dic.js
0 → 100644
| 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 | +} | ... | ... |
utils/httpEnum.js
0 → 100644
| 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 | +}) | ... | ... |