add
This commit is contained in:
@@ -37,6 +37,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>mysql</groupId>
|
<groupId>mysql</groupId>
|
||||||
<artifactId>mysql-connector-java</artifactId>
|
<artifactId>mysql-connector-java</artifactId>
|
||||||
|
<version>8.0.33</version>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -51,6 +51,29 @@ public class ConfessionController {
|
|||||||
return ApiResponse.ok(confessionRepository.save(confession));
|
return ApiResponse.ok(confessionRepository.save(confession));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/order/{orderId}")
|
||||||
|
public ApiResponse<Confession> getByOrder(@PathVariable Long orderId) {
|
||||||
|
Order order = orderRepository.findById(orderId)
|
||||||
|
.orElseThrow(() -> new ApiException(404, "订单不存在"));
|
||||||
|
if (!order.getUserId().equals(AuthContext.get().getId())) {
|
||||||
|
throw new ApiException(403, "无权限");
|
||||||
|
}
|
||||||
|
return ApiResponse.ok(confessionRepository.findByOrderId(orderId).orElse(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ApiResponse<Confession> update(@PathVariable Long id, @RequestBody CreateConfessionRequest request) {
|
||||||
|
Confession confession = confessionRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new ApiException(404, "告白不存在"));
|
||||||
|
if (!confession.getUserId().equals(AuthContext.get().getId())) {
|
||||||
|
throw new ApiException(403, "无权限");
|
||||||
|
}
|
||||||
|
confession.setTitle(request.getTitle());
|
||||||
|
confession.setMessage(request.getMessage());
|
||||||
|
confession.setImageUrl(request.getImageUrl());
|
||||||
|
return ApiResponse.ok(confessionRepository.save(confession));
|
||||||
|
}
|
||||||
|
|
||||||
@PublicEndpoint
|
@PublicEndpoint
|
||||||
@GetMapping("/{code}")
|
@GetMapping("/{code}")
|
||||||
public ApiResponse<Confession> get(@PathVariable String code) {
|
public ApiResponse<Confession> get(@PathVariable String code) {
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ import java.util.Optional;
|
|||||||
|
|
||||||
public interface ConfessionRepository extends JpaRepository<Confession, Long> {
|
public interface ConfessionRepository extends JpaRepository<Confession, Long> {
|
||||||
Optional<Confession> findByCode(String code);
|
Optional<Confession> findByCode(String code);
|
||||||
|
Optional<Confession> findByOrderId(Long orderId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,21 @@ import org.springframework.beans.factory.annotation.Value;
|
|||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class WebConfig implements WebMvcConfigurer {
|
public class WebConfig implements WebMvcConfigurer {
|
||||||
private final AuthInterceptor authInterceptor;
|
private final AuthInterceptor authInterceptor;
|
||||||
private final String allowedOrigins;
|
private final String allowedOrigins;
|
||||||
|
private final String uploadDir;
|
||||||
|
|
||||||
public WebConfig(AuthInterceptor authInterceptor,
|
public WebConfig(AuthInterceptor authInterceptor,
|
||||||
@Value("${app.cors.allowed-origins}") String allowedOrigins) {
|
@Value("${app.cors.allowed-origins}") String allowedOrigins,
|
||||||
|
@Value("${app.upload.dir}") String uploadDir) {
|
||||||
this.authInterceptor = authInterceptor;
|
this.authInterceptor = authInterceptor;
|
||||||
this.allowedOrigins = allowedOrigins;
|
this.allowedOrigins = allowedOrigins;
|
||||||
|
this.uploadDir = uploadDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -30,4 +34,21 @@ public class WebConfig implements WebMvcConfigurer {
|
|||||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||||
.allowCredentials(true);
|
.allowCredentials(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
|
registry.addResourceHandler("/uploads/**")
|
||||||
|
.addResourceLocations("file:" + toFileLocation(uploadDir));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String toFileLocation(String path) {
|
||||||
|
String normalized = path.replace("\\", "/");
|
||||||
|
if (normalized.matches("^[A-Za-z]:/.*")) {
|
||||||
|
return "/" + normalized + "/";
|
||||||
|
}
|
||||||
|
if (!normalized.startsWith("/")) {
|
||||||
|
return "/" + normalized + "/";
|
||||||
|
}
|
||||||
|
return normalized.endsWith("/") ? normalized : normalized + "/";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ server:
|
|||||||
|
|
||||||
spring:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:mysql://localhost:3306/flower_shop?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
|
url: jdbc:mysql://localhost:3307/flower_shop?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
|
||||||
username: root
|
username: root
|
||||||
password: root
|
password: qq5211314
|
||||||
jpa:
|
jpa:
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: update
|
ddl-auto: update
|
||||||
@@ -16,9 +16,18 @@ spring:
|
|||||||
jackson:
|
jackson:
|
||||||
date-format: yyyy-MM-dd HH:mm:ss
|
date-format: yyyy-MM-dd HH:mm:ss
|
||||||
time-zone: Asia/Shanghai
|
time-zone: Asia/Shanghai
|
||||||
|
servlet:
|
||||||
|
multipart:
|
||||||
|
max-file-size: 10MB
|
||||||
|
max-request-size: 10MB
|
||||||
|
|
||||||
app:
|
app:
|
||||||
cors:
|
cors:
|
||||||
allowed-origins: http://localhost:8081
|
allowed-origins: http://localhost:8081
|
||||||
qr:
|
qr:
|
||||||
base-url: http://localhost:8081/#/gift
|
base-url: http://localhost:8081/#/gift
|
||||||
|
upload:
|
||||||
|
dir: D:/bs/flower/files
|
||||||
|
ai:
|
||||||
|
token: b676775d829147dea4955ce809cc1beb.GZIuVsLf19YT5B12
|
||||||
|
model: glm-4.7
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"core-js": "^3.36.0",
|
"core-js": "^3.36.0",
|
||||||
|
"element-china-area-data": "^6.1.0",
|
||||||
"element-ui": "^2.15.14",
|
"element-ui": "^2.15.14",
|
||||||
"vue": "^2.7.16",
|
"vue": "^2.7.16",
|
||||||
"vue-router": "^3.6.5"
|
"vue-router": "^3.6.5"
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import http from './http';
|
import http from './http';
|
||||||
|
|
||||||
export const createConfession = (data) => http.post('/confessions', data);
|
export const createConfession = (data) => http.post('/confessions', data);
|
||||||
|
export const getConfessionByOrder = (orderId) => http.get(`/confessions/order/${orderId}`);
|
||||||
|
export const updateConfession = (id, data) => http.put(`/confessions/${id}`, data);
|
||||||
export const getConfession = (code) => http.get(`/confessions/${code}`);
|
export const getConfession = (code) => http.get(`/confessions/${code}`);
|
||||||
export const listBarrages = (code) => http.get(`/confessions/${code}/barrages`);
|
export const listBarrages = (code) => http.get(`/confessions/${code}/barrages`);
|
||||||
export const sendBarrage = (code, data) => http.post(`/confessions/${code}/barrages`, data);
|
export const sendBarrage = (code, data) => http.post(`/confessions/${code}/barrages`, data);
|
||||||
|
|||||||
@@ -4,19 +4,38 @@
|
|||||||
<el-button type="text" @click="$router.push('/orders')">返回订单</el-button>
|
<el-button type="text" @click="$router.push('/orders')">返回订单</el-button>
|
||||||
</el-card>
|
</el-card>
|
||||||
<el-card class="content">
|
<el-card class="content">
|
||||||
<h3>定制告白弹幕</h3>
|
<div class="title">定制告白弹幕</div>
|
||||||
<el-form :model="form">
|
<el-form :model="form" label-width="90px" class="form">
|
||||||
<el-form-item label="标题">
|
<el-form-item label="标题">
|
||||||
<el-input v-model="form.title" />
|
<el-input v-model="form.title" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="祝福文字">
|
<el-form-item label="祝福文字">
|
||||||
<el-input type="textarea" v-model="form.message" />
|
<el-input type="textarea" v-model="form.message" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="配图链接">
|
<el-form-item label="AI 生成">
|
||||||
<el-input v-model="form.imageUrl" placeholder="https://..." />
|
<div class="ai-row">
|
||||||
|
<el-input v-model="aiPrompt" placeholder="例如:生成3条简短表白祝福语" />
|
||||||
|
<el-button type="primary" :loading="aiLoading" @click="generateWithAi">AI 生成</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="配图上传">
|
||||||
|
<el-upload
|
||||||
|
:action="uploadAction"
|
||||||
|
:headers="uploadHeaders"
|
||||||
|
name="file"
|
||||||
|
:show-file-list="false"
|
||||||
|
:on-success="handleUpload"
|
||||||
|
>
|
||||||
|
<el-button type="primary">上传图片</el-button>
|
||||||
|
</el-upload>
|
||||||
|
<div v-if="form.imageUrl" class="preview">
|
||||||
|
<img :src="form.imageUrl" alt="preview" />
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<el-button type="primary" @click="submit">生成页面</el-button>
|
<div class="actions">
|
||||||
|
<el-button type="primary" @click="submit">生成页面</el-button>
|
||||||
|
</div>
|
||||||
<div v-if="confession" class="result">
|
<div v-if="confession" class="result">
|
||||||
<p>专属链接:{{ giftUrl }}</p>
|
<p>专属链接:{{ giftUrl }}</p>
|
||||||
<img :src="qrUrl" class="qr" />
|
<img :src="qrUrl" class="qr" />
|
||||||
@@ -26,7 +45,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { createConfession, getQrUrl } from '../api/confession';
|
import { createConfession, getConfessionByOrder, updateConfession, getQrUrl } from '../api/confession';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
@@ -36,7 +55,9 @@ export default {
|
|||||||
message: '',
|
message: '',
|
||||||
imageUrl: ''
|
imageUrl: ''
|
||||||
},
|
},
|
||||||
confession: null
|
confession: null,
|
||||||
|
aiPrompt: ' 约50字,文艺但不晦涩,避免过度肉麻,表达温柔与坚定。仅输出文案本身,不要加标题或解释。',
|
||||||
|
aiLoading: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -45,16 +66,84 @@ export default {
|
|||||||
},
|
},
|
||||||
qrUrl() {
|
qrUrl() {
|
||||||
return this.confession ? getQrUrl(this.confession.code) : '';
|
return this.confession ? getQrUrl(this.confession.code) : '';
|
||||||
|
},
|
||||||
|
uploadAction() {
|
||||||
|
return '/api/upload';
|
||||||
|
},
|
||||||
|
uploadHeaders() {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
this.loadExisting();
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
loadExisting() {
|
||||||
|
const orderId = this.$route.params.orderId;
|
||||||
|
getConfessionByOrder(orderId).then((res) => {
|
||||||
|
const data = res.data.data;
|
||||||
|
if (data) {
|
||||||
|
this.confession = data;
|
||||||
|
this.form.title = data.title || '';
|
||||||
|
this.form.message = data.message || '';
|
||||||
|
this.form.imageUrl = data.imageUrl || '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
generateWithAi() {
|
||||||
|
if (!this.aiPrompt) {
|
||||||
|
this.$message.warning('请输入提示词');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.aiLoading = true;
|
||||||
|
fetch('https://yunwu.ai/v1/chat/completions', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer sk-sloS0Nm2VJRPJKJ1c3D2nD4w68d3IESuvJng4NxEnSk1SdhK',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: 'gpt-4o',
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: '你是文艺风格的祝福文案写手,语言自然、真诚、不过度夸张。' },
|
||||||
|
{ role: 'user', content: '请生成一条表白文案,要求如下:'+this.aiPrompt }
|
||||||
|
],
|
||||||
|
stream: false,
|
||||||
|
temperature: 1
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
const content = res && res.choices && res.choices[0] && res.choices[0].message
|
||||||
|
? res.choices[0].message.content
|
||||||
|
: '';
|
||||||
|
if (!content) {
|
||||||
|
this.$message.error('AI 返回为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.form.message = content;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.$message.error('AI 生成失败');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.aiLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleUpload(response) {
|
||||||
|
if (response && response.data && response.data.url) {
|
||||||
|
this.form.imageUrl = response.data.url;
|
||||||
|
}
|
||||||
|
},
|
||||||
submit() {
|
submit() {
|
||||||
createConfession({
|
const payload = { orderId: this.$route.params.orderId, ...this.form };
|
||||||
orderId: this.$route.params.orderId,
|
const action = this.confession && this.confession.id
|
||||||
...this.form
|
? updateConfession(this.confession.id, payload)
|
||||||
}).then((res) => {
|
: createConfession(payload);
|
||||||
|
action.then((res) => {
|
||||||
this.confession = res.data.data;
|
this.confession = res.data.data;
|
||||||
this.$message.success('生成成功');
|
this.$message.success('保存成功');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,9 +157,34 @@ export default {
|
|||||||
.content {
|
.content {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.form {
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
.ai-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.ai-row .el-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
.result {
|
.result {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
.preview img {
|
||||||
|
margin-top: 8px;
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
.qr {
|
.qr {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
|
|||||||
@@ -6,8 +6,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<el-button type="text" @click="$router.push('/orders')">我的订单</el-button>
|
<el-button type="text" @click="$router.push('/orders')">我的订单</el-button>
|
||||||
<el-button type="text" @click="$router.push('/profile')">个人中心</el-button>
|
<el-button type="text" @click="$router.push('/profile')">个人中心</el-button>
|
||||||
<el-button type="text" @click="$router.push('/admin')">后台管理</el-button>
|
<el-button v-if="isAdmin" type="text" @click="$router.push('/admin')">后台管理</el-button>
|
||||||
<el-button type="primary" @click="$router.push('/login')">登录</el-button>
|
<el-button v-if="!isLoggedIn" type="primary" @click="$router.push('/login')">登录</el-button>
|
||||||
|
<el-button v-else type="danger" @click="handleLogout">注销</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-header>
|
</el-header>
|
||||||
<el-main>
|
<el-main>
|
||||||
@@ -41,6 +42,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { listProducts, listCategories } from '../api/product';
|
import { listProducts, listCategories } from '../api/product';
|
||||||
|
import { logout } from '../api/auth';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
@@ -50,14 +52,36 @@ export default {
|
|||||||
categories: [],
|
categories: [],
|
||||||
categoryId: null,
|
categoryId: null,
|
||||||
keyword: '',
|
keyword: '',
|
||||||
placeholder: 'https://via.placeholder.com/300x200?text=Flower'
|
placeholder: 'https://via.placeholder.com/300x200?text=Flower',
|
||||||
|
isAdmin: false,
|
||||||
|
isLoggedIn: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
const user = JSON.parse(localStorage.getItem('user') || 'null');
|
||||||
|
this.isAdmin = user && user.role === 'ADMIN';
|
||||||
|
this.isLoggedIn = Boolean(localStorage.getItem('token'));
|
||||||
this.loadCategories();
|
this.loadCategories();
|
||||||
this.loadProducts();
|
this.loadProducts();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleLogout() {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
this.$confirm('确认退出登录?', '提示', { type: 'warning' })
|
||||||
|
.then(() => {
|
||||||
|
logout().finally(() => {
|
||||||
|
this.$message.success('已退出登录');
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
this.isLoggedIn = false;
|
||||||
|
this.isAdmin = false;
|
||||||
|
this.$router.push('/login');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
},
|
||||||
loadCategories() {
|
loadCategories() {
|
||||||
listCategories().then((res) => {
|
listCategories().then((res) => {
|
||||||
this.categories = res.data.data || [];
|
this.categories = res.data.data || [];
|
||||||
|
|||||||
@@ -6,7 +6,11 @@
|
|||||||
<el-card class="content">
|
<el-card class="content">
|
||||||
<el-table :data="orders">
|
<el-table :data="orders">
|
||||||
<el-table-column prop="order.orderNo" label="订单号" />
|
<el-table-column prop="order.orderNo" label="订单号" />
|
||||||
<el-table-column prop="order.status" label="状态" />
|
<el-table-column label="状态">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ statusText(scope.row.order.status) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="order.totalAmount" label="金额" />
|
<el-table-column prop="order.totalAmount" label="金额" />
|
||||||
<el-table-column label="操作">
|
<el-table-column label="操作">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
@@ -33,6 +37,16 @@ export default {
|
|||||||
this.loadOrders();
|
this.loadOrders();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
statusText(status) {
|
||||||
|
const map = {
|
||||||
|
CREATED: '待支付',
|
||||||
|
PAID: '已支付',
|
||||||
|
SHIPPED: '已发货',
|
||||||
|
COMPLETED: '已完成',
|
||||||
|
CANCELED: '已取消'
|
||||||
|
};
|
||||||
|
return map[status] || status;
|
||||||
|
},
|
||||||
loadOrders() {
|
loadOrders() {
|
||||||
listOrders().then((res) => {
|
listOrders().then((res) => {
|
||||||
this.orders = res.data.data || [];
|
this.orders = res.data.data || [];
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
import { getProduct } from '../api/product';
|
import { getProduct } from '../api/product';
|
||||||
import { listAddresses } from '../api/address';
|
import { listAddresses } from '../api/address';
|
||||||
import { createOrder } from '../api/order';
|
import { createOrder } from '../api/order';
|
||||||
|
import { codeToText } from 'element-china-area-data';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
@@ -65,7 +66,18 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
formatAddress(addr) {
|
formatAddress(addr) {
|
||||||
return `${addr.recipientName} ${addr.phone} ${addr.province || ''}${addr.city || ''}${addr.district || ''}${addr.detail || ''}`;
|
const province = this.formatRegion(addr.province);
|
||||||
|
const city = this.formatRegion(addr.city);
|
||||||
|
const district = this.formatRegion(addr.district);
|
||||||
|
return `${addr.recipientName} ${addr.phone} ${province}${city}${district}${addr.detail || ''}`;
|
||||||
|
},
|
||||||
|
formatRegion(value) {
|
||||||
|
if (!value) return '';
|
||||||
|
const key = String(value);
|
||||||
|
if (codeToText && codeToText[key]) {
|
||||||
|
return codeToText[key];
|
||||||
|
}
|
||||||
|
return value;
|
||||||
},
|
},
|
||||||
submitOrder() {
|
submitOrder() {
|
||||||
if (!this.addressId) {
|
if (!this.addressId) {
|
||||||
|
|||||||
@@ -50,9 +50,13 @@
|
|||||||
<el-input v-model="addressForm.phone" />
|
<el-input v-model="addressForm.phone" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="省市区">
|
<el-form-item label="省市区">
|
||||||
<el-input v-model="addressForm.province" placeholder="省" />
|
<el-cascader
|
||||||
<el-input v-model="addressForm.city" placeholder="市" />
|
v-model="region"
|
||||||
<el-input v-model="addressForm.district" placeholder="区" />
|
:options="regionOptions"
|
||||||
|
:props="regionProps"
|
||||||
|
placeholder="请选择省市区"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="详细地址">
|
<el-form-item label="详细地址">
|
||||||
<el-input v-model="addressForm.detail" />
|
<el-input v-model="addressForm.detail" />
|
||||||
@@ -73,6 +77,7 @@
|
|||||||
import { me } from '../api/auth';
|
import { me } from '../api/auth';
|
||||||
import { listAddresses, createAddress, updateAddress, deleteAddress } from '../api/address';
|
import { listAddresses, createAddress, updateAddress, deleteAddress } from '../api/address';
|
||||||
import http from '../api/http';
|
import http from '../api/http';
|
||||||
|
import { regionData } from 'element-china-area-data';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
@@ -80,7 +85,10 @@ export default {
|
|||||||
profile: { nickname: '', phone: '', email: '' },
|
profile: { nickname: '', phone: '', email: '' },
|
||||||
addresses: [],
|
addresses: [],
|
||||||
showDialog: false,
|
showDialog: false,
|
||||||
addressForm: {}
|
addressForm: {},
|
||||||
|
regionOptions: regionData,
|
||||||
|
regionProps: { value: 'label', label: 'label', children: 'children' },
|
||||||
|
region: []
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@@ -105,6 +113,7 @@ export default {
|
|||||||
},
|
},
|
||||||
editAddress(row) {
|
editAddress(row) {
|
||||||
this.addressForm = { ...row };
|
this.addressForm = { ...row };
|
||||||
|
this.region = [row.province, row.city, row.district].filter(Boolean);
|
||||||
this.showDialog = true;
|
this.showDialog = true;
|
||||||
},
|
},
|
||||||
removeAddress(id) {
|
removeAddress(id) {
|
||||||
@@ -114,11 +123,15 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
saveAddress() {
|
saveAddress() {
|
||||||
|
this.addressForm.province = this.region[0] || '';
|
||||||
|
this.addressForm.city = this.region[1] || '';
|
||||||
|
this.addressForm.district = this.region[2] || '';
|
||||||
const api = this.addressForm.id ? updateAddress(this.addressForm.id, this.addressForm) : createAddress(this.addressForm);
|
const api = this.addressForm.id ? updateAddress(this.addressForm.id, this.addressForm) : createAddress(this.addressForm);
|
||||||
api.then(() => {
|
api.then(() => {
|
||||||
this.$message.success('保存成功');
|
this.$message.success('保存成功');
|
||||||
this.showDialog = false;
|
this.showDialog = false;
|
||||||
this.addressForm = {};
|
this.addressForm = {};
|
||||||
|
this.region = [];
|
||||||
this.loadAddresses();
|
this.loadAddresses();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container class="layout">
|
<el-container class="layout">
|
||||||
<el-aside width="200px" class="aside">
|
<el-aside width="220px" class="aside">
|
||||||
<div class="title">后台管理</div>
|
<div class="title">植愈后台</div>
|
||||||
<el-menu router>
|
<el-menu
|
||||||
|
router
|
||||||
|
background-color="#1f2d3d"
|
||||||
|
text-color="#c0c4cc"
|
||||||
|
active-text-color="#409eff"
|
||||||
|
:default-active="$route.path"
|
||||||
|
>
|
||||||
<el-menu-item index="/admin">仪表盘</el-menu-item>
|
<el-menu-item index="/admin">仪表盘</el-menu-item>
|
||||||
<el-menu-item index="/admin/products">商品管理</el-menu-item>
|
<el-menu-item index="/admin/products">商品管理</el-menu-item>
|
||||||
<el-menu-item index="/admin/orders">订单管理</el-menu-item>
|
<el-menu-item index="/admin/orders">订单管理</el-menu-item>
|
||||||
@@ -26,14 +32,28 @@
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
.aside {
|
.aside {
|
||||||
background: #2d3a4b;
|
background: linear-gradient(180deg, #1f2d3d 0%, #233044 100%);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
padding: 20px;
|
padding: 20px 16px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
.header {
|
.header {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.el-menu {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
.el-menu-item {
|
||||||
|
height: 48px;
|
||||||
|
line-height: 48px;
|
||||||
|
}
|
||||||
|
.el-menu-item.is-active {
|
||||||
|
background: rgba(64, 158, 255, 0.15) !important;
|
||||||
|
border-right: 3px solid #409eff;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,7 +2,11 @@
|
|||||||
<div>
|
<div>
|
||||||
<el-table :data="orders">
|
<el-table :data="orders">
|
||||||
<el-table-column prop="order.orderNo" label="订单号" />
|
<el-table-column prop="order.orderNo" label="订单号" />
|
||||||
<el-table-column prop="order.status" label="状态" />
|
<el-table-column label="状态">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ statusText(scope.row.order.status) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="order.totalAmount" label="金额" />
|
<el-table-column prop="order.totalAmount" label="金额" />
|
||||||
<el-table-column label="操作">
|
<el-table-column label="操作">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
@@ -31,6 +35,16 @@ export default {
|
|||||||
this.load();
|
this.load();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
statusText(status) {
|
||||||
|
const map = {
|
||||||
|
CREATED: '待支付',
|
||||||
|
PAID: '已支付',
|
||||||
|
SHIPPED: '已发货',
|
||||||
|
COMPLETED: '已完成',
|
||||||
|
CANCELED: '已取消'
|
||||||
|
};
|
||||||
|
return map[status] || status;
|
||||||
|
},
|
||||||
load() {
|
load() {
|
||||||
adminOrders.list().then((res) => {
|
adminOrders.list().then((res) => {
|
||||||
this.orders = res.data.data || [];
|
this.orders = res.data.data || [];
|
||||||
|
|||||||
@@ -26,8 +26,19 @@
|
|||||||
<el-form-item label="库存">
|
<el-form-item label="库存">
|
||||||
<el-input v-model="form.stock" />
|
<el-input v-model="form.stock" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="封面">
|
<el-form-item label="封面上传">
|
||||||
<el-input v-model="form.coverUrl" />
|
<el-upload
|
||||||
|
:action="uploadAction"
|
||||||
|
:headers="uploadHeaders"
|
||||||
|
name="file"
|
||||||
|
:show-file-list="false"
|
||||||
|
:on-success="handleUpload"
|
||||||
|
>
|
||||||
|
<el-button type="primary">上传图片</el-button>
|
||||||
|
</el-upload>
|
||||||
|
<div v-if="form.coverUrl" class="preview">
|
||||||
|
<img :src="form.coverUrl" alt="cover" />
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="描述">
|
<el-form-item label="描述">
|
||||||
<el-input type="textarea" v-model="form.description" />
|
<el-input type="textarea" v-model="form.description" />
|
||||||
@@ -55,13 +66,25 @@ export default {
|
|||||||
return {
|
return {
|
||||||
products: [],
|
products: [],
|
||||||
showDialog: false,
|
showDialog: false,
|
||||||
form: {}
|
form: {},
|
||||||
|
uploadAction: '/api/upload'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
uploadHeaders() {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||||
|
}
|
||||||
|
},
|
||||||
created() {
|
created() {
|
||||||
this.load();
|
this.load();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleUpload(response) {
|
||||||
|
if (response && response.data && response.data.url) {
|
||||||
|
this.form.coverUrl = response.data.url;
|
||||||
|
}
|
||||||
|
},
|
||||||
load() {
|
load() {
|
||||||
adminProducts.list().then((res) => {
|
adminProducts.list().then((res) => {
|
||||||
this.products = res.data.data || [];
|
this.products = res.data.data || [];
|
||||||
@@ -90,3 +113,13 @@ export default {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.preview img {
|
||||||
|
margin-top: 8px;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ module.exports = {
|
|||||||
devServer: {
|
devServer: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
|
target: 'http://localhost:8080/',
|
||||||
|
changeOrigin: true
|
||||||
|
},
|
||||||
|
'/uploads': {
|
||||||
target: 'http://localhost:8080',
|
target: 'http://localhost:8080',
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user