Compare commits

..

12 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
16 changed files with 286 additions and 117 deletions
+46 -12
View File
@@ -5,17 +5,25 @@
<view v-if="!canApply" class="deny">当前账号没有发起申请权限</view>
<view v-else>
<view class="search">
<input v-model="q" placeholder="搜索审批流程或申请事项..." />
<wd-icon name="search" size="28rpx" color="#9CA3AF" />
<input v-model="q" placeholder="搜索审批流程或申请事项" placeholder-class="ph" confirm-type="search" />
<view v-if="q" class="clear" @click="q = ''">
<wd-icon name="close-bold" size="22rpx" color="#9CA3AF" />
</view>
</view>
<view v-if="!groups.length" class="empty">没有匹配的申请</view>
<view v-for="g in groups" :key="g.category" class="block">
<text class="cat">{{ g.category }}</text>
<view class="card">
<view v-for="k in g.items" :key="k.kind" class="item" @click="open(k)">
<view>
<view class="ico" :style="{ background: visual(k).bg }">
<wd-icon :name="visual(k).name" size="36rpx" :color="visual(k).color" />
</view>
<view class="txt">
<text class="t">{{ k.title }}</text>
<text class="d">{{ k.hint }}</text>
</view>
<text class="arr"></text>
<wd-icon name="arrow-right" size="28rpx" color="#D1D5DB" />
</view>
</view>
</view>
@@ -29,6 +37,7 @@ import FyHeader from '../../components/fy-header.vue'
import FyScroll from '../../components/fy-scroll.vue'
import { store } from '../../utils/store.js'
import { groupedApplyKinds, visibleApplyKinds } from '../../utils/apply.js'
import { tileVisual } from '../../utils/nav.js'
export default {
components: { FyHeader, FyScroll },
@@ -50,6 +59,9 @@ export default {
}
},
methods: {
visual(k) {
return tileVisual(k)
},
open(k) {
uni.navigateTo({ url: '/pages/apply/form?kind=' + k.kind })
}
@@ -58,13 +70,35 @@ export default {
</script>
<style scoped>
.search { padding: 16rpx 24rpx; }
.search input { height: 80rpx; background: #F3F6FB; border-radius: 16rpx; padding: 0 24rpx; }
.block { margin: 8rpx 24rpx 24rpx; }
.cat { font-size: 22rpx; color: #4B5563; font-weight: 600; }
.item { display: flex; justify-content: space-between; align-items: center; padding: 24rpx; border-bottom: 1rpx solid #E8ECF2; }
.t { display: block; font-size: 28rpx; font-weight: 600; }
.d { font-size: 22rpx; color: #6B7280; }
.arr { color: #D1D5DB; }
.deny { padding: 80rpx; text-align: center; color: #6B7280; }
.search {
margin: 16rpx 24rpx 8rpx;
height: 72rpx;
padding: 0 20rpx;
background: #fff;
border-radius: 16rpx;
display: flex;
align-items: center;
gap: 12rpx;
border: 1rpx solid rgba(17, 24, 39, 0.06);
}
.search input { flex: 1; height: 72rpx; font-size: 28rpx; }
.ph { color: #9CA3AF; }
.clear { width: 40rpx; height: 40rpx; display: flex; align-items: center; justify-content: center; }
.block { margin: 20rpx 24rpx 8rpx; }
.cat { display: block; margin: 0 8rpx 12rpx; font-size: 24rpx; color: #6B7280; font-weight: 600; }
.item { display: flex; align-items: center; padding: 22rpx 24rpx; }
.item + .item { border-top: 1rpx solid #F0F2F5; }
.ico {
width: 72rpx;
height: 72rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.txt { flex: 1; min-width: 0; margin: 0 20rpx; }
.t { display: block; font-size: 30rpx; font-weight: 600; color: #111827; }
.d { display: block; margin-top: 6rpx; font-size: 24rpx; color: #6B7280; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.empty, .deny { padding: 120rpx 40rpx; text-align: center; color: #6B7280; font-size: 28rpx; }
</style>
+2 -2
View File
@@ -95,11 +95,11 @@ export default {
.stage { flex: 1; position: relative; }
.wv { position: absolute; inset: 0; }
.wait { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.av { width: 160rpx; height: 160rpx; border-radius: 50%; background: #1677FF; display: flex; align-items: center; justify-content: center; font-size: 64rpx; font-weight: 700; }
.av { width: 160rpx; height: 160rpx; border-radius: 40rpx; background: #E8F1FF; color: #1677FF; display: flex; align-items: center; justify-content: center; font-size: 64rpx; font-weight: 700; }
.name { margin-top: 24rpx; font-size: 36rpx; font-weight: 700; }
.st { margin-top: 8rpx; color: rgba(255,255,255,0.65); }
.acts { display: flex; justify-content: center; gap: 32rpx; padding: 32rpx 24rpx calc(32rpx + env(safe-area-inset-bottom)); }
.btn { min-width: 200rpx; height: 80rpx; border-radius: 999rpx; display: flex; align-items: center; justify-content: center; font-size: 28rpx; font-weight: 600; }
.ok { background: #006c49; }
.ok { background: #16A34A; }
.no { background: #DC2626; }
</style>
+10 -8
View File
@@ -26,7 +26,7 @@
<view v-if="!list.length" class="empty">暂无消息发一条试试</view>
<view v-for="(m, i) in list" :key="m.id || i">
<view v-if="showTime(i)" class="chip-time">{{ timeOf(m) }}</view>
<view v-if="m.recalled" class="sys">{{ m.text || '已撤回一条消息' }}</view>
<view v-if="m.recalled || m.kind === 'system'" class="sys">{{ m.text || '已撤回一条消息' }}</view>
<view
v-else
class="row"
@@ -37,7 +37,7 @@
<view v-if="selecting" class="ck" :class="{ on: isPicked(m) }" />
<view v-if="!m.mine" class="av" @click.stop="openSender(m)">
<fy-file-mark v-if="isFiles" />
<image v-else-if="avatarOf(m)" :src="avatarOf(m)" mode="aspectFill" />
<image v-else-if="avatarOf(m)" class="face" :src="avatarOf(m)" mode="aspectFill" />
<text v-else>{{ initialOf(m) }}</text>
</view>
<view class="col" :class="{ mine: m.mine }">
@@ -61,7 +61,7 @@
<text v-if="m.mine" class="tm" :class="{ read: readDone(m) }" @click="showRead(m)">{{ readLabel(m) }}</text>
</view>
<view v-if="m.mine" class="av self">
<image v-if="myAvatar" :src="myAvatar" mode="aspectFill" />
<image v-if="myAvatar" class="face" :src="myAvatar" mode="aspectFill" />
<text v-else>{{ myInitial }}</text>
</view>
</view>
@@ -160,11 +160,11 @@
<wd-icon name="folder" size="52rpx" color="#111827" />
<text>文件</text>
</view>
<view v-if="!isGroup" class="tile" @click="startCall(false)">
<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" class="tile" @click="startCall(true)">
<view v-if="!isGroup && !isFiles" class="tile" @click="startCall(true)">
<wd-icon name="video" size="52rpx" color="#111827" />
<text>视频通话</text>
</view>
@@ -779,7 +779,9 @@ export default {
}
},
openSender(m) {
if (m.fromId) uni.navigateTo({ url: '/pages/contact/detail?id=' + encodeURIComponent(m.fromId) })
const id = m && m.fromId
if (!id || !store.person(id)) return
uni.navigateTo({ url: '/pages/contact/detail?id=' + encodeURIComponent(id) })
},
goMore() {
if (this.isGroup) uni.navigateTo({ url: '/pages/chat/group?sid=' + encodeURIComponent(this.sid) })
@@ -836,8 +838,8 @@ export default {
.col { max-width: 72%; display: flex; flex-direction: column; }
.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.self { background: #1677FF; color: #fff; }
.av image { width: 76rpx; height: 76rpx; }
.av.self { background: #E8F1FF; color: #1677FF; }
.face { width: 76rpx; height: 76rpx; display: block; }
.who { font-size: 22rpx; color: #9CA3AF; margin: 0 4rpx 8rpx; }
.ball {
background: #fff;
+14 -12
View File
@@ -8,7 +8,8 @@
<view class="card">
<view v-for="p in people" :key="p.id" class="item" @click="toggle(p.id)">
<view class="box" :class="{ on: picked.includes(p.id) }" />
<view class="av">{{ p.name.slice(0, 1) }}</view>
<image v-if="p.avatar" class="av img" :src="p.avatar" mode="aspectFill" />
<view v-else class="av">{{ (p.name || '?').slice(0, 1) }}</view>
<view class="meta">
<text class="n">{{ p.name }}</text>
<text class="s">{{ p.title }} · {{ p.department }}</text>
@@ -16,7 +17,7 @@
</view>
</view>
<view class="pad">
<view class="btn-primary" :class="{ off: !can }" @click="ok">创建并进入</view>
<wd-button type="primary" block size="large" :disabled="!can" @click="ok">创建并进入</wd-button>
</view>
</view>
</template>
@@ -65,17 +66,18 @@ export default {
</script>
<style scoped>
.field { margin: 24rpx; padding: 8rpx 24rpx; }
.field input { height: 80rpx; font-size: 30rpx; }
.hint { margin: 0 32rpx 12rpx; font-size: 22rpx; color: #6B7280; }
.field { margin: 16rpx 24rpx; padding: 8rpx 24rpx; }
.field input { height: 88rpx; font-size: 30rpx; color: #111827; }
.hint { margin: 0 32rpx 12rpx; font-size: 24rpx; color: #6B7280; }
.card { margin: 0 24rpx 24rpx; overflow: hidden; }
.item { display: flex; align-items: center; padding: 20rpx 24rpx; }
.box { width: 36rpx; height: 36rpx; border: 2rpx solid #D1D5DB; border-radius: 8rpx; margin-right: 16rpx; }
.item { display: flex; align-items: center; padding: 0 24rpx; }
.item + .item .meta { border-top: 1rpx solid #F0F2F5; }
.box { width: 36rpx; height: 36rpx; border: 2rpx solid #D1D5DB; border-radius: 8rpx; margin-right: 16rpx; flex-shrink: 0; }
.box.on { background: #1677FF; border-color: #1677FF; }
.av { width: 72rpx; height: 72rpx; border-radius: 50%; background: #E8F1FF; color: #1677FF; display: flex; align-items: center; justify-content: center; font-weight: 600; }
.meta { margin-left: 16rpx; }
.n { display: block; font-size: 28rpx; font-weight: 600; }
.s { font-size: 22rpx; color: #6B7280; }
.av { width: 72rpx; height: 72rpx; border-radius: 18rpx; background: #E8F1FF; color: #1677FF; display: flex; align-items: center; justify-content: center; font-weight: 600; margin: 16rpx 0; flex-shrink: 0; overflow: hidden; }
.av.img { display: block; }
.meta { margin-left: 16rpx; flex: 1; min-width: 0; padding: 18rpx 0; }
.n { display: block; font-size: 28rpx; font-weight: 600; color: #111827; }
.s { display: block; margin-top: 4rpx; font-size: 22rpx; color: #6B7280; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pad { padding: 0 24rpx 40rpx; }
.off { opacity: 0.45; }
</style>
+11 -4
View File
@@ -3,7 +3,10 @@
<fy-header title="群聊设置" back />
<view class="card head">
<view class="grid">
<view v-for="p in members.slice(0, 4)" :key="p.id" class="cell">{{ (p.name || '?').slice(0, 1) }}</view>
<view v-for="p in members.slice(0, 4)" :key="p.id" class="cell">
<image v-if="p.avatar" class="cell-img" :src="p.avatar" mode="aspectFill" />
<text v-else>{{ (p.name || '?').slice(0, 1) }}</text>
</view>
</view>
<view class="meta">
<text class="gn">{{ title }}</text>
@@ -17,7 +20,8 @@
</view>
<view class="mems">
<view v-for="p in members" :key="p.id" class="mem" @click="open(p)">
<view class="mav">{{ (p.name || '?').slice(0, 1) }}</view>
<image v-if="p.avatar" class="mav img" :src="p.avatar" mode="aspectFill" />
<view v-else class="mav">{{ (p.name || '?').slice(0, 1) }}</view>
<text class="mn">{{ p.name }}</text>
</view>
</view>
@@ -76,6 +80,7 @@ export default {
this.members = ids.map((id) => store.person(id) || { id, name: id, title: '', department: '' })
},
open(p) {
if (!p || !store.person(p.id)) return
uni.navigateTo({ url: '/pages/contact/detail?id=' + encodeURIComponent(p.id) })
},
toggle(field) {
@@ -106,7 +111,8 @@ export default {
<style scoped>
.head { margin: 24rpx; padding: 28rpx; display: flex; align-items: center; }
.grid { width: 96rpx; height: 96rpx; background: #E8F1FF; border-radius: 22rpx; display: flex; flex-wrap: wrap; padding: 8rpx; box-sizing: border-box; }
.cell { width: 36rpx; height: 36rpx; margin: 2rpx; background: #fff; color: #1677FF; font-size: 18rpx; font-weight: 700; display: flex; align-items: center; justify-content: center; border-radius: 8rpx; }
.cell { width: 36rpx; height: 36rpx; margin: 2rpx; background: #fff; color: #1677FF; font-size: 18rpx; font-weight: 700; display: flex; align-items: center; justify-content: center; border-radius: 8rpx; overflow: hidden; }
.cell-img { width: 36rpx; height: 36rpx; display: block; }
.meta { margin-left: 20rpx; }
.gn { font-size: 32rpx; font-weight: 700; display: block; }
.gc { font-size: 22rpx; color: #6B7280; }
@@ -114,7 +120,8 @@ export default {
.cap { padding: 20rpx 24rpx 8rpx; font-size: 26rpx; font-weight: 700; }
.mems { display: flex; flex-wrap: wrap; padding: 8rpx 16rpx 24rpx; }
.mem { width: 20%; display: flex; flex-direction: column; align-items: center; margin-bottom: 16rpx; }
.mav { width: 80rpx; height: 80rpx; border-radius: 18rpx; background: #E8F1FF; color: #1677FF; display: flex; align-items: center; justify-content: center; font-weight: 700; }
.mav { width: 80rpx; height: 80rpx; border-radius: 18rpx; background: #E8F1FF; color: #1677FF; display: flex; align-items: center; justify-content: center; font-weight: 700; overflow: hidden; }
.mav.img { display: block; }
.mn { font-size: 20rpx; margin-top: 8rpx; }
.row { display: flex; justify-content: space-between; align-items: center; padding: 28rpx 24rpx; font-size: 28rpx; border-bottom: 1rpx solid #F0F2F5; }
.row:last-child { border-bottom: none; }
+15 -13
View File
@@ -1,7 +1,8 @@
<template>
<view class="page">
<fy-header title="同事资料" back />
<view v-if="p" class="body">
<wd-status-tip v-if="!p" image="content" tip="未找到这位同事" />
<view v-else class="body">
<view class="card profile">
<image v-if="p.avatar" class="av img" :src="p.avatar" mode="aspectFill" />
<view v-else class="av">{{ p.name.slice(0, 1) }}</view>
@@ -108,22 +109,23 @@ export default {
</script>
<style scoped>
.body { padding: 24rpx; }
.profile { padding: 28rpx; display: flex; align-items: center; }
.av { width: 128rpx; height: 128rpx; border-radius: 20rpx; background: #1677FF; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 48rpx; font-weight: 700; }
.av.img { border-radius: 20rpx; }
.info { margin-left: 24rpx; flex: 1; min-width: 0; }
.body { padding: 16rpx 24rpx 48rpx; }
.profile { padding: 32rpx 24rpx; display: flex; align-items: center; }
.av { width: 112rpx; height: 112rpx; border-radius: 24rpx; background: #E8F1FF; color: #1677FF; display: flex; align-items: center; justify-content: center; font-size: 44rpx; font-weight: 650; }
.av.img { border-radius: 24rpx; }
.info { margin-left: 20rpx; flex: 1; min-width: 0; }
.nm { display: flex; align-items: center; gap: 12rpx; }
.n { font-size: 36rpx; font-weight: 700; }
.tag { font-size: 20rpx; background: #E8F1FF; color: #4B5563; padding: 2rpx 10rpx; border-radius: 8rpx; }
.s, .ph { display: block; font-size: 24rpx; color: #6B7280; margin-top: 8rpx; }
.n { font-size: 36rpx; font-weight: 650; color: #111827; }
.tag { font-size: 22rpx; background: #E8F8EF; color: #16A34A; padding: 2rpx 10rpx; border-radius: 8rpx; }
.s, .ph { display: block; font-size: 24rpx; color: #6B7280; margin-top: 6rpx; }
.copy { color: #1677FF; margin-left: 12rpx; }
.acts { display: flex; gap: 16rpx; margin: 20rpx 0; }
.act { flex: 1; background: #fff; border-radius: 16rpx; padding: 20rpx 8rpx; display: flex; flex-direction: column; align-items: center; font-size: 22rpx; font-weight: 600; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.03); }
.act { flex: 1; background: #fff; border-radius: 20rpx; border: 1rpx solid rgba(17,24,39,0.06); padding: 20rpx 8rpx; display: flex; flex-direction: column; align-items: center; font-size: 22rpx; font-weight: 600; color: #111827; }
.ai { font-size: 36rpx; color: #1677FF; margin-bottom: 8rpx; }
.rows { overflow: hidden; margin-bottom: 20rpx; }
.row { display: flex; justify-content: space-between; padding: 28rpx 24rpx; font-size: 28rpx; border-bottom: 1rpx solid #E8ECF2; }
.k { color: #4B5563; }
.row { display: flex; justify-content: space-between; align-items: center; padding: 28rpx 24rpx; font-size: 28rpx; }
.row + .row { border-top: 1rpx solid #F0F2F5; }
.k { color: #6B7280; }
.v { color: #111827; }
.brand { color: #1677FF; }
.brand { color: #1677FF; font-weight: 600; }
</style>
+21 -15
View File
@@ -7,20 +7,24 @@
</fy-header>
<fy-scroll class="edge-scroll" @refresh="load">
<view class="sec">待我办理</view>
<view v-if="!pending.length" class="empty">暂无</view>
<wd-status-tip v-if="!pending.length" image="content" tip="暂无待办" />
<view v-for="t in pending" :key="t.id" class="card item" @click="open(t)">
<view class="stripe" />
<view class="body">
<view class="main">
<text class="type">待办理</text>
<text class="t">{{ t.title }}</text>
<text class="s">{{ t.assignerName || '' }} · {{ t.dueAt || '' }}</text>
<text class="s">{{ lineOf(t.assignerName, t.dueAt) }}</text>
</view>
<text class="arr"></text>
</view>
<view class="sec">我发起的</view>
<wd-status-tip v-if="!mine.length" image="content" tip="暂无发起" />
<view v-for="t in mine" :key="'m' + t.id" class="card item" @click="open(t)">
<view class="body wide">
<view class="main">
<text class="type">协同</text>
<text class="t">{{ t.title }}</text>
<text class="s">{{ t.assigneeName || '' }} · {{ t.status || '' }}</text>
<text class="s">{{ lineOf(t.assigneeName, t.status) }}</text>
</view>
<text class="arr"></text>
</view>
</fy-scroll>
</view>
@@ -49,6 +53,9 @@ export default {
this.mine = asList(await oaGet('/work-tasks?tab=review', store.token))
} catch (_) {}
},
lineOf(a, b) {
return [a, b].filter(Boolean).join(' · ')
},
open(t) {
uni.navigateTo({ url: '/pages/coop/detail?id=' + encodeURIComponent(t.id) })
},
@@ -60,13 +67,12 @@ export default {
</script>
<style scoped>
.sec { padding: 24rpx 24rpx 8rpx; font-size: 28rpx; font-weight: 700; }
.item { margin: 12rpx 24rpx; display: flex; overflow: hidden; }
.stripe { width: 8rpx; background: #1677FF; }
.body { flex: 1; padding: 24rpx; }
.body.wide { padding: 24rpx; }
.t { display: block; font-size: 28rpx; font-weight: 600; }
.s { font-size: 22rpx; color: #6B7280; }
.empty { padding: 24rpx; text-align: center; color: #6B7280; }
.add { color: #1677FF; font-size: 26rpx; font-weight: 700; padding: 8rpx; }
.sec { padding: 24rpx 32rpx 4rpx; font-size: 26rpx; font-weight: 650; color: #6B7280; }
.item { margin: 16rpx 24rpx; padding: 24rpx; display: flex; align-items: center; }
.main { flex: 1; min-width: 0; }
.type { display: inline-block; font-size: 22rpx; color: #1677FF; background: #E8F1FF; border-radius: 8rpx; padding: 2rpx 10rpx; }
.t { display: block; margin-top: 10rpx; font-size: 28rpx; font-weight: 650; color: #111827; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.s { display: block; margin-top: 6rpx; font-size: 24rpx; color: #6B7280; }
.arr { color: #C4C9D4; font-size: 32rpx; margin-left: 8rpx; }
.add { color: #1677FF; font-size: 26rpx; font-weight: 650; padding: 8rpx; }
</style>
+70 -16
View File
@@ -2,15 +2,28 @@
<view class="page">
<fy-header title="修改密码" back />
<view class="body">
<text class="hint">首次登录或管理员要求改密请设置新密码后继续</text>
<view class="hero">
<view class="mark">
<wd-icon name="lock-on" size="44rpx" color="#1677FF" />
</view>
<text class="title">设置新密码</text>
<text class="hint">首次登录或管理员要求改密时请设置新密码后继续使用</text>
</view>
<view class="card group">
<view class="field">
<text class="lab">新密码</text>
<view class="box"><input v-model="next" password placeholder="至少 6 位" /></view>
<view class="box">
<input v-model="next" :password="!showNext" placeholder="至少 6 位" placeholder-class="ph" />
<text class="eye" @click="showNext = !showNext">{{ showNext ? '隐藏' : '显示' }}</text>
</view>
</view>
<view class="field">
<text class="lab">确认新密码</text>
<view class="box"><input v-model="confirm" password placeholder="再输入一次" /></view>
<view class="box" :class="{ bad: mismatch }">
<input v-model="confirm" :password="!showConfirm" placeholder="再输入一次" placeholder-class="ph" />
<text class="eye" @click="showConfirm = !showConfirm">{{ showConfirm ? '隐藏' : '显示' }}</text>
</view>
<text v-if="mismatch" class="tip">两次输入不一致</text>
</view>
</view>
<wd-button type="primary" block size="large" @click="ok">确认修改</wd-button>
@@ -26,39 +39,80 @@ import { toast } from '../../utils/format.js'
export default {
components: { FyHeader },
computed: {
mismatch() {
return this.confirm.length > 0 && this.next !== this.confirm
}
},
data() {
return { next: '', confirm: '', error: '', off: null, left: false }
return { next: '', confirm: '', error: '', off: null, left: false, waiting: false, showNext: false, showConfirm: false }
},
onShow() {
if (this.off) this.off()
this.off = store.on(() => {
this.error = store.error
if (this.left || store.phase !== 'app') return
this.left = true
uni.switchTab({ url: '/pages/tabs/messages' })
if (this.left || !this.waiting || store.phase !== 'app') return
this.leave()
})
},
onHide() {
if (this.off) this.off()
this.off = null
this.drop()
},
onUnload() {
this.drop()
},
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() {
if (!this.next || this.next.length < 6) return toast('密码至少 6 位')
if (this.next !== this.confirm) return toast('两次密码不一致')
this.waiting = true
await store.changePassword(this.next, this.confirm)
if (store.error) this.waiting = false
}
}
}
</script>
<style scoped>
.body { padding: 16rpx 24rpx 48rpx; }
.hint { font-size: 26rpx; color: #6B7280; display: block; margin: 8rpx 8rpx 20rpx; line-height: 1.5; }
.group { padding: 8rpx 24rpx 24rpx; margin-bottom: 24rpx; }
.field { margin-top: 20rpx; }
.lab { font-size: 26rpx; color: #6B7280; display: block; margin-bottom: 10rpx; }
.box { height: 92rpx; background: #F5F7FA; border-radius: 16rpx; padding: 0 24rpx; display: flex; align-items: center; }
.body { padding: 32rpx 32rpx 48rpx; }
.hero { display: flex; flex-direction: column; align-items: center; margin: 16rpx 0 36rpx; }
.mark {
width: 96rpx;
height: 96rpx;
border-radius: 28rpx;
background: #E8F1FF;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20rpx;
}
.title { font-size: 36rpx; font-weight: 650; color: #111827; }
.hint { margin-top: 10rpx; font-size: 24rpx; color: #6B7280; line-height: 1.55; text-align: center; }
.group { padding: 8rpx 28rpx 28rpx; margin-bottom: 32rpx; }
.field { margin-top: 24rpx; }
.lab { font-size: 26rpx; color: #374151; display: block; margin-bottom: 12rpx; }
.box {
height: 96rpx;
background: #F5F7FA;
border-radius: 16rpx;
padding: 0 24rpx;
display: flex;
align-items: center;
border: 1rpx solid transparent;
}
.box.bad { border-color: #FECACA; background: #FEF2F2; }
.box input { flex: 1; font-size: 30rpx; color: #111827; }
.err { color: #DC2626; font-size: 24rpx; margin-top: 16rpx; display: block; }
.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>
+16 -4
View File
@@ -2,8 +2,12 @@
<view class="page">
<fy-header title="附件预览" back />
<view class="body">
<text class="name">{{ name || '附件' }}</text>
<view class="btn-primary" @click="open">打开 / 预览</view>
<view class="card sheet">
<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>
</template>
@@ -31,6 +35,14 @@ export default {
</script>
<style scoped>
.body { padding: 48rpx 32rpx; }
.name { display: block; font-size: 28rpx; margin-bottom: 32rpx; text-align: center; }
.body { padding: 32rpx 24rpx; }
.sheet { padding: 48rpx 32rpx; margin-bottom: 32rpx; display: flex; flex-direction: column; align-items: center; }
.mark {
width: 112rpx; height: 112rpx; border-radius: 28rpx;
background: #E8F1FF; color: #1677FF;
display: flex; align-items: center; justify-content: center;
font-size: 44rpx; font-weight: 650;
}
.name { display: block; margin-top: 24rpx; font-size: 30rpx; font-weight: 650; color: #111827; text-align: center; word-break: break-all; }
.hint { display: block; margin-top: 12rpx; font-size: 24rpx; color: #6B7280; text-align: center; }
</style>
+4 -2
View File
@@ -2,7 +2,9 @@
<view class="page edge">
<fy-header title="通讯录">
<template #right>
<text class="ico" @click="goGroup">建群</text>
<view class="ico" @click="goGroup">
<wd-icon name="usergroup" size="40rpx" color="#111827" />
</view>
</template>
</fy-header>
<fy-scroll class="edge-scroll" dock @refresh="reload">
@@ -226,5 +228,5 @@ export default {
.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 { padding: 8rpx 4rpx; color: #1677FF; font-size: 28rpx; font-weight: 600; }
.ico { width: 56rpx; height: 56rpx; display: flex; align-items: center; justify-content: center; }
</style>
+9 -8
View File
@@ -15,10 +15,6 @@
<wd-icon name="usergroup" size="32rpx" color="#374151" />
<text>发起群聊</text>
</view>
<view class="drop-item" @click="addMate">
<wd-icon name="user-add" size="32rpx" color="#374151" />
<text>添加同事</text>
</view>
</view>
<fy-scroll class="edge-scroll list" dock @refresh="reload">
<view class="finder" @click="goSearch">
@@ -37,6 +33,9 @@
<view class="av-wrap">
<image v-if="c.avatar && c.type !== 'files'" class="av" :src="c.avatar" mode="aspectFill" />
<view v-else-if="c.type === 'files'" class="av"><fy-file-mark /></view>
<view v-else-if="facesOf(c).length" class="av mosaic">
<image v-for="f in facesOf(c)" :key="f.id" class="tile" :src="f.avatar" mode="aspectFill" />
</view>
<view v-else class="av txt" :class="avClass(c)">{{ avText(c) }}</view>
<text v-if="c.unread && !c.muted" class="badge abs">{{ c.unread > 99 ? '99+' : c.unread }}</text>
<text v-else-if="c.unread && c.muted" class="dotu" />
@@ -102,6 +101,10 @@ export default {
reload() {
store.refreshPeoplePhotos().finally(() => this.pull())
},
facesOf(c) {
if (!c || c.type !== 'group' || c.avatar) return []
return (c.memberIds || []).map((id) => store.person(id)).filter((p) => p && p.avatar).slice(0, 4)
},
avText(c) {
if (c.type === 'work') return '工'
if (c.type === 'group') return '群'
@@ -149,10 +152,6 @@ export default {
this.menu = false
uni.navigateTo({ url: '/pages/chat/create' })
},
addMate() {
this.menu = false
uni.navigateTo({ url: '/pages/search/search' })
},
goSearch() {
uni.navigateTo({
url: '/pages/search/search',
@@ -187,6 +186,8 @@ export default {
.av.work { background: #1677FF; color: #fff; }
.av.file { background: #F3F6FB; color: #4B5563; }
.av.plain { background: #E8F1FF; color: #1677FF; }
.mosaic { display: flex; flex-wrap: wrap; align-content: center; justify-content: center; gap: 4rpx; background: #E8F1FF; padding: 6rpx; box-sizing: border-box; }
.tile { width: 38rpx; height: 38rpx; border-radius: 8rpx; }
.abs { position: absolute; top: -8rpx; right: -8rpx; }
.dotu { position: absolute; top: -4rpx; right: -4rpx; width: 16rpx; height: 16rpx; border-radius: 50%; background: #DC2626; }
.meta { flex: 1; margin-left: 20rpx; min-width: 0; padding: 8rpx 0 18rpx; border-bottom: 1rpx solid #F0F2F5; }
+4 -2
View File
@@ -2,7 +2,9 @@
<view class="page edge">
<fy-header :title="greet">
<template #right>
<text class="ico" @click="goSearch">搜索</text>
<view class="ico" @click="goSearch">
<wd-icon name="search" size="40rpx" color="#111827" />
</view>
</template>
</fy-header>
<fy-scroll class="edge-scroll" dock @refresh="reload">
@@ -268,5 +270,5 @@ export default {
.empty-card { background: #fff; border-radius: 24rpx; padding: 48rpx 32rpx; text-align: center; border: 1rpx solid rgba(17,24,39,0.06); }
.et { display: block; font-size: 28rpx; font-weight: 600; }
.es { display: block; font-size: 24rpx; color: #6B7280; margin-top: 8rpx; }
.ico { padding: 8rpx 8rpx; font-size: 28rpx; color: #1677FF; }
.ico { width: 56rpx; height: 56rpx; display: flex; align-items: center; justify-content: center; }
</style>
+5 -2
View File
@@ -5,7 +5,7 @@
<view class="card">
<view class="top">
<text class="type">{{ row.typeLabel || row.type || '审批' }}</text>
<wd-tag type="primary" plain>{{ row.status || '待处理' }}</wd-tag>
<wd-tag type="primary" plain>{{ statusText }}</wd-tag>
</view>
<text class="t">{{ row.title || row.no }}</text>
<text class="s">{{ row.submitter || '' }}</text>
@@ -69,7 +69,7 @@ import FyHeader from '../../components/fy-header.vue'
import FyFiles from '../../components/fy-files.vue'
import { store } from '../../utils/store.js'
import { oaGet } from '../../utils/api.js'
import { toast } from '../../utils/format.js'
import { flowStatus, toast } from '../../utils/format.js'
import { collectFiles, fileId } from '../../utils/files.js'
const LABELS = {
@@ -124,6 +124,9 @@ export default {
const logs = (this.row && this.row.logs) || []
return Array.isArray(logs) ? [...logs].reverse() : []
},
statusText() {
return flowStatus(this.row && this.row.status)
},
canAct() {
if (!this.row) return false
const st = String(this.row.status || '')
+3 -2
View File
@@ -17,7 +17,7 @@
<text class="t">{{ r.title || r.no }}</text>
<text class="s">{{ r.submitter || r.assigneeName || '' }}</text>
</view>
<wd-tag type="primary" plain>{{ r.status || '待处理' }}</wd-tag>
<wd-tag type="primary" plain>{{ statusText(r) }}</wd-tag>
<text class="arr"></text>
</view>
</view>
@@ -29,7 +29,7 @@
import FyHeader from '../../components/fy-header.vue'
import FyScroll from '../../components/fy-scroll.vue'
import { store } from '../../utils/store.js'
import { asList } from '../../utils/format.js'
import { asList, flowStatus } from '../../utils/format.js'
import { oaGet } from '../../utils/api.js'
export default {
@@ -82,6 +82,7 @@ export default {
this.loading = false
}
},
statusText(r) { return flowStatus(r && r.status) },
open(r) {
if (r.source === 'work' || r.taskId) {
uni.navigateTo({ url: '/pages/coop/detail?id=' + encodeURIComponent(r.id || r.taskId) })
+28
View File
@@ -148,6 +148,34 @@ export function convType(s) {
return 'direct'
}
const FLOW_STATUS = {
pending: '待审批',
waiting: '待处理',
processing: '审批中',
reviewing: '审阅中',
review: '待审阅',
approved: '已通过',
passed: '已通过',
rejected: '已驳回',
refused: '已拒绝',
denied: '已拒绝',
cancelled: '已撤销',
canceled: '已撤销',
withdrawn: '已撤回',
draft: '草稿',
done: '已完成',
completed: '已完成',
handled: '已处理',
reviewed: '已审阅',
running: '进行中'
}
export function flowStatus(status) {
const raw = String(status || '').trim()
if (!raw) return '待处理'
return FLOW_STATUS[raw.toLowerCase()] || raw
}
export function toast(title, icon = 'none') {
uni.showToast({ title: String(title || ''), icon, duration: 2200 })
}
+28 -15
View File
@@ -487,7 +487,7 @@ export const store = {
return msg
})
const seen = new Set(incoming.map((m) => m.id).filter(Boolean))
const extras = (conv.messages || []).filter((m) => m.mine && m.id && !seen.has(m.id) && Date.now() - Number(m.time) < 120000)
const extras = (conv.messages || []).filter((m) => m.id && !seen.has(m.id) && (m.kind === 'system' || (m.mine && Date.now() - Number(m.time) < 120000)))
conv.messages = incoming.concat(extras)
conv.unread = 0
await api.markRead(conv.id, this.token)
@@ -794,12 +794,15 @@ export const store = {
async createGroup(name, memberIds) {
const ids = [...new Set([this.me && this.me.id, ...(memberIds || []).map(String)])].filter(Boolean)
const title = String(name || '').trim() || '群聊'
let sid = ''
try {
const data = await api.createGroup({ name: title, memberIds: ids }, this.token)
sid = String((data && (data.id || data.sessionId)) || '')
} catch (_) {}
if (!sid) sid = 'g:' + Date.now()
if (ids.length < 2) throw new Error('请至少再选一位同事')
const data = await api.createGroup({
title,
name: title,
memberIds: ids,
members: ids
}, this.token)
const sid = String((data && (data.id || data.sessionId)) || '')
if (!sid) throw new Error('创建失败')
let conv = this.findConv(sid)
if (!conv) {
conv = {
@@ -809,8 +812,16 @@ export const store = {
avatar: '',
peerId: '',
memberIds: ids,
messages: [],
unread: 0,
messages: [{
id: 'sys-' + sid,
kind: 'system',
text: '你创建了群聊',
time: Date.now(),
mine: false,
fromId: '',
recalled: false
}],
lastMessage: '你创建了群聊',
lastTime: fmtListTime(Date.now()),
pinned: false,
@@ -854,13 +865,15 @@ export const store = {
try {
this.attendRules.locations = asList(await api.oaGet('/profile/attendance/locations', this.token))
} catch (_) {}
try {
const pack = await api.oaGet('/system/attend-rules', this.token)
if (pack && typeof pack === 'object') {
this.attendRules.workStart = pack.workStart || this.attendRules.workStart
this.attendRules.workEnd = pack.workEnd || this.attendRules.workEnd
}
} catch (_) {}
if (this.can('system', 'attend')) {
try {
const pack = await api.oaGet('/system/attend-rules', this.token)
if (pack && typeof pack === 'object') {
this.attendRules.workStart = pack.workStart || this.attendRules.workStart
this.attendRules.workEnd = pack.workEnd || this.attendRules.workEnd
}
} catch (_) {}
}
try {
this.attendance = asList(await api.oaGet('/profile/attendance', this.token))
} catch (_) {}