Compare commits
33 Commits
76eea1adbe
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| ff5747c21b | |||
| b9db7d7f5c | |||
| 4c896c69fc | |||
| 7f2a02d6ca | |||
| 94024faba9 | |||
| df04d0f4ba | |||
| 79499a5de5 | |||
| b6b999e47a | |||
| 1f93f4cd91 | |||
| 0a1a312418 | |||
| 6e8bb2e0a0 | |||
| fb312af2fc | |||
| 471cfe6188 | |||
| 892acc6d23 | |||
| 5e77d4fdb1 | |||
| 365bba0555 | |||
| 403f7e835f | |||
| bef7f00970 | |||
| 30578fcf6c | |||
| 75b8414605 | |||
| af8e8c4623 | |||
| 5e6e0b5648 | |||
| 87a8700b30 | |||
| bf6396089d | |||
| 2078923deb | |||
| e899b30562 | |||
| 74bdeb7a11 | |||
| bfec3f99b1 | |||
| 6ffd33bf56 | |||
| c5245b0441 | |||
| 9ad38418c0 | |||
| b008a414f0 | |||
| 5a5e51b821 |
@@ -51,7 +51,7 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.hd {
|
||||
background: rgba(245, 247, 250, 0.94);
|
||||
background: #F5F7FA;
|
||||
border-bottom: 1rpx solid rgba(17, 24, 39, 0.06);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
<template>
|
||||
<wd-tabbar
|
||||
v-model="active"
|
||||
fixed
|
||||
bordered
|
||||
safe-area-inset-bottom
|
||||
placeholder
|
||||
active-color="#1677FF"
|
||||
inactive-color="#8A9199"
|
||||
@change="onChange"
|
||||
>
|
||||
<wd-tabbar-item title="消息" icon="chat" />
|
||||
<wd-tabbar-item title="通讯录" icon="usergroup" />
|
||||
<wd-tabbar-item title="打卡" icon="clock" />
|
||||
<wd-tabbar-item title="工作台" icon="app" />
|
||||
<wd-tabbar-item title="我" icon="user" />
|
||||
</wd-tabbar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const PATHS = [
|
||||
'/pages/tabs/messages',
|
||||
'/pages/tabs/contacts',
|
||||
'/pages/tabs/clock',
|
||||
'/pages/tabs/workbench',
|
||||
'/pages/tabs/me'
|
||||
]
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return { active: 0 }
|
||||
},
|
||||
mounted() {
|
||||
this.sync()
|
||||
uni.$on('fy-tab', (i) => { this.active = i })
|
||||
},
|
||||
unmounted() {
|
||||
uni.$off('fy-tab')
|
||||
},
|
||||
methods: {
|
||||
sync() {
|
||||
const pages = getCurrentPages()
|
||||
const route = pages.length ? '/' + pages[pages.length - 1].route : ''
|
||||
const i = PATHS.indexOf(route)
|
||||
if (i >= 0) this.active = i
|
||||
},
|
||||
onChange({ value }) {
|
||||
const url = PATHS[value]
|
||||
if (url) uni.switchTab({ url })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -45,15 +45,15 @@
|
||||
"navigationBarBackgroundColor": "#F5F7FA",
|
||||
"backgroundColor": "#F5F7FA",
|
||||
"backgroundColorTop": "#F5F7FA",
|
||||
"backgroundColorBottom": "#F5F7FA"
|
||||
"backgroundColorBottom": "#E8F1FF"
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#8A9199",
|
||||
"selectedColor": "#1677FF",
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"backgroundColor": "#E8F1FF",
|
||||
"borderStyle": "white",
|
||||
"list": [
|
||||
{ "pagePath": "pages/tabs/messages", "text": "消息", "iconPath": "static/tab/msg.png", "selectedIconPath": "static/tab/msg-on.png" },
|
||||
{ "pagePath": "pages/tabs/messages", "text": "消息", "iconPath": "static/tab/msg2.png", "selectedIconPath": "static/tab/msg2-on.png" },
|
||||
{ "pagePath": "pages/tabs/contacts", "text": "通讯录", "iconPath": "static/tab/contacts.png", "selectedIconPath": "static/tab/contacts-on.png" },
|
||||
{ "pagePath": "pages/tabs/clock", "text": "打卡", "iconPath": "static/tab/clock.png", "selectedIconPath": "static/tab/clock-on.png" },
|
||||
{ "pagePath": "pages/tabs/workbench", "text": "工作台", "iconPath": "static/tab/work.png", "selectedIconPath": "static/tab/work-on.png" },
|
||||
|
||||
@@ -51,9 +51,11 @@
|
||||
<text class="lab">审批链</text>
|
||||
<text class="hint">{{ suggestedLabel || '将按固定审批链流转' }}</text>
|
||||
</view>
|
||||
<view class="submit">
|
||||
<wd-button type="primary" block size="large" @click="submit">提交申请</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -164,14 +166,15 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.body { padding: 24rpx 28rpx 48rpx; }
|
||||
.body { padding: 16rpx 24rpx 48rpx; }
|
||||
.field { margin-bottom: 24rpx; }
|
||||
.lab { font-size: 26rpx; color: #374151; display: block; margin-bottom: 10rpx; }
|
||||
.box { min-height: 92rpx; background: #fff; border-radius: 16rpx; padding: 22rpx 24rpx; font-size: 30rpx; border: 1rpx solid rgba(17,24,39,0.06); }
|
||||
.lab { font-size: 26rpx; color: #6B7280; display: block; margin-bottom: 10rpx; }
|
||||
.box { min-height: 92rpx; background: #fff; border-radius: 16rpx; padding: 22rpx 24rpx; font-size: 30rpx; color: #111827; border: 1rpx solid rgba(17,24,39,0.06); }
|
||||
.box.tall { min-height: 180rpx; }
|
||||
.box textarea, .box input { width: 100%; font-size: 30rpx; }
|
||||
.box textarea, .box input { width: 100%; font-size: 30rpx; color: #111827; }
|
||||
.inv { padding: 24rpx; margin-bottom: 16rpx; }
|
||||
.inv .box { margin-top: 12rpx; }
|
||||
.link { color: #1677FF; font-size: 28rpx; display: block; margin-bottom: 20rpx; }
|
||||
.hint { font-size: 24rpx; color: #6B7280; line-height: 1.5; }
|
||||
.inv .box { margin-top: 12rpx; background: #F5F7FA; }
|
||||
.link { color: #1677FF; font-size: 28rpx; font-weight: 600; display: block; margin: 4rpx 8rpx 24rpx; }
|
||||
.hint { font-size: 26rpx; color: #111827; line-height: 1.5; }
|
||||
.submit { margin-top: 8rpx; }
|
||||
</style>
|
||||
|
||||
@@ -5,17 +5,25 @@
|
||||
<view v-if="!canApply" class="deny">当前账号没有发起申请权限</view>
|
||||
<view v-else>
|
||||
<view class="search">
|
||||
<input v-model="q" placeholder="搜索审批流程或申请事项..." />
|
||||
<wd-icon name="search" size="28rpx" color="#9CA3AF" />
|
||||
<input v-model="q" placeholder="搜索审批流程或申请事项" placeholder-class="ph" confirm-type="search" />
|
||||
<view v-if="q" class="clear" @click="q = ''">
|
||||
<wd-icon name="close-bold" size="22rpx" color="#9CA3AF" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="!groups.length" class="empty">没有匹配的申请</view>
|
||||
<view v-for="g in groups" :key="g.category" class="block">
|
||||
<text class="cat">{{ g.category }}</text>
|
||||
<view class="card">
|
||||
<view v-for="k in g.items" :key="k.kind" class="item" @click="open(k)">
|
||||
<view>
|
||||
<view class="ico" :style="{ background: visual(k).bg }">
|
||||
<wd-icon :name="visual(k).name" size="36rpx" :color="visual(k).color" />
|
||||
</view>
|
||||
<view class="txt">
|
||||
<text class="t">{{ k.title }}</text>
|
||||
<text class="d">{{ k.hint }}</text>
|
||||
</view>
|
||||
<text class="arr">›</text>
|
||||
<wd-icon name="arrow-right" size="28rpx" color="#D1D5DB" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -29,6 +37,7 @@ import FyHeader from '../../components/fy-header.vue'
|
||||
import FyScroll from '../../components/fy-scroll.vue'
|
||||
import { store } from '../../utils/store.js'
|
||||
import { groupedApplyKinds, visibleApplyKinds } from '../../utils/apply.js'
|
||||
import { tileVisual } from '../../utils/nav.js'
|
||||
|
||||
export default {
|
||||
components: { FyHeader, FyScroll },
|
||||
@@ -50,6 +59,9 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
visual(k) {
|
||||
return tileVisual(k)
|
||||
},
|
||||
open(k) {
|
||||
uni.navigateTo({ url: '/pages/apply/form?kind=' + k.kind })
|
||||
}
|
||||
@@ -58,13 +70,35 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search { padding: 16rpx 24rpx; }
|
||||
.search input { height: 80rpx; background: #F3F6FB; border-radius: 16rpx; padding: 0 24rpx; }
|
||||
.block { margin: 8rpx 24rpx 24rpx; }
|
||||
.cat { font-size: 22rpx; color: #4B5563; font-weight: 600; }
|
||||
.item { display: flex; justify-content: space-between; align-items: center; padding: 24rpx; border-bottom: 1rpx solid #E8ECF2; }
|
||||
.t { display: block; font-size: 28rpx; font-weight: 600; }
|
||||
.d { font-size: 22rpx; color: #6B7280; }
|
||||
.arr { color: #D1D5DB; }
|
||||
.deny { padding: 80rpx; text-align: center; color: #6B7280; }
|
||||
.search {
|
||||
margin: 16rpx 24rpx 8rpx;
|
||||
height: 72rpx;
|
||||
padding: 0 20rpx;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
border: 1rpx solid rgba(17, 24, 39, 0.06);
|
||||
}
|
||||
.search input { flex: 1; height: 72rpx; font-size: 28rpx; }
|
||||
.ph { color: #9CA3AF; }
|
||||
.clear { width: 40rpx; height: 40rpx; display: flex; align-items: center; justify-content: center; }
|
||||
.block { margin: 20rpx 24rpx 8rpx; }
|
||||
.cat { display: block; margin: 0 8rpx 12rpx; font-size: 24rpx; color: #6B7280; font-weight: 600; }
|
||||
.item { display: flex; align-items: center; padding: 22rpx 24rpx; }
|
||||
.item + .item { border-top: 1rpx solid #F0F2F5; }
|
||||
.ico {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.txt { flex: 1; min-width: 0; margin: 0 20rpx; }
|
||||
.t { display: block; font-size: 30rpx; font-weight: 600; color: #111827; }
|
||||
.d { display: block; margin-top: 6rpx; font-size: 24rpx; color: #6B7280; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.empty, .deny { padding: 120rpx 40rpx; text-align: center; color: #6B7280; font-size: 28rpx; }
|
||||
</style>
|
||||
|
||||
@@ -2,14 +2,20 @@
|
||||
<view class="page edge">
|
||||
<fy-header :title="title" back />
|
||||
<fy-scroll class="edge-scroll" @refresh="load">
|
||||
<view v-if="loading" class="empty">加载中…</view>
|
||||
<view v-else-if="!rows.length" class="empty">暂无记录</view>
|
||||
<view v-for="r in rows" :key="r.id || r.no" class="card item" @click="open(r)">
|
||||
<view>
|
||||
<text class="t">{{ r.title || r.no || r.typeLabel }}</text>
|
||||
<text class="s">{{ r.status || '' }} · {{ r.createdAt || r.date || '' }}</text>
|
||||
<view v-if="loading" class="empty">
|
||||
<wd-skeleton theme="paragraph" />
|
||||
</view>
|
||||
<text class="amt">{{ r.amount != null ? '¥' + r.amount : '' }}</text>
|
||||
<wd-status-tip v-else-if="!rows.length" image="content" tip="暂无记录" />
|
||||
<view v-for="r in rows" :key="r.id || r.no" class="card item" @click="open(r)">
|
||||
<view class="main">
|
||||
<text class="t">{{ r.title || r.no || r.typeLabel }}</text>
|
||||
<text class="s">{{ r.createdAt || r.date || '' }}</text>
|
||||
</view>
|
||||
<view class="side">
|
||||
<text v-if="r.amount != null" class="amt">¥{{ r.amount }}</text>
|
||||
<wd-tag v-if="r.status" type="primary" plain>{{ r.status }}</wd-tag>
|
||||
</view>
|
||||
<text class="arr">›</text>
|
||||
</view>
|
||||
</fy-scroll>
|
||||
</view>
|
||||
@@ -54,9 +60,12 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.item { margin: 16rpx 24rpx; padding: 24rpx; display: flex; justify-content: space-between; }
|
||||
.t { display: block; font-size: 28rpx; font-weight: 600; }
|
||||
.s { font-size: 22rpx; color: #6B7280; }
|
||||
.amt { color: #1677FF; font-weight: 700; }
|
||||
.empty { padding: 80rpx; text-align: center; color: #6B7280; }
|
||||
.item { margin: 16rpx 24rpx; padding: 24rpx; display: flex; align-items: center; }
|
||||
.main { flex: 1; min-width: 0; }
|
||||
.t { display: block; font-size: 28rpx; font-weight: 650; color: #111827; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.s { display: block; margin-top: 8rpx; font-size: 24rpx; color: #6B7280; }
|
||||
.side { display: flex; flex-direction: column; align-items: flex-end; gap: 8rpx; margin-left: 16rpx; }
|
||||
.amt { color: #1677FF; font-weight: 700; font-size: 28rpx; }
|
||||
.arr { color: #C4C9D4; font-size: 32rpx; margin-left: 8rpx; }
|
||||
.empty { padding: 80rpx 40rpx; }
|
||||
</style>
|
||||
|
||||
@@ -95,11 +95,11 @@ export default {
|
||||
.stage { flex: 1; position: relative; }
|
||||
.wv { position: absolute; inset: 0; }
|
||||
.wait { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; }
|
||||
.av { width: 160rpx; height: 160rpx; border-radius: 50%; background: #1677FF; display: flex; align-items: center; justify-content: center; font-size: 64rpx; font-weight: 700; }
|
||||
.av { width: 160rpx; height: 160rpx; border-radius: 40rpx; background: #E8F1FF; color: #1677FF; display: flex; align-items: center; justify-content: center; font-size: 64rpx; font-weight: 700; }
|
||||
.name { margin-top: 24rpx; font-size: 36rpx; font-weight: 700; }
|
||||
.st { margin-top: 8rpx; color: rgba(255,255,255,0.65); }
|
||||
.acts { display: flex; justify-content: center; gap: 32rpx; padding: 32rpx 24rpx calc(32rpx + env(safe-area-inset-bottom)); }
|
||||
.btn { min-width: 200rpx; height: 80rpx; border-radius: 999rpx; display: flex; align-items: center; justify-content: center; font-size: 28rpx; font-weight: 600; }
|
||||
.ok { background: #006c49; }
|
||||
.ok { background: #16A34A; }
|
||||
.no { background: #DC2626; }
|
||||
</style>
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
@left="exitSelect"
|
||||
>
|
||||
<template #right>
|
||||
<text v-if="!selecting && isGroup" class="ico" @click="goMore">···</text>
|
||||
<text v-else-if="!selecting && peerId" class="ico" @click="goMore">···</text>
|
||||
<view v-if="!selecting && (isGroup || peerId)" class="ico" @click="goMore">
|
||||
<wd-icon name="ellipsis" size="44rpx" color="#111827" />
|
||||
</view>
|
||||
</template>
|
||||
</fy-header>
|
||||
|
||||
@@ -25,23 +26,23 @@
|
||||
<view v-if="!list.length" class="empty">暂无消息,发一条试试</view>
|
||||
<view v-for="(m, i) in list" :key="m.id || i">
|
||||
<view v-if="showTime(i)" class="chip-time">{{ timeOf(m) }}</view>
|
||||
<view v-if="m.recalled" class="sys">{{ m.text || '已撤回一条消息' }}</view>
|
||||
<view v-if="m.recalled || m.kind === 'system'" class="sys">{{ m.text || '已撤回一条消息' }}</view>
|
||||
<view
|
||||
v-else
|
||||
class="row"
|
||||
:class="{ mine: m.mine, pick: selecting }"
|
||||
@click="selecting ? togglePick(m) : tapMsg(m)"
|
||||
@longpress="act(m)"
|
||||
@longpress="act(m, i)"
|
||||
>
|
||||
<view v-if="selecting" class="ck" :class="{ on: isPicked(m) }" />
|
||||
<view v-if="!m.mine" class="av" @click.stop="openSender(m)">
|
||||
<fy-file-mark v-if="isFiles" />
|
||||
<image v-else-if="avatarOf(m)" :src="avatarOf(m)" mode="aspectFill" />
|
||||
<image v-else-if="avatarOf(m)" class="face" :src="avatarOf(m)" mode="aspectFill" />
|
||||
<text v-else>{{ initialOf(m) }}</text>
|
||||
</view>
|
||||
<view class="col" :class="{ mine: m.mine }">
|
||||
<text v-if="isGroup && !m.mine" class="who">{{ sender(m) }}</text>
|
||||
<view class="ball" :class="ballClass(m)">
|
||||
<view :id="'bubble-' + i" class="ball" :class="ballClass(m)">
|
||||
<view v-if="m.quote" class="quote">{{ quoteLine(m.quote) }}</view>
|
||||
<image v-if="m.kind === 'image' && m.fileUrl" class="pic" :src="m.fileUrl" mode="widthFix" />
|
||||
<view v-else-if="m.kind === 'voice'" class="voice" :style="{ width: voiceW(m) }">
|
||||
@@ -57,10 +58,10 @@
|
||||
</view>
|
||||
<text v-else>{{ m.text }}</text>
|
||||
</view>
|
||||
<text v-if="m.mine" class="tm" :class="{ read: m.read || readLabel(m) === '已读' }" @click="showRead(m)">{{ readLabel(m) }}</text>
|
||||
<text v-if="m.mine" class="tm" :class="{ read: readDone(m) }" @click="showRead(m)">{{ readLabel(m) }}</text>
|
||||
</view>
|
||||
<view v-if="m.mine" class="av self">
|
||||
<image v-if="myAvatar" :src="myAvatar" mode="aspectFill" />
|
||||
<image v-if="myAvatar" class="face" :src="myAvatar" mode="aspectFill" />
|
||||
<text v-else>{{ myInitial }}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -81,20 +82,13 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="actionMsg && !selecting" class="mask" @click="closeAct">
|
||||
<view class="sheet" :style="{ paddingBottom: (safeBottom + 12) + 'px' }" @click.stop>
|
||||
<text class="sheet-t">消息操作</text>
|
||||
<text class="sheet-s">{{ actPreview }}</text>
|
||||
<view class="sheet-grid">
|
||||
<view v-if="actionMsg.kind === 'voice'" class="cell" @click="doPlay"><text class="ce">▶</text><text>播放</text></view>
|
||||
<view v-if="actionMsg.kind !== 'voice'" class="cell" @click="doCopy"><text class="ce">文</text><text>复制</text></view>
|
||||
<view class="cell" @click="doForward"><text class="ce">↪</text><text>转发</text></view>
|
||||
<view class="cell" @click="doQuote"><text class="ce">❝</text><text>引用</text></view>
|
||||
<view class="cell" @click="enterSelect"><text class="ce">多</text><text>多选</text></view>
|
||||
<view v-if="canRecall" class="cell" @click="doRecall"><text class="ce">↩</text><text>撤回</text></view>
|
||||
<view class="cell" @click="doDelete"><text class="ce">删</text><text>删除</text></view>
|
||||
<view v-if="actionMsg && !selecting" class="pop-mask" @click="closeAct" @touchmove.stop.prevent>
|
||||
<view class="pop" :class="{ below: popBelow }" :style="popBoxStyle" @click.stop>
|
||||
<view v-for="it in actItems" :key="it.k" class="pop-item" @click="runAct(it.k)">
|
||||
<wd-icon :name="it.icon" size="22px" color="#fff" />
|
||||
<text class="pop-lab">{{ it.label }}</text>
|
||||
</view>
|
||||
<view class="sheet-cancel" @click="closeAct">取消</view>
|
||||
<view class="pop-caret" :style="{ left: caretLeft + 'px' }" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -110,7 +104,18 @@
|
||||
<text class="qx" @click="quoting = null">×</text>
|
||||
</view>
|
||||
<view class="bar">
|
||||
<text class="tool" @click="toggleVoice">{{ voiceMode ? '⌨' : '🎤' }}</text>
|
||||
<view class="tool" @click="toggleVoice">
|
||||
<view v-if="voiceMode" class="kbd">
|
||||
<view class="krow" />
|
||||
<view class="krow" />
|
||||
<view class="kspace" />
|
||||
</view>
|
||||
<view v-else class="mic">
|
||||
<view class="mic-head" />
|
||||
<view class="mic-arc" />
|
||||
<view class="mic-stem" />
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="voiceMode"
|
||||
class="hold"
|
||||
@@ -143,12 +148,30 @@
|
||||
<text v-for="e in emos" :key="e" class="em" @click="insertEmoji(e)">{{ e }}</text>
|
||||
</view>
|
||||
<view v-else-if="more && !isWork && !selecting" class="panel" :style="{ paddingBottom: safeBottom + 'px' }">
|
||||
<view class="tile" @click="pick('album')"><text class="te">🖼</text><text>相册</text></view>
|
||||
<view class="tile" @click="pick('camera')"><text class="te">📷</text><text>拍摄</text></view>
|
||||
<view class="tile" @click="pick('file')"><text class="te">📎</text><text>文件</text></view>
|
||||
<view v-if="!isGroup" class="tile" @click="startCall(false)"><text class="te">☎</text><text>语音通话</text></view>
|
||||
<view v-if="!isGroup" class="tile" @click="startCall(true)"><text class="te">▷</text><text>视频通话</text></view>
|
||||
<view v-if="isGroup" class="tile" @click="mention"><text class="te">@</text><text>提醒成员</text></view>
|
||||
<view class="tile" @click="pick('album')">
|
||||
<wd-icon name="picture" size="52rpx" color="#111827" />
|
||||
<text>相册</text>
|
||||
</view>
|
||||
<view class="tile" @click="pick('camera')">
|
||||
<wd-icon name="camera" size="52rpx" color="#111827" />
|
||||
<text>拍摄</text>
|
||||
</view>
|
||||
<view class="tile" @click="pick('file')">
|
||||
<wd-icon name="folder" size="52rpx" color="#111827" />
|
||||
<text>文件</text>
|
||||
</view>
|
||||
<view v-if="!isGroup && !isFiles" class="tile" @click="startCall(false)">
|
||||
<wd-icon name="call" size="52rpx" color="#111827" />
|
||||
<text>语音通话</text>
|
||||
</view>
|
||||
<view v-if="!isGroup && !isFiles" class="tile" @click="startCall(true)">
|
||||
<wd-icon name="video" size="52rpx" color="#111827" />
|
||||
<text>视频通话</text>
|
||||
</view>
|
||||
<view v-if="isGroup" class="tile" @click="mention">
|
||||
<wd-icon name="usergroup" size="52rpx" color="#111827" />
|
||||
<text>提醒成员</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="isWork" class="hint">工作通知为办件卡片,不支持快捷回复</view>
|
||||
</view>
|
||||
@@ -191,6 +214,12 @@ export default {
|
||||
recWillCancel: false,
|
||||
recSec: 0,
|
||||
actionMsg: null,
|
||||
popTop: 0,
|
||||
popLeft: 8,
|
||||
popW: 224,
|
||||
caretLeft: 24,
|
||||
popBelow: false,
|
||||
popAt: 0,
|
||||
quoting: null,
|
||||
selecting: false,
|
||||
picked: [],
|
||||
@@ -221,8 +250,25 @@ export default {
|
||||
const m = this.actionMsg
|
||||
return !!(m && m.mine && !m.recalled && Date.now() - Number(m.time) < 120000)
|
||||
},
|
||||
actPreview() {
|
||||
return previewOf(this.actionMsg)
|
||||
actItems() {
|
||||
const m = this.actionMsg
|
||||
if (!m) return []
|
||||
const items = []
|
||||
if (m.kind === 'voice') items.push({ k: 'play', icon: 'play', label: '播放' })
|
||||
else items.push({ k: 'copy', icon: 'copy', label: '复制' })
|
||||
items.push({ k: 'forward', icon: 'share', label: '转发' })
|
||||
items.push({ k: 'quote', icon: 'chat', label: '引用' })
|
||||
items.push({ k: 'select', icon: 'view-list', label: '多选' })
|
||||
if (this.canRecall) items.push({ k: 'recall', icon: 'rollback', label: '撤回' })
|
||||
items.push({ k: 'delete', icon: 'delete', label: '删除' })
|
||||
return items
|
||||
},
|
||||
popBoxStyle() {
|
||||
return {
|
||||
top: this.popTop + 'px',
|
||||
left: this.popLeft + 'px',
|
||||
width: this.popW + 'px'
|
||||
}
|
||||
}
|
||||
},
|
||||
onLoad(q) {
|
||||
@@ -230,12 +276,14 @@ export default {
|
||||
try {
|
||||
this.safeBottom = Number(uni.getSystemInfoSync().safeAreaInsets && uni.getSystemInfoSync().safeAreaInsets.bottom) || 0
|
||||
} catch (_) {}
|
||||
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()
|
||||
@@ -278,7 +326,7 @@ export default {
|
||||
}
|
||||
this.scrollEnd()
|
||||
}
|
||||
uni.onKeyboardHeightChange(this._kb)
|
||||
if (typeof uni.onKeyboardHeightChange === 'function') uni.onKeyboardHeightChange(this._kb)
|
||||
},
|
||||
unbindKb() {
|
||||
if (this._kb) {
|
||||
@@ -327,10 +375,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('、') || '暂无'
|
||||
@@ -423,6 +482,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 (_) {
|
||||
@@ -563,14 +627,53 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
act(m) {
|
||||
act(m, index) {
|
||||
if (!m || m.recalled || this.selecting) return
|
||||
this.more = false
|
||||
this.emoji = false
|
||||
this.hideKb()
|
||||
this.popAt = Date.now()
|
||||
this.actionMsg = m
|
||||
this.$nextTick(() => this.placePop(index))
|
||||
},
|
||||
placePop(index) {
|
||||
const q = uni.createSelectorQuery().in(this)
|
||||
q.select('#bubble-' + index).boundingClientRect()
|
||||
q.exec((res) => {
|
||||
const r = res && res[0]
|
||||
if (!r || !r.width) return
|
||||
const sys = uni.getSystemInfoSync()
|
||||
const n = this.actItems.length
|
||||
const cols = Math.min(n, 5)
|
||||
const itemW = 56
|
||||
const w = cols * itemW
|
||||
const rows = Math.ceil(n / cols)
|
||||
const h = rows * 62 + 8
|
||||
let left = r.left + r.width / 2 - w / 2
|
||||
left = Math.max(8, Math.min(left, sys.windowWidth - w - 8))
|
||||
const below = r.top < h + 16
|
||||
const top = below ? r.bottom + 10 : r.top - h - 10
|
||||
const caret = r.left + r.width / 2 - left - 6
|
||||
this.popW = w
|
||||
this.popLeft = left
|
||||
this.popTop = Math.max(8, top)
|
||||
this.popBelow = below
|
||||
this.caretLeft = Math.max(14, Math.min(caret, w - 26))
|
||||
})
|
||||
},
|
||||
runAct(k) {
|
||||
if (k === 'copy') return this.doCopy()
|
||||
if (k === 'play') return this.doPlay()
|
||||
if (k === 'forward') return this.doForward()
|
||||
if (k === 'quote') return this.doQuote()
|
||||
if (k === 'select') return this.enterSelect()
|
||||
if (k === 'recall') return this.doRecall()
|
||||
if (k === 'delete') return this.doDelete()
|
||||
},
|
||||
closeAct() {
|
||||
if (Date.now() - this.popAt < 400) return
|
||||
this.actionMsg = null
|
||||
},
|
||||
closeAct() { this.actionMsg = null },
|
||||
liveMsg() {
|
||||
const c = store.findConv(this.sid)
|
||||
const m = this.actionMsg
|
||||
@@ -676,7 +779,9 @@ export default {
|
||||
}
|
||||
},
|
||||
openSender(m) {
|
||||
if (m.fromId) uni.navigateTo({ url: '/pages/contact/detail?id=' + encodeURIComponent(m.fromId) })
|
||||
const id = m && m.fromId
|
||||
if (!id || !store.person(id)) return
|
||||
uni.navigateTo({ url: '/pages/contact/detail?id=' + encodeURIComponent(id) })
|
||||
},
|
||||
goMore() {
|
||||
if (this.isGroup) uni.navigateTo({ url: '/pages/chat/group?sid=' + encodeURIComponent(this.sid) })
|
||||
@@ -715,89 +820,136 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chat { display: flex; flex-direction: column; height: 100vh; overflow: hidden; background: #ededed; }
|
||||
.msgs { flex: 1; height: 0; padding: 12rpx 20rpx 8rpx; box-sizing: border-box; }
|
||||
.chip-time { text-align: center; margin: 16rpx auto; background: rgba(0,0,0,.08); color: #fff; font-size: 20rpx; padding: 4rpx 16rpx; border-radius: 8rpx; width: fit-content; }
|
||||
.sys { text-align: center; color: #888; font-size: 22rpx; padding: 12rpx 0 20rpx; }
|
||||
.empty { text-align: center; color: #888; padding: 80rpx 0; font-size: 26rpx; }
|
||||
.row { margin-bottom: 22rpx; display: flex; align-items: flex-start; gap: 12rpx; }
|
||||
.chat { display: flex; flex-direction: column; height: 100%; overflow: hidden; background: #F5F7FA; }
|
||||
.msgs { flex: 1; height: 0; padding: 8rpx 24rpx 12rpx; box-sizing: border-box; }
|
||||
.chip-time {
|
||||
text-align: center;
|
||||
margin: 20rpx auto 8rpx;
|
||||
color: #9CA3AF;
|
||||
font-size: 22rpx;
|
||||
width: fit-content;
|
||||
}
|
||||
.sys { text-align: center; color: #9CA3AF; font-size: 24rpx; padding: 12rpx 40rpx 20rpx; }
|
||||
.empty { text-align: center; color: #9CA3AF; padding: 120rpx 40rpx; font-size: 26rpx; }
|
||||
.row { margin-bottom: 24rpx; display: flex; align-items: flex-start; gap: 16rpx; }
|
||||
.row.mine { justify-content: flex-end; }
|
||||
.row.pick { padding-left: 0; }
|
||||
.ck { width: 36rpx; height: 36rpx; border: 2rpx solid #bbb; border-radius: 50%; margin-top: 20rpx; flex-shrink: 0; }
|
||||
.ck.on { background: #07c160; border-color: #07c160; }
|
||||
.col { max-width: 70%; display: flex; flex-direction: column; }
|
||||
.ck { width: 36rpx; height: 36rpx; border: 2rpx solid #D1D5DB; border-radius: 50%; margin-top: 22rpx; flex-shrink: 0; box-sizing: border-box; }
|
||||
.ck.on { background: #1677FF; border-color: #1677FF; }
|
||||
.col { max-width: 72%; display: flex; flex-direction: column; }
|
||||
.col.mine { align-items: flex-end; }
|
||||
.av { width: 80rpx; height: 80rpx; border-radius: 10rpx; background: #dae2fd; color: #1677FF; overflow: hidden; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 28rpx; font-weight: 700; }
|
||||
.av.self { background: #1677FF; color: #fff; }
|
||||
.av image { width: 80rpx; height: 80rpx; }
|
||||
.who { font-size: 20rpx; color: #888; margin-bottom: 4rpx; }
|
||||
.ball { background: #fff; padding: 16rpx 20rpx; border-radius: 8rpx; font-size: 30rpx; word-break: break-all; line-height: 1.45; }
|
||||
.ball.self { background: #95ec69; color: #111; }
|
||||
.ball.work { background: #fff; }
|
||||
.ball.voice { min-width: 140rpx; }
|
||||
.quote { background: rgba(0,0,0,.06); color: #666; font-size: 22rpx; padding: 8rpx 12rpx; border-radius: 6rpx; margin-bottom: 10rpx; }
|
||||
.pic { max-width: 420rpx; border-radius: 8rpx; display: block; }
|
||||
.file, .cardtxt { display: flex; flex-direction: column; }
|
||||
.av { width: 76rpx; height: 76rpx; border-radius: 16rpx; background: #E8F1FF; color: #1677FF; overflow: hidden; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 28rpx; font-weight: 650; }
|
||||
.av.self { background: #E8F1FF; color: #1677FF; }
|
||||
.face { width: 76rpx; height: 76rpx; display: block; }
|
||||
.who { font-size: 22rpx; color: #9CA3AF; margin: 0 4rpx 8rpx; }
|
||||
.ball {
|
||||
background: #fff;
|
||||
padding: 18rpx 22rpx;
|
||||
border-radius: 8rpx 20rpx 20rpx 20rpx;
|
||||
font-size: 30rpx;
|
||||
word-break: break-all;
|
||||
line-height: 1.5;
|
||||
color: #111827;
|
||||
box-shadow: 0 2rpx 8rpx rgba(17, 24, 39, 0.04);
|
||||
}
|
||||
.ball.self {
|
||||
background: #1677FF;
|
||||
color: #fff;
|
||||
border-radius: 20rpx 8rpx 20rpx 20rpx;
|
||||
box-shadow: none;
|
||||
}
|
||||
.ball.work { background: #fff; color: #111827; border-radius: 20rpx; }
|
||||
.ball.voice { min-width: 160rpx; }
|
||||
.quote { background: rgba(17, 24, 39, 0.05); color: #6B7280; font-size: 22rpx; padding: 8rpx 12rpx; border-radius: 10rpx; margin-bottom: 10rpx; }
|
||||
.ball.self .quote { background: rgba(255, 255, 255, 0.18); color: rgba(255, 255, 255, 0.9); }
|
||||
.pic { max-width: 420rpx; border-radius: 16rpx; display: block; }
|
||||
.file, .cardtxt { display: flex; flex-direction: column; min-width: 280rpx; }
|
||||
.fn, .ct { font-weight: 600; }
|
||||
.fs, .cs { font-size: 22rpx; margin-top: 6rpx; color: #1677FF; }
|
||||
.tm { font-size: 20rpx; color: #b2b2b2; margin-top: 6rpx; }
|
||||
.tm.read { color: #07c160; }
|
||||
.fs, .cs { font-size: 22rpx; margin-top: 8rpx; color: #1677FF; }
|
||||
.ball.self .fs, .ball.self .cs { color: rgba(255, 255, 255, 0.85); }
|
||||
.tm { font-size: 22rpx; color: #C4C9D4; margin-top: 6rpx; }
|
||||
.tm.read { color: #16A34A; }
|
||||
.recmask {
|
||||
position: fixed; left: 0; right: 0; top: 0; bottom: 0; z-index: 30;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.01); touch-action: none;
|
||||
}
|
||||
.recbox {
|
||||
width: 320rpx; padding: 48rpx 24rpx; background: rgba(17, 24, 39, 0.82); color: #fff;
|
||||
border-radius: 24rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 26rpx;
|
||||
}
|
||||
.recbox.cancel { background: rgba(220, 38, 38, 0.9); }
|
||||
.recsec { font-size: 48rpx; font-weight: 650; margin-bottom: 12rpx; }
|
||||
.composer { background: #F5F7FA; border-top: 1rpx solid #E8ECF2; }
|
||||
.qbar { display: flex; align-items: center; padding: 12rpx 24rpx 0; }
|
||||
.qt { flex: 1; font-size: 24rpx; color: #6B7280; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.qx { padding: 0 8rpx 0 16rpx; color: #9CA3AF; font-size: 32rpx; }
|
||||
.bar { display: flex; align-items: flex-end; padding: 12rpx 16rpx 8rpx; gap: 8rpx; }
|
||||
.tool { width: 64rpx; height: 72rpx; display: flex; align-items: center; justify-content: center; font-size: 36rpx; color: #374151; }
|
||||
.mic { width: 28rpx; height: 40rpx; display: flex; flex-direction: column; align-items: center; }
|
||||
.mic-head { width: 14rpx; height: 20rpx; border: 3rpx solid #111827; border-radius: 10rpx; box-sizing: border-box; }
|
||||
.mic-arc { width: 24rpx; height: 12rpx; margin-top: -4rpx; border: 3rpx solid #111827; border-top: none; border-radius: 0 0 14rpx 14rpx; box-sizing: border-box; }
|
||||
.mic-stem { width: 3rpx; height: 8rpx; background: #111827; }
|
||||
.kbd { width: 36rpx; display: flex; flex-direction: column; gap: 4rpx; }
|
||||
.krow { height: 5rpx; background: #111827; border-radius: 2rpx; }
|
||||
.kspace { width: 18rpx; height: 5rpx; margin: 0 auto; background: #111827; border-radius: 2rpx; }
|
||||
.plus { font-size: 48rpx; font-weight: 300; color: #111827; }
|
||||
.box { flex: 1; min-height: 72rpx; background: #fff; border-radius: 12rpx; display: flex; align-items: center; padding: 0 20rpx; }
|
||||
.box input { flex: 1; height: 72rpx; font-size: 30rpx; }
|
||||
.hold {
|
||||
flex: 1; height: 72rpx; background: #fff; border-radius: 12rpx;
|
||||
text-align: center; line-height: 72rpx; font-size: 28rpx; font-weight: 600; color: #111827;
|
||||
touch-action: none; user-select: none;
|
||||
}
|
||||
.go {
|
||||
height: 64rpx; padding: 0 24rpx; margin-bottom: 4rpx;
|
||||
background: #1677FF; color: #fff; border-radius: 12rpx;
|
||||
font-size: 26rpx; font-weight: 600; display: flex; align-items: center;
|
||||
}
|
||||
.panel { display: flex; flex-wrap: wrap; background: #F5F7FA; padding: 8rpx 24rpx 28rpx; gap: 24rpx; }
|
||||
.panel.emo { gap: 0; padding-top: 8rpx; }
|
||||
.tile {
|
||||
width: 148rpx; height: 148rpx; background: #fff; border-radius: 20rpx;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: 12rpx; font-size: 22rpx; color: #4B5563;
|
||||
}
|
||||
.em { width: 12.5%; text-align: center; font-size: 44rpx; line-height: 88rpx; }
|
||||
.hint { text-align: center; font-size: 24rpx; color: #9CA3AF; padding: 24rpx; background: #fff; }
|
||||
.ico { padding: 8rpx 8rpx; font-size: 40rpx; color: #111827; letter-spacing: 2rpx; }
|
||||
.pop-mask { position: fixed; left: 0; right: 0; top: 0; bottom: 0; z-index: 40; }
|
||||
.pop {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 30;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
background: #4C4C4C;
|
||||
border-radius: 8px;
|
||||
padding: 4px 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.pop-item {
|
||||
width: 56px;
|
||||
height: 62px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.01);
|
||||
touch-action: none;
|
||||
color: #fff;
|
||||
}
|
||||
.recbox { width: 360rpx; height: 280rpx; background: rgba(0,0,0,.72); color: #fff; border-radius: 20rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 26rpx; }
|
||||
.recbox.cancel { background: rgba(186,26,26,.85); }
|
||||
.recsec { font-size: 48rpx; font-weight: 700; margin-bottom: 12rpx; }
|
||||
.composer { background: #f7f7f7; border-top: 1rpx solid #e5e5e5; }
|
||||
.qbar { display: flex; align-items: center; padding: 10rpx 20rpx 0; }
|
||||
.qt { flex: 1; font-size: 22rpx; color: #888; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.qx { padding: 0 12rpx; color: #888; }
|
||||
.bar { display: flex; align-items: flex-end; padding: 12rpx 12rpx 0; gap: 10rpx; }
|
||||
.tool { width: 64rpx; height: 64rpx; text-align: center; line-height: 64rpx; font-size: 36rpx; color: #111; }
|
||||
.plus { font-size: 44rpx; font-weight: 300; }
|
||||
.box { flex: 1; min-height: 72rpx; background: #fff; border-radius: 8rpx; display: flex; align-items: center; padding: 0 16rpx; }
|
||||
.box input { flex: 1; height: 72rpx; }
|
||||
.hold {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
background: #fff;
|
||||
border-radius: 8rpx;
|
||||
text-align: center;
|
||||
line-height: 72rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
.pop-lab { margin-top: 4px; font-size: 12px; line-height: 1; color: #fff; }
|
||||
.pop-caret {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 6px solid transparent;
|
||||
border-top-color: #4C4C4C;
|
||||
bottom: -12px;
|
||||
}
|
||||
.go { height: 64rpx; padding: 0 24rpx; background: #07c160; color: #fff; border-radius: 8rpx; font-size: 26rpx; font-weight: 600; display: flex; align-items: center; margin-bottom: 4rpx; }
|
||||
.panel { display: flex; flex-wrap: wrap; background: #f7f7f7; padding: 20rpx 24rpx 24rpx; gap: 24rpx; }
|
||||
.panel.emo { gap: 8rpx; }
|
||||
.tile { width: 140rpx; height: 140rpx; background: #fff; border-radius: 16rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 22rpx; color: #333; }
|
||||
.te { font-size: 48rpx; margin-bottom: 8rpx; }
|
||||
.em { width: 12.5%; text-align: center; font-size: 44rpx; line-height: 88rpx; }
|
||||
.hint { text-align: center; font-size: 22rpx; color: #888; padding: 20rpx; }
|
||||
.ico { padding: 8rpx 16rpx; font-size: 36rpx; }
|
||||
.mask { position: fixed; left: 0; right: 0; top: 0; bottom: 0; background: rgba(0,0,0,.45); z-index: 40; display: flex; align-items: flex-end; }
|
||||
.sheet { width: 100%; background: #fff; border-radius: 24rpx 24rpx 0 0; padding: 28rpx 28rpx 12rpx; }
|
||||
.sheet-t { display: block; text-align: center; font-size: 30rpx; font-weight: 700; }
|
||||
.sheet-s { display: block; text-align: center; font-size: 22rpx; color: #888; margin: 8rpx 0 24rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.sheet-grid { display: flex; flex-wrap: wrap; gap: 16rpx; }
|
||||
.cell { width: calc(25% - 12rpx); height: 140rpx; background: #f7f7f7; border-radius: 16rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 22rpx; color: #333; }
|
||||
.ce { font-size: 36rpx; margin-bottom: 8rpx; color: #111; font-weight: 700; }
|
||||
.sheet-cancel { margin: 20rpx 0 8rpx; height: 88rpx; border-radius: 16rpx; background: #f7f7f7; display: flex; align-items: center; justify-content: center; font-size: 28rpx; }
|
||||
.selbar { display: flex; background: #fff; border-top: 1rpx solid #eee; }
|
||||
.selbtn { flex: 1; height: 96rpx; display: flex; align-items: center; justify-content: center; font-size: 28rpx; }
|
||||
.selbtn.danger { color: #fa5151; }
|
||||
.pop.below .pop-caret {
|
||||
top: -12px;
|
||||
bottom: auto;
|
||||
border-top-color: transparent;
|
||||
border-bottom-color: #4C4C4C;
|
||||
}
|
||||
.selbar { display: flex; background: #fff; border-top: 1rpx solid #E8ECF2; }
|
||||
.selbtn { flex: 1; height: 96rpx; display: flex; align-items: center; justify-content: center; font-size: 28rpx; color: #1677FF; }
|
||||
.selbtn.danger { color: #DC2626; }
|
||||
</style>
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
<view class="card">
|
||||
<view v-for="p in people" :key="p.id" class="item" @click="toggle(p.id)">
|
||||
<view class="box" :class="{ on: picked.includes(p.id) }" />
|
||||
<view class="av">{{ p.name.slice(0, 1) }}</view>
|
||||
<image v-if="p.avatar" class="av img" :src="p.avatar" mode="aspectFill" />
|
||||
<view v-else class="av">{{ (p.name || '?').slice(0, 1) }}</view>
|
||||
<view class="meta">
|
||||
<text class="n">{{ p.name }}</text>
|
||||
<text class="s">{{ p.title }} · {{ p.department }}</text>
|
||||
@@ -16,7 +17,7 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="pad">
|
||||
<view class="btn-primary" :class="{ off: !can }" @click="ok">创建并进入</view>
|
||||
<wd-button type="primary" block size="large" :disabled="!can" @click="ok">创建并进入</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -65,17 +66,18 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.field { margin: 24rpx; padding: 8rpx 24rpx; }
|
||||
.field input { height: 80rpx; font-size: 30rpx; }
|
||||
.hint { margin: 0 32rpx 12rpx; font-size: 22rpx; color: #6B7280; }
|
||||
.field { margin: 16rpx 24rpx; padding: 8rpx 24rpx; }
|
||||
.field input { height: 88rpx; font-size: 30rpx; color: #111827; }
|
||||
.hint { margin: 0 32rpx 12rpx; font-size: 24rpx; color: #6B7280; }
|
||||
.card { margin: 0 24rpx 24rpx; overflow: hidden; }
|
||||
.item { display: flex; align-items: center; padding: 20rpx 24rpx; }
|
||||
.box { width: 36rpx; height: 36rpx; border: 2rpx solid #D1D5DB; border-radius: 8rpx; margin-right: 16rpx; }
|
||||
.item { display: flex; align-items: center; padding: 0 24rpx; }
|
||||
.item + .item .meta { border-top: 1rpx solid #F0F2F5; }
|
||||
.box { width: 36rpx; height: 36rpx; border: 2rpx solid #D1D5DB; border-radius: 8rpx; margin-right: 16rpx; flex-shrink: 0; }
|
||||
.box.on { background: #1677FF; border-color: #1677FF; }
|
||||
.av { width: 72rpx; height: 72rpx; border-radius: 50%; background: #E8F1FF; color: #1677FF; display: flex; align-items: center; justify-content: center; font-weight: 600; }
|
||||
.meta { margin-left: 16rpx; }
|
||||
.n { display: block; font-size: 28rpx; font-weight: 600; }
|
||||
.s { font-size: 22rpx; color: #6B7280; }
|
||||
.av { width: 72rpx; height: 72rpx; border-radius: 18rpx; background: #E8F1FF; color: #1677FF; display: flex; align-items: center; justify-content: center; font-weight: 600; margin: 16rpx 0; flex-shrink: 0; overflow: hidden; }
|
||||
.av.img { display: block; }
|
||||
.meta { margin-left: 16rpx; flex: 1; min-width: 0; padding: 18rpx 0; }
|
||||
.n { display: block; font-size: 28rpx; font-weight: 600; color: #111827; }
|
||||
.s { display: block; margin-top: 4rpx; font-size: 22rpx; color: #6B7280; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.pad { padding: 0 24rpx 40rpx; }
|
||||
.off { opacity: 0.45; }
|
||||
</style>
|
||||
|
||||
@@ -97,13 +97,13 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search { padding: 12rpx 24rpx; }
|
||||
.search input { height: 72rpx; background: #ededed; border-radius: 8rpx; padding: 0 24rpx; font-size: 26rpx; }
|
||||
.snip { padding: 8rpx 32rpx 16rpx; font-size: 22rpx; color: #888; }
|
||||
.sec { padding: 16rpx 32rpx 8rpx; font-size: 22rpx; color: #888; background: #f7f7f7; }
|
||||
.item { display: flex; align-items: center; padding: 20rpx 24rpx; background: #fff; }
|
||||
.av { width: 80rpx; height: 80rpx; border-radius: 10rpx; background: #E8F1FF; color: #1677FF; display: flex; align-items: center; justify-content: center; font-weight: 700; overflow: hidden; flex-shrink: 0; }
|
||||
.search { padding: 8rpx 24rpx 4rpx; }
|
||||
.search input { height: 72rpx; background: #fff; border-radius: 12rpx; padding: 0 24rpx; font-size: 28rpx; color: #111827; border: 1rpx solid rgba(17, 24, 39, 0.06); }
|
||||
.snip { padding: 12rpx 32rpx 8rpx; font-size: 22rpx; color: #6B7280; }
|
||||
.sec { padding: 20rpx 32rpx 8rpx; font-size: 22rpx; color: #6B7280; }
|
||||
.item { display: flex; align-items: center; margin: 0 24rpx; padding: 16rpx 0; border-bottom: 1rpx solid #F0F2F5; }
|
||||
.av { width: 80rpx; height: 80rpx; border-radius: 18rpx; background: #E8F1FF; color: #1677FF; display: flex; align-items: center; justify-content: center; font-weight: 700; overflow: hidden; flex-shrink: 0; }
|
||||
.av.img { width: 80rpx; height: 80rpx; }
|
||||
.n { margin-left: 20rpx; font-size: 30rpx; }
|
||||
.empty { padding: 80rpx; text-align: center; color: #888; }
|
||||
.n { margin-left: 20rpx; font-size: 30rpx; color: #111827; }
|
||||
.empty { padding: 120rpx 40rpx; text-align: center; color: #6B7280; font-size: 28rpx; }
|
||||
</style>
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
<fy-header title="群聊设置" back />
|
||||
<view class="card head">
|
||||
<view class="grid">
|
||||
<view v-for="p in members.slice(0, 4)" :key="p.id" class="cell">{{ (p.name || '?').slice(0, 1) }}</view>
|
||||
<view v-for="p in members.slice(0, 4)" :key="p.id" class="cell">
|
||||
<image v-if="p.avatar" class="cell-img" :src="p.avatar" mode="aspectFill" />
|
||||
<text v-else>{{ (p.name || '?').slice(0, 1) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="meta">
|
||||
<text class="gn">{{ title }}</text>
|
||||
@@ -17,7 +20,8 @@
|
||||
</view>
|
||||
<view class="mems">
|
||||
<view v-for="p in members" :key="p.id" class="mem" @click="open(p)">
|
||||
<view class="mav">{{ (p.name || '?').slice(0, 1) }}</view>
|
||||
<image v-if="p.avatar" class="mav img" :src="p.avatar" mode="aspectFill" />
|
||||
<view v-else class="mav">{{ (p.name || '?').slice(0, 1) }}</view>
|
||||
<text class="mn">{{ p.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -76,6 +80,7 @@ export default {
|
||||
this.members = ids.map((id) => store.person(id) || { id, name: id, title: '', department: '' })
|
||||
},
|
||||
open(p) {
|
||||
if (!p || !store.person(p.id)) return
|
||||
uni.navigateTo({ url: '/pages/contact/detail?id=' + encodeURIComponent(p.id) })
|
||||
},
|
||||
toggle(field) {
|
||||
@@ -105,8 +110,9 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.head { margin: 24rpx; padding: 28rpx; display: flex; align-items: center; }
|
||||
.grid { width: 96rpx; height: 96rpx; background: #E8ECF2; border-radius: 16rpx; display: flex; flex-wrap: wrap; padding: 8rpx; }
|
||||
.cell { width: 36rpx; height: 36rpx; margin: 2rpx; background: #E8ECF2; color: #1677FF; font-size: 18rpx; font-weight: 700; display: flex; align-items: center; justify-content: center; border-radius: 6rpx; }
|
||||
.grid { width: 96rpx; height: 96rpx; background: #E8F1FF; border-radius: 22rpx; display: flex; flex-wrap: wrap; padding: 8rpx; box-sizing: border-box; }
|
||||
.cell { width: 36rpx; height: 36rpx; margin: 2rpx; background: #fff; color: #1677FF; font-size: 18rpx; font-weight: 700; display: flex; align-items: center; justify-content: center; border-radius: 8rpx; overflow: hidden; }
|
||||
.cell-img { width: 36rpx; height: 36rpx; display: block; }
|
||||
.meta { margin-left: 20rpx; }
|
||||
.gn { font-size: 32rpx; font-weight: 700; display: block; }
|
||||
.gc { font-size: 22rpx; color: #6B7280; }
|
||||
@@ -114,11 +120,13 @@ export default {
|
||||
.cap { padding: 20rpx 24rpx 8rpx; font-size: 26rpx; font-weight: 700; }
|
||||
.mems { display: flex; flex-wrap: wrap; padding: 8rpx 16rpx 24rpx; }
|
||||
.mem { width: 20%; display: flex; flex-direction: column; align-items: center; margin-bottom: 16rpx; }
|
||||
.mav { width: 80rpx; height: 80rpx; border-radius: 16rpx; background: #E8ECF2; color: #00174b; display: flex; align-items: center; justify-content: center; font-weight: 700; }
|
||||
.mav { width: 80rpx; height: 80rpx; border-radius: 18rpx; background: #E8F1FF; color: #1677FF; display: flex; align-items: center; justify-content: center; font-weight: 700; overflow: hidden; }
|
||||
.mav.img { display: block; }
|
||||
.mn { font-size: 20rpx; margin-top: 8rpx; }
|
||||
.row { display: flex; justify-content: space-between; padding: 28rpx 24rpx; font-size: 28rpx; border-bottom: 1rpx solid #E8ECF2; }
|
||||
.row { display: flex; justify-content: space-between; align-items: center; padding: 28rpx 24rpx; font-size: 28rpx; border-bottom: 1rpx solid #F0F2F5; }
|
||||
.row:last-child { border-bottom: none; }
|
||||
.val { color: #6B7280; }
|
||||
.sw { color: #6B7280; }
|
||||
.sw.on { color: #07c160; font-weight: 600; }
|
||||
.danger { color: #fa5151; justify-content: center; border-bottom: none; }
|
||||
.sw { color: #9CA3AF; }
|
||||
.sw.on { color: #1677FF; font-weight: 600; }
|
||||
.danger { color: #DC2626; justify-content: center; }
|
||||
</style>
|
||||
|
||||
@@ -56,10 +56,10 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search { padding: 16rpx 24rpx; }
|
||||
.search input { height: 72rpx; background: #ededed; border-radius: 8rpx; padding: 0 24rpx; }
|
||||
.empty { text-align: center; color: #888; padding: 80rpx 24rpx; font-size: 26rpx; }
|
||||
.item { margin: 0 24rpx; padding: 20rpx 0; border-bottom: 1rpx solid #f0f0f0; }
|
||||
.who { display: block; font-size: 22rpx; color: #888; }
|
||||
.txt { display: block; font-size: 28rpx; margin-top: 6rpx; }
|
||||
.search { padding: 8rpx 24rpx 12rpx; }
|
||||
.search input { height: 72rpx; background: #fff; border-radius: 12rpx; padding: 0 24rpx; font-size: 28rpx; color: #111827; border: 1rpx solid rgba(17, 24, 39, 0.06); }
|
||||
.empty { text-align: center; color: #6B7280; padding: 120rpx 40rpx; font-size: 28rpx; }
|
||||
.item { margin: 0 24rpx; padding: 22rpx 0; border-bottom: 1rpx solid #F0F2F5; }
|
||||
.who { display: block; font-size: 22rpx; color: #6B7280; }
|
||||
.txt { display: block; font-size: 28rpx; margin-top: 8rpx; color: #111827; }
|
||||
</style>
|
||||
|
||||
@@ -105,15 +105,16 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.head { margin: 24rpx; padding: 28rpx; display: flex; align-items: center; }
|
||||
.av { width: 96rpx; height: 96rpx; border-radius: 12rpx; background: #1677FF; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 36rpx; font-weight: 700; overflow: hidden; }
|
||||
.av { width: 96rpx; height: 96rpx; border-radius: 22rpx; background: #E8F1FF; color: #1677FF; display: flex; align-items: center; justify-content: center; font-size: 36rpx; font-weight: 700; overflow: hidden; }
|
||||
.av.img { width: 96rpx; height: 96rpx; }
|
||||
.meta { flex: 1; margin-left: 20rpx; }
|
||||
.n { display: block; font-size: 32rpx; font-weight: 700; }
|
||||
.s { font-size: 22rpx; color: #6B7280; }
|
||||
.arr { color: #D1D5DB; }
|
||||
.card { margin: 0 24rpx 24rpx; overflow: hidden; }
|
||||
.row { display: flex; justify-content: space-between; padding: 28rpx 24rpx; font-size: 28rpx; border-bottom: 1rpx solid #f0f0f0; }
|
||||
.sw { color: #6B7280; }
|
||||
.sw.on { color: #07c160; font-weight: 600; }
|
||||
.danger { color: #fa5151; justify-content: center; border-bottom: none; }
|
||||
.row { display: flex; justify-content: space-between; align-items: center; padding: 28rpx 24rpx; font-size: 28rpx; border-bottom: 1rpx solid #F0F2F5; }
|
||||
.row:last-child { border-bottom: none; }
|
||||
.sw { color: #9CA3AF; }
|
||||
.sw.on { color: #1677FF; font-weight: 600; }
|
||||
.danger { color: #DC2626; justify-content: center; }
|
||||
</style>
|
||||
|
||||
@@ -1,36 +1,33 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="page edge">
|
||||
<fy-header title="工作通知" back />
|
||||
<fy-scroll class="edge-scroll" @refresh="load">
|
||||
<view class="bar">
|
||||
<text>审批 · 协同 · 投标 · 用章 · 公告</text>
|
||||
<text class="badge">{{ items.length }} 项待办</text>
|
||||
<text class="count">{{ items.length }} 项待办</text>
|
||||
</view>
|
||||
<view v-if="!items.length" class="empty">暂无待办通知</view>
|
||||
<wd-status-tip v-if="!items.length" image="content" tip="暂无待办通知" />
|
||||
<view v-for="t in items" :key="t.no || t.id" class="card item" @click="open(t)">
|
||||
<view class="stripe" />
|
||||
<view class="body">
|
||||
<view class="top">
|
||||
<text class="kind">{{ t.typeLabel || t.tag || t.type || '待办' }}</text>
|
||||
<text class="time">{{ t.createdAt || t.time || '' }}</text>
|
||||
</view>
|
||||
<view class="main">
|
||||
<text class="type">{{ t.typeLabel || t.tag || t.type || '待办' }}</text>
|
||||
<text class="title">{{ t.title || t.summary || t.no }}</text>
|
||||
<text class="sub">{{ t.submitter || t.applicant || '' }}{{ t.dept ? ' · ' + t.dept : '' }}</text>
|
||||
<view class="acts">
|
||||
<text class="go">去办理</text>
|
||||
</view>
|
||||
<text class="sub">{{ metaOf(t) }}</text>
|
||||
</view>
|
||||
<text class="arr">›</text>
|
||||
</view>
|
||||
</fy-scroll>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FyHeader from '../../components/fy-header.vue'
|
||||
import FyScroll from '../../components/fy-scroll.vue'
|
||||
import { store } from '../../utils/store.js'
|
||||
import { asList, fmtTime } from '../../utils/format.js'
|
||||
import { oaGet } from '../../utils/api.js'
|
||||
|
||||
export default {
|
||||
components: { FyHeader },
|
||||
components: { FyHeader, FyScroll },
|
||||
data() {
|
||||
return { items: [] }
|
||||
},
|
||||
@@ -51,6 +48,11 @@ export default {
|
||||
const conv = store.conversations.find((c) => c.type === 'work')
|
||||
if (conv) conv.unread = 0
|
||||
},
|
||||
metaOf(t) {
|
||||
const who = [t.submitter || t.applicant, t.dept].filter(Boolean).join(' · ')
|
||||
const time = t.createdAt || t.time || ''
|
||||
return [who, time].filter(Boolean).join(' · ')
|
||||
},
|
||||
open(t) {
|
||||
const no = t.no || t.id || ''
|
||||
if (String(t.source || t.type || '') === 'work' || t.taskId) {
|
||||
@@ -64,16 +66,12 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bar { margin: 16rpx 24rpx; background: #F3F6FB; border-radius: 16rpx; padding: 16rpx 20rpx; display: flex; justify-content: space-between; font-size: 22rpx; color: #4B5563; }
|
||||
.item { margin: 0 24rpx 20rpx; display: flex; overflow: hidden; }
|
||||
.stripe { width: 8rpx; background: #1677FF; }
|
||||
.body { flex: 1; padding: 24rpx; }
|
||||
.top { display: flex; justify-content: space-between; }
|
||||
.kind { font-size: 28rpx; font-weight: 700; }
|
||||
.time { font-size: 22rpx; color: #6B7280; }
|
||||
.title { display: block; font-size: 28rpx; font-weight: 600; margin: 12rpx 0 6rpx; }
|
||||
.sub { font-size: 22rpx; color: #6B7280; }
|
||||
.acts { margin-top: 16rpx; }
|
||||
.go { color: #1677FF; font-size: 24rpx; font-weight: 600; }
|
||||
.empty { text-align: center; color: #6B7280; padding: 80rpx; }
|
||||
.bar { margin: 16rpx 24rpx 8rpx; padding: 0 8rpx; display: flex; justify-content: space-between; align-items: center; font-size: 22rpx; color: #6B7280; }
|
||||
.count { color: #1677FF; font-weight: 600; }
|
||||
.item { margin: 16rpx 24rpx; padding: 24rpx; display: flex; align-items: center; }
|
||||
.main { flex: 1; min-width: 0; }
|
||||
.type { display: inline-block; font-size: 22rpx; color: #1677FF; background: #E8F1FF; border-radius: 8rpx; padding: 2rpx 10rpx; }
|
||||
.title { display: block; margin-top: 10rpx; font-size: 28rpx; font-weight: 650; color: #111827; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.sub { display: block; margin-top: 6rpx; font-size: 24rpx; color: #6B7280; }
|
||||
.arr { color: #C4C9D4; font-size: 32rpx; margin-left: 8rpx; }
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<fy-header title="同事资料" back />
|
||||
<view v-if="p" class="body">
|
||||
<wd-status-tip v-if="!p" image="content" tip="未找到这位同事" />
|
||||
<view v-else class="body">
|
||||
<view class="card profile">
|
||||
<image v-if="p.avatar" class="av img" :src="p.avatar" mode="aspectFill" />
|
||||
<view v-else class="av">{{ p.name.slice(0, 1) }}</view>
|
||||
@@ -108,22 +109,23 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.body { padding: 24rpx; }
|
||||
.profile { padding: 28rpx; display: flex; align-items: center; }
|
||||
.av { width: 128rpx; height: 128rpx; border-radius: 20rpx; background: #1677FF; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 48rpx; font-weight: 700; }
|
||||
.av.img { border-radius: 20rpx; }
|
||||
.info { margin-left: 24rpx; flex: 1; min-width: 0; }
|
||||
.body { padding: 16rpx 24rpx 48rpx; }
|
||||
.profile { padding: 32rpx 24rpx; display: flex; align-items: center; }
|
||||
.av { width: 112rpx; height: 112rpx; border-radius: 24rpx; background: #E8F1FF; color: #1677FF; display: flex; align-items: center; justify-content: center; font-size: 44rpx; font-weight: 650; }
|
||||
.av.img { border-radius: 24rpx; }
|
||||
.info { margin-left: 20rpx; flex: 1; min-width: 0; }
|
||||
.nm { display: flex; align-items: center; gap: 12rpx; }
|
||||
.n { font-size: 36rpx; font-weight: 700; }
|
||||
.tag { font-size: 20rpx; background: #E8F1FF; color: #4B5563; padding: 2rpx 10rpx; border-radius: 8rpx; }
|
||||
.s, .ph { display: block; font-size: 24rpx; color: #6B7280; margin-top: 8rpx; }
|
||||
.n { font-size: 36rpx; font-weight: 650; color: #111827; }
|
||||
.tag { font-size: 22rpx; background: #E8F8EF; color: #16A34A; padding: 2rpx 10rpx; border-radius: 8rpx; }
|
||||
.s, .ph { display: block; font-size: 24rpx; color: #6B7280; margin-top: 6rpx; }
|
||||
.copy { color: #1677FF; margin-left: 12rpx; }
|
||||
.acts { display: flex; gap: 16rpx; margin: 20rpx 0; }
|
||||
.act { flex: 1; background: #fff; border-radius: 16rpx; padding: 20rpx 8rpx; display: flex; flex-direction: column; align-items: center; font-size: 22rpx; font-weight: 600; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.03); }
|
||||
.act { flex: 1; background: #fff; border-radius: 20rpx; border: 1rpx solid rgba(17,24,39,0.06); padding: 20rpx 8rpx; display: flex; flex-direction: column; align-items: center; font-size: 22rpx; font-weight: 600; color: #111827; }
|
||||
.ai { font-size: 36rpx; color: #1677FF; margin-bottom: 8rpx; }
|
||||
.rows { overflow: hidden; margin-bottom: 20rpx; }
|
||||
.row { display: flex; justify-content: space-between; padding: 28rpx 24rpx; font-size: 28rpx; border-bottom: 1rpx solid #E8ECF2; }
|
||||
.k { color: #4B5563; }
|
||||
.row { display: flex; justify-content: space-between; align-items: center; padding: 28rpx 24rpx; font-size: 28rpx; }
|
||||
.row + .row { border-top: 1rpx solid #F0F2F5; }
|
||||
.k { color: #6B7280; }
|
||||
.v { color: #111827; }
|
||||
.brand { color: #1677FF; }
|
||||
.brand { color: #1677FF; font-weight: 600; }
|
||||
</style>
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
<text class="lab">说明</text>
|
||||
<view class="box tall"><textarea v-model="content" /></view>
|
||||
</view>
|
||||
<view class="btn-primary" @click="ok">创建</view>
|
||||
<view class="submit">
|
||||
<wd-button type="primary" block size="large" @click="ok">创建</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -65,10 +67,11 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.body { padding: 24rpx; }
|
||||
.field { margin-bottom: 20rpx; }
|
||||
.lab { font-size: 24rpx; color: #4B5563; display: block; margin-bottom: 8rpx; }
|
||||
.box { min-height: 88rpx; background: #fff; border-radius: 16rpx; padding: 20rpx 24rpx; }
|
||||
.box.tall { min-height: 160rpx; }
|
||||
.box input, .box textarea { width: 100%; }
|
||||
.body { padding: 16rpx 24rpx 48rpx; }
|
||||
.field { margin-bottom: 24rpx; }
|
||||
.lab { font-size: 26rpx; color: #6B7280; display: block; margin-bottom: 10rpx; }
|
||||
.box { min-height: 92rpx; background: #fff; border-radius: 16rpx; padding: 22rpx 24rpx; border: 1rpx solid rgba(17,24,39,0.06); }
|
||||
.box.tall { min-height: 180rpx; }
|
||||
.box input, .box textarea { width: 100%; font-size: 30rpx; color: #111827; }
|
||||
.submit { margin-top: 8rpx; }
|
||||
</style>
|
||||
|
||||
@@ -3,10 +3,20 @@
|
||||
<fy-header title="协同详情" back />
|
||||
<view v-if="row" class="body">
|
||||
<view class="card">
|
||||
<view class="top">
|
||||
<text class="type">协同</text>
|
||||
<wd-tag :type="row.status === 'done' ? 'success' : 'primary'" plain>{{ row.status === 'done' ? '已办结' : '待办理' }}</wd-tag>
|
||||
</view>
|
||||
<text class="t">{{ row.title }}</text>
|
||||
<text class="s">发起 {{ row.assignerName || '' }} · 办理 {{ row.assigneeName || '' }}</text>
|
||||
<text class="s">{{ row.content || row.remark || '' }}</text>
|
||||
<text class="chip">{{ row.status === 'done' ? '已办结' : '待办理' }}</text>
|
||||
<view class="kv">
|
||||
<text class="k">发起</text>
|
||||
<text class="v">{{ row.assignerName || '-' }}</text>
|
||||
</view>
|
||||
<view class="kv">
|
||||
<text class="k">办理</text>
|
||||
<text class="v">{{ row.assigneeName || '-' }}</text>
|
||||
</view>
|
||||
<text v-if="row.content || row.remark" class="c">{{ row.content || row.remark }}</text>
|
||||
</view>
|
||||
<view v-if="canDone" class="btn-primary" @click="done">标记已办理</view>
|
||||
</view>
|
||||
@@ -55,9 +65,14 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.body { padding: 24rpx; }
|
||||
.body { padding: 16rpx 24rpx 48rpx; }
|
||||
.card { padding: 28rpx; margin-bottom: 24rpx; }
|
||||
.t { display: block; font-size: 34rpx; font-weight: 700; }
|
||||
.s { display: block; font-size: 24rpx; color: #6B7280; margin-top: 8rpx; }
|
||||
.chip { display: inline-block; margin-top: 16rpx; }
|
||||
.top { display: flex; align-items: center; justify-content: space-between; }
|
||||
.type { font-size: 22rpx; color: #1677FF; background: #E8F1FF; border-radius: 8rpx; padding: 2rpx 10rpx; }
|
||||
.t { display: block; margin-top: 16rpx; font-size: 34rpx; font-weight: 650; color: #111827; line-height: 1.4; }
|
||||
.kv { display: flex; justify-content: space-between; padding: 20rpx 0; }
|
||||
.kv + .kv { border-top: 1rpx solid #F0F2F5; }
|
||||
.k { color: #6B7280; font-size: 26rpx; }
|
||||
.v { font-size: 26rpx; color: #111827; }
|
||||
.c { display: block; margin-top: 8rpx; padding-top: 16rpx; border-top: 1rpx solid #F0F2F5; font-size: 28rpx; color: #374151; line-height: 1.65; }
|
||||
</style>
|
||||
|
||||
@@ -7,20 +7,24 @@
|
||||
</fy-header>
|
||||
<fy-scroll class="edge-scroll" @refresh="load">
|
||||
<view class="sec">待我办理</view>
|
||||
<view v-if="!pending.length" class="empty">暂无</view>
|
||||
<wd-status-tip v-if="!pending.length" image="content" tip="暂无待办" />
|
||||
<view v-for="t in pending" :key="t.id" class="card item" @click="open(t)">
|
||||
<view class="stripe" />
|
||||
<view class="body">
|
||||
<view class="main">
|
||||
<text class="type">待办理</text>
|
||||
<text class="t">{{ t.title }}</text>
|
||||
<text class="s">{{ t.assignerName || '' }} · {{ t.dueAt || '' }}</text>
|
||||
<text class="s">{{ lineOf(t.assignerName, t.dueAt) }}</text>
|
||||
</view>
|
||||
<text class="arr">›</text>
|
||||
</view>
|
||||
<view class="sec">我发起的</view>
|
||||
<wd-status-tip v-if="!mine.length" image="content" tip="暂无发起" />
|
||||
<view v-for="t in mine" :key="'m' + t.id" class="card item" @click="open(t)">
|
||||
<view class="body wide">
|
||||
<view class="main">
|
||||
<text class="type">协同</text>
|
||||
<text class="t">{{ t.title }}</text>
|
||||
<text class="s">{{ t.assigneeName || '' }} · {{ t.status || '' }}</text>
|
||||
<text class="s">{{ lineOf(t.assigneeName, t.status) }}</text>
|
||||
</view>
|
||||
<text class="arr">›</text>
|
||||
</view>
|
||||
</fy-scroll>
|
||||
</view>
|
||||
@@ -49,6 +53,9 @@ export default {
|
||||
this.mine = asList(await oaGet('/work-tasks?tab=review', store.token))
|
||||
} catch (_) {}
|
||||
},
|
||||
lineOf(a, b) {
|
||||
return [a, b].filter(Boolean).join(' · ')
|
||||
},
|
||||
open(t) {
|
||||
uni.navigateTo({ url: '/pages/coop/detail?id=' + encodeURIComponent(t.id) })
|
||||
},
|
||||
@@ -60,13 +67,12 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sec { padding: 24rpx 24rpx 8rpx; font-size: 28rpx; font-weight: 700; }
|
||||
.item { margin: 12rpx 24rpx; display: flex; overflow: hidden; }
|
||||
.stripe { width: 8rpx; background: #1677FF; }
|
||||
.body { flex: 1; padding: 24rpx; }
|
||||
.body.wide { padding: 24rpx; }
|
||||
.t { display: block; font-size: 28rpx; font-weight: 600; }
|
||||
.s { font-size: 22rpx; color: #6B7280; }
|
||||
.empty { padding: 24rpx; text-align: center; color: #6B7280; }
|
||||
.add { color: #1677FF; font-size: 26rpx; font-weight: 700; padding: 8rpx; }
|
||||
.sec { padding: 24rpx 32rpx 4rpx; font-size: 26rpx; font-weight: 650; color: #6B7280; }
|
||||
.item { margin: 16rpx 24rpx; padding: 24rpx; display: flex; align-items: center; }
|
||||
.main { flex: 1; min-width: 0; }
|
||||
.type { display: inline-block; font-size: 22rpx; color: #1677FF; background: #E8F1FF; border-radius: 8rpx; padding: 2rpx 10rpx; }
|
||||
.t { display: block; margin-top: 10rpx; font-size: 28rpx; font-weight: 650; color: #111827; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.s { display: block; margin-top: 6rpx; font-size: 24rpx; color: #6B7280; }
|
||||
.arr { color: #C4C9D4; font-size: 32rpx; margin-left: 8rpx; }
|
||||
.add { color: #1677FF; font-size: 26rpx; font-weight: 650; padding: 8rpx; }
|
||||
</style>
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="page edge">
|
||||
<fy-header :title="title" back />
|
||||
<view v-if="!allowed" class="deny">当前账号没有「{{ title }}」查看权限</view>
|
||||
<view v-else>
|
||||
<scroll-view scroll-x class="tabs">
|
||||
<text v-for="t in tabs" :key="t.id" class="tab" :class="{ on: tab === t.id }" @click="switchTab(t)">{{ t.title }}</text>
|
||||
</scroll-view>
|
||||
<view v-if="loading" class="empty">加载中…</view>
|
||||
<view v-else-if="!rows.length" class="empty">暂无数据</view>
|
||||
<fy-scroll v-else class="edge-scroll" @refresh="load">
|
||||
<wd-tabs v-if="tabs.length" v-model="tab" slidable="always" @change="onTab">
|
||||
<wd-tab v-for="t in tabs" :key="t.id" :title="t.title" :name="t.id" />
|
||||
</wd-tabs>
|
||||
<view v-if="loading" class="empty">
|
||||
<wd-skeleton theme="paragraph" />
|
||||
</view>
|
||||
<wd-status-tip v-else-if="!rows.length" image="content" tip="暂无数据" />
|
||||
<view v-for="r in rows" :key="r.id || r.no || r.code" class="card item">
|
||||
<view class="main">
|
||||
<text class="t">{{ titleOf(r) }}</text>
|
||||
<text class="s">{{ subOf(r) }}</text>
|
||||
<text v-if="subOf(r)" class="s">{{ subOf(r) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</fy-scroll>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FyHeader from '../../components/fy-header.vue'
|
||||
import FyScroll from '../../components/fy-scroll.vue'
|
||||
import { store } from '../../utils/store.js'
|
||||
import { ERP_MODULES, ERP_TABS } from '../../utils/catalog.js'
|
||||
import { asList, firstText } from '../../utils/format.js'
|
||||
@@ -25,7 +30,7 @@ import { oaGet } from '../../utils/api.js'
|
||||
import { canMenu } from '../../utils/perms.js'
|
||||
|
||||
export default {
|
||||
components: { FyHeader },
|
||||
components: { FyHeader, FyScroll },
|
||||
data() {
|
||||
return { id: '', tab: '', rows: [], loading: false }
|
||||
},
|
||||
@@ -45,6 +50,12 @@ export default {
|
||||
if (this.allowed) this.load()
|
||||
},
|
||||
methods: {
|
||||
onTab(e) {
|
||||
const id = (e && e.name) || this.tab
|
||||
if (id === this.tab && this.rows.length) return
|
||||
this.tab = id
|
||||
this.load()
|
||||
},
|
||||
switchTab(t) {
|
||||
this.tab = t.id
|
||||
this.load()
|
||||
@@ -110,11 +121,10 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tabs { white-space: nowrap; padding: 16rpx 16rpx 0; }
|
||||
.tab { display: inline-block; padding: 12rpx 20rpx; font-size: 24rpx; color: #6B7280; }
|
||||
.tab.on { color: #1677FF; font-weight: 700; border-bottom: 4rpx solid #1677FF; }
|
||||
.item { margin: 16rpx 24rpx; padding: 24rpx; }
|
||||
.t { display: block; font-weight: 600; font-size: 28rpx; }
|
||||
.s { font-size: 22rpx; color: #6B7280; }
|
||||
.empty, .deny { padding: 80rpx 40rpx; text-align: center; color: #6B7280; }
|
||||
.main { min-width: 0; }
|
||||
.t { display: block; font-weight: 650; font-size: 28rpx; color: #111827; }
|
||||
.s { display: block; margin-top: 8rpx; font-size: 24rpx; color: #6B7280; }
|
||||
.empty { padding: 40rpx 28rpx; }
|
||||
.deny { padding: 120rpx 40rpx; text-align: center; color: #6B7280; font-size: 28rpx; }
|
||||
</style>
|
||||
|
||||
@@ -1,46 +1,91 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="page edge">
|
||||
<fy-header title="我的资料" back />
|
||||
<view class="card">
|
||||
<fy-scroll class="edge-scroll" @refresh="load">
|
||||
<view class="head">
|
||||
<view class="av">{{ initial }}</view>
|
||||
<view class="meta">
|
||||
<text class="name">{{ name }}</text>
|
||||
<text class="sub">{{ deptLine }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="group">
|
||||
<view v-for="r in rows" :key="r.k" class="row">
|
||||
<text class="k">{{ r.k }}</text>
|
||||
<text class="v">{{ r.v }}</text>
|
||||
<text class="v">{{ r.v || '-' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</fy-scroll>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FyHeader from '../../components/fy-header.vue'
|
||||
import FyScroll from '../../components/fy-scroll.vue'
|
||||
import { store } from '../../utils/store.js'
|
||||
import { oaGet } from '../../utils/api.js'
|
||||
|
||||
export default {
|
||||
components: { FyHeader },
|
||||
components: { FyHeader, FyScroll },
|
||||
data() { return { profile: {} } },
|
||||
computed: {
|
||||
me() { return store.me || {} },
|
||||
name() {
|
||||
return this.profile.realName || this.me.name || '未填写'
|
||||
},
|
||||
initial() { return this.name.slice(0, 1) },
|
||||
deptLine() {
|
||||
return [this.profile.dept || this.me.department, this.profile.role || this.me.title].filter(Boolean).join(' · ')
|
||||
},
|
||||
rows() {
|
||||
const m = store.me || {}
|
||||
const m = this.me
|
||||
const p = this.profile || {}
|
||||
return [
|
||||
{ k: '姓名', v: p.realName || m.name },
|
||||
{ k: '手机', v: p.phone || m.phone },
|
||||
{ k: '部门', v: p.dept || m.department },
|
||||
{ k: '职务', v: p.role || m.title },
|
||||
{ k: '工号', v: p.jobNo || m.jobId || '-' },
|
||||
{ k: '工号', v: p.jobNo || m.jobId },
|
||||
{ k: '状态', v: p.employeeStatus || m.employeeStatus || '在职' }
|
||||
]
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
onShow() { this.load() },
|
||||
methods: {
|
||||
load() {
|
||||
oaGet('/profile', store.token).then((d) => { this.profile = d || {} }).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card { margin: 24rpx; overflow: hidden; }
|
||||
.row { display: flex; justify-content: space-between; padding: 28rpx 24rpx; border-bottom: 1rpx solid #E8ECF2; }
|
||||
.k { color: #6B7280; font-size: 26rpx; }
|
||||
.v { font-size: 26rpx; }
|
||||
.head {
|
||||
margin: 16rpx 24rpx 20rpx;
|
||||
padding: 32rpx 24rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid rgba(17, 24, 39, 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.av {
|
||||
width: 96rpx; height: 96rpx; border-radius: 22rpx;
|
||||
background: #E8F1FF; color: #1677FF;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 40rpx; font-weight: 650; flex-shrink: 0;
|
||||
}
|
||||
.meta { margin-left: 20rpx; min-width: 0; }
|
||||
.name { display: block; font-size: 34rpx; font-weight: 650; color: #111827; }
|
||||
.sub { display: block; margin-top: 6rpx; font-size: 24rpx; color: #6B7280; }
|
||||
.group {
|
||||
margin: 0 24rpx 32rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid rgba(17, 24, 39, 0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
.row { display: flex; justify-content: space-between; align-items: center; padding: 28rpx 24rpx; gap: 24rpx; }
|
||||
.row + .row { border-top: 1rpx solid #F0F2F5; }
|
||||
.k { color: #6B7280; font-size: 28rpx; flex-shrink: 0; }
|
||||
.v { font-size: 28rpx; color: #111827; text-align: right; }
|
||||
</style>
|
||||
|
||||
@@ -1,32 +1,51 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="page edge">
|
||||
<fy-header title="工资条" back />
|
||||
<view v-if="!rows.length" class="empty">暂无工资条</view>
|
||||
<fy-scroll class="edge-scroll" @refresh="load">
|
||||
<wd-status-tip v-if="!rows.length" image="content" tip="暂无工资条" />
|
||||
<view v-for="r in rows" :key="r.id || r.month" class="card item">
|
||||
<view class="main">
|
||||
<text class="t">{{ r.month || r.period || '工资条' }}</text>
|
||||
<text class="s">应发 {{ r.gross || r.shouldPay || '-' }} · 实发 {{ r.net || r.actual || '-' }}</text>
|
||||
<text class="s">应发 {{ money(r.gross || r.shouldPay) }}</text>
|
||||
</view>
|
||||
<view class="amt">
|
||||
<text class="lab">实发</text>
|
||||
<text class="num">{{ money(r.net || r.actual) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</fy-scroll>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FyHeader from '../../components/fy-header.vue'
|
||||
import FyScroll from '../../components/fy-scroll.vue'
|
||||
import { store } from '../../utils/store.js'
|
||||
import { asList } from '../../utils/format.js'
|
||||
import { oaGet } from '../../utils/api.js'
|
||||
|
||||
export default {
|
||||
components: { FyHeader },
|
||||
components: { FyHeader, FyScroll },
|
||||
data() { return { rows: [] } },
|
||||
onShow() {
|
||||
onShow() { this.load() },
|
||||
methods: {
|
||||
load() {
|
||||
oaGet('/profile/payslips', store.token).then((d) => { this.rows = asList(d) }).catch(() => { this.rows = [] })
|
||||
},
|
||||
money(v) {
|
||||
if (v === 0) return '0'
|
||||
return v == null || v === '' ? '-' : String(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.item { margin: 16rpx 24rpx; padding: 24rpx; }
|
||||
.t { display: block; font-weight: 600; }
|
||||
.s { font-size: 24rpx; color: #6B7280; }
|
||||
.empty { padding: 80rpx; text-align: center; color: #6B7280; }
|
||||
.item { margin: 16rpx 24rpx; padding: 28rpx 24rpx; display: flex; align-items: center; }
|
||||
.main { flex: 1; min-width: 0; }
|
||||
.t { display: block; font-size: 30rpx; font-weight: 650; color: #111827; }
|
||||
.s { display: block; margin-top: 8rpx; font-size: 24rpx; color: #6B7280; }
|
||||
.amt { text-align: right; margin-left: 16rpx; }
|
||||
.lab { display: block; font-size: 22rpx; color: #6B7280; }
|
||||
.num { display: block; margin-top: 4rpx; font-size: 32rpx; font-weight: 700; color: #1677FF; }
|
||||
</style>
|
||||
|
||||
@@ -1,32 +1,45 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="page edge">
|
||||
<fy-header title="我的绩效" back />
|
||||
<view v-if="!rows.length" class="empty">暂无绩效记录</view>
|
||||
<fy-scroll class="edge-scroll" @refresh="load">
|
||||
<wd-status-tip v-if="!rows.length" image="content" tip="暂无绩效记录" />
|
||||
<view v-for="r in rows" :key="r.id || r.period" class="card item">
|
||||
<view class="main">
|
||||
<text class="t">{{ r.period || r.quarter || '绩效' }}</text>
|
||||
<text class="s">评级 {{ r.grade || r.level || '-' }} · {{ r.score || '' }}</text>
|
||||
<text v-if="scoreOf(r)" class="s">得分 {{ scoreOf(r) }}</text>
|
||||
</view>
|
||||
<text class="chip">{{ r.grade || r.level || '-' }}</text>
|
||||
</view>
|
||||
</fy-scroll>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FyHeader from '../../components/fy-header.vue'
|
||||
import FyScroll from '../../components/fy-scroll.vue'
|
||||
import { store } from '../../utils/store.js'
|
||||
import { asList } from '../../utils/format.js'
|
||||
import { oaGet } from '../../utils/api.js'
|
||||
|
||||
export default {
|
||||
components: { FyHeader },
|
||||
components: { FyHeader, FyScroll },
|
||||
data() { return { rows: [] } },
|
||||
onShow() {
|
||||
onShow() { this.load() },
|
||||
methods: {
|
||||
load() {
|
||||
oaGet('/profile/performance', store.token).then((d) => { this.rows = asList(d) }).catch(() => { this.rows = [] })
|
||||
},
|
||||
scoreOf(r) {
|
||||
if (r.score == null || r.score === '') return ''
|
||||
return String(r.score)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.item { margin: 16rpx 24rpx; padding: 24rpx; }
|
||||
.t { display: block; font-weight: 600; }
|
||||
.s { font-size: 24rpx; color: #6B7280; }
|
||||
.empty { padding: 80rpx; text-align: center; color: #6B7280; }
|
||||
.item { margin: 16rpx 24rpx; padding: 28rpx 24rpx; display: flex; align-items: center; }
|
||||
.main { flex: 1; min-width: 0; }
|
||||
.t { display: block; font-size: 30rpx; font-weight: 650; color: #111827; }
|
||||
.s { display: block; margin-top: 8rpx; font-size: 24rpx; color: #6B7280; }
|
||||
</style>
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
<fy-scroll class="edge-scroll" @refresh="load">
|
||||
<wd-status-tip v-if="!rows.length" image="content" tip="暂无公告" />
|
||||
<view v-for="r in rows" :key="r.id" class="card item">
|
||||
<text class="t">{{ r.title }}</text>
|
||||
<view class="top">
|
||||
<text class="chip">公告</text>
|
||||
<text class="s">{{ r.createdAt || r.publishAt || '' }}</text>
|
||||
</view>
|
||||
<text class="t">{{ r.title }}</text>
|
||||
<text class="c">{{ r.content || r.summary || '' }}</text>
|
||||
</view>
|
||||
</fy-scroll>
|
||||
@@ -32,9 +35,9 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.item { margin: 16rpx 28rpx; padding: 28rpx; }
|
||||
.t { display: block; font-weight: 650; font-size: 30rpx; line-height: 1.45; }
|
||||
.s { display: block; font-size: 24rpx; color: #6B7280; margin-top: 10rpx; }
|
||||
.c { display: block; font-size: 28rpx; margin-top: 16rpx; color: #374151; line-height: 1.65; }
|
||||
.empty { padding: 120rpx 40rpx; text-align: center; color: #6B7280; font-size: 28rpx; }
|
||||
.item { margin: 16rpx 24rpx; padding: 28rpx 24rpx; }
|
||||
.top { display: flex; align-items: center; justify-content: space-between; }
|
||||
.s { font-size: 22rpx; color: #9CA3AF; }
|
||||
.t { display: block; margin-top: 16rpx; font-weight: 650; font-size: 30rpx; line-height: 1.45; color: #111827; }
|
||||
.c { display: block; font-size: 26rpx; margin-top: 12rpx; color: #4B5563; line-height: 1.65; }
|
||||
</style>
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<fy-header title="修改密码" />
|
||||
<fy-header title="修改密码" back />
|
||||
<view class="body">
|
||||
<text class="hint">首次登录或管理员要求改密,请设置新密码后继续。</text>
|
||||
<view class="hero">
|
||||
<view class="mark">
|
||||
<wd-icon name="lock-on" size="44rpx" color="#1677FF" />
|
||||
</view>
|
||||
<text class="title">设置新密码</text>
|
||||
<text class="hint">首次登录或管理员要求改密时,请设置新密码后继续使用。</text>
|
||||
</view>
|
||||
<view class="card group">
|
||||
<view class="field">
|
||||
<text class="lab">新密码</text>
|
||||
<view class="box"><input v-model="next" password placeholder="至少 6 位" /></view>
|
||||
<view class="box">
|
||||
<input v-model="next" :password="!showNext" placeholder="至少 6 位" placeholder-class="ph" />
|
||||
<text class="eye" @click="showNext = !showNext">{{ showNext ? '隐藏' : '显示' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="field">
|
||||
<text class="lab">确认新密码</text>
|
||||
<view class="box"><input v-model="confirm" password placeholder="再输入一次" /></view>
|
||||
<view class="box" :class="{ bad: mismatch }">
|
||||
<input v-model="confirm" :password="!showConfirm" placeholder="再输入一次" placeholder-class="ph" />
|
||||
<text class="eye" @click="showConfirm = !showConfirm">{{ showConfirm ? '隐藏' : '显示' }}</text>
|
||||
</view>
|
||||
<view class="btn-primary" @click="ok">确认修改</view>
|
||||
<text v-if="mismatch" class="tip">两次输入不一致</text>
|
||||
</view>
|
||||
</view>
|
||||
<wd-button type="primary" block size="large" @click="ok">确认修改</wd-button>
|
||||
<text v-if="error" class="err">{{ error }}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -24,38 +39,80 @@ import { toast } from '../../utils/format.js'
|
||||
|
||||
export default {
|
||||
components: { FyHeader },
|
||||
computed: {
|
||||
mismatch() {
|
||||
return this.confirm.length > 0 && this.next !== this.confirm
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return { next: '', confirm: '', error: '', off: null, left: false }
|
||||
return { next: '', confirm: '', error: '', off: null, left: false, waiting: false, showNext: false, showConfirm: false }
|
||||
},
|
||||
onShow() {
|
||||
if (this.off) this.off()
|
||||
this.off = store.on(() => {
|
||||
this.error = store.error
|
||||
if (this.left || store.phase !== 'app') return
|
||||
this.left = true
|
||||
uni.switchTab({ url: '/pages/tabs/messages' })
|
||||
if (this.left || !this.waiting || store.phase !== 'app') return
|
||||
this.leave()
|
||||
})
|
||||
},
|
||||
onHide() {
|
||||
this.drop()
|
||||
},
|
||||
onUnload() {
|
||||
this.drop()
|
||||
},
|
||||
methods: {
|
||||
drop() {
|
||||
if (this.off) this.off()
|
||||
this.off = null
|
||||
},
|
||||
methods: {
|
||||
leave() {
|
||||
this.left = true
|
||||
this.waiting = false
|
||||
uni.switchTab({ url: '/pages/tabs/messages' })
|
||||
},
|
||||
async ok() {
|
||||
if (!this.next || this.next.length < 6) return toast('密码至少 6 位')
|
||||
if (this.next !== this.confirm) return toast('两次密码不一致')
|
||||
this.waiting = true
|
||||
await store.changePassword(this.next, this.confirm)
|
||||
if (store.error) this.waiting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.body { padding: 40rpx; }
|
||||
.hint { font-size: 26rpx; color: #6B7280; display: block; margin-bottom: 32rpx; }
|
||||
.field { margin-bottom: 24rpx; }
|
||||
.lab { font-size: 24rpx; color: #4B5563; display: block; margin-bottom: 8rpx; }
|
||||
.box { height: 96rpx; background: #fff; border-radius: 16rpx; padding: 0 24rpx; display: flex; align-items: center; }
|
||||
.box input { flex: 1; }
|
||||
.err { color: #DC2626; font-size: 24rpx; margin-top: 16rpx; display: block; }
|
||||
.body { padding: 32rpx 32rpx 48rpx; }
|
||||
.hero { display: flex; flex-direction: column; align-items: center; margin: 16rpx 0 36rpx; }
|
||||
.mark {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 28rpx;
|
||||
background: #E8F1FF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.title { font-size: 36rpx; font-weight: 650; color: #111827; }
|
||||
.hint { margin-top: 10rpx; font-size: 24rpx; color: #6B7280; line-height: 1.55; text-align: center; }
|
||||
.group { padding: 8rpx 28rpx 28rpx; margin-bottom: 32rpx; }
|
||||
.field { margin-top: 24rpx; }
|
||||
.lab { font-size: 26rpx; color: #374151; display: block; margin-bottom: 12rpx; }
|
||||
.box {
|
||||
height: 96rpx;
|
||||
background: #F5F7FA;
|
||||
border-radius: 16rpx;
|
||||
padding: 0 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1rpx solid transparent;
|
||||
}
|
||||
.box.bad { border-color: #FECACA; background: #FEF2F2; }
|
||||
.box input { flex: 1; font-size: 30rpx; color: #111827; }
|
||||
.ph { color: #9CA3AF; }
|
||||
.eye { font-size: 26rpx; color: #1677FF; padding-left: 16rpx; }
|
||||
.tip { display: block; margin-top: 10rpx; font-size: 22rpx; color: #DC2626; }
|
||||
.err { color: #DC2626; font-size: 26rpx; margin-top: 20rpx; display: block; text-align: center; }
|
||||
</style>
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
<view class="page">
|
||||
<fy-header title="附件预览" back />
|
||||
<view class="body">
|
||||
<view class="card sheet">
|
||||
<view class="mark">文</view>
|
||||
<text class="name">{{ name || '附件' }}</text>
|
||||
<view class="btn-primary" @click="open">打开 / 预览</view>
|
||||
<text class="hint">系统会用本机应用打开这个附件</text>
|
||||
</view>
|
||||
<wd-button type="primary" block size="large" @click="open">打开 / 预览</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -31,6 +35,14 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.body { padding: 48rpx 32rpx; }
|
||||
.name { display: block; font-size: 28rpx; margin-bottom: 32rpx; text-align: center; }
|
||||
.body { padding: 32rpx 24rpx; }
|
||||
.sheet { padding: 48rpx 32rpx; margin-bottom: 32rpx; display: flex; flex-direction: column; align-items: center; }
|
||||
.mark {
|
||||
width: 112rpx; height: 112rpx; border-radius: 28rpx;
|
||||
background: #E8F1FF; color: #1677FF;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 44rpx; font-weight: 650;
|
||||
}
|
||||
.name { display: block; margin-top: 24rpx; font-size: 30rpx; font-weight: 650; color: #111827; text-align: center; word-break: break-all; }
|
||||
.hint { display: block; margin-top: 12rpx; font-size: 24rpx; color: #6B7280; text-align: center; }
|
||||
</style>
|
||||
|
||||
@@ -1,32 +1,38 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="page edge">
|
||||
<fy-header title="工作汇报" back />
|
||||
<view v-if="!can" class="deny">当前账号没有写汇报权限</view>
|
||||
<view v-else>
|
||||
<fy-scroll v-else class="edge-scroll" @refresh="load">
|
||||
<view class="card form">
|
||||
<text class="lab">类型</text>
|
||||
<picker :range="kinds" @change="(e) => kind = kinds[e.detail.value]">
|
||||
<view class="box">{{ kind }}</view>
|
||||
</picker>
|
||||
<text class="lab">内容</text>
|
||||
<view class="box tall"><textarea v-model="content" placeholder="今日工作内容 / 周报摘要" /></view>
|
||||
<view class="btn-primary" @click="submit">提交汇报</view>
|
||||
<wd-button type="primary" block size="large" @click="submit">提交汇报</wd-button>
|
||||
</view>
|
||||
<wd-status-tip v-if="!rows.length" image="content" tip="暂无汇报" />
|
||||
<view v-for="r in rows" :key="r.id" class="card item">
|
||||
<text class="t">{{ r.title || r.kind || '汇报' }}</text>
|
||||
<view class="top">
|
||||
<text class="type">{{ r.kind || r.title || '汇报' }}</text>
|
||||
<text class="s">{{ r.createdAt || r.date || '' }}</text>
|
||||
<text class="c">{{ r.content || r.summary || '' }}</text>
|
||||
</view>
|
||||
<text class="c">{{ r.content || r.summary || r.title || '' }}</text>
|
||||
</view>
|
||||
</fy-scroll>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FyHeader from '../../components/fy-header.vue'
|
||||
import FyScroll from '../../components/fy-scroll.vue'
|
||||
import { store } from '../../utils/store.js'
|
||||
import { asList, toast } from '../../utils/format.js'
|
||||
import { oaGet, oaPost } from '../../utils/api.js'
|
||||
|
||||
export default {
|
||||
components: { FyHeader },
|
||||
components: { FyHeader, FyScroll },
|
||||
data() {
|
||||
return { kinds: ['日报', '周报', '月报', '工时'], kind: '日报', content: '', rows: [] }
|
||||
},
|
||||
@@ -58,12 +64,15 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form { margin: 24rpx; padding: 24rpx; }
|
||||
.box { background: #F3F6FB; border-radius: 12rpx; padding: 20rpx; margin-bottom: 16rpx; }
|
||||
.box.tall { min-height: 160rpx; }
|
||||
.form { margin: 16rpx 24rpx; padding: 24rpx; }
|
||||
.lab { display: block; font-size: 26rpx; color: #6B7280; margin-bottom: 10rpx; }
|
||||
.box { background: #F5F7FA; border-radius: 16rpx; padding: 22rpx 24rpx; margin-bottom: 20rpx; font-size: 30rpx; color: #111827; }
|
||||
.box.tall { min-height: 180rpx; }
|
||||
.box textarea { width: 100%; font-size: 30rpx; }
|
||||
.item { margin: 0 24rpx 16rpx; padding: 24rpx; }
|
||||
.t { display: block; font-weight: 600; }
|
||||
.s { font-size: 22rpx; color: #6B7280; }
|
||||
.c { display: block; font-size: 26rpx; margin-top: 8rpx; }
|
||||
.deny { padding: 80rpx; text-align: center; color: #6B7280; }
|
||||
.top { display: flex; align-items: center; justify-content: space-between; }
|
||||
.type { font-size: 22rpx; color: #1677FF; background: #E8F1FF; border-radius: 8rpx; padding: 2rpx 10rpx; }
|
||||
.s { font-size: 22rpx; color: #9CA3AF; }
|
||||
.c { display: block; font-size: 28rpx; margin-top: 14rpx; color: #374151; line-height: 1.65; }
|
||||
.deny { padding: 120rpx 40rpx; text-align: center; color: #6B7280; font-size: 28rpx; }
|
||||
</style>
|
||||
|
||||
@@ -4,39 +4,70 @@
|
||||
<fy-scroll class="edge-scroll" dock @refresh="reload">
|
||||
<view v-if="!canPunch" class="deny">当前账号没有打卡权限</view>
|
||||
<view v-else class="body">
|
||||
<view class="clock-box">
|
||||
<view class="day">
|
||||
<text class="date">{{ dateLabel }}</text>
|
||||
<text class="shift">弹性 09:00–09:30 / 18:00–18:30</text>
|
||||
</view>
|
||||
<view class="lieu">多出工时记调休 {{ lieuText }}</view>
|
||||
|
||||
<view class="slots">
|
||||
<view class="slot">
|
||||
<view class="dot" :class="{ on: inRec }" />
|
||||
<view class="slot-m">
|
||||
<text class="lab">上班 09:00–09:30</text>
|
||||
<text class="val" :class="{ ok: inRec }">{{ inRec ? (inRec.time || '') + ' 已打卡' : '未打卡' }}</text>
|
||||
</view>
|
||||
<text class="tiny">{{ inRec ? (inRec.status || '正常') : '' }}</text>
|
||||
</view>
|
||||
<view class="slot">
|
||||
<view class="dot" :class="{ on: outRec }" />
|
||||
<view class="slot-m">
|
||||
<text class="lab">下班 18:00–18:30</text>
|
||||
<text class="val" :class="{ ok: outRec }">{{ outRec ? (outRec.time || '') + ' 已打卡' : '未打卡' }}</text>
|
||||
</view>
|
||||
<text class="tiny">{{ outRec ? (outRec.status || '正常') : '' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="ring-wrap">
|
||||
<view class="ring" :class="{ busy: punching }" @click="doPunch">
|
||||
<text class="now">{{ now }}</text>
|
||||
<text class="shift">班次 {{ rules.workStart }}–{{ rules.workEnd }}{{ locName ? ' · ' + locName : '' }}</text>
|
||||
</view>
|
||||
<view class="grid">
|
||||
<view class="stat card">
|
||||
<text class="lab">上班</text>
|
||||
<text class="val" :class="{ ok: inRec }">{{ inRec ? inRec.time + ' 已打卡' : '未打卡' }}</text>
|
||||
<text class="tiny">{{ inRec ? (inRec.status || '正常') : '待打卡' }}</text>
|
||||
</view>
|
||||
<view class="stat card">
|
||||
<text class="lab">下班</text>
|
||||
<text class="val" :class="{ ok: outRec }">{{ outRec ? outRec.time + ' 已打卡' : '未打卡' }}</text>
|
||||
<text class="tiny">{{ outRec ? (outRec.status || '正常') : '待打卡' }}</text>
|
||||
<text class="act">{{ punchLabel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<wd-button type="primary" block size="large" :loading="punching" @click="doPunch">{{ outRec ? '更新打卡' : '打卡' }}</wd-button>
|
||||
<text class="geo">将使用手机定位校验打卡范围</text>
|
||||
|
||||
<view class="geo">
|
||||
<wd-icon name="location" size="28rpx" color="#1677FF" />
|
||||
<text>{{ locName || '将使用手机定位校验打卡范围' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="quick">
|
||||
<text class="q" @click="goKind('card')">补卡</text>
|
||||
<text class="q" @click="goKind('leave')">请假</text>
|
||||
<text class="q" @click="goKind('overtime')">加班</text>
|
||||
<text class="q" @click="goKind('outing')">外出</text>
|
||||
<view class="q" @click="goKind('card')">
|
||||
<wd-icon name="edit-1" size="40rpx" color="#1677FF" />
|
||||
<text>补卡</text>
|
||||
</view>
|
||||
<view class="sec">
|
||||
<text class="st">今日及近期记录</text>
|
||||
<view class="q" @click="goKind('leave')">
|
||||
<wd-icon name="calendar" size="40rpx" color="#1677FF" />
|
||||
<text>请假</text>
|
||||
</view>
|
||||
<view class="card">
|
||||
<view class="q" @click="goKind('overtime')">
|
||||
<wd-icon name="time" size="40rpx" color="#1677FF" />
|
||||
<text>加班</text>
|
||||
</view>
|
||||
<view class="q" @click="goKind('outing')">
|
||||
<wd-icon name="location" size="40rpx" color="#1677FF" />
|
||||
<text>外出</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="sec">打卡记录</view>
|
||||
<view class="card log">
|
||||
<view v-if="!records.length" class="empty">暂无记录</view>
|
||||
<view v-for="r in records" :key="r.id || r.time" class="rec">
|
||||
<view>
|
||||
<view class="rail" />
|
||||
<view class="rec-m">
|
||||
<text class="rt">{{ r.title || r.kind || '打卡' }} {{ r.time || r.punchTime || '' }}</text>
|
||||
<text class="rl">{{ r.location || r.address || '' }}</text>
|
||||
<text class="rl">{{ r.location || r.address || locName || '' }}</text>
|
||||
</view>
|
||||
<text class="ok">{{ r.status || '正常' }}</text>
|
||||
</view>
|
||||
@@ -50,7 +81,7 @@
|
||||
import FyHeader from '../../components/fy-header.vue'
|
||||
import FyScroll from '../../components/fy-scroll.vue'
|
||||
import { store } from '../../utils/store.js'
|
||||
import { clockText, toast } from '../../utils/format.js'
|
||||
import { clockText, flexCreditMinutes, flexCreditText, toast } from '../../utils/format.js'
|
||||
|
||||
export default {
|
||||
components: { FyHeader, FyScroll },
|
||||
@@ -61,6 +92,17 @@ export default {
|
||||
canPunch() { return store.can('personal', 'punch') },
|
||||
rules() { return store.attendRules },
|
||||
records() { return store.attendance || [] },
|
||||
dateLabel() {
|
||||
const d = new Date()
|
||||
const w = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||
return (d.getMonth() + 1) + '月' + d.getDate() + '日 ' + w[d.getDay()]
|
||||
},
|
||||
punchLabel() {
|
||||
if (this.punching) return '打卡中'
|
||||
if (!this.inRec) return '上班打卡'
|
||||
if (!this.outRec) return '下班打卡'
|
||||
return '更新打卡'
|
||||
},
|
||||
locName() {
|
||||
const locs = this.rules.locations || []
|
||||
return locs[0] ? (locs[0].name || locs[0].title || '') : ''
|
||||
@@ -70,6 +112,11 @@ export default {
|
||||
},
|
||||
outRec() {
|
||||
return this.records.find((r) => /下班|out/i.test(String(r.title || r.kind || r.type || '')))
|
||||
},
|
||||
lieuText() {
|
||||
const inn = this.inRec && (this.inRec.time || this.inRec.punchTime)
|
||||
const out = this.outRec && (this.outRec.time || this.outRec.punchTime)
|
||||
return flexCreditText(flexCreditMinutes(inn, out))
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
@@ -105,24 +152,43 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.body { padding: 16rpx 24rpx 40rpx; }
|
||||
.clock-box { background: #fff; border-radius: 24rpx; padding: 48rpx 0 40rpx; text-align: center; border: 1rpx solid rgba(17,24,39,0.06); }
|
||||
.now { display: block; font-size: 72rpx; font-weight: 650; letter-spacing: 2rpx; color: #111827; }
|
||||
.shift { font-size: 22rpx; color: #4B5563; }
|
||||
.grid { display: flex; gap: 16rpx; margin: 20rpx 0; }
|
||||
.stat { flex: 1; padding: 24rpx; }
|
||||
.lab { font-size: 26rpx; font-weight: 600; display: block; }
|
||||
.val { font-size: 24rpx; color: #4B5563; display: block; margin-top: 12rpx; }
|
||||
.val.ok { color: #16A34A; font-weight: 650; }
|
||||
.tiny { font-size: 20rpx; color: #6B7280; }
|
||||
.geo { display: block; text-align: center; font-size: 22rpx; color: #4B5563; margin: 16rpx 0 24rpx; }
|
||||
.body :deep(.wd-button) { margin-top: 8rpx; }
|
||||
.quick { display: flex; gap: 12rpx; }
|
||||
.q { flex: 1; height: 72rpx; background: #fff; border-radius: 12rpx; text-align: center; line-height: 72rpx; font-size: 24rpx; }
|
||||
.sec { margin: 32rpx 0 12rpx; font-size: 28rpx; font-weight: 600; }
|
||||
.rec { display: flex; justify-content: space-between; align-items: center; padding: 24rpx 28rpx; border-bottom: 1rpx solid #E8ECF2; }
|
||||
.rt { display: block; font-size: 28rpx; font-weight: 600; }
|
||||
.rl { display: block; font-size: 22rpx; color: #6B7280; }
|
||||
.body { padding: 8rpx 28rpx 48rpx; }
|
||||
.day { display: flex; justify-content: space-between; align-items: baseline; padding: 8rpx 8rpx 20rpx; }
|
||||
.date { font-size: 32rpx; font-weight: 650; color: #111827; }
|
||||
.shift { font-size: 24rpx; color: #6B7280; }
|
||||
.lieu { margin: 0 8rpx 16rpx; font-size: 24rpx; color: #1677FF; }
|
||||
.slots { background: #fff; border-radius: 20rpx; padding: 8rpx 8rpx; border: 1rpx solid rgba(17,24,39,0.06); }
|
||||
.slot { display: flex; align-items: center; padding: 22rpx 20rpx; }
|
||||
.slot + .slot { border-top: 1rpx solid #F0F2F5; }
|
||||
.dot { width: 16rpx; height: 16rpx; border-radius: 50%; background: #D1D5DB; margin-right: 16rpx; flex-shrink: 0; }
|
||||
.dot.on { background: #1677FF; }
|
||||
.slot-m { flex: 1; min-width: 0; }
|
||||
.lab { display: block; font-size: 28rpx; font-weight: 600; color: #111827; }
|
||||
.val { display: block; font-size: 24rpx; color: #9CA3AF; margin-top: 6rpx; }
|
||||
.val.ok { color: #16A34A; }
|
||||
.tiny { font-size: 22rpx; color: #16A34A; }
|
||||
.ring-wrap { display: flex; justify-content: center; padding: 56rpx 0 28rpx; }
|
||||
.ring {
|
||||
width: 320rpx; height: 320rpx; border-radius: 50%;
|
||||
background: linear-gradient(180deg, #3D8BFF 0%, #1677FF 100%);
|
||||
box-shadow: 0 16rpx 40rpx rgba(22, 119, 255, 0.35);
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
color: #fff;
|
||||
}
|
||||
.ring.busy { opacity: 0.72; }
|
||||
.now { font-size: 44rpx; font-weight: 650; letter-spacing: 1rpx; font-variant-numeric: tabular-nums; }
|
||||
.act { margin-top: 10rpx; font-size: 30rpx; }
|
||||
.geo { display: flex; align-items: center; justify-content: center; gap: 8rpx; font-size: 24rpx; color: #6B7280; }
|
||||
.quick { display: flex; margin-top: 36rpx; background: #fff; border-radius: 20rpx; border: 1rpx solid rgba(17,24,39,0.06); }
|
||||
.q { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 10rpx; padding: 28rpx 0; font-size: 24rpx; color: #374151; }
|
||||
.sec { margin: 36rpx 8rpx 14rpx; font-size: 28rpx; font-weight: 650; color: #111827; }
|
||||
.log { padding: 8rpx 0; }
|
||||
.rec { display: flex; align-items: center; padding: 22rpx 24rpx; position: relative; }
|
||||
.rec + .rec { border-top: 1rpx solid #F0F2F5; }
|
||||
.rail { width: 12rpx; height: 12rpx; border-radius: 50%; background: #1677FF; margin-right: 16rpx; flex-shrink: 0; }
|
||||
.rec-m { flex: 1; min-width: 0; }
|
||||
.rt { display: block; font-size: 28rpx; font-weight: 600; color: #111827; }
|
||||
.rl { display: block; font-size: 22rpx; color: #6B7280; margin-top: 4rpx; }
|
||||
.ok { color: #16A34A; font-size: 24rpx; }
|
||||
.empty { padding: 40rpx; text-align: center; color: #6B7280; }
|
||||
.deny { padding: 120rpx 40rpx; text-align: center; color: #6B7280; }
|
||||
|
||||
@@ -2,33 +2,48 @@
|
||||
<view class="page edge">
|
||||
<fy-header title="通讯录">
|
||||
<template #right>
|
||||
<text class="ico" @click="goGroup">建群</text>
|
||||
<view class="ico" @click="goGroup">
|
||||
<wd-icon name="usergroup" size="40rpx" color="#111827" />
|
||||
</view>
|
||||
</template>
|
||||
</fy-header>
|
||||
<fy-scroll class="edge-scroll" dock @refresh="reload">
|
||||
<wd-search v-model="q" placeholder="搜索同事、部门或手机号" hide-cancel placeholder-left />
|
||||
<view class="finder">
|
||||
<wd-icon name="search" size="28rpx" color="#9CA3AF" />
|
||||
<input v-model="q" class="finder-input" placeholder="搜索同事、部门或手机号" placeholder-class="finder-ph" confirm-type="search" />
|
||||
<view v-if="q" class="finder-clear" @click="q = ''">
|
||||
<wd-icon name="close-bold" size="22rpx" color="#9CA3AF" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="shortcuts">
|
||||
<view class="org" @click="goGroups">
|
||||
<view class="obox">👥</view>
|
||||
<view class="obox group">
|
||||
<wd-icon name="usergroup" size="36rpx" color="#1677FF" />
|
||||
</view>
|
||||
<view class="ometa">
|
||||
<text class="on">群聊</text>
|
||||
<text class="os">{{ groupCount }} 个群</text>
|
||||
</view>
|
||||
<text class="arr">›</text>
|
||||
<wd-icon name="arrow-right" size="28rpx" color="#D1D5DB" />
|
||||
</view>
|
||||
<view class="org" @click="goSearch">
|
||||
<view class="obox">🏛</view>
|
||||
<view class="obox orgn">
|
||||
<wd-icon name="layers" size="36rpx" color="#16A34A" />
|
||||
</view>
|
||||
<view class="ometa">
|
||||
<text class="on">全员架构</text>
|
||||
<text class="os">共 {{ deptCount }} 个部门 · {{ peopleCount }} 位在职成员</text>
|
||||
<text class="os">{{ deptCount }} 个部门 · {{ peopleCount }} 位在职</text>
|
||||
</view>
|
||||
<wd-icon name="arrow-right" size="28rpx" color="#D1D5DB" />
|
||||
</view>
|
||||
<text class="arr">›</text>
|
||||
</view>
|
||||
<view v-for="g in groups" :key="g.name" class="block">
|
||||
<view class="dept" @click="toggle(g.name)">
|
||||
<text>{{ g.name }}</text>
|
||||
<text class="cnt">({{ g.people.length }})</text>
|
||||
<text class="dept-name">{{ g.name }}</text>
|
||||
<text class="cnt">{{ g.people.length }}</text>
|
||||
<wd-icon class="chev" :name="open[g.name] === false ? 'arrow-right' : 'arrow-down'" size="24rpx" color="#9CA3AF" />
|
||||
</view>
|
||||
<view v-if="open[g.name] !== false" class="card">
|
||||
<view v-if="open[g.name] !== false" class="card people">
|
||||
<view v-for="p in g.people" :key="p.id" class="item" @click="openPerson(p)">
|
||||
<view class="av-wrap">
|
||||
<image v-if="p.avatar" class="av" :src="p.avatar" mode="aspectFill" />
|
||||
@@ -37,16 +52,21 @@
|
||||
</view>
|
||||
<view class="meta">
|
||||
<text class="name">{{ p.name }}</text>
|
||||
<text class="sub">{{ p.online ? '在线' : '离线' }} · {{ p.title }}</text>
|
||||
<text class="sub">{{ p.title || (p.online ? '在线' : '离线') }}</text>
|
||||
</view>
|
||||
<view class="acts">
|
||||
<view class="mini" @click.stop="chat(p)">
|
||||
<wd-icon name="chat" size="34rpx" color="#1677FF" />
|
||||
</view>
|
||||
<view class="mini" @click.stop="call(p)">
|
||||
<wd-icon name="phone" size="34rpx" color="#1677FF" />
|
||||
</view>
|
||||
</view>
|
||||
<text class="chip">{{ p.title }}</text>
|
||||
<text class="mini" @click.stop="chat(p)">💬</text>
|
||||
<text class="mini" @click.stop="call(p)">☎</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="!groups.length" class="empty">没有匹配的同事</view>
|
||||
<text class="foot">仅显示当前所属企业成员 · 数据实时同步</text>
|
||||
<text class="foot">仅显示当前所属企业成员</text>
|
||||
</fy-scroll>
|
||||
</view>
|
||||
</template>
|
||||
@@ -132,28 +152,81 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search { padding: 16rpx 24rpx; }
|
||||
.search input { height: 80rpx; background: #fff; border-radius: 20rpx; padding: 0 28rpx; font-size: 28rpx; border: 1rpx solid rgba(17,24,39,0.06); }
|
||||
.org { margin: 0 24rpx 16rpx; background: #fff; border-radius: 16rpx; padding: 20rpx; display: flex; align-items: center; }
|
||||
.obox { width: 64rpx; height: 64rpx; border-radius: 12rpx; background: #E8ECF2; display: flex; align-items: center; justify-content: center; }
|
||||
.ometa { flex: 1; margin-left: 16rpx; }
|
||||
.on { display: block; font-size: 28rpx; font-weight: 700; }
|
||||
.os { font-size: 22rpx; color: #6B7280; }
|
||||
.arr { color: #D1D5DB; }
|
||||
.block { margin: 8rpx 24rpx 24rpx; }
|
||||
.dept { display: flex; align-items: center; padding: 16rpx 12rpx; font-size: 26rpx; font-weight: 600; background: #F3F6FB; border-radius: 12rpx 12rpx 0 0; }
|
||||
.cnt { color: #6B7280; font-weight: 400; font-size: 22rpx; margin-left: 8rpx; }
|
||||
.item { display: flex; align-items: center; padding: 20rpx 20rpx; }
|
||||
.av-wrap { position: relative; width: 72rpx; height: 72rpx; }
|
||||
.av { width: 72rpx; height: 72rpx; border-radius: 50%; }
|
||||
.av.txt { background: #E8ECF2; color: #4B5563; display: flex; align-items: center; justify-content: center; font-weight: 600; }
|
||||
.dot { position: absolute; right: 0; bottom: 0; width: 16rpx; height: 16rpx; border-radius: 50%; background: #006c49; border: 2rpx solid #fff; }
|
||||
.meta { flex: 1; margin-left: 16rpx; min-width: 0; }
|
||||
.name { display: block; font-size: 28rpx; font-weight: 600; }
|
||||
.sub { font-size: 22rpx; color: #6B7280; }
|
||||
.chip { font-size: 20rpx; color: #1677FF; background: #E8F1FF; padding: 4rpx 12rpx; border-radius: 999rpx; margin-right: 8rpx; }
|
||||
.mini { width: 56rpx; text-align: center; font-size: 28rpx; color: #1677FF; }
|
||||
.empty { text-align: center; color: #6B7280; padding: 80rpx; }
|
||||
.foot { display: block; text-align: center; color: #6B7280; font-size: 22rpx; padding: 20rpx 0 40rpx; }
|
||||
.ico { padding: 8rpx 16rpx; color: #1677FF; font-size: 26rpx; }
|
||||
.finder {
|
||||
margin: 8rpx 24rpx 12rpx;
|
||||
height: 56rpx;
|
||||
padding: 0 20rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
.finder-input {
|
||||
flex: 1;
|
||||
height: 56rpx;
|
||||
margin-left: 8rpx;
|
||||
font-size: 26rpx;
|
||||
line-height: 56rpx;
|
||||
color: #111827;
|
||||
}
|
||||
.finder-ph { color: #9CA3AF; font-size: 26rpx; }
|
||||
.finder-clear {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.shortcuts {
|
||||
margin: 0 24rpx 8rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid rgba(17, 24, 39, 0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
.org {
|
||||
padding: 22rpx 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.org + .org { border-top: 1rpx solid #F0F2F5; }
|
||||
.obox {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 18rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.obox.group { background: #E8F1FF; }
|
||||
.obox.orgn { background: #E8F8EF; }
|
||||
.ometa { flex: 1; margin: 0 16rpx; min-width: 0; }
|
||||
.on { display: block; font-size: 30rpx; font-weight: 600; color: #111827; }
|
||||
.os { display: block; margin-top: 4rpx; font-size: 22rpx; color: #6B7280; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.block { margin: 20rpx 24rpx 0; }
|
||||
.dept {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8rpx 8rpx 12rpx;
|
||||
}
|
||||
.dept-name { font-size: 24rpx; font-weight: 600; color: #6B7280; }
|
||||
.cnt { margin-left: 8rpx; font-size: 22rpx; color: #9CA3AF; }
|
||||
.chev { margin-left: auto; }
|
||||
.people { overflow: hidden; }
|
||||
.item { display: flex; align-items: center; padding: 0 20rpx 0 24rpx; }
|
||||
.item + .item .meta { border-top: 1rpx solid #F0F2F5; }
|
||||
.av-wrap { position: relative; width: 80rpx; height: 80rpx; flex-shrink: 0; margin: 18rpx 0; }
|
||||
.av { width: 80rpx; height: 80rpx; border-radius: 18rpx; }
|
||||
.av.txt { background: #E8F1FF; color: #1677FF; display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 30rpx; }
|
||||
.dot { position: absolute; right: -2rpx; bottom: -2rpx; width: 16rpx; height: 16rpx; border-radius: 50%; background: #16A34A; border: 3rpx solid #fff; }
|
||||
.meta { flex: 1; margin-left: 20rpx; min-width: 0; padding: 22rpx 0; }
|
||||
.name { display: block; font-size: 30rpx; font-weight: 600; color: #111827; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.sub { display: block; margin-top: 4rpx; font-size: 22rpx; color: #6B7280; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.acts { display: flex; align-items: center; flex-shrink: 0; margin-left: 8rpx; }
|
||||
.mini { width: 64rpx; height: 64rpx; display: flex; align-items: center; justify-content: center; }
|
||||
.empty { text-align: center; color: #6B7280; padding: 120rpx 40rpx; font-size: 28rpx; }
|
||||
.foot { display: block; text-align: center; color: #9CA3AF; font-size: 22rpx; padding: 28rpx 0 48rpx; }
|
||||
.ico { width: 56rpx; height: 56rpx; display: flex; align-items: center; justify-content: center; }
|
||||
</style>
|
||||
|
||||
@@ -3,48 +3,56 @@
|
||||
<fy-header title="我的" />
|
||||
<fy-scroll class="edge-scroll" dock @refresh="reload">
|
||||
<view class="body">
|
||||
<view class="card profile">
|
||||
<view class="row">
|
||||
<view class="profile" @click="goArchive">
|
||||
<image v-if="me && me.avatar" class="av" :src="me.avatar" mode="aspectFill" />
|
||||
<view v-else class="av txt">{{ initial }}</view>
|
||||
<view class="info">
|
||||
<view class="nm">
|
||||
<text class="name">{{ me ? me.name : '' }}</text>
|
||||
<wd-tag type="success" plain>在职</wd-tag>
|
||||
<text v-if="statusText" class="tag">{{ statusText }}</text>
|
||||
</view>
|
||||
<text class="dept">{{ me ? (me.department + ' · ' + me.title) : '' }}</text>
|
||||
<text class="ph">{{ phone }}{{ job }}</text>
|
||||
<text class="sub">{{ deptLine }}</text>
|
||||
<text class="sub">{{ phone }}{{ job }}</text>
|
||||
</view>
|
||||
<wd-icon name="qrcode" size="36rpx" color="#111827" />
|
||||
<text class="chev">›</text>
|
||||
</view>
|
||||
<view class="stats">
|
||||
<view class="s" @click="switchClock">
|
||||
<text class="n">{{ attendDays }}</text>
|
||||
<text class="l">本月出勤(天)</text>
|
||||
</view>
|
||||
<view class="s mid" @click="goTodo">
|
||||
<text class="n">{{ todoCount }}</text>
|
||||
<text class="l">待办申请(件)</text>
|
||||
</view>
|
||||
<view class="s">
|
||||
<text class="n ok">正常</text>
|
||||
<text class="l">考勤状态</text>
|
||||
|
||||
<view class="group">
|
||||
<view class="cell" @click="switchClock">
|
||||
<wd-icon name="location" size="40rpx" color="#1677FF" />
|
||||
<text class="ct">本月出勤</text>
|
||||
<text class="cv">{{ attendDays }} 天</text>
|
||||
<text class="chev">›</text>
|
||||
</view>
|
||||
<view class="cell" @click="goTodo">
|
||||
<wd-icon name="check-circle" size="40rpx" color="#16A34A" />
|
||||
<text class="ct">待办申请</text>
|
||||
<text class="cv">{{ todoCount }}</text>
|
||||
<text class="chev">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card menu">
|
||||
<wd-cell-group border>
|
||||
<wd-cell v-for="a in menus" :key="a.id" :title="a.title" is-link @click="open(a)" />
|
||||
</wd-cell-group>
|
||||
<view v-if="menus.length" class="group">
|
||||
<view v-for="a in menus" :key="a.id" class="cell" @click="open(a)">
|
||||
<wd-icon :name="iconOf(a)" size="40rpx" :color="colorOf(a)" />
|
||||
<text class="ct">{{ a.title }}</text>
|
||||
<text class="chev">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card menu">
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="修改密码" is-link @click="goPwd" />
|
||||
<wd-cell is-link @click="logout">
|
||||
<template #title><text class="danger">退出登录</text></template>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
<view class="group">
|
||||
<view class="cell" @click="goPwd">
|
||||
<wd-icon name="lock-on" size="40rpx" color="#576B95" />
|
||||
<text class="ct">修改密码</text>
|
||||
<text class="chev">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="group">
|
||||
<view class="cell center" @click="logout">
|
||||
<text class="danger">退出登录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</fy-scroll>
|
||||
@@ -56,6 +64,7 @@ import FyHeader from '../../components/fy-header.vue'
|
||||
import FyScroll from '../../components/fy-scroll.vue'
|
||||
import { store } from '../../utils/store.js'
|
||||
import { maskPhone } from '../../utils/format.js'
|
||||
import { tileVisual } from '../../utils/nav.js'
|
||||
|
||||
const ME_IDS = ['archive', 'salary', 'perf', 'expense', 'report', 'apply']
|
||||
|
||||
@@ -67,6 +76,13 @@ export default {
|
||||
computed: {
|
||||
me() { return store.me },
|
||||
initial() { return this.me ? this.me.name.slice(0, 1) : '我' },
|
||||
statusText() {
|
||||
return (this.me && this.me.employeeStatus) || '在职'
|
||||
},
|
||||
deptLine() {
|
||||
if (!this.me) return ''
|
||||
return [this.me.department, this.me.title].filter(Boolean).join(' · ')
|
||||
},
|
||||
phone() { return maskPhone(this.me && this.me.phone) },
|
||||
job() { return this.me && this.me.jobId ? ' · 工号: ' + this.me.jobId : '' },
|
||||
todoCount() { return store.todoCount || 0 },
|
||||
@@ -81,14 +97,19 @@ export default {
|
||||
},
|
||||
onHide() { if (this.off) this.off() },
|
||||
methods: {
|
||||
iconOf(a) { return tileVisual(a).name },
|
||||
colorOf(a) { return tileVisual(a).color },
|
||||
goArchive() {
|
||||
const a = store.personalApps().find((x) => x.id === 'archive')
|
||||
if (a) this.open(a)
|
||||
else uni.navigateTo({ url: '/pages/me/archive' })
|
||||
},
|
||||
open(a) {
|
||||
if (a.url.startsWith('/pages/tabs/')) uni.switchTab({ url: a.url.split('?')[0] })
|
||||
else uni.navigateTo({ url: a.url })
|
||||
},
|
||||
switchClock() { uni.switchTab({ url: '/pages/tabs/clock' }) },
|
||||
goTodo() {
|
||||
if (store.can('personal', 'approve')) uni.navigateTo({ url: '/pages/todo/list' })
|
||||
},
|
||||
goTodo() { uni.navigateTo({ url: '/pages/todo/list' }) },
|
||||
reload() { store.refreshPerms() },
|
||||
goPwd() { uni.navigateTo({ url: '/pages/password/password' }) },
|
||||
logout() {
|
||||
@@ -108,27 +129,34 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.body { padding: 8rpx 28rpx 48rpx; }
|
||||
.profile { padding: 32rpx 28rpx 24rpx; }
|
||||
.row { display: flex; align-items: center; }
|
||||
.av { width: 112rpx; height: 112rpx; border-radius: 50%; }
|
||||
.av.txt { background: #1677FF; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 40rpx; font-weight: 650; }
|
||||
.info { margin-left: 24rpx; flex: 1; min-width: 0; }
|
||||
.body { padding: 16rpx 0 48rpx; }
|
||||
.profile {
|
||||
display: flex; align-items: center;
|
||||
margin: 8rpx 24rpx 20rpx;
|
||||
padding: 32rpx 24rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid rgba(17, 24, 39, 0.06);
|
||||
}
|
||||
.av { width: 112rpx; height: 112rpx; border-radius: 24rpx; flex-shrink: 0; }
|
||||
.av.txt { background: #E8F1FF; color: #1677FF; display: flex; align-items: center; justify-content: center; font-size: 44rpx; font-weight: 650; }
|
||||
.info { flex: 1; min-width: 0; margin: 0 20rpx; }
|
||||
.nm { display: flex; align-items: center; gap: 12rpx; }
|
||||
.name { font-size: 36rpx; font-weight: 650; }
|
||||
.st { font-size: 22rpx; background: #E8F8EF; color: #16A34A; padding: 4rpx 12rpx; border-radius: 8rpx; }
|
||||
.dept, .ph { display: block; font-size: 24rpx; color: #6B7280; margin-top: 8rpx; }
|
||||
.stats { display: flex; margin-top: 28rpx; padding-top: 24rpx; border-top: 1rpx solid #F0F2F5; }
|
||||
.s { flex: 1; text-align: center; }
|
||||
.mid { border-left: 1rpx solid #F0F2F5; border-right: 1rpx solid #F0F2F5; }
|
||||
.n { display: block; font-size: 34rpx; font-weight: 650; color: #111827; }
|
||||
.n.ok { color: #16A34A; font-size: 28rpx; }
|
||||
.l { font-size: 22rpx; color: #6B7280; margin-top: 4rpx; }
|
||||
.menu { margin-top: 20rpx; overflow: hidden; }
|
||||
.mi { display: flex; align-items: center; min-height: 100rpx; padding: 0 28rpx; }
|
||||
.mi + .mi { border-top: 1rpx solid #F0F2F5; }
|
||||
.ii { width: 48rpx; color: #1677FF; font-weight: 650; }
|
||||
.it { flex: 1; font-size: 28rpx; }
|
||||
.arr { color: #C4C9D4; font-size: 32rpx; }
|
||||
.danger { color: #DC2626; }
|
||||
.name { font-size: 36rpx; font-weight: 650; color: #111827; }
|
||||
.tag { font-size: 22rpx; color: #16A34A; background: #E8F8EF; border-radius: 8rpx; padding: 2rpx 10rpx; }
|
||||
.sub { display: block; font-size: 24rpx; color: #6B7280; margin-top: 6rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.group {
|
||||
margin: 0 24rpx 20rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid rgba(17, 24, 39, 0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
.cell { display: flex; align-items: center; min-height: 104rpx; padding: 0 24rpx; gap: 20rpx; }
|
||||
.cell + .cell { border-top: 1rpx solid #F0F2F5; }
|
||||
.cell.center { justify-content: center; }
|
||||
.ct { flex: 1; font-size: 30rpx; color: #111827; }
|
||||
.cv { font-size: 26rpx; color: #9CA3AF; }
|
||||
.chev { color: #C8C8C8; font-size: 36rpx; }
|
||||
.danger { font-size: 30rpx; color: #DC2626; }
|
||||
</style>
|
||||
|
||||
@@ -2,9 +2,20 @@
|
||||
<view class="page msg edge">
|
||||
<fy-header title="消息" :extra="unreadLabel">
|
||||
<template #right>
|
||||
<text class="ico" @click="plus">+</text>
|
||||
<view class="plus" @click.stop="menu = !menu">
|
||||
<view class="plus-h" />
|
||||
<view class="plus-v" />
|
||||
</view>
|
||||
</template>
|
||||
</fy-header>
|
||||
<view v-if="menu" class="mask" @click="menu = false" />
|
||||
<view v-if="menu" class="drop" :style="{ top: dropTop + 'px' }">
|
||||
<view class="caret" />
|
||||
<view class="drop-item" @click="newGroup">
|
||||
<wd-icon name="usergroup" size="32rpx" color="#374151" />
|
||||
<text>发起群聊</text>
|
||||
</view>
|
||||
</view>
|
||||
<fy-scroll class="edge-scroll list" dock @refresh="reload">
|
||||
<view class="finder" @click="goSearch">
|
||||
<wd-icon name="search" size="28rpx" color="#9CA3AF" />
|
||||
@@ -22,6 +33,9 @@
|
||||
<view class="av-wrap">
|
||||
<image v-if="c.avatar && c.type !== 'files'" class="av" :src="c.avatar" mode="aspectFill" />
|
||||
<view v-else-if="c.type === 'files'" class="av"><fy-file-mark /></view>
|
||||
<view v-else-if="facesOf(c).length" class="av mosaic">
|
||||
<image v-for="f in facesOf(c)" :key="f.id" class="tile" :src="f.avatar" mode="aspectFill" />
|
||||
</view>
|
||||
<view v-else class="av txt" :class="avClass(c)">{{ avText(c) }}</view>
|
||||
<text v-if="c.unread && !c.muted" class="badge abs">{{ c.unread > 99 ? '99+' : c.unread }}</text>
|
||||
<text v-else-if="c.unread && c.muted" class="dotu" />
|
||||
@@ -50,7 +64,7 @@ import { store } from '../../utils/store.js'
|
||||
export default {
|
||||
components: { FyHeader, FyFileMark, FyScroll },
|
||||
data() {
|
||||
return { rows: [], unread: 0, off: null }
|
||||
return { rows: [], unread: 0, off: null, menu: false, dropTop: 64 }
|
||||
},
|
||||
computed: {
|
||||
unreadLabel() {
|
||||
@@ -58,6 +72,11 @@ export default {
|
||||
return '(' + (this.unread > 99 ? '99+' : this.unread) + ')'
|
||||
}
|
||||
},
|
||||
created() {
|
||||
let status = 20
|
||||
try { status = uni.getSystemInfoSync().statusBarHeight || 20 } catch (_) {}
|
||||
this.dropTop = status + uni.upx2px(88) + 6
|
||||
},
|
||||
onShow() {
|
||||
if (this.off) this.off()
|
||||
this.guard()
|
||||
@@ -66,6 +85,7 @@ export default {
|
||||
this.off = store.on(() => this.pull())
|
||||
},
|
||||
onHide() {
|
||||
this.menu = false
|
||||
if (this.off) this.off()
|
||||
this.off = null
|
||||
},
|
||||
@@ -81,6 +101,10 @@ export default {
|
||||
reload() {
|
||||
store.refreshPeoplePhotos().finally(() => this.pull())
|
||||
},
|
||||
facesOf(c) {
|
||||
if (!c || c.type !== 'group' || c.avatar) return []
|
||||
return (c.memberIds || []).map((id) => store.person(id)).filter((p) => p && p.avatar).slice(0, 4)
|
||||
},
|
||||
avText(c) {
|
||||
if (c.type === 'work') return '工'
|
||||
if (c.type === 'group') return '群'
|
||||
@@ -124,14 +148,9 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
plus() {
|
||||
uni.showActionSheet({
|
||||
itemList: ['发起群聊', '添加同事'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) uni.navigateTo({ url: '/pages/chat/create' })
|
||||
else uni.navigateTo({ url: '/pages/search/search' })
|
||||
}
|
||||
})
|
||||
newGroup() {
|
||||
this.menu = false
|
||||
uni.navigateTo({ url: '/pages/chat/create' })
|
||||
},
|
||||
goSearch() {
|
||||
uni.navigateTo({
|
||||
@@ -145,19 +164,19 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.msg { display: flex; flex-direction: column; height: 100%; background: #fff; overflow: hidden; }
|
||||
.msg { display: flex; flex-direction: column; height: 100%; background: #F5F7FA; overflow: hidden; }
|
||||
.finder {
|
||||
margin: 8rpx 24rpx 12rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #F3F4F6;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
.finder-txt { margin-left: 8rpx; font-size: 26rpx; line-height: 1; }
|
||||
.list { background: #fff; }
|
||||
.list { background: #F5F7FA; }
|
||||
.ico.sm { font-size: 34rpx; color: #1677FF; }
|
||||
.item { display: flex; align-items: center; padding: 22rpx 28rpx; background: #fff; }
|
||||
.item.pin { background: #F7F8FA; }
|
||||
@@ -167,6 +186,8 @@ export default {
|
||||
.av.work { background: #1677FF; color: #fff; }
|
||||
.av.file { background: #F3F6FB; color: #4B5563; }
|
||||
.av.plain { background: #E8F1FF; color: #1677FF; }
|
||||
.mosaic { display: flex; flex-wrap: wrap; align-content: center; justify-content: center; gap: 4rpx; background: #E8F1FF; padding: 6rpx; box-sizing: border-box; }
|
||||
.tile { width: 38rpx; height: 38rpx; border-radius: 8rpx; }
|
||||
.abs { position: absolute; top: -8rpx; right: -8rpx; }
|
||||
.dotu { position: absolute; top: -4rpx; right: -4rpx; width: 16rpx; height: 16rpx; border-radius: 50%; background: #DC2626; }
|
||||
.meta { flex: 1; margin-left: 20rpx; min-width: 0; padding: 8rpx 0 18rpx; border-bottom: 1rpx solid #F0F2F5; }
|
||||
@@ -176,6 +197,62 @@ export default {
|
||||
.bot { display: flex; align-items: center; margin-top: 8rpx; }
|
||||
.prev { flex: 1; font-size: 24rpx; color: #6B7280; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.mute { font-size: 22rpx; margin-left: 8rpx; color: #9CA3AF; }
|
||||
.empty { padding: 160rpx 40rpx; text-align: center; color: #6B7280; font-size: 28rpx; background: #fff; }
|
||||
.empty { padding: 160rpx 40rpx; text-align: center; color: #6B7280; font-size: 28rpx; background: transparent; }
|
||||
.ico { font-size: 36rpx; color: #1677FF; padding: 8rpx 12rpx; }
|
||||
.plus {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
.plus-h, .plus-v {
|
||||
position: absolute;
|
||||
background: #111827;
|
||||
border-radius: 2rpx;
|
||||
transition: transform 180ms ease;
|
||||
}
|
||||
.plus-h { width: 28rpx; height: 3rpx; }
|
||||
.plus-v { width: 3rpx; height: 28rpx; }
|
||||
.mask { position: fixed; left: 0; right: 0; top: 0; bottom: 0; z-index: 30; background: rgba(17, 24, 39, 0.04); }
|
||||
.drop {
|
||||
position: fixed;
|
||||
right: 16rpx;
|
||||
z-index: 31;
|
||||
width: auto;
|
||||
padding: 8rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 12rpx 40rpx rgba(17, 24, 39, 0.1);
|
||||
transform-origin: calc(100% - 28rpx) 0;
|
||||
animation: drop-in 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
.caret {
|
||||
position: absolute;
|
||||
top: -10rpx;
|
||||
right: 28rpx;
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
background: #fff;
|
||||
transform: rotate(45deg);
|
||||
border-radius: 4rpx;
|
||||
box-shadow: -2rpx -2rpx 6rpx rgba(17, 24, 39, 0.04);
|
||||
}
|
||||
.drop-item {
|
||||
height: 80rpx;
|
||||
padding: 0 20rpx 0 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
font-size: 28rpx;
|
||||
color: #111827;
|
||||
border-radius: 14rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.drop-item + .drop-item { margin-top: 4rpx; }
|
||||
@keyframes drop-in {
|
||||
from { opacity: 0; transform: translateY(-8rpx) scale(0.96); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
<view class="page edge">
|
||||
<fy-header :title="greet">
|
||||
<template #right>
|
||||
<text class="ico" @click="goSearch">搜索</text>
|
||||
<view class="ico" @click="goSearch">
|
||||
<wd-icon name="search" size="40rpx" color="#111827" />
|
||||
</view>
|
||||
</template>
|
||||
</fy-header>
|
||||
<fy-scroll class="edge-scroll" dock @refresh="reload">
|
||||
@@ -40,7 +42,9 @@
|
||||
<view v-if="!flows.length" class="empty">暂无可用审批流,请确认账号已开通申请权限</view>
|
||||
<view class="grid">
|
||||
<view v-for="a in flows" :key="a.key" class="app" @click="openFlow(a)">
|
||||
<view class="icon"><text class="mark">{{ mark(a.label) }}</text></view>
|
||||
<view class="icon" :style="{ background: tileOf(a).bg }">
|
||||
<wd-icon :name="tileOf(a).name" size="44rpx" :color="tileOf(a).color" />
|
||||
</view>
|
||||
<text class="al">{{ a.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -52,8 +56,8 @@
|
||||
<view v-if="apps.length" class="card apps">
|
||||
<view class="grid">
|
||||
<view v-for="a in apps" :key="a.id" class="app" @click="open(a)">
|
||||
<view class="icon">
|
||||
<text class="mark">{{ mark(a.title) }}</text>
|
||||
<view class="icon" :style="{ background: tileOf(a).bg }">
|
||||
<wd-icon :name="tileOf(a).name" size="44rpx" :color="tileOf(a).color" />
|
||||
<text v-if="a.badge === 'todo' && todoCount" class="badge abs">{{ todoCount }}</text>
|
||||
</view>
|
||||
<text class="al">{{ a.title }}</text>
|
||||
@@ -66,7 +70,9 @@
|
||||
</view>
|
||||
<view v-if="erp.length" class="card mods">
|
||||
<view v-for="m in erp" :key="m.id" class="mod" @click="open(m)">
|
||||
<view class="mi"><text>{{ mark(m.title) }}</text></view>
|
||||
<view class="mi" :style="{ background: tileOf(m).bg }">
|
||||
<wd-icon :name="tileOf(m).name" size="36rpx" :color="tileOf(m).color" />
|
||||
</view>
|
||||
<text class="mt">{{ m.title }}</text>
|
||||
<text class="arr">›</text>
|
||||
</view>
|
||||
@@ -101,7 +107,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 +127,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 +141,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 +195,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 +247,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; }
|
||||
@@ -263,5 +270,5 @@ export default {
|
||||
.empty-card { background: #fff; border-radius: 24rpx; padding: 48rpx 32rpx; text-align: center; border: 1rpx solid rgba(17,24,39,0.06); }
|
||||
.et { display: block; font-size: 28rpx; font-weight: 600; }
|
||||
.es { display: block; font-size: 24rpx; color: #6B7280; margin-top: 8rpx; }
|
||||
.ico { padding: 8rpx 8rpx; font-size: 28rpx; color: #1677FF; }
|
||||
.ico { width: 56rpx; height: 56rpx; display: flex; align-items: center; justify-content: center; }
|
||||
</style>
|
||||
|
||||
@@ -3,8 +3,12 @@
|
||||
<fy-header title="审批详情" back />
|
||||
<view v-if="row" class="body">
|
||||
<view class="card">
|
||||
<view class="top">
|
||||
<text class="type">{{ row.typeLabel || row.type || '审批' }}</text>
|
||||
<wd-tag type="primary" plain>{{ statusText }}</wd-tag>
|
||||
</view>
|
||||
<text class="t">{{ row.title || row.no }}</text>
|
||||
<text class="s">{{ row.typeLabel || row.type }} · {{ row.submitter || '' }}</text>
|
||||
<text class="s">{{ row.submitter || '' }}</text>
|
||||
<text class="s">单号 {{ row.no }}</text>
|
||||
<text v-if="row.node" class="s">当前节点 {{ row.node }}</text>
|
||||
<text v-if="row.amount != null && row.amount !== ''" class="amt">¥ {{ row.amount }}</text>
|
||||
@@ -56,7 +60,7 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty">{{ err || '加载中…' }}</view>
|
||||
<wd-status-tip v-else image="content" :tip="err || '加载中…'" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -65,7 +69,7 @@ import FyHeader from '../../components/fy-header.vue'
|
||||
import FyFiles from '../../components/fy-files.vue'
|
||||
import { store } from '../../utils/store.js'
|
||||
import { oaGet } from '../../utils/api.js'
|
||||
import { toast } from '../../utils/format.js'
|
||||
import { flowStatus, toast } from '../../utils/format.js'
|
||||
import { collectFiles, fileId } from '../../utils/files.js'
|
||||
|
||||
const LABELS = {
|
||||
@@ -120,6 +124,9 @@ export default {
|
||||
const logs = (this.row && this.row.logs) || []
|
||||
return Array.isArray(logs) ? [...logs].reverse() : []
|
||||
},
|
||||
statusText() {
|
||||
return flowStatus(this.row && this.row.status)
|
||||
},
|
||||
canAct() {
|
||||
if (!this.row) return false
|
||||
const st = String(this.row.status || '')
|
||||
@@ -155,6 +162,8 @@ export default {
|
||||
<style scoped>
|
||||
.body { padding: 24rpx 28rpx 200rpx; }
|
||||
.card { padding: 28rpx; margin-bottom: 16rpx; }
|
||||
.top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12rpx; }
|
||||
.type { font-size: 22rpx; color: #1677FF; background: #E8F1FF; border-radius: 8rpx; padding: 2rpx 10rpx; }
|
||||
.block { padding: 24rpx 28rpx; }
|
||||
.h { display: block; font-size: 30rpx; font-weight: 650; margin-bottom: 16rpx; }
|
||||
.t { display: block; font-size: 34rpx; font-weight: 650; line-height: 1.4; }
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<text class="t">{{ r.title || r.no }}</text>
|
||||
<text class="s">{{ r.submitter || r.assigneeName || '' }}</text>
|
||||
</view>
|
||||
<wd-tag type="primary" plain>{{ r.status || '待处理' }}</wd-tag>
|
||||
<wd-tag type="primary" plain>{{ statusText(r) }}</wd-tag>
|
||||
<text class="arr">›</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -29,7 +29,7 @@
|
||||
import FyHeader from '../../components/fy-header.vue'
|
||||
import FyScroll from '../../components/fy-scroll.vue'
|
||||
import { store } from '../../utils/store.js'
|
||||
import { asList } from '../../utils/format.js'
|
||||
import { asList, flowStatus } from '../../utils/format.js'
|
||||
import { oaGet } from '../../utils/api.js'
|
||||
|
||||
export default {
|
||||
@@ -82,6 +82,7 @@ export default {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
statusText(r) { return flowStatus(r && r.status) },
|
||||
open(r) {
|
||||
if (r.source === 'work' || r.taskId) {
|
||||
uni.navigateTo({ url: '/pages/coop/detail?id=' + encodeURIComponent(r.id || r.taskId) })
|
||||
|
||||
|
Before Width: | Height: | Size: 225 B After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 290 B After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 267 B After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 267 B After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 270 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 270 B After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 197 B After Width: | Height: | Size: 476 B |
|
Before Width: | Height: | Size: 197 B After Width: | Height: | Size: 477 B |
|
After Width: | Height: | Size: 476 B |
|
After Width: | Height: | Size: 477 B |
|
Before Width: | Height: | Size: 166 B After Width: | Height: | Size: 493 B |
|
Before Width: | Height: | Size: 167 B After Width: | Height: | Size: 502 B |
@@ -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' }
|
||||
]
|
||||
|
||||
|
||||
@@ -92,6 +92,35 @@ export function pad2(n) {
|
||||
return String(n).padStart(2, '0')
|
||||
}
|
||||
|
||||
/** 弹性:9:00–9:30 上班,18:00–18:30 下班。早于 9:00、晚于 18:30 的分钟计入调休。 */
|
||||
export function flexCreditMinutes(inTime, outTime) {
|
||||
const inn = hmMinutes(inTime)
|
||||
const out = hmMinutes(outTime)
|
||||
let extra = 0
|
||||
if (inn != null && inn < 9 * 60) extra += 9 * 60 - inn
|
||||
if (out != null && out > 18 * 60 + 30) extra += out - (18 * 60 + 30)
|
||||
return extra
|
||||
}
|
||||
|
||||
export function flexCreditText(minutes) {
|
||||
const n = Math.max(0, Math.round(Number(minutes) || 0))
|
||||
if (!n) return '0 分钟'
|
||||
const h = Math.floor(n / 60)
|
||||
const m = n % 60
|
||||
if (h && m) return h + ' 小时 ' + m + ' 分钟'
|
||||
if (h) return h + ' 小时'
|
||||
return m + ' 分钟'
|
||||
}
|
||||
|
||||
function hmMinutes(value) {
|
||||
const m = String(value || '').match(/(\d{1,2}):(\d{2})/)
|
||||
if (!m) return null
|
||||
const h = Number(m[1])
|
||||
const min = Number(m[2])
|
||||
if (h > 23 || min > 59) return null
|
||||
return h * 60 + min
|
||||
}
|
||||
|
||||
export function clockText(date) {
|
||||
const d = date || new Date()
|
||||
return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`
|
||||
@@ -119,6 +148,34 @@ export function convType(s) {
|
||||
return 'direct'
|
||||
}
|
||||
|
||||
const FLOW_STATUS = {
|
||||
pending: '待审批',
|
||||
waiting: '待处理',
|
||||
processing: '审批中',
|
||||
reviewing: '审阅中',
|
||||
review: '待审阅',
|
||||
approved: '已通过',
|
||||
passed: '已通过',
|
||||
rejected: '已驳回',
|
||||
refused: '已拒绝',
|
||||
denied: '已拒绝',
|
||||
cancelled: '已撤销',
|
||||
canceled: '已撤销',
|
||||
withdrawn: '已撤回',
|
||||
draft: '草稿',
|
||||
done: '已完成',
|
||||
completed: '已完成',
|
||||
handled: '已处理',
|
||||
reviewed: '已审阅',
|
||||
running: '进行中'
|
||||
}
|
||||
|
||||
export function flowStatus(status) {
|
||||
const raw = String(status || '').trim()
|
||||
if (!raw) return '待处理'
|
||||
return FLOW_STATUS[raw.toLowerCase()] || raw
|
||||
}
|
||||
|
||||
export function toast(title, icon = 'none') {
|
||||
uni.showToast({ title: String(title || ''), icon, duration: 2200 })
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,9 +478,16 @@ 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)
|
||||
const extras = (conv.messages || []).filter((m) => m.id && !seen.has(m.id) && (m.kind === 'system' || (m.mine && Date.now() - Number(m.time) < 120000)))
|
||||
conv.messages = incoming.concat(extras)
|
||||
conv.unread = 0
|
||||
await api.markRead(conv.id, this.token)
|
||||
@@ -781,12 +794,15 @@ export const store = {
|
||||
async createGroup(name, memberIds) {
|
||||
const ids = [...new Set([this.me && this.me.id, ...(memberIds || []).map(String)])].filter(Boolean)
|
||||
const title = String(name || '').trim() || '群聊'
|
||||
let sid = ''
|
||||
try {
|
||||
const data = await api.createGroup({ name: title, memberIds: ids }, this.token)
|
||||
sid = String((data && (data.id || data.sessionId)) || '')
|
||||
} catch (_) {}
|
||||
if (!sid) sid = 'g:' + Date.now()
|
||||
if (ids.length < 2) throw new Error('请至少再选一位同事')
|
||||
const data = await api.createGroup({
|
||||
title,
|
||||
name: title,
|
||||
memberIds: ids,
|
||||
members: ids
|
||||
}, this.token)
|
||||
const sid = String((data && (data.id || data.sessionId)) || '')
|
||||
if (!sid) throw new Error('创建失败')
|
||||
let conv = this.findConv(sid)
|
||||
if (!conv) {
|
||||
conv = {
|
||||
@@ -796,8 +812,16 @@ export const store = {
|
||||
avatar: '',
|
||||
peerId: '',
|
||||
memberIds: ids,
|
||||
messages: [],
|
||||
unread: 0,
|
||||
messages: [{
|
||||
id: 'sys-' + sid,
|
||||
kind: 'system',
|
||||
text: '你创建了群聊',
|
||||
time: Date.now(),
|
||||
mine: false,
|
||||
fromId: '',
|
||||
recalled: false
|
||||
}],
|
||||
lastMessage: '你创建了群聊',
|
||||
lastTime: fmtListTime(Date.now()),
|
||||
pinned: false,
|
||||
@@ -841,6 +865,7 @@ export const store = {
|
||||
try {
|
||||
this.attendRules.locations = asList(await api.oaGet('/profile/attendance/locations', this.token))
|
||||
} catch (_) {}
|
||||
if (this.can('system', 'attend')) {
|
||||
try {
|
||||
const pack = await api.oaGet('/system/attend-rules', this.token)
|
||||
if (pack && typeof pack === 'object') {
|
||||
@@ -848,36 +873,42 @@ export const store = {
|
||||
this.attendRules.workEnd = pack.workEnd || this.attendRules.workEnd
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
try {
|
||||
this.attendance = asList(await api.oaGet('/profile/attendance', this.token))
|
||||
} catch (_) {}
|
||||
this.emit()
|
||||
},
|
||||
|
||||
punch() {
|
||||
_locate() {
|
||||
let type = 'gcj02'
|
||||
// #ifdef H5
|
||||
type = 'wgs84'
|
||||
// #endif
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getLocation({
|
||||
type: 'gcj02',
|
||||
isHighAccuracy: true,
|
||||
success: async (pos) => {
|
||||
try {
|
||||
const rec = await api.oaPost('/profile/attendance/punch', {
|
||||
source: 'mobile',
|
||||
latitude: pos.latitude,
|
||||
longitude: pos.longitude,
|
||||
accuracyM: pos.accuracy
|
||||
}, this.token)
|
||||
await this.refreshAttendance()
|
||||
resolve((rec && rec.title) || '打卡成功')
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
type,
|
||||
isHighAccuracy: false,
|
||||
timeout: 8000,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
},
|
||||
fail() {
|
||||
reject(new Error('需要定位权限才能打卡'))
|
||||
|
||||
async punch() {
|
||||
const body = { source: 'mobile' }
|
||||
try {
|
||||
const pos = await this._locate()
|
||||
body.latitude = pos.latitude
|
||||
body.longitude = pos.longitude
|
||||
body.accuracyM = pos.accuracy
|
||||
} catch (_) {
|
||||
// 桌面浏览器经常没有定位;坐标缺省时仍提交,由服务端决定是否允许
|
||||
}
|
||||
})
|
||||
})
|
||||
const rec = await api.oaPost('/profile/attendance/punch', body, this.token)
|
||||
await this.refreshAttendance()
|
||||
return (rec && rec.title) || '打卡成功'
|
||||
},
|
||||
|
||||
async audit(no, action, comment) {
|
||||
|
||||