This commit is contained in:
weipengfei 2024-04-01 17:45:21 +08:00
parent 299a1875f0
commit 40b6de6fe8
16 changed files with 643 additions and 69 deletions

2
.env.development Normal file
View File

@ -0,0 +1,2 @@
# VITE_BASE_URL = 'http://192.168.1.24:8324'
VITE_BASE_URL = 'https://crmeb-test.shop.lihaink.cn'

1
.env.production Normal file
View File

@ -0,0 +1 @@
VITE_BASE_URL = ''

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title> <title>里海收银系统</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

8
src/api/store.js Normal file
View File

@ -0,0 +1,8 @@
import request from '@/utils/axios.js'
/**
* @description 商品列表
*/
export function storeListApi(id, data) {
return request.get(`server/${30}/product/lst`, { params: data })
}

28
src/api/user.js Normal file
View File

@ -0,0 +1,28 @@
import request from '@/utils/axios.js'
/**
* @description 验证码
*/
export function captchaApi() {
return request.get(`captcha`)
}
/**
* @description 登录
*/
export function login(data) {
return request.post(`auth/login`, data)
}
/**
* @description 信息
*/
export function info(data) {
return request.get(`user`, data)
}
/**
* @description 退出登录
*/
export function logout() {
return request.post(`logout`)
}

View File

@ -1,33 +1,98 @@
<script setup> <script setup>
import { useUserStore } from "@/store/user.js";
import { ref } from "vue";
import { info, logout } from "@/api/user.js";
import { ElMessage } from "element-plus";
import { useRouter } from 'vue-router'
const userStore = useUserStore();
const merInfo = ref({});
merInfo.value = userStore.userInfo.mer_info;
const getInfo = () => {
info().then((res) => {
merInfo.value = res.data;
});
};
const router = useRouter();
const onLogout = () => {
logout().then(() => {
userStore.setUserInfo({});
userStore.setToken("");
router.push("/login");
}).catch(() => {
ElMessage({
message: "退出失败",
type: "error",
})
});
}
// getInfo();
</script> </script>
<template> <template>
<div class="my-card"> <div class="my-card">
<div class="card-header"> <div class="card-header">
<el-image src="https://multi-store.crmeb.net/uploads/attach/store/2024/03/20240314/6cea2e0fd02480fc6fb62a9783a9ac43.png"></el-image> <el-image
<div class="card-title">里海收银系统</div> src="https://multi-store.crmeb.net/uploads/attach/store/2024/03/20240314/6cea2e0fd02480fc6fb62a9783a9ac43.png"
</div> ></el-image>
<div class="card-body">我的</div> <div class="card-title">里海收银系统</div>
</div> </div>
<div class="card-body">
<el-dropdown trigger="hover">
<div class="el-dropdown-link">
<el-avatar :src="merInfo.mer_avatar" icon="user-filled"/>
<div class="info">
<div>{{ merInfo.mer_name }}</div>
<div>{{ merInfo.service_phone }}</div>
</div>
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="onLogout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.my-card{ .my-card {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex;
justify-content: space-between;
align-items: center;
.card-header {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
.card-header{ .card-title {
font-size: 1.6rem;
margin-left: 1rem;
}
}
}
.el-dropdown-link{
width: auto;
display: flex;
color: #fff;
align-items: center;
.info{
margin: 0 0.5rem;
display: flex; display: flex;
align-items: center; flex-direction: column;
.card-title{ justify-content: space-around;
font-size: 1.6rem; height: 100%;
margin-left: 1rem;
}
} }
} }
</style> .el-dropdown{
border: none;
}
</style>

View File

@ -6,15 +6,17 @@ import 'element-plus/dist/index.css'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs' import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import router from './router' import router from './router'
import * as ElementPlusIconsVue from '@element-plus/icons-vue' import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { createPinia } from 'pinia'
const app = createApp(App) const app = createApp(App)
// 注册图标 // 注册图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) { for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component) app.component(key, component)
} }
const pinia = createPinia();
app.use(router) app.use(router)
app.use(pinia)
app.use(ElementPlus, { app.use(ElementPlus, {
locale: zhCn, locale: zhCn,
}) })

View File

@ -1,5 +1,8 @@
import { createRouter, createWebHistory } from 'vue-router'; import { createRouter, createWebHistory } from 'vue-router';
import layout from '@/layout/index.vue'; import layout from '@/layout/index.vue';
import { useUserStore } from '@/store/user.js';
const routes = [ const routes = [
{ {
path: '/', path: '/',
@ -14,6 +17,11 @@ const routes = [
} }
] ]
}, },
{
path: '/login',
name: 'login',
component: () => import('@/views/login/index.vue'),
},
]; ];
const router = createRouter({ const router = createRouter({
@ -21,4 +29,16 @@ const router = createRouter({
routes routes
}); });
// 全局前置守卫
router.beforeEach((to, from, next) => {
const userStore = useUserStore();
// 如果用户访问的不是登录页面,并且未登录,则重定向到登录页面
if (to.name !== 'login' && !userStore.Token) {
next({ name: 'login' });
} else {
next(); // 放行
}
});
export default router; export default router;

24
src/store/user.js Normal file
View File

@ -0,0 +1,24 @@
import { defineStore } from "pinia"
import { ref } from "vue"
export const useUserStore = defineStore('user', () => {
const userInfo = ref(JSON.parse(localStorage.getItem('userInfo') || '{}'));
const Token = ref(localStorage.getItem('Token'));
const setUserInfo = (e)=>{
userInfo.value = e;
localStorage.setItem('userInfo',JSON.stringify(e));
}
const setToken = (e)=>{
Token.value = e;
localStorage.setItem('Token',e);
}
return {
userInfo,
Token,
setUserInfo,
setToken
}
})

39
src/utils/axios.js Normal file
View File

@ -0,0 +1,39 @@
import axios from "axios";
const request = axios.create({
baseURL: import.meta.env.VITE_BASE_URL + '/api',
timeout: 5000
})
// 请求拦截器
request.interceptors.request.use(
config => {
// 在发送请求之前做些什么例如添加token、修改请求头等
const token = localStorage.getItem('Token');
if (token) {
config.headers['X-Token'] = 'Bearer ' + token;
}
return config;
},
error => {
// 处理请求错误
console.error(error);
return Promise.reject(error);
}
);
// 响应拦截器
request.interceptors.response.use(
response => {
// 对响应数据做些什么,例如解析数据、统一处理错误等
return response.data;
},
error => {
// 处理响应错误
console.error(error);
return Promise.reject(error);
}
);
export default request;

View File

@ -1,4 +1,25 @@
<script setup> <script setup>
import { ref } from 'vue';
const list = ref([])
const clearAll = ()=>{
list.value = []
}
const deleteOne = (index)=>{
list.value.splice(index,1)
}
const getList = ()=>{
list = {
num: 1
}
}
defineExpose({
getList
})
</script> </script>
@ -7,10 +28,24 @@
<div class="my-order"> <div class="my-order">
<div class="header-nav"> <div class="header-nav">
<div class="nav-item">已选购 <span>{{ 0 }}</span> </div> <div class="nav-item">已选购 <span>{{ 0 }}</span> </div>
<div class="nav-item-clear"><el-icon><Delete /></el-icon></div> <div class="nav-item-clear" @click="clearAll"><el-icon><Delete /></el-icon></div>
</div> </div>
<div class="order-list"> <div class="order-list">
订单列表 <el-empty v-if="list.length==0" description="请点击右侧添加商品" />
<div v-else class="order-item" v-for="(item, index) in list" :key="index">
<el-image class="order-item-img" src="https://multi-store.crmeb.net/uploads/attach/2024/03/01/8149b6d6bfc22ad622ec478528310c43.jpg"></el-image>
<div class="order-item-info">
<div class="order-item-title">
<div class="title">很不错的商品名称很不错的商品名称很不错的商品名称很不错的商品名称</div>
<div class="delete" @click="deleteOne">删除</div>
</div>
<div class="order-item-sku">设备规格</div>
<div class="order-item-price">
<div>¥<span>100</span></div>
<div><el-input-number v-model="item.num" :min="1" :step="1" /></div>
</div>
</div>
</div>
</div> </div>
<div class="order-footer"> <div class="order-footer">
<div class="order-total"> <div class="order-total">
@ -33,8 +68,6 @@
height: 100%; height: 100%;
background-color: #fff; background-color: #fff;
width: 30rem; width: 30rem;
box-sizing: border-box;
padding-bottom: 10rem;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
@ -42,6 +75,7 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding: 1rem; padding: 1rem;
height: 1.5rem;
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
span{ span{
@ -51,10 +85,52 @@
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 0.8rem; font-size: 0.8rem;
cursor: pointer;
} }
} }
.order-list{ .order-list{
padding: 1rem; height: calc(100vh - 100px - 14rem);
overflow-y: auto;
.order-item{
display: flex;
padding: 1rem;
border-bottom: 1px solid #eee;
.order-item-img{
width: 5rem;
height: 5rem;
border-radius: 0.5rem;
}
.order-item-info{
flex: 1;
box-sizing: border-box;
padding-left: 12px;
display: flex;
flex-direction: column;
justify-content: space-between;
&>div{
display: flex;
align-items: center;
justify-content: space-between;
}
.order-item-title{
.title{
width: 18rem;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.delete{
color: #1890ff;
cursor: pointer;
}
}
.order-item-sku{
font-size: 0.8rem;
color: #999;
}
}
}
} }
.order-footer{ .order-footer{
position: absolute; position: absolute;

View File

@ -0,0 +1,122 @@
<script setup>
import { ref } from 'vue'
import { ElMessageBox } from 'element-plus'
const dialogVisible = ref(false)
const show = (e)=>{
dialogVisible.value = e;
}
defineExpose({
show
})
</script>
<template>
<el-dialog
v-model="dialogVisible"
title="商品规格"
width="650"
>
<div class="shop">
<div class="shop-info">
<div class="shop-info-left">
<el-image src="https://multi-store.crmeb.net/uploads/attach/2024/03/01/8149b6d6bfc22ad622ec478528310c43.jpg"></el-image>
</div>
<div class="shop-info-right">
<div class="shop-info-right-top">香奈儿Chanel五号香水经典50ml礼盒装送女 香奈儿Chanel五号香水经典50ml礼盒装送女 香奈儿Chanel五号香水经典50ml礼盒装送女香奈儿Chanel五号香水经典50ml礼盒装送女</div>
<div class="shop-info-right-center">库存100</div>
<div class="shop-info-right-price">¥<span>{{0.10}}</span></div>
</div>
</div>
<div class="shop-sku">
<div class="title">产品</div>
<div class="sku">
<el-space wrap :size="20">
<div class="sku-item sku-item_active"></div>
<div class="sku-item"></div>
<div class="sku-item"></div>
</el-space>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button class="ok-btn" type="primary" @click="dialogVisible = false">
确定
</el-button>
</div>
</template>
</el-dialog>
</template>
<style scoped lang="scss">
.dialog-footer{
.ok-btn{
width: 100%;
height: 2.5rem;
border-radius: 2.5rem;
}
}
.shop{
border-top: 1px solid #eee;
padding-top: 1rem;
.shop-info{
display: flex;
.shop-info-left{
flex-shrink: 0;
margin-right: 0.8rem;
height: 8rem;
width: 8rem;
overflow: hidden;
border-radius: 0.5rem;
}
.shop-info-right{
display: flex;
flex-direction: column;
justify-content: space-between;
.shop-info-right-top{
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
-webkit-line-clamp: 2; /* 限制文本显示为两行 */
font-size: 1.1rem;
}
.shop-info-right-center{
font-size: 0.8rem;
}
.shop-info-right-price{
color: #ff4a00;
font-size: 1.2rem;
font-weight: bold;
span{
font-size: 1.4rem;
margin-left: 0.2rem;
}
}
}
}
.shop-sku{
min-height: 20rem;
.title{
font-size: 1.1rem;
font-weight: bold;
padding: 1rem 0 0.5rem 0;
}
.sku{
.sku-item{
padding: 0.5rem 1rem;
background-color: #f5f5f5;
border-radius: 3rem;
&_active{
background-color: #1890ff;
color: #fff;
}
}
}
}
}
</style>

View File

@ -1,9 +1,25 @@
<script setup> <script setup>
import { ref } from "vue"; import { ref, watch } from "vue";
const input = ref("");
const props = defineProps({
storeList:{
type:Array,
default:()=>[]
}
})
const emit = defineEmits(['getStoreList'])
const bar_code = ref('');
watch(bar_code, (val) => {
emit('getStoreList', {
bar_code: val
})
})
const loadMore = () => { const loadMore = () => {
console.log("loadMore"); console.log("loadMore");
} };
</script> </script>
<template> <template>
@ -12,7 +28,7 @@ const loadMore = () => {
<div class="nav-item-label">搜索</div> <div class="nav-item-label">搜索</div>
<div class="nav-item-input"> <div class="nav-item-input">
<el-input <el-input
v-model="input" v-model="bar_code"
placeholder="搜索商品名称/ID/唯一码或点击聚焦扫码" placeholder="搜索商品名称/ID/唯一码或点击聚焦扫码"
clearable clearable
/> />
@ -23,12 +39,22 @@ const loadMore = () => {
></el-button> ></el-button>
</div> </div>
</div> </div>
<div class="shop-list" v-infinite-scroll="loadMore" infinite-scroll-distance="100" infinite-scroll-delay="500" style="overflow:auto"> <div
class="shop-list"
v-infinite-scroll="loadMore"
infinite-scroll-distance="100"
infinite-scroll-delay="500"
style="overflow: auto"
>
<el-space wrap :size="20"> <el-space wrap :size="20">
<div class="shop-item" v-for="(item, index) in 26" :key="index"> <div class="shop-item" v-for="(item, index) in storeList" :key="index">
<el-image src="https://multi-store.crmeb.net/uploads/attach/2024/03/01/8149b6d6bfc22ad622ec478528310c43.jpg"></el-image> <el-image
<div class="shop-name">桌面好物, 100cm*60cm*70cm大桌子</div> :src="item.image"
<div class="shop-price">¥<span>{{0.10}}</span></div> ></el-image>
<div class="shop-name">{{ item.store_name }}</div>
<div class="shop-price">
¥<span>{{ item.price }}</span>
</div>
</div> </div>
</el-space> </el-space>
</div> </div>
@ -80,29 +106,31 @@ const loadMore = () => {
} }
} }
.shop-list{ .shop-list {
height: calc(100vh - 100px - 3rem); height: calc(100vh - 100px - 3rem);
width: auto; width: auto;
overflow-y: auto; overflow-y: auto;
display: flex;
flex-wrap: wrap;
box-sizing: border-box; box-sizing: border-box;
padding-top: 20px; padding-top: 20px;
.shop-item{ .shop-item {
width: 11rem; width: 11rem;
height: 16rem; height: 16rem;
cursor: pointer;
background-color: #fff; background-color: #fff;
border-radius: 1rem; border-radius: 1rem;
padding: 0.5rem; padding: 0.5rem;
display: flex;
justify-content: space-between;
flex-direction: column;
.el-image{ .el-image {
border-radius: 0.5rem; border-radius: 0.5rem;
width: 11rem; width: 11rem;
height: 11rem; height: 11rem;
} }
.shop-name{ .shop-name {
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
@ -110,40 +138,23 @@ const loadMore = () => {
-webkit-line-clamp: 2; /* 将文本限制为两行 */ -webkit-line-clamp: 2; /* 将文本限制为两行 */
} }
.shop-price{ .shop-price {
font-size: 0.8rem; font-size: 0.8rem;
span{ color: #f5222d;
span {
font-size: 1.2rem; font-size: 1.2rem;
margin-left: 3px;
} }
} }
&:hover{ &:hover {
background-color: #1890ff; background-color: #1890ff;
color: #fff; color: #fff;
.shop-price {
color: #fff;
}
} }
} }
} }
/* 修改滚动条的样式 */
::-webkit-scrollbar {
width: 5px; /* 设置滚动条的宽度 */
}
/* 设置滚动条的轨道样式 */
::-webkit-scrollbar-track {
background-color: #f1f1f1; /* 设置轨道的背景色 */
margin: 20px 0;
}
/* 设置滚动条的滑块样式 */
::-webkit-scrollbar-thumb {
background-color: #ccc; /* 设置滑块的背景色 */
border-radius: 5px; /* 设置滑块的圆角 */
}
/* 设置滚动条鼠标悬停时的滑块样式 */
::-webkit-scrollbar-thumb:hover {
background-color: #999; /* 设置鼠标悬停时滑块的背景色 */
}
} }
</style> </style>

View File

@ -2,6 +2,34 @@
import order from './component/order.vue'; import order from './component/order.vue';
import shop from './component/shop.vue'; import shop from './component/shop.vue';
import padding from './component/padding.vue'; import padding from './component/padding.vue';
import pupop from './component/pupop.vue';
import { ref, nextTick } from 'vue';
import { storeListApi } from "@/api/store.js";
import { useUserStore } from "@/store/user.js";
const pupopRef = ref(null);
const storeList = ref([]);
const where = ref({
page: 1,
limit: 30,
})
const getStoreList = (data)=>{
where.value = {
...where.value,
...data
}
storeListApi(where.value).then(res=>{
storeList.value = res.data.list;
})
}
getStoreList();
nextTick(() => {
// pupopRef.value.show(true);
})
</script> </script>
@ -9,12 +37,34 @@ import padding from './component/padding.vue';
<div class="my-card"> <div class="my-card">
<order /> <order />
<padding /> <padding />
<shop style="flex: 1" /> <shop style="flex: 1" :storeList="storeList" @getStoreList="getStoreList"/>
<pupop ref="pupopRef"/>
</div> </div>
</template> </template>
<style scoped lang="scss"> <style lang="scss">
.my-card{ .my-card{
display: flex; display: flex;
} }
/* 修改滚动条的样式 */
::-webkit-scrollbar {
width: 5px; /* 设置滚动条的宽度 */
}
/* 设置滚动条的轨道样式 */
::-webkit-scrollbar-track {
background-color: #f1f1f1; /* 设置轨道的背景色 */
margin: 20px 0;
}
/* 设置滚动条的滑块样式 */
::-webkit-scrollbar-thumb {
background-color: #ccc; /* 设置滑块的背景色 */
border-radius: 5px; /* 设置滑块的圆角 */
}
/* 设置滚动条鼠标悬停时的滑块样式 */
::-webkit-scrollbar-thumb:hover {
background-color: #999; /* 设置鼠标悬停时滑块的背景色 */
}
</style> </style>

122
src/views/login/index.vue Normal file
View File

@ -0,0 +1,122 @@
<script setup>
import { ref, onMounted } from "vue";
import { ElMessage } from "element-plus";
import { captchaApi, login, info } from "@/api/user.js";
import { useUserStore } from "@/store/user.js";
import { useRouter } from "vue-router";
const userStore = useUserStore();
const router = useRouter();
const formLogin = ref({
account: "",
password: "",
key: "",
captchaVerification: "",
code: "",
});
const getCaptchaApi = () => {
captchaApi()
.then((res) => {
formLogin.value.key = res.data.key;
})
.catch((err) => {
ElMessage({
message: err.message,
type: "error",
});
});
};
const onLogin = () => {
if (!formLogin.value.account)
return ElMessage({
message: "请填写账号",
type: "error",
});
if (!formLogin.value.password)
return ElMessage({
message: "请填写密码",
type: "error",
});
login(formLogin.value)
.then((res) => {
userStore.setToken(res.data.token);
info().then(({data}) => {
userStore.setUserInfo(data);
router.push("/");
});
})
.catch((err) => {
ElMessage({
message: err.message,
type: "error",
});
});
};
onMounted(() => {
// getCaptchaApi();
});
</script>
<template>
<div class="body">
<div class="login">
<div class="title">里海收银系统 - 登录</div>
<el-form>
<el-form-item>
<el-input v-model="formLogin.account" placeholder="请输入账号">
<template #prefix>
<el-icon><User /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-input
v-model="formLogin.password"
type="password"
placeholder="请输入密码"
show-password
>
<template #prefix>
<el-icon><Lock /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button style="width: 100%" type="primary" @click="onLogin"
>登录</el-button
>
</el-form-item>
</el-form>
</div>
</div>
</template>
<style scoped lang="scss">
.body {
width: 100vw;
height: 100vh;
box-sizing: border-box;
background-image: url(https://multi-store.crmeb.net/view_cashier/img/bg.b8f6b872.png);
background-size: 100% 100%;
background-repeat: no-repeat;
display: flex;
justify-content: center;
align-items: center;
.login {
width: 18rem;
/* height: 12rem; */
background-color: #fff;
border-radius: 2rem;
padding: 2rem 3rem;
.title {
text-align: center;
font-size: 1.3rem;
padding-bottom: 2rem;
}
}
}
</style>

View File

@ -4,6 +4,10 @@ import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [vue()], plugins: [vue()],
server: {
port: '5175',
host: '0.0.0.0',
},
resolve: { resolve: {
alias: { alias: {
'@': '/src' '@': '/src'