创建面板项目
This commit is contained in:
commit
f65321a3ae
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node_modules
|
||||
12
index.html
Normal file
12
index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>Vue App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
23
mock-server.js
Normal file
23
mock-server.js
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Simple mock server for login credential handling.
|
||||
* Run with: node mock-server.js
|
||||
* It listens on port 4000 and exposes:
|
||||
* POST /api/login (expects { username, password } ), returns token on success
|
||||
* GET /api/menus and GET /api/user are served from /public/api by vite dev server in normal flow.
|
||||
*/
|
||||
import express from 'express'
|
||||
import bodyParser from 'body-parser'
|
||||
const app = express()
|
||||
app.use(bodyParser.json())
|
||||
|
||||
app.post('/api/login', (req,res) => {
|
||||
const { username, password } = req.body || {}
|
||||
// simple check: admin / 123456 succeed, or if username === 'scan' allow (for QR)
|
||||
if ((username === 'admin' && password === '123456') || username === 'scan') {
|
||||
return res.json({ code: 200, token: 'mocked-jwt-token' })
|
||||
}
|
||||
return res.status(401).json({ code: 401, message: '账号或密码错误' })
|
||||
})
|
||||
|
||||
const port = 4000
|
||||
app.listen(port, () => console.log(`Mock server running on http://localhost:${port}`))
|
||||
1464
package-lock.json
generated
Normal file
1464
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
package.json
Normal file
21
package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "my-vue-app-login-scan",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"mock": "node mock-server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.5.0",
|
||||
"vite": "^5.0.0",
|
||||
"express": "^4.18.2"
|
||||
}
|
||||
}
|
||||
56
public/api/data.json
Normal file
56
public/api/data.json
Normal file
@ -0,0 +1,56 @@
|
||||
[
|
||||
{
|
||||
"label": "销售数据",
|
||||
"cards": [
|
||||
"BFS渠道Q3销售进度",
|
||||
"华北西门子Q3重点品销售进度",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"新品/套系销售情况"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "CX/供应链看板",
|
||||
"cards": [
|
||||
"供应链Q3销售进度",
|
||||
"供应链Q3销售进度",
|
||||
"供应链Q3销售进度",
|
||||
"供应链Q3销售进度",
|
||||
"供应链Q3销售进度",
|
||||
"供应链Q3销售进度",
|
||||
"库存分析",
|
||||
"交付情况"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "产品内容",
|
||||
"cards": [
|
||||
"产品详情",
|
||||
"产品详情",
|
||||
"产品详情",
|
||||
"产品详情",
|
||||
"产品详情",
|
||||
"产品详情",
|
||||
"产品详情",
|
||||
"新产品规划"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "战略地图",
|
||||
"cards": [
|
||||
"战略目标",
|
||||
"战略目标",
|
||||
"战略目标",
|
||||
"战略目标",
|
||||
"战略目标",
|
||||
"战略目标",
|
||||
"战略目标",
|
||||
"市场布局"
|
||||
]
|
||||
}
|
||||
]
|
||||
4
public/api/login.json
Normal file
4
public/api/login.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"code": 200,
|
||||
"token": "mocked-jwt-token"
|
||||
}
|
||||
66
public/api/menus.json
Normal file
66
public/api/menus.json
Normal file
@ -0,0 +1,66 @@
|
||||
[
|
||||
{
|
||||
"id": "sales",
|
||||
"name": "销售数据",
|
||||
"cards": [
|
||||
"BFS渠道Q3销售进度",
|
||||
"华北西门子Q3重点品销售进度",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"新品/套系销售情况"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "cx",
|
||||
"name": "CX/供应链看板",
|
||||
"cards": [
|
||||
"BFS渠道Q3销售进度",
|
||||
"华北西门子Q3重点品销售进度",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"新品/套系销售情况"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "product",
|
||||
"name": "产品内容",
|
||||
"cards": [
|
||||
"BFS渠道Q3销售进度",
|
||||
"华北西门子Q3重点品销售进度",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"新品/套系销售情况"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "map",
|
||||
"name": "战略地图",
|
||||
"cards": [
|
||||
"BFS渠道Q3销售进度",
|
||||
"华北西门子Q3重点品销售进度",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"2025年销售数据看板",
|
||||
"新品/套系销售情况"
|
||||
]
|
||||
}
|
||||
]
|
||||
4
public/api/user.json
Normal file
4
public/api/user.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "Admin",
|
||||
"avatar": "https://i.pravatar.cc/100"
|
||||
}
|
||||
10
src/App.vue
Normal file
10
src/App.vue
Normal file
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html,body,#app { height: 100%; margin: 0; }
|
||||
</style>
|
||||
13
src/api/index.js
Normal file
13
src/api/index.js
Normal file
@ -0,0 +1,13 @@
|
||||
import request from './request'
|
||||
|
||||
export function apiLogin(payload){
|
||||
return request.post('/api/login', payload)
|
||||
}
|
||||
|
||||
export function getMenus(){
|
||||
return request.get('/api/menus.json')
|
||||
}
|
||||
|
||||
export function getUser(){
|
||||
return request.get('/api/user.json')
|
||||
}
|
||||
16
src/api/request.js
Normal file
16
src/api/request.js
Normal file
@ -0,0 +1,16 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const service = axios.create({
|
||||
baseURL: '', // use relative so vite proxy/public works
|
||||
timeout: 5000
|
||||
})
|
||||
|
||||
service.interceptors.request.use(cfg => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) cfg.headers['Authorization'] = `Bearer ${token}`
|
||||
return cfg
|
||||
}, err => Promise.reject(err))
|
||||
|
||||
service.interceptors.response.use(res => res.data, err => Promise.reject(err))
|
||||
|
||||
export default service
|
||||
BIN
src/assets/Rectangle.png
Normal file
BIN
src/assets/Rectangle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
BIN
src/assets/card-icon.png
Normal file
BIN
src/assets/card-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 395 B |
BIN
src/assets/img/avatar.png
Normal file
BIN
src/assets/img/avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
0
src/assets/left-illustration.jpg
Normal file
0
src/assets/left-illustration.jpg
Normal file
0
src/assets/logo.png
Normal file
0
src/assets/logo.png
Normal file
BIN
src/assets/qrcode.png
Normal file
BIN
src/assets/qrcode.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 951 B |
5
src/assets/style.css
Normal file
5
src/assets/style.css
Normal file
@ -0,0 +1,5 @@
|
||||
/* global styles */
|
||||
*{box-sizing:border-box}
|
||||
body,html{height:100%;margin:0;font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial;}
|
||||
a{color:inherit}
|
||||
button{font-family:inherit}
|
||||
6
src/main.js
Normal file
6
src/main.js
Normal file
@ -0,0 +1,6 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './assets/style.css'
|
||||
|
||||
createApp(App).use(router).mount('#app')
|
||||
16
src/router/index.js
Normal file
16
src/router/index.js
Normal file
@ -0,0 +1,16 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Login from '../views/Login.vue'
|
||||
import Dashboard from '../views/Dashboard.vue'
|
||||
|
||||
const routes = [
|
||||
{ path: '/', redirect: '/login' },
|
||||
{ path: '/login', component: Login },
|
||||
{ path: '/dashboard', component: Dashboard }
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
24
src/utils/request.js
Normal file
24
src/utils/request.js
Normal file
@ -0,0 +1,24 @@
|
||||
import axios from "axios"
|
||||
|
||||
const service = axios.create({
|
||||
baseURL: "http://localhost:5173",
|
||||
timeout: 5000
|
||||
})
|
||||
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem("token")
|
||||
if (token) {
|
||||
config.headers["Authorization"] = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
)
|
||||
|
||||
service.interceptors.response.use(
|
||||
(response) => response.data,
|
||||
(error) => Promise.reject(error)
|
||||
)
|
||||
|
||||
export default service
|
||||
319
src/views/Dashboard.vue
Normal file
319
src/views/Dashboard.vue
Normal file
@ -0,0 +1,319 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<!-- 顶部导航 -->
|
||||
<header class="topbar">
|
||||
<button class="menu-btn" @click="sidebarOpen = !sidebarOpen">☰</button>
|
||||
<div class="brand">
|
||||
<img src="/src/assets/logo.png" alt="logo" />
|
||||
<span>销售数据平台</span>
|
||||
</div>
|
||||
<div class="user" @click="showProfile = true">
|
||||
<img :src="user.avatar" alt="avatar" class="avatar" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 左侧导航 -->
|
||||
<aside :class="['sidebar', { open: sidebarOpen }]">
|
||||
<ul>
|
||||
<li
|
||||
v-for="(item, index) in menus"
|
||||
:key="item.id"
|
||||
@click="onNav(item.id, index)"
|
||||
:class="{ active: activeMenu === index }"
|
||||
>
|
||||
{{ item.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
<div v-if="sidebarOpen" class="drawer-overlay" @click="sidebarOpen = false"></div>
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<main class="content">
|
||||
<section v-for="m in menus" :key="m.id" :id="m.id" class="section">
|
||||
<h3>{{ m.name }}</h3>
|
||||
<div class="card-grid">
|
||||
<div class="card" v-for="(item, index) in m.cards" :key="index">
|
||||
<div class="icon">
|
||||
<img src="/src/assets/card-icon.png" alt="icon" />
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="title">{{ item.title }}</div>
|
||||
<div class="desc">{{ item.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- 修改资料弹窗 -->
|
||||
<div v-if="showProfile" class="modal-overlay" @click.self="showProfile = false">
|
||||
<div class="modal">
|
||||
<h3>修改个人信息</h3>
|
||||
<label>头像</label>
|
||||
<input type="file" />
|
||||
<label>账号</label>
|
||||
<input v-model="profile.username" />
|
||||
<label>密码</label>
|
||||
<input v-model="profile.password" type="password" />
|
||||
<div class="actions">
|
||||
<button @click="showProfile = false">取消</button>
|
||||
<button @click="saveProfile">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { getMenus, getUser } from "../api";
|
||||
|
||||
const menus = ref([]);
|
||||
const user = ref({ avatar: "https://i.pravatar.cc/100", name: "" });
|
||||
const profile = ref({ username: "", password: "" });
|
||||
const showProfile = ref(false);
|
||||
const sidebarOpen = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const m = await getMenus();
|
||||
// 模拟数据结构:[{ id, name, cards: [{ title, desc }] }]
|
||||
menus.value = m.map((section) => ({
|
||||
...section,
|
||||
cards: section.cards.map((c) =>
|
||||
typeof c === "string"
|
||||
? { title: c, desc: "每日12次更新" }
|
||||
: c
|
||||
),
|
||||
}));
|
||||
const u = await getUser();
|
||||
user.value = u;
|
||||
profile.value.username = u.name || "";
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
const activeMenu = ref(0);
|
||||
function onNav(id, index) {
|
||||
activeMenu.value = index;
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
const topbarHeight = 56;
|
||||
const mobileHeight = 44;
|
||||
const offset = window.innerWidth <= 768 ? mobileHeight : topbarHeight;
|
||||
const y = el.getBoundingClientRect().top + window.scrollY - offset;
|
||||
window.scrollTo({ top: y, behavior: "smooth" });
|
||||
}
|
||||
sidebarOpen.value = false;
|
||||
}
|
||||
|
||||
function saveProfile() {
|
||||
alert("已保存(模拟)");
|
||||
showProfile.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 顶部导航 */
|
||||
.topbar {
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
background: #0f7c72;
|
||||
color: #fff;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
}
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.brand img {
|
||||
height: 28px;
|
||||
}
|
||||
.menu-btn {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.user img.avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 左侧导航 */
|
||||
.sidebar {
|
||||
width: 220px;
|
||||
background: #f8f9fb;
|
||||
padding: 12px 0;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 56px;
|
||||
bottom: 0;
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid #e5e7eb;
|
||||
}
|
||||
.sidebar ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.sidebar li {
|
||||
padding: 10px 18px;
|
||||
cursor: pointer;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
border-left: 3px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.sidebar li:hover {
|
||||
background: #eef6f6;
|
||||
color: #0f7c72;
|
||||
}
|
||||
.sidebar li.active {
|
||||
background: #e6f3f2;
|
||||
border-left: 3px solid #0f7c72;
|
||||
color: #0f7c72;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 内容区 */
|
||||
.content {
|
||||
margin-left: 220px;
|
||||
padding: 24px;
|
||||
background: #f5f6f9;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* section 标题 */
|
||||
.section h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin: 20px 0 16px;
|
||||
border-left: 4px solid #0f7c72;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
/* 卡片网格 */
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 10px rgba(14, 42, 51, 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
.card:hover {
|
||||
box-shadow: 0 6px 16px rgba(14, 42, 51, 0.12);
|
||||
}
|
||||
.card .icon img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
.card .info .title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
.card .info .desc {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* 弹窗 */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 200;
|
||||
}
|
||||
.modal {
|
||||
background: #fff;
|
||||
padding: 18px;
|
||||
width: 340px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.modal label {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: #555;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
.drawer-overlay {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.menu-btn {
|
||||
display: block;
|
||||
}
|
||||
.sidebar {
|
||||
left: -260px;
|
||||
transition: left 0.28s;
|
||||
top: 44px;
|
||||
width: 200px;
|
||||
z-index: 60;
|
||||
}
|
||||
.sidebar.open {
|
||||
left: 0;
|
||||
}
|
||||
.drawer-overlay {
|
||||
display: block;
|
||||
position: fixed;
|
||||
inset: 44px 0 0 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 50;
|
||||
}
|
||||
.content {
|
||||
margin-left: 0;
|
||||
padding: 16px;
|
||||
margin-top: 56px;
|
||||
}
|
||||
.topbar {
|
||||
height: 44px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.brand span {
|
||||
display: none;
|
||||
}
|
||||
.modal {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
144
src/views/Login.vue
Normal file
144
src/views/Login.vue
Normal file
@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<div class="login-left"></div>
|
||||
|
||||
<div class="login-right">
|
||||
<div class="login-card">
|
||||
<div class="corner-qrcode" @click="tab='scan'">
|
||||
</div>
|
||||
<h2 class="title">账号登录</h2>
|
||||
|
||||
<!-- <div class="tabs">
|
||||
<button :class="{active:tab==='account'}" @click="tab='account'">账号登录</button>
|
||||
<button :class="{active:tab==='scan'}" @click="tab='scan'">扫码登录</button>
|
||||
</div> -->
|
||||
|
||||
<div v-if="tab==='account'" class="form">
|
||||
<label>账号</label>
|
||||
<input v-model="username" placeholder="请输入账号" />
|
||||
<label>密码</label>
|
||||
<input v-model="password" type="password" placeholder="请输入密码" />
|
||||
<button class="login-btn" @click="onLogin">登录</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="qr-area">
|
||||
<img :src="qrSrc" alt="qr" class="qr-img" />
|
||||
<p class="qr-tip">请使用微信扫码授权登录</p>
|
||||
<button class="login-btn" @click="onScanLogin">已扫码,确认登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { apiLogin } from '../api'
|
||||
const router = useRouter()
|
||||
const tab = ref('account')
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const qrSrc = ref('https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=wechat-login')
|
||||
|
||||
async function onLogin(){
|
||||
try{
|
||||
// const res = await apiLogin({ username: username.value, password: password.value })
|
||||
// if(res && res.token){
|
||||
// localStorage.setItem('token', res.token)
|
||||
localStorage.setItem('token', '2222222222')
|
||||
router.push('/dashboard')
|
||||
// } else {
|
||||
// alert('登录失败')
|
||||
// }
|
||||
} catch(e){
|
||||
alert(e?.response?.data?.message || '登录失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function onScanLogin(){
|
||||
// simulate qr login success, call login with special username 'scan'
|
||||
try{
|
||||
const res = await apiLogin({ username: 'scan', password: 'scan' })
|
||||
if(res && res.token){
|
||||
localStorage.setItem('token', res.token)
|
||||
router.push('/dashboard')
|
||||
}
|
||||
} catch(e){
|
||||
alert('扫码登录失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-page{
|
||||
height:100vh;
|
||||
display:flex;
|
||||
background: url('/src/assets/Rectangle.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
}
|
||||
/* left large image-ish area (you can replace with your image) */
|
||||
.login-left{
|
||||
flex:1;
|
||||
}
|
||||
/* right card */
|
||||
.login-right{
|
||||
width:520px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
padding-right:6%;
|
||||
}
|
||||
.login-card{
|
||||
width:360px;
|
||||
background:#fff;
|
||||
border-radius:12px;
|
||||
padding:60px 28px;
|
||||
box-shadow: 0 10px 30px rgba(14,42,51,0.08);
|
||||
position:relative;
|
||||
}
|
||||
.corner-qrcode{
|
||||
position:absolute;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
right: -3px;
|
||||
top: -3px;
|
||||
font-size:12px;
|
||||
color:#009688;
|
||||
cursor:pointer;
|
||||
background: url('/src/assets/qrcode.png') no-repeat center;
|
||||
}
|
||||
/* title */
|
||||
.title{
|
||||
text-align:center;
|
||||
margin:0 0 12px;
|
||||
font-size:20px;
|
||||
color:#333;
|
||||
font-weight:700;
|
||||
}
|
||||
/* tabs */
|
||||
.tabs{ display:flex; gap:10px; justify-content:center; margin-bottom:12px;}
|
||||
.tabs button{ padding:8px 14px; border-radius:20px; border:none; background:#f3f6f6; cursor:pointer;}
|
||||
.tabs button.active{ background:#009688; color:#fff; font-weight:600;}
|
||||
|
||||
/* form */
|
||||
.form label{ display:block; margin-top:8px; color:#666; font-size:13px;}
|
||||
.form input{ width:100%; padding:10px; margin-top:6px; border-radius:6px; border:1px solid #eee; background:#fafafa;}
|
||||
.login-btn{ width:100%; padding:12px; margin-top:60px; border-radius:16px; border:none; background:#009688; color:#fff; cursor:pointer; font-size:16px;}
|
||||
|
||||
/* qr area */
|
||||
.qr-area{ text-align:center; margin-top:6px;}
|
||||
.qr-img{ width:160px; height:160px; display:block; margin:12px auto; border-radius:6px; border:1px solid #eee;}
|
||||
.qr-tip{ color:#777; font-size:13px; margin-bottom:8px;}
|
||||
|
||||
/* responsive */
|
||||
@media (max-width: 768px){
|
||||
.login-page{ align-items:flex-start; padding-top:40px; }
|
||||
.login-left{ display:none; }
|
||||
.login-right{ width:100%; padding:0 16px; margin:0 auto; }
|
||||
.login-card{ width:100%; padding:18px; border-radius:10px; }
|
||||
.qr-img{ width:120px; height:120px; }
|
||||
}
|
||||
</style>
|
||||
9
vite.config.js
Normal file
9
vite.config.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: 5173
|
||||
}
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user