feat 完善商品的管理

This commit is contained in:
LittleBoy 2022-12-08 15:45:15 +08:00
parent 440824c98f
commit 4935b9c24c
12 changed files with 266 additions and 63 deletions

View File

@ -119,14 +119,46 @@ body {
.pointer-cursor { .pointer-cursor {
cursor: pointer; cursor: pointer;
} }
table{
width: 100%; .table-wrapper {
border-left: solid 1px #eee; .table-toolbar{
border-top: solid 1px #eee; display: flex;
border-collapse: collapse; justify-content: space-between;
margin-bottom: 20px;
}
table {
width: 100%;
border-left: solid 1px #eee;
border-top: solid 1px #eee;
border-collapse: collapse;
}
td, th {
border-right: solid 1px #eee;
border-bottom: solid 1px #eee;
padding: 6px;
}
} }
td,th {
border-right: solid 1px #eee; .page-wrapper {
border-bottom: solid 1px #eee; margin: 20px 0;
padding:6px;
.page-item {
cursor: pointer;
border: solid 1px var(--primary-2);
border-radius: var(--border-radius-middle);
display: inline-block;
padding: 4px 10px;
margin: 0 5px;
min-width: 40px;
text-align: center;
&:hover, &.current-page {
border-color: var(--primary-color);
color: var(--primary-color)
}
}
}
.display-flex{
display: flex;
} }

View File

@ -24,12 +24,12 @@ import Eye from "../icon/Eye.vue";
import EyeClose from "../icon/EyeClose.vue"; import EyeClose from "../icon/EyeClose.vue";
interface PInputEvents { interface PInputEvents {
'update:modelValue': (value: string) => void 'update:modelValue': (value: any) => void
} }
type PInputProps = { type PInputProps = {
placeholder?: string placeholder?: string
modelValue?: string modelValue?: any
type?: 'text' | 'password' | 'textarea' type?: 'text' | 'password' | 'textarea'
} }

View File

@ -77,25 +77,3 @@ export default defineComponent({
} }
}) })
</script> </script>
<style scoped lang="less">
.page-wrapper {
margin: 20px 0;
.page-item {
cursor: pointer;
border: solid 1px var(--primary-2);
border-radius: var(--border-radius-middle);
display: inline-block;
padding: 4px 10px;
margin: 0 5px;
min-width: 40px;
text-align: center;
&:hover, &.current-page {
border-color: var(--primary-color);
color: var(--primary-color)
}
}
}
</style>

View File

@ -0,0 +1,62 @@
<template>
<div class="upload-wrapper">
<input type="file" :accept="accept" @change="onFileSelectChange"/>
<span>
<!-- 上传触发元素的插槽 -->
<slot/>
</span>
</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>()
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>
</style>

View File

@ -4,7 +4,7 @@ import {createPinia} from 'pinia'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import {httpConfig} from "./util/http"; import {httpConfig} from "./util/http";
import './assets/app.css' import './assets/app.less'
import PInput from "./components/input"; import PInput from "./components/input";
httpConfig.baseURL = "http://localhost:8080" httpConfig.baseURL = "http://localhost:8080"

View File

@ -7,7 +7,7 @@ export type HttpMethod = 'get' | 'post' | 'delete' | 'put'
export enum RequestDataContentType { export enum RequestDataContentType {
JSON = 'application/json;charset=UTF-8', JSON = 'application/json;charset=UTF-8',
FORM = 'application/x-www-form-urlencoded;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 = { export const httpConfig = {
@ -68,7 +68,9 @@ class Http {
*/ */
request<T>(url: string, data: any = null, method: HttpMethod = 'post', type: 'normal' | 'form' = 'normal') { request<T>(url: string, data: any = null, method: HttpMethod = 'post', type: 'normal' | 'form' = 'normal') {
return new Promise<T>((resolve, reject) => { return new Promise<T>((resolve, reject) => {
let contentType = RequestDataContentType.FORM; const headers: any = {
'Content-Type': RequestDataContentType.FORM
}
if (data) { if (data) {
if (type === 'form') { if (type === 'form') {
const form = new FormData(); const form = new FormData();
@ -76,12 +78,13 @@ class Http {
form.append(key, data[key]); form.append(key, data[key]);
} }
data = form; // 将data有{} => formData data = form; // 将data有{} => formData
contentType = RequestDataContentType.FORM_DATA; // 由于使用formData // contentType = RequestDataContentType.FORM_DATA; // 由于使用formData
delete headers['Content-Type'];
method = 'post'; method = 'post';
} else if (method == 'post') { } else if (method == 'post' || method == 'put') {
// 将数据对象 转成json // 将数据对象 转成json
data = JSON.stringify(data); data = JSON.stringify(data);
contentType = RequestDataContentType.JSON; headers['Content-Type'] = RequestDataContentType.JSON;
} else { } else {
// 装对象转成 key=value&key=value // 装对象转成 key=value&key=value
const params = []; const params = [];
@ -92,10 +95,6 @@ class Http {
url += '?' + params.join('&') url += '?' + params.join('&')
} }
} }
const headers: any = {
'Content-Type': contentType
}
const token = useUserStore().token(); const token = useUserStore().token();
if (token) headers.token = token if (token) headers.token = token
fetch(httpConfig.baseURL + url, { fetch(httpConfig.baseURL + url, {

View File

@ -1,16 +1,21 @@
<template> <template>
<div> <div class="table-wrapper">
<div class="search-form"> <div class="table-toolbar">
<span>标题<PInput v-model="param.title" placeholder="要查询的商品标题"/></span> <div class="search-form">
<span> <span>标题<PInput v-model="param.title" placeholder="要查询的商品标题"/></span>
<span>
分类 分类
<select v-model="param.category"> <select v-model="param.category">
<option v-for="(text,value) in CategoryEnum" :key="value" :value="value">{{ text }}</option> <option v-for="(text,value) in CategoryEnum" :key="value" :value="value">{{ text }}</option>
</select> </select>
</span> </span>
<button @click="onReset">重置</button> <button @click="onReset">重置</button>
<button @click="onSearch">搜索</button> <button @click="onSearch">搜索</button>
</div>
<div class="toolbar-extra">
<button @click="onEditData({id:0})">新增</button>
</div>
</div> </div>
<table> <table>
<tr> <tr>
@ -53,24 +58,84 @@
<Modal v-model="modalVisible"> <Modal v-model="modalVisible">
<div> <div>
<div> <div>
<span>昵称</span> <span>标题</span>
<p> <p>
<PInput placeholder="请输入昵称" v-model="editData.nickname"/> <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> </p>
</div> </div>
<div> <div>
<span>省份</span> <span>原价</span>
<p> <p>
<PInput placeholder="请输入省份" v-model="editData.province"/> <PInput type="number" placeholder="请输入原价" v-model="editData.originPrice"/>
</p> </p>
</div> </div>
<div> <div>
<span>城市</span> <span>销售价格</span>
<p> <p>
<PInput placeholder="请输入城市" v-model="editData.city"/> <PInput type="number" placeholder="请输入销售价格" v-model="editData.price"/>
</p> </p>
</div> </div>
<button @click="updateData">提交</button> <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>
<button @click="saveGoodsData">提交</button>
</div> </div>
</Modal> </Modal>
</template> </template>
@ -81,6 +146,7 @@ import http, {DataListModel} from "../../util/http";
import message from "../../components/message"; import message from "../../components/message";
import Modal from "../../components/modal/modal.vue"; import Modal from "../../components/modal/modal.vue";
import Pager from "../../components/pager/Pager.vue"; import Pager from "../../components/pager/Pager.vue";
import Uploader from "../../components/uploader/uploader.vue";
// //
//(1: 2: 3: 4:) //(1: 2: 3: 4:)
const CategoryEnum = { const CategoryEnum = {
@ -138,18 +204,44 @@ function loadDataList() {
}) })
} }
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) { function onEditData(data: GoodsModel) {
fillDataToObject(editData, data, fieldKey)
modalVisible.value = true modalVisible.value = true
} }
async function updateData() { async function saveGoodsData() {
try { try {
await http.put(`/admin/goods/${editData.id}`, editData) console.log(editData)
if (editData.id <= 0) {
await http.post('/admin/goods', editData)
} else {
await http.put(`/admin/goods/${editData.id}`, editData)
}
modalVisible.value = false; modalVisible.value = false;
message.toast('更新成功'); message.toast('保存成功');
loadDataList(); loadDataList();
} catch (e) { } catch (e) {
message.toast(e.message || '更新失败') message.toast(e.message || '保存失败')
} }
} }

View File

@ -2,9 +2,10 @@ import vue from '@vitejs/plugin-vue'
import vueJsxPlugin from "@vitejs/plugin-vue-jsx"; import vueJsxPlugin from "@vitejs/plugin-vue-jsx";
export default { export default {
hmr: true,
// 开发服务信息 // 开发服务信息
server: { server: {
host:'::' host: '::'
}, },
// 必须配置vue插件 // 必须配置vue插件
plugins: [ plugins: [

View File

@ -11,7 +11,9 @@ public class ApplicationConfig implements WebMvcConfigurer {
// 跨域 目前小程序不需要暂时不用考虑 // 跨域 目前小程序不需要暂时不用考虑
@Override @Override
public void addCorsMappings(CorsRegistry registry) { 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

@ -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;
}