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)