Files
medical-mall/pages/mall/admin/product/protection/index.uvue

458 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="alert-info-box">
<text class="alert-info-txt">商品保障可在商品详情页展示,提升用户购买意愿。</text>
</view>
<!-- 数据表格区域 -->
<view class="table-card">
<view class="table-toolbar">
<button class="btn-primary-add" @click="openModal()">添加商品保障</button>
</view>
<view class="table-header-pane">
<view class="th flex-1">ID</view>
<view class="th flex-2">图标</view>
<view class="th flex-4">服务条款名称</view>
<view class="th flex-5">服务描述</view>
<view class="th flex-2 text-center">状态</view>
<view class="th flex-2 text-center">操作</view>
</view>
<view class="table-body">
<view v-if="list.length === 0" class="empty-box">
<text class="empty-text">暂无数据</text>
</view>
<view v-for="(item, index) in pagedList" :key="index" class="table-row-item">
<text class="td flex-1 color-9">{{ item.id }}</text>
<view class="td flex-2">
<image class="protection-icon-img" :src="item.icon" mode="aspectFit"></image>
</view>
<text class="td flex-4">{{ item.name }}</text>
<text class="td flex-5 color-6">{{ item.desc }}</text>
<view class="td flex-2 row-center">
<StatusSwitch v-model="item.status" activeText="显示" inactiveText="隐藏" />
</view>
<view class="td flex-2 row-center">
<text class="btn-action-blue" @click="openModal(item)">编辑</text>
<view class="v-divider-line"></view>
<text class="btn-action-red" @click="deleteItem(index)">删除</text>
</view>
</view>
</view>
</view>
<CommonPagination
v-if="true"
: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"
/>
<!-- 添加/编辑弹窗 (居中 Modal) -->
<view class="modal-overlay" v-if="showModal" @click="closeModal">
<view class="modal-main-pane" @click.stop>
<view class="modal-header-box">
<text class="modal-title-txt">添加保障</text>
<text class="modal-close-icon" @click="closeModal">×</text>
</view>
<view class="modal-body-form">
<view class="form-item-box">
<view class="label-box"><text class="form-label font-star">保障名称:</text></view>
<view class="val-box">
<input class="input-ctrl" v-model="form.name" placeholder="请输入保障名称" />
</view>
</view>
<view class="form-item-box row-align-start">
<view class="label-box"><text class="form-label font-star">保障内容:</text></view>
<view class="val-box">
<textarea class="textarea-ctrl" v-model="form.desc" placeholder="请输入保障内容"></textarea>
</view>
</view>
<view class="form-item-box">
<view class="label-box"><text class="form-label">图标:</text></view>
<view class="val-box">
<view class="icon-upload-placeholder" @click="mockIconPicker">
<image v-if="form.icon" :src="form.icon" class="icon-preview-img"></image>
<image v-else src="/static/logo.png" class="icon-empty-img" mode="aspectFit"></image>
</view>
</view>
</view>
<view class="form-item-box">
<view class="label-box"><text class="form-label">排序:</text></view>
<view class="val-box">
<input type="number" class="input-ctrl" v-model="form.sort" />
</view>
</view>
<view class="form-item-box">
<view class="label-box"><text class="form-label">是否显示:</text></view>
<view class="val-box row-center-start">
<view class="radio-item" @click="form.status = true">
<view class="radio-circle" :class="form.status ? 'radio-checked' : ''"></view>
<text class="radio-txt">显示</text>
</view>
<view class="radio-item" @click="form.status = false">
<view class="radio-circle" :class="!form.status ? 'radio-checked' : ''"></view>
<text class="radio-txt">隐藏</text>
</view>
</view>
</view>
</view>
<view class="modal-footer-box">
<button class="btn-foot-cancel" @click="closeModal">取消</button>
<button class="btn-foot-submit" @click="saveProtection">确定</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, computed } from 'vue'
import StatusSwitch from '@/components/StatusSwitch.uvue'
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
interface ProtectionItem {
id: number;
name: string;
icon: string;
desc: string;
status: boolean;
sort: number;
}
// ========== MOCK DATA START ==========
// TODO: 接真实接口时替换此处 list 为 fetchProtectionList() 调用
const list = reactive<ProtectionItem[]>([
{ id: 1, name: '正品保障', icon: '/static/logo.png', desc: '该商品由平台认证,保证百分百正品。', status: true, sort: 0 },
{ id: 2, name: '七天无理由', icon: '/static/logo.png', desc: '商品在不影响二次销售的情况下,支持 7 天无理由退换。', status: true, sort: 0 },
{ id: 3, name: '价格保指', icon: '/static/logo.png', desc: '购买后 30 天内如遇同款低价,即可申请价差补偿。', status: true, sort: 1 },
{ id: 4, name: '全程颜料隐形', icon: '/static/logo.png', desc: '顺丰乐丰包装,不露商品信息,注重隐次保护。', status: true, sort: 2 },
{ id: 5, name: '隐私保护', icon: '/static/logo.png', desc: '尥尺保护您的个人信息,不向任何第三方泄露。', status: true, sort: 3 },
{ id: 6, name: '即时客服', icon: '/static/logo.png', desc: '7×24小时在线客服随时解决您的问题。', status: true, sort: 4 },
{ id: 7, name: '准时发货', icon: '/static/logo.png', desc: '下单后 48 小时内发货,快递全程跟踪。', status: false, sort: 5 },
{ id: 8, name: '免费退返运', icon: '/static/logo.png', desc: '指定品类商品支持免费退返运。', status: true, sort: 6 },
{ id: 9, name: '官方维修', icon: '/static/logo.png', desc: '各地维修中心 500+,提供上门维修服务。', status: true, sort: 7 },
{ id: 10, name: '分期免息', icon: '/static/logo.png', desc: '支持花唉/支付分期,指定商品免息付款。', status: false, sort: 8 }
])
// ========== MOCK DATA END ==========
// ========== PAGINATION STATE ==========
const currentPage = ref(1)
const pageSize = ref(10)
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(() => list.length)
const totalPage = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
const pagedList = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
return list.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 showModal = ref(false)
const isEdit = ref(false)
const editIndex = ref(-1)
const form = reactive({
name: '',
icon: '',
desc: '',
status: true,
sort: 0
})
function openModal(item: ProtectionItem | null = null) {
if (item) {
isEdit.value = true
form.name = item.name
form.icon = item.icon
form.desc = item.desc
form.status = item.status
form.sort = item.sort
editIndex.value = list.indexOf(item)
} else {
isEdit.value = false
form.name = ''
form.icon = ''
form.desc = ''
form.status = true
form.sort = 0
}
showModal.value = true
}
function closeModal() {
showModal.value = false
}
function mockIconPicker() {
uni.showToast({ title: '已模拟选择图标', icon: 'none' })
form.icon = '/static/logo.png'
}
function saveProtection() {
if (!form.name || !form.desc) {
uni.showToast({ title: '请输入必填项', icon: 'none' })
return
}
if (isEdit.value) {
const item = list[editIndex.value]
item.name = form.name
item.icon = form.icon
item.desc = form.desc
item.status = form.status
item.sort = form.sort
} else {
list.unshift({
id: Date.now() % 1000,
name: form.name,
icon: form.icon || '/static/logo.png',
desc: form.desc,
status: form.status,
sort: form.sort
})
}
closeModal()
uni.showToast({ title: '保存成功', icon: 'success' })
}
function toggleStatus(index: number) {
list[index].status = !list[index].status
}
function deleteItem(index: number) {
uni.showModal({
title: '提示',
content: '确定删除该保障条款吗?',
success: (res) => {
if (res.confirm) {
list.splice(index, 1)
}
}
})
}
</script>
<style scoped lang="scss">
.admin-main {
padding: 0;
background-color: transparent;
min-height: auto;
}
.alert-info-box {
background-color: #e6f7ff;
border: 1px solid #91d5ff;
padding: 10px 16px;
border-radius: 4px;
margin-bottom: 20px;
}
.alert-info-txt { font-size: 14px; color: #1890ff; }
.table-card {
background-color: #fff;
padding: 24px;
border-radius: 4px;
}
.table-toolbar { margin-bottom: 20px; }
.btn-primary-add {
background-color: #1890ff;
color: #fff;
height: 32px;
line-height: 32px;
padding: 0 16px;
font-size: 14px;
border-radius: 4px;
border: none;
margin: 0;
}
.table-header-pane {
display: flex;
flex-direction: row;
background-color: #f8f9fa;
height: 44px;
align-items: center;
}
.th { font-size: 14px; font-weight: bold; padding: 0 12px; }
.table-row-item {
display: flex;
flex-direction: row;
border-bottom: 1px solid #f0f0f0;
min-height: 60px;
align-items: center;
}
.td { padding: 0 12px; font-size: 14px; }
.color-9 { color: #999; }
.color-6 { color: #666; }
.protection-icon-img {
width: 30px;
height: 30px;
}
.btn-action-blue { color: #1890ff; font-size: 14px; cursor: pointer; }
.btn-action-red { color: #ff4d4f; font-size: 14px; cursor: pointer; }
.v-divider-line { width: 1px; height: 12px; background-color: #eee; margin: 0 10px; }
.row-center { display: flex; flex-direction: row; justify-content: center; align-items: center; }
/* Modal */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.5);
z-index: 2000;
display: flex;
justify-content: center;
align-items: center;
}
.modal-main-pane {
width: 600px;
background-color: #fff;
border-radius: 8px;
display: flex;
flex-direction: column;
}
.modal-header-box {
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.modal-title-txt { font-size: 16px; font-weight: bold; }
.modal-close-icon { font-size: 18px; color: #999; cursor: pointer; }
.modal-body-form { padding: 24px; }
.form-item-box { display: flex; flex-direction: row; margin-bottom: 20px; align-items: center; }
.row-align-start { align-items: flex-start; }
.label-box { width: 100px; text-align: right; margin-right: 16px; }
.form-label { font-size: 14px; color: #333; }
.font-star::before { content: '*'; color: #ff4d4f; margin-right: 4px; }
.val-box { flex: 1; }
.input-ctrl {
width: 100%;
height: 36px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 12px;
font-size: 14px;
}
.textarea-ctrl {
width: 100%;
height: 80px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 8px 12px;
font-size: 14px;
}
.icon-upload-placeholder {
width: 60px;
height: 60px;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.icon-empty-img { width: 24px; height: 24px; opacity: 0.3; }
.icon-preview-img { width: 100%; height: 100%; }
.row-center-start { display: flex; flex-direction: row; align-items: center; }
.radio-item { display: flex; flex-direction: row; align-items: center; margin-right: 20px; cursor: pointer; }
.radio-circle {
width: 16px;
height: 16px;
border: 1px solid #dcdfe6;
border-radius: 50%;
margin-right: 6px;
position: relative;
}
.radio-checked { border-color: #1890ff; background-color: #1890ff; }
.radio-checked::after {
content: '';
width: 6px;
height: 6px;
background-color: #fff;
border-radius: 50%;
position: absolute;
top: 4px;
left: 4px;
}
.radio-txt { font-size: 14px; color: #333; }
.modal-footer-box {
padding: 12px 20px;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: flex-end;
}
.btn-foot-cancel, .btn-foot-submit {
height: 32px;
line-height: 32px;
padding: 0 20px;
border-radius: 4px;
font-size: 14px;
margin-left: 12px;
}
.btn-foot-cancel { background-color: #fff; border: 1px solid #dcdfe6; color: #606266; }
.btn-foot-submit { background-color: #1890ff; color: #fff; border: none; }
.flex-1 { flex: 1; }
.flex-2 { flex: 2; }
.flex-4 { flex: 4; }
.flex-5 { flex: 5; }
</style>