feat(admin): complete integration of auth, delivery, and system infrastructure modules
This commit is contained in:
@@ -8,17 +8,17 @@
|
||||
<view class="config-form">
|
||||
<view class="form-item">
|
||||
<text class="label">APPID:</text>
|
||||
<input class="form-input" value="wx277a269f3d736d67" />
|
||||
<input class="form-input" v-model="formData.appid" placeholder="微信开放平台申请移动应用后给予的APPID" />
|
||||
<text class="tip">微信开放平台申请移动应用后给予的APPID</text>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">AppSecret:</text>
|
||||
<input class="form-input" value="bd08741a055c2ecac5826ff1c048464b" />
|
||||
<input class="form-input" v-model="formData.appsecret" placeholder="微信开放平台申请移动应用后给予的AppSecret" />
|
||||
<text class="tip">微信开放平台申请移动应用后给予的AppSecret</text>
|
||||
</view>
|
||||
|
||||
<view class="form-btns">
|
||||
<button class="btn primary">提交</button>
|
||||
<button class="btn primary" @click="handleSubmit">提交</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -26,7 +26,40 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
// APP配置逻辑
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { getSystemConfig, saveSystemConfig } from "@/services/admin/systemConfigService.uts"
|
||||
|
||||
const formData = reactive({
|
||||
appid: '',
|
||||
appsecret: ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadConfig()
|
||||
})
|
||||
|
||||
async function loadConfig() {
|
||||
const res = await getSystemConfig('mobile_app_config')
|
||||
if (res != null) {
|
||||
Object.assign(formData, res as any)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
uni.showLoading({ title: '正在保存...' })
|
||||
try {
|
||||
const ok = await saveSystemConfig('mobile_app_config', formData as UTSJSONObject, '移动应用(APP)配置')
|
||||
if (ok) {
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '系统错误', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
<template>
|
||||
<view class="admin-page">
|
||||
<view class="admin-card content-card">
|
||||
@@ -25,7 +24,7 @@
|
||||
<view class="form-item row">
|
||||
<text class="label">联系电话:</text>
|
||||
<view class="form-content">
|
||||
<input class="form-input" v-model="config.phone" placeholder="400-8888-794" />
|
||||
<input class="form-input" v-model="config.phone" placeholder="请输入联系电话" />
|
||||
<text class="tip">PC底部显示的联系电话</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -33,7 +32,7 @@
|
||||
<view class="form-item row">
|
||||
<text class="label">公司地址:</text>
|
||||
<view class="form-content">
|
||||
<input class="form-input" v-model="config.address" placeholder="陕西省西安市..." />
|
||||
<input class="form-input" v-model="config.address" placeholder="请输入公司地址" />
|
||||
<text class="tip">PC底部显示的公司地址</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -96,12 +95,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { getSystemConfig, saveSystemConfig } from "@/services/admin/systemConfigService.uts"
|
||||
|
||||
const config = ref({
|
||||
logo: 'https://v5.crmeb.net/uploads/attach/2022/05/20220516/6198f7e6f8a8b.png',
|
||||
phone: '400-8888-794',
|
||||
address: '陕西省西安市西咸新区洋东新城能源金融贸易区金湾大厦3层',
|
||||
const config = reactive({
|
||||
logo: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
keywords: '',
|
||||
description: '',
|
||||
qrCodeType: 'routine',
|
||||
@@ -109,17 +109,40 @@ const config = ref({
|
||||
newProductCount: 5
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadConfig()
|
||||
})
|
||||
|
||||
async function loadConfig() {
|
||||
const res = await getSystemConfig('pc_site_config')
|
||||
if (res != null) {
|
||||
Object.assign(config, res as any)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUploadLogo = () => {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
success: (res) => {
|
||||
config.value.logo = res.tempFilePaths[0]
|
||||
config.logo = res.tempFilePaths[0]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
const handleSubmit = async () => {
|
||||
uni.showLoading({ title: '正在保存...' })
|
||||
try {
|
||||
const ok = await saveSystemConfig('pc_site_config', config as UTSJSONObject, 'PC站点配置')
|
||||
if (ok) {
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '系统错误', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -308,4 +331,3 @@ const handleSubmit = () => {
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</view>
|
||||
<view class="table-body">
|
||||
<view class="table-row">
|
||||
<view class="col col-title"><text>ZC2884891</text></view>
|
||||
<view class="col col-title"><text>{{ info?.auth_id || '检测中...' }}</text></view>
|
||||
<view class="col col-action">
|
||||
<text class="action-btn" @click="gotoOfficial">进入官网</text>
|
||||
</view>
|
||||
@@ -22,29 +22,6 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 自定义版权信息 -->
|
||||
<view class="admin-card info-section mt-24">
|
||||
<view class="section-header">
|
||||
<text class="section-title">自定义版权信息</text>
|
||||
</view>
|
||||
<view class="table-container header-table">
|
||||
<view class="table-header">
|
||||
<view class="col col-text"><text>文字版权信息</text></view>
|
||||
<view class="col col-image"><text>底部版权图片</text></view>
|
||||
<view class="col col-action"><text>操作</text></view>
|
||||
</view>
|
||||
<view class="table-body">
|
||||
<view class="table-row">
|
||||
<view class="col col-text"><text></text></view>
|
||||
<view class="col col-image"><text></text></view>
|
||||
<view class="col col-action">
|
||||
<text class="action-btn" @click="editCopyright">编辑</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 服务器信息 -->
|
||||
<view class="admin-card info-section mt-24">
|
||||
<view class="section-header">
|
||||
@@ -60,53 +37,71 @@
|
||||
<view class="table-row">
|
||||
<view class="col col-env"><text>服务器系统</text></view>
|
||||
<view class="col col-req"><text>类UNIX</text></view>
|
||||
<view class="col col-status"><text>Linux</text></view>
|
||||
<view class="col col-status"><text>{{ info?.server_os || 'Loading...' }}</text></view>
|
||||
</view>
|
||||
<view class="table-row">
|
||||
<view class="col col-env"><text>WEB环境</text></view>
|
||||
<view class="col col-req"><text>Apache/Nginx/IIS</text></view>
|
||||
<view class="col col-status"><text>nginx/1.24.0</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 系统环境要求 -->
|
||||
<view class="admin-card info-section mt-24">
|
||||
<view class="section-header">
|
||||
<text class="section-title">系统环境要求</text>
|
||||
</view>
|
||||
<view class="table-container header-table">
|
||||
<view class="table-header">
|
||||
<view class="col col-env"><text>环境</text></view>
|
||||
<view class="col col-req"><text>要求</text></view>
|
||||
<view class="col col-status"><text>状态</text></view>
|
||||
</view>
|
||||
<view class="table-body">
|
||||
<view class="table-row">
|
||||
<view class="col col-env"><text>PHP版本</text></view>
|
||||
<view class="col col-req"><text>7.1-7.4</text></view>
|
||||
<view class="col col-status"><text>7.4.33</text></view>
|
||||
<view class="col col-status"><text>{{ info?.web_server || 'Loading...' }}</text></view>
|
||||
</view>
|
||||
<view class="table-row">
|
||||
<view class="col col-env"><text>MySQL版本</text></view>
|
||||
<view class="col col-req"><text>5.6-8.0</text></view>
|
||||
<view class="col col-status"><text>8.0.35</text></view>
|
||||
<view class="col col-env"><text>数据库引擎</text></view>
|
||||
<view class="col col-req"><text>PostgreSQL</text></view>
|
||||
<view class="col col-status"><text>{{ info?.db_engine || 'Loading...' }}</text></view>
|
||||
</view>
|
||||
<view class="table-row">
|
||||
<view class="col col-env"><text>数据库版本</text></view>
|
||||
<view class="col col-req"><text>15.0+</text></view>
|
||||
<view class="col col-status"><text>{{ info?.db_version || 'Loading...' }}</text></view>
|
||||
</view>
|
||||
<view class="table-row">
|
||||
<view class="col col-env"><text>运行环境</text></view>
|
||||
<view class="col col-req"><text>UTS Runtime</text></view>
|
||||
<view class="col col-status"><text>{{ info?.uts_runtime || 'Loading...' }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="isLoading" class="loading-overlay">
|
||||
<text>系统环境加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
function gotoOfficial() {
|
||||
uni.showToast({ title: '正在打开官网...', icon: 'none' })
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { fetchSystemInfo, SystemInfo } from '@/services/admin/maintainService.uts'
|
||||
|
||||
const info = ref<SystemInfo | null>(null)
|
||||
const isLoading = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const res = await fetchSystemInfo()
|
||||
if (res != null) {
|
||||
info.value = res
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '获取系统信息失败', icon: 'none' })
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function editCopyright() {
|
||||
uni.showToast({ title: '编辑版权信息', icon: 'none' })
|
||||
function gotoOfficial() {
|
||||
// #ifdef H5
|
||||
window.open('https://www.crmeb.com', '_blank')
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
uni.showToast({ title: '请在浏览器中访问官网', icon: 'none' })
|
||||
// #endif
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -158,13 +153,10 @@ function editCopyright() {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 各表格占位列宽 */
|
||||
.col-title { flex: 1; }
|
||||
.col-text { flex: 1; }
|
||||
.col-image { flex: 1; }
|
||||
.col-env { flex: 1; }
|
||||
.col-req { flex: 1; }
|
||||
.col-status { flex: 1; }
|
||||
.col-status { flex: 1; font-family: monospace; }
|
||||
.col-action { width: 150px; justify-content: flex-end; }
|
||||
|
||||
.action-btn {
|
||||
@@ -175,4 +167,14 @@ function editCopyright() {
|
||||
.mt-24 {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background-color: rgba(255,255,255,0.6);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 999;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,15 +5,13 @@
|
||||
<view class="search-wrap">
|
||||
<view class="search-item">
|
||||
<text class="label">状态:</text>
|
||||
<picker mode="selector" :range="statusRange" @change="onStatusChange">
|
||||
<view class="picker-input">{{ statusText }}</view>
|
||||
</picker>
|
||||
<uni-data-select v-model="statusValue" :localdata="statusOptions" style="width: 120px;" @change="handleQuery" />
|
||||
</view>
|
||||
<view class="search-item">
|
||||
<text class="label">搜索:</text>
|
||||
<input class="input" placeholder="请输入姓名或者账号" v-model="searchKey" />
|
||||
<input class="input" placeholder="请输入姓名或者账号" v-model="searchKey" @confirm="handleQuery" />
|
||||
</view>
|
||||
<button class="btn btn-primary" @click="onSearch">查询</button>
|
||||
<button class="btn btn-primary" @click="handleQuery">查询</button>
|
||||
</view>
|
||||
|
||||
<view class="action-wrap">
|
||||
@@ -21,7 +19,7 @@
|
||||
</view>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<view class="table-wrap">
|
||||
<view class="table-wrap border-shadow">
|
||||
<view class="table-header">
|
||||
<view class="th" style="flex: 2;">姓名</view>
|
||||
<view class="th" style="flex: 2;">账号</view>
|
||||
@@ -32,9 +30,42 @@
|
||||
<view class="th" style="flex: 2;">操作</view>
|
||||
</view>
|
||||
<view class="table-body">
|
||||
<view class="no-data">
|
||||
<text class="no-data-text">暂无数据</text>
|
||||
<view v-if="loading" class="loading-box">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="adminList.length === 0" class="no-data">
|
||||
<text class="no-data-text">暂无管理员数据</text>
|
||||
</view>
|
||||
<view v-else v-for="item in adminList" :key="item.id" class="tr">
|
||||
<view class="td" style="flex: 2;"><text class="td-txt">{{ item.real_name || '-' }}</text></view>
|
||||
<view class="td" style="flex: 2;"><text class="td-txt">{{ item.username }}</text></view>
|
||||
<view class="td" style="flex: 2;">
|
||||
<view class="role-tags">
|
||||
<text v-for="role in (item.roles || [])" :key="role" class="role-tag">{{ role }}</text>
|
||||
<text v-if="!item.roles || item.roles.length === 0" class="td-txt">-</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="td" style="flex: 3;"><text class="td-txt-small">{{ formatTime(item.last_login_at) }}</text></view>
|
||||
<view class="td" style="flex: 3;"><text class="td-txt-small">{{ item.last_login_ip || '-' }}</text></view>
|
||||
<view class="td" style="flex: 1;">
|
||||
<switch :checked="item.is_active" color="#1890ff" scale="0.7" disabled />
|
||||
</view>
|
||||
<view class="td" style="flex: 2;">
|
||||
<text class="action-btn" @click="onEdit(item)">编辑</text>
|
||||
<view class="divider"></view>
|
||||
<text class="action-btn danger">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页栏 -->
|
||||
<view class="pagination-footer">
|
||||
<text class="total-txt">共 {{ total }} 条</text>
|
||||
<view class="page-btns">
|
||||
<text :class="['p-btn', page <= 1 ? 'disabled' : '']" @click="prevPage"> < </text>
|
||||
<text class="p-btn active">{{ page }}</text>
|
||||
<text :class="['p-btn', adminList.length < pageSize ? 'disabled' : '']" @click="nextPage"> > </text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -42,136 +73,101 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { fetchAdminPage, type AdminUser } from '@/services/admin/authService.uts'
|
||||
|
||||
const adminList = ref<AdminUser[]>([])
|
||||
const loading = ref(false)
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = 15
|
||||
const searchKey = ref('')
|
||||
const statusRange = ['所有', '启用', '禁用']
|
||||
const statusIndex = ref(0)
|
||||
const statusText = ref('请选择')
|
||||
const statusValue = ref('all')
|
||||
|
||||
function onStatusChange(e: any) {
|
||||
statusIndex.value = parseInt(e.detail.value.toString())
|
||||
statusText.value = statusRange[statusIndex.value]
|
||||
const statusOptions = [
|
||||
{ value: 'all', text: '所有' },
|
||||
{ value: '1', text: '启用' },
|
||||
{ value: '0', text: '禁用' }
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const status = statusValue.value === 'all' ? null : parseInt(statusValue.value)
|
||||
const res = await fetchAdminPage(page.value, pageSize, searchKey.value || null, status)
|
||||
adminList.value = res.items
|
||||
total.value = res.total
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
console.log('Search:', searchKey.value, statusText.value)
|
||||
function handleQuery() {
|
||||
page.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function onAdd() {
|
||||
console.log('Add admin')
|
||||
uni.showToast({ title: '添加管理员功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
function onEdit(item : AdminUser) {
|
||||
console.log('Edit admin:', item.id)
|
||||
uni.showToast({ title: '编辑功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
function prevPage() { if (page.value > 1) { page.value--; loadData(); } }
|
||||
function nextPage() { if (adminList.value.length >= pageSize) { page.value++; loadData(); } }
|
||||
|
||||
function formatTime(iso : string | null) : string {
|
||||
if (!iso) return '-'
|
||||
return iso.substring(0, 16).replace('T', ' ')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-page-container {
|
||||
padding: 15px;
|
||||
background-color: #f5f7f9;
|
||||
min-height: 100vh;
|
||||
}
|
||||
<style scoped lang="scss">
|
||||
.admin-page-container { padding: 24px; background-color: #f5f7f9; min-height: 100vh; }
|
||||
.page-card { background-color: #fff; border-radius: 4px; padding: 24px; }
|
||||
|
||||
.page-card {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
}
|
||||
.search-wrap { display: flex; flex-direction: row; align-items: center; padding-bottom: 24px; border-bottom: 1px solid #f0f0f0; margin-bottom: 24px; }
|
||||
.search-item { display: flex; flex-direction: row; align-items: center; margin-right: 24px; }
|
||||
.label { font-size: 14px; color: #606266; margin-right: 8px; }
|
||||
.input { width: 200px; height: 32px; border: 1px solid #dcdfe6; border-radius: 4px; padding: 0 12px; font-size: 14px; }
|
||||
|
||||
.search-wrap {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.btn { height: 32px; padding: 0 20px; border-radius: 4px; border: none; cursor: pointer; display: flex; align-items: center; }
|
||||
.btn-primary { background-color: #1890ff; color: #fff; font-size: 14px; }
|
||||
|
||||
.search-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-right: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.action-wrap { margin-bottom: 20px; }
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.table-wrap { border: 1px solid #f0f0f0; border-radius: 4px; }
|
||||
.table-header { display: flex; flex-direction: row; background-color: #f8f8f9; }
|
||||
.th { padding: 12px 10px; font-size: 14px; font-weight: bold; color: #515a6e; border-bottom: 1px solid #f0f0f0; text-align: center; }
|
||||
.tr { display: flex; flex-direction: row; border-bottom: 1px solid #f0f0f0; min-height: 54px; align-items: center; }
|
||||
.td { padding: 10px; display: flex; align-items: center; justify-content: center; }
|
||||
.td-txt { font-size: 13px; color: #606266; }
|
||||
.td-txt-small { font-size: 12px; color: #999; }
|
||||
|
||||
.picker-input {
|
||||
width: 150px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
background-color: #fff;
|
||||
}
|
||||
.role-tags { display: flex; flex-direction: row; flex-wrap: wrap; gap: 4px; justify-content: center; }
|
||||
.role-tag { background-color: #e6f7ff; color: #1890ff; border: 1px solid #91d5ff; padding: 1px 6px; border-radius: 2px; font-size: 11px; }
|
||||
|
||||
.input {
|
||||
width: 220px;
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
.action-btn { color: #1890ff; font-size: 13px; cursor: pointer; }
|
||||
.danger { color: #ff4d4f; }
|
||||
.divider { width: 1px; height: 12px; background-color: #e8e8e8; margin: 0 8px; }
|
||||
|
||||
.btn {
|
||||
height: 32px;
|
||||
line-height: 30px;
|
||||
padding: 0 15px;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.no-data { padding: 60px 0; text-align: center; }
|
||||
.no-data-text { font-size: 14px; color: #c5c8ce; }
|
||||
.loading-box { padding: 60px 0; text-align: center; color: #1890ff; }
|
||||
|
||||
.btn-primary {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.action-wrap {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
width: 100%;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #f8f8f9;
|
||||
}
|
||||
|
||||
.th {
|
||||
padding: 12px 10px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #515a6e;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.table-body {
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.no-data-text {
|
||||
font-size: 14px;
|
||||
color: #c5c8ce;
|
||||
}
|
||||
.pagination-footer { margin-top: 24px; display: flex; flex-direction: row; align-items: center; justify-content: flex-end; gap: 12px; }
|
||||
.total-txt { font-size: 13px; color: #999; }
|
||||
.page-btns { display: flex; flex-direction: row; gap: 8px; }
|
||||
.p-btn { width: 30px; height: 30px; border: 1px solid #dcdee2; display: flex; align-items: center; justify-content: center; border-radius: 4px; cursor: pointer; font-size: 13px; }
|
||||
.p-btn.active { background-color: #1890ff; color: #fff; border-color: #1890ff; }
|
||||
.p-btn.disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
</style>
|
||||
|
||||
@@ -1,188 +1,263 @@
|
||||
<template>
|
||||
<view class="admin-page-container">
|
||||
<view class="page-card">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-wrap">
|
||||
<view class="search-item">
|
||||
<text class="label">按钮名称:</text>
|
||||
<input class="input" placeholder="请输入按钮名称" v-model="searchKey" />
|
||||
</view>
|
||||
<button class="btn btn-primary" @click="onSearch">查询</button>
|
||||
<!-- 顶部操作栏 -->
|
||||
<view class="action-wrap">
|
||||
<button class="btn btn-primary" @click="onAdd(null)">添加顶级权限</button>
|
||||
<button class="btn btn-ghost ml-10" @click="loadData">刷新</button>
|
||||
</view>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<view class="table-wrap">
|
||||
<!-- 树形表格区域 -->
|
||||
<view class="table-wrap border-shadow">
|
||||
<view class="table-header">
|
||||
<view class="th" style="flex: 4; text-align: left; padding-left: 20px;">按钮名称</view>
|
||||
<view class="th" style="flex: 3;">类型</view>
|
||||
<view class="th" style="flex: 2;">排序</view>
|
||||
<view class="th" style="flex: 2;">是否显示</view>
|
||||
<view class="th" style="flex: 2;">操作</view>
|
||||
<view class="th" style="flex: 4; text-align: left; padding-left: 20px;">菜单/按钮名称</view>
|
||||
<view class="th" style="flex: 2;">编码</view>
|
||||
<view class="th" style="flex: 2;">类型</view>
|
||||
<view class="th" style="flex: 1;">排序</view>
|
||||
<view class="th" style="flex: 1;">显示</view>
|
||||
<view class="th" style="flex: 3;">操作</view>
|
||||
</view>
|
||||
|
||||
<view class="table-body">
|
||||
<view v-for="item in permissionList" :key="item.id" class="tr">
|
||||
<view v-if="loading" class="loading-box">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="permissionList.length === 0" class="no-data">
|
||||
<text class="no-data-text">暂无数据</text>
|
||||
</view>
|
||||
|
||||
<!-- 递归渲染或平铺渲染 (这里采用平铺+缩进模拟树形) -->
|
||||
<view v-else v-for="item in permissionList" :key="item.id" class="tr">
|
||||
<view class="td" style="flex: 4; text-align: left; padding-left: 20px;">
|
||||
<text v-if="item.hasChildren" class="expand-icon">▶</text>
|
||||
<text class="menu-name">{{ item.name }}</text>
|
||||
</view>
|
||||
<view class="td" style="flex: 3;">{{ item.type }}</view>
|
||||
<view class="td" style="flex: 2;">{{ item.sort }}</view>
|
||||
<view class="td" style="flex: 2;"><text class="td-txt-small">{{ item.code }}</text></view>
|
||||
<view class="td" style="flex: 2;">
|
||||
<switch :checked="item.isshow" color="#1890ff" @change="onToggleShow(item)" />
|
||||
<text :class="['type-tag', item.type === 'menu' ? 'menu' : 'button']">
|
||||
{{ item.type === 'menu' ? '菜单' : '按钮' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="td" style="flex: 2;">
|
||||
<text class="action-btn" @click="onEdit(item)">编辑</text>
|
||||
<view class="td" style="flex: 1;"><text class="td-txt">{{ item.sort_order }}</text></view>
|
||||
<view class="td" style="flex: 1;">
|
||||
<switch :checked="item.is_visible" color="#1890ff" scale="0.6" @change="onToggleVisible(item)" />
|
||||
</view>
|
||||
<view class="td" style="flex: 3;">
|
||||
<view class="op-links">
|
||||
<text class="action-btn" @click="onAdd(item.id)">新增子项</text>
|
||||
<text class="op-split">|</text>
|
||||
<text class="action-btn" @click="onEdit(item)">编辑</text>
|
||||
<text class="op-split">|</text>
|
||||
<text class="action-btn danger" @click="onDelete(item)">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 添加/编辑 弹窗 -->
|
||||
<view v-if="showModal" class="modal-overlay" @click="closeModal">
|
||||
<view class="modal-card" @click.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">{{ isEdit ? '编辑权限' : '添加权限' }}</text>
|
||||
<text class="close-btn" @click="closeModal">×</text>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<scroll-view scroll-y="true" style="max-height: 500px;">
|
||||
<view class="form-item">
|
||||
<text class="f-label">父级ID:</text>
|
||||
<input class="f-input disabled" :value="form.parent_id || '顶级'" disabled />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="f-label">名称:</text>
|
||||
<input class="f-input" v-model="form.name" placeholder="请输入菜单或按钮名称" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="f-label">编码:</text>
|
||||
<input class="f-input" v-model="form.code" placeholder="如: user_view" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="f-label">类型:</text>
|
||||
<radio-group class="radio-group" @change="(e : any) => form.type = e.detail.value">
|
||||
<label class="radio-label"><radio value="menu" :checked="form.type === 'menu'" color="#1890ff" /> 菜单</label>
|
||||
<label class="radio-label"><radio value="button" :checked="form.type === 'button'" color="#1890ff" /> 按钮</label>
|
||||
</radio-group>
|
||||
</view>
|
||||
<view class="form-item" v-if="form.type === 'menu'">
|
||||
<text class="f-label">路由路径:</text>
|
||||
<input class="f-input" v-model="form.path" placeholder="请输入前端路由地址" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="f-label">排序:</text>
|
||||
<input class="f-input" type="number" v-model="form.sort_order" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<button class="btn" @click="closeModal">取消</button>
|
||||
<button class="btn btn-primary" @click="handleSave">提交</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { fetchPermissionList, savePermission, deletePermission, type AdminPermission } from '@/services/admin/authService.uts'
|
||||
|
||||
const searchKey = ref('')
|
||||
const permissionList = ref<AdminPermission[]>([])
|
||||
const loading = ref(false)
|
||||
const showModal = ref(false)
|
||||
const isEdit = ref(false)
|
||||
|
||||
type PermissionItem = {
|
||||
id: number
|
||||
name: string
|
||||
type: string
|
||||
sort: number
|
||||
isshow: boolean
|
||||
hasChildren: boolean
|
||||
const form = reactive({
|
||||
id: '' as string | null,
|
||||
parent_id: '' as string | null,
|
||||
name: '',
|
||||
code: '',
|
||||
type: 'menu',
|
||||
path: '',
|
||||
icon: '',
|
||||
sort_order: 0,
|
||||
is_visible: true
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchPermissionList()
|
||||
permissionList.value = res
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const permissionList = reactive<PermissionItem[]>([
|
||||
{ id: 1, name: '主页', type: '菜单:/admin/index', sort: 127, isshow: true, hasChildren: false },
|
||||
{ id: 2, name: '用户', type: '菜单:/admin/user', sort: 125, isshow: true, hasChildren: true },
|
||||
{ id: 3, name: '订单', type: '菜单:/admin/order', sort: 120, isshow: true, hasChildren: true },
|
||||
{ id: 4, name: '商品', type: '菜单:/admin/product', sort: 115, isshow: true, hasChildren: true },
|
||||
{ id: 5, name: '营销', type: '菜单:/admin/marketing', sort: 110, isshow: true, hasChildren: true }
|
||||
])
|
||||
|
||||
function onSearch() {
|
||||
console.log('Search:', searchKey.value)
|
||||
function onAdd(parentId : string | null) {
|
||||
isEdit.value = false
|
||||
form.id = null
|
||||
form.parent_id = parentId
|
||||
form.name = ''
|
||||
form.code = ''
|
||||
form.type = 'menu'
|
||||
form.path = ''
|
||||
form.sort_order = 0
|
||||
form.is_visible = true
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function onToggleShow(item: PermissionItem) {
|
||||
item.isshow = !item.isshow
|
||||
function onEdit(item : AdminPermission) {
|
||||
isEdit.value = true
|
||||
form.id = item.id
|
||||
form.parent_id = item.parent_id
|
||||
form.name = item.name
|
||||
form.code = item.code
|
||||
form.type = item.type
|
||||
form.path = item.path || ''
|
||||
form.sort_order = item.sort_order
|
||||
form.is_visible = item.is_visible
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function onEdit(item: PermissionItem) {
|
||||
console.log('Edit:', item.name)
|
||||
async function onToggleVisible(item : AdminPermission) {
|
||||
const nextVal = !item.is_visible
|
||||
const ok = await savePermission({ ...item, is_visible: nextVal })
|
||||
if (ok != null) {
|
||||
item.is_visible = nextVal
|
||||
uni.showToast({ title: '显示状态已更新' })
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.name || !form.code) {
|
||||
uni.showToast({ title: '请填写必填项', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const resId = await savePermission(form)
|
||||
if (resId != null) {
|
||||
uni.showToast({ title: '保存成功' })
|
||||
showModal.value = false
|
||||
loadData()
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onDelete(item : AdminPermission) {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: `确定要删除权限项 "${item.name}" 吗?此操作不可撤销。`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
const ok = await deletePermission(item.id)
|
||||
if (ok) {
|
||||
uni.showToast({ title: '删除成功' })
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
showModal.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-page-container {
|
||||
padding: 15px;
|
||||
background-color: #f5f7f9;
|
||||
min-height: 100vh;
|
||||
}
|
||||
<style scoped lang="scss">
|
||||
.admin-page-container { padding: 24px; background-color: #f5f7f9; min-height: 100vh; }
|
||||
.page-card { background-color: #fff; border-radius: 4px; padding: 24px; }
|
||||
|
||||
.page-card {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
}
|
||||
.action-wrap { margin-bottom: 24px; display: flex; flex-direction: row; }
|
||||
.btn { height: 32px; padding: 0 16px; font-size: 14px; border-radius: 4px; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; }
|
||||
.btn-primary { background-color: #1890ff; color: #fff; }
|
||||
.btn-ghost { background-color: #fff; color: #666; border: 1px solid #dcdfe6; }
|
||||
.ml-10 { margin-left: 10px; }
|
||||
|
||||
.search-wrap {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.table-wrap { border: 1px solid #f0f0f0; border-radius: 4px; }
|
||||
.table-header { display: flex; flex-direction: row; background-color: #f8f8f9; }
|
||||
.th { padding: 12px 10px; font-size: 14px; font-weight: bold; color: #515a6e; border-bottom: 1px solid #f0f0f0; text-align: center; }
|
||||
.tr { display: flex; flex-direction: row; border-bottom: 1px solid #f0f0f0; min-height: 50px; align-items: center; }
|
||||
.td { padding: 8px 10px; font-size: 13px; color: #606266; text-align: center; display: flex; align-items: center; justify-content: center; }
|
||||
|
||||
.search-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-right: 20px;
|
||||
}
|
||||
.menu-name { font-weight: 500; color: #333; }
|
||||
.td-txt-small { font-size: 12px; color: #999; }
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.type-tag { padding: 2px 8px; border-radius: 4px; font-size: 11px; }
|
||||
.type-tag.menu { background-color: #e6f7ff; color: #1890ff; border: 1px solid #91d5ff; }
|
||||
.type-tag.button { background-color: #f6ffed; color: #52c41a; border: 1px solid #b7eb8f; }
|
||||
|
||||
.input {
|
||||
width: 200px;
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
.op-links { display: flex; flex-direction: row; align-items: center; gap: 8px; }
|
||||
.action-btn { color: #1890ff; font-size: 13px; cursor: pointer; }
|
||||
.danger { color: #ff4d4f; }
|
||||
.op-split { color: #eee; }
|
||||
|
||||
.btn {
|
||||
height: 32px;
|
||||
line-height: 30px;
|
||||
padding: 0 15px;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
}
|
||||
.loading-box, .no-data { padding: 60px 0; text-align: center; width: 100%; }
|
||||
.no-data-text { font-size: 14px; color: #ccc; }
|
||||
|
||||
.btn-primary {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
width: 100%;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #f8f8f9;
|
||||
}
|
||||
|
||||
.th {
|
||||
padding: 12px 10px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #515a6e;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tr {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.td {
|
||||
padding: 12px 10px;
|
||||
font-size: 14px;
|
||||
color: #515a6e;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.menu-name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
color: #1890ff;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* Modal */
|
||||
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0,0,0,0.5); z-index: 1000; display: flex; align-items: center; justify-content: center; }
|
||||
.modal-card { width: 500px; background-color: #fff; border-radius: 8px; overflow: hidden; }
|
||||
.modal-header { padding: 16px 20px; border-bottom: 1px solid #f0f0f0; display: flex; flex-direction: row; justify-content: space-between; align-items: center; }
|
||||
.modal-title { font-size: 16px; font-weight: bold; color: #333; }
|
||||
.close-btn { font-size: 24px; color: #999; cursor: pointer; }
|
||||
.modal-body { padding: 24px; }
|
||||
.form-item { margin-bottom: 20px; display: flex; flex-direction: row; align-items: center; }
|
||||
.align-start { align-items: flex-start; }
|
||||
.f-label { width: 90px; font-size: 14px; color: #666; text-align: right; margin-right: 15px; }
|
||||
.f-input { flex: 1; border: 1px solid #dcdfe6; height: 36px; padding: 0 12px; border-radius: 4px; font-size: 14px; }
|
||||
.f-input.disabled { background-color: #f5f5f5; color: #999; }
|
||||
.radio-group { display: flex; flex-direction: row; gap: 20px; }
|
||||
.radio-label { display: flex; flex-direction: row; align-items: center; font-size: 14px; }
|
||||
.modal-footer { padding: 16px 24px; border-top: 1px solid #f0f0f0; display: flex; flex-direction: row; justify-content: flex-end; gap: 12px; }
|
||||
</style>
|
||||
|
||||
@@ -5,15 +5,13 @@
|
||||
<view class="search-wrap">
|
||||
<view class="search-item">
|
||||
<text class="label">状态:</text>
|
||||
<picker mode="selector" :range="statusRange" @change="onStatusChange">
|
||||
<view class="picker-input">{{ statusText }}</view>
|
||||
</picker>
|
||||
<uni-data-select v-model="statusValue" :localdata="statusOptions" style="width: 120px;" @change="handleQuery" />
|
||||
</view>
|
||||
<view class="search-item">
|
||||
<text class="label">身份昵称:</text>
|
||||
<input class="input" placeholder="请输入身份昵称" v-model="searchKey" />
|
||||
<input class="input" placeholder="请输入身份昵称" v-model="searchKey" @confirm="handleQuery" />
|
||||
</view>
|
||||
<button class="btn btn-primary" @click="onSearch">查询</button>
|
||||
<button class="btn btn-primary" @click="handleQuery">查询</button>
|
||||
</view>
|
||||
|
||||
<view class="action-wrap">
|
||||
@@ -21,17 +19,70 @@
|
||||
</view>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<view class="table-wrap">
|
||||
<view class="table-wrap border-shadow">
|
||||
<view class="table-header">
|
||||
<view class="th" style="flex: 1;">ID</view>
|
||||
<view class="th" style="flex: 1;">序号</view>
|
||||
<view class="th" style="flex: 3;">身份昵称</view>
|
||||
<view class="th" style="flex: 2;">状态</view>
|
||||
<view class="th" style="flex: 2;">操作</view>
|
||||
</view>
|
||||
<view class="table-body">
|
||||
<view class="no-data">
|
||||
<view v-if="loading" class="loading-box">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="roleList.length === 0" class="no-data">
|
||||
<text class="no-data-text">暂无数据</text>
|
||||
</view>
|
||||
<view v-else v-for="(item, index) in roleList" :key="item.id" class="tr">
|
||||
<view class="td" style="flex: 1;"><text class="td-txt">{{ (page - 1) * pageSize + index + 1 }}</text></view>
|
||||
<view class="td" style="flex: 3;"><text class="td-txt">{{ item.name }} ({{ item.code }})</text></view>
|
||||
<view class="td" style="flex: 2;">
|
||||
<switch :checked="item.is_active" color="#1890ff" scale="0.7" @change="onToggleStatus(item)" />
|
||||
</view>
|
||||
<view class="td" style="flex: 2;">
|
||||
<text class="action-btn" @click="onEdit(item)">编辑</text>
|
||||
<view class="divider"></view>
|
||||
<text class="action-btn danger" @click="onDelete(item)">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页栏 -->
|
||||
<view class="pagination-footer">
|
||||
<text class="total-txt">共 {{ total }} 条</text>
|
||||
<view class="page-btns">
|
||||
<text :class="['p-btn', page <= 1 ? 'disabled' : '']" @click="prevPage"> < </text>
|
||||
<text class="p-btn active">{{ page }}</text>
|
||||
<text :class="['p-btn', roleList.length < pageSize ? 'disabled' : '']" @click="nextPage"> > </text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 添加/编辑 弹窗 -->
|
||||
<view v-if="showModal" class="modal-overlay" @click="closeModal">
|
||||
<view class="modal-card" @click.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">{{ isEdit ? '编辑身份' : '添加身份' }}</text>
|
||||
<text class="close-btn" @click="closeModal">×</text>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="form-item">
|
||||
<text class="f-label">身份名称:</text>
|
||||
<input class="f-input" v-model="form.name" placeholder="请输入身份名称" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="f-label">身份编码:</text>
|
||||
<input class="f-input" v-model="form.code" placeholder="如: super_admin" :disabled="isEdit" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="f-label">描述:</text>
|
||||
<textarea class="f-textarea" v-model="form.description" placeholder="请输入备注描述" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<button class="btn" @click="closeModal">取消</button>
|
||||
<button class="btn btn-primary" @click="handleSave">保存</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -39,136 +90,176 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { fetchRolePage, saveRole, deleteRole, type AdminRole } from '@/services/admin/authService.uts'
|
||||
|
||||
const roleList = ref<AdminRole[]>([])
|
||||
const loading = ref(false)
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = 15
|
||||
const searchKey = ref('')
|
||||
const statusRange = ['所有', '启用', '禁用']
|
||||
const statusIndex = ref(0)
|
||||
const statusText = ref('请选择')
|
||||
const statusValue = ref('all')
|
||||
|
||||
function onStatusChange(e: any) {
|
||||
statusIndex.value = parseInt(e.detail.value.toString())
|
||||
statusText.value = statusRange[statusIndex.value]
|
||||
const statusOptions = [
|
||||
{ value: 'all', text: '所有' },
|
||||
{ value: '1', text: '启用' },
|
||||
{ value: '0', text: '禁用' }
|
||||
]
|
||||
|
||||
// 弹窗表单状态
|
||||
const showModal = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const form = reactive({
|
||||
id: '' as string | null,
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
is_active: true
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchRolePage(page.value, pageSize, searchKey.value || null)
|
||||
roleList.value = res.items
|
||||
total.value = res.total
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
console.log('Search:', searchKey.value, statusText.value)
|
||||
function handleQuery() {
|
||||
page.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function onAdd() {
|
||||
console.log('Add role')
|
||||
isEdit.value = false
|
||||
form.id = null
|
||||
form.name = ''
|
||||
form.code = ''
|
||||
form.description = ''
|
||||
form.is_active = true
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function onEdit(item : AdminRole) {
|
||||
isEdit.value = true
|
||||
form.id = item.id
|
||||
form.name = item.name
|
||||
form.code = item.code
|
||||
form.description = item.description || ''
|
||||
form.is_active = item.is_active
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
showModal.value = false
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.name || !form.code) {
|
||||
uni.showToast({ title: '请完善必要信息', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const resId = await saveRole(form)
|
||||
if (resId != null) {
|
||||
uni.showToast({ title: '保存成功' })
|
||||
closeModal()
|
||||
loadData()
|
||||
} else {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onDelete(item : AdminRole) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: `确定要删除角色 "${item.name}" 吗?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
const ok = await deleteRole(item.id)
|
||||
if (ok) {
|
||||
uni.showToast({ title: '删除成功' })
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function onToggleStatus(item : AdminRole) {
|
||||
const nextStatus = !item.is_active
|
||||
const ok = await saveRole({ ...item, is_active: nextStatus })
|
||||
if (ok != null) {
|
||||
item.is_active = nextStatus
|
||||
uni.showToast({ title: '状态已更新' })
|
||||
}
|
||||
}
|
||||
|
||||
function prevPage() { if (page.value > 1) { page.value--; loadData(); } }
|
||||
function nextPage() { if (roleList.value.length >= pageSize) { page.value++; loadData(); } }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-page-container {
|
||||
padding: 15px;
|
||||
background-color: #f5f7f9;
|
||||
min-height: 100vh;
|
||||
}
|
||||
<style scoped lang="scss">
|
||||
.admin-page-container { padding: 24px; background-color: #f5f7f9; min-height: 100vh; }
|
||||
.page-card { background-color: #fff; border-radius: 4px; padding: 24px; }
|
||||
|
||||
.page-card {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
}
|
||||
.search-wrap { display: flex; flex-direction: row; align-items: center; padding-bottom: 24px; border-bottom: 1px solid #f0f0f0; margin-bottom: 24px; }
|
||||
.search-item { display: flex; flex-direction: row; align-items: center; margin-right: 24px; }
|
||||
.label { font-size: 14px; color: #606266; margin-right: 8px; }
|
||||
.input { width: 200px; height: 32px; border: 1px solid #dcdfe6; border-radius: 4px; padding: 0 12px; font-size: 14px; }
|
||||
|
||||
.search-wrap {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.btn { height: 32px; padding: 0 20px; border-radius: 4px; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; }
|
||||
.btn-primary { background-color: #1890ff; color: #fff; font-size: 14px; }
|
||||
|
||||
.search-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-right: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.action-wrap { margin-bottom: 20px; }
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.table-wrap { border: 1px solid #f0f0f0; border-radius: 4px; }
|
||||
.table-header { display: flex; flex-direction: row; background-color: #f8f8f9; }
|
||||
.th { padding: 12px 10px; font-size: 14px; font-weight: bold; color: #515a6e; border-bottom: 1px solid #f0f0f0; text-align: center; }
|
||||
.tr { display: flex; flex-direction: row; border-bottom: 1px solid #f0f0f0; min-height: 54px; align-items: center; }
|
||||
.td { padding: 10px; display: flex; align-items: center; justify-content: center; }
|
||||
.td-txt { font-size: 13px; color: #606266; }
|
||||
|
||||
.picker-input {
|
||||
width: 150px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
background-color: #fff;
|
||||
}
|
||||
.action-btn { color: #1890ff; font-size: 13px; cursor: pointer; }
|
||||
.danger { color: #ff4d4f; }
|
||||
.divider { width: 1px; height: 12px; background-color: #e8e8e8; margin: 0 8px; }
|
||||
|
||||
.input {
|
||||
width: 180px;
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
.no-data { padding: 60px 0; text-align: center; }
|
||||
.no-data-text { font-size: 14px; color: #c5c8ce; }
|
||||
.loading-box { padding: 60px 0; text-align: center; color: #1890ff; }
|
||||
|
||||
.btn {
|
||||
height: 32px;
|
||||
line-height: 30px;
|
||||
padding: 0 15px;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.pagination-footer { margin-top: 24px; display: flex; flex-direction: row; align-items: center; justify-content: flex-end; gap: 12px; }
|
||||
.total-txt { font-size: 13px; color: #999; }
|
||||
.page-btns { display: flex; flex-direction: row; gap: 8px; }
|
||||
.p-btn { width: 30px; height: 30px; border: 1px solid #dcdee2; display: flex; align-items: center; justify-content: center; border-radius: 4px; cursor: pointer; font-size: 13px; }
|
||||
.p-btn.active { background-color: #1890ff; color: #fff; border-color: #1890ff; }
|
||||
.p-btn.disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
.btn-primary {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.action-wrap {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
width: 100%;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #f8f8f9;
|
||||
}
|
||||
|
||||
.th {
|
||||
padding: 12px 10px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #515a6e;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.table-body {
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.no-data-text {
|
||||
font-size: 14px;
|
||||
color: #c5c8ce;
|
||||
}
|
||||
/* 弹窗样式 */
|
||||
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0,0,0,0.5); z-index: 1000; display: flex; align-items: center; justify-content: center; }
|
||||
.modal-card { width: 500px; background-color: #fff; border-radius: 8px; overflow: hidden; }
|
||||
.modal-header { padding: 16px 20px; border-bottom: 1px solid #f0f0f0; display: flex; flex-direction: row; justify-content: space-between; align-items: center; }
|
||||
.modal-title { font-size: 16px; font-weight: bold; color: #333; }
|
||||
.close-btn { font-size: 24px; color: #999; cursor: pointer; }
|
||||
.modal-body { padding: 24px; }
|
||||
.form-item { margin-bottom: 20px; display: flex; flex-direction: row; align-items: flex-start; }
|
||||
.f-label { width: 90px; font-size: 14px; color: #666; text-align: right; padding-top: 6px; }
|
||||
.f-input { flex: 1; border: 1px solid #dcdfe6; height: 36px; padding: 0 12px; border-radius: 4px; font-size: 14px; }
|
||||
.f-textarea { flex: 1; border: 1px solid #dcdfe6; height: 80px; padding: 8px 12px; border-radius: 4px; font-size: 14px; }
|
||||
.modal-footer { padding: 16px 24px; border-top: 1px solid #f0f0f0; display: flex; flex-direction: row; justify-content: flex-end; gap: 12px; }
|
||||
</style>
|
||||
|
||||
@@ -6,157 +6,253 @@
|
||||
</view>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<view class="table-wrap">
|
||||
<view class="table-wrap border-shadow">
|
||||
<view class="table-header">
|
||||
<view class="th" style="flex: 1;">ID</view>
|
||||
<view class="th" style="flex: 1;">序号</view>
|
||||
<view class="th" style="flex: 1.5;">头像</view>
|
||||
<view class="th" style="flex: 2;">名称</view>
|
||||
<view class="th" style="flex: 2.5;">手机号码</view>
|
||||
<view class="th" style="flex: 1.5;">是否显示</view>
|
||||
<view class="th" style="flex: 1.5;">状态</view>
|
||||
<view class="th" style="flex: 3;">添加时间</view>
|
||||
<view class="th" style="flex: 2;">操作</view>
|
||||
</view>
|
||||
<view class="table-body">
|
||||
<view v-for="item in courierList" :key="item.id" class="tr">
|
||||
<view class="td" style="flex: 1;">{{ item.id }}</view>
|
||||
<view v-if="loading" class="loading-box">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="staffList.length === 0" class="no-data">
|
||||
<text class="no-data-text">暂无配送员数据</text>
|
||||
</view>
|
||||
<view v-else v-for="(item, index) in staffList" :key="item.id" class="tr">
|
||||
<view class="td" style="flex: 1;"><text class="td-txt">{{ (page - 1) * pageSize + index + 1 }}</text></view>
|
||||
<view class="td" style="flex: 1.5;">
|
||||
<image class="avatar" :src="item.avatar" mode="aspectFill" />
|
||||
<image v-if="item.avatar" class="avatar" :src="item.avatar" mode="aspectFill" />
|
||||
<view v-else class="avatar-placeholder"><text>👤</text></view>
|
||||
</view>
|
||||
<view class="td" style="flex: 2;">{{ item.name }}</view>
|
||||
<view class="td" style="flex: 2.5;">{{ item.phone }}</view>
|
||||
<view class="td" style="flex: 2;"><text class="td-txt">{{ item.nickname }}</text></view>
|
||||
<view class="td" style="flex: 2.5;"><text class="td-txt">{{ item.phone }}</text></view>
|
||||
<view class="td" style="flex: 1.5;">
|
||||
<switch :checked="item.isshow" color="#1890ff" @change="onToggleShow(item)" />
|
||||
<switch :checked="item.status === 1" color="#1890ff" scale="0.7" @change="onToggleStatus(item)" />
|
||||
</view>
|
||||
<view class="td" style="flex: 3;">{{ item.addTime }}</view>
|
||||
<view class="td" style="flex: 3;"><text class="td-txt-small">{{ formatTime(item.created_at) }}</text></view>
|
||||
<view class="td" style="flex: 2;">
|
||||
<text class="action-btn" @click="onEdit(item)">编辑</text>
|
||||
<text class="action-btn-del" @click="onDel(item)">删除</text>
|
||||
<view class="divider"></view>
|
||||
<text class="action-btn danger" @click="onDelete(item)">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页栏 -->
|
||||
<view class="pagination-footer">
|
||||
<text class="total-txt">共 {{ total }} 条</text>
|
||||
<view class="page-btns">
|
||||
<text :class="['p-btn', page <= 1 ? 'disabled' : '']" @click="prevPage"> < </text>
|
||||
<text class="p-btn active">{{ page }}</text>
|
||||
<text :class="['p-btn', staffList.length < pageSize ? 'disabled' : '']" @click="nextPage"> > </text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 添加/编辑 弹窗 -->
|
||||
<view v-if="showModal" class="modal-overlay" @click="closeModal">
|
||||
<view class="modal-card" @click.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">{{ isEdit ? '编辑配送员' : '添加配送员' }}</text>
|
||||
<text class="close-btn" @click="closeModal">×</text>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="form-item">
|
||||
<text class="f-label">名称:</text>
|
||||
<input class="f-input" v-model="form.nickname" placeholder="请输入名称" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="f-label">手机号:</text>
|
||||
<input class="f-input" v-model="form.phone" type="number" placeholder="请输入手机号" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="f-label">头像:</text>
|
||||
<view class="upload-placeholder" @click="handleUpload">
|
||||
<image v-if="form.avatar" :src="form.avatar" mode="aspectFill" class="avatar-preview" />
|
||||
<text v-else>+</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<button class="btn" @click="closeModal">取消</button>
|
||||
<button class="btn btn-primary" @click="handleSave">保存</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { fetchDeliveryStaffPage, saveDeliveryStaff, deleteDeliveryStaff, type DeliveryStaff } from '@/services/admin/deliveryService.uts'
|
||||
|
||||
type CourierItem = {
|
||||
id: number
|
||||
avatar: string
|
||||
name: string
|
||||
phone: string
|
||||
isshow: boolean
|
||||
addTime: string
|
||||
const staffList = ref<DeliveryStaff[]>([])
|
||||
const loading = ref(false)
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = 15
|
||||
|
||||
// Modal state
|
||||
const showModal = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const form = reactive({
|
||||
id: '' as string | null,
|
||||
nickname: '',
|
||||
phone: '',
|
||||
avatar: '',
|
||||
status: 1
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchDeliveryStaffPage(page.value, pageSize)
|
||||
staffList.value = res.items
|
||||
total.value = res.total
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const courierList = reactive<CourierItem[]>([
|
||||
{ id: 106, avatar: '/static/logo.png', name: 'cheshi', phone: '18943652356', isshow: true, addTime: '2025-06-29 21:45:19' },
|
||||
{ id: 105, avatar: '/static/logo.png', name: 'dl', phone: '15648569914', isshow: true, addTime: '2025-06-28 18:40:26' },
|
||||
{ id: 102, avatar: '/static/logo.png', name: '小牛马', phone: '13548652258', isshow: true, addTime: '2025-06-26 15:14:40' }
|
||||
])
|
||||
|
||||
function onAdd() {
|
||||
console.log('Add courier')
|
||||
isEdit.value = false
|
||||
form.id = null
|
||||
form.nickname = ''
|
||||
form.phone = ''
|
||||
form.avatar = ''
|
||||
form.status = 1
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function onToggleShow(item: CourierItem) {
|
||||
item.isshow = !item.isshow
|
||||
function onEdit(item : DeliveryStaff) {
|
||||
isEdit.value = true
|
||||
form.id = item.id
|
||||
form.nickname = item.nickname
|
||||
form.phone = item.phone
|
||||
form.avatar = item.avatar || ''
|
||||
form.status = item.status
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function onEdit(item: CourierItem) {
|
||||
console.log('Edit:', item.name)
|
||||
function closeModal() {
|
||||
showModal.value = false
|
||||
}
|
||||
|
||||
function onDel(item: CourierItem) {
|
||||
console.log('Delete:', item.id)
|
||||
async function handleSave() {
|
||||
if (!form.nickname || !form.phone) {
|
||||
uni.showToast({ title: '请填写姓名和手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const resId = await saveDeliveryStaff(form)
|
||||
if (resId != null) {
|
||||
uni.showToast({ title: '保存成功' })
|
||||
closeModal()
|
||||
loadData()
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onDelete(item : DeliveryStaff) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: `确定要删除配送员 "${item.nickname}" 吗?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
const ok = await deleteDeliveryStaff(item.id)
|
||||
if (ok) {
|
||||
uni.showToast({ title: '删除成功' })
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function onToggleStatus(item : DeliveryStaff) {
|
||||
const nextStatus = item.status === 1 ? 0 : 1
|
||||
const ok = await saveDeliveryStaff({ ...item, status: nextStatus })
|
||||
if (ok != null) {
|
||||
item.status = nextStatus
|
||||
uni.showToast({ title: '状态已更新' })
|
||||
}
|
||||
}
|
||||
|
||||
function prevPage() { if (page.value > 1) { page.value--; loadData(); } }
|
||||
function nextPage() { if (staffList.value.length >= pageSize) { page.value++; loadData(); } }
|
||||
|
||||
function formatTime(iso : string | null) : string {
|
||||
if (!iso) return '-'
|
||||
return iso.substring(0, 16).replace('T', ' ')
|
||||
}
|
||||
|
||||
function handleUpload() {
|
||||
uni.showToast({ title: '上传功能开发中', icon: 'none' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-page-container {
|
||||
padding: 15px;
|
||||
background-color: #f5f7f9;
|
||||
min-height: 100vh;
|
||||
}
|
||||
<style scoped lang="scss">
|
||||
.admin-page-container { padding: 24px; background-color: #f5f7f9; min-height: 100vh; }
|
||||
.page-card { background-color: #fff; border-radius: 4px; padding: 24px; }
|
||||
|
||||
.page-card {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
}
|
||||
.action-wrap { margin-bottom: 24px; }
|
||||
.btn { height: 32px; padding: 0 20px; border-radius: 4px; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; }
|
||||
.btn-primary { background-color: #1890ff; color: #fff; }
|
||||
|
||||
.action-wrap {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.table-wrap { border: 1px solid #f0f0f0; border-radius: 4px; }
|
||||
.table-header { display: flex; flex-direction: row; background-color: #f8f8f9; }
|
||||
.th { padding: 12px 10px; font-size: 14px; font-weight: bold; color: #515a6e; border-bottom: 1px solid #f0f0f0; text-align: center; }
|
||||
.tr { display: flex; flex-direction: row; border-bottom: 1px solid #f0f0f0; min-height: 60px; align-items: center; }
|
||||
.td { padding: 10px; display: flex; align-items: center; justify-content: center; }
|
||||
.td-txt { font-size: 13px; color: #606266; }
|
||||
.td-txt-small { font-size: 12px; color: #999; }
|
||||
|
||||
.btn {
|
||||
height: 32px;
|
||||
line-height: 30px;
|
||||
padding: 0 15px;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
}
|
||||
.avatar { width: 40px; height: 40px; border-radius: 4px; }
|
||||
.avatar-placeholder { width: 40px; height: 40px; border-radius: 4px; background-color: #f0f0f0; display: flex; align-items: center; justify-content: center; font-size: 20px; }
|
||||
|
||||
.btn-primary {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
.action-btn { color: #1890ff; font-size: 13px; cursor: pointer; }
|
||||
.danger { color: #ff4d4f; }
|
||||
.divider { width: 1px; height: 12px; background-color: #e8e8e8; margin: 0 8px; }
|
||||
|
||||
.table-wrap {
|
||||
width: 100%;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
.no-data, .loading-box { padding: 60px 0; text-align: center; width: 100%; }
|
||||
.no-data-text { font-size: 14px; color: #ccc; }
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #f8f8f9;
|
||||
}
|
||||
.pagination-footer { margin-top: 24px; display: flex; flex-direction: row; align-items: center; justify-content: flex-end; gap: 12px; }
|
||||
.total-txt { font-size: 13px; color: #999; }
|
||||
.page-btns { display: flex; flex-direction: row; gap: 8px; }
|
||||
.p-btn { width: 30px; height: 30px; border: 1px solid #dcdee2; display: flex; align-items: center; justify-content: center; border-radius: 4px; cursor: pointer; font-size: 13px; }
|
||||
.p-btn.active { background-color: #1890ff; color: #fff; border-color: #1890ff; }
|
||||
.p-btn.disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
.th {
|
||||
padding: 12px 10px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #515a6e;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
text-align: center;
|
||||
}
|
||||
/* Modal */
|
||||
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0,0,0,0.5); z-index: 1000; display: flex; align-items: center; justify-content: center; }
|
||||
.modal-card { width: 450px; background-color: #fff; border-radius: 8px; overflow: hidden; }
|
||||
.modal-header { padding: 16px 20px; border-bottom: 1px solid #f0f0f0; display: flex; flex-direction: row; justify-content: space-between; align-items: center; }
|
||||
.modal-title { font-size: 16px; font-weight: bold; color: #333; }
|
||||
.close-btn { font-size: 24px; color: #999; cursor: pointer; }
|
||||
.modal-body { padding: 24px; }
|
||||
.form-item { margin-bottom: 20px; display: flex; flex-direction: row; align-items: center; }
|
||||
.f-label { width: 80px; font-size: 14px; color: #666; text-align: right; margin-right: 15px; }
|
||||
.f-input { flex: 1; border: 1px solid #dcdfe6; height: 36px; padding: 0 12px; border-radius: 4px; font-size: 14px; }
|
||||
|
||||
.tr {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.upload-placeholder { width: 64px; height: 64px; border: 1px dashed #dcdfe6; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 24px; color: #ccc; cursor: pointer; }
|
||||
.avatar-preview { width: 100%; height: 100%; border-radius: 4px; }
|
||||
|
||||
.td {
|
||||
padding: 12px 10px;
|
||||
font-size: 14px;
|
||||
color: #515a6e;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
color: #1890ff;
|
||||
font-size: 13px;
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-btn-del {
|
||||
color: #ed4014;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.modal-footer { padding: 16px 24px; border-top: 1px solid #f0f0f0; display: flex; flex-direction: row; justify-content: flex-end; gap: 12px; }
|
||||
</style>
|
||||
|
||||
@@ -5,16 +5,9 @@
|
||||
<view class="search-wrap">
|
||||
<view class="search-item">
|
||||
<text class="label">提货点搜索:</text>
|
||||
<input class="input" placeholder="请输入提货点名称,电话" v-model="searchKey" />
|
||||
<input class="input" placeholder="请输入提货点名称,电话" v-model="searchKey" @confirm="handleQuery" />
|
||||
</view>
|
||||
<button class="btn btn-primary" @click="onSearch">查询</button>
|
||||
</view>
|
||||
|
||||
<!-- 状态 Tabs -->
|
||||
<view class="tabs-wrap">
|
||||
<view class="tab-item active"><text class="tab-text">显示中的提货点(2)</text></view>
|
||||
<view class="tab-item"><text class="tab-text">隐藏中的提货点(0)</text></view>
|
||||
<view class="tab-item"><text class="tab-text">回收站中的提货点(9)</text></view>
|
||||
<button class="btn btn-primary" @click="handleQuery">查询</button>
|
||||
</view>
|
||||
|
||||
<view class="action-wrap">
|
||||
@@ -22,209 +15,272 @@
|
||||
</view>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<view class="table-wrap">
|
||||
<view class="table-wrap border-shadow">
|
||||
<view class="table-header">
|
||||
<view class="th" style="flex: 1;">ID</view>
|
||||
<view class="th" style="flex: 1;">序号</view>
|
||||
<view class="th" style="flex: 2;">提货点图片</view>
|
||||
<view class="th" style="flex: 2;">提货点名称</view>
|
||||
<view class="th" style="flex: 2;">提货点电话</view>
|
||||
<view class="th" style="flex: 3;">地址</view>
|
||||
<view class="th" style="flex: 3;">营业时间</view>
|
||||
<view class="th" style="flex: 1.5;">是否显示</view>
|
||||
<view class="th" style="flex: 2;">操作</view>
|
||||
</view>
|
||||
<view class="table-body">
|
||||
<view v-for="item in stationList" :key="item.id" class="tr">
|
||||
<view class="td" style="flex: 1;">{{ item.id }}</view>
|
||||
<view v-if="loading" class="loading-box">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="stationList.length === 0" class="no-data">
|
||||
<text class="no-data-text">暂无提货点数据</text>
|
||||
</view>
|
||||
<view v-else v-for="(item, index) in stationList" :key="item.id" class="tr">
|
||||
<view class="td" style="flex: 1;"><text class="td-txt">{{ (page - 1) * pageSize + index + 1 }}</text></view>
|
||||
<view class="td" style="flex: 2;">
|
||||
<image class="station-img" :src="item.image" mode="aspectFill" />
|
||||
<image v-if="item.image" class="station-img" :src="item.image" mode="aspectFill" />
|
||||
<view v-else class="img-placeholder"><text>🖼️</text></view>
|
||||
</view>
|
||||
<view class="td" style="flex: 2;">{{ item.name }}</view>
|
||||
<view class="td" style="flex: 2;">{{ item.phone }}</view>
|
||||
<view class="td" style="flex: 3;">{{ item.address }}</view>
|
||||
<view class="td" style="flex: 3;">{{ item.hours }}</view>
|
||||
<view class="td" style="flex: 2;"><text class="td-txt">{{ item.name }}</text></view>
|
||||
<view class="td" style="flex: 2;"><text class="td-txt">{{ item.phone }}</text></view>
|
||||
<view class="td" style="flex: 3;"><text class="td-txt ellipsis-2">{{ item.address }}</text></view>
|
||||
<view class="td" style="flex: 1.5;">
|
||||
<switch :checked="item.isshow" color="#1890ff" />
|
||||
<switch :checked="item.status === 1" color="#1890ff" scale="0.7" @change="onToggleStatus(item)" />
|
||||
</view>
|
||||
<view class="td" style="flex: 2;">
|
||||
<text class="action-btn" @click="onEdit(item)">编辑</text>
|
||||
<text class="action-btn-del" @click="onDel(item)">删除</text>
|
||||
<view class="divider"></view>
|
||||
<text class="action-btn danger" @click="onDelete(item)">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页栏 -->
|
||||
<view class="pagination-footer">
|
||||
<text class="total-txt">共 {{ total }} 条</text>
|
||||
<view class="page-btns">
|
||||
<text :class="['p-btn', page <= 1 ? 'disabled' : '']" @click="prevPage"> < </text>
|
||||
<text class="p-btn active">{{ page }}</text>
|
||||
<text :class="['p-btn', stationList.length < pageSize ? 'disabled' : '']" @click="nextPage"> > </text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 添加/编辑 弹窗 -->
|
||||
<view v-if="showModal" class="modal-overlay" @click="closeModal">
|
||||
<view class="modal-card" @click.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">{{ isEdit ? '编辑提货点' : '添加提货点' }}</text>
|
||||
<text class="close-btn" @click="closeModal">×</text>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<scroll-view scroll-y="true" style="max-height: 500px;">
|
||||
<view class="form-item">
|
||||
<text class="f-label">提货点名称:</text>
|
||||
<input class="f-input" v-model="form.name" placeholder="请输入名称" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="f-label">联系电话:</text>
|
||||
<input class="f-input" v-model="form.phone" type="number" placeholder="请输入电话" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="f-label">详细地址:</text>
|
||||
<input class="f-input" v-model="form.address" placeholder="请输入详细地址" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="f-label">展示图片:</text>
|
||||
<view class="upload-placeholder" @click="handleUpload">
|
||||
<image v-if="form.image" :src="form.image" mode="aspectFill" class="img-preview" />
|
||||
<text v-else>+</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<button class="btn" @click="closeModal">取消</button>
|
||||
<button class="btn btn-primary" @click="handleSave">保存</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { fetchDeliveryStationPage, saveDeliveryStation, deleteDeliveryStation, type DeliveryStation } from '@/services/admin/deliveryService.uts'
|
||||
|
||||
const stationList = ref<DeliveryStation[]>([])
|
||||
const loading = ref(false)
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = 15
|
||||
const searchKey = ref('')
|
||||
|
||||
type StationItem = {
|
||||
id: number
|
||||
image: string
|
||||
name: string
|
||||
phone: string
|
||||
address: string
|
||||
hours: string
|
||||
isshow: boolean
|
||||
// Modal state
|
||||
const showModal = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const form = reactive({
|
||||
id: '' as string | null,
|
||||
name: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
image: '',
|
||||
status: 1,
|
||||
sort_order: 0
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchDeliveryStationPage(page.value, pageSize, searchKey.value || null)
|
||||
stationList.value = res.items
|
||||
total.value = res.total
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const stationList = reactive<StationItem[]>([
|
||||
{ id: 46, image: '/static/logo.png', name: '提货点222', phone: '13769102384', address: '能看见你的困难', hours: '00:00:00 - 23:00:00', isshow: true },
|
||||
{ id: 44, image: '/static/logo.png', name: '美东科技', phone: '15912341234', address: '襄阳火车站', hours: '08:00:00 - 22:00:00', isshow: true }
|
||||
])
|
||||
|
||||
function onSearch() {
|
||||
console.log('Search:', searchKey.value)
|
||||
function handleQuery() {
|
||||
page.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function onAdd() {
|
||||
console.log('Add station')
|
||||
isEdit.value = false
|
||||
form.id = null
|
||||
form.name = ''
|
||||
form.phone = ''
|
||||
form.address = ''
|
||||
form.image = ''
|
||||
form.status = 1
|
||||
form.sort_order = 0
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function onEdit(item: StationItem) {
|
||||
console.log('Edit:', item.name)
|
||||
function onEdit(item : DeliveryStation) {
|
||||
isEdit.value = true
|
||||
form.id = item.id
|
||||
form.name = item.name
|
||||
form.phone = item.phone
|
||||
form.address = item.address
|
||||
form.image = item.image || ''
|
||||
form.status = item.status
|
||||
form.sort_order = item.sort_order
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function onDel(item: StationItem) {
|
||||
console.log('Delete:', item.id)
|
||||
function closeModal() {
|
||||
showModal.value = false
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.name || !form.phone || !form.address) {
|
||||
uni.showToast({ title: '请完善必要信息', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const resId = await saveDeliveryStation(form)
|
||||
if (resId != null) {
|
||||
uni.showToast({ title: '保存成功' })
|
||||
closeModal()
|
||||
loadData()
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onDelete(item : DeliveryStation) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: `确定要删除提货点 "${item.name}" 吗?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
const ok = await deleteDeliveryStation(item.id)
|
||||
if (ok) {
|
||||
uni.showToast({ title: '删除成功' })
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function onToggleStatus(item : DeliveryStation) {
|
||||
const nextStatus = item.status === 1 ? 0 : 1
|
||||
const ok = await saveDeliveryStation({ ...item, status: nextStatus })
|
||||
if (ok != null) {
|
||||
item.status = nextStatus
|
||||
uni.showToast({ title: '状态已更新' })
|
||||
}
|
||||
}
|
||||
|
||||
function prevPage() { if (page.value > 1) { page.value--; loadData(); } }
|
||||
function nextPage() { if (stationList.value.length >= pageSize) { page.value++; loadData(); } }
|
||||
|
||||
function handleUpload() {
|
||||
uni.showToast({ title: '上传功能开发中', icon: 'none' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-page-container {
|
||||
padding: 15px;
|
||||
background-color: #f5f7f9;
|
||||
min-height: 100vh;
|
||||
}
|
||||
<style scoped lang="scss">
|
||||
.admin-page-container { padding: 24px; background-color: #f5f7f9; min-height: 100vh; }
|
||||
.page-card { background-color: #fff; border-radius: 4px; padding: 24px; }
|
||||
|
||||
.page-card {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
}
|
||||
.search-wrap { display: flex; flex-direction: row; align-items: center; padding-bottom: 24px; border-bottom: 1px solid #f0f0f0; margin-bottom: 24px; }
|
||||
.search-item { display: flex; flex-direction: row; align-items: center; margin-right: 24px; }
|
||||
.label { font-size: 14px; color: #606266; margin-right: 8px; }
|
||||
.input { width: 250px; height: 32px; border: 1px solid #dcdfe6; border-radius: 4px; padding: 0 12px; font-size: 14px; }
|
||||
|
||||
.search-wrap {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.btn { height: 32px; padding: 0 20px; border-radius: 4px; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; }
|
||||
.btn-primary { background-color: #1890ff; color: #fff; }
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
.action-wrap { margin-bottom: 20px; }
|
||||
|
||||
.input {
|
||||
width: 250px;
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-right: 15px;
|
||||
}
|
||||
.table-wrap { border: 1px solid #f0f0f0; border-radius: 4px; }
|
||||
.table-header { display: flex; flex-direction: row; background-color: #f8f8f9; }
|
||||
.th { padding: 12px 10px; font-size: 14px; font-weight: bold; color: #515a6e; border-bottom: 1px solid #f0f0f0; text-align: center; }
|
||||
.tr { display: flex; flex-direction: row; border-bottom: 1px solid #f0f0f0; min-height: 60px; align-items: center; }
|
||||
.td { padding: 10px; display: flex; align-items: center; justify-content: center; }
|
||||
.td-txt { font-size: 13px; color: #606266; text-align: center; }
|
||||
.ellipsis-2 { overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
||||
|
||||
.tabs-wrap {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
}
|
||||
.station-img { width: 44px; height: 44px; border-radius: 4px; background-color: #f5f5f5; }
|
||||
.img-placeholder { width: 44px; height: 44px; border-radius: 4px; background-color: #f0f0f0; display: flex; align-items: center; justify-content: center; font-size: 20px; }
|
||||
|
||||
.tab-item {
|
||||
padding: 10px 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.action-btn { color: #1890ff; font-size: 13px; cursor: pointer; }
|
||||
.danger { color: #ff4d4f; }
|
||||
.divider { width: 1px; height: 12px; background-color: #e8e8e8; margin: 0 8px; }
|
||||
|
||||
.tab-item.active {
|
||||
border-bottom: 2px solid #1890ff;
|
||||
}
|
||||
.no-data, .loading-box { padding: 60px 0; text-align: center; width: 100%; }
|
||||
.no-data-text { font-size: 14px; color: #ccc; }
|
||||
|
||||
.tab-item.active .tab-text {
|
||||
color: #1890ff;
|
||||
}
|
||||
.pagination-footer { margin-top: 24px; display: flex; flex-direction: row; align-items: center; justify-content: flex-end; gap: 12px; }
|
||||
.total-txt { font-size: 13px; color: #999; }
|
||||
.page-btns { display: flex; flex-direction: row; gap: 8px; }
|
||||
.p-btn { width: 30px; height: 30px; border: 1px solid #dcdee2; display: flex; align-items: center; justify-content: center; border-radius: 4px; cursor: pointer; font-size: 13px; }
|
||||
.p-btn.active { background-color: #1890ff; color: #fff; border-color: #1890ff; }
|
||||
.p-btn.disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
.tab-text {
|
||||
font-size: 14px;
|
||||
color: #515a6e;
|
||||
}
|
||||
/* Modal */
|
||||
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0,0,0,0.5); z-index: 1000; display: flex; align-items: center; justify-content: center; }
|
||||
.modal-card { width: 500px; background-color: #fff; border-radius: 8px; overflow: hidden; }
|
||||
.modal-header { padding: 16px 20px; border-bottom: 1px solid #f0f0f0; display: flex; flex-direction: row; justify-content: space-between; align-items: center; }
|
||||
.modal-title { font-size: 16px; font-weight: bold; color: #333; }
|
||||
.close-btn { font-size: 24px; color: #999; cursor: pointer; }
|
||||
.modal-body { padding: 24px; }
|
||||
.form-item { margin-bottom: 20px; display: flex; flex-direction: row; align-items: center; }
|
||||
.f-label { width: 90px; font-size: 14px; color: #666; text-align: right; margin-right: 15px; }
|
||||
.f-input { flex: 1; border: 1px solid #dcdfe6; height: 36px; padding: 0 12px; border-radius: 4px; font-size: 14px; }
|
||||
|
||||
.action-wrap {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.upload-placeholder { width: 64px; height: 64px; border: 1px dashed #dcdfe6; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 24px; color: #ccc; cursor: pointer; }
|
||||
.img-preview { width: 100%; height: 100%; border-radius: 4px; }
|
||||
|
||||
.btn {
|
||||
height: 32px;
|
||||
line-height: 30px;
|
||||
padding: 0 15px;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
width: 100%;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #f8f8f9;
|
||||
}
|
||||
|
||||
.th {
|
||||
padding: 12px 10px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #515a6e;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tr {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.td {
|
||||
padding: 12px 10px;
|
||||
font-size: 14px;
|
||||
color: #515a6e;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.station-img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
color: #1890ff;
|
||||
font-size: 13px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.action-btn-del {
|
||||
color: #ed4014;
|
||||
font-size: 13px;
|
||||
}
|
||||
.modal-footer { padding: 16px 24px; border-top: 1px solid #f0f0f0; display: flex; flex-direction: row; justify-content: flex-end; gap: 12px; }
|
||||
</style>
|
||||
|
||||
@@ -2,332 +2,234 @@
|
||||
<view class="admin-page">
|
||||
<view class="admin-sections">
|
||||
<view class="admin-card settings-card">
|
||||
<!-- 顶部导航标签 (1:1 复刻 CRMEB: 横向排列) -->
|
||||
<!-- 顶部导航标签 -->
|
||||
<view class="tabs-container">
|
||||
<scroll-view class="tabs-scroll" scroll-x="true" show-scrollbar="false" :enable-flex="true">
|
||||
<view class="tabs-bar">
|
||||
<view
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="index"
|
||||
class="tab-item"
|
||||
:class="{ active: currentTab === index }"
|
||||
@click="currentTab = index"
|
||||
>
|
||||
<text class="tab-text">{{ tab.name }}</text>
|
||||
<view class="tab-line" v-if="currentTab === index"></view>
|
||||
<scroll-view class="tabs-scroll" scroll-x="true" show-scrollbar="false" :enable-flex="true">
|
||||
<view class="tabs-bar">
|
||||
<view
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="index"
|
||||
class="tab-item"
|
||||
:class="{ active: currentTab === index }"
|
||||
@click="currentTab = index"
|
||||
>
|
||||
<text class="tab-text">{{ tab.name }}</text>
|
||||
<view class="tab-line" v-if="currentTab === index"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view class="form-container">
|
||||
|
||||
<!-- 1. 基础配置 -->
|
||||
<view v-if="currentTab === 0" class="form-content">
|
||||
<view class="form-item">
|
||||
<view class="form-label">站点开启:</view>
|
||||
<view class="form-right">
|
||||
<radio-group class="radio-group" @change="formData.site_open = parseInt(($event.detail.value as string))">
|
||||
<label class="radio-label"><radio value="1" :checked="formData.site_open == 1" color="#1890ff" />开启</label>
|
||||
<label class="radio-label"><radio value="0" :checked="formData.site_open == 0" color="#1890ff" />关闭</label>
|
||||
</radio-group>
|
||||
<view class="form-tip">站点开启/关闭(用于升级等临时关闭),关闭后前端会弹窗显示站点升级中,请稍后访问</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="form-label">网站名称:</view>
|
||||
<view class="form-right">
|
||||
<input class="form-input" v-model="formData.site_name" placeholder="请输入网站名称" />
|
||||
<view class="form-tip">网站名称很多地方会显示的,建议认真填写</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="form-label">网站地址:</view>
|
||||
<view class="form-right">
|
||||
<input class="form-input" v-model="formData.site_url" placeholder="请输入网站地址" />
|
||||
<view class="form-tip">安装自动配置,不要轻易修改,更换后会影响网站访问、接口请求、本地文件储存、支付回调、微信授权、支付、小程序图片访问、部分二维码、官方授权等</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="form-label">消息队列:</view>
|
||||
<view class="form-right">
|
||||
<radio-group class="radio-group" @change="formData.msg_queue = parseInt(($event.detail.value as string))">
|
||||
<label class="radio-label"><radio value="1" :checked="formData.msg_queue == 1" color="#1890ff" />开启</label>
|
||||
<label class="radio-label"><radio value="0" :checked="formData.msg_queue == 0" color="#1890ff" />关闭</label>
|
||||
</radio-group>
|
||||
<view class="form-tip">是否启用消息队列,启用后提升程序运行速度,启用前必须配置Redis缓存,文档地址:https://doc.crmeb.com/single/crmeb_v4/7217</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="form-label">联系电话:</view>
|
||||
<view class="form-right">
|
||||
<input class="form-input" v-model="formData.contact_phone" placeholder="请输入联系电话" />
|
||||
<view class="form-tip">联系电话</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="form-label">授权密钥:</view>
|
||||
<view class="form-right">
|
||||
<input class="form-input" v-model="formData.auth_key" placeholder="请输入授权密钥" />
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 2. 分享配置 -->
|
||||
<view v-else-if="currentTab === 1" class="form-content">
|
||||
<view class="form-item">
|
||||
<view class="form-label">分享图片:</view>
|
||||
<view class="form-right">
|
||||
<view class="upload-placeholder" @click="handleUpload('share_img')">上传图片</view>
|
||||
<view class="form-tip">分享图片比例5:4,建议小于50KB</view>
|
||||
</view>
|
||||
<!-- 表单区域 -->
|
||||
<view class="form-container">
|
||||
<view v-if="isLoading" class="loading-state">
|
||||
<text>加载配置中...</text>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="form-label">分享标题:</view>
|
||||
<view class="form-right">
|
||||
<input class="form-input" v-model="formData.share_title" />
|
||||
|
||||
<view v-else class="form-scroll-wrap">
|
||||
<!-- 1. 基础配置 -->
|
||||
<view v-if="currentTab === 0" class="form-content">
|
||||
<view class="form-item">
|
||||
<view class="form-label">站点开启:</view>
|
||||
<view class="form-right">
|
||||
<radio-group class="radio-group" @change="formData.site_open = parseInt(($event.detail.value as string))">
|
||||
<label class="radio-label"><radio value="1" :checked="formData.site_open == 1" color="#1890ff" />开启</label>
|
||||
<label class="radio-label"><radio value="0" :checked="formData.site_open == 0" color="#1890ff" />关闭</label>
|
||||
</radio-group>
|
||||
<view class="form-tip">站点开启/关闭(用于升级等临时关闭)</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="form-label">网站名称:</view>
|
||||
<view class="form-right">
|
||||
<input class="form-input" v-model="formData.site_name" placeholder="请输入网站名称" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="form-label">网站地址:</view>
|
||||
<view class="form-right">
|
||||
<input class="form-input" v-model="formData.site_url" placeholder="请输入网站地址" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="form-label">联系电话:</view>
|
||||
<view class="form-right">
|
||||
<input class="form-input" v-model="formData.contact_phone" placeholder="请输入联系电话" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="form-label">分享简介:</view>
|
||||
<view class="form-right">
|
||||
<textarea class="form-textarea" v-model="formData.share_desc" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 3. LOGO配置 -->
|
||||
<view v-else-if="currentTab === 2" class="form-content">
|
||||
<view class="form-item">
|
||||
<view class="form-label">后台登录LOGO:</view>
|
||||
<view class="form-right">
|
||||
<view class="upload-placeholder" @click="handleUpload('login_logo')">上传截图</view>
|
||||
<view class="form-tip">建议尺寸270*75</view>
|
||||
<!-- 2. 分享配置 -->
|
||||
<view v-else-if="currentTab === 1" class="form-content">
|
||||
<view class="form-item">
|
||||
<view class="form-label">分享标题:</view>
|
||||
<view class="form-right">
|
||||
<input class="form-input" v-model="formData.share_title" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="form-label">分享简介:</view>
|
||||
<view class="form-right">
|
||||
<textarea class="form-textarea" v-model="formData.share_desc" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="form-label">后台小LOGO:</view>
|
||||
<view class="form-right">
|
||||
<view class="upload-placeholder" @click="handleUpload('small_logo')">上传图片</view>
|
||||
<view class="form-tip">建议尺寸180*180</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="form-label">后台大LOGO:</view>
|
||||
<view class="form-right">
|
||||
<view class="upload-placeholder" @click="handleUpload('big_logo')">上传图片</view>
|
||||
<view class="form-tip">建议尺寸170*50</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 4. 自定义JS -->
|
||||
<view v-else-if="currentTab === 3" class="form-content">
|
||||
<view class="form-item">
|
||||
<view class="form-label">移动端JS:</view>
|
||||
<view class="form-right">
|
||||
<textarea class="form-textarea code-bg" v-model="formData.mobile_js" />
|
||||
<!-- 3. LOGO配置 -->
|
||||
<view v-else-if="currentTab === 2" class="form-content">
|
||||
<view class="form-item">
|
||||
<view class="form-label">后台登录LOGO:</view>
|
||||
<view class="form-right">
|
||||
<view class="upload-placeholder" @click="handleUpload('login_logo')">
|
||||
<image v-if="formData.login_logo" :src="formData.login_logo" mode="aspectFit" class="logo-preview" />
|
||||
<text v-else>上传图片</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="form-label">管理端JS:</view>
|
||||
<view class="form-right">
|
||||
<textarea class="form-textarea code-bg" v-model="formData.admin_js" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="form-label">PC端JS:</view>
|
||||
<view class="form-right">
|
||||
<textarea class="form-textarea code-bg" v-model="formData.pc_js" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 5. 地图配置 -->
|
||||
<view v-else-if="currentTab === 4" class="form-content">
|
||||
<view class="form-item">
|
||||
<view class="form-label">腾讯地图KEY:</view>
|
||||
<view class="form-right">
|
||||
<input class="form-input" v-model="formData.tencent_map_key" />
|
||||
<view class="form-tip">申请地址:https://lbs.qq.com</view>
|
||||
<!-- 9. WAF配置 -->
|
||||
<view v-else-if="currentTab === 8" class="form-content">
|
||||
<view class="form-item">
|
||||
<view class="form-label">WAF类型:</view>
|
||||
<view class="form-right">
|
||||
<radio-group class="radio-group" @change="formData.waf_type = parseInt(($event.detail.value as string))">
|
||||
<label class="radio-label"><radio value="0" :checked="formData.waf_type == 0" color="#1890ff" />关闭</label>
|
||||
<label class="radio-label"><radio value="1" :checked="formData.waf_type == 1" color="#1890ff" />拦截</label>
|
||||
<label class="radio-label"><radio value="2" :checked="formData.waf_type == 2" color="#1890ff" />过滤</label>
|
||||
</radio-group>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="form-label">WAF规则:</view>
|
||||
<view class="form-right">
|
||||
<textarea class="form-textarea code-bg" v-model="formData.waf_config" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 6. 备案配置 -->
|
||||
<view v-else-if="currentTab === 5" class="form-content">
|
||||
<view class="form-item">
|
||||
<view class="form-label">备案号:</view>
|
||||
<view class="form-right">
|
||||
<input class="form-input" v-model="formData.filing_no" />
|
||||
<!-- 提交按钮 -->
|
||||
<view class="submit-section">
|
||||
<view class="form-label"></view>
|
||||
<view class="form-right">
|
||||
<button class="btn-submit" :disabled="isSaving" @click="handleSubmit">
|
||||
<text class="btn-txt">{{ isSaving ? '保存中...' : '提交' }}</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="form-label">ICP链接:</view>
|
||||
<view class="form-right">
|
||||
<input class="form-input" v-model="formData.icp_link" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 7. 模块配置 -->
|
||||
<view v-else-if="currentTab === 6" class="form-content">
|
||||
<view class="form-item">
|
||||
<view class="form-label">功能开启:</view>
|
||||
<view class="form-right">
|
||||
<checkbox-group class="checkbox-group" @change="formData.module_config = ($event.detail.value as string[])">
|
||||
<label class="checkbox-label"><checkbox value="秒杀" :checked="formData.module_config.includes('秒杀')" color="#1890ff" />秒杀</label>
|
||||
<label class="checkbox-label"><checkbox value="砍价" :checked="formData.module_config.includes('砍价')" color="#1890ff" />砍价</label>
|
||||
<label class="checkbox-label"><checkbox value="拼团" :checked="formData.module_config.includes('拼团')" color="#1890ff" />拼团</label>
|
||||
</checkbox-group>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 8. 远程登录 -->
|
||||
<view v-else-if="currentTab === 7" class="form-content">
|
||||
<view class="form-item">
|
||||
<view class="form-label">远程登录地址:</view>
|
||||
<view class="form-right">
|
||||
<input class="form-input" v-model="formData.remote_login_url" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 9. WAF配置 -->
|
||||
<view v-else-if="currentTab === 8" class="form-content">
|
||||
<view class="form-item">
|
||||
<view class="form-label">WAF类型:</view>
|
||||
<view class="form-right">
|
||||
<radio-group class="radio-group" @change="formData.waf_type = parseInt(($event.detail.value as string))">
|
||||
<label class="radio-label"><radio value="0" :checked="formData.waf_type == 0" color="#1890ff" />关闭</label>
|
||||
<label class="radio-label"><radio value="1" :checked="formData.waf_type == 1" color="#1890ff" />拦截</label>
|
||||
<label class="radio-label"><radio value="2" :checked="formData.waf_type == 2" color="#1890ff" />过滤</label>
|
||||
</radio-group>
|
||||
<view class="form-tip">WAF类型:关闭(所有参数都能正常请求),拦截(匹配到WAF配置的参数阻断接口请求),过滤(匹配到WAF配置的参数过滤参数,正常请求接口)</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="form-label">WAF配置:</view>
|
||||
<view class="form-right">
|
||||
<textarea class="form-textarea code-bg waf-textarea" v-model="formData.waf_config" />
|
||||
<view class="form-tip">WAF配置验证参数,过滤掉不需要的参数或拦截请求,多个参数用回车换行分隔</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="submit-section">
|
||||
<view class="form-label"></view> <!-- 占位用于对齐 -->
|
||||
<view class="form-right">
|
||||
<button class="btn-submit" @click="handleSubmit">提交</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from "vue"
|
||||
import { ref, reactive, onMounted } from "vue"
|
||||
import { getSystemConfig, saveSystemConfig } from "@/services/admin/systemConfigService.uts"
|
||||
|
||||
const currentTab = ref(0)
|
||||
const tabs = [
|
||||
{ name: "基础配置" }, { name: "分享配置" }, { name: "LOGO配置" },
|
||||
{ name: "自定义JS" }, { name: "地图配置" }, { name: "备案配置" },
|
||||
{ name: "模块配置" }, { name: "远程登录配置" }, { name: "WAF配置" }
|
||||
{ name: "模块配置" }, { name: "远程登录" }, { name: "WAF配置" }
|
||||
]
|
||||
|
||||
const formData = ref({
|
||||
const isLoading = ref(false)
|
||||
const isSaving = ref(false)
|
||||
|
||||
const formData = reactive({
|
||||
site_open: 1,
|
||||
site_name: "CRMEB标准版",
|
||||
site_url: "https://v5.crmeb.net",
|
||||
site_name: "",
|
||||
site_url: "",
|
||||
msg_queue: 0,
|
||||
contact_phone: "",
|
||||
auth_key: "AO9azvBW9vEcOH7swklTM0RYRb6EB4RLWMSD88MnKTi8Vd6cjXVd",
|
||||
auth_key: "",
|
||||
share_img: "",
|
||||
share_title: "CRMEB v5标准版",
|
||||
share_desc: "完善的文档 全心而来!",
|
||||
share_title: "",
|
||||
share_desc: "",
|
||||
login_logo: "",
|
||||
small_logo: "",
|
||||
big_logo: "",
|
||||
mobile_js: "",
|
||||
admin_js: "",
|
||||
pc_js: "",
|
||||
tencent_map_key: "SMJBZ-WCHK4-ZPZUA-DSIXI-XDDVQ-XWFX7",
|
||||
filing_no: "陕ICP备14011498号-3",
|
||||
icp_link: "https://beian.miit.gov.cn/",
|
||||
module_config: ["秒杀", "砍价", "拼团"] as string[],
|
||||
tencent_map_key: "",
|
||||
filing_no: "",
|
||||
icp_link: "",
|
||||
module_config: [] as string[],
|
||||
remote_login_url: "",
|
||||
waf_type: 2,
|
||||
waf_config: "/\\.\\.\\//\n/\\<\\?/\n/\\bor\\b.*=\\s*\\*/i\n/(select[\\s\\S]*?)(from|limit)/i\n/(union[\\s\\S]*?select)/i\n/(having\\s+updatexml|extractvalue)/i"
|
||||
waf_type: 0,
|
||||
waf_config: ""
|
||||
})
|
||||
|
||||
const handleUpload = (field: string) => {
|
||||
uni.showToast({ title: "选择文件: " + field, icon: "none" })
|
||||
onMounted(() => {
|
||||
loadConfig()
|
||||
})
|
||||
|
||||
async function loadConfig() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const res = await getSystemConfig('system_settings')
|
||||
if (res != null) {
|
||||
Object.assign(formData, res as any)
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
uni.showLoading({ title: "保存中..." })
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: "保存成功", icon: "success" })
|
||||
}, 800)
|
||||
async function handleSubmit() {
|
||||
isSaving.value = true
|
||||
try {
|
||||
const ok = await saveSystemConfig('system_settings', formData as UTSJSONObject, '系统全局配置项')
|
||||
if (ok) {
|
||||
uni.showToast({ title: "保存成功", icon: "success" })
|
||||
} else {
|
||||
uni.showToast({ title: "保存失败", icon: "none" })
|
||||
}
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpload = (field: string) => {
|
||||
uni.showToast({ title: "上传功能开发中: " + field, icon: "none" })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-card {
|
||||
box-shadow: 0 1px 4px rgba(0,21,41,0.08);
|
||||
}
|
||||
.admin-page { padding: 24px; background-color: #f0f2f5; min-height: 100vh; }
|
||||
.settings-card { background: #fff; border-radius: 4px; box-shadow: 0 1px 4px rgba(0,21,41,0.08); }
|
||||
|
||||
/* 核心修复:确保 Tabs 横向排列并且可以滑动 */
|
||||
.tabs-container { margin-bottom: 30px; border-bottom: 1px solid #e8eaec; width: 100%; overflow: hidden; }
|
||||
.tabs-container { margin-bottom: 30px; border-bottom: 1px solid #e8eaec; }
|
||||
.tabs-scroll { width: 100%; white-space: nowrap; }
|
||||
.tabs-bar { display: inline-flex; flex-direction: row; min-width: 100%; }
|
||||
|
||||
.tab-item {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 12px 24px;
|
||||
font-size: 14px;
|
||||
color: #515a6e;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tab-item.active { color: #1890ff; font-weight: bold; }
|
||||
.tabs-bar { display: inline-flex; flex-direction: row; }
|
||||
.tab-item { padding: 12px 24px; cursor: pointer; position: relative; }
|
||||
.tab-text { font-size: 14px; color: #515a6e; }
|
||||
.tab-item.active .tab-text { color: #1890ff; font-weight: bold; }
|
||||
.tab-line { position: absolute; bottom: 0; left: 0; right: 0; height: 2px; background-color: #1890ff; }
|
||||
|
||||
.form-container { padding-left: 20px; }
|
||||
.form-container { padding: 0 20px 40px; }
|
||||
.loading-state { padding: 60px; text-align: center; color: #999; }
|
||||
|
||||
/* 核心修复:确保表单项横向排列 (Label left, Input right) */
|
||||
.form-item { display: flex; flex-direction: row; margin-bottom: 25px; align-items: flex-start; }
|
||||
|
||||
.form-label { width: 140px; font-size: 14px; color: #303133; text-align: right; padding-right: 20px; padding-top: 8px; flex-shrink: 0; }
|
||||
.form-right { flex: 1; display: flex; flex-direction: column; width: 100%; }
|
||||
.form-right { flex: 1; display: flex; flex-direction: column; }
|
||||
|
||||
.form-input { border: 1px solid #dcdfe6; width: 450px; height: 32px; padding: 0 15px; border-radius: 4px; font-size: 14px; color: #606266; outline: none; transition: border-color .2s; }
|
||||
.form-input:focus { border-color: #409eff; }
|
||||
.form-input { border: 1px solid #dcdfe6; width: 100%; max-width: 450px; height: 32px; padding: 0 12px; border-radius: 4px; font-size: 14px; }
|
||||
.form-textarea { border: 1px solid #dcdfe6; width: 100%; max-width: 550px; height: 100px; padding: 8px 12px; border-radius: 4px; font-size: 14px; }
|
||||
.code-bg { background-color: #f5f7fa; }
|
||||
.form-tip { font-size: 12px; color: #c0c4cc; margin-top: 8px; }
|
||||
|
||||
.form-textarea { border: 1px solid #dcdfe6; width: 550px; height: 120px; padding: 10px 15px; border-radius: 4px; font-size: 14px; color: #606266; line-height: 1.6; outline: none; }
|
||||
.form-textarea:focus { border-color: #409eff; }
|
||||
.radio-group { display: flex; flex-direction: row; gap: 20px; height: 32px; align-items: center; }
|
||||
.radio-label { display: flex; flex-direction: row; align-items: center; font-size: 14px; color: #606266; }
|
||||
|
||||
.waf-textarea { height: 200px; }
|
||||
.code-bg { background-color: #f5f7fa; font-family: "Lucida Console", Monaco, monospace; }
|
||||
.upload-placeholder { width: 80px; height: 80px; border: 1px dashed #dcdfe6; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 12px; color: #909399; cursor: pointer; }
|
||||
.logo-preview { width: 100%; height: 100%; }
|
||||
|
||||
.form-tip { font-size: 12px; color: #c0c4cc; margin-top: 8px; line-height: 1.5; width: 550px; }
|
||||
|
||||
.radio-group, .checkbox-group { display: flex; flex-direction: row; align-items: center; min-height: 32px; }
|
||||
.radio-label, .checkbox-label { display: flex; flex-direction: row; align-items: center; margin-right: 30px; font-size: 14px; color: #606266; cursor: pointer; }
|
||||
|
||||
.upload-placeholder { width: 80px; height: 80px; border: 1px dashed #dcdfe6; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 12px; color: #909399; cursor: pointer; transition: all .2s; }
|
||||
.upload-placeholder:hover { border-color: #409eff; color: #409eff; }
|
||||
|
||||
.submit-section { display: flex; flex-direction: row; margin-top: 20px; padding-top: 10px; }
|
||||
.btn-submit { background-color: #1890ff; color: #fff; width: 65px; height: 32px; line-height: 32px; font-size: 14px; border-radius: 4px; margin: 0; border: none; cursor: pointer; text-align: center; }
|
||||
.btn-submit:active { background-color: #096dd9; }
|
||||
</style>
|
||||
.submit-section { display: flex; flex-direction: row; margin-top: 20px; }
|
||||
.btn-submit { background-color: #1890ff; border: none; padding: 0 24px; height: 36px; border-radius: 4px; cursor: pointer; }
|
||||
.btn-txt { color: #fff; font-size: 14px; }
|
||||
.btn-submit[disabled] { opacity: 0.6; }
|
||||
</style>
|
||||
|
||||
@@ -1,81 +1,177 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<view class="page-header">
|
||||
<text class="page-title">数据概览</text>
|
||||
<text class="page-subtitle">Component: StatisticIndex</text>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<view class="placeholder-card">
|
||||
<text class="placeholder-title">页面占位</text>
|
||||
<text class="placeholder-desc">该功能模块正在开发中</text>
|
||||
<text class="placeholder-info">当前采用 CRMEB 路由体系 1:1 映射</text>
|
||||
<view class="admin-statistic">
|
||||
<!-- 核心指标卡片:累计数据 -->
|
||||
<view class="stats-grid">
|
||||
<view class="stat-card border-shadow" v-for="(item, key) in totalStats" :key="key">
|
||||
<view class="stat-icon-box" :class="item.color">
|
||||
<text class="stat-ic">{{ item.icon }}</text>
|
||||
</view>
|
||||
<view class="stat-info">
|
||||
<text class="stat-val">{{ item.value }}</text>
|
||||
<text class="stat-label">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 今日实时数据 -->
|
||||
<view class="section-title mt-24">
|
||||
<text class="title-txt">今日实时</text>
|
||||
<text class="sub-txt">更新时间:{{ currentTime }}</text>
|
||||
</view>
|
||||
<view class="stats-grid-mini">
|
||||
<view class="stat-mini-card border-shadow" v-for="(item, key) in todayStats" :key="key">
|
||||
<text class="sm-label">{{ item.label }}</text>
|
||||
<text class="sm-val">{{ item.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 待办任务提醒 -->
|
||||
<view class="section-title mt-24">
|
||||
<text class="title-txt">待办提醒</text>
|
||||
</view>
|
||||
<view class="pending-grid">
|
||||
<view class="pending-card border-shadow" v-for="(item, key) in pendingStats" :key="key">
|
||||
<view class="p-left">
|
||||
<text class="p-label">{{ item.label }}</text>
|
||||
<text class="p-count">{{ item.value }}</text>
|
||||
</view>
|
||||
<view class="p-right">
|
||||
<text class="p-link" @click="handlePending(key)">去处理 ></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 图表占位 -->
|
||||
<view class="chart-section mt-24 border-shadow">
|
||||
<view class="chart-header">
|
||||
<text class="chart-title">销售趋势概览</text>
|
||||
</view>
|
||||
<view class="chart-body">
|
||||
<text class="chart-placeholder-txt">趋势图表正在对接中...</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="isLoading" class="global-loading">
|
||||
<text class="loading-txt">数据聚合中...</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
// TODO: 实现 数据概览 的具体功能
|
||||
const loading = ref<boolean>(false)
|
||||
const isLoading = ref(false)
|
||||
const currentTime = ref('')
|
||||
|
||||
const totalStats = ref({
|
||||
sales: { label: '总销售额', value: '0.00', icon: '💰', color: 'blue' },
|
||||
orders: { label: '总订单数', value: '0', icon: '📦', color: 'orange' },
|
||||
users: { label: '总用户数', value: '0', icon: '👥', color: 'green' },
|
||||
products: { label: '总商品数', value: '0', icon: '🛒', color: 'pink' }
|
||||
})
|
||||
|
||||
const todayStats = ref({
|
||||
sales: { label: '今日成交额', value: '0.00' },
|
||||
orders: { label: '今日订单数', value: '0' },
|
||||
new_users: { label: '今日新增用户', value: '0' }
|
||||
})
|
||||
|
||||
const pendingStats = ref({
|
||||
delivery: { label: '待发货订单', value: '0' },
|
||||
stock: { label: '库存预警商品', value: '0' },
|
||||
extract: { label: '待审核提现', value: '0' }
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
updateTime()
|
||||
loadStats()
|
||||
})
|
||||
|
||||
function updateTime() {
|
||||
const now = new Date()
|
||||
currentTime.value = `${now.getHours()}:${now.getMinutes().toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const { data, error } = await supa.rpc('rpc_admin_get_overall_stats', {} as any)
|
||||
if (error == null && data != null) {
|
||||
const res = data as UTSJSONObject
|
||||
const totals = res.get('totals') as UTSJSONObject
|
||||
const today = res.get('today') as UTSJSONObject
|
||||
const pending = res.get('pending') as UTSJSONObject
|
||||
|
||||
totalStats.value.sales.value = (totals.getNumber('total_sales') ?? 0).toFixed(2)
|
||||
totalStats.value.orders.value = (totals.getInt('total_orders') ?? 0).toString()
|
||||
totalStats.value.users.value = (totals.getInt('total_users') ?? 0).toString()
|
||||
totalStats.value.products.value = (totals.getInt('total_products') ?? 0).toString()
|
||||
|
||||
todayStats.value.sales.value = (today.getNumber('today_sales') ?? 0).toFixed(2)
|
||||
todayStats.value.orders.value = (today.getInt('today_orders') ?? 0).toString()
|
||||
todayStats.value.new_users.value = (today.getInt('today_new_users') ?? 0).toString()
|
||||
|
||||
pendingStats.value.delivery.value = (pending.getInt('pending_delivery') ?? 0).toString()
|
||||
pendingStats.value.stock.value = (pending.getInt('stock_warning') ?? 0).toString()
|
||||
pendingStats.value.extract.value = (pending.getInt('pending_extract') ?? 0).toString()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch stats', e)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handlePending(type: string) {
|
||||
uni.showToast({ title: '正在跳转: ' + type, icon: 'none' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.admin-statistic { padding: 24px; background-color: #f0f2f5; min-height: 100vh; }
|
||||
.border-shadow { background-color: #fff; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); }
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.mt-24 { margin-top: 24px; }
|
||||
|
||||
.page-title {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
/* 核心指标卡片 */
|
||||
.stats-grid { display: flex; flex-direction: row; gap: 20px; }
|
||||
.stat-card { flex: 1; display: flex; flex-direction: row; align-items: center; padding: 24px; }
|
||||
.stat-icon-box { width: 56px; height: 56px; border-radius: 28px; display: flex; align-items: center; justify-content: center; margin-right: 16px; }
|
||||
.stat-icon-box.blue { background-color: #e6f7ff; color: #1890ff; }
|
||||
.stat-icon-box.orange { background-color: #fff7e6; color: #ffa940; }
|
||||
.stat-icon-box.green { background-color: #f6ffed; color: #52c41a; }
|
||||
.stat-icon-box.pink { background-color: #fff0f6; color: #eb2f96; }
|
||||
.stat-ic { font-size: 24px; }
|
||||
.stat-info { display: flex; flex-direction: column; }
|
||||
.stat-val { font-size: 24px; font-weight: bold; color: #303133; }
|
||||
.stat-label { font-size: 13px; color: #909399; margin-top: 4px; }
|
||||
|
||||
.page-subtitle {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
/* 标题 */
|
||||
.section-title { display: flex; flex-direction: row; align-items: center; justify-content: space-between; margin-bottom: 16px; }
|
||||
.title-txt { font-size: 16px; font-weight: bold; color: #333; position: relative; padding-left: 12px; }
|
||||
.title-txt::before { content: ''; position: absolute; left: 0; top: 2px; width: 4px; height: 16px; background-color: #1890ff; border-radius: 2px; }
|
||||
.sub-txt { font-size: 12px; color: #999; }
|
||||
|
||||
.page-content {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
}
|
||||
/* 今日实时 */
|
||||
.stats-grid-mini { display: flex; flex-direction: row; gap: 16px; }
|
||||
.stat-mini-card { flex: 1; padding: 20px; display: flex; flex-direction: column; }
|
||||
.sm-label { font-size: 13px; color: #666; margin-bottom: 8px; }
|
||||
.sm-val { font-size: 20px; font-weight: bold; color: #1890ff; }
|
||||
|
||||
.placeholder-card {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
/* 待办提醒 */
|
||||
.pending-grid { display: flex; flex-direction: row; gap: 16px; }
|
||||
.pending-card { flex: 1; padding: 20px; display: flex; flex-direction: row; justify-content: space-between; align-items: center; }
|
||||
.p-label { font-size: 13px; color: #666; display: block; margin-bottom: 4px; }
|
||||
.p-count { font-size: 22px; font-weight: bold; color: #f5222d; }
|
||||
.p-link { font-size: 12px; color: #1890ff; cursor: pointer; }
|
||||
|
||||
.placeholder-title {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
/* 图表占位 */
|
||||
.chart-section { padding: 24px; height: 350px; display: flex; flex-direction: column; }
|
||||
.chart-title { font-size: 15px; font-weight: bold; color: #333; }
|
||||
.chart-body { flex: 1; display: flex; align-items: center; justify-content: center; }
|
||||
.chart-placeholder-txt { font-size: 14px; color: #ccc; }
|
||||
|
||||
.placeholder-desc {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.placeholder-info {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
}
|
||||
.global-loading { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(255,255,255,0.6); z-index: 100; display: flex; align-items: center; justify-content: center; }
|
||||
.loading-txt { font-size: 14px; color: #1890ff; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user