Files
medical-mall/pages/mall/admin/product/classify.uvue

729 lines
14 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-main">
<!-- 头部搜索和操作 -->
<view class="search-card">
<view class="search-row">
<view class="search-item">
<text class="search-label">分类名称:</text>
<input class="search-input" placeholder="请输入分类名称" />
</view>
<view class="search-item">
<text class="search-label">状态:</text>
<view class="mock-select">
<text>全部</text>
<text class="arrow-down"></text>
</view>
</view>
<button class="btn-query">查询</button>
<button class="btn-reset">重置</button>
</view>
</view>
<!-- 数据表格区域 -->
<view class="table-card">
<view class="table-toolbar">
<button class="btn-add" @click="openDrawer()">添加商品分类</button>
</view>
<view class="table-header">
<text class="th-cell flex-1">ID</text>
<text class="th-cell flex-3">分类名称</text>
<text class="th-cell flex-2">分类图标</text>
<text class="th-cell flex-1">排序</text>
<text class="th-cell flex-2 text-center">状态</text>
<text class="th-cell flex-3 text-center">操作</text>
</view>
<view class="table-body">
<view v-for="(item, index) in list" :key="index" class="table-row-group">
<!-- 父级 -->
<view class="table-row">
<text class="td-cell flex-1 color-9">{{ item.id }}</text>
<view class="td-cell flex-3 row-layout">
<text class="expand-icon" @click="item.expanded = !item.expanded">{{ item.expanded ? '' : '' }}</text>
<text>{{ item.name }}</text>
</view>
<view class="td-cell flex-2">
<image class="cate-icon" :src="item.icon" mode="aspectFit"></image>
</view>
<text class="td-cell flex-1">{{ item.sort }}</text>
<view class="td-cell flex-2 row-center">
<view class="switch-mock" :class="item.status ? 'switch-on' : ''" @click="toggleStatus(item)">
<view class="switch-dot"></view>
<text class="switch-text">{{ item.status ? '开启' : '关闭' }}</text>
</view>
</view>
<view class="td-cell flex-3 row-center">
<text class="btn-link" @click="openDrawer(item)">编辑</text>
<view class="divider"></view>
<text class="btn-link delete" @click="deleteItem(item)">删除</text>
</view>
</view>
<!-- 子级 -->
<view v-if="item.expanded">
<view v-for="(child, cIndex) in item.children" :key="cIndex" class="table-row sub-row">
<text class="td-cell flex-1 color-9">{{ child.id }}</text>
<view class="td-cell flex-3 row-layout pl-20">
<text class="child-line"></text>
<text>{{ child.name }}</text>
</view>
<view class="td-cell flex-2">
<image class="cate-icon" :src="child.icon" mode="aspectFit"></image>
</view>
<text class="td-cell flex-1">{{ child.sort }}</text>
<view class="td-cell flex-2 row-center">
<view class="switch-mock" :class="child.status ? 'switch-on' : ''" @click="toggleStatus(child)">
<view class="switch-dot"></view>
<text class="switch-text">{{ child.status ? '开启' : '关闭' }}</text>
</view>
</view>
<view class="td-cell flex-3 row-center">
<text class="btn-link" @click="openDrawer(child)">编辑</text>
<view class="divider"></view>
<text class="btn-link delete" @click="deleteItem(child)">删除</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 添加/编辑抽屉 (50% 宽度,贴右侧展示) -->
<view class="drawer-mask" v-if="showDrawerMask" @click="closeDrawer">
<view class="drawer-content" @click.stop="" :class="{ 'drawer-show': showDrawer }">
<view class="drawer-header">
<text class="drawer-title">{{ isEdit ? '编辑分类' : '添加分类' }}</text>
<text class="drawer-close" @click="closeDrawer"></text>
</view>
<scroll-view class="drawer-body" scroll-y="true">
<view class="drawer-form">
<view class="form-item">
<view class="form-label-box"><text class="form-label">上级分类:</text></view>
<view class="form-input-box">
<view class="mock-select-full">
<text>{{ form.parentName || '顶级分类' }}</text>
<text class="arrow-down"></text>
</view>
</view>
</view>
<view class="form-item">
<view class="form-label-box"><text class="form-label required">分类名称:</text></view>
<view class="form-input-box">
<input class="drawer-input" v-model="form.name" placeholder="请输入分类名称" />
</view>
</view>
<view class="form-item align-start">
<view class="form-label-box"><text class="form-label">分类图标:</text></view>
<view class="form-input-box">
<view class="upload-box">
<text class="plus">+</text>
<text class="upload-text">上传图片</text>
</view>
<text class="form-tip">建议尺寸180*180</text>
</view>
</view>
<view class="form-item">
<view class="form-label-box"><text class="form-label">排序:</text></view>
<view class="form-input-box">
<input type="number" class="drawer-input" v-model="form.sort" />
</view>
</view>
<view class="form-item">
<view class="form-label-box"><text class="form-label">状态:</text></view>
<view class="form-input-box">
<view class="switch-mock" :class="form.status ? 'switch-on' : ''" @click="form.status = !form.status">
<view class="switch-dot"></view>
<text class="switch-text">{{ form.status ? '开启' : '关闭' }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="drawer-footer">
<button class="btn-footer-cancel" @click="closeDrawer">取消</button>
<button class="btn-footer-submit" @click="saveCate">确定</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, onMounted } from 'vue'
import {
fetchAdminCategoryList,
createAdminCategory,
updateAdminCategory,
deleteAdminCategory,
type AdminCategory
} from '@/services/admin/productCategoryService.uts'
interface CateItem {
id: string;
name: string;
icon: string;
sort: number;
status: boolean;
expanded?: boolean;
children?: CateItem[];
parentId?: string | null;
}
const list = ref<Array<CateItem>>([])
const showDrawerMask = ref(false)
const showDrawer = ref(false)
const isEdit = ref(false)
const editingId = ref<string | null>(null)
const form = reactive({
name: '',
parentId: null as string | null,
parentName: '',
sort: 0,
status: true
})
onMounted(() => {
loadList()
})
function buildTree(items: Array<AdminCategory>): Array<CateItem> {
const map: Record<string, CateItem> = {}
const roots: Array<CateItem> = []
for (let i = 0; i < items.length; i++) {
const c = items[i]
map[c.id] = {
id: c.id,
name: c.name,
icon: c.icon ?? '',
sort: c.sort ?? 0,
status: c.is_active === true,
expanded: false,
children: [],
parentId: c.parent_id ?? null
}
}
const ids = Object.keys(map)
for (let i = 0; i < ids.length; i++) {
const id = ids[i]
const node = map[id]
if (node.parentId != null && map[node.parentId] != null) {
map[node.parentId].children = map[node.parentId].children ?? []
map[node.parentId].children!.push(node)
} else {
roots.push(node)
}
}
return roots
}
async function loadList() {
const items = await fetchAdminCategoryList({})
list.value = buildTree(items)
}
function openDrawer(item: CateItem | null = null) {
if (item != null) {
isEdit.value = true
editingId.value = item.id
form.name = item.name
form.sort = item.sort
form.status = item.status
form.parentId = item.parentId ?? null
form.parentName = item.parentId != null ? '子分类' : '顶级分类'
} else {
isEdit.value = false
editingId.value = null
form.name = ''
form.sort = 0
form.status = true
form.parentId = null
form.parentName = '顶级分类'
}
showDrawerMask.value = true
setTimeout(() => {
showDrawer.value = true
}, 50)
}
function closeDrawer() {
showDrawer.value = false
setTimeout(() => {
showDrawerMask.value = false
}, 300)
}
async function saveCate() {
if (isEdit.value && editingId.value != null) {
await updateAdminCategory({
id: editingId.value,
parentId: form.parentId,
name: form.name,
sortOrder: form.sort,
isActive: form.status
})
} else {
await createAdminCategory({
parentId: form.parentId,
name: form.name,
sortOrder: form.sort,
isActive: form.status
})
}
uni.showToast({ title: '保存成功', icon: 'success' })
closeDrawer()
loadList()
}
async function toggleStatus(item: CateItem) {
await updateAdminCategory({
id: item.id,
parentId: item.parentId,
name: item.name,
sortOrder: item.sort,
isActive: !item.status
})
item.status = !item.status
}
async function deleteItem(item: CateItem) {
uni.showModal({
title: '提示',
content: '确定删除该分类吗?',
success: async (res) => {
if (res.confirm) {
await deleteAdminCategory(item.id)
uni.showToast({ title: '删除成功', icon: 'success' })
loadList()
}
}
})
}
</script>
<style scoped lang="scss">
.admin-main {
padding: 24px;
background-color: #f0f2f5;
min-height: 100vh;
}
.search-card {
background-color: #fff;
padding: 20px;
border-radius: 4px;
margin-bottom: 20px;
}
.search-row {
display: flex;
flex-direction: row;
align-items: center;
}
.search-item {
display: flex;
flex-direction: row;
align-items: center;
margin-right: 24px;
}
.search-label {
font-size: 14px;
color: #333;
margin-right: 8px;
}
.search-input {
width: 200px;
height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 12px;
font-size: 14px;
}
.mock-select {
width: 150px;
height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0 12px;
background-color: #fff;
}
.arrow-down {
font-size: 10px;
color: #c0c4cc;
}
.btn-query {
background-color: #1890ff;
color: #fff;
height: 32px;
line-height: 32px;
padding: 0 16px;
border-radius: 4px;
font-size: 14px;
border: none;
margin-right: 12px;
}
.btn-reset {
background-color: #fff;
border: 1px solid #dcdfe6;
color: #666;
height: 32px;
line-height: 32px;
padding: 0 16px;
border-radius: 4px;
font-size: 14px;
margin-right: 0;
}
.table-card {
background-color: #fff;
padding: 24px;
border-radius: 4px;
}
.table-toolbar {
margin-bottom: 20px;
}
.btn-add {
background-color: #1890ff;
color: #fff;
height: 32px;
line-height: 32px;
padding: 0 16px;
border-radius: 4px;
font-size: 14px;
border: none;
margin: 0;
}
.table-header {
display: flex;
flex-direction: row;
background-color: #fafafa;
height: 44px;
align-items: center;
border: 1px solid #f0f0f0;
}
.th-cell {
font-size: 14px;
font-weight: bold;
color: #333;
padding: 0 12px;
}
.table-row-group {
border-bottom: 1px solid #f0f0f0;
}
.table-row {
display: flex;
flex-direction: row;
height: 60px;
align-items: center;
border-left: 1px solid #f0f0f0;
border-right: 1px solid #f0f0f0;
}
.sub-row {
background-color: #fafafa;
}
.td-cell {
padding: 0 12px;
font-size: 14px;
color: #666;
}
.color-9 {
color: #999;
}
.pl-20 {
padding-left: 30px;
}
.child-line {
color: #ccc;
margin-right: 5px;
}
.cate-icon {
width: 40px;
height: 40px;
border-radius: 4px;
}
.expand-icon {
font-size: 10px;
color: #999;
margin-right: 8px;
}
.btn-link {
font-size: 14px;
color: #1890ff;
cursor: pointer;
}
.btn-link.delete {
color: #ff4d4f;
}
.divider {
width: 1px;
height: 14px;
background-color: #f0f0f0;
margin: 0 12px;
}
.text-center {
text-align: center;
}
.row-layout {
display: flex;
flex-direction: row;
align-items: center;
}
.row-center {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
/* Switch Mock */
.switch-mock {
width: 54px;
height: 24px;
background-color: #ccc;
border-radius: 12px;
position: relative;
transition: all 0.3s;
display: flex;
align-items: center;
padding: 0 6px;
}
.switch-on {
background-color: #1890ff;
}
.switch-dot {
width: 18px;
height: 18px;
background-color: #fff;
border-radius: 50%;
position: absolute;
left: 3px;
transition: all 0.3s;
}
.switch-on .switch-dot {
left: 33px;
}
.switch-text {
font-size: 12px;
color: #fff;
margin-left: auto;
}
.switch-on .switch-text {
margin-left: 0;
margin-right: auto;
}
/* Drawer styles */
.drawer-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
background-color: rgba(0,0,0,0.5);
}
.drawer-content {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 50%;
background-color: #fff;
transform: translateX(100%);
transition: transform 0.3s ease-in-out;
display: flex;
flex-direction: column;
}
.drawer-show {
transform: translateX(0);
}
.drawer-header {
padding: 20px 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;
}
.drawer-close {
font-size: 20px;
color: #999;
cursor: pointer;
}
.drawer-body {
flex: 1;
padding: 24px;
}
.drawer-form {
display: flex;
flex-direction: column;
}
.form-item {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 24px;
}
.align-start {
align-items: flex-start;
}
.form-label-box {
width: 100px;
text-align: right;
margin-right: 16px;
}
.form-label {
font-size: 14px;
color: #606266;
}
.required::before {
content: '*';
color: #ff4d4f;
margin-right: 4px;
}
.form-input-box {
flex: 1;
}
.drawer-input {
width: 100%;
height: 36px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 12px;
font-size: 14px;
}
.mock-select-full {
width: 100%;
height: 36px;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0 12px;
background-color: #fff;
}
.upload-box {
width: 100px;
height: 100px;
border: 1px dashed #dcdfe6;
border-radius: 4px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: pointer;
}
.plus {
font-size: 24px;
color: #909399;
}
.upload-text {
font-size: 12px;
color: #909399;
}
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 8px;
}
.drawer-footer {
padding: 16px 24px;
border-top: 1px solid #f0f0f0;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.btn-footer-cancel,
.btn-footer-submit {
height: 32px;
line-height: 32px;
padding: 0 20px;
border-radius: 4px;
font-size: 14px;
margin-left: 12px;
}
.btn-footer-cancel {
background-color: #fff;
border: 1px solid #dcdfe6;
color: #606266;
}
.btn-footer-submit {
background-color: #1890ff;
color: #fff;
border: none;
}
.flex-1 { flex: 1; }
.flex-2 { flex: 2; }
.flex-3 { flex: 3; }
</style>