mirror of
https://github.com/kuaifan/dootask.git
synced 2026-10-29 22:10:42 +00:00
feat(contacts): 通讯录支持按部门显示,成员按需加载
- 新增 contacts/departments 接口,返回部门树及成员数 - 通讯录搜索框右侧新增字母/部门视图切换,部门视图可折叠,展开时才加载成员 - users/search 的 keys.departments 改为参数绑定(修复 SQL 注入),0 表示未加入任何部门 - 同步翻译与 ai-kb
This commit is contained in:
parent
adac87bf45
commit
ca1a1b5415
37
app/Http/Controllers/Api/ContactsController.php
Normal file
37
app/Http/Controllers/Api/ContactsController.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Module\Base;
|
||||
use App\Module\ContactsDepartment;
|
||||
|
||||
/**
|
||||
* @apiDefine contacts
|
||||
*
|
||||
* 通讯录
|
||||
*/
|
||||
class ContactsController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @api {get} api/contacts/departments 通讯录部门树
|
||||
*
|
||||
* @apiDescription 需要token身份,返回深度优先展开的部门列表;部门成员通过 api/users/search(keys.departments)按需加载
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup contacts
|
||||
* @apiName departments
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
* - total: 联系人总数
|
||||
* - no_department_count: 未加入任何部门的联系人数
|
||||
* - departments: 部门列表,字段 id、name、parent_id、owner_userid、level(层级,顶级为0)、member_count(直属成员数)、total_count(含下级部门的成员数)
|
||||
*/
|
||||
public function departments()
|
||||
{
|
||||
User::auth();
|
||||
//
|
||||
return Base::retSuccess('success', ContactsDepartment::tree());
|
||||
}
|
||||
}
|
||||
@ -658,7 +658,7 @@ class UsersController extends AbstractController
|
||||
* - keys.project_id 在指定项目ID
|
||||
* - keys.no_project_id 不在指定项目ID
|
||||
* - keys.dialog_id 在指定对话ID
|
||||
* - keys.departments 部门ID(多个用逗号分隔)
|
||||
* - keys.departments 部门ID(多个用逗号分隔,0 表示未加入任何部门)
|
||||
* @apiParam {Object} sorts 排序方式
|
||||
* - sorts.az 按字母:asc|desc
|
||||
* @apiParam {Number} updated_time 在这个时间戳之后更新的
|
||||
@ -740,13 +740,20 @@ class UsersController extends AbstractController
|
||||
$query->select('userid')->from('web_socket_dialog_users')->where('dialog_id', $keys['dialog_id']);
|
||||
});
|
||||
}
|
||||
if ($keys['departments']) {
|
||||
if (!is_array($keys['departments'])) {
|
||||
$keys['departments'] = explode(",", $keys['departments']);
|
||||
}
|
||||
$builder->where(function($query) use ($keys) {
|
||||
foreach ($keys['departments'] AS $department) {
|
||||
$query->orWhereRaw("FIND_IN_SET('{$department}', department)");
|
||||
if (isset($keys['departments']) && $keys['departments'] !== '' && $keys['departments'] !== []) {
|
||||
$departments = is_array($keys['departments']) ? $keys['departments'] : explode(",", $keys['departments']);
|
||||
// 只保留数字项,避免空串/非法值被 intval 成 0 后误匹配“未加入任何部门”
|
||||
$departments = array_map('intval', array_filter($departments, fn($item) => is_numeric($item)));
|
||||
$builder->where(function($query) use ($departments) {
|
||||
foreach ($departments AS $department) {
|
||||
if ($department === 0) {
|
||||
// 0 表示未加入任何部门
|
||||
$query->orWhere(function($q) {
|
||||
$q->whereNull("department")->orWhere("department", "");
|
||||
});
|
||||
} else {
|
||||
$query->orWhereRaw("FIND_IN_SET(?, department)", [$department]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
90
app/Module/ContactsDepartment.php
Normal file
90
app/Module/ContactsDepartment.php
Normal file
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\UserDepartment;
|
||||
|
||||
/**
|
||||
* 通讯录部门树
|
||||
*/
|
||||
class ContactsDepartment
|
||||
{
|
||||
/**
|
||||
* 构建通讯录部门树(深度优先展开的扁平列表)
|
||||
*
|
||||
* 成员口径与 users/search 默认一致:未禁用、非机器人。
|
||||
* - member_count: 直属成员数
|
||||
* - total_count: 含全部下级部门的成员数(去重)
|
||||
*
|
||||
* @return array{total: int, no_department_count: int, departments: array}
|
||||
*/
|
||||
public static function tree(): array
|
||||
{
|
||||
$departments = UserDepartment::select(['id', 'name', 'parent_id', 'owner_userid'])->orderBy('id')->get()->keyBy('id');
|
||||
|
||||
// 部门 => 直属成员
|
||||
$members = [];
|
||||
$noDepartmentCount = 0;
|
||||
$total = 0;
|
||||
User::whereNull('disable_at')->where('bot', 0)->pluck('department', 'userid')->each(function ($department, $userid) use ($departments, &$members, &$noDepartmentCount, &$total) {
|
||||
$total++;
|
||||
// pluck 会经过 User::getDepartmentAttribute,得到的已是 int 数组
|
||||
$ids = array_filter((array)$department, fn($id) => $departments->has($id));
|
||||
if (empty($ids)) {
|
||||
$noDepartmentCount++;
|
||||
return;
|
||||
}
|
||||
foreach ($ids as $id) {
|
||||
$members[$id][$userid] = true;
|
||||
}
|
||||
});
|
||||
|
||||
// 上级部门不存在时视为顶级
|
||||
$children = [];
|
||||
foreach ($departments as $department) {
|
||||
$parentId = $departments->has($department->parent_id) && $department->parent_id != $department->id ? $department->parent_id : 0;
|
||||
$children[$parentId][] = $department->id;
|
||||
}
|
||||
|
||||
$list = [];
|
||||
$visited = [];
|
||||
$walk = function ($id, $level) use (&$walk, &$list, &$visited, &$children, &$members, $departments) {
|
||||
$visited[$id] = true;
|
||||
$department = $departments[$id];
|
||||
$index = count($list);
|
||||
$list[] = [
|
||||
'id' => $department->id,
|
||||
'name' => $department->name,
|
||||
'parent_id' => (int)$department->parent_id,
|
||||
'owner_userid' => (int)$department->owner_userid,
|
||||
'level' => $level,
|
||||
'member_count' => count($members[$id] ?? []),
|
||||
'total_count' => 0,
|
||||
];
|
||||
$all = $members[$id] ?? [];
|
||||
foreach ($children[$id] ?? [] as $childId) {
|
||||
if (isset($visited[$childId])) {
|
||||
continue; // 防止异常数据形成环
|
||||
}
|
||||
$all += $walk($childId, $level + 1);
|
||||
}
|
||||
$list[$index]['total_count'] = count($all);
|
||||
return $all;
|
||||
};
|
||||
foreach ($children[0] ?? [] as $rootId) {
|
||||
$walk($rootId, 0);
|
||||
}
|
||||
foreach ($departments as $id => $department) {
|
||||
if (!isset($visited[$id])) {
|
||||
$walk($id, 0); // 异常数据形成环时兜底,避免部门丢失
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'no_department_count' => $noDepartmentCount,
|
||||
'departments' => $list,
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -2842,3 +2842,5 @@ APP 新版本
|
||||
用时 (*)
|
||||
暂不支持在线更新
|
||||
更新内容
|
||||
按字母显示
|
||||
按部门显示
|
||||
|
||||
@ -40930,5 +40930,29 @@
|
||||
"fr": "Mise à jour du système lancée",
|
||||
"id": "Pembaruan sistem dimulai",
|
||||
"ru": "Обновление системы запущено"
|
||||
},
|
||||
{
|
||||
"key": "按字母显示",
|
||||
"zh": "",
|
||||
"zh-CHT": "按字母顯示",
|
||||
"en": "Sort by Letter",
|
||||
"ko": "알파벳순 보기",
|
||||
"ja": "文字順で表示",
|
||||
"de": "Nach Buchstaben anzeigen",
|
||||
"fr": "Afficher par ordre alphabétique",
|
||||
"id": "Tampilkan menurut abjad",
|
||||
"ru": "Показать по алфавиту"
|
||||
},
|
||||
{
|
||||
"key": "按部门显示",
|
||||
"zh": "",
|
||||
"zh-CHT": "按部門顯示",
|
||||
"en": "Group by Department",
|
||||
"ko": "부서별 보기",
|
||||
"ja": "部門別に表示",
|
||||
"de": "Nach Abteilung anzeigen",
|
||||
"fr": "Afficher par département",
|
||||
"id": "Tampilkan menurut departemen",
|
||||
"ru": "Показать по отделам"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
2
public/language/web/de.js
vendored
2
public/language/web/de.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/en.js
vendored
2
public/language/web/en.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/fr.js
vendored
2
public/language/web/fr.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/id.js
vendored
2
public/language/web/id.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/ja.js
vendored
2
public/language/web/ja.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/key.js
vendored
2
public/language/web/key.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/ko.js
vendored
2
public/language/web/ko.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/ru.js
vendored
2
public/language/web/ru.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/zh-CHT.js
vendored
2
public/language/web/zh-CHT.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/zh.js
vendored
2
public/language/web/zh.js
vendored
File diff suppressed because one or more lines are too long
@ -17,7 +17,7 @@ prerequisites: []
|
||||
negative:
|
||||
- 不支持完全隐藏消息模块,仅可逐个隐藏单条会话
|
||||
- 没有「未登录" 访客访问入口
|
||||
last_verified: v1.7.90
|
||||
last_verified: v1.9.59
|
||||
---
|
||||
|
||||
# 消息列表入口在哪
|
||||
@ -47,4 +47,4 @@ last_verified: v1.7.90
|
||||
|
||||
## 相关
|
||||
- 入口详解:[[messenger.entry.menu-map]]
|
||||
- 通讯录:联系人列表
|
||||
- 通讯录:联系人列表,默认按字母分组;点击搜索框右侧的图标可切换为「按部门显示」(部门树,点击部门标题展开/收起并查看成员,部门负责人排在最前,标题右侧为含下级部门的成员数),再次点击切回按字母显示;搜索时统一显示平铺的搜索结果;所选显示方式会被记住
|
||||
|
||||
@ -28,6 +28,15 @@
|
||||
clearable/>
|
||||
</Form>
|
||||
</div>
|
||||
<div
|
||||
v-if="tabActive==='contacts'"
|
||||
class="search-mode"
|
||||
:class="{active: contactsViewMode==='department'}"
|
||||
:title="$L(contactsViewMode==='department' ? '按字母显示' : '按部门显示')"
|
||||
@click="toggleContactsView">
|
||||
<i v-if="contactsViewMode==='department'" class="taskfont"></i>
|
||||
<i v-else class="taskfont team"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tabActive==='dialog' && !dialogSearchKey" class="messenger-nav">
|
||||
<EDropdown
|
||||
@ -131,10 +140,16 @@
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-else class="contacts">
|
||||
<template v-if="contactsFilter.length > 0">
|
||||
<li v-for="items in contactsList">
|
||||
<div class="label">{{items.az}}</div>
|
||||
<template v-if="contactsList.length > 0">
|
||||
<li v-for="items in contactsList" :key="items.key" :style="contactsGroupStyle(items)">
|
||||
<div v-if="items.dept" class="label department-label" @click="toggleDepartment(items.dept)">
|
||||
<Icon class="arrow" :class="{expanded: items.expanded}" type="ios-arrow-forward"/>
|
||||
<span class="name">{{items.az}}</span>
|
||||
<span class="count">{{items.dept.total_count}}</span>
|
||||
</div>
|
||||
<div v-else class="label">{{items.az}}</div>
|
||||
<ul>
|
||||
<li v-if="items.loading && items.list.length === 0" class="members-loading"><Loading/></li>
|
||||
<li
|
||||
v-for="(user, index) in items.list"
|
||||
:key="index"
|
||||
@ -156,10 +171,10 @@
|
||||
</li>
|
||||
<li class="loaded">
|
||||
<template v-if="contactsKey">{{$L('搜索到(*)位联系人', contactsFilter.length)}}</template>
|
||||
<template v-else>{{$L('共(*)位联系人', contactsTotal)}}</template>
|
||||
<template v-else>{{$L('共(*)位联系人', contactsDepartmentMode ? departmentData.total : contactsTotal)}}</template>
|
||||
</li>
|
||||
</template>
|
||||
<li v-else-if="contactsLoad == 0" class="nothing">
|
||||
<li v-else-if="contactsLoad == 0 && departmentLoad == 0" class="nothing">
|
||||
{{$L(contactsKey ? `没有任何与"${contactsKey}"相关的结果` : `没有任何联系人`)}}
|
||||
</li>
|
||||
</ul>
|
||||
@ -280,6 +295,8 @@ import TransferDom from "../../directives/transfer-dom";
|
||||
import emitter from "../../store/events";
|
||||
import transformEmojiToHtml from "../../utils/emoji";
|
||||
|
||||
let contactsViewModeCache = 'list';
|
||||
|
||||
const navDatas = {
|
||||
menus: [
|
||||
{type: '', name: '全部'},
|
||||
@ -319,6 +336,13 @@ export default {
|
||||
contactsCurrentPage: 1,
|
||||
contactsHasMorePages: false,
|
||||
contactsLastTime: 0,
|
||||
contactsPartial: false, // 部门视图下搜索会往 contactsData 混入部分数据,切回列表视图时需重新加载
|
||||
contactsViewMode: contactsViewModeCache, // list|department
|
||||
|
||||
departmentLoad: 0,
|
||||
departmentData: null,
|
||||
departmentExpanded: {}, // 部门ID => 是否展开
|
||||
departmentMembers: {}, // 部门ID => {list, loading}
|
||||
|
||||
operateItem: {},
|
||||
operateStyles: {},
|
||||
@ -332,6 +356,8 @@ export default {
|
||||
if (navDatas.historys.length === 0) {
|
||||
navDatas.historys = navDatas.menus.map(item => Object.assign(item, {time: 0}))
|
||||
}
|
||||
const viewMode = await $A.IDBString("messengerContactsViewMode", contactsViewModeCache)
|
||||
contactsViewModeCache = ['list', 'department'].includes(viewMode) ? viewMode : 'list'
|
||||
next()
|
||||
},
|
||||
|
||||
@ -486,7 +512,58 @@ export default {
|
||||
return contactsData;
|
||||
},
|
||||
|
||||
contactsDepartmentMode() {
|
||||
return this.contactsViewMode === 'department' && !this.contactsKey
|
||||
},
|
||||
|
||||
departmentGroups() {
|
||||
const {departmentData, departmentExpanded, departmentMembers} = this;
|
||||
if (departmentData === null) {
|
||||
return [];
|
||||
}
|
||||
const departments = departmentData.departments.slice();
|
||||
if (departmentData.no_department_count > 0) {
|
||||
departments.push({
|
||||
id: 0,
|
||||
name: this.$L('默认部门'),
|
||||
owner_userid: 0,
|
||||
level: 0,
|
||||
total_count: departmentData.no_department_count,
|
||||
});
|
||||
}
|
||||
const groups = [];
|
||||
let skipLevel = null;
|
||||
departments.forEach(dept => {
|
||||
if (skipLevel !== null && dept.level > skipLevel) {
|
||||
return;
|
||||
}
|
||||
skipLevel = null;
|
||||
const expanded = !!departmentExpanded[dept.id];
|
||||
if (!expanded) {
|
||||
skipLevel = dept.level;
|
||||
}
|
||||
const members = departmentMembers[dept.id];
|
||||
let list = expanded && members ? members.list : [];
|
||||
if (dept.owner_userid > 0) {
|
||||
// 部门负责人排在最前
|
||||
list = [...list.filter(u => u.userid == dept.owner_userid), ...list.filter(u => u.userid != dept.owner_userid)];
|
||||
}
|
||||
groups.push({
|
||||
key: `department-${dept.id}`,
|
||||
az: dept.name,
|
||||
dept,
|
||||
expanded,
|
||||
loading: expanded && !!members?.loading,
|
||||
list,
|
||||
});
|
||||
});
|
||||
return groups;
|
||||
},
|
||||
|
||||
contactsList() {
|
||||
if (this.contactsDepartmentMode) {
|
||||
return this.departmentGroups;
|
||||
}
|
||||
const {contactsKey} = this;
|
||||
const list = [];
|
||||
this.contactsFilter.some(data => {
|
||||
@ -502,6 +579,7 @@ export default {
|
||||
}
|
||||
} else {
|
||||
list.push({
|
||||
key: az,
|
||||
az,
|
||||
list: [user]
|
||||
})
|
||||
@ -547,11 +625,11 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
searchLoading({tabActive, loadDialogs, dialogSearchLoad, contactsLoad}) {
|
||||
searchLoading({tabActive, loadDialogs, dialogSearchLoad, contactsLoad, departmentLoad}) {
|
||||
if (tabActive === 'dialog') {
|
||||
return loadDialogs > 0 || dialogSearchLoad > 0
|
||||
} else {
|
||||
return contactsLoad > 0
|
||||
return contactsLoad > 0 || departmentLoad > 0
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -615,14 +693,7 @@ export default {
|
||||
tabActive: {
|
||||
handler(val) {
|
||||
if (val == 'contacts') {
|
||||
if ($A.dayjs().unix() - this.contactsLastTime > 24 * 3600) {
|
||||
this.contactsData = null; // 24个小时重新加载列表
|
||||
}
|
||||
if (this.contactsData === null) {
|
||||
this.getContactsList(1);
|
||||
} else {
|
||||
this.updateContactsList(1000);
|
||||
}
|
||||
this.loadContactsView();
|
||||
this.dialogSearchSelectedParams = null;
|
||||
} else {
|
||||
this.updateDialogs(1000);
|
||||
@ -632,6 +703,14 @@ export default {
|
||||
immediate: true
|
||||
},
|
||||
|
||||
contactsViewMode(val) {
|
||||
contactsViewModeCache = val
|
||||
$A.IDBSet("messengerContactsViewMode", val)
|
||||
if (this.tabActive === 'contacts') {
|
||||
this.loadContactsView()
|
||||
}
|
||||
},
|
||||
|
||||
dialogId: {
|
||||
handler(id) {
|
||||
if (id > 0) {
|
||||
@ -949,7 +1028,108 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
contactsGroupStyle(items) {
|
||||
if (items.dept && items.dept.level > 0) {
|
||||
return {marginLeft: `${24 + items.dept.level * 14}px`}
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
toggleContactsView() {
|
||||
this.contactsViewMode = this.contactsViewMode === 'department' ? 'list' : 'department'
|
||||
},
|
||||
|
||||
loadContactsView() {
|
||||
if (this.contactsViewMode === 'department') {
|
||||
this.getDepartmentList();
|
||||
return;
|
||||
}
|
||||
if (this.contactsPartial || $A.dayjs().unix() - this.contactsLastTime > 24 * 3600) {
|
||||
this.contactsData = null; // 24个小时重新加载列表
|
||||
this.contactsPartial = false;
|
||||
}
|
||||
if (this.contactsData === null) {
|
||||
this.getContactsList(1);
|
||||
} else {
|
||||
this.updateContactsList(1000);
|
||||
}
|
||||
},
|
||||
|
||||
toggleDepartment(dept) {
|
||||
const expanded = !this.departmentExpanded[dept.id]
|
||||
this.$set(this.departmentExpanded, dept.id, expanded)
|
||||
if (expanded) {
|
||||
this.getDepartmentMembers(dept.id) // 每次展开都刷新,已有缓存时静默更新
|
||||
}
|
||||
},
|
||||
|
||||
getDepartmentList() {
|
||||
this.departmentLoad++;
|
||||
this.$store.dispatch("call", {
|
||||
url: 'contacts/departments',
|
||||
}).then(({data}) => {
|
||||
if (this.departmentData === null) {
|
||||
// 首次加载:展开顶级部门(没有任何部门时展开默认部门)
|
||||
data.departments.forEach(dept => {
|
||||
if (dept.level === 0) {
|
||||
this.$set(this.departmentExpanded, dept.id, true)
|
||||
}
|
||||
})
|
||||
if (data.departments.length === 0) {
|
||||
this.$set(this.departmentExpanded, 0, true)
|
||||
}
|
||||
}
|
||||
this.departmentData = data;
|
||||
// 加载或刷新已展开部门的成员
|
||||
Object.keys(this.departmentExpanded).forEach(id => {
|
||||
if (this.departmentExpanded[id]) {
|
||||
this.getDepartmentMembers(Number(id))
|
||||
}
|
||||
})
|
||||
}).catch(() => {}).finally(_ => {
|
||||
this.departmentLoad--;
|
||||
});
|
||||
},
|
||||
|
||||
getDepartmentMembers(id) {
|
||||
let state = this.departmentMembers[id]
|
||||
if (state?.loading) {
|
||||
return;
|
||||
}
|
||||
if (!state) {
|
||||
this.$set(this.departmentMembers, id, {list: [], loading: true})
|
||||
state = this.departmentMembers[id]
|
||||
} else {
|
||||
state.loading = true
|
||||
}
|
||||
const list = []
|
||||
const load = page => {
|
||||
return this.$store.dispatch("call", {
|
||||
url: 'users/search',
|
||||
data: {
|
||||
keys: {departments: String(id)},
|
||||
sorts: {az: 'asc'},
|
||||
page,
|
||||
pagesize: 100
|
||||
},
|
||||
}).then(({data}) => {
|
||||
list.push(...data.data)
|
||||
if (data.current_page < data.last_page && page < 200) {
|
||||
return load(page + 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
load(1).then(() => {
|
||||
state.list = list
|
||||
}).catch(() => {}).finally(() => {
|
||||
state.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
getContactsList(page) {
|
||||
if (this.contactsViewMode === 'department') {
|
||||
this.contactsPartial = true;
|
||||
}
|
||||
this.contactsLoad++;
|
||||
const key = this.contactsKey
|
||||
this.$store.dispatch("call", {
|
||||
@ -991,6 +1171,7 @@ export default {
|
||||
getContactsNextPage() {
|
||||
if (this.scrollE() < 10
|
||||
&& this.tabActive === 'contacts'
|
||||
&& !this.contactsDepartmentMode
|
||||
&& this.contactsLoad === 0
|
||||
&& this.contactsHasMorePages) {
|
||||
this.getContactsList(this.contactsCurrentPage + 1);
|
||||
@ -1001,7 +1182,7 @@ export default {
|
||||
this.__updateContactsList && clearTimeout(this.__updateContactsList)
|
||||
if (timeout > -1) {
|
||||
this.__updateContactsList = setTimeout(_ => {
|
||||
if (this.tabActive === 'contacts') {
|
||||
if (this.tabActive === 'contacts' && this.contactsViewMode === 'list' && this.contactsData !== null) {
|
||||
this.$store.dispatch("call", {
|
||||
url: 'users/search',
|
||||
data: {
|
||||
|
||||
54
resources/assets/sass/pages/page-messenger.scss
vendored
54
resources/assets/sass/pages/page-messenger.scss
vendored
@ -21,6 +21,25 @@
|
||||
height: 54px;
|
||||
padding: 0 12px;
|
||||
flex-shrink: 0;
|
||||
.search-mode {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
color: #808695;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
.team {
|
||||
font-size: 20px;
|
||||
}
|
||||
&:hover,
|
||||
&.active {
|
||||
color: #84C56A;
|
||||
}
|
||||
}
|
||||
.search-wrapper {
|
||||
flex: 1;
|
||||
background-color: #F7F7F7;
|
||||
@ -424,6 +443,36 @@
|
||||
background-color: #f2f2f2;
|
||||
transform: scaleY(0.5);
|
||||
}
|
||||
&.department-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
.arrow {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
color: #808695;
|
||||
margin-right: 6px;
|
||||
transition: transform 0.2s;
|
||||
&.expanded {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
.name {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.count {
|
||||
flex-shrink: 0;
|
||||
margin-left: 8px;
|
||||
padding-right: 8px;
|
||||
font-size: 12px;
|
||||
color: #808695;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.loaded {
|
||||
margin: 0;
|
||||
@ -433,6 +482,11 @@
|
||||
justify-content: center;
|
||||
}
|
||||
> ul {
|
||||
> li.members-loading {
|
||||
height: 40px;
|
||||
cursor: default;
|
||||
justify-content: center;
|
||||
}
|
||||
> li {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
> 此文件由 `php artisan doc:api-map` 生成,勿手改。
|
||||
|
||||
接口总数:333
|
||||
接口总数:334
|
||||
|
||||
## 路由规则
|
||||
|
||||
@ -86,6 +86,12 @@ API 使用动态路由(见 `routes/web.php`),URL 段映射为控制器方
|
||||
| api/users/favorites/clean | favorites__clean() | post | 清理用户收藏 |
|
||||
| api/users/favorite/check | favorite__check() | get | 检查收藏状态 |
|
||||
|
||||
## contacts(ContactsController)
|
||||
|
||||
| URL | 方法名 | HTTP | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| api/contacts/departments | departments() | get | 通讯录部门树 |
|
||||
|
||||
## project(ProjectController)
|
||||
|
||||
| URL | 方法名 | HTTP | 说明 |
|
||||
|
||||
@ -7,6 +7,7 @@ use App\Http\Controllers\Api\TestController;
|
||||
use App\Http\Controllers\Api\FileController;
|
||||
use App\Http\Controllers\Api\FileDavController;
|
||||
use App\Http\Controllers\Api\UsersController;
|
||||
use App\Http\Controllers\Api\ContactsController;
|
||||
use App\Http\Controllers\Api\DialogController;
|
||||
use App\Http\Controllers\Api\PublicController;
|
||||
use App\Http\Controllers\Api\ReportController;
|
||||
@ -40,6 +41,9 @@ Route::prefix('api')->middleware(['webapi'])->group(function () {
|
||||
// 会员
|
||||
Route::any('users/{method}', UsersController::class);
|
||||
Route::any('users/{method}/{action}', UsersController::class);
|
||||
// 通讯录
|
||||
Route::any('contacts/{method}', ContactsController::class);
|
||||
Route::any('contacts/{method}/{action}', ContactsController::class);
|
||||
// 项目
|
||||
Route::any('project/{method}', ProjectController::class);
|
||||
Route::any('project/{method}/{action}', ProjectController::class);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user