279 lines
6.9 KiB
Vue
279 lines
6.9 KiB
Vue
<script setup lang="ts">
|
||
import { onShow } from '@dcloudio/uni-app'
|
||
import { computed, ref } from 'vue'
|
||
import { useToolsStore } from '@/stores/tools'
|
||
import { useUserStore } from '@/stores/user'
|
||
import { navPadTop } from '@/utils/safearea'
|
||
import { Z } from '@/utils/z-index'
|
||
|
||
const toolsStore = useToolsStore()
|
||
const userStore = useUserStore()
|
||
const keys = ref<string[]>([])
|
||
const saving = ref(false)
|
||
/** 导航栏顶部留白:MP 用真实状态栏高度 */
|
||
const navPad = navPadTop()
|
||
|
||
function goBack() {
|
||
uni.navigateBack()
|
||
}
|
||
|
||
onShow(async () => {
|
||
await userStore.ensureLogin()
|
||
await toolsStore.fetchAll()
|
||
keys.value = [...toolsStore.workbenchKeys]
|
||
})
|
||
|
||
const pinned = computed(() =>
|
||
keys.value
|
||
.map((key) => {
|
||
const server = toolsStore.tools.find(t => t.tool_key === key)
|
||
const def = toolsStore.libraryByModule(server?.module_key || '').find(d => d.key === key)
|
||
return def ? { ...def, locked: server?.locked ?? true } : null
|
||
})
|
||
.filter((x): x is NonNullable<typeof x> => x !== null),
|
||
)
|
||
|
||
const available = computed(() =>
|
||
toolsStore.tools
|
||
.filter(t => !t.locked && !keys.value.includes(t.tool_key))
|
||
.map(t => ({ key: t.tool_key, name: t.name, icon: t.icon, desc: t.description })),
|
||
)
|
||
|
||
function add(key: string) {
|
||
if (!keys.value.includes(key))
|
||
keys.value.push(key)
|
||
}
|
||
function remove(key: string) {
|
||
keys.value = keys.value.filter(k => k !== key)
|
||
}
|
||
function move(index: number, delta: -1 | 1) {
|
||
const target = index + delta
|
||
if (target < 0 || target >= keys.value.length)
|
||
return
|
||
const next = [...keys.value]
|
||
;[next[index], next[target]] = [next[target], next[index]]
|
||
keys.value = next
|
||
}
|
||
|
||
async function save() {
|
||
if (!keys.value.length) {
|
||
uni.showToast({ title: '至少保留一个工具', icon: 'none' })
|
||
return
|
||
}
|
||
saving.value = true
|
||
try {
|
||
await toolsStore.saveWorkbench(keys.value)
|
||
uni.showToast({ title: '已保存', icon: 'success' })
|
||
setTimeout(() => uni.navigateBack(), 500)
|
||
}
|
||
finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<view class="page">
|
||
<view class="nav" :style="{ paddingTop: navPad }">
|
||
<view class="nav__back press" hover-class="press-active" @tap="goBack">
|
||
<wd-icon name="arrow-left" size="36rpx" color="#3d2a1e" />
|
||
</view>
|
||
<text class="nav__title">
|
||
编辑工作台
|
||
</text>
|
||
<!-- 与返回键等宽的占位,保证标题居中 -->
|
||
<view class="nav__slot" />
|
||
</view>
|
||
|
||
<view class="section">
|
||
<text class="tb-section-title">
|
||
已固定({{ pinned.length }})
|
||
</text>
|
||
<view v-if="!pinned.length" class="tb-empty">
|
||
<text class="tb-empty__sub">
|
||
工作台为空,从下方添加工具
|
||
</text>
|
||
</view>
|
||
<view v-for="(t, i) in pinned" :key="t.key" class="row">
|
||
<view class="row__ops">
|
||
<view class="op press" hover-class="press-active" @tap="move(i, -1)">
|
||
<wd-icon name="arrow-up" size="26rpx" color="#9a7b65" />
|
||
</view>
|
||
<view class="op press" hover-class="press-active" @tap="move(i, 1)">
|
||
<wd-icon name="arrow-down" size="26rpx" color="#9a7b65" />
|
||
</view>
|
||
</view>
|
||
<view class="row__icon">
|
||
<wd-icon :name="t.icon" size="36rpx" color="#ff6b35" />
|
||
</view>
|
||
<view class="row__body">
|
||
<text class="row__name">
|
||
{{ t.name }}
|
||
</text>
|
||
<text class="row__desc">
|
||
{{ t.desc }}
|
||
</text>
|
||
</view>
|
||
<view class="row__remove press" hover-class="press-active" @tap="remove(t.key)">
|
||
<wd-icon name="delete" size="32rpx" color="#DC2626" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section">
|
||
<text class="tb-section-title">
|
||
可添加({{ available.length }})
|
||
</text>
|
||
<view v-if="!available.length" class="tb-empty">
|
||
<text class="tb-empty__sub">
|
||
没有更多可添加的工具了
|
||
</text>
|
||
</view>
|
||
<view v-for="t in available" :key="t.key" class="row">
|
||
<view class="row__icon">
|
||
<wd-icon :name="t.icon" size="36rpx" color="#ff6b35" />
|
||
</view>
|
||
<view class="row__body">
|
||
<text class="row__name">
|
||
{{ t.name }}
|
||
</text>
|
||
<text class="row__desc">
|
||
{{ t.desc }}
|
||
</text>
|
||
</view>
|
||
<view class="row__add press" hover-class="press-active" @tap="add(t.key)">
|
||
<wd-icon name="add" size="32rpx" color="#ff6b35" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 底部悬浮保存栏 -->
|
||
<view class="save-bar" :style="{ zIndex: Z.stickyBar }">
|
||
<wd-button
|
||
type="primary"
|
||
block
|
||
:loading="saving"
|
||
custom-style="border-radius: 999rpx; height: 88rpx; font-size: 30rpx; box-shadow: 0 12rpx 28rpx rgba(251, 78, 20, 0.32);"
|
||
@click="save"
|
||
>
|
||
保存工作台
|
||
</wd-button>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.page {
|
||
min-height: 100vh;
|
||
/* 底部给悬浮保存栏留出空间 */
|
||
padding: 0 32rpx calc(180rpx + env(safe-area-inset-bottom, 0px));
|
||
box-sizing: border-box;
|
||
}
|
||
.nav {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 16rpx 0 20rpx;
|
||
}
|
||
.nav__back {
|
||
flex-shrink: 0;
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
border-radius: 50%;
|
||
background: #ffffff;
|
||
border: 1rpx solid #ffe9dd;
|
||
box-shadow: 0 6rpx 16rpx rgba(107, 47, 15, 0.08);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.nav__title {
|
||
font-size: 34rpx;
|
||
font-weight: 700;
|
||
color: #3d2a1e;
|
||
}
|
||
.nav__slot {
|
||
flex-shrink: 0;
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
}
|
||
.section {
|
||
margin-top: 32rpx;
|
||
}
|
||
.section > .tb-section-title {
|
||
margin-bottom: 20rpx;
|
||
}
|
||
.row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20rpx;
|
||
background: #ffffff;
|
||
border: 1rpx solid rgba(255, 195, 155, 0.55);
|
||
border-radius: 30rpx;
|
||
padding: 22rpx 24rpx;
|
||
margin-top: 18rpx;
|
||
box-shadow: 0 10rpx 26rpx rgba(107, 47, 15, 0.06);
|
||
}
|
||
.row__ops {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8rpx;
|
||
}
|
||
.op {
|
||
width: 52rpx;
|
||
height: 52rpx;
|
||
border-radius: 16rpx;
|
||
background: #fff9f2;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.row__icon {
|
||
flex-shrink: 0;
|
||
width: 72rpx;
|
||
height: 72rpx;
|
||
border-radius: 22rpx;
|
||
background: #fff9f2;
|
||
border: 1rpx solid #ffe3d3;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.row__body {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
.row__name {
|
||
font-size: 30rpx;
|
||
font-weight: 700;
|
||
color: #3d2a1e;
|
||
}
|
||
.row__desc {
|
||
display: block;
|
||
margin-top: 4rpx;
|
||
font-size: 22rpx;
|
||
color: #9a7b65;
|
||
}
|
||
.row__remove,
|
||
.row__add {
|
||
flex-shrink: 0;
|
||
width: 68rpx;
|
||
height: 68rpx;
|
||
border-radius: 20rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.row__add {
|
||
background: #ffe3d3;
|
||
}
|
||
|
||
/* 底部悬浮保存栏(Home Indicator 之上) */
|
||
.save-bar {
|
||
position: fixed;
|
||
left: 32rpx;
|
||
right: 32rpx;
|
||
bottom: calc(20rpx + env(safe-area-inset-bottom, 0px));
|
||
}
|
||
</style>
|