436 lines
10 KiB
Vue
436 lines
10 KiB
Vue
<template>
|
||
<view class="my-apps-wrap">
|
||
<!-- 展示模式 -->
|
||
<template v-if="!editing">
|
||
<!-- 顶部:标题 + 编辑按钮 -->
|
||
<view class="pt-34 pr-30 pb-20 pl-32 bg--w111-fff rd-t-16rpx mt-20 service ml-20 mr-20">
|
||
<view class="flex-row-between">
|
||
<view class="fs-30 fw-500 lh-42rpx text--w111-333">我的应用</view>
|
||
<view class="edit-btn" @click="enterEdit">
|
||
<text class="fs-24 text--w111-1890ff">编辑首页应用</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<!-- 应用列表 -->
|
||
<view class="pt-10 pr-30 pb-32 pl-32 bg--w111-fff rd-b-16rpx service ml-20 mr-20" v-if="allApps.length">
|
||
<view class="grid-column-4 grid-gap-x-76rpx grid-gap-y-54rpx">
|
||
<view v-for="(item, index) in allApps" :key="index">
|
||
<view class="flex-col flex-center" @click="onAppTap(item)">
|
||
<image :src="item.logo" class="app-icon rd-16rpx" v-if="item.logo"></image>
|
||
<view class="empty-box" v-else></view>
|
||
<text class="fs-26 lh-36rpx text--w111-282828 pt-22">{{ item.name }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<!-- 编辑模式 -->
|
||
<template v-else>
|
||
<!-- 编辑卡片:整合头栏 + 首页展示栏 + 全部应用 -->
|
||
<view class="bg--w111-fff rd-16rpx mt-20 ml-20 mr-20">
|
||
<!-- 顶部操作栏 -->
|
||
<view class="edit-header">
|
||
<view class="edit-header-inner">
|
||
<view class="header-btn btn-cancel" @click="cancelEdit">取消</view>
|
||
<view class="fs-30 fw-500 text--w111-333">编辑首页展示</view>
|
||
<view class="header-btn btn-save" @click="saveEdit">保存</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 首页展示栏(横向滚动) -->
|
||
<view class="home-bar-wrap" v-if="editSelectedIds.length">
|
||
<view class="home-bar-label fs-28 text--w111-333">首页展示栏</view>
|
||
<scroll-view class="home-bar-scroll" scroll-x enhanced show-scrollbar="false">
|
||
<view class="home-bar-inner">
|
||
<view
|
||
class="home-bar-item"
|
||
v-for="(item, index) in editSelectedList"
|
||
:key="item.id"
|
||
>
|
||
<view class="home-bar-icon-wrap">
|
||
<image :src="item.logo" class="home-bar-icon rd-16rpx" v-if="item.logo"></image>
|
||
<view class="empty-box-sm" v-else></view>
|
||
<!-- 删除标记 -->
|
||
<view class="badge badge-remove" @click="removeFromHome(item.id)">✕</view>
|
||
</view>
|
||
<text class="home-bar-name">{{ item.name }}</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<!-- 全部应用列表 -->
|
||
<view class="all-apps-list">
|
||
<view class="fs-28 text--w111-333 mb-20">全部应用</view>
|
||
<view class="grid-column-4 grid-gap-x-76rpx grid-gap-y-54rpx">
|
||
<view v-for="(item, index) in allApps" :key="index">
|
||
<view
|
||
class="flex-col flex-center pos-relative"
|
||
@click="toggleAppSelect(item)"
|
||
>
|
||
<view class="app-icon-wrap">
|
||
<image :src="item.logo" class="app-icon rd-16rpx" v-if="item.logo"></image>
|
||
<view class="empty-box" v-else></view>
|
||
<!-- 已选遮罩 -->
|
||
<view class="selected-overlay" v-if="isSelected(item.id)">
|
||
<text class="selected-check">✓</text>
|
||
</view>
|
||
</view>
|
||
<!-- 添加标记(未选中时) -->
|
||
<view
|
||
class="badge badge-add"
|
||
v-if="!isSelected(item.id) && editSelectedIds.length < 6"
|
||
@click.stop="addToHome(item)"
|
||
>+</view>
|
||
<text
|
||
class="fs-26 lh-36rpx pt-22"
|
||
:class="isSelected(item.id) ? 'text--w111-ccc' : 'text--w111-282828'"
|
||
>{{ item.name }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
const STORAGE_KEY = 'MY_HOME_APPS'
|
||
const MAX_HOME_APPS = 6
|
||
|
||
export default {
|
||
name: 'userMyApps',
|
||
data() {
|
||
return {
|
||
allApps: [],
|
||
// Storage 数据
|
||
homeApps: {
|
||
showOnHome: true,
|
||
appIds: []
|
||
},
|
||
editing: false,
|
||
// 编辑中的临时副本
|
||
editAppIds: []
|
||
}
|
||
},
|
||
computed: {
|
||
editSelectedIds() {
|
||
return this.editAppIds
|
||
},
|
||
// 编辑模式中已选应用的完整对象列表(按 allApps 顺序排列)
|
||
editSelectedList() {
|
||
const idSet = new Set(this.editAppIds)
|
||
const filtered = this.allApps.filter(item => idSet.has(item.id))
|
||
const indexMap = new Map(this.allApps.map((item, i) => [item.id, i]))
|
||
filtered.sort((a, b) => (indexMap.get(a.id) ?? 999) - (indexMap.get(b.id) ?? 999))
|
||
return filtered
|
||
}
|
||
},
|
||
mounted() {
|
||
this.loadData()
|
||
this.fetchApps()
|
||
},
|
||
// 从首页"更多"跳转过来时刷新数据
|
||
onShow() {
|
||
this.loadData()
|
||
},
|
||
methods: {
|
||
// ---------- 数据层 ----------
|
||
loadData() {
|
||
try {
|
||
const raw = uni.getStorageSync(STORAGE_KEY)
|
||
if (raw) {
|
||
this.homeApps = raw
|
||
}
|
||
} catch (e) {
|
||
console.error('[myApps] loadData error:', e)
|
||
}
|
||
},
|
||
saveData() {
|
||
try {
|
||
uni.setStorageSync(STORAGE_KEY, this.homeApps)
|
||
} catch (e) {
|
||
console.error('[myApps] saveData error:', e)
|
||
}
|
||
},
|
||
fetchApps() {
|
||
uni.request({
|
||
url: 'https://mp.yizhezhe.com/api/mp/apps',
|
||
data: {
|
||
isPublished: 1,
|
||
pageSize: 50
|
||
},
|
||
success: (res) => {
|
||
if (res.data?.code === 200 && res.data?.result?.items) {
|
||
this.allApps = res.data.result.items
|
||
} else if (res.data?.status === 200 && res.data?.data) {
|
||
this.allApps = res.data.data
|
||
} else {
|
||
return
|
||
}
|
||
this.autoFillHomeApps()
|
||
},
|
||
fail: () => {
|
||
this.allApps = []
|
||
}
|
||
})
|
||
},
|
||
// 自动填充首页展示应用(与 appCenter 逻辑一致)
|
||
autoFillHomeApps() {
|
||
const ids = this.homeApps.appIds || []
|
||
const currentCount = ids.length
|
||
if (currentCount >= 6) return
|
||
|
||
const idSet = new Set(ids)
|
||
const recommended = this.allApps
|
||
.filter(item => item.isRecommended && !idSet.has(item.id))
|
||
|
||
if (!recommended.length) return
|
||
|
||
const slots = 6 - currentCount
|
||
const toAdd = recommended.slice(0, slots).map(item => item.id)
|
||
|
||
this.homeApps.appIds = [...ids, ...toAdd]
|
||
this.saveData()
|
||
},
|
||
|
||
// ---------- 展示模式 ----------
|
||
onAppTap(item) {
|
||
if (!item.url) return
|
||
this.$util.JumpPath(item.url)
|
||
},
|
||
|
||
// ---------- 编辑模式 ----------
|
||
enterEdit() {
|
||
// 复制当前 appIds 作为编辑副本
|
||
this.editAppIds = [...(this.homeApps.appIds || [])]
|
||
this.editing = true
|
||
},
|
||
cancelEdit() {
|
||
this.editing = false
|
||
this.editAppIds = []
|
||
},
|
||
saveEdit() {
|
||
this.homeApps.appIds = [...this.editAppIds]
|
||
this.saveData()
|
||
this.editing = false
|
||
},
|
||
// 判断应用是否已选
|
||
isSelected(id) {
|
||
return this.editAppIds.includes(id)
|
||
},
|
||
// 添加到首页栏(从全部应用点击+)
|
||
addToHome(item) {
|
||
if (this.editAppIds.length >= MAX_HOME_APPS) {
|
||
uni.showToast({ title: `最多选择${MAX_HOME_APPS}个应用`, icon: 'none' })
|
||
return
|
||
}
|
||
if (!this.editAppIds.includes(item.id)) {
|
||
this.editAppIds.push(item.id)
|
||
}
|
||
},
|
||
// 从首页栏移除(点击✕)
|
||
removeFromHome(id) {
|
||
this.editAppIds = this.editAppIds.filter(i => i !== id)
|
||
},
|
||
// 点击应用图标切换选中状态
|
||
toggleAppSelect(item) {
|
||
if (this.isSelected(item.id)) {
|
||
this.removeFromHome(item.id)
|
||
} else {
|
||
this.addToHome(item)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.my-apps-wrap {
|
||
// 左右布局
|
||
.flex-row-between {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
// 应用图标(展示模式 + 编辑模式通用)
|
||
.app-icon {
|
||
width: 96rpx;
|
||
height: 96rpx;
|
||
}
|
||
.empty-box {
|
||
width: 96rpx;
|
||
height: 96rpx;
|
||
background-color: #f3f9ff;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.app-icon-wrap {
|
||
position: relative;
|
||
width: 96rpx;
|
||
height: 96rpx;
|
||
}
|
||
|
||
// 编辑按钮
|
||
.edit-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 6rpx 20rpx;
|
||
border: 2rpx solid #1890ff;
|
||
border-radius: 10rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
// ---------- 编辑模式 ----------
|
||
.edit-header {
|
||
padding: 24rpx 30rpx 0;
|
||
|
||
.edit-header-inner {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.header-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 56rpx;
|
||
padding: 0 28rpx;
|
||
border-radius: 28rpx;
|
||
font-size: 26rpx;
|
||
font-weight: 500;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.btn-cancel {
|
||
color: #666;
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.btn-save {
|
||
color: #fff;
|
||
background: linear-gradient(135deg, #1890ff, #096dd9);
|
||
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.35);
|
||
}
|
||
}
|
||
|
||
// 首页展示栏
|
||
.home-bar-wrap {
|
||
padding: 24rpx 30rpx 0;
|
||
margin-top: 20rpx;
|
||
|
||
.home-bar-label {
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.home-bar-scroll {
|
||
white-space: nowrap;
|
||
overflow: visible !important;
|
||
|
||
.home-bar-inner {
|
||
display: flex;
|
||
gap: 24rpx;
|
||
|
||
.home-bar-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
width: 100rpx;
|
||
padding-top: 12rpx;
|
||
|
||
.home-bar-icon-wrap {
|
||
position: relative;
|
||
width: 90rpx;
|
||
height: 90rpx;
|
||
overflow: visible;
|
||
|
||
.home-bar-icon {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 16rpx;
|
||
}
|
||
|
||
.empty-box-sm {
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: #f3f9ff;
|
||
border-radius: 16rpx;
|
||
}
|
||
}
|
||
|
||
.home-bar-name {
|
||
font-size: 22rpx;
|
||
color: #333;
|
||
margin-top: 8rpx;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
max-width: 100rpx;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 全部应用列表
|
||
.all-apps-list {
|
||
padding: 30rpx 30rpx 34rpx;
|
||
}
|
||
|
||
// ---------- 通用徽标 ----------
|
||
.badge {
|
||
position: absolute;
|
||
width: 36rpx;
|
||
height: 36rpx;
|
||
border-radius: 50%;
|
||
font-size: 20rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #fff;
|
||
z-index: 2;
|
||
}
|
||
|
||
.badge-remove {
|
||
top: -8rpx;
|
||
right: -8rpx;
|
||
background: #ff4d4f;
|
||
}
|
||
|
||
.badge-add {
|
||
top: -8rpx;
|
||
right: 6rpx;
|
||
background: #52c41a;
|
||
}
|
||
|
||
.selected-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.35);
|
||
border-radius: 8rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1;
|
||
|
||
.selected-check {
|
||
color: #fff;
|
||
font-size: 28rpx;
|
||
font-weight: bold;
|
||
}
|
||
}
|
||
|
||
.pos-relative {
|
||
position: relative;
|
||
}
|
||
}
|
||
</style>
|