用户 / 订单 / 商品 管理

This commit is contained in:
zk
2026-07-10 15:27:16 +08:00
parent 20ab909329
commit bcdd1419e5
11 changed files with 462 additions and 19 deletions
@@ -5,8 +5,6 @@ import com.auth0.jwt.algorithms.Algorithm;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.jiayunet.admin.constant.AdminRedisKeyName;
import org.jiayunet.exception.BusinessException;
import org.jiayunet.exception.BusinessExpCodeEnum;
import org.jiayunet.mapper.AdminUserMapper;
import org.jiayunet.pojo.login.RedisLoginTokenInfo;
import org.jiayunet.pojo.po.AdminUser;
@@ -74,7 +72,7 @@ public class AdminLoginService {
// 校验密码:MD5(明文 + salt)
if (!md5(password + secret).equals(admin.getPassword())) {
handleLoginFail(username);
throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "账号或密码错误");
throw new IllegalArgumentException("账号或密码错误");
}
// 登录成功,清除失败计数
@@ -0,0 +1,13 @@
/**
* 订单管理 API
*/
const orderApi = {
/** 订单分页列表 */
list(params) {
return request.post('/order/list', params);
},
/** 订单统计 */
stats() {
return request.get('/order/stats');
}
};
@@ -0,0 +1,21 @@
/**
* 商品管理 API
*/
const productApi = {
/** 商品分页列表 */
list(params) {
return request.post('/product/list', params);
},
/** 新增/编辑商品 */
save(data) {
return request.post('/product/save', data);
},
/** 上架/下架 */
updateStatus(id, status) {
return request.post('/product/updateStatus?id=' + id + '&status=' + status);
},
/** 删除商品 */
delete(id) {
return request.post('/product/delete?id=' + id);
}
};
@@ -0,0 +1,17 @@
/**
* 用户管理 API
*/
const userApi = {
/** 用户分页列表 */
list(params) {
return request.post('/user/list', params);
},
/** 用户统计 */
stats() {
return request.get('/user/stats');
},
/** 禁用/启用用户 */
updateStatus(userId, status) {
return request.post('/user/updateStatus?userId=' + userId + '&status=' + status);
}
};
+12 -6
View File
@@ -24,12 +24,18 @@ const request = (() => {
return Promise.reject('未登录');
}
const result = await response.json();
// 解析响应体
let result;
try { result = await response.json(); } catch (e) {
const msg = 'HTTP ' + response.status + ' 请求失败';
ElementPlus ? ElementPlus.ElMessage.error(msg) : alert(msg);
return Promise.reject(msg);
}
// 业务异常
if (result.status !== 200) {
const msg = result.message || '请求失败';
alert(msg);
// 业务异常code 不为 "0" 表示失败
if (result.code !== '0') {
const msg = result.msg || '请求失败';
ElementPlus ? ElementPlus.ElMessage.error(msg) : alert(msg);
return Promise.reject(msg);
}
@@ -42,7 +48,7 @@ const request = (() => {
return http(url + query, { method: 'GET' });
},
post(url, data) {
return http(url, { method: 'POST', body: JSON.stringify(data) });
return http(url, { method: 'POST', body: data != null ? JSON.stringify(data) : undefined });
},
put(url, data) {
return http(url, { method: 'PUT', body: JSON.stringify(data) });
@@ -0,0 +1,58 @@
const DashboardView = {
template: `
<div>
<h2 style="margin-bottom: 24px; font-size: 18px; font-weight: 600; color: #303133;">数据概览</h2>
<el-row :gutter="20" style="margin-bottom: 24px;">
<el-col :span="6">
<el-card shadow="hover" body-style="padding: 24px;">
<el-statistic title="用户总数" :value="userStats.total"><template #suffix><span style="font-size: 12px; color: #909399;"> 人</span></template></el-statistic>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" body-style="padding: 24px;">
<el-statistic title="今日新增" :value="userStats.today"><template #suffix><span style="font-size: 12px; color: #909399;"> 人</span></template></el-statistic>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" body-style="padding: 24px;">
<el-statistic title="本月订单" :value="orderStats.monthPaid"><template #suffix><span style="font-size: 12px; color: #909399;"> 笔</span></template></el-statistic>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" body-style="padding: 24px;">
<el-statistic title="本月收入" :value="monthIncome"><template #prefix><span style="font-size: 14px;">¥</span></template></el-statistic>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-card shadow="hover" body-style="padding: 24px;"><el-statistic title="本周新增用户" :value="userStats.thisWeek"/></el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" body-style="padding: 24px;"><el-statistic title="本月新增用户" :value="userStats.thisMonth"/></el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" body-style="padding: 24px;"><el-statistic title="本周订单" :value="orderStats.weekPaid"/></el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" body-style="padding: 24px;">
<el-statistic title="本周收入" :value="weekIncome"><template #prefix><span style="font-size: 14px;">¥</span></template></el-statistic>
</el-card>
</el-col>
</el-row>
</div>
`,
setup() {
const { ref, onMounted, computed } = Vue;
const userStats = ref({ total: 0, today: 0, thisWeek: 0, thisMonth: 0 });
const orderStats = ref({ totalCount: 0, paidCount: 0, todayPaid: 0, weekPaid: 0, monthPaid: 0, monthIncome: 0, weekIncome: 0 });
const monthIncome = computed(() => (orderStats.value.monthIncome / 100).toFixed(2));
const weekIncome = computed(() => (orderStats.value.weekIncome / 100).toFixed(2));
const toNum = (obj) => { const r = {}; for (const k in obj) r[k] = Number(obj[k]); return r; };
onMounted(async () => {
try { userStats.value = toNum(await userApi.stats()); } catch(e) {}
try { orderStats.value = toNum(await orderApi.stats()); } catch(e) {}
});
return { userStats, orderStats, monthIncome, weekIncome };
}
};
@@ -0,0 +1,77 @@
const OrderView = {
template: `
<div>
<el-row :gutter="16" style="margin-bottom: 20px;">
<el-col :span="6"><el-card shadow="hover" body-style="padding: 20px;"><el-statistic title="总订单" :value="stats.totalCount"/></el-card></el-col>
<el-col :span="6"><el-card shadow="hover" body-style="padding: 20px;"><el-statistic title="已支付" :value="stats.paidCount"/></el-card></el-col>
<el-col :span="6"><el-card shadow="hover" body-style="padding: 20px;"><el-statistic title="今日支付" :value="stats.todayPaid"/></el-card></el-col>
<el-col :span="6"><el-card shadow="hover" body-style="padding: 20px;"><el-statistic title="今日收入(元)" :value="todayIncome"/></el-card></el-col>
</el-row>
<el-card shadow="never">
<template #header><span style="font-size: 16px; font-weight: 600;">订单管理</span></template>
<el-form :inline="true" style="margin-bottom: 16px;">
<el-form-item label="手机号"><el-input v-model="query.mobileNumber" placeholder="用户手机号" clearable @clear="loadData" style="width: 150px;"/></el-form-item>
<el-form-item label="状态">
<el-select v-model="query.status" placeholder="全部" clearable @change="loadData" style="width: 110px;">
<el-option label="待支付" :value="0"/><el-option label="已支付" :value="1"/><el-option label="已退款" :value="2"/><el-option label="已关闭" :value="3"/>
</el-select>
</el-form-item>
<el-form-item label="渠道">
<el-select v-model="query.payChannel" placeholder="全部" clearable @change="loadData" style="width: 100px;">
<el-option label="微信" :value="1"/><el-option label="支付宝" :value="2"/>
</el-select>
</el-form-item>
<el-form-item><el-button type="primary" @click="loadData">查询</el-button></el-form-item>
</el-form>
<el-table :data="tableData" stripe v-loading="loading" style="width: 100%;">
<el-table-column prop="orderNo" label="订单编号" min-width="150"/>
<el-table-column prop="userNick" label="用户" min-width="100"/>
<el-table-column prop="mobileNumber" label="手机号" min-width="120"/>
<el-table-column prop="productName" label="商品" min-width="120"/>
<el-table-column label="金额(元)" width="100" align="center">
<template #default="{row}">{{(row.payAmount/100).toFixed(2)}}</template>
</el-table-column>
<el-table-column label="渠道" width="80" align="center">
<template #default="{row}"><el-tag size="small" :type="row.payChannel===1?'success':'primary'">{{row.payChannel===1?'微信':'支付宝'}}</el-tag></template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<template #default="{row}"><el-tag size="small" :type="statusType(row.status)">{{statusText(row.status)}}</el-tag></template>
</el-table-column>
<el-table-column label="下单时间" min-width="160">
<template #default="{row}">{{formatTime(row.createTime)}}</template>
</el-table-column>
<el-table-column label="支付时间" min-width="160">
<template #default="{row}">{{formatTime(row.payTime)}}</template>
</el-table-column>
</el-table>
<div style="margin-top: 16px; display: flex; justify-content: flex-end;">
<el-pagination background layout="total, prev, pager, next" :total="total" :page-size="query.pageSize" v-model:current-page="query.pageNum" @current-change="loadData"/>
</div>
</el-card>
</div>
`,
setup() {
const { ref, reactive, onMounted, computed } = Vue;
const loading = ref(false);
const tableData = ref([]);
const total = ref(0);
const stats = ref({ totalCount: 0, paidCount: 0, todayPaid: 0, todayIncome: 0 });
const todayIncome = computed(() => (stats.value.todayIncome / 100).toFixed(2));
const query = reactive({ pageNum: 1, pageSize: 10, mobileNumber: '', status: null, payChannel: null });
const loadData = async () => {
loading.value = true;
try { const res = await orderApi.list(query); tableData.value = res.list; total.value = Number(res.total); } catch(e) {}
finally { loading.value = false; }
};
const loadStats = async () => { try { const s = await orderApi.stats(); const r = {}; for (const k in s) r[k] = Number(s[k]); stats.value = r; } catch(e) {} };
const statusText = (s) => ['待支付','已支付','已退款','已关闭'][s] || '-';
const statusType = (s) => ['warning','success','info','danger'][s] || 'info';
const formatTime = (ts) => {
if (!ts) return '-';
const d = new Date(ts);
return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0') + ' ' + String(d.getHours()).padStart(2,'0') + ':' + String(d.getMinutes()).padStart(2,'0');
};
onMounted(() => { loadData(); loadStats(); });
return { loading, tableData, total, stats, todayIncome, query, loadData, statusText, statusType, formatTime };
}
};
@@ -0,0 +1,107 @@
const ProductView = {
template: `
<div>
<el-card shadow="never">
<template #header>
<div style="display: flex; align-items: center; justify-content: space-between;">
<span style="font-size: 16px; font-weight: 600;">商品管理</span>
<el-button type="primary" @click="openDialog(null)"><el-icon><Plus/></el-icon> 新增商品</el-button>
</div>
</template>
<el-form :inline="true" style="margin-bottom: 16px;">
<el-form-item label="状态">
<el-select v-model="query.status" placeholder="全部" clearable @change="loadData" style="width: 120px;">
<el-option label="上架" :value="1"/><el-option label="下架" :value="0"/>
</el-select>
</el-form-item>
<el-form-item><el-button type="primary" @click="loadData">查询</el-button></el-form-item>
</el-form>
<el-table :data="tableData" stripe v-loading="loading" style="width: 100%;">
<el-table-column prop="productName" label="商品名称" min-width="140"/>
<el-table-column prop="tag" label="标签" width="120"/>
<el-table-column label="价格(元)" width="100" align="center">
<template #default="{row}">{{(row.price/100).toFixed(2)}}</template>
</el-table-column>
<el-table-column label="划线价(元)" width="110" align="center">
<template #default="{row}">{{row.originalPrice?(row.originalPrice/100).toFixed(2):'-'}}</template>
</el-table-column>
<el-table-column prop="durationDays" label="有效天数" width="90" align="center"/>
<el-table-column prop="sortOrder" label="排序" width="70" align="center"/>
<el-table-column label="状态" width="80" align="center">
<template #default="{row}"><el-tag :type="row.status===1?'success':'info'" size="small">{{row.status===1?'上架':'下架'}}</el-tag></template>
</el-table-column>
<el-table-column label="操作" width="180" align="center" fixed="right">
<template #default="{row}">
<el-button link type="primary" size="small" @click="openDialog(row)">编辑</el-button>
<el-button link :type="row.status===1?'warning':'success'" size="small" @click="toggleStatus(row)">{{row.status===1?'下架':'上架'}}</el-button>
<el-popconfirm title="确定删除该商品?" @confirm="handleDelete(row)"><template #reference><el-button link type="danger" size="small">删除</el-button></template></el-popconfirm>
</template>
</el-table-column>
</el-table>
<div style="margin-top: 16px; display: flex; justify-content: flex-end;">
<el-pagination background layout="total, prev, pager, next" :total="total" :page-size="query.pageSize" v-model:current-page="query.pageNum" @current-change="loadData"/>
</div>
</el-card>
<el-dialog v-model="dialogVisible" :title="form.id?'编辑商品':'新增商品'" width="520px" destroy-on-close>
<el-form :model="form" label-width="90px">
<el-form-item label="商品名称"><el-input v-model="form.productName" maxlength="32"/></el-form-item>
<el-form-item label="标签"><el-input v-model="form.tag" maxlength="16" placeholder="如:限时优惠"/></el-form-item>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="价格(分)"><el-input-number v-model="form.price" :min="1" style="width: 100%;"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="划线价(分)"><el-input-number v-model="form.originalPrice" :min="0" style="width: 100%;"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="月价(分)"><el-input-number v-model="form.monthlyPrice" :min="0" style="width: 100%;"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="有效天数"><el-input-number v-model="form.durationDays" :min="1" style="width: 100%;"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="排序"><el-input-number v-model="form.sortOrder" :min="0" style="width: 100%;"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="主推"><el-switch v-model="form.isFeatured" :active-value="1" :inactive-value="0"/></el-form-item></el-col>
</el-row>
<el-form-item label="按钮文字"><el-input v-model="form.buyButtonText" maxlength="16" placeholder="如:立即开通"/></el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible=false">取消</el-button>
<el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
</template>
</el-dialog>
</div>
`,
setup() {
const { ref, reactive, onMounted } = Vue;
const loading = ref(false);
const saving = ref(false);
const dialogVisible = ref(false);
const tableData = ref([]);
const total = ref(0);
const query = reactive({ pageNum: 1, pageSize: 10, status: null });
const form = reactive({ id: null, productName: '', tag: '', price: null, originalPrice: null, monthlyPrice: null, durationDays: null, sortOrder: 0, isFeatured: 0, buyButtonText: '' });
const loadData = async () => {
loading.value = true;
try { const res = await productApi.list(query); tableData.value = res.list; total.value = Number(res.total); } catch(e) {}
finally { loading.value = false; }
};
const openDialog = (row) => {
if (row) { Object.assign(form, { id: row.id, productName: row.productName, tag: row.tag, price: row.price, originalPrice: row.originalPrice, monthlyPrice: row.monthlyPrice, durationDays: row.durationDays, sortOrder: row.sortOrder, isFeatured: row.isFeatured, buyButtonText: row.buyButtonText }); }
else { Object.assign(form, { id: null, productName: '', tag: '', price: null, originalPrice: null, monthlyPrice: null, durationDays: null, sortOrder: 0, isFeatured: 0, buyButtonText: '' }); }
dialogVisible.value = true;
};
const handleSave = async () => {
if (!form.productName || !form.price || !form.durationDays) { ElementPlus.ElMessage.warning('请填写必填项'); return; }
saving.value = true;
try { await productApi.save(form); dialogVisible.value = false; ElementPlus.ElMessage.success('保存成功'); loadData(); } catch(e) {}
finally { saving.value = false; }
};
const toggleStatus = async (row) => {
const newStatus = row.status === 1 ? 0 : 1;
try { await productApi.updateStatus(row.id, newStatus); row.status = newStatus; ElementPlus.ElMessage.success('操作成功'); } catch(e) {}
};
const handleDelete = async (row) => {
try { await productApi.delete(row.id); ElementPlus.ElMessage.success('删除成功'); loadData(); } catch(e) {}
};
onMounted(loadData);
return { loading, saving, dialogVisible, tableData, total, query, form, loadData, openDialog, handleSave, toggleStatus, handleDelete };
}
};
@@ -0,0 +1,64 @@
const UserView = {
template: `
<div>
<el-card shadow="never">
<template #header><span style="font-size: 16px; font-weight: 600;">用户管理</span></template>
<el-form :inline="true" style="margin-bottom: 16px;">
<el-form-item label="手机号"><el-input v-model="query.mobileNumber" placeholder="手机号" clearable @clear="loadData" style="width: 150px;"/></el-form-item>
<el-form-item label="昵称"><el-input v-model="query.keyword" placeholder="昵称" clearable @clear="loadData" style="width: 150px;"/></el-form-item>
<el-form-item label="状态">
<el-select v-model="query.status" placeholder="全部" clearable @change="loadData" style="width: 100px;">
<el-option label="正常" :value="0"/><el-option label="禁用" :value="1"/>
</el-select>
</el-form-item>
<el-form-item><el-button type="primary" @click="loadData">查询</el-button></el-form-item>
</el-form>
<el-table :data="tableData" stripe v-loading="loading" style="width: 100%;">
<el-table-column prop="nick" label="昵称" min-width="100"/>
<el-table-column prop="realName" label="真实姓名" min-width="100"/>
<el-table-column prop="mobileNumber" label="手机号" min-width="130"/>
<el-table-column prop="resumeCount" label="简历数" width="80" align="center"/>
<el-table-column label="状态" width="80" align="center">
<template #default="{row}"><el-tag :type="row.status===0?'success':'danger'" size="small">{{row.status===0?'正常':'禁用'}}</el-tag></template>
</el-table-column>
<el-table-column label="注册时间" min-width="160">
<template #default="{row}">{{formatTime(row.createTime)}}</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center" fixed="right">
<template #default="{row}">
<el-popconfirm :title="row.status===0?'确定禁用该用户?':'确定启用该用户?'" @confirm="toggleStatus(row)">
<template #reference><el-button link :type="row.status===0?'danger':'success'" size="small">{{row.status===0?'禁用':'启用'}}</el-button></template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<div style="margin-top: 16px; display: flex; justify-content: flex-end;">
<el-pagination background layout="total, prev, pager, next" :total="total" :page-size="query.pageSize" v-model:current-page="query.pageNum" @current-change="loadData"/>
</div>
</el-card>
</div>
`,
setup() {
const { ref, reactive, onMounted } = Vue;
const loading = ref(false);
const tableData = ref([]);
const total = ref(0);
const query = reactive({ pageNum: 1, pageSize: 10, mobileNumber: '', keyword: '', status: null });
const loadData = async () => {
loading.value = true;
try { const res = await userApi.list(query); tableData.value = res.list; total.value = Number(res.total); } catch(e) {}
finally { loading.value = false; }
};
const toggleStatus = async (row) => {
const newStatus = row.status === 0 ? 1 : 0;
try { await userApi.updateStatus(row.id, newStatus); row.status = newStatus; ElementPlus.ElMessage.success('操作成功'); } catch(e) {}
};
const formatTime = (ts) => {
if (!ts) return '-';
const d = new Date(ts);
return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0') + ' ' + String(d.getHours()).padStart(2,'0') + ':' + String(d.getMinutes()).padStart(2,'0');
};
onMounted(loadData);
return { loading, tableData, total, query, loadData, toggleStatus, formatTime };
}
};
+81 -2
View File
@@ -4,9 +4,88 @@
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>OfferPie 管理后台</title>
<link rel="stylesheet" href="https://unpkg.com/element-plus@2.7.3/dist/index.css"/>
<style>
html, body, #app { margin: 0; padding: 0; height: 100%; font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif; }
.layout { height: 100%; }
.aside { background: #1d1e3c; overflow-y: auto; }
.aside .logo { height: 60px; display: flex; align-items: center; justify-content: center; padding: 0 16px; border-bottom: 1px solid rgba(255,255,255,0.06); }
.aside .logo img { height: 32px; max-width: 100%; object-fit: contain; }
.aside .el-menu { border-right: none; background: transparent; padding-top: 8px; }
.aside .el-menu-item { margin: 4px 10px; border-radius: 6px; }
.aside .el-menu-item.is-active { background: rgba(64,158,255,0.18) !important; }
.header { background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; height: 60px; box-shadow: 0 1px 4px rgba(0,21,41,0.08); z-index: 10; }
.header .title { font-size: 16px; font-weight: 600; color: #303133; }
.header .admin-info { display: flex; align-items: center; gap: 10px; font-size: 14px; color: #606266; }
.main-content { padding: 20px; background: #f0f2f5; overflow-y: auto; }
</style>
</head>
<body>
<h1>OfferPie 管理后台 - 首页</h1>
<p>管理后台首页占位 - 待实现</p>
<div id="app" v-cloak>
<el-container class="layout">
<el-aside width="210px" class="aside">
<div class="logo">
<img src="/admin/img/logo.png" alt="logo"/>
</div>
<el-menu :default-active="currentView" background-color="#1d1e3c" text-color="rgba(255,255,255,0.65)" active-text-color="#fff" @select="handleMenuSelect">
<el-menu-item index="dashboard"><el-icon><data-board/></el-icon><span>数据概览</span></el-menu-item>
<el-menu-item index="user"><el-icon><user/></el-icon><span>用户管理</span></el-menu-item>
<el-menu-item index="order"><el-icon><tickets/></el-icon><span>订单管理</span></el-menu-item>
<el-menu-item index="product"><el-icon><goods/></el-icon><span>商品管理</span></el-menu-item>
</el-menu>
</el-aside>
<el-container>
<el-header class="header">
<span class="title">{{ title }}</span>
<div class="admin-info">
<el-button type="primary" plain @click="handleLogout">退出登录</el-button>
</div>
</el-header>
<el-main class="main-content">
<component :is="currentComponent"></component>
</el-main>
</el-container>
</el-container>
</div>
<script src="https://unpkg.com/vue@3.4.21/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/element-plus@2.7.3/dist/index.full.min.js"></script>
<script src="https://unpkg.com/@element-plus/icons-vue@2.3.1/dist/index.iife.min.js"></script>
<script src="/admin/js/request.js"></script>
<script src="/admin/js/api/auth.js"></script>
<script src="/admin/js/api/user.js"></script>
<script src="/admin/js/api/order.js"></script>
<script src="/admin/js/api/product.js"></script>
<script src="/admin/js/views/dashboard.js"></script>
<script src="/admin/js/views/user.js"></script>
<script src="/admin/js/views/order.js"></script>
<script src="/admin/js/views/product.js"></script>
<style>[v-cloak] { display: none; }</style>
<script>
const { createApp, ref, computed } = Vue;
const app = createApp({
setup() {
const currentView = ref('dashboard');
const views = { dashboard: DashboardView, user: UserView, order: OrderView, product: ProductView };
const titles = { dashboard: '数据概览', user: '用户管理', order: '订单管理', product: '商品管理' };
const currentComponent = computed(() => views[currentView.value]);
const title = computed(() => titles[currentView.value]);
const handleMenuSelect = (index) => { currentView.value = index; };
const handleLogout = () => {
document.cookie = 'Token=; path=/; max-age=0';
window.location.href = '/admin/page/login';
};
return { currentView, currentComponent, title, handleMenuSelect, handleLogout };
}
});
// 注册 Element Plus 图标(容错:CDN 未加载时不阻断应用)
if (window.ElementPlusIconsVue) {
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
}
app.use(ElementPlus);
app.mount('#app');
</script>
</body>
</html>
+11 -8
View File
@@ -4,7 +4,8 @@
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>OfferPie 管理后台</title>
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css"/>
<link rel="stylesheet" href="https://unpkg.com/element-plus@2.7.3/dist/index.css"/>
<style>[v-cloak] { display: none; }</style>
<style>
body { margin: 0; padding: 0; }
#app {
@@ -55,7 +56,7 @@
</style>
</head>
<body>
<div id="app">
<div id="app" v-cloak>
<div class="left-panel">
<div class="logo">
<img src="/admin/img/logo.png" alt="logo"/>
@@ -83,9 +84,9 @@
</div>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/element-plus/dist/index.full.min.js"></script>
<script src="https://unpkg.com/@element-plus/icons-vue"></script>
<script src="https://unpkg.com/vue@3.4.21/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/element-plus@2.7.3/dist/index.full.min.js"></script>
<script src="https://unpkg.com/@element-plus/icons-vue@2.3.1/dist/index.iife.min.js"></script>
<script src="/admin/js/request.js"></script>
<script src="/admin/js/api/auth.js"></script>
<script>
@@ -112,9 +113,11 @@
return { formRef, form, rules, loading, handleLogin };
}
});
// 注册 Element Plus 图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
// 注册 Element Plus 图标(容错)
if (window.ElementPlusIconsVue) {
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
}
app.use(ElementPlus);
app.mount('#app');