Files
medical-mall/pages/mall/admin/decoration/home.uvue

334 lines
15 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>