kuaifan fd6a8a3650 feat(user): 会员卡片支持查看该会员参与的项目和任务
- 新增权限闸门 UserDepartment::userWorksContext(本人/管理员/部门负责人只读,排除机器人与系统账号)
- 新增接口 project/user/projects、project/user/tasks、project/user/counts
- users/extra 返回 works_visible 标记控制入口显隐
- 会员卡片新增「项目与任务」入口,弹出 UserWorksModal(项目/待办/已完成三 Tab、角标计数、工作流状态徽章、懒加载)
- 部门只读视角下任务仅展示全员可见(visibility=1),与 findForDepartmentView 对齐
- 补充 i18n 文案与暗色样式

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:24:50 +00:00

467 lines
16 KiB
Vue
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<ModalAlive
v-model="showModal"
class-name="common-user-detail-modal"
:fullscreen="isFullscreen"
:mask-closable="false"
:footer-hide="true"
width="480"
>
<div class="user-detail-body">
<div class="profile-header">
<div class="cover-photo" :style="{ '--user-cover-photo': `url(${userData.userimg || ''})` }"></div>
<div class="profile-avatar">
<UserAvatar
:userid="userData.userid"
:size="96"
:show-state-dot="false"
@on-click="onOpenAvatar"
/>
</div>
</div>
<div class="profile-content">
<div class="user-info-top">
<h1 class="username">
{{ userData.nickname }}
</h1>
<div class="meta">
<span @click="commonDialogShow = true" class="common-dialog">{{ $L(userId == userData.userid ? "我的群组" : "共同群组") }}:<em>{{ $L("(*)个", commonDialog.total) }}</em></span>
<template v-if="worksVisible">
<span class="separator">|</span>
<span @click="worksModalShow = true" class="common-dialog works-entry">{{ $L("项目与任务") }}</span>
</template>
<span class="separator">|</span>
<span :title="lineAtDisplay.title">{{ $L("最后在线") }}: {{ lineAtDisplay.text }}</span>
</div>
</div>
<div class="profile-actions">
<Button @click="onOpenDialog"><i class="taskfont">&#xe6eb;</i>{{ $L("开始聊天") }}</Button>
<Button @click="onCreateGroup"><i class="taskfont">&#xe63f;</i>{{ $L("创建群组") }}</Button>
</div>
<div v-if="userData.introduction" class="profile-bio">
<p>{{ userData.introduction }}</p>
</div>
<div class="profile-information">
<h2>{{ $L("个人信息") }}</h2>
<ul>
<li>
<Icon type="ios-briefcase-outline" />
<span class="label">{{ $L("职位/职称") }}</span>
<span class="value">{{userData.profession || "-"}}</span>
</li>
<li>
<Icon type="ios-people-outline" />
<span class="label">{{ $L("部门") }}</span>
<span class="value">{{userData.department_name || "-"}}</span>
</li>
<li>
<Icon type="ios-mail-outline" />
<span class="label">{{ $L("邮箱") }}</span>
<span @click="onOpenEmail" class="value" :class="{ 'clickable': userData.email }">{{userData.email || "-"}}</span>
</li>
<li>
<Icon type="ios-call-outline" />
<span class="label">{{ $L("电话") }}</span>
<span @click="onOpenTel" class="value" :class="{ 'clickable': userData.tel }">{{ userData.tel || "-" }}</span>
</li>
<li v-if="userData.birthday">
<Icon type="ios-calendar-outline" />
<span class="label">{{ $L("生日") }}</span>
<span class="value">{{userData.birthday || "-"}}</span>
</li>
</ul>
<div class="profile-tags">
<div v-if="displayTags.length" class="tags-list">
<Button
v-for="tag in displayTags"
:key="tag.id"
:type="tag.recognized ? 'primary' : 'default'"
@click="onOpenTagsModal"
>
{{ tag.name }}
<span v-if="tag.recognition_total > 0" class="recognition-total">{{tag.recognition_total}}</span>
</Button>
<Button
type="dashed"
class="manage-tags-btn icon"
@click="onOpenTagsModal"
>
<Icon type="ios-settings-outline" /> 管理
</Button>
</div>
<div v-else class="tags-empty">
<Button
type="dashed"
icon="md-add"
class="add-tag-btn"
@click="onOpenTagsModal"
>{{ $L("添加标签") }}</Button
>
</div>
</div>
</div>
</div>
</div>
<UserTagsModal
v-if="userData.userid"
v-model="tagModalVisible"
:userid="userData.userid"
@updated="onTagsUpdated"
/>
<CommonDialogModal
v-model="commonDialogShow"
:target-user-id="userData.userid"
:total-count="commonDialog.total || 0"
@open-chat="onOpenCommonDialogChat"
/>
<UserWorksModal
v-if="worksVisible && userData.userid"
v-model="worksModalShow"
:target-user-id="userData.userid"
@navigate="onHide"
/>
</ModalAlive>
</template>
<script>
import emitter from "../../../store/events";
import { mapState } from "vuex";
import transformEmojiToHtml from "../../../utils/emoji";
import UserTagsModal from "./UserTagsModal.vue";
import CommonDialogModal from "./CommonDialogModal.vue";
import UserWorksModal from "./UserWorksModal.vue";
export default {
name: "UserDetail",
components: { UserTagsModal, CommonDialogModal, UserWorksModal },
data() {
return {
userData: {
userid: 0,
},
showModal: false,
tagModalVisible: false,
commonDialog: {
userid: null,
total: null,
list: [],
page: 1,
has_more: false,
},
commonDialogShow: false,
commonDialogLoading: 0,
worksModalShow: false,
};
},
mounted() {
emitter.on("openUser", this.onShow);
},
beforeDestroy() {
emitter.off("openUser", this.onShow);
},
watch: {
...mapState(["cacheUserBasic"]),
commonDialogShow() {
if (!this.commonDialogShow || this.commonDialog.list.length > 0) {
return;
}
this.loadCommonDialogList(false);
},
},
computed: {
isFullscreen({ windowWidth }) {
return windowWidth < 576;
},
displayTags() {
return Array.isArray(this.userData.personal_tags)
? this.userData.personal_tags
: [];
},
personalTagTotal() {
if (typeof this.userData.personal_tags_total === "number") {
return this.userData.personal_tags_total;
}
return this.displayTags.length;
},
commonDialogList() {
return this.commonDialog.list || [];
},
worksVisible() {
return !!this.userData.works_visible;
},
lineAtDisplay({ userData }) {
const value = userData.line_at;
if (!value) {
return { text: "-", title: "" };
}
const now = $A.daytz();
const line = $A.dayjs(value);
const title = line.format("YYYY-MM-DD HH:mm");
const seconds = now.unix() - line.unix();
let text;
if (seconds < 60) {
text = this.$L("刚刚");
} else if (seconds < 3600) {
text = this.$L("(*)分钟前", Math.floor(seconds / 60));
} else if (seconds < 3600 * 24) {
text = this.$L("(*)小时前", Math.floor(seconds / 3600));
} else if (seconds < 3600 * 24 * 7) {
text = this.$L("(*)天前", Math.floor(seconds / 86400));
} else if (line.isAfter(now.clone().subtract(1, "month"))) {
text = line.format("MM-DD HH:mm");
} else {
text = line.format("YYYY-MM-DD");
}
return { text, title };
},
},
methods: {
transformEmojiToHtml,
onShow(userid) {
if (!/^\d+$/.test(userid)) {
return;
}
this.$store.dispatch("showSpinner", 600);
Promise.all([
this.$store.dispatch("getUserData", userid).catch(() => null),
this.$store.dispatch("getUserExtra", userid).catch(() => null),
])
.then(([user, extra]) => {
const baseData = $A.isJson(user) ? user : {};
const extraData = $A.isJson(extra) ? extra : {};
this.userData = Object.assign({}, baseData, extraData);
this.ensureTagDefaults();
this.showModal = true;
this.loadCommonDialogCount();
})
.finally((_) => {
this.$store.dispatch("hiddenSpinner");
});
},
onHide() {
this.showModal = false;
this.tagModalVisible = false;
this.commonDialogShow = false;
this.worksModalShow = false;
},
onOpenAvatar() {
this.$store.dispatch("previewImage", this.userData.userimg);
},
onOpenDialog() {
this.$store
.dispatch("openDialogUserid", this.userData.userid)
.then((_) => {
this.onHide();
})
.catch(({ msg }) => {
$A.modalError(msg);
});
},
onCreateGroup() {
const userids = [this.$store.state.userId];
if (this.userData.userid && this.$store.state.userId != this.userData.userid) {
userids.push(this.userData.userid);
}
emitter.emit('createGroup', userids);
this.onHide();
},
ensureTagDefaults() {
if (!Array.isArray(this.userData.personal_tags)) {
this.$set(this.userData, "personal_tags", []);
}
if (typeof this.userData.personal_tags_total !== "number") {
this.$set(
this.userData,
"personal_tags_total",
this.userData.personal_tags.length
);
}
},
onOpenTagsModal() {
if (!this.userData.userid) {
return;
}
this.tagModalVisible = true;
},
onTagsUpdated({ top, total }) {
this.$set(
this.userData,
"personal_tags",
Array.isArray(top) ? top : []
);
this.$set(
this.userData,
"personal_tags_total",
typeof total === "number"
? total
: this.userData.personal_tags.length
);
},
loadCommonDialogCount() {
const target_userid = this.userData.userid;
const previousUserId = this.commonDialog.userid;
if (!target_userid) {
this.commonDialog = {
...this.commonDialog,
userid: target_userid || null,
total: null,
list: [],
page: 1,
has_more: false,
};
return;
}
if (previousUserId !== target_userid) {
this.commonDialog = {
...this.commonDialog,
userid: target_userid,
total: null,
list: [],
page: 1,
has_more: false,
};
}
const cacheMap = this.$store.state.dialogCommonCountCache || {};
const cached = cacheMap[String(target_userid)];
if (cached && typeof cached.total !== "undefined") {
this.commonDialog = {
...this.commonDialog,
total: cached.total,
};
}
this.$store
.dispatch("call", {
url: "dialog/common/list",
data: {
target_userid,
only_count: "yes",
},
})
.then(({ data }) => {
if (target_userid !== this.userData.userid) {
return;
}
const parsedTotal = Number(data.total);
const total = Number.isNaN(parsedTotal) ? 0 : parsedTotal;
this.commonDialog = {
...this.commonDialog,
userid: target_userid,
total,
list: [],
page: 1,
has_more: false,
};
this.$store.commit("common/dialog/count/save", {
userid: target_userid,
total,
});
});
},
loadCommonDialogList(loadMore = false) {
this.commonDialogLoading++;
const target_userid = this.userData.userid;
this.$store
.dispatch("call", {
url: "dialog/common/list",
data: {
target_userid,
page: loadMore ? this.commonDialog.page + 1 : 1,
},
})
.then(({ data }) => {
if (target_userid !== this.userData.userid) {
return;
}
this.commonDialog = {
...this.commonDialog,
list: loadMore
? [...this.commonDialog.list, ...data.data]
: data.data,
total: data.total,
page: data.current_page,
has_more: !!data.next_page_url,
};
})
.catch(({ msg }) => {
$A.modalError(msg || this.$L("加载失败"));
})
.finally(() => {
this.commonDialogLoading--;
});
},
onOpenCommonDialogChat(dialog) {
this.$store
.dispatch("openDialog", dialog.id)
.then(() => {
this.onHide();
})
.catch(({ msg }) => {
$A.modalError(msg);
});
},
onOpenEmail() {
if (!this.userData.email) {
return;
}
$A.modalConfirm({
content: `是否发送邮件给 ${this.userData.nickname}`,
onOk: () => {
window.open(`mailto:${this.userData.email}`);
}
});
},
onOpenTel() {
if (!this.userData.tel) {
return;
}
$A.modalConfirm({
content: `是否拨打电话给 ${this.userData.nickname}`,
onOk: () => {
if ($A.isEEUIApp()) {
$A.eeuiAppSendMessage({
action: 'callTel',
tel: this.userData.tel
});
} else {
window.open(`tel:${this.userData.tel}`);
}
}
});
},
},
};
</script>