用户 / 订单 / 商品 管理
This commit is contained in:
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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 };
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user