mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-10 18:02:55 +00:00
feat: 添加应用排序功能
This commit is contained in:
parent
f6e4ed7c60
commit
d30b38d4b9
@ -36,6 +36,7 @@ use App\Models\UserFavorite;
|
||||
use App\Models\UserRecentItem;
|
||||
use App\Models\UserTag;
|
||||
use App\Models\UserTagRecognition;
|
||||
use App\Models\UserAppSort;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Models\UserEmailVerification;
|
||||
use App\Module\AgoraIO\AgoraTokenGenerator;
|
||||
@ -3456,6 +3457,51 @@ class UsersController extends AbstractController
|
||||
return Base::retSuccess('删除成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/appsort 获取个人应用排序
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName appsort
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function appsort()
|
||||
{
|
||||
$user = User::auth();
|
||||
$sorts = UserAppSort::getSorts($user->userid);
|
||||
return Base::retSuccess('success', [
|
||||
'sorts' => $sorts,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/users/appsort/save 保存个人应用排序
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName appsort__save
|
||||
*
|
||||
* @apiParam {Object} sorts 排序配置,示例:{"base":["micro:calendar"],"admin":["system:ldap"]}
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function appsort__save()
|
||||
{
|
||||
$user = User::auth();
|
||||
$sorts = UserAppSort::normalizeSorts(Request::input('sorts'));
|
||||
$record = UserAppSort::saveSorts($user->userid, $sorts);
|
||||
return Base::retSuccess('保存成功', [
|
||||
'sorts' => $record->sorts ?? $sorts,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/favorites 获取用户收藏列表
|
||||
*
|
||||
@ -3679,4 +3725,5 @@ class UsersController extends AbstractController
|
||||
//
|
||||
return Base::retSuccess('success', ['favorited' => $isFavorited]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
96
app/Models/UserAppSort.php
Normal file
96
app/Models/UserAppSort.php
Normal file
@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/**
|
||||
* App\Models\UserAppSort
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $userid 用户ID
|
||||
* @property array|null $sorts 排序配置
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort whereSorts($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UserAppSort extends AbstractModel
|
||||
{
|
||||
protected $fillable = [
|
||||
'userid',
|
||||
'sorts',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'sorts' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取用户排序配置
|
||||
* @param int $userid
|
||||
* @return array
|
||||
*/
|
||||
public static function getSorts(int $userid): array
|
||||
{
|
||||
$record = static::whereUserid($userid)->first();
|
||||
if (!$record) {
|
||||
return self::normalizeSorts([]);
|
||||
}
|
||||
return self::normalizeSorts($record->sorts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存排序配置
|
||||
* @param int $userid
|
||||
* @param array $sorts
|
||||
* @return static
|
||||
*/
|
||||
public static function saveSorts(int $userid, array $sorts): self
|
||||
{
|
||||
return static::updateOrCreate(
|
||||
['userid' => $userid],
|
||||
['sorts' => self::normalizeSorts($sorts)]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化排序数据
|
||||
* @param mixed $sorts
|
||||
* @return array
|
||||
*/
|
||||
public static function normalizeSorts($sorts): array
|
||||
{
|
||||
$result = [
|
||||
'base' => [],
|
||||
'admin' => [],
|
||||
];
|
||||
if (!is_array($sorts)) {
|
||||
return $result;
|
||||
}
|
||||
foreach (['base', 'admin'] as $group) {
|
||||
$list = $sorts[$group] ?? [];
|
||||
if (!is_array($list)) {
|
||||
$list = [];
|
||||
}
|
||||
$normalized = [];
|
||||
foreach ($list as $value) {
|
||||
if (!is_string($value)) {
|
||||
continue;
|
||||
}
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
continue;
|
||||
}
|
||||
$normalized[] = $value;
|
||||
}
|
||||
$result[$group] = array_values(array_unique($normalized));
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateUserAppSortsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
if (Schema::hasTable('user_app_sorts')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create('user_app_sorts', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('userid')->unique()->comment('用户ID');
|
||||
$table->json('sorts')->nullable()->comment('排序配置');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('user_app_sorts');
|
||||
}
|
||||
}
|
||||
@ -7,74 +7,116 @@
|
||||
<div class="apply-nav">
|
||||
<h1>{{ $L('应用') }}</h1>
|
||||
</div>
|
||||
<div class="apply-nav-actions">
|
||||
<Dropdown trigger="click" placement="bottom-end" transfer @on-click="handleActionMenu">
|
||||
<div class="apply-action-btn">
|
||||
<Icon type="ios-more"/>
|
||||
</div>
|
||||
<DropdownMenu slot="list">
|
||||
<DropdownItem v-if="!sortingMode" name="sort">{{ $L('调整排序') }}</DropdownItem>
|
||||
<DropdownItem v-else name="cancelSort">{{ $L('退出排序') }}</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="sortingMode" class="apply-sort-bar">
|
||||
<div class="apply-sort-tip">
|
||||
<Icon type="md-move"/>
|
||||
<span>{{ $L('拖动卡片调整顺序,保存后仅自己可见') }}</span>
|
||||
</div>
|
||||
<div class="apply-sort-actions">
|
||||
<Button @click="exitSortMode">{{ $L('取消') }}</Button>
|
||||
<Button @click="restoreDefaultSort">{{ $L('恢复默认') }}</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
:disabled="!sortHasChanges"
|
||||
:loading="appSortSaving"
|
||||
@click="submitSort">
|
||||
{{ $L('保存') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="apply-content">
|
||||
<template v-for="t in applyTypes">
|
||||
<div v-if="isExistAdminList" class="apply-row-title">
|
||||
{{ t == 'base' ? $L('常用') : $L('管理员') }}
|
||||
</div>
|
||||
<Row :gutter="16">
|
||||
<Col
|
||||
v-for="(item, key) in (t == 'base' ? filterMicroAppsMenus : filterMicroAppsMenusAdmin)"
|
||||
:key="`micro_` + key"
|
||||
:xs="{ span: 6 }"
|
||||
:sm="{ span: 6 }"
|
||||
:lg="{ span: 6 }"
|
||||
:xl="{ span: 6 }"
|
||||
:xxl="{ span: 3 }">
|
||||
<div class="apply-col">
|
||||
<div class="apply-item" @click="applyClick({value: 'microApp'}, item)">
|
||||
<div class="logo">
|
||||
<div class="apply-icon no-dark-content" :style="{backgroundImage: `url(${item.icon})`}"></div>
|
||||
</div>
|
||||
<p>{{ item.label }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col
|
||||
v-for="(item, key) in applyList"
|
||||
:key="key"
|
||||
v-if="((t=='base' && !item.type) || item.type == t) && item.show !== false"
|
||||
:xs="{ span: 6 }"
|
||||
:sm="{ span: 6 }"
|
||||
:lg="{ span: 6 }"
|
||||
:xl="{ span: 6 }"
|
||||
:xxl="{ span: 3 }">
|
||||
<div class="apply-col">
|
||||
<template v-if="item.value === 'exportManage'">
|
||||
<EPopover
|
||||
v-model="exportPopoverShow"
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
popperClass="apply-export-popover"
|
||||
:transfer="true">
|
||||
<div slot="reference" class="apply-item">
|
||||
<template v-if="t === 'base' || adminAppItems.length > 0">
|
||||
<div
|
||||
v-if="(t === 'base' && isExistAdminList) || t === 'admin'"
|
||||
class="apply-row-title">
|
||||
{{ t === 'base' ? $L('常用') : $L('管理员') }}
|
||||
</div>
|
||||
<Draggable
|
||||
v-for="cards in [currentCards(t)]"
|
||||
:key="`apps_${t}`"
|
||||
tag="Row"
|
||||
class="apply-sort-list"
|
||||
:list="cards"
|
||||
:disabled="!sortingMode"
|
||||
:component-data="{ props: { gutter: 16 } }"
|
||||
:options="getDraggableOptions(t)">
|
||||
<Col
|
||||
v-for="card in cards"
|
||||
:key="card.sortKey"
|
||||
class="apply-col-wrapper"
|
||||
:xs="{ span: 6 }"
|
||||
:sm="{ span: 6 }"
|
||||
:lg="{ span: 6 }"
|
||||
:xl="{ span: 6 }"
|
||||
:xxl="{ span: 3 }">
|
||||
<div class="apply-col">
|
||||
<template v-if="card.category === 'micro'">
|
||||
<div class="apply-item" :class="{'is-sorting': sortingMode}" @click="handleCardClick(card)">
|
||||
<div class="logo">
|
||||
<div class="apply-icon no-dark-content" :class="getLogoClass(item.value)"></div>
|
||||
<div class="apply-icon no-dark-content" :style="{backgroundImage: `url(${card.micro.icon})`}"></div>
|
||||
</div>
|
||||
<p>{{ $L(item.label) }}</p>
|
||||
<p>{{ card.micro.label }}</p>
|
||||
</div>
|
||||
<ul class="apply-export-menu">
|
||||
<li @click="handleExport('task')">{{ $L('导出任务统计') }}</li>
|
||||
<li @click="handleExport('overdue')">{{ $L('导出超期任务') }}</li>
|
||||
<li @click="handleExport('approve')">{{ $L('导出审批数据') }}</li>
|
||||
<li @click="handleExport('checkin')">{{ $L('导出签到数据') }}</li>
|
||||
</ul>
|
||||
</EPopover>
|
||||
</template>
|
||||
<div v-else class="apply-item" @click="applyClick(item)">
|
||||
<div class="logo">
|
||||
<div class="apply-icon no-dark-content" :class="getLogoClass(item.value)"></div>
|
||||
<div @click.stop="applyClick(item, 'badge')" class="apply-box-top-report">
|
||||
<Badge v-if="showBadge(item,'approve')" :overflow-count="999" :count="approveUnreadNumber"/>
|
||||
<Badge v-if="showBadge(item,'report')" :overflow-count="999" :count="reportUnreadNumber"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="card.system.value === 'exportManage' && !sortingMode">
|
||||
<EPopover
|
||||
v-model="exportPopoverShow"
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
popperClass="apply-export-popover"
|
||||
:transfer="true">
|
||||
<div slot="reference" class="apply-item" :class="{'is-sorting': sortingMode}">
|
||||
<div class="logo">
|
||||
<div class="apply-icon no-dark-content" :class="getLogoClass(card.system.value)"></div>
|
||||
</div>
|
||||
<p>{{ $L(card.system.label) }}</p>
|
||||
</div>
|
||||
<ul class="apply-export-menu">
|
||||
<li @click="handleExport('task')">{{ $L('导出任务统计') }}</li>
|
||||
<li @click="handleExport('overdue')">{{ $L('导出超期任务') }}</li>
|
||||
<li @click="handleExport('approve')">{{ $L('导出审批数据') }}</li>
|
||||
<li @click="handleExport('checkin')">{{ $L('导出签到数据') }}</li>
|
||||
</ul>
|
||||
</EPopover>
|
||||
</template>
|
||||
<div
|
||||
v-else
|
||||
class="apply-item"
|
||||
:class="{'is-sorting': sortingMode}"
|
||||
@click="handleCardClick(card)">
|
||||
<div class="logo">
|
||||
<div class="apply-icon no-dark-content" :class="getLogoClass(card.system.value)"></div>
|
||||
<div
|
||||
v-if="!sortingMode"
|
||||
@click.stop="handleCardClick(card, 'badge')"
|
||||
class="apply-box-top-report">
|
||||
<Badge v-if="showBadge(card.system,'approve')" :overflow-count="999" :count="approveUnreadNumber"/>
|
||||
<Badge v-if="showBadge(card.system,'report')" :overflow-count="999" :count="reportUnreadNumber"/>
|
||||
</div>
|
||||
</div>
|
||||
<p>{{ $L(card.system.label) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p>{{ $L(item.label) }}</p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Draggable>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@ -289,9 +331,11 @@ import SystemAppPush from "./setting/components/SystemAppPush";
|
||||
import emitter from "../../store/events";
|
||||
import ImgUpload from "../../components/ImgUpload.vue";
|
||||
import {webhookEventOptions} from "../../utils/webhook";
|
||||
import Draggable from "vuedraggable";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Draggable,
|
||||
ImgUpload,
|
||||
UserSelect,
|
||||
DrawerOverlay,
|
||||
@ -306,6 +350,22 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
applyTypes: ['base', 'admin'],
|
||||
sortingMode: false,
|
||||
sortLists: {
|
||||
base: [],
|
||||
admin: [],
|
||||
},
|
||||
sortInitialLists: {
|
||||
base: [],
|
||||
admin: [],
|
||||
},
|
||||
appSorts: {
|
||||
base: [],
|
||||
admin: [],
|
||||
},
|
||||
appSortLoaded: false,
|
||||
appSortLoading: false,
|
||||
appSortSaving: false,
|
||||
//
|
||||
mybotShow: false,
|
||||
mybotList: [],
|
||||
@ -337,6 +397,9 @@ export default {
|
||||
sendType: '',
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchAppSorts();
|
||||
},
|
||||
activated() {
|
||||
this.$store.dispatch("updateMicroAppsStatus")
|
||||
},
|
||||
@ -349,6 +412,7 @@ export default {
|
||||
'approveUnreadNumber',
|
||||
'cacheDialogs',
|
||||
'windowOrientation',
|
||||
'windowPortrait',
|
||||
'formOptions',
|
||||
'routeLoading',
|
||||
'microAppsIds'
|
||||
@ -359,6 +423,7 @@ export default {
|
||||
]),
|
||||
applyList() {
|
||||
const list = [
|
||||
// 常用应用
|
||||
{value: "approve", label: "审批中心", sort: 30, show: this.microAppsIds.includes('approve')},
|
||||
{value: "favorite", label: "我的收藏", sort: 45},
|
||||
{value: "recent", label: "最近打开", sort: 47},
|
||||
@ -372,6 +437,14 @@ export default {
|
||||
{value: "addProject", label: "创建项目", sort: 110},
|
||||
{value: "addTask", label: "添加任务", sort: 120},
|
||||
{value: "scan", label: "扫一扫", sort: 130, show: $A.isEEUIApp},
|
||||
|
||||
// 管理员应用
|
||||
{type: 'admin', value: "ldap", label: "LDAP", sort: 160, show: this.userIsAdmin},
|
||||
{type: 'admin', value: "mail", label: "邮件通知", sort: 170, show: this.userIsAdmin},
|
||||
{type: 'admin', value: "appPush", label: "APP 推送", sort: 180, show: this.userIsAdmin},
|
||||
{type: 'admin', value: "complaint", label: "举报管理", sort: 190, show: this.userIsAdmin},
|
||||
{type: 'admin', value: "exportManage", label: "数据导出", sort: 195, show: this.userIsAdmin},
|
||||
{type: 'admin', value: "allUser", label: "团队管理", sort: 200, show: this.userIsAdmin},
|
||||
]
|
||||
// 竖屏模式
|
||||
if (this.windowPortrait) {
|
||||
@ -381,25 +454,293 @@ export default {
|
||||
{value: "setting", label: "设置", sort: 140},
|
||||
])
|
||||
}
|
||||
// 管理员
|
||||
if (this.userIsAdmin) {
|
||||
list.push(...[
|
||||
{type: 'admin', value: "ldap", label: "LDAP", sort: 160},
|
||||
{type: 'admin', value: "mail", label: "邮件通知", sort: 170},
|
||||
{type: 'admin', value: "appPush", label: "APP 推送", sort: 180},
|
||||
{type: 'admin', value: "complaint", label: "举报管理", sort: 190},
|
||||
{type: 'admin', value: "exportManage", label: "数据导出", sort: 195},
|
||||
{type: 'admin', value: "allUser", label: "团队管理", sort: 200},
|
||||
])
|
||||
}
|
||||
//
|
||||
return list.sort((a, b) => a.sort - b.sort);
|
||||
},
|
||||
isExistAdminList() {
|
||||
return this.filterMicroAppsMenusAdmin.length > 0 || this.applyList.map(h => h.type).indexOf('admin') !== -1;
|
||||
return this.adminAppItems.length > 0;
|
||||
},
|
||||
baseAppItems() {
|
||||
return this.applySavedSort(this.collectAppItems('base'), 'base');
|
||||
},
|
||||
adminAppItems() {
|
||||
return this.applySavedSort(this.collectAppItems('admin'), 'admin');
|
||||
},
|
||||
sortHasChanges() {
|
||||
if (!this.sortingMode) {
|
||||
return false;
|
||||
}
|
||||
const groups = ['base', 'admin'];
|
||||
return groups.some(group => {
|
||||
const current = (this.sortLists[group] || []).map(item => item.sortKey);
|
||||
const initial = this.sortInitialLists[group] || [];
|
||||
if (current.length !== initial.length) {
|
||||
return true;
|
||||
}
|
||||
return current.some((key, index) => key !== initial[index]);
|
||||
});
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
sortingMode(val) {
|
||||
if (val) {
|
||||
this.bootstrapSortLists();
|
||||
} else {
|
||||
this.resetSortState();
|
||||
}
|
||||
},
|
||||
baseAppItems() {
|
||||
if (this.sortingMode) {
|
||||
this.mergeSortListWithSource('base');
|
||||
}
|
||||
},
|
||||
adminAppItems() {
|
||||
if (this.sortingMode) {
|
||||
this.mergeSortListWithSource('admin');
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleActionMenu(action) {
|
||||
if (action === 'sort') {
|
||||
this.enterSortMode();
|
||||
} else if (action === 'cancelSort') {
|
||||
this.exitSortMode();
|
||||
}
|
||||
},
|
||||
currentCards(type) {
|
||||
return this.sortingMode ? (this.sortLists[type] || []) : this.getDisplayItems(type);
|
||||
},
|
||||
getDisplayItems(type) {
|
||||
return type === 'admin' ? this.adminAppItems : this.baseAppItems;
|
||||
},
|
||||
collectAppItems(group) {
|
||||
const items = [];
|
||||
const microSource = group === 'admin' ? this.filterMicroAppsMenusAdmin : this.filterMicroAppsMenus;
|
||||
microSource.forEach(menu => {
|
||||
if (!menu || menu.show === false) {
|
||||
return;
|
||||
}
|
||||
items.push(this.createMicroCard(menu, group));
|
||||
});
|
||||
this.applyList.forEach(item => {
|
||||
if (item.show === false) {
|
||||
return;
|
||||
}
|
||||
const isAdminItem = item.type === 'admin';
|
||||
if (group === 'admin') {
|
||||
if (!isAdminItem) {
|
||||
return;
|
||||
}
|
||||
} else if (isAdminItem) {
|
||||
return;
|
||||
}
|
||||
items.push(this.createSystemCard(item, group));
|
||||
});
|
||||
return items;
|
||||
},
|
||||
createMicroCard(menu, group) {
|
||||
const fallback = menu?.id || menu?.value || menu?.url || menu?.label || 'unknown';
|
||||
const key = menu?.name || fallback;
|
||||
return {
|
||||
sortKey: `micro:${key}`,
|
||||
category: 'micro',
|
||||
group,
|
||||
micro: menu,
|
||||
};
|
||||
},
|
||||
createSystemCard(item, group) {
|
||||
return {
|
||||
sortKey: `system:${item.value}`,
|
||||
category: 'system',
|
||||
group,
|
||||
system: item,
|
||||
};
|
||||
},
|
||||
applySavedSort(items, group) {
|
||||
const saved = this.appSorts[group] || [];
|
||||
if (!saved.length) {
|
||||
return items;
|
||||
}
|
||||
const map = {};
|
||||
items.forEach(card => {
|
||||
map[card.sortKey] = card;
|
||||
});
|
||||
const ordered = [];
|
||||
saved.forEach(key => {
|
||||
if (map[key]) {
|
||||
ordered.push(map[key]);
|
||||
delete map[key];
|
||||
}
|
||||
});
|
||||
items.forEach(card => {
|
||||
if (map[card.sortKey]) {
|
||||
ordered.push(card);
|
||||
delete map[card.sortKey];
|
||||
}
|
||||
});
|
||||
return ordered;
|
||||
},
|
||||
async enterSortMode() {
|
||||
if (this.sortingMode) {
|
||||
return;
|
||||
}
|
||||
if (!this.appSortLoaded && !this.appSortLoading) {
|
||||
await this.fetchAppSorts();
|
||||
}
|
||||
this.sortingMode = true;
|
||||
},
|
||||
exitSortMode() {
|
||||
this.sortingMode = false;
|
||||
},
|
||||
bootstrapSortLists() {
|
||||
const base = this.cloneAppItems(this.baseAppItems);
|
||||
const admin = this.cloneAppItems(this.adminAppItems);
|
||||
this.$set(this.sortLists, 'base', base);
|
||||
this.$set(this.sortLists, 'admin', admin);
|
||||
this.$set(this.sortInitialLists, 'base', base.map(item => item.sortKey));
|
||||
this.$set(this.sortInitialLists, 'admin', admin.map(item => item.sortKey));
|
||||
},
|
||||
resetSortState() {
|
||||
this.$set(this.sortLists, 'base', []);
|
||||
this.$set(this.sortLists, 'admin', []);
|
||||
this.$set(this.sortInitialLists, 'base', []);
|
||||
this.$set(this.sortInitialLists, 'admin', []);
|
||||
},
|
||||
mergeSortListWithSource(group) {
|
||||
const source = this.cloneAppItems(this.getDisplayItems(group));
|
||||
if (!source.length) {
|
||||
this.$set(this.sortLists, group, []);
|
||||
this.$set(this.sortInitialLists, group, []);
|
||||
return;
|
||||
}
|
||||
const sourceMap = new Map(source.map(item => [item.sortKey, item]));
|
||||
const next = [];
|
||||
(this.sortLists[group] || []).forEach(item => {
|
||||
if (sourceMap.has(item.sortKey)) {
|
||||
next.push(sourceMap.get(item.sortKey));
|
||||
sourceMap.delete(item.sortKey);
|
||||
}
|
||||
});
|
||||
sourceMap.forEach(item => next.push(item));
|
||||
this.$set(this.sortLists, group, this.cloneAppItems(next));
|
||||
const snapshot = this.sortInitialLists[group] ? [...this.sortInitialLists[group]] : [];
|
||||
next.forEach(item => {
|
||||
if (!snapshot.includes(item.sortKey)) {
|
||||
snapshot.push(item.sortKey);
|
||||
}
|
||||
});
|
||||
this.$set(this.sortInitialLists, group, snapshot);
|
||||
},
|
||||
cloneAppItems(items = []) {
|
||||
return items.map(item => Object.assign({}, item));
|
||||
},
|
||||
getDraggableOptions(type) {
|
||||
return {
|
||||
animation: 200,
|
||||
draggable: '.apply-col-wrapper',
|
||||
group: {
|
||||
name: `${type}-apps`,
|
||||
pull: false,
|
||||
put: false,
|
||||
}
|
||||
};
|
||||
},
|
||||
async fetchAppSorts() {
|
||||
if (this.appSortLoading) {
|
||||
return;
|
||||
}
|
||||
this.appSortLoading = true;
|
||||
try {
|
||||
const {data} = await this.$store.dispatch("call", {
|
||||
url: 'users/appsort',
|
||||
method: 'get',
|
||||
});
|
||||
this.appSorts = this.normalizeSortPayload(data?.sorts);
|
||||
} catch (error) {
|
||||
const msg = error?.msg || error?.message;
|
||||
msg && console.warn(msg);
|
||||
} finally {
|
||||
this.appSortLoading = false;
|
||||
this.appSortLoaded = true;
|
||||
}
|
||||
},
|
||||
normalizeSortPayload(raw) {
|
||||
const result = {base: [], admin: []};
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return result;
|
||||
}
|
||||
['base', 'admin'].forEach(group => {
|
||||
const list = Array.isArray(raw[group]) ? raw[group] : [];
|
||||
result[group] = list
|
||||
.filter(item => typeof item === 'string')
|
||||
.map(item => item.trim())
|
||||
.filter(item => item.length > 0);
|
||||
});
|
||||
return result;
|
||||
},
|
||||
submitSort() {
|
||||
if (!this.sortHasChanges) {
|
||||
this.exitSortMode();
|
||||
return;
|
||||
}
|
||||
const payload = this.buildSortPayload();
|
||||
this.appSortSaving = true;
|
||||
this.$store.dispatch("call", {
|
||||
url: 'users/appsort/save',
|
||||
method: 'post',
|
||||
data: {
|
||||
sorts: payload,
|
||||
}
|
||||
}).then(({data, msg}) => {
|
||||
this.appSorts = this.normalizeSortPayload(data?.sorts || payload);
|
||||
this.exitSortMode();
|
||||
$A.messageSuccess(msg || this.$L('保存成功'));
|
||||
}).catch(({msg}) => {
|
||||
$A.modalError(msg || this.$L('保存失败'));
|
||||
}).finally(() => {
|
||||
this.appSortSaving = false;
|
||||
});
|
||||
},
|
||||
restoreDefaultSort() {
|
||||
if (!this.sortingMode) {
|
||||
return;
|
||||
}
|
||||
['base', 'admin'].forEach(group => {
|
||||
this.$set(this.sortLists, group, this.cloneAppItems(this.collectAppItems(group)));
|
||||
});
|
||||
},
|
||||
buildSortPayload() {
|
||||
const payload = {base: [], admin: []};
|
||||
['base', 'admin'].forEach(group => {
|
||||
const keys = (this.sortLists[group] || []).map(item => item.sortKey);
|
||||
const defaults = this.getDefaultSortKeys(group);
|
||||
payload[group] = this.arraysEqual(keys, defaults) ? [] : keys;
|
||||
});
|
||||
return payload;
|
||||
},
|
||||
getDefaultSortKeys(group) {
|
||||
return this.collectAppItems(group).map(item => item.sortKey);
|
||||
},
|
||||
arraysEqual(a = [], b = []) {
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
return a.every((item, index) => item === b[index]);
|
||||
},
|
||||
handleCardClick(card, params = '') {
|
||||
if (this.sortingMode) {
|
||||
return;
|
||||
}
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
if (card.category === 'micro') {
|
||||
this.applyClick({value: 'microApp'}, card.micro);
|
||||
return;
|
||||
}
|
||||
this.applyClick(card.system, params);
|
||||
},
|
||||
normalizeWebhookEvents(events = [], useFallback = false) {
|
||||
if (!Array.isArray(events)) {
|
||||
events = events ? [events] : [];
|
||||
|
||||
65
resources/assets/sass/pages/page-apply.scss
vendored
65
resources/assets/sass/pages/page-apply.scss
vendored
@ -30,6 +30,52 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.apply-nav-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.apply-action-btn {
|
||||
font-size: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px;
|
||||
color: #6f6f6f;
|
||||
cursor: pointer;
|
||||
transition: all .2s;
|
||||
|
||||
&:hover {
|
||||
color: $primary-title-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.apply-sort-bar {
|
||||
margin: 16px 32px 0;
|
||||
padding: 12px 16px;
|
||||
border: 1px dashed rgba($primary-color, 0.4);
|
||||
border-radius: 8px;
|
||||
background-color: mix(#ffffff, $primary-color, 92%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.apply-sort-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: $primary-color;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.apply-sort-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.apply-content {
|
||||
@ -39,7 +85,6 @@
|
||||
|
||||
.apply-row-title {
|
||||
margin-bottom: 16px;
|
||||
|
||||
}
|
||||
|
||||
> div.apply-row-title:nth-last-child(2) {
|
||||
@ -83,11 +128,23 @@
|
||||
top: -16px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
&.is-sorting {
|
||||
border-style: dashed;
|
||||
border-color: $primary-color;
|
||||
background: rgba($primary-color, 0.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 510px) {
|
||||
.apply-sort-bar {
|
||||
margin: 12px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.apply-row-title {
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
@ -531,7 +588,11 @@ body.window-portrait {
|
||||
background-color: #FFFFFF;
|
||||
|
||||
.apply-head {
|
||||
margin: 24px 24px 0 24px;
|
||||
margin: 24px 24px 0;
|
||||
}
|
||||
|
||||
.apply-sort-bar {
|
||||
margin: 16px 20px 0;
|
||||
}
|
||||
|
||||
.apply-content {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user