Files
medical-mall/pages/mall/admin/cms/article/list.uvue
2026-02-26 09:50:58 +08:00

490 lines
22 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-article">
<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">{{ filterCategory }}</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-img"><text class="th-txt">文章图片</text></view>
<view class="th col-name"><text class="th-txt">文章名称</text></view>
<view class="th col-cat"><text class="th-txt">文章分类</text></view>
<view class="th col-link"><text class="th-txt">关联商品</text></view>
<view class="th col-v"><text class="th-txt">浏览量</text></view>
<view class="th col-time"><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 articleList" :key="item.id">
<view class="td col-id"><text class="td-txt">{{ item.id }}</text></view>
<view class="td col-img">
<view class="img-box"><text class="img-placeholder">🖼️</text></view>
</view>
<view class="td col-name"><text class="td-txt">{{ item.name }}</text></view>
<view class="td col-cat"><text class="td-txt">{{ item.categoryName }}</text></view>
<view class="td col-link"><text class="td-txt">{{ item.linkedProduct }}</text></view>
<view class="td col-v"><text class="td-txt">{{ item.views }}</text></view>
<view class="td col-time"><text class="td-txt">{{ item.time }}</text></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">关联</text>
<view class="divider"></view>
<text class="link-txt danger">删除</text>
<view class="divider"></view>
<text class="link-txt">复制链接</text>
<text class="arrow-small">▼</text>
</view>
</view>
</view>
</view>
<view class="pagination-bar">
<view class="page-info">
<text class="page-total">共 {{ articleList.length }} 条</text>
</view>
</view>
</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">
<view class="tab-item active">
<text class="tab-txt">文章信息</text>
<view class="tab-line"></view>
</view>
<text class="close-btn" @click="closeDrawer">×</text>
</view>
<scroll-view class="drawer-body" :scroll-y="true">
<view class="form-grid">
<view class="form-row">
<view class="form-col">
<view class="label-box"><text class="required">*</text><text class="label-txt">标题:</text></view>
<view class="input-box">
<input class="input-base" v-model="formTitle" placeholder="请输入" />
<text class="input-count">{{ formTitle.length }}/80</text>
</view>
</view>
<view class="form-col">
<view class="label-box"><text class="label-txt">作者:</text></view>
<view class="input-box">
<input class="input-base" v-model="formAuthor" placeholder="请输入" />
<text class="input-count">{{ formAuthor.length }}/10</text>
</view>
</view>
</view>
<view class="form-row mt-20" style="position: relative; z-index: 10;">
<view class="form-col" style="position: relative; z-index: 11;">
<view class="label-box"><text class="required">*</text><text class="label-txt">文章分类:</text></view>
<view class="input-box z-20" style="position: relative;">
<view class="select-mock" @click.stop="toggleDropdown">
<text class="select-val">{{ formCategory || '请选择' }}</text>
<text class="arrow-down">▼</text>
</view>
<view v-if="dropdownVisible" class="dropdown-list">
<view
v-for="(cat, index) in categoryList"
:key="index"
class="dropdown-item"
@click="selectCategory(cat)">
<text class="dropdown-txt">{{ cat }}</text>
</view>
</view>
</view>
</view>
<view class="form-col">
<view class="label-box"><text class="label-txt">文章简介:</text></view>
<view class="input-box">
<textarea class="textarea-base" v-model="formIntro" placeholder="请输入"></textarea>
<text class="input-count">{{ formIntro.length }}/300</text>
</view>
</view>
</view>
<view class="form-row mt-20">
<view class="form-col full">
<view class="label-box"><text class="required">*</text><text class="label-txt">图文封面:</text></view>
<view class="upload-container" @click="handleUploadCover">
<view class="upload-btn">
<text class="plus-icon">+</text>
</view>
<text class="tip-txt mt-10">建议尺寸500 x 312 px</text>
</view>
</view>
</view>
</view>
<!-- 文章内容区块 -->
<view class="section-title mt-40">
<view class="title-inner active">
<text class="title-txt">文章内容</text>
<view class="title-line"></view>
</view>
</view>
<view class="editor-section">
<view class="label-box mb-10"><text class="required">*</text><text class="label-txt">文章内容:</text></view>
<view class="rich-editor-mock">
<view class="editor-toolbar">
<text class="tool-ic" @click="formatText('bold')">B</text>
<text class="tool-ic" @click="formatText('italic')">I</text>
<text class="tool-ic" @click="formatText('underline')">U</text>
<text class="tool-ic" @click="insertImage">🖼️</text>
</view>
<view class="editor-content-container">
<editor id="editor" class="editor-instance" placeholder="在此输入文章内容..." @ready="onEditorReady"></editor>
</view>
</view>
</view>
<!-- 其他设置 -->
<view class="section-title mt-40">
<view class="title-inner active">
<text class="title-txt">其他设置</text>
<view class="title-line"></view>
</view>
</view>
<view class="settings-section">
<view class="form-item">
<view class="label-box-wide"><text class="label-txt">banner显示:</text></view>
<view class="radio-group">
<view class="radio-item" @click="formBanner = true">
<view :class="['radio-circle', formBanner ? 'checked' : '']"><view v-if="formBanner" class="radio-in"></view></view>
<text class="radio-la">显示</text>
</view>
<view class="radio-item" @click="formBanner = false">
<view :class="['radio-circle', !formBanner ? 'checked' : '']"><view v-if="!formBanner" class="radio-in"></view></view>
<text class="radio-la">不显示</text>
</view>
</view>
</view>
<view class="form-item mt-20">
<view class="label-box-wide"><text class="label-txt">热门文章:</text></view>
<view class="radio-group">
<view class="radio-item" @click="formHot = true">
<view :class="['radio-circle', formHot ? 'checked' : '']"><view v-if="formHot" class="radio-in"></view></view>
<text class="radio-la">显示</text>
</view>
<view class="radio-item" @click="formHot = false">
<view :class="['radio-circle', !formHot ? 'checked' : '']"><view v-if="!formHot" class="radio-in"></view></view>
<text class="radio-la">不显示</text>
</view>
</view>
</view>
</view>
<view class="submit-container mt-40">
<view class="btn-submit" @click="handleConfirm">
<text class="submit-txt">提交</text>
</view>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref } from 'vue'
const filterCategory = ref('全部')
const filterKeyword = ref('')
const articleList = ref([
{ id: '240', name: '赋能消费 | 卷狗优选迈向文化消费新时代', categoryName:'消费文化', linkedProduct: '', views: '3349', time: '2025-04-01 16:34' },
{ id: '237', name: '把重要的日子放在桌面', categoryName:'品牌资讯', linkedProduct: '2024新款吹风机...', views: '260', time: '2025-04-01 16:32' }
])
const categoryList = ref(['购物心得', '消费文化', '品牌资讯', '全部']) // Mock data mirroring categories
const dropdownVisible = ref(false)
const showDrawer = ref(false)
const isClosing = ref(false)
const formTitle = ref('')
const formAuthor = ref('')
const formCategory = ref('')
const formIntro = ref('')
const formContent = ref('')
const formBanner = ref(false)
const formHot = ref(false)
const toggleDropdown = () => {
dropdownVisible.value = !dropdownVisible.value
}
const selectCategory = (cat: string) => {
formCategory.value = cat
dropdownVisible.value = false
}
let editorCtx: any = null
const onEditorReady = () => {
// @ts-ignore
uni.createSelectorQuery().select('#editor').context((res) => {
editorCtx = res.context
}).exec()
}
const formatText = (name: string) => {
if (editorCtx) {
editorCtx.format(name)
}
}
const insertImage = () => {
uni.chooseImage({
count: 1,
success: (res) => {
if (editorCtx) {
editorCtx.insertImage({
src: res.tempFilePaths[0],
alt: '图像'
})
}
}
})
}
const handleAdd = () => {
formTitle.value = ''
formAuthor.value = ''
formCategory.value = ''
formIntro.value = ''
formContent.value = ''
formBanner.value = false
formHot.value = false
isClosing.value = false
dropdownVisible.value = false
showDrawer.value = true
}
const handleEdit = (item: any) => {
formTitle.value = item.name
formAuthor.value = '管理员'
formCategory.value = item.categoryName
formIntro.value = '这是一段文章简介...'
formContent.value = '<p>这是一段富文本内容...</p>'
formBanner.value = false
formHot.value = true
isClosing.value = false
dropdownVisible.value = false
showDrawer.value = true
}
const closeDrawer = () => {
isClosing.value = true
setTimeout(() => {
showDrawer.value = false
isClosing.value = false
}, 300)
}
const handleUploadCover = () => {
uni.chooseImage({
count: 1,
success: (res) => {
// 在实际应用中这里应该上传到服务器
// 这里简单模拟展示
uni.showToast({ title: '上传成功', icon: 'success' })
// formCover.value = res.tempFilePaths[0]
}
})
}
const handleConfirm = () => {
if (formTitle.value.trim() == '') {
uni.showToast({ title: '请输入标题', icon: 'none' })
return
}
// 模拟保存逻辑
const newId = String(241 + articleList.value.length)
articleList.value.unshift({
id: newId,
name: formTitle.value,
categoryName: formCategory.value || '未分类',
linkedProduct: '',
views: '0',
time: '2026-02-26 12:00'
})
uni.showToast({ title: '发布成功', icon: 'success' })
closeDrawer()
}
const handleQuery = () => { console.log('Querying...') }
</script>
<style scoped lang="scss">
.admin-cms-article { 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: #333; }
.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: 100px; 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: 80px; display: flex; flex-direction: row; align-items: center; border-bottom: 1px solid #f0f0f0; }
.td { padding: 0 15px; }
.td-txt { font-size: 13px; color: #606266; }
.img-box { width: 60px; height: 40px; background-color: #f5f7fa; border-radius: 4px; display: flex; align-items: center; justify-content: center; }
.img-placeholder { font-size: 20px; }
.col-id { width: 80px; justify-content: center; }
.col-img { width: 100px; }
.col-name { flex: 2; }
.col-cat { width: 120px; }
.col-link { flex: 2; }
.col-v { width: 100px; justify-content: center; }
.col-time { width: 180px; justify-content: center; }
.col-op { width: 220px; }
.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 8px; }
.arrow-small { font-size: 8px; color: #2d8cf0; margin-left: 2px; }
.pagination-bar { padding: 20px; display: flex; flex-direction: row; align-items: center; justify-content: flex-end; }
.page-info { display: flex; flex-direction: row; align-items: center; }
.page-total { font-size: 13px; color: #606266; margin-right: 15px; }
.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: 54px; padding: 0 20px; border-bottom: 1px solid #f0f0f0; display: flex; flex-direction: row; justify-content: space-between; align-items: center; }
.tab-item { height: 100%; display: flex; align-items: center; position: relative; }
.tab-txt { font-size: 15px; color: #2d8cf0; font-weight: 500; }
.tab-line { position: absolute; bottom: 0; left: 0; right: 0; height: 2px; background-color: #2d8cf0; }
.close-btn { font-size: 24px; color: #999; cursor: pointer; }
.drawer-body { flex: 1; padding: 24px; background-color: #fff; }
.section-title { height: 44px; border-bottom: 1px solid #f0f0f0; margin-bottom: 24px; display: flex; flex-direction: row; }
.title-inner { height: 100%; display: flex; align-items: center; position: relative; padding: 0 10px; }
.title-txt { font-size: 15px; color: #333; font-weight: 500; }
.title-inner.active .title-txt { color: #2d8cf0; }
.title-line { position: absolute; bottom: -1px; left: 0; right: 0; height: 2px; background-color: #2d8cf0; display: none; }
.title-inner.active .title-line { display: block; }
.form-grid { display: flex; flex-direction: column; }
.form-row { display: flex; flex-direction: row; gap: 30px; }
.form-col { flex: 1; display: flex; flex-direction: row; align-items: flex-start; }
.form-col.full { flex: none; width: 100%; }
.label-box { width: 100px; display: flex; flex-direction: row; justify-content: flex-end; margin-right: 15px; padding-top: 8px; flex-shrink: 0; }
.label-box-wide { width: 120px; display: flex; flex-direction: row; justify-content: flex-end; margin-right: 15px; flex-shrink: 0; }
.required { color: #ed4014; margin-right: 4px; }
.label-txt { font-size: 14px; color: #606266; }
.input-box { flex: 1; position: relative; }
.z-20 { z-index: 20; }
.input-base { width: 100%; height: 38px; border: 1px solid #dcdee2; border-radius: 4px; padding: 0 45px 0 12px; font-size: 14px; }
.textarea-base { width: 100%; height: 100px; border: 1px solid #dcdee2; border-radius: 4px; padding: 10px 12px; font-size: 14px; }
.input-count { position: absolute; bottom: 8px; right: 12px; font-size: 12px; color: #999; }
.upload-container { display: flex; flex-direction: column; }
.upload-btn { width: 80px; height: 80px; border: 1px dashed #dcdee2; border-radius: 4px; display: flex; align-items: center; justify-content: center; background-color: #f8f8f9; }
.plus-icon { font-size: 30px; color: #999; }
.tip-txt { font-size: 12px; color: #999; }
.editor-section { display: flex; flex-direction: column; }
.rich-editor-mock { border: 1px solid #dcdee2; border-radius: 4px; min-height: 400px; display: flex; flex-direction: column; }
.editor-toolbar { height: 44px; background-color: #fafafa; border-bottom: 1px solid #dcdee2; display: flex; flex-direction: row; align-items: center; padding: 0 15px; gap: 20px; flex-wrap: wrap; }
.tool-ic { font-size: 16px; color: #515a6e; cursor: pointer; padding: 5px 10px; border: 1px solid #eee; border-radius: 4px; }
.tool-ic:active { background-color: #f0f0f0; }
.editor-content-container { flex: 1; background-color: #fff; padding: 15px; }
.editor-instance { width: 100%; height: 350px; font-size: 14px; }
.settings-section { display: flex; flex-direction: column; }
.form-item { display: flex; flex-direction: row; align-items: center; }
.radio-group { display: flex; flex-direction: row; gap: 30px; }
.radio-item { display: flex; flex-direction: row; align-items: center; cursor: pointer; }
.radio-circle { width: 16px; height: 16px; border: 1px solid #dcdee2; border-radius: 50%; margin-right: 8px; display: flex; align-items: center; justify-content: center; }
.radio-circle.checked { border-color: #2d8cf0; }
.radio-in { width: 8px; height: 8px; background-color: #2d8cf0; border-radius: 50%; }
.radio-la { font-size: 14px; color: #606266; }
.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;
}
.submit-container { display: flex; justify-content: flex-start; padding-left: 135px; padding-bottom: 50px; }
.btn-submit { width: 120px; height: 40px; background-color: #2d8cf0; border-radius: 4px; display: flex; align-items: center; justify-content: center; cursor: pointer; }
.submit-txt { color: #fff; font-size: 14px; }
.drawer-footer { height: 60px; border-top: 1px solid #f0f0f0; display: flex; flex-direction: row; justify-content: flex-end; align-items: center; padding: 0 24px; background-color: #fff; }
.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; }
.mt-20 { margin-top: 20px; }
.mt-40 { margin-top: 40px; }
.mt-10 { margin-top: 10px; }
.mb-10 { margin-bottom: 10px; }
</style>