This commit is contained in:
LittleBoy 2023-02-07 11:33:16 +08:00
commit 1387938b75
57 changed files with 1932 additions and 243 deletions

View File

@ -20,9 +20,10 @@
"@types/node": "^18.11.10",
"@vitejs/plugin-vue": "^3.2.0",
"@vitejs/plugin-vue-jsx": "^2.1.1",
"typescript": "^4.9.3",
"vite": "^3.2.4",
"less": "^4.1.3",
"typescript": "^4.9.3",
"unplugin-vue-define-options": "^1.0.0",
"vite": "^3.2.4",
"vue-tsc": "^1.0.11"
}
}

View File

@ -38,6 +38,8 @@
--info-color: #1890ff;
--info-color-deprecated-bg: #e6f7ff;
--info-color-deprecated-border: #91d5ff;
--placeholder-color: #ccc;
--border-color: #d9d9d9;
--primary-color-text: #333;
--font-size: 14px;
@ -48,7 +50,8 @@
--border-radius-middle: calc(var(--border-radius) + 2px);
--border-radius-large: calc(var(--border-radius) + 3px);
--header-height: 80px;
--header-bg: #001529;
--header-height: 50px;
--left-menu-width: 200px;
}
@ -119,13 +122,55 @@ body {
.pointer-cursor {
cursor: pointer;
}
table{
width: 100%;
border-left: solid 1px #eee;
border-top: solid 1px #eee;
.table-wrapper {
.table-toolbar{
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
table {
width: 100%;
border: solid 1px #eee;
border-collapse: collapse;
font-size: 13px;
}
th{
background-color: #f5f7fa;
}
td, th {
text-align: left;
border-bottom: solid 1px #eee;
padding: 6px 8px;
}
tr{
&:hover{
background-color: #f5f7fa;
}
}
}
td,th {
border-right: solid 1px #eee;
border-bottom: solid 1px #eee;
padding:2px 4px;
.page-wrapper {
margin: 20px 0;
font-size: 12px;
text-align: right;
.page-item {
cursor: pointer;
border: solid 1px var(--primary-2);
border-radius: var(--border-radius-middle);
display: inline-block;
padding: 4px 6px;
margin: 0 5px;
min-width: 26px;
text-align: center;
&:hover, &.current-page {
border-color: var(--primary-color);
color: var(--primary-color)
}
}
}
.display-flex{
display: flex;
}

View File

@ -0,0 +1,94 @@
<template>
<button :disabled="disabled || loading" class="btn" :class="{
'btn-primary':btnType == 'primary',
'btn-default':btnType == 'default',
'btn-text':btnType == 'text',
'btn-link':btnType == 'link',
}">
<span v-if="loading" class="btn-icon">
<Loading v-if="loading" class="btn-icon-loading"/>
</span>
<slot></slot>
</button>
</template>
<script setup lang="ts">
import {defineOptions} from "unplugin-vue-define-options/macros";
import {computed} from "vue";
import Loading from "../icon/Loading.vue";
defineOptions({
name: 'PButton'
})
type ButtonProps = {
type?: 'primary' | 'default' | 'text' | 'link'
/**
* 是否正在加载中
*/
loading?: boolean
disabled?: boolean
}
const props = defineProps<ButtonProps>()
const btnType = computed(() => props.type || 'primary')
</script>
<style lang="less">
@keyframes anim-loading {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.btn {
cursor: pointer;
padding: 5px 15px;
line-height: 20px;
border-radius: var(--border-radius-middle);
position: relative;
display: inline-block;
border: none;
outline: none;
&.btn-primary {
background-color: var(--primary-color);
color: white;
&:hover{
background-color: var(--primary-color-hover);
}
&:active{
background-color: var(--primary-color-active);
}
&[disabled]{
background-color: var(--primary-3);
}
}
&.btn-default {
}
&.btn-text {
}
&.btn-link {
padding:2px;
background: none;
color: var(--primary-color)
}
}
.btn-icon {
margin-right: 5px;
.btn-icon-loading {
height: 16px !important;
width: 16px !important;
vertical-align: middle;
animation: anim-loading 1s infinite linear;
//transition: transform ;
color: white;
}
}
</style>

View File

@ -0,0 +1,17 @@
<template>
<svg :style="sizeStyle" class="icon-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path fill="currentColor"
d="M500.8 604.779L267.307 371.392l-45.227 45.27 278.741 278.613L779.307 416.66l-45.248-45.248z"></path>
</svg>
</template>
<script lang="ts">
import {defineComponent} from "vue";
import iconComponent from "./iconComponent";
export default defineComponent({
name: "ArrowDown",
props: iconComponent.props,
setup: iconComponent.setup
})
</script>

View File

@ -0,0 +1,18 @@
<template>
<svg :style="sizeStyle" class="icon-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path fill="currentColor"
d="M270.4 214.4C336 160 420 128 512 128c212 0 384 172 384 384h64c0-247.2-200.8-448-448-448-107.2 0-205.6 37.6-282.4 100l40.8 50.4z"
p-id="2703"></path>
</svg>
</template>
<script lang="ts">
import {defineComponent} from "vue";
import iconComponent from "./iconComponent";
export default defineComponent({
name: "Loading",
props: iconComponent.props,
setup: iconComponent.setup
})
</script>

View File

@ -24,144 +24,151 @@ import Eye from "../icon/Eye.vue";
import EyeClose from "../icon/EyeClose.vue";
interface PInputEvents {
'update:modelValue': (value: string) => void
'update:modelValue': (value: any) => void
}
type PInputProps = {
placeholder?: string
modelValue?: string
type?: 'text' | 'password' | 'textarea'
placeholder?: string
modelValue?: any
type?: 'text' | 'password' | 'textarea'
}
const props = defineProps<PInputProps>()
const emit = defineEmits<PInputEvents>()
const slots = useSlots()
const state = reactive({
focus: false,
hover: false,
passwordVisible: false
focus: false,
hover: false,
passwordVisible: false
})
const inputType = computed(() => {
const type = props.type || 'text';
if (type === 'password' && state.passwordVisible) return 'text';
return type;
const type = props.type || 'text';
if (type === 'password' && state.passwordVisible) return 'text';
return type;
})
const value = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
</script>
<style lang="less">
@height: 30px;
.p-input-box-focus {
border-color: #2a7dc9;
box-shadow: 0 0 0 2px #0960bd33;
outline: 0;
border-color: #2a7dc9;
box-shadow: 0 0 0 2px #0960bd33;
outline: 0;
}
.p-input {
box-sizing: border-box;
margin: 0;
list-style: none;
position: relative;
display: inline-block;
width: 100%;
min-width: 0;
padding: 4px 11px;
color: #000000d9;
font-size: 14px;
line-height: 1.5715;
background-color: #fff;
background-image: none;
border: 1px solid #d9d9d9;
border-radius: 2px;
transition: all .3s;
box-sizing: border-box;
margin: 0;
list-style: none;
position: relative;
display: inline-block;
width: 100%;
min-width: 0;
padding: 0px 11px;
color: #000000d9;
font-size: 14px;
line-height: 1.5715;
background-color: #fff;
background-image: none;
border: 1px solid #d9d9d9;
border-radius: 2px;
transition: all .3s;
height: @height;
&::-moz-placeholder {
opacity: 1
}
&::-moz-placeholder {
opacity: 1
}
&::placeholder {
color: #bfbfbf;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none
}
&::placeholder {
color: #bfbfbf;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none
}
&:placeholder-shown {
text-overflow: ellipsis
}
&:placeholder-shown {
text-overflow: ellipsis
}
&:hover {
border-color: #2a7dc9;
border-right-width: 1px !important
}
&:hover {
border-color: #2a7dc9;
border-right-width: 1px !important
}
.p-input-rtl .p-input:hover {
border-right-width: 0;
border-left-width: 1px !important
}
.p-input-rtl .p-input:hover {
border-right-width: 0;
border-left-width: 1px !important
}
&:focus, .p-input-focused {
border-right-width: 1px !important;
}
&:focus, .p-input-focused {
border-right-width: 1px !important;
}
.p-input-disabled {
color: #00000040;
background-color: #f5f5f5;
border-color: #d9d9d9;
box-shadow: none;
cursor: not-allowed;
opacity: 1
}
.p-input-disabled {
color: #00000040;
background-color: #f5f5f5;
border-color: #d9d9d9;
box-shadow: none;
cursor: not-allowed;
opacity: 1
}
}
.p-input-wrapper {
display: inline-block;
display: inline-block;
width: 100%;
}
.p-input-wrapper-group {
display: inline-flex;
position: relative;
max-width: 100%;
min-width: 0;
padding: 4px 6px;
color: #000000d9;
font-size: 14px;
line-height: 1.5715;
background-color: #fff;
background-image: none;
border: 1px solid #d9d9d9;
border-radius: var(--border-radius);
transition: all .3s;
display: inline-flex;
position: relative;
max-width: 100%;
min-width: 0;
padding: 0px 6px;
color: #000000d9;
font-size: 14px;
line-height: 1.5715;
background-color: #fff;
background-image: none;
border: 1px solid #d9d9d9;
border-radius: var(--border-radius);
transition: all .3s;
height: @height;
&:hover {
border-color: #2a7dc9;
border-right-width: 1px !important
}
.p-input {
padding: 0;
border: none;
outline: none;
margin: 0 6px;
&:focus {
box-shadow: none;
}
&:hover {
border-color: #2a7dc9;
border-right-width: 1px !important
}
.p-input {
padding: 0;
border: none;
outline: none;
margin: 0 6px;
height: 28px;
&:focus {
box-shadow: none;
}
}
}
.p-input-prefix, .p-input-suffix {
display: flex;
flex: none;
align-items: center;
display: flex;
flex: none;
align-items: center;
}
</style>

View File

@ -1,7 +1,9 @@
<template>
<div class="modal" v-if="modalVisible">
<div class="modal-mask"></div>
<div class="modal-content">
<div class="modal-content" :style="{
width: width+(/(px|%)/.test(width)?'':'px')
}">
<div class="modal-close" @click="modalVisible = false">关闭</div>
<slot/>
</div>
@ -12,8 +14,12 @@
import {computed} from "vue";
export default {
name: "modal",
name: "Modal",
props: {
width: {
type: String,
default: '500px'
},
modelValue: {
type: Boolean,
require: true

View File

@ -0,0 +1,79 @@
<template>
<div class="page-wrapper">
<span v-if="showRefresh" class="page-item refresh" @click="$emit('refresh')">刷新</span>
<span v-if="page > 1" class="prev-page page-item" @click="$emit('page-change',--page)">上一页</span>
<span class="page-item page-first" :class="{'current-page':page == 1}"
@click="$emit('page-change',(page=1))">1</span>
<template v-for="index in pageData">
<span class="page-item" :class="{'current-page':page == index}" @click="$emit('page-change',(page=index))">{{
index
}}</span>
</template>
<span class="page-item page-end" :class="{'current-page':page == totalPage}"
@click="$emit('page-change',(page=totalPage))">{{
totalPage
}}</span>
<span v-if="page < totalPage" class="page-item next-page" @click="$emit('page-change',++ page)">下一页</span>
</div>
</template>
<script>
import {computed, defineComponent, ref} from 'vue'
export default defineComponent({
name: "Pager",
props: {
/**
* 总条数
*/
total: {
type: Number,
require: true
},
showRefresh: {
type: Boolean,
default: false
},
/**
* 每页的条数
*/
pageSize: {
type: Number,
default: 10
},
currentPage: {
type: Number,
default: 1
}
},
emits: ['refresh', 'page-change'],
setup(props) {
const totalPage = computed(() => Math.ceil((props.total || 0) / props.pageSize))
const page = ref(props.currentPage)
const pageData = computed(() => {
// 使
let start = page.value - 2, end = page.value + 2
if (start <= 1) {
//
end += 1 - start;
start = 2;
}
if (end >= totalPage.value) {
end = totalPage.value - 1;
start = end - 4;
if (start <= 1) {
start = 2;
}
}
const pages = []
for (; start <= end; start++) {
pages.push(start)
}
return pages
})
return {
totalPage, page, pageData
}
}
})
</script>

View File

@ -0,0 +1,4 @@
import PagerComponent from './Pager.vue'
const Pager = PagerComponent
export default Pager

View File

@ -0,0 +1,33 @@
<template>
<li :ref="element" @click="onClick($event)">
<slot></slot>
</li>
</template>
<script lang="ts">
import {defineComponent, onMounted, onUpdated, ref} from "vue";
export default defineComponent({
name: "POption",
props: {
value: {
type: [String, Number, Object],
}
},
emits: ['select'],
setup(props,context) {
function onClick(e) {
e.target.data = props.value
}
const element = ref<HTMLElement>()
return {
element,onClick
}
}
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,159 @@
<template>
<div class="select-wrapper" :class="{active:isActive}" v-click-outside="onClose">
<div class="select-box-wrapper" @click="isActive = !isActive">
<div class="select-value">{{ selectValue }}<span v-if="!selectValue" class="placeholder">{{ placeholder }}</span>
</div>
<div class="icon-arrow">
<ArrowDown class="icon-select-arrow"/>
</div>
</div>
<div class="select-options-wrapper">
<ul @click="onSelect">
<slot></slot>
</ul>
</div>
</div>
</template>
<script lang="ts">
import ArrowDown from "../icon/ArrowDown.vue";
import {defineComponent, nextTick, ref, SetupContext} from "vue";
import {CHANGE_EVENT, UPDATE_MODEL_EVENT} from "../../service/constants";
export interface OptionItemType {
label: string;
value?: any;
}
// declare var OptionItemArray: OptionItemType[];
export default defineComponent({
name: "PSelect",
components: {ArrowDown},
props: {
placeholder: {
type: String,
default: '请选择'
},
modelValue: {
type: [String, Number, Object]
}
},
emits: [
UPDATE_MODEL_EVENT,
CHANGE_EVENT,
'focus',
'blur',
],
setup(props, ctx: SetupContext) {
const selectValue = ref(props.modelValue)
const isActive = ref(false)
function onSelect(e: MouseEvent) {
// dom
const target = e.target as HTMLElement;
if (target.tagName.toLowerCase() != 'li') return;
e.preventDefault();
e.stopPropagation();
//
const text = target.textContent
selectValue.value = text
const arr = [].slice.call (target.parentElement.children)
arr.forEach(ele => (ele as HTMLElement).classList.remove("selected"))
target.classList.add("selected")
console.log(target.data)
ctx.emit(UPDATE_MODEL_EVENT,target.data)
onClose()
}
function onClose() {
isActive.value = false
}
return {
selectValue, isActive, onClose, onSelect
}
}
})
</script>
<style lang="less">
.select-wrapper {
position: relative;
display: inline-block;
&.active {
.select-options-wrapper {
display: block;
}
.select-box-wrapper {
border-color: var(--primary-color);
box-shadow: 0 0 3px var(--primary-color);
}
}
}
.select-box-wrapper {
display: inline-flex;
position: relative;
width: 100%;
padding: 3px 6px;
color: #000000d9;
font-size: 14px;
background-color: #fff;
background-image: none;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-middle);
transition: all .3s;
cursor: pointer;
&:hover {
border-color: var(--primary-color);
}
.placeholder {
color: var(--placeholder-color)
}
.select-value {
flex: 1;
}
//
.icon-arrow {
display: flex;
align-items: center;
color: var(--border-color);
}
}
.select-options-wrapper {
display: none;
position: absolute;
background-color: #fff;
left: 0;
right: 0;
top: 32px;
box-shadow: 0 0 7px rgba(0, 0, 0, 0.1);
border-radius: var(--border-radius-middle);
ul, li {
list-style: none;
}
li {
line-height: 30px;
padding: 0 10px;
cursor: pointer;
&:hover {
background-color: #efefef;
}
&.selected {
color: var(--primary-color);
}
}
}
</style>

View File

@ -0,0 +1,71 @@
<template>
<div class="upload-wrapper">
<input type="file" :id="fileUploaderId" :accept="accept" @change="onFileSelectChange" class="file-handler"/>
<label :for="fileUploaderId">
<!-- 上传触发元素的插槽 -->
<slot/>
</label>
</div>
</template>
<script lang="ts">
import {defineComponent} from "vue";
export default defineComponent({
name: "Uploader"
})
</script>
<script lang="ts" setup>
import http from "../../util/http";
type PropsType = {
/**
* 上传路径
*/
action: string
/**
* 文件上传默认的参数名称默认为file
*/
fieldName?: string
/**
* 允许上传的文件类型默认为*表示所有文件
*/
accept?: string
modelValue?: string
}
type EmitsType = {
onSuccess: () => void
onError: () => void
'update:modelValue': (value: string) => void
}
type FileUploadResultModel = {
url: string
name?: string
}
const props = defineProps<PropsType>()
const emits = defineEmits<EmitsType>()
const fileUploaderId = Math.random().toString(16).substring(3)
async function onFileSelectChange(e: InputEvent) {
//@ts-ignore
const files = e.target.files as FileList
if (!files || files.length == 0) return;
const data = {},fieldName = props.fieldName || 'file'
data[fieldName] = files[0];
const ret = await http.request<FileUploadResultModel>(props.action, data, 'post', 'form')
emits('update:modelValue', ret.url)
}
</script>
<style scoped>
.upload-wrapper{
position: relative;
}
.file-handler{
opacity: 0;
width: 1px;
height: 1px;
position: absolute;
}
</style>

View File

@ -0,0 +1,60 @@
import type {
ObjectDirective,
} from 'vue'
const nodeList = new Map<Element, Function>();
document.addEventListener('mousedown', (e: MouseEvent) => {
e.stopPropagation()
// @ts-ignore
const paths = e.path as Element[];
// console.log(paths)
// 已经绑定过指令的元素
nodeList.forEach((fn, ele) => {
// 当前点击元素的数组
for (const c of paths) {
// 找到对应的元素 说明是点击了该元素或者他的子元素
if (c == ele) {
return;
}
}
// 没有找到 说明在外面 可以出发事件
fn.call(null)
})
})
// 定义指令
const clickOutside: ObjectDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
nodeList.set(el,binding.value)
// console.log(nodeList)
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {
},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {
},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {
},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {
},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {
nodeList.delete(el)
// console.log('remove ', index, el)
// nodeList.splice(index, 1)// 移除要销毁的元素
// console.log(nodeList)
},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {
}
}
export default clickOutside

View File

@ -4,8 +4,9 @@ import {createPinia} from 'pinia'
import App from './App.vue'
import router from './router'
import {httpConfig} from "./util/http";
import './assets/app.css'
import './assets/app.less'
import PInput from "./components/input";
import clickOutside from "./directive/click_outside";
httpConfig.baseURL = "http://localhost:8080"
@ -14,5 +15,6 @@ const app = createApp(App)
app.use(router)
app.use(createPinia());
app.component('PInput',PInput)
app.directive('click-outside',clickOutside)
// 将应用实例挂载到 模板中
app.mount('#vue-root-app')

View File

@ -2,24 +2,61 @@ import {RouteRecordRaw} from "vue-router";
import Home from '../views/admin/Home.vue'
import Login from '../views/Login.vue'
import NotFound from '../views/NotFound.vue'
import Test from '../views/Test.vue'
import AdminLayout from '../views/layout/AdminLayout.vue'
import UserIndex from '../views/user/index.vue'
import GoodsIndex from '../views/goods/index.vue'
import OrderIndex from '../views/goods/Order.vue'
export const AdminRoutes: RouteRecordRaw[] = [
{
path: 'home',
name: 'Home',
component: Home,
meta: {
title: 'Home'
}
},
{
path: 'user',
name: 'UserIndex',
component: UserIndex,
meta: {
title: '用户管理'
}
},
{
path: 'goods',
name: 'GoodsIndex',
component: GoodsIndex,
meta: {
title: '商品管理'
}
},
{
path: 'order',
name: 'OrderIndex',
component: OrderIndex,
meta: {
title: '订单管理'
}
},
{
path: 'test',
name: 'TestIndex',
component: Test,
meta: {
title: '测试页'
}
}
]
const routes: RouteRecordRaw[] = [
{
path: '/',
component: AdminLayout,
redirect: '/home',
children: [
{
path: 'home',
component: Home
},
{
path: 'user',
component: UserIndex
}
]
children: AdminRoutes
},
{
path: '/login',

View File

@ -0,0 +1,3 @@
export const UPDATE_MODEL_EVENT = 'update:modelValue'
export const CHANGE_EVENT = 'change'
export const INPUT_EVENT = 'input'

View File

@ -52,7 +52,6 @@ export const useUserStore = defineStore('user-store', () => {
const data = await http.get<AdminLoginModel>('/admin/user/info')
localStorage.removeItem(TOKEN_KEY)
userinfo.value = null
}
return {userinfo, login, logout, updateInfo, token}

View File

@ -26,4 +26,35 @@ type UserInfoModel = {
status: number;
pointInfo: PointInfoModel;
parent?: UserInfoModel | null;
}
type GoodsModel = {
id?: number;
category?: number;
type?: number;
title?: string;
originPrice?: number;
price?: number;
stock?: number;
limitCount?: number;
cover?: string;
description?: string;
notice?: string;
onlineTime?: string;
offlineTime?: string;
createTime?: string;
updateTime?: string;
status?: number;
}
type OrderInfoModel = {
id?: string;
gid?: number;
orderTitle?: string;
price?: number;
count?: number;
uid?: number;
data?: any;
createTime?: string;
updateTime?: string;
status?: number;
owner?: UserInfoModel;
}

View File

@ -1,12 +1,13 @@
import {toast} from "../components/message";
import {useUserStore} from "../service/store";
import router from '../router'
export type HttpMethod = 'get' | 'post' | 'delete' | 'put'
export enum RequestDataContentType {
JSON = 'application/json;charset=UTF-8',
FORM = 'application/x-www-form-urlencoded;charset=UTF-8',
FORM_DATA = 'multipart/form-data;charset=UTF-8',
FORM_DATA = 'multipart/form-data',
}
export const httpConfig = {
@ -41,6 +42,17 @@ class Http {
return this.request<T>(url, data, 'get')
}
put<T>(url, data = null) {
return this.request<T>(url, data, 'put')
}
remove<T>(url, data = null) {
return this.request<T>(url, data, 'delete')
}
delete<T>(url, data = null) {
return this.request<T>(url, data, 'delete')
}
/**
* GET /xxxx?a=1&b=2
* NAME: value
@ -56,7 +68,9 @@ class Http {
*/
request<T>(url: string, data: any = null, method: HttpMethod = 'post', type: 'normal' | 'form' = 'normal') {
return new Promise<T>((resolve, reject) => {
let contentType = RequestDataContentType.FORM;
const headers: any = {
'Content-Type': RequestDataContentType.FORM
}
if (data) {
if (type === 'form') {
const form = new FormData();
@ -64,12 +78,13 @@ class Http {
form.append(key, data[key]);
}
data = form; // 将data有{} => formData
contentType = RequestDataContentType.FORM_DATA; // 由于使用formData
// contentType = RequestDataContentType.FORM_DATA; // 由于使用formData
delete headers['Content-Type'];
method = 'post';
} else if (method == 'post') {
} else if (method == 'post' || method == 'put') {
// 将数据对象 转成json
data = JSON.stringify(data);
contentType = RequestDataContentType.JSON;
headers['Content-Type'] = RequestDataContentType.JSON;
} else {
// 装对象转成 key=value&key=value
const params = [];
@ -80,10 +95,6 @@ class Http {
url += '?' + params.join('&')
}
}
const headers: any = {
'Content-Type': contentType
}
const token = useUserStore().token();
if (token) headers.token = token
fetch(httpConfig.baseURL + url, {
@ -99,9 +110,11 @@ class Http {
// 需要统一处理数据
if (code === 403) {
toast("登录凭证无效或者已过期")
// if (r.fullPath != '/login') {
// router.replace('/login');
// }
// 通过router获取当前路由
const route = router.currentRoute;
if (route.fullPath != '/login') {
router.replace('/login');
}
return;
} else if (err) {
toast(httpConfig.globalErrorHandler[code])

View File

@ -63,7 +63,7 @@ p{
margin: 10px 0;
}
.login-wrapper {
width: 500px;
width: 300px;
margin: 50px auto;
}
</style>

View File

@ -1,43 +1,59 @@
<template>
<div class="demo-box">
<div class="form-item">
<PInput placeholder="测试21" v-model="data.a">
<template #suffix>
<span>密码</span>
</template>
</PInput>
</div>
<div class="form-item">
<PInput type="password" placeholder="测试12" v-model="data.b"/>
</div>
<div class="form-item">
<PInput type="password" placeholder="测试12" v-model="data.b"/>
</div>
<div>
{{ JSON.stringify(data) }}
</div>
<div class="demo-box">
<div>
<div class="form-item">
<PInput placeholder="测试21" v-model="data.a">
<template #suffix>
<span>密码</span>
</template>
</PInput>
</div>
<div class="form-item">
<PInput type="password" placeholder="测试12" v-model="data.b"/>
</div>
<div class="form-item">
<PInput type="password" placeholder="测试12" v-model="data.b"/>
</div>
<div>
{{ JSON.stringify(data) }}
</div>
</div>
<hr>
商品类型
<p-select v-model="select">
<p-option v-for="op in options" :key="op.value" :value="op">{{ op.label }}</p-option>
</p-select>
{{ JSON.stringify(select) }}
</div>
</template>
<script lang="ts" setup>
import PInput from "../components/input";
import {reactive} from "vue";
import {reactive, ref} from "vue";
import PSelect from "../components/select/Select.vue";
import POption from "../components/select/Option.vue";
const data = reactive({
a: '1',
b: '2'
a: '1',
b: '2'
})
const options = [
{value: 1, label: '普通商品'},
{value: 2, label: '精选商品'},
{value: 3, label: '秒杀商品'},
]
const select = ref()
</script>
<style scoped>
.demo-box {
width: 500px;
border: solid 1px #f00;
margin: auto;
padding: 20px;
width: 500px;
border: solid 1px #f00;
margin: auto;
padding: 20px;
}
.form-item {
margin: 10px 0;
margin: 10px 0;
}
</style>

View File

@ -0,0 +1,140 @@
<template>
<div class="table-wrapper">
<div class="table-toolbar">
<div class="search-form">
<span>订单编号<PInput style="width:200px" v-model="param.id" placeholder="要查询的订单编号"/></span>
<span>标题<PInput style="width:200px" v-model="param.title" placeholder="要查询的订单标题"/></span>
<span>订单开始<PInput style="width:200px" v-model="param.createTimeStart" placeholder="要查询的订单开始时间"/></span>
<span>订单结束<PInput style="width:200px" v-model="param.createTimeEnd" placeholder="要查询的订单结束时间"/></span>
<span>
<PButton type="default" @click="onReset">重置</PButton>
</span>
<span><PButton :loading="searching" @click="onSearch">搜索</PButton></span>
</div>
</div>
<table>
<tr>
<th>订单编号</th>
<th>标题</th>
<th>价格</th>
<th>数量</th>
<th>状态</th>
<th>用户</th>
<th>创建时间</th>
<th>操作</th>
</tr>
<tr v-for="it in goodsList" :key="it.id">
<td>{{ it.id }}</td>
<td>{{ it.orderTitle }}</td>
<td>{{ it.price }}</td>
<td>{{ it.count }}</td>
<td>{{ StatusEnum[it.status] || '未知' }}</td>
<td>{{ it.owner?.nickname }}</td>
<td>{{ it.createTime }}</td>
<td width="160">
<PButton v-if="it.status == 1" type="link" @click="updateStatus(it.id,2)">确认</PButton>
<PButton v-if="it.status < 3" type="link" @click="updateStatus(it.id,3)">取消</PButton>
<PButton v-if="it.status < 3" type="link" @click="updateStatus(it.id,4)">完成</PButton>
<PButton type="link" @click="removeData(it.id)">删除</PButton>
</td>
</tr>
</table>
<Pager :total="totalCount" show-refresh @refresh="loadDataList" @page-change="onPageChange"/>
</div>
</template>
<script setup lang="ts">
import {onMounted, reactive, ref} from "vue";
import http, {DataListModel} from "../../util/http";
import message from "../../components/message";
import Pager from "../../components/pager/Pager.vue";
import PButton from "../../components/button/Index.vue";
const StatusEnum = {
1: '待确认',
2: '已确认',
3: '已取消',
4: '已完成',
0: '已删除'
}
const param = reactive({
id: null,
title: null,
createTimeStart: null,
createTimeEnd: null,
page: 1,
pageSize: 10
})
const goodsList = ref<OrderInfoModel[]>([]);
const totalCount = ref(0)
const searching = ref(false)
function onPageChange(currentPage: number) {
param.page = currentPage;
loadDataList();
}
function onReset() {
param.page = 1;
param.title = null;
param.id = null
param.createTimeStart = null
param.createTimeEnd = null
loadDataList();
}
function onSearch() {
param.page = 1;
loadDataList();
}
function loadDataList() {
searching.value = true
http.post<DataListModel<OrderInfoModel>>('/admin/order/list', param).then(res => {
goodsList.value = res.items
totalCount.value = res.total
}).finally(() => searching.value = false)
}
async function updateStatus(id: string, status: number) {
if (!confirm('是否继续操作?')) {
return;
}
try {
await http.put(`/admin/order/${id}/status?status=${status}`);
message.toast('操作成功');
loadDataList();
} catch (e) {
message.toast(e.message || '操作失败')
}
}
/**
* 删除用户
* @param id 要删除的用户id
*/
async function removeData(id: string) {
if (!confirm('是否删除?')) {
return;
}
try {
await http.delete(`/admin/order/${id}`);
message.toast('删除成功');
loadDataList();
} catch (e) {
message.toast(e.message || '删除失败')
}
}
onMounted(loadDataList)
</script>
<style scoped>
.search-form span {
display: inline-block;
margin-right: 10px;
}
</style>

View File

@ -0,0 +1,296 @@
<template>
<div class="table-wrapper">
<div class="table-toolbar">
<div class="search-form">
<span>标题<PInput style="width:200px" v-model="param.title" placeholder="要查询的商品标题"/></span>
<span>
分类
<PSelect v-model="param.category">
<POption v-for="(text,value) in CategoryEnum" :key="value" :value="value">{{text}}</POption>
</PSelect>
</span>
<span>
<PButton type="default" @click="onReset">重置</PButton>
</span>
<span><PButton :loading="searching" @click="onSearch">搜索</PButton></span>
</div>
<div class="toolbar-extra">
<PButton @click="onEditData({id:0})">新增</PButton>
</div>
</div>
<table>
<tr>
<th>商品编号</th>
<th>标题</th>
<th>图片</th>
<th>分类</th>
<th>类型</th>
<th>原价</th>
<th>售卖价</th>
<th>库存</th>
<th>限购数量</th>
<th>上架时间</th>
<th>下架时间</th>
<th>状态</th>
<th>操作</th>
</tr>
<tr v-for="it in goodsList" :key="it.id">
<td>{{ it.id }}</td>
<td>{{ it.title }}</td>
<td><img :src="it.cover" style="width: 32px;height: 32px;"/></td>
<td>{{ CategoryEnum[it.category] || '未知' }}</td>
<td>{{ TypeEnum[it.type] || '未知' }}</td>
<td>{{ it.originPrice == 0 ? '-' : it.originPrice }}</td>
<td>{{ it.price }}</td>
<td>{{ it.stock }}</td>
<td>{{ it.limitCount }}</td>
<td>{{ it.onlineTime }}</td>
<td>{{ it.offlineTime }}</td>
<td>{{ StatusEnum[it.status] || '未知' }}</td>
<td>
<PButton type="link" @click="onEditData(it)">编辑</PButton>
<PButton type="link">禁用</PButton>
<PButton type="link" @click="removeData(it.id)">删除</PButton>
</td>
</tr>
</table>
<Pager :total="totalCount" show-refresh @refresh="loadDataList" @page-change="onPageChange"/>
</div>
<Modal v-model="modalVisible">
<div>
<div>
<span>标题</span>
<p>
<PInput placeholder="请输入标题" v-model="editData.title"/>
</p>
</div>
<div class="display-flex">
<div>
<span>分类</span>
<p>
<select v-model="editData.category">
<option v-for="(text,value) in CategoryEnum" :key="value" :value="value">{{ text }}</option>
</select>
</p>
</div>
<div>
<span>类型</span>
<p>
<select v-model="editData.type">
<option v-for="(text,value) in TypeEnum" :key="value" :value="value">{{ text }}</option>
</select>
</p>
</div>
</div>
<div>
<span>商品图片</span>
<!-- 上传组件 -->
<Uploader action="/file/upload" v-model="editData.cover">
<div v-if="editData.cover">
<img :src="editData.cover" alt="" style="width: 120px;height: 120px;">
</div>
<div v-else>选择商品图</div>
</Uploader>
</div>
<div>
<span>描述</span>
<p>
<PInput placeholder="请输入描述" v-model="editData.description"/>
</p>
</div>
<div>
<span>原价</span>
<p>
<PInput type="number" placeholder="请输入原价" v-model="editData.originPrice"/>
</p>
</div>
<div>
<span>销售价格</span>
<p>
<PInput type="number" placeholder="请输入销售价格" v-model="editData.price"/>
</p>
</div>
<div>
<span>库存</span>
<p>
<PInput type="number" placeholder="请输入库存" v-model="editData.stock"/>
</p>
</div>
<div>
<span>限购数量</span>
<p>
<PInput type="number" placeholder="请输入限购数量" v-model="editData.limitCount"/>
</p>
</div>
<div>
<span>上架时间</span>
<p>
<PInput placeholder="请输入上架时间" v-model="editData.onlineTime"/>
</p>
</div>
<div>
<span>下架时间</span>
<p>
<PInput placeholder="请输入下架时间" v-model="editData.offlineTime"/>
</p>
</div>
<PButton @click="saveGoodsData">提交</PButton>
</div>
</Modal>
</template>
<script setup lang="ts">
import {onMounted, reactive, ref} from "vue";
import http, {DataListModel} from "../../util/http";
import message from "../../components/message";
import Modal from "../../components/modal/modal.vue";
import Pager from "../../components/pager/Pager.vue";
import Uploader from "../../components/uploader/uploader.vue";
import PButton from "../../components/button/Index.vue";
import PSelect, {OptionItemType} from "../../components/select/Select.vue";
import POption from "../../components/select/Option.vue";
//
//(1: 2: 3: 4:)
const CategoryEnum = {
1: '普通',
2: '精选',
3: '秒杀',
4: '抽奖',
}
//(1: 2:)
const TypeEnum = {
1: '实物',
2: '虚拟'
}
const options: OptionItemType[] = [
{
label: '普通', value: 1
},
{
label: '精选', value: 2
},
{
label: '秒杀', value: 3
}
]
const StatusEnum = {
1: '正常',
2: '禁用',
0: '已删除'
}
const param = reactive({
title: null,
category: null,
page: 1,
pageSize: 10
})
const modalVisible = ref(false)
const goodsList = ref<GoodsModel[]>([]);
const editData = reactive<GoodsModel>({
id: 0
});
const totalCount = ref(0)
const searching = ref(false)
const saving = ref(false)
function onPageChange(currentPage: number) {
param.page = currentPage;
loadDataList();
}
function onReset() {
param.page = 1;
param.title = null;
param.category = null
loadDataList();
}
function onSearch() {
param.page = 1;
loadDataList();
}
function loadDataList() {
searching.value = true
http.post<DataListModel<GoodsModel>>('/admin/goods/list', param).then(res => {
goodsList.value = res.items
totalCount.value = res.total
}).finally(() => searching.value = false)
}
function fillDataToObject(obj: any, data: any, keys: string[]) {
keys.forEach(key => {
if (typeof (data[key]) === 'undefined') {
if (typeof (obj[key]) != 'undefined') {
// null
obj[key] = null;
}
return;
}
obj[key] = data[key]
})
}
const fieldKey = [
'id', 'cover', 'title',
'type', 'category', 'description',
'offlineTime', 'onlineTime', 'price',
'originPrice', 'stock', 'limitCount',
]
function onEditData(data: GoodsModel) {
fillDataToObject(editData, data, fieldKey)
modalVisible.value = true
}
async function saveGoodsData() {
try {
saving.value = true
if (editData.id <= 0) {
await http.post('/admin/goods', editData)
} else {
await http.put(`/admin/goods/${editData.id}`, editData)
}
modalVisible.value = false;
message.toast('保存成功');
loadDataList();
} catch (e) {
message.toast(e.message || '保存失败')
} finally {
saving.value = false
}
}
/**
* 删除用户
* @param id 要删除的用户id
*/
async function removeData(id: number) {
if (!confirm('是否删除?')) {
return;
}
try {
await http.delete(`/admin/goods/${id}`);
message.toast('删除成功');
loadDataList();
} catch (e) {
message.toast(e.message || '删除失败')
}
}
onMounted(loadDataList)
</script>
<style scoped>
.search-form span {
display: inline-block;
margin-right: 10px;
}
</style>

View File

@ -6,14 +6,17 @@
<span>
{{ userStore.userinfo?.account }}
</span>
<span @click="logout">退出</span>
</div>
</div>
<div class="main">
<div class="left-menu">
<div class="title">积分管理系统</div>
<div class="menu-list">
<router-link active-class="active-menu" to="/home">HOME</router-link>
<router-link active-class="active-menu" to="/user">用户管理</router-link>
<router-link v-for="r in AdminRoutes" active-class="active-menu" :to="'/' + r.path">{{
r.meta.title
}}
</router-link>
</div>
</div>
<div class="content-wrapper">
@ -27,18 +30,26 @@
</template>
<script setup>
import {useUserStore} from "../../service/store";
import {useRouter} from "vue-router";
import {AdminRoutes} from './../../router/routes'
const userStore = useUserStore();
const router = useRouter()
function logout() {
userStore.logout()
router.replace('/login')
}
</script>
<style lang="less">
.app-admin-layout {
min-height: 100vh;
background-color: #eee;
}
.header {
background-color: var(--primary-color);
background-color: var(--header-bg);
color: white;
line-height: var(--header-height);
padding: 0 20px;
@ -48,6 +59,7 @@ const userStore = useUserStore();
left: 0;
right: 0;
top: 0;
z-index: 9;
}
.main {
@ -56,34 +68,42 @@ const userStore = useUserStore();
.left-menu {
width: 200px;
background-color: black;
color: white;
background-color: #fff;
color:#333;
box-shadow: 0 2px 3px rgba(0,0,0,0.4);
position: fixed;
left: 0;
top: 0;
bottom: 0;
z-index: 10;
font-size: 14px;
}
.title{
.title {
height: var(--header-height);
background-color: #333;
background-color: var(--header-bg);
line-height: var(--header-height);
text-align: center;
color:white;
font-size: var(--font-size-large);
}
.menu-list{
.menu-list {
margin-top: 20px;
a{
a {
display: block;
text-decoration: none;
color:white;
padding:10px 20px;
&.active-menu{
background-color: #666666;
color: white;
padding: 10px 20px;
color:#333;
&.active-menu {
background-color: var(--primary-1);
color: var(--primary-color);
}
}
}
.content-wrapper {
margin: 20px;
background-color: #fff;

View File

@ -1,35 +1,38 @@
<template>
<table>
<tr>
<th>UID</th>
<th>昵称</th>
<th>OPEN_ID</th>
<th>性别</th>
<th>地区</th>
<th>推荐者</th>
<th>状态</th>
<th>积分</th>
<th>注册时间</th>
<th>操作</th>
</tr>
<tr v-for="u in userList" :key="u.id">
<td>{{ u.id }}</td>
<td>{{ u.nickname }}</td>
<td>{{ u.openId }}</td>
<td>{{ GenderEnum[u.gender] || '未知' }}</td>
<td>{{ u.province + '-' + u.city }}</td>
<td>{{ u.parent?.nickname || '-' }}</td>
<td>{{ StatusEnum[u.status] || '未知' }}</td>
<td>{{ u.pointInfo?.totalPoint }}</td>
<td>{{ u.firstLoginTime }}</td>
<td>
<span @click="editUser(u)">编辑</span>
<span>禁用</span>
<span @click="removeUser(u.id)">删除</span>
</td>
</tr>
</table>
<div class="table-wrapper">
<table>
<tr>
<th>UID</th>
<th>昵称</th>
<th>OPEN_ID</th>
<th>性别</th>
<th>地区</th>
<th>推荐者</th>
<th>状态</th>
<th>积分</th>
<th>注册时间</th>
<th>操作</th>
</tr>
<tr v-for="u in userList" :key="u.id">
<td>{{ u.id }}</td>
<td>{{ u.nickname }}</td>
<td>{{ u.openId }}</td>
<td>{{ GenderEnum[u.gender] || '未知' }}</td>
<td>{{ u.province + '-' + u.city }}</td>
<td>{{ u.parent?.nickname || '-' }}</td>
<td>{{ StatusEnum[u.status] || '未知' }}</td>
<td>{{ u.pointInfo?.totalPoint }}</td>
<td>{{ u.firstLoginTime }}</td>
<td>
<span @click="editUser(u)">编辑</span>
<span>禁用</span>
<span @click="removeUser(u.id)">删除</span>
</td>
</tr>
</table>
<Pager :total="userTotalCount" show-refresh @refresh="loadUserList" @page-change="onPageChange"/>
</div>
<Modal v-model="modalVisible">
<div>
<div>
@ -60,6 +63,7 @@ import {onMounted, reactive, ref} from "vue";
import http, {DataListModel} from "../../util/http";
import message from "../../components/message";
import Modal from "../../components/modal/modal.vue";
import Pager from "../../components/pager/Pager.vue";
//
const GenderEnum = {
1: '男', 2: '女'
@ -88,6 +92,11 @@ const editUserData = reactive({
});
const userTotalCount = ref(0)
function onPageChange(currentPage: number) {
param.page = currentPage;
loadUserList();
}
function loadUserList() {
http.post<DataListModel<UserInfoModel>>('/admin/user/list', param).then(res => {
userList.value = res.items

View File

@ -12,6 +12,15 @@
"**/*.js"
],
"compilerOptions": {
"jsx": "preserve"
"jsx": "preserve",
"types": [
"unplugin-vue-define-options/macros-global"
],
"lib": [
"dom",
"es5",
"scripthost",
"es2015.collection"
]
}
}

View File

@ -1,14 +1,17 @@
import vue from '@vitejs/plugin-vue'
import vueJsxPlugin from "@vitejs/plugin-vue-jsx";
import DefineOptions from 'unplugin-vue-define-options/vite'
export default {
hmr: true,
// 开发服务信息
server: {
host:'::'
host: '::'
},
// 必须配置vue插件
plugins: [
vue(),
DefineOptions(),
vueJsxPlugin(), // 如果需要使用jsx或者tsx
]
}

View File

@ -200,7 +200,7 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
"@babel/parser@^7.16.4", "@babel/parser@^7.18.10", "@babel/parser@^7.20.5":
"@babel/parser@^7.16.4", "@babel/parser@^7.18.10", "@babel/parser@^7.20.0", "@babel/parser@^7.20.5":
version "7.20.5"
resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.20.5.tgz#7f3c7335fe417665d929f34ae5dceae4c04015e8"
integrity sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==
@ -253,7 +253,7 @@
debug "^4.1.0"
globals "^11.1.0"
"@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5":
"@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5":
version "7.20.5"
resolved "https://registry.npmmirror.com/@babel/types/-/types-7.20.5.tgz#e206ae370b5393d94dfd1d04cd687cace53efa84"
integrity sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==
@ -312,6 +312,14 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"
"@rollup/pluginutils@^4.2.1":
version "4.2.1"
resolved "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"
integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==
dependencies:
estree-walker "^2.0.1"
picomatch "^2.2.2"
"@types/node@^18.11.10":
version "18.11.10"
resolved "https://registry.npmmirror.com/@types/node/-/node-18.11.10.tgz#4c64759f3c2343b7e6c4b9caf761c7a3a05cee34"
@ -376,6 +384,15 @@
"@volar/typescript" "1.0.11"
"@volar/vue-language-core" "1.0.11"
"@vue-macros/common@~0.13.4":
version "0.13.4"
resolved "https://registry.npmmirror.com/@vue-macros/common/-/common-0.13.4.tgz#f1a12c63aad18ad0020101bf386f5e9c95d444ba"
integrity sha512-mQooO33XcY4kQyKBrbGfdIPPsYhpcfmH75SQnXx2vNsNLSNvhLuDaIIV0fhMJ0HV5Z02V9Ka1gx7v1g5bk9Q0A==
dependencies:
"@babel/types" "^7.20.2"
"@vue/compiler-sfc" "^3.2.45"
magic-string "^0.26.7"
"@vue/babel-helper-vue-transform-on@^1.0.2":
version "1.0.2"
resolved "https://registry.npmmirror.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.0.2.tgz#9b9c691cd06fc855221a2475c3cc831d774bc7dc"
@ -491,6 +508,11 @@
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.2.45.tgz#a3fffa7489eafff38d984e23d0236e230c818bc2"
integrity sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==
acorn@^8.8.1:
version "8.8.1"
resolved "https://registry.npmmirror.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73"
integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==
ansi-styles@^3.2.1:
version "3.2.1"
resolved "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
@ -498,11 +520,32 @@ ansi-styles@^3.2.1:
dependencies:
color-convert "^1.9.0"
anymatch@~3.1.2:
version "3.1.3"
resolved "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
dependencies:
normalize-path "^3.0.0"
picomatch "^2.0.4"
ast-walker-scope@^0.3.0:
version "0.3.0"
resolved "https://registry.npmmirror.com/ast-walker-scope/-/ast-walker-scope-0.3.0.tgz#955b00af19946e76d39ba86d3046b9bc2b7312d9"
integrity sha512-bsOBv3jB+1kGaxwPHhkLiagS+75KfzEqtkNWvATgMGtXM6kJZG3PlG4fYQFMiHeLpoAkwc6G61w07+hEXx39aA==
dependencies:
"@babel/parser" "^7.20.0"
"@babel/types" "^7.20.0"
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
binary-extensions@^2.0.0:
version "2.2.0"
resolved "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
brace-expansion@^2.0.1:
version "2.0.1"
resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
@ -510,6 +553,13 @@ brace-expansion@^2.0.1:
dependencies:
balanced-match "^1.0.0"
braces@~3.0.2:
version "3.0.2"
resolved "https://registry.npmmirror.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
dependencies:
fill-range "^7.0.1"
browserslist@^4.21.3:
version "4.21.4"
resolved "https://registry.npmmirror.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987"
@ -539,6 +589,21 @@ chalk@^2.0.0:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.npmmirror.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
dependencies:
anymatch "~3.1.2"
braces "~3.0.2"
glob-parent "~5.1.2"
is-binary-path "~2.1.0"
is-glob "~4.0.1"
normalize-path "~3.0.0"
readdirp "~3.6.0"
optionalDependencies:
fsevents "~2.3.2"
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@ -742,11 +807,18 @@ escape-string-regexp@^1.0.5:
resolved "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
estree-walker@^2.0.2:
estree-walker@^2.0.1, estree-walker@^2.0.2:
version "2.0.2"
resolved "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
dependencies:
to-regex-range "^5.0.1"
fsevents@~2.3.2:
version "2.3.2"
resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
@ -762,6 +834,13 @@ gensync@^1.0.0-beta.2:
resolved "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
dependencies:
is-glob "^4.0.1"
globals@^11.1.0:
version "11.12.0"
resolved "https://registry.npmmirror.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
@ -806,6 +885,13 @@ image-size@~0.5.0:
resolved "https://registry.npmmirror.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"
integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
dependencies:
binary-extensions "^2.0.0"
is-core-module@^2.9.0:
version "2.11.0"
resolved "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144"
@ -813,6 +899,23 @@ is-core-module@^2.9.0:
dependencies:
has "^1.0.3"
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
is-glob@^4.0.1, is-glob@~4.0.1:
version "4.0.3"
resolved "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
dependencies:
is-extglob "^2.1.1"
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
is-what@^3.14.1:
version "3.14.1"
resolved "https://registry.npmmirror.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1"
@ -857,6 +960,13 @@ magic-string@^0.25.7:
dependencies:
sourcemap-codec "^1.4.8"
magic-string@^0.26.7:
version "0.26.7"
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.26.7.tgz#caf7daf61b34e9982f8228c4527474dac8981d6f"
integrity sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==
dependencies:
sourcemap-codec "^1.4.8"
make-dir@^2.1.0:
version "2.1.0"
resolved "https://registry.npmmirror.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
@ -911,6 +1021,11 @@ node-releases@^2.0.6:
resolved "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"
integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
parse-node-version@^1.0.1:
version "1.0.1"
resolved "https://registry.npmmirror.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b"
@ -926,6 +1041,11 @@ picocolors@^1.0.0:
resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2:
version "2.3.1"
resolved "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
pify@^4.0.1:
version "4.0.1"
resolved "https://registry.npmmirror.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
@ -953,6 +1073,13 @@ prr@~1.0.1:
resolved "https://registry.npmmirror.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
dependencies:
picomatch "^2.2.1"
resolve@^1.22.1:
version "1.22.1"
resolved "https://registry.npmmirror.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
@ -1026,6 +1153,13 @@ to-fast-properties@^2.0.0:
resolved "https://registry.npmmirror.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
dependencies:
is-number "^7.0.0"
tslib@^2.3.0:
version "2.4.1"
resolved "https://registry.npmmirror.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
@ -1036,6 +1170,26 @@ typescript@^4.9.3:
resolved "https://registry.npmmirror.com/typescript/-/typescript-4.9.3.tgz#3aea307c1746b8c384435d8ac36b8a2e580d85db"
integrity sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==
unplugin-vue-define-options@^1.0.0:
version "1.0.0"
resolved "https://registry.npmmirror.com/unplugin-vue-define-options/-/unplugin-vue-define-options-1.0.0.tgz#0aabe28748d5b445e68b6d44bc1170e3d01622a9"
integrity sha512-j90zM7NhZXBL5uMlHKzSOjvU98lFcIErdgAhj7bEEdvZarkwOkEUgMFsZDwStN9FEcMAiS/BTvcyGfItu3ry/g==
dependencies:
"@rollup/pluginutils" "^4.2.1"
"@vue-macros/common" "~0.13.4"
ast-walker-scope "^0.3.0"
unplugin "^1.0.0"
unplugin@^1.0.0:
version "1.0.0"
resolved "https://registry.npmmirror.com/unplugin/-/unplugin-1.0.0.tgz#8d12e0d116bf56313d42755094fc370e9c18da86"
integrity sha512-H5UnBUxfhTXBXGo2AwKsl0UaLSHzSNDZNehPQSgdhVfO/t+XAS1Yoj3vmLrrlBrS9ZwtH5tejbX/TCp5DcyCKg==
dependencies:
acorn "^8.8.1"
chokidar "^3.5.3"
webpack-sources "^3.2.3"
webpack-virtual-modules "^0.4.6"
update-browserslist-db@^1.0.9:
version "1.0.10"
resolved "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3"
@ -1094,3 +1248,13 @@ vue@^3.2.45:
"@vue/runtime-dom" "3.2.45"
"@vue/server-renderer" "3.2.45"
"@vue/shared" "3.2.45"
webpack-sources@^3.2.3:
version "3.2.3"
resolved "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
webpack-virtual-modules@^0.4.6:
version "0.4.6"
resolved "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.4.6.tgz#3e4008230731f1db078d9cb6f68baf8571182b45"
integrity sha512-5tyDlKLqPfMqjT3Q9TAqf2YqjwmnUleZwzJi1A5qXnlBCdj2AtOJ6wAWdglTIDOPgOiOrXeBeFcsQ8+aGQ6QbA==

View File

@ -11,7 +11,9 @@ public class ApplicationConfig implements WebMvcConfigurer {
// 跨域 目前小程序不需要暂时不用考虑
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*");
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE");
}

View File

@ -0,0 +1,21 @@
package me.xiaoyan.point.api.controller;
import lombok.SneakyThrows;
import me.xiaoyan.point.api.pojo.dto.FileUploadResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("file")
public class FileController {
@SneakyThrows
@PostMapping("upload")
public FileUploadResult upload(MultipartFile multipartFile) {
Thread.sleep(1);
String url = "https://m.360buyimg.com/mobilecms/s750x750_jfs/t1/211852/28/15135/312413/6235a37fEc6496443/de07faa29ece45ee.jpg";
return new FileUploadResult(url, "");
}
}

View File

@ -1,13 +1,21 @@
package me.xiaoyan.point.api.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import me.xiaoyan.point.api.error.BizException;
import me.xiaoyan.point.api.pojo.OrderInfo;
import me.xiaoyan.point.api.pojo.UserInfo;
import me.xiaoyan.point.api.pojo.vo.CreateOrderData;
import me.xiaoyan.point.api.pojo.vo.OrderQueryParam;
import me.xiaoyan.point.api.pojo.vo.PageDataResult;
import me.xiaoyan.point.api.service.GoodsService;
import me.xiaoyan.point.api.service.OrderInfoService;
import me.xiaoyan.point.api.util.OrderStatus;
import me.xiaoyan.point.api.util.QueryWrapperUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@ -47,9 +55,9 @@ public class ShopOrderInfoController {
//TODO 应该定时更新缓存数据
goodsService.queryAllGoodsIdAndStock().forEach(g -> {
log.info("缓存 id:{} stock:{} ", g.getId(), g.getStock());
if(g.getStock() == 0){
if (g.getStock() == 0) {
// 原本就没有库存
stockOutMap.put(g.getId(),true);
stockOutMap.put(g.getId(), true);
}
// 缓存库存
stringRedisTemplate.opsForValue().set(cacheKey(g.getId()), g.getStock().toString());
@ -64,7 +72,7 @@ public class ShopOrderInfoController {
int uid = StpUtil.getLoginIdAsInt();
// 如果限制用户只能购买的数量可以添加一个map记录用户的请求数
int size = stockOutMap.size();
Long buyId = (long)data.getGoodsId();
Long buyId = (long) data.getGoodsId();
//1.内存判断
if (size > 0 && stockOutMap.containsKey(buyId) && stockOutMap.get(buyId)) {
throw BizException.create("库存不足");
@ -74,22 +82,59 @@ public class ShopOrderInfoController {
if (count < 0) {
log.info("stock count ===>" + count);
// 对缓存进行库存 还原
stringRedisTemplate.opsForValue().increment(cacheKey(buyId),data.getBuyCount()); //
stringRedisTemplate.opsForValue().increment(cacheKey(buyId), data.getBuyCount()); //
throw BizException.create("库存不足");
}
if(count == 0){
if (count == 0) {
// 此时库存没有了 , 保存到已买完的对象
stockOutMap.put(buyId,true);
stockOutMap.put(buyId, true);
}
//3.数据库
try{
try {
return orderInfoService.create(uid, data);
}catch (BizException e){
} catch (BizException e) {
//下单失败 对缓存进行库存 还原
stringRedisTemplate.opsForValue().increment(cacheKey(data.getGoodsId()),data.getBuyCount()); //
stockOutMap.put((long)data.getGoodsId(),false);
stringRedisTemplate.opsForValue().increment(cacheKey(data.getGoodsId()), data.getBuyCount()); //
stockOutMap.put((long) data.getGoodsId(), false);
throw e;
}
}
// 只能查询自己关联的订单信息
@PostMapping("/query")
public Page<OrderInfo> query(@RequestBody OrderQueryParam param) {
param.setUid(StpUtil.getLoginIdAsInt());
return orderInfoService.queryByPage(param, true, false);
}
@GetMapping("/{id}")
public OrderInfo get(@PathVariable("id") String id) {
return orderInfoService.getOneByIdAndUid(id, StpUtil.getLoginIdAsInt());
}
private boolean updateStatus(String id, int status) {
if (!StringUtils.hasText(id)) throw BizException.paramError();
QueryWrapper<OrderInfo> q = QueryWrapperUtil.builder()
.eq("id", id)
.eq("uid", StpUtil.getLoginIdAsInt())
.build();
return orderInfoService.update(
OrderInfo.builder()
.status(status)
.build(),
q
);
}
@PutMapping("/{id}/cancel")
public boolean cancelOrder(@PathVariable("id") String id) {
return updateStatus(id, OrderStatus.CANCELED);
}
@PutMapping("/{id}/delete")
public boolean deleteOrder(@PathVariable("id") String id) {
return updateStatus(id, OrderStatus.DELETE);
}
}

View File

@ -1,14 +1,15 @@
package me.xiaoyan.point.api.controller.admin;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import me.xiaoyan.point.api.error.BizException;
import me.xiaoyan.point.api.pojo.Goods;
import me.xiaoyan.point.api.pojo.vo.GoodsQueryParam;
import me.xiaoyan.point.api.pojo.vo.PageDataResult;
import me.xiaoyan.point.api.pojo.vo.PageParam;
import me.xiaoyan.point.api.service.GoodsService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import me.xiaoyan.point.api.util.DataStatus;
import org.apache.ibatis.annotations.Delete;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@ -22,4 +23,43 @@ public class GoodsAdminController {
public PageDataResult list(@RequestBody GoodsQueryParam param) {
return PageDataResult.convert(goodsService.queryByPage(param));
}
// 新增
@PostMapping
public Goods create(@RequestBody Goods goods) {
if (!goodsService.save(goods)) {
throw BizException.saveFail();
}
return goods;
}
// 修改
@PutMapping("/{id}")
public Goods update(@RequestBody Goods goods, @PathVariable("id") long id) {
goods.setId(id);
if (!goodsService.updateById(goods)) {
throw BizException.saveFail();
}
return goods;
}
// 查询
@GetMapping("/{id}")
public Goods get(@PathVariable("id") long id) {
return goodsService.getById(id);
}
// 删除
@DeleteMapping("/{id}")
public boolean remove(@PathVariable("id") long id) {
if (id < 1) throw BizException.paramError();
// 只是逻辑删除 -> 将状态更新未删除即可
return goodsService.updateById(
Goods.builder()
.id(id)
.status(DataStatus.DELETE) // 状态设置为删除
.build()
);
}
}

View File

@ -0,0 +1,66 @@
package me.xiaoyan.point.api.controller.admin;
import me.xiaoyan.point.api.error.BizException;
import me.xiaoyan.point.api.pojo.OrderInfo;
import me.xiaoyan.point.api.pojo.vo.OrderQueryParam;
import me.xiaoyan.point.api.pojo.vo.PageDataResult;
import me.xiaoyan.point.api.service.OrderInfoService;
import me.xiaoyan.point.api.util.DataStatus;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@RestController
@RequestMapping("/admin/order")
public class OrderAdminController {
@Resource
private OrderInfoService orderInfoService;
@PostMapping("/list")
public PageDataResult list(@RequestBody OrderQueryParam param) {
return PageDataResult.convert(orderInfoService.queryByPage(param,false,true));
}
// 修改
@PutMapping("/{id}")
public OrderInfo update(@RequestBody OrderInfo info, @PathVariable("id") String id) {
info.setId(id);
if (!orderInfoService.updateById(info)) {
throw BizException.saveFail();
}
return info;
}
// 查询
@GetMapping("/{id}")
public OrderInfo get(@PathVariable("id") String id) {
return orderInfoService.getById(id);
}
// 删除
@DeleteMapping("/{id}")
public boolean remove(@PathVariable("id") String id) {
if (!StringUtils.hasText(id)) throw BizException.paramError();
// 只是逻辑删除 -> 将状态更新未删除即可
return orderInfoService.updateById(
OrderInfo.builder()
.id(id)
.status(DataStatus.DELETE) // 状态设置为删除
.build()
);
}
@PutMapping("/{id}/status")
public boolean setStatus(@PathVariable("id") String id, int status) {
if (!StringUtils.hasText(id)) throw BizException.paramError();
return orderInfoService.updateById(
OrderInfo.builder()
.id(id)
.status(status)
.build()
);
}
}

View File

@ -40,6 +40,12 @@ public class UserAdminController {
userStoreMap.put("test", UserAdminInfo.create(2, "test", "123123"));
}
@PostMapping
public UserInfo update(@RequestBody UserInfo userInfo) {
if (!userInfoService.updateById(userInfo)) throw BizException.saveFail();
return userInfo;
}
/**
* 用户登录
* @status released
@ -50,7 +56,7 @@ public class UserAdminController {
@SneakyThrows
@PostMapping("login")
public UserAdminInfo login(@Validated @RequestBody UserAdminInfo user) {
Thread.sleep(1);
Thread.sleep(2000);
// 判断是否存在账号
if (!userStoreMap.containsKey(user.getAccount())) {
throw BizException.create("账号不存在");

View File

@ -17,7 +17,20 @@ public class BizException extends RuntimeException {
public static BizException create(String message) {
return new BizException(-1, message);
}
public static BizException create(int code,String message) {
public static BizException create(int code, String message) {
return new BizException(code, message);
}
public static BizException saveFail() {
return create(1022, "保存失败");
}
public static BizException paramError(String message) {
return create(1201, message);
}
public static BizException paramError() {
return paramError("请求参数不正确");
}
}

View File

@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -83,21 +84,25 @@ public class Goods implements Serializable {
/**
* 上架时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date onlineTime;
/**
* 下架时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date offlineTime;
/**
*
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
*
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
/**

View File

@ -2,9 +2,11 @@ package me.xiaoyan.point.api.pojo;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -24,13 +26,14 @@ import java.io.Serializable;
@AllArgsConstructor
@NoArgsConstructor
@Builder
@TableName(value ="order_info")
@TableName(value = "order_info")
public class OrderInfo {
@TableId
//订单编号
private String id;
//商品编号
private Long gid;
private String orderTitle;
//价格
private Integer price;
//购买数量
@ -39,12 +42,17 @@ public class OrderInfo {
private Integer uid;
//订单数据
private String data;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
//订单状态(0:已删除 1:已取消 2:待确认 3:已完成)
private Integer status;
@TableField(exist = false)
private UserInfo owner;
@TableField(exist = false)
private Goods goods;
}

View File

@ -2,6 +2,7 @@ package me.xiaoyan.point.api.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -22,6 +23,7 @@ public class Point implements Serializable {
private Integer totalPoint;
private Integer validPoint;
private Integer expirePoint;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date expireTime;
private Date updateTime;
}

View File

@ -3,6 +3,7 @@ package me.xiaoyan.point.api.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -25,6 +26,8 @@ public class PointRecord implements Serializable {
private Integer point;
private Integer currentTotalPoint;
private String reason;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date validTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date expireTime;
}

View File

@ -2,6 +2,7 @@ package me.xiaoyan.point.api.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -22,5 +23,7 @@ public class SignRecord implements Serializable {
private Integer uid;
private Integer point;
private String ip;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}

View File

@ -4,12 +4,14 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Date;
@ -24,6 +26,7 @@ public class UserInfo implements Serializable {
* 用户id
*/
@TableId(type = IdType.AUTO)
@NotNull
private Integer id;
/**
* 微信openid
@ -56,6 +59,7 @@ public class UserInfo implements Serializable {
/**
* 第一次登录时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date firstLoginTime;
private Date updateTime;
/**

View File

@ -0,0 +1,16 @@
package me.xiaoyan.point.api.pojo.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class FileUploadResult implements Serializable {
private String url;
private String name;
}

View File

@ -2,7 +2,8 @@ package me.xiaoyan.point.api.pojo.dto;
public class OrderStatus {
public static final int DELETE = 0;
public static final int CANCEL = 1;
public static final int NOT_CONFIRM = 1;
public static final int CONFIRM = 2;
public static final int DONE = 3;
public static final int CANCEL = 3;
public static final int FINISH = 4;
}

View File

@ -5,5 +5,5 @@ import lombok.Data;
@Data
public class GoodsQueryParam extends PageParam {
private String title;
private int category;
private Integer category;
}

View File

@ -0,0 +1,13 @@
package me.xiaoyan.point.api.pojo.vo;
import lombok.Data;
@Data
public class OrderQueryParam extends PageParam {
private String id;
private String title;
private String createTimeStart;
private String createTimeEnd;
private int uid;
private Integer status;
}

View File

@ -13,10 +13,10 @@ import lombok.experimental.Accessors;
@Accessors(chain = true)
@Builder
public class PageParam {
private Integer page;
private Integer page = 1;
private Integer pageSize = 20;
public Page getPage(){
public Page getPage() {
return new Page().setCurrent(page).setSize(pageSize);
}
}

View File

@ -25,4 +25,5 @@ public interface GoodsService extends IService<Goods> {
boolean deductStock(int id,int count);
List<Goods> queryAllGoodsIdAndStock();
Goods queryByTitle(String title);
}

View File

@ -1,8 +1,10 @@
package me.xiaoyan.point.api.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import me.xiaoyan.point.api.pojo.OrderInfo;
import me.xiaoyan.point.api.pojo.vo.CreateOrderData;
import me.xiaoyan.point.api.pojo.vo.OrderQueryParam;
/**
* 订单表(OrderInfo)表服务接口
@ -13,5 +15,9 @@ import me.xiaoyan.point.api.pojo.vo.CreateOrderData;
public interface OrderInfoService extends IService<OrderInfo> {
OrderInfo create(int uid, CreateOrderData data);
Page queryByPage(OrderQueryParam param,boolean queryGoods,boolean queryUser);
OrderInfo getOneByIdAndUid(String id, int uid);
}

View File

@ -7,6 +7,7 @@ import me.xiaoyan.point.api.pojo.Goods;
import me.xiaoyan.point.api.pojo.vo.GoodsQueryParam;
import me.xiaoyan.point.api.service.GoodsService;
import me.xiaoyan.point.api.mapper.GoodsMapper;
import me.xiaoyan.point.api.util.DataStatus;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
@ -31,9 +32,15 @@ public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods>
if (StringUtils.hasText(param.getTitle())) {
q.eq("title", param.getTitle().trim());
}
if (param.getCategory() != null) {
q.eq("category", param.getCategory());
}
// 不查询删除状态数据
q.ne("status", DataStatus.DELETE);
return getBaseMapper().selectPage(param.getPage(), q);
}
@Override
public boolean deductStock(int id, int count) {
return getBaseMapper().deductCount(id, count) == 1;
@ -43,6 +50,16 @@ public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods>
public List<Goods> queryAllGoodsIdAndStock() {
return getBaseMapper().queryAllGoodsIdAndStock();
}
@Override
public Goods queryByTitle(String title) {
if (!StringUtils.hasText(title)) {
return null;
}
QueryWrapper q = new QueryWrapper();
q.eq("title", title);
return getOne(q);
}
}

View File

@ -1,6 +1,7 @@
package me.xiaoyan.point.api.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import me.xiaoyan.point.api.error.BizException;
import me.xiaoyan.point.api.mapper.OrderInfoMapper;
@ -9,8 +10,11 @@ import me.xiaoyan.point.api.pojo.OrderInfo;
import me.xiaoyan.point.api.pojo.UserInfo;
import me.xiaoyan.point.api.pojo.dto.OrderStatus;
import me.xiaoyan.point.api.pojo.vo.CreateOrderData;
import me.xiaoyan.point.api.pojo.vo.OrderQueryParam;
import me.xiaoyan.point.api.service.*;
import me.xiaoyan.point.api.util.DataStatus;
import me.xiaoyan.point.api.util.OrderIdGenerator;
import me.xiaoyan.point.api.util.QueryWrapperUtil;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -68,9 +72,11 @@ public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo
.id(OrderIdGenerator.next())
.gid((long) data.getGoodsId())
.uid(uid)
.orderTitle(goods.getTitle())
.count(data.getBuyCount())
.price(goods.getPrice())
.status(OrderStatus.CONFIRM)
// 默认为待确认
.status(OrderStatus.NOT_CONFIRM)
.build();
if (save(orderInfo)) {
return orderInfo;
@ -78,6 +84,47 @@ public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo
throw BizException.create("创建订单失败");
}
@Override
public Page queryByPage(OrderQueryParam param, boolean queryGoods, boolean queryUser) {
final QueryWrapper q = QueryWrapperUtil.builder()
.eq("id", param.getId())
.eq("order_title", param.getTitle())
.ge("create_time", param.getCreateTimeStart())
.le("create_time", param.getCreateTimeEnd())
.ne("status", DataStatus.DELETE)
.build();
// 根据状态筛选
if (param.getStatus() != null && param.getStatus() > 0) {
q.eq("status", param.getStatus());
}
q.orderByDesc("create_time");
final Page<OrderInfo> page = getBaseMapper().selectPage(param.getPage(), q);
page.getRecords().forEach(o -> {
if (queryUser) {
// 查询订单的归属用户
o.setOwner(userInfoService.getById(o.getUid()));
}
if (queryGoods) {
// 查询订单商品信息
o.setGoods(goodsService.getById(o.getGid()));
}
});
return page;
}
@Override
public OrderInfo getOneByIdAndUid(String id, int uid) {
QueryWrapper q = new QueryWrapper();
q.eq("uid", uid);
q.eq("id", id);
final OrderInfo o = getOne(q);
if(null != o){
o.setGoods(goodsService.getById(o.getGid()));
}
return o;
}
public long buyHistoryCount(int uid, int gid) {
QueryWrapper q = new QueryWrapper();
q.eq("uid", uid);

View File

@ -2,6 +2,7 @@ package me.xiaoyan.point.api.util;
public class DataStatus {
public static final int NORMAL = 1;
public static final int DISABLED = 2;
public static final int DELETE = 0;
}

View File

@ -0,0 +1,9 @@
package me.xiaoyan.point.api.util;
public class OrderStatus {
public static final int NOT_CONFIRM = 1;
public static final int CONFIRMED = 2;
public static final int CANCELED = 3;
public static final int FINISH = 4;
public static final int DELETE = 0;
}

View File

@ -24,12 +24,30 @@ public class QueryWrapperUtil {
}
return this;
}
public QueryWrapperUtil eq(String column, int value) {
q.eq(column, value);
return this;
}
public QueryWrapperUtil ne(String column, Object value) {
q.ne(column, value);
return this;
}
public QueryWrapperUtil le(String column, String value) {
if (StringUtils.hasText(value)) {
q.le(column, value);
}
return this;
}
public QueryWrapperUtil ge(String column, String value) {
if (StringUtils.hasText(value)) {
q.ge(column, value);
}
return this;
}
public QueryWrapper build() {
return this.q;
}

View File

@ -48,25 +48,25 @@ create table sign_record
create_time datetime default current_timestamp
) engine = innodb
collate = 'utf8mb4_general_ci' comment '打卡记录表';
drop table if exists goods;
drop table if exists goods;
create table goods
(
id bigint(15) primary key auto_increment,
category tinyint(2) null default 1 comment '商品类别(1:普通 2:精选 3:秒杀 4:抽奖)',
type tinyint(2) null default 1 comment '商品类型(1:实物 2:虚拟)',
category tinyint(2) null default 1 comment '商品类别(1:普通 2:精选 3:秒杀 4:抽奖)',
type tinyint(2) null default 1 comment '商品类型(1:实物 2:虚拟)',
title varchar(50) not null,
origin_price int(10) unsigned comment '原价' default 0,
price int(10) unsigned not null comment '价格',
stock int(10) unsigned not null comment '库存数量',
limit_count int(10) unsigned null default 1 comment '购买最大数量(0表示不限制)',
limit_count int(10) unsigned null default 1 comment '购买最大数量(0表示不限制)',
cover varchar(200) not null comment '商品图',
description text not null comment '描述',
notice varchar(500) null comment '提示',
online_time datetime not null comment '上架时间',
offline_time datetime not null comment '下架时间',
create_time datetime default current_timestamp,
create_time datetime default current_timestamp,
update_time datetime null on update current_timestamp,
status tinyint(2) default 1,
status tinyint(2) default 1,
index ix_title (title)
) engine = innodb
collate = 'utf8mb4_general_ci' comment '商品表';
@ -75,6 +75,7 @@ create table order_info
(
id varchar(50) not null comment '订单编号',
gid bigint(15) not null comment '商品编号',
order_title varchar(50) null comment '订单名称',
price int(10) not null comment '价格',
uid int(10) not null comment '用户编号',
data json null comment '订单数据',

View File

@ -45,6 +45,7 @@
and stock > 0
and category = #{category}
and status != 0
order by online_time desc,id desc
</select>
<select id="queryAllGoodsIdAndStock" resultType="me.xiaoyan.point.api.pojo.Goods">
select id,stock

View File

@ -1,4 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="me.xiaoyan.point.api.mapper.OrderInfoMapper">
<resultMap id="orderInfoMap" type="me.xiaoyan.point.api.pojo.OrderInfo">
<association property="owner"/>
</resultMap>
<select id="selectByPage" resultMap="orderInfoMap"></select>
</mapper>