From 2078923deb07868fc3fdb967c59b5e043c1f0297 Mon Sep 17 00:00:00 2001 From: zhaichao Date: Thu, 24 Sep 2026 09:54:39 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E5=8F=B0=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/chat/chat.vue | 34 +++++++++---- pages/tabs/workbench.vue | 31 +++++++----- utils/catalog.js | 22 ++++----- utils/nav.js | 101 ++++++++++++++++++++++++++++++++------- utils/store.js | 21 ++++++-- 5 files changed, 157 insertions(+), 52 deletions(-) diff --git a/pages/chat/chat.vue b/pages/chat/chat.vue index 6a1af28..1ec87bd 100644 --- a/pages/chat/chat.vue +++ b/pages/chat/chat.vue @@ -57,7 +57,7 @@ {{ m.text }} - {{ readLabel(m) }} + {{ readLabel(m) }} @@ -241,12 +241,14 @@ export default { try { this.safeBottom = Number(uni.getSystemInfoSync().safeAreaInsets && uni.getSystemInfoSync().safeAreaInsets.bottom) || 0 } catch (_) {} - this._rec = uni.getRecorderManager() - this._rec.onStop((res) => this.afterRecord(res)) - this._rec.onError(() => { - this.recording = false - toast('录音失败,请检查麦克风权限') - }) + if (typeof uni.getRecorderManager === 'function') { + this._rec = uni.getRecorderManager() + this._rec.onStop((res) => this.afterRecord(res)) + this._rec.onError(() => { + this.recording = false + toast('录音失败,请检查麦克风权限') + }) + } }, onShow() { this.bindKb() @@ -289,7 +291,7 @@ export default { } this.scrollEnd() } - uni.onKeyboardHeightChange(this._kb) + if (typeof uni.onKeyboardHeightChange === 'function') uni.onKeyboardHeightChange(this._kb) }, unbindKb() { if (this._kb) { @@ -338,10 +340,21 @@ export default { if (this.isGroup && m.readReceipt) { const r = (m.readReceipt.read || []).length const u = (m.readReceipt.unread || []).length + if (!r && !u) return m.read ? '已读' : '未读' return '已读 ' + r + '/' + (r + u) } return m.read ? '已读' : '未读' }, + readDone(m) { + if (!m || !m.mine || m.recalled) return false + if (this.isGroup && m.readReceipt) { + const r = (m.readReceipt.read || []).length + const u = (m.readReceipt.unread || []).length + if (!r && !u) return !!m.read + return u === 0 + } + return !!m.read + }, showRead(m) { if (!this.isGroup || !m || !m.readReceipt) return const r = (m.readReceipt.read || []).map((p) => p.name || p).join('、') || '暂无' @@ -434,6 +447,11 @@ export default { this.recSec = Math.floor((Date.now() - this._recAt) / 1000) }, 300) this.bindRecDoc() + if (!this._rec) { + this.unbindRecDoc() + this.recording = false + return + } try { this._rec.start({ format: 'mp3', duration: 60000, sampleRate: 16000 }) } catch (_) { diff --git a/pages/tabs/workbench.vue b/pages/tabs/workbench.vue index 23b8627..f823b20 100644 --- a/pages/tabs/workbench.vue +++ b/pages/tabs/workbench.vue @@ -40,7 +40,9 @@ 暂无可用审批流,请确认账号已开通申请权限 - {{ mark(a.label) }} + + + {{ a.label }} @@ -52,8 +54,8 @@ - - {{ mark(a.title) }} + + {{ todoCount }} {{ a.title }} @@ -66,7 +68,9 @@ - {{ mark(m.title) }} + + + {{ m.title }} @@ -101,7 +105,7 @@ import { store } from '../../utils/store.js' import { toast } from '../../utils/format.js' import { oaPost } from '../../utils/api.js' import { visibleApplyKinds } from '../../utils/apply.js' -import { openHref, tileEmoji } from '../../utils/nav.js' +import { openHref, tileVisual } from '../../utils/nav.js' export default { components: { FyHeader, FyScroll }, @@ -121,7 +125,7 @@ export default { label: s.label || s.title || '入口', desc: s.desc || '', to: s.to || s.path || s.url || '', - emoji: tileEmoji(s) + emoji: tileVisual(s).name })).filter((s) => s.to) if (fromApi.length) return fromApi const me = store.me || {} @@ -135,7 +139,7 @@ export default { label: k.title, desc: k.hint, to: '/pages/apply/form?kind=' + k.kind, - emoji: tileEmoji({ id: k.kind, title: k.title }) + emoji: tileVisual({ id: k.kind, title: k.title }).name })) }, pending() { @@ -189,7 +193,9 @@ export default { else toast('没有发起申请权限') }, goTodo() { uni.navigateTo({ url: '/pages/todo/list' }) }, - mark(s) { return String(s || '应').replace(/\s/g, '').slice(0, 1) }, + tileOf(item) { + return tileVisual(item || {}) + }, pendKey(t) { return t.no || t.id || t.to || t.title }, openPending(t) { if (t.source === 'work' || t.taskId) { @@ -239,19 +245,18 @@ export default { .grid { display: flex; flex-wrap: wrap; } .app { width: 25%; display: flex; flex-direction: column; align-items: center; padding: 16rpx 4rpx; box-sizing: border-box; } .icon { - width: 88rpx; height: 88rpx; border-radius: 24rpx; background: #F3F6FB; + width: 88rpx; height: 88rpx; border-radius: 24rpx; background: #E8F1FF; display: flex; align-items: center; justify-content: center; position: relative; margin-bottom: 10rpx; } -.mark { font-size: 30rpx; font-weight: 650; color: #1677FF; } .abs { position: absolute; top: -8rpx; right: -8rpx; } .al { font-size: 24rpx; text-align: center; color: #111827; line-height: 1.35; } .mod { display: flex; align-items: center; padding: 22rpx 16rpx; } .mod + .mod { border-top: 1rpx solid #F0F2F5; } .mi { - width: 64rpx; height: 64rpx; border-radius: 16rpx; background: #F3F6FB; - color: #1677FF; display: flex; align-items: center; justify-content: center; - font-weight: 650; margin-right: 16rpx; + width: 64rpx; height: 64rpx; border-radius: 16rpx; background: #E8F1FF; + display: flex; align-items: center; justify-content: center; + margin-right: 16rpx; } .mt { flex: 1; font-size: 28rpx; } .todo { display: flex; align-items: center; padding: 24rpx 24rpx; margin-bottom: 16rpx; } diff --git a/utils/catalog.js b/utils/catalog.js index 8c02a1a..ae1f6d9 100644 --- a/utils/catalog.js +++ b/utils/catalog.js @@ -8,25 +8,25 @@ export const PERSONAL_APPS = [ { id: 'todo', title: '待办审批', icon: '☑', menu: 'personal', action: 'approve', url: '/pages/todo/list', badge: 'todo' }, { id: 'apply', title: '发起申请', icon: '✎', menu: 'personal', action: 'apply', url: '/pages/apply/list' }, { id: 'expense', title: '我的报销', icon: '🧾', menu: 'personal', action: 'view', url: '/pages/bills/list?type=expense&title=我的报销' }, - { id: 'loan', title: '我的借支', icon: '¥', menu: 'personal', action: 'view', url: '/pages/bills/list?type=loan&title=我的借支' }, - { id: 'report', title: '工作汇报', icon: '▤', menu: 'personal', action: 'report', url: '/pages/reports/list' }, + { id: 'loan', title: '我的借支', icon: '💰', menu: 'personal', action: 'view', url: '/pages/bills/list?type=loan&title=我的借支' }, + { id: 'report', title: '工作汇报', icon: '📝', menu: 'personal', action: 'report', url: '/pages/reports/list' }, { id: 'salary', title: '工资条', icon: '💳', menu: 'personal', action: 'view', url: '/pages/me/payslip' }, - { id: 'perf', title: '我的绩效', icon: '↗', menu: 'personal', action: 'view', url: '/pages/me/performance' }, + { id: 'perf', title: '我的绩效', icon: '📈', menu: 'personal', action: 'view', url: '/pages/me/performance' }, { id: 'archive', title: '我的资料', icon: '👤', menu: 'personal', action: 'view', url: '/pages/me/archive' }, { id: 'notice', title: '公告', icon: '📢', menu: 'personal', action: 'view', url: '/pages/notice/list' }, { id: 'coop', title: '工作协同', icon: '🤝', menu: 'personal', action: 'view', url: '/pages/coop/list' }, - { id: 'seals', title: '我的用章', icon: '©', menu: 'personal', action: 'view', url: '/pages/bills/list?type=seal&title=我的用章' }, + { id: 'seals', title: '我的用章', icon: '🔖', menu: 'personal', action: 'view', url: '/pages/bills/list?type=seal&title=我的用章' }, { id: 'contacts', title: '通讯录', icon: '☎', menu: 'personal', action: 'view', url: '/pages/tabs/contacts' } ] export const ERP_MODULES = [ - { id: 'bidding', title: '投标管理', icon: '◎', menu: 'bidding', action: 'view', url: '/pages/erp/hub?id=bidding' }, - { id: 'project', title: '项目管理', icon: '▣', menu: 'project', action: 'view', url: '/pages/erp/hub?id=project' }, - { id: 'finance', title: '财务管理', icon: '◈', menu: 'finance', action: 'view', url: '/pages/erp/hub?id=finance' }, - { id: 'hr', title: '人事管理', icon: '☰', menu: 'hr', action: 'view', url: '/pages/erp/hub?id=hr' }, - { id: 'contracts', title: '合同管理', icon: '▤', menu: 'contracts', action: 'view', url: '/pages/erp/hub?id=contracts' }, - { id: 'seals', title: '证照用章', icon: '◆', menu: 'seals', action: 'view', url: '/pages/erp/hub?id=seals' }, - { id: 'crm', title: '客户管理', icon: '◇', menu: 'crm', action: 'view', url: '/pages/erp/hub?id=crm' }, + { id: 'bidding', title: '投标管理', icon: '🎯', menu: 'bidding', action: 'view', url: '/pages/erp/hub?id=bidding' }, + { id: 'project', title: '项目管理', icon: '📁', menu: 'project', action: 'view', url: '/pages/erp/hub?id=project' }, + { id: 'finance', title: '财务管理', icon: '💹', menu: 'finance', action: 'view', url: '/pages/erp/hub?id=finance' }, + { id: 'hr', title: '人事管理', icon: '👥', menu: 'hr', action: 'view', url: '/pages/erp/hub?id=hr' }, + { id: 'contracts', title: '合同管理', icon: '📄', menu: 'contracts', action: 'view', url: '/pages/erp/hub?id=contracts' }, + { id: 'seals', title: '证照用章', icon: '🔏', menu: 'seals', action: 'view', url: '/pages/erp/hub?id=seals' }, + { id: 'crm', title: '客户管理', icon: '🏢', menu: 'crm', action: 'view', url: '/pages/erp/hub?id=crm' }, { id: 'system', title: '系统管理', icon: '⚙', menu: 'system', action: 'view', url: '/pages/erp/hub?id=system' } ] diff --git a/utils/nav.js b/utils/nav.js index c05d122..624838e 100644 --- a/utils/nav.js +++ b/utils/nav.js @@ -63,23 +63,92 @@ export function openHref(webPath, extra = {}) { return true } -const EMOJI = { - expense: '🧾', loan: '¥', leave: '休', trip: '✈', procurement: '购', - overtime: '加', outing: '出', card: '补', transfer: '转', regularize: '正', - resign: '离', layoff: '裁', seal: '章', receive: '领', clock: '📍', - todo: '☑', apply: '✎', report: '▤', salary: '💳', perf: '↗', - archive: '👤', notice: '📢', coop: '🤝', contacts: '☎', - bidding: '◎', project: '▣', finance: '◈', hr: '☰', contracts: '▤', - seals: '◆', crm: '◇', system: '⚙' +const BLUE = ['#1677FF', '#E8F1FF'] +const CYAN = ['#0891B2', '#E6F7FB'] +const GREEN = ['#16A34A', '#E8F8EF'] +const ORANGE = ['#EA580C', '#FFF1E8'] +const AMBER = ['#D97706', '#FFF6E8'] +const VIOLET = ['#7C3AED', '#F3EEFF'] +const ROSE = ['#E11D48', '#FFE8EE'] +const TEAL = ['#0D9488', '#E6F7F4'] + +function tile(name, pair) { + return { name, color: pair[0], bg: pair[1] } +} + +const TILES = { + expense: tile('money-circle', ORANGE), + loan: tile('wallet', AMBER), + leave: tile('calendar', GREEN), + trip: tile('location', BLUE), + procurement: tile('cart', ORANGE), + overtime: tile('time', AMBER), + outing: tile('location', CYAN), + card: tile('edit-1', ROSE), + transfer: tile('swap', VIOLET), + regularize: tile('check-circle', GREEN), + resign: tile('logout', ROSE), + layoff: tile('user-clear', ROSE), + seal: tile('secured', AMBER), + receive: tile('goods', TEAL), + clock: tile('location', BLUE), + todo: tile('check-circle', GREEN), + apply: tile('edit', ORANGE), + report: tile('note', VIOLET), + salary: tile('creditcard', GREEN), + perf: tile('chart-bar', BLUE), + archive: tile('user', CYAN), + notice: tile('notification', ROSE), + coop: tile('usergroup', TEAL), + contacts: tile('call', BLUE), + bidding: tile('flag', ORANGE), + project: tile('folder', BLUE), + finance: tile('chart-pie', GREEN), + hr: tile('usergroup', CYAN), + contracts: tile('file', VIOLET), + seals: tile('secured', VIOLET), + crm: tile('shop', TEAL), + system: tile('setting', BLUE) +} + +const LABEL_TILES = [ + ['报销', 'expense'], ['借支', 'loan'], ['请假', 'leave'], ['出差', 'trip'], ['采购', 'procurement'], + ['加班', 'overtime'], ['外出', 'outing'], ['补卡', 'card'], ['转岗', 'transfer'], ['转正', 'regularize'], + ['离职', 'resign'], ['裁员', 'layoff'], ['用章', 'seal'], ['领用', 'receive'], ['打卡', 'clock'], + ['考勤', 'clock'], ['待办', 'todo'], ['审批', 'todo'], ['申请', 'apply'], ['汇报', 'report'], + ['工资', 'salary'], ['绩效', 'perf'], ['资料', 'archive'], ['公告', 'notice'], ['协同', 'coop'], + ['通讯', 'contacts'], ['投标', 'bidding'], ['项目', 'project'], ['财务', 'finance'], ['人事', 'hr'], + ['合同', 'contracts'], ['证照', 'seals'], ['印章', 'seals'], ['客户', 'crm'], ['系统', 'system'] +] + +const FALLBACK = tile('app', BLUE) + +function applyKind(to) { + const q = to.indexOf('kind=') + if (q >= 0) { + const rest = to.slice(q + 5) + const end = rest.indexOf('&') + return decodeURIComponent(end >= 0 ? rest.slice(0, end) : rest) + } + const mark = '/apply/' + const p = to.indexOf(mark) + if (p < 0) return '' + const rest = to.slice(p + mark.length) + const cut = rest.search(/[?#]/) + return decodeURIComponent(cut >= 0 ? rest.slice(0, cut) : rest) +} + +export function tileVisual(item) { + const to = String((item && (item.to || item.url || item.path)) || '') + const kind = applyKind(to) + if (kind && TILES[kind]) return TILES[kind] + const id = String((item && (item.id || item.kind || item.key)) || '') + if (TILES[id]) return TILES[id] + const label = String((item && (item.label || item.title)) || '') + const hit = LABEL_TILES.find(([k]) => label.includes(k)) + return hit && TILES[hit[1]] ? TILES[hit[1]] : FALLBACK } export function tileEmoji(item) { - if (item && item.emoji) return item.emoji - const to = String((item && (item.to || item.url || item.path)) || '') - const m = to.match(/\/apply\/([^/?]+)/) - if (m && EMOJI[m[1]]) return EMOJI[m[1]] - const id = String((item && (item.id || item.kind || item.key)) || '') - if (EMOJI[id]) return EMOJI[id] - const label = String((item && (item.label || item.title)) || '') - return label ? label.slice(0, 1) : '·' + return tileVisual(item).name } diff --git a/utils/store.js b/utils/store.js index a707037..b4ffe27 100644 --- a/utils/store.js +++ b/utils/store.js @@ -73,7 +73,7 @@ function mapMsg(raw, meId, typeu) { href: String((raw && raw.href) || p.href || ''), duration: Number((raw && raw.duration) || p.duration) || 0, quote: p.quote || raw.quote || null, - read: !!(raw && (raw.read === true || raw.readAt || raw.isRead)), + read: !!(raw && (raw.read === true || raw.read === 1 || raw.read === '1' || raw.readAt || raw.isRead === true || raw.isRead === 1)), readReceipt: (raw && raw.readReceipt) || p.readReceipt || null } } @@ -394,8 +394,14 @@ export const store = { return } if (typeu === ImTypeu.read) { - const sid = String(data.sessionId || pack.to || '') - const conv = this.findConv(sid) || this.findConv(this.dmIdOf(pack.from)) + const from = String(pack.from || data.from || '') + const me = this.me ? String(this.me.id) : '' + if (!from || from === me) { + this.emit() + return + } + const sid = String(data.sessionId || '') || this.dmIdOf(from) + const conv = this.findConv(sid) if (conv) { ;(conv.messages || []).forEach((m) => { if (m.mine && !m.recalled) m.read = true @@ -472,7 +478,14 @@ export const store = { this.activeSid = conv.id try { const rows = await api.messages(conv.id, this.token) - const incoming = asList(rows && rows.messages ? rows.messages : rows).map((m) => mapMsg(m, this.me && this.me.id, m.typeu)) + const prev = new Map((conv.messages || []).filter((m) => m.id).map((m) => [m.id, m])) + const incoming = asList(rows && rows.messages ? rows.messages : rows).map((m) => { + const msg = mapMsg(m, this.me && this.me.id, m.typeu) + const old = prev.get(msg.id) + if (old && old.read) msg.read = true + if (old && old.readReceipt && !msg.readReceipt) msg.readReceipt = old.readReceipt + return msg + }) const seen = new Set(incoming.map((m) => m.id).filter(Boolean)) const extras = (conv.messages || []).filter((m) => m.mine && m.id && !seen.has(m.id) && Date.now() - Number(m.time) < 120000) conv.messages = incoming.concat(extras)