334 lines
15 KiB
Plaintext
334 lines
15 KiB
Plaintext
<template>
|
||
<view class="admin-decoration-home">
|
||
<view class="content-container">
|
||
<!-- 左侧:手机预览区 (保持原样作为预览参考) -->
|
||
<view class="preview-section border-shadow">
|
||
<view class="phone-mock">
|
||
<view class="phone-inner">
|
||
<view class="phone-header-img">
|
||
<view class="status-bar-mock"></view>
|
||
<view class="search-bar-mock">
|
||
<text class="search-ic">🔍</text>
|
||
<text class="search-ph">请输入搜索词</text>
|
||
</view>
|
||
<view class="tabs-mock">
|
||
<text class="tab-item active">首页</text>
|
||
<text class="tab-item">生活家居</text>
|
||
<text class="tab-item">运动专区</text>
|
||
<text class="tab-more">≡</text>
|
||
</view>
|
||
</view>
|
||
|
||
<scroll-view class="phone-scroll" scroll-y="true">
|
||
<view class="banner-mock">
|
||
<view class="banner-box">
|
||
<text class="banner-txt">DIY 页面预览</text>
|
||
</view>
|
||
</view>
|
||
<view class="notice-mock">
|
||
<view class="notice-ic">📢</view>
|
||
<text class="notice-txt">此处展示选中的装修模板预览</text>
|
||
</view>
|
||
<view style="height: 100px;"></view>
|
||
</scroll-view>
|
||
|
||
<view class="tabbar-mock">
|
||
<view class="tb-item active"><text class="tb-ic">🏠</text><text class="tb-txt">首页</text></view>
|
||
<view class="tb-item"><text class="tb-ic">👤</text><text class="tb-txt">我的</text></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 右侧:列表管理区 -->
|
||
<view class="list-section">
|
||
<view class="manage-card border-shadow">
|
||
<view class="action-bar">
|
||
<view class="btn-primary-blue mr-10" @click="handleAdd">
|
||
<text class="btn-txt">添加页面</text>
|
||
</view>
|
||
<view class="filter-item ml-20">
|
||
<input class="search-input-box" placeholder="搜索模板名称" v-model="searchQuery" @confirm="onSearch" />
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 表格 -->
|
||
<view class="table-container">
|
||
<view v-if="isLoading" class="loading-state">
|
||
<text class="loading-txt">数据加载中...</text>
|
||
</view>
|
||
<view v-else-if="tableData.length === 0" class="empty-state">
|
||
<text class="empty-txt">暂无装修模板,请点击左上角添加</text>
|
||
</view>
|
||
<template v-else>
|
||
<view class="table-header-row">
|
||
<view class="th" style="width: 80px;">序号</view>
|
||
<view class="th" style="flex: 2;">模板名称</view>
|
||
<view class="th" style="flex: 1;">模板类型</view>
|
||
<view class="th" style="width: 100px; text-align: center;">状态</view>
|
||
<view class="th" style="flex: 2;">更新时间</view>
|
||
<view class="th" style="width: 220px;">操作</view>
|
||
</view>
|
||
|
||
<view v-for="(item, index) in tableData" :key="item.id" class="table-body-row">
|
||
<view class="td" style="width: 80px;">{{ (page - 1) * pageSize + index + 1 }}</view>
|
||
<view class="td" style="flex: 2;">
|
||
<text class="td-name">{{ item.name }}</text>
|
||
<view v-if="item.is_home" class="home-tag"><text class="ht-txt">当前首页</text></view>
|
||
</view>
|
||
<view class="td" style="flex: 1;">
|
||
<text class="type-txt">{{ getTypeText(item.type) }}</text>
|
||
</view>
|
||
<view class="td" style="width: 100px; justify-content: center;">
|
||
<text :class="['status-dot', item.is_active ? 'active' : '']"></text>
|
||
<text class="status-txt">{{ item.is_active ? '已启用' : '未启用' }}</text>
|
||
</view>
|
||
<view class="td" style="flex: 2;">{{ item.updated_at.substring(0, 16).replace('T', ' ') }}</view>
|
||
<view class="td" style="width: 220px;">
|
||
<view class="op-links">
|
||
<text class="op-link" @click="handleEdit(item)">设计</text>
|
||
<text class="op-split">|</text>
|
||
<text class="op-link" v-if="!item.is_home" @click="handleSetHome(item)">设为首页</text>
|
||
<text class="op-split" v-if="!item.is_home">|</text>
|
||
<text class="op-link text-danger" v-if="!item.is_home" @click="handleDelete(item)">删除</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
</view>
|
||
|
||
<!-- 分页器 -->
|
||
<view class="pagination-footer" v-if="total > 0">
|
||
<text class="total-txt">共 {{ total }} 条</text>
|
||
<view class="page-btns">
|
||
<text :class="['p-btn', page <= 1 ? 'disabled' : '']" @click="onPrevPage"> < </text>
|
||
<text class="p-btn active">{{ page }}</text>
|
||
<text :class="['p-btn', page >= totalPages ? 'disabled' : '']" @click="onNextPage"> > </text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 添加页面侧边栏 -->
|
||
<view v-if="showDrawer" :class="['drawer-mask', isClosing ? 'mask-fade-out' : '']" @click="closeDrawer">
|
||
<view :class="['drawer-content', isClosing ? 'slide-out' : '']" @click.stop="">
|
||
<view class="drawer-header">
|
||
<text class="title-txt">添加装修页面</text>
|
||
<text class="close-btn" @click="closeDrawer">×</text>
|
||
</view>
|
||
|
||
<view class="drawer-body">
|
||
<view class="form-item-v">
|
||
<text class="v-label">页面名称</text>
|
||
<input class="v-input" v-model="formName" placeholder="例如:2026年货节首页" />
|
||
</view>
|
||
|
||
<view class="form-item-v">
|
||
<text class="v-label">页面类型</text>
|
||
<view class="radio-group">
|
||
<view class="radio-item" @click="formType = 'home'">
|
||
<view :class="['radio-dot', formType === 'home' ? 'active' : '']"></view>
|
||
<text class="radio-txt">首页</text>
|
||
</view>
|
||
<view class="radio-item" @click="formType = 'topic'">
|
||
<view :class="['radio-dot', formType === 'topic' ? 'active' : '']"></view>
|
||
<text class="radio-txt">专题页</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="drawer-footer">
|
||
<button class="btn-cancel" @click="closeDrawer">取消</button>
|
||
<button class="btn-save" @click="handleSavePage">确定添加</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, onMounted, computed } from 'vue'
|
||
import { fetchDiyPageList, saveDiyPage, deleteDiyPage, setAsHomePage, type DiyPage } from '@/services/admin/decorationService.uts'
|
||
|
||
const tableData = ref<DiyPage[]>([])
|
||
const total = ref(0)
|
||
const page = ref(1)
|
||
const pageSize = 15
|
||
const isLoading = ref(false)
|
||
const searchQuery = ref('')
|
||
|
||
const showDrawer = ref(false)
|
||
const isClosing = ref(false)
|
||
const formName = ref('')
|
||
const formType = ref('home')
|
||
|
||
const totalPages = computed((): number => {
|
||
return Math.ceil(total.value / pageSize) || 1
|
||
})
|
||
|
||
onMounted(() => {
|
||
loadData()
|
||
})
|
||
|
||
async function loadData() {
|
||
isLoading.value = true
|
||
try {
|
||
const res = await fetchDiyPageList(searchQuery.value || null, null, page.value, pageSize)
|
||
tableData.value = res.items
|
||
total.value = res.total
|
||
} catch (e) {
|
||
uni.showToast({ title: '数据加载失败', icon: 'none' })
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
|
||
function onSearch() {
|
||
page.value = 1
|
||
loadData()
|
||
}
|
||
|
||
async function handleSetHome(item: DiyPage) {
|
||
uni.showLoading({ title: '正在设置...' })
|
||
const success = await setAsHomePage(item.id)
|
||
uni.hideLoading()
|
||
if (success) {
|
||
uni.showToast({ title: '首页设置成功' })
|
||
loadData()
|
||
}
|
||
}
|
||
|
||
async function handleDelete(item: DiyPage) {
|
||
uni.showModal({
|
||
title: '确认删除',
|
||
content: `确定要删除模板 "${item.name}" 吗?`,
|
||
success: async (res) => {
|
||
if (res.confirm) {
|
||
const success = await deleteDiyPage(item.id)
|
||
if (success) {
|
||
uni.showToast({ title: '删除成功' })
|
||
loadData()
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
const handleAdd = () => {
|
||
formName.value = ''
|
||
formType.value = 'home'
|
||
showDrawer.value = true
|
||
isClosing.value = false
|
||
}
|
||
|
||
const closeDrawer = () => {
|
||
isClosing.value = true
|
||
setTimeout(() => {
|
||
showDrawer.value = false
|
||
isClosing.value = false
|
||
}, 300)
|
||
}
|
||
|
||
const handleSavePage = async () => {
|
||
if (!formName.value) {
|
||
uni.showToast({ title: '请输入页面名称', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
const id = await saveDiyPage(null, formName.value, formType.value, {} as UTSJSONObject)
|
||
if (id != null) {
|
||
uni.showToast({ title: '添加成功' })
|
||
closeDrawer()
|
||
loadData()
|
||
}
|
||
}
|
||
|
||
const handleEdit = (item: DiyPage) => {
|
||
uni.showToast({ title: '装修编辑器加载中...', icon: 'none' })
|
||
}
|
||
|
||
function onPrevPage() { if (page.value > 1) { page.value--; loadData(); } }
|
||
function onNextPage() { if (page.value < totalPages.value) { page.value++; loadData(); } }
|
||
|
||
function getTypeText(type: string): string {
|
||
if (type === 'home') return '首页'
|
||
if (type === 'topic') return '专题页'
|
||
if (type === 'user') return '个人中心'
|
||
return type
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.admin-decoration-home { background-color: #f0f2f5; min-height: 100vh; padding: 24px; }
|
||
.border-shadow { background-color: #fff; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); }
|
||
.content-container { display: flex; flex-direction: row; gap: 20px; }
|
||
|
||
/* 左侧手机预览区 (简化版) */
|
||
.preview-section { width: 360px; height: 720px; display: flex; justify-content: center; align-items: center; padding: 20px 0; }
|
||
.phone-mock { width: 300px; height: 600px; background-color: #fff; border: 8px solid #ececec; border-radius: 32px; overflow: hidden; position: relative; }
|
||
.phone-inner { height: 100%; display: flex; flex-direction: column; }
|
||
.phone-header-img { background-color: #f7f7f7; height: 100px; }
|
||
.search-bar-mock { height: 32px; background-color: #fff; margin: 10px 12px; border-radius: 16px; border: 1px solid #eee; display: flex; flex-direction: row; align-items: center; padding: 0 12px; }
|
||
.search-ph { font-size: 11px; color: #999; margin-left: 5px; }
|
||
.phone-scroll { flex: 1; background-color: #f8f8f8; }
|
||
.banner-mock { height: 120px; background: #eee; margin: 10px; border-radius: 8px; display: flex; align-items: center; justify-content: center; }
|
||
.banner-txt { color: #999; font-size: 14px; }
|
||
.tabbar-mock { height: 50px; background: #fff; border-top: 1px solid #eee; display: flex; flex-direction: row; }
|
||
.tb-item { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; opacity: 0.5; }
|
||
.tb-item.active { opacity: 1; }
|
||
.tb-ic { font-size: 18px; }
|
||
.tb-txt { font-size: 10px; margin-top: 2px; }
|
||
|
||
/* 右侧列表区 */
|
||
.list-section { flex: 1; }
|
||
.manage-card { padding: 20px; min-height: 720px; display: flex; flex-direction: column; }
|
||
.action-bar { margin-bottom: 20px; display: flex; flex-direction: row; align-items: center; }
|
||
.btn-primary-blue { background-color: #2d8cf0; padding: 8px 20px; border-radius: 4px; cursor: pointer; }
|
||
.btn-txt { color: #fff; font-size: 14px; }
|
||
.search-input-box { border: 1px solid #dcdfe6; height: 34px; padding: 0 12px; border-radius: 4px; font-size: 13px; width: 200px; }
|
||
|
||
.table-container { flex: 1; margin-top: 10px; }
|
||
.table-header-row { display: flex; flex-direction: row; background-color: #f8f8f9; border-bottom: 1px solid #e8eaec; padding: 12px 0; }
|
||
.th { padding: 0 15px; font-size: 13px; color: #515a6e; font-weight: bold; }
|
||
.table-body-row { display: flex; flex-direction: row; border-bottom: 1px solid #e8eaec; align-items: center; min-height: 60px; }
|
||
.td { padding: 10px 15px; font-size: 13px; color: #515a6e; display: flex; align-items: center; }
|
||
|
||
.home-tag { background-color: #f2270c; padding: 2px 6px; border-radius: 4px; margin-left: 10px; }
|
||
.ht-txt { color: #fff; font-size: 10px; }
|
||
.status-dot { width: 8px; height: 8px; border-radius: 4px; background-color: #ccc; margin-right: 8px; }
|
||
.status-dot.active { background-color: #52c41a; }
|
||
|
||
.op-link { color: #2d8cf0; cursor: pointer; font-size: 13px; }
|
||
.op-split { color: #e8eaec; margin: 0 8px; }
|
||
.text-danger { color: #ed4014; }
|
||
|
||
.pagination-footer { margin-top: 20px; display: flex; flex-direction: row; align-items: center; justify-content: flex-end; gap: 15px; }
|
||
.total-txt { font-size: 13px; color: #999; }
|
||
.p-btn { width: 30px; height: 30px; border: 1px solid #dcdfe6; border-radius: 4px; display: flex; align-items: center; justify-content: center; cursor: pointer; }
|
||
.p-btn.active { background-color: #2d8cf0; color: #fff; border-color: #2d8cf0; }
|
||
.p-btn.disabled { opacity: 0.5; cursor: not-allowed; }
|
||
|
||
/* 抽屉样式 */
|
||
.drawer-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.4); z-index: 2000; display: flex; justify-content: flex-end; }
|
||
.drawer-content { width: 400px; height: 100%; background-color: #fff; display: flex; flex-direction: column; animation: slideIn 0.3s ease-out; }
|
||
.drawer-header { padding: 20px; border-bottom: 1px solid #f0f0f0; display: flex; flex-direction: row; justify-content: space-between; align-items: center; }
|
||
.close-btn { font-size: 24px; color: #999; cursor: pointer; }
|
||
.drawer-body { flex: 1; padding: 30px; }
|
||
.form-item-v { margin-bottom: 24px; }
|
||
.v-label { font-size: 14px; color: #666; margin-bottom: 10px; display: block; }
|
||
.v-input { border: 1px solid #dcdfe6; height: 38px; padding: 0 12px; border-radius: 4px; font-size: 14px; width: 100%; }
|
||
.radio-group { display: flex; flex-direction: row; gap: 30px; margin-top: 10px; }
|
||
.radio-item { display: flex; flex-direction: row; align-items: center; cursor: pointer; }
|
||
.radio-dot { width: 16px; height: 16px; border: 1px solid #dcdfe6; border-radius: 8px; margin-right: 8px; }
|
||
.radio-dot.active { border-color: #2d8cf0; background-color: #2d8cf0; }
|
||
.drawer-footer { padding: 20px; border-top: 1px solid #f0f0f0; display: flex; flex-direction: row; justify-content: flex-end; gap: 12px; }
|
||
.btn-cancel { background: #fff; border: 1px solid #dcdfe6; padding: 8px 20px; border-radius: 4px; }
|
||
.btn-save { background: #2d8cf0; color: #fff; padding: 8px 20px; border-radius: 4px; border: none; }
|
||
|
||
@keyframes slideIn { from { transform: translateX(100%); } to { transform: translateX(0); } }
|
||
.slide-out { animation: slideOut 0.3s ease-in forwards; }
|
||
@keyframes slideOut { from { transform: translateX(0); } to { transform: translateX(100%); } }
|
||
</style>
|