Files
medical-mall/pages/mall/admin/cms/category/index.uvue
2026-03-18 08:36:49 +08:00

392 lines
16 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-cms-category">
<view class="content-body">
<!-- 顶部过滤栏 -->
<view class="filter-card border-shadow">
<view class="filter-item">
<text class="label-txt">是否显示:</text>
<view class="select-mock">
<text class="select-val">请选择</text>
<text class="arrow-down">▼</text>
</view>
</view>
<view class="filter-item">
<text class="label-txt">分类名称:</text>
<input class="search-input" placeholder="请输入分类名称" v-model="filterKeyword" />
</view>
<view class="btn-query" @click="handleQuery">
<text class="query-txt">查询</text>
</view>
</view>
<!-- 主要内容区域 -->
<view class="table-card border-shadow">
<view class="card-header">
<view class="btn-primary" @click="handleAdd">
<text class="btn-txt">添加文章分类</text>
</view>
</view>
<!-- 数据表格 -->
<view class="table-header">
<view class="th col-id"><text class="th-txt">ID</text></view>
<view class="th col-name"><text class="th-txt">分类名称</text></view>
<view class="th col-img"><text class="th-txt">分类图片</text></view>
<view class="th col-status"><text class="th-txt">状态</text></view>
<view class="th col-op"><text class="th-txt">操作</text></view>
</view>
<view class="table-body">
<view class="table-row" v-for="item in pagedList" :key="item.id">
<view class="td col-id"><text class="td-txt">{{ item.id }}</text></view>
<view class="td col-name"><text class="td-txt">{{ item.name }}</text></view>
<view class="td col-img">
<view class="img-box-placeholder"></view>
</view>
<view class="td col-status">
<StatusSwitch v-model="item.status" />
</view>
<view class="td col-op">
<view class="op-links">
<text class="link-txt" @click="handleEdit(item)">编辑</text>
<view class="divider"></view>
<text class="link-txt danger">删除</text>
<view class="divider"></view>
<text class="link-txt">查看文章</text>
</view>
</view>
</view>
</view>
<CommonPagination
v-if="total > 0"
:total="total"
:loading="false"
:currentPage="currentPage"
:pageSize="pageSize"
:pageSizeOptionLabels="pageSizeOptionLabels"
:pageSizeIndex="pageSizeIndex"
:visiblePages="visiblePages"
:totalPage="totalPage"
:jumpPageInput="jumpPageInput"
@page-size-change="handlePageSizeChange"
@page-change="handlePageChange"
@update:jumpPageInput="(val: string) => { jumpPageInput.value = val }"
@jump-page="handleJumpPage"
/>
</view>
</view>
<!-- 侧边弹窗 (Drawer) -->
<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="drawer-title">添加分类</text>
<text class="close-btn" @click="closeDrawer">×</text>
</view>
<scroll-view class="drawer-body" :scroll-y="true">
<view class="form-item row">
<view class="label-box"><text class="label-txt">上级分类:</text></view>
<view class="input-box z-10" style="position: relative;">
<view class="select-mock" @click="toggleParentDropdown">
<text class="select-val">{{ formParentCategory }}</text>
<text class="arrow-down">▼</text>
</view>
<view v-if="parentDropdownVisible" class="dropdown-list">
<view
v-for="(cat, index) in parentCategoryList"
:key="index"
class="dropdown-item"
@click="selectParentCategory(cat)">
<text class="dropdown-txt">{{ cat }}</text>
</view>
</view>
</view>
</view>
<view class="form-item row">
<view class="label-box"><text class="required">*</text><text class="label-txt">分类名称:</text></view>
<view class="input-box">
<input class="input-base" v-model="formName" placeholder="请输入分类名称" />
</view>
</view>
<view class="form-item row align-start">
<view class="label-box pt-10"><text class="required">*</text><text class="label-txt">分类简介:</text></view>
<view class="input-box">
<textarea class="textarea-mini" v-model="formDesc" placeholder="请输入分类简介"></textarea>
</view>
</view>
<view class="form-item row">
<view class="label-box"><text class="label-txt">分类图片:</text></view>
<view class="input-box">
<view class="upload-btn" @click="handleUpload">
<view v-if="!formImage" class="img-icon">🖼️</view>
<text v-else style="font-size:12px;">已上传</text>
</view>
</view>
</view>
<view class="form-item row">
<view class="label-box"><text class="label-txt">排序:</text></view>
<view class="input-box">
<input class="input-base" type="number" v-model="formSort" />
</view>
</view>
<view class="form-item row">
<view class="label-box"><text class="label-txt">状态:</text></view>
<StatusSwitch v-model="formStatus" activeText="显示" inactiveText="隐藏" />
</view>
</scroll-view>
<view class="drawer-footer">
<view class="btn-cancel" @click="closeDrawer"><text class="cancel-txt">取消</text></view>
<view class="btn-confirm" @click="handleConfirm"><text class="confirm-txt">确定</text></view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, computed } from 'vue'
import StatusSwitch from '@/components/StatusSwitch.uvue'
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
const filterKeyword = ref('')
// ========== MOCK DATA START ==========
// TODO: 接真实接口时替换此处 categoryList 为 fetchCategoryList() 调用
const categoryList = ref([
{ id: '200', name: '购物心得', status: true },
{ id: '199', name: '消费文化', status: true },
{ id: '198', name: '品牌资讯', status: true },
{ id: '197', name: '新品上市', status: true },
{ id: '196', name: '生活方式', status: false },
{ id: '195', name: '健康养生', status: true },
{ id: '194', name: '科技数码', status: true },
{ id: '193', name: '家居家装', status: false },
{ id: '192', name: '美食美味', status: true },
{ id: '191', name: '旅行户外', status: true },
{ id: '190', name: '时尚穿搭', status: true },
{ id: '189', name: '亲子活动', status: false },
{ id: '188', name: '身心健康', status: true },
{ id: '187', name: '财经商业', status: true },
{ id: '186', name: '文化艺术', status: true },
{ id: '185', name: '社交情感', status: false },
{ id: '184', name: '工具技能', status: true },
{ id: '183', name: '玩具游戏', status: true },
{ id: '182', name: '守护联盟', status: true },
{ id: '181', name: '全部分类', status: true }
])
// ========== MOCK DATA END ==========
// ========== PAGINATION STATE ==========
const currentPage = ref(1)
const pageSize = ref(15)
const jumpPageInput = ref('')
const pageSizeOptions = [10, 15, 20, 30, 50]
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n: number) => `${n}条/页`))
const pageSizeIndex = computed(() => { const idx = pageSizeOptions.indexOf(pageSize.value); return idx >= 0 ? idx : 0 })
const total = computed(() => categoryList.value.length)
const totalPage = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
const pagedList = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
return categoryList.value.slice(start, start + pageSize.value)
})
const visiblePages = computed((): number[] => {
const t = totalPage.value; const cur = currentPage.value
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
return [1, -1, cur - 1, cur, cur + 1, -1, t]
})
const handlePageChange = (p: number) => { currentPage.value = p }
const handlePageSizeChange = (e: any) => {
const idx = Number(e.detail.value)
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
currentPage.value = 1
}
const handleJumpPage = () => {
const p = parseInt(jumpPageInput.value)
if (!isNaN(p) && p >= 1 && p <= totalPage.value) currentPage.value = p
}
// ========== END PAGINATION STATE ==========
const showDrawer = ref(false)
const isClosing = ref(false)
const formName = ref('')
const formDesc = ref('')
const formSort = ref(0)
const formStatus = ref(true)
const formParentCategory = ref('顶级分类')
const formImage = ref('')
const parentCategoryList = ref(['顶级分类', '购物心得', '消费文化', '品牌资讯'])
const parentDropdownVisible = ref(false)
const toggleParentDropdown = () => {
parentDropdownVisible.value = !parentDropdownVisible.value
}
const selectParentCategory = (val: string) => {
formParentCategory.value = val
parentDropdownVisible.value = false
}
const handleUpload = () => {
formImage.value = 'uploaded'
uni.showToast({ title: '模拟上传图片', icon: 'none' })
}
const handleAdd = () => {
formName.value = ''
formDesc.value = ''
formSort.value = 0
formStatus.value = true
formParentCategory.value = '顶级分类'
formImage.value = ''
isClosing.value = false
parentDropdownVisible.value = false
showDrawer.value = true
}
const handleEdit = (item: any) => {
formName.value = item.name
// 模拟填充其他字段
formDesc.value = '分类简介...'
formSort.value = 0
formStatus.value = item.status
formParentCategory.value = '顶级分类'
formImage.value = 'exists'
isClosing.value = false
parentDropdownVisible.value = false
showDrawer.value = true
}
const closeDrawer = () => {
isClosing.value = true
setTimeout(() => {
showDrawer.value = false
isClosing.value = false
}, 300)
}
const handleConfirm = () => {
if (formName.value.trim() == '') {
uni.showToast({ title: '请输入分类名称', icon: 'none' })
return
}
// 模拟保存逻辑
const newId = String(182 + categoryList.value.length)
categoryList.value.unshift({
id: newId,
name: formName.value,
status: formStatus.value
})
uni.showToast({ title: '添加成功', icon: 'success' })
closeDrawer()
}
const handleQuery = () => { console.log('Querying...') }
</script>
<style scoped lang="scss">
.admin-cms-category { padding: 0; background-color: transparent; min-height: auto; }
.border-shadow { background-color: #fff; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); }
.filter-card { padding: 20px; display: flex; flex-direction: row; align-items: center; gap: 30px; margin-bottom: 20px; }
.filter-item { display: flex; flex-direction: row; align-items: center; }
.label-txt { font-size: 14px; color: #606266; margin-right: 12px; }
.select-mock { width: 220px; height: 38px; border: 1px solid #dcdfe6; border-radius: 4px; display: flex; flex-direction: row; align-items: center; justify-content: space-between; padding: 0 15px; }
.select-val { font-size: 13px; color: #c0c4cc; }
.arrow-down { font-size: 10px; color: #999; }
.search-input { width: 220px; height: 38px; border: 1px solid #dcdfe6; border-radius: 4px; padding: 0 15px; font-size: 13px; }
.btn-query { width: 76px; height: 34px; background-color: #2d8cf0; border-radius: 4px; display: flex; align-items: center; justify-content: center; }
.query-txt { color: #fff; font-size: 14px; }
.table-card { display: flex; flex-direction: column; }
.card-header { padding: 20px; }
.btn-primary { width: 140px; height: 36px; background-color: #2d8cf0; border-radius: 4px; display: flex; align-items: center; justify-content: center; }
.btn-txt { color: #fff; font-size: 13px; }
.table-header { height: 50px; background-color: #eaf2ff; display: flex; flex-direction: row; align-items: center; }
.th { padding: 0 15px; }
.th-txt { font-size: 13px; color: #606266; font-weight: bold; }
.table-row { height: 60px; display: flex; flex-direction: row; align-items: center; border-bottom: 1px solid #f0f0f0; }
.td { padding: 0 15px; }
.td-txt { font-size: 13px; color: #606266; }
.col-id { width: 80px; justify-content: center; }
.col-name { flex: 1; justify-content: flex-start; }
.col-img { flex: 1; justify-content: center; }
.col-status { width: 120px; justify-content: center; }
.col-op { width: 220px; justify-content: center; }
.img-box-placeholder { width: 40px; height: 40px; background-color: #f5f7fa; border-radius: 4px; }
.op-links { display: flex; flex-direction: row; align-items: center; }
.link-txt { font-size: 13px; color: #2d8cf0; cursor: pointer; }
.danger { color: #ed4014; }
.divider { width: 1px; height: 12px; background-color: #e8eaec; margin: 0 10px; }
.drawer-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.4); z-index: 2000; transition: opacity 0.3s; }
.mask-fade-out { opacity: 0; }
.drawer-content { position: absolute; top: 0; right: 0; width: 50%; height: 100%; background-color: #fff; display: flex; flex-direction: column; box-shadow: -2px 0 12px rgba(0, 0, 0, 0.2); animation: slideIn 0.3s ease; }
.slide-out { animation: slideOut 0.3s ease forwards; }
@keyframes slideIn { from { transform: translateX(100%); } to { transform: translateX(0); } }
@keyframes slideOut { from { transform: translateX(0); } to { transform: translateX(100%); } }
.drawer-header { height: 60px; padding: 0 24px; border-bottom: 1px solid #f0f0f0; display: flex; flex-direction: row; justify-content: space-between; align-items: center; }
.drawer-title { font-size: 16px; font-weight: bold; color: #333; }
.close-btn { font-size: 24px; color: #999; cursor: pointer; padding: 5px; }
.drawer-body { flex: 1; padding: 24px 30px; }
.form-item { margin-bottom: 24px; display: flex; flex-direction: row; align-items: center; }
.label-box { width: 120px; display: flex; justify-content: flex-end; align-items: center; margin-right: 20px; flex-shrink: 0; }
.required { color: #ed4014; margin-right: 4px; }
.label-txt { font-size: 14px; color: #606266; text-align: right; }
.input-box { flex: 1; }
.input-base { width: 100%; height: 38px; border: 1px solid #dcdee2; border-radius: 4px; padding: 0 12px; font-size: 14px; }
.dropdown-list {
position: absolute;
top: 40px;
left: 0;
width: 100%;
background-color: #fff;
border: 1px solid #dcdee2;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
z-index: 100;
max-height: 200px;
overflow-y: auto;
}
.dropdown-item {
padding: 10px 15px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
}
.dropdown-item:last-child {
border-bottom: none;
}
.dropdown-item:hover {
background-color: #f5f7fa;
}
.dropdown-txt {
font-size: 14px;
color: #606266;
}
.textarea-mini { width: 100%; height: 80px; border: 1px solid #dcdee2; border-radius: 4px; padding: 10px 12px; font-size: 14px; }
.upload-btn {
width: 70px;
height: 70px;
border: 1px solid #dcdee2;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.img-icon { font-size: 30px; color: #ccc; }
.drawer-footer { height: 60px; border-top: 1px solid #f0f0f0; display: flex; flex-direction: row; justify-content: flex-end; align-items: center; padding: 0 24px; }
.btn-cancel { padding: 8px 20px; border: 1px solid #dcdee2; border-radius: 4px; margin-right: 15px; }
.btn-confirm { padding: 8px 20px; background-color: #2d8cf0; border-radius: 4px; }
.cancel-txt { font-size: 14px; color: #606266; }
.confirm-txt { font-size: 14px; color: #fff; }
</style>