Compare commits

..

31 Commits

Author SHA1 Message Date
zhaichao ff5747c21b 文件传输助手移除视频语音通话 2026-09-24 16:12:04 +08:00
zhaichao b9db7d7f5c 建群文字改图标 2026-09-24 16:08:20 +08:00
zhaichao 4c896c69fc 搜索文字改图标 2026-09-24 16:07:15 +08:00
zhaichao 7f2a02d6ca 待办审批状态标签 2026-09-24 16:07:02 +08:00
zhaichao 94024faba9 修改密码页面跳转首页bug修复 2026-09-24 16:02:52 +08:00
zhaichao df04d0f4ba 修改密码 2026-09-24 15:56:37 +08:00
zhaichao 79499a5de5 发起申请 2026-09-24 15:54:47 +08:00
zhaichao b6b999e47a 移除添加同事功能 2026-09-24 15:38:45 +08:00
zhaichao 1f93f4cd91 打卡403权限报错 2026-09-24 15:12:49 +08:00
zhaichao 0a1a312418 群聊 2026-09-24 14:58:23 +08:00
zhaichao 6e8bb2e0a0 同事资料 2026-09-24 14:32:40 +08:00
zhaichao fb312af2fc 工作协同 2026-09-24 14:31:24 +08:00
zhaichao 471cfe6188 申请表单、审批详情、新建协同、协同详情、工作汇报和修改密码 2026-09-24 14:25:44 +08:00
zhaichao 892acc6d23 单据、业务中心、公告和工作通知 2026-09-24 14:24:08 +08:00
zhaichao 5e77d4fdb1 我的资料,工资条,我的绩效样式修改 2026-09-24 14:22:35 +08:00
zhaichao 365bba0555 我的样式优化 2026-09-24 14:19:36 +08:00
zhaichao 403f7e835f 打卡定位 2026-09-24 11:41:39 +08:00
zhaichao bef7f00970 代办申请 2026-09-24 10:53:49 +08:00
zhaichao 30578fcf6c 修改密码 2026-09-24 10:49:17 +08:00
zhaichao 75b8414605 我的 2026-09-24 10:41:35 +08:00
zhaichao af8e8c4623 打卡 2026-09-24 10:34:25 +08:00
zhaichao 5e6e0b5648 他人信息图标 2026-09-24 10:11:22 +08:00
zhaichao 87a8700b30 聊天功能区图标变更 2026-09-24 10:07:08 +08:00
zhaichao bf6396089d 聊天界面信息长按操作风格变更 2026-09-24 10:04:34 +08:00
zhaichao 2078923deb 工作台图标 2026-09-24 09:54:39 +08:00
zhaichao e899b30562 tabbar 2026-09-23 19:21:49 +08:00
zhaichao 74bdeb7a11 通讯录页面布局 2026-09-23 19:10:16 +08:00
zhaichao bfec3f99b1 加号下拉框优化 2026-09-23 18:54:02 +08:00
zhaichao 6ffd33bf56 消息页面加号样式以及交互逻辑优化 2026-09-23 18:51:46 +08:00
zhaichao c5245b0441 移除旧版tabbar 2026-09-23 18:44:28 +08:00
zhaichao 9ad38418c0 页面背景色作统一 2026-09-23 18:43:51 +08:00
41 changed files with 1335 additions and 680 deletions
+1 -1
View File
@@ -51,7 +51,7 @@ export default {
<style scoped> <style scoped>
.hd { .hd {
background: rgba(245, 247, 250, 0.94); background: #F5F7FA;
border-bottom: 1rpx solid rgba(17, 24, 39, 0.06); border-bottom: 1rpx solid rgba(17, 24, 39, 0.06);
position: sticky; position: sticky;
top: 0; top: 0;
-125
View File
@@ -1,125 +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="消息">
<template #icon="{ active }">
<view class="ico" :class="{ on: active }"><view class="bubble" /><view class="tail" /></view>
</template>
</wd-tabbar-item>
<wd-tabbar-item title="通讯录">
<template #icon="{ active }">
<view class="ico heads" :class="{ on: active }">
<view class="head"><view class="dot" /><view class="body" /></view>
<view class="head b"><view class="dot" /><view class="body" /></view>
</view>
</template>
</wd-tabbar-item>
<wd-tabbar-item title="打卡">
<template #icon="{ active }">
<view class="ico" :class="{ on: active }"><view class="clock"><view class="hh" /><view class="mh" /></view></view>
</template>
</wd-tabbar-item>
<wd-tabbar-item title="工作台">
<template #icon="{ active }">
<view class="ico grid" :class="{ on: active }">
<view /><view /><view /><view />
</view>
</template>
</wd-tabbar-item>
<wd-tabbar-item title="我">
<template #icon="{ active }">
<view class="ico" :class="{ on: active }"><view class="me"><view class="dot" /><view class="body" /></view></view>
</template>
</wd-tabbar-item>
</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>
<style scoped>
.ico { width: 44rpx; height: 44rpx; position: relative; }
.bubble {
width: 30rpx; height: 22rpx; margin: 6rpx auto 0;
border: 3rpx solid #8A9199; border-radius: 8rpx; box-sizing: border-box;
}
.on .bubble { border-color: #1677FF; }
.tail {
width: 0; height: 0; margin-left: 14rpx;
border-top: 8rpx solid #8A9199; border-right: 8rpx solid transparent;
}
.on .tail { border-top-color: #1677FF; }
.heads { display: flex; align-items: flex-end; height: 44rpx; padding-left: 2rpx; }
.head {
width: 22rpx; display: flex; flex-direction: column; align-items: center;
}
.head.b { margin-left: -6rpx; }
.dot {
width: 12rpx; height: 12rpx; border: 3rpx solid #8A9199; border-radius: 50%; box-sizing: border-box;
}
.body {
width: 18rpx; height: 8rpx; margin-top: 2rpx;
border: 3rpx solid #8A9199; border-bottom: none; border-radius: 10rpx 10rpx 0 0; box-sizing: border-box;
}
.on .dot, .on .body { border-color: #1677FF; }
.clock {
width: 30rpx; height: 30rpx; margin: 6rpx auto 0;
border: 3rpx solid #8A9199; border-radius: 50%; box-sizing: border-box; position: relative;
}
.on .clock { border-color: #1677FF; }
.hh, .mh { position: absolute; background: #8A9199; border-radius: 2rpx; }
.on .hh, .on .mh { background: #1677FF; }
.hh { width: 3rpx; height: 10rpx; left: 12rpx; top: 4rpx; }
.mh { width: 8rpx; height: 3rpx; left: 12rpx; top: 12rpx; }
.grid {
width: 32rpx; height: 32rpx; margin: 6rpx auto 0;
display: flex; flex-wrap: wrap; gap: 4rpx;
}
.grid view {
width: 12rpx; height: 12rpx; border: 3rpx solid #8A9199; border-radius: 3rpx; box-sizing: border-box;
}
.on .grid view { border-color: #1677FF; }
.me { display: flex; flex-direction: column; align-items: center; padding-top: 4rpx; }
.me .dot { width: 14rpx; height: 14rpx; }
.me .body { width: 26rpx; height: 10rpx; }
</style>
+3 -3
View File
@@ -45,15 +45,15 @@
"navigationBarBackgroundColor": "#F5F7FA", "navigationBarBackgroundColor": "#F5F7FA",
"backgroundColor": "#F5F7FA", "backgroundColor": "#F5F7FA",
"backgroundColorTop": "#F5F7FA", "backgroundColorTop": "#F5F7FA",
"backgroundColorBottom": "#F5F7FA" "backgroundColorBottom": "#E8F1FF"
}, },
"tabBar": { "tabBar": {
"color": "#8A9199", "color": "#8A9199",
"selectedColor": "#1677FF", "selectedColor": "#1677FF",
"backgroundColor": "#FFFFFF", "backgroundColor": "#E8F1FF",
"borderStyle": "white", "borderStyle": "white",
"list": [ "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/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/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" }, { "pagePath": "pages/tabs/workbench", "text": "工作台", "iconPath": "static/tab/work.png", "selectedIconPath": "static/tab/work-on.png" },
+11 -8
View File
@@ -51,7 +51,9 @@
<text class="lab">审批链</text> <text class="lab">审批链</text>
<text class="hint">{{ suggestedLabel || '将按固定审批链流转' }}</text> <text class="hint">{{ suggestedLabel || '将按固定审批链流转' }}</text>
</view> </view>
<wd-button type="primary" block size="large" @click="submit">提交申请</wd-button> <view class="submit">
<wd-button type="primary" block size="large" @click="submit">提交申请</wd-button>
</view>
</view> </view>
</view> </view>
</template> </template>
@@ -164,14 +166,15 @@ export default {
</script> </script>
<style scoped> <style scoped>
.body { padding: 24rpx 28rpx 48rpx; } .body { padding: 16rpx 24rpx 48rpx; }
.field { margin-bottom: 24rpx; } .field { margin-bottom: 24rpx; }
.lab { font-size: 26rpx; color: #374151; display: block; margin-bottom: 10rpx; } .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; border: 1rpx solid rgba(17,24,39,0.06); } .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.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 { padding: 24rpx; margin-bottom: 16rpx; }
.inv .box { margin-top: 12rpx; } .inv .box { margin-top: 12rpx; background: #F5F7FA; }
.link { color: #1677FF; font-size: 28rpx; display: block; margin-bottom: 20rpx; } .link { color: #1677FF; font-size: 28rpx; font-weight: 600; display: block; margin: 4rpx 8rpx 24rpx; }
.hint { font-size: 24rpx; color: #6B7280; line-height: 1.5; } .hint { font-size: 26rpx; color: #111827; line-height: 1.5; }
.submit { margin-top: 8rpx; }
</style> </style>
+46 -12
View File
@@ -5,17 +5,25 @@
<view v-if="!canApply" class="deny">当前账号没有发起申请权限</view> <view v-if="!canApply" class="deny">当前账号没有发起申请权限</view>
<view v-else> <view v-else>
<view class="search"> <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>
<view v-if="!groups.length" class="empty">没有匹配的申请</view>
<view v-for="g in groups" :key="g.category" class="block"> <view v-for="g in groups" :key="g.category" class="block">
<text class="cat">{{ g.category }}</text> <text class="cat">{{ g.category }}</text>
<view class="card"> <view class="card">
<view v-for="k in g.items" :key="k.kind" class="item" @click="open(k)"> <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="t">{{ k.title }}</text>
<text class="d">{{ k.hint }}</text> <text class="d">{{ k.hint }}</text>
</view> </view>
<text class="arr"></text> <wd-icon name="arrow-right" size="28rpx" color="#D1D5DB" />
</view> </view>
</view> </view>
</view> </view>
@@ -29,6 +37,7 @@ import FyHeader from '../../components/fy-header.vue'
import FyScroll from '../../components/fy-scroll.vue' import FyScroll from '../../components/fy-scroll.vue'
import { store } from '../../utils/store.js' import { store } from '../../utils/store.js'
import { groupedApplyKinds, visibleApplyKinds } from '../../utils/apply.js' import { groupedApplyKinds, visibleApplyKinds } from '../../utils/apply.js'
import { tileVisual } from '../../utils/nav.js'
export default { export default {
components: { FyHeader, FyScroll }, components: { FyHeader, FyScroll },
@@ -50,6 +59,9 @@ export default {
} }
}, },
methods: { methods: {
visual(k) {
return tileVisual(k)
},
open(k) { open(k) {
uni.navigateTo({ url: '/pages/apply/form?kind=' + k.kind }) uni.navigateTo({ url: '/pages/apply/form?kind=' + k.kind })
} }
@@ -58,13 +70,35 @@ export default {
</script> </script>
<style scoped> <style scoped>
.search { padding: 16rpx 24rpx; } .search {
.search input { height: 80rpx; background: #F3F6FB; border-radius: 16rpx; padding: 0 24rpx; } margin: 16rpx 24rpx 8rpx;
.block { margin: 8rpx 24rpx 24rpx; } height: 72rpx;
.cat { font-size: 22rpx; color: #4B5563; font-weight: 600; } padding: 0 20rpx;
.item { display: flex; justify-content: space-between; align-items: center; padding: 24rpx; border-bottom: 1rpx solid #E8ECF2; } background: #fff;
.t { display: block; font-size: 28rpx; font-weight: 600; } border-radius: 16rpx;
.d { font-size: 22rpx; color: #6B7280; } display: flex;
.arr { color: #D1D5DB; } align-items: center;
.deny { padding: 80rpx; text-align: center; color: #6B7280; } 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> </style>
+19 -10
View File
@@ -2,14 +2,20 @@
<view class="page edge"> <view class="page edge">
<fy-header :title="title" back /> <fy-header :title="title" back />
<fy-scroll class="edge-scroll" @refresh="load"> <fy-scroll class="edge-scroll" @refresh="load">
<view v-if="loading" class="empty">加载中</view> <view v-if="loading" class="empty">
<view v-else-if="!rows.length" class="empty">暂无记录</view> <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" class="card item" @click="open(r)"> <view v-for="r in rows" :key="r.id || r.no" class="card item" @click="open(r)">
<view> <view class="main">
<text class="t">{{ r.title || r.no || r.typeLabel }}</text> <text class="t">{{ r.title || r.no || r.typeLabel }}</text>
<text class="s">{{ r.status || '' }} · {{ r.createdAt || r.date || '' }}</text> <text class="s">{{ r.createdAt || r.date || '' }}</text>
</view> </view>
<text class="amt">{{ r.amount != null ? '¥' + r.amount : '' }}</text> <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> </view>
</fy-scroll> </fy-scroll>
</view> </view>
@@ -54,9 +60,12 @@ export default {
</script> </script>
<style scoped> <style scoped>
.item { margin: 16rpx 24rpx; padding: 24rpx; display: flex; justify-content: space-between; } .item { margin: 16rpx 24rpx; padding: 24rpx; display: flex; align-items: center; }
.t { display: block; font-size: 28rpx; font-weight: 600; } .main { flex: 1; min-width: 0; }
.s { font-size: 22rpx; color: #6B7280; } .t { display: block; font-size: 28rpx; font-weight: 650; color: #111827; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.amt { color: #1677FF; font-weight: 700; } .s { display: block; margin-top: 8rpx; font-size: 24rpx; color: #6B7280; }
.empty { padding: 80rpx; text-align: center; 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> </style>
+2 -2
View File
@@ -95,11 +95,11 @@ export default {
.stage { flex: 1; position: relative; } .stage { flex: 1; position: relative; }
.wv { position: absolute; inset: 0; } .wv { position: absolute; inset: 0; }
.wait { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; } .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; } .name { margin-top: 24rpx; font-size: 36rpx; font-weight: 700; }
.st { margin-top: 8rpx; color: rgba(255,255,255,0.65); } .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)); } .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; } .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; } .no { background: #DC2626; }
</style> </style>
+168 -55
View File
@@ -7,8 +7,9 @@
@left="exitSelect" @left="exitSelect"
> >
<template #right> <template #right>
<text v-if="!selecting && isGroup" class="ico" @click="goMore">···</text> <view v-if="!selecting && (isGroup || peerId)" class="ico" @click="goMore">
<text v-else-if="!selecting && peerId" class="ico" @click="goMore">···</text> <wd-icon name="ellipsis" size="44rpx" color="#111827" />
</view>
</template> </template>
</fy-header> </fy-header>
@@ -25,23 +26,23 @@
<view v-if="!list.length" class="empty">暂无消息发一条试试</view> <view v-if="!list.length" class="empty">暂无消息发一条试试</view>
<view v-for="(m, i) in list" :key="m.id || i"> <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="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 <view
v-else v-else
class="row" class="row"
:class="{ mine: m.mine, pick: selecting }" :class="{ mine: m.mine, pick: selecting }"
@click="selecting ? togglePick(m) : tapMsg(m)" @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="selecting" class="ck" :class="{ on: isPicked(m) }" />
<view v-if="!m.mine" class="av" @click.stop="openSender(m)"> <view v-if="!m.mine" class="av" @click.stop="openSender(m)">
<fy-file-mark v-if="isFiles" /> <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> <text v-else>{{ initialOf(m) }}</text>
</view> </view>
<view class="col" :class="{ mine: m.mine }"> <view class="col" :class="{ mine: m.mine }">
<text v-if="isGroup && !m.mine" class="who">{{ sender(m) }}</text> <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> <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" /> <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) }"> <view v-else-if="m.kind === 'voice'" class="voice" :style="{ width: voiceW(m) }">
@@ -57,10 +58,10 @@
</view> </view>
<text v-else>{{ m.text }}</text> <text v-else>{{ m.text }}</text>
</view> </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>
<view v-if="m.mine" class="av self"> <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> <text v-else>{{ myInitial }}</text>
</view> </view>
</view> </view>
@@ -81,20 +82,13 @@
</view> </view>
</view> </view>
<view v-if="actionMsg && !selecting" class="mask" @click="closeAct"> <view v-if="actionMsg && !selecting" class="pop-mask" @click="closeAct" @touchmove.stop.prevent>
<view class="sheet" :style="{ paddingBottom: (safeBottom + 12) + 'px' }" @click.stop> <view class="pop" :class="{ below: popBelow }" :style="popBoxStyle" @click.stop>
<text class="sheet-t">消息操作</text> <view v-for="it in actItems" :key="it.k" class="pop-item" @click="runAct(it.k)">
<text class="sheet-s">{{ actPreview }}</text> <wd-icon :name="it.icon" size="22px" color="#fff" />
<view class="sheet-grid"> <text class="pop-lab">{{ it.label }}</text>
<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> </view>
<view class="sheet-cancel" @click="closeAct">取消</view> <view class="pop-caret" :style="{ left: caretLeft + 'px' }" />
</view> </view>
</view> </view>
@@ -154,12 +148,30 @@
<text v-for="e in emos" :key="e" class="em" @click="insertEmoji(e)">{{ e }}</text> <text v-for="e in emos" :key="e" class="em" @click="insertEmoji(e)">{{ e }}</text>
</view> </view>
<view v-else-if="more && !isWork && !selecting" class="panel" :style="{ paddingBottom: safeBottom + 'px' }"> <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('album')">
<view class="tile" @click="pick('camera')"><text class="te">📷</text><text>拍摄</text></view> <wd-icon name="picture" size="52rpx" color="#111827" />
<view class="tile" @click="pick('file')"><text class="te">📎</text><text>文件</text></view> <text>相册</text>
<view v-if="!isGroup" class="tile" @click="startCall(false)"><text class="te"></text><text>语音通话</text></view> </view>
<view v-if="!isGroup" class="tile" @click="startCall(true)"><text class="te"></text><text>视频通话</text></view> <view class="tile" @click="pick('camera')">
<view v-if="isGroup" class="tile" @click="mention"><text class="te">@</text><text>提醒成员</text></view> <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>
<view v-else-if="isWork" class="hint">工作通知为办件卡片不支持快捷回复</view> <view v-else-if="isWork" class="hint">工作通知为办件卡片不支持快捷回复</view>
</view> </view>
@@ -202,6 +214,12 @@ export default {
recWillCancel: false, recWillCancel: false,
recSec: 0, recSec: 0,
actionMsg: null, actionMsg: null,
popTop: 0,
popLeft: 8,
popW: 224,
caretLeft: 24,
popBelow: false,
popAt: 0,
quoting: null, quoting: null,
selecting: false, selecting: false,
picked: [], picked: [],
@@ -232,8 +250,25 @@ export default {
const m = this.actionMsg const m = this.actionMsg
return !!(m && m.mine && !m.recalled && Date.now() - Number(m.time) < 120000) return !!(m && m.mine && !m.recalled && Date.now() - Number(m.time) < 120000)
}, },
actPreview() { actItems() {
return previewOf(this.actionMsg) 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) { onLoad(q) {
@@ -241,12 +276,14 @@ export default {
try { try {
this.safeBottom = Number(uni.getSystemInfoSync().safeAreaInsets && uni.getSystemInfoSync().safeAreaInsets.bottom) || 0 this.safeBottom = Number(uni.getSystemInfoSync().safeAreaInsets && uni.getSystemInfoSync().safeAreaInsets.bottom) || 0
} catch (_) {} } catch (_) {}
this._rec = uni.getRecorderManager() if (typeof uni.getRecorderManager === 'function') {
this._rec.onStop((res) => this.afterRecord(res)) this._rec = uni.getRecorderManager()
this._rec.onError(() => { this._rec.onStop((res) => this.afterRecord(res))
this.recording = false this._rec.onError(() => {
toast('录音失败,请检查麦克风权限') this.recording = false
}) toast('录音失败,请检查麦克风权限')
})
}
}, },
onShow() { onShow() {
this.bindKb() this.bindKb()
@@ -289,7 +326,7 @@ export default {
} }
this.scrollEnd() this.scrollEnd()
} }
uni.onKeyboardHeightChange(this._kb) if (typeof uni.onKeyboardHeightChange === 'function') uni.onKeyboardHeightChange(this._kb)
}, },
unbindKb() { unbindKb() {
if (this._kb) { if (this._kb) {
@@ -338,10 +375,21 @@ export default {
if (this.isGroup && m.readReceipt) { if (this.isGroup && m.readReceipt) {
const r = (m.readReceipt.read || []).length const r = (m.readReceipt.read || []).length
const u = (m.readReceipt.unread || []).length const u = (m.readReceipt.unread || []).length
if (!r && !u) return m.read ? '已读' : '未读'
return '已读 ' + r + '/' + (r + u) return '已读 ' + r + '/' + (r + u)
} }
return m.read ? '已读' : '未读' 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) { showRead(m) {
if (!this.isGroup || !m || !m.readReceipt) return if (!this.isGroup || !m || !m.readReceipt) return
const r = (m.readReceipt.read || []).map((p) => p.name || p).join('、') || '暂无' const r = (m.readReceipt.read || []).map((p) => p.name || p).join('、') || '暂无'
@@ -434,6 +482,11 @@ export default {
this.recSec = Math.floor((Date.now() - this._recAt) / 1000) this.recSec = Math.floor((Date.now() - this._recAt) / 1000)
}, 300) }, 300)
this.bindRecDoc() this.bindRecDoc()
if (!this._rec) {
this.unbindRecDoc()
this.recording = false
return
}
try { try {
this._rec.start({ format: 'mp3', duration: 60000, sampleRate: 16000 }) this._rec.start({ format: 'mp3', duration: 60000, sampleRate: 16000 })
} catch (_) { } catch (_) {
@@ -574,14 +627,53 @@ export default {
} }
}) })
}, },
act(m) { act(m, index) {
if (!m || m.recalled || this.selecting) return if (!m || m.recalled || this.selecting) return
this.more = false this.more = false
this.emoji = false this.emoji = false
this.hideKb() this.hideKb()
this.popAt = Date.now()
this.actionMsg = m 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() { liveMsg() {
const c = store.findConv(this.sid) const c = store.findConv(this.sid)
const m = this.actionMsg const m = this.actionMsg
@@ -687,7 +779,9 @@ export default {
} }
}, },
openSender(m) { 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() { goMore() {
if (this.isGroup) uni.navigateTo({ url: '/pages/chat/group?sid=' + encodeURIComponent(this.sid) }) if (this.isGroup) uni.navigateTo({ url: '/pages/chat/group?sid=' + encodeURIComponent(this.sid) })
@@ -744,8 +838,8 @@ export default {
.col { max-width: 72%; display: flex; flex-direction: column; } .col { max-width: 72%; display: flex; flex-direction: column; }
.col.mine { align-items: flex-end; } .col.mine { align-items: flex-end; }
.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 { 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: #1677FF; color: #fff; } .av.self { background: #E8F1FF; color: #1677FF; }
.av image { width: 76rpx; height: 76rpx; } .face { width: 76rpx; height: 76rpx; display: block; }
.who { font-size: 22rpx; color: #9CA3AF; margin: 0 4rpx 8rpx; } .who { font-size: 22rpx; color: #9CA3AF; margin: 0 4rpx 8rpx; }
.ball { .ball {
background: #fff; background: #fff;
@@ -816,25 +910,44 @@ export default {
.tile { .tile {
width: 148rpx; height: 148rpx; background: #fff; border-radius: 20rpx; width: 148rpx; height: 148rpx; background: #fff; border-radius: 20rpx;
display: flex; flex-direction: column; align-items: center; justify-content: center; display: flex; flex-direction: column; align-items: center; justify-content: center;
font-size: 22rpx; color: #4B5563; gap: 12rpx; font-size: 22rpx; color: #4B5563;
} }
.te { font-size: 44rpx; margin-bottom: 10rpx; }
.em { width: 12.5%; text-align: center; font-size: 44rpx; line-height: 88rpx; } .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; } .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; } .ico { padding: 8rpx 8rpx; font-size: 40rpx; color: #111827; letter-spacing: 2rpx; }
.mask { position: fixed; left: 0; right: 0; top: 0; bottom: 0; background: rgba(17, 24, 39, 0.45); z-index: 40; display: flex; align-items: flex-end; } .pop-mask { position: fixed; left: 0; right: 0; top: 0; bottom: 0; z-index: 40; }
.sheet { width: 100%; background: #fff; border-radius: 28rpx 28rpx 0 0; padding: 28rpx 28rpx 12rpx; } .pop {
.sheet-t { display: block; text-align: center; font-size: 30rpx; font-weight: 650; } position: fixed;
.sheet-s { display: block; text-align: center; font-size: 24rpx; color: #9CA3AF; margin: 8rpx 0 24rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } display: flex;
.sheet-grid { display: flex; flex-wrap: wrap; gap: 16rpx; } flex-wrap: wrap;
.cell { background: #4C4C4C;
width: calc(25% - 12rpx); height: 140rpx; background: #F5F7FA; border-radius: 16rpx; border-radius: 8px;
display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 22rpx; color: #374151; padding: 4px 0;
box-sizing: border-box;
} }
.ce { font-size: 32rpx; margin-bottom: 8rpx; color: #111827; font-weight: 650; } .pop-item {
.sheet-cancel { width: 56px;
margin: 20rpx 0 8rpx; height: 88rpx; border-radius: 16rpx; background: #F5F7FA; height: 62px;
display: flex; align-items: center; justify-content: center; font-size: 28rpx; color: #111827; display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
}
.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;
}
.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; } .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 { flex: 1; height: 96rpx; display: flex; align-items: center; justify-content: center; font-size: 28rpx; color: #1677FF; }
+14 -12
View File
@@ -8,7 +8,8 @@
<view class="card"> <view class="card">
<view v-for="p in people" :key="p.id" class="item" @click="toggle(p.id)"> <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="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"> <view class="meta">
<text class="n">{{ p.name }}</text> <text class="n">{{ p.name }}</text>
<text class="s">{{ p.title }} · {{ p.department }}</text> <text class="s">{{ p.title }} · {{ p.department }}</text>
@@ -16,7 +17,7 @@
</view> </view>
</view> </view>
<view class="pad"> <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>
</view> </view>
</template> </template>
@@ -65,17 +66,18 @@ export default {
</script> </script>
<style scoped> <style scoped>
.field { margin: 24rpx; padding: 8rpx 24rpx; } .field { margin: 16rpx 24rpx; padding: 8rpx 24rpx; }
.field input { height: 80rpx; font-size: 30rpx; } .field input { height: 88rpx; font-size: 30rpx; color: #111827; }
.hint { margin: 0 32rpx 12rpx; font-size: 22rpx; color: #6B7280; } .hint { margin: 0 32rpx 12rpx; font-size: 24rpx; color: #6B7280; }
.card { margin: 0 24rpx 24rpx; overflow: hidden; } .card { margin: 0 24rpx 24rpx; overflow: hidden; }
.item { display: flex; align-items: center; padding: 20rpx 24rpx; } .item { display: flex; align-items: center; padding: 0 24rpx; }
.box { width: 36rpx; height: 36rpx; border: 2rpx solid #D1D5DB; border-radius: 8rpx; margin-right: 16rpx; } .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; } .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; } .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; }
.meta { margin-left: 16rpx; } .av.img { display: block; }
.n { display: block; font-size: 28rpx; font-weight: 600; } .meta { margin-left: 16rpx; flex: 1; min-width: 0; padding: 18rpx 0; }
.s { font-size: 22rpx; color: #6B7280; } .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; } .pad { padding: 0 24rpx 40rpx; }
.off { opacity: 0.45; }
</style> </style>
+8 -8
View File
@@ -97,13 +97,13 @@ export default {
</script> </script>
<style scoped> <style scoped>
.search { padding: 12rpx 24rpx; } .search { padding: 8rpx 24rpx 4rpx; }
.search input { height: 72rpx; background: #ededed; border-radius: 8rpx; padding: 0 24rpx; font-size: 26rpx; } .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: 8rpx 32rpx 16rpx; font-size: 22rpx; color: #888; } .snip { padding: 12rpx 32rpx 8rpx; font-size: 22rpx; color: #6B7280; }
.sec { padding: 16rpx 32rpx 8rpx; font-size: 22rpx; color: #888; background: #f7f7f7; } .sec { padding: 20rpx 32rpx 8rpx; font-size: 22rpx; color: #6B7280; }
.item { display: flex; align-items: center; padding: 20rpx 24rpx; background: #fff; } .item { display: flex; align-items: center; margin: 0 24rpx; padding: 16rpx 0; border-bottom: 1rpx solid #F0F2F5; }
.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; } .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; } .av.img { width: 80rpx; height: 80rpx; }
.n { margin-left: 20rpx; font-size: 30rpx; } .n { margin-left: 20rpx; font-size: 30rpx; color: #111827; }
.empty { padding: 80rpx; text-align: center; color: #888; } .empty { padding: 120rpx 40rpx; text-align: center; color: #6B7280; font-size: 28rpx; }
</style> </style>
+17 -9
View File
@@ -3,7 +3,10 @@
<fy-header title="群聊设置" back /> <fy-header title="群聊设置" back />
<view class="card head"> <view class="card head">
<view class="grid"> <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>
<view class="meta"> <view class="meta">
<text class="gn">{{ title }}</text> <text class="gn">{{ title }}</text>
@@ -17,7 +20,8 @@
</view> </view>
<view class="mems"> <view class="mems">
<view v-for="p in members" :key="p.id" class="mem" @click="open(p)"> <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> <text class="mn">{{ p.name }}</text>
</view> </view>
</view> </view>
@@ -76,6 +80,7 @@ export default {
this.members = ids.map((id) => store.person(id) || { id, name: id, title: '', department: '' }) this.members = ids.map((id) => store.person(id) || { id, name: id, title: '', department: '' })
}, },
open(p) { open(p) {
if (!p || !store.person(p.id)) return
uni.navigateTo({ url: '/pages/contact/detail?id=' + encodeURIComponent(p.id) }) uni.navigateTo({ url: '/pages/contact/detail?id=' + encodeURIComponent(p.id) })
}, },
toggle(field) { toggle(field) {
@@ -105,8 +110,9 @@ export default {
<style scoped> <style scoped>
.head { margin: 24rpx; padding: 28rpx; display: flex; align-items: center; } .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; } .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: #E8ECF2; color: #1677FF; font-size: 18rpx; font-weight: 700; display: flex; align-items: center; justify-content: center; border-radius: 6rpx; } .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; } .meta { margin-left: 20rpx; }
.gn { font-size: 32rpx; font-weight: 700; display: block; } .gn { font-size: 32rpx; font-weight: 700; display: block; }
.gc { font-size: 22rpx; color: #6B7280; } .gc { font-size: 22rpx; color: #6B7280; }
@@ -114,11 +120,13 @@ export default {
.cap { padding: 20rpx 24rpx 8rpx; font-size: 26rpx; font-weight: 700; } .cap { padding: 20rpx 24rpx 8rpx; font-size: 26rpx; font-weight: 700; }
.mems { display: flex; flex-wrap: wrap; padding: 8rpx 16rpx 24rpx; } .mems { display: flex; flex-wrap: wrap; padding: 8rpx 16rpx 24rpx; }
.mem { width: 20%; display: flex; flex-direction: column; align-items: center; margin-bottom: 16rpx; } .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; } .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; } .val { color: #6B7280; }
.sw { color: #6B7280; } .sw { color: #9CA3AF; }
.sw.on { color: #07c160; font-weight: 600; } .sw.on { color: #1677FF; font-weight: 600; }
.danger { color: #fa5151; justify-content: center; border-bottom: none; } .danger { color: #DC2626; justify-content: center; }
</style> </style>
+6 -6
View File
@@ -56,10 +56,10 @@ export default {
</script> </script>
<style scoped> <style scoped>
.search { padding: 16rpx 24rpx; } .search { padding: 8rpx 24rpx 12rpx; }
.search input { height: 72rpx; background: #ededed; border-radius: 8rpx; padding: 0 24rpx; } .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: #888; padding: 80rpx 24rpx; font-size: 26rpx; } .empty { text-align: center; color: #6B7280; padding: 120rpx 40rpx; font-size: 28rpx; }
.item { margin: 0 24rpx; padding: 20rpx 0; border-bottom: 1rpx solid #f0f0f0; } .item { margin: 0 24rpx; padding: 22rpx 0; border-bottom: 1rpx solid #F0F2F5; }
.who { display: block; font-size: 22rpx; color: #888; } .who { display: block; font-size: 22rpx; color: #6B7280; }
.txt { display: block; font-size: 28rpx; margin-top: 6rpx; } .txt { display: block; font-size: 28rpx; margin-top: 8rpx; color: #111827; }
</style> </style>
+6 -5
View File
@@ -105,15 +105,16 @@ export default {
<style scoped> <style scoped>
.head { margin: 24rpx; padding: 28rpx; display: flex; align-items: center; } .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; } .av.img { width: 96rpx; height: 96rpx; }
.meta { flex: 1; margin-left: 20rpx; } .meta { flex: 1; margin-left: 20rpx; }
.n { display: block; font-size: 32rpx; font-weight: 700; } .n { display: block; font-size: 32rpx; font-weight: 700; }
.s { font-size: 22rpx; color: #6B7280; } .s { font-size: 22rpx; color: #6B7280; }
.arr { color: #D1D5DB; } .arr { color: #D1D5DB; }
.card { margin: 0 24rpx 24rpx; overflow: hidden; } .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; } .row { display: flex; justify-content: space-between; align-items: center; padding: 28rpx 24rpx; font-size: 28rpx; border-bottom: 1rpx solid #F0F2F5; }
.sw { color: #6B7280; } .row:last-child { border-bottom: none; }
.sw.on { color: #07c160; font-weight: 600; } .sw { color: #9CA3AF; }
.danger { color: #fa5151; justify-content: center; border-bottom: none; } .sw.on { color: #1677FF; font-weight: 600; }
.danger { color: #DC2626; justify-content: center; }
</style> </style>
+24 -26
View File
@@ -1,36 +1,33 @@
<template> <template>
<view class="page"> <view class="page edge">
<fy-header title="工作通知" back /> <fy-header title="工作通知" back />
<fy-scroll class="edge-scroll" @refresh="load">
<view class="bar"> <view class="bar">
<text>审批 · 协同 · 投标 · 用章 · 公告</text> <text>审批 · 协同 · 投标 · 用章 · 公告</text>
<text class="badge">{{ items.length }} 项待办</text> <text class="count">{{ items.length }} 项待办</text>
</view> </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 v-for="t in items" :key="t.no || t.id" class="card item" @click="open(t)">
<view class="stripe" /> <view class="main">
<view class="body"> <text class="type">{{ t.typeLabel || t.tag || t.type || '待办' }}</text>
<view class="top">
<text class="kind">{{ t.typeLabel || t.tag || t.type || '待办' }}</text>
<text class="time">{{ t.createdAt || t.time || '' }}</text>
</view>
<text class="title">{{ t.title || t.summary || t.no }}</text> <text class="title">{{ t.title || t.summary || t.no }}</text>
<text class="sub">{{ t.submitter || t.applicant || '' }}{{ t.dept ? ' · ' + t.dept : '' }}</text> <text class="sub">{{ metaOf(t) }}</text>
<view class="acts">
<text class="go">去办理</text>
</view>
</view> </view>
<text class="arr"></text>
</view> </view>
</fy-scroll>
</view> </view>
</template> </template>
<script> <script>
import FyHeader from '../../components/fy-header.vue' import FyHeader from '../../components/fy-header.vue'
import FyScroll from '../../components/fy-scroll.vue'
import { store } from '../../utils/store.js' import { store } from '../../utils/store.js'
import { asList, fmtTime } from '../../utils/format.js' import { asList, fmtTime } from '../../utils/format.js'
import { oaGet } from '../../utils/api.js' import { oaGet } from '../../utils/api.js'
export default { export default {
components: { FyHeader }, components: { FyHeader, FyScroll },
data() { data() {
return { items: [] } return { items: [] }
}, },
@@ -51,6 +48,11 @@ export default {
const conv = store.conversations.find((c) => c.type === 'work') const conv = store.conversations.find((c) => c.type === 'work')
if (conv) conv.unread = 0 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) { open(t) {
const no = t.no || t.id || '' const no = t.no || t.id || ''
if (String(t.source || t.type || '') === 'work' || t.taskId) { if (String(t.source || t.type || '') === 'work' || t.taskId) {
@@ -64,16 +66,12 @@ export default {
</script> </script>
<style scoped> <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; } .bar { margin: 16rpx 24rpx 8rpx; padding: 0 8rpx; display: flex; justify-content: space-between; align-items: center; font-size: 22rpx; color: #6B7280; }
.item { margin: 0 24rpx 20rpx; display: flex; overflow: hidden; } .count { color: #1677FF; font-weight: 600; }
.stripe { width: 8rpx; background: #1677FF; } .item { margin: 16rpx 24rpx; padding: 24rpx; display: flex; align-items: center; }
.body { flex: 1; padding: 24rpx; } .main { flex: 1; min-width: 0; }
.top { display: flex; justify-content: space-between; } .type { display: inline-block; font-size: 22rpx; color: #1677FF; background: #E8F1FF; border-radius: 8rpx; padding: 2rpx 10rpx; }
.kind { font-size: 28rpx; font-weight: 700; } .title { display: block; margin-top: 10rpx; font-size: 28rpx; font-weight: 650; color: #111827; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.time { font-size: 22rpx; color: #6B7280; } .sub { display: block; margin-top: 6rpx; font-size: 24rpx; color: #6B7280; }
.title { display: block; font-size: 28rpx; font-weight: 600; margin: 12rpx 0 6rpx; } .arr { color: #C4C9D4; font-size: 32rpx; margin-left: 8rpx; }
.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; }
</style> </style>
+15 -13
View File
@@ -1,7 +1,8 @@
<template> <template>
<view class="page"> <view class="page">
<fy-header title="同事资料" back /> <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"> <view class="card profile">
<image v-if="p.avatar" class="av img" :src="p.avatar" mode="aspectFill" /> <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 v-else class="av">{{ p.name.slice(0, 1) }}</view>
@@ -108,22 +109,23 @@ export default {
</script> </script>
<style scoped> <style scoped>
.body { padding: 24rpx; } .body { padding: 16rpx 24rpx 48rpx; }
.profile { padding: 28rpx; display: flex; align-items: center; } .profile { padding: 32rpx 24rpx; 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 { 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: 20rpx; } .av.img { border-radius: 24rpx; }
.info { margin-left: 24rpx; flex: 1; min-width: 0; } .info { margin-left: 20rpx; flex: 1; min-width: 0; }
.nm { display: flex; align-items: center; gap: 12rpx; } .nm { display: flex; align-items: center; gap: 12rpx; }
.n { font-size: 36rpx; font-weight: 700; } .n { font-size: 36rpx; font-weight: 650; color: #111827; }
.tag { font-size: 20rpx; background: #E8F1FF; color: #4B5563; padding: 2rpx 10rpx; border-radius: 8rpx; } .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: 8rpx; } .s, .ph { display: block; font-size: 24rpx; color: #6B7280; margin-top: 6rpx; }
.copy { color: #1677FF; margin-left: 12rpx; } .copy { color: #1677FF; margin-left: 12rpx; }
.acts { display: flex; gap: 16rpx; margin: 20rpx 0; } .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; } .ai { font-size: 36rpx; color: #1677FF; margin-bottom: 8rpx; }
.rows { overflow: hidden; margin-bottom: 20rpx; } .rows { overflow: hidden; margin-bottom: 20rpx; }
.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; }
.k { color: #4B5563; } .row + .row { border-top: 1rpx solid #F0F2F5; }
.k { color: #6B7280; }
.v { color: #111827; } .v { color: #111827; }
.brand { color: #1677FF; } .brand { color: #1677FF; font-weight: 600; }
</style> </style>
+10 -7
View File
@@ -16,7 +16,9 @@
<text class="lab">说明</text> <text class="lab">说明</text>
<view class="box tall"><textarea v-model="content" /></view> <view class="box tall"><textarea v-model="content" /></view>
</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>
</view> </view>
</template> </template>
@@ -65,10 +67,11 @@ export default {
</script> </script>
<style scoped> <style scoped>
.body { padding: 24rpx; } .body { padding: 16rpx 24rpx 48rpx; }
.field { margin-bottom: 20rpx; } .field { margin-bottom: 24rpx; }
.lab { font-size: 24rpx; color: #4B5563; display: block; margin-bottom: 8rpx; } .lab { font-size: 26rpx; color: #6B7280; display: block; margin-bottom: 10rpx; }
.box { min-height: 88rpx; background: #fff; border-radius: 16rpx; padding: 20rpx 24rpx; } .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: 160rpx; } .box.tall { min-height: 180rpx; }
.box input, .box textarea { width: 100%; } .box input, .box textarea { width: 100%; font-size: 30rpx; color: #111827; }
.submit { margin-top: 8rpx; }
</style> </style>
+22 -7
View File
@@ -3,10 +3,20 @@
<fy-header title="协同详情" back /> <fy-header title="协同详情" back />
<view v-if="row" class="body"> <view v-if="row" class="body">
<view class="card"> <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="t">{{ row.title }}</text>
<text class="s">发起 {{ row.assignerName || '' }} · 办理 {{ row.assigneeName || '' }}</text> <view class="kv">
<text class="s">{{ row.content || row.remark || '' }}</text> <text class="k">发起</text>
<text class="chip">{{ row.status === 'done' ? '已办结' : '待办理' }}</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>
<view v-if="canDone" class="btn-primary" @click="done">标记已办理</view> <view v-if="canDone" class="btn-primary" @click="done">标记已办理</view>
</view> </view>
@@ -55,9 +65,14 @@ export default {
</script> </script>
<style scoped> <style scoped>
.body { padding: 24rpx; } .body { padding: 16rpx 24rpx 48rpx; }
.card { padding: 28rpx; margin-bottom: 24rpx; } .card { padding: 28rpx; margin-bottom: 24rpx; }
.t { display: block; font-size: 34rpx; font-weight: 700; } .top { display: flex; align-items: center; justify-content: space-between; }
.s { display: block; font-size: 24rpx; color: #6B7280; margin-top: 8rpx; } .type { font-size: 22rpx; color: #1677FF; background: #E8F1FF; border-radius: 8rpx; padding: 2rpx 10rpx; }
.chip { display: inline-block; margin-top: 16rpx; } .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> </style>
+21 -15
View File
@@ -7,20 +7,24 @@
</fy-header> </fy-header>
<fy-scroll class="edge-scroll" @refresh="load"> <fy-scroll class="edge-scroll" @refresh="load">
<view class="sec">待我办理</view> <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 v-for="t in pending" :key="t.id" class="card item" @click="open(t)">
<view class="stripe" /> <view class="main">
<view class="body"> <text class="type">待办理</text>
<text class="t">{{ t.title }}</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> </view>
<text class="arr"></text>
</view> </view>
<view class="sec">我发起的</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 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="t">{{ t.title }}</text>
<text class="s">{{ t.assigneeName || '' }} · {{ t.status || '' }}</text> <text class="s">{{ lineOf(t.assigneeName, t.status) }}</text>
</view> </view>
<text class="arr"></text>
</view> </view>
</fy-scroll> </fy-scroll>
</view> </view>
@@ -49,6 +53,9 @@ export default {
this.mine = asList(await oaGet('/work-tasks?tab=review', store.token)) this.mine = asList(await oaGet('/work-tasks?tab=review', store.token))
} catch (_) {} } catch (_) {}
}, },
lineOf(a, b) {
return [a, b].filter(Boolean).join(' · ')
},
open(t) { open(t) {
uni.navigateTo({ url: '/pages/coop/detail?id=' + encodeURIComponent(t.id) }) uni.navigateTo({ url: '/pages/coop/detail?id=' + encodeURIComponent(t.id) })
}, },
@@ -60,13 +67,12 @@ export default {
</script> </script>
<style scoped> <style scoped>
.sec { padding: 24rpx 24rpx 8rpx; font-size: 28rpx; font-weight: 700; } .sec { padding: 24rpx 32rpx 4rpx; font-size: 26rpx; font-weight: 650; color: #6B7280; }
.item { margin: 12rpx 24rpx; display: flex; overflow: hidden; } .item { margin: 16rpx 24rpx; padding: 24rpx; display: flex; align-items: center; }
.stripe { width: 8rpx; background: #1677FF; } .main { flex: 1; min-width: 0; }
.body { flex: 1; padding: 24rpx; } .type { display: inline-block; font-size: 22rpx; color: #1677FF; background: #E8F1FF; border-radius: 8rpx; padding: 2rpx 10rpx; }
.body.wide { padding: 24rpx; } .t { display: block; margin-top: 10rpx; font-size: 28rpx; font-weight: 650; color: #111827; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.t { display: block; font-size: 28rpx; font-weight: 600; } .s { display: block; margin-top: 6rpx; font-size: 24rpx; color: #6B7280; }
.s { font-size: 22rpx; color: #6B7280; } .arr { color: #C4C9D4; font-size: 32rpx; margin-left: 8rpx; }
.empty { padding: 24rpx; text-align: center; color: #6B7280; } .add { color: #1677FF; font-size: 26rpx; font-weight: 650; padding: 8rpx; }
.add { color: #1677FF; font-size: 26rpx; font-weight: 700; padding: 8rpx; }
</style> </style>
+28 -18
View File
@@ -1,23 +1,28 @@
<template> <template>
<view class="page"> <view class="page edge">
<fy-header :title="title" back /> <fy-header :title="title" back />
<view v-if="!allowed" class="deny">当前账号没有{{ title }}查看权限</view> <view v-if="!allowed" class="deny">当前账号没有{{ title }}查看权限</view>
<view v-else> <fy-scroll v-else class="edge-scroll" @refresh="load">
<scroll-view scroll-x class="tabs"> <wd-tabs v-if="tabs.length" v-model="tab" slidable="always" @change="onTab">
<text v-for="t in tabs" :key="t.id" class="tab" :class="{ on: tab === t.id }" @click="switchTab(t)">{{ t.title }}</text> <wd-tab v-for="t in tabs" :key="t.id" :title="t.title" :name="t.id" />
</scroll-view> </wd-tabs>
<view v-if="loading" class="empty">加载中</view> <view v-if="loading" class="empty">
<view v-else-if="!rows.length" class="empty">暂无数据</view> <wd-skeleton theme="paragraph" />
<view v-for="r in rows" :key="r.id || r.no || r.code" class="card item">
<text class="t">{{ titleOf(r) }}</text>
<text class="s">{{ subOf(r) }}</text>
</view> </view>
</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 v-if="subOf(r)" class="s">{{ subOf(r) }}</text>
</view>
</view>
</fy-scroll>
</view> </view>
</template> </template>
<script> <script>
import FyHeader from '../../components/fy-header.vue' import FyHeader from '../../components/fy-header.vue'
import FyScroll from '../../components/fy-scroll.vue'
import { store } from '../../utils/store.js' import { store } from '../../utils/store.js'
import { ERP_MODULES, ERP_TABS } from '../../utils/catalog.js' import { ERP_MODULES, ERP_TABS } from '../../utils/catalog.js'
import { asList, firstText } from '../../utils/format.js' import { asList, firstText } from '../../utils/format.js'
@@ -25,7 +30,7 @@ import { oaGet } from '../../utils/api.js'
import { canMenu } from '../../utils/perms.js' import { canMenu } from '../../utils/perms.js'
export default { export default {
components: { FyHeader }, components: { FyHeader, FyScroll },
data() { data() {
return { id: '', tab: '', rows: [], loading: false } return { id: '', tab: '', rows: [], loading: false }
}, },
@@ -45,6 +50,12 @@ export default {
if (this.allowed) this.load() if (this.allowed) this.load()
}, },
methods: { 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) { switchTab(t) {
this.tab = t.id this.tab = t.id
this.load() this.load()
@@ -110,11 +121,10 @@ export default {
</script> </script>
<style scoped> <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; } .item { margin: 16rpx 24rpx; padding: 24rpx; }
.t { display: block; font-weight: 600; font-size: 28rpx; } .main { min-width: 0; }
.s { font-size: 22rpx; color: #6B7280; } .t { display: block; font-weight: 650; font-size: 28rpx; color: #111827; }
.empty, .deny { padding: 80rpx 40rpx; text-align: center; color: #6B7280; } .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> </style>
+61 -16
View File
@@ -1,46 +1,91 @@
<template> <template>
<view class="page"> <view class="page edge">
<fy-header title="我的资料" back /> <fy-header title="我的资料" back />
<view class="card"> <fy-scroll class="edge-scroll" @refresh="load">
<view v-for="r in rows" :key="r.k" class="row"> <view class="head">
<text class="k">{{ r.k }}</text> <view class="av">{{ initial }}</view>
<text class="v">{{ r.v }}</text> <view class="meta">
<text class="name">{{ name }}</text>
<text class="sub">{{ deptLine }}</text>
</view>
</view> </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>
</view>
</view>
</fy-scroll>
</view> </view>
</template> </template>
<script> <script>
import FyHeader from '../../components/fy-header.vue' import FyHeader from '../../components/fy-header.vue'
import FyScroll from '../../components/fy-scroll.vue'
import { store } from '../../utils/store.js' import { store } from '../../utils/store.js'
import { oaGet } from '../../utils/api.js' import { oaGet } from '../../utils/api.js'
export default { export default {
components: { FyHeader }, components: { FyHeader, FyScroll },
data() { return { profile: {} } }, data() { return { profile: {} } },
computed: { 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() { rows() {
const m = store.me || {} const m = this.me
const p = this.profile || {} const p = this.profile || {}
return [ return [
{ k: '姓名', v: p.realName || m.name },
{ k: '手机', v: p.phone || m.phone }, { k: '手机', v: p.phone || m.phone },
{ k: '部门', v: p.dept || m.department }, { k: '部门', v: p.dept || m.department },
{ k: '职务', v: p.role || m.title }, { k: '职务', v: p.role || m.title },
{ k: '工号', v: p.jobNo || m.jobId || '-' }, { k: '工号', v: p.jobNo || m.jobId },
{ k: '状态', v: p.employeeStatus || m.employeeStatus || '在职' } { k: '状态', v: p.employeeStatus || m.employeeStatus || '在职' }
] ]
} }
}, },
onShow() { onShow() { this.load() },
oaGet('/profile', store.token).then((d) => { this.profile = d || {} }).catch(() => {}) methods: {
load() {
oaGet('/profile', store.token).then((d) => { this.profile = d || {} }).catch(() => {})
}
} }
} }
</script> </script>
<style scoped> <style scoped>
.card { margin: 24rpx; overflow: hidden; } .head {
.row { display: flex; justify-content: space-between; padding: 28rpx 24rpx; border-bottom: 1rpx solid #E8ECF2; } margin: 16rpx 24rpx 20rpx;
.k { color: #6B7280; font-size: 26rpx; } padding: 32rpx 24rpx;
.v { font-size: 26rpx; } 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> </style>
+32 -13
View File
@@ -1,32 +1,51 @@
<template> <template>
<view class="page"> <view class="page edge">
<fy-header title="工资条" back /> <fy-header title="工资条" back />
<view v-if="!rows.length" class="empty">暂无工资条</view> <fy-scroll class="edge-scroll" @refresh="load">
<view v-for="r in rows" :key="r.id || r.month" class="card item"> <wd-status-tip v-if="!rows.length" image="content" tip="暂无工资条" />
<text class="t">{{ r.month || r.period || '工资条' }}</text> <view v-for="r in rows" :key="r.id || r.month" class="card item">
<text class="s">应发 {{ r.gross || r.shouldPay || '-' }} · 实发 {{ r.net || r.actual || '-' }}</text> <view class="main">
</view> <text class="t">{{ r.month || r.period || '工资条' }}</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> </view>
</template> </template>
<script> <script>
import FyHeader from '../../components/fy-header.vue' import FyHeader from '../../components/fy-header.vue'
import FyScroll from '../../components/fy-scroll.vue'
import { store } from '../../utils/store.js' import { store } from '../../utils/store.js'
import { asList } from '../../utils/format.js' import { asList } from '../../utils/format.js'
import { oaGet } from '../../utils/api.js' import { oaGet } from '../../utils/api.js'
export default { export default {
components: { FyHeader }, components: { FyHeader, FyScroll },
data() { return { rows: [] } }, data() { return { rows: [] } },
onShow() { onShow() { this.load() },
oaGet('/profile/payslips', store.token).then((d) => { this.rows = asList(d) }).catch(() => { this.rows = [] }) 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> </script>
<style scoped> <style scoped>
.item { margin: 16rpx 24rpx; padding: 24rpx; } .item { margin: 16rpx 24rpx; padding: 28rpx 24rpx; display: flex; align-items: center; }
.t { display: block; font-weight: 600; } .main { flex: 1; min-width: 0; }
.s { font-size: 24rpx; color: #6B7280; } .t { display: block; font-size: 30rpx; font-weight: 650; color: #111827; }
.empty { padding: 80rpx; text-align: center; color: #6B7280; } .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> </style>
+26 -13
View File
@@ -1,32 +1,45 @@
<template> <template>
<view class="page"> <view class="page edge">
<fy-header title="我的绩效" back /> <fy-header title="我的绩效" back />
<view v-if="!rows.length" class="empty">暂无绩效记录</view> <fy-scroll class="edge-scroll" @refresh="load">
<view v-for="r in rows" :key="r.id || r.period" class="card item"> <wd-status-tip v-if="!rows.length" image="content" tip="暂无绩效记录" />
<text class="t">{{ r.period || r.quarter || '绩效' }}</text> <view v-for="r in rows" :key="r.id || r.period" class="card item">
<text class="s">评级 {{ r.grade || r.level || '-' }} · {{ r.score || '' }}</text> <view class="main">
</view> <text class="t">{{ r.period || r.quarter || '绩效' }}</text>
<text v-if="scoreOf(r)" class="s">得分 {{ scoreOf(r) }}</text>
</view>
<text class="chip">{{ r.grade || r.level || '-' }}</text>
</view>
</fy-scroll>
</view> </view>
</template> </template>
<script> <script>
import FyHeader from '../../components/fy-header.vue' import FyHeader from '../../components/fy-header.vue'
import FyScroll from '../../components/fy-scroll.vue'
import { store } from '../../utils/store.js' import { store } from '../../utils/store.js'
import { asList } from '../../utils/format.js' import { asList } from '../../utils/format.js'
import { oaGet } from '../../utils/api.js' import { oaGet } from '../../utils/api.js'
export default { export default {
components: { FyHeader }, components: { FyHeader, FyScroll },
data() { return { rows: [] } }, data() { return { rows: [] } },
onShow() { onShow() { this.load() },
oaGet('/profile/performance', store.token).then((d) => { this.rows = asList(d) }).catch(() => { this.rows = [] }) 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> </script>
<style scoped> <style scoped>
.item { margin: 16rpx 24rpx; padding: 24rpx; } .item { margin: 16rpx 24rpx; padding: 28rpx 24rpx; display: flex; align-items: center; }
.t { display: block; font-weight: 600; } .main { flex: 1; min-width: 0; }
.s { font-size: 24rpx; color: #6B7280; } .t { display: block; font-size: 30rpx; font-weight: 650; color: #111827; }
.empty { padding: 80rpx; text-align: center; color: #6B7280; } .s { display: block; margin-top: 8rpx; font-size: 24rpx; color: #6B7280; }
</style> </style>
+9 -6
View File
@@ -4,8 +4,11 @@
<fy-scroll class="edge-scroll" @refresh="load"> <fy-scroll class="edge-scroll" @refresh="load">
<wd-status-tip v-if="!rows.length" image="content" tip="暂无公告" /> <wd-status-tip v-if="!rows.length" image="content" tip="暂无公告" />
<view v-for="r in rows" :key="r.id" class="card item"> <view v-for="r in rows" :key="r.id" class="card item">
<view class="top">
<text class="chip">公告</text>
<text class="s">{{ r.createdAt || r.publishAt || '' }}</text>
</view>
<text class="t">{{ r.title }}</text> <text class="t">{{ r.title }}</text>
<text class="s">{{ r.createdAt || r.publishAt || '' }}</text>
<text class="c">{{ r.content || r.summary || '' }}</text> <text class="c">{{ r.content || r.summary || '' }}</text>
</view> </view>
</fy-scroll> </fy-scroll>
@@ -32,9 +35,9 @@ export default {
</script> </script>
<style scoped> <style scoped>
.item { margin: 16rpx 28rpx; padding: 28rpx; } .item { margin: 16rpx 24rpx; padding: 28rpx 24rpx; }
.t { display: block; font-weight: 650; font-size: 30rpx; line-height: 1.45; } .top { display: flex; align-items: center; justify-content: space-between; }
.s { display: block; font-size: 24rpx; color: #6B7280; margin-top: 10rpx; } .s { font-size: 22rpx; color: #9CA3AF; }
.c { display: block; font-size: 28rpx; margin-top: 16rpx; color: #374151; line-height: 1.65; } .t { display: block; margin-top: 16rpx; font-weight: 650; font-size: 30rpx; line-height: 1.45; color: #111827; }
.empty { padding: 120rpx 40rpx; text-align: center; color: #6B7280; font-size: 28rpx; } .c { display: block; font-size: 26rpx; margin-top: 12rpx; color: #4B5563; line-height: 1.65; }
</style> </style>
+79 -22
View File
@@ -1,17 +1,32 @@
<template> <template>
<view class="page"> <view class="page">
<fy-header title="修改密码" /> <fy-header title="修改密码" back />
<view class="body"> <view class="body">
<text class="hint">首次登录或管理员要求改密请设置新密码后继续</text> <view class="hero">
<view class="field"> <view class="mark">
<text class="lab">新密码</text> <wd-icon name="lock-on" size="44rpx" color="#1677FF" />
<view class="box"><input v-model="next" password placeholder="至少 6 位" /></view> </view>
<text class="title">设置新密码</text>
<text class="hint">首次登录或管理员要求改密时请设置新密码后继续使用</text>
</view> </view>
<view class="field"> <view class="card group">
<text class="lab">确认新密码</text> <view class="field">
<view class="box"><input v-model="confirm" password placeholder="再输入一次" /></view> <text class="lab">新密码</text>
<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" :class="{ bad: mismatch }">
<input v-model="confirm" :password="!showConfirm" placeholder="再输入一次" placeholder-class="ph" />
<text class="eye" @click="showConfirm = !showConfirm">{{ showConfirm ? '隐藏' : '显示' }}</text>
</view>
<text v-if="mismatch" class="tip">两次输入不一致</text>
</view>
</view> </view>
<view class="btn-primary" @click="ok">确认修改</view> <wd-button type="primary" block size="large" @click="ok">确认修改</wd-button>
<text v-if="error" class="err">{{ error }}</text> <text v-if="error" class="err">{{ error }}</text>
</view> </view>
</view> </view>
@@ -24,38 +39,80 @@ import { toast } from '../../utils/format.js'
export default { export default {
components: { FyHeader }, components: { FyHeader },
computed: {
mismatch() {
return this.confirm.length > 0 && this.next !== this.confirm
}
},
data() { data() {
return { next: '', confirm: '', error: '', off: null, left: false } return { next: '', confirm: '', error: '', off: null, left: false, waiting: false, showNext: false, showConfirm: false }
}, },
onShow() { onShow() {
if (this.off) this.off() if (this.off) this.off()
this.off = store.on(() => { this.off = store.on(() => {
this.error = store.error this.error = store.error
if (this.left || store.phase !== 'app') return if (this.left || !this.waiting || store.phase !== 'app') return
this.left = true this.leave()
uni.switchTab({ url: '/pages/tabs/messages' })
}) })
}, },
onHide() { onHide() {
if (this.off) this.off() this.drop()
this.off = null },
onUnload() {
this.drop()
}, },
methods: { methods: {
drop() {
if (this.off) this.off()
this.off = null
},
leave() {
this.left = true
this.waiting = false
uni.switchTab({ url: '/pages/tabs/messages' })
},
async ok() { async ok() {
if (!this.next || this.next.length < 6) return toast('密码至少 6 位') if (!this.next || this.next.length < 6) return toast('密码至少 6 位')
if (this.next !== this.confirm) return toast('两次密码不一致') if (this.next !== this.confirm) return toast('两次密码不一致')
this.waiting = true
await store.changePassword(this.next, this.confirm) await store.changePassword(this.next, this.confirm)
if (store.error) this.waiting = false
} }
} }
} }
</script> </script>
<style scoped> <style scoped>
.body { padding: 40rpx; } .body { padding: 32rpx 32rpx 48rpx; }
.hint { font-size: 26rpx; color: #6B7280; display: block; margin-bottom: 32rpx; } .hero { display: flex; flex-direction: column; align-items: center; margin: 16rpx 0 36rpx; }
.field { margin-bottom: 24rpx; } .mark {
.lab { font-size: 24rpx; color: #4B5563; display: block; margin-bottom: 8rpx; } width: 96rpx;
.box { height: 96rpx; background: #fff; border-radius: 16rpx; padding: 0 24rpx; display: flex; align-items: center; } height: 96rpx;
.box input { flex: 1; } border-radius: 28rpx;
.err { color: #DC2626; font-size: 24rpx; margin-top: 16rpx; display: block; } 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> </style>
+16 -4
View File
@@ -2,8 +2,12 @@
<view class="page"> <view class="page">
<fy-header title="附件预览" back /> <fy-header title="附件预览" back />
<view class="body"> <view class="body">
<text class="name">{{ name || '附件' }}</text> <view class="card sheet">
<view class="btn-primary" @click="open">打开 / 预览</view> <view class="mark"></view>
<text class="name">{{ name || '附件' }}</text>
<text class="hint">系统会用本机应用打开这个附件</text>
</view>
<wd-button type="primary" block size="large" @click="open">打开 / 预览</wd-button>
</view> </view>
</view> </view>
</template> </template>
@@ -31,6 +35,14 @@ export default {
</script> </script>
<style scoped> <style scoped>
.body { padding: 48rpx 32rpx; } .body { padding: 32rpx 24rpx; }
.name { display: block; font-size: 28rpx; margin-bottom: 32rpx; text-align: center; } .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> </style>
+24 -15
View File
@@ -1,32 +1,38 @@
<template> <template>
<view class="page"> <view class="page edge">
<fy-header title="工作汇报" back /> <fy-header title="工作汇报" back />
<view v-if="!can" class="deny">当前账号没有写汇报权限</view> <view v-if="!can" class="deny">当前账号没有写汇报权限</view>
<view v-else> <fy-scroll v-else class="edge-scroll" @refresh="load">
<view class="card form"> <view class="card form">
<text class="lab">类型</text>
<picker :range="kinds" @change="(e) => kind = kinds[e.detail.value]"> <picker :range="kinds" @change="(e) => kind = kinds[e.detail.value]">
<view class="box">{{ kind }}</view> <view class="box">{{ kind }}</view>
</picker> </picker>
<text class="lab">内容</text>
<view class="box tall"><textarea v-model="content" placeholder="今日工作内容 / 周报摘要" /></view> <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> </view>
<wd-status-tip v-if="!rows.length" image="content" tip="暂无汇报" />
<view v-for="r in rows" :key="r.id" class="card item"> <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="s">{{ r.createdAt || r.date || '' }}</text> <text class="type">{{ r.kind || r.title || '汇报' }}</text>
<text class="c">{{ r.content || r.summary || '' }}</text> <text class="s">{{ r.createdAt || r.date || '' }}</text>
</view>
<text class="c">{{ r.content || r.summary || r.title || '' }}</text>
</view> </view>
</view> </fy-scroll>
</view> </view>
</template> </template>
<script> <script>
import FyHeader from '../../components/fy-header.vue' import FyHeader from '../../components/fy-header.vue'
import FyScroll from '../../components/fy-scroll.vue'
import { store } from '../../utils/store.js' import { store } from '../../utils/store.js'
import { asList, toast } from '../../utils/format.js' import { asList, toast } from '../../utils/format.js'
import { oaGet, oaPost } from '../../utils/api.js' import { oaGet, oaPost } from '../../utils/api.js'
export default { export default {
components: { FyHeader }, components: { FyHeader, FyScroll },
data() { data() {
return { kinds: ['日报', '周报', '月报', '工时'], kind: '日报', content: '', rows: [] } return { kinds: ['日报', '周报', '月报', '工时'], kind: '日报', content: '', rows: [] }
}, },
@@ -58,12 +64,15 @@ export default {
</script> </script>
<style scoped> <style scoped>
.form { margin: 24rpx; padding: 24rpx; } .form { margin: 16rpx 24rpx; padding: 24rpx; }
.box { background: #F3F6FB; border-radius: 12rpx; padding: 20rpx; margin-bottom: 16rpx; } .lab { display: block; font-size: 26rpx; color: #6B7280; margin-bottom: 10rpx; }
.box.tall { min-height: 160rpx; } .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; } .item { margin: 0 24rpx 16rpx; padding: 24rpx; }
.t { display: block; font-weight: 600; } .top { display: flex; align-items: center; justify-content: space-between; }
.s { font-size: 22rpx; color: #6B7280; } .type { font-size: 22rpx; color: #1677FF; background: #E8F1FF; border-radius: 8rpx; padding: 2rpx 10rpx; }
.c { display: block; font-size: 26rpx; margin-top: 8rpx; } .s { font-size: 22rpx; color: #9CA3AF; }
.deny { padding: 80rpx; text-align: center; color: #6B7280; } .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> </style>
+109 -43
View File
@@ -4,39 +4,70 @@
<fy-scroll class="edge-scroll" dock @refresh="reload"> <fy-scroll class="edge-scroll" dock @refresh="reload">
<view v-if="!canPunch" class="deny">当前账号没有打卡权限</view> <view v-if="!canPunch" class="deny">当前账号没有打卡权限</view>
<view v-else class="body"> <view v-else class="body">
<view class="clock-box"> <view class="day">
<text class="now">{{ now }}</text> <text class="date">{{ dateLabel }}</text>
<text class="shift">班次 {{ rules.workStart }}{{ rules.workEnd }}{{ locName ? ' · ' + locName : '' }}</text> <text class="shift">弹性 09:0009:30 / 18:0018:30</text>
</view> </view>
<view class="grid"> <view class="lieu">多出工时记调休 {{ lieuText }}</view>
<view class="stat card">
<text class="lab">上班</text> <view class="slots">
<text class="val" :class="{ ok: inRec }">{{ inRec ? inRec.time + ' 已打卡' : '未打卡' }}</text> <view class="slot">
<text class="tiny">{{ inRec ? (inRec.status || '正常') : '待打卡' }}</text> <view class="dot" :class="{ on: inRec }" />
<view class="slot-m">
<text class="lab">上班 09:0009:30</text>
<text class="val" :class="{ ok: inRec }">{{ inRec ? (inRec.time || '') + ' 已打卡' : '未打卡' }}</text>
</view>
<text class="tiny">{{ inRec ? (inRec.status || '正常') : '' }}</text>
</view> </view>
<view class="stat card"> <view class="slot">
<text class="lab">下班</text> <view class="dot" :class="{ on: outRec }" />
<text class="val" :class="{ ok: outRec }">{{ outRec ? outRec.time + ' 已打卡' : '未打卡' }}</text> <view class="slot-m">
<text class="tiny">{{ outRec ? (outRec.status || '正常') : '待打卡' }}</text> <text class="lab">下班 18:0018:30</text>
<text class="val" :class="{ ok: outRec }">{{ outRec ? (outRec.time || '') + ' 已打卡' : '未打卡' }}</text>
</view>
<text class="tiny">{{ outRec ? (outRec.status || '正常') : '' }}</text>
</view> </view>
</view> </view>
<wd-button type="primary" block size="large" :loading="punching" @click="doPunch">{{ outRec ? '更新打卡' : '打卡' }}</wd-button>
<text class="geo">将使用手机定位校验打卡范围</text> <view class="ring-wrap">
<view class="ring" :class="{ busy: punching }" @click="doPunch">
<text class="now">{{ now }}</text>
<text class="act">{{ punchLabel }}</text>
</view>
</view>
<view class="geo">
<wd-icon name="location" size="28rpx" color="#1677FF" />
<text>{{ locName || '将使用手机定位校验打卡范围' }}</text>
</view>
<view class="quick"> <view class="quick">
<text class="q" @click="goKind('card')">补卡</text> <view class="q" @click="goKind('card')">
<text class="q" @click="goKind('leave')">请假</text> <wd-icon name="edit-1" size="40rpx" color="#1677FF" />
<text class="q" @click="goKind('overtime')">加班</text> <text>补卡</text>
<text class="q" @click="goKind('outing')">外出</text> </view>
<view class="q" @click="goKind('leave')">
<wd-icon name="calendar" size="40rpx" color="#1677FF" />
<text>请假</text>
</view>
<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>
<view class="sec">
<text class="st">今日及近期记录</text> <view class="sec">打卡记录</view>
</view> <view class="card log">
<view class="card">
<view v-if="!records.length" class="empty">暂无记录</view> <view v-if="!records.length" class="empty">暂无记录</view>
<view v-for="r in records" :key="r.id || r.time" class="rec"> <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="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> </view>
<text class="ok">{{ r.status || '正常' }}</text> <text class="ok">{{ r.status || '正常' }}</text>
</view> </view>
@@ -50,7 +81,7 @@
import FyHeader from '../../components/fy-header.vue' import FyHeader from '../../components/fy-header.vue'
import FyScroll from '../../components/fy-scroll.vue' import FyScroll from '../../components/fy-scroll.vue'
import { store } from '../../utils/store.js' import { store } from '../../utils/store.js'
import { clockText, toast } from '../../utils/format.js' import { clockText, flexCreditMinutes, flexCreditText, toast } from '../../utils/format.js'
export default { export default {
components: { FyHeader, FyScroll }, components: { FyHeader, FyScroll },
@@ -61,6 +92,17 @@ export default {
canPunch() { return store.can('personal', 'punch') }, canPunch() { return store.can('personal', 'punch') },
rules() { return store.attendRules }, rules() { return store.attendRules },
records() { return store.attendance || [] }, 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() { locName() {
const locs = this.rules.locations || [] const locs = this.rules.locations || []
return locs[0] ? (locs[0].name || locs[0].title || '') : '' return locs[0] ? (locs[0].name || locs[0].title || '') : ''
@@ -70,6 +112,11 @@ export default {
}, },
outRec() { outRec() {
return this.records.find((r) => /下班|out/i.test(String(r.title || r.kind || r.type || ''))) 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() { onShow() {
@@ -105,24 +152,43 @@ export default {
</script> </script>
<style scoped> <style scoped>
.body { padding: 16rpx 24rpx 40rpx; } .body { padding: 8rpx 28rpx 48rpx; }
.clock-box { background: #fff; border-radius: 24rpx; padding: 48rpx 0 40rpx; text-align: center; border: 1rpx solid rgba(17,24,39,0.06); } .day { display: flex; justify-content: space-between; align-items: baseline; padding: 8rpx 8rpx 20rpx; }
.now { display: block; font-size: 72rpx; font-weight: 650; letter-spacing: 2rpx; color: #111827; } .date { font-size: 32rpx; font-weight: 650; color: #111827; }
.shift { font-size: 22rpx; color: #4B5563; } .shift { font-size: 24rpx; color: #6B7280; }
.grid { display: flex; gap: 16rpx; margin: 20rpx 0; } .lieu { margin: 0 8rpx 16rpx; font-size: 24rpx; color: #1677FF; }
.stat { flex: 1; padding: 24rpx; } .slots { background: #fff; border-radius: 20rpx; padding: 8rpx 8rpx; border: 1rpx solid rgba(17,24,39,0.06); }
.lab { font-size: 26rpx; font-weight: 600; display: block; } .slot { display: flex; align-items: center; padding: 22rpx 20rpx; }
.val { font-size: 24rpx; color: #4B5563; display: block; margin-top: 12rpx; } .slot + .slot { border-top: 1rpx solid #F0F2F5; }
.val.ok { color: #16A34A; font-weight: 650; } .dot { width: 16rpx; height: 16rpx; border-radius: 50%; background: #D1D5DB; margin-right: 16rpx; flex-shrink: 0; }
.tiny { font-size: 20rpx; color: #6B7280; } .dot.on { background: #1677FF; }
.geo { display: block; text-align: center; font-size: 22rpx; color: #4B5563; margin: 16rpx 0 24rpx; } .slot-m { flex: 1; min-width: 0; }
.body :deep(.wd-button) { margin-top: 8rpx; } .lab { display: block; font-size: 28rpx; font-weight: 600; color: #111827; }
.quick { display: flex; gap: 12rpx; } .val { display: block; font-size: 24rpx; color: #9CA3AF; margin-top: 6rpx; }
.q { flex: 1; height: 72rpx; background: #fff; border-radius: 12rpx; text-align: center; line-height: 72rpx; font-size: 24rpx; } .val.ok { color: #16A34A; }
.sec { margin: 32rpx 0 12rpx; font-size: 28rpx; font-weight: 600; } .tiny { font-size: 22rpx; color: #16A34A; }
.rec { display: flex; justify-content: space-between; align-items: center; padding: 24rpx 28rpx; border-bottom: 1rpx solid #E8ECF2; } .ring-wrap { display: flex; justify-content: center; padding: 56rpx 0 28rpx; }
.rt { display: block; font-size: 28rpx; font-weight: 600; } .ring {
.rl { display: block; font-size: 22rpx; color: #6B7280; } 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; } .ok { color: #16A34A; font-size: 24rpx; }
.empty { padding: 40rpx; text-align: center; color: #6B7280; } .empty { padding: 40rpx; text-align: center; color: #6B7280; }
.deny { padding: 120rpx 40rpx; text-align: center; color: #6B7280; } .deny { padding: 120rpx 40rpx; text-align: center; color: #6B7280; }
+119 -46
View File
@@ -2,33 +2,48 @@
<view class="page edge"> <view class="page edge">
<fy-header title="通讯录"> <fy-header title="通讯录">
<template #right> <template #right>
<text class="ico" @click="goGroup">建群</text> <view class="ico" @click="goGroup">
<wd-icon name="usergroup" size="40rpx" color="#111827" />
</view>
</template> </template>
</fy-header> </fy-header>
<fy-scroll class="edge-scroll" dock @refresh="reload"> <fy-scroll class="edge-scroll" dock @refresh="reload">
<wd-search v-model="q" placeholder="搜索同事、部门或手机号" hide-cancel placeholder-left /> <view class="finder">
<view class="org" @click="goGroups"> <wd-icon name="search" size="28rpx" color="#9CA3AF" />
<view class="obox">👥</view> <input v-model="q" class="finder-input" placeholder="搜索同事、部门或手机号" placeholder-class="finder-ph" confirm-type="search" />
<view class="ometa"> <view v-if="q" class="finder-clear" @click="q = ''">
<text class="on">群聊</text> <wd-icon name="close-bold" size="22rpx" color="#9CA3AF" />
<text class="os">{{ groupCount }} 个群</text>
</view> </view>
<text class="arr"></text>
</view> </view>
<view class="org" @click="goSearch"> <view class="shortcuts">
<view class="obox">🏛</view> <view class="org" @click="goGroups">
<view class="ometa"> <view class="obox group">
<text class="on">全员架构</text> <wd-icon name="usergroup" size="36rpx" color="#1677FF" />
<text class="os"> {{ deptCount }} 个部门 · {{ peopleCount }} 位在职成员</text> </view>
<view class="ometa">
<text class="on">群聊</text>
<text class="os">{{ groupCount }} 个群</text>
</view>
<wd-icon name="arrow-right" size="28rpx" color="#D1D5DB" />
</view>
<view class="org" @click="goSearch">
<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>
</view>
<wd-icon name="arrow-right" size="28rpx" color="#D1D5DB" />
</view> </view>
<text class="arr"></text>
</view> </view>
<view v-for="g in groups" :key="g.name" class="block"> <view v-for="g in groups" :key="g.name" class="block">
<view class="dept" @click="toggle(g.name)"> <view class="dept" @click="toggle(g.name)">
<text>{{ g.name }}</text> <text class="dept-name">{{ g.name }}</text>
<text class="cnt">({{ g.people.length }})</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>
<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 v-for="p in g.people" :key="p.id" class="item" @click="openPerson(p)">
<view class="av-wrap"> <view class="av-wrap">
<image v-if="p.avatar" class="av" :src="p.avatar" mode="aspectFill" /> <image v-if="p.avatar" class="av" :src="p.avatar" mode="aspectFill" />
@@ -37,16 +52,21 @@
</view> </view>
<view class="meta"> <view class="meta">
<text class="name">{{ p.name }}</text> <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> </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>
</view> </view>
<view v-if="!groups.length" class="empty">没有匹配的同事</view> <view v-if="!groups.length" class="empty">没有匹配的同事</view>
<text class="foot">仅显示当前所属企业成员 · 数据实时同步</text> <text class="foot">仅显示当前所属企业成员</text>
</fy-scroll> </fy-scroll>
</view> </view>
</template> </template>
@@ -132,28 +152,81 @@ export default {
</script> </script>
<style scoped> <style scoped>
.search { padding: 16rpx 24rpx; } .finder {
.search input { height: 80rpx; background: #fff; border-radius: 20rpx; padding: 0 28rpx; font-size: 28rpx; border: 1rpx solid rgba(17,24,39,0.06); } margin: 8rpx 24rpx 12rpx;
.org { margin: 0 24rpx 16rpx; background: #fff; border-radius: 16rpx; padding: 20rpx; display: flex; align-items: center; } height: 56rpx;
.obox { width: 64rpx; height: 64rpx; border-radius: 12rpx; background: #E8ECF2; display: flex; align-items: center; justify-content: center; } padding: 0 20rpx;
.ometa { flex: 1; margin-left: 16rpx; } border-radius: 12rpx;
.on { display: block; font-size: 28rpx; font-weight: 700; } background: #fff;
.os { font-size: 22rpx; color: #6B7280; } display: flex;
.arr { color: #D1D5DB; } align-items: center;
.block { margin: 8rpx 24rpx 24rpx; } color: #9CA3AF;
.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; } .finder-input {
.item { display: flex; align-items: center; padding: 20rpx 20rpx; } flex: 1;
.av-wrap { position: relative; width: 72rpx; height: 72rpx; } height: 56rpx;
.av { width: 72rpx; height: 72rpx; border-radius: 50%; } margin-left: 8rpx;
.av.txt { background: #E8ECF2; color: #4B5563; display: flex; align-items: center; justify-content: center; font-weight: 600; } font-size: 26rpx;
.dot { position: absolute; right: 0; bottom: 0; width: 16rpx; height: 16rpx; border-radius: 50%; background: #006c49; border: 2rpx solid #fff; } line-height: 56rpx;
.meta { flex: 1; margin-left: 16rpx; min-width: 0; } color: #111827;
.name { display: block; font-size: 28rpx; font-weight: 600; } }
.sub { font-size: 22rpx; color: #6B7280; } .finder-ph { color: #9CA3AF; font-size: 26rpx; }
.chip { font-size: 20rpx; color: #1677FF; background: #E8F1FF; padding: 4rpx 12rpx; border-radius: 999rpx; margin-right: 8rpx; } .finder-clear {
.mini { width: 56rpx; text-align: center; font-size: 28rpx; color: #1677FF; } width: 40rpx;
.empty { text-align: center; color: #6B7280; padding: 80rpx; } height: 40rpx;
.foot { display: block; text-align: center; color: #6B7280; font-size: 22rpx; padding: 20rpx 0 40rpx; } display: flex;
.ico { padding: 8rpx 16rpx; color: #1677FF; font-size: 26rpx; } 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> </style>
+88 -60
View File
@@ -3,48 +3,56 @@
<fy-header title="我的" /> <fy-header title="我的" />
<fy-scroll class="edge-scroll" dock @refresh="reload"> <fy-scroll class="edge-scroll" dock @refresh="reload">
<view class="body"> <view class="body">
<view class="card profile"> <view class="profile" @click="goArchive">
<view class="row"> <image v-if="me && me.avatar" class="av" :src="me.avatar" mode="aspectFill" />
<image v-if="me && me.avatar" class="av" :src="me.avatar" mode="aspectFill" /> <view v-else class="av txt">{{ initial }}</view>
<view v-else class="av txt">{{ initial }}</view> <view class="info">
<view class="info"> <view class="nm">
<view class="nm"> <text class="name">{{ me ? me.name : '' }}</text>
<text class="name">{{ me ? me.name : '' }}</text> <text v-if="statusText" class="tag">{{ statusText }}</text>
<wd-tag type="success" plain>在职</wd-tag>
</view>
<text class="dept">{{ me ? (me.department + ' · ' + me.title) : '' }}</text>
<text class="ph">{{ phone }}{{ job }}</text>
</view> </view>
<text class="sub">{{ deptLine }}</text>
<text class="sub">{{ phone }}{{ job }}</text>
</view> </view>
<view class="stats"> <wd-icon name="qrcode" size="36rpx" color="#111827" />
<view class="s" @click="switchClock"> <text class="chev"></text>
<text class="n">{{ attendDays }}</text> </view>
<text class="l">本月出勤()</text>
</view> <view class="group">
<view class="s mid" @click="goTodo"> <view class="cell" @click="switchClock">
<text class="n">{{ todoCount }}</text> <wd-icon name="location" size="40rpx" color="#1677FF" />
<text class="l">待办申请()</text> <text class="ct">本月出勤</text>
</view> <text class="cv">{{ attendDays }} </text>
<view class="s"> <text class="chev"></text>
<text class="n ok">正常</text> </view>
<text class="l">考勤状态</text> <view class="cell" @click="goTodo">
</view> <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> </view>
<view class="card menu"> <view v-if="menus.length" class="group">
<wd-cell-group border> <view v-for="a in menus" :key="a.id" class="cell" @click="open(a)">
<wd-cell v-for="a in menus" :key="a.id" :title="a.title" is-link @click="open(a)" /> <wd-icon :name="iconOf(a)" size="40rpx" :color="colorOf(a)" />
</wd-cell-group> <text class="ct">{{ a.title }}</text>
<text class="chev"></text>
</view>
</view> </view>
<view class="card menu"> <view class="group">
<wd-cell-group border> <view class="cell" @click="goPwd">
<wd-cell title="修改密码" is-link @click="goPwd" /> <wd-icon name="lock-on" size="40rpx" color="#576B95" />
<wd-cell is-link @click="logout"> <text class="ct">修改密码</text>
<template #title><text class="danger">退出登录</text></template> <text class="chev"></text>
</wd-cell> </view>
</wd-cell-group> </view>
<view class="group">
<view class="cell center" @click="logout">
<text class="danger">退出登录</text>
</view>
</view> </view>
</view> </view>
</fy-scroll> </fy-scroll>
@@ -56,6 +64,7 @@ import FyHeader from '../../components/fy-header.vue'
import FyScroll from '../../components/fy-scroll.vue' import FyScroll from '../../components/fy-scroll.vue'
import { store } from '../../utils/store.js' import { store } from '../../utils/store.js'
import { maskPhone } from '../../utils/format.js' import { maskPhone } from '../../utils/format.js'
import { tileVisual } from '../../utils/nav.js'
const ME_IDS = ['archive', 'salary', 'perf', 'expense', 'report', 'apply'] const ME_IDS = ['archive', 'salary', 'perf', 'expense', 'report', 'apply']
@@ -67,6 +76,13 @@ export default {
computed: { computed: {
me() { return store.me }, me() { return store.me },
initial() { return this.me ? this.me.name.slice(0, 1) : '我' }, 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) }, phone() { return maskPhone(this.me && this.me.phone) },
job() { return this.me && this.me.jobId ? ' · 工号: ' + this.me.jobId : '' }, job() { return this.me && this.me.jobId ? ' · 工号: ' + this.me.jobId : '' },
todoCount() { return store.todoCount || 0 }, todoCount() { return store.todoCount || 0 },
@@ -81,14 +97,19 @@ export default {
}, },
onHide() { if (this.off) this.off() }, onHide() { if (this.off) this.off() },
methods: { 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) { open(a) {
if (a.url.startsWith('/pages/tabs/')) uni.switchTab({ url: a.url.split('?')[0] }) if (a.url.startsWith('/pages/tabs/')) uni.switchTab({ url: a.url.split('?')[0] })
else uni.navigateTo({ url: a.url }) else uni.navigateTo({ url: a.url })
}, },
switchClock() { uni.switchTab({ url: '/pages/tabs/clock' }) }, switchClock() { uni.switchTab({ url: '/pages/tabs/clock' }) },
goTodo() { goTodo() { uni.navigateTo({ url: '/pages/todo/list' }) },
if (store.can('personal', 'approve')) uni.navigateTo({ url: '/pages/todo/list' })
},
reload() { store.refreshPerms() }, reload() { store.refreshPerms() },
goPwd() { uni.navigateTo({ url: '/pages/password/password' }) }, goPwd() { uni.navigateTo({ url: '/pages/password/password' }) },
logout() { logout() {
@@ -108,27 +129,34 @@ export default {
</script> </script>
<style scoped> <style scoped>
.body { padding: 8rpx 28rpx 48rpx; } .body { padding: 16rpx 0 48rpx; }
.profile { padding: 32rpx 28rpx 24rpx; } .profile {
.row { display: flex; align-items: center; } display: flex; align-items: center;
.av { width: 112rpx; height: 112rpx; border-radius: 50%; } margin: 8rpx 24rpx 20rpx;
.av.txt { background: #1677FF; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 40rpx; font-weight: 650; } padding: 32rpx 24rpx;
.info { margin-left: 24rpx; flex: 1; min-width: 0; } 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; } .nm { display: flex; align-items: center; gap: 12rpx; }
.name { font-size: 36rpx; font-weight: 650; } .name { font-size: 36rpx; font-weight: 650; color: #111827; }
.st { font-size: 22rpx; background: #E8F8EF; color: #16A34A; padding: 4rpx 12rpx; border-radius: 8rpx; } .tag { font-size: 22rpx; color: #16A34A; background: #E8F8EF; border-radius: 8rpx; padding: 2rpx 10rpx; }
.dept, .ph { display: block; font-size: 24rpx; color: #6B7280; margin-top: 8rpx; } .sub { display: block; font-size: 24rpx; color: #6B7280; margin-top: 6rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.stats { display: flex; margin-top: 28rpx; padding-top: 24rpx; border-top: 1rpx solid #F0F2F5; } .group {
.s { flex: 1; text-align: center; } margin: 0 24rpx 20rpx;
.mid { border-left: 1rpx solid #F0F2F5; border-right: 1rpx solid #F0F2F5; } background: #fff;
.n { display: block; font-size: 34rpx; font-weight: 650; color: #111827; } border-radius: 20rpx;
.n.ok { color: #16A34A; font-size: 28rpx; } border: 1rpx solid rgba(17, 24, 39, 0.06);
.l { font-size: 22rpx; color: #6B7280; margin-top: 4rpx; } overflow: hidden;
.menu { margin-top: 20rpx; overflow: hidden; } }
.mi { display: flex; align-items: center; min-height: 100rpx; padding: 0 28rpx; } .cell { display: flex; align-items: center; min-height: 104rpx; padding: 0 24rpx; gap: 20rpx; }
.mi + .mi { border-top: 1rpx solid #F0F2F5; } .cell + .cell { border-top: 1rpx solid #F0F2F5; }
.ii { width: 48rpx; color: #1677FF; font-weight: 650; } .cell.center { justify-content: center; }
.it { flex: 1; font-size: 28rpx; } .ct { flex: 1; font-size: 30rpx; color: #111827; }
.arr { color: #C4C9D4; font-size: 32rpx; } .cv { font-size: 26rpx; color: #9CA3AF; }
.danger { color: #DC2626; } .chev { color: #C8C8C8; font-size: 36rpx; }
.danger { font-size: 30rpx; color: #DC2626; }
</style> </style>
+91 -14
View File
@@ -2,9 +2,20 @@
<view class="page msg edge"> <view class="page msg edge">
<fy-header title="消息" :extra="unreadLabel"> <fy-header title="消息" :extra="unreadLabel">
<template #right> <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> </template>
</fy-header> </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"> <fy-scroll class="edge-scroll list" dock @refresh="reload">
<view class="finder" @click="goSearch"> <view class="finder" @click="goSearch">
<wd-icon name="search" size="28rpx" color="#9CA3AF" /> <wd-icon name="search" size="28rpx" color="#9CA3AF" />
@@ -22,6 +33,9 @@
<view class="av-wrap"> <view class="av-wrap">
<image v-if="c.avatar && c.type !== 'files'" class="av" :src="c.avatar" mode="aspectFill" /> <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="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> <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-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" /> <text v-else-if="c.unread && c.muted" class="dotu" />
@@ -50,7 +64,7 @@ import { store } from '../../utils/store.js'
export default { export default {
components: { FyHeader, FyFileMark, FyScroll }, components: { FyHeader, FyFileMark, FyScroll },
data() { data() {
return { rows: [], unread: 0, off: null } return { rows: [], unread: 0, off: null, menu: false, dropTop: 64 }
}, },
computed: { computed: {
unreadLabel() { unreadLabel() {
@@ -58,6 +72,11 @@ export default {
return '(' + (this.unread > 99 ? '99+' : this.unread) + ')' 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() { onShow() {
if (this.off) this.off() if (this.off) this.off()
this.guard() this.guard()
@@ -66,6 +85,7 @@ export default {
this.off = store.on(() => this.pull()) this.off = store.on(() => this.pull())
}, },
onHide() { onHide() {
this.menu = false
if (this.off) this.off() if (this.off) this.off()
this.off = null this.off = null
}, },
@@ -81,6 +101,10 @@ export default {
reload() { reload() {
store.refreshPeoplePhotos().finally(() => this.pull()) 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) { avText(c) {
if (c.type === 'work') return '工' if (c.type === 'work') return '工'
if (c.type === 'group') return '群' if (c.type === 'group') return '群'
@@ -124,14 +148,9 @@ export default {
} }
}) })
}, },
plus() { newGroup() {
uni.showActionSheet({ this.menu = false
itemList: ['发起群聊', '添加同事'], uni.navigateTo({ url: '/pages/chat/create' })
success: (res) => {
if (res.tapIndex === 0) uni.navigateTo({ url: '/pages/chat/create' })
else uni.navigateTo({ url: '/pages/search/search' })
}
})
}, },
goSearch() { goSearch() {
uni.navigateTo({ uni.navigateTo({
@@ -145,19 +164,19 @@ export default {
</script> </script>
<style scoped> <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 { .finder {
margin: 8rpx 24rpx 12rpx; margin: 8rpx 24rpx 12rpx;
height: 56rpx; height: 56rpx;
border-radius: 12rpx; border-radius: 12rpx;
background: #F3F4F6; background: #fff;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #9CA3AF; color: #9CA3AF;
} }
.finder-txt { margin-left: 8rpx; font-size: 26rpx; line-height: 1; } .finder-txt { margin-left: 8rpx; font-size: 26rpx; line-height: 1; }
.list { background: #fff; } .list { background: #F5F7FA; }
.ico.sm { font-size: 34rpx; color: #1677FF; } .ico.sm { font-size: 34rpx; color: #1677FF; }
.item { display: flex; align-items: center; padding: 22rpx 28rpx; background: #fff; } .item { display: flex; align-items: center; padding: 22rpx 28rpx; background: #fff; }
.item.pin { background: #F7F8FA; } .item.pin { background: #F7F8FA; }
@@ -167,6 +186,8 @@ export default {
.av.work { background: #1677FF; color: #fff; } .av.work { background: #1677FF; color: #fff; }
.av.file { background: #F3F6FB; color: #4B5563; } .av.file { background: #F3F6FB; color: #4B5563; }
.av.plain { background: #E8F1FF; color: #1677FF; } .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; } .abs { position: absolute; top: -8rpx; right: -8rpx; }
.dotu { position: absolute; top: -4rpx; right: -4rpx; width: 16rpx; height: 16rpx; border-radius: 50%; background: #DC2626; } .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; } .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; } .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; } .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; } .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; } .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> </style>
+22 -15
View File
@@ -2,7 +2,9 @@
<view class="page edge"> <view class="page edge">
<fy-header :title="greet"> <fy-header :title="greet">
<template #right> <template #right>
<text class="ico" @click="goSearch">搜索</text> <view class="ico" @click="goSearch">
<wd-icon name="search" size="40rpx" color="#111827" />
</view>
</template> </template>
</fy-header> </fy-header>
<fy-scroll class="edge-scroll" dock @refresh="reload"> <fy-scroll class="edge-scroll" dock @refresh="reload">
@@ -40,7 +42,9 @@
<view v-if="!flows.length" class="empty">暂无可用审批流请确认账号已开通申请权限</view> <view v-if="!flows.length" class="empty">暂无可用审批流请确认账号已开通申请权限</view>
<view class="grid"> <view class="grid">
<view v-for="a in flows" :key="a.key" class="app" @click="openFlow(a)"> <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> <text class="al">{{ a.label }}</text>
</view> </view>
</view> </view>
@@ -52,8 +56,8 @@
<view v-if="apps.length" class="card apps"> <view v-if="apps.length" class="card apps">
<view class="grid"> <view class="grid">
<view v-for="a in apps" :key="a.id" class="app" @click="open(a)"> <view v-for="a in apps" :key="a.id" class="app" @click="open(a)">
<view class="icon"> <view class="icon" :style="{ background: tileOf(a).bg }">
<text class="mark">{{ mark(a.title) }}</text> <wd-icon :name="tileOf(a).name" size="44rpx" :color="tileOf(a).color" />
<text v-if="a.badge === 'todo' && todoCount" class="badge abs">{{ todoCount }}</text> <text v-if="a.badge === 'todo' && todoCount" class="badge abs">{{ todoCount }}</text>
</view> </view>
<text class="al">{{ a.title }}</text> <text class="al">{{ a.title }}</text>
@@ -66,7 +70,9 @@
</view> </view>
<view v-if="erp.length" class="card mods"> <view v-if="erp.length" class="card mods">
<view v-for="m in erp" :key="m.id" class="mod" @click="open(m)"> <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="mt">{{ m.title }}</text>
<text class="arr"></text> <text class="arr"></text>
</view> </view>
@@ -101,7 +107,7 @@ import { store } from '../../utils/store.js'
import { toast } from '../../utils/format.js' import { toast } from '../../utils/format.js'
import { oaPost } from '../../utils/api.js' import { oaPost } from '../../utils/api.js'
import { visibleApplyKinds } from '../../utils/apply.js' import { visibleApplyKinds } from '../../utils/apply.js'
import { openHref, tileEmoji } from '../../utils/nav.js' import { openHref, tileVisual } from '../../utils/nav.js'
export default { export default {
components: { FyHeader, FyScroll }, components: { FyHeader, FyScroll },
@@ -121,7 +127,7 @@ export default {
label: s.label || s.title || '入口', label: s.label || s.title || '入口',
desc: s.desc || '', desc: s.desc || '',
to: s.to || s.path || s.url || '', to: s.to || s.path || s.url || '',
emoji: tileEmoji(s) emoji: tileVisual(s).name
})).filter((s) => s.to) })).filter((s) => s.to)
if (fromApi.length) return fromApi if (fromApi.length) return fromApi
const me = store.me || {} const me = store.me || {}
@@ -135,7 +141,7 @@ export default {
label: k.title, label: k.title,
desc: k.hint, desc: k.hint,
to: '/pages/apply/form?kind=' + k.kind, 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() { pending() {
@@ -189,7 +195,9 @@ export default {
else toast('没有发起申请权限') else toast('没有发起申请权限')
}, },
goTodo() { uni.navigateTo({ url: '/pages/todo/list' }) }, 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 }, pendKey(t) { return t.no || t.id || t.to || t.title },
openPending(t) { openPending(t) {
if (t.source === 'work' || t.taskId) { if (t.source === 'work' || t.taskId) {
@@ -239,19 +247,18 @@ export default {
.grid { display: flex; flex-wrap: wrap; } .grid { display: flex; flex-wrap: wrap; }
.app { width: 25%; display: flex; flex-direction: column; align-items: center; padding: 16rpx 4rpx; box-sizing: border-box; } .app { width: 25%; display: flex; flex-direction: column; align-items: center; padding: 16rpx 4rpx; box-sizing: border-box; }
.icon { .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; display: flex; align-items: center; justify-content: center;
position: relative; margin-bottom: 10rpx; position: relative; margin-bottom: 10rpx;
} }
.mark { font-size: 30rpx; font-weight: 650; color: #1677FF; }
.abs { position: absolute; top: -8rpx; right: -8rpx; } .abs { position: absolute; top: -8rpx; right: -8rpx; }
.al { font-size: 24rpx; text-align: center; color: #111827; line-height: 1.35; } .al { font-size: 24rpx; text-align: center; color: #111827; line-height: 1.35; }
.mod { display: flex; align-items: center; padding: 22rpx 16rpx; } .mod { display: flex; align-items: center; padding: 22rpx 16rpx; }
.mod + .mod { border-top: 1rpx solid #F0F2F5; } .mod + .mod { border-top: 1rpx solid #F0F2F5; }
.mi { .mi {
width: 64rpx; height: 64rpx; border-radius: 16rpx; background: #F3F6FB; width: 64rpx; height: 64rpx; border-radius: 16rpx; background: #E8F1FF;
color: #1677FF; display: flex; align-items: center; justify-content: center; display: flex; align-items: center; justify-content: center;
font-weight: 650; margin-right: 16rpx; margin-right: 16rpx;
} }
.mt { flex: 1; font-size: 28rpx; } .mt { flex: 1; font-size: 28rpx; }
.todo { display: flex; align-items: center; padding: 24rpx 24rpx; margin-bottom: 16rpx; } .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); } .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; } .et { display: block; font-size: 28rpx; font-weight: 600; }
.es { display: block; font-size: 24rpx; color: #6B7280; margin-top: 8rpx; } .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> </style>
+12 -3
View File
@@ -3,8 +3,12 @@
<fy-header title="审批详情" back /> <fy-header title="审批详情" back />
<view v-if="row" class="body"> <view v-if="row" class="body">
<view class="card"> <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="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 class="s">单号 {{ row.no }}</text>
<text v-if="row.node" class="s">当前节点 {{ row.node }}</text> <text v-if="row.node" class="s">当前节点 {{ row.node }}</text>
<text v-if="row.amount != null && row.amount !== ''" class="amt">¥ {{ row.amount }}</text> <text v-if="row.amount != null && row.amount !== ''" class="amt">¥ {{ row.amount }}</text>
@@ -56,7 +60,7 @@
</view> </view>
</view> </view>
</view> </view>
<view v-else class="empty">{{ err || '加载中…' }}</view> <wd-status-tip v-else image="content" :tip="err || '加载中…'" />
</view> </view>
</template> </template>
@@ -65,7 +69,7 @@ import FyHeader from '../../components/fy-header.vue'
import FyFiles from '../../components/fy-files.vue' import FyFiles from '../../components/fy-files.vue'
import { store } from '../../utils/store.js' import { store } from '../../utils/store.js'
import { oaGet } from '../../utils/api.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' import { collectFiles, fileId } from '../../utils/files.js'
const LABELS = { const LABELS = {
@@ -120,6 +124,9 @@ export default {
const logs = (this.row && this.row.logs) || [] const logs = (this.row && this.row.logs) || []
return Array.isArray(logs) ? [...logs].reverse() : [] return Array.isArray(logs) ? [...logs].reverse() : []
}, },
statusText() {
return flowStatus(this.row && this.row.status)
},
canAct() { canAct() {
if (!this.row) return false if (!this.row) return false
const st = String(this.row.status || '') const st = String(this.row.status || '')
@@ -155,6 +162,8 @@ export default {
<style scoped> <style scoped>
.body { padding: 24rpx 28rpx 200rpx; } .body { padding: 24rpx 28rpx 200rpx; }
.card { padding: 28rpx; margin-bottom: 16rpx; } .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; } .block { padding: 24rpx 28rpx; }
.h { display: block; font-size: 30rpx; font-weight: 650; margin-bottom: 16rpx; } .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; } .t { display: block; font-size: 34rpx; font-weight: 650; line-height: 1.4; }
+3 -2
View File
@@ -17,7 +17,7 @@
<text class="t">{{ r.title || r.no }}</text> <text class="t">{{ r.title || r.no }}</text>
<text class="s">{{ r.submitter || r.assigneeName || '' }}</text> <text class="s">{{ r.submitter || r.assigneeName || '' }}</text>
</view> </view>
<wd-tag type="primary" plain>{{ r.status || '待处理' }}</wd-tag> <wd-tag type="primary" plain>{{ statusText(r) }}</wd-tag>
<text class="arr"></text> <text class="arr"></text>
</view> </view>
</view> </view>
@@ -29,7 +29,7 @@
import FyHeader from '../../components/fy-header.vue' import FyHeader from '../../components/fy-header.vue'
import FyScroll from '../../components/fy-scroll.vue' import FyScroll from '../../components/fy-scroll.vue'
import { store } from '../../utils/store.js' 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' import { oaGet } from '../../utils/api.js'
export default { export default {
@@ -82,6 +82,7 @@ export default {
this.loading = false this.loading = false
} }
}, },
statusText(r) { return flowStatus(r && r.status) },
open(r) { open(r) {
if (r.source === 'work' || r.taskId) { if (r.source === 'work' || r.taskId) {
uni.navigateTo({ url: '/pages/coop/detail?id=' + encodeURIComponent(r.id || r.taskId) }) uni.navigateTo({ url: '/pages/coop/detail?id=' + encodeURIComponent(r.id || r.taskId) })
Binary file not shown.

Before

Width:  |  Height:  |  Size: 952 B

After

Width:  |  Height:  |  Size: 476 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

+11 -11
View File
@@ -8,25 +8,25 @@ export const PERSONAL_APPS = [
{ id: 'todo', title: '待办审批', icon: '☑', menu: 'personal', action: 'approve', url: '/pages/todo/list', badge: 'todo' }, { 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: '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: '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: '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: 'report', title: '工作汇报', icon: '📝', menu: 'personal', action: 'report', url: '/pages/reports/list' },
{ id: 'salary', title: '工资条', icon: '💳', menu: 'personal', action: 'view', url: '/pages/me/payslip' }, { 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: 'archive', title: '我的资料', icon: '👤', menu: 'personal', action: 'view', url: '/pages/me/archive' },
{ id: 'notice', title: '公告', icon: '📢', menu: 'personal', action: 'view', url: '/pages/notice/list' }, { 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: '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' } { id: 'contacts', title: '通讯录', icon: '☎', menu: 'personal', action: 'view', url: '/pages/tabs/contacts' }
] ]
export const ERP_MODULES = [ export const ERP_MODULES = [
{ id: 'bidding', title: '投标管理', icon: '', menu: 'bidding', action: 'view', url: '/pages/erp/hub?id=bidding' }, { 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: '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: '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: '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: '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: '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: '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' } { id: 'system', title: '系统管理', icon: '⚙', menu: 'system', action: 'view', url: '/pages/erp/hub?id=system' }
] ]
+57
View File
@@ -92,6 +92,35 @@ export function pad2(n) {
return String(n).padStart(2, '0') return String(n).padStart(2, '0')
} }
/** 弹性:9:009:30 上班,18:0018: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) { export function clockText(date) {
const d = date || new Date() const d = date || new Date()
return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}` return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`
@@ -119,6 +148,34 @@ export function convType(s) {
return 'direct' 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') { export function toast(title, icon = 'none') {
uni.showToast({ title: String(title || ''), icon, duration: 2200 }) uni.showToast({ title: String(title || ''), icon, duration: 2200 })
} }
+85 -16
View File
@@ -63,23 +63,92 @@ export function openHref(webPath, extra = {}) {
return true return true
} }
const EMOJI = { const BLUE = ['#1677FF', '#E8F1FF']
expense: '🧾', loan: '¥', leave: '休', trip: '✈', procurement: '购', const CYAN = ['#0891B2', '#E6F7FB']
overtime: '加', outing: '出', card: '补', transfer: '转', regularize: '正', const GREEN = ['#16A34A', '#E8F8EF']
resign: '离', layoff: '裁', seal: '章', receive: '领', clock: '📍', const ORANGE = ['#EA580C', '#FFF1E8']
todo: '☑', apply: '✎', report: '▤', salary: '💳', perf: '↗', const AMBER = ['#D97706', '#FFF6E8']
archive: '👤', notice: '📢', coop: '🤝', contacts: '☎', const VIOLET = ['#7C3AED', '#F3EEFF']
bidding: '◎', project: '▣', finance: '◈', hr: '☰', contracts: '▤', const ROSE = ['#E11D48', '#FFE8EE']
seals: '◆', crm: '◇', system: '⚙' 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) { export function tileEmoji(item) {
if (item && item.emoji) return item.emoji return tileVisual(item).name
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) : '·'
} }
+70 -39
View File
@@ -73,7 +73,7 @@ function mapMsg(raw, meId, typeu) {
href: String((raw && raw.href) || p.href || ''), href: String((raw && raw.href) || p.href || ''),
duration: Number((raw && raw.duration) || p.duration) || 0, duration: Number((raw && raw.duration) || p.duration) || 0,
quote: p.quote || raw.quote || null, 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 readReceipt: (raw && raw.readReceipt) || p.readReceipt || null
} }
} }
@@ -394,8 +394,14 @@ export const store = {
return return
} }
if (typeu === ImTypeu.read) { if (typeu === ImTypeu.read) {
const sid = String(data.sessionId || pack.to || '') const from = String(pack.from || data.from || '')
const conv = this.findConv(sid) || this.findConv(this.dmIdOf(pack.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) { if (conv) {
;(conv.messages || []).forEach((m) => { ;(conv.messages || []).forEach((m) => {
if (m.mine && !m.recalled) m.read = true if (m.mine && !m.recalled) m.read = true
@@ -472,9 +478,16 @@ export const store = {
this.activeSid = conv.id this.activeSid = conv.id
try { try {
const rows = await api.messages(conv.id, this.token) 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 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.messages = incoming.concat(extras)
conv.unread = 0 conv.unread = 0
await api.markRead(conv.id, this.token) await api.markRead(conv.id, this.token)
@@ -781,12 +794,15 @@ export const store = {
async createGroup(name, memberIds) { async createGroup(name, memberIds) {
const ids = [...new Set([this.me && this.me.id, ...(memberIds || []).map(String)])].filter(Boolean) const ids = [...new Set([this.me && this.me.id, ...(memberIds || []).map(String)])].filter(Boolean)
const title = String(name || '').trim() || '群聊' const title = String(name || '').trim() || '群聊'
let sid = '' if (ids.length < 2) throw new Error('请至少再选一位同事')
try { const data = await api.createGroup({
const data = await api.createGroup({ name: title, memberIds: ids }, this.token) title,
sid = String((data && (data.id || data.sessionId)) || '') name: title,
} catch (_) {} memberIds: ids,
if (!sid) sid = 'g:' + Date.now() members: ids
}, this.token)
const sid = String((data && (data.id || data.sessionId)) || '')
if (!sid) throw new Error('创建失败')
let conv = this.findConv(sid) let conv = this.findConv(sid)
if (!conv) { if (!conv) {
conv = { conv = {
@@ -796,8 +812,16 @@ export const store = {
avatar: '', avatar: '',
peerId: '', peerId: '',
memberIds: ids, memberIds: ids,
messages: [],
unread: 0, unread: 0,
messages: [{
id: 'sys-' + sid,
kind: 'system',
text: '你创建了群聊',
time: Date.now(),
mine: false,
fromId: '',
recalled: false
}],
lastMessage: '你创建了群聊', lastMessage: '你创建了群聊',
lastTime: fmtListTime(Date.now()), lastTime: fmtListTime(Date.now()),
pinned: false, pinned: false,
@@ -841,45 +865,52 @@ export const store = {
try { try {
this.attendRules.locations = asList(await api.oaGet('/profile/attendance/locations', this.token)) this.attendRules.locations = asList(await api.oaGet('/profile/attendance/locations', this.token))
} catch (_) {} } catch (_) {}
try { if (this.can('system', 'attend')) {
const pack = await api.oaGet('/system/attend-rules', this.token) try {
if (pack && typeof pack === 'object') { const pack = await api.oaGet('/system/attend-rules', this.token)
this.attendRules.workStart = pack.workStart || this.attendRules.workStart if (pack && typeof pack === 'object') {
this.attendRules.workEnd = pack.workEnd || this.attendRules.workEnd this.attendRules.workStart = pack.workStart || this.attendRules.workStart
} this.attendRules.workEnd = pack.workEnd || this.attendRules.workEnd
} catch (_) {} }
} catch (_) {}
}
try { try {
this.attendance = asList(await api.oaGet('/profile/attendance', this.token)) this.attendance = asList(await api.oaGet('/profile/attendance', this.token))
} catch (_) {} } catch (_) {}
this.emit() this.emit()
}, },
punch() { _locate() {
let type = 'gcj02'
// #ifdef H5
type = 'wgs84'
// #endif
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
uni.getLocation({ uni.getLocation({
type: 'gcj02', type,
isHighAccuracy: true, isHighAccuracy: false,
success: async (pos) => { timeout: 8000,
try { success: resolve,
const rec = await api.oaPost('/profile/attendance/punch', { fail: reject
source: 'mobile',
latitude: pos.latitude,
longitude: pos.longitude,
accuracyM: pos.accuracy
}, this.token)
await this.refreshAttendance()
resolve((rec && rec.title) || '打卡成功')
} catch (e) {
reject(e)
}
},
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) { async audit(no, action, comment) {
await api.oaPost(`/workflow/requests/${no}/audit`, { action, comment }, this.token) await api.oaPost(`/workflow/requests/${no}/audit`, { action, comment }, this.token)
await this.refreshWorkbench() await this.refreshWorkbench()